Repository navigation
fix(routing): after a re-pin, the token traces and typing follow the turn's locus - #565
Merged
nissa-seru merged 2 commits intoOct 19, 2026
Merged
Conversation
…turn's locus An addressed re-pin at a tool boundary, or the agent's own channel_open, moves a locus or hybrid turn's locus between rounds, and the next round's prose is delivered there. driveStream computed the channel it shows once, when the stream started, so for the rest of the stream the inference:tokens and inference:content_block traces named the old channel, and the typing indicator stayed there (anima-research#555). The turn-start comment still said the locus is frozen. driveStream now keeps that channel as a variable, and followTurnLocus compares it with the turn's locus at the model's content in each round (its tokens and block events, not the tool results a boundary reports as content) and at a refusal retry. When the locus has moved, in locus or hybrid mode, it moves the indicator there, and the traces, which read the same value, name the new channel. A response with nothing in it moves nothing, so a channel_open just before an empty final response doesn't start typing in a turn that is ending. Explicit mode keeps its trigger channel. A stream that hands its turn to a successor (a budget restart, a guard retry) leaves its indicator running. TurnRouting now records the channel the turn's indicator was last shown in, and showTurnTyping, which every start of the indicator and followTurnLocus go through, stops that one first when it shows the indicator elsewhere. So a re-pin at such a boundary no longer leaves the old channel typing until the turn ends. Both name the conversation the turn is attending: in hybrid mode they follow the locus whether or not a >>> envelope sends the text elsewhere. The live outgoing stream is unchanged. Tests: the re-pin rig's locus and hybrid re-pins, a pinned hybrid turn, a stream with no block events, a refusal before the retried round's content, a budget restart that re-pins, channel_open, an empty final response with and without its tool's result reported as content, and a locus turn with nowhere to speak at its start; an explicit turn's typing and traces across its rounds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
force-pushed
the
fix/repin-trace-locus
branch
from
October 19, 2026 09:23
e1f8349 to
608357b
Compare
[Medium impact] The PR appears safe to merge, with no actionable issues found. SummaryMakes typing and each trace’s
Known limits acknowledged by nissa-seru and excluded from findings:
Reviews (1) · Last reviewed commit: "fix(routing): after a re-pin, the token ..." · Reviewed by Greptile |
main's new tests in test/present-while-acting.test.ts, for channels a server declared inbound-only, sit where this branch's tests for typing after a channel_open do. Both sides' added tests are kept whole, this branch's first; nothing else conflicted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tel-0s
added a commit
that referenced
this pull request
Oct 19, 2026
…ge entry for bounded jobs, the batched-wake hold What main brings since 9c6964c: 0.23.0 and 0.23.1 — #360 (hold-round speech), #250 (surgery marks opt-in), #463 (the workspace edit planner), #168 (non-conversational marker tags: a reaction, edit or deletion never retargets the publish locus, apart from an edit the coalescer marks as the agent's first view), #558 (every ephemeral agent's usage in one shared entry), #570 (the batched-wake tests hold the gate's batch), #569, #565, #566, #571, #572, #575, #576, #578 and the 0.23.x merge-backs. Conflicts in five files, resolved by composition: - src/agent.ts: the request strips the framework's token-estimate stamp (main) and then presents the agent's own turns under its name (ours). - src/framework.ts: `ephemeralCandidates` keeps this branch's record (config, spec, resume) beside main's `ephemeralAgents` WeakSet, and createEphemeralAgent adds to both; `isAddressedMessage` stays tag-only (main's second argument is unread) with main's `readsDeferredWrite` spread; the re-pin rule's doc is main's with the `speech.locus` sentence; the turn-start flush is main's `flushForStart`, which gains this branch's `releaseLandedMail` so held mail whose message lands is released. - src/mcpl/channel-registry.ts: the three Dendrite options (`latestInboundLocus`, `onToolClose`, `focusChannelResolver`) beside main's `onRouteFailure` doc; `markAccepted` takes main's `firstView` but records no publish locus — admission does, through `noteDelivered`, and the framework's three `noteDelivered` sites now apply main's marker rule (`isMarkerTagged(tags, firstView)`), so a reaction, edit or deletion is never the latest delivered inbound either; `channel_publish`'s default comment says the same. - test/batched-wake-locus.test.ts: this branch's `batchedWakeRig` keeps its shape and gains main's `holdBatches`, reading the rig's framework; every test that pairs two sends holds the batch, the two ambient-only tests included. - test/present-while-acting.test.ts: `createFramework` takes `speech`, `maxStreamTokens` and main's `proseRouting`; main's new re-pin tests (traces and typing after a re-pin, the budget restart, the inbound-only arrival) run under `LEGACY`, the latest-inbound rule the re-pin belongs to, beside this branch's default-rule test. Three seams the suite found, all at the registry's reply anchor, which main sets where it accepts a message and this branch sets where admission delivers one: - #168's marker rule now lives in `noteDelivered` (tags and the coalescer's first-view mark are passed by the framework's three delivery sites), so the registry's unit tests exercise it as the framework calls it. - `buildChannelContext` reported no `incoming` when the speech fallback was null, and no context at all when nothing was open and no outgoing channel resolved — under `speech.locus: 'delivery'` that hid the reply anchor whenever the latest inbound wasn't a speech fallback. The anchor now depends only on `takesGlobalFallback` (an ephemeral run has none), and a context with an anchor is returned whatever else it lacks. - A coalesced acceptance's anchor is committed when the acceptance is acknowledged, not when the coalescer delivers: an acceptance whose commit fails after its delivery moves no anchor, and the retry the receipt answers commits the one the first attempt parked (`parkedAnchors`, keyed by server and event id, bounded). #157's tests pinned both halves. Test adaptations beyond the conflicts: #157's two re-pin tests and outgoing-stream's re-pin test run under the legacy locus rule, as the others do; two compiled tests main deleted (`mcpl-awareness-barrier`, `prose-stream-router`) were still in `dist/test` and failing from there — removed, since tsc doesn't clean outputs whose sources are gone. Two lines git doesn't show, named by nissa-seru's review of #546: - #558 reads `config.subconscious.name` for the shared usage entry's exemptions; a forks reader has no agent of its own, so the name is read only when `reader !== 'forks'` (the union has none otherwise). - resumeAgent makes its agent with `new Agent`, so it adds the agent to `ephemeralAgents` itself: a resumed job's usage counts in the shared entry, not under its own name. Composed rig: npm test 5408 tests, 5378 pass, 0 fail, 30 skipped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Problem
An addressed message mid-turn, a follow-up in a channel the turn engaged, or the agent's own
channel_opencan move alocusorhybridturn's locus between rounds (repinTurnLocus), and the next round's prose is delivered to the new channel.driveStreamcomputed the channel it shows once, when the stream started, and never read the locus again. So for the rest of the stream:inference:tokensandinference:content_blocktraces named the old channel. That's what After a mid-turn re-pin, token and content-block traces keep naming the old channel #555 reports: trace consumers that tag streamed text bychannelIdput the re-pinned round in the wrong conversation;Fixes #555.
Changes
driveStreamkeeps the channel it shows as a variable. At the model's content in each round (itstokensandblockevents, not the tool results a boundary reports as content), and at a refusal retry,followTurnLocuschecks it against the turn's locus. When the locus has moved, inlocusorhybridmode, it stops typing on the old channel and starts it on the new one. The locus moves only between rounds, so this happens at the round after a move. A response with nothing in it moves nothing.TurnRoutingnow records the channel the turn's indicator was last shown in, and a start, or a move, that shows it elsewhere stops that one first (showTurnTyping). Before, a re-pin at such a boundary left the old channel's indicator running beside the new one until the turn ended.main. So from the round after a move they name the new locus. Inhybridmode they name the locus whether or not a>>>envelope sends the text elsewhere: both the traces and the indicator name the conversation the turn is attending.explicitmode is unchanged: its typing and traces show the trigger channel.src/types/trace.ts's twochannelIddocs say what the field now names. The turn-start comment says what moves the locus, and the typing comment says which exits stop the indicator.What hosts and servers newly see, after a locus moves mid-stream in
locusorhybridmode:inference:tokensandinference:content_blocktraces carry the new channel;A turn whose locus doesn't move, and an
explicitturn, see no change.What each part is for
main, where the traces named "the channel this stream shows typing in". Both now say which conversation the turn is attending. Where ahybridround's text was published is delivery's to say, and the traces don't claim it.channel_opentoo, and, by our reading of membrane 0.5.87, so do the tool results it reports as content in XML tool mode, or for a result with an image. Those belong to the round before, and would otherwise start typing in the channel just opened for a final response with nothing in it.inference:tokens, comes before the retried round's first content, so it names the round's channel only if the move happens there.Tests
CI runs the typecheck, the build and the whole suite on Node 20 and 24. The runs below were on Linux with Node 24.22.0, on the tree of
e1f83494. Since then the branch has changed onlysrc/types/trace.ts's doc comments and the second changelog fragment, and mergedmain15aadc96. At that merge the typecheck passed again, and so didpresent-while-acting(69/69, withmain's three new tests),explicit-prose-routing,channel-lifecycle-order,channel-registry-routing,trunk-channel-routingandtool-result-guard. CI doesn't run the red-first or the removals.CI at
126f0dbd: run 39235670668, on GitHub's merge of this head withmain15aadc96, passed its four Build & Test legs and the packed install, and the Changelog check (run 39235670519) passed. The Build & Test legs ran 4,830 tests on Ubuntu and 4,832 on macOS, 27 of them skipped, which is 10 more thanmain's own run at15aadc96(39235309716: 4,820 and 4,822): the ten tests this change adds.present-while-acting66/66,explicit-prose-routing89/89,tool-result-guard146/146,tool-wrapper-prose-guard-integration18/18,channel-registry-routing24/24,trunk-channel-routing23/23,batched-wake-locus5/5,silent-heartbeat7/7,prose-stream-router23/23.test/present-while-acting.test.ts, on its re-pin rig, where someone addresses the agent from a DM during round 1's tool and round 2 answers:locusand inhybridmode, round 2's traces and typing move to the DM;hybridmode while a>>>destination holds, round 2 is delivered to the destination, and its traces and typing move to the DM;channel_openmoving the locus moves the next round's traces and typing (the existing test, extended);channel_openstarts no typing in the opened channel, also when the tool's result is reported as content, as in XML tool mode;locusturn that starts with nowhere to speak shows typing where itschannel_openmoves it, and stops nothing else.test/explicit-prose-routing.test.ts: anexplicitturn's typing and traces stay on its trigger channel across its rounds.main'ssrc/framework.tsand this branch's tests: the eight new and changed tests that need the change fail. The two empty-final-round tests and theexplicittest pass, as their controls.followTurnLocusacting only inlocusandhybridmodeexplicit-prose-routing("a silent turn's resume shows no locus…")showTurnTypingstopping the channel the turn last showedfollowTurnLocusdriveStream's own start of the indicator also goes through the record. By our reading nothing can move the locus between the turn's start and the stream's, so its channel is the turn start's, and with it taken out,present-while-acting,explicit-prose-routingand four of the nearest files still pass.Not verified
Out of scope
Each of these is by our reading of the code.
channels/outgoing/chunkandcomplete) still goes to the channel the stream started in after a re-pin, and inhybridmode a later stream of the turn starts at the locus whatever>>>destination or hold governs delivery. fix(streaming): the outgoing stream carries only what delivery publishes, and a script's calls count as its round's #360 replaces that stream with one fed by delivery.channelIdof an activation and keeps it.mainalready, a turn's end stops every channel's typing, other agents' included.DiscordModule's own typing keepalive, underautoTyping, keeps the reply channel it resolved when the inference started.channel_opennotices tell ahybridagent its plain speech now lands at the new locus, even while a>>>destination or hold governs it.Merge notes
main15aadc96, after fix(channels): the host posts nothing into a channel its server declared inbound-only #350 landed. The two added tests at the same place intest/present-while-acting.test.ts, and the merge keeps both sides' tests whole.main15aadc96. With this change each gains a conflict, or a conflict's text changes, insrc/framework.tsortest/present-while-acting.test.ts.changelog.d/555-repin-traces-follow-locus.fixed.md, andchangelog.d/555-trace-channel-follows-locus.changed.mdfor trace consumersReported by Anarchid in #555. Written by Milo-2653 (Claude Opus 5.5), contributing through nissa-seru, on design calls by Ada-1017 (Claude Opus 5.5). Petra-1012 (Claude Opus 5.5) read and approved the source; its last two changes are her suggestions.
Checked and opened by Fiona-2757 (Claude Opus 5.5).
🤖 Written with Claude Opus 5.5 through the Rookery's tooling.