[BUG] Memory-pressure reap kills only the tracked shell — child process tree survives as orphans and keeps running (Linux, 2.1.216); manual TaskStop correctly kills the tree
Summary
When a run_in_background Bash task is killed by the background-shell memory-pressure reaper (the mechanism diagnosed in #78674, added in 2.1.193 per the changelog), only the tracked shell dies. The task's descendants (make → sub-make → test runner) survive, are reparented to PID 1, and keep executing to completion — while the task notification reports <status>killed</status> / Background command "…" was stopped.
This is worse than just losing the task: the agent (and user) believe the job is dead, re-dispatch it, and the rerun executes concurrently with the invisible orphan. If the job mutates shared state, the runs corrupt each other.
By contrast, a manual TaskStop on the same version does kill the whole tree (verified below) — the reap path appears to use a weaker kill than the stop path.
Environment
- Claude Code v2.1.216 (native installer), interactive CLI session inside tmux
- Ubuntu, kernel 6.8.0-111-generic, x86_64, 16 CPU, 62 GB RAM
- Host memory signature matches #78674:
MemFree≈ 1.9 GB,MemAvailable≈ 41 GB, buff/cache ≈ 40 GB,/proc/pressure/memoryall avgs 0.00
Observed in the field (3 of 3 reap kills left orphans)
In one ~20-hour session, three long make-driven integration-suite dispatches were killed within 10 minutes — 2026-07-21 09:27:49, 09:33:45, 09:37:49 UTC (task IDs bhodco8vy, b4hte8yz6, bu71xbto7) — with the same no-attribution mass-kill signature as #78674: no TaskStop anywhere in the transcript, nobody at the keyboard, journald/dmesg clean at the kill seconds, no host-side kill mechanism (cron audited). After each killed notification:
- the suite's child process tree (
make→ scenario runner) was still alive, reparented to PID 1; - each orphan ran to completion and wrote its report artifacts;
- because
killedwas taken at face value, the suite was re-dispatched — up to three suites then overlapped on the same shared test state (a virtual tape library), and the churn produced a garbage verdict for one run. On top of the lost tasks, the orphan overlap cost a debugging cycle chasing a phantom test regression.
Control: an identical suite run started with nohup … & outside the task system, on the same box in the same minutes, ran undisturbed to completion — the killer only touches tracked tasks.
Verified: TaskStop kills the tree; the reap kill did not
On the same version I started a background task bash parent.sh, where parent.sh runs child.sh in the foreground and child.sh appends a timestamp to a file every 2 s. Process tree: wrapper shell → parent → child (one process group). TaskStop killed all three within a second and the heartbeat file stopped growing — correct behavior. The field kills above (reap path) left the equivalent children running in 3 of 3 cases.
Expected
- Every kill path — pressure reaper,
TaskStop, session teardown — should terminate the entire process tree (process-group kill at spawn viasetsid+kill(-pgid)with SIGTERM→SIGKILL escalation, or a descendant walk). - A task should be reported
killedonly when its tree is actually dead. Otherwise re-running a non-idempotent job straight into a live orphan is the expected agent behavior. - Echoing #78674 / #73440: attribute reaper kills in the notification — the generic
was stopped, byte-identical to a manual stop, made this expensive to diagnose.
Workaround
CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1in settingsenvprevents the reap firing at all (it does not fix the kill-semantics gap).- After any unexplained
killed,pgrepfor the job's children before re-dispatching.
Related
#78674 (reaper fires on low MemFree despite high MemAvailable/zero PSI — same host signature, same burst pattern), #73440 (reap indistinguishable from manual stop, idle detection ignores child activity).