Skip to content

[Bug]: Stopped Claude thread silently loses native context when switching compatible provider instances #4766

Description

@reed-yang

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

  1. Configure two enabled Claude provider instances, A and B, that use the
    claudeAgent driver and resolve to the same Claude home / continuation key.
  2. Start a T3 thread on instance A and complete enough turns to persist a
    non-empty Claude resume cursor.
  3. Leave the thread idle until its provider session is soft-stopped.
  4. In the existing thread, select instance B before sending the next message.
  5. Send a short reply that depends on the immediately preceding assistant
    message, such as A in response to a list of proposed options.

The failure boundary is important: switching compatible instances while the
old provider session is active has a path that carries
activeSession.resumeCursor; switching after the session has stopped does
not.

Expected behavior

Because A and B advertise the same continuation identity, T3 should explicitly
carry the prior Claude resume cursor into B. If that is not safe for a given
pair of instances, T3 should reject the switch or require a transcript
handoff before sending the message.

A non-empty T3 thread should never silently start a blank provider-native
session.

Actual behavior

T3 keeps the full message history visible in the same UI thread but starts a
new Claude Agent SDK session without a resume cursor. Claude receives only the
new short message and replies as if the earlier conversation never happened.

In the observed incident:

  • The old Claude JSONL remained present and readable.
  • Neither the old nor new JSONL contained a compact_boundary entry.
  • The execution host, cwd, model, Claude home, driver, and continuation key
    were unchanged.
  • The replacement native session's persisted cursor became turnCount: 1.
  • T3's projected messages and checkpoints remained intact.

This rules out context-window exhaustion, compaction, missing native session
data, or a UI-history deletion.

The stopped-session path appears to lose the previous instance identity before
the turn starts:

  1. The model-selection update to B is projected before the turn-start event.
  2. With no active session, ProviderCommandReactor.ensureSessionForThread
    derives currentInstanceId from the already-updated
    thread.modelSelection.instanceId.
  3. The reactor therefore sees B as both the current and desired instance and
    starts it without explicitly carrying A's cursor.
  4. ProviderService.startSession only falls back to the persisted cursor when
    persistedBinding.providerInstanceId === resolvedInstanceId.
  5. The persisted binding belongs to A while the target is B, so the cursor is
    rejected even though both instances have the same continuation key.
  6. ClaudeAdapter consequently supplies a newly generated sessionId rather
    than resume to the Agent SDK.

Related but distinct reports:

Impact

Major degradation or frequent failure

The reset is silent and the UI still displays the old conversation, so the
user cannot tell that approvals, architectural decisions, constraints, and
prior tool results are absent from the model's context.

Version or commit

Observed on v0.0.29 (1153afb4). The relevant logic is unchanged on main at
887dd6e455bb / v0.0.30-nightly.20260728.933.

Environment

T3 Code Desktop on macOS with a remote Linux execution host; Claude Code
2.1.218 via two configured claudeAgent instances; same model, host, cwd,
Claude home, and continuation key across the switch.

Logs or stack traces

# Last context-bearing turn completes on instance A.
# The provider session is later soft-stopped while idle.
# The user selects compatible instance B immediately before the next turn.

provider.instance_id=B
provider.resume_cursor.present=false
provider.resume_cursor.source=none
claude.resume.source=generated-session
claude.query.resume=""

# After the context-free response:
resumeCursor.turnCount=1

No credentials, prompt contents, project paths, or full local identifiers are
included above.

Suggested fix and regression test

Resolve the previous provider instance and resume cursor from the persisted
binding or stopped session before the newly projected model selection replaces
that identity. Then:

  • If continuation identities match, explicitly pass the prior cursor to the
    target instance.
  • If they do not match, reject the switch or use a reviewed transcript handoff.
  • Emit a warning/activity entry when a thread with completed turns starts a
    provider session without a resume cursor.

Regression test:

  1. Start a Claude thread on instance A.
  2. Persist a non-empty cursor and soft-stop the session.
  3. Update the selection to compatible Claude instance B.
  4. Start the next turn.
  5. Assert that B receives A's prior cursor, or that the switch is explicitly
    rejected before the user message is sent. It must not silently start blank.

Screenshots, recordings, or supporting files

No response.

Workaround

Use one provider instance per T3 thread. When changing route or provider
instance, start a new T3 thread with a reviewed handoff. If the original
Claude session still exists, resume it directly from the same Claude home and
working directory.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions