Skip to content

fix(opencode): persist and reuse session cursor#3735

Open
davidvanderklay wants to merge 3 commits into
pingdotgg:mainfrom
davidvanderklay:fix/opencode-resume-cursor
Open

fix(opencode): persist and reuse session cursor#3735
davidvanderklay wants to merge 3 commits into
pingdotgg:mainfrom
davidvanderklay:fix/opencode-resume-cursor

Conversation

@davidvanderklay

@davidvanderklay davidvanderklay commented Jul 6, 2026

Copy link
Copy Markdown

What Changed

OpenCode sessions now persist a small resume cursor containing the upstream OpenCode session id.

When T3 Code recovers a provider session, the OpenCode adapter uses session.get with the persisted id instead of always creating a fresh upstream OpenCode session.

The race cleanup path now only aborts OpenCode sessions created by the losing local start attempt, so an adopted durable session is not aborted.

Why

Fixes #3604.

Previously, after provider reaping or a server restart, follow-up messages could lose OpenCode conversation context because T3 Code created a new upstream OpenCode session.

This keeps the change adapter-local and avoids broader lifecycle, cwd-forking, permission reapplication, or reaper changes.

Manual Smoke Test

Verified locally with OpenCode:

  1. Asked the thread to remember t3code-opencode-resume-smoke-3604.
  2. Confirmed the phrase was recalled.
  3. Restarted the T3 Code dev server.
  4. Reopened the same thread.
  5. Confirmed the phrase was still recalled after restart.

Validation

  • corepack pnpm exec vp check
  • corepack pnpm exec vp run typecheck
  • corepack pnpm exec vp run --filter t3 test -- apps/server/src/provider/Layers/OpenCodeAdapter.test.ts

UI Changes

None.


Note

Medium Risk
Touches provider session lifecycle (resume vs create, conditional remote abort, and in-memory state preservation) where mistakes could drop conversation context or leave stray upstream sessions; behavior is well covered by new adapter tests.

Overview
OpenCode startSession now exposes a resumeCursor (schemaVersion: 1, upstream sessionId) on the returned session and honors an incoming cursor by calling session.get instead of always session.create. Invalid or malformed cursors are ignored and behave like a fresh start.

Restarting a thread with the same upstream session id no longer **session.abort**s that remote session on teardown; in-memory adapter state (pending permissions/questions, turns, active turn, etc.) is carried over when the cursor matches the existing binding. Concurrent startSession races only abort newly created remote sessions, not adopted resumed ones; failed session.get for a valid cursor surfaces as a process error instead of silently creating an empty session.

Tests extend the OpenCode runtime double with session.get, permission replies, and delays, and add coverage for cursor return, resume, restart preservation, follow-up prompts, failure paths, and race cleanup.

Reviewed by Cursor Bugbot for commit 2df4ec5. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Persist and reuse session cursor in OpenCodeAdapter.startSession

  • startSession now returns a resumeCursor ({schemaVersion: 1, sessionId}) for new sessions and accepts one on subsequent calls to resume the same upstream session via client.session.get instead of creating a new one.
  • When restarting with a matching cursor, in-memory state (pending permissions, message/part caches, turns, active fields) is carried forward into the new context without aborting the remote session.
  • A new closeOpenCodeContext effect supports closing local resources with abortRemote: false, used when handing off a session to a resumed context.
  • On a concurrency race, only newly created losing sessions are aborted; adopted (resumed) sessions are preserved.
  • Behavioral Change: stopOpenCodeContext now delegates to closeOpenCodeContext with abortRemote: true, and malformed or version-mismatched cursors are treated as absent rather than erroring.

Macroscope summarized 2df4ec5.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b9d56320-cf2f-4b08-adfa-4883f97c3b39

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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 Jul 6, 2026
permission: buildOpenCodePermissionRules(input.runtimeMode),
}),
);
const resumeCursor = parseOpenCodeResumeCursor(input.resumeCursor);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/OpenCodeAdapter.ts:1103

