pingdotgg / pingdotgg/t3code

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

Open
#4,766 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

Update 2026-09-02: reproduced on v0.0.38; see the latest comment and proposed fix #6148.

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:

  • #2188 covered idle/restart recovery on the same provider session. This repro
    is the remaining stopped-session + compatible-instance-switch boundary.
  • #2365 discusses why genuinely different Claude-compatible endpoints may not
    be safe to resume across.
  • #3797 proposes transcript handoff for incompatible provider/model switches.
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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in apps/server by tracing ProviderCommandReactor.ensureSessionForThread, ProviderService.startSession, and ClaudeAdapter through the stopped-session and instance-switch path. Reproduce the regression with compatible instances A and B, then verify that B receives A's persisted cursor or that the switch is rejected before sending the message, with no silent blank session.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.