Repository navigation
feat(typing): show typing in disabled prose-routing mode - #233
theaspirational wants to merge 7 commits into
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>
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