When a session is stopped (via stopSession or the layer finalizer), stopOpenCodeContext calls session.abort, which destroys the upstream OpenCode session. But the persisted resumeCursor pointing to that now-dead session id is never cleared. The next startSession with that resumeCursor calls session.get on the aborted upstream session, which fails with a ProviderAdapterProcessError instead of restoring the conversation. Consider clearing or invalidating the persisted resumeCursor when stopOpenCodeContext destroys the upstream session, so the resume path does not try to recover from a session that no longer exists.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/OpenCodeAdapter.ts around line 1103:

When a session is stopped (via `stopSession` or the layer finalizer), `stopOpenCodeContext` calls `session.abort`, which destroys the upstream OpenCode session. But the persisted `resumeCursor` pointing to that now-dead session id is never cleared. The next `startSession` with that `resumeCursor` calls `session.get` on the aborted upstream session, which fails with a `ProviderAdapterProcessError` instead of restoring the conversation. Consider clearing or invalidating the persisted `resumeCursor` when `stopOpenCodeContext` destroys the upstream session, so the resume path does not try to recover from a session that no longer exists.

Comment thread apps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeapp Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces significant new session resume functionality with ~426 additions of new logic. Two unresolved review comments identify potential bugs: resume cursor not being cleared on abort, and premature turn completion during idle events. Human review recommended for both feature complexity and outstanding correctness concerns.

You can customize Macroscope's approvability policy. Learn more.

@davidvanderklay

Copy link
Copy Markdown
Author

Addressed in the latest commit. startSession now preserves the upstream OpenCode session when replacing a local in-memory context with a matching resumeCursor, so the adapter no longer aborts the same session it is about to re-adopt via session.get.

Explicit stop/stopAll cleanup still aborts remote sessions; this change is limited to the orchestrated same-session restart path flagged by Bugbot.

Comment thread apps/server/src/provider/Layers/OpenCodeAdapter.ts
@davidvanderklay

Copy link
Copy Markdown
Author

Addressed the latest Macroscope feedback in the latest commit. Same-session re-adoption now preserves the prior OpenCode context volatile in-memory state, including pending permissions/questions, part/text tracking, completed assistant parts, turn snapshots, and active turn metadata.

That keeps outstanding permission replies and streamed message bookkeeping available after local context replacement while still limiting remote-session preservation to the matching resumeCursor path.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2df4ec5. Configure here.

messageRoleById: preservedSessionMemory?.messageRoleById ?? new Map(),
completedAssistantPartIds: preservedSessionMemory?.completedAssistantPartIds ?? new Set(),
turns: preservedSessionMemory?.turns ?? [],
activeTurnId: preservedSessionMemory?.activeTurnId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserved turn id closes on idle

Medium Severity

When startSession restarts with a matching resume cursor, it copies the prior activeTurnId into the new context. After the new event subscription starts, a session.status idle event still runs the existing idle branch and emits turn.completed whenever that preserved id is set. OpenCode often reports idle while a permission is pending, so a preserve restart during an in-flight turn can mark the turn complete even though the approval is still open.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2df4ec5. Configure here.

@davidvanderklay

Copy link
Copy Markdown
Author

Noting that this overlaps with #3617.

The intent of this PR is the narrower fix for #3604: persist/reuse the OpenCode session id and avoid silently replacing a valid persisted cursor with a fresh empty session if resume fails. I intentionally left cwd forking, stale-session fallback, and permission reapplication out of scope to keep the behavior change smaller.

Happy to close this if maintainers prefer #3617’s broader approach or a different approach altogether. Otherwise I can keep this PR focused as the minimal alternative.

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

Labels

size:L 100-499 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 provider loses thread context on follow-up — t3code starts a new OpenCode session instead of resuming (no durable session binding)

1 participant