Skip to content

No consumed-cursor visibility: agents reply to a stale event prefix; senders can't tell messages aren't being consumed #60

Description

@mostlydev

Summary

During a two-agent review/implement session (claude as reviewer, codex as implementer, v0.10.0), we hit the same failure three times in one hour: one agent replies to a stale prefix of the event stream, and the other agent has no way to see that its messages are not being consumed. The cost was three redundant round-trips and one wrong edit that was applied and then had to be reverted — under a production freeze where turnaround mattered.

Timeline (single session, room 82877902, 2026-08-03)

  1. 12:19 — codex goes idle holding the turn. tt health shows listener.active: false, no standby registered. Its OS process is alive, so nothing about the room looks wrong from its side.
  2. 12:33–13:20 — claude sends events 12233–12238 (a gate-red warning, a five-failure diagnosis, a fix spec). Every tt msg send returns success. Nothing indicates the recipient has consumed none of them. The only way claude discovered this was tt health (listener scan) plus ps on the codex pid.
  3. Codex is externally nudged, catches up "through event 12238", and replies — while 12241/12242 (a retraction of one of the fixes it is about to apply) already exist. It applies the retracted fix.
  4. Codex then asks a four-point sync question already answered in event 12245 and announces it is "holding further edits until your reply" — a reply that had been in the stream for several minutes.
  5. Claude sends a condensed "you are not waiting on me, read 12245" message. Third round-trip for zero new information.

Why this is a tool gap and not just harness misuse

Reply-before-drain by the codex harness is real, but the tool gives neither side the affordance to catch it:

  • The consumed cursor (event_cursor_seq) lives only in each client's local cli-sessions.json (src/session-store.ts:16). The service has no per-member notion of "consumed through seq N."
  • sendMessage (src/service.ts:1266) returns the new event_seq but nothing about the recipient: not its last-consumed seq, not a pending count, not even last_wait_at. A sender cannot distinguish "delivered into a live listener" from "accumulating unread."
  • tt state members carry last_seen_at, but it is touched by any CLI invocation (touchMember), so it reads as fresh even when the member has consumed nothing — it measures liveness, not consumption. (Presence/liveness tracks the guardian, not the harness: live harness reads as inactive; orphaned guardians hold the turn forever #29 covers the liveness half; this issue is the consumption half.)

Proposals, in increasing order of ambition

  1. Track last_consumed_event_seq per member server-side, updated whenever wait/try/events actually returns events to that member. Expose it on state/health member rows. This is the primitive everything else needs, and the data is already computed client-side — it is just stored in the wrong place.
  2. msg send response includes recipient staleness: recipient_last_consumed_event_seq and recipient_pending_count. One field would have told claude at 12:33 that codex was consuming nothing, instead of an hour later via ps.
  3. Sender-side stale-reply warning: msg send (and release/assign handoffs) warns when the sender's own consumed cursor is behind the room head — "you are replying while N unconsumed events are pending, M addressed to you." This is the one that would have prevented the retracted-fix application directly: codex sent its plan while 4 events behind, 2 of them addressed to it.
  4. Optional: msg send --if-current <seq> compare-and-send guard that refuses when the room head has moved past <seq>, for protocol-critical replies like contract ACKs.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions