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. A kill -9 of the backend skips every finalizer, and startup recovery then marks the run cancelled while the claude process is still alive.
What happens
ClaudeAgentSdkQueryRunner.open calls query() without a spawnClaudeCodeProcess (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, and kill -9 of that pid doesn't signal the group. The claude process gets reparented (to systemd --user in your trace) and keeps going.
Graceful shutdown does close the query. closeSession calls query.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/acted row shows.
On the next boot, ProviderRuntimeRecoveryService assumes the processes are gone (the comment around line 570), writes run.updated with status: "cancelled", and idles the provider thread. No pid is recorded and nothing is reaped first. The invalid_mcp_credential 401 is the new server correctly refusing the orphan's MCP reconnect. Local tools don't go through that check, so git, file edits, and gh carry 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
- On Linux, one option is a wrapper that sets
PR_SET_PDEATHSIGand then execs, installed throughspawnClaudeCodeProcess. It doesn't carry over to grandchildren, and macOS has noprctl. - Another option works across platforms and already exists in the tree as
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 claude processes whose parent is pid 1 or the systemd user manager is the right workaround. A maintainer will decide on the fix direction.