Skip to content

[Bug]: Claude follow-up message kills the running turn when the model selection is unchanged #14524

Closed

Description

@praveenperera

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

  1. Open a thread with the Claude provider and send a message whose model selection is [{"id":"effort","value":"high"},{"id":"contextWindow","value":"1m"}].
  2. 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, since fastMode: false is the default. In my case the two shapes came from different clients connected to the same server.

B. Follow-up after a server restart

  1. Start a long Claude turn.
  2. Restart the server while the turn is running, for example by installing a desktop update.
  3. On startup the server resumes the interrupted turn with "Continue where you left off."
  4. 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:

  1. The options array is compared structurally, so a default option that is present in one selection and omitted in the other counts as a change (path A).
  2. threadModelSelections is an in-memory cache. It is filled only after buildSendTurnRequestForThread and on the compaction path. The post-restart continuation in serverRuntimeStartup.ts calls providerService.sendTurn directly, so the cache stays empty for resumed threads. The next user message then compares undefined with 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 modelSelection when the cache has no entry, or fill the cache from the continuation path.
  • Never replace a running session 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

  1. juliusmarminge commented on Oct 1, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks for tracking both paths down so carefully. I confirmed this on main at 0cf482b, 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.ensureSessionForThread restarts a live Claude session whenever Equal.equals fails between the in-memory threadModelSelections entry and the selection on the new turn. That check only applies to Claude, and it compares the whole options array structurally, so order matters and so do entries Claude ignores. A real effort change is still supposed to restart (see the test restarts claude sessions when claude effort changes): effort, fast mode, and context window are applied in ClaudeAdapter.startSession, and sendTurn doesn'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 fastMode descriptor and the stored selection left it out, web dispatch adds { id: "fastMode", value: false } (withImplicitFastModeDefault in apps/web/src/components/chat/composerProviderState.tsx) and then rebuilds the array in descriptor order. Mobile normalizes with buildExplicitProviderOptionSelectionsFromDescriptors and doesn't add that default (apps/mobile/src/lib/modelOptions.ts). Claude only turns on fast mode when the value is exactly true (ClaudeAdapter.startSession), so leaving it out and sending false start the same process, but Equal.equals still sees them as different. That matches your two shapes exactly. modelChanged stays false because the model slug didn't change.

    Path B. threadModelSelections only gets filled after buildSendTurnRequestForThread and on the compaction path. The post-restart continuation in serverRuntimeStartup.ts calls providerService.sendTurn directly, which recovers the session in ProviderService.recoverSessionForThread from the persisted binding and never writes the cache. The next user turn then compares undefined with the requested selection, so shouldRestartForModelSelectionChange comes out true for the session the continuation just started.

    Replacing the session isn't the same as steering it. ClaudeAdapter.startSession logs claude.session.replacing with existingSessionStatus: "running", and stopSessionInternal interrupts 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.modelSelection on a cache miss isn't quite right. thread.turn.start doesn'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 to startSession on 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.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 1, 2026
  3. t3dotgg commented on Oct 2, 2026

    @t3dotgg
    Member

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions