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

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

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

Conversation

@davidvanderklay

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

coderabbitaiBot 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-actionsgithub-actionsBot 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.

🟡 MediumLayers/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 threadapps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeappBot 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 threadapps/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-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursorcursorBot 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR 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

@davidvanderklay
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

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

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

Conversation

@davidvanderklay

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

coderabbitaiBot 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-actionsgithub-actionsBot 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.

🟡 MediumLayers/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 threadapps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeappBot 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 threadapps/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-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursorcursorBot 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR 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

@davidvanderklay
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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

Conversation

@davidvanderklay

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

coderabbitaiBot 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-actionsgithub-actionsBot 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.

🟡 MediumLayers/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 threadapps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeappBot 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 threadapps/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-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursorcursorBot 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR 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

@davidvanderklay
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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

Conversation

@davidvanderklay

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

coderabbitaiBot 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-actionsgithub-actionsBot 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.

🟡 MediumLayers/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 threadapps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeappBot 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 threadapps/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-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursorcursorBot 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR 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

@davidvanderklay
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

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

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

Conversation

@davidvanderklay

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

coderabbitaiBot 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-actionsgithub-actionsBot 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.

🟡 MediumLayers/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 threadapps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeappBot 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 threadapps/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-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursorcursorBot 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR 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

@davidvanderklay
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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

Conversation

@davidvanderklay

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

coderabbitaiBot 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-actionsgithub-actionsBot 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.

🟡 MediumLayers/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 threadapps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeappBot 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 threadapps/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-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursorcursorBot 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR 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

@davidvanderklay
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

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

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

Conversation

@davidvanderklay

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

coderabbitaiBot 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-actionsgithub-actionsBot 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.

🟡 MediumLayers/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 threadapps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeappBot 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 threadapps/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-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursorcursorBot 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR 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

@davidvanderklay
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

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

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

Conversation

@davidvanderklay

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

coderabbitaiBot 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-actionsgithub-actionsBot 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.

🟡 MediumLayers/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 threadapps/server/src/provider/Layers/OpenCodeAdapter.ts
@macroscopeapp

macroscopeappBot 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 threadapps/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-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 6, 2026

@cursorcursorBot 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR 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

@davidvanderklay