Skip to content

fix(server): resume active turns when T3 Code reopens - #9360

Open
danieliser wants to merge 5 commits into
pingdotgg:mainfrom
danieliser:fix/resume-active-turns-after-restart
Open

fix(server): resume active turns when T3 Code reopens#9360
danieliser wants to merge 5 commits into
pingdotgg:mainfrom
danieliser:fix/resume-active-turns-after-restart

Conversation

@danieliser

@danieliser danieliser commented Sep 3, 2026

Copy link
Copy Markdown

User-facing behavior

This PR fixes one specific desktop-app failure:

  1. A model is still working in a T3 Code thread.
  2. The user fully quits T3 Code.
  3. The user opens T3 Code again.

Previously, the reopened app treated that active turn as an orphaned provider session and stopped it with an error. The user had to send another message manually.

With this change, reopening T3 Code recognizes the interrupted active turn, restores its saved provider conversation, and asks that conversation to continue.

Important

This is restart recovery, not background execution. Fully quitting T3 Code still stops its local backend and provider processes. The model does not continue computing while the app is closed; it resumes after the app is opened again.

Why this code lives in the server

The T3 Code desktop app starts an embedded local backend. Quitting the app stops that backend, and reopening the app starts a new backend process. The recovery decision therefore happens during backend startup even though the user-facing action is simply quitting and reopening the desktop app.

The same startup logic also benefits standalone T3 Code server processes, but the motivating and tested user flow is the desktop app restart described above. Reloading only the browser or frontend is unrelated.

How recovery works

At startup, an interrupted turn becomes a candidate for automatic continuation when:

  • the stored thread session still has an active turn ID;
  • the provider binding has a saved resume cursor;
  • the thread is not archived or deleted; and
  • there is no conflicting valid self-update continuation marker.

A previously cleared marker (continueAfterServerUpdate: null) counts as no marker. A valid marker containing a different turn ID remains authoritative and prevents the wrong turn from being resumed.

Before starting background recovery, T3 Code persists the interrupted turn ID as a continuation marker. If the app quits again during recovery, the next launch can retry instead of orphaning the turn. The marker is cleared only after the provider accepts the continuation.

Provider recovery is verified before any continuation turn is sent. Each adapter must prove that the live provider session corresponds to the saved resume cursor. Claude waits for the SDK to report its actual durable session ID instead of trusting the requested ID echoed during startup; OpenCode validates the session returned by its API; ACP providers require a successful native session load. A malformed cursor, a failed native load, or a confirmed identity mismatch fails closed rather than continuing in the wrong conversation.

Once verified, the existing continuation path is reused:

  • providers with native promptless continuation, currently Codex, receive a continuation turn;
  • other providers receive the existing “Continue where you left off” fallback prompt.

If continuation fails, the thread reports that it could not continue after T3 Code restarted and asks the user to send a new message.

Relationship to existing work

#9167 added continuation for a managed T3 Code self-update. That flow writes an explicit continuation marker before restarting. An ordinary desktop-app quit does not write that marker, which is the gap fixed here.

Other open PRs address different behavior:

This also restores the user-visible quit/reopen behavior covered downstream by patroza/t3code#442 and patroza/t3code#443, while reusing upstream’s continuation path instead of importing their larger recovery subsystem.

Out of scope

  • Keeping models running while the desktop app is closed
  • Adding a macOS LaunchAgent or other background service
  • Resuming an exact half-finished local tool process
  • Changing frontend-only reload behavior

Verification

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts src/provider/Layers/ProviderService.test.ts src/serverRuntimeStartup.reconcile.test.ts — 136 tests passed
  • vp run typecheck in apps/server — passed (existing suggestions only)
  • Targeted vp lint across the changed server files — passed (one pre-existing warning)
  • Targeted formatting across the changed server files — passed
  • git diff --check — passed
  • Live Grok ACP probe confirmed that loading a nonexistent session fails instead of creating a fresh conversation
  • Cursor Bugbot summary on commit bc0226f7e includes the durable Claude session-ID guard

Regression coverage includes absent and cleared self-update markers, crash-safe retry state, successful same-conversation recovery, and rejection of a fresh replacement conversation.

Checklist

  • This PR remains focused on restart recovery
  • I explained what changed and why
  • No UI changes
  • No animation or interaction changes

Note

Medium Risk
Recovery now auto-continues more orphaned sessions at startup; a cursor mismatch fails closed (session stopped, user must send again), but wrong eligibility could still attempt continuation on edge cases.

Overview
Reopening T3 Code after a full quit can now automatically continue an interrupted active turn instead of orphaning the provider session, when the thread still has an active turn ID and a persisted resume cursor (and no conflicting self-update continuation marker).

Provider recovery is gated by resume-cursor equality. Adapters gain optional isSameResumeCursor; ProviderService.sendTurn accepts requireResumeCursor and rejects recovery when the live or newly started session does not match the saved conversation (stopping the bad session on fresh-start mismatch). Claude’s check waits for the SDK’s durable session_id confirmation so an echoed cursor on a brand-new session is treated as a failed resume.

Startup reconciliation widens continuation eligibility beyond the explicit update marker, persists a retry marker for crash-safe recovery, and issues continuation turns with requireResumeCursor (promptless where supported). User-facing failure copy now refers to T3 Code restarting rather than a server update.

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

Note

Resume active turns when T3 Code reopens by validating provider resume cursors

  • On restart, orphaned active sessions with an active turn and persisted resume cursor now automatically resume, even without a continuation marker. The persisted cursor is passed as a strict requirement to ProviderService.sendTurn.
  • Adds an optional isSameResumeCursor comparison operation to ProviderAdapterShape; each adapter (Claude, Codex, Cursor, Grok, OpenCode) implements it. Claude additionally waits up to 10 seconds for the SDK to confirm the durable session id.
  • ProviderService.recoverSessionForThread and resolveRoutableSession now reject sessions whose cursor does not match the required persisted cursor; a mismatched recovered session is stopped and its MCP state cleared.
  • Behavioral Change: reconcileProviderSessions in serverRuntimeStartup.ts now resumes unmarked orphaned sessions with an active turn and persisted cursor. Adapters without a resume-cursor comparator are treated as unable to prove recovery and will not auto-continue. Explicit markers whose turn id does not match remain ineligible.

Macroscope summarized bc0226f.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
Comment thread apps/server/src/serverRuntimeStartup.ts
@danieliser danieliser changed the title fix(server): resume active turns after ordinary restarts fix(server): resume active turns when T3 Code reopens Sep 3, 2026
Comment thread apps/server/src/serverRuntimeStartup.ts
Comment thread apps/server/src/serverRuntimeStartup.ts
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Sep 3, 2026
@danieliser
danieliser marked this pull request as ready for review September 3, 2026 09:53

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default 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 6fff6e1. Configure here.

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

macroscopeapp Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces automatic, cross-provider continuation of active turns after T3 Code restarts, changing startup reconciliation and shared provider recovery behavior across multiple adapters. Provider conversation identity validation also has a material unresolved risk in the Grok recovery path.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

isSameResumeCursor: (_threadId, persisted, recovered) => {
const persistedSession = parseGrokResume(persisted);
const recoveredSession = parseGrokResume(recovered);
return Effect.succeed(

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.

🟠 High Layers/GrokAdapter.ts:2120

isSameResumeCursor returns true for any syntactically valid persisted cursor, even when session/load has fallen back to a fresh conversation. resumed.resumeCursor is reconstructed from options.resumeSessionId, so the comparison cannot validate the loaded session and restart recovery proceeds in the wrong conversation instead of failing closed. Compare against the session identity returned by the load operation, or otherwise detect fallback before accepting recovery.

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

`isSameResumeCursor` returns `true` for any syntactically valid persisted cursor, even when `session/load` has fallen back to a fresh conversation. `resumed.resumeCursor` is reconstructed from `options.resumeSessionId`, so the comparison cannot validate the loaded session and restart recovery proceeds in the wrong conversation instead of failing closed. Compare against the session identity returned by the load operation, or otherwise detect fallback before accepting recovery.

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

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant