[Bug]: Sending to another Claude instance leaves the old Claude process running unseen#20343

opened17d agobyAAlexChambers

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 Claude provider instances, A and B, with different Claude config directories, so they don't share native history.
  2. On instance A in Full access, ask Claude to start a background subagent (Agent tool, run_in_background) that works for a few minutes and then edits a file. Let the turn finish, so the thread is idle while the subagent runs.
  3. Select instance B in the composer and send a message. The thread shows a "Provider handoff" item and the run starts on B.
  4. Check instance A's claude process with ps, and wait for the subagent to finish.

Expected behavior

When the send hands the thread to B, T3 detaches A's session and stops its process. It already does this when the same switch starts from a queued run or from thread.model-selection.set. If the background subagent should survive, T3 refuses or defers the switch, as discussed in #16000. No work runs on A that the thread doesn't show.

Actual behavior

A's process keeps running and stays bound to the thread. Its session row still reads ready. When the subagent finishes, Claude Code on A starts its own wake turn. T3 offers one continuation. It runs on B for about 5 seconds with no output, because B can't replay A's buffer (#17099). After that, A keeps taking turns on its own. T3 reads them from the stream but shows none of them.

This is an unsupervised Full access session. It changes files with no UI, and it keeps spending A's quota after the user moved the thread off A.

One case, nightly build, times UTC:

  • 14:57 T3 writes its last message to A. No query.close for A is logged afterwards.
  • 15:47:40 the user sends with B selected. Handoff from A to B. Only provider-turn.start is queued. The thread has no provider-session.detach effect at all.
  • 15:51 to 17:05 the provider event log for A's session has 7 task_notification, 6 task_started and 7 result frames. In that time A ran shell commands and made 9 git commits in the workspace.
  • 17:05:45 A's account reaches its weekly usage limit.
  • 17:20 the process was stopped by hand with SIGTERM.

Code on main @ e58a485:

  • Confirmed: the send path ignores the release list. dispatchMessage detaches the old session only when canResumeAcrossInstances is true (Orchestrator.ts#L6492-L6519). Instances with different config dirs get create_with_handoff (ProviderSessionTransitionPolicy.ts#L48-L53), and ProviderSwitchService puts A's live session in releaseProviderSessionIds (ProviderSwitchService.ts#L180-L193). startNextQueuedRun (L1736-L1741, L1873-L1882) and dispatchThreadMutation (L3381) use that list. The send path does not. A detach would have stopped the process: Claude sets supportsMultipleProviderThreadsPerSession: false, so detach calls releaseEntry (ProviderSessionManager.ts#L2414-L2423).
  • Suspected: the idle timeout can't catch it. Every event from a session calls touchActivity(..., { providerActivity: true }) (L1946), which also clears the background-work pin (L1226). A session that keeps starting its own wake turns never reaches the 30-minute idle release.
  • Related, already reported: the wake is offered to the thread's current provider, once per native thread (ClaudeAdapterV2.ts#L6206-L6234). That is #17099. PR #17109 drops such wakes. That removes the empty run on B, but A's process still runs.

Impact

Major degradation or frequent failure

Version or commit

0.1.1-nightly.20261016.3190 (9b150f7). The cited code is unchanged on main @ e58a485.

Environment

macOS 26.6.2, T3 Code Nightly desktop. The message was sent from the mobile app; web sends the selection the same way through client-runtime. Claude Code 2.1.301, claude-opus-5-5 on both instances.

Workaround

Wait for background subagents to finish before switching instances. After a switch, stop the old claude process by its PID. Archiving or settling the thread also detaches every bound session.

Suggested fix

  • In dispatchMessage, detach the sessions in the switch plan's releaseProviderSessionIds for every instance switch, as startNextQueuedRun does. Apply the background-work guard that #16000 settles on.
  • Don't let a session the thread no longer uses extend its own life through provider activity, so a missed detach still ends.

Related: #16000, #17099 and #17109, #16565 and #17096 (same trigger, different symptom).

Note

Grok responding on behalf of Julius.

Confirmed on main: this is a bug in the send path.

When a sent message switches the thread to another instance, dispatchMessage only detaches the old session when canResumeAcrossInstances is true, meaning the instances share native history. For instances with different config dirs it starts the run on the new instance and never detaches the old session. It never reads the switch plan's releaseProviderSessionIds. The same switch started from a queued run (startNextQueuedRun) or from a selection mutation (dispatchThreadMutation) does detach the sessions in that list. So the old claude process stays bound to the thread and can keep taking wake turns that the UI doesn't show.

How this relates to other issues:

  • #16000 is the opposite problem: detaching kills live background subagents with no background-work check. The fix here should use whatever guard #16000 settles on, not a bare detach.
  • #17099 / PR #17109 handle the empty wake run on the new provider. They don't stop the old process.
  • #16565 / PR #17096 have the same trigger but a different symptom (the MCP token is stale).

Until this is fixed, wait for background subagents to finish before you switch instances, or stop the old claude process after the switch.

Reproduced on main b5a226e49c with the web dev client on Linux and Claude Code 2.1.303. Started a native background subagent on instance A, let the parent turn finish, selected B with a separate config/history directory, then sent a message. B replied while A remained alive and its session still read ready; the send queued no detach effect. The delayed file-write check was interrupted by a dev-server watcher restart. A focused regression test also fails because both sessions remain after the handoff.