Repository navigation
Description
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
Summary
When the desktop backend dies abruptly (SIGKILL), the claude processes it spawned keep running. On restart, recovery marks their runs cancelled and assumes the processes are gone. They aren't. The agents carry on calling tools: running git, editing files, posting to GitHub. None of their output reaches T3. The user sees a cancelled thread while the agent keeps changing their machine.
Visual explainer with the full timeline: https://post.patchyhq.com/d/9yc2qhd8o4yy
Steps to reproduce
- In a Claude thread, ask the agent to run
until [ -e /tmp/release ]; do sleep 1; donein the foreground, then runtouch /tmp/actedand reply "done". - While it waits,
kill -9the backend from a terminal outside T3 (its pid is in~/.t3/userdata/server-runtime.json). Let the desktop restart it. - Wait for the run to show as cancelled, then
touch /tmp/release.
Expected behavior
The agent process dies with the backend, or recovery kills it before marking the run cancelled.
Actual behavior
The agent process is reparented to the user's systemd manager and keeps going. It creates /tmp/acted and replies "done". T3 shows neither.
From our repro (UTC):
| Time | T3 recorded | The agent did |
|---|---|---|
| 21:50:57.092 | Backend killed. The agent shares the backend's pgid and sid. | Waiting in its shell loop |
| 21:50:58.861 | Run cancelled, provider thread idle, no assistant item |
Still alive, ppid now 1167 (systemd --user) |
| 21:51:24.508 | Nothing | Next tool call: writes /tmp/acted |
| 21:51:25.488 | Nothing | Final reply written to its Claude transcript |
In the original incident, two mid-turn agents went on for 4 and 7 minutes after the crash and filed GitHub issues that never appeared in T3. An idle agent with a Claude Code Monitor running also survived. Its Monitor fired 91 seconds after the crash, and the orphan ran a whole new turn on its own for about 10 minutes. Another idle agent, waiting on a plain background command, exited instead (that case is #15358). So what an orphan does depends on Claude Code internals.
Orphans reach the restarted backend but get refused. Their MCP client reconnects and gets 401 invalid_mcp_credential, which is right. The problem is that they're still alive.
Cause
ClaudeAdapterV2.ts:614opens Claude through the SDK'squery(). The SDK spawns the CLI withoutdetached, so it shares the backend's process group and session, and nothing kills it when the backend dies abruptly. Graceful shutdown closes the query in a finalizer (ClaudeAdapterV2.ts:7314), but SIGKILL skips that.ProviderRuntimeRecoveryService.ts:570says// All provider processes are gone on startup/shutdown, then marks the runs cancelled (:306). There is no pid ledger, no reaping of orphans, and no way to reattach.
#14926 hit the same behaviour in the test suite: the fake Claude CLI was "reparented to PID 1 and runs until reboot", so it now exits when stdin closes. The real CLI keeps working after stdin closes mid-turn. PR #15323 handles SIGINT and SIGTERM but not SIGKILL, so orphans aren't covered there either.
Possible fix
Make that comment true. Either set PR_SET_PDEATHSIG on provider children (or spawn them through a small wrapper that does), or record each provider's pid and start time and kill survivors during startup recovery, before marking runs cancelled.
Impact
Major degradation or frequent failure
Version or commit
0.0.46-nightly.20261003.2632 (f391794), Orchestrator V2
Environment
Linux 7.2.5 (Arch-based), desktop AppImage, Claude Code 2.1.288 via Agent SDK 0.3.276, Claude Opus 5.5
Logs or stack traces
# Claude Code MCP log, 11 seconds after the backend was killed
Error POSTing to endpoint: {"error":"invalid_mcp_credential","message":"A valid provider-scoped MCP bearer credential is required."}
Server rejected the configured Authorization header (HTTP 401).
# desktop.trace.ndjson, runBackendProcess exit cause
Process interrupted due to receipt of signal: 'SIGKILL'Workaround
None in T3. After a backend crash, check for claude processes whose parent is pid 1 (or your systemd user manager) and kill them by hand.
Related reports from the same incident: #15358 (a thread waiting on background work stays stranded after a restart), #15359 (a killed backend is logged as "Failed to read the exit status").
Filed by Allison Mahmood. Investigated and drafted with Claude Opus 5.5 in Claude Code.
Activity
juliusmarminge commentedon Oct 3, 2026
Note
Grok responding on behalf of Julius.
Triage
Thanks for the precise report and the timeline, @allisonmahmood! The code on current
main(5e35272f) matches what you found. Akill -9of the backend skips every finalizer, and startup recovery then marks the run cancelled while theclaudeprocess is still alive.What happens
ClaudeAgentSdkQueryRunner.opencallsquery()without aspawnClaudeCodeProcess(apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts). The child shares the backend's process group and session and gets no parent-death signal. On Linux, Effect spawns the desktop backend detached, so the group id is the backend's pid, andkill -9of that pid doesn't signal the group. Theclaudeprocess gets reparented (to systemd--userin your trace) and keeps going.Graceful shutdown does close the query.
closeSessioncallsquery.close()through a session-scope finalizer, but SIGKILL never runs it. Merged #14926 only made the fake test CLI exit when stdin closes. The real CLI keeps working after that, which is what your/tmp/actedrow shows.On the next boot,
ProviderRuntimeRecoveryServiceassumes the processes are gone (the comment around line 570), writesrun.updatedwithstatus: "cancelled", and idles the provider thread. No pid is recorded and nothing is reaped first. Theinvalid_mcp_credential401 is the new server correctly refusing the orphan's MCP reconnect. Local tools don't go through that check, so git, file edits, andghcarry on with nothing written back to the thread.The idle Monitor case is the same orphan. #15358 is the opposite situation, where the process really is gone and the thread is never told, and #15359 is only about the log line. Open #15323 handles SIGINT and SIGTERM but doesn't record or kill a process that outlived SIGKILL. Upstream, claude-agent-sdk-typescript#142 asked for parent-death termination and was closed as a duplicate.
Likely fix area
PR_SET_PDEATHSIGand then execs, installed throughspawnClaudeCodeProcess. It doesn't carry over to grandchildren, and macOS has noprctl.OpenCodeServerLedger. Record the pid, start time, and command, then on the next start kill only a process that still matches, before the cancelledrun.updatedevents are committed. A group kill would need the same start-time check, because the dead backend's pid can be recycled.Until then, killing leftover
claudeprocesses whose parent is pid 1 or the systemd user manager is the right workaround. A maintainer will decide on the fix direction.smagnetize commentedon Oct 11, 2026
Another reproduction from daily use. It adds one consequence the report doesn't cover: after recovery, restart continuation starts a second agent in the orphan's worktree, so two agents work one checkout.
For example, on 2026-10-06 the backend was SIGKILLed at 21:41:08 UTC. Recovery marked the runs cancelled, and continuation sent "Continue where you left off" to a fresh Claude session on each thread. Meanwhile the original
claudeprocesses, reparented to the systemd user manager, kept working in the same worktrees.What two agents in one checkout did
Environment and frequency
t3code-nightly-bin 0.0.46_nightly.20261006.2735, Orchestrator V2, desktop on Arch Linux (kernel 7.2.5), Claude Code via the Agent SDK.desktop.trace.ndjsonlogs "Process interrupted due to receipt of signal: 'SIGKILL'" for the backend at 2026-10-06 21:41:08, 2026-10-07 19:51:49, 2026-10-07 21:07:23 and 2026-10-08 04:40:24 UTC.For #15450
The ledger reap would prevent this, because it runs before recovery and so before continuation. Two suggestions:
Our workaround until then: a pre-tool hook refuses edits, commits, pushes and paid calls while another
claudeorcodexprocess has the same working directory, and names that process so whoever coordinates can pick one.fix(server): agents stop when the server that launched them dies on L…
fix(harness): record agent processes so the next engine can end orphans
feat(engine): reap agents an earlier engine left running at startup
never102 commentedon Oct 17, 2026
Another reproduction, on macOS, triggered by an in-app update rather than a crash. PR #15450 covers Linux only (its notes say macOS wasn't run live), so this may help scope the macOS half.
What happened (T3 Code Nightly, macOS, Claude provider, 2026-10-16)
query.openwithsessionId) and starts a long turn.query.openwithresumeset to the same session id) and offers "Continue where you left off." The log has noquery.closefor the first query.claudeprocess (--session-id <id>) is still alive, now parented by launchd (ppid 1). The new one runs--resume <id>under the new backend. Both write to the same session transcript.So on macOS the update path does what this issue describes for a SIGKILL on Linux, including the second-agent effect @smagnetize reported.
Workaround we run meanwhile: a Claude Code SessionStart hook that stops any
claudeprocess whose command line carries the same session id and whose parent is pid 1, skipping its own ancestors. It's the triage's second option (a pid ledger) done from the agent side, and it would be redundant once the server reaps its own agents on every platform.fix(server): agents stop when the server that launched them dies on L…
Cosmonaut-itc commentedon Oct 19, 2026
Another macOS reproduction from an in-app update, with one detail that may help scope the fix: the update path's graceful stop ends in SIGKILL when the backend is busy, so it lands in the same case as a crash.
Timeline (T3 Code Nightly, macOS, updating to
0.1.1-nightly.20261018.3335; Claude Code 2.1.303; times fromdesktop.trace.ndjson, UTC)desktop.updates.installDownloadedUpdate→desktop.backendInstance.stop.desktop.backendInstance.finalizeRun. That is about 6 s later, just pastDEFAULT_BACKEND_TERMINATE_GRACE(5 s, thenforceKillAfter). About seven Claude threads were open and several were mid-turn, so the backend most likely didn't finish closing its queries and was force-killed.claude --resume=<id>for each thread at once.Result
Three threads ended up with two live
claudeprocesses on the same session id. The old ones had ppid 1, the new ones the new backend. Both appended to the same transcript.jsonl. The orphans kept working for 25 to 40 minutes, invisible in T3:Nothing was lost only because we noticed and had each resumed session read the transcript, take over the orphan's pending work, and kill it.
Workaround we run meanwhile
This is similar to the one described above. A Claude Code
SessionStarthook finds any otherclaudeprocess with--input-format stream-jsonand the same--session-id/--resumevalue that isn't one of its own ancestors. It then injects its PID and child processes into the new session's context, so the session hands the work over before killing the orphan. It doesn't kill anything itself, because killing the orphan outright would drop in-flight work.A server-side fix probably needs both halves: reap or reattach surviving agents before resuming a session, and give the update path's stop enough time, or a clean kill of the provider children, so it never escalates to SIGKILL with agents still attached.
feat(claude): hook que avisa de otro proceso en la misma conversación (…
coygeek commentedon Oct 20, 2026
Current-source update to the cause analysis above: the Claude launch and startup cleanup have changed since the original report, but macOS still bypasses the leftover-agent cleanup.
I inspected the latest published nightly,
v0.1.1-nightly.20261020.3411atd36b7da2485bc889b320a285deea181f637d760a, and refreshedmainat6ab39ff6b8f60447c2580b818351fe4387fffa82. The relevant Claude spawn, agent-scope, and desktop shutdown files are byte-identical between those revisions.spawnClaudeProcessGroup, which setsdetached: true. The query runner selects this function outside Windows at lines 921–924. Its child-exit callback kills the remainder of the group, but that callback runs in the owning server and cannot run after that server dies. This updates the original diagnosis that Claude shares the backend's process group.AgentScope.makereturns the no-op default on every non-Linux platform. ThestopLeftoverAgentsstartup reap in that file therefore does not run on macOS. Its own comment states the intended invariant: stop agents left by a killed server before resumed sessions work in the same checkout.quitAndInstall. Normal quit/relaunch also waits for shutdown inapps/desktop/src/app/DesktopLifecycle.ts. The backend still has a five-second termination grace inapps/desktop/src/backend/DesktopBackendManager.ts. These calls establish intended cleanup; they do not establish that the update incidents above timed out or identify their actual termination signal.As a bounded mechanism check, I extracted the current spawn function, removed only its TypeScript signature annotations, and ran it with a harmless Node heartbeat provider that continues after stdin EOF. Once per baseline, killing only the captured stand-in server PID with SIGKILL left the provider alive under PPID 1 with advancing heartbeats. It overlapped a second manually launched stand-in provider. In the control, SIGTERM triggered the stand-in server's AbortController and its provider exited. All fixture processes were stopped afterward.
This was a process-level stub check, not a full T3 Code/real-Claude reproduction. It does not establish real Claude's EOF behavior, automatic restart continuation, or the cause of a particular app restart. The useful source distinction is that current process-group cleanup is parent-owned, while startup reclamation remains Linux-only. The existing macOS incident evidence in this issue is still needed to connect that mechanism to real provider activity.
t3dotgg commentedon Oct 21, 2026
Note
🤖 Claude Opus 5.5 responding on behalf of Theo
Thanks for the report. One part of this is fixed on main in #17662. Please test on the nightly built from main on 2026-10-21, or on the next stable release. One part is still open: reap orphaned Claude agents on macOS before recovery or resume. If that part still matters to you, please open a new issue for that part only.