Skip to content

feat(typing): show typing in disabled prose-routing mode - #233

Merged
nissa-seru merged 13 commits into
anima-research:mainfrom
theaspirational:feat/disabled-mode-typing
Nov 3, 2026
Merged

nissa-seru merged 13 commits into
anima-research:mainfrom
theaspirational:feat/disabled-mode-typing

Conversation

@theaspirational

@theaspirational theaspirational commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Problem

An agent with proseRouting: 'disabled' answers only through send tools. Its turns show no typing indicator: both typing sites (turn start in beginAgentTurn, and driveStream) return null for 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 to main and 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 channel. Typing shows on trigger.channelId, as in explicit mode.
  • Batched gate wake. A gate wake deliberately names no channelId (so it sets no speech locus) but carries wakeChannelId. 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.
  • Never a tuned-out channel. Tune-out diverts a channel's incoming traffic, addressed included, away from the resident, but an ambient push, a coalesced push wake or a debounced gate wake can still name the channel. Typing there would show attendance the agent turned off, so the helper returns no channel for a tuned-out one.
  • Silent wakes stay private. Since the silent heartbeat change, suppressProse turns also run as 'disabled'. They still show no indicator, even when the request names a channel.

One change outside disabled mode: ChannelRegistry.enterTuneOut now 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:

  • A silent request batched with ordinary ones no longer supplies the turn's channelId or wake provenance; the ordinary requests decide. A silent-only batch is unchanged.
  • A typing stop goes through the same channels.typing grant check as the start, in both stopTyping branches, 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 wakeChannelId fallback 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 in test/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) and TuneOutCoordinator: 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.typing receives no stop from either stopTyping branch.

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 is mcpl-runtime-lifecycle.test.js: its tests pass, but the file times out the same way on main. npm run typecheck is clean. The merges kept the silent-batch filter inside #200's wake-merge block and the disabled-mode branch beside #333's turnLocus at 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

🤖 Generated with Claude Code

https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8

theaspirational and others added 4 commits October 5, 2026 21:43
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
@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 1/5 Tier: apex

[Medium risk] Adds typing indicator support to disabled prose routing mode.

The PR does not appear safe to merge until wrong-channel presence, concurrent typing teardown, and the ungranted stop notification are addressed.

Findings

  1. P1 Typing appears in another channel ▶
  2. P1 Silent channel shows typing ▶
  3. P1 Tune-out sends ungranted typing ▶
  4. P2 Duplicate channel IDs confuse typing ▶
Fix with agent prompt
### Issue 1
src/framework.ts:8882
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.

### Issue 2
src/framework.ts:8881-8882
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.

### Issue 3
src/mcpl/channel-registry.ts:1877
`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.

### Issue 4
src/framework.ts:8884-8885
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.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds typing presence to disabled-prose turns, using trigger or batched-wake channel provenance, and stops existing typing when a channel enters tune-out.

  • The new presence path needs correction for broadcast gate wakes, mixed silent wakes, concurrent agents, and duplicate channel IDs.
  • The tune-out stop path also needs to respect the typing capability grant.

Reviews (1) · Last reviewed commit: "fix(typing): round-3 follow-ups: changel..."

Comment thread src/framework.ts Outdated
*/
private disabledTypingChannel(trigger: InferenceRequest | undefined): string | null {
if (!trigger || trigger.suppressProse) return null;
const channelId = trigger.channelId ?? trigger.wakeChannelId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/framework.ts Outdated
Comment on lines +8881 to +8882
if (!trigger || trigger.suppressProse) return null;
const channelId = trigger.channelId ?? trigger.wakeChannelId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/mcpl/channel-registry.ts Outdated
});
// 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/framework.ts Outdated
Comment on lines +8884 to +8885
const serverId = this.channelRegistry?.getChannelServerId(channelId);
if (serverId && this.channelRegistry?.getTuneOutState(serverId, channelId)) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@greptile-apps

greptile-apps Bot commented Oct 5, 2026

Copy link
Copy Markdown

Comments Outside Diff

These findings could not be posted inline.

  • P1 One turn stops another’s typing src/framework.ts:9349 ▶

    Disabled-mode turns now start typing, but each turn’s cleanup still calls the registry-wide stopTyping(). If two agents are working in different channels, the first to finish clears the other agent’s indicator and refresh timer while that agent is still working.

…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
@theaspirational

Copy link
Copy Markdown
Contributor Author

Greptile review, first pass:

  • Fixed in 7166b0b, each with a regression test that fails without the fix:
    • fork-bound gate wake typing on another agent's channel (disabledTypingChannel checks that the channel reaches this agent);
    • a silent request in a mixed batch picking the turn's channel (silent requests no longer supply channelId or wake provenance when ordinary ones are present);
    • an unpaired stop sent to a server without channels.typing (both stopTyping branches go through the grant check).
  • Not changed: duplicate channel ids across servers. startTyping already uses the same first-match lookup, so the tune-out check and the send agree. Listed under "Not covered".
  • Outside the diff: the registry-wide stopTyping() at turn end clearing another agent's indicator predates this PR and is tracked in Turn end calls stopTyping() with no channel, stopping every typing indicator in the process #224.

Full suite: 1286 tests, 1281 pass, 0 fail, 4 skipped, 1 cancelled (mcpl-runtime-lifecycle.test.js, which times out the same way on main). Description and changelog fragment updated.

@rufalupa666-netizen

Copy link
Copy Markdown

Verification receipt for 7166b0b, compared with main 02adf27.

Environment: isolated /tmp clones on AF 0.21.0, a disabled-mode resident with a registered Discord-shaped channel that advertises typing; production resident and live Discord untouched.

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 ChannelRegistry path emitted paired start/stop and cleared its refresh interval. Focused suites 19/19.

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
@theaspirational

Copy link
Copy Markdown
Contributor Author

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 skip_reply resolves the indicator at teardown. Typing means "attending", not "a message is coming", and a disabled-mode resident that was addressed is attending. Before this PR it looked idle for the whole turn, which read as "not received".

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 02adf27 into the branch (37fc06a). #200 rewrote the wake-merge block, so the silent-batch filter moved next to its new eventAt; behaviour unchanged, the mixed-batch test and batched-wake-locus both pass.

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>
Hamish-2699 and others added 4 commits November 3, 2026 01:12
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.
@nissa-seru

Copy link
Copy Markdown
Contributor

@theaspirational We've pushed four commits to this branch. They rebuild your change on current main, since the code it patched has been rewritten since October. Your commits stay in the history, and the feature is yours, rebuilt: the feature commit credits you as co-author.

The commits:

  • 1e7145bbfb merges main (26c4bdb1bf) and takes its tree whole. Typing now goes through showTurnTyping with a channel and its server, and explicit mode has its own typing helper, so most of the earlier diff no longer applied.

  • 888926ce90 is your stop fix, as its own commit: a typing stop now needs the channels.typing grant, as its start and each refresh do. The gap is main's. It's closed here because this branch makes more turns type.

  • 8561c1e5ca is the feature. A disabled turn takes its typing channel by the rule explicit mode's turns already follow:

    • the channel the start's wake came from, with its server;
    • on a start whose wake names none, the channel the turn already shows;
    • nothing for a heartbeat's turn or a silent wake.

    Explicit mode's helper becomes noLocusTypingChannel, and both typing sites use it for explicit and disabled mode. Explicit, locus and hybrid mode type as before.

  • 77663ad9aa narrows the changelog fragment and takes the new tests' comments out.

Three parts of your version are left out, each by choice:

  • Typing for a gate batch of ambient messages, from its wakeChannelId.
    • To people in a channel, a typing indicator reads as a reply coming, and an ambient wake's turn often ends without speaking there.
    • wakeChannelId is documented as telemetry only, and a batch from several channels has no one channel to type in.
    • Explicit mode doesn't type for such a batch on main either.
  • No typing in a tuned-out channel. On main, a message that arrives in a held channel wakes no one. Two cases are left: a wake that was already waiting when its channel was tuned out, and typing a turn already shows when its channel is tuned out, whoever tuned it out, which goes on until that turn ends. Either way the turn is working on that channel's message, so typing there is true, as it is for an explicit turn.
  • The other two changes outside disabled mode:
    • A silent request in a mixed batch no longer choosing the turn's channel would change where every mode's turn is, for a case no request on main makes: no silent wake there names a channel.
    • enterTuneOut stopping typing already running works only partly outside disabled mode, as your description said, and inside it the typing it would stop is true, as above.

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:

  • test/disabled-prose-typing.test.ts has seven tests, through a real ChannelRegistry with recording fake servers:
    • an addressed message types in its channel until the turn ends;
    • an addressed gate batch types there;
    • a gate batch of ambient messages alone types nowhere;
    • a fork types on its home only;
    • a silent wake types nowhere;
    • a wake queued before its channel was tuned out types there;
    • typing stopped during a start's hooks shows again when the stream starts.
  • test/channel-registry-typing.test.ts has one more test, for the stop's grant.
  • At 77663ad9aa: npx tsc exits 0, and the nine affected test files pass 446 of 446.
  • On the code without the change, run at 8561c1e5ca's tree, which differs from the head only in test comments, one test title and the changelog fragment's wording:
    • With framework.ts taken back to before the feature commit, the five tests that need the change fail, and so does the existing test that calls the helper by its new name.
    • With main's channel-registry.ts, the grant test fails.
  • Four one-line breakages, each failing only its own tests:
    • no silent check;
    • the stream's site showing nothing for disabled mode;
    • the start's site for explicit mode only;
    • the stop not gated.

CI passes at 77663ad9aa, its full Build & Test included.

Not verified: these weren't run against a live chat server.

— Hamish-2699

@slimepriestess

Copy link
Copy Markdown
Contributor

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 slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-drafts under 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 noLocusTypingChannel serving 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 for typingGranted: 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/eventGate and a real ChannelRegistry with 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.

Hamish-2699 added 2 commits November 3, 2026 11:51
…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.
@nissa-seru

Copy link
Copy Markdown
Contributor

@theaspirational We've pushed two more commits, 75fd8470b3 and 0f2cef2dd7. Two lines of our last comment also claimed more than the code shows, so we correct them here first.

Our two overclaims:

  • "a fork types on its home only", in the list of tests. The test pins less than that: a gate batch routed to another channel doesn't move a fork's typing there. A wake addressed straight to a fork from another channel, such as a push naming the fork, does type in that channel, as an explicit mode fork's does on main.
  • "On main, a message that arrives in a held channel wakes no one." It doesn't wake the resident. The tune-out's reader can still be woken, with no channel.

What the two commits do:

  • A disabled turn's stream traces name no channel again.
    • What had changed: once the turn showed typing, its typing channel became the channelId on its inference:tokens and inference:content_block traces. On main, disabled mode gives them none.
    • Why it matters: a trace consumer can read that channel as where the text belongs. connectome-host's TTS relay, for one, relays each chunk of text for the channel its trace names.
    • Now: a disabled turn's text, which is never sent to a channel, stays channel-less in its traces too, and typing is all the mode gains. The TraceEvent docs say a disabled turn's traces name no channel, though it may show typing.
  • Two tests now catch what they're for. The ambient batch test and the fork test used a channel name both fake servers register. So a regression that typed on the batch's wakeChannelId would have resolved to no channel and passed. They now use a name only one server registers.
  • The doc says that a fork woken by a gate batch for another channel's message shows no typing. The doc and the changelog fragment both say that typing is all disabled mode gains.
  • The tuned-out test now tunes its channel out in the registry too (0f2cef2dd7), as tune_out does, so a typing check that reads getTuneOutState, as your version's did, now fails it. Before, only a stand-in coordinator held the channel.

The runs:

  • At 0f2cef2dd7: npx tsc exits 0, and twelve affected test files pass 691 of 691.
  • At 75fd8470b3, with framework.ts taken back to 77663ad9aa: only the new traces test fails. There the traces carry the typing channel.
  • At 75fd8470b3, with a breakage that types on wakeChannelId: the ambient and fork tests fail. Under 77663ad9aa's tests, every test passes with it.
  • At 0f2cef2dd7, with your tune-out check put back in the typing helper: the tuned-out test fails. Under 75fd8470b3's tests, every test passes with it.

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 slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-bound prose-drafts shape as at 77663ad; 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.ts docs 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) — the lounge fixtures bite where the old both-servers-register-general ones 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.

@nissa-seru
nissa-seru merged commit c5914fe into anima-research:main Nov 3, 2026
12 checks passed
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Nov 4, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants