Repository navigation
[Bug]: OpenCode follow-up sent before the idle event lands is folded into the previous turn #10973
Description
Activity
Triage
Confirmed as a T3-side OpenCode adapter race, not a duplicate and not already fixed.
sendTurndecides steer vs new turn from the locally cachedactiveTurnId(OpenCodeAdapter.ts~3132). That field is only cleared after the event pump handles native idle (session.status: idle→completeOpenCodeTurn, ~2618 → ~1132). A follow-up in that gap reuses the previous turn id, skipsturn.started(~3189), and accrues tokens on the previous accumulator (~3167).session.promptAsyncstill runs on both paths, so OpenCode gets the prompt; T3’s turn boundary does not. BumpingpromptGenerationcan also make a pending T1 completion look stale (~1118).This matches the report. Live reproduction is a narrow window; the adapter-level sequence (hold idle in the mock
subscribedEventsqueue, thensendTurn) is the right deterministic test, same technique as the existing steer/idle case.Not the same bug
- Chat shows 'working...' indefinitely after opencode CLI already finished responding #2644 is the opposite symptom: OpenCode already finished and T3 stays on Working… (completion never observed).
- fix(server): prevent premature OpenCode turn completion #10805 hardens admission/reconnect/idle completion so turns do not finish too early. It does not change the
sendTurnsteer-vs-new-turn predicate. Do not fold this fix into that PR.
Constraint
OpenCodeAdapter.test.ts~1523 (“does not let an old idle status complete a successful steer”) must stay green.session.statuscan read idle during a legitimate mid-turn steer, so a naive status preflight is not sufficient.Suggested fix
- Add a focused adapter test for this ordering: hold idle, send the follow-up, assert a new
turn.started/ distinct turn id; release idle and assert T1turn.completedis distinct from T2. - Classify already-queued native events before
sendTurnreadsactiveTurnId. Withheld/stale idle (admission /awaitingBusyAfterInterruption/reconcileIdleStatus) should still steer; an authoritative idle should clearactiveTurnIdso the follow-up starts a new turn. - Keep the change in
apps/serverOpenCode adapter only.
Severity: minor / occasional. Workaround: wait until the thread settles, or resend.
Accepting as a bug.
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 9, 2026
Before submitting
Area
apps/server
Steps to reproduce
This is a narrow timing window, so deliberate reproduction is unreliable. The ordering is:
session.status: idle.Deterministically, this is reachable at the adapter level by holding the idle event in the mock's
subscribedEventsqueue and callingsendTurnbefore releasing it — the same techniqueapps/server/src/provider/Layers/OpenCodeAdapter.test.ts:1523already uses for the adjacent case.Expected behavior
The follow-up starts a new turn with its own
turn.startedandturn.completed, and its own token accounting.Actual behavior
The follow-up is classified as a steer. It reuses the previous turn's ID, emits no
turn.started(OpenCodeAdapter.ts:3189), and its tokens accrue to the previous turn's accumulator (OpenCodeAdapter.ts:3167). T1 does not receive its own distinctturn.completed; the follow-up can be attributed to the same turn, or an older pending completion can be invalidated by the newer prompt generation (OpenCodeAdapter.ts:1118). To the user the follow-up can look like it did nothing; resending after the thread settles works.Root cause
sendTurnuses the locally cachedactiveTurnId(OpenCodeAdapter.ts:3132) to distinguish steering from a new turn. The event pump clears that field asynchronously after processing native idle evidence (OpenCodeAdapter.ts:2618→:1132). A follow-up submitted during that ordering gap reuses the prior turn ID and prompt generation context, so T3 can merge the follow-up into the previous turn or discard the previous turn's pending completion as stale. The prompt itself is still submitted to OpenCode —session.promptAsyncis invoked identically on both paths (OpenCodeAdapter.ts:3211) — so the defect is T3-side turn-boundary bookkeeping.Evidence and limitations
Any fix has to preserve legitimate mid-turn steering.
OpenCodeAdapter.test.ts:1523("does not let an old idle status complete a successful steer") asserts that a follow-up must still steer whensession.statusreads as idle, so a naive status preflight is not sufficient.I could not reproduce this against a live OpenCode session; the evidence is a structural race read from source plus a user report of a follow-up appearing to be ignored. The focused adapter suite passes unmodified at
6c583620f: 110 tests.Related work
No exact duplicate issue found. Related work includes #2644 and #10805, which address adjacent OpenCode completion/admission races but do not change the current steer-vs-new-turn decision in
sendTurn.Impact
Minor bug or occasional failure
Version or commit
main @ 6c58362
Environment
macOS; OpenCode version/model not captured; base commit 6c58362
Logs or stack traces
No provider event log was captured for the reported occurrence.
Workaround
Wait until the thread visibly settles before sending the follow-up, or resend the follow-up.