[BUG] Background "bg" sessions permanently zombie after their owning window closes — daemon never marks them settled, visible as active in cross-window fleet view (macOS, 2.1.210)
Summary
Claude Code runs a single background daemon per machine (claude daemon run, tracked in ~/.claude/daemon.lock / daemon.log / daemon.status.json) that manages background ("template": "bg") sessions across all terminal windows and working directories, not just the one that spawned it. Any UI that lists "agents"/fleet across windows reads job state from ~/.claude/jobs/<id>/state.json.
When the terminal window/process owning one of these bg sessions is closed (or otherwise dies) while the job is mid-flight, the daemon never reaps it: state.json is left with "state": "working", and no corresponding bg settled <id> (done|killed) line is ever written to daemon.log. The job then persists indefinitely as an apparently-active agent in any cross-window agent/fleet view, with no live process behind it.
This looks like the same underlying defect class as #65423 (no timeout/reaping for agents that die mid-flight) but manifesting in a different subsystem: not a parallel Agent sub-agent stuck in one session's TUI switcher, but a whole daemon-tracked bg session orphaned at the cross-window/daemon level.
Environment
- Claude Code 2.1.210
- macOS (Darwin 25.3.0)
- Multiple concurrent windows/sessions across three separate local checkouts of the same monorepo (
repoA,repoB,repoC), all sharing one daemon (spawnedBy.cwdindaemon.lockshows the daemon was actually started from a third, unrelated window)
Evidence
Job 04e3d857 (cwd: repoB), compared against sibling jobs from the same daemon:
| Check | 04e3d857 | Sibling jobs (3cf8d140, 5eb4a370, 6fb75b2a) |
|---|---|---|
| state in state.json | "working" | "done" |
| updatedAt vs now | stale (2h+ with no further update) | consistent with a normal finish |
| Matching OS process (ps aux \| grep <sessionId>) | none | none (expected, they finished cleanly) |
| bg settled <id> (...) line in daemon.log | absent | present ((done) or (killed)) |
| daemon.status.json → workers | {} (daemon itself has no live worker for it) | n/a |
No ~/.claude/daemon.json index exists to cross-check against, so the only record of this job's state is the stale state.json file itself.
Repro (best-effort — root cause is inferred, not a guaranteed recipe)
- Start a background
bgsession from one window/repo (e.g. via a backgroundAgent//loop/async session) and let it reach"state": "working". - Close the terminal window/process that owns that session while it's still working (not via a graceful stop).
- Inspect
~/.claude/jobs/<id>/state.jsonfrom a different window:stateremains"working",daemon.lognever logs asettledline for that id, and no OS process matches the session id. - Any agents/fleet view that aggregates jobs across windows still lists it as active.
Expected
The daemon should detect that the process backing a bg job has exited (regardless of cause) and mark it settled (done/killed) in state.json, so cross-window views never show a dead job as active.
Actual
The job is left in "state": "working" forever. Only manual filesystem cleanup (renaming/removing ~/.claude/jobs/<id>/) clears it; there is no user-facing way to reap it.
Impact
- Cross-window agent/fleet views accumulate stale entries that look active, with no way to tell them apart from real in-progress work without inspecting the filesystem.
- No programmatic recovery: the zombie job has no live process, isn't in any session's task list, and doesn't respond to
TaskStop(which only resolves jobs known to the current session).
Related
- #65423 — same defect family (no timeout/reaping for agents that die mid-flight), but at the in-session parallel-
Agent/TUI-switcher layer on Windows, not the cross-window daemon-trackedbgjob layer on macOS. Filing separately since the repro, evidence, and affected subsystem differ.
Suggested fix
- Daemon should periodically verify liveness (PID check) of every
bgjob it tracks and mark dead onessettled(e.g.killed) instead of leavingstate.jsonuntouched. - Cross-window agent/fleet views should do their own liveness check before rendering a job as active, rather than trusting
state.jsonverbatim.
Workaround
Manually inspect ~/.claude/jobs/*/state.json, cross-reference against daemon.log for a missing settled line and against ps aux for a missing process, then rename/remove the stale job directory.