fix(server): resume active turns when T3 Code reopens - #9360
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
ApprovabilityVerdict: 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:
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( |
There was a problem hiding this comment.
🟠 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.

User-facing behavior
This PR fixes one specific desktop-app failure:
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:
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:
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
Verification
vp test run src/provider/Layers/ClaudeAdapter.test.ts src/provider/Layers/ProviderService.test.ts src/serverRuntimeStartup.reconcile.test.ts— 136 tests passedvp run typecheckinapps/server— passed (existing suggestions only)vp lintacross the changed server files — passed (one pre-existing warning)git diff --check— passedbc0226f7eincludes the durable Claude session-ID guardRegression coverage includes absent and cleared self-update markers, crash-safe retry state, successful same-conversation recovery, and rejection of a fresh replacement conversation.
Checklist
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.sendTurnacceptsrequireResumeCursorand 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 durablesession_idconfirmation 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
ProviderService.sendTurn.isSameResumeCursorcomparison operation toProviderAdapterShape; 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.recoverSessionForThreadandresolveRoutableSessionnow reject sessions whose cursor does not match the required persisted cursor; a mismatched recovered session is stopped and its MCP state cleared.reconcileProviderSessionsin 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.