Skip to content

[Bug]: OpenCode follow-up sent before the idle event lands is folded into the previous turn #10973

Description

@jmfrank63

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:

  1. Open an OpenCode thread and send a prompt.
  2. OpenCode finishes and emits session.status: idle.
  3. 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 @ 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.

Activity

  1. juliusmarminge commented on Sep 9, 2026

    @juliusmarminge
    Member

    Triage

    Confirmed as a T3-side OpenCode adapter race, not a duplicate and not already fixed.

    sendTurn decides steer vs new turn from the locally cached activeTurnId (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, skips turn.started (~3189), and accrues tokens on the previous accumulator (~3167). session.promptAsync still runs on both paths, so OpenCode gets the prompt; T3’s turn boundary does not. Bumping promptGeneration can 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 subscribedEvents queue, then sendTurn) is the right deterministic test, same technique as the existing steer/idle case.

    Not the same bug

    Constraint

    OpenCodeAdapter.test.ts ~1523 (“does not let an old idle status complete a successful steer”) must stay green. session.status can read idle during a legitimate mid-turn steer, so a naive status preflight is not sufficient.

    Suggested fix

    1. 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 T1 turn.completed is distinct from T2.
    2. Classify already-queued native events before sendTurn reads activeTurnId. Withheld/stale idle (admission / awaitingBusyAfterInterruption / reconcileIdleStatus) should still steer; an authoritative idle should clear activeTurnId so the follow-up starts a new turn.
    3. Keep the change in apps/server OpenCode adapter only.

    Severity: minor / occasional. Workaround: wait until the thread settles, or resend.

    Accepting as a bug.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions