Skip to content

fix(tune-out): fit the cancel's backlog in one message of each resident that reads it - #593

Merged
nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/tune-out-dump-fits-one-message
Oct 19, 2026
Merged

nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/tune-out-dump-fits-one-message

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

A tune-out cancel's <tuned-out-backlog> now holds only as many of the newest diverted messages as fit whole in one message of each resident that reads it. The dump is one message, and a resident's strategy cut it like any message over its per-message cap, keeping its first characters. So a long backlog reached the resident without its newest messages, which still counted as shown. The messages the dump now leaves out are the oldest, and they're counted in the dump's truncated= and in the subconscious's report notice.

Problem

A run at main 037aa793, on Linux with Node 24.22.0, chronicle 0.4.0, context-manager 0.14.1 and membrane 0.5.87: a resident on AutobiographicalStrategy with maxMessageTokens 120, six messages of 148 characters each in a tuned-out channel under backlogCap 10, then a cancel from the resident.

  • The stored dump row is 1,079 characters and holds all six, under <tuned-out-backlog channel="disc:guild:noisy" messages=6>.
  • The resident's next request carries it cut to its first 480 characters, followed by "[truncated — original was 270 tokens]". That's messages 1 to 3, the oldest, and no closing tag. Messages 4 to 6, the newest, are gone.
  • The subconscious's notice reads "[Tune-out cancelled: disc:guild:noisy — agent request.]". Since backlogCap left nothing out, the dump's tag has no truncated= and the notice no count. The strategy's note tells the resident that the message was cut, but neither the resident nor the subconscious is told that three messages weren't shown.
  • With maxMessageTokens 10,000, the same dump reaches the resident whole.

By reading context-manager 0.14.1, the version this repository installs, AutobiographicalStrategy and WindowedPassthroughStrategy count a text block at a token per four characters, rounded up. A message over maxMessageTokens keeps its text up to maxMessageTokens × 4 characters (truncateContent, dist/src/strategies/autobiographical.js:11643 and dist/src/strategies/windowed-passthrough.js:380). So a cut keeps the dump's header and its oldest lines. connectome-host gives a resident on its autobiographical or frontdesk strategy maxMessageTokens 10,000 unless the recipe sets another (src/framework-strategy.ts:158 on its main 537a863d). Its FrontdeskStrategy extends AutobiographicalStrategy, and its select wraps the parent's. 10,000 tokens is 40,000 characters, which backlogCap's default of 200 messages reaches at 200 characters a line.

Changes

  • The fit. The dump goes into the residents' shared slot, and each resident's strategy compiles it. The coordinator now gets the smallest per-message bound among the agents that read that slot, each one's maxMessageTokens × 4 as the tool-result cap already reads it (strategyDerivedToolResultChars). When the dump would be longer, it keeps the newest lines that fit whole. Each step is measured exactly, with truncated= at the count that step leaves out, so the dump is never longer than the bound when the bound has room for the dump's frame. The frame is its header and tags, about 200 characters with a short channel id. Below that, no line fits, and the residents' strategies cut the frame itself. A dump no reader caps is left as it was.

  • What it leaves out is counted. The lines it leaves out are the oldest. They count as left out along with those above backlogCap:

    • in the dump's truncated=, which still calls them the oldest;
    • in the subconscious's report notice, so its report covers them;
    • for a cancel the subconscious makes itself with no note, which then gets the notice and a report turn, as when backlogCap leaves messages out.

    Held messages among them follow the notice as rows of their own, as held messages backlogCap leaves out do.

  • A newest message too long to fit alone is shown cut to the room left and marked "[cut to fit; the whole message is in the channel's history]", with nothing older, when at least a character of it fits beside the mark. So the resident still gets its head, as it would from a row of its own. If it's still held, it also follows the notice, as a held message whose media the dump drops does, since the subconscious's view can't have it yet.

  • The notice's count now reads "N of M diverted messages, the oldest, weren't delivered raw to the resident". It said they "were above the cap", which no longer names the only reason.

  • A backlogCap between 0 and 1 now delivers no raw backlog. The backlog was taken with slice(-backlogCap), which starts at the first message for such a cap, so it delivered every one.

  • tune_out's description is unchanged. backlogCap is "Max raw messages delivered at cancel", an upper bound that still holds. The dump's truncated= says how many didn't arrive and where they are. By our reading, a change to the tool's text would also cost every resident with tune_out a prompt-cache miss at the release.

#546 (open) builds the dump as one row too (src/tune-out/coordinator.ts:492-512 at its head 220f4cff). A trial merge of this head into 220f4cff conflicts in the same six files as merging main 037aa793 does, with the same number of conflict hunks in five of them, and four in src/tune-out/coordinator.ts where main's merge has five.

What each part is for

  • residentMessageChars (a hook on TuneOutFrameworkHooks, which framework.ts gives): the bound the fit uses. The coordinator can't see the residents' strategies, and the framework already converts a strategy's cap for tool results. It takes the smallest over the slot's readers (sharedSlotAgents), since each of them compiles the dump.
  • The fit (in cancel(), src/tune-out/coordinator.ts): keeps the newest lines that fit whole, so no reader's strategy cuts the dump.
    • render builds the dump's text, so the fit measures the text it will add.
    • frameShowing(k): the dump's length with no line, for the count that showing k lines leaves out. truncated= takes that count's digits and goes at 0, so each step is measured with its own count and the fit is exact at a digit boundary too.
    • The cut newest line (prefixOf, CUT_MARK): a newest message too long to fit alone still reaches the residents, marked, rather than leaving the dump empty. The cut is made only when its head holds at least one character: a room under one character, as a fractional cap can leave, or one that would end on half of a surrogate pair, shows none. prefixOf keeps a cut from ending on half of a pair.
  • Math.floor on backlogCap: a cap below one shows no message, as its doc's upper bound says.
  • The cut clause in heldForReport: a held newest message shown cut goes to the report, as a held message whose media the dump drops does.
  • The notice's count wording, and the comments on the report and the self-cancel, which named backlogCap as the only reason.
  • The docs of TuneOutParams.backlogCap and ModuleMessage.heldFromResidents, three sentences of fix(tune-out): mark held messages in module reads, keep them out of a live turn, and carry them at the cancel #583's pending changelog fragment, and test/tune-out-cancel-held.test.ts's header: they said the dump holds the last backlogCap messages, or that only the cap leaves messages out.
  • The changelog fragment: the release notes' account of what residents and the subconscious now get at a cancel, and of the backlogCap fix, for operators and module authors reading what changed.
  • test/tune-out-dump-fit.test.ts: the coordinator alone with a stubbed bound, and two tests through the framework with AutobiographicalStrategy.
    • lengthShowing(k) gives the exact length of a dump showing the newest k, from backlogCap k with no bound, so the boundary tests don't rebuild the format, and linesOf reads a dump's lines between its tags.
    • cancelSix opens the framework with the given residents and a subconscious, tunes out the fixture's channel, diverts six messages and cancels, so each framework test only reads what it checks. It reads requests made after the cancel only (callsBefore), so an earlier request can't stand in for the one the cancel brings.
    • dumpIn, assertNewestThreeWhole and assertNoticeCountsThree are what both framework tests check, written once.
    • roomFor, in test 4, gives the room that leaves exactly a given number of code units for the newest's head beside the frame and the mark, so the half-character and half-emoji cases sit on the edge they test rather than near it.

The fit relies on context-manager's strategies keeping maxMessageTokens × 4 characters of a message's text, as the tool-result cap does. A strategy that counts otherwise could still cut the dump.

Tests

test/tune-out-dump-fit.test.ts:

  1. With room for the whole dump, it shows every message. One character less leaves the oldest out.
  2. With room for exactly the newest two of five, it shows them, fills the room and keeps its closing tag. It counts the other three in truncated= and the notice. One character less shows only the newest.
  3. At every room from 0 to past the whole dump, over six backlogs: five messages; twelve, where truncated= takes one or two digits; ten and twelve under a backlogCap of 9; five with a long newest; and a long message alone, where showing it drops truncated=.
    • Its lines are the newest, whole, no more than backlogCap allows, rendered as backlogCap would render that many, and the next older line wouldn't have fit.
    • The newest is shown cut exactly when no line fits whole and some of it fits beside the mark, and then the dump fills the room. Over the backlogs with a long newest, some room cuts it, and over the others none does.
    • The dump fits whenever it shows a line or the room holds its frame.
    • truncated= and the notice count the rest.
  4. A cut shows at least one character of the newest or nothing: half a character of room, as a fractional cap can leave, shows nothing and counts the newest as left out, and so does room for one code unit of an author's emoji.
  5. A newest message too long to fit alone is shown cut and marked, filling the room, and the five older ones are counted.
  6. A held newest message shown cut follows the notice with its content. A stored one doesn't, since it's in the subconscious's view.
  7. Held messages the fit leaves out follow the notice as rows, and stored ones don't.
  8. Without a per-message cap, nothing is fitted, and a 100,000-character message is in the dump whole.
  9. A room smaller than the dump's frame shows no message and counts all five.
  10. backlogCap 0 shows no message at any room, and so does backlogCap 0.5.
  11. The cut never ends on half of a surrogate pair.
  12. A cancel the subconscious makes itself with no note gets the notice and a report turn when the fit left messages out, and neither with room for all.
  13. Through the framework, with the resident on AutobiographicalStrategy at maxMessageTokens 170 and six 148-character messages: after a cancel, the resident's next request shows the newest three whole, with messages=6 truncated=3 and the closing tag, and the strategy cut nothing. The subconscious's notice counts the three.
  14. With two residents, the primary at maxMessageTokens 10,000 and the second at 170, the dump fits the second's cap: its view and the primary's request both show the newest three whole.

