Skip to content

fix(compression): follow-up to #116's review (R3-1 to R3-3) - #205

Merged
nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/116-review-followup
Oct 18, 2026
Merged

nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/116-review-followup

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

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 positive level or maxLevel leaves 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 main at b0bbe64, one per finding, and a merge of main at d9851aa, 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.md line names 0.15.0, the release that made the change.

Problem

  • R3-1. A recall answer in the canonical L1 prompt is the agent's turn. When the agent's own raw turn came right after it, collapseConsecutiveMessages joined 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, and buildRecallCurveVariants, 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. On main it 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.
  • R3-2. 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.
  • R3-3. b4f116c, released in 0.15.0, made a pin with a positive level or maxLevel compressible under kv-stable and kv-unified with adaptive resolution, but ContextManager.pinRange, PinnableStrategy and the strategy's pinRange still said pinned ranges aren't compressed, markDocument called 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, as applyMintCacheSeams already recognised it, and the two now share recallHeaderId.

    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.69 allows, 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.md fragment.

  • 3f7f12d (R3-3). The four comments, the strategy's own markDocument and ProtectedRange's doc 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 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 at level, or no deeper than maxLevel. Where bounds are read and pins overlap at a message, the bound ProtectedRange names holds it, which can compress a message a raw pin covers. getCompressibleMessages' doc names where bounds are read. leveled-pins-compress.changed.md puts the change under Changed for the release after 0.15.0. It names markDocument(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 a level or maxLevel, with no pin that has one over them.

  • d1a2491 merges main at d9851aa. 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: priceRequest now 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 helper forest() keep both PRs' points. The doc also corrects one clause of main'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 after stripUnpairedToolBlocks drops such a block (priceRequest(built.cleaned)), as the gate's comment on main already 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 level or maxLevel isn't compressed, which 0.15.0 changed.

  1. A Context Manager turn after a recall answer that the agent's raw turn follows (separateRecallAnswers, about 15 lines, called at the L1 and merge sites, and recallHeaderId, which applyMintCacheSeams now 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 at 3f7f12d, 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.
  2. The look past messages with no content (the second half of the condition, and one strip of unpaired tool blocks per prompt). The request drops a message left with nothing, which would bring the answer and the raw turn together at the wire, so the turn also goes in when the first message with content after the answer is the agent's. Dropping empty messages before the separation would be simpler, but the collapse would then join messages that an empty one now keeps apart, changing prompts with no recall answer in them.
  3. The retry line in the admission's price (R3-2). Either retry of an admitted request fits its budget. The cost is at the edge: a pinned request within a retry line of its budget now has its pins recalled, where they used to be kept and a retry could go over.
  4. The pin docs and comments (R3-3) say which pins stay raw, and the long-context harness doc's suggested check asks for pins without a bound.
  5. The tests: eight new and one changed in l1-recall-raw-pins-frontier. Each of the ten removals in the receipts fails at least one.
  6. The changelog fragments, which the repository asks for: two .fixed.md and 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:

  • An opened chunk that starts on the agent's turn, the L1 after an L2 with the agent speaking on even turns, as in the review's probe: the recall answer before it is the memory alone, the Context Manager's turn follows, and the raw turn is a message of its own. Formatted with membrane's NativeFormatter, the memory's assistant turn holds no raw text.
  • The same past a message with no content: a user's turn of whitespace alone between the recall answer and the agent's raw turn, which the request drops.
  • The same past a user's turn of a tool result whose call no message carries, with blank text, which the request strips and then drops.
  • An agent turn that is thinking alone, before a user's turn: the recall answer before it is the memory alone, with no separator joined to it.
  • The zero-recall surgery on the agent-first request: every recall pair goes, and the raw turn stays, with the Context Manager's turn before it.
  • Its refusal-fallback variant: after a canonical refusal, the first variant expands the summary just before the chunk.
  • A merge prompt whose L2 target starts on the agent's turn: no ladder answer carries a raw turn, and the agent's turn follows the Context Manager's.
  • A control: a recall answer before another speaker's turn, and two raw turns of one speaker, get no Context Manager turn.
  • The exact-boundary admission test now sends both retries and prices them as sent: pins are kept at the longer one's price and recalled one token below it, where the request without a retry 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.

Receipts, on Linux x86_64:

Not verified

  • A real summarizer. The tests send no request to a model, so how a summarizer reads the new turn is by design, not measured. Nor have I run this on a production store.
  • Signed reasoning in a recall answer. The fixtures' summaries carry none. By reading, an answer's signed reasoning now stays in a turn of its own, where it used to share its turn with the raw turn after it.
  • macOS and the newest Node, which CI covers.

Out of scope

  • The live view. A compile can render a recall answer and the agent's raw turn after it as consecutive messages of the agent's, which the formatter then sends as one turn. In a probe under kv-stable it did once, at a 6,000-token budget, at main (b0bbe64) and at 3f7f12d alike. This PR changes only the compression and merge prompts.

  • Changelog fragments: changelog.d/recall-answer-resume-turn.fixed.md, changelog.d/l1-pins-retry-line.fixed.md and changelog.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.

Beth-2389 and others added 3 commits October 16, 2026 19:39
…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>
@greptile-apps

greptile-apps Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no new actionable issue was found.

Summary

This PR keeps recalled memories separate from following raw agent turns in compression and merge prompts.

  • Compression and merge prompts keep recalled memories apart from following agent turns.
  • Pinned L1 prompts leave room for either retry instruction.
  • Pin guidance explains when pinned messages can still be compressed.

Reviews (2) · Last reviewed commit: "docs(pins): a positive level or maxLevel..." · Reviewed by Greptile

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.
@nissa-seru
nissa-seru merged commit 627e2df into anima-research:main Oct 18, 2026
6 checks passed
@nissa-seru

nissa-seru commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

@antra-tess Flagging for your awareness R3-1 from this PR, which made the following change in brief:

  • In the requests that write and merge memories, a recalled memory and the agent's own next turn used to be joined with ---. Now a [CM] The conversation resumes. turn sits between them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

greptile no-greptile Maintainers' and the team's: Greptile skips this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant