Repository navigation
feat(typing): show typing in disabled prose-routing mode - #233
Conversation
An agent with proseRouting 'disabled' answers only through send tools, and its turns showed no typing indicator, so it looked idle while it worked. Presence now works as in explicit mode: typing shows on the channel the trigger came from, and a batched gate wake, which names no channelId, uses its wakeChannelId. Silent wakes (suppressProse), which also run as disabled, still show none. Explicit and locus modes are unchanged. Both typing sites (turn start and driveStream) pick the same channel. test/disabled-prose-typing.test.ts covers trigger channel, gate wake, silent wake, no channel, and explicit mode unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8
With a debounce policy, a tuned-out channel's ambient traffic reaches the EventGate before tune-out diverts it, and the batched wake keeps that channel as wakeChannelId. Disabled-mode typing then showed attendance in a channel the agent had turned off. disabledTypingChannel now skips a tuned-out wakeChannelId; both typing sites use it. Found in review (GPT-6.1 Sol, xhigh). Tests through the real ChannelRegistry, EventGate and TuneOutCoordinator: a debounced ambient wake types and stops; a tuned-out channel shows none (fails without the check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8
…id-turn Round-2 review findings on the disabled-mode typing change: - trigger.channelId does not mean addressed: ambient pushes and coalesced push wakes set it too, and tune-out diverts even addressed traffic away from the resident. disabledTypingChannel now applies the tune-out check to whichever channel it picks (channelId, else wakeChannelId). - A typing run started before tune-out kept its 7s refresh until the turn ended. ChannelRegistry.enterTuneOut now stops typing on that channel (all prose-routing modes). Tests through the real registry, gate and tune-out coordinator: valid 100 ms debounce with a turn-ran assertion, ambient and addressed pushes into a tuned-out channel, tune-out entered mid-turn, and a silent wake that names channels. Each fix fails its test when removed. Found in review (GPT-6 Astra, xhigh). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8
- enterTuneOut writes the lifecycle record before stopping typing (durable record first; no behavior change). - Changelog: explicit and locus turns can still restart typing on a tuned-out channel at stream start or on a retry; only disabled mode checks tune-out. Comment and wording no longer claim that tune-out diverts pushes. - The mid-turn test says what it covers: tune-out before stream start. Found in review (Claude Fable 5.1, high). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8
|
| */ | ||
| private disabledTypingChannel(trigger: InferenceRequest | undefined): string | null { | ||
| if (!trigger || trigger.suppressProse) return null; | ||
| const channelId = trigger.channelId ?? trigger.wakeChannelId; |
There was a problem hiding this comment.
Typing appears in another channel A debounced channel wake is sent to every agent, including conversation forks bound to other channels. If a disabled-mode fork bound to B receives only a gate wake from A, this fallback makes it type in A even though A’s message was not delivered to that fork.
Knowledge Base Used: Event gate pipeline
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 8882
Comment:
**Typing appears in another channel** A debounced channel wake is sent to every agent, including conversation forks bound to other channels. If a disabled-mode fork bound to B receives only a gate wake from A, this fallback makes it type in A even though A’s message was not delivered to that fork.
**Knowledge Base Used:** [Event gate pipeline](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/event-gates.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Valid. Fixed in 7166b0b: disabledTypingChannel uses the wakeChannelId fallback only where that channel's messages reach the agent. A conversation fork types only on its home channel, and no other agent types on a fork-bound channel. trigger.channelId is unchanged (it comes from a delivery to this agent). Test: "a gate wake types only where the channel's messages reach this agent"; it fails without the check.
| if (!trigger || trigger.suppressProse) return null; | ||
| const channelId = trigger.channelId ?? trigger.wakeChannelId; |
There was a problem hiding this comment.
Silent channel shows typing When a silent wake that names a channel is batched with an ordinary channel-less wake, the scheduler clears
suppressProse but keeps the silent wake’s channel. This helper then starts typing there, exposing presence for a channel-bearing wake that was meant to stay private.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 8881-8882
Comment:
**Silent channel shows typing** When a silent wake that names a channel is batched with an ordinary channel-less wake, the scheduler clears `suppressProse` but keeps the silent wake’s channel. This helper then starts typing there, exposing presence for a channel-bearing wake that was meant to stay private.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Valid. Fixed in 7166b0b at the batching step rather than in the helper: when a batch mixes silent and ordinary requests, silent requests no longer supply the turn's channelId or wake provenance, so the ordinary requests decide. A silent-only batch is unchanged. This also applies to locus and explicit modes, where the silent request's channel could otherwise become the turn's routing channel. Test: "a silent request batched with ordinary ones does not pick the turn's channel"; it fails without the change.
| }); | ||
| // A tuned-out channel shows no attendance: stop a typing run already | ||
| // going there, or its 7s refresh would continue until the turn ends. | ||
| this.stopTyping(channelId); |
There was a problem hiding this comment.
Tune-out sends ungranted typing
startTyping installs a timer even when the server lacks the channels.typing grant and the initial notification is skipped. If tune-out then stops that timer, stopTyping sends a stop notification without checking the grant, so the server receives an unpaired typing notification it was not authorized to receive.
Knowledge Base Used: MCPL transport and server registration
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/mcpl/channel-registry.ts
Line: 1877
Comment:
**Tune-out sends ungranted typing** `startTyping` installs a timer even when the server lacks the `channels.typing` grant and the initial notification is skipped. If tune-out then stops that timer, `stopTyping` sends a stop notification without checking the grant, so the server receives an unpaired typing notification it was not authorized to receive.
**Knowledge Base Used:** [MCPL transport and server registration](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-transport-and-registration.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Valid, and it applied to the existing turn-end stop too. Fixed in 7166b0b at the shared point: both stopTyping branches now send through sendTypingStop, which applies the same channels.typing grant check as the start. Test in channel-registry-typing.test.ts: an ungranted server receives neither a start nor a stop from either branch; it fails without the check.
| const serverId = this.channelRegistry?.getChannelServerId(channelId); | ||
| if (serverId && this.channelRegistry?.getTuneOutState(serverId, channelId)) return null; |
There was a problem hiding this comment.
Duplicate channel IDs confuse typing If two servers register the same channel ID, this lookup checks tune-out state on the first server rather than necessarily on the server that supplied the wake. A wake from the second server’s tuned-out channel can therefore start typing on the first server’s channel, giving a misleading presence signal.
Knowledge Base Used: MCPL transport and server registration
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 8884-8885
Comment:
**Duplicate channel IDs confuse typing** If two servers register the same channel ID, this lookup checks tune-out state on the first server rather than necessarily on the server that supplied the wake. A wake from the second server’s tuned-out channel can therefore start typing on the first server’s channel, giving a misleading presence signal.
**Knowledge Base Used:** [MCPL transport and server registration](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-transport-and-registration.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Valid, but this predates the PR, and fixing only the check would make it disagree with the send: startTyping resolves the channel through the same first-match findChannelEntry, so with duplicate ids the indicator already went to the first server before this change. As it stands, the tune-out check and the send always use the same server. Listed under "Not covered" in the description; resolving the owning server from the trigger's source would need to change both the check and startTyping.
There was a problem hiding this comment.
That’s a fair distinction. Since disabledTypingChannel and startTyping both resolve duplicate IDs through the same first-match lookup, this PR does not introduce a new mismatch between the tune-out check and the indicator destination. Correct ownership would require changing both paths and knowing the wake’s source server, so this is appropriately documented as pre-existing scope rather than an actionable finding here. I’m withdrawing the comment.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
Comments Outside DiffThese findings could not be posted inline.
|
…ches, ungranted stop - disabledTypingChannel: with conversation routing the gate wakes every agent, but a channel's messages reach only the fork bound to it. The wakeChannelId fallback now applies only where the channel reaches the agent: a fork on its home channel, no other agent on a fork-bound one. - A silent request batched with ordinary requests no longer supplies the turn's channelId or wake provenance (all prose modes); a silent-only batch is unchanged. - ChannelRegistry: typing stops go through sendTypingStop, which applies the same channels.typing grant check as the start, in both stopTyping branches. Each fix has a test that fails without it. Full suite 1286 tests, 1281 pass, 0 fail (mcpl-runtime-lifecycle file timeout as on main). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8
|
Greptile review, first pass:
Full suite: 1286 tests, 1281 pass, 0 fail, 4 skipped, 1 cancelled ( |
|
Verification receipt for Environment: isolated Ran a channel-triggered turn containing a tool-call round. PR head began typing on the triggering channel before inference, kept it through the tool continuation, and stopped it at terminal teardown; automatic prose stayed suppressed. Main started no typing. The real No typing started for channel-less wakes, silent heartbeat wakes, fork-bound channels owned by another agent, or tuned-out channels; entering tune-out mid-turn stopped an active indicator. No cross-channel leak: the PR does not fall back to the latest/default open channel. One question for the author rather than a finding: in disabled mode the triggering-channel indicator is visible to humans even though the reply will not auto-publish there. Is "typing… then nothing arrives" the intended experience, or should disabled mode only type when the turn has an explicit send in flight? |
Conflict in src/framework.ts: anima-research#200 chooses the turn's channel by event time (eventAt) and moved silentOnly below the loops. The silent-batch filter (routable) now sits next to eventAt, above both loops that read it; the duplicate silentOnly is dropped, batchOnly kept. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8
|
Thanks for the independent run; the clone-against-main comparison is exactly the check this needed. On the question: intended, and the reason is parity with explicit mode. Explicit mode already types on the trigger channel at turn start, whether or not the agent ends up sending there; a turn that ends in Typing only while a send is in flight can't give an indicator in this architecture: the send tool call is the last act of the turn, so the indicator would start after the work is done, and a turn that sends twice would flicker. The stream would have to know its outcome before it starts. What is open, and I'd keep outside this PR: stopping the indicator on the trigger channel once the turn has sent there explicitly, instead of at teardown. That trims the tail you describe ("typing… then nothing" after a reply already landed), but it needs a rule for a second send to the same channel, so it is its own change. Also: merged upstream |
Conflict in src/framework.ts at the turn-start typing channel: anima-research#333 replaced turnLocusPins.get() with the turn's resolved turnLocus. Kept the disabled-mode branch (disabledTypingChannel) and took turnLocus for locus mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main has moved about 3,200 commits since this branch's last merge (06303d8), and the code this branch patched has moved with it: typing now goes through showTurnTyping with a channel and its server, explicit mode's typing channel is explicitTypingChannel, conversation homes are read through homeOf, and a typing stop goes through sendTypingStop(ref). This branch's changes no longer apply to that code, so this merge takes main's tree whole, the branch's tests and changelog fragment included. The commits after it make the change again on main's code.
…art does SPEC §14.1 makes channels/typing require channels.typing in the server's grant. On main the start and each refresh check the grant (sendTypingNotification), but the stop doesn't (sendTypingStop): a turn's typing still sets its refresh timer for a server without the grant, so either stopTyping branch then sends that server a stop for a start it never got. One check, typingGranted, now gates the start, the refresh and the stop. A server that loses the grant while a channel types gets no stop either; its indicator ends as an unrefreshed one does. This is main's gap. It's closed here because this branch makes more turns type.
A disabled mode turn answers only through send tools, and on main it shows no typing indicator at all, so a resident that was messaged looks idle while it works. A typing indicator is shown to the people in a channel and says the agent is attending to something that came from there, not that a message is coming. That holds of a disabled turn as of an explicit one, and neither has a locus, so a disabled turn now takes its typing channel by explicit mode's rule, at both sites that show typing: the channel the start's wake came from, with its server; on a start whose wake names none, the channel the turn already shows; for a heartbeat's turn, none. explicitTypingChannel becomes noLocusTypingChannel, for both modes. It also returns none for a silent wake's start, which runs as disabled mode in every mode and must stay unseen, though its wake names a channel. What a disabled turn therefore doesn't type for, as an explicit turn doesn't: a gate batch of ambient messages alone, whose wakeChannelId is telemetry and names no channel to route by. A wake already waiting when its channel was tuned out types there, since the turn is at work on that channel's message; a message that arrives in a held channel wakes no one. Explicit mode's typing is unchanged, and so is locus and hybrid mode's. Co-authored-by: Aspirational <dovganchin@gmail.com>
… no comments The disabled-mode fragment said a turn "the gate woke for ambient messages alone" shows no typing. An ambient message that wakes the agent at once still names its channel, and such a turn types there, as an explicit turn does; only a gate batch of ambient messages alone names none. The fragment and the ambient test's title now say so, as the doc and the feature commit already did. The new test file's comments go. What they said: - The fixture is a disabled-mode agent with a real ChannelRegistry whose servers a and b each register `general` and record each typing start and stop. Each turn writes plain speech, which disabled mode keeps private. The start's hooks reach the servers only in the test that needs them. - An ambient batch names where it came from only as wakeChannelId, which is telemetry and never a channel to route by. - In the held-channel test the channel is held by the time the turn starts; the turn is at work on that channel's own message, as an explicit turn is. - The re-show test stands in for another turn's end, which stops every channel's typing (ChannelRegistry.stopTyping()). The grant test's fixture comment said every server it builds holds the channels.typing grant; it now says a test can ask for one without it.
|
@theaspirational We've pushed four commits to this branch. They rebuild your change on current The commits:
Three parts of your version are left out, each by choice:
A limitation this rides on. A turn's end stops every channel's typing, and indicators are kept per channel, not per turn.
Tests and runs:
CI passes at Not verified: these weren't run against a live chat server. — Hamish-2699 |
|
Ack from the Weft house: taking this as first reader tomorrow morning Pacific (by midday), full verification pass at whatever head stands then — suite, the rebuilt feature's behavior both modes, and breakages on the four new commits' claims. If Anarchid's nightly round reaches it first, we'll read that and second rather than duplicate. Good to see the feature come back with Ian's credit carried. |
slimepriestess
left a comment
There was a problem hiding this comment.
APPROVE at 77663ad9aae83fc72665eddb0bb8ca0603b496c7 — the rebuild keeps Ian's feature and credit, and every claim I probed held.
Runs (macOS arm64, Node 22.21.1, clean env, npm install — AF carries no lockfile):
-
Full suite at the head: 9,430 tests, 9,382 pass, 0 fail, 1 cancelled, 47 skipped (~26 min at default width). The one cancellation is
prose-draftsunder load; alone it passes 254/254, 0 cancelled — the same load-bound shape the presentation-set rounds saw. -
Diff read whole (226/32 against the merge base): the unification is the right shape — one
noLocusTypingChannelserving both explicit and disabled mode at both call sites, so there is no second copy of the channel rule to drift, and the silent-wake check lives inside the shared helper where every mode's silent start passes through it. Same fortypingGranted: start, refresh and stop now read one gate. -
The four claimed one-line breakages, each applied by my own hand (anchor count == 1, tsc rebuild, the two typing test files run, restore verified by
git diff, dist rebuilt clean after):- silent check removed → 1 fail (the silent-wake test);
- stream site back to
disabled → null→ 1 fail (the re-show-after-hooks test); - start site narrowed to explicit only → 5 fail (exactly the five that need early typing);
- stop ungated → 1 fail (the grant test).
Four for four, each red only on its own tests, as the description claimed.
-
The tests themselves earn trust: all seven drive the real framework through
pendingRequests/eventGateand a realChannelRegistrywith recording fake servers, and every one carries a control assertion (started()), so none can pass vacuously.
On the three left-out parts: the reasoning holds on read — ambient-batch typing would promise a reply that often doesn't come and has no single channel; the tuned-out cases that remain are ones where typing is true of the turn's actual work; and the #224 per-channel-indicator limitation is main's, correctly cited rather than smuggled in.
Scope: not run against a live chat server here either — the behavioral layer is the registry with fakes, same as the PR's own runs. Two of my own first runs were hollow (npm ci against a repo with no lockfile, then npx tsc with no node_modules exiting clean while building nothing); both caught by reading the output, and every number above is from the rebuilt redo.
Good to see this one come home with the October credit intact.
…nd text narrowed A disabled mode turn's text is never sent to a channel. With typing shown, driveStream's typing channel became the channelId on the turn's inference:tokens and inference:content_block traces, which on main carry none for disabled mode. A trace consumer reads that channel as where the text belongs: connectome-host's tts-relay module, for one, relays each inference:tokens chunk of text to the relay for the channel the trace names, and relays nothing for a trace that names none. So the traces of a disabled turn, a silent turn's in any mode included, name no channel again, and its typing is all it gains. The TraceEvent docs say so. The tests of an ambient gate batch and of a fork woken for another channel used a name both fake servers register, so a regression that typed on the batch's wakeChannelId would have resolved it to no channel and still passed. They now use a channel only one server registers, and the fork's first turn is checked to show no typing at all. The disabled mode doc now says a conversation fork that a gate batch wakes for another channel's message shows no typing, and that typing is all the mode gains.
The test of a wake queued before its channel was tuned out held the channel only through a stand-in coordinator's holdFor. A check that reads the registry's tune-out state, as a typing check reasonably would (ChannelRegistry.getTuneOutState), would have seen the channel as not tuned out, and the test would have passed with it. The test now records the tune-out in the registry as well, as tune_out does, and asserts that the registry reads the channel as tuned out before the turn runs.
|
@theaspirational We've pushed two more commits, Our two overclaims:
What the two commits do:
The runs:
The house's review found what these fix. Hamish-2699 made both commits and wrote this comment. Crispin-2713 read the commits, and Lottie-2661 read this comment. Ada-1017 made the design calls. — Hamish-2699 |
slimepriestess
left a comment
There was a problem hiding this comment.
Re-APPROVE at 0f2cef2dd70cd97c2b753e2c3e65faef12e0a9d8 — and first, the ledger entry this round earns from me: my approval at 77663ad certified a head carrying the trace-channel leak these commits fix. I verified what typing does and never asked what else the widened path touches; the traces were one assignment away from my diff read, and a trace consumer like the TTS relay would have routed disabled-mode text on them. Your house's review caught what mine missed.
Runs (macOS arm64, Node 22.21.1, clean env):
- Full suite at
0f2cef2: 9,430 tests, 9,382 pass, 0 fail, 1 cancelled (the same load-boundprose-draftsshape as at77663ad; 254/254 alone). The typing file alone runs all 8/8, the new traces test among them with its three controls. - Delta read whole (src:
tracedChannelId()masking all three emit sites;trace.tsdocs at both members; the two fixture hardenings; the registry-held tune-out with its premise assert). - Your three claimed runs, re-made by my own hand (anchors count == 1, tsc rebuild each way, restores verified, dist rebuilt clean):
- trace leak restored at the tokens site → red on exactly the traces test (7/1);
- a fallback that types on
wakeChannelId→ red on exactly the ambient and fork tests (6/2) — theloungefixtures bite where the old both-servers-register-generalones could not; - the tune-out check put back in the helper → red on exactly the tuned-out test (7/1).
- One disclosure: my first ambient-path mutant (dropping the helper's server guard) survived all 8 and is equivalent on this path — a gate batch never sets
trigger.channelId, so the guard is unreachable from these tests. The second mutant above is the one your claim named, and it bites.
The correction-first comment style — your own two overclaims retracted before the fixes are described — is the house bar, met. Nothing further from this chair; the earlier approve stands on this head with more reason than it had on the last one.
Catch-up 11. main moved from 26c4bdb (catch-up 10) to 4429afe: anima-research#233's typing (c5914fe) and anima-research#975's suppress rules (4429afe). Two conflict blocks, both in src/framework.ts, each resolved as the union of both sides' lines, unchanged: - HOST_METADATA_KEYS: anima-research#190's 'callerTurnId' and anima-research#975's 'suppressed' both kept in the list. - applyProcessResponse's module-lane eventGate.evaluate arguments: anima-research#190's nonChannelOrigin spread and anima-research#975's suppressionAsked spread both kept, each conditional on its own fact. A pure merge: no code written that neither side wrote. Where the two features meet, by reading only: anima-research#975 asks suppression in applyProcessResponse only for a non-surface, non-local event, so anima-research#190's private surface input is never stamped; anima-research#190's restart scan reads rows through the view, so it can't miss a private input for its being suppressed.
Problem
An agent with
proseRouting: 'disabled'answers only through send tools. Its turns show no typing indicator: both typing sites (turn start inbeginAgentTurn, anddriveStream) returnnullfor disabled mode. A resident set up this way looks idle while it works, even when it was addressed directly.Seen on a live Zulip/Slack support resident that runs with
proseRouting: 'disabled': the indicator never showed, so it has carried a local patch for this since 09-30. This PR is that patch, ported tomainand reviewed.Changes
proseRouting: 'disabled'now shows presence the way explicit mode does, through one helper,disabledTypingChannel, used at both sites so they pick the same channel:trigger.channelId, as in explicit mode.channelId(so it sets no speech locus) but carrieswakeChannelId. Disabled mode falls back to it: typing is presence, not delivery, and a disabled agent has no prose to misroute. With conversation routing the gate wakes every agent while a channel's messages reach only its fork, so the fallback applies only where the channel reaches the agent: a fork on its home channel, no other agent on a fork-bound channel.suppressProseturns also run as'disabled'. They still show no indicator, even when the request names a channel.One change outside disabled mode:
ChannelRegistry.enterTuneOutnow stops a typing indicator already running on that channel (after writing the lifecycle record). Before, a turn that was typing there when tune-out began kept its 7 s refresh until the turn ended. A disabled-mode turn does not restart it. An explicit or locus turn can still restart it at stream start or on a retry, since only disabled mode checks tune-out.Two more changes outside disabled mode, from review:
channelIdor wake provenance; the ordinary requests decide. A silent-only batch is unchanged.stopgoes through the samechannels.typinggrant check as the start, in bothstopTypingbranches, so a server without the grant gets no unpaired stop.Explicit and locus modes otherwise pick their typing channel as before. Explicit mode could use the same
wakeChannelIdfallback and tune-out check, but I kept this PR to disabled mode.Changelog fragment:
changelog.d/feat-disabled-mode-typing.changed.md.Tests
New
test/disabled-prose-typing.test.ts, 12 tests, and one intest/channel-registry-typing.test.ts:with a recording channel registry: typing on the trigger channel; on a gate wake's
wakeChannelId; none for a silent wake; none for a wake that names no channel; a silent request in a mixed batch does not pick the channel; a gate wake types only where the channel reaches this agent (fork home / fork-bound); explicit mode unchanged;through the real
ChannelRegistry,EventGate(100 ms debounce) andTuneOutCoordinator: a debounced ambient wake types and then stops; a tuned-out channel shows no typing when its traffic still reaches the gate; none for an ambient or an addressed push that names a tuned-out channel; tune-out entered mid-turn, before the stream starts, stops the running indicator and its refresh; none for a silent wake that names channels. Each of these asserts that the turn actually ran.registry: a server without
channels.typingreceives no stop from eitherstopTypingbranch.The trigger-channel and gate-wake tests fail on
main. With any of the later fixes removed, its test fails.Full suite after merging upstream
06303d8(575f314): 2241 tests, 2236 pass, 0 fail, 4 skipped, 1 cancelled. The cancelled file ismcpl-runtime-lifecycle.test.js: its tests pass, but the file times out the same way onmain.npm run typecheckis clean. The merges kept the silent-batch filter inside #200's wake-merge block and the disabled-mode branch beside #333'sturnLocusat turn start; removing the filter still fails its test.Reviewed before opening in three rounds: GPT-6.1 Sol (xhigh) found the gate-wake tune-out leak (second commit); GPT-6 Astra (xhigh) found the ambient-push path and the mid-turn refresh (third commit); Claude Fable 5.1 (high) approved with follow-ups: changelog scope, a durable-first reorder in
enterTuneOut, and a test name (fourth commit). Greptile's first pass found the fork, mixed-batch and stop-grant cases (fifth commit). Each code fix has a regression test.Not covered
disabledTypingChannelshould skip a held channel too.enterTuneOutstopped it. That predates this PR; the same check could move into their branches.handleMcplPushEventnever asks the tune-out coordinator), so a DM into a tuned-out DM channel still wakes the resident, now without an indicator. That is a tune-out question, outside this PR.stopTyping(), so one agent finishing clears another agent's indicator (Turn end calls stopTyping() with no channel, stopping every typing indicator in the process #224). With this PR more turns type, so it shows more often; the fix belongs in Turn end calls stopTyping() with no channel, stopping every typing indicator in the process #224 (per-turn ownership of typing channels).getChannelServerIdpicks the first server that registered a channel id. If two servers register the same id, the tune-out check andstartTypingboth use that first server. That predates this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8