The count's new wording changes five assertions and a test's title in test/tune-out-cancel-held.test.ts, and one assertion in test/module-rows-held-from-residents.test.ts.

Runs at 7309de32, on Linux with Node 24.22.0, chronicle 0.4.0, context-manager 0.14.1 and membrane 0.5.87:

  • At this head, from a clean checkout: typecheck passes, and the new file and 11 neighbours pass, 185 tests with none failing. The neighbours are the other tune-out test files and those that mention backlogCap or the dump: tune-out-cancel-held, module-rows-held-from-residents, tune-out-e2e, tune-out-messages, tune-out-lifecycle, tune-out-timers, tune-out-gate-composition, channel-lifecycle-order, refusals-are-error-results, channel-decision-log-first and state-read-and-write-errors.
  • The whole suite at this head, from a clean checkout: 254 files, 5,371 tests, 5,344 passing, 27 skipped and none failing.
  • Red first: at main 037aa793 with this head's three test files, the new file fails 13 of its 14 tests. The one that passes is test 8, which fits nothing. The count's wording fails 4 of tune-out-cancel-held's 6 tests and one of module-rows-held-from-residents'.
  • Removals: each of 21 pieces, taken out alone at this head with the build passing, fails a test in the new file or its two neighbours:
    • the fit, which fails tests 1 to 7, 9 and 11 to 14;
    • fitting every message rather than backlogCap's newest, which fails tests 2, 3, 4, 7 and 12;
    • the framework's bound (tests 13 and 14), its smallest-of-readers choice made the other way or the primary's alone (test 14), and a bound of 0 for a strategy with no cap, which fails two of module-rows-held-from-residents' tests;
    • keeping the oldest lines rather than the newest;
    • the cut newest line, its room for the mark, its frame for one line shown, a gate on its room rather than its head, and a head of nothing;
    • a line's newline in its cost;
    • each step measured with the widest frame, or with too narrow a one;
    • fitting a dump exactly the room's length, and leaving out a line that exactly fills it;
    • the cut clause in heldForReport (test 6), prefixOf's surrogate check, the count's old wording, and Math.floor on backlogCap (test 10).

Not verified

  • macOS and Node 20, where CI runs the typecheck, the build and the suite.
  • A strategy that counts a message's size otherwise than context-manager's.
  • What the subconscious writes in its report. The tests check what the notice tells it, not what a model then says.

  • Changelog fragment added: changelog.d/tune-out-dump-fits-one-message.fixed.md

Written by Hilda-2681 (Claude Opus 5.5), contributing through nissa-seru. Ada-1017 (Claude Opus 5.5) made the design calls, and Hilda-2580 (Claude Opus 5.5) read each round.

Description checked and posted by Lottie-2661 (Claude Opus 5.5).

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.

Hilda-2681 and others added 4 commits October 19, 2026 15:17
…ages

A tune-out cancel delivers its <tuned-out-backlog> to the resident as one
message, and the resident's strategy cuts any message over its per-message
cap (maxMessageTokens) from the end: context-manager's strategies count a
text block at a token per four characters, rounded up, and keep a message's
first cap × 4 characters. So a long backlog reached the resident with its
oldest lines and without its newest lines or its closing tag, and the lines
cut still counted as shown: truncated= didn't count them, and the
subconscious's report notice wasn't told of them.

The coordinator now gets the primary resident's per-message bound
(residentMessageChars: the strategy's maxMessageTokens × 4, as the
tool-result cap reads it) and keeps the newest lines that fit whole in it,
measured with the widest count truncated= can take. The lines it leaves out,
the oldest, count as truncated along with those above backlogCap: in the
attribute, in the report notice's count, and for a note-less self-cancel's
notice and report turn. Held messages among them follow the notice as rows
of their own, as held messages the cap leaves out do. A newest message too
long to fit alone is shown cut to the room left and marked "[cut to fit;
fetch_history has it whole]"; if it's still held, it follows the notice too.
A strategy with no per-message cap gets the dump as before.

The report notice's count now reads "N of M diverted messages, the oldest,
weren't delivered raw to the resident", since "above the cap" no longer
names the only reason.

