Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
Two paths reach the same restart branch.
A. Equivalent model selection with a different shape
- Open a thread with the Claude provider and send a message whose model selection is
[{"id":"effort","value":"high"},{"id":"contextWindow","value":"1m"}]. - While that turn is running, send a follow-up whose model selection is
[{"id":"effort","value":"high"},{"id":"fastMode","value":false},{"id":"contextWindow","value":"1m"}]. This is the same effective selection, sincefastMode: falseis the default. In my case the two shapes came from different clients connected to the same server.
B. Follow-up after a server restart
- Start a long Claude turn.
- Restart the server while the turn is running, for example by installing a desktop update.
- On startup the server resumes the interrupted turn with "Continue where you left off."
- While that resumed turn is running, send a follow-up message with the same model selection the thread already uses.
Expected behavior
The follow-up is queued or steered into the running session. The running Claude process keeps going, because the model, runtime mode, cwd, and provider instance did not change.
Actual behavior
ProviderCommandReactor.ensureSessionForThread takes its restart branch with shouldRestartForModelSelectionChange: true and modelChanged: false. ClaudeAdapter.startSession then replaces the running session (claude.session.replacing, existingSessionStatus: "running"). The in-flight turn is killed mid tool call. When the agent resumes, it reports that its previous session ended in the middle of a tool call and re-runs the work.
The check is in apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:
const previousModelSelection = threadModelSelections.get(threadId);
const shouldRestartForModelSelectionChange =
preferredProvider === "claudeAgent" &&
requestedModelSelection !== undefined &&
!Equal.equals(previousModelSelection, requestedModelSelection);Two problems:
- The
optionsarray is compared structurally, so a default option that is present in one selection and omitted in the other counts as a change (path A). threadModelSelectionsis an in-memory cache. It is filled only afterbuildSendTurnRequestForThreadand on the compaction path. The post-restart continuation inserverRuntimeStartup.tscallsproviderService.sendTurndirectly, so the cache stays empty for resumed threads. The next user message then comparesundefinedwith a real selection and restarts the session that the continuation just started (path B).
Possible fixes:
- Normalize selections before comparing, by sorting options and dropping default values, or compare only the fields that need a new Claude process.
- Fall back to the thread's persisted
modelSelectionwhen the cache has no entry, or fill the cache from the continuation path. - Never replace a
runningsession for a selection-only change. Defer the restart until the turn ends, or apply the change in-session when the provider supports it.
Related: #12694 covers what happens to live subagent tasks once this restart branch runs. This report is about the branch being taken when nothing meaningful changed.
Impact
Major degradation or frequent failure
Version or commit
0.0.44 (desktop and t3 serve). The same check is on main @ 0cf482b.
Environment
macOS 26, T3 Code desktop 0.0.44 with the background service, Claude provider (claude-opus-5-5, 1M context, effort high, full-access).
Logs or stack traces
# path A: previous selection had no fastMode, follow-up sent fastMode: false
INFO provider command reactor restarting provider session
runtimeModeChanged: false, cwdChanged: false, modelChanged: false,
instanceChanged: false, shouldRestartForModelChange: false,
shouldRestartForModelSelectionChange: true, hasResumeCursor: true
WARN claude.session.replacing
existingSessionStatus: 'running',
reason: 'startSession called with existing active session'
# path B: two minutes after a restart and its "Continue where you left off." continuation
INFO provider command reactor restarting provider session
modelChanged: false, shouldRestartForModelSelectionChange: true
WARN claude.session.replacing
existingSessionStatus: 'running'In one day of logs, every Claude session restart on the background service had shouldRestartForModelSelectionChange: true as its only reason.
Workaround
Avoid sending messages to a Claude thread while a turn is running, especially right after a server restart or when switching between clients.
Activity
juliusmarminge commentedon Oct 1, 2026
Note
Grok responding on behalf of Julius.
Triage
Thanks for tracking both paths down so carefully. I confirmed this on
mainat0cf482b, and it isn't a duplicate of #12694 (open). That issue covers what happens to live subagent tasks after this restart branch runs. This one is about the branch running when the Claude process didn't need to be replaced.ProviderCommandReactor.ensureSessionForThreadrestarts a live Claude session wheneverEqual.equalsfails between the in-memorythreadModelSelectionsentry and the selection on the new turn. That check only applies to Claude, and it compares the wholeoptionsarray structurally, so order matters and so do entries Claude ignores. A real effort change is still supposed to restart (see the testrestarts claude sessions when claude effort changes): effort, fast mode, and context window are applied inClaudeAdapter.startSession, andsendTurndoesn't push a new effort into the running query. The bug is that a cache miss and a default-equivalent option shape both end up on that same restart path.Path A. When the model advertises a
fastModedescriptor and the stored selection left it out, web dispatch adds{ id: "fastMode", value: false }(withImplicitFastModeDefaultinapps/web/src/components/chat/composerProviderState.tsx) and then rebuilds the array in descriptor order. Mobile normalizes withbuildExplicitProviderOptionSelectionsFromDescriptorsand doesn't add that default (apps/mobile/src/lib/modelOptions.ts). Claude only turns on fast mode when the value is exactlytrue(ClaudeAdapter.startSession), so leaving it out and sendingfalsestart the same process, butEqual.equalsstill sees them as different. That matches your two shapes exactly.modelChangedstays false because the model slug didn't change.Path B.
threadModelSelectionsonly gets filled afterbuildSendTurnRequestForThreadand on the compaction path. The post-restart continuation inserverRuntimeStartup.tscallsproviderService.sendTurndirectly, which recovers the session inProviderService.recoverSessionForThreadfrom the persisted binding and never writes the cache. The next user turn then comparesundefinedwith the requested selection, soshouldRestartForModelSelectionChangecomes out true for the session the continuation just started.Replacing the session isn't the same as steering it.
ClaudeAdapter.startSessionlogsclaude.session.replacingwithexistingSessionStatus: "running", andstopSessionInternalinterrupts the in-flight turn. The follow-up keeps the resume cursor, which is why Claude comes back saying its previous session died mid tool call.One note on the suggested fixes: falling back to
thread.modelSelectionon a cache miss isn't quite right.thread.turn.startdoesn't write that field, and the web client often updates it to the composer's selection before the turn, so after a restart it could hide a real change from what the recovered process was started with. A better approach is to seed the cache from the selection actually passed tostartSessionon the continuation path, and to compare a normalized selection (same option ids, default-equivalent values dropped) instead of the raw array. Deferring every replacement of a running session would be a separate behavior change from this bug.t3dotgg commentedon Oct 2, 2026
Note
🤖 GPT-6.1-Sol responding on behalf of Theo
This issue should be resolved in the next nightly build by Orchestrator v2 (#2829).
Please try that nightly. If the problem still exists, open a new issue with the nightly version you tested and steps to reproduce it.