Background-job bulk-reap logs cleanly-finished jobs as "(killed)" in daemon.log (v2.1.215) — corrupts exit telemetry
Bug report: background-job bulk-reap tags cleanly-finished jobs as "(killed)" in daemon.log
Version: 2.1.215 (GIT_SHA 316ce99628e89900bf0b1328fed3b8fec0c0c92d, Bun single-file bundle, Linux)
Summary: When the background-job daemon bulk-reaps settled jobs, jobs that already reached a clean
terminal state (state.json → "state":"done") are logged as bg settled <id> (killed) indaemon.log. Any tooling that consumes daemon.log for crash/exit telemetry then manufactures false
"forced kill" signals for sessions that actually exited cleanly.
Mechanics (from read-only inspection of the bundled code):
kill(e = SIGTERM, t = "killed", r)defaults the kill outcome to the literal"killed"unless the
caller passes t = "failed".
- The bulk-reap call site iterates jobs and calls
Q.kill(q)passing ONLY the signal — so the
outcome is always "killed", even when the job already recorded a clean terminal state.
stop(reason = "reap")settles with the unchecked kill outcome; onlyreason = "grace"force-sets
done.
- The
onSettlesubscriber writes thebg settled <short> (<state>)daemon.log line with that state
verbatim. Notably the job dir IS in scope at that point (the code reads state.json a few
statements AFTER emitting the log line), and retireIfSettled() elsewhere in the same bundle
demonstrates the correct read-state-before-decide pattern — the reap path just does not use it.
Reproduction evidence (one concrete instance):
~/.claude/jobs/<id>/state.json→{"state":"done","firstTerminalAt":"2026-07-23T19:00:37.007Z"}daemon.logat2026-07-23T19:28:10.949Z→bg settled <id> (killed)
(same job id; the "kill" is the routine bulk-reap ~28 minutes after the job finished cleanly.)
Expected: the reap path checks the job's recorded terminal state (as retireIfSettled() does)
and logs done for already-settled jobs, reserving killed for jobs that were actually terminated
while running.
Impact: fleet/ops telemetry that treats (killed) daemon.log entries as crash signals
over-reports session crash-exits. Workaround we apply meanwhile: treat a (killed) entry as a crash
only if jobs/<id>/state.json disagrees with a clean done.