Repository navigation
fix(tune-out): fit the cancel's backlog in one message of each resident that reads it - #593
Merged
nissa-seru merged 4 commits intoOct 19, 2026
Conversation
…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>
[Medium impact] The PR appears safe to merge; no actionable issues were found. SummaryFits a cancel's
Tests were reviewed but not run. Reviews (1) · Last reviewed commit: "test(tune-out): the capped sweeps hold t..." · Reviewed by Greptile |
1 task done
Contributor
Author
|
@antra-tess Flagging for your awareness this PR, which made the following changes to what residents see (in brief):
— Lottie-2661 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'struncated=and in the subconscious's report notice.Problem
A run at
main037aa793, on Linux with Node 24.22.0, chronicle 0.4.0, context-manager 0.14.1 and membrane 0.5.87: a resident onAutobiographicalStrategywithmaxMessageTokens120, six messages of 148 characters each in a tuned-out channel underbacklogCap10, then a cancel from the resident.<tuned-out-backlog channel="disc:guild:noisy" messages=6>.backlogCapleft nothing out, the dump's tag has notruncated=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.maxMessageTokens10,000, the same dump reaches the resident whole.By reading context-manager 0.14.1, the version this repository installs,
AutobiographicalStrategyandWindowedPassthroughStrategycount a text block at a token per four characters, rounded up. A message overmaxMessageTokenskeeps its text up tomaxMessageTokens × 4characters (truncateContent,dist/src/strategies/autobiographical.js:11643anddist/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 strategymaxMessageTokens10,000 unless the recipe sets another (src/framework-strategy.ts:158on itsmain537a863d). ItsFrontdeskStrategyextendsAutobiographicalStrategy, and itsselectwraps the parent's. 10,000 tokens is 40,000 characters, whichbacklogCap'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 × 4as 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, withtruncated=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:truncated=, which still calls them the oldest;backlogCapleaves messages out.Held messages among them follow the notice as rows of their own, as held messages
backlogCapleaves 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
backlogCapbetween 0 and 1 now delivers no raw backlog. The backlog was taken withslice(-backlogCap), which starts at the first message for such a cap, so it delivered every one.tune_out's description is unchanged.backlogCapis "Max raw messages delivered at cancel", an upper bound that still holds. The dump'struncated=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 withtune_outa prompt-cache miss at the release.#546 (open) builds the dump as one row too (
src/tune-out/coordinator.ts:492-512at its head220f4cff). A trial merge of this head into220f4cffconflicts in the same six files as mergingmain037aa793does, with the same number of conflict hunks in five of them, and four insrc/tune-out/coordinator.tswheremain's merge has five.What each part is for
residentMessageChars(a hook onTuneOutFrameworkHooks, whichframework.tsgives): 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.cancel(),src/tune-out/coordinator.ts): keeps the newest lines that fit whole, so no reader's strategy cuts the dump.renderbuilds 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.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.prefixOfkeeps a cut from ending on half of a pair.Math.flooronbacklogCap: a cap below one shows no message, as its doc's upper bound says.heldForReport: a held newest message shown cut goes to the report, as a held message whose media the dump drops does.backlogCapas the only reason.TuneOutParams.backlogCapandModuleMessage.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, andtest/tune-out-cancel-held.test.ts's header: they said the dump holds the lastbacklogCapmessages, or that only the cap leaves messages out.backlogCapfix, 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 withAutobiographicalStrategy.lengthShowing(k)gives the exact length of a dump showing the newest k, frombacklogCapk with no bound, so the boundary tests don't rebuild the format, andlinesOfreads a dump's lines between its tags.cancelSixopens 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,assertNewestThreeWholeandassertNoticeCountsThreeare 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 × 4characters 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:truncated=and the notice. One character less shows only the newest.truncated=takes one or two digits; ten and twelve under abacklogCapof 9; five with a long newest; and a long message alone, where showing it dropstruncated=.backlogCapallows, rendered asbacklogCapwould render that many, and the next older line wouldn't have fit.truncated=and the notice count the rest.backlogCap0 shows no message at any room, and so doesbacklogCap0.5.AutobiographicalStrategyatmaxMessageTokens170 and six 148-character messages: after a cancel, the resident's next request shows the newest three whole, withmessages=6 truncated=3and the closing tag, and the strategy cut nothing. The subconscious's notice counts the three.maxMessageTokens10,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 intest/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:backlogCapor 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-firstandstate-read-and-write-errors.main037aa793with 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 oftune-out-cancel-held's 6 tests and one ofmodule-rows-held-from-residents'.backlogCap's newest, which fails tests 2, 3, 4, 7 and 12;module-rows-held-from-residents' tests;heldForReport(test 6),prefixOf's surrogate check, the count's old wording, andMath.flooronbacklogCap(test 10).Not verified
changelog.d/tune-out-dump-fits-one-message.fixed.mdWritten 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-seruaccount.