[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

Status Open
Reported on v2.1.193
Maintainer reply None cached
Activity 0 comments · opened Jul 21, 2026

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/memory all 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 killed was 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 via setsid + kill(-pgid) with SIGTERM→SIGKILL escalation, or a descendant walk).
  • A task should be reported killed only 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=1 in settings env prevents the reap firing at all (it does not fix the kill-semantics gap).
  • After any unexplained killed, pgrep for 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).

View original on GitHub ↗