Repository navigation
feat(framework): keep the rows that open a turn raw while it runs - #867
Merged
nissa-seru merged 6 commits intoOct 29, 2026
Merged
Conversation
added 2 commits
October 27, 2026 21:30
A long turn whose older rounds are summarised could lose the request it was answering: once compression covered the opening message, a budget restart's compile rendered it as a summary, and the turn went on from a summary of what it was asked. In a kv-unified turn of 40 tool rounds whose module awaits at each tool result (budget 20,000, raw tail 6,000), the opening request was missing from 25 of the turn's 35 requests. At a start that opens a turn (not a resume, and not a silent heartbeat tick), the framework now pins raw, with context-manager's pinRange and the name agent-framework:turn-inputs, the rows the agent sees that were stored after both the newest of its own rows that it sees and its consumed watermark, starting at the first such row that holds more than tool results. Its compiles, a maxStreamTokens restart's included, render those rows raw until the turn ends. Every opening start first removes any pin of that name, and a silent start removes them and pins nothing. The pin is removed when the turn settles, when its start or continuation is dropped (endDroppedTurn), and at stop(). A range, rather than the rows the turn's trigger delivered: the trigger doesn't name those rows, since a merged trigger keeps a stored message's id only when every batched wake shares it and the gate's batched wake passes none, and they aren't contiguous, since ambient lines are stored as they arrive, residents share one message slot, and the start writes its own clock row. A range pin keeps everything between its ends raw. The strategy reads the slot through the agent's view filter, so a row the agent doesn't see, such as a tuned-out message, stays out of its view inside the range. Both bounds: the consumed watermark moves at a tool boundary only when that boundary injected messages, so after a long turn it sits at that turn's last compile, and alone it would pin the previous turn's rounds. The agent's own newest row alone has no floor when it hasn't spoken for a long time. Rows that hold only tool results are left out of the start: they answer the previous turn's calls, left when that turn ended after storing results. The view filter is read before the own-row stop, so a tuned-out row under the agent's own name doesn't end the scan. The fallback: an OverBudgetError from a committing compile while a pin of that name is held removes the pin and compiles once more, so the turn goes on as it would have without it, a refusal included. It isn't keyed on the refusal naming the pin, since only the adaptive plan's middle refusal names pins and the hierarchical one doesn't. It wraps the bare context-manager compile inside compileAtLiveBudget, so that wrapper's boot-restore branch never takes the pin's refusal for the restored budget's. compileWithInjections (not its dry runs) and compileContext both use it. It reads listPins() before the compile starts, so a pin a crash or a failed write left is found, and two compiles that refuse at once both compile again. Pins are per agent: each resident's ContextManager keeps its strategy state under agents/<name>. Agent.pinTurnInputs and unpinTurnInputs are @internal. Not covered, each falling back to the behaviour before this change: - a turn whose request was stored before the host restarted, which sits under the watermark the agent's creation sets; - the first turn after a branch switch, whose watermark names the other branch; - rows stored before the agent's newest reply that no request carried; - rows a request carried whose turn failed before replying; - the start's own notices and afterUser injections, and a co-resident's row stored during the start's hooks, which land after the range; - a resume after stop() on the same instance, which continues unpinned. Not shown by a test: the fallback's placement against compileAtLiveBudget's boot restore, which is by reading; and a range whose rows had no summaries when they were pinned, where after a fallback the unpinned compile renders them raw. Tests (test/turn-inputs-pin.test.ts, 15): a long kv-unified turn whose module awaits at its tool results keeps its opening request whole in each of its streams' requests; the range after a multi-round turn, after a turn that ended on stored results, and past rows the agent doesn't see; pins of the name left from before are cleared at the next opening, and a silent start clears them and pins nothing; no pin is left after a continuation the inference policy drops or a retry stop() cancels; the fallback under hierarchical and kv-unified, a second refusal, two committing compiles that refuse at once, and a dry run that leaves the pin; a strategy without pins logs nothing; one resident's opening, and its turn's end, leave another resident's pin. With the parent's src/framework.ts and src/agent.ts, every test fails except the one for a strategy without pins.
The test of two committing compiles that refuse at once while the turn's pin is held checked only that both compiled in the end. Had its fixture fit with the pin held, both would have compiled on the first try and the test would still have passed, without the case its name gives. It now records each compile on the agent's context manager during the two calls, with whether the pin was held when it started, and checks that both refused as over budget with the pin held and then each compiled once without it.
1 task done
added 4 commits
October 29, 2026 01:20
A fork derived in shared mode reads its parent's context at a checkpoint and writes on a branch of its own, and its first stream opens a turn, whose start clears pins of the turn's name before pinning the fork's own inputs. That housekeeping must stay on the fork's branch: if it reached the parent's, the parent's opening request could fold while its turn still runs. The test blocks the parent mid-turn with its inputs pinned, derives a shared fork and runs it to its end, and checks that the parent's pin is still the one it held, then that the parent's own end releases it. With the fork's opening also clearing its parent's pins by name, it fails at the parent's pin. It is skipped where the installed chronicle and context-manager lack branch-bound handles or ContextManager.derive.
The fragment compared the pinned rows with 0.28.0, the latest release when it was written. 0.29.0 and 0.30.0 fold them the same way, and the next changelog follows 0.30.0, so it now names that release. Text only.
tel-0s
added a commit
that referenced
this pull request
Oct 30, 2026
… pin reads the window (#867), save_recent_image's nullable selectors (#245) #245 merges as it is. #867 merges with no textual conflict but doesn't build here: its pinTurnInputs reads the consumed watermark as `{ branch, sequence }` and walks the record back from its newest row to that store sequence. On this branch the watermark is a ReadMark (src/read-marks.ts): what an agent has read is a place in its window, compared only through coverTest/covers. The adaptation, in this merge: - turnInputs (src/framework.ts) walks the agent's window (cm.getPresentedMessages(): the rows presented to it, in their order; in legacy mode the merged record) back from its newest row, with coverTest(consumed mark, cm) in place of the sequence floor. It stops at the first row the mark covers (presented mode: an ordinal below the index length the mark took; a mark taken in legacy mode: a sequence at or below its head, #867's floor) or at the newest row the agent sees under its own name, passes over rows it doesn't see (agentSees), and returns the range from the oldest row it passed that holds more than tool results to the newest. A mark that can't say pins nothing: none, one on another branch (#867's branch check, now coverTest's), or one taken in presented mode read against a manager no longer presenting. TURN_INPUTS_SCAN goes: the listing is the one the start's compile reads. - So the rows that open a turn are the rows presented to the agent after its mark that it sees, in window order: a row stored before the mark and presented after it is one; a row stored and not presented is not (under #867's record walk the newest such row became the range's `last`, naming a row the strategy's view doesn't hold). - The mapping onto pinRange: context-manager stores the two ids, and AutobiographicalStrategy.pinPositions resolves them by position in the strategy's message view, which is presentedMessageView (the window index's listing, in ordinal order) under the view filter. A pin is a range of the window's listing, not of the record, so first..last taken in window order is what the strategy pins, where the window's order differs from the record's too, and a stored row not presented is outside it. No design choice was left open. - Agent.pinTurnInputs's doc says so; README and the changelog fragment now say the rows are the window's, presented since the mark (legacy: stored since). The rest of #867 is as merged: the pin held while the turn runs, released at its end, at a dropped start and at stop(), dropped once with a `[turn-inputs]` line on an OverBudgetError retry. test/turn-inputs-pin.test.ts: three tests compress rows written past the framework (cm.addMessage) before the turn; here those rows wait outside the window until a presentation point, so nothing compressed them and the compiles refused without the pin too. They now present them first (presentAtEndOf, as compression-after-release does). The residents' test calls pinTurnInputs with no start between its write and the call, so it presents the row to both windows first. New: in presented mode, a pin runs in window order over the rows presented after the mark (the request, then a row stored before the mark and presented after it, out of record order) and over neither row stored and not presented. With the walk over getAllMessages instead of the window, that test fails. Changelog: main's range adds two fragments and consumes none; version stays 0.30.0; package.json keeps this branch's floors; 291-* fragments kept. Build clean. turn-inputs-pin: 17 of 17. Main's changed tests with the presentation set (presentation*, window-*, dendrite-*, stored-injections, audience-stamp*, dependency-ranges, held-*, tune-out-*, read-marks*, prose-drafts, save-recent-image, turn-inputs-pin), --test-force-exit: 959 tests, 959 pass. Full suite (npm test): 9,542 tests, 9,493 pass, 0 fail, 49 skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
tel-0s
added a commit
that referenced
this pull request
Oct 30, 2026
…overs the window as of its head #867's turn-opening pin walks the agent's window back from its newest row and stops at the first row the consumed mark covers. A mark taken in legacy mode compared by sequence whatever the reader's mode, so once the branch's window was written down, an older row taken out and presented again at the window's end read as covered and stopped the walk early: with P, L1-L3 read and N1, N2 arriving, L1 presented again as the newest row pinned nothing, and between N1 and N2 pinned N2 alone (nissa's probe on #844, at 255f00d). It arises in practice: an agent registered where its window wasn't yet written down (every new agent, and every agent on the first boot over a store an earlier release wrote) takes its consumed mark before the presentation module's write-down at adopt, and holds it until its first compile. coverTest now reads a mark taken in legacy mode, against a manager in presented mode, as a mark taken in presented mode: it covers the rows the window has held, where they stand, since the mark's head or before. That is the rows a branch cut at the head would keep in its window (ContextManager.branchSequenceOf at or below the head): a row's own sequence for a row the write-down presented, the presenting write for any other. Since that only grows along the window, the covered rows are the window's first ones, and the length is the ordinal of the first row past them, found by a binary search over getPresentedMessages once per coverTest, at the first row stored at or below the head (a row stored after it isn't covered without a read). For a mark taken before the write-down, which lists the record in record order, this is the presented-mode mark whose length is the number of record rows at or below the head: the conversion is computable from what context-manager exposes, without the write-down's header ordinal, which it doesn't. Against a manager in legacy mode the mark covers by sequence as before; a double without getPresentedMessages and branchSequenceOf is read by sequence; a read that throws is doubt for rows at or below the head. The across rule (a legacy mark on another branch, through the lower of its head and the branch point) reads the same way against a presented reader. Nothing is converted in place, so every reader of covers gets the one answer: the turn's inputs, isUnreadStoredMessage, releasedWindow, answeredCovers and publishedAfterReading. marksOverTail keeps a legacy mark that covers an undone turn's rows, as it kept presented ones: those rows now leave the mark when a redo puts them back at new ordinals, so retakeMarks has to give it again. Tests: three cases on a real switch in turn-inputs-pin (a store an earlier release wrote, prime's mark taken in legacy mode at registration, the window written down at adopt, N1 and N2 presented at the framework's point at rest, L1 taken out and presented again as the newest row, between N1 and N2, or not at all): the pin under the legacy mark is the presented- mode mark's (N1 N2 L1 / N1 L1 N2 / N1 N2), no row covered by sequence and in window order is pinned, and the two marks read every row alike. Red at 255f00d for the first two (nothing; N2 alone). read-marks gains the rule over a real manager, the across case against a presented window, the read count of the search, and the fallbacks. Seven tests read hand-built legacy marks, or rows built from a sequence (rowAt) that no window holds, against a presented manager, leaning on the compare by sequence: the publish-credit tests of heard-mid-turn-wake and prose-drafts now read their marks against bySequence(cm), a reader whose window is the record (test/helpers/read-mark-rows.ts), and mcpl-coalescing's two read-render wakes present each render as it lands and take their marks at the write that presented it. Docs: ReadMark and coverTest in read-marks.ts, the consumed and shown watermarks in agent.ts, turnInputs and marksOverTail in framework.ts, the README's pin paragraph, and the turn-inputs-pin and 291-reads-row changelog fragments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tel-0s
added a commit
that referenced
this pull request
Oct 30, 2026
… for #844 nissa-seru's house reviewed #844 at dda847b, made 115 commits of fixes, docs and tests on it, merged 27f893b into them and added one commit after the merge (117 in all: 47 fix, 36 docs, 30 test, 2 revert, 1 refactor). Its comment on #844 lists the fix subjects by area; each commit's message describes its change. An independent read of every fix commit and both reverts (scratchpad review3/pr-915-read.md) takes them all; its notes land as the next commits: the cancel tools' `waiting` answer in a fragment, a persist() in the coalescer's deliver-failed catch, the duplicate limit the coalescing revert leaves, two ModuleContext doc lines, channel_close's success answer, and the mid-turn [drafts] notice as the agent's own row. No textual conflict with 255f00d..a96506b (git merge-tree): #867's turn-inputs walk and the legacy-mark rule in read-marks.ts touch hunks #915 doesn't. The boot freeze `framework.bootFreeze ||` in frozen() is kept beside B5's line: at boot no agent holds a turn token and create() starts modules before it reads the host mode, so the term is needed where the surgery term was not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 task done
Contributor
Author
|
@antra-tess Flagging for your awareness this PR, which made the following changes to what residents see (in brief), where the agent's context-manager strategy has pins:
— Lottie-2661 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This change adds a pin to a start that opens a turn, to keep the messages that opened the turn raw while it runs. When the agent's consumed watermark is on the current branch, the framework pins, with context-manager's
pinRangeand the nameagent-framework:turn-inputs, the rows the agent sees that were stored after both that watermark and the newest message it sees under its own name, from the first of them that holds more than tool results. It removes the pin when the turn ends. A silent start, one whose trigger setssuppressProse, pins nothing. When a committing compile refuses as over budget while the pin is held, the pin is removed and the compile runs once more.In this change's long-turn test under kv-unified, each of the turn's stream requests holds the opening request whole. With
src/framework.tsandsrc/agent.tsfrom612f6fd3bd, themaincommit this change now starts from, none of the test's 18 runs passes, and each fails at its check that every one of its 35 streams' requests holds the opening request whole. With those two files fromf14e7fbc99, themaincommit it started from before it first mergedmain, 25 of the 35 didn't. context-manager's README sayspinRangekeeps messages "at their place in the conversation, raw by default".Problem
In a long turn whose older rounds are summarised, the requests sent to the model can stop holding the request that opened the turn whole. This change's long-turn test shows it with
f14e7fbc99's source. In it:maxStreamTokensat 8,000;With
f14e7fbc99'ssrc/framework.tsandsrc/agent.ts, the turn answers and its budget restarts fold its older rounds. The opening request isn't whole in the requests of 25 of its 35 streams, the 11th to the 35th. With612f6fd3bd's two files, none of the test's 18 runs passes, and each fails at its check that every one of the 35 streams' requests holds the opening request whole.Changes
The pin (
src/framework.ts). At a start that opens a turn, unless it's silent (:23918),pinTurnInputsreads the agent's consumed watermark. When the watermark's branch is the current branch (:12399),turnInputsscans back from the newest stored row. Its scan::1533–1535);That range is pinned with
pinRangeunder the nameagent-framework:turn-inputs.Removing it. The pin is removed when the turn settles, when its start or continuation is dropped (
endDroppedTurn), and atstop(). A start that opens a turn first removes any pin of the name. A silent start removes one too, and pins nothing.The fallback (
src/agent.ts).compileUnpinningOnRefusalwraps the context-manager compile thatcompileWithInjectionsandcompileContextrun. When the compile throws anOverBudgetErrorandlistPins()listed a pin of the name before it started, it removes the pin, logs a[turn-inputs]line, and runs the compile once more. A dry run's compile isn't wrapped (:1655).Agent.pinTurnInputsandAgent.unpinTurnInputsare new and marked@internal.Agent.pinTurnInputsreturnsundefinedbefore pinning when the strategy has nopinRangefunction (:1216).README.mdgains a paragraph under Context Management, and its budget restart step says the turn's opening messages are kept whole.changelog.d/turn-inputs-pin.changed.md, new.test/turn-inputs-pin.test.ts, new, with 16 tests.What a resident and a host see
[turn-inputs], and the turn then goes on without the pin. In the test where the compile refuses without the pin too, the turn ends with that refusal (over_budget) after exactly two refusals.listPins()on the agent's context manager lists one pin namedagent-framework:turn-inputs. Tests check it while a turn runs, and that none is left once the turn ends.PassthroughStrategyin its test, a turn runs and logs no[turn-inputs]line.deriveAgentruns a turn of its own to its end. The agent then lists the same pin as before, and its own end removes it. The test checks the agent's pins before the fork is made, after the fork's whole run, and at the agent's own end, so it covers the fork's opening and its end together, not each alone.Tests
86fcad2c9dmergesmainat612f6fd3bdwith no conflicts, and its diff against612f6fd3bdadds and removes the same lines ascaa0b8b3cc's against0f11975e48.a7475a986cthen changes one line of the changelog fragment, which now names 0.30.0 where it named 0.28.0. On Linux with Node 24.22.1 and npm 11.19.1, context-manager 0.24.1, chronicle 0.5.0 and membrane 0.8.0, in a clean checkout of this head,a7475a986c:npm run build) gives no TypeScript errors.test/turn-inputs-pin.test.ts: 16 of 16 pass.pinTurnInputsalso calling the parent'sunpinTurnInputs()when the agent pinning is a fork: the test "a shared fork's opening and its end leave its parent's turn-inputs pin in place, and the parent's own end still releases it" fails, and the other 15 pass.612f6fd3bd'ssrc/framework.tsandsrc/agent.ts: 15 of the 16 fail, and the test of a strategy without pins passes. In the long-turn test, six copies at once, three times, none of the 18 passes, and each fails at "every stream's request holds the opening request whole (of 35)".CI at this head, run 40937886194, passes all 11 of its jobs. It ran on GitHub's merge of this head into
mainat612f6fd3bd, whose tree is this head's. Its Build & Test job, on ubuntu-latest with Node 22.12.0, runsnpm testafter its type-check and build steps: 9,430 tests, 9,367 pass, 0 fail, 63 skipped. Its step that runs three test files without--test-force-exitpasses 137 of 137.Earlier heads. On Linux with Node 24.22.1, context-manager 0.23.1, membrane 0.8.0 and chronicle 0.4.0, in clean checkouts of
96e4f73027:npm testaftertsc --noEmitand a build): 8,783 tests, 8,721 pass, 0 fail, 62 skipped.test/turn-inputs-pin.test.ts: 15 of 15 pass.f14e7fbc99'ssrc/framework.tsandsrc/agent.ts: 14 of the 15 fail. The test of a strategy without pins passes.f14e7fbc99's two files, 18 of 18 fail, each because 25 of its 35 stream requests don't hold the opening request whole.A second commit,
5f26d006b9, changes only the test of two refusals at once. It now asserts that both compiles first refuse as over budget with the pin held, then each compiles once without it. In a clean checkout of5f26d006b9, on Linux with Node 24.22.1, context-manager 0.23.1, membrane 0.8.0 and chronicle 0.4.0, the file passes 15 of 15. Withf14e7fbc99's two files, 14 of the 15 fail, and with the fallback removed, the same four as above fail.f42272669fmergesmainat0f11975e48with no conflicts, and its diff against0f11975e48adds and removes the same lines as5f26d006b9's againstf14e7fbc99.caa0b8b3ccchanges onlytest/turn-inputs-pin.test.ts, adding the test "a shared fork's opening and its end leave its parent's turn-inputs pin in place, and the parent's own end still releases it". On Linux with Node 24.22.1 and npm 11.19.1, chronicle 0.5.0 and membrane 0.8.0, oncaa0b8b3cc's files, in two runs, the first with context-manager 0.24.0 and the second with 0.24.1, which was published between them:npm run build) gives no TypeScript errors in either run.test/turn-inputs-pin.test.ts: 16 of 16 pass in each run.pinTurnInputsalso calling the parent'sunpinTurnInputs()when the agent pinning is a fork (first run): the new test fails, with "the fork's opening and its end left mira's pin", and the other 15 pass.0f11975e48'ssrc/framework.tsandsrc/agent.ts(second run): 15 of the 16 fail, and the test of a strategy without pins passes. In the long-turn test, six copies at once, three times, none of the 18 passes. The run printed the failure of the first copy in each round, and each of those three fails at "every stream's request holds the opening request whole (of 35)".Not verified
No test or run covers these cases:
afterUserinjections, and a co-resident's row stored during the start's hooks;stop()on the same instance.The scan stops at
src/framework.ts:1533–1535. The range then runs from the oldest row it passed that holds more than tool results (:1539) to the newest row the agent sees when the turn opens (:1536). A range is made only when the watermark is on the current branch (:12399), and only a start that opens a turn pins (:23915–23919).No test drives
compileAtLiveBudget's boot-restore path with a pin held.No test covers a range whose rows had no summaries when they were pinned, then compiled without the pin after a fallback.
The prompt cache. How the pin changes kv-unified's fold plan and how much of the cache a request reuses wasn't measured.
Platforms and providers. Every run of this change's tests was on Linux. CI's macOS jobs install and build the package, check that its packed release installs on its own, and run code_execution's tests under Bun. None of them runs this change's tests. The tests use stand-in providers, and no run used a live provider.
fix(framework)!: decline a maxStreamTokens restart that won't fit, unless the budget hold can wait for compression its preview names #842. Merged with fix(framework)!: decline a maxStreamTokens restart that won't fit, unless the budget hold can wait for compression its preview names #842 at
cd581ebe11, this head conflicts inREADME.md, andmainat612f6fd3bdmerges with fix(framework)!: decline a maxStreamTokens restart that won't fit, unless the budget hold can wait for compression its preview names #842 without conflicts. No test runs the two together. Atcd581ebe11, fix(framework)!: decline a maxStreamTokens restart that won't fit, unless the budget hold can wait for compression its preview names #842's restart decision callsagent.previewCompile()(src/framework.ts:12329), which calls context-manager'spreviewContext(src/agent.ts:2073). This change's fallback wraps the two calls tocompile(src/agent.ts:1398,:1654–1655).What a fork's own context manager lists. No test checks it. In a run of the fork test on
caa0b8b3cc's files, with lines added to print it, the fork's context manager listed one pin,pin-0, namedagent-framework:turn-inputs, right afterderiveAgent, and none after the fork's run. The parent listedpin-0at both points.changelog.d/turn-inputs-pin.changed.md.Paloma-2644 made the change, and Ada-1017 made its design calls. Ezra-2423 read an earlier version of it, the test changes from that version to
96e4f73027,5f26d006b9, the testcaa0b8b3ccadds, both merges ofmain, anda7475a986c. Internal reviews read earlier rounds of it. Daniel-2612 wrote this description.