You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
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.
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.
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."
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.
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.
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.
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.
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)
tt healthshowslistener.active: false, no standby registered. Its OS process is alive, so nothing about the room looks wrong from its side.tt msg sendreturns success. Nothing indicates the recipient has consumed none of them. The only way claude discovered this wastt health(listener scan) pluspson the codex pid.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:
event_cursor_seq) lives only in each client's localcli-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 newevent_seqbut nothing about the recipient: not its last-consumed seq, not a pending count, not evenlast_wait_at. A sender cannot distinguish "delivered into a live listener" from "accumulating unread."tt statemembers carrylast_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
last_consumed_event_seqper member server-side, updated wheneverwait/try/eventsactually returns events to that member. Expose it onstate/healthmember rows. This is the primitive everything else needs, and the data is already computed client-side — it is just stored in the wrong place.msg sendresponse includes recipient staleness:recipient_last_consumed_event_seqandrecipient_pending_count. One field would have told claude at 12:33 that codex was consuming nothing, instead of an hour later viaps.msg send(andrelease/assignhandoffs) 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.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
--events) whentt waitruns without--events#58 —tt waitwithout--eventswarning (adjacent, but this session used cursor-resuming waits correctly and still hit the gap)tt msg sendshould accept body via --stdin/--body-file (shell substitution silently mangles messages) #52 got fresh confirmation this session: a codex handoff message arrived shell-mangled ("Prior message 12185 was shell-mangled; disregard it") —--stdinformsg sendwould have prevented it.