Skip to content

fix(server): open a new OpenCode turn after the provider goes idle - #11090

Open
jmfrank63 wants to merge 1 commit into
pingdotgg:mainfrom
jmfrank63:fix/opencode-stale-active-turn
Open

jmfrank63 wants to merge 1 commit into
pingdotgg:mainfrom
jmfrank63:fix/opencode-stale-active-turn

Conversation

@jmfrank63

@jmfrank63 jmfrank63 commented Sep 10, 2026

Copy link
Copy Markdown

What Changed

sendTurn no longer decides steer-vs-new-turn from the cached activeTurnId alone. Before reusing it, the adapter confirms the native session status — but only where an idle reading is actually authoritative for that turn.

An idle reading is not authoritative when idle evidence is deliberately withheld (prompt admission, post-interruption, or pending reconciliation each mean a known-stale idle is in flight and the prompt is a genuine mid-turn steer), nor before OpenCode has reported the turn busy, since it then describes the state before the turn started rather than after it ended. An unavailable or undecodable status keeps the previous behavior: never guess idle.

Two follow-on details the change needs to be correct:

  • Completing a turn from the preflight leaves the native idle it stood in for still in flight, so idle reconciliation is armed. The next idle is checked against live status instead of trusted, and retires the turn it belongs to rather than the turn the follow-up just opened.
  • The status request is itself a suspension point, so activeTurnId is re-read after it rather than trusting the id captured before it — the event pump can retire the turn while the request is in flight, which is the same race in a narrower window.

Two deterministic regression tests cover both orderings: the idle event arriving before the follow-up, and the idle event landing during the preflight. Both were verified to fail against the unfixed adapter with the reported symptom (the follow-up reusing the finished turn's id).

Why

Fixes #10973.

After an OpenCode task finished and the thread went idle, the first follow-up message was ignored. activeTurnId is cleared asynchronously by the event pump once native idle evidence is processed, so a prompt submitted during that gap reused the finished turn's id. That skipped turn.started and accrued the new work's tokens to the previous turn — from the user's side, the message looked like it did nothing.

The narrow fix is to confirm the provider's real status before trusting a cached turn id, while leaving every case where the cached id is legitimately still live untouched. In particular, the existing coverage for a mid-turn steer racing a stale idle status is unchanged and still green: that path is classified as withheld idle evidence and skips the status read entirely.

Verification: focused adapter suite 112/112 (run repeatedly for stability), all eight OpenCode test files 195/195, apps/server typecheck clean, targeted lint and format clean.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — N/A, server-only change with no UI surface
  • I included a video for animation/interaction changes — N/A

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of concurrent follow-up turns to prevent completed turns from being incorrectly reused.
    • Ensured stale or delayed idle-status events do not end newer active turns.
    • Improved recovery when status updates race with turn submission, preserving the correct active turn and running state.

`sendTurn` decided steer-vs-new-turn from the cached `activeTurnId`, which the
event pump clears asynchronously once native idle evidence is processed. A
follow-up submitted during that gap reused the finished turn's id, skipped
`turn.started`, and accrued its tokens to the previous turn, so the message
could look like it did nothing.

Confirm the native session status before reusing `activeTurnId`, but only where
an idle reading is authoritative for that turn. It is not authoritative while
idle evidence is deliberately withheld — admission, post-interruption, and
reconciliation each mean a known-stale idle is in flight and the prompt is a
genuine mid-turn steer — nor before OpenCode has reported the turn busy, since
it then describes the state before the turn started. An unavailable or
undecodable status keeps the previous behavior.

Completing a turn from the preflight leaves the native idle it stood in for
still in flight, so arm idle reconciliation: the next idle is checked against
live status instead of trusted, and retires the turn it belongs to rather than
the turn the follow-up just opened.

The status request is itself a suspension point, so re-read `activeTurnId`
after it rather than trusting the id captured before it: the pump can retire
the turn while the request is in flight, and that is exactly the race being
fixed.

Fixes pingdotgg#10973

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Sep 10, 2026
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 6cad2ede-37b9-4cda-9850-1981cd362c36

📥 Commits

Reviewing files that changed from the base of the PR and between d29c56a and 0596dc4.

📒 Files selected for processing (2)
  • apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
  • apps/server/src/provider/Layers/OpenCodeAdapter.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The OpenCode adapter now tracks native busy status before reusing an active turn. It performs a status preflight when required, completes stale turns, and preserves newer turns across delayed idle events. Two regression tests cover these concurrency races.

Changes

OpenCode turn boundary

Layer / File(s) Summary
Native busy-state tracking
apps/server/src/provider/Layers/OpenCodeAdapter.ts
The session context records native busy or retry status. The flag resets when turns complete, fail, are interrupted, encounter errors, or start fresh.
Guarded steering and race coverage
apps/server/src/provider/Layers/OpenCodeAdapter.ts, apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
sendTurn checks native status before reusing a busy active turn. Tests cover delayed idle events and suspended status preflight.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Severity of issue fixed: Low

Sequence Diagram(s)

sequenceDiagram
  participant FollowUpCaller
  participant sendTurn
  participant OpenCode
  participant TurnState
  FollowUpCaller->>sendTurn: submit follow-up
  sendTurn->>OpenCode: request session.status
  OpenCode-->>sendTurn: idle or missing status
  sendTurn->>TurnState: complete stale active turn
  sendTurn->>OpenCode: start distinct follow-up turn
Loading

Suggested reviewers: t3dotgg, maria-rcks, juliusmarminge

Merge Risk: ⚪ Minimal · up to 0596d

This change separates completed OpenCode follow-up turns from legitimate mid-turn steering by checking native status before reusing cached turn IDs. Focused tests and validation checks pass, with no concrete current-head correctness or availability issue identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: opening a new OpenCode turn after the provider becomes idle.
Description check ✅ Passed The description includes complete What Changed, Why, UI Changes, and Checklist sections. It explains the fix, preserves steering behavior, describes regression coverage, and reports verification resul…
Linked Issues check ✅ Passed The implementation directly addresses issue #10973. It prevents completed turns from being reused, preserves valid mid-turn steering, handles status and event races, and adds regression tests for both…
Out of Scope Changes check ✅ Passed The changes are limited to the OpenCode adapter and focused concurrency regression tests. They support the linked issue and do not introduce unrelated functionality.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant