Repository navigation
fix(render): renderAsNewest gives null only for a message the next compile doesn't send - #314
Conversation
…mpile doesn't send The memory strategy rendered only from the recent window's start, or from 0 for a head window with nothing after it, and gave null for any id before that. The compile still sends those messages raw: a batch bigger than the recent window went out whole, in order, as the compile's last messages, while the render gave null for its older part (anima-research#275's review, R1-1). So did every message of a head window a reset hasn't filled, and a head message with a middle after it. - The render runs from the oldest given id's body to the view's newest message, whatever region holds it, with the steps it already took: cappedForSend, the body merge under adaptive resolution, then applyImageStripping. Both selects send raw, through cappedForSend, every message the view holds that no summary covers, and the image pass decides by stored position, so the same steps give the form wherever the compile places the message. - null now means only that the next compile doesn't send the message: the view leaves it out, or windowed passthrough's anchor is past it. - The docs name what still differs: a message a summary covers comes back raw; a sharded body the adaptive middle sends comes back as the windows join it, where this version's middle sends its shards' text alone, uncut; and the form isn't the place, as after a head-window reset that hasn't filled. The interface doc adds that a batch's images can strip older images a request already carried (R1-2), and the manager's JSDoc names the tool exception, including splitMixedToolMessages (R1-3). - ContextManager.renderAsNewest no longer releases timed-out compression holds: they moved the recent window's start, which the render no longer reads, so the release changed nothing it gives. - Tests (R1-5): the review's repro under the hierarchical, adaptive and kv-unified selects; eight screenshots under maxLiveImages 6; a message past the cap before the window under both selects; count and hysteresis on the adaptive select; a trailing tool_use. Five tests that pinned null are turned round, and so is the hold test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the render no longer starting at the recent window, nothing that decides what it gives reads the chunk table: cappedForSend, tool pruning, the body merge, the image depth and the hysteresis replay read the stored messages. The image pass still computes the window bounds the chunks move, but decides nothing by them. The whole suite passes with the rebuild taken out, so it goes, and the doc says why. The calibration load stays: the store's estimates it sets measure the image depth, which the calibration test pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pin the hold and the refusal From the review of the render's new contract: - The manager's doc points to the strategy's own doc for where its compile still sends a message otherwise than its render. The memory strategy's doc says a summary-covered message, rendered raw, also counts toward older messages' image limits, and names every body the compile doesn't send together: the adaptive middle's run, kv-unified's included, sends a body's shards as text alone, uncut, and a body across the middle and a window, or across a reset head window's end with the history from before the reset between, goes out in more than one entry. Its sentence on cappedForSend excepts the middle's run. - The tool exception: the hierarchical select leaves out a newest message holding a tool_use with no result after it, while the adaptive select refuses that compile; both drop an unpaired tool_result. The manager doc and the fragment say so. - Tests: the adaptive select's refusal of a trailing tool_use; a body across a reset head's end, whole in the render and in pieces around the history in the compile; and a render past a hold's deadline releasing no hold while the next compile does, counted through onCompressionHoldsReleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge. The review found nothing of medium or high severity.
Reviewed commit 2e4aba3 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This PR fits. It fixes the existing renderAsNewest hook, which returned null for messages the next compile still sends, so the hook now does what it was meant to do.
5 reasons with sources
The fix stays inside the strategy, manager and interface docs that already own that behavior, and it comes with tests and a changelog fragment.
- It fixes behavior the project already has and adds no new feature. The base doc says the hook exists so a caller appending just-stored messages gets 'what the next compile sends'. Returning null for messages the compile still sends raw went against that. The diff removes the recent-window start cutoff so those messages come back rendered. (src/types/strategy.ts (ContextStrategy.renderAsNewest doc, base))
- The changes stay in the parts that already own this behavior. The rendering logic stays in AutobiographicalStrategy.renderAsNewest and reuses the steps it already took (cappedForSend, mergeAdjacentBodyGroupRaw, applyImageStripping). The manager wrapper only changes its JSDoc and drops a hold release. There is no new dependency or public API, and there is no second way to do something the project already does. (src/strategies/autobiographical.ts, src/context-manager.ts)
- It comes with tests that would fail on the old code, plus a changelog fragment, as the contributing guide asks. Several tests that used to assert null now assert the rendered form. The new tests rerun the review's repro under all three selects. The fragment
changelog.d/fix-render-as-newest-null-not-sent.fixed.mdfollows the slug.category naming. The separate code review notes one gap: no test covers the summary-covered case the doc names. (CONTRIBUTING.md (What a PR should contain; Changelog)) - It is one coherent change that answers review findings on #275. Removing the hold release and the chunk rebuild follows from the new render no longer depending on where the recent window starts. The doc edits for R1-2 and R1-3 describe limits of the same hook. Nothing unrelated is bundled in. (CONTRIBUTING.md (Review process))
- The PR mentions agent-framework#720 as related work on the caller side, but this change does not depend on it. This package is consumed by agent-framework, so PRs in both repos are normal. The description that is visible gives no merge-order note for #720. Because this change only makes the render's output more accurate, either side looks safe to land first. (CONTRIBUTING.md (How changes land: companion PRs))
Findings
No findings of medium or high severity.
1 more finding, less severe
- Low severity, test coverage,
test/render-as-newest.test.ts:448-476. No test covers the summary-covered case the doc names. The new doc and the changelog name a message a summary covers as one that comes back raw while the compile sends its summary. They also say its images count toward older messages' limits. No test compresses a chunk and then renders an id inside it. So nothing pins this behavior or the image-count side effect, and a later change to either would go unnoticed.
A message a summary covers comes back raw, and its images count toward older messages' limits, where the compile sends its summary and counts none of them. The test folds an image message by compression and checks both: the render gives that message raw, and strips the head message's image for it, while the compile sends the summary and keeps the head image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manager's doc named the memory strategy's handling of a trailing tool_use, though it also points to the strategy's own doc for where a compile sends otherwise. The two clauses (the hierarchical select leaves the message out, the adaptive select refuses the compile) and the dropped unpaired tool_result move to the memory strategy's render doc, beside the sentence that already says the render leaves out the tool pass. The manager's doc keeps what is the manager's: tool blocks as the cap leaves them, and splitMixedToolMessages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Replying to this review. The low finding is fixed in
Written by Milo-2653 (Claude Opus 5.5). — Daniel-2612 |
|
@Anarchid @antra-tess We're asking you to review this pull request at Written by Milo-2653 (Claude Opus 5.5). — Daniel-2612 |
main gained the 0.21.0 release and anima-research#331, the thinking binding, whose renderAsNewest docs say a render comes before the manager leaves out any thinking under SelectOptions.thinkingBinding. Two doc conflicts, each resolved by keeping this change's account of the render with anima-research#331's clause: - src/context-manager.ts, the manager's renderAsNewest doc: "in the form the compile sends each, wherever it places it", then anima-research#331's "before any thinking the thinking binding leaves out". - src/types/strategy.ts, ContextStrategy.renderAsNewest's doc: this change's paragraph, with "before the manager leaves out any thinking under SelectOptions.thinkingBinding" after "in the order given". No code conflicted. The manager's renderAsNewest still releases no compression hold, as this change made it.
ContextStrategy.renderAsNewest and the manager's renderAsNewest framed the render as for messages just stored, which a caller appends to a compiled request. ContextManager.noteRequestSent, from the thinking binding, also renders every row a stream has sent over a whole turn, to digest each as the next compile would send it. Both docs now name both callers; the form and null sentences are as they were. noteRequestSent's doc said a note releases compression holds that have timed out, "as renderAsNewest does". Since this change the render releases none, so neither does a note; the doc now says so. The fixed fragment adds a line for whom the render is for.
…dow keeps its thinking Under the memory strategy, a turn whose rows pass recentWindowTokens: three rounds of a reply calling a tool and a long result, then the last reply, each round noted. The next compile has room for everything and no summaries, so it sends every row raw. Each reply keeps its thinking, under the hierarchical and the adaptive select. Before this change the render gave null for the rows before the window, each note recorded them as unvouched, and the next compile left out the thinking of every reply from the first.
noteRequestSent adds nothing to the request it notes; it renders rows already sent so the next compile can tell which it sends as they were sent. The contract's sentence now frames both callers as working from a compiled request, not adding to one.
|
@Anarchid @antra-tess The head for review is now
The description's Problem, Changes, Tests and Merge notes are updated for this head. Written by Milo-2653 (Claude Opus 5.5). — Daniel-2612 |
One conflict, in AutobiographicalStrategy.renderAsNewest's doc: main's side carries anima-research#268's correction that the tool pass left out of the render only pairs by the window, and what sending prunes from a message's tool blocks reaches the render through cappedForSend. This branch's side says the render covers a message the compile sends raw, and names the trailing tool_use and the unanswered tool_result the pairing leaves out. The resolution keeps both: the pass pairs, the pruning comes through cappedForSend, and the two pairing limits follow. Read beside the conflict: the render's steps (cappedForSend, mergeAdjacentBodyGroupRaw, applyImageStripping) are main's own. Its applyImageStripping call passes no `returned`, so the dry render notes no ceiling and says no line, as main's own test pins. A message of system changes is kept raw as a pin keeps a message (resolvedPinBounds), and the render gives every message raw. Compression input's dropping of system changes, the head's sending of earlier changes ahead of a reset head, and the observer's compile reports don't reach the render, which renders the newest messages from the batch's oldest on.
…a reset head's system changes are sent ahead too The render's doc said the tool pass pairs, then that pruning reaches the render through cappedForSend, then "So" the pairing's two consequences. The consequences now follow the pairing, and the pruning sentence comes after them. After a head-window reset, headPositions sends each system change stored before the head with the head, ahead of the history from before the reset. The doc and the fragment named the head's place but not theirs; they now name both, as places the render can't show.
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R1 at 08b0c2c · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.
Reviewed at 08b0c2c against main 1fe572e (the merge base). main has since moved to 1a9e1b7. This change merges into it cleanly, and the checks below were run at the head and on that merge.
Verdict: approve. The fix takes the second option from our review of #275, and that was the right choice. A message just stored is not covered by any summary, so both selects send it raw through cappedForSend. The image pass decides by stored position, so rendering from the batch's oldest body to the newest message gives the form the compile sends. The contract change is safe for callers: the only line removed from the render is the start check, so every null it gives now, it also gave before, and callers get content where they used to get null. The two findings are a doc sentence that claims a divergence for every select when it holds only for one, and a merge-order note that is no longer true now that #310 merges without conflicts.
Our review of #275, which this PR answers
- #275 R1-1: fixed. The render starts at the oldest given body (
src/strategies/autobiographical.ts:20082-20089). The review's repro is a test under the hierarchical, adaptive and kv-unified selects, and the kv-unified case really runs kv-unified (isKvUnifiedEnabled()is true, and its marker reconciliation runs at the compile). All three pass at the head and fail with the base's source. - #275 R1-2: fixed.
src/types/strategy.tsnow says a batch's images can strip older images the request already carried. - #275 R1-3: fixed. The manager's JSDoc, the strategy's doc and the fragment all name the tool exception. Tests pin a trailing
tool_useunder both selects. - #275 R1-4: not this PR's, and resolved. #268 has landed (f387896), and
mainbuilds. - #275 R1-5: fixed. The new cases use small recent windows. The count and hysteresis cases run on the adaptive select, and a trailing
tool_usecase is included.
Minor
R1-1. The summary-covered image exception is described for every select, but it holds only for the hierarchical select without imageStripHysteresisRatio. (found by both reviewers)
src/strategies/autobiographical.ts:20042-20045 says that a covered message, rendered raw, has images that "count toward the image limits of the messages older than it, where the compile, sending its summary, doesn't count them." The adaptive select strips with asEstimated: true (:14069), and with the hysteresis option every path takes the same verdicts (:17677-17719). Those verdicts come from postStripEstimatesHeld, which counts every stored message's images newest-first, whether or not the plan folds the message (:17995-18030). That is the store-wide rule applyImageStripping's own doc describes. I reran the PR's own setup: a head image, then an image message that compression folds, under maxLiveImages: 1.
| Select | Compile keeps the head image | Render keeps the head image |
|---|---|---|
| Hierarchical | yes (1) | no (0) |
Hierarchical with imageStripHysteresisRatio: 0.5 |
no (0) | no (0) |
| Adaptive, budgets 6,000 down to 1,800 (the plan sends the summary) | no (0) | no (0) |
Only the hierarchical row diverges, as the doc says. Under the adaptive select, which the PR's reach section names as the one that matters, the render and the compile agree.
Fix: scope the sentence ("under the hierarchical select without imageStripHysteresisRatio, ..."), and the matching bullet in the description's Changes section. Optionally, add the adaptive case to the summary-covered test as an equality.
R1-2. #310 and this change now merge without conflicts, and the result fails three of this file's tests. (coordination)
The merge notes say #310 "conflicts with main in four regions, and in four with this change". At #310's current head, b1ad54d, git merge-tree finds no conflicts, either with main 1a9e1b7 or with this change merged into it. That merge typechecks, and render-as-newest then passes 60 of 63. Three assertions fail:
- "a body too big for the window's recent part ...": "the compile sends the body in 1 pieces"
- "a body that starts in the head window ...": "... in 1 pieces"
- "a compression hold, held or timed out, ...": "released, the compile sends the body in 1 pieces"
These are the three tests your merge notes predict. But no conflict will now stop whoever lands second, and a green check run before the first one lands is stale. This is the same trap as #275's R1-4.
Fix: update the merge note to say the two now merge cleanly and the second to land must rerun CI on the new main (or turn those three assertions to compare against the compile's one entry). Consider saying the same on #310.
Nit
R1-3. The description's merge notes are stale against main. (Sol found the observer part, Claude the heads)
The notes say the merge at 8ef2f4e brings "#335's observer", and that "the observer's reports are off the render's path". main has since reverted #335 (#340, 1b557de). The cited heads of #310 (7236560) and #315 (c6592c3) are now b1ad54d and 4c6a313. #315 still conflicts with this change in src/strategies/autobiographical.ts and test/render-as-newest.test.ts, as described. Refresh the notes before merge.
Coordination: agent-framework's null branch
agent-framework 0.27.0 (#720) injects an id the render gives null "as stored". Its comment still describes null as "a message the render places before its window". Once this lands, the only nulls left are messages the next compile doesn't send at all:
- one the view leaves out;
- an earlier-flush write that windowed passthrough's anchor has passed.
For both, leaving the message out is the right action. This is agent-framework's follow-up, as your "For hosts" says, and it is not a reason to call this change breaking. No caller sees a new null. Callers on 0.24.1 to 0.26.x, which leave out a null, get more right, and 0.27.0 gets the compiled form where it used to inject the stored one. The .fixed category is fine.
Verified fine
-
Build and suite. At the head,
tscis clean. Merged withmain1a9e1b7,tscis clean and the whole suite passes 3,149 of 3,149 (Node 22.22.1). At both,render-as-newest(63),thinking-binding(61) and seven neighbouring files pass 421 of 421:request-image-ceilings,system-changes,head-window-reset,live-image-accounting,image-strip-hysteresis,kv-unified-render-stateandstructural-render-order. CI is green at 08b0c2c. -
Red-first. This branch's tests run with the base's
src/give 16 failures: 15 inrender-as-newestand the newthinking-bindingtest. That matches the description. -
The set of
nulls only shrinks. The base's render skippedbody.from < start. That line is the only skip removed, so a message the view leaves out still comes backnull, andrendered.get(first)always finds the first shard, since the merge keeps its id. -
Raw paths. The hierarchical select's head, middle raw (pins, uncompressed chunks, the frontier) and tail all go through
cappedForSend(:16174,:16594,:16695,emitRecentNewestFirst). The adaptive and kv-unified paths do too (:13314-13328,:13972-14000) before the body merge and image pass. The image pass orders entries by stored position (:17656-17662). -
Hold release removed. Timed-out holds are still released by
addMessage, the hold queries,compileandtick(src/context-manager.ts:582,:861,:870,:1214,:2190). The render's path decides nothing by holds: the hierarchical strip computes the window's start, but only into the unusedbucket. agent-framework's one hold, the tool-result guard's, has no timeout. -
Chunk rebuild removed. None of the helpers the render calls read
this.chunks:mergeAdjacentBodyGroupRaw,getImageStripStart,truncateContent,getRecentWindowStart, the head bounds,resolvedImageLimitsandloadCalibration. The dry state is saved and restored as before. -
Thinking binding. agent-framework's
noteStreamRowscompares each rendered row with what the stream sent before vouching for it (roundRowsinsrc/sent-rows.ts). So more rendered rows can't make it vouch for a row the stream sent in another form. The manager's record is checked against the next compile's own messages, so a render that differs from the compile only costs thinking, never a refusal. -
Cost (the description's "not verified"), on a 3,200-message store, hierarchical and adaptive alike:
Operation Time Render 1 id 0.1 ms Render 50 ids 0.4–0.6 ms Render 200 ids 1.2 ms Render from the store's oldest message 6 ms Compile 9.6–11.8 ms agent-framework's note path renders a turn's rows twice per round (its own render, then the one in
noteRequestSent), which stays in milliseconds.
Considered and not raised (second reviewer's):
- Repeated rendering over long turns. It grows with the turn, but the measurements above put it in milliseconds per round.
- "The form and
nullsentences are unchanged." Sol read this sentence as contradicting the redefinednull. It is about the callers-doc commits, which leave this PR's own sentences as they were.
Method: two reviewers. Codex gpt-5.6-sol (xhigh, read-only sandbox, no network) reviewed the diff against the merge base and current main in 10 min 55 s, with 114 shell commands, 5.99M input tokens (5.66M cached) and 29.3k output tokens. It couldn't run the suite in its sandbox. Claude (Opus 5.5) ran everything else: the builds and suites at the head and on the merge with current main, the red-first run, the trial merge with #310 and the #315 conflict check, and probes against the built package (the summary-covered images under three selects, kv-unified engagement, the cost measurement). It also read agent-framework 0.27.0's two callers. R1-1 was found by both reviewers independently. R1-2 is Claude's only. R1-3: Sol found the observer part, Claude the heads. Both read agent-framework's null branch. Sol rated it major and asked for a breaking fragment, and it is kept here as a coordination note because the set of nulls only shrinks. Sol's cost finding was not raised, after measurement.
…chical select (R1-1) The render's doc said a message a summary covers, rendered raw, has its images count toward older messages' image limits where the compile, sending its summary, doesn't count them. That holds only for the hierarchical select without imageStripHysteresisRatio. The adaptive select strips with asEstimated, and every path under the hysteresis option takes the rule's verdicts; both count every stored message's images newest-first, folded or not, as the render does. The doc now says so. A test pins the adaptive case as an equality: with the image message folded into a summary, the render and the compile keep the same images of the head message. The hierarchical test's title names its select.
|
Replying to this review. @Anarchid At
Thank you for measuring the render's cost. — Milo-2653 |
33 commits from main, #314 (renderAsNewest's null is not sent) and the revert of #335's context observer among them. One conflict where open() finishes a manager: this branch's slots record stands and the observer block goes with main's revert. Suite 2809 / 500 pass, 0 fail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…inth catch-up This brings in 33 commits from context-manager main through the -main branch. They include #314 (renderAsNewest gives null only for a message the next compile doesn't send, and it no longer releases a timed-out hold), #342 (merge runs, settled summaries and the topology repair), #347 (membrane ^0.8.0), the 0.22.1 and 0.23.0 releases, and the revert of #335's context observer. There were four conflict hunks in three files. package.json: both sides changed the dependencies, so the result keeps both. It has this branch's @animalabs/chronicle ^0.5.0 and main's @animalabs/membrane ^0.8.0. package-lock.json merged without a conflict and is main's lock with membrane 0.8. It still resolves chronicle 0.4.0, as on this branch before the merge, so npm ci still waits for chronicle 0.5.0 to be published. src/context-manager.ts, the renderAsNewest doc: main's #314 text, which says a null means the next compile doesn't send the message and that the render releases no hold. This branch adds one clause to it: a row the agent hasn't been presented yet also comes back null, so present a row before rendering it. The code took main's removal of expireCompressionHolds() without a conflict. src/context-manager.ts, the noteRequestSent doc: main's sentence that a render releases no hold, plus this branch's clause that, once a host presents rows, a row the agent's window doesn't hold matches nothing. src/strategies/autobiographical.ts, the private fields: both sides added one, so the result keeps both. They are this branch's _spanLeftWindow (onRetracted) and main's mergePicksDeferred (#342). The observer revert merged without a conflict and is main's. observer.ts, its test and fixture, ContextManagerConfig.observer, StrategyContext.observer, the compile wrapper, OVER_BUDGET_STAGES, the observer exports and the README section all go. open() keeps the slots record as the -main branch has it. Every presentation API stays as it was: the window index and its batched write-down, presentRows, retractRows, retractRemoved, retractAbsentRows, windowStatusOf, branchSequenceOf, ranged getUnpresentedMessages, resetHeadWindow, onPresented, onRetracted, and reinitializeOnStoreBranch. No hunk was either/or. Suite: npm run build is clean. node --test dist/test/*.test.js gives 2963/2963 pass, and node --test dist/test/adaptive/*.test.js gives 500/500 pass, against the linked chronicle at its own ninth catch-up (ad1aaea), which has the batched append. An earlier run, made while that tree was in the middle of its merge with a binary that had no appendManyToStateJsonWithIdentity, skipped the 10 batch-only tests by name, with nothing failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…count the floor's turn as the window does R2-1: main's renderAsNewest tests (anima-research#314) pinned the adaptive middle sending a sharded body in pieces, which this change ends. The three tests now pin the body sent in one entry, as renderAsNewest gives it, and its doc and anima-research#314's changelog fragment name only bodies sent in pieces across a reset head window's end. R2-2: the head window's end stops outside a body that leaves a call waiting for a later result. The loop that stepped it around call runs now also moves a boundary inside such a body to the body's start, and repeats the two steps until neither moves; main's one-message guard runs first, unchanged, and with the step that keeps a call with its result it takes a boundary just after such a body inside it. With blocks in place a reply's call sits on its last shard, so after a head-window reset the head's end could land inside a long reply that thinks and makes a call, and the move to where chunks resume (anima-research#122) then sent its thinking and opening before the history from before the reset and its call after it. A body that answers its own calls gets no such step. Refusing the move to where chunks resume inside a reply, or stepping back after it, kept the reply whole in a probe but left its first shards raw, and the refusal's were chunked out of order once more messages came. The tokens the move counts against its cap no longer count twice the shards the loop stepped back over, so a step back over a long reply doesn't make it give up sooner; the loop reads one call-run pass from the head's start. R2-3: turnStart is where the recent window starts at a boundary, and the smaller-window floor now counts the newest turn from it too. In a chain the floor counted from the call alone and named a smaller window that no non-empty window gives. Tests: - body-blocks-in-place: after a head-window reset, at heads of 150 and 300 tokens, a reply of 12 or more shards that thinks and makes a call goes out in one entry, its blocks in order and its text whole, after the history from before the reset, and a chunk owns its first shard; a reply making three calls answered one to a message goes out in one entry, its blocks in order and its text whole, after the history; a reply that opens with text, with the head's budget ending between its parallel results, goes out whole after the history; where the head's budget reaches past a long body that answers an earlier call and makes and answers its own, the head ends after it; where the head's end steps back to a reply's start, the move to where chunks resume reaches the chunk after its result, and gives up past twice the budget. - render-as-newest: the three tests that pinned a body sent in pieces now pin it sent in one entry, as the render gives it. - overbudget-diagnostics: a chain whose message with a long result the recent window keeps names no smaller window, at windows of 100,000, 20 and 5 tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.23.1 folded anima-research#314's changelog fragment into CHANGELOG.md and deleted it, and this branch had rewritten that fragment's sentence on where a sharded body goes out in pieces. Its correction of the released entry is now a fragment of its own, 829-render-as-newest-entry.fixed.md. In src/types/strategy.ts, main's new recoverLiteralThinkingWrappedCompression option and this branch's maxMessageTokens doc both stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
renderAsNewestgavenullfor messages its next compile sends. It now givesnullonly for a message the compile doesn't send at all, and every other message in the form the compile sends it, wherever the compile places it. This answers R1-1 of @Anarchid's review of #275, and with it R1-2, R1-3 and R1-5.Problem
The memory strategy rendered only from the recent window's start (
max(getRecentWindowStart, headEnd), or 0 for a head window with nothing after it). Any id whose message starts before that came backnull. The interface doc definednullas "the next compile doesn't send that message as one of its newest", and a caller was meant to leave such a message out.But the compile sends those messages. The review's repro, rerun on
mainbefore this change: six messages of history, a compile, then four screenshots underrecentWindowTokens: 4000. The render gave[null, null, content, content], and the next compile's last four messages were all four screenshots, raw, in order. That held under the hierarchical and the adaptive select alike, by our run.agent-framework's tool boundary followed the contract: from 0.24.1 through 0.26.0 it leaves such a message out of the live round and stores a
[not shown]line in its place, though the next request sends the message after the tool result, by our run. anima-research/agent-framework#720 makes the boundary inject anullmessage as stored. That stops the agent missing it, but as stored still differs from the compile where the cap or an image limit reaches the message: eight screenshots, six of themnull, went live with 8 images while the next request carried 6, by our run. Only this package can give those messages their form.The same
nullalso came back for every message of a head window that a reset hasn't filled (connectome-host's/newtopic), and for a head message with a middle after it, though the compile sends both raw.On
main, #331'sContextManager.noteRequestSentdigests every row a stream has sent through the same render. So a turn whose rows pass the recent window had those rows recorded as unvouched, and the next compile left out the thinking of every reply from the first, though it sent the rows raw as the stream had. A test below pins that case.Changes
AutobiographicalStrategy.renderAsNewestrenders from the oldest given id's body to the view's newest message, whatever region holds it. Its steps are the ones it already took: each message cut bycappedForSend, a body's shards joined bymergeAdjacentBodyGroupRawunder adaptive resolution, thenapplyImageStripping. Both selects send raw every message the view holds that no summary covers: in the head window, in the middle (the history before a reset head included) and in the recent window. Each goes throughcappedForSend, apart from a body's shards in the adaptive middle (below). The image pass decides by stored position, newest first, not by where a region puts an entry. So the same steps give a message's form wherever the compile places it.nullmeans only "not sent": the view leaves the message out, or windowed passthrough's anchor is past it (unchanged there).imageStripHysteresisRatio, its images then count toward the image limits of older messages, where the compile, sending its summary, doesn't count them. The adaptive select, and every path underimageStripHysteresisRatio, count every stored message's images, folded or not, as the render does. A just-stored batch has none.tool_usewith no result after it, and its adaptive select refuses that compile. Both drop atool_resultwhose call isn't sent before it, and the manager splits a message other than a user's that holds atool_resultinto several turns.ContextManager.renderAsNewestno longer releases timed-out compression holds. feat(strategy): renderAsNewest, what the next compile sends for messages just stored #275 released them so the render read the holds the next compile reads, since holds move the recent window's start. The render no longer starts there. The image pass still computes that start, but decides nothing by it, so the release changed nothing the render gives. The next add, compile, tick or hold query releases them, as before.noteRequestSentdoes over a whole turn's rows, so the next compile can tell which it sends as they were sent. The form andnullsentences are unchanged.noteRequestSent's doc no longer says a note releases timed-out compression holds, since the render it calls releases none now.What each part is for
nullfor "not sent" only: a caller has one meaning to act on. The review's other suggestion, a tagged "sent, not predicted" answer, would send a caller back to the stored form, which that eight-screenshot run shows differs under the image limits.mainnow meetsnoteRequestSentrendering rows a stream has already sent, and the contract says that's a use it serves.sentHoldingfinds the compile's message for a label, for the cases where the compile doesn't send it last.KV_UNIFIEDand the select table run the review's repro under the hierarchical, adaptive and kv-unified selects, and kv-unified needs a complete configuration to compile at all.Tests
test/render-as-newest.test.ts:recentWindowTokens: 4000, each rendered as the next compile's last four.maxLiveImages: 6, some before the recent window: the render strips the two oldest, as the compile does.tool_use, pinning R1-3's exception: the render gives it, while the hierarchical select leaves it out and the adaptive select refuses the compile.maxLiveImages1, so an older head message's image is stripped. The compile sends the summary instead and keeps the head image.null:test/thinking-binding.test.ts:Since
08b0c2ca,a341812eanswers R1-1 of the review of08b0c2ca: the summary-covered image sentence now says it holds only under the hierarchical select withoutimageStripHysteresisRatio. A new test pins the adaptive case as an equality: with the image message folded into a summary, the render and the compile keep the same images of the head message. The hierarchical test's title now names its select. Insrcthe commit changes comments only: TypeScript 5.9.3's printer, with comments removed, printssrc/strategies/autobiographical.tsthe same at both commits, and prints it differently across this change's merge ofmain, as a control.a341812e, from a clean tree, on Linux with Node 24.22.1: the typecheck and build are clean, andrender-as-newestpasses 64 of 64,system-changes62 of 62,thinking-binding61 of 61 andrequest-image-ceilings81 of 81. With the render's image pass taken out, both summary-covered tests fail, the new adaptive one among them.Runs at
8ef2f4ec, this change merged withmain1fe572ee, from a clean tree, on Linux with Node 24.22.1. The head after it,08b0c2ca, changes comments and the fragment only.render-as-newestpasses 63 of 63,thinking-binding61 of 61,request-image-ceilings81 of 81 andsystem-changes62 of 62. Five neighbouring files pass too:structural-render-order,kv-unified-render-state,live-image-accounting,image-strip-hysteresisandhead-window-reset.08b0c2caand three times withmain1fe572ee's source, at a load average of 93 to 96, it passed all six. This change doesn't touch the picker.main1fe572ee's source: 15 of the 63 inrender-as-newestfail, and so does the new thinking-binding test.render-as-newestand the new thinking-binding test. Putting the manager's hold release back fails 1, the test that a render releases no hold.08b0c2ca: the typecheck and build are clean, andrender-as-newestandsystem-changespass, 63 of 63 and 62 of 62.08b0c2ca: green, 6 of 6 checks: Build & Test on macOS (Node 22.12.0 and latest) and Ubuntu (Node 22.12.0, 24 and latest), and the changelog entry. Run 40168777912 checked out3c224d2, GitHub's merge of08b0c2caintomain1fe572ee, and its Ubuntu Node 24 leg ran 3,166 tests, all passing, the stress test among them.Runs at
7dcf65c9, from a clean tree, on Linux with Node 24.22.1. The head after it,c9604746, changes four lines of one doc comment.render-as-newestpasses 63 of 63 andthinking-binding61 of 61. Five neighbouring files pass too:structural-render-order,kv-unified-render-state,live-image-accounting,image-strip-hysteresisandhead-window-reset. The whole suite runs 2,918 tests, and all pass.main4fbe6c94's source: 15 of the 63 inrender-as-newestfail, as before, and so does the new thinking-binding test.render-as-newestand the new thinking-binding test. Putting the manager's hold release back fails 1, the test that a render releases no hold.c9604746: green, 6 of 6 checks: Build & Test on macOS (Node 22.12.0 and latest) and Ubuntu (Node 22.12.0, 24 and latest), and the changelog entry. Run 40059032878 checked out99b825e, GitHub's merge ofc9604746intomain4fbe6c94, and each leg ran 2,918 tests, all passing.At
fc728aae, this change merged withmain4fbe6c94before the two commits above: the same checks passed, with the whole suite at 2,917, and CI was green, 6 of 6, in run 40057544537.Runs at
e094ac59, before that merge, on Linux with Node 24.22.0:render-as-newestpasses 63 of 63. Five neighbouring files pass too:structural-render-order,kv-unified-render-state,live-image-accounting,image-strip-hysteresisandhead-window-reset. The whole suite runs 2,778 tests, and all pass.main's source and this branch's tests, 15 of the 63 fail.04bb8834, which changed only its message, the whole suite passed, and that's why the rebuild goes.2e4aba31: one test, and a doc change that moves the memory strategy's tool behaviour from the manager's doc to the strategy's.git diff 2e4aba31 e094ac59 -- srcchanges comment lines only.maxLiveImages6. On 0.20.0, the live round got the newest two and a[not shown]line, while the next request carried all of them, then the line. With this change, the live round got every message, in the form the next request carries it right after the call and its result. A short message after a head-window reset also reached the live round, where the next request sends the head ahead of the history from before the reset, as Out of scope says.e094ac59: green, 6 of 6 checks: Build & Test on macOS (Node 22.12.0 and latest) and Ubuntu (Node 22.12.0, 24 and latest), and the changelog entry.Not verified
nullonly for a message the view leaves out, and the boundary skips a tune-out-stamped message before it renders. The end-to-end run had no filter.Out of scope
nullthere means "not sent". What the boundary does with one then is agent-framework's to decide.For hosts
renderAsNewest: an id that came backnullbecause it sat before the recent window, or in a head window followed by other messages, now comes back rendered. agent-framework's tool boundary is the one caller we know of. From 0.24.1 it left such messages out of the live round, and with fix(injection): a message the render gives null goes in as stored agent-framework#720 it injects them as stored. With this release it gets their compiled form.nullwas left out in 0.24.1 to 0.26.0, and goes in as stored with agent-framework#720. With this release it goes in its compiled form, cut and with the image limits counted, which is what the next request sends anyway.Merge notes
main1a9e1b71.main1fe572eein8ef2f4ec, which brought the 0.22.0 release, fix(images): image ceilings for the live window and compression prompts #268's image ceilings, feat: carry membrane's system changes in place under every strategy; firstChanged on compile results #298's system changes and fix(greedy-fold): raise the oldest chunk at a level that can rise, passing over one with no summary above it #339.mainhas since taken feat(observer): tell a host's observer of each compile, compression refusal and calibration sample #335's observer back out (revert: take out the context observer (#335), which no release carried #340). One conflict came up, in the memory strategy'srenderAsNewestdoc. It keeps fix(images): image ceilings for the live window and compression prompts #268's correction, that the tool pass only pairs and what sending prunes reaches the render throughcappedForSend, with this change's account of the render. No code conflicted. Beside the conflict: the render passes noreturnedtoapplyImageStripping, so it notes no ceiling and says no ceiling line, asmain's own test pins. By our reading of the merge, and Gladys-2413's, a message of system changes is kept raw, as a pin keeps a message, so it renders as the compile sends it, and compression input is off the render's path. No test renders a message of system changes under the memory strategy.main4fbe6c94(0.21.0 and feat(thinking-binding): leave out thinking a binding model can no longer take (#155) #331's thinking binding) infc728aae, with two doc conflicts, each keeping this change's account and feat(thinking-binding): leave out thinking a binding model can no longer take (#155) #331's clause onSelectOptions.thinkingBinding.b1ad54da) and this change now merge with no conflict, ontomainand with each other. The merged result fails three ofrender-as-newest's assertions, the ones that expect the compile to send a body in more than one piece, from the adaptive middle or across it, which fix(shards): a sharded body keeps each block in place and goes out as given, and window edges keep a tool call with its results #310 ends (the review of08b0c2ca, R1-2). No conflict will stop whichever lands second, so it has to turn those three to compare the render with the compile's one entry, drop the docs' and the fragment's sentence about the middle's shard run, and take CI again on the newmain.4c6a3130) merges ontomainwith no conflict and gains four regions with this change: in the memory strategy'srenderAsNewest, its doc and its start, and inrender-as-newest's reset-head test. It sends a reset head window that is still filling last, after the history from before the reset, and moves the render's start to match. By our reading, whichever lands second keeps this change's start at the batch's oldest message, which needs no window, joins the two docs' accounts of where the compile places a filling head, and turns the test to fix(reset): send store order while a reset head fills, and keep a reset head's messages whole #315's order.mainalone (git merge-treeof each head, 04:21Z Oct 26).changelog.d/fix-render-as-newest-null-not-sent.fixed.md.Written by Milo-2653 (Claude Opus 5.5), on calls by Oscar-1005 (Claude Opus 5.5). Kestrel-2409 (Claude Opus 5.5) read its first commit, and Gladys-2413 (Claude Opus 5.5) read its merge of
main1fe572ee. Greta-2425 (Claude Opus 5.5) checked whether each part is needed, and Esther-2584 (Claude Opus 5.5) checked by runs what it changes for an agent using it.Description checked and posted by Daniel-2612 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.