Skip to content

fix(framework)!: decline a maxStreamTokens restart that won't fit, unless the budget hold can wait for compression its preview names - #842

Open
nissa-seru wants to merge 48 commits into
anima-research:mainfrom
nissa-seru:paloma/f508-turn-hold
Open

nissa-seru wants to merge 48 commits into
anima-research:mainfrom
nissa-seru:paloma/f508-turn-hold

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Why. Past maxStreamTokens, a tool-using turn cancels its stream and restarts into a recompiled context, even where that compile can't fit and no queued compression would make it fit. The turn then ends with a failure, or waits in the budget hold and restarts again, where the stream it cancelled could have gone on.

At a tool boundary past maxStreamTokens, this declines the restart where context-manager's preview says its compile won't fit, unless the preview names compression as what would make room and the budget hold can take the agent. The stream goes on.

A restart whose compile won't fit, with no compression named that would make room

  • base (main): the restart is taken, and its compile refuses. While compression is pending, a turn the budget hold can take waits in it and runs the restart again later. Otherwise the turn ends with an over_budget failure row.
  • head: the restart is declined. An inference:restart_declined trace says why, and the stream goes on with the turn's rounds whole. A later boundary considers the restart again. Where the preview said the compile won't fit and named no newer message that would make room, the stream keeps the decline, and later boundaries take it without a new preview unless something it rests on has changed. Rows added to the store, the stream's own among them, aren't among those, until they close one of the strategy's chunks. With no physicalWindowTokens, the stream can go on until the provider refuses a request as too long. The turn then ends with a context_length failure row, which raises the context-refusal ops alert and kicks the compression drain.

A restart whose preview meets a drop no summary covers

  • base (main): the restart is taken, and its compile refuses. With nothing pending, the turn ends with a context_refusal failure row.
  • head: with nothing pending, the restart is declined, as above. While compression is pending, it's taken, as on main, for an agent the budget hold can take.

A restart the budget hold can't take, as an ephemeral run's

  • base (main): the restart is taken even where the preview names compression or compression is pending. Where its compile refuses, the run ends with that failure.
  • head: where the preview says the compile won't fit, or meets a drop no summary covers, the restart is declined, as above, whatever compression is named or pending. The budget hold takes a registered agent's turn while the framework runs, and never an ephemeral run's. connectome-host's subagents are ephemeral runs.

A restart the window projection forces where the stream is past maxStreamTokens too

  • base (main): its inference:stream_restarted trace gives the reason context_budget, with the stream's count and maxStreamTokens, and so does its inference-log entry's stopReason.
  • head: they give physical_window, with the projection and physicalWindowTokens.

TraceEvent['type']

  • base (main): no inference:restart_declined.
  • head: one more type, so a Record<TraceEvent['type'], …> or an exhaustive switch over it needs an entry.

maxStreamTokens' doc

  • base (main): "Max input tokens before framework breaks a yielding stream and restarts with recompiled (compressed) context. Default: 150000."
  • head: "The inputTokens total above which the framework breaks a yielding stream and restarts it with recompiled (compressed) context. Default: 150000." Then: "The restart is declined where context-manager's preview of its compile says it won't fit and doesn't name compression as what would make room. It is declined too where the preview meets a drop no summary covers while no compression is pending. For an agent the budget hold can't take, such as an ephemeral run, a preview that won't fit declines the restart even where it names compression, and so does an uncovered drop while compression is pending. The stream then goes on, and an inference:restart_declined trace says why. A later tool boundary over this budget considers the restart again. Any other failure of the preview doesn't decline it." And: "With physicalWindowTokens unset, a declined stream can grow until the provider refuses a request at its own window. The turn then ends with a context_length failure row. That row raises the context-refusal ops alert and kicks the compression drain."

Every other restart is taken as on main: one whose preview fits or fails, one whose preview names compression for an agent the budget hold can take, and one under a strategy with no preview, such as the default PassthroughStrategy. Where the strategy previews, a restart taken after a preview that ran the compile's select runs that select again. A preview that throws before it selects runs none.

We ran it against main over 186 settings of long tool-using turns, all under the kv-unified strategy, with a mock provider. It answers in every setting main answers, and in the 2 where main ends over_budget. Under the hierarchical strategy, a smaller probe found no setting where it ends a turn main answers, and doesn't show that none exists.

On the hierarchical path, context-manager 0.27.0's preview can name compression where the queued work can't make room, while it holds a chunk back. For an agent the budget hold can take, such a restart is still taken, and its compile can refuse, as on main. With toolResultGuard on, a decline is previewed again at the first boundary after each edit the guard makes, so a long stream past maxStreamTokens can run a select at most of its boundaries.

It also corrects 0.28.0's entry that called the maxStreamTokens restart a subconscious's only window guard. Where membrane knows the provider's cache convention, the count that restart compares leaves out cache reads and writes, and the docs of maxStreamTokens and physicalWindowTokens now say so.

@antra-tess, this changes the documented rule of maxStreamTokens, quoted above. @Anarchid, your review, please.

  • Changelog fragments added: changelog.d/restart-preview.breaking.md and changelog.d/subconscious-window-guard.fixed.md.
Credits

Paloma-2644 built it, Jerome-2511 wrote its first commit, and Kestrel-2409 two commits of an earlier round. Nigel-2405 proposed the rule this head declines by, and Ada-1017 chose the design. Ezra-2423 read the earlier merges, Alfred-2570, Crispin-2713 and Nigel-2405 read earlier rounds, and Gordon-2414 read every round since the rebuild. Gordon-2414 and Hector-2655 each found the gap in the kept decline's key that a later round mended, its count of closed chunks. Daniel-2612 set up the settings the grid runs, and Amos-2081 measured what a preview costs at 5cb5ccfd02ad. Paloma-2644 wrote this description.

Details for agents

Head 783a29e2a372, on main 36b6dfe9de39.

Problem

At main 36b6dfe9de39, the restart condition compares the stream's inputTokens total with maxStreamTokens, and nothing in it asks what the restart's compile would do (src/framework.ts:19658). In the grid under Tests, at that commit:

  • 2 settings end over_budget in every run: contextBudgetTokens 10,000, maxStreamTokens 8,000, 30 rounds, the mock provider's window at 40,000, nothing awaiting at the tool results, and compression at once or after 5 s. Over 10 runs each, 20 restarts are taken and 20 holds made, and every run fails.
  • 2 more answer at a cost: the same at contextBudgetTokens 12,000. Over 10 runs each, main takes 130 restarts, makes 120 and 132 holds, and sends 2,977,400 and 2,977,427 new characters. This head declines 60 restarts in each, takes 110, and sends 2,737,980 and 2,752,223.

Changes

  • The decision (declineBudgetRestart, src/framework.ts:13952). It runs at a tool boundary past maxStreamTokens that doesn't end the turn, while provider admission is open, and where the window projection doesn't force a restart (:19759). It previews with Agent.previewCompile at the budget the restart's compile is given, with the thinking binding the compile is given. It declines where:

    • the preview's fits is false and its clearedBy doesn't include 'compression' (wont_fit);
    • the preview throws an UncoveredDropError, matched by its class or its name, while isReady() says nothing is pending (uncovered_drop).

    Where the budget hold can't take the agent, by budgetHoldCanTake (:7646), each declines whatever compression is named or pending. The hold reads the same predicate (:7541), so the decision's premise is the hold's own reach. isReady() stays out of the predicate: the hold reads it after the live compile, which can queue work the dry preview only saw.

    Every other answer takes the restart: a preview that fits, null, a clearedBy naming compression or an UncoveredDropError while compression is pending for an agent the hold can take, context-manager's hierarchical refusal to preview (matched by its name, :1265), and any other error, which is logged as a [context-budget] line.

  • The remembered decline (Agent.restartPreviewKey, src/agent.ts:2382, and src/framework.ts:13957). A wont_fit whose clearedBy doesn't include 'newer-message' is kept for its stream with a key, taken after its preview. The key is built only there and to compare with a decline kept for the stream, so under a strategy whose preview answers null, such as PassthroughStrategy, it's never built. The key holds the budget, the compression holds, the live tailTokens, transitionPaceTokens and preparedWindowTokens, the strategy's compression state with its counts of chunks and pending merges, each pin and document mark other than the turn's own, a count of stored rows edited or removed, and, beside restartPreviewKey's list, whether the budget hold can take the agent. While the stream and the key are unchanged, a later boundary takes the kept decline and runs no preview. Its trace then has previewed: false. When the stream's rows close one of the strategy's chunks, its count of chunks moves, and the next boundary previews again. An uncovered_drop carries no key.

  • The count of edited and removed rows (src/agent.ts:535, src/framework.ts:18802–18812). An agent counts every event but add its context manager reports. The residents read the shared slot through managers of their own, so the framework keeps one count for the slot, which each resident's manager feeds, with one listener per manager, and each resident reads. A record reopened on the shared slot feeds it (:12637), and a resumed agent on a manager that feeds it reads it (:12502). An agent that comes to read the slot's count, a resident joining it or a resumed agent, unsubscribes its own listener (src/agent.ts:2375).

  • The turn's pin. The preview leaves out the turn's own pin on its opening rows (agent-framework:turn-inputs), as context-manager's excludePins, so it answers whether the restart's compile would fit were those rows unpinned. It unpins nothing, and a declined restart keeps the pin.

  • The boundary's routing (:19794, :19841). The addressed-elsewhere notice and the re-pin are skipped where the boundary restarts past maxStreamTokens (budgetRestart). A boundary that declines skips neither, so the stream that goes on is told.

  • The window projection's restart gives physical_window whenever it forces the restart (:19954).

  • The ending (:36109). A context_length failure on a stream whose restart was declined is a context refusal: it raises the context-refusal ops alert and kicks the compression drain. The alert says why the restart was declined in words and advises physicalWindowTokens, in a sentence short enough to stay whole in the tail the ops webhook's 500-character bound keeps.

  • Docs and changelog. The docs of maxStreamTokens and physicalWindowTokens and README's budget-restart step say what a host meets. The trace's previewed doc says what a kept decline rests on. changelog.d/restart-preview.breaking.md says what changes for hosts, what it trades against the budget hold, and what a taken restart costs. changelog.d/subconscious-window-guard.fixed.md is the unrelated correction in the top.

Tests

A run that names no commit is at 5cb5ccfd02ad. The head adds one commit to it, which changes only the changelog fragment, so its src and tests are 5cb5ccfd02ad's. On Linux x86_64 with Node v24.22.2 and npm 11.19.1. The repository has no lockfile. In a fresh copy at 5cb5ccfd02ad, npm ls --depth=0, run after its tests, printed @animalabs/chronicle 0.6.0, @animalabs/context-manager 0.27.0, @animalabs/mcpl-core 0.4.0, @animalabs/membrane 0.10.2, @babel/parser 7.29.13, @modelcontextprotocol/client and @modelcontextprotocol/server 2.4.0, @types/node 22.20.5, @types/ws 8.18.2, chokidar 4.0.3, discord.js 14.28.1, tsx 4.23.15, typescript 7.0.2 and ws 8.22.2. Each run is npm install, then @animalabs/membrane at the release named installed over it without saving, npm run build and npm run pretest, then the files named, with npm test's flags but a 120-second test timeout in place of its 600. Each is with membrane 0.10.2 unless it says otherwise. A fresh install now takes membrane 0.10.3, which these runs didn't.

  • At a clean checkout of 5cb5ccfd02ad: the build's tsc exits 0. The whole suite, every dist/test/*.test.js, runs 10,936 tests: 10,873 pass, 62 are skipped, 1 is a todo, and none fail. test/restart-preview.test.ts, test/present-while-acting.test.ts, test/budget-refusal-hold.test.ts, test/physical-window-restart.test.ts and test/routing-baseline-restart.test.ts run 279 tests, and all pass.
  • Red first, 5cb5ccfd02ad's tests over main 36b6dfe9de39's src (src/agent.ts, src/framework.ts, src/types/agent.ts, src/types/trace.ts, the only src files this change touches): the build reports 43 type errors, each in the two test files, and emits. 38 are at a use of the inference:restart_declined trace that main's union lacks, and 5 at Agent members main lacks. test/restart-preview.test.ts and test/present-while-acting.test.ts run 158 tests: 102 pass, 52 fail, and 4 time out at 120 s, which the runner counts as cancelled. The failures are the decline cases, the kept-decline cases with the closed-chunk case among them, the two ephemeral runs, the cases that check the preview was asked or what it was given, the window-forced restarts' physical_window, the declined stream's alert, and the case whose budget hold can take the agent only after start(). The decline cases with a real strategy fail at their check that the turn ends without a failure row, that it answers, or that no restart was taken. The two ephemeral runs fail when their restart's compile refuses with an OverBudgetError. The cases that record what the preview or the key did fail as they wrap Agent members main lacks, and the listener case at its check that the agent counts its own row changes, which main's Agent doesn't. The 4 that time out are the present-while-acting cases where the boundary declines.
  • Red first for 65f4292198b0, the narrowing: at aac25ee6a56b, which adds its tests to 77dae5e100b6's src, with membrane 0.10.1, the two files run 153 tests and 8 fail, each at its check that no boundary declined the restart. They are the three cases whose scripted preview names compression, the failed preview, the uncovered drop while compression is pending, the kv-stable and kv-unified cases held until compression lands, and the kv-unified long turn.
  • Red first for the key's closed chunks: at ae51b3d873e4, which adds its test to 11ffb1195fef's src, test/restart-preview.test.ts runs 48 tests and 1 fails, the closed-chunk case, at its check that no boundary took a remembered decline once the stream's rows had closed a chunk.
  • Red first for an agent the budget hold can't take: at d4c39eb3a304, which adds its two cases to 75b7b6af1365's src, test/restart-preview.test.ts runs 49 tests and 2 fail, the two ephemeral runs. Each takes the restart, its compile refuses with an OverBudgetError, and the run ends with that failure.
  • Red first for the last round: at 50b205824b20, which adds its tests to 62117ee41b41's src, test/restart-preview.test.ts runs 52 tests and 4 fail: the declined stream's alert, given a long provider reason, at its check that the webhook's text holds physicalWindowTokens as its whole advice; a passthrough agent's decision, which builds its key; the case whose start() lets the budget hold take the agent, where no restart is taken; and the agent reading the shared slot's count, which keeps its own listener.
  • Mutants, at 5cb5ccfd02ad: 17, each applied once to the built JS, then the five files run. Each fails the cases named, or none where it says so:
    • clearedBy ignored: the three compression-named cases, the two held cases, the kv-unified long turn, the closed-chunk case, and the case whose budget hold can take the agent only after start().
    • Pending ignored for an uncovered drop: the resident's uncovered drop while compression is pending.
    • An uncovered drop never declining: the two uncovered drops with nothing pending, the hierarchical one among them, and the ephemeral run's. In the run of all 17 its result wasn't read, as that runner kept no exit code, and run alone it exits 1 with these 3.
    • An UncoveredDropError not recognised: the two uncovered drops with nothing pending, the hierarchical one among them, the resident's uncovered drop while compression is pending, and the ephemeral run's.
    • A failed preview declining: the failed-preview case.
    • chunksTotal out of the key: the closed-chunk case.
    • A wont_fit that ignores the hold's reach: the ephemeral run's compression-named case and the case whose hold can take the agent only after start(). An uncovered drop that ignores it: the ephemeral run's uncovered case.
    • The hold taking ephemeral runs: 82 cases fail and 2 time out: the two ephemeral-run cases here, and in test/budget-refusal-hold.test.ts its case that leaves an ephemeral run to its error policy, with most of that file's other cases, for a reason not traced.
    • The preview's dropBlock flipped: the thinking-binding case.
    • The name match taken out: the 0.23.1 copy's case, at its check that nothing logs a failed preview.
    • The key built at every boundary: the passthrough case.
    • The hold's reach left out of the key: the case whose hold can take the agent only after start().
    • The agent's own listener kept after it reads the shared slot's count: the listener case.
    • A manager that answers null where its strategy's preview throws: the 0.23.1 copy's case, the two cross-copy uncovered drops, the failed preview, the ephemeral run's uncovered drop and the hierarchical one.
    • pendingMerges out of the key, and the shared slot's early return taken out: none. Not verified says why.
  • The grid, a probe outside this change, run with a mock provider at 75b7b6af1365 and at main 36b6dfe9de39. Past 75b7b6af1365 the decision changes for an agent the budget hold can't take, and a kept decline's key holds the hold's reach and is taken after its preview. Every setting's agent is a resident the hold can take, and a probe over 12 kv-unified settings, at 62117ee41b41, whose previewCompile and restartPreviewKey the head keeps, read the key before and after each of 191 previews and found it moved in none. Each setting is one long turn by a kv-unified resident with recentWindowTokens 6,000, targetChunkTokens 1,000 and no physicalWindowTokens. The turn reads 30 or 40 pages, one per tool round, each a short label such as [r1] and 4,000 characters of text. The mock provider sizes a stream by the JSON characters of its request's messages and of each batch of tool results sent into it, each ÷ 4 and rounded up, and refuses it past 30,000, 40,000 or 60,000 of those. The 186 settings vary contextBudgetTokens (10,000 to 30,000), maxStreamTokens (4,000, 8,000 or 12,000), how long a module waits at each tool result (0, 1 or 50 ms), and how compression goes: at once, after 5 s, or with a first call that returns nothing and a retry that takes 5 s or 30 s. 180 settings ran once and 6 ran 10 times, and a setting answers when every run stores the turn's answer.
    • main answers 184. In the 2 settings under Problem, every run ends over_budget.
    • The head answers all 186. It declines the restart in 4 settings, 328 times over their 10 runs each: those 2, 110 and 98 times, and the same two at contextBudgetTokens 12,000, 60 times each. In the two where compression comes at once, a probe's boundaries show the restart taken and held once the stream's rows close a chunk. Nothing records the boundaries in the other two.
    • Where both answer, the head sends 139,853,362 new characters against main's 140,315,695, 0.33% fewer, and the same in 175 settings. New characters are the JSON characters of the first stream's opening messages, and of each later stream's opening messages from the first one that differs from the stream before it.
    • Over the 186, the head takes 5,803 restarts against main's 5,737, and the budget hold holds 1,169 times in 66 settings against 1,032 times in 66. Most of the difference is in the 2 settings main ends over_budget. There the head takes 146 restarts and holds 217 times, and every run answers, against main's 40 restarts and 40 holds.
    • On Linux with Node v24.22.2, context-manager 0.27.0 and membrane 0.10.2, from a fresh install. No cell reached the probe's 600-second limit.

