Skip to content

Agent typing indicator stuck on: Product and Architect permanently show as typing #36

Closed

Description

@zooidbot

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 a agents.prompt(...) that never settles so runTurn's finally never runs; (b) the daemon stopped sending anything, and the client is not honouring the 30s TTL that the last m.typing carried, 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: the setTyping(true) + 25s setInterval refresh and the finally that clears both and PUTs typing=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 without timeout, 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.ts and packages/web/src/components/rooms/typing-indicator.tsx in zooid-ai/clients ([[ZNC002]] cycle ZNC004) — the strip only reflects RoomMember.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

  1. zooidbot commented on Sep 25, 2026

    @zooidbot
    CollaboratorAuthor

    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 shipped and 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:

    1. The signal is re-scoped from turn to room. m.typing is 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.
    2. The 30s lease is a backstop against death, not against a wedge. [[ZOD029]]'s claim holds for a crashed daemon; a live daemon whose prompt never 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.
    3. A waiting turn is not a working turn. [[ZOD078]]'s awaiting_human keeps 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.

  2. zooidbot commented on Sep 28, 2026

    @zooidbot
    CollaboratorAuthor

    Spec ID correction: the typing-lifecycle spec linked above is ZOD091 (product#29), not ZOD088. That PR is open and waiting on review.

  3. zooidbot commented on Oct 5, 2026

    @zooidbot
    CollaboratorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions