[Bug]: App restart leaves a mid-turn Claude agent running, then resumes its thread in a second process#23370

opened18d agobyMmichael0903

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. Start a Claude thread and send a prompt whose turn runs a long tool call, for example a Bash command sleep 60; touch /tmp/t3-orphan-marker.
  2. While that tool call runs, restart the desktop app. Any of these triggered it here: installing a downloaded Nightly update, changing the server exposure mode (which relaunches the app), or a crash of the desktop process.
  3. After the app comes back, open the thread so it resumes, then run ps -axo pid,ppid,pgid,command | grep -E -- '--(resume|session-id)='.

Expected behavior

Only one claude process runs for the session. The process from before the restart is stopped when its server goes away, or at the latest before the thread is resumed.

Actual behavior

The process from before the restart keeps running with PPID 1, finishes its turn, and then exits. The marker file appears. Meanwhile T3 starts a second process for the same session with --resume=<session>. Both act on the same worktree. Nothing the old process does reaches the UI, but its side effects do: here it committed, pushed and posted pull request review replies alongside the resumed agent.

Root cause, as far as I can tell:

  • spawnClaudeProcessGroup (apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts) spawns the CLI with detached: true, so it leads its own process group. That is deliberate for #3089. The group SIGKILL, though, is attached to the child's own exit event, and the abort signal only works while the server process is alive. When the server exits first, through a crash, an update install or a relaunch, nothing signals the detached groups.
  • In the update case, desktop.backendInstance.stop took 5,010 ms, which looks like a graceful-stop timeout followed by a hard stop.
  • Startup reconciliation (#7719) settles sessions whose provider process is gone. It cannot see a provider process that survived, so the thread is resumed next to it.
  • The Claude CLI keeps executing its in-flight turn after its stdin and stdout pipes close. A parent that starts it with detached: true and pipes, sends a prompt running sleep 15; touch marker, then exits as soon as the tool call starts, still gets the marker, with the CLI alive afterwards. SIGTERM stops the CLI immediately, so signalling the group on shutdown would work.

Possible fixes: record provider process-group IDs and signal them on backend stop and relaunch, and on startup terminate recorded survivors (or any claude process carrying the session ID with PPID 1) before resuming a thread.

Impact

Major degradation or frequent failure

Version or commit

0.1.2-nightly.20261021.3499 (Claude Code 2.1.307)

Environment

macOS 26.6.2, Apple Silicon, desktop app, Claude provider

Logs or stack traces

shell
1# desktop.trace.ndjson, one session, three restarts (local time)
210:35:39 desktop.updates.installDownloadedUpdate -> desktop.backendInstance.stop (5010ms) -> backend start 10:35:56
311:02:03 desktop.startup (no quit/relaunch events before it)
411:12:10 desktop.serverExposure.setMode -> desktop.lifecycle.relaunch -> startup 11:12:16
5
6# after each restart: the old process, orphaned, plus the resumed one
7PID PPID PGID COMMAND
890835 1 90835 claude --output-format stream-json ... --session-id=<session>
999730 98076 99730 claude --output-format stream-json ... --resume=<session>

Workaround

A Claude Code SessionStart hook with matcher resume that sends SIGTERM to other claude processes carrying the same session ID whose PPID is 1. It closes the double-writer window once the thread resumes. It does not cover the time between the restart and the resume.

Reproduced on main 3b95e7831b with the real web/dev server on Linux and Claude Code 2.1.303. Started a Claude turn running sleep 60; touch /tmp/t3-23370-orphan-marker, killed the test backend during the tool call, then restarted it. Without a systemd user manager, the old Claude process survived and a second process resumed the same session; the original command created the marker after the restart. With systemd scopes enabled, startup cleanup killed the survivor before the marker appeared. The macOS desktop update/relaunch paths have not been verified here.

Verified the landed fix 5ffe045931 from #23389. On Linux without a systemd user manager, ran the real app with Claude Code 2.1.303, started sleep 60; touch marker, killed the test backend mid-tool, and restarted it. The old Claude and its separate Bash process group stopped; only one resumed Claude remained, and the marker was absent after the original deadline. The focused query-runner suite passed all 3 tests. Runtime transcript and test output. macOS Electron update/relaunch/crash were not run on this Linux VM. This issue is already closed by the landed fix; no duplicate PR was opened.