Background worker crash family (#61740 successor): no claim-time cwd validation for spares; adopted workers mis-settled as "crashed" across daemon upgrade
Summary
Follow-up to #61740 (closed as stale on 2026-06-23 without a fix). We root-caused a family of background-worker crashes on macOS via daemon.log forensics (182 job lifecycles, 50 crashes across v2.1.160–2.1.197), inspection of the v2.1.198 bundle, and a controlled 5-launch reproduction. Three distinct modes; one appears fixed in 2.1.198, two remain.
Environment
- macOS (Darwin 25.3.0, arm64), Claude Code native install
- Observed across v2.1.160 → v2.1.197; verified against the v2.1.198 binary (GIT_SHA b80c4964)
- Launch shape:
claude --bg --agent <name> --permission-mode acceptEdits --name <n> "<prompt>"
Mode 1 (still present in 2.1.198 code): claimed spare dies instantly — no claim-time cwd validation
26 crashes, signature: bg claimed-spare <id> (shell) → bg settled <id> (crashed) in 0.036–0.394s, no transcript ever written. Since v2.1.160.
From the 2.1.198 bundle: the claim path builds the worker handle optimistically and sends the claim frame {cwd, env, argv, sessionId, auth} with the dispatch cwd passed through verbatim — there is no fs.access/existence check at claim time. The cwd is first touched spare-side in post-claim init (chdir after realpath-resolve); any throw becomes a spare_postclaim:<ERRNO> breadcrumb and the spare dies. 2.1.198 adds special handling for spare_postclaim:ENOENT + missing cwd (immediate no-respawn settle "working directory no longer exists"), which improves reporting but still burns the launch. #61740's original ENAMETOOLONG shape would not even match that string compare.
Suggested fix (matches the proposal in #61740): validate the dispatch cwd daemon-side before claiming the spare; on failure, fail the dispatch with a clear error instead of consuming a spare and settling "crashed".
Mode 2 (appears fixed in 2.1.198 — please confirm intentional): memory-pressure fresh-spawn death at ~10.83s
23 crashes on 2.1.196/197 (one precursor on 2.1.183), signature: cold daemon (workers=0, no spares), fresh bg spawned <id> (shell), settle "crashed" at a tight 10.819–10.837s. The 10.83s is emergent, not a timeout: attempt 1 dies pre-init (<1s), fixed 10s respawn backoff (Dyf=10000), attempt 2 dies pre-init, !workerReady && attempt>=2 settles crashed. All bursts occurred under macOS memory pressure ≥ warn (the daemon's own kern.memorystatus_vm_pressure_level threshold), which also suppressed the spare pool — explaining the apparent "load-sensitivity".
On 2.1.198 we could not reproduce (0/5 crashes, launches under 76–448MB free with 33 claude processes running) and observed the new bg: low memory (NMB free) — retiring settled workers before spawning <id> behavior, which was not in the 2.1.198 changelog. If that was the fix, great — flagging so it doesn't regress silently.
Mode 3 (still present): adopted worker mis-settled as "crashed" across daemon upgrade
n=1 but fully evidenced: a job completed its turn cleanly (transcript shows turn_duration 189269ms, clean stop-hook record, no errors), sat idle ~33min, the daemon self-restarted for a 2.1.170→2.1.172 upgrade (bg adopt: adopted=3 respawned=0 dead=0), and the new daemon settled the finished job (crashed) 62s later. A bookkeeping loss recorded as a crash.
Repro / evidence available
Full daemon.log with all 50 crash lifecycles, latency distributions per mode, and the per-mode signatures is preserved; happy to attach on request.