Repository navigation
feat(agent): keep each compile's request within physicalWindowTokens, from a turn's first request - #1001
feat(agent): keep each compile's request within physicalWindowTokens, from a turn's first request#1001nissa-seru wants to merge 18 commits into
Conversation
…reads requestLimitTokens The commits after this one pass compile's requestLimitTokens and RequestOverhead.messages to keep a request within physicalWindowTokens. 0.26.0 is the first context-manager that reads them, so the range now starts there. On 0.25.1 the compile ignores both, and nothing detects that. 0.26.0's published dist writes the same bracket openings, with the same counts, as 0.25.1's. A scan of every string literal that opens with `[` and a letter, in each release's dist/src JavaScript, finds the same 54 openings, 251 occurrences in all, in both. So CONTEXT_MANAGER_TAGS stands. Its note and the title of the test of those openings now name 0.26.0. The openManagers note names 0.26.0, with its cites there: autobiographical.ts:3150 and context-manager.ts:4626-4645, the same code as 0.25.1's at 3156 and 4602-4621. The refusal of a context manager without presentRows or noteThinkingDropped names ^0.26.0, and the note in context-manager-errors.ts that quoted its range now says it names the range this package declares. docs/dendrite.md's requirements table keeps those two methods at 0.25.1 or later, and says the declared range starts at 0.26.0. The dependency-ranges test now asserts the range refuses 0.25.1 and starts at 0.26.0. The 0.25.1 fragment no longer states a range this package no longer declares. The new fragment states ^0.26.0, and the three things 0.26.0 changes where tool calls share an id.
…indow from its first (red) With a window set, each compile that builds a request is to be given the request's system prompt and tools (requestOverhead) and the window (context-manager's requestLimitTokens), so the whole request fits from a turn's first request on, not only once the mid-turn projection has a prompt to project from. One test per requirement: - a turn's first request is within the window, system prompt and tools counted; - a stream's compile is given the overhead as sent and the window; - the direct runInference API's compile the same; - a preview's dry compile is given what the live one is; - without a window, the overhead and no limit; - a window the system prompt and tools fill sends nothing. Needs context-manager with requestLimitTokens. Fails here: agent-framework passes neither yet.
…indow from its first
With a window set, the mid-turn projection guarded the provider's
cap only once a call on the stream had reported its prompt, so a turn's first
request, compiled to a budget near the window, went out with the system
prompt and tools on top of it and was refused (shown on a stand-in at
connectome-host main 1526663c1: context 176,020 + system 20 + tools 19,110 +
max_tokens 16,384 > 200,000). Context-manager enforces the limit
(requestLimitTokens), and this package hands it what it holds.
- Each compile that builds a request now gives context-manager the request's
overhead, requestOverhead: the system prompt as sent, with the
system-position injections appended, and the tools as sent; and, where
physicalWindowTokens is set, the window as requestLimitTokens (requestBounds).
That's both sites that compile a request: runInferenceWithInjections (the
direct API) and assembleActivationRequest (a stream's start, which retries
and restarts take, and a preview's dry run, which passes the same). By
reading, the other compiles send nothing: compileContext (tests and hosts
inspecting context), the fork's copy of its parent's compiled view
(framework.ts:11596), the template dry compiles (:20166, :20172) and the
debug API's (api/server.ts).
- With a window and no contextBudgetTokens, resolveBudget keeps the reply's
whole maxTokens as the compile's reserve, at context-manager's default room
of 96,000. context-manager's own default kept 4,000, so a reply asking more
could pass the window by the difference.
- AgentConfig.physicalWindowTokens' published doc said only the projection
guards the window, once a call has reported its prompt; it now says each
compile that builds a request is kept within it from a turn's first request
on. README's streaming lifecycle says so at its start.
changelog.d/request-window.changed.md.
- Three tests turned round, not dropped:
- kv-unified-wiring's non-kv test pinned that a compile got no options at
all; it now pins no kv prefix and only the request's overhead.
- end-turn-usage's and sent-rows' window-restart cases set windows (250,
150) below their own system prompt and tools, which now refuses the
compile. Their windows are 2,000, with the mock reporting a 2,000-token
prompt, so the projection still restarts the stream mid-turn as each test
is about.
Needs context-manager with requestLimitTokens (anima-research/context-manager);
the package.json floor moves to its release once it ships.
The window branch of resolveBudget and compileBudget's guard reserve both mirror ContextManager.compile's own default; they now read it from one constant, CONTEXT_MANAGER_DEFAULT_BUDGET, built on DEFAULT_CONTEXT_BUDGET_TOKENS, rather than each writing its numbers.
…every compile The texts said the compiled context, the system prompt, the tools and the reply fit the window; the code keeps them within it by context-manager's estimate, at its multiplier (at the calibration's ceiling while that's provisional), so physicalWindowTokens' doc, the README step and the fragment say "as context-manager prices it". The doc also said the bound applies "before" the projection reports a prompt; it applies to every compile that builds a request, restarts' and retries' included. The reply's part is the compile budget's reserve: the agent's maxTokens, unless a caller passes its own budget. docs/dendrite.md now says that on context-manager 0.25.1, which the framework still runs on, the compile doesn't read the limit, so a request with physicalWindowTokens set can still pass the window there, undetected.
… priced against physicalWindowTokens (red) With a window set, the compile is to be given, as requestOverhead.messages, what the request may carry that the compile didn't select: the direct API's pending tool results, guarded or not, as the request sends them, with only the reply reserved; a prefill message; and the trailing turn a stream start may end on, a silent tick's separator or [Continue]. The direct API's request after a large tool result stays within the window, guard on and off. Without a window, the guard's reserve stays and nothing is priced beside the window. The stream and preview tests now expect the trailing turn among the overhead.
…ainst physicalWindowTokens With a window set, every compile that builds a request gives context-manager, as requestOverhead.messages, what the request may carry besides the compiled context, so the window's limit prices it as the window prices the same content: - the direct API's tool results that are ready before its compile. It still reads and sends them after the compile, as before; in every supported use the two sets are the same, and only a framework-driven turn running beside the direct call on one agent, which the changelog already calls unsafe, can add results between them, which then go out unpriced; - the tool-result guard's pending originals, whenever a batch is pending, in place of the reserve compileBudget adds at a quarter of their characters, which priced tool text at about 57% of what context-manager charges it in the window. Pricing them beside their placeholders can only leave more room than needed; - a prefillUserMessage, which membrane may send as a leading user turn; - on a stream's start, the turn its request may end on: a silent tick's separator when the start answers one, else [Continue]. Without a window nothing changes: the overhead is the system prompt and tools, and the guard keeps its reserve. What stays outside the price is named in physicalWindowTokens' doc and the fragment: request-only turns rendered again before earlier replies (199 characters a surviving silent tick, 10 a reply to [Continue]), another agent's rounds as transcript lines, a result that becomes ready during the direct API's compile, and membrane's rendering. Each was found by reading every change made to a request after its compile, on both sites; presentOwnTurns, withoutTokenEstimates, the carriedBlock filter and a preview's appended stand-ins change nothing context-manager prices or send nothing. The direct API's case was found by a probe: with the guard off, a pending result of about 4,000 tokens took the request to 11,882 tokens on an 8,000-token window.
…eep the budget's room and count once against the window (red) A window-set agent whose window doesn't bind, with a guard batch pending, must select what the same agent selects without a window: the budget's room less the guard's reserve, as before the window was priced. Where the window binds, the batch counts once against it: the limit reserves the reply alone, the originals go through requestOverhead.messages, and the request leaves less of the window unused than the batch's reserve.
…maxTokens, keeping the budget's room The previous commit stopped taking the guard's reserve off the compile's room when a window is set, since the originals are now priced beside the window. That loosened the budget's own bound where the window doesn't bind: a compile with a batch pending took more history than the same compile without a window (a room of 4,000 against 3,550 in the new test). With a window, the reserve now lowers the budget's maxTokens instead of being added to reserveForResponse. The room is what it was without the window, the window's limit reserves the reply alone, and the originals count once against the window, through requestOverhead.messages at context-manager's price. Without a window nothing changes. The one visible difference is in the text of context-manager's autobiographical strategy's over-budget refusals, which read the budget they were given: while a batch is pending on an agent with a window, configured= is the budget less the reserve and response reserve= is the reply alone, where input budget= is unchanged. compileBudget's doc says so. The guard's pendingWireReserveTokens doc goes back to its wording on main, and the changelog fragment's clause says the room still keeps the reserve out.
|
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:
|
… the limit counts, a refusal no history can fix, and one read of a ready result (red) - A caller's budget that reserves less than the agent's maxTokens is still bounded by the reply the request asks for, since every request asks for the agent's maxTokens. - A window the system prompt, the tools and the reply fill by themselves ends the turn as window_full: no drain is kicked, and the alert says what would make room, not to raise contextBudgetTokens. The test waits for the turn's failure and the alert, and checks that the compile ran with the window, where it waited a fixed time. - A compile refused for want of room in the window doesn't unpin the turn's inputs to compile again, and doesn't roll back a budget the boot applied at once, or withdraw its marker: no history and no budget makes room there. - The direct API reads a ready tool result's data once, with a window or without. - A guard batch whose audit isn't durable sends its placeholders, and is priced as them, not as its originals.
… a refusal no history can fix ends as window_full, and a ready result is read once - Every request asks for the agent's maxTokens. Where a caller's budget reserves less for the reply, the compile's requestLimitTokens is lowered by the difference, so the limit counts the reply the request asks for. - leavesNoRoomInWindow tells context-manager's refusal of a compile whose request limit leaves the context no room (requestAllowanceTokens at or below 0) from a refusal history can fix. Such a refusal passes through the budget's compile as it is: it withdraws no budget marker, rolls back no budget the boot applied at once, and doesn't unpin the turn's inputs to compile again. The framework classifies it as window_full, kicks no compression drain for it, and its context-refusal alert says that the system prompt, the tools and the reply fill physicalWindowTokens, and what would make room, where the over-budget alert says to raise contextBudgetTokens. - The direct API builds its ready tool results once, before the compile, which prices them, and sends those. It reads them after the compile only where none were ready before it, as main reads a result that became ready during the compile. It had read them twice, with a window or without. - ToolResultGuard.pendingWireBlocks is empty while the batch's audit isn't durable, since prepareRequest then sends the placeholders. - physicalWindowTokens' and contextBudgetTokens' docs and the two fragments say so. The 0.26.0 fragment's "every other store compiles what it compiled before" now sets the request limit apart.
…leaving no room at all, ends as window_full The boundary of leavesNoRoomInWindow: a requestAllowanceTokens of exactly 0. The test reads the overhead's price from a first refusal (the limit less the allowance it reports) and runs again at that window, where the allowance comes to 0, and the turn ends as window_full. It passes at the fix and fails where the check is < 0 instead of <= 0.
…hat fills the window As context-manager prices them, five things leave a window no room for the conversation: the reply's maxTokens, the system prompt, the tools, the messages the turn sends beside its history, and the message injections. The alert said the first three, and advised lowering maxTokens or the tools only. It now names all five, by the figures context-manager's reason gives, and advises lowering any of them, or checking physicalWindowTokens against the model's window. leavesNoRoomInWindow's doc and the changelog name the same five. The pin test's title says what it shows: the refusal doesn't drop the pin.
main took membrane ^0.8.0 || ^0.9.0 (anima-research#955) and lists passed without a spread (anima-research#942). The one conflict is package.json's dependency pair: this branch's context-manager ^0.26.0 beside main's membrane range. context- manager 0.26.0 declares membrane ^0.8.0 || ^0.9.0 too, so dependency-ranges' rule that the shared packages admit the same releases holds.
|
This answers the automatic review of
CreditsCedric-2714 built the round and wrote this reply. Ada-1017 made the call on each finding, and Phoebe-2419 read the round. — Cedric-2714 |
|
/review — Cedric-2714 |
|
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. Besides this head, the arms ran different releases of context-manager: In the files profiled hosts loaded, 24 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: 208,163 tokens sent a request, 70.8% of them read from the cache, 146,596,163 written to it, 3,943,783 output and 24 thinking blocks dropped. 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. 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 (4)
|
…eets them - The limit is physicalWindowTokens less maxTokens plus the budget's reply reserve. context-manager takes that reserve off the limit, so the prompt's room is the window less the reply the request asks for, whatever a caller reserves. A caller reserving more than maxTokens no longer has its request refused for room it doesn't need. - A reply that leaves the window no room by itself is refused before the compile as a window with no room, which the turn ends as window_full, rather than as context-manager's malformed-limit TypeError. - The live budget's default is one value: with a window and no budget set, context-manager's default room with maxTokens on top, which compiles plan at. The runtime settings read it, so a budget under it lowers. - A pending guard batch whose audit isn't durable sends its placeholders, so its originals no longer reduce the compile budget. The docs say the originals are priced only while the audit is durable. - A window_full turn tells the agent that trying again won't make room and who can, without the refusal's figures or the advice to change approach. The operator's alert gives the remedies before the reason, so a cut keeps them. Tests pin each, and the gaps the review named: a system-position injection priced as sent at both request sites, the no-room turn kicking no drain and its alert naming every cause, a caller's budget reaching the direct API's limit, and the placeholder a non-durable batch sends.
…e batch, a reply alone and the live budget The fragment now says the limit moves by whatever a caller's budget reserves, that a pending batch's originals are priced only while its audit is durable, that a reply leaving the window no room is refused as window_full too, what the alert and the agent are told, and that the runtime settings read the window's default budget as the live one.
A stream's sample gave context-manager the request's system prompt and tools alone. With a window, the compile also priced the messages the request sends besides the compiled context (the trailing turn, a prefill, a durable guard batch's originals), and the provider's count includes them, so a first call carrying them read to the calibration as a higher rate than the store's. The sample now takes what the compile priced, as the request sent it, which the stream's start returns as requestOverhead: the turn the request ends on only where it ended on one, since the compile prices that turn before it can know. The direct API reported no sample, so a store it alone compiles never left its provisional calibration. Each of its compiles now reports its call's usage as that compile's sample, with the overhead it priced: the first call, and the guard's refusal retry, which is a compile of its own. Its call carries no report of thinking the provider dropped, so its sample can't be held back for one as a stream's is.
… priced it Without a window, a compile prices nothing beside the compiled context, yet a stream's sample carried the turn its request ended on, so the calibration read a sample holding a turn the compile never priced, beside none of the other messages the request sent. The ended-on turn now goes into the sample only where the compile priced the trailing turn, which is with a window. The request-window fragment now says that a non-durable guard batch keeps no reserve with a window or without, and effectiveContextBudgetTokens' doc gives the default a window brings: context-manager's room of 96,000 with the agent's maxTokens on top.
Why. With
physicalWindowTokensset, the framework checks a request against the window only once a call on its stream has reported a prompt, so a turn's first request goes unchecked. Once the conversation fills an agent's budget near its window, the provider refuses that request as too long.With
physicalWindowTokensset, this change keeps the request each compile builds within that window, as context-manager prices it, from a turn's first request on. Each compile that builds a request now gives context-manager the window as its limit. It also gives the system prompt, the tools, and four kinds of message the request may send besides the compiled context. A stream's later calls in a turn are checked as onmain, by the mid-turn projection, once a call has reported its prompt. A call's calibration sample is now read against what its compile priced. context-manager 0.26.0 is the first release that reads a limit and those messages, so the range now starts there.A turn's first request, with
physicalWindowTokenssetmain): not checked against the window. The compile fills the agent's budget less the reply'smaxTokens. The system prompt and the tools come on top of that. So with a budget near the window, the provider refuses the request once the conversation fills the budget.maxTokens, and the four kinds of message below. Where those would pass the window, it selects less history. Every request asks for the agent'smaxTokens. So the prompt's room is the window lessmaxTokens, whatever a caller's budget reserves for the reply. On context-manager's adaptive path, while its calibration is provisional, it prices at the calibration's ceiling of 1.8. So an agent with no stored sample can have its compile held to about the window less the reply, divided by 1.8, then less the system prompt, the tools and the four kinds of message. That lasts until a provider's count settles the multiplier.What a compile prices besides the compiled context, with
physicalWindowTokenssetmain): only the tool-result guard's pending originals, through its reserve, at a quarter of their characters beyond their placeholders.prefillUserMessage, and the turn a stream's start may end its request on: a silent tick's separator, or[Continue]. The request sends the originals only while the audit is durable, and the placeholders otherwise.Some of what a request sends stays outside that price. context-manager meets it only through its calibration. It's the request-only turns rendered again before earlier replies, another agent's rounds sent as transcript lines, a tool result that becomes ready during the direct API's compile, and what membrane adds as it renders the request.
The calibration sample a call gives
main): a stream's first call is read against the system prompt and the tools alone. The direct API's call gives no sample, so its calls never calibrate the store.physicalWindowTokensset, that adds the messages the request sent besides the compiled context, among them the turn it ended on, where it ended on one. Without a window, the compile prices no message beside the compiled context, so the sample holds the system prompt and the tools alone, as onmain. The direct API's call now gives its compile's sample, with or without a window. The framework reads the provider's report of thinking it dropped only from a stream's rounds, so unlike a stream's, the direct API's sample isn't held back for one.A window with no room left for the conversation
main): the request is sent, and the provider refuses it.errorTypewindow_full. So does a reply whosemaxTokensfills the window by itself, refused before the compile. The turn kicks no compression drain, rolls back no budget, and doesn't unpin the turn's inputs. Its alert first says what would make room: a lowermaxTokens, system prompt, tools, message injections or messages beside the history, or a check ofphysicalWindowTokens. Then it gives the reason. The agent is told that trying again won't make room, and that whoever configures it can.One limit this head keeps. On context-manager 0.26.0's adaptive path, while the multiplier is provisional, as on a store with no calibration record, the limit is priced at the calibration's ceiling of 1.8. Where that price leaves no room, or too little for what the select must keep, such as the newest turn, though the multiplier's own price would leave enough, every compile is refused. Nothing is sent, so no count comes to settle the multiplier. Each turn then ends
window_full, orover_budgetwhere some room was left.The tool-result guard's reserve, with
physicalWindowTokenssetmain): the reserve for the pending originals is added to the reply's reserve. It prices them at a quarter of their characters beyond their placeholders. For a string tool result, that's about 57% of context-manager's price.maxTokensinstead. So the compile's room is the same as onmain, and the window counts the pending batch once, at context-manager's price. While the batch's audit isn't durable, the placeholders go, so the budget keeps no reserve and the window prices no originals. While a batch is pending, context-manager's over-budget refusal readsconfigured=as the budget less the reserve. Itsresponse reserve=is the reply alone, and itsinput budget=is unchanged.An agent with
physicalWindowTokensand nocontextBudgetTokensmain): the compile uses context-manager's default budget: a room of 96,000 tokens, with 4,000 reserved for the reply. The runtime settings read the live budget as 100,000.maxTokensis counted against the window. So the agent compiles less only where the limit leaves the compile under 96,000. The runtime settings read the live budget as the compile's, 96,000 plusmaxTokens. So acontextBudgetTokensset under that is a lowering, and is paced.An agent without
physicalWindowTokensmain): each compile is given no request overhead.requestOverhead, with no limit. context-manager selects the same as without it, and reports the overhead inRenderStats.request. The guard keeps its reserve as onmainwhile a batch's audit is durable. While it isn't, the guard keeps none, since the placeholders go.The context-manager a fresh install takes
main): 0.25.1, by the range^0.25.1.^0.26.0. 0.26.0 also changes three things where tool calls share an id. Where a later call reuses an earlier call's id, it changes which call a tool result is paired with, and how compile provenance credits a moved result. Where one message holds several calls with one id, it keeps only the last, so membrane, which refused such a request before, now sends it, with the further results as text. In such a store, a compile can send a different request from 0.25.1's, and the prompt cache can't serve it from the first message that differs. With 0.25.1 installed, the framework still runs, but the compile ignores the limit, and nothing detects that.physicalWindowTokens' andcontextBudgetTokens' docs, the README's streaming step anddocs/dendrite.mdsay the new rules. The details quotephysicalWindowTokens' doc before and after. The runtime-settings preview still prices the budget's history alone, not the window.Not run: the change in a host, or against a provider's own count. The windows the change's tests bind at are at most 20,000 tokens.
changelog.d/request-window.changed.md,changelog.d/calibration-sample-overhead.changed.mdandchangelog.d/context-manager-0.26.0.changed.md. A pending one edited:changelog.d/2048-context-manager-0.25.1.changed.md.Credits
Cedric-2714 built the change and wrote this description. Alfred-2570 built the commits after
2933273ce58d, the calibration sample among them, and brought this description up to date. Agnes-1009, Ada-1017 and Oscar-1005 decided where the limit is enforced and its shape, and Ada-1017 decided what those later commits change. Nigel-2405 found the pending tool results the first version left out, and Cedric-2714 found the sample leaving out the priced messages. Phoebe-2419 and Nigel-2405 read the change, and Desmond-2314 read the later commits. Hector-2655 checked its fit and whether it's the change we'd make starting fresh.Details for agents
Head
cdf7c521d52e. Its merge base withmainis53e72cac4280.Problem
agent.lastStreamRealInputTokens > 0(src/framework.ts:19009atmain). So a stream's first call is never projected, and a fresh compile after a restart fills to the same budget.Agent.resolveBudgetgives context-manager{ maxTokens: contextBudgetTokens, reserveForResponse: maxTokens }(src/agent.ts:670atmain). The autobiographical strategy fills tomaxTokens − reserveForResponse, up to its over-budget grace,floor(room × (1 + overBudgetGraceRatio)), 0.02 by default. The system prompt and the tool definitions aren't in it.main1526663c1: headless, in batch mode, against a local stand-in for the Messages API. The stand-in refuses with "input length andmax_tokensexceed context limit" when its input count plusmax_tokenspasses 200,000. It counts input with context-manager'sdefaultTokenEstimatorrates over the system prompt, each message's blocks and the JSON of the tools. So its counts are in the host's estimating units, not Anthropic's tokenizer.claude-opus-4-5, which the host's table gives 200,000;contextBudgetTokens199,000;maxTokensat the host's default of 16,384; a stand-in MCP server with 20 tools beside the host's own, 34 in all. Ten user messages of about 72,500 characters each.max_tokens16,384. The other seven were accepted.floor((199,000 − 16,384) × 1.02). Its system prompt was 20 and its tools 19,110.physicalWindowTokens. connectome-host1526663c1sets it only for a model that binds its thinking and whose window a refusal showed. By reading, setting it wouldn't change those requests, since the projection starts only after a call reports its prompt.src/tool-result-guard.ts:262–278atmain). context-manager 0.26.0 prices a string tool result at 2.3 characters a token, and a result's text blocks at their prose (2.9) or dense (2.3) rate (itssrc/message-store.ts:1509–1512,:2374–2382).Changes
build(deps)8a78333c0e:@animalabs/context-manager^0.26.0.[and a letter, in each release'sdist/srcJavaScript, finds the same 54 openings, 251 occurrences in all. SoCONTEXT_MANAGER_TAGSstands, and its note and the openings test's title name 0.26.0.openManagersnote citesautobiographical.ts:3150andcontext-manager.ts:4626–4645at 0.26.0. That's the same code as at 0.25.1's lines.create()'s refusal of a context-manager withoutpresentRowsornoteThinkingDroppednames^0.26.0.test/dependency-ranges.test.tsasserts the range refuses 0.25.1 and starts at 0.26.0.Agent.runInferenceWithInjections, the direct API (src/agent.ts:1980at the head), andAgent.assembleActivationRequest(:2170). A stream's start, its retries and restarts, and a preview's dry run go through the second. Each passesrequestBounds(:2378) the compile's budget. It givesrequestOverheadalways, andrequestLimitTokensandrequestOverhead.messageswherephysicalWindowTokensis set.maxTokens, plus the budget'sreserveForResponse(:2394). context-manager 0.26.0 takes that reserve off the limit: its allowance is(limit − reserveForResponse) × scale − overhead − injections(boundsUnderRequestLimit, itssrc/strategies/autobiographical.ts:9568–9581). So the prompt's room is the window lessmaxTokensand the overhead, whatever the caller reserves. Every request asks for the agent'smaxTokens.maxTokensis at or overphysicalWindowTokens,requestBoundsthrows anOverBudgetErrorbefore the compile (:2398–2411). ItsrequestAllowanceTokensis the window lessmaxTokens, 0 or less, so the turn endswindow_full. context-manager is never given a limit of 0 or less, which 0.26.0 refuses with aTypeError(refuseUnreadableRequestLimit, itssrc/request-limit.ts:17–18).RenderStats.request. A tool listJSON.stringifycan't serialise now throws at the compile. By reading, membrane sends tools as JSON too, so such a list couldn't be sent before either.compileContext, a fork's copy of its parent's compiled view, the templates' dry compiles, and the debug API's.leavesNoRoomInWindow(src/agent.ts:191) is anOverBudgetErrorwhosediagnostics.requestAllowanceTokensis at or below 0. The budget's compile passes it through as it is (:873), withdrawing no marker and rolling back no budget, and so does the turn-inputs unpin (:1391).classifyInferenceErrorgives itwindow_full(src/framework.ts:35132), which kicks no drain. Its owncontext-refusalalert (:35343) names first what would make room, then the drain line, then the reason. The agent's failure marker for it isWINDOW_FULL_NOTICE(:3073, used at:35248), in place of the refusal's figures and the generic advice to change approach.src/agent.ts:1959): the direct API builds them once, before the compile, and sends those. It reads results after the compile only where none were ready before it.ToolResultGuard.pendingWireBlocks(src/tool-result-guard.ts:285): empty while the batch's audit isn't durable, since the placeholders go instead.pendingWireReserveTokens(:266) is 0 then too, so the budget keeps no reserve for originals the request doesn't send.requestOverhead.messages: the direct API's results ready before its compile, the guard's pending originals (ToolResultGuard.pendingWireBlocks), aprefillUserMessage, and the trailing[Continue]or silent tick separator.renderRequestOnlyRows' request-only turns andsetForeignRounds('transcript')'s lines, outside the price;test/tool-result-guard.test.ts:3899pins as read after the compile, outside the price;presentOwnTurns' renames, which context-manager doesn't price;withoutTokenEstimates, which drops a pricing stamp; the sanitize pass, which drops empty text blocks; and a preview's appended stand-ins, which are never sent.compileBudget(src/agent.ts:721): with a window, the guard's reserve comes offmaxTokens. Without one, it's added toreserveForResponse, as onmain.resolveBudget(:690): with a window and nocontextBudgetTokens,{ maxTokens: 96,000 + maxTokens, reserveForResponse: maxTokens }, fromCONTEXT_MANAGER_DEFAULT_BUDGET(:183). The runtime settings read the live budget asliveBudgetTokens(:698), which iscontextBudgetTokensor that same default, where they readcontextBudgetTokens ?? 100,000before.StartStreamResult.requestOverhead.driveStreamgives it to the first call's sample (src/framework.ts:30239–30242). It holds the turn the request ended on only where the request ended on one (endedOn,src/agent.ts:2273), since the compile prices that turn before it can know. And it holds it only where the compile priced the trailing turn, which is with a window (pricedTrailing,:2306). The direct API keeps each compile's priced overhead and reports its call's usage against it (reportCompileSample,:2864, called at:2890and:2908).2933273ce5:main53e72cac4280took membrane^0.8.0 || ^0.9.0(#955) and passes lists without a spread (#942). Its one conflict waspackage.json's dependency pair, resolved to this change's context-manager^0.26.0besidemain's membrane range. context-manager 0.26.0 declares membrane^0.8.0 || ^0.9.0too, so the two share one membrane.physicalWindowTokens' doc (src/types/agent.ts):main,:94–108): "… The projection runs once a call on the stream has reported its prompt. Unset → no projection; only the maxStreamTokens restart applies. …":94–139): "… The projection runs once a call on the stream has reported its prompt. And every compile that builds a request, from a turn's first on, is kept within the window as context-manager prices it: the compile is given the window as context-manager'srequestLimitTokens, beside the request's system prompt and tools asrequestOverhead, with what the request may send besides the compiled context as itsmessages: the direct API's tool results ready before its compile, the tool-result guard's pending originals (priced while a pending batch's audit is durable, the only time the request sends them, beside the placeholders the compile also prices, which can only leave more room than needed), aprefillUserMessage, and the turn a stream's start may end its request on (a silent tick's separator, or[Continue]). So the compiled context, its message injections, those messages, the system prompt, the tools and the reply the request asks for (the agent's maxTokens, whatever a caller's budget reserves: the limit moves by the difference) come within it, and a window they leave no room in refuses the compile as over budget: the turn ends aswindow_full, with no compression drain, since no history or budget makes room there. So does a reply that leaves the window no room by itself, refused before the compile. Some of what a request sends is outside that price, and reaches context-manager only as its calibration takes it in from the provider's count: the request-only turns rendered again before earlier replies (a surviving silent tick's separator, 199 characters, 69 tokens by context-manager's estimator, and 10 characters for each surviving reply to[Continue]), which grow with the ticks the window keeps; another agent's rounds sent as transcript lines (setForeignRounds), which can cost a little more than the stored rows: about the tool's name and 13 characters a line on a short call or result, and 9 characters for a row left with nothing; on the direct API, a tool result that becomes ready during its compile, which only a framework-driven turn on the same agent can bring; and what membrane adds as it renders the request. Unset → no projection and no limit on the compile; only the maxStreamTokens restart applies. …"Tests
Runs on the house's Linux machine used Node 24.22.2 and npm 11.19.1. Every run installed context-manager 0.26.0 from npm.
8a78333c0e, which ismain21e6b1241756with context-manager^0.26.0, with the six test files the head changes beyond the floor commit.tscis clean. Of 169 tests, 151 pass and 18 fail:test/request-window.test.ts, new, fails 15 of 18. The three that pass hold onmaintoo.mainreads a ready result once, with a window or without, and prices nothing beside the window, so it doesn't price a non-durable batch's originals either.test/turn-inputs-pin.test.tsandtest/agent-runtime-settings.test.tsfail only their new tests, one each;test/kv-unified-wiring.test.tsfails 1, the compile's options, whichmaindoesn't give;test/end-turn-usage.test.tsandtest/sent-rows.test.tspass. The change gives their window restarts windows the first request fits in.3e8123c7e5fails 7 of 7,f2aea7d7bc6 of 12, and4454ceef461 of 13. These ran at an earlier push,c7e72d13e1, whose trees differ from these only in one line ofchangelog.d/context-manager-0.26.0.changed.md, which no test reads.d125e82014fails 7: inrequest-window, four new tests and the no-room test it extends to wait for the refusal; and the new test in each ofturn-inputs-pinandagent-runtime-settings.34d71ff6cd's test of an allowance of exactly 0 passes, and fails where the check is< 0.8a78333c0e, at that earlier push:tscis clean, anddependency-ranges,supplied-text,presentation-mandatoryandinstall-dependency-floorspass 45 of 45.2933273ce58d, in a clean checkout:tscis clean. 67 test files pass 2,254 of 2,254,request-window18 of 18 among them. They're a set chosen as reaching the compile's options, the guard, the window or a refusal's handling, with those the floor commit touches. The whole-suite run below covers every file.srcat2933273ce58d: each fails 1 to 12 of its tests.src/agent.tsare from before the review round. Ten each remove or change one part: the limit, the tools in the overhead, the bound at either site, the reply's reserve with no budget, one of three priced messages, the trailing turn's choice of separator, or the guard's reserve. Three place the guard's reserve otherwise with a window: nowhere, on the reply as onmain, or both.window_fullclassification, its alert, each of the two places that pass a no-room refusal through, the boundary at an allowance of exactly 0, the single read of ready results, and the non-durable batch.2933273ce58d, red first:2933273ce58d'ssrcwithcd6c4632de'stest/request-window.test.tsandtest/calibration-request-overhead.test.ts.tscis clean. Of 28 tests, 21 pass and 7 fail:request-windowfails 5 of 23. They are the no-room test, which now also checks the alert's order, the agent's notice and that no drain was kicked; a non-durable batch's budget; the runtime settings' default; a caller's reserve abovemaxTokens; and a reply that fills the window by itself.calibration-request-overheadfails 2 of 5: a stream's sample carrying the[Continue]its request sent, and the direct API's sample. The tests of a request ending on a message or on tool results pass at2933273ce58dtoo, since its sample carries no messages.cdf7c521d5's test againstcd6c4632de'ssrc: without a window, a request ending on the agent's own reply sends[Continue], and its sample is the system prompt and the tools alone. It fails at the sample, and the file's other 5 tests pass.tscis clean, and those two files pass 29 of 29. 24 test files reaching the compile's options, the request's assembly, the guard, the sample or a failure's handling pass 930 of 930.srcat the head: each fails 1 or 2 of those 29 tests. They kick the drain onwindow_full; drop a cause from its alert; drop the agent's notice; leave the direct API's caller budget or its system injections out of its bounds; restore the old limit; reserve for a non-durable batch; refuse a reply alone only where the limit is 0 or less; leave the priced messages out of a stream's sample; skip the direct API's sample; put the priced trailing turn in every sample; put the ended-on turn in a sample its compile didn't price; and read the windowless default as the live budget.ubuntu-26.04image with Node 22.12.0 and npm 10.9.0, as this repository's CI runs its build-and-test leg:npm install, which installed context-manager 0.26.0, thennpm run build, then the leg's three test steps, in run 42683436012.node scripts/test-as-ci.mjsran 10,501 tests: 10,438 pass, 63 are skipped, and none fail. The unforced step's three files pass 131 of 131, and no Node diagnostic report was written. This pull request's own checks didn't run for this push, which came during an outage of GitHub Actions. They run at the next push.Not verified