Tests: test/tune-out-dump-fit.test.ts, with the coordinator at each room
size and through the framework with AutobiographicalStrategy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cancel's dump goes into the residents' shared slot, and every agent
that reads that slot compiles it with its own strategy. The bound the
fit uses is now the smallest per-message bound among them (each one's
maxMessageTokens × 4), not the primary's alone, so a second resident with
a smaller cap no longer cuts the dump's newest lines.

The fit now measures each step exactly: the dump showing the newest k
lines is the frame for the count that leaves out, whose truncated= takes
that count's digits and goes at 0, and the lines. Measuring every step
with the widest count could leave out a line that fit, or show a newest
line cut that fit whole, at a digit boundary or where truncated= goes. A
newest line too long to fit alone is cut to the room left beside the frame
for one shown line and the mark, when that room is above zero.

The cut's mark now reads "[cut to fit; the whole message is in the
channel's history]": over the tool-result cap with no writable workspace,
fetch_history's result is cut too, so the mark no longer says the tool
has it whole.

backlogCap is floored at the cancel, so a cap between 0 and 1 delivers
no raw backlog. slice(-0.5) started at the first message and delivered
them all.

The comments, the backlogCap and heldFromResidents docs, the pending
changelog fragments and tune-out-cancel-held's header say the dump fits
whenever the bound has room for its frame, name the strategies whose cut
was checked, and name the dump, not the cap alone, as what leaves
messages out.

Tests: the room sweep runs over five messages and twelve, with exact
lengths from backlogCap k and maximality at each room; a two-resident
framework test; backlogCap 0 and 0.5; the 100,000-character message
whole without a cap; the surrogate test checks its line is there; the
framework tests read only requests made after the cancel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cut of a newest message too long to fit alone was gated on the room
left for its head, not on the head prefixOf gives: a fractional room
under one character, as a fractional maxMessageTokens can leave, or one
that would end on half of a surrogate pair gave an empty head, so the
dump showed the mark alone and counted the newest as shown. The cut now
takes the head first and is made only when it holds a character; with
no line to cut, the head is empty too.

The bound no longer filters readers by registration: no reader of the
residents' shared slot leaves the agent registry. Residents are created
once, when the framework is, and only isolated forks and ephemeral runs
are deregistered.

The room sweep now also runs over ten and twelve messages under a
backlogCap of 9, over five messages with a newest longer than the cut's
mark, and over that long message alone, where showing it drops
truncated=. With the 60-character mark its short lines never reached the
cut. A new test pins a cut of half a character and of one code unit of
a surrogate pair.

The backlogCap doc says the newest is shown cut only when some of it
fits; the fit's comment says that when no line fits, whole or cut, the
dump shows none, where it said a room too small for the frame leaves no
line, which a lone message whose line fits without truncated= belies;
and tune-out-cancel-held's header names a held newest shown cut among
the rows after the notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The room sweep's capped backlogs took how many lines a dump may show from
the dump itself, so a fit that ignored backlogCap's newest-first slice
passed them. Each sweep now asserts the dump shows no more lines than
the cap allows. Its comment says which digit crossing each backlog
makes, and the fit's comment says the residents' strategies, not the
coordinator, cut a frame too long for the room.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Fits a cancel's <tuned-out-backlog> to the smallest message cap among its readers, keeping the newest messages.

  • Tune-out dumps keep the newest messages that fit each resident's limit.

Tests were reviewed but not run.

Reviews (1) · Last reviewed commit: "test(tune-out): the capped sweeps hold t..." · Reviewed by Greptile

@nissa-seru
nissa-seru merged commit 51a66ec into anima-research:main Oct 19, 2026
7 checks passed
@nissa-seru

Copy link
Copy Markdown
Contributor Author

@antra-tess Flagging for your awareness this PR, which made the following changes to what residents see (in brief):

  • The <tuned-out-backlog> a tune-out's cancel delivers used to be the newest backlogCap lines, whole, and a resident's strategy cut it like any message over its maxMessageTokens, keeping its first characters, so a long backlog lost its newest lines and its closing tag. Where the residents that read it have such a cap, it now holds only as many of the newest lines as fit whole in one message of each (the smallest cap, at four characters a token), still at most backlogCap. A newest line too long to fit by itself shows its head, ending [cut to fit; the whole message is in the channel's history].
  • A backlogCap between 0 and 1 now delivers no raw backlog. Before, it delivered every diverted message.
  • The subconscious's [Tune-out cancelled: …] notice: <n> of <m> diverted messages were above the cap and not delivered raw to the resident. → <n> of <m> diverted messages, the oldest, weren't delivered raw to the resident.

— Lottie-2661

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant