Skip to content

feat(agent): keep each compile's request within physicalWindowTokens, from a turn's first request - #1001

Open
nissa-seru wants to merge 18 commits into
anima-research:mainfrom
nissa-seru:cedric-2714/a2049-request-limit
Open

nissa-seru wants to merge 18 commits into
anima-research:mainfrom
nissa-seru:cedric-2714/a2049-request-limit

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Why. With physicalWindowTokens set, 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 physicalWindowTokens set, 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 on main, 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 physicalWindowTokens set

  • base (main): not checked against the window. The compile fills the agent's budget less the reply's maxTokens. 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.
  • head: kept within the window, as context-manager prices it. The compile counts the system prompt, the tools, the reply's maxTokens, and the four kinds of message below. Where those would pass the window, it selects less history. Every request asks for the agent's maxTokens. So the prompt's room is the window less maxTokens, 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 physicalWindowTokens set

  • base (main): only the tool-result guard's pending originals, through its reserve, at a quarter of their characters beyond their placeholders.
  • head: four things. These are the direct API's tool results that are ready before its compile, the tool-result guard's pending originals while their batch's audit is durable, a 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

  • base (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.
  • head: each is read against what its compile priced, as the request sent it. With physicalWindowTokens set, 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 on main. 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

  • base (main): the request is sent, and the provider refuses it.
  • head: the compile refuses it as over budget, and nothing is sent. The turn ends with errorType window_full. So does a reply whose maxTokens fills 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 lower maxTokens, system prompt, tools, message injections or messages beside the history, or a check of physicalWindowTokens. 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, or over_budget where some room was left.

The tool-result guard's reserve, with physicalWindowTokens set

  • base (main): 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.
  • head: the reserve comes off the budget's maxTokens instead. So the compile's room is the same as on main, 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 reads configured= as the budget less the reserve. Its response reserve= is the reply alone, and its input budget= is unchanged.

An agent with physicalWindowTokens and no contextBudgetTokens

  • base (main): 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.
  • head: the room stays 96,000, and the reply's whole maxTokens is 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 plus maxTokens. So a contextBudgetTokens set under that is a lowering, and is paced.

An agent without physicalWindowTokens

  • base (main): each compile is given no request overhead.
  • head: each compile that builds a request is given the system prompt and the tools as requestOverhead, with no limit. context-manager selects the same as without it, and reports the overhead in RenderStats.request. The guard keeps its reserve as on main while 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

  • base (main): 0.25.1, by the range ^0.25.1.
  • head: 0.26.0, by the range ^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' and contextBudgetTokens' docs, the README's streaming step and docs/dendrite.md say the new rules. The details quote physicalWindowTokens' 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 fragments added: changelog.d/request-window.changed.md, changelog.d/calibration-sample-overhead.changed.md and changelog.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 with main is 53e72cac4280.

Problem

  • Where the projection starts: the physical-window projection at a tool boundary runs only once agent.lastStreamRealInputTokens > 0 (src/framework.ts:19009 at main). So a stream's first call is never projected, and a fresh compile after a restart fills to the same budget.
  • What a compile counts: Agent.resolveBudget gives context-manager { maxTokens: contextBudgetTokens, reserveForResponse: maxTokens } (src/agent.ts:670 at main). The autobiographical strategy fills to maxTokens − 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.
  • A run, at connectome-host main 1526663c1: headless, in batch mode, against a local stand-in for the Messages API. The stand-in refuses with "input length and max_tokens exceed context limit" when its input count plus max_tokens passes 200,000. It counts input with context-manager's defaultTokenEstimator rates 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.
    • Recipe: claude-opus-4-5, which the host's table gives 200,000; contextBudgetTokens 199,000; maxTokens at 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.
    • The host's load-time fit check passed. Three first requests were refused, at inputs of 195,150, 195,263 and 195,332 with max_tokens 16,384. The other seven were accepted.
    • The first refused request had a context of 176,020, inside the compile's bound of 186,268, which is floor((199,000 − 16,384) × 1.02). Its system prompt was 20 and its tools 19,110.
    • That resident had no physicalWindowTokens. connectome-host 1526663c1 sets 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.
  • The guard's reserve against context-manager's rate: the guard prices its pending originals at characters / 4 (src/tool-result-guard.ts:262–278 at main). 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 (its src/message-store.ts:1509–1512, :2374–2382).

Changes

  • build(deps) 8a78333c0e: @animalabs/context-manager ^0.26.0.
    • 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. So CONTEXT_MANAGER_TAGS stands, and its note and the openings test's title name 0.26.0.
    • The openManagers note cites autobiographical.ts:3150 and context-manager.ts:4626–4645 at 0.26.0. That's the same code as at 0.25.1's lines.
    • create()'s refusal of a context-manager without presentRows or noteThinkingDropped names ^0.26.0. test/dependency-ranges.test.ts asserts the range refuses 0.25.1 and starts at 0.26.0.
  • The two sites that compile a request the framework sends: Agent.runInferenceWithInjections, the direct API (src/agent.ts:1980 at the head), and Agent.assembleActivationRequest (:2170). A stream's start, its retries and restarts, and a preview's dry run go through the second. Each passes requestBounds (:2378) the compile's budget. It gives requestOverhead always, and requestLimitTokens and requestOverhead.messages where physicalWindowTokens is set.
  • The limit is the window less the agent's maxTokens, plus the budget's reserveForResponse (:2394). context-manager 0.26.0 takes that reserve off the limit: its allowance is (limit − reserveForResponse) × scale − overhead − injections (boundsUnderRequestLimit, its src/strategies/autobiographical.ts:9568–9581). So the prompt's room is the window less maxTokens and the overhead, whatever the caller reserves. Every request asks for the agent's maxTokens.
  • A reply that fills the window by itself: where maxTokens is at or over physicalWindowTokens, requestBounds throws an OverBudgetError before the compile (:2398–2411). Its requestAllowanceTokens is the window less maxTokens, 0 or less, so the turn ends window_full. context-manager is never given a limit of 0 or less, which 0.26.0 refuses with a TypeError (refuseUnreadableRequestLimit, its src/request-limit.ts:17–18).
  • The overhead without a window: it changes no selection. It adds RenderStats.request. A tool list JSON.stringify can'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.
  • The compiles that build no request are given no limit or overhead: compileContext, a fork's copy of its parent's compiled view, the templates' dry compiles, and the debug API's.
  • A refusal no history can fix: leavesNoRoomInWindow (src/agent.ts:191) is an OverBudgetError whose diagnostics.requestAllowanceTokens is 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). classifyInferenceError gives it window_full (src/framework.ts:35132), which kicks no drain. Its own context-refusal alert (:35343) names first what would make room, then the drain line, then the reason. The agent's failure marker for it is WINDOW_FULL_NOTICE (:3073, used at :35248), in place of the refusal's figures and the generic advice to change approach.
  • Ready results (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), a prefillUserMessage, and the trailing [Continue] or silent tick separator.
  • The list of what a request carries beyond its compile was closed by reading every change made to a request after its compile:
    • the four above, which are priced;
    • renderRequestOnlyRows' request-only turns and setForeignRounds('transcript')'s lines, outside the price;
    • the direct API's results that become ready during its compile, which test/tool-result-guard.test.ts:3899 pins 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 off maxTokens. Without one, it's added to reserveForResponse, as on main.
  • resolveBudget (:690): with a window and no contextBudgetTokens, { maxTokens: 96,000 + maxTokens, reserveForResponse: maxTokens }, from CONTEXT_MANAGER_DEFAULT_BUDGET (:183). The runtime settings read the live budget as liveBudgetTokens (:698), which is contextBudgetTokens or that same default, where they read contextBudgetTokens ?? 100,000 before.
  • The calibration sample: a stream's start returns what its compile priced, as StartStreamResult.requestOverhead. driveStream gives 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 :2890 and :2908).
  • The catch-up, merge 2933273ce5: main 53e72cac4280 took membrane ^0.8.0 || ^0.9.0 (#955) and passes lists without a spread (#942). Its one conflict was package.json's dependency pair, resolved to this change'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 the two share one membrane.
  • physicalWindowTokens' doc (src/types/agent.ts):
    • base (main, :94–108): "… The projection runs once a call on the stream has reported its prompt. Unset → no projection; only the maxStreamTokens restart applies. …"
    • head (: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's requestLimitTokens, beside the request's system prompt and tools as requestOverhead, with what the request may send besides the compiled context as its messages: 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), a prefillUserMessage, 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 as window_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.

  • Red first, at the floor commit 8a78333c0e, which is main 21e6b1241756 with context-manager ^0.26.0, with the six test files the head changes beyond the floor commit. tsc is clean. Of 169 tests, 151 pass and 18 fail:
    • test/request-window.test.ts, new, fails 15 of 18. The three that pass hold on main too. main reads 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.ts and test/agent-runtime-settings.test.ts fail only their new tests, one each;
    • test/kv-unified-wiring.test.ts fails 1, the compile's options, which main doesn't give;
    • test/end-turn-usage.test.ts and test/sent-rows.test.ts pass. The change gives their window restarts windows the first request fits in.
  • Each test commit against the code before its change:
    • 3e8123c7e5 fails 7 of 7, f2aea7d7bc 6 of 12, and 4454ceef46 1 of 13. These ran at an earlier push, c7e72d13e1, whose trees differ from these only in one line of changelog.d/context-manager-0.26.0.changed.md, which no test reads.
    • The review round's d125e82014 fails 7: in request-window, four new tests and the no-room test it extends to wait for the refusal; and the new test in each of turn-inputs-pin and agent-runtime-settings.
    • 34d71ff6cd's test of an allowance of exactly 0 passes, and fails where the check is < 0.
  • The floor commit 8a78333c0e, at that earlier push: tsc is clean, and dependency-ranges, supplied-text, presentation-mandatory and install-dependency-floors pass 45 of 45.
  • At 2933273ce58d, in a clean checkout: tsc is clean. 67 test files pass 2,254 of 2,254, request-window 18 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.
  • 21 mutants of src at 2933273ce58d: each fails 1 to 12 of its tests.
    • Thirteen in src/agent.ts are 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 on main, or both.
    • Eight are for the review round: the caller's budget in the limit, the window_full classification, 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.
  • The commits after 2933273ce58d, red first: 2933273ce58d's src with cd6c4632de's test/request-window.test.ts and test/calibration-request-overhead.test.ts. tsc is clean. Of 28 tests, 21 pass and 7 fail:
    • request-window fails 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 above maxTokens; and a reply that fills the window by itself.
    • calibration-request-overhead fails 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 at 2933273ce58d too, since its sample carries no messages.
  • cdf7c521d5's test against cd6c4632de's src: 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.
  • At the head, in a clean checkout: tsc is 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.
  • 13 mutants of src at the head: each fails 1 or 2 of those 29 tests. They kick the drain on window_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.
  • The whole suite at the head itself, on GitHub's ubuntu-26.04 image 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, then npm run build, then the leg's three test steps, in run 42683436012. node scripts/test-as-ci.mjs ran 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

  • A host. The change hasn't run in connectome-host or another host.
  • A provider's own count. context-manager's price is an estimate. Whether a provider counts the same request within the window depends on how close that estimate is. Its calibration narrows the gap as its samples come in.
  • The parts outside the price are bounded by reading, not by a run: the request-only turns, another agent's transcript lines, a result ready during the direct API's compile, and membrane's additions.
  • Large windows. The change's tests bind at windows of 1,500 to 20,000 tokens. Some also run at 100,000 and 200,000, where the window doesn't bind.

Cedric-2714 added 9 commits November 9, 2026 07:41
…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.
@nissa-seru
nissa-seru requested a review from Anarchid November 9, 2026 13:09
@nissa-seru nissa-seru self-assigned this Nov 9, 2026
@anima-review

anima-review Bot commented Nov 9, 2026

Copy link
Copy Markdown

Automatic review of 92b19d434d94.

About these findings

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

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

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

Findings, most severe first:

  • 1. should fix at src/agent.ts:673, src/agent.ts:1947, src/agent.ts:2128, src/agent.ts:2275, src/agent.ts:672, src/agent.ts:2132, src/agent.ts:2341, src/agent.ts:671, src/agent.ts:2273, src/agent.ts:2339: A caller-supplied budget's smaller reply reserve is used for the window while the request still asks for the agent's maxTokens.
    • Failing case: With physicalWindowTokens set, maxTokens 8,000 and a caller budget reserving 1,000 for the response, a compile may select input fitting the window after a 1,000-token reserve, while the request asks the provider for 8,000 output tokens and can exceed the window.
  • 2. should fix at src/framework.ts:35120, src/framework.ts:35262, src/framework.ts:35295, src/framework.ts:35317, src/framework.ts:35323, src/framework.ts:35233, src/framework.ts:3067, src/agent.ts:846, src/agent.ts:858, src/agent.ts:872, src/agent.ts:2331, src/agent.ts:2341, src/agent.ts:1364, src/framework.ts:35321: Refusals caused by irreducible request overhead are handled as history-budget refusals: drains, unpinning, budget rollback and wrong alerts follow.
    • Failing case: When the system prompt, tools and reply reserve already fill physicalWindowTokens, an OverBudgetError kicks the drain; after it finishes, every new wake that encounters the same refusal can kick it again, wasting maintenance work and potentially repeatedly folding history without resolving the outage.
  • 3. should fix at src/agent.ts:1931, src/agent.ts:1960, src/agent.ts:2889, src/tool-result-history.ts:283: The direct API serializes unguarded ready results twice, so what is priced can differ from what is sent.
    • Failing case: A tool result whose data getter returns a short string on its first read and a large string on its second is priced as the short result but sent as the large one, potentially exceeding the window; without a window, the additional read still changes what the caller's result sends compared with the previous single read.
  • 4. should fix at src/agent.ts:2128, src/framework.ts:10748, src/framework.ts:10839, src/framework.ts:11191: Runtime-settings feasibility previews don't pass the new request limit or overhead.
    • Failing case: With a history that fits the context budget but a system prompt, tools and reply reserve that fill the physical window, previewAgentRuntimeSettings can report effective.fits=true and boot restore can accept that as proof, while the actual bounded activation compile refuses without sending a request.
  • 5. should fix at test/request-window.test.ts:286, test/request-window.test.ts:293, test/request-window.test.ts:294: The refusal test uses a fixed wait and never confirms a window-limit refusal actually happened.
    • Failing case: If the event is still queued on a loaded worker, or processing fails before reaching the compile, the test passes with zero calls even though no window-limit refusal occurred; wait for the terminal failure and assert its over-budget cause and that compilation was reached.
  • 6. minor at src/agent.ts:2333, src/tool-result-guard.ts:281, src/tool-result-guard.ts:480, src/tool-result-guard.ts:437, src/tool-result-guard.ts:447, src/tool-result-guard.ts:482: Pending guard originals are charged even when audit durability failed and only placeholders would be sent.
    • Failing case: If an audit write fails and a large batch remains non-durable, prepareRequest would send only its small placeholders, but pendingWireBlocks charges the originals; when those originals exceed the physical window, compilation fails instead of sending the safe placeholders and allowing the batch to settle.
  • 7. minor at src/agent.ts:1931, src/agent.ts:1949, src/agent.ts:1960, test/tool-result-guard.test.ts:3899: The direct API prices a frozen readyResults snapshot but sends results that became ready during compile.
    • Failing case: As in the existing late-result test, a tool round completes while compile awaits; with a large result and a nearly full physical window, requestOverhead.messages omits that result even though the resulting request includes it.
  • 8. minor at src/agent.ts:2128, src/agent.ts:2213, src/agent.ts:2214: Stream compiles don't price the historical request-only turns that are reinserted after compilation.
    • Failing case: After enough surviving silent-heartbeat replies, their reinserted separators add many unpriced tokens to a request whose compiled context already approaches the physical limit, so the request as sent can exceed that limit.
  • 9. minor at src/agent.ts:2128, src/agent.ts:2195, src/agent.ts:1956, src/foreign-rounds.ts:124: Foreign tool blocks are priced before being converted into larger transcript text.
    • Failing case: A watcher whose selected history contains many foreign short tool calls or results can compile up to the physical limit and then send a larger transcript than the compile priced.
  • 10. minor at src/framework.ts:19004, src/framework.ts:19006, src/agent.ts:2465, change-text.md:5: Stream continuations skip the window check when the first call reported no prompt usage.
    • Failing case: A provider omits prompt usage on the first response, which calls a tool; a large tool result is then appended and the continuation is sent without either a projection or a fresh bounded compile, even when it exceeds the window.
  • 11. minor at src/agent.ts:2329, src/framework.ts:5164, test/dependency-ranges.test.ts:129: The startup dependency check doesn't verify that context-manager compile honors requestLimitTokens.
    • Failing case: A host that resolves or links context-manager 0.25.1 passes create() and receives requests with physicalWindowTokens set, but that compile ignores requestLimitTokens and can send a request past the window; the linked-dependency test also checks only the old methods and skips the version check.
  • 12. minor at changelog.d/context-manager-0.26.0.changed.md:1, src/agent.ts:2341: The changelog claims other stores compile unchanged, overlooking the new request limit.
    • Failing case: A host with ordinary unique tool-call IDs, physicalWindowTokens set, and enough history to fill the window can receive a shorter compiled context after upgrading, so the claim misleads an operator assessing prompt-cache invalidation.
  • 13. minor at src/agent.ts:1920, src/agent.ts:1931, src/agent.ts:1949, src/agent.ts:1960, src/agent.ts:1964, src/agent.ts:2809, src/tool-result-guard.ts:548, src/framework.ts:8842: After the guard is disabled, a direct-API refusal retry reappends cached originals the guard already withheld.
    • Failing case: With the guard enabled, make a direct call with ready tool results whose compile fails, for example because a system injection fills the physical window; disable the guard, then retry without the large injection. The pending batch is still submitted by prepareRequest, while the same results are cached as unguarded; if the provider refuses, withhold clears pending and the second compile prices the cached ready results and the withheld stubs, but the retry sends the refused originals again alongside the stubs. Disabling the guard is documented as affecting future results only, and this retry behavior also exists at the base.
  • 14. minor at src/types/agent.ts:136, src/agent.ts:673, src/agent.ts:676: contextBudgetTokens docs ignore the different budget built when physicalWindowTokens is set.
    • Failing case: With physicalWindowTokens set, contextBudgetTokens unset, and maxTokens 16,000, the compile receives maxTokens 112,000 and reserveForResponse 16,000 rather than the built-in 100,000/4,000 budget, misleading a host configuring its context budget.

Cedric-2714 added 5 commits November 9, 2026 08:32
… 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.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

This answers the automatic review of 92b19d434d94. d125e82014 adds the tests, and 9d759c9097 fixes 1, 2, 3, 6, 12 and 14. 34d71ff6cd tests 2 where exactly no room is left, and 6f3bcfef22 names in 2's alert all that leaves no room. The tests answer 5. 4, 7, 8, 9, 10, 11 and 13 stay as they are, each for the reason below.

  • 1: every request asks for the agent's maxTokens. Where a caller's budget reserves less for the reply, requestLimitTokens is now lowered by the difference. So the limit counts the reply the request asks for.
  • 2: a refusal whose limit leaves the context no room is now told apart, by context-manager's requestAllowanceTokens at or below 0. The turn ends with errorType window_full, and no compression drain is kicked. The compile neither rolls back a budget nor unpins the turn's inputs for it. Its context-refusal alert names what leaves no room, as context-manager prices it: the reply's maxTokens, the system prompt, the tools, the messages the turn sends beside its history, and the message injections. It says what would make room. One case 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 request is priced at the calibration's ceiling of 1.8. Where the system prompt, the tools, the messages the turn sends beside its history and the message injections fit beside the reply at a multiplier of 1, but not priced at 1.8, every compile is refused this way. Nothing is sent, so no sample comes to settle the multiplier, and each turn ends window_full. The description names this as a limit of this head.
  • 3: the direct API now builds its ready results once, before the compile, which prices them, and sends those. It reads results after the compile only where none were ready before it, as main does for a result that becomes ready during the compile.
  • 4 declined: outside a turn, the runtime-settings preview has no system-position injections, so it could only price the overhead approximately. A second, looser price beside the compile's would mislead more than it helps. The preview speaks of the budget's history, not the window.
  • 5: the test now waits for the turn's failure and the alert. It asserts window_full, and that the compile ran with the window.
  • 6: pendingWireBlocks is now empty while the batch's audit isn't durable, since the placeholders go instead.
  • 7 declined: a result that becomes ready during the direct API's compile is read after it, as on main, and test/tool-result-guard.test.ts pins that read. Only a framework-driven turn on the same agent can bring one. physicalWindowTokens' doc names it as outside the price.
  • 8 declined: the request-only turns are outside the price, and the doc names them with their size. A surviving silent tick's separator is 199 characters, 69 tokens by context-manager's estimator. They reach context-manager through its calibration.
  • 9 declined: another agent's rounds sent as transcript lines are outside the price too. The doc names them with what they can add: about the tool's name and 13 characters a line.
  • 10 declined: the projection waits for a reported prompt on main too. This change bounds the compile, not a continuation.
  • 11 declined: the declared range is ^0.26.0, so an install takes a context-manager that reads the limit. A linked or forced 0.25.1 isn't detected, and the description and docs/dendrite.md say so. No class or export tells 0.25.1 from 0.26.0. Its version number could be read, as chronicle's is, but a linked context-manager keeps its main's number until its release is cut, as test/dependency-ranges.test.ts notes. So the number can't tell a linked tree that reads the limit from one that doesn't.
  • 12: the fragment now reads "Apart from the request limit, every other store compiles what it compiled before."
  • 13 declined: the finding notes that this retry behaviour exists at the base, and this change doesn't touch it.
  • 14: contextBudgetTokens' doc now gives the budget a window gives when it's unset: the default's room of 96,000, with the agent's maxTokens reserved on top.
Credits

Cedric-2714 built the round and wrote this reply. Ada-1017 made the call on each finding, and Phoebe-2419 read the round.

— Cedric-2714

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Cedric-2714

@anima-review

anima-review Bot commented Nov 9, 2026

Copy link
Copy Markdown

Automatic context view of 2933273ce58d merged onto 96504c579a36.

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: 0.25.1 in the base and 0.26.0 in the head. The view chose the base's context-manager 0.25.1, since connectome-host's lockfile holds 0.25.0, which doesn't meet ^0.25.1 from the base. The view chose the head's context-manager 0.26.0, since connectome-host's lockfile holds 0.25.0, which doesn't meet ^0.26.0 from this head. So a difference this view shows may come from context-manager as well as from this head, and it can't tell which.

In the files profiled hosts loaded, 24 changed source lines that map to code ran in no profiled drive: src/agent.ts lines 689–690, 1947–1949, 1957–1958, 1960–1961 and 1964–1967; src/framework.ts lines 35131, 35342–35346 and 35351–35352; src/tool-result-guard.ts lines 285–287. 91 changed source lines map to nothing the host runs: 38 of src/agent.ts's, 4 of src/context-manager-errors.ts's, 10 of src/framework.ts's, 3 of src/notice-tags.ts's, 4 of src/tool-result-guard.ts's and 32 of src/types/agent.ts's. The whole list is in the fold below.

Usage: the figures of the agents' requests, subagents' included, in the 76 runs are the same in the base and this head: 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
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,813 52.6% 993,917 20,252 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,549 56.9% 862,775 20,252 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,549 56.9% 862,775 20,252 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,813 52.6% 993,917 20,252 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,549 56.9% 862,775 20,252 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,549 56.9% 862,775 20,252 0 0 1.29, confirmed

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

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

src/agent.ts: lines 689–690, 1947–1949, 1957–1958, 1960–1961 and 1964–1967 ran in no profiled drive; lines 135, 177, 179–185, 191, 682–684, 699–707, 860, 1832–1836, 1946, 1974–1976, 2345–2346, 2356–2357 and 2363–2364 map to nothing the host runs.

src/context-manager-errors.ts: lines 10–13 map to nothing the host runs.

src/framework.ts: lines 35131, 35342–35346 and 35351–35352 ran in no profiled drive; lines 4860–4861, 4864, 5172, 35129, 35340 and 35347–35350 map to nothing the host runs.

src/notice-tags.ts: lines 79–81 map to nothing the host runs.

src/tool-result-guard.ts: lines 285–287 ran in no profiled drive; lines 281–282 and 288–289 map to nothing the host runs.

src/types/agent.ts: lines 103–131 and 139–141 map to nothing the host runs.

Held the same in base and head: connectome-host 75b9e445d87b, chronicle 0.6.0, membrane 0.8.0.

@anima-review

anima-review Bot commented Nov 9, 2026

Copy link
Copy Markdown

Automatic review of 2933273ce58d.

About these findings

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

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

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

Findings, most severe first:

  • 1. should fix at src/agent.ts:2361, src/framework.ts:30230, src/framework.ts:35349, changelog.d/request-window.changed.md:1, src/types/agent.ts:103: Provisional adaptive calibration prices at the 1.8 ceiling, so refused compiles never produce the sample to settle it, deadlocking the agent; docs omit this.
    • Failing case: With no stored calibration, overhead and the mandatory newest turn fit at multiplier 1 but not at 1.8, so every request-building compile is refused before a provider call; drain ticks cannot make that mandatory content fit, and hard-down escalation only alerts, so later turns repeat until the configuration or calibration is changed externally.
  • 2. should fix at src/agent.ts:861, src/agent.ts:1379, src/framework.ts:35127, src/framework.ts:35269, src/framework.ts:35324, src/agent.ts:190, src/framework.ts:35124, src/framework.ts:35330: Only a nonpositive allowance counts as window_full, so positive-but-insufficient window allowances go through budget-refusal handling.
    • Failing case: If the physical window leaves 100 tokens for a mandatory newest turn costing 500, raising contextBudgetTokens cannot increase the allowance, yet the error withdraws the budget proof, may roll back a boot-restored budget and unpin inputs, kicks the drain, and tells the operator to raise the budget or grace ratio.
    • Raised before as finding 2 of the review of 92b19d434d94, fixed then; this is a case the fix leaves.
  • 3. minor at src/agent.ts:2149, src/agent.ts:1864, src/agent.ts:2268, src/agent.ts:1966, src/agent.ts:1981, src/tool-result-guard.ts:245, src/agent.ts:1379, src/tool-result-guard.ts:550: Pending guard batch is snapshotted for pricing before async compile and not revalidated at assembly, and the annotation retry misses acceptance and throws.
    • Failing case: A direct API compile begins with no pending batch while a framework-driven turn on the same agent stages a durable large result; if the compile selects its placeholder, prepareRequest substitutes the original even though requestOverhead.messages did not include it, so a request can exceed physicalWindowTokens as priced by context-manager. The same stale options also survive the unpin retry while compileBudget is recalculated from the new batch.
  • 4. minor at src/agent.ts:690, src/agent.ts:923, src/agent.ts:1057, src/framework.ts:9352, src/agent.ts:1008, src/framework.ts:10749, src/framework.ts:10836, src/framework.ts:9872, src/agent.ts:853, src/agent.ts:907, src/framework.ts:11253, src/framework.ts:11188: Runtime settings treat the default compile budget as 100,000 though with a window it is 96,000 plus maxTokens.
    • Failing case: With a physical window, no configured context budget, and maxTokens=16,000, the compile budget is 112,000; setting contextBudgetTokens to 105,000 is a 7,000-token decrease, but the code treats it as an immediate non-lowering change and applies it even if its preflight says the folded floor does not fit.
  • 5. minor at test/request-window.test.ts:151, test/request-window.test.ts:163, src/agent.ts:1958, src/agent.ts:1970, src/agent.ts:2135, src/agent.ts:2153: Request-site tests never supply a system-position injection, leaving its pricing and priced/sent parity untested.
    • Failing case: A context-manager that transforms, filters, or returns a different set of system injections could make either request site's priced system differ from its sent system while both existing parity tests still pass.
  • 6. minor at src/agent.ts:1949, src/agent.ts:1966, src/agent.ts:1977, src/agent.ts:2829: Direct-API refusal retry prices the stale pre-compile readyResults snapshot instead of the results it will actually send.
    • Failing case: If an unguarded result becomes ready during the initial compile, it is captured in unguardedResults and sent, but readyResults remains empty; if a pending guard batch then causes a refusal retry, the retry sends that same result without pricing it despite it now being known.
  • 7. minor at src/framework.ts:34653, src/framework.ts:35124, src/framework.ts:35269, src/framework.ts:35335: The exhaustion handler's reason-only fallback cannot recognize a window_full refusal.
    • Failing case: When an OverBudgetError's reason reaches inference:exhausted without errorType, the handler either matches its generic hard-budget wording and kicks a drain with budget advice or leaves it unclassified; neither path emits the window_full diagnosis or its corrective advice.
  • 8. minor at src/agent.ts:2149, src/agent.ts:2237, src/agent.ts:2248, src/framework.ts:35335: Stream compile always charges a prospective trailing [Continue] turn even when assembly sends none.
    • Failing case: When a normal activation ends on a user message, assembly does not append [Continue], but the compile still prices it; at a window boundary where that extra charge exhausts the allowance, no request is sent and the alert claims the messages actually sent leave no room.
  • 9. minor at src/tool-result-guard.ts:265, src/tool-result-guard.ts:283, src/agent.ts:710, test/request-window.test.ts:380: Non-durable pending batch's originals still reduce the compile budget though only placeholders are sent.
    • Failing case: If the audit sync fails for a large result and the configured context budget has less remaining room than its originals' reserve, compilation can refuse or shed useful history even though the small placeholder fits; the added test checks overhead but not the budget deduction.
    • Raised before as finding 6 of the review of 92b19d434d94, fixed then; this is a case the fix leaves.
  • 10. minor at src/framework.ts:35124, src/framework.ts:35228, src/framework.ts:35240: window_full still sends the agent the generic failure marker with host-facing reason and wrong advice.
    • Failing case: When the configured system prompt, tools and reply leave no room, the next successful turn sees internal window and budget diagnostics and advice about dropping an attachment or unsupported setting, rather than being told that repeating the turn cannot fix the configuration and who can fix it.
  • 11. minor at src/framework.ts:35339, src/framework.ts:35342, src/framework.ts:3701, src/framework.ts:35033: window_full alert places remedies after long reason and explanation, so webhook truncation can cut them.
    • Failing case: For a sufficiently long compile refusal, a webhook operator receives a cut alert whose middle omits the instruction to lower maxTokens and part of the explanation, even though those are needed to recover the agent.
  • 12. minor at test/request-window.test.ts:303, test/request-window.test.ts:307, src/framework.ts:35288: No-room test asserts the alert's no-drain claim but never observes whether a drain ran.
    • Failing case: If the window_full path starts a compression drain while still emitting the current alert, the test passes even though the turn performs the work it promises to avoid.
  • 13. minor at test/request-window.test.ts:306, src/framework.ts:35340: No-room alert test checks only maxTokens, not the other named causes and remedies.
    • Failing case: Removing the system prompt, tools, message injections, or beside-history messages from the alert, or removing their remedies, leaves the assertions passing while the alert no longer explains what can make room.
  • 14. minor at src/agent.ts:1966, src/agent.ts:2358, test/request-window.test.ts:348, test/request-window.test.ts:163: Caller-supplied reply-reserve adjustment to the request limit is untested at the direct inference call site.
    • Failing case: If runInferenceWithInjections stops passing its explicit budget to requestBounds, the direct-API test still uses the default budget and the caller-budget test still exercises only buildActivationRequest, although a direct request with a smaller reserve can exceed the priced window.
  • 15. minor at test/request-window.test.ts:393, test/request-window.test.ts:397: Non-durable guard test infers a placeholder was sent only from absence of the original text.
    • Failing case: If the request drops the result altogether and the overhead also omits the original, both assertions pass even though the request was not priced or sent as the placeholder the test describes.
  • 16. minor at src/types/agent.ts:117, src/agent.ts:2357: Doc claim that no budget can make room in window_full overlooks a caller reserve exceeding maxTokens.
    • Failing case: If a direct API caller reserves 8,000 tokens for a reply while the agent requests 1,000 and the window is 5,000, the oversized reserve can exhaust the request allowance; lowering that budget reserve to 1,000 can make the same request fit without changing the window or request contents.
  • 17. minor at change-text.md:5, src/framework.ts:18989, src/framework.ts:19009: Description promises every request stays in the window, but continuations skip projection without reported prompt usage.
    • Failing case: If a provider does not report prompt usage for the first call and its tool result pushes the continuation past the window, the projection is skipped and the continuation can be sent over the limit; an operator relying on the stated universal protection would not expect this.
    • Raised before as finding 10 of the review of 92b19d434d94 and answered: «**10** declined: the projection waits for a reported prompt on main too. This change bounds the compile, not a continuation.» At this head, that doesn't hold: The answer says 'This change bounds the compile, not a continuation', but the description's line 5, which the finding cited, still claims every request the framework sends is kept within the window.
  • 18. minor at test/request-window.test.ts:293, test/request-window.test.ts:316, test/request-window.test.ts:348: No test covers a zero or negative requestLimitTokens boundary.
    • Failing case: With maxTokens 16,384, physicalWindowTokens 16,000 and an explicit compile budget reserving zero for the reply, requestBounds passes -384 as the limit; no test checks that this produces a window_full turn and a cause-naming alert rather than a different error or an unchecked request.
  • 19. minor at src/types/agent.ts:109, src/tool-result-guard.ts:283, src/types/agent.ts:417, src/tool-result-guard.ts:292: Doc says pending originals are priced whenever a batch is pending, but non-durable batches are excluded.
    • Failing case: If the guard's audit sync fails, the batch remains pending but the compile prices the placeholders instead of the originals, contrary to the contract a host consulting this field reads.
Raised before, still answered (4)
  • minor at src/agent.ts:2149, src/agent.ts:2230, src/agent.ts:2231: Stream compile does not price the request-only turns (silent-tick separators) that are reinserted after it. Raised before as finding 8 of the review of 92b19d434d94, answered by @nissa-seru as a choice; at this head: Declining is a scope choice, and its facts hold: the doc names the request-only turns as unpriced, with 199 characters per separator, and on the adaptive path the provider's count feeds them back through calibration.
  • minor at src/framework.ts:10822, src/framework.ts:10840, src/framework.ts:11192, src/framework.ts:11200, src/agent.ts:861: Runtime-settings preview and boot preflight check only budget fit, ignoring the window limit and request overhead. Raised before as finding 4 of the review of 92b19d434d94, answered by @nissa-seru as a choice; at this head: Declining is a scope choice, and its facts hold: the preview prices the budget's history alone, without the request's system injections. For a window_full refusal, keeping the budget is consistent, since no budget makes room there.
  • minor at changelog.d/request-window.changed.md:1, changelog.d/context-manager-0.26.0.changed.md:1: Changelog fragments don't name the prompt-cache miss from the window limit selecting different history. Raised before as finding 12 of the review of 92b19d434d94, answered by @nissa-seru; at this head: The fragment does now read 'Apart from the request limit, every other store compiles what it compiled before.' Inside its Prompt cache paragraph, that marks the limit as a source of changed compiles.
  • minor at src/types/agent.ts:103, docs/dendrite.md:391: physicalWindowTokens doc promises bounded compiles though accepted context-manager 0.25.1 ignores the limit. Raised before as finding 11 of the review of 92b19d434d94, answered by @nissa-seru; at this head: package.json declares ^0.26.0, so an install takes a context-manager that reads the limit, and docs/dendrite.md says a linked or forced 0.25.1 isn't detected. A lockfile can't hold 0.25.1 against that range, so only a deliberately linked or forced tree is left out.

Alfred-2570 added 4 commits November 9, 2026 11:18
…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.
@nissa-seru nissa-seru changed the title feat(agent): keep each request within physicalWindowTokens from a turn's first request feat(agent): keep each compile's request within physicalWindowTokens, from a turn's first request Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant