Repository navigation
fix(compression): follow-up to #116's review (R3-1 to R3-3) - #205
Merged
nissa-seru merged 4 commits intoOct 18, 2026
Merged
Conversation
…before it (R3-1) A recall answer is the agent's turn, so when the agent's own raw turn came right after it, collapseConsecutiveMessages joined the two with a `---` separator, and without that, membrane's native formatter merges consecutive messages of one role at the wire. The summarizer then read the raw turn as the end of the memory it had just recalled, and the answer no longer matched the exact body buildRecallCurveVariants looks for, so that summary lost its refusal-fallback variant. Keeping the raw turn as a message of its own isn't enough on its own, since the formatter would still merge the two; matching the answer as a prefix would bring the variant back but leave the summarizer reading the turn as memory. It happens wherever the agent's raw turn follows a recall pair: in the L1 prompt, at an opened chunk that starts on the agent's turn (anima-research#116) and at the raw middle; in a merge prompt, at an L2 target or the raw middle after the last ladder answer. It happens past messages with no content too: the request drops them after the collapse, and the formatter then joins the two. separateRecallAnswers now puts a Context Manager turn, "[CM] The conversation resumes.", right after such an answer, at both sites, before the collapse: when the message right after the answer is the agent's, which the collapse would join to it even with nothing left in it, or when the first message after it that keeps any content, once its unpaired tool blocks and empty text are stripped as the request strips them, is. A recall pair is recognised by its header, as applyMintCacheSeams recognises it, and the two now share recallHeaderId. A prompt without that adjacency keeps its bytes. The one-shot zero-recall surgery, which takes a request's recall pairs out, is unchanged; given a request built now, it keeps the raw turn it used to drop with the answer, and the Context Manager's turn before it. Tests: an agent-first opened chunk, asserting on message boundaries and on the formatted wire; the same past a whitespace-only message, and past a user's turn of an unmatched tool result and blank text; an agent turn that is thinking alone, before a user's turn; the zero-recall surgery on the agent-first request; the refusal-fallback variant of the summary before it; and an L2 merge target that starts on the agent's turn. All seven fail on main. A control shows that a recall answer before another speaker's turn, and two raw turns of one speaker, get no resume turn; it passes on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The no-tools and plain-prose retries append a line to the admitted canonical request's instruction, so a pinned request admitted within a few dozen tokens of compressionContextBudgetTokens could go over it on a retry. The admission now prices the request with the longer of the two lines, so either retry of an admitted request fits, and the over-budget warning gives that price, "counting the line a retry adds", as does the compression:pinned-raw-over-budget event's requestTokens. compressionContextBudgetTokens' doc says so too, and that the opt-in tool-prose rung isn't priced. Recall-curve variants are admitted on their own. The opt-in tool-prose rung, sent only after a refusal, adds a tool round per hoisted field and isn't priced here. The exact-boundary test now prices the two retries as they are sent: pins are kept at the longer one's price and recalled one token below it, where the request without a line would still fit. Its request also opens a chunk on the agent's turn after a recall pair, so the price it checks counts the Context Manager's turn R3-1 puts there. Against R3-1 alone it fails at the retry line's price. 0.15.0 released anima-research#116's admission without the line, so this change has a fragment of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3-3) Under kv-stable and kv-unified with adaptive resolution, a pin or document whose bound is above 0 is compressed (b4f116c), but ContextManager.pinRange, PinnableStrategy and the strategy's pinRange still said pinned ranges aren't compressed, and ContextManager.markDocument said a document is retained in full. They now say which pins stay raw: a pin's bound is its level, or without one its maxLevel, and a pin with no bound or a bound of 0 is raw, except, where bounds are read, at a message where an overlapping pin's bound takes its place. They point to ProtectedRange for the levels and for overlaps, and getCompressibleMessages' doc names where bounds are read. The strategy's own markDocument and ProtectedRange's own doc said the same and get the same change, and so does the pinned-range check the long-context harness doc suggests. 0.15.0 shipped b4f116c with anima-research#116's fragment, which lists the change under Fixed. A .changed.md fragment for the next release names 0.15.0 and markDocument(id, { maxLevel }), since kv-stable is connectome-host's default selector, and says, as 0.15.0's notes do, that under kv-unified a pinned level on a message between the head window and the recent window that no summary supplies yet fails every compile until one does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The PR appears safe to merge; no new actionable issue was found. SummaryThis PR keeps recalled memories separate from following raw agent turns in compression and merge prompts.
Reviews (2) · Last reviewed commit: "docs(pins): a positive level or maxLevel..." · Reviewed by Greptile |
This was referenced Oct 17, 2026
main gained anima-research#217 (serialized media bounds, d9851aa), which changes the L1 pins gate this branch also changes. Git merged everything else; three regions conflicted, and each keeps both sides: - autobiographical.ts, the pins gate's comment: anima-research#217's text (priceRequest, images at the store's estimate, the raw's charge against the recall budget counting a pinned tool block the request drops, so the gate errs toward recall), then this branch's: the request is priced with the longer of the retry lines, recall-curve variants are admitted on their own, and the opt-in tool-prose rung isn't priced. The gate's code merged without a conflict: this branch's Math.max over L1_RETRY_LINES now prices each line through anima-research#217's priceRequest. - strategy.ts, compressionContextBudgetTokens' doc: the request is priced as the store prices the messages it would send (after splitting, collapsing and dropping unpaired tool blocks, before the image cap), with anima-research#217's serialized media and thinking, and with this branch's retry lines. anima-research#217's "each pinned message (its tool blocks included, even where the request drops one whose partner it lacks ...)" describes the raw's charge against the recall budget rather than this request's price, which is of built.cleaned, so it isn't carried here; the gate's comment in autobiographical.ts keeps that point. - Both texts' word on the tool-prose rung: this branch said the pins decision doesn't price it, and anima-research#217 admits it on its own (compressionAdmission), as it does the recall-curve variants. The comment and the doc now say that neither is priced in the pins decision, since each is admitted on its own. - test/l1-recall-raw-pins-frontier.test.ts, the forest() helper's summarizer: this branch's scripted refusal, tool_use and thinking-wrapped answers, and anima-research#217's respond() for every other answer.
Contributor
Author
|
@antra-tess Flagging for your awareness R3-1 from this PR, which made the following change in brief:
|
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.
TL;DR: this is the follow-up to #116 for the three findings of Anarchid's review of it at
0259684(R3-1 to R3-3). A compression or merge prompt no longer joins the agent's own raw turn to the recall answer before it, a pinned L1 request is admitted only if it fits with the longer retry line, and the pin docs say that a positivelevelormaxLevelleaves a pin compressible.We won't merge this before 01:00 UTC on October 18, or before our pre-merge checks pass on its merge with the then-current
main, and a line here asking us to wait holds it.We said on #116, before it merged, that these would come as a PR of their own. It's three commits on
mainatb0bbe64, one per finding, and a merge ofmainatd9851aa, after #217 changed the same pins gate. 0.15.0 came out with #116 before this PR, so R3-2 has a fragment of its own, and R3-3's.changed.mdline names 0.15.0, the release that made the change.Problem
collapseConsecutiveMessagesjoined the two into one message with a---separator. So the summarizer got the raw turn as the end of the memory it had just recalled, andbuildRecallCurveVariants, which looks for the answer's exact body, no longer found it, so that summary lost its refusal-fallback variant. With fix(compression): honor raw pins in L1 recall #116 this happens at every opened chunk that starts on the agent's turn right after a recall pair. An opened chunk is one a pin opens, shown raw among the recall pairs. Onmainit happens in merge prompts too, pinned or not. An L2 merge's target, or the raw middle before it, follows the last recall pair of its ladder, so whenever its first message is the agent's, that message joins the answer. In fix(compression): honor raw pins in L1 recall #116's test fixture with the agent speaking first, 6 of the 8 merge prompts did.compressionContextBudgetTokenscould go over it on a retry.b4f116c, released in 0.15.0, made a pin with a positivelevelormaxLevelcompressible under kv-stable and kv-unified with adaptive resolution, butContextManager.pinRange,PinnableStrategyand the strategy'spinRangestill said pinned ranges aren't compressed,markDocumentcalled a document "a body of information to retain in full", and 0.15.0's notes list the change under Fixed.Changes
a59e71f(R3-1). A Context Manager turn,[CM] The conversation resumes., now goes right after a recall answer that the agent's own raw turn follows (separateRecallAnswers), in the L1 prompt and in the merge prompt, before the collapse. It goes in when the message right after the answer is the agent's, even one with nothing left once its thinking is stripped, since the collapse would join that message to the answer. It also goes in when the first message after the answer that keeps any content is the agent's, counting content as the request does once it strips unpaired tool blocks and empty text. The request drops a message left with nothing after the collapse, and the formatter would then join the two. A prompt without that adjacency is unchanged. A recall pair is recognised by its[CM] Recall memory <id>.header, asapplyMintCacheSeamsalready recognised it, and the two now sharerecallHeaderId.We took neither of the review's two options. These requests go through membrane's native formatter, which merges consecutive messages of one role at the wire (
mergeConsecutiveRoles, in every published version^0.5.69allows, 0.5.69 to 0.5.87). So a raw turn kept as a message of its own would still reach the summarizer inside the memory's assistant turn, only without the---. Matching the answer as a prefix would bring the variant back and leave the raw turn inside the memory's turn. Only a turn of the other role between them keeps them apart, and a[CM]turn follows the convention the recall header itself uses.The merge prompt goes past what the review raised. It's the same join from the same collapse, so one helper serves both sites.
The one-shot zero-recall surgery (
transformZeroRecallCompression), which takes the recall pairs out of a compression request, is unchanged, but the requests it's given changed. It used to drop an agent-first chunk's first raw turn along with the recall answer that turn was joined to. Now that turn stays, with the Context Manager's turn before it.a839c98(R3-2). The admission prices the request with the longer of the two retry lines, so either retry of an admitted request fits. The warning gives that price, "counting the line a retry adds".compressionContextBudgetTokens' doc says so too. The recall-curve variants and the opt-in tool-prose rung aren't priced in this admission, since each is admitted on its own when the refusal ladder reaches it (compressionAdmission, from fix(compression): bound the data-URL matcher, end a payload where its encoding does, and price what each request sends #217). 0.15.0 prices the request without the line, so the fix has its own.fixed.mdfragment.3f7f12d(R3-3). The four comments, the strategy's ownmarkDocumentandProtectedRange's doc now say which pins stay raw. A pin's bound is itslevel, or without one itsmaxLevel, and a pin with no bound or a bound of 0 keeps its messages raw and out of compression. A bound above 0 is read only under kv-stable and kv-unified with adaptive resolution, where the range is compressed and held atlevel, or no deeper thanmaxLevel. Where bounds are read and pins overlap at a message, the boundProtectedRangenames holds it, which can compress a message a raw pin covers.getCompressibleMessages' doc names where bounds are read.leveled-pins-compress.changed.mdputs the change under Changed for the release after 0.15.0. It namesmarkDocument(id, { maxLevel }), since kv-stable is connectome-host's default selector, says that 0.15.0's notes list the change under Fixed, and repeats their warning about kv-unified compiles that fail until a summary supplies a pinned level. The pinned-range check that the long-context harness doc suggests now asks for pins without alevelormaxLevel, with no pin that has one over them.d1a2491mergesmainatd9851aa. fix(compression): bound the data-URL matcher, end a payload where its encoding does, and price what each request sends #217 changed the same pins gate:priceRequestnow prices serialized media as the markers that replace them, and thinking as the request carries it. The admission prices each retry line through it. Git merged the gate's code, and the gate's comment,compressionContextBudgetTokens' doc and the test helperforest()keep both PRs' points. The doc also corrects one clause ofmain's, which said the request is priced with each pinned message's tool blocks included, even where the request drops one whose partner it lacks. That is how the raw is charged against the recall budget. The request is priced afterstripUnpairedToolBlocksdrops such a block (priceRequest(built.cleaned)), as the gate's comment onmainalready says.What each part buys and costs
What it's for. These are the three findings of Anarchid's review of #116, which we said would come as a PR of their own. Without it, in a compression or merge prompt, the agent's raw turn after a recall answer reaches the summarizer as the end of the recalled memory, and that summary loses its refusal-fallback variant. A pinned L1 request admitted within a retry line of its budget can go over it on a retry. And the pin docs say a pin with a positive
levelormaxLevelisn't compressed, which 0.15.0 changed.separateRecallAnswers, about 15 lines, called at the L1 and merge sites, andrecallHeaderId, whichapplyMintCacheSeamsnow shares). The summarizer gets the raw turn as the conversation's, and the variant finds the answer's body again. It adds one short turn where that adjacency occurs: 12 of the 168 requests on our fixtures at3f7f12d, with the other 156 byte-identical. Keeping the raw turn a message of its own, the review's first option, would be smaller, but membrane's formatter merges consecutive messages of one role at the wire, so the two would still reach the summarizer as one turn. Matching the answer as a prefix, its second, would bring the variant back and leave the raw turn in the memory's turn.l1-recall-raw-pins-frontier. Each of the ten removals in the receipts fails at least one..fixed.mdand one.changed.md, the line we promised on fix(compression): honor raw pins in L1 recall #116.Not taken: the review's two options for R3-1 (part 1), dropping empty messages first (part 2), and pricing the recall-curve variants and the opt-in tool-prose rung in this admission, since each is admitted on its own when the refusal ladder reaches it.
Tests
test/l1-recall-raw-pins-frontier.test.ts, eight new tests and one changed:NativeFormatter, the memory's assistant turn holds no raw text.Receipts, on Linux x86_64:
d1a2491, the merge:npm ci,tsc --noEmitand a clean build are clean, and the 15 test filesmainchanged sinceb0bbe64pass 469 of 469 on Node 24.22.0, 46 of them in this PR's test file. CI runs the whole suite.3f7f12d(tree92e0b57c):npm ci,tsc --noEmitand a clean build are clean, and 1454 of 1454 tests pass in 144 files on Node 24.22.0 and on Node 22.12.0.main(b0bbe64) passes 1446 of 1446 in 144 files on each, and this test file is the only one whose count changed, from 32 to 40.mainatb0bbe64, with only an export of the new constant added so the file compiles, the seven R3-1 tests and the admission test fail, and the control passes. Againsta59e71f, R3-1 without R3-2, only the admission test fails, at the retry line's price.3f7f12d, each run over the file's 40 tests:3f7f12dagainstmainatb0bbe64: on three fixtures, of the 168 compression and merge requests each build sent, 156 are byte-identical, and the other 12 differ only by the Context Manager's turn. Taking it out and joining the two messages asmaindoes givesmain's request.3f7f12d, with every PR then open. None conflicts with this change. fix(merge): merge around quarantined sources; add operator merge holds #117, RenderStats.nesting; the kv-overlap warning stops claiming a double render #130 and feat(adaptive): [fold-diff] — which chunks moved between which levels, and where #157 conflict withmainalready, in the same hunks with or without it. Merged with thirteen others at once (feat(identity): witnessedIdentityReminder: inherited and lived history get their own reminders #129, fix(compression): progress past quarantined chunks; transient failures never spend the ladder; pace the lane #138, chore(deprecate): ContextInjection and compile(injections) #140, feat(cache): first breakpoint at the end of the deep band, not the last head message #158, fix: send signed thinking only while the prefix it was minted under holds #160, docs: add a README #177, fix(compression): count cache writes and reads in every provider's input total (#182) #183, fix(store): reads leave out content that doesn't match ContentBlock, and writes refuse it (#184) #185, fix(kv-stable): the carried frontier keeps the overlap exemption #186, feat(errors): OverBudgetError.clearedBy says what clears a refusal #190, feat(diagnostics): log compile time on the plan-vs-actual line #200, fix(autobiographical): moves counts the saved levels a compile changes (#195) #203 and fix(split-stitch): discardedAttempts carries the discarded calls' cache counts #204), 1763 of 1764 tests pass in 150 files on Node 24.22.0. The one failure is a test that fix(split-stitch): discardedAttempts carries the discarded calls' cache counts #204 adds intest/split-stitch-l1.test.ts, and it fails the same way with fix(compression): progress past quarantined chunks; transient failures never spend the ladder; pace the lane #138 and fix(split-stitch): discardedAttempts carries the discarded calls' cache counts #204 merged intomainalone, without this change. fix(kv-stable): phase C un-folds youngest by position, not deepest level first #156 conflicts with fix(kv-stable): the carried frontier keeps the overlap exemption #186 insrc/adaptive/kv-control.ts, and fix(images): register the image watermark's state, and say when its write fails #202 with fix(compression): progress past quarantined chunks; transient failures never spend the ladder; pace the lane #138 insrc/strategies/autobiographical.ts, so neither is in that composition. Merged with this change together, they pass 1467 of 1467 in 147 files. I didn't build feat: folding-strategy migration utility (kv-stable ⇄ kv-unified) #94, a draft.Not verified
Out of scope
main(b0bbe64) and at3f7f12dalike. This PR changes only the compression and merge prompts.changelog.d/recall-answer-resume-turn.fixed.md,changelog.d/l1-pins-retry-line.fixed.mdandchangelog.d/leveled-pins-compress.changed.md.— Beth-2389 (Claude Opus 5.5). Oscar-1005 read it, besides an automated internal review, and Desmond-2314 read its merge of
main.