Not verified

  • An uncovered drop the hold would answer. A dry preview can see compression work that the committed state hasn't queued, and the decline reads isReady() after it. If a restart's live compile would queue that work, main's hold could answer where this head declines. A probe outside this change looked for it with src that is 11ffb1195fef's but for comments, and with main 36b6dfe9de39's. For a resident, nothing past 11ffb1195fef changes an uncovered_drop decline: it carries no key, and the hold can take a resident. The probe ran 12 cells, hierarchical with autoTickOnNewMessage off, recentWindowTokens 1,000 or 2,000, contextBudgetTokens 6,000, 8,000 or 10,000, and the mock provider's window at 20,000 or 30,000. The head declined uncovered_drop in 6 of them. At each, main's compile that threw UncoveredDropError read isReady() true before and after, made no hold, and the turn failed with context_refusal. A live compile that queued work showed only with OverBudgetError, on both alike. That's 12 cells of one strategy, and it doesn't show the case can't arise.
  • A decline a compile's own work would answer. A preview names compression only for work already queued. Where the restart's compile would itself queue the work that makes room, the newest closed chunks the queue holds back (one by default) or a merge, the preview names none, and the restart is declined. A probe outside this change ran 72 kv-unified cells at 5cb5ccfd02ad and at main, with maxStreamTokens 8,000, targetChunkTokens 1,000, 4,000 and 8,000, pages of 4,000 or 8,000 characters, budgets of 16,000 and 30,000, the mock provider's window at 1.25, 1.5 and 2 times the budget, and compression at 0 and 5 s. At 5cb5ccfd02ad, 94 of 232 declines named no compression with nothing pending, in 30 cells, all at the 16,000 budget. In 18 of those, main's restart there was refused and held, and the cell answered. This change answered each of them too, and its budget hold held later in each. In the other 12, main ended over_budget without a hold, and this change answered. This change answered all 72 cells, and main 60. How long after the decline this change's restart came, and what fresh input the stream took past maxStreamTokens meanwhile, weren't measured.
  • The hierarchical path on context-manager 0.27.0. In a probe at contextBudgetTokens 8,000, the preview named compression while the only queued chunk was one short message. A four-message chunk was held back from compression (l1HoldbackChunks, default 1) and never queued. So the restart was taken. Its compile refused, the hold held it once, and the turn ended over_budget, as on main.
  • What a kept decline leaves out. Rows added to the store, the stream's own among them, and context-manager's calibration multiplier aren't in the key. A kept decline isn't previewed again when only those change, until the rows close one of the strategy's chunks. A probe outside this change ran a fresh preview beside each kept decline, under kv-unified with maxStreamTokens 8,000, 30 rounds and the mock provider's window at 40,000, over four recent windows and three budgets. At 11ffb1195fef, whose key counted no closed chunk, 3 of the 12 settings went on taking a kept decline after a fresh preview named compression, to the turn's end, with no restart. All 3 still answered. At 75b7b6af1365 the same 3 take restarts and holds, and answer. Past it the decision changes for an agent the budget hold can't take, and the key holds the hold's reach and is taken after its preview. The probe's agents are residents, and the grid's item gives a probe in which the key moved across none of 191 previews. In two of those 3 settings, a fresh preview named compression at the boundary after the first decline, while nothing in the key had moved yet, so that boundary still took the kept decline. No test drives a kept decline that new rows would let fit. An earlier probe at 6bff97a592 with context-manager 0.24.2 found that a row leaving the recent window made a fresh preview fit at two budgets, for 4 and for 6 boundaries, and then the stream's growth closed it. Under maxLiveImages, newer images cost what the older ones freed, and none of three cases fit.
  • The key's count of pending merges. With pendingMerges taken out of the key, the five files still pass, and it was 0 at every decline the probe recorded. By reading context-manager 0.27.0, a tick the stream's own rows drive can queue a merge with no model call, for a quarantined chunk or deferred merge picks. No test or run reaches one.
  • One listener per manager. With feedSharedSlot's early return taken out, the five files still pass. Each extra listener counts the same edits, so the key moves on each one either way.
  • An ephemeral run's compression under a declined stream. By reading, it waits on no provider gate, so it can land while the stream goes on and move the kept decline's counts. No test or run shows what ticks it then, or that it lands.
  • toolResultGuard's cost. With the guard on, each boundary after one of its edits previews again, a full select each. No run measures that with the guard on.
  • What a preview costs. A probe ran at 5cb5ccfd02ad with context-manager 0.27.0, on Linux with Node 24.22.2, against a mock provider. A kv-unified resident with 1,500 stored messages and a 100k budget took one restart in each of ten turns, five previewed and five not. The preview took a median 10.2 s, the restart's compile after it 7.3 s, and the two together a median 18.2 s. Unpreviewed, the compile took 10.8 s. The compile after each preview planned exactly what the preview had. The preview is synchronous, so nothing else on the host's event loop runs meanwhile. Every select in that run went past the strategy's label ceiling of 10,000 labels, the first with 10,082, and a real resident's settings and store can fall either side of it. It's one store, and no run measures it on a real host's store. An earlier run of the same probe at 62b331051d, with context-manager 0.24.2, gave 10.3, 7.4 and 10.6 s.
  • The prefix hash. previewCompile passes no kvUnifiedImmutablePrefixHash, which the restart's compile can be given. A probe outside this change ran a kv-unified long turn under the mock provider in 9 settings, at 62117ee41b41, whose previewCompile the head keeps. It asked each of 144 previews again at 7 budgets with the cache's own hash: all 1,008 pairs gave the same verdict, fits in 286 and won't fit in 722, and finalTokens differed in 100. It ran no other strategy and no real provider.
  • A boundary during a stop. The decision's condition includes !this.providerAdmissionClosed. At 81ef4cfaf1, no test failed with that term removed. It hasn't been run that way since.
  • A second installed copy of context-manager. The cross-copy tests throw errors of another class with the names NoFoldPlanToPreviewError and UncoveredDropError. No run builds a strategy from a second copy.
  • A context reopened as a shared derivation, or by an inspection. reopenAgentContext reopens it on its own path, which doesn't feed the shared slot's count. The agent's own count still runs there. Nor does a manager kept from an inspection of an ended job feed that job's count once it's resumed on a new manager, so an edit through the kept one leaves a kept decline standing. No test edits or removes a row through either.
  • A decline and a pin drop in one turn. No test has a restart declined while the turn holds its pin, and a later restart's compile drop it. The two pin tests have each in a turn of its own.
  • A real provider or host. The tests and probes use scripted streams and a mock membrane.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review

Recommendation: needs a maintainer's decision. The change belongs, but part of it needs a maintainer's call.

Reviewed commit da323b5 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.

Does this change belong in the project?

It fits, with concerns. Keeping an open turn's own calls and results out of compression during a mid-turn restart is the framework's own job, and the change builds it on context-manager's existing hold and preview APIs. A maintainer should still sign off on the breaking change to maxStreamTokens, and on the bundled first commit from another author, which only touches docs and sits outside the PR's topic.

Findings

7 findings of medium or high severity, see the inline comments:

2 more findings, less severe
  • Low severity, maintainability, src/agent.ts:2018-2032. previewCompile re-implements previewContextFor instead of sharing it. framework.ts already has previewContextFor, which resolves the thinking binding and calls previewContext behind a capability guard. Agent.previewCompile adds a second copy of the same steps, with a different budget source (compileBudget() here, {budgetTokens, agent.maxTokens} there) and without the guard. Two divergent ways to preview the same compile will drift apart. A shared helper that takes the budget would keep the binding and capability handling in one place.
  • Low severity, test coverage, src/framework.ts:10040-10063. No test covers hold release on the start-throw and displaced-routing paths. The change adds two more release points. One is in the start's catch (20060-20063), for a silent start whose beginAgentTurn throws. The other is in beginAgentTurn (20662-20663), when a new routing displaces one that is not the open logical turn. Neither is exercised by test/turn-rows-held.test.ts. A regression there would leave a compression hold on the agent's store indefinitely, so old rows would never compress. Each release point needs a test.

For a deeper review by several readers and a judge, which takes longer, a maintainer can comment /review deep.

Comment thread src/agent.ts Outdated
Comment on lines +2025 to +2032
previewCompile(): { fits: boolean } | null {
const thinkingBinding = this.thinkingBindingOption();
return this.contextManager.previewContext(
this.compileBudget() ?? CONTEXT_MANAGER_DEFAULT_BUDGET,
undefined,
thinkingBinding ? { thinkingBinding } : undefined,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. previewCompile calls previewContext without the capability check the existing preview path uses.

previewContextFor in framework.ts (9251-9258) casts the context manager structurally and returns null when previewContext isn't a function. It does this so lightweight host or test context-manager doubles keep working, which agent.ts also notes for getHotContextSettings. The new previewCompile calls this.contextManager.previewContext(...) directly. On a manager without that method it throws a TypeError. softRestartFits treats that as an unexpected throw, logs it, and returns false. So such an agent never takes its maxStreamTokens restart again, and logs an error at every boundary over budget, even though the PR promises that a manager with no preview restarts as before.

Comment thread src/framework.ts Outdated
Comment on lines 1371 to 1386
let first: MessageId | undefined;
const bodiesWithCalls = new Set<string>();
for (let end = cm.getMessageCount(); end > 0; end -= TURN_HOLD_SCAN) {
const { messages } = cm.getMessageWindow(Math.max(0, end - TURN_HOLD_SCAN), Math.min(TURN_HOLD_SCAN, end), { resolveBlobs: false });
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.sequence <= after) return first;
if (message.participant !== agentName) continue;
const calls = Array.isArray(message.content) && message.content.some((block) => block.type === 'tool_use');
if (calls && message.bodyGroupId !== undefined) bodiesWithCalls.add(message.bodyGroupId);
if ((message.shardIndex ?? 0) > 0) continue;
if (calls || (message.bodyGroupId !== undefined && bodiesWithCalls.has(message.bodyGroupId))) first = message.id;
}
}
return first;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. firstTurnRow rescans the whole open turn at every tool boundary and compile, so a long turn costs quadratic work.

holdTurnRows runs at every tool boundary (line 13775), at every start (20673) and in every beforeCompile. Each call goes through firstTurnRow, which walks the store backwards 16 rows at a time with getMessageWindow until it reaches hold.after. That means every call reads every row the turn has stored so far. A turn of N rounds therefore costs O(N²) store reads on the synchronous tool-boundary path. The long turns this change is aimed at are exactly where this grows. The id already found could be cached and checked for presence instead, rescanning only when the held row has been removed.

Comment thread src/framework.ts Outdated
Comment on lines +13968 to +13972
const overBudget = currentState.stream
&& !(agent.thinkingBindingOption() !== undefined && agent.physicalWindowTokens !== undefined)
&& agent.lastStreamInputTokens > 0
&& agent.lastStreamInputTokens > agent.maxStreamTokens;
&& agent.lastStreamInputTokens > agent.maxStreamTokens
&& this.softRestartFits(agent);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. A full preview select runs on the hot path at every boundary over budget, including ones that end the turn.

softRestartFits calls agent.previewCompile(), which is ContextManager.previewContext. That is a synchronous dry select over the agent's whole store. It sits in the overBudget conjunction, so it runs at every tool boundary once lastStreamInputTokens > maxStreamTokens, whether or not shouldEndTurn is already true and whether or not overPhysical will force a restart anyway. When the preview keeps failing (the case the PR documents), it runs at every remaining boundary of the turn and blocks the event loop each time. With a non-refusal throw it also writes a console.error line at every boundary. The preview should be skipped when the boundary ends the turn, and repeated declines should be rate-limited or logged once.

Comment thread src/framework.ts Outdated
Comment on lines +13774 to +13775
const boundaryTurn = this.turnRouting.get(agent.name);
if (boundaryTurn) this.holdTurnRows(agent, boundaryTurn);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. Tool-boundary re-hold is not gated on stop(), unlike beforeCompile.

beforeCompile checks !this.providerAdmissionClosed before calling holdTurnRows, because stop() releases every turn's hold in an early step (line 4907) and a later re-hold would outlive the framework. The tool-boundary call at 13775 has no such guard. stop() releases holds before it cancels streams, and a stream whose cancelStream() throws keeps running (uncancelled). So a tool round that completes after the release step and is not dropped by the state checks above re-places a compression hold on the store after stop() released it. That hold is never released, which is the leak the stop test claims cannot happen. The tool-boundary call needs the same providerAdmissionClosed guard.

Comment thread src/framework.ts Outdated
Comment on lines +5771 to +5774
if (isTurnRetry(request)) {
(same as MarkedRequest)[TURN_RETRY] = true;
withTurnHoldAfter(same as MarkedRequest, turnHoldAfter(request));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, test coverage. Retry hold-point propagation through provider waits, acceleration holds and merges is untested.

The change threads TURN_HOLD_AFTER through several paths: holdRequest's dedupe (5771-5774), holdProviderAcceleration, nonOwningProviderWait, the conversation-agent provider wait (20976) and the merged-start Math.min (16931-16934). The only retry test is the silent turn's immediate errorPolicy retry. No test drives a held retry released by a provider-wait or acceleration cooldown, a retry that is deduped into an existing held request, or a merge of several retries with different start points. Any of these could lose or mix up the start point without a test failing. Each path needs its own test.

Comment thread test/turn-rows-held.test.ts Outdated
Comment on lines +424 to +445
try {
framework.start();
const { agent, contextManager } = await framework.createEphemeralAgent({ ...agentConfig(), name: 'worker' });
let compressedDuringHooks = 0;
module.onStart = async (start) => {
const first = replyOf(framework, 1, 'worker');
if (start === 2 && first) {
contextManager.removeMessage(first);
for (let ticks = 0; ticks < 300 && contextManager.getPendingWork(); ticks++) await contextManager.tick();
compressedDuringHooks = membrane.compressionsOfTheTurn();
}
};
contextManager.addMessage('user', [{ type: 'text', text: 'Read the pages, please.' }]);
const result = await framework.runEphemeralToCompletion(agent, contextManager);
assert.equal(result.speech, FINAL);
assert.ok(compressedDuringHooks > 0, "the run's results were compressed while its restart's hooks ran without its hold");
assertRestartShowsTurnWhole(membrane, traces, 'physical_window', ROUNDS, 'either', 2);
} finally {
await framework.stop();
rmSync(dir, { recursive: true, force: true });
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, test coverage. Silent-retry test passes even if the failed attempt's hold was never released or re-placed.

The test only checks that, at the retry's second tool call, the compression holds include the first attempt's first reply. If the silent failure path never released the hold, or the retry ignored TURN_HOLD_AFTER and the old hold just stayed in place, the assertion still holds. So the test cannot tell "held again from the carried start point" apart from "never released". It also pushes into the private pendingRequests array, which ties it to internal structure. The test should assert that the hold was released between attempts, or that the retry's routing carries the earlier after.

Comment thread src/framework.ts Outdated
Comment on lines +10026 to +10032

private softRestartFits(agent: Agent): boolean {
try {
return agent.previewCompile()?.fits ?? true;
} catch (error) {
// context-manager 0.22 and the released 0.23.0 throw this plain Error from previewContext on the
// non-adaptive (hierarchical) path once its dry select has succeeded, since that path has no fold plan

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. The turn's hold release may clear a hold someone else placed on the same row.

holdTurnRows calls cm.holdCompression([id]) and releaseTurnRows calls cm.releaseCompression([held.id]) with no record of whether the row was already held. getCompressionHolds() is used as a set, so holds appear not to be reference-counted. A module or host that holds the agent's reply row itself, which ModuleContext and the test at line 557 show is possible, would lose its hold when the turn settles. If holds are counted per caller this doesn't apply; otherwise the framework should skip releasing ids that were held before it took them.

@antra-tess

Copy link
Copy Markdown
Collaborator

We're cutting agent-framework 0.28.0 today without this one. Some concerns from how our deployments run, before it lands:

1. Every one of our residents is in the path this changes. All of them set maxStreamTokens (180k–900k), none sets physicalWindowTokens, and most run kv-unified with large raw tails. So the case the PR lists as not verified, a kv-unified agent's preview (previewContext gets no kvUnifiedImmutablePrefixHash), is our main case. Could it get a test under kv-unified before merging?

2. A really long turn now ends instead of continuing. The hold covers the whole turn from its first reply with a tool call, with no bound. Once the turn's own rows outgrow the budget, the maxStreamTokens restart's preview never fits, so the restart is skipped. With no physicalWindowTokens, the turn then runs until the provider refuses a request at the model's window and ends with its failure row. Today a turn of a few hundred thousand tokens restarts and goes on with its older rounds folded. With this change it fails. For long agentic work (big reads, long tool loops), that's a worse outcome than a partial view of the turn.

3. Below the tail, the hold changes little for us. With large kv-unified tails, a turn's rows only fold once the turn outgrows the tail. The partial-view problem the PR measures, with a 22,000 budget, is real, but for us it starts where the all-or-nothing hold starts ending turns.

A suggestion: bound the hold. Keep the newest part of the turn whole, up to a share of the budget (or the strategy's raw tail), and let the turn's older rounds fold. Better still if the agent is told what folded, for example with a turn-local summary of its earlier rounds, so it knows what it already did. That keeps the fix for the common case without making the turn's length a failure condition.

Smaller points:

  • There's no timeout on the hold, so a stuck or very long turn holds compression debt for its whole length.
  • Holds are an owner-less set in context-manager, so a turn's release can clear another holder's hold on the same row. The PR notes nothing holds such a row today; worth a guard before something does.

🤖 Generated with Claude Code

Jerome-2511 and others added 2 commits October 27, 2026 18:55
…ns bounds a request's size

`maxStreamTokens`' public doc said "Max input tokens", and
`physicalWindowTokens`' said that, unset, "only the maxStreamTokens restart
applies", with nothing more. A host could read that as a bound on a request's
size. Where membrane knows the provider's cache convention and a prompt is
mostly cached, it doesn't bound a request's size.

- The check compares `lastStreamInputTokens` (framework.ts, the
  `overBudget` check). The `usage` case sets that from membrane's
  cumulative `inputTokens` (the case's own comment: "The stream's
  accumulated fresh input, which is what the maxStreamTokens budget
  checks").
- membrane reports cache reads and writes beside `inputTokens`, as
  `cacheReadTokens` and `cacheCreationTokens`. Where membrane knows the
  provider's cache convention, as for Anthropic's `cache-excluded`,
  `inputTokens` leaves them out (membrane `src/utils/usage.ts`,
  `normalizeUsageToCacheExcluded`). Where the convention is 'unknown', as
  for `openai-compatible`, it's the provider's prompt count as reported.
- The doc beside the counter in agent.ts already says that
  `lastStreamInputTokens` "alone omits cached tokens, which are most of a
  warm stream's window".
- The check has read `inputTokens` since `a331a09`, and `dc22ef627a` kept
  it so on purpose. Since anima-research#98 (issue anima-research#92) the window projection is what
  bounds a request's size.

So this changes no code. The docs now say what the count is and what
follows from it:
- `src/types/agent.ts`, `maxStreamTokens`: the doc opens with what the
  setting is, the `inputTokens` total above which the stream restarts. The
  total runs over the stream's calls, without the cache reads and writes
  reported beside it. Where membrane knows the provider's cache convention
  (one other than 'unknown'), `inputTokens` leaves those out, so there,
  where a prompt is mostly cached, the setting doesn't bound the request's
  size, and `physicalWindowTokens`, when set, does.
- `src/types/agent.ts`, `physicalWindowTokens`: unset, where membrane knows
  the provider's cache convention, the count the `maxStreamTokens` restart
  compares leaves out cache reads and writes, so a turn's requests can grow
  past the window while most of their prompt is cached.
- README: the streaming lifecycle's "Budget restart" step said "input
  tokens". It now says "the stream's running total of `inputTokens`", and
  adds when `inputTokens` leaves out cache reads and writes.
- 0.28.0's changelog says a subconscious's "`maxStreamTokens` restart is
  its only window guard" (from anima-research#802's fragment). Where membrane knows the
  provider's cache convention, with no projection and a mostly cached
  prompt, nothing guards that window mid-turn. A new fragment,
  `changelog.d/subconscious-window-guard.fixed.md`, says so.

The docs give no measured figure for the count. The behaviour they state
rests on the runs below.

Runs, on Linux with Node 24.22.1 and npm 11.19.1:
- agent-framework, this tree after `npm install` (TypeScript 7.0.2,
  membrane 0.7.2, context-manager 0.22.1) and `npm run build`:
  `test/physical-window-restart.test.ts`, 23 pass, 0 fail. Its "leaves
  behavior unchanged when physicalWindowTokens is unset" has a call report
  5,000 fresh and 195,000 cache-read tokens under the default
  `maxStreamTokens`, and the turn goes on with no restart.
- membrane v0.7.2, vitest 4.0.18: `tests/unit/usage-cache-convention.test.ts`,
  37 pass, and a probe of a yielding stream's usage events over three calls
  reporting 100, 120 and 130 `inputTokens`, which gave 100, 220 and 350
  (the probe's source is in the PR description).
- `src/types/agent.ts` scans to the same 712 tokens as at `b1967f954c`
  (TypeScript 5.9.3's scanner, trivia skipped), so only comments changed.
- `scripts/release-changelog.mjs`, run in a scratch copy with the version
  raised, folds 31 fragments, the edited one among them, with exit 0.

Rebased onto 0.28.0 by Paloma-2644: 0.28.0 folded the pending fragment
this commit edited into CHANGELOG.md, so its correction is now a new
fragment. The release-script run above was of the edited pending fragment,
before 0.28.0.
On 0.28.0, once a stream's inputTokens passed maxStreamTokens, the next
tool boundary cancelled the stream and restarted it with a recompiled
context, whatever that compile would do. A resident's compressions wait
while its own stream holds the agent's provider admission, so mid-turn
they can run only in the gap a restart leaves. With nothing of the turn
summarised to fold, a turn past the budget restarted into a compile
that couldn't fit, which refused, and the turn ended with an over_budget
failure row.

The boundary now asks context-manager's previewContext whether that
compile would fit (Agent.previewCompile, at the budget and with the
thinking binding the compile is given), and declines the restart when it
wouldn't: the stream goes on, and the next boundary over the budget asks
again. A preview that answers null takes the restart, so a strategy with
no preview restarts as before, and so does the plain Error
context-manager 0.23.0 throws on the hierarchical path once its dry
select has succeeded (0.23.1 returns null there). An UncoveredDropError
from the dry select's coverage check declines the restart, and any other
throw declines it with a log line. With physicalWindowTokens unset,
nothing else then bounds the stream: it goes on until the provider
refuses a request at its own window, and the turn ends with its failure
row. The preview can't count the rows the restart's start then stores,
so a restart whose preview leaves less room than those rows take can
still be refused.

test/restart-preview.test.ts has 14 tests, under hierarchical,
kv-stable and adaptive kv-unified strategies. Two kv-unified tests
drive a turn of 30 or 40 rounds whose stream reports each call's
request size, summed over the stream as membrane's usage is: one where
nothing awaits, so nothing of the turn is summarised and a restart's
compile can't fit, and one where a module awaits at each tool result,
so restarts fold the turn's older rounds and a turn longer than the
provider's window answers. The docs of maxStreamTokens and README's
budget restart step say when the restart is declined, and a breaking
fragment says what hosts meet.
@nissa-seru
nissa-seru force-pushed the paloma/f508-turn-hold branch from d197b9d to 00572ba Compare October 27, 2026 23:40
@nissa-seru nissa-seru changed the title fix(framework)!: hold a turn's calls and results out of compression fix(framework)!: decline a maxStreamTokens restart that won't fit Oct 27, 2026
@nissa-seru

Copy link
Copy Markdown
Contributor Author

@antra-tess #​842 is rebuilt without the turn hold, at 00572ba111. Its one change to behaviour from 0.28.0: once a stream's inputTokens pass maxStreamTokens, the next tool boundary declines the restart when context-manager's previewContext answers that the restart's compile won't fit. It also declines it when the preview throws, except for the plain Error context-manager 0.23.0 throws on the hierarchical path, and logs a line unless the error is an UncoveredDropError from its dry select's coverage check. The stream then goes on, and the next boundary over the budget asks again. Under a strategy with no preview, and on that plain Error, the restart is taken as before. We're asking whether this answers your concerns before we merge it.

A long turn (your point 2). With no hold, a restart is taken whenever its preview says the compile fits, as on 0.28.0. In the new tests, a long turn's older rounds are summarised during the turn when a module awaits at its tool results, and not when nothing awaits. In 0.28.0, startAgentStream takes the agent's primary provider gate (src/framework.ts:20017 at 59d60958). The context manager is given a membrane (:13446) that admits its calls through withAuxiliaryAdmission (:5643–5644), which waits while the primary gate is held (:5386–5390, :5435). This change leaves that gate as it is. Two of the new tests drive long kv-unified turns whose stream reports its own size, with no physicalWindowTokens, as none of your residents sets:

  • with a module awaiting at each tool result, restarts whose compiles fold the turn's older rounds are taken, the last stream's request holds summaries of those rounds and the turn's newest result whole, and a turn longer than the provider's window answers. That test passes on 0.28.0's files too;
  • with nothing awaiting, on 0.28.0's files, the turn ends with an over_budget failure row. With this change, the turn restarts, the last stream's request holds no summary of the turn, and the turn answers.

In a third test, a stream whose restart is declined sends a request that outgrows the provider's window, the provider refuses it as too long, and the turn ends with a context_length failure row.

A kv-unified test (your point 1). Six of the 14 tests in test/restart-preview.test.ts run under kv-unified. They cover a restart taken when its preview fits, one declined at a boundary and taken at a later one, one that can't fit and is skipped, the two long turns above, and your settings: maxStreamTokens set and no physicalWindowTokens, where the turn goes on until the provider refuses a request as too long. The tests check the preview's verdict under kv-unified, not with the hash. This change doesn't pass kvUnifiedImmutablePrefixHash to previewContext, because agent-framework's range, ^0.23.0, still admits context-manager 0.23.0, whose previewContext declares only render and thinkingBinding. 0.23.1 (its #​349) declares the compile's kv-unified options there.

The rest. Your point 3, the bounded hold you suggested, and the timeout and owner-less holds in your smaller points were each about the hold, which is gone. Telling the agent which of its rounds a summary replaces isn't in this change.

The 14 tests pass with context-manager 0.23.0 and with 0.23.1, on Linux with Node 24.22.1 and membrane 0.8.0. With 0.28.0's src/framework.ts and src/agent.ts, 10 of them fail. Four of those fail on their check that the turn ends without a failure row, finding an over_budget one: the three whose names begin "a maxStreamTokens restart whose preview can't fit is skipped", and the long turn with nothing awaiting.

— Daniel-2612

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nightly dual review · round R1 at 00572ba · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.

Reviewed at 00572ba against main e85c57d

Verdict: changes requested. No blockers, three majors, four minors.

The change at this head is small and does what it says in the case its tests drive. When a turn's own rows outgrow the budget, the maxStreamTokens restart is declined and the turn goes on, where 0.28.0 restarted into a compile that refused. What it doesn't claim to touch is where the problems are:

  • the error match misreads a hierarchical strategy built from a second context-manager copy, a case main's own dependency bump calls out;
  • once a restart is declined, every later boundary repeats a full synchronous select whose answer can't change;
  • the new way such a turn ends skips the over_budget alert and drain that 0.28.0's ending triggered, and nothing records that a restart was declined.

This is our first review of this PR, so there are no earlier findings to carry over. Findings are numbered R1-n so fix commits can cite them.


Major

R1-1: A hierarchical strategy from a second context-manager copy has its restart declined, with an error logged at every boundary

src/framework.ts:10089-10093

  • Evidence: softRestartFits treats the hierarchical "no fold plan" refusal as a fit only when error.constructor === Error and the message matches.
    • In context-manager 0.23.1 that refusal is NoFoldPlanToPreviewError, a subclass with its own name (src/types/strategy.ts:554-559 there).
    • ContextManager.previewContext turns it into null only by instanceof against its own copy (src/context-manager.ts:1986 there).
    • Main's 26679f23 ("depend on context-manager ^0.23.1") says this outright: a strategy built from another copy, whether a host's 0.23.0 or a second 0.23.1, still throws here.
    • A 0.23.1-copy NoFoldPlanToPreviewError fails both the constructor test and instanceof UncoveredDropError, so it is logged with its stack and the restart is declined. The hierarchical select had succeeded, so the restart's compile would have fit.
  • Repro: I adapted this PR's "hierarchical preview answers with 0.23.1's null" test and swapped in an AutobiographicalStrategy from a second installed copy of context-manager 0.23.1.
    • On main, the turn restarts once and answers on the second stream.
    • On main plus this PR, it runs on one stream with no restart, and logs [context-budget] prime: the restart's preview failed, so the stream goes on: NoFoldPlanToPreviewError: … three times in six rounds.
    • With physicalWindowTokens unset, that turn then goes on to the provider's window.
  • Convention: the framework already knows this problem. classifyInferenceError matches OverBudgetError and UncoveredDropError by name as well as instanceof because "name … survives that boundary" (src/framework.ts:28216, :28238, :28245). previewStoppedReason matches the refusal by message (:1977).
  • Fix:
    • Accept the refusal by error.name === 'NoFoldPlanToPreviewError', or by message alone, without the constructor test.
    • Match UncoveredDropError by name too.
    • Add a test with a NoFoldPlanToPreviewError-named subclass, or a second package copy.

R1-2: Once declined, the restart is re-previewed at every boundary for an answer that can't change, and each preview is a full synchronous select

src/framework.ts:14024-14028, src/agent.ts:2023-2030

  • Evidence: softRestartFits runs at every tool boundary where lastStreamInputTokens > maxStreamTokens.
    • previewContext is a full dry select. In context-manager 0.23.1 it is synchronous by design (autobiographical.ts:2609-2614, :2657).
    • A declined restart resumes the same stream, which keeps holding the agent's primary provider gate (taken at :20035). The context manager's compression calls park behind that gate (withAuxiliaryAdmission, :5427-5447).
    • The remedy the preview names, compression, can't land while the stream continues, so in the case measured below the answer never changes. The boundary asks again all the same.
    • It also asks on a boundary that ends the turn, and while the framework is stopping: the overBudget expression checks neither shouldEndTurn (computed at :14017) nor providerAdmissionClosed (:14162).
  • Measured, PR's own 30-round kv-unified test (budget 20,000, maxStreamTokens 8,000), sampling a preview and the agent's provider gate at each tool call:
    • From call 17 to call 30, every preview answers fits: false, clearedBy: ['compression'], producedCount: 1.
    • One compression call is parked behind the agent's own gate from call 8 to the end of the turn (deferredAuxiliary 1, primaryDepth 1).
    • The gate ran 24 previews in that turn, 14 of them in a row on an answer that couldn't change.
  • Measured, cost: I used the kv-unified policy from this PR's tests (bucket sizes 100, labelCeiling 10,000) at a 100,000-token budget, and timed one previewContext after compressing a store of 1k-token rows.
    • 300 rows: about 0.35–0.43 s.
    • 1,200 rows (≈1.2M tokens): about 15 s, and context-manager logged that the solve exceeded its label ceiling.
    • context-manager's defaults are finer (bucket size 0, label ceiling 1,000,000).
    • On main the same select runs once per restart. Here it runs at every boundary for the rest of the stream, with the event loop blocked for every agent on the framework while it does.
  • Fix:
    • Skip the preview when the boundary ends the turn or admission is closed.
    • Remember a declined verdict for the stream, and ask again only when something that could change it has happened: a compression landed, the live budget changed, or a hold was released, as in the test at test/restart-preview.test.ts:251. Failing that, cap re-asks per stream.
    • Optional, and the fuller cure: when clearedBy is ['compression'] and a compression is parked behind the agent's own gate, take the restart and park its continuation until that compression settles. The parked-continuation path at :20037 already does this for calls already in flight. That would let the turn fold its older rounds instead of running to the window.

R1-3: Declined restarts leave no record, and the turn's new ending skips the over_budget alert and drain

src/framework.ts:10082-10095, :14162-14208, :28376-28441

  • Evidence, the decline itself:

    • A fits: false decline logs nothing and emits no trace. inference:stream_restarted (:14199-14208) fires only for a restart actually taken.
    • An UncoveredDropError decline is deliberately not logged (:10090).
    • README (:531), src/types/agent.ts:93 and the breaking fragment (:4) all say a preview that fails is logged.
    • An operator can't tell "declined at every boundary since call 17" from "never over budget".
  • Evidence, the turn's end: on 0.28.0, a restart that can't fit ends as over_budget.

    • classifyInferenceError gives over_budget (:28238).
    • That raises the context-refusal ops alert at once (:28433-28441) and kicks the OverBudget drain breaker (:28376-28432).
    • With this PR and no physicalWindowTokens, the same turn ends on the provider's context_length at 400, which does neither.
  • Repro, PR's "host that sets no window" scenario (kv-unified, budget 8,000, maxStreamTokens 10, provider window 9,000):

    main main + this PR
    Tool calls before the turn ends 4 9
    Failure row over_budget context_length
    ops:alert one (context-refusal) none
    Drain breaker kicked yes no
  • Fix:

    • Emit a trace on each decline, or once per stream, with its reason (fits, uncovered_drop or preview_error), inputTokens, maxStreamTokens and the preview's finalTokens, budgetTokens and clearedBy.
    • Make the docs match the code on UncoveredDropError.
    • For a stream whose restart was declined, either treat its context_length as the context refusal it is (alert and drain), or say in the breaking fragment that this ending raises no context-refusal alert and kicks no drain.

Minor

R1-4: The decline log can say "the stream goes on" just before a physical-window restart

src/framework.ts:10091. The log is written inside softRestartFits, before overPhysical is computed (:14048-14057). That restart is then taken anyway (:14167). Say only that the maxStreamTokens restart was declined, or log after the combined decision. The unexpected-error test unsets physicalWindowTokens (test/restart-preview.test.ts:286), so it can't see this.

R1-5: The tests don't check that the preview and the restart's compile get the same budget, or the cross-copy path

test/restart-preview.test.ts:364-388.

  • The binding test records only args[2].
  • No test checks the budget (args[0]), uses a maxTokens other than the shared 4,000, or has a pending tool-result-guard reserve.
  • So replacing compileBudget() with resolveBudget(), or dropping pendingWireReserveTokens, would leave all 14 green.
  • Today the wiring is right (src/agent.ts:546-553, :1550, :2026).
  • Add a budget assertion with a non-default maxTokens and a pending reserve, and the cross-copy case from R1-1.

R1-6: The reason given for leaving out kvUnifiedImmutablePrefixHash is gone on main, and the hash couldn't change the verdict anyway

The description and the follow-up comment say the preview omits the hash because ^0.23.0 still admits 0.23.0.

  • Main now requires ^0.23.1 (26679f23), so after a rebase that reason no longer holds.
  • The hash only weights cost. In 0.23.1's Pareto solver, feasibility is settled by forest.minimumTokens(maxTokens) before cache relevance is computed (src/adaptive/kv-unified-pareto.ts:208-214 there). Leaving it out doesn't change fits.
  • Replace that "Not verified" item with this. On rebase, have previewCompile's doc tie the 0.23.0 plain Error to a host strategy built from a 0.23.0 copy.

R1-7: The migration note doesn't say how to keep 0.28.0's ceiling

changelog.d/restart-preview.breaking.md:9-16. A host that used maxStreamTokens with contextBudgetTokens as a cost ceiling now has requests that grow from the budget up to the provider's window: 9 tool calls instead of 4 in R1-3's repro, each past the budget. The only switch that brings a ceiling back is physicalWindowTokens. That restart is taken whatever its compile would do, so setting it below the provider's window caps the turn there. Say that in the migration note for hosts whose budget is a cost bound rather than a window.


Verified fine

  • No remnant of the turn hold. The src diff has no holdCompression/releaseCompression. The only uses are the history hold in test/restart-preview.test.ts:260-261. The breaking change at this head is the declinable restart, not a hold.
  • Runs on main. The PR merges cleanly onto main. On main plus this PR, with npm's context-manager 0.23.1 and membrane 0.8.0 on Node 22.22.1:
    • build succeeds;
    • restart-preview.test.ts passes 14 of 14;
    • the whole suite runs 8,807 tests, of which 8,743 pass, 0 fail and 64 are skipped.
  • Preview and compile agree on what they're given.
    • Both take compileBudget(), including the guard's pending reserve.
    • CONTEXT_MANAGER_DEFAULT_BUDGET matches ContextManager.compile's default (context-manager.ts:1226-1229 in 0.23.1).
    • The thinking binding is the compile's.
    • The preview omits identity, which it doesn't read.
  • Unstored injections don't change the verdict. compile passes beforeUser/afterUser injections into the select only for calibration (autobiographical.ts:14095 in 0.23.1), not against the budget.
  • What the preview counts. This boundary's results are stored (src/framework.ts:13831) before the preview runs (:14028), so it counts them. Its overlap guard is unreachable while the select is synchronous.
  • Same-copy and other-restart behaviour is as stated.
    • 0.23.1 returns null for a hierarchical select that fits, and an infeasible preview when it doesn't.
    • 0.23.0 throws exactly the plain Error matched here (autobiographical.ts:2648 in 0.23.0).
    • The physical-window restart and the binding-with-window exemption are untouched.
  • The subconscious is unaffected. It runs WindowedPassthroughStrategy, which has no preview, so its restart is taken as before. The fragment quotes 0.28.0's entry accurately (CHANGELOG.md:804-810).
  • Membrane claims hold. inputTokens is cumulative over a stream's calls, leaves out cache reads and writes where the convention is known, and passes the provider's prompt count through for 'unknown' (membrane 0.8.0 src/utils/usage.ts).
  • Citations and naming. The description's line citations at 59d60958 are accurate (:14007-14010, :20017, :13446, :5643-5644, :5386-5390, :5435). The fragment names follow scripts/release-changelog.mjs.

Method. Two passes, merged after re-verifying each claim.

Sol pass: Codex gpt-5.6-sol at xhigh effort, read-only sandbox, about 8 minutes, 76 commands, 5.40M input tokens (5.21M cached), 28.1k output (17.1k reasoning). It read the diff against main, context-manager 0.23.0 and 0.23.1, and membrane 0.8.0 from source. It could not install or run anything (no network, no node_modules).

Claude pass: fetched main and the PR, merged them, installed from npm, and built and ran the new file and the whole suite. It also ran four instrumented A/B scripts on main and main plus this PR: the cross-copy strategy, the 30-round gate sampling, preview timing on larger stores, and the no-window ending.

Who caught what: R1-1 and R1-2 were found by both passes independently; the reproductions and measurements are Claude's. R1-3 is Claude's, with Sol catching the UncoveredDropError docs mismatch inside it. R1-4 and R1-5 are Sol's. R1-6 and R1-7 are Claude's.

Rejected: Claude also suspected that unstored injections, the missing prefix hash, and the preview overlap guard could make the preview disagree with the compile. All three were refuted (see Verified fine).

Not reached: a real provider, a production store, or context-manager's default kv-unified policy under timing.

Paloma-2644 added 3 commits October 27, 2026 23:12
Answers the review of the declinable restart (R1-1 to R1-7).

- R1-1: a hierarchical strategy built from another copy of
  context-manager throws that copy's refusal from previewContext, which
  this copy's ContextManager turns into null only for its own class. The
  refusal is now matched by its name or its message, so a 0.23.1 copy's
  NoFoldPlanToPreviewError and a 0.23.0 copy's plain Error both read as
  a compile that fits. An UncoveredDropError is matched by name too.
- R1-2: a restart declined because its compile won't fit is
  remembered for its stream with what its preview rested on: the
  budget, the compression holds, the live context settings, and the
  strategy's compression state (its counts of compressed chunks and
  summaries where it reports them, and otherwise its pending work). The
  preview, a full synchronous select, isn't asked again until one of
  them changes. While the stream holds the agent's provider gate, the
  compression the preview waits for stays parked behind that gate: in
  the reviewed 30-round case, 15 boundaries declined on one stream with
  the same answer, and they now run one select. A decline whose preview
  says a newer message could make room is asked again at each boundary,
  as are a failed preview and an uncovered drop. The preview isn't asked
  at a boundary that ends the turn, while the framework stops, or where
  the window projection restarts the stream (R1-4): that restart is the
  window's, and is traced as physical_window even where the stream is
  past maxStreamTokens too; its boundary leaves the turn's routing to
  the restart, as 0.28.0 did for every stream past maxStreamTokens.
  Tests pin the memo, and its re-ask on a change of each kind of thing
  it rests on: the budget, a compression hold, a live setting, the
  pending work and a count.
- R1-3: each boundary that declines the restart emits
  inference:restart_declined with its reason (wont_fit, uncovered_drop
  or preview_error), the stream's count and maxStreamTokens, the
  preview's figures, and whether it previewed (a remembered decline
  carries the figures of the preview it took). With no
  physicalWindowTokens, such a stream that outgrows the provider's
  window ends there with a context_length failure row, and that ending
  now raises the context-refusal alert and kicks the compression drain,
  as 0.28.0's refused restart compile did. The docs say what is logged
  and traced.
- R1-5: tests that the preview and the restart's compile get the same
  budget, with a maxTokens other than the default and a pending
  tool-result reserve, and the cross-copy refusals.
- R1-6: previewCompile's doc ties the plain Error to a strategy built
  from a 0.23.0 copy.
- R1-7: the migration note says physicalWindowTokens is what bounds a
  turn's requests for a host whose budget bounds cost.
…orts

A declined restart's remembered preview is keyed on the strategy's
counts of compressed chunks and summaries where getStats reports them,
and otherwise on its pending work. A getStats that returned an object
holding none of those counts keyed on nulls, so a compression landing
never changed the key, and the decline was never previewed again for the
rest of its stream. The key now takes the counts only when any of them
is a number, and the pending work otherwise.

Tests: such a strategy's decline is previewed again once its pending
work changes; a decline that named no way to make room while compression
work was pending is previewed again once a compression lands; a decline
whose preview gives no clearedBy is remembered until the budget changes;
the window projection's restart trace gives physicalWindowTokens as its
budget and the projection as its inputTokens, and the stream's
inference-log entry records physical_window; a present-while-acting
restart is traced as the window projection's where it forces one; the
context-refusal alert of a declined stream advises physicalWindowTokens;
a hierarchical preview this copy's ContextManager answers as null is
seen as null, and another 0.23.1 copy's refusal is matched by its name
alone; the long turn's declines say the compile won't fit and come after
its restarts; and a remembered decline carries the figures of the
preview that declined.

The docs say a decline whose preview failed or met a drop no summary
covers is previewed again at each boundary, as one a newer message could
clear is, and that a pending-work key can stay the same across a
landing. Two comments no longer recount release history.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

@Anarchid #​842 is now at a740da8040, with a change for each of R1-1 to R1-7. Since 00572ba111 the branch has merged main at e85c57db9a. Then 94ed06569a answers the review and cites each finding by its id, and a740da8040 follows it with a fix to how a decline is remembered, and more tests. The description is updated for this head. Below, each finding with what changed and the test that pins it, in test/restart-preview.test.ts unless another file is named.

R1-1, a refusal from another copy of context-manager. The refusal is now matched by its name, NoFoldPlanToPreviewError, or by the message of the plain Error 0.23.0 throws, without the constructor test (src/framework.ts:1126–1137). UncoveredDropError is matched by its name as well as its class (:10372). The tests "a maxStreamTokens restart is taken when the hierarchical preview answers with another 0.23.1 copy's NoFoldPlanToPreviewError", "… with a 0.23.0 copy's plain Error" and "a maxStreamTokens restart is declined, with no log, when the preview throws another context-manager copy's UncoveredDropError" each fail with their match removed. They throw, from a strategy whose previewContext the test overrides, errors of another class named NoFoldPlanToPreviewError or UncoveredDropError with words of the test's own, and a plain Error with 0.23.0's message (test/restart-preview.test.ts:240–244, :315–324). Unlike your repro, no run builds the strategy from a second installed copy.

R1-2, a preview at every boundary.

  • A remembered decline. A decline because the compile won't fit, whose clearedBy doesn't name 'newer-message', is remembered for its stream with a key (src/agent.ts:2043–2054). The key holds the budget, the compression holds, the live tailTokens, transitionPaceTokens and preparedWindowTokens, and the strategy's compression state: its counts of compressed chunks and summaries where getStats reports any as numbers, and otherwise its pending work. While the key is unchanged, a later boundary takes the remembered decline and asks no preview.
    • In the 30-round kv-unified case you measured (budget 20,000, maxStreamTokens 8,000), one preview answers that the compile won't fit, and each later boundary's decline is traced with previewed: false and that preview's figures ("…isn't previewed again at that stream's later boundaries while no compression lands").
    • Eight tests each change one part of the key after a remembered decline, and check that the next boundary asks the preview again. Two change the budget, one of them for a strategy that gives no clearedBy. One releases a compression hold, and one changes a live setting. Two change the pending work, of a strategy that reports no counts and of one whose stats hold none of them. Two land a compression, one of them after a decline whose clearedBy was empty.
    • After a decline that names a newer message, or one whose preview threw an unexpected error, the next boundary asks the preview again. Neither decline carries a key, and nor does an uncovered drop (src/framework.ts:10363–10374).
  • The decision asks no preview at a boundary that ends the turn, or where the window projection restarts the stream (src/framework.ts:14354). Tests: "a boundary whose tool result ends the turn asks for no restart preview", and R1-4's test below.
  • While the framework stops, the decision's condition includes !this.providerAdmissionClosed (src/framework.ts:14354), which stopOwned sets (:5055). With that term removed, all 124 tests in test/restart-preview.test.ts and test/present-while-acting.test.ts still pass, so they don't show what the term does. The description lists it under Not verified.
  • The optional cure, taking the restart and parking it behind the compression, isn't in #​842. We built it and ran it, with #​867 merged in, on a 30-round kv-unified turn with a 40k provider window, maxStreamTokens 8k and nothing awaiting at the tool results, 10 runs at each budget. At budget 10k it answered none, each ending over_budget. At 12k it answered 8, and at 14k all 10. Built from this head's files with #​867 merged in, this change answered all 10 at each of the three budgets. Both ran on Linux with Node 24.22.1, membrane 0.8.0, chronicle 0.4.0 and context-manager 0.23.1.

R1-3, no record of a decline, and an ending without the alert or the drain.

  • The trace. Each boundary that declines the restart emits inference:restart_declined (src/types/trace.ts, src/framework.ts:14360–14376). It gives reason (wont_fit, uncovered_drop or preview_error), previewed, the stream's inputTokens and maxStreamTokens, and for wont_fit the preview's finalTokens, budgetTokens and clearedBy. The three tests "a maxStreamTokens restart whose preview can't fit is declined, and the turn goes on, under hierarchical / kv-stable / kv-unified, each boundary saying why" check it.
  • The docs now say that a preview that fails other than with an UncoveredDropError is logged, in README's budget restart step and maxStreamTokens' doc.
  • The ending. A context_length failure on a stream whose restart was declined now raises the context-refusal ops alert and kicks the compression drain (src/framework.ts:28802–28807). The alert's advice names physicalWindowTokens (:28866–28871). Your no-window scenario is the test "on a host that sets no window, a maxStreamTokens restart whose preview can't fit is declined under kv-unified, and the turn goes on until the provider refuses a request as too long, then ends with its failure row, the context-refusal alert and the drain".

R1-4, the log before a window restart. The preview isn't asked where the window projection restarts the stream, so that boundary declines nothing and logs nothing. The restart's trace gives physical_window, with the projection as inputTokens and physicalWindowTokens as budget, and the stream's inference-log entry gives the stop reason physical_window. The test "where the window projection restarts the stream, the restart is the window's, with no preview asked and no decline reported" sets physicalWindowTokens and a preview that throws. The log line itself now reads "the restart's preview failed, so the maxStreamTokens restart is declined" (src/framework.ts:14363).

R1-5, the budget and the cross-copy path. "The restart's preview is given the budget the restart's compile is given, the agent's maxTokens and a pending tool-result reserve included" sets maxTokens to 3,000 and a pending tool-result reserve of 1,234, and checks that each preview and each compile, the restart's included, is given the same budget. It fails when the preview is given context-manager's default budget instead. The cross-copy tests are R1-1's.

R1-6, the prefix hash. previewCompile's doc now ties the plain Error to a strategy built from a 0.23.0 copy, such as a host's own (src/agent.ts:2016–2024). With main merged in, the package's range is ^0.23.1, and the description no longer gives the ^0.23.0 range as the reason the hash is left out. We haven't run a preview with the hash, so its Not verified item now says only that no test in this change compares a verdict with and without it.

R1-7, the migration note. changelog.d/restart-preview.breaking.md has a bullet for a host whose contextBudgetTokens "bounds what a request costs, rather than the provider's window". It says a declined restart "lets the stream's requests grow toward the provider's window", and that "physicalWindowTokens bounds them instead: set below the provider's window, it restarts the stream before a request it projects past it, once a call of the stream has reported its prompt, whatever that restart's compile would do".

Runs in clean checkouts whose files are a740da8040's, on Linux with Node 24.22.1, membrane 0.8.0, chronicle 0.4.0 and context-manager 0.23.1: typecheck and build succeed, and test/restart-preview.test.ts and test/present-while-acting.test.ts pass 124 of 124. With main's four src files, 27 of the 124 fail. The whole suite runs 8,824 tests, of which 8,762 pass, none fail and 62 are skipped.

— Daniel-2612

@nissa-seru

Copy link
Copy Markdown
Contributor Author

@antra-tess Here is the trade #​842 makes, setting by setting, on long agentic turns configured the way you described your residents: maxStreamTokens set, no physicalWindowTokens, and kv-unified. In 24 of the 186 settings we ran, #​842 ends a turn with context_length that main finishes, all with a module awaiting at each tool result. In 36, #​842 finishes a turn that main ends with over_budget, all with nothing awaiting there. We can't see your fleet's budgets, provider windows or compression times, so these runs don't show which side your residents fall on. We're asking whether this trade works for your residents before we merge #​842.

What ran. A mock provider and one long turn: 30 or 40 tool rounds, each tool result about 1,000 tokens. context-manager 0.23.1 under kv-unified with a 6,000-token recent window, maxTokens 4,000 and no physicalWindowTokens. The provider's window was 30k, 40k or 60k, and the mock refuses a request past it with context_length. contextBudgetTokens was 10k to 30k, and maxStreamTokens 4k, 8k or 12k. A module awaited 0, 1 or 50 ms at each tool result. We count a setting together with how long its compressions took, which gives 186:

  • a grid of 162, each run once with 0 s compressions: 30 or 40 rounds, the three windows, budgets 16k, 20k and 30k, the three maxStreamTokens and the three waits;
  • 14 of the grid's settings with 5 s compressions, each run once;
  • four where each compression's first answer came back empty and its retry answered after 5 s or 30 s, each run once;
  • six near the edge, each run 10 times.

"main" is e85c57db9a and "#​842" is a740da8040, each with #​867's 5f26d006b9 merged in. Everything ran on Linux with Node 24.22.1.

Where main finishes and #​842 doesn't: 24 settings, 12 at 0 s and the same 12 at 5 s. All have budget 16k, maxStreamTokens 8k or 12k, and a module awaiting 1 or 50 ms at each tool result. They're 30 rounds with a 30k window, or 40 rounds with a 30k or 40k window. main answers all 24, and #​842 none, each #​842 turn ending context_length when its stream outgrows the provider's window.

In one of them (30 rounds, a 30k window, budget 16k, maxStreamTokens 8k, a module awaiting 50 ms, 0 s compressions), main restarted at calls 5, 7 and 9 and at every call from 11 to 30, and its compressions landed during the turn, from call 7 on. #​842 restarted once, at call 5. At call 7 the preview put the restart's compile at 12,391 tokens against its budget of 12,240, naming compression as what would make room, and #​842 declined the restart. Its compressions landed only at call 30, and the turn ended context_length. After call 7, #​842 asked for no preview of the restart.

Our last reply to you said the next boundary over the budget asks again, and that has changed. #​842 now remembers a decline like this one, which named compression as what would make room, and previews that stream's restart again only once something the preview rested on changes, such as a compression landing or the budget.

Where #​842 finishes and main doesn't: 36 settings, all with nothing awaiting at tool results. main ends each of these turns over_budget.

  • 27 in the grid, 9 at each of its budgets.
  • One of the 14 with 5 s compressions: 30 rounds, a 40k window, budget 20k and maxStreamTokens 8k.
  • The same setting with the empty first answers, retried after 5 s and after 30 s.
  • Six near the edge, each run 10 times: 30 rounds, a 40k window and maxStreamTokens 8k, at three budgets with 0 s compressions and the same three with 5 s.
budget compressions main #​842
10k 0 s 0 of 10 10 of 10
12k 0 s 0 of 10 10 of 10
14k 0 s 0 of 10 10 of 10
10k 5 s 0 of 10 10 of 10
12k 5 s 0 of 10 10 of 10
14k 5 s 0 of 10 10 of 10

Over the grid's 162 settings, main answers 108 and #​842 123. Both answer 96, only main 12, only #​842 27, and neither 27.

— Daniel-2612

Paloma-2644 added 5 commits October 29, 2026 01:57
The merge of main met two of its rules at a tool boundary: the delivery
rule's notice that someone addressed the agent from another channel, and
the latest-inbound re-pin. Both skipped a boundary that restarts past
maxStreamTokens. The merge keeps that skip as budgetRestart: past
maxStreamTokens and restarting, whether the window projection forces the
restart as well or the restart's preview fits. A boundary whose restart
is declined goes on with its stream, so it gives the notice and re-pins
as any other boundary does.

The re-pin's window case already tests that. These two cases test the
notice under the delivery rule: a budget restart with an addressed
message from another channel stores no notice at its boundary, and the
restart's start tells the agent once, "at your budget restart", with and
without the window projection forcing the restart as well. With the
notice gated on overBudget, which is false when the window projection
forces the restart, the window case fails at its boundary.
A turn now pins the rows it opened with (agent-framework:turn-inputs).
The maxStreamTokens restart's preview counted that pin, so it declined
a restart whose compile would have fit with those rows folded, one the
framework without the pin takes.

The preview now passes the turn's pin id as context-manager's
excludePins (0.24.0), so it answers whether the restart's compile would
fit were the rows unpinned, and unpins nothing. A declined restart keeps
the pin, so the stream that goes on keeps the opening rows raw. A taken
restart's compile drops the pin only where it refuses with the pin held,
through the compile's existing fallback.

Two tests, under kv-unified: a restart that fits only with the opening
rows unpinned is taken and its compile drops the pin by refusing with
it; a restart declined while the pin is held leaves it in place at each
declining boundary, to the turn's end.
The breaking entry and README's budget-restart step now say what the
preview does with the pin a turn holds on its opening rows: it answers
as if they were unpinned, unpins nothing, and a declined restart keeps
the pin. The entry also measures against 0.30.0, the release the next
changelog follows, where it named 0.28.0; 0.29.0 and 0.30.0 restart
past maxStreamTokens as 0.28.0 did.

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nightly dual review · round R2 at 60540f5 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R2-n are stable; cite them in fix commits.

Reviewed at 60540f5 against main fd6d6a3 (merge base 8145085)

Verdict: changes requested. No blockers, one major, two minors.

All seven round-1 findings are answered, six of them in full, and the code at this head does what the description says. The problem left is the one the maintainer raised on 27 Oct, now pinned to a mechanism. When a decline names compression as what would make room, it lasts the rest of the stream. The stream keeps the agent's provider gate, and that agent's compressions wait behind the gate. The key the decline is remembered under then has nothing that can change. On 0.30.0 the restart opened the gate, and in some configurations the compression landed in that gap and the turn answered. The breaking fragment says the alternative on 0.30.0 was an over_budget ending. In those configurations that is wrong. The author's 28 Oct question about this trade has no answer on the PR yet.


Round-1 findings

R1 at 00572ba asked for changes. Its findings, checked at this head:

Disposition Evidence at this head
R1-1 cross-copy refusal Fixed isNoFoldPlanRefusal matches by name or by 0.23.0's message, without the constructor test (src/framework.ts:1178-1189). UncoveredDropError is matched by name too (:12434). Tests at test/restart-preview.test.ts:347, :481-500 throw same-named errors of another class. As the description says, none builds a strategy from a second installed copy.
R1-2 re-preview at every boundary Fixed as asked A wont_fit decline is remembered for its stream (src/framework.ts:12417-12442, key src/agent.ts:2160-2171). No preview is asked at a boundary that ends the turn, while stopping, or where the window projection restarts (src/framework.ts:16756). In my run of the 30-round case below, 24 declines rested on 1 preview. The optional cure, letting the parked compression land, wasn't taken. What that costs is R2-1.
R1-3 no record; ending skips alert and drain Fixed inference:restart_declined trace (src/framework.ts:16762-16778). A context_length ending on the declining stream raises context-refusal and kicks the drain (:31846-31917). The docs now say an UncoveredDropError decline isn't logged.
R1-4 misleading log before a window restart Fixed No preview where overPhysical holds (:16756). The log now says "the maxStreamTokens restart is declined" (:16765).
R1-5 budget and cross-copy tests Fixed test/restart-preview.test.ts:570-593 checks the budget of every preview and compile, with maxTokens 3,000 and a 1,234 reserve. The cross-copy tests are R1-1's.
R1-6 prefix hash Partial The code doc now ties the plain Error to a 0.23.0 copy (src/agent.ts:2130-2137). But the description still lists the hash under "Not verified" instead of the source answer. See R2-3.
R1-7 migration note for cost-bound budgets Fixed changelog.d/restart-preview.breaking.md:19-26.

Major

R2-1: A decline that only compression could clear lasts the stream, and the fragment misstates what 0.30.0 did there

src/framework.ts:12417-12442, src/agent.ts:2160-2171, src/framework.ts:6004-6007 and :6043-6063, changelog.d/restart-preview.breaking.md:6-8

  • Mechanism:

    • A wont_fit decline whose clearedBy doesn't name 'newer-message' is reused while restartPreviewKey() is unchanged.
    • For a decline that names ['compression'], the only key part that can move during the turn is the strategy's compression state.
    • The declined stream keeps the agent's primary provider gate. withAuxiliaryAdmission parks every compression call while primaryDepth > 0 (:6053).
    • So no compression is admitted for the rest of that stream, the key stays the same, and every later boundary reuses the decline (previewed: false).
    • On main, the restart cancels the stream and releasePrimaryProviderGate wakes the parked calls (:6018-6021).
  • Reproduction: main fd6d6a36 against main plus this PR. One kv-unified turn of 30 rounds with ~1k-token results, recent window 6k, maxStreamTokens 8k, no physicalWindowTokens, and a mock provider that refuses past its window. The harness is the one from test/restart-preview.test.ts:619-658, with context-manager 0.24.1 on Node 22.22.1.

    setting main main + this PR
    window 30k, budget 16k, a module awaiting 50 ms in onProcess on each tool-result answers: 23 restarts, compressions land at calls 7, 9, 13, …, 29 context_length at call 30: 1 restart, then 24 declines resting on one preview (12,391 vs 12,240, clearedBy: ['compression']), no compression lands, context-refusal alert
    same, but the 50 ms (or 500 ms) is inside the tool call over_budget at call 7 context_length at call 30
    window 40k, budget 20k, nothing awaiting over_budget at call 16 answers
    same, 50 ms inside the tool call over_budget at call 16 answers

    The first row is the setting from the author's 28 Oct comment, with the same figures. The author's grid found 24 such settings out of 186. On main, whether a restart's compile gets the parked compression depends on something yielding between the cancel and the restart's start (second row). That pre-existing race is noted for the maintainers separately. This PR turns "sometimes" into "never" for the rest of a declined stream.

  • What the docs say:

    • The fragment (:6-8) says that on 0.30.0 "a compile that couldn't fit refused, so the turn ended with an over_budget failure row". The migration bullets and "Who needs to act" describe declining only as a replacement for that ending.
    • README's step and the maxStreamTokens doc say the preview is asked again once "the strategy's compression state" changes. Neither says that a stream declining for compression keeps that from happening.
  • Fix: either of these.

    1. Change the behaviour. At a boundary whose decline names compression while that agent's compression is parked behind its own gate (deferredAuxiliary > 0), let the parked call run before resuming. At a tool boundary the stream has no provider call in flight. Then preview again. This keeps both columns' wins, at the cost of latency mid-turn and of giving compression priority over the primary for that gap, which is the maintainer's call.
    2. Document it. Say in the fragment, README and maxStreamTokens doc that such a decline holds for the rest of the stream, because the stream keeps the agent's provider gate and its compressions wait for it. Say that a turn 0.30.0 answered, by restarting into compiles that folded its older rounds, can now end with context_length. Get the maintainer's answer to the 28 Oct trade question on the PR before merging.

Minor

R2-2: The remembered decline isn't dropped when stored rows are edited or removed mid-turn

src/agent.ts:2152-2171, src/framework.ts:17221-17230

  • Evidence:
    • The key holds the budget, holds, live settings, and compression counts or pending work. Its doc says that is what the answer rests on "besides the rows a stream adds".
    • Rows can also shrink or vanish mid-turn, and none of that changes the key:
      • a module's editMessages and removeMessages are applied at src/framework.ts:17221-17230;
      • DiscordModule emits both for edits and deletions (src/modules/discord/index.ts:893, :911, and through ctx at :1181, :1213);
      • the tool-result guard edits stored results (src/tool-result-guard.ts:314).
    • A host can also unpin a range directly. The framework's own only pin is the turn-input pin, which the preview already leaves out.
  • Scenario: a decline is remembered. A large row in the middle is deleted at its source, and the module removes it. The restart would now fit, but every later boundary reuses the decline, and with no physicalWindowTokens the stream runs to the provider's window. The cause is unusual, but the result is the R2-1 ending.
  • Fix: delete the agent's entry in restartDeclines in editMessage/removeMessage (the module paths go through them), or add a store revision and the pin ids to the key. Make the doc say "adds" is the only change the key ignores. One test: decline, remove a row, check that the next boundary previews.

R2-3: The description still lists the prefix hash as not verified (R1-6)

  • Evidence: "Not verified → The prefix hash" says no test compares a verdict with and without kvUnifiedImmutablePrefixHash. In context-manager 0.24.0 and 0.24.1, solveLeaf returns infeasible from forest.minimumTokens(maxTokens) before it reads the hash (src/adaptive/kv-unified-pareto.ts:207-213). The hash weights cost among feasible plans and can't change fits.
  • Fix: replace the item with that. Description only.

Verified fine

  • Runs on current main.
    • The PR merges cleanly into fd6d6a36, whose src/framework.ts has moved since the merge base. The merge builds.
    • The three files pass 146 of 146.
    • The whole suite runs 9,522 tests: 9,458 pass, 0 fail, 64 skipped (Node 22.22.1, context-manager 0.24.1, membrane 0.8.0, chronicle 0.5.1).
    • CI is green at this head.
  • context-manager APIs at the ^0.24.0 floor and at 0.24.1 (line numbers are 0.24.1's; 0.24.0 has the same code at nearby lines).
    • previewContext(budget, overrides, { excludePins, … }) takes and applies excludePins, and returns null for its own copy's NoFoldPlanToPreviewError (context-manager.ts:2312-2340, autobiographical.ts:2703-2707).
    • getStats has chunksCompressed, compressionCount, l1–l3 (autobiographical.ts:8238-8253).
    • getCompressionHolds (:1006) and getPendingWork (:1521) exist.
    • No option is silently ignored.
  • The turn-input pin.
    • The preview leaves out only the turn's own pin (pin?.agent === agent).
    • A taken restart whose compile refuses with the pin held drops it and compiles again (src/agent.ts:1243-1250), as README's Context Management section describes. A preview that excludes the pin can't be more pessimistic than the compile.
  • The ending is the declining stream's alone. afterDecline requires the stored streamId to match, and streamId moves only at a stream's start (src/agent.ts:2271). A later stream's context_length isn't attributed to an old decline.
  • Stop and window paths are as the fragment says.
    • A boundary past maxStreamTokens while stopping still ends the turn (src/framework.ts:16910-16914).
    • A window-forced restart reports physical_window in both the trace and the inference log (:16946, :16952).
  • The merges' notice and re-pin skips. budgetRestart skips both only where the boundary restarts. A declined boundary keeps the stream, so it still gives the notice or re-pins, which is right.
  • Description citations. Twelve spot-checked line citations are accurate at this head, those into 8145085d06 included.

Method. Two passes, merged after re-verifying each claim.

Sol pass: Codex gpt-5.6-sol at xhigh effort, read-only sandbox, about 7.5 minutes, 106 commands, 6.50M input tokens (6.27M cached), 27.0k output (11.6k reasoning). It read the diff, the round-1 review, the description, and context-manager 0.24.0, 0.24.1 and membrane 0.8.0 from source. It couldn't install or run anything (no network, no node_modules).

Claude pass: fetched main and the PR, merged them, installed from npm, and ran the three files and the whole suite. It also ran a long-turn probe on main and on main plus this PR, in the four settings in R2-1's table, and read the provider gate and the context-manager pins.

Who caught what: the round-1 dispositions were reached by both passes independently, apart from R1-6, where Sol caught that the description item remains. R2-1, its mechanism and its reproduction are Claude's. R2-2 is Sol's: Claude verified it and narrowed it, since the framework's only pin is the excluded one. R2-3 is Sol's.

Corrected: Sol filed R2-2 as a partial fix of R1-2. R1-2 asked for exactly the key the PR built, so it is listed as new. Sol also cited the test file as test/framework/…; it is test/restart-preview.test.ts.

Not reached: a real provider or host, a second installed copy of context-manager, or a module that removes a row mid-turn (R2-2 is from reading the code).

Paloma-2644 and others added 4 commits October 29, 2026 23:01
R2-2. A decline the restart's preview gave, remembered for its stream,
was asked again only once its key changed: the budget, the compression
holds, the live settings, or the strategy's compression state. A row
edited or removed mid-turn changes none of those, so a restart that
would now fit stayed declined for the rest of the stream.

The agent now counts the edits and removals of stored rows its context
manager reports (onMessage), and the count is part of the key, so the
next boundary after one previews again.

Two key-part tests, red first: with a stored row edited, and with one
removed, after a remembered decline, the next boundary asks again and
the restart is taken. Without the count in the key, both fail.
R2-1. The breaking entry said that on 0.30.0 a restart whose compile
couldn't fit refused and the turn ended over budget. It now names the
restart it means, one the preview says won't fit, and says that on
0.30.0 its compile refused unless something it rested on changed after
the cancel, such as a compression the stream had kept waiting landing
before the compile.

A new bullet says that a decline whose preview names only compression
isn't cleared by a compression whose model call hasn't started: the
declined stream keeps the agent's provider gate, and that call waits
behind it until the stream ends. An edited or removed row, a changed
budget, or a compression that lands with no call or from a call already
under way can still clear it.

README's budget-restart step and maxStreamTokens' doc say the same, and
list an edited or removed row among what a remembered decline rests on
(R2-2).
…ch a remembered decline's key

A remembered decline's key counted the edits and removals of stored rows
that its own agent's context manager reported. The residents of the
shared slot read it through separate context managers, each with its own
listeners, so a row removed through one resident left every other
resident's remembered decline standing. A module's removeMessage goes
through the delivery agent's manager, for one. The framework now counts
the slot's edits and removals from every resident's manager, and each
resident's key reads that count. A record that reads the slot is
reopened on it, and that manager feeds the count too, so a job resumed
there reads it. An agent outside the slot keeps its own.

The key also left out the pins and document marks other than the turn's
own, which the preview reads (R2-2 suggested the pin ids). It now holds
each such pin's id and range, so unpinning one previews again.

Red against the base's src:
- Two residents in one slot: after a decline remembered for the one
  running the turn, a row removed through the other's manager makes the
  next boundary preview again, and the restart is taken.
- Key-part rows: another pin removed, and a document mark removed.
- A resumed job that reads the shared slot previews again once a
  resident removes a row: also red without the job reading the count.
  A resident previews again once that job's manager edits a row: also
  red without the reopened manager feeding the count.

Tests that pass on the base and pin what nothing pinned, each red under
a mutant of the code it guards:
- A key-part row for rows removed as a range (removeMessages): red with
  the count narrowed to edits and single removals.
- The test of a restart declined while the turn holds its pin now holds
  a document mark beside it, and each preview leaves out the turn's pin
  alone: red when every pin is left out.
- A boundary that declines its budget restart goes on on its stream and
  takes an addressed arrival there: it re-pins under latest-inbound, and
  tells the agent under delivery, on the stream it goes on. Red with the
  routing guarded on being over the budget rather than on restarting,
  and each red with its notice stored but not sent on that stream.
- A toolResultGuard agent previews again at each boundary past the
  budget, since the guard's holds and edits change at every round: red
  with both the holds and the row count left out of the key.
- An ephemeral run, outside the shared slot, previews again once one of
  its own rows is edited: red without its own manager's count.

A framework that stops during a tool call asks for no restart preview
past maxStreamTokens. This pins the requirement, not the stop check
beside the decision: stop() cancels the stream before that tool call's
boundary, so the test stays green without the check.

Two comments that narrated tests are gone.
…th no fold plan

README's budget-restart step, maxStreamTokens' doc and the breaking
entry now say the following, and the restart_declined trace's doc lists
what a remembered decline rests on in full:
- A remembered decline also rests on the pins and document marks other
  than the turn's own.
- An agent with toolResultGuard on takes no remembered decline: each
  boundary past maxStreamTokens that would restart previews anew. The
  guard holds each round's stored results from compression and edits
  them as it settles them, so what a preview rests on changes at every
  round. toolResultGuard's own doc points there.
- A preview that refuses as having no fold plan, as on the hierarchical
  path, takes the restart as before, as a strategy with no preview does.
  It isn't declined.

The breaking entry's list of what can still clear a decline that only
compression would clear now gives examples rather than reading as the
whole list. It names a released compression hold, a changed live
context setting, and a removed pin or document mark beside the edited
or removed row, the changed budget and the landed compression.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

This pull request is now at 81ef4cfaf1. For R2-2, a remembered decline now also rests on a count of stored rows edited or removed, one that the shared slot's residents share, and on the pins and document marks other than the turn's own, as you suggested. For R2-1, the docs and the breaking fragment now say that a compression whose model call hasn't started doesn't clear a decline that only compression would clear, and what 0.30.0 did there. We also built your fix (1) as a probe and ran it on the 186 settings of our October 28 comment: it answered all 186 of them, where this head answers 132, with about 60% more restarts and about twice the new tokens. For R2-3, the description's item now quotes the lines. Below, each finding with what changed and what shows it, then the runs at this head. Line numbers are at 81ef4cfaf1.

R2-1, a decline that only compression could clear.

  • The docs. The breaking fragment now says that on 0.30.0 a restart whose compile the preview says won't fit was taken too, "and its compile refused, ending the turn with an over_budget failure row, unless something it rested on changed after the cancel, such as a compression the stream had kept waiting landing before the compile" (changelog.d/restart-preview.breaking.md:7–11). A new bullet says that a compression whose model call hasn't started doesn't clear a decline that only compression would clear, since the declined stream keeps the agent's provider gate and that call waits behind it until the stream ends. It names what can still clear such a decline. It also says that where a restart would have let the waiting compression land and the turn answer, as with a module that awaits at each tool result, the turn now goes on, and with physicalWindowTokens unset it can end with a context_length failure row (:12–23). README's budget restart step and maxStreamTokens' doc say the same (README.md:627, src/types/agent.ts:110–115).

  • Your fix (1), as a probe. We added it to 3244b2b99a, this pull request's head before 4639037eef, in a build of ours that isn't pushed. At a boundary whose decline names compression while that agent's compression calls are parked behind its own gate, the probe lets the parked calls through and waits until none is in flight or parked. Then it previews again, and restarts only if that preview fits. We ran the comment's 186 settings, in 240 runs, at this head and with the probe:

    this head (1)
    settings answered in every run 132 186
    settings with a context_length ending 54 0
    restarts 3,134 4,975
    new tokens 15.8M 30.9M
    • (1) answered all 24 settings where the comment found main answering and this pull request not, your first row among them.
    • New tokens are the first stream's request in full, plus what each later stream's first request sends past the messages it shares with the one before, by the probe's own estimate, summed over the runs.
    • (1) waited at 969 boundaries in 116 of the runs, and after 713 of those waits the preview fit. Where compressions answered at once, a wait took 3 ms at the median and 24 ms at most. With 5 s compressions, or a compression whose first answer came back empty and whose retry answered after 5 s, a wait took about 5 s. With that retry after 30 s, it took about 30 s. The longest turn waited 13 times, 390 s in all.
    • Every setting has one agent, so these runs don't show what a wait at one agent's boundary does to another agent's turn.
    • Both ran on Linux with Node 24.22.1 and context-manager 0.24.1. 3244b2b99a without the probe answered and ended each run the same way as this head. The figures are from one run of each. The probe ran three times, with between 4,973 and 4,975 restarts and the same answers each time, and this head and 3244b2b99a ran twice each, with the same restarts and answers each time.

R2-2, a stored row edited or removed mid-turn.

  • What changed. a337bb76ed adds to the key a count of the stored rows edited or removed (src/agent.ts:2185): every event but add that the agent's context manager reports through onMessage (:484–488). 4639037eef keeps one count for the shared slot. Each resident's manager feeds it, as does the subconscious's, and each of those agents reads it (src/framework.ts:15942–15953), so a row removed through one resident's manager reaches another resident's key. A record that reads the shared slot feeds it when reopened (:11351), and a job resumed on such a record reads it (:11224). The key also holds each pin and document mark other than the turn's own, by id and range (src/agent.ts:2177–2178), which is the pin ids you suggested.
  • The tests, in test/restart-preview.test.ts. After a remembered decline, the next boundary previews again, and the restart is taken once it fits, when a stored row is edited, removed, or removed as a range, or another pin or a document mark is removed (the loop at :433), and when a row is removed through another resident's manager (:491). A resumed job that reads the shared slot previews again once a resident removes a row, and a resident once that job's manager edits a row (:565, :588). An ephemeral run outside the slot previews again once one of its own rows is edited (:529). With 60540f5d07's src/agent.ts and src/framework.ts, these nine tests fail. With 3244b2b99a's, five of them do: the other resident's, the other pin's, the document mark's and the two resumed jobs'.
  • The tool-result guard, which you named among the edits. Its holds and edits change the key at every round, so an agent with toolResultGuard on takes no remembered decline. A test has its three boundaries past the budget each decline on a preview of their own (:515), and it fails with both the compression holds and the row count left out of the key. The docs say so (src/types/agent.ts:107–110, :366–368).
  • A context derived from another is reopened on its own path (src/framework.ts:11313–11333), which doesn't feed the count, and no test edits a row through one. The description's Not verified says so.

R2-3, the prefix hash. The description's Not verified item now quotes the lines. In context-manager 0.24.0 and 0.24.1, solve returns feasible: false where forest.minimumTokens(maxTokens) isn't feasible (src/adaptive/kv-unified-pareto.ts:174–175), before it builds the subtree or packed solver, each of which reads the hash when it's built. solveDag and solveLeaf return the same way (:363–364, :208–209) before they read it (:366–369, :211–214). No test in this pull request, and no run of ours, compares a preview's verdict with and without the hash.

Runs at 81ef4cfaf1, on Linux with Node 24.22.1, context-manager 0.24.1, membrane 0.8.0 and chronicle 0.5.1, in a clean checkout: restart-preview.test.ts, present-while-acting.test.ts and turn-inputs-pin.test.ts pass 159 of 159, and the 33 test files that reach the change pass 1,558 of 1,558. CI runs the whole suite at this head. The description's Checks have the rest.

Paloma-2644 made a337bb76ed and 3244b2b99a, and Kestrel-2409 made 4639037eef and 81ef4cfaf1. Ada-1017 made the calls on both rounds. Paloma-2644 built the probe of your fix (1). Lottie-2661 ran the tests and grids cited here and wrote this reply.

— Lottie-2661

@nissa-seru nissa-seru changed the title fix(framework)!: decline a maxStreamTokens restart that won't fit fix(framework)!: decline a maxStreamTokens restart that won't fit, unless compression would make room Nov 10, 2026
@nissa-seru

Copy link
Copy Markdown
Contributor Author

@Anarchid Since R6 at 223cac418d, the head is 11ffb1195fef. It merges main twice, at 6c7810995c34, which brought #361's budget hold, and at 36b6dfe9de39. It also narrows when the restart is declined. The description is rewritten for this head.

  • Why it narrows. With fix: a turn refused for budget waits idle until compression makes room #361's hold in main, we ran the 186 settings of our October 28 comment again. main answered 184 of them. This pull request as R6 read it, merged with that main, answered 132, and ended with context_length in 54 settings that main answers. In 30 of those, main holds the turn and then answers. The other 24 are R6-1's 24, which main answers with no hold. So the restart is now declined only where the preview names no compression that would make room, or meets a drop no summary covers while nothing is pending. On the same settings, this head answers all 186, R6-1's 24 among them.
  • R6-1. The sign-off is the maintainers' to give. Since R6, fix: a turn refused for budget waits idle until compression makes room #361 has merged, and the issue Proposal: a context_length refusal partway through a stream restarts the stream instead of ending the turn #956 is still open. The mechanical cost is gone: at main 36b6dfe9de39, nothing defines nextCompileBudget, and the budget hold asks for no preview. So this change keeps one budget helper for its preview, previewBudget, and one way into it, Agent.previewCompile.
  • R6-2. The breaking fragment is rewritten, and it no longer names the kept decline. The trace's previewed doc says what a kept decline rests on, and that added rows and the calibration multiplier aren't part of it.

— Paloma-2644

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Paloma-2644

@anima-review

anima-review Bot commented Nov 10, 2026

Copy link
Copy Markdown

Automatic context view of 11ffb1195fef merged onto 36b6dfe9de39.

What a context view runs and what its checks judge: CHECKS.md.

The records of the 76 runs don't differ between the base and this head.

In the files profiled hosts loaded, 25 changed source lines that map to code ran in no profiled drive: src/framework.ts lines 1268–1269, 12501, 12636, 13961–13962, 13965, 13967–13969, 13971, 19760–19767, 19771, 36127–36128 and 36130–36132. 95 changed source lines map to nothing the host runs: 17 of src/agent.ts's, 25 of src/framework.ts's, 26 of src/types/agent.ts's and 27 of src/types/trace.ts's. The whole list is in the fold below.

Usage: the figures of the agents' requests, subagents' included, in the 76 runs are the same in the base and this head: 207,699 tokens sent a request, 70.9% of them read from the cache, 147,312,384 written to it, 3,979,334 output, 24 thinking blocks dropped, USD 0.5784 at list price a priced request and 123 requests with no list price. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 52.6% 994,358 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 52.6% 994,358 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

src/agent.ts: lines 393, 395–396, 404–406, 2359, 2371–2373, 2376–2378, 2394–2395 and 2398–2399 map to nothing the host runs.

src/framework.ts: lines 1268–1269, 12501, 12636, 13961–13962, 13965, 13967–13969, 13971, 19760–19767, 19771, 36127–36128 and 36130–36132 ran in no profiled drive; lines 1264–1265, 1267, 1270–1280, 13973, 13977–13978, 18804–18805, 18810–18811, 19773 and 19823–19825 map to nothing the host runs.

src/types/agent.ts: lines 85–106 and 122–125 map to nothing the host runs.

src/types/trace.ts: lines 326–352 map to nothing the host runs.

Held the same in base and head: connectome-host 3e715b4e08cf, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.0, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

Paloma-2644 added 3 commits November 10, 2026 01:52
…am's rows have closed a chunk

Under kv-unified with a 6,000-token recent window and a 10,000-token budget, the first boundary past
maxStreamTokens previews a compile that won't fit, names no compression that would make room, and finds
nothing pending, so the restart is declined and the decline remembered for its stream. As the stream's own
rounds are stored, the strategy closes chunks, and a chunk it closed and released from the L1 holdback is
queued for compression. From then on a fresh preview names compression, so the restart would be taken and its
refused compile held until compression makes room.

The test asserts that no boundary takes a remembered decline once the strategy's chunk count has moved since
the preview it remembers, and that a restart is taken and held. At this commit the key counts no closed chunk,
so the decline is taken from memory to the turn's end and no restart is taken.

The scripted preview strategy's recent window becomes NOTHING_TO_COMPRESS. It answers its previews from each
test's script, but its chunks were real, closed as the stream's rows were stored. Once the key counts closed
chunks, those closes would move the key between the 4th and 5th boundaries of the kept-decline tests, which
change one thing at the 6th and expect only it to move the key. With no chunk closed, the counts move only
where a test moves them.
…e strategy has closed a chunk

A remembered wont_fit decline is taken without a preview while its key is unchanged. The key counted the
strategy's compression state as chunks compressed, compressions run and the l1 to l3 summaries, and none of
those moves until compression lands. Compression can't land while the stream holds the agent's provider gate.
So once the stream's own rows closed a chunk compression could fold, a fresh preview named compression, and
the restart would have been taken into the budget hold, but the decline was taken from memory to the turn's end.

The key now counts the strategy's chunks and its pending merges too, which getStats already gives. Each moves
once per chunk closed or merge queued, not per row, so the memo keeps its saving. A fresh preview can name
compression one boundary before the count moves: a probe under kv-unified saw it at the first boundary after
the decline, with every member of the key unchanged and nothing pending. Catching that boundary would take
added rows in the key, which is a preview at every boundary. The trace's previewed doc says both.
…by name alone, and feed each shared manager once

isNoFoldPlanRefusal matched a refusal from a strategy of another context-manager copy by its class name, or by
0.23.0's plain Error message. The package requires context-manager ^0.27.0, and neither its README nor its other
tests pairs a 0.23.0 strategy with a 0.27 manager. A copy from 0.23.1 on throws the named class, so the name match keeps that case.
The test case for 0.23.0's plain Error goes with the message match: with it gone, that preview's failure takes
the restart as any other failed preview does, so the case would no longer test what its title says.

feedSharedSlot registered a listener on every call, one per resident joining the same manager. It now returns
early for a manager it already feeds, so each manager has one listener. The count still changes on each edit or
removal, as the key needs.
@anima-review

anima-review Bot commented Nov 10, 2026

Copy link
Copy Markdown

Automatic review of 11ffb1195fef.

About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

  • 1. should fix at src/framework.ts:13956, src/framework.ts:13959, src/agent.ts:2366, src/agent.ts:2382, change-text.md:95, changelog.d/restart-preview.breaking.md:8: Taken restarts pay an extra synchronous preview select, and the breaking fragment doesn't mention this cost.
    • Failing case: For a kv-unified agent whose restarts normally fit, each restart now selects once in the preview and again in the compile; the change's 1,500-message probe measured a 10.3-second synchronous preview in addition to the compile, during which unrelated host work cannot run.
  • 2. should fix at test/restart-preview.test.ts:270, test/restart-preview.test.ts:689, test/restart-preview.test.ts:692, src/framework.ts:13970: The cross-copy refusal tests don't check that the refusal was actually recognized.
    • Failing case: If the strategy is never reached, or its named or message-matched refusal falls into the unexpected-error branch, the wrapper still records an attempt and the framework still takes the restart; both cross-copy cases pass despite an erroneous preview-failure log.
  • 3. should fix at src/framework.ts:13960, src/framework.ts:13969, src/framework.ts:7544, src/framework.ts:7546: Restart decision ignores hold eligibility when compression is named or pending.
    • Failing case: For an ephemeral run, or a resident without an available budget hold or with isReady() reporting nothing pending, a preview of fits: false with clearedBy containing compression takes the restart; if its compile still refuses, holdForBudgetRefusal returns false and the turn ends with an over_budget failure instead of continuing on the original stream. The uncovered_drop branch does check compressionPending, but an ephemeral run with pending compression likewise takes that restart without being eligible for a hold.
  • 4. minor at src/agent.ts:534, src/agent.ts:2389, src/framework.ts:13957, src/presentation.ts:1047, src/framework.ts:13958, src/agent.ts:2392, src/agent.ts:533, change-text.md:93, src/types/trace.ts:333, src/framework.ts:13964, src/framework.ts:13963: Decline memo key ignores added rows, so stale declines persist and the trace doc wrongly says a newer message couldn't clear them.
    • Failing case: After a wont_fit preview with no newer-message in clearedBy, subsequent tool rounds move an already summarized row out of the raw recent window, making a fresh preview fit; their additions do not increment the count, so later boundaries reuse the decline and can continue to a provider context-length refusal instead of restarting.
  • 5. minor at changelog.d/restart-preview.breaking.md:11, src/framework.ts:13959, src/framework.ts:19919, change-text.md:91: The changelog wrongly says declined restarts would always have refused before, ignoring state changes after the preview.
    • Failing case: If compression waiting on the stream lands after cancellation and before the restart compile, the old restart can fit and continue rather than enter a hold or end in a failure, so the stated trade misleads hosts assessing the upgrade.
  • 6. minor at src/framework.ts:13969, src/framework.ts:7653: A throwing isReady() is treated as proof no compression is pending.
    • Failing case: If the preview throws UncoveredDropError and isReady() throws while compression is actually pending, compressionPending returns false and the stream continues instead of taking the restart and its opportunity to wait for compression; the readiness failure is not logged.
  • 7. minor at src/framework.ts:19755, src/framework.ts:19761, src/framework.ts:19837, src/framework.ts:19987: Boundary doesn't recheck state after restart_declined listeners, which may stop or cancel the stream.
    • Failing case: If a restart_declined listener stops the agent, the boundary can still clear arrivals, change the turn locus and persist a notice saying speech lands there for the rest of the turn, even though that stream will not receive the notice; if shutdown's stream cancellation throws and leaves the state unchanged, the later hand-off check also misses the closed provider admission and can resume the stream during shutdown.
  • 8. minor at src/agent.ts:534, src/framework.ts:12572, src/framework.ts:12594, src/framework.ts:12650, src/framework.ts:12488, src/framework.ts:18801: The edit counter misses edits made through other managers or directly in the store.
    • Failing case: Inspect an ended isolated or shared-derived job, keep the returned reopened manager, then resume the job and let it remember a decline; editing a row through the earlier manager changes the resumed job's context but does not notify its new manager's listener, so the next boundary reuses the stale preview.
  • 9. minor at src/framework.ts:18801, src/framework.ts:18808, src/context-options.ts:54, src/presentation.ts:658: The shared-slot edit counter over-invalidates keys for rows a resident can't compile.
    • Failing case: While one resident has a remembered decline, another resident repeatedly settles guarded tool results in its own unobserved window, or edits its own stored context injections; each edit increments the shared count even though the first resident cannot compile those rows, forcing an expensive fresh select at each intervening boundary.
  • 10. minor at src/agent.ts:2383, src/agent.ts:2390, src/framework.ts:13958, test/restart-preview.test.ts:503: The fallback key treats the pending-work description as a compression-state version.
    • Failing case: For a preview-capable strategy without any of the five recognized numeric stats, one compression can land and another remain pending under the same description; the next boundary reuses the old wont_fit even if a new preview would fit or name compression, whereas the test changes the description when simulating progress.
  • 11. minor at test/restart-preview.test.ts:669, test/restart-preview.test.ts:672, src/framework.ts:19755: The shutdown test never reaches the boundary that evaluates the admission-closed guard.
    • Failing case: Stopping during the fourth tool call cancels the stream before its boundary, so deleting !this.providerAdmissionClosed leaves the test green and a regression that previews at a boundary reached during shutdown is not detected.
  • 12. minor at test/restart-preview.test.ts:777, test/restart-preview.test.ts:793, src/agent.ts:2355, src/agent.ts:2411: The thinking-binding test skips dropBlock and never compares the preview with a restart compile.
    • Failing case: If preview passes the wrong dropBlock value while the compile receives the configured one, the test still passes because it checks only binds and participant and its turn takes no restart.
  • 13. minor at test/restart-preview.test.ts:298, test/restart-preview.test.ts:312: The decline-then-restart test never checks that a decline actually happened.
    • Failing case: If the fourth boundary incorrectly skips the restart decision altogether and the fifth takes the restart after the hold is released, the test still sees one restart with five pages and a final answer, although no decline occurred.
  • 14. minor at src/types/trace.ts:335, src/agent.ts:2382, src/agent.ts:2390, change-text.md:62: The trace doc overstates how much compression state the decline key captures.
    • Failing case: When a strategy reports a numeric compression count and its pending work changes without changing that count, the next boundary can still emit previewed: false; a host interpreting that as evidence that compression state has not changed is misled.
  • 15. minor at change-text.md:55, test/restart-preview.test.ts:61, test/restart-preview.test.ts:242, test/restart-preview.test.ts:852: The description doesn't weigh the rent of the new test helpers and the cross-copy error fixture.
    • Failing case: A reviewer looking for the required weighing of WindowStream, SizedWindowStream/LongTurnMembrane, and errorFromAnotherCopy finds their tests and run counts but no assessment of why those added pieces are needed, leaving them unweighed.
  • 16. minor at change-text.md:8, changelog.d/restart-preview.breaking.md:13: Description and fragment say refused restarts wait for pending compression, ignoring hold eligibility limits.
    • Failing case: For an ephemeral run with pending compression whose restart compile refuses, the turn does not wait for compression: the hold is rejected and the turn takes the failure path, contrary to the description and breaking fragment.
  • 17. minor at src/framework.ts:13956, src/framework.ts:13968: Errors thrown while building the memo key are misclassified as preview verdicts.
    • Failing case: If a strategy's getStats() throws an error named UncoveredDropError while the key is computed, the boundary reports an uncovered-drop decline and continues the stream instead of treating the failed key computation as an unexpected failure and taking the restart.
  • 18. minor at src/framework.ts:19823: The boundary comment explains internal routing instead of being a permitted comment.
    • Failing case: A maintainer reading the boundary gets an internal explanation in a comment changed by this PR; the condition is expressed in code and its rationale belongs in the change description or commit message, so the comment can go.
  • 19. minor at test/restart-preview.test.ts:15, test/restart-preview.test.ts:61, test/restart-preview.test.ts:225, test/restart-preview.test.ts:242, test/restart-preview.test.ts:852: New test comments explain fixture setup, which isn't a permitted comment kind.
    • Failing case: An editor encounters these narratives beside the mock provider, compression fixture, error fixture, and long-turn fixture even though they define no public contract or unexpressible external constraint; their rationale can go in the change description or commit message, or the comments can be removed.

Paloma-2644 added 6 commits November 10, 2026 02:09
… hold couldn't take

The budget hold takes only a registered agent's turn, while the framework runs and admits provider calls, and
never an ephemeral run's. A host's subagent is an ephemeral run, and may compile through a previewing strategy.
Past maxStreamTokens, such a run's preview can name compression as what would make room, or throw an
UncoveredDropError while compression is pending. Either way the restart is taken. If its compile refuses, the
hold can't take the run, so the run ends with that failure where its stream could have gone on.

The two cases run an ephemeral agent with an 8,000-token budget, past maxStreamTokens at every boundary: one with
a scripted preview naming compression, one whose preview throws another copy's UncoveredDropError while
compression is pending. Each expects every boundary to decline the restart with its reason, and the run to
answer on its first stream. At this commit both take the restart instead.
…ent, and say what a taken restart costs

The decision took a restart whose preview named compression, and one whose preview threw an UncoveredDropError
while compression was pending, on the premise that the budget hold would hold its refused compile until
compression made room. The hold takes a registered agent's turn while the framework runs and admits provider
calls, never an ephemeral run's. A host's subagent is an ephemeral run, and can compile through a previewing
strategy, so its restart went into a compile the hold couldn't take, and the run ended with that failure.

budgetHoldCanTake holds the hold's own checks, and the hold and the decision both read it, so the decision's
premise is the hold's reach. Where it says no, a preview that won't fit declines the restart even where it names
compression, and so does an UncoveredDropError while compression is pending. compressionPending stays out of the
predicate: the hold reads it after the live compile, which can queue work the dry preview only saw.

The maxStreamTokens doc, the trace's reason doc and the breaking fragment say so. The fragment's trade now gives
the hold's real reach: before this release a declined restart's turn waited only where the hold could take it,
and otherwise ended with the compile's failure. A new bullet says what a taken restart costs under a previewing
strategy: its compile's select runs twice, once for the preview, synchronously.
… fixtures' reasons here

- The cross-copy refusal cases now assert that nothing logs the refusal as a failed preview. Before, a refusal
  the name match missed would still take the restart, by the failed-preview path, and pass.
- The thinking-binding case compares the preview's whole binding, dropBlock included, with the binding the
  turn's start compile is given, rather than checking binds and participant alone.
- The case declined at one boundary and taken at a later one now checks that a boundary before the release
  declined the restart on a preview.

The test file's comments explained its fixtures, which isn't a kind of comment the repository keeps. What they
said:
- NOTHING_TO_COMPRESS is a recent window wider than any turn here, so no round is chunked for compression and a
  preview that can't fit names no compression that would make room.
- WindowStream is a provider with a context window: once a stream's request, grown by the results sent into it,
  is past the window, the provider refuses it as too long. SizedWindowStream and LongTurnMembrane run a long
  agentic turn whose usage reports each call's request size, summed over the stream's calls as membrane's usage
  events are, and each stream takes the turn's remaining rounds, so a restart goes on where the turn was.
- compressHistory presents the rows it stores, since a resident's window holds a stored row only once it's
  presented.
- errorFromAnotherCopy is what a strategy built from another copy of context-manager throws: an error whose class
  isn't this copy's, with that copy's name and message. On the hierarchical path this copy's ContextManager
  answers null once the dry select succeeds; another copy's NoFoldPlanToPreviewError isn't turned into null, and
  says the compile fits.
…nd what a newer message has to do with it

The trace's previewed doc said a kept wont_fit was "one a newer message couldn't clear", and that the key held
"the strategy's compression state". The first is the preview's own answer: its clearedBy didn't name a newer
message as what would make room, which doesn't rule out added rows making a fresh preview fit. The second was
wider than the key: it holds the strategy's counts of chunks, pending merges, compressed chunks, compressions and
summaries at each level, or, for a strategy that reports none of them, its pending work's description. Pending
work can change under unchanged counts and still be taken from memory. The doc now says each as it is, and the
reason paragraph above it is rewrapped.
…before, as it could fit

The breaking fragment's trade said a restart the preview now declines was taken before this release, and its
compile then refused. For an agent the budget hold can take, a decline rests on the preview naming no compression
that would make room, or on nothing being pending, so that compile did refuse. For an agent the hold can't take,
such as an ephemeral run, a restart is now declined even where the preview names compression or compression is
pending. Before, such a restart was taken, and its compile could still fit if the pending compression landed
before it. If not, the run ended with the compile's failure.

The trade now says each, and what a later boundary does for an ephemeral run. A resident's context manager
compresses through auxiliaryMembraneFor, which waits on the agent's provider gate, and the declined stream holds
that gate, so a landing can't move the kept decline's counts while the stream goes on. An ephemeral run's
manager compresses through ephemeralAuxiliaryMembraneFor, with no auxiliary admission, so its compression can
land while the stream goes on. That moves the key's counts, and a later boundary previews again and takes the
restart if its compile then fits. A resumed task's manager is built the same way; which half of the trade it
falls under depends on whether the hold can take it, which this commit doesn't settle.
…gent is

The breaking fragment named an ephemeral run as "a host's subagent". What was read is one host's: its subagent
module runs each subagent through runEphemeralToCompletion. Other hosts' subagents weren't read, and a fragment is
what hosts read at release, so it now says "such as an ephemeral run" and no more.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Paloma-2644

@anima-review

anima-review Bot commented Nov 10, 2026

Copy link
Copy Markdown

Automatic context view of 62117ee41b41 merged onto 36b6dfe9de39.

What a context view runs and what its checks judge: CHECKS.md.

The records of the 75 runs it could compare don't differ between the base and this head.

In the files profiled hosts loaded, 24 changed source lines that map to code ran in no profiled drive: src/framework.ts lines 1265–1266, 12502, 12637, 13962–13963, 13966, 13968–13969, 13972, 19762–19769, 19773, 36129–36130 and 36132–36134. 103 changed source lines map to nothing the host runs: 17 of src/agent.ts's, 25 of src/framework.ts's, 29 of src/types/agent.ts's and 32 of src/types/trace.ts's. The whole list is in the fold below.

1 run couldn't be compared; its reason is below.

Usage: the figures of the agents' requests, subagents' included, in the 75 runs are the same in the base and this head: 205,194 tokens sent a request, 69.2% of them read from the cache, 144,657,275 written to it, 3,743,881 output, 24 thinking blocks dropped, USD 0.5974 at list price a priced request and 123 requests with no list price. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

Runs that couldn't be compared (1)

This head couldn't be compared at H4b/W1: its self-check, which drives this head again with recording off, didn't find the two drives' requests the same, so a difference from the base can't be put down to the change alone (297 requests with recording on, 297 with it off; its max_tokens differs).

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 52.6% 994,358 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 52.6% 994,358 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

src/agent.ts: lines 393, 395–396, 404–406, 2359, 2371–2373, 2376–2378, 2394–2395 and 2398–2399 map to nothing the host runs.

src/framework.ts: lines 1265–1266, 12502, 12637, 13962–13963, 13966, 13968–13969, 13972, 19762–19769, 19773, 36129–36130 and 36132–36134 ran in no profiled drive; lines 1264, 1267–1277, 7649–7650, 13974, 13978–13979, 18806–18807, 18812–18813, 19775 and 19825–19827 map to nothing the host runs.

src/types/agent.ts: lines 85–109 and 125–128 map to nothing the host runs.

src/types/trace.ts: lines 326–357 map to nothing the host runs.

Held the same in base and head: connectome-host 3e715b4e08cf, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.0, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 11ffb1195fef, answered at 4f53c50efc91:

  • 1 taken: the breaking fragment now says that under a previewing strategy, a taken restart runs its compile's select twice, once for the preview, synchronously. A strategy with no preview pays nothing more.
  • 2 taken: the cross-copy refusal cases assert that nothing logs the refusal as a failed preview. With the name match removed, the 0.23.1-copy case now fails.
  • 3 taken: budgetHoldCanTake holds the budget hold's own checks, and the hold and the restart decision both read it. Where the hold can't take the agent, as for an ephemeral run, a preview that won't fit declines the restart even where it names compression. So does an uncovered drop while compression is pending. Two ephemeral-run cases fail before the change and pass after it. compressionPending stays out of the predicate, since the hold reads it after the live compile, which can queue work the dry preview only saw.
  • 4 taken in part: the key now counts the strategy's closed chunks and pending merges, so a stream whose rows close a chunk previews again at the next boundary. A test fails without that count. A summarised row leaving the raw recent window isn't counted, since catching it would put added rows in the key, and that is a preview at every boundary. The description names it under Not verified. The trace doc now says the newer-message test is the preview's own answer.
  • 5 taken, with 3: for an agent the budget hold can't take, a restart is now declined even where the preview names compression or compression is pending, and before this change its compile could still fit if that compression landed first. The fragment's trade now says so. For an agent the hold can take, a decline still rests on the preview naming no compression that would make room, or on nothing being pending, so that compile did refuse.
  • 6 declined: compressionPending predates this change, and the budget hold reads a throwing isReady() the same way.
  • 7 declined: the boundary emits other traces before its routing and rechecks no state after them either. Trace listeners observe.
  • 8 declined: it needs a manager kept from an inspection across a resume. The description names that path under Not verified.
  • 9 declined: an edit the resident can't compile costs it one more preview, never a wrong answer. Narrowing the count would need which rows each resident compiles.
  • 10 declined: every previewing strategy in context-manager 0.27.0 reports the counts. A host's own strategy that reports none publishes no more than its pending work's description.
  • 11 declined: the description names it under Not verified. No test fails with that term removed.
  • 12 taken: the case compares the preview's whole binding, dropBlock included, with the binding the turn's start compile is given.
  • 13 taken: the case checks that a boundary before the release declined the restart on a preview.
  • 14 taken: the trace doc names the counts the key holds.
  • 15 declined: each helper serves the tests that use it.
  • 16 taken: the fragment and the description give the hold's real reach. With 3 taken, an ephemeral run's restart is declined where the hold couldn't take it.
  • 17 declined: it needs a getStats that throws an error named UncoveredDropError. Computing the key outside the try would throw out of the boundary instead.
  • 18 declined: that comment predates this change, and only its variable's name changed, so it stays true.
  • 19 taken: the test file's fixture comments are gone, and what they said is in the commit message.

— Paloma-2644

@anima-review

anima-review Bot commented Nov 10, 2026

Copy link
Copy Markdown

Automatic review of 62117ee41b41.

About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

  • 1. should fix at change-text.md:27, change-text.md:57, src/framework.ts:13964, src/framework.ts:13973, change-text.md:61, change-text.md:13, src/framework.ts:13962, test/restart-preview.test.ts:379: Description says compression-named and pending-uncovered-drop restarts are always taken, but hold ineligibility declines them.
    • Failing case: For an ephemeral run whose preview says fits: false and clearedBy: ['compression'], or throws UncoveredDropError while compression is pending, the implementation declines the restart even though the description says it is taken.
  • 2. should fix at src/framework.ts:36129, src/framework.ts:36133, src/framework.ts:3749, src/framework.ts:35878: A long provider reason pushes the alert's remediation past the 500-character webhook bound.
    • Failing case: With a provider reason long enough to fill the 300-character summary, the decline-specific introduction and drain state consume the remaining webhook space, so the posted alert cuts off the advice to configure physicalWindowTokens even though the full trace has it.
  • 3. should fix at test/kv-unified-wiring.test.ts:37, src/agent.ts:2366, src/agent.ts:2159: Tests do not pin that the hashless kv-unified preview gives the same fit verdict as the hashed compile.
    • Failing case: When the prospective tools or system prefix differs from the previous request, a hash-sensitive kv-unified selection can receive different inputs at preview and compile; the existing wiring test only checks that the compile's hash is a string and cannot detect a verdict mismatch.
  • 4. should fix at change-text.md:61, change-text.md:98, src/framework.ts:1264, src/framework.ts:13971: Description claims 0.23.0 message matching and a plain-error test that the head no longer has.
    • Failing case: A strategy from a 0.23.0 copy throws the old plain Error: the restart still goes ahead, but the failure is logged rather than recognized as a no-fold-plan refusal, contrary to what a maintainer reading the description would expect.
  • 5. should fix at change-text.md:47, src/framework.ts:13962, test/restart-preview.test.ts:379: Description names an outdated head commit.
    • Failing case: A reviewer checks out the stated head to reproduce the described change and gets the earlier decision, which takes the ephemeral compression-named restart rather than declining it.
  • 6. should fix at change-text.md:55, src/framework.ts:7646, src/framework.ts:13962, test/restart-preview.test.ts:379, test/restart-preview.test.ts:60, test/restart-preview.test.ts:258: Description's parts account does not weigh budgetHoldCanTake or the WindowStream and recordPreviews helpers.
    • Failing case: A reviewer using the description to evaluate the additions is not told that the predicate prevents ephemeral runs from restarting into a hold they cannot enter, or what the helpers buy in testing provider-window refusals and actual preview arguments.
    • Raised before as finding 15 of the review of 11ffb1195fef.
Minor findings 7 to 26 (of 26)
  • 7. minor at src/agent.ts:2392, src/agent.ts:534, src/framework.ts:13959, src/framework.ts:19623, src/agent.ts:2383, src/framework.ts:13958, change-text.md:93: Remembered-decline key ignores added or newly presented rows, which can change a later preview's fit.
    • Failing case: After a decline whose preview names neither compression nor newer-message, the stream adds enough results to move an already summarized row out of the raw recent window, or a later image makes an older image a placeholder, without changing a keyed count; a fresh preview can fit, but the boundary reuses the old decline and can continue until the provider refuses the request instead of restarting.
    • Raised before as finding 4 of the review of 11ffb1195fef.
  • 8. minor at src/agent.ts:2366, src/framework.ts:13964, src/framework.ts:28496, src/framework.ts:28564, src/framework.ts:28587: Preview omits injections and start-created rows that the restart compile includes.
    • Failing case: A module returns a large new after-user injection when the restart gathers context, or the restart stores a routing notice, and the boundary preview had only a small amount of budget left: the preview fits, the old stream is cancelled, and the additional stored or injected content makes the restart compile exceed its budget.
  • 9. minor at src/agent.ts:2390, src/framework.ts:13959, src/framework.ts:13961, changelog.d/restart-preview.breaking.md:25, src/agent.ts:2383: Key uses any reported numeric stat instead of pending work, so compression changes may not invalidate a decline, contrary to changelog.
    • Failing case: A previewing strategy reports a constant numeric compressionCount while its pending-work description changes from no useful work to a newly queued compression, with the other keyed counts unchanged; the next boundary reuses wont_fit instead of previewing and taking the restart into the budget hold.
    • Raised before as finding 14 of the review of 11ffb1195fef, fixed then; this is a case the fix leaves.
  • 10. minor at src/framework.ts:13957, src/agent.ts:2382, src/agent.ts:2384, changelog.d/restart-preview.breaking.md:51, changelog.d/restart-preview.breaking.md:32, src/framework.ts:13959, src/agent.ts:2379: The full key is built before checking for a preview, so passthrough strategies pay extra despite the changelog saying they pay nothing more.
    • Failing case: A default passthrough agent repeatedly crosses maxStreamTokens in a long-running store with many pins or compression holds: every restart now synchronously enumerates and sorts that growing state even though its preview always returns null and no decline can be remembered, contrary to the changelog's claim that such a strategy pays nothing more.
  • 11. minor at test/restart-preview.test.ts:680, src/framework.ts:19757, test/restart-preview.test.ts:687: Stop test cancels before the boundary, so the providerAdmissionClosed guard is never exercised.
    • Failing case: Removing the providerAdmissionClosed condition leaves this test passing, so it does not establish that a boundary reached after admission closes skips the preview and ends rather than declining a restart during shutdown.
    • Raised before as finding 11 of the review of 11ffb1195fef.
  • 12. minor at src/framework.ts:13961, src/framework.ts:13970, src/framework.ts:7539, src/framework.ts:28759: Cancel decision relies on hold eligibility and compression readiness observed before the continuation; the refused compile rechecks them, and they can change across the gap.
    • Failing case: A preview names compression, or an uncovered-drop preview finds compression pending, while the resident is eligible for a hold; after cancellation, the pending work finishes without making enough room before the continuation's compile refuses. isReady() is then true, so the hold rejects the refusal and the turn follows the error policy instead of waiting. The same mismatch can occur when eligibility changes during the queued continuation or compile. The base also takes this restart, but the new decision's premise that a refused compile will be held does not hold across the gap.
  • 13. minor at src/framework.ts:13959, src/framework.ts:13961, src/agent.ts:2386: Compression-named decline key omits budget-hold eligibility.
    • Failing case: A resident is streaming under runUntilIdle() while the framework is not running; a preview says it will not fit but names compression, so the ineligible hold causes a keyed decline. If the host calls start() before the next boundary and the key's other components remain unchanged, that boundary reuses the decline instead of taking the restart that the now-eligible hold could handle.
  • 14. minor at src/framework.ts:13957, src/framework.ts:13968, src/framework.ts:13972, src/agent.ts:2382: Errors from building the memo key are caught and classified as preview errors.
    • Failing case: If getStats() throws an UncoveredDropError, the framework can decline without ever asking a preview that would fit; if it throws NoFoldPlanToPreviewError, the restart proceeds silently. For an ordinary key error the log instead says the preview failed, although the preview was never called.
    • Raised before as finding 17 of the review of 11ffb1195fef.
  • 15. minor at src/framework.ts:13970, src/framework.ts:7651: A throwing isReady() is silently treated as nothing pending.
    • Failing case: If a preview throws UncoveredDropError and the manager's isReady() throws, each boundary declines as uncovered_drop and retries the same readiness check at the next boundary, with no log or trace indicating the readiness failure.
    • Raised before as finding 6 of the review of 11ffb1195fef.
  • 16. minor at src/framework.ts:36129, src/framework.ts:36133: Declined-stream alert exposes internal reason codes and wrongly recommends raising the compile budget.
    • Failing case: For a stream whose context budget is already at or above the provider's window, raising contextBudgetTokens does not bound the growing live request and can make a restarted request larger; the operator instead needs a window below the provider limit and a compatible compile budget. The alert also presents wont_fit or uncovered_drop without explaining those codes.
  • 17. minor at src/framework.ts:18803, src/framework.ts:18810, src/framework.ts:18827, src/agent.ts:2392: Single shared-slot edit counter invalidates every resident's decline for rows it cannot compile.
    • Failing case: While one resident has a remembered decline, another resident's tool-result guard edits its own stored context injection, or a suppressed or held shared-slot row is edited; the first resident cannot compile that row, but its next boundary still runs a full synchronous preview, potentially at every round of the other resident.
    • Raised before as finding 9 of the review of 11ffb1195fef.
  • 18. minor at src/agent.ts:534, src/agent.ts:2392, src/framework.ts:13961, test/restart-preview.test.ts:564: Tool-result-guard settlement bumps the row counter each round, defeating the memo.
    • Failing case: A long guarded turn whose restart continues to be declined runs a full context select after each settled result; the change's own measurement found one select taking seconds on 1,500 messages, so a long turn can repeatedly block the host event loop and delay unrelated agents instead of paying for a single remembered preview.
  • 19. minor at src/agent.ts:532, src/agent.ts:534, src/agent.ts:2375, src/index.ts:8: Each Agent registers a permanent row-change listener on its context manager with no removal.
    • Failing case: A host repeatedly constructs and discards agents over the same long-lived ContextManager: each old closure remains registered and is called on every subsequent row event, so listener storage and per-edit work grow with the number of past agents even after those agents are gone.
  • 20. minor at src/framework.ts:19757, src/framework.ts:19943, src/framework.ts:28498, src/framework.ts:10027, src/agent.ts:1883: Preview budget is not bound to the compile, which rereads the live budget after async preparation.
    • Failing case: A restart preview fits at the current budget; while its start awaits a module's gatherContext, an operator applies a lower immediate budget with allowInfeasible, so the restart compile reads that lower budget and fails after the original stream has already been cancelled.
  • 21. minor at test/restart-preview.test.ts:345, test/restart-preview.test.ts:355, test/restart-preview.test.ts:700, test/restart-preview.test.ts:708: Cross-copy no-log tests do not prove the exception reached the restart decision.
    • Failing case: If the context manager instead returns a fitting preview or null without propagating the scripted exception, the pending uncovered-drop case and the other-copy no-fold-plan case still restart and log nothing; recording entry to previewContext alone does not prove the exception was thrown through it.
    • Raised before as finding 2 of the review of 11ffb1195fef, fixed then; this is a case the fix leaves.
  • 22. minor at changelog.d/restart-preview.breaking.md:29, change-text.md:27, test/restart-preview.test.ts:327, src/framework.ts:13961: Docs say every taken restart selects twice, but a throwing preview restarts without a select.
    • Failing case: The unexpected-error test uses a strategy whose preview immediately throws; the restart is taken without that preview running the compile's select, so an operator estimating the added cost from the unconditional claim gets the wrong account.
  • 23. minor at src/types/agent.ts:96, changelog.d/restart-preview.breaking.md:38, changelog.d/restart-preview.breaking.md:45, src/framework.ts:495, src/framework.ts:19746, src/framework.ts:36133: Docs promise physicalWindowTokens bounds request size, but the projection uses rough estimates and needs reported usage.
    • Failing case: A first call without reported prompt usage, or a continuation whose actual appended tokens exceed the chars-per-four and flat-image estimates, can exceed the configured limit or provider window without triggering the projection; a host following the migration or alert advice cannot rely on the promised bound.
  • 24. minor at src/framework.ts:19763, src/framework.ts:19792, src/framework.ts:19839, src/framework.ts:19865, src/framework.ts:19983, src/framework.ts:19989: A synchronous stop from the restart-declined trace is checked only after routing and hand-off act.
    • Failing case: If the listener calls framework.stop() or aborts inference and an addressed message was presented at this boundary, delivery or locked routing can store an addressed-elsewhere notice; latest-inbound routing can clear arrivals, move the locus and store a re-pin notice, and handOff can present missing rows, although the turn is stopping. The later roundStoppedDuringBuild check catches the cancellation and prevents provideToolResults, but cannot undo those earlier effects.
    • Raised before as finding 7 of the review of 11ffb1195fef.
  • 25. minor at src/framework.ts:19763, src/framework.ts:19881, src/framework.ts:19989: A stop during the declined-restart trace skips noteStreamEnd because the precomputed flags stay false.
    • Failing case: When a listener cancels at this trace, the round's reply is already stored but noteStreamEnd is skipped, and the later stop check returns without recording it as the stream's final reply; its thinking therefore lacks the final stream note needed to retain it at a subsequent compile.
  • 26. minor at src/framework.ts:19825: Changed routing comment narrates internal rationale rather than a permitted comment kind.
    • Failing case: A maintainer reading this private routing block encounters a changed explanatory comment outside the house's retained kinds; its rationale belongs in the change description or commit message, and the comment can go.
    • Raised before as finding 18 of the review of 11ffb1195fef.

Paloma-2644 added 3 commits November 10, 2026 04:50
… the key, passthrough's key, the shared slot's listener

Red against the head before the fix:
- After a declined restart, the context-refusal alert says why in words and advises physicalWindowTokens
  alone, and that advice survives the ops webhook's 500-character bound where the provider's reason is long.
  The test window stream's refusal text takes an optional tail for it.
- A compression-named restart declined while the budget hold can't take the agent (a framework run by
  runUntilIdle, not yet started) is previewed again once start() lets the hold take it, and taken.
- Under a strategy with no preview, the restart decision never builds its key.
- An agent that reads the shared slot's count of row changes unsubscribes its own listener on the manager.

Also: the cross-copy refusal cases record what each preview did as the decision met it, so a manager that
answered instead of propagating the copy's error would fail them.
…alWindowTokens, the key holds the hold's reach and is built only where it's read

- The context-refusal alert after a declined restart said the decline's reason code and advised raising
  contextBudgetTokens, which bounds nothing once the budget is at or past the provider's window. It now
  says why in words and advises physicalWindowTokens alone. That advice is short enough to sit whole in the
  tail the ops webhook's 500-character bound keeps, where the old advice lost its start when the provider's
  reason was long.
- A kept decline's key now holds whether the budget hold can take the agent. A compression-named restart is
  declined only while it can't, so a decline kept from then is previewed again once it can (as when a host
  runs runUntilIdle() and then calls start()).
- The key is built only where it's read: to compare with a decline kept for the stream, and for a new
  decline, after its preview. So under a strategy whose preview answers null, such as PassthroughStrategy,
  the decision builds none, and a decline's key holds the state its preview answered on.
- An agent that reads the shared slot's count of row changes unsubscribes its own listener on the manager,
  which before stayed registered for the manager's life, one more each time an agent was rebuilt over it.
…alWindowTokens does

- The fragment said every taken restart under a previewing strategy runs its select twice. A preview that
  throws before it selects runs none, so it now says each preview runs the select, and a restart taken
  after one that selected runs it again.
- The fragment and maxStreamTokens' doc said physicalWindowTokens bounds a request's size. It restarts the
  stream once a projection of its next request passes it, and the projection is as before this release. They
  now say that.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 62117ee41b41, answered at 5cb5ccfd02ad. It read the description written for 11ffb1195fef, which this pull request carried until 09:12:16Z on Nov 10:

  • 1 answered by the description now on this pull request: it gives the budget hold's reach, an agent the hold can't take declining whatever compression is named or pending.
  • 2 taken, with 16: the alert's advice is now physicalWindowTokens alone, short enough to stay whole in the tail the ops webhook's 500-character bound keeps. The old advice lost its start there when the provider's reason was long. A test with a long provider reason fails before the change and passes after it.
  • 3 declined by a run. At a kv-unified long turn in 9 settings, each of 144 previews was asked again at 7 budgets, with no prefix hash and with the cache's own. All 1,008 pairs gave the same verdict: fits in 286, won't fit in 722. Only finalTokens differed, in 100.
  • 4 answered by the description now on this pull request, which claims no 0.23.0 message match.
  • 5 answered by the description now on this pull request, which names its head.
  • 6 answered for budgetHoldCanTake by the description now on this pull request. The helpers are declined, as before: each serves the tests that use it.
  • 7 declined: a later image's placeholder is a row added to the store, the same class as a summarised row leaving the raw window. Not verified names that class.
  • 8 declined: where the preview fits and the restart's compile then refuses, the restart was taken, as on main. Previewing the modules' gatherContext would run hosts' async code at every boundary.
  • 9 declined: each previewing strategy in context-manager 0.27.0 moves its count of chunks or pending merges when it queues work.
  • 10 taken: under a strategy with no preview, the decision returns before building its key. A test fails before the change and passes after it.
  • 11 declined, as before: Not verified names it, and no test fails with that term removed.
  • 12 declined: the head takes that restart where main does.
  • 13 taken: the key the decision compares holds whether the budget hold can take the agent. A test that starts the framework mid-turn fails before the change and passes after it.
  • 14 declined, as before: it needs a getStats that throws an error named UncoveredDropError. The logged error's frames say where it threw.
  • 15 declined, as before: compressionPending predates this change, and the budget hold reads a throwing isReady() the same way.
  • 16 taken, with 2: the alert says why the restart was declined in words, and its advice is physicalWindowTokens alone.
  • 17 declined, as before: an edit the resident can't compile costs it one more preview, never a wrong answer.
  • 18 declined: Not verified names toolResultGuard's cost.
  • 19 taken: an agent that comes to read the shared slot's count unsubscribes its own listener. A test fails before the change and passes after it.
  • 20 declined: the head takes that restart where main does.
  • 21 taken: the cross-copy cases record what each preview did as the decision met it.
  • 22 taken: the fragment says each preview runs the select, and a restart taken after one that selected runs it again.
  • 23 taken: the fragment and maxStreamTokens' doc say physicalWindowTokens restarts the stream once a projection of its next request passes it, projected as before this release.
  • 24 declined, as before: the boundary emits other traces before its routing and rechecks no state after them either. Trace listeners observe.
  • 25 declined, as 24: the boundary emits tool:result_dropped before it decides whether the stream ends, so a stop from that listener leaves the same decision unmade.
  • 26 declined, as before: that comment predates this change, and only its variable's name changed.

— Paloma-2644

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Paloma-2644

@anima-review

anima-review Bot commented Nov 10, 2026

Copy link
Copy Markdown

Automatic context view of 5cb5ccfd02ad merged onto 36b6dfe9de39.

What a context view runs and what its checks judge: CHECKS.md.

The records of the 76 runs don't differ between the base and this head.

In the files profiled hosts loaded, 42 changed source lines that map to code ran in no profiled drive: src/agent.ts lines 2382–2396; src/framework.ts lines 1265–1266, 12502, 12637, 13960, 13964–13965, 13968, 13970–13971, 13974, 19764–19771, 19775, 36128–36133 and 36137. 106 changed source lines map to nothing the host runs: 17 of src/agent.ts's, 25 of src/framework.ts's, 31 of src/types/agent.ts's and 33 of src/types/trace.ts's. The whole list is in the fold below.

Usage: the figures of the agents' requests, subagents' included, in the 76 runs are the same in the base and this head: 207,609 tokens sent a request, 71.0% of them read from the cache, 147,320,914 written to it, 3,984,918 output, 24 thinking blocks dropped, USD 0.5774 at list price a priced request and 123 requests with no list price. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 52.6% 994,358 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 52.6% 994,358 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 56.9% 863,216 20,328 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

src/agent.ts: lines 2382–2396 ran in no profiled drive; lines 393, 395–396, 404–406, 2360, 2372–2374, 2379–2381, 2397–2398 and 2401–2402 map to nothing the host runs.

src/framework.ts: lines 1265–1266, 12502, 12637, 13960, 13964–13965, 13968, 13970–13971, 13974, 19764–19771, 19775, 36128–36133 and 36137 ran in no profiled drive; lines 1264, 1267–1277, 7649–7650, 13976, 13980–13981, 18808–18809, 18814–18815, 19777 and 19827–19829 map to nothing the host runs.

src/types/agent.ts: lines 85–111 and 127–130 map to nothing the host runs.

src/types/trace.ts: lines 326–358 map to nothing the host runs.

Held the same in base and head: connectome-host 3e715b4e08cf, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.0, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

@anima-review

anima-review Bot commented Nov 10, 2026

Copy link
Copy Markdown

Automatic review of 5cb5ccfd02ad.

About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

  • 1. minor at src/agent.ts:535, src/agent.ts:2375, src/index.ts:8: An Agent using its own row count never removes its row-change listener.
    • Failing case: A host repeatedly constructs and discards public Agent instances over the same long-lived ContextManager; every construction adds another listener, and edits continue invoking all of them after the agents no longer need them, so memory and per-edit work grow with the number of instances.
    • Raised before as finding 19 of the review of 62117ee41b41 and answered: «19 taken: an agent that comes to read the shared slot's count unsubscribes its own listener. A test fails before the change and passes after it.» At this head, that doesn't hold: The answer's fix, readRowChangesFrom unsubscribing (src/agent.ts:2376), covers only agents that switch to the shared slot's count. The earlier finding's case, a host constructing and discarding agents over one long-lived manager, still leaves each constructor's listener at :535 registered.
  • 2. minor at test/restart-preview.test.ts:1059, test/restart-preview.test.ts:1071, test/restart-preview.test.ts:1076: The closed-chunk test never establishes that a chunk count changes or that the restart follows a compression-naming preview.
    • Failing case: If the first decline is followed by a restart for some other key change without any intervening closed chunk, the loop can be vacuous and the unrelated restart and hold satisfy the remaining assertions.
  • 3. minor at test/restart-preview.test.ts:518, test/restart-preview.test.ts:561, change-text.md:112: Test titles call the decline one 'a newer message can't clear' when previews merely omit newer-message.
    • Failing case: The description's probe reports cases where added rows can make a fresh preview fit despite the earlier answer; a maintainer reading these test names could conclude that such a decline cannot be cleared by a newer row, which the tests do not establish.
  • 4. minor at src/framework.ts:19010, src/agent.ts:535, src/agent.ts:2395, src/framework.ts:13957: The subconscious's decline key reads only its isolated manager's counter, not the shared-slot counter.
    • Failing case: If the subconscious holds a kept wont_fit decline and a resident edits or removes a shared-slot row without changing another key component, the next boundary reuses the decline with previewed: false rather than checking whether the restart now fits. The built-in subconscious strategy is reported to have no preview, so this kept-decline state is hypothetical under the current strategy.
Raised before, still answered (8)

@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 5cb5ccfd02ad, answered with no change to the code:

  • 1 declined. Our answer to the earlier review's 19 said "taken", and that was narrower than the finding. The change unsubscribes only an agent that comes to read the shared slot's count. An Agent constructed and discarded over a manager that outlives it still keeps its listener (src/agent.ts:535). We decline to add an end of life for it:
    • Agent has none in its API to unsubscribe at.
    • Each of the framework's five new Agent( sites opens a manager for that agent.
    • connectome-host at 3e715b4e08cf constructs an Agent only in a test, and connectome-smoke at 6bf0b7de4df3 constructs none. So a dispose() would answer a case no host we know of meets.
  • 2 declined by a run: among the mutants at this head, the one that takes chunksTotal out of the key fails exactly the closed-chunk case, as the description's mutant list says.
  • 3 declined: the titles give the preview's own answer, a clearedBy without 'newer-message'. The probe in which a row leaving the recent window let a fresh preview fit is named under Not verified. A rename would change no behaviour.
  • 4 declined: the subconscious opens with a strategy that has no preview, so it never keeps a decline, and its key is never built.

— Paloma-2644

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nightly dual review · round R7 at 5cb5ccf · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: COMMENT (the formal review event is left to the maintainer). Finding ids R7-n are stable; cite them in fix commits.

Reviewed at 5cb5ccf against main 36b6dfe (also the merge base: the branch is up to date with main)

Round R7. Finding ids R7-n are stable; cite them in fix commits. This round's second reviewer was not the usual Codex pass, whose quota had run out; see the method note at the end.

Verdict: comment. No blockers, one major that is the maintainers' to settle, one minor and one nit.

Since R6 the change is a different one. It merged main twice, which brought #361's budget hold, and it now declines the restart only where compression can't make room, or where the hold can't take the agent. That narrowing removed R6-1's regression: by the description's grid, main answers 184 of 186 settings and this head answers all 186. We found the code as correct as it claims. Every claim we checked against context-manager 0.27.0 and membrane 0.10.3 holds, the five relevant test files pass at this head (279 of 279), and CI is green on all 16 checks. What stands between this PR and merge is now mostly a question of price. The decline changes the outcome in 4 of the 186 settings. The preview it needs runs before every restart that's taken under a previewing strategy, in all 186.


Round-6 findings

The desk's note ("Prior R6 at 223cac4 commented, no blockers") is correct: R6 was a comment-type review with no blockers.

Disposition Evidence at this head
R6-1 merge needs the maintainers' sign-off on the trade, and a decision on sequencing against #361 and #956 Partial. The sequencing is settled, but the sign-off isn't. Carried as R7-1. #361 merged into main (6c781099), and this branch merged it (4c3208bd). The mechanical overlap R6 named is gone: main has no nextCompileBudget, and this change has one budget helper (previewBudget, src/agent.ts:2399-2401) and one route into the preview (Agent.previewCompile, :2361-2372). The regression R6-1 measured is gone too: by the description, 186 of 186 against main's 184, where R6's head had 132 against 120. No maintainer has commented since 27 Oct. The description asks again on 10 Nov.
R6-2 (nit) the breaking fragment used "remembered decline" without defining it Not fixed. 4185f59e fixed it, and the rewrite undid that. Carried as R7-3. The rewrite in 11ffb119 dropped the definition. Your 10 Nov reply says the fragment "no longer names the kept decline", but changelog.d/restart-preview.breaking.md:25-27 still does: "compression landing while its stream goes on moves the kept decline's counts". Nothing earlier in the fragment says what a kept decline is or which counts it holds.

Major

R7-1: After the narrowing, the decline helps in 4 of 186 settings, and its preview runs before every restart that's taken (R6-1, sharper)

src/framework.ts:13952-13980, :19759-19762; src/agent.ts:2361-2372; changelog.d/restart-preview.breaking.md:29-34

  • Where the benefit is now. In the grid your description reports, the head declines a restart in 4 settings. They are maxStreamTokens 8,000 with contextBudgetTokens 10,000 or 12,000, each at two compression timings. main ends 2 of those with over_budget and answers the other 2 at more characters sent. In the other 182 settings no boundary declines.
  • Where the cost is. declineBudgetRestart runs at every tool boundary past maxStreamTokens that doesn't end the turn, unless a kept decline matches (:19759). When its preview fits, the restart is taken, and the continuation's compile runs the same select again. The grid's head takes 5,803 restarts, and each was previewed first. By your own measurement at 1,500 rows under kv-unified, a taken restart costs a median 18.2 s against 10.8 s unpreviewed. That's 7.4 s more, synchronous, so the whole host's event loop waits: every other agent's stream, MCPL sockets and the web UI. The fragment says that the select runs twice (:29-34). It doesn't say how large that is next to what the decline wins.
  • Why it matters to the maintainers' own deployments. The 27 Oct comment describes residents that all set maxStreamTokens, set no physicalWindowTokens, and mostly run kv-unified with large stores. That's where a select costs seconds, and where every restart now pays one more. Nothing in the grid shows a decline in a setting like theirs.
  • Fix (the maintainers' call), any of:
    • An explicit sign-off on the price as it stands, which the description already asks for.
    • Run the preview only where the restart's compile could plausibly refuse. For example, skip it where the compile's budget leaves room for the last compile's finalTokens plus what the stream has stored since. Folding only shrinks that, so such a compile can't refuse for size. The 4 settings that decline would still preview.
    • Ask context-manager for a way to hand a preview's selection to the compile that follows it. Your measurement found the compile "planned exactly what the preview had".

Minor

R7-2: A preview names no compression for work only the restart's own compile would queue, so a resident's restart can be declined where main would hold it

src/framework.ts:13963; changelog.d/restart-preview.breaking.md:15-24

  • Evidence. For an agent the hold can take, the decline turns on !preview.clearedBy?.includes('compression') (:13963). context-manager 0.27.0 says, at the line that builds that list (strategies/autobiographical.ts:15774-15787 at v0.27.0), that a dry run names compression as the live compile does, "unless the work is only what the ops would add: the newest closed chunks, which the holdback (l1HoldbackChunks) keeps out of the queue until they're demanded, or a merge. There a dry run names nothing." A live compile enqueues that work before its plan check refuses. So isReady() then reads false, and holdForBudgetRefusal (:7539-7542) holds the turn. PreviewResult.clearedBy's doc says the same: "the live list can add compression for what the plan's own ops queue" (dist/src/types/strategy.d.ts:872-880).
  • It's reachable in your own tests. "A remembered decline isn't taken again once the stream's own rows have closed a chunk" (test/restart-preview.test.ts:1052-1078) asserts that the first decline came with nothing pending (:1070), and that a later restart was taken and held.
  • Failure scenario. A kv-unified resident, with physicalWindowTokens unset and everything queued already landed. The restart's compile needs only the held-back newest chunk's summary, or a merge, to fit. The preview names nothing, so the restart is declined, and the kept decline stands until the stream's rows close another chunk. If the stream reaches the provider's window first (a few large tool results, say), the turn ends context_length. On main, the restart's compile would have demanded that work and been held until it landed.
  • Why it's minor. It closes itself at the next chunk or two, and your grid never shows it costing a setting. In the grid setting closest to that test, main's early hold is what ends over_budget. Your "Not verified" section covers this mechanism for uncovered drops ("An uncovered drop the hold would answer"), but not for wont_fit, and the fragment's "What it trades" doesn't mention it.
  • Fix. At least, say it in the fragment's "What it trades" and under "Not verified": a preview names no compression for work only the restart's compile would demand, such as the held-back chunk or a merge. If you change the code, PreviewResult has what you'd need (producedCount, headTokens, tailTokens, types/strategy.d.ts:883-899). Read producedCount > 0 with head and tail under the budget as compression named, for an agent the hold can take. Rerun the grid before you do, since that's the case where main's hold lost.

R7-3 (nit): The breaking fragment says "the kept decline's counts" without saying what a kept decline is (R6-2)

changelog.d/restart-preview.breaking.md:24-28

  • Evidence. Lines 25-27: "compression landing while its stream goes on moves the kept decline's counts, and a later boundary takes the restart if its compile then fits". The fragment says nowhere else that a decline is kept, what it rests on, or that a later boundary takes it without previewing. Lines 10-11 say only "A later boundary past maxStreamTokens considers the restart again." The trace's previewed doc (src/types/trace.ts:334-347) defines it, but the release note is what most hosts read.
  • Fix. Either restore one sentence, after line 11 for example: "A decline whose preview doesn't name a newer message as what would make room is kept for the stream. Later boundaries take it without previewing until something it rests on changes, as the inference:restart_declined trace's previewed doc lists." Or reword lines 25-28 so they don't lean on the term: "...so compression that lands while its stream goes on makes a later boundary preview again, and that boundary takes the restart if its compile then fits."

Verified fine

  • Up to date and green. The merge base is main 36b6dfe9, with 0 commits behind, and GitHub reports CLEAN. CI passes all 16 checks, including the dependency-floor jobs (membrane 0.9.0 and 0.10.0, and the lowest floors).
  • Tests at this head. A fresh npm install now resolves membrane 0.10.3, not the description's 0.10.2, with context-manager 0.27.0 and chronicle 0.6.0. npm run build exits 0. restart-preview, present-while-acting, budget-refusal-hold, physical-window-restart and routing-baseline-restart pass 279 of 279, the same count the description gives.
  • The budgetHoldCanTake extraction (src/framework.ts:7646-7649) is equivalent to the two lines it replaces in holdForBudgetRefusal (:7541).
  • context-manager 0.27.0's surface, as installed:
    • previewContext(budget, overrides?, opts?) takes excludePins and returns PreviewResult | null (dist/src/context-manager.d.ts:1871-1874).
    • PreviewResult has fits, finalTokens, budgetTokens and clearedBy, and clearedBy's values are 'compression' | 'newer-message' | 'smaller-recent-window' (types/strategy.d.ts:857-881, adaptive/picker.d.ts:199). A 'smaller-recent-window'-only decline is kept, and the key holds tailTokens, so shrinking the window makes the next boundary preview again.
    • getStats() reports every count the key reads (strategies/autobiographical.d.ts:2622-2631).
    • The manager itself answers null for the strategy's NoFoldPlanToPreviewError, so isNoFoldPlanRefusal is reached only through a strategy from another copy, as its comment says.
  • The inputTokens docs. In membrane 0.10.3, normalizeUsageToCacheExcluded (dist/utils/usage.js) passes counts through under 'unknown', subtracts cache reads and writes under 'cache-inclusive', and leaves 'cache-excluded' counts as they are. So "where membrane knows the provider's cache convention, it leaves out cache reads and writes" holds.
  • The routing gate. At every boundary that doesn't decline (one that ends the turn, a stopping framework, a window-forced restart), budgetRestart equals main's overBudget (:19759-19762). So the notice and the re-pin are skipped exactly where they were before.
  • The turn's pin. A committing compile that refuses with the pin held unpins it and compiles again (compileUnpinningOnRefusal, src/agent.ts:1366-1375). A preview with excludePins therefore answers what the restart's last-resort compile does.
  • The ending. afterDecline compares the decline's streamId with the failing stream's (:36108-36109). A window-forced restart or a new turn gets a new id, so a stale decline can't mark a later stream.
  • The corrected 0.28.0 entry exists as quoted (CHANGELOG.md:3587-3593).
  • Description citations. We checked 14 line citations at this head and main's :19658, and all are accurate.
  • The hierarchical path's over-claimed compression. We confirmed it by reading: context-manager 0.27.0 names compression for any queued work that folds a message in the refused prefix, however little room it makes (strategies/autobiographical.ts:18562 at v0.27.0). Such a restart is taken, as on main, so this PR loses nothing there. context-manager's open issues #401, #408 and #415 track the family.
  • Downstream heads-up, unchanged since R6. connectome-host main (00d56c54) still has no inference:restart_declined entry in TRACE_SCOPES (src/modules/web-ui-observers.ts:477-491). Its test at test/web-ui-observers.test.ts:753 will fail on the bump that brings this, as the fragment's last item predicts.

Method. Two passes, merged after re-verifying each claim. The usual Codex gpt-5.6-sol pass didn't run: its quota was exhausted at launch, so it produced no output. An independent Claude claude-opus-5-5 subagent got the same brief and none of our conclusions, and stood in for it.

Second pass (stand-in): about 15 minutes and 103 tool calls. It read the three-dot diff, R1 to R6, the thread with the three automated reviews and your answers to them, the description, context-manager 0.27.0 as installed and at its tag, membrane 0.10.3, and connectome-host main. tsc --noEmit passed. It ran no tests and made read-only gh calls.

Claude pass: fetched main and the PR, installed from npm, built, and ran five test files at this head. Checked CI, the PR's timeline for a maintainer's sign-off, and connectome-host main's trace table. Read context-manager 0.27.0's preview, clearedBy and getStats surface, membrane 0.10.3's usage normalization, the pin's unpin-and-recompile path, and the boundary's routing gate. Spot-checked 15 description citations.

Who caught what:

• Both, independently: the R6-1 and R6-2 dispositions, including that "kept decline" is still in the fragment.

• Stand-in only: R7-2. We confirmed it from context-manager's own comment at v0.27.0 and from the closed-chunk test's assertions.

• Claude only: R7-1's framing (the decline in 4 of 186 settings against a preview before every taken restart), the test run at this head, and the membrane 0.10.3 and connectome-host checks.

Not reached: your grid and probes (their figures are quoted, not reproduced), a real provider or host, a store of resident size, and the whole suite at this head, which CI ran on all its jobs.

… work would answer, and drops an undefined term

Two sentences in "What it trades", from Anarchid's R7 review at 5cb5ccf.

R7-3: "moves the kept decline's counts" leaned on a term the fragment never
defines. It now says what that does: compression that lands while an
ephemeral run's stream goes on makes a later boundary preview again.

R7-2: a preview names compression only for work already queued. Where the
restart's compile would itself queue the work that makes room (the newest
closed chunks the queue holds back until a compile demands them, one by
default, or a merge), the preview names none, so for an agent the hold
can take the restart is declined where before it was taken, refused and
held for that work. A probe of 72 kv-unified cells at 5cb5ccf and at main
36b6dfe found it in 18 cells, where main held and the head declined;
the head answered all 72 and main 60, so it cost no answer there. The
code is left as built.

Only this file changes: src and test are 5cb5ccf's.
@nissa-seru nissa-seru changed the title fix(framework)!: decline a maxStreamTokens restart that won't fit, unless compression would make room fix(framework)!: decline a maxStreamTokens restart that won't fit, unless the budget hold can wait for compression its preview names Nov 11, 2026
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@Anarchid, thank you for R7. The head is now 783a29e2a372. It adds one commit to 5cb5ccfd02ad, which changes only the breaking fragment, for R7-2 and R7-3. src and the tests are unchanged.

R7-1: the check isn't added. We ran it before deciding. At each preview in the grid at 5cb5ccfd02ad, a probe computed your check: the last live compile's renderStats.total.tokens, scaled to the current calibration multiplier, plus getChannelTokenStats({ fromMs }) from that compile's start, against the preview's budgetTokens. The probe changed nothing the framework did.

  • The units hold. At the 3,152 previews whose plan moved no chunk and produced no summary, the check's figure equals the preview's finalTokens within 0.05% in every one.
  • It would skip most taken restarts' previews. It passes for 3,690 of 5,920 taken restarts, 62%.
  • It never passes at a decline. That's 0 of 199 previewed declines, so the declining settings would still decline.
  • As worded, it's wrong where another writer's rows arrive. It passed wrongly at 6 of 6,119 previews: the check said the restart's compile would fit, and the preview or the compile itself said it wouldn't. All 6 were in the grid's two cells with rows arriving every 5 s. A rerun of those two cells printed every false pass. There were 5, and in each the preview didn't fit, naming compression, and the taken restart's compile refused and then held. Of the first run's 6, the probe printed 5, all the same kind, and didn't print the sixth. Each gap was about one arriving row: a row stored before the last compile and shown to the agent after it is in neither term.
  • Why it's left out: as worded, it can't yet be made safe where another writer's rows arrive. And we think a compile that reuses its preview's selection, inside context-manager, is the better shape. It would remove the second select from every taken restart, not 62% of them, and the check would then save nothing.

R7-2: the mechanism reproduces, and it cost no answer in the cells we ran. The code is left as built, and the fragment and the description now say it.

  • The probe: 72 kv-unified cells at 5cb5ccfd02ad and at main 36b6dfe9de39, with targetChunkTokens 1,000, 4,000 and 8,000, pages of 4,000 or 8,000 characters, budgets of 16,000 and 30,000, and compression at 0 and 5 s.
  • What it found: 94 of 232 declines named no compression with nothing pending. They fall in 30 cells, all at the 16,000 budget.
    • In 18 of those cells, main's restart was refused and held, and the cell answered. This change answered them too, and its budget hold held later in each.
    • In the other 12, main ended over_budget without a hold, and this change answered.
    • This change answered all 72 cells, and main 60.
  • Your route (producedCount > 0 with head and tail under the budget, read as compression named) wasn't measured. It could take the restart in those 12 cells, where main's compile refused and the cell failed, so it could cost answers this change has.
  • Not measured: how long after the decline this change's restart came, or what fresh input the stream took past maxStreamTokens meanwhile.

R7-3: taken, with your second wording. Our reply to R6 said the fragment no longer named the kept decline. That was true when we wrote it, and a later commit put the term back, so by the time you read it, it was false. The fragment now reads "…so compression that lands while its stream goes on makes a later boundary preview again, and that boundary takes the restart if its compile then fits."

Also: a fresh install now takes membrane 0.10.3, as you found. Our runs used 0.10.2, and the description's Tests section says so.

— Paloma-2644

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants