[Bug]: OpenCode follow-up sent before the idle event lands is folded into the previous turn
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
This is a narrow timing window, so deliberate reproduction is unreliable. The ordering is:
- Open an OpenCode thread and send a prompt.
- OpenCode finishes and emits
session.status: idle. - Before T3's event pump processes that event, submit a follow-up prompt.
Deterministically, this is reachable at the adapter level by holding the idle event in the mock's subscribedEvents queue and calling sendTurn before releasing it — the same technique apps/server/src/provider/Layers/OpenCodeAdapter.test.ts:1523 already uses for the adjacent case.
Expected behavior
The follow-up starts a new turn with its own turn.started and turn.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 distinct turn.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
sendTurn uses the locally cached activeTurnId (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.promptAsync is 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 when session.status reads 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 @ 6c583620f
Environment
macOS; OpenCode version/model not captured; base commit 6c583620f
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/server/src/provider/OpenCodeAdapter.ts, reading sendTurn around lines 3132-3211 and the idle-event handling around lines 2618 and 1132. Run the focused adapter test suite, including OpenCodeAdapter.test.ts:1523, and add a deterministic test for the queued-idle ordering described here. Done means a follow-up in that window gets its own turn lifecycle and token accounting while legitimate mid-turn steering still passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 46/100