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
[bug] Concurrent chat turns on one instance produce two contradictory replies — step 1/40 then step 0/40, 1.8s apart, and one message answered twice while another got no reply #429
Two chat turns run concurrently on one instance, and the user gets two contradictory answers — progress appearing to go backwards
Observed live in bd43f4de-… ("Chess coder 2") while the owner was working, and reproduced in a second instance a month earlier. Not a stale report — the Chess coder transcript below is from 06:11 UTC today.
Evidence 1 — live, today
06:09:33.668 user "retry now"
06:11:01.749 user "https://www.youtube.com" ← accidental send
06:11:07.951 system **Loop → engine** (step 1)
06:11:14.671 system ✅ check_work … run 47bf3703 : running
06:11:14.671 assistant "Still running — it's at step 1/40 … The executor is actively working
through the implementation."
06:11:16.483 system ✅ github_list_issues / github_read_issue ×2 / repo_tree / repo_read_file
06:11:16.483 assistant "The run is still at step 0/40 — the executor is initialising and
hasn't produced output yet."
Two assistant messages 1.8 seconds apart, each with its own tool block, saying:
step 1/40, "actively working through the implementation"
step 0/40, "initialising and hasn't produced output yet"
Progress runs backwards on screen. The later message reports less progress, because the two turns sampled the run at different moments (turn B started at 06:11:01 and read before step 1 was recorded at 06:11:07; turn A started at 06:09:33 and read after). Both are individually truthful about the instant they sampled; interleaved, they read as the agent contradicting itself.
And neither turn answered the message that started the second one.https://www.youtube.com — an obvious mis-send — got a run-status report instead of "that looks like it was sent by accident". The user's actual input vanished into a reply about something else.
06:46:26.845 user 我现在想不Fairview Park往前走。
06:46:30.903 user 你好!今天我们练习什么? ← sent 4s later, before any reply
06:46:33.899 assistant 听起来很好!法尔维尤公园很美吗? ← answers Fairview Park
06:46:36.512 assistant 很好,小明!Fairview Park 听起来很美! ← ALSO answers Fairview Park
One message got two answers; the other got none.你好!今天我们练习什么? ("what are we practising today?") was never replied to.
This is the hands-free case specifically: the mic reopens and the user speaks again before the reply lands, so overlapping turns are not an edge case there — they are the normal rhythm of talking to an agent that takes a few seconds to answer.
Verified: concurrent turns are permitted by design, and nothing reconciles them
agent-do.ts:379handleChat awaits think() and returns inline. Grepping the DO for blockConcurrency, inFlight-as-a-lock, mutex, or a busy check returns nothing.
Storage is safe: appendMessage (agent-do.ts:752) writes one key per message (msg:${createdAt}:${id}), so there is no lost update — both replies persist. That is why the failure is contradiction, not data loss.
Each turn builds its context from history at its own start time, so turn B cannot see turn A's not-yet-written reply. Two turns therefore answer the same state, and the second has no idea the first is in flight.
Why this is worth fixing rather than accepting
The marketplace pitch is an agent you can talk to hands-free. In that mode the user cannot see the screen, so the only signal that a turn is in progress is the reply itself — and speaking again before it arrives is not user error, it is the interface working as designed. Today that produces two answers to one question, none to the other, and a progress number that moves backwards.
It also multiplies the cost: two full BYOK turns, five tool calls in the second one, for one answer the user wanted.
What to do
Three options, cheapest first. I would take (2).
1. Serialize per instance. Queue a turn behind an in-flight one in the DO. Correct, and the simplest to state — but it makes the second speaker wait with no feedback, which in hands-free is indistinguishable from the agent ignoring them.
2. Coalesce (recommended). If a turn is already in flight for this instance, append the new user message to it rather than starting a second turn — the marker in chat-inflight.ts already tells you a turn is live, so the detection is in place. The user gets one reply that accounts for everything they said, which is what a human interlocutor does. you 你好… would have been answered.
3. Make it visible. At minimum, if a second turn does start, tell the second one that another is in flight and let it say so. This does not fix the contradiction; it stops it reading as the agent being confused.
Fix (2) needs a decision on the cut-off: a message arriving after the model has begun generating cannot be folded in, so there is still a window where (1) or (3) applies. Worth specifying rather than discovering.
Alternatives considered and rejected
Client-side lock — disable send while awaiting a reply. Rejected: the console already holds the composer during a turn, and this still happened, because in hands-free voice sends the turn, not the composer. A UI-level guard cannot cover the surface where the problem actually occurs. It would also block the legitimate "wait, ignore that, I meant…" interjection the Loop feature explicitly supports.
Treat it as a prompt problem (tell the model to check for concurrent turns). Rejected: the model cannot see a turn that has written nothing yet.
Order replies by turn start time. Rejected: it fixes the display order and none of the substance — the user would still get two answers to one question.
Acceptance criteria
Two user messages sent 4 seconds apart, with a reply outstanding, produce one reply that addresses both — or an explicit statement that a turn is already running.
No two assistant messages report contradictory state for the same run.
A message that receives no reply is impossible: every user message is either answered or explicitly acknowledged as folded into another turn.
Regression test at the DO level: two concurrent POST /chat calls on one instance.
Related: #251 (in-flight turn markers — the detection this can reuse), #427 (the timeouts in the same live transcript), #406 (assistant messages asserting state that was not verified).
Two chat turns run concurrently on one instance, and the user gets two contradictory answers — progress appearing to go backwards
Observed live in
bd43f4de-…("Chess coder 2") while the owner was working, and reproduced in a second instance a month earlier. Not a stale report — the Chess coder transcript below is from 06:11 UTC today.Evidence 1 — live, today
Two assistant messages 1.8 seconds apart, each with its own tool block, saying:
Progress runs backwards on screen. The later message reports less progress, because the two turns sampled the run at different moments (turn B started at 06:11:01 and read before step 1 was recorded at 06:11:07; turn A started at 06:09:33 and read after). Both are individually truthful about the instant they sampled; interleaved, they read as the agent contradicting itself.
And neither turn answered the message that started the second one.
https://www.youtube.com— an obvious mis-send — got a run-status report instead of "that looks like it was sent by accident". The user's actual input vanished into a reply about something else.Evidence 2 — the same shape, a month earlier
6d3b28f1-…("Language Buddy"), timestamps monotonic, ordering correct:One message got two answers; the other got none.
你好!今天我们练习什么?("what are we practising today?") was never replied to.This is the hands-free case specifically: the mic reopens and the user speaks again before the reply lands, so overlapping turns are not an edge case there — they are the normal rhythm of talking to an agent that takes a few seconds to answer.
Verified: concurrent turns are permitted by design, and nothing reconciles them
agent-do.ts:379handleChatawaitsthink()and returns inline. Grepping the DO forblockConcurrency,inFlight-as-a-lock,mutex, or a busy check returns nothing.lib/chat-inflight.tsis not a lock. Its purpose is [bug] A chat turn is bound to the request — navigating away can lose the reply while its tool side effects persist #251 — recording that a turn started so an interrupted one can be named. Its own comment states the intent plainly: "one key per in-flight turn, so concurrent turns can't clobber" — i.e. concurrency is expected and accommodated, not prevented.appendMessage(agent-do.ts:752) writes one key per message (msg:${createdAt}:${id}), so there is no lost update — both replies persist. That is why the failure is contradiction, not data loss.Why this is worth fixing rather than accepting
The marketplace pitch is an agent you can talk to hands-free. In that mode the user cannot see the screen, so the only signal that a turn is in progress is the reply itself — and speaking again before it arrives is not user error, it is the interface working as designed. Today that produces two answers to one question, none to the other, and a progress number that moves backwards.
It also multiplies the cost: two full BYOK turns, five tool calls in the second one, for one answer the user wanted.
What to do
Three options, cheapest first. I would take (2).
1. Serialize per instance. Queue a turn behind an in-flight one in the DO. Correct, and the simplest to state — but it makes the second speaker wait with no feedback, which in hands-free is indistinguishable from the agent ignoring them.
2. Coalesce (recommended). If a turn is already in flight for this instance, append the new user message to it rather than starting a second turn — the marker in
chat-inflight.tsalready tells you a turn is live, so the detection is in place. The user gets one reply that accounts for everything they said, which is what a human interlocutor does.you 你好…would have been answered.3. Make it visible. At minimum, if a second turn does start, tell the second one that another is in flight and let it say so. This does not fix the contradiction; it stops it reading as the agent being confused.
Fix (2) needs a decision on the cut-off: a message arriving after the model has begun generating cannot be folded in, so there is still a window where (1) or (3) applies. Worth specifying rather than discovering.
Alternatives considered and rejected
Acceptance criteria
POST /chatcalls on one instance.Regression risk
chat-inflight.ts([bug] A chat turn is bound to the request — navigating away can lose the reply while its tool side effects persist #251) andlib/chat-turns.ts(turnSpanFor) both reason about; the interrupted-turn notice must not start firing for a folded message./loop-decidepath). Coalescing must not swallow a human interjection into the loop's own turn — that interjection is specifically meant to interrupt.Related: #251 (in-flight turn markers — the detection this can reuse), #427 (the timeouts in the same live transcript), #406 (assistant messages asserting state that was not verified).