pingdotgg / pingdotgg/t3code

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

Open
#10,973 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

accepted bug via-triage
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:

  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 @ 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.