Repository navigation
Agent typing indicator stuck on: Product and Architect permanently show as typing #36
Description
Problem
In the Zoon web client, the typing strip above the composer shows Architect is typing… (and Product is typing…) and never clears — both agents read as permanently busy, including right now while this issue was being written and both agents are idle. Other members of the same room (Coding, CPO, UX Tester) don't show as stuck, so this isn't a client-wide rendering fault.
Room: !s7vsGwJuGHKd6z3Lwb:zooid.zoon.eco (#product) on zooid.zoon.eco. Affected identities: @cloud.product:zooid.zoon.eco, @ori-macbook.architect:zooid.zoon.eco. Observed 2026-09-25.
Why it matters
A stuck typing indicator is indistinguishable from a hung agent: it tells a human in the room that the agent is working when it isn't, and it trains people to ignore the indicator entirely — so when a genuinely slow turn does show typing, nobody reads it. [[ZOD029]] calls this out explicitly: "Stuck 'typing forever' reads as a hung agent — worse than no indicator."
Acceptance criteria
- The root cause is identified and written down in the issue, with the turn/room that reproduces it. In particular, distinguish the two candidate causes: (a) the daemon is actively re-sending
typing=true— a 25s refresh interval that outlived its turn, or aagents.prompt(...)that never settles sorunTurn'sfinallynever runs; (b) the daemon stopped sending anything, and the client is not honouring the 30s TTL that the lastm.typingcarried, so the indicator never expires. - An agent that finishes a turn (success, throw, or cancel/interrupt) is not typing in the room more than a few seconds later, in a fresh room and in a thread.
- An agent whose daemon is killed mid-turn stops showing as typing within the 30s TTL — this is the backstop the spec relies on and is currently not observable.
- Delegated/subagent turns ([[ZOD072]] thread-fanout-delegation, [[ZOD071]] per-handoff session isolation) leave the parent and the child in the correct typing state: only the agent whose turn is actually in flight shows typing.
- A regression test covers the failing path —
setTyping(false)is called (or the refresh interval is cleared) for the turn shape that was stuck, not only for the plain success case the existing tests already cover.
Pointers
packages/transport-matrix/src/transport.ts—runTurn: thesetTyping(true)+ 25ssetIntervalrefresh and thefinallythat clears both and PUTstyping=false. This is the spec'd invariant and the first place to look.packages/transport-matrix/src/matrix-client.ts—setTyping; note{ typing: false }is sent withouttimeout, so a dropped or failed off-PUT leaves only the TTL to clear it.- Client side, for cause (b) only:
packages/web/src/hooks/use-typing.tsandpackages/web/src/components/rooms/typing-indicator.tsxinzooid-ai/clients([[ZNC002]] cycle ZNC004) — the strip only reflectsRoomMember.typing. - Check whether the stuck agents differ from the working ones by turn shape, not by host: Product and Architect are the two that ran long or delegated turns in this room.
Blocked by
- none
Spec: [[ZOD029]]
Activity
zooidbot commentedon Sep 25, 2026
Triaged into a new spec: [[ZOD088]] — Typing lifecycle, a room-scoped, in-flight turn signal.
PR: https://github.com/zooid-ai/product/pull/29
[[ZOD029]] is
shippedand already states the invariant this violates ("Whether the prompt resolved or threw, typing must drop"), so the diagnosis — refresh interval outliving its turn, versus a client not honouring the 30s lease — stays here in the issue. The spec settles the contract underneath, and three parts of it are not bug-shaped:m.typingis keyed by(room, user), but the shipped bookkeeping is per turn. That was correct when an agent had at most one turn in flight per room; [[ZOD071]] and [[ZOD072]] made concurrent sessions per agent normal, so a turn ending can now lower an indicator another live turn is relying on. The spec makes it a per-(room, agent)in-flight set, edge-triggered on the wire, with the 25s refresh owned by the count instead of the turn.promptnever settles re-arms the lease forever. Only a turn-liveness budget catches that, and [[ZOD072]] § Limits declines one on purpose — the spec names the limit instead of hiding it.awaiting_humankeeps the turn open across a human answer, which reads as typing for as long as the human takes. Typing gets a meaning it can keep: working, not open.The daemon half of the cycle fixes the symptom you are seeing; the client half turns your third acceptance criterion — the killed-daemon TTL backstop, currently not observable — into a test. Both halves ship in one PR.
Two calls worth your review: the elicitation pause (#3) is scope I added beyond the report, and seq 045 puts it next to the fan-out work that created the concurrency.
zooidbot commentedon Sep 28, 2026
Spec ID correction: the typing-lifecycle spec linked above is ZOD091 (product#29), not ZOD088. That PR is open and waiting on review.
zooidbot commentedon Oct 5, 2026
Fixed by #108 (the daemon now lowers typing itself, as the room aggregate of in-flight turns) and clients#53. Remaining Tuwunel limitation: an expired typing lease is never pushed to an incremental /sync, so a crashed daemon can still leave a stale indicator in an open client until something else changes typing in that room. That's pinned by packages/cli/tests/typing-lease.integration.test.ts and isn't fixable on our side.