Repository navigation
fix(framework)!: decline a maxStreamTokens restart that won't fit, unless the budget hold can wait for compression its preview names - #842
Conversation
There was a problem hiding this comment.
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:
- Medium severity. previewCompile calls previewContext without the capability check the existing preview path uses.
- Medium severity. firstTurnRow rescans the whole open turn at every tool boundary and compile, so a long turn costs quadratic work.
- Medium severity. A full preview select runs on the hot path at every boundary over budget, including ones that end the turn.
- Medium severity. Tool-boundary re-hold is not gated on stop(), unlike beforeCompile.
- Medium severity, test coverage. Retry hold-point propagation through provider waits, acceleration holds and merges is untested.
- Medium severity, test coverage. Silent-retry test passes even if the failed attempt's hold was never released or re-placed.
- Medium severity. The turn's hold release may clear a hold someone else placed on the same row.
2 more findings, less severe
- Low severity, maintainability,
src/agent.ts:2018-2032. previewCompile re-implements previewContextFor instead of sharing it. framework.ts already haspreviewContextFor, which resolves the thinking binding and callspreviewContextbehind a capability guard.Agent.previewCompileadds 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 whosebeginAgentTurnthrows. The other is inbeginAgentTurn(20662-20663), when a new routing displaces one that is not the open logical turn. Neither is exercised bytest/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.
| previewCompile(): { fits: boolean } | null { | ||
| const thinkingBinding = this.thinkingBindingOption(); | ||
| return this.contextManager.previewContext( | ||
| this.compileBudget() ?? CONTEXT_MANAGER_DEFAULT_BUDGET, | ||
| undefined, | ||
| thinkingBinding ? { thinkingBinding } : undefined, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| const overBudget = currentState.stream | ||
| && !(agent.thinkingBindingOption() !== undefined && agent.physicalWindowTokens !== undefined) | ||
| && agent.lastStreamInputTokens > 0 | ||
| && agent.lastStreamInputTokens > agent.maxStreamTokens; | ||
| && agent.lastStreamInputTokens > agent.maxStreamTokens | ||
| && this.softRestartFits(agent); |
There was a problem hiding this comment.
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.
| const boundaryTurn = this.turnRouting.get(agent.name); | ||
| if (boundaryTurn) this.holdTurnRows(agent, boundaryTurn); |
There was a problem hiding this comment.
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.
| if (isTurnRetry(request)) { | ||
| (same as MarkedRequest)[TURN_RETRY] = true; | ||
| withTurnHoldAfter(same as MarkedRequest, turnHoldAfter(request)); | ||
| } |
There was a problem hiding this comment.
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.
| 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 }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
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.
|
|
||
| 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 |
There was a problem hiding this comment.
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.
|
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 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 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:
🤖 Generated with Claude Code |
…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.
d197b9d to
00572ba
Compare
|
@antra-tess #842 is rebuilt without the turn hold, at 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,
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 A kv-unified test (your point 1). Six of the 14 tests in 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 — Daniel-2612 |
Anarchid
left a comment
There was a problem hiding this comment.
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_budgetalert 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:
softRestartFitstreats the hierarchical "no fold plan" refusal as a fit only whenerror.constructor === Errorand the message matches.- In context-manager 0.23.1 that refusal is
NoFoldPlanToPreviewError, a subclass with its ownname(src/types/strategy.ts:554-559there). ContextManager.previewContextturns it intonullonly byinstanceofagainst its own copy (src/context-manager.ts:1986there).- 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
NoFoldPlanToPreviewErrorfails both the constructor test andinstanceof 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.
- In context-manager 0.23.1 that refusal is
- Repro: I adapted this PR's "hierarchical preview answers with 0.23.1's null" test and swapped in an
AutobiographicalStrategyfrom 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
physicalWindowTokensunset, that turn then goes on to the provider's window.
- Convention: the framework already knows this problem.
classifyInferenceErrormatchesOverBudgetErrorandUncoveredDropErrorbynameas well asinstanceofbecause "name… survives that boundary" (src/framework.ts:28216,:28238,:28245).previewStoppedReasonmatches the refusal by message (:1977). - Fix:
- Accept the refusal by
error.name === 'NoFoldPlanToPreviewError', or by message alone, without the constructor test. - Match
UncoveredDropErrorbynametoo. - Add a test with a
NoFoldPlanToPreviewError-named subclass, or a second package copy.
- Accept the refusal by
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:
softRestartFitsruns at every tool boundary wherelastStreamInputTokens > maxStreamTokens.previewContextis 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
overBudgetexpression checks neithershouldEndTurn(computed at:14017) norproviderAdmissionClosed(:14162).
- Measured, PR's own 30-round kv-unified test (budget 20,000,
maxStreamTokens8,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 (
deferredAuxiliary1,primaryDepth1). - The gate ran 24 previews in that turn, 14 of them in a row on an answer that couldn't change.
- From call 17 to call 30, every preview answers
- Measured, cost: I used the kv-unified policy from this PR's tests (bucket sizes 100,
labelCeiling10,000) at a 100,000-token budget, and timed onepreviewContextafter 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
clearedByis['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:20037already 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: falsedecline logs nothing and emits no trace.inference:stream_restarted(:14199-14208) fires only for a restart actually taken. - An
UncoveredDropErrordecline is deliberately not logged (:10090). - README (
:531),src/types/agent.ts:93and 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".
- A
-
Evidence, the turn's end: on 0.28.0, a restart that can't fit ends as
over_budget.classifyInferenceErrorgivesover_budget(:28238).- That raises the
context-refusalops 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'scontext_lengthat 400, which does neither.
-
Repro, PR's "host that sets no window" scenario (kv-unified, budget 8,000,
maxStreamTokens10, provider window 9,000):main main + this PR Tool calls before the turn ends 4 9 Failure row over_budgetcontext_lengthops:alertone ( context-refusal)none Drain breaker kicked yes no -
Fix:
- Emit a trace on each decline, or once per stream, with its reason (
fits,uncovered_droporpreview_error),inputTokens,maxStreamTokensand the preview'sfinalTokens,budgetTokensandclearedBy. - Make the docs match the code on
UncoveredDropError. - For a stream whose restart was declined, either treat its
context_lengthas the context refusal it is (alert and drain), or say in the breaking fragment that this ending raises nocontext-refusalalert and kicks no drain.
- Emit a trace on each decline, or once per stream, with its reason (
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 amaxTokensother than the shared 4,000, or has a pending tool-result-guard reserve. - So replacing
compileBudget()withresolveBudget(), or droppingpendingWireReserveTokens, 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
maxTokensand 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-214there). Leaving it out doesn't changefits. - Replace that "Not verified" item with this. On rebase, have
previewCompile's doc tie the 0.23.0 plainErrorto 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 intest/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.tspasses 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_BUDGETmatchesContextManager.compile's default (context-manager.ts:1226-1229in 0.23.1).- The thinking binding is the compile's.
- The preview omits
identity, which it doesn't read.
- Both take
- Unstored injections don't change the verdict.
compilepassesbeforeUser/afterUserinjections into the select only for calibration (autobiographical.ts:14095in 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
nullfor a hierarchical select that fits, and an infeasible preview when it doesn't. - 0.23.0 throws exactly the plain
Errormatched here (autobiographical.ts:2648in 0.23.0). - The physical-window restart and the binding-with-window exemption are untouched.
- 0.23.1 returns
- 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.
inputTokensis 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.0src/utils/usage.ts). - Citations and naming. The description's line citations at
59d60958are accurate (:14007-14010,:20017,:13446,:5643-5644,:5386-5390,:5435). The fragment names followscripts/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.
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.
|
@Anarchid #842 is now at R1-1, a refusal from another copy of context-manager. The refusal is now matched by its name, R1-2, a preview at every boundary.
R1-3, no record of a decline, and an ending without the alert or 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 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 R1-6, the prefix hash. R1-7, the migration note. Runs in clean checkouts whose files are — Daniel-2612 |
|
@antra-tess Here is the trade #842 makes, setting by setting, on long agentic turns configured the way you described your residents: 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,
" Where In one of them (30 rounds, a 30k window, budget 16k, 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
Over the grid's 162 settings, — Daniel-2612 |
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
left a comment
There was a problem hiding this comment.
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_fitdecline whoseclearedBydoesn't name'newer-message'is reused whilerestartPreviewKey()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.
withAuxiliaryAdmissionparks every compression call whileprimaryDepth > 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
releasePrimaryProviderGatewakes the parked calls (:6018-6021).
- A
-
Reproduction: main
fd6d6a36against main plus this PR. One kv-unified turn of 30 rounds with ~1k-token results, recent window 6k,maxStreamTokens8k, nophysicalWindowTokens, and a mock provider that refuses past its window. The harness is the one fromtest/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 onProcesson eachtool-resultanswers: 23 restarts, compressions land at calls 7, 9, 13, …, 29 context_lengthat call 30: 1 restart, then 24 declines resting on one preview (12,391 vs 12,240,clearedBy: ['compression']), no compression lands,context-refusalalertsame, but the 50 ms (or 500 ms) is inside the tool call over_budgetat call 7context_lengthat call 30window 40k, budget 20k, nothing awaiting over_budgetat call 16answers same, 50 ms inside the tool call over_budgetat call 16answers 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 anover_budgetfailure row". The migration bullets and "Who needs to act" describe declining only as a replacement for that ending. - README's step and the
maxStreamTokensdoc 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.
- The fragment (
-
Fix: either of these.
- 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. - Document it. Say in the fragment, README and
maxStreamTokensdoc 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 withcontext_length. Get the maintainer's answer to the 28 Oct trade question on the PR before merging.
- Change the behaviour. At a boundary whose decline names compression while that agent's compression is parked behind its own gate (
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
editMessagesandremoveMessagesare applied atsrc/framework.ts:17221-17230; DiscordModuleemits both for edits and deletions (src/modules/discord/index.ts:893,:911, and throughctxat:1181,:1213);- the tool-result guard edits stored results (
src/tool-result-guard.ts:314).
- a module's
- 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
physicalWindowTokensthe 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
restartDeclinesineditMessage/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,solveLeafreturns infeasible fromforest.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 changefits. - Fix: replace the item with that. Description only.
Verified fine
- Runs on current main.
- The PR merges cleanly into
fd6d6a36, whosesrc/framework.tshas 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.
- The PR merges cleanly into
- context-manager APIs at the
^0.24.0floor 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 appliesexcludePins, and returnsnullfor its own copy'sNoFoldPlanToPreviewError(context-manager.ts:2312-2340,autobiographical.ts:2703-2707).getStatshaschunksCompressed,compressionCount,l1–l3(autobiographical.ts:8238-8253).getCompressionHolds(:1006) andgetPendingWork(: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 preview leaves out only the turn's own pin (
- The ending is the declining stream's alone.
afterDeclinerequires the storedstreamIdto match, andstreamIdmoves only at a stream's start (src/agent.ts:2271). A later stream'scontext_lengthisn't attributed to an old decline. - Stop and window paths are as the fragment says.
- A boundary past
maxStreamTokenswhile stopping still ends the turn (src/framework.ts:16910-16914). - A window-forced restart reports
physical_windowin both the trace and the inference log (:16946,:16952).
- A boundary past
- The merges' notice and re-pin skips.
budgetRestartskips 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
8145085d06included.
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).
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.
|
Replying to this review. This pull request is now at R2-1, a decline that only compression could clear.
R2-2, a stored row edited or removed mid-turn.
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, Runs at Paloma-2644 made — Lottie-2661 |
|
@Anarchid Since R6 at
— Paloma-2644 |
|
/review — Paloma-2644 |
|
Automatic context view of 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: 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 Upgraded runs beside their fresh twins
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
Held the same in base and head: connectome-host |
…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.
|
Automatic review of About these findingsThe 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:
|
… 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.
|
/review — Paloma-2644 |
|
Automatic context view of 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: 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 Runs that couldn't be compared (1)This head couldn't be compared at Upgraded runs beside their fresh twins
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
Held the same in base and head: connectome-host |
|
The automatic review of
— Paloma-2644 |
|
Automatic review of About these findingsThe 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:
Minor findings 7 to 26 (of 26)
|
… 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.
|
The automatic review of
— Paloma-2644 |
|
/review — Paloma-2644 |
|
Automatic context view of 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: 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 Upgraded runs beside their fresh twins
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
Held the same in base and head: connectome-host |
|
Automatic review of About these findingsThe 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:
Raised before, still answered (8)
|
|
The automatic review of
— Paloma-2644 |
Anarchid
left a comment
There was a problem hiding this comment.
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
maxStreamTokens8,000 withcontextBudgetTokens10,000 or 12,000, each at two compression timings.mainends 2 of those withover_budgetand answers the other 2 at more characters sent. In the other 182 settings no boundary declines. - Where the cost is.
declineBudgetRestartruns at every tool boundary pastmaxStreamTokensthat 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 nophysicalWindowTokens, 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
finalTokensplus 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-15787atv0.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. SoisReady()then reads false, andholdForBudgetRefusal(: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
physicalWindowTokensunset 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 endscontext_length. Onmain, 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 endsover_budget. Your "Not verified" section covers this mechanism for uncovered drops ("An uncovered drop the hold would answer"), but not forwont_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,
PreviewResulthas what you'd need (producedCount,headTokens,tailTokens,types/strategy.d.ts:883-899). ReadproducedCount > 0with 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 wheremain'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
maxStreamTokensconsiders the restart again." The trace'sprevieweddoc (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_declinedtrace'sprevieweddoc 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
main36b6dfe9, with 0 commits behind, and GitHub reportsCLEAN. 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 installnow 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 buildexits 0.restart-preview,present-while-acting,budget-refusal-hold,physical-window-restartandrouting-baseline-restartpass 279 of 279, the same count the description gives. - The
budgetHoldCanTakeextraction (src/framework.ts:7646-7649) is equivalent to the two lines it replaces inholdForBudgetRefusal(:7541). - context-manager 0.27.0's surface, as installed:
previewContext(budget, overrides?, opts?)takesexcludePinsand returnsPreviewResult | null(dist/src/context-manager.d.ts:1871-1874).PreviewResulthasfits,finalTokens,budgetTokensandclearedBy, andclearedBy'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 holdstailTokens, 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
nullfor the strategy'sNoFoldPlanToPreviewError, soisNoFoldPlanRefusalis reached only through a strategy from another copy, as its comment says.
- The
inputTokensdocs. 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),
budgetRestartequalsmain'soverBudget(: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 withexcludePinstherefore answers what the restart's last-resort compile does. - The ending.
afterDeclinecompares the decline'sstreamIdwith 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 namescompressionfor any queued work that folds a message in the refused prefix, however little room it makes (strategies/autobiographical.ts:18562atv0.27.0). Such a restart is taken, as onmain, 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 noinference:restart_declinedentry inTRACE_SCOPES(src/modules/web-ui-observers.ts:477-491). Its test attest/web-ui-observers.test.ts:753will 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.
|
Replying to this review. @Anarchid, thank you for R7. The head is now R7-1: the check isn't added. We ran it before deciding. At each preview in the grid at
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.
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 |
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
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 anover_budgetfailure row.inference:restart_declinedtrace 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 nophysicalWindowTokens, the stream can go on until the provider refuses a request as too long. The turn then ends with acontext_lengthfailure row, which raises thecontext-refusalops alert and kicks the compression drain.A restart whose preview meets a drop no summary covers
main): the restart is taken, and its compile refuses. With nothing pending, the turn ends with acontext_refusalfailure row.main, for an agent the budget hold can take.A restart the budget hold can't take, as an ephemeral run's
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.A restart the window projection forces where the stream is past
maxStreamTokenstoomain): itsinference:stream_restartedtrace gives the reasoncontext_budget, with the stream's count andmaxStreamTokens, and so does its inference-log entry'sstopReason.physical_window, with the projection andphysicalWindowTokens.TraceEvent['type']main): noinference:restart_declined.Record<TraceEvent['type'], …>or an exhaustiveswitchover it needs an entry.maxStreamTokens' docmain): "Max input tokens before framework breaks a yielding stream and restarts with recompiled (compressed) context. Default: 150000."inputTokenstotal 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 aninference:restart_declinedtrace says why. A later tool boundary over this budget considers the restart again. Any other failure of the preview doesn't decline it." And: "WithphysicalWindowTokensunset, a declined stream can grow until the provider refuses a request at its own window. The turn then ends with acontext_lengthfailure row. That row raises thecontext-refusalops 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 defaultPassthroughStrategy. 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
mainover 186 settings of long tool-using turns, all under the kv-unified strategy, with a mock provider. It answers in every settingmainanswers, and in the 2 wheremainendsover_budget. Under the hierarchical strategy, a smaller probe found no setting where it ends a turnmainanswers, 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. WithtoolResultGuardon, a decline is previewed again at the first boundary after each edit the guard makes, so a long stream pastmaxStreamTokenscan run a select at most of its boundaries.It also corrects 0.28.0's entry that called the
maxStreamTokensrestart 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 ofmaxStreamTokensandphysicalWindowTokensnow say so.@antra-tess, this changes the documented rule of
maxStreamTokens, quoted above. @Anarchid, your review, please.changelog.d/restart-preview.breaking.mdandchangelog.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, onmain36b6dfe9de39.Problem
At
main36b6dfe9de39, the restart condition compares the stream'sinputTokenstotal withmaxStreamTokens, and nothing in it asks what the restart's compile would do (src/framework.ts:19658). In the grid under Tests, at that commit:over_budgetin every run:contextBudgetTokens10,000,maxStreamTokens8,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.contextBudgetTokens12,000. Over 10 runs each,maintakes 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 pastmaxStreamTokensthat doesn't end the turn, while provider admission is open, and where the window projection doesn't force a restart (:19759). It previews withAgent.previewCompileat the budget the restart's compile is given, with the thinking binding the compile is given. It declines where:fitsis false and itsclearedBydoesn't include'compression'(wont_fit);UncoveredDropError, matched by its class or its name, whileisReady()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, aclearedBynaming compression or anUncoveredDropErrorwhile 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, andsrc/framework.ts:13957). Awont_fitwhoseclearedBydoesn'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 asPassthroughStrategy, it's never built. The key holds the budget, the compression holds, the livetailTokens,transitionPaceTokensandpreparedWindowTokens, 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, besiderestartPreviewKey'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 haspreviewed: false. When the stream's rows close one of the strategy's chunks, its count of chunks moves, and the next boundary previews again. Anuncovered_dropcarries no key.The count of edited and removed rows (
src/agent.ts:535,src/framework.ts:18802–18812). An agent counts every event butaddits 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'sexcludePins, 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 pastmaxStreamTokens(budgetRestart). A boundary that declines skips neither, so the stream that goes on is told.The window projection's restart gives
physical_windowwhenever it forces the restart (:19954).The ending (
:36109). Acontext_lengthfailure on a stream whose restart was declined is a context refusal: it raises thecontext-refusalops alert and kicks the compression drain. The alert says why the restart was declined in words and advisesphysicalWindowTokens, 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
maxStreamTokensandphysicalWindowTokensand README's budget-restart step say what a host meets. The trace'sprevieweddoc says what a kept decline rests on.changelog.d/restart-preview.breaking.mdsays what changes for hosts, what it trades against the budget hold, and what a taken restart costs.changelog.d/subconscious-window-guard.fixed.mdis 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 itssrcand tests are5cb5ccfd02ad's. On Linux x86_64 with Node v24.22.2 and npm 11.19.1. The repository has no lockfile. In a fresh copy at5cb5ccfd02ad,npm ls --depth=0, run after its tests, printed@animalabs/chronicle0.6.0,@animalabs/context-manager0.27.0,@animalabs/mcpl-core0.4.0,@animalabs/membrane0.10.2,@babel/parser7.29.13,@modelcontextprotocol/clientand@modelcontextprotocol/server2.4.0,@types/node22.20.5,@types/ws8.18.2,chokidar4.0.3,discord.js14.28.1,tsx4.23.15,typescript7.0.2 andws8.22.2. Each run isnpm install, then@animalabs/membraneat the release named installed over it without saving,npm run buildandnpm run pretest, then the files named, withnpm 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.5cb5ccfd02ad: the build'stscexits 0. The whole suite, everydist/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.tsandtest/routing-baseline-restart.test.tsrun 279 tests, and all pass.5cb5ccfd02ad's tests overmain36b6dfe9de39'ssrc(src/agent.ts,src/framework.ts,src/types/agent.ts,src/types/trace.ts, the onlysrcfiles this change touches): the build reports 43 type errors, each in the two test files, and emits. 38 are at a use of theinference:restart_declinedtrace thatmain's union lacks, and 5 atAgentmembersmainlacks.test/restart-preview.test.tsandtest/present-while-acting.test.tsrun 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 afterstart(). 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 anOverBudgetError. The cases that record what the preview or the key did fail as they wrapAgentmembersmainlacks, and the listener case at its check that the agent counts its own row changes, whichmain'sAgentdoesn't. The 4 that time out are thepresent-while-actingcases where the boundary declines.65f4292198b0, the narrowing: ataac25ee6a56b, which adds its tests to77dae5e100b6'ssrc, 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.ae51b3d873e4, which adds its test to11ffb1195fef'ssrc,test/restart-preview.test.tsruns 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.d4c39eb3a304, which adds its two cases to75b7b6af1365'ssrc,test/restart-preview.test.tsruns 49 tests and 2 fail, the two ephemeral runs. Each takes the restart, its compile refuses with anOverBudgetError, and the run ends with that failure.50b205824b20, which adds its tests to62117ee41b41'ssrc,test/restart-preview.test.tsruns 52 tests and 4 fail: the declined stream's alert, given a long provider reason, at its check that the webhook's text holdsphysicalWindowTokensas its whole advice; a passthrough agent's decision, which builds its key; the case whosestart()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.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:clearedByignored: 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 afterstart().UncoveredDropErrornot 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.chunksTotalout of the key: the closed-chunk case.wont_fitthat ignores the hold's reach: the ephemeral run's compression-named case and the case whose hold can take the agent only afterstart(). An uncovered drop that ignores it: the ephemeral run's uncovered case.test/budget-refusal-hold.test.tsits case that leaves an ephemeral run to its error policy, with most of that file's other cases, for a reason not traced.dropBlockflipped: the thinking-binding case.start().pendingMergesout of the key, and the shared slot's early return taken out: none. Not verified says why.75b7b6af1365and atmain36b6dfe9de39. Past75b7b6af1365the 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, at62117ee41b41, whosepreviewCompileandrestartPreviewKeythe 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 withrecentWindowTokens6,000,targetChunkTokens1,000 and nophysicalWindowTokens. 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 varycontextBudgetTokens(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.mainanswers 184. In the 2 settings under Problem, every run endsover_budget.contextBudgetTokens12,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.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.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 settingsmainendsover_budget. There the head takes 146 restarts and holds 217 times, and every run answers, againstmain's 40 restarts and 40 holds.Not verified
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 withsrcthat is11ffb1195fef's but for comments, and withmain36b6dfe9de39's. For a resident, nothing past11ffb1195fefchanges anuncovered_dropdecline: it carries no key, and the hold can take a resident. The probe ran 12 cells, hierarchical withautoTickOnNewMessageoff,recentWindowTokens1,000 or 2,000,contextBudgetTokens6,000, 8,000 or 10,000, and the mock provider's window at 20,000 or 30,000. The head declineduncovered_dropin 6 of them. At each,main's compile that threwUncoveredDropErrorreadisReady()true before and after, made no hold, and the turn failed withcontext_refusal. A live compile that queued work showed only withOverBudgetError, on both alike. That's 12 cells of one strategy, and it doesn't show the case can't arise.5cb5ccfd02adand atmain, withmaxStreamTokens8,000,targetChunkTokens1,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. At5cb5ccfd02ad, 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,mainendedover_budgetwithout a hold, and this change answered. This change answered all 72 cells, andmain60. How long after the decline this change's restart came, and what fresh input the stream took pastmaxStreamTokensmeanwhile, weren't measured.contextBudgetTokens8,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 endedover_budget, as onmain.maxStreamTokens8,000, 30 rounds and the mock provider's window at 40,000, over four recent windows and three budgets. At11ffb1195fef, 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. At75b7b6af1365the 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 at6bff97a592with 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. UndermaxLiveImages, newer images cost what the older ones freed, and none of three cases fit.pendingMergestaken 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.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.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.5cb5ccfd02adwith 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 at62b331051d, with context-manager 0.24.2, gave 10.3, 7.4 and 10.6 s.previewCompilepasses nokvUnifiedImmutablePrefixHash, 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, at62117ee41b41, whosepreviewCompilethe 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, andfinalTokensdiffered in 100. It ran no other strategy and no real provider.!this.providerAdmissionClosed. At81ef4cfaf1, no test failed with that term removed. It hasn't been run that way since.NoFoldPlanToPreviewErrorandUncoveredDropError. No run builds a strategy from a second copy.reopenAgentContextreopens 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.