Conversation
Set JPY_PARENT_PID so ipykernel's parent poller exits kernels when the owning process hard-dies (SIGKILL/crash/OOM), add a parent-death watchdog thread to the forkserver script, and register kernel and forkserver pids in the orphan process journal so supervisor recovery can reap them. fixes ENG-5310
ipykernel's Unix poller distrusts a parent_handle that differs from getppid() at startup and falls back to watching for pid-1 reparenting, which subreapers (systemd --user) never trigger. Forked children now pass their real parent (the forkserver) whose own watchdog ties it to the worker, so the death chain holds on every platform. The fork-request env no longer carries an intentionally-ignored JPY_PARENT_PID.
alexzhang13
requested changes
Aug 19, 2026
…rotocol Forked kernels were signaled by bare pid from Node (process.kill), which can hit a reused pid and write a wrong inactive journal record that masks a sibling manager's active one. The forkserver is the kernels' parent and waitpid-reaps them, so it now owns kill and liveness: new id-keyed protocol messages let KernelManager kill/poll through a ForkedKernelHandle, the Python side only signals a pid found in its un-reaped-children table while SIGCHLD delivery is excluded (blocked in all threads, handled only by the main thread outside the check+kill section), and the inactive journal write is gated on a confirmed outcome — uncertainty leaves the active record for the supervisor reaper, which verifies process identity before acting. fixes ENG-5310
Raw-pid keying could alias across forkserver children: a reaped pid reused by a later fork made kill/alive act on a sibling manager's kernel. Fork request ids are unique and never reused, so the forkserver now keeps a bounded id -> (pid, alive) registry (FIFO, 4096) and kill/alive by id can only ever act on the caller's own incarnation; evicted ids fail closed. Fork bookkeeping now runs with SIGCHLD blocked so a fast-exiting child can't be reaped before registration (the forked child unblocks the inherited mask before running the kernel), and the inactive orphan-journal write is restricted to the 'signaled' outcome — the only one that proves the pid still named our child at kill time. fixes ENG-5310
…ound journal writes A stale in-flight doStart (superseded by a public restart) could resume and tear down or corrupt the successor kernel; a hung forkserver could stretch startup failure past the 5s budgets via the 10s protocol timeout; and the forkserver's inactive journal write was unconditional. Starts now own a generation token bumped by every teardown: stale resumes and stale failure catches bail without side effects, shutdown and dispose skip cleanup when superseded mid-await, and liveness probes during startup are bounded by the remaining budget (timeout counts as alive so the loop deadline owns failure). The forkserver journal write now requires an observed exit or confirmed handle-based delivery. fixes ENG-5310
shutdown() now reports whether it performed the cleanup; start recovery resurrects to idle only as the owning cleanup, so a kill() racing the recovery can no longer be undone. Replaces the generation+1 idiom and the remaining inline staleness comparisons with the one startStale predicate.
# Conflicts: # packages/coding-agent/CHANGELOG.md
# Conflicts: # packages/coding-agent/CHANGELOG.md
# Conflicts: # packages/coding-agent/CHANGELOG.md # packages/coding-agent/src/core/kernel/index.ts
FIFO eviction bounded total forks, not dead entries, so the 4097th fork on one forkserver dropped the oldest still-running kernel: its liveness read false (tearing down a healthy kernel) and its kill was unroutable — the exact orphan leak this change prevents. Eviction now sweeps exited entries only; live entries are bounded by real concurrent kernels.
# Conflicts: # packages/coding-agent/CHANGELOG.md
alexzhang13
requested changes
Aug 20, 2026
An earlier merge-with-main resolved the generated model catalog as ours, reverting newer pricing/context data; the watchdog PR must not touch it.
alexzhang13
previously approved these changes
Aug 20, 2026
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3393591. Configure here.
…e journal writes on a delivered signal A forkserver stalled in a slow fork rejects isAlive with a request timeout; the liveness monitor took any rejection as death and tore down healthy kernels. ForkServerUnavailable now carries a timedOut flag and the monitor treats a timed-out probe as unknown (alive), with an in-flight latch so 1s polls cannot pile up behind a stalled probe. Proven unavailability (socket death) still counts as dead. Direct-spawn cleanup wrote an inactive journal record even for a child that had long exited, which can mask a sibling manager's active record for a reused pid; it now writes inactive only when kill() delivered a signal, matching the forked branch's rule. Also retargets the hung-probe startup test to the probe budget itself (main's cold-boot change raised the ports budget to 30s, past the test's wall-clock bound).
…cleanup generation check cleanupResources bumps startGeneration, so the final startStale check read every non-superseded shutdown as superseded and returned false; startup-failure recovery then never resurrected the manager to idle, leaving it bricked in shutdown after a failed ports resolve. Ownership is now captured where the cleanup decision is made. The superseded-shutdown watchdog test was passing because of this bug (its parked send short-circuited via waitForKernelExit on a missing kernel handle); it now parks genuinely and still pins false.
alexzhang13
self-requested a review
August 20, 2026 18:24
alexzhang13
approved these changes
Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Hundreds of leaked ipykernel processes (and ~1,900 stale
prime-agent-kernel-*temp dirs) were draining batteries: every graceful shutdown path is clean, but when the owning process hard-dies (SIGKILL, crash, OOM — including root TUI sessions and killed test runners) the kernel's dispose never runs and it lives forever.What changes:
JPY_PARENT_PIDin their environment, arming ipykernel's built-in parent poller: direct spawns watch the Node owner; forked kernels watch their real parent, the forkserver (ipykernel's Unix poller distrusts a parent_handle that differs fromgetppid()and would fall back to a pid-1 reparent watch that subreapers never trigger — so the forked child passes the forkserver pid explicitly after the per-kernel env is applied, the trait default being frozen at template import).os.getppid()and exits when its parent (the worker) disappears. A dead forkserver reparents its kernels, which trips their pollers — closing the chain on every platform, subreaper or not.Follow-up (review feedback on pid-reuse): forked kernels are no longer signaled by bare pid from Node — after pid reuse that could kill an innocent process and write a wrong inactive journal record masking a sibling manager's active one. The forkserver (their parent, which waitpid-reaps them) now owns kill and liveness through new additive control-protocol messages, keyed by the fork request id rather than the pid: ids are unique and never reused, so a kill/alive can only ever act on the caller's own child incarnation, even if that child's pid was recycled by a later fork. The forkserver keeps a bounded id → (pid, alive) registry (FIFO, 4096 entries; evicted ids fail closed as
unknown-pid), reaps children under a SIGCHLD discipline that blocks the signal in every thread but the main one and across both fork bookkeeping and the check+kill section (a parent signaling its own un-reaped child is POSIX-race-free; the forked child unblocks the inherited mask before running the kernel). The inactive orphan-journal write is restricted to thesignaledoutcome — the only one proving the pid still named our child at kill time;already-exited,unknown-pid, and errors leave the active record stale for the identity-verifying supervisor reaper, since the journal itself is pid-keyed and a wrong inactive write could mask a sibling manager's record.Verified:
already-exited, unknown-fork-id never signals, liveness from the registry, kill outcomes truthful under concurrent child churn/reaping, a fast-exiting child never lands alive in the registry, evicting a still-alive entry stays fail-closed and reap-safe, dead-forkserver fallback never signals, journal gating (signaled→ inactive; everything else → stale-active).kill -9of a real daemon worker took forkserver and kernel down in 2s; full feature smoke (persistent kernel state,%%bash, rlm subagent + agent messaging, direct-spawn path) clean with zero stray processes after daemon shutdown.kernel.pid_max=400, canary process recycled onto the dead kernel's pid before dispose): old code wrongly killed the canary 8/8 iterations; fixed code 0/8 with 8/8 recycles — the race reproduces deterministically and the fix eliminates it.npm run checkclean.Out of scope (ENG-5311, parallel PR): reaping pre-existing orphans and sweeping stale temp dirs via doctor.
LOC: +866 / -74 total (src +271/-74, tests +593/-0, changelog +2).
Fixes ENG-5310 (https://linear.app/primeintellect/issue/ENG-5310/kernels-must-not-outlive-their-owner-parent-death-watchdog-orphan)
Note
Make IPython kernels and forkserver exit when their owner dies
JPY_PARENT_PIDso the kernel exits if the owning process dies. Forked kernels delegate kill and liveness queries through the forkserver, which also watches its own parent and terminates on owner death.ForkServerrefactored to a request/reply protocol returningForkedKernelHandle(withkill()andisAlive()) instead of a raw pid. This avoids pid-reuse races by routing all signaling and liveness through the forkserver with explicit outcomes.KernelManageradds generation-based guards (startGeneration,startStale()) to fence off stale concurrent start/shutdown interactions, and bounds liveness probes by startup budgets so hangs don't extend timeouts.deepseek/deepseek-v4-flashandmoonshotai/kimi-k2.6in models.generated.ts.forkKernelnow returnsForkedKernelHandleinstead of a numeric pid;dispose()writes orphan journal inactive records only on confirmed kill or observed exit, leaving records active otherwise.Changes since #1559 opened
_serveserver loop to retain live child kernels beyond thehistoryBoundby selectively evicting only entries withaliveflag set to false, iterating through tracked children and removing exited entries until within the bound, eliminating removal of live entries and their pid-to-id routing mappings [967b2e9]historyBoundand only exited entries are evicted [967b2e9]deepseek/deepseek-v4-flashmodel [14288cf]moonshotai/kimi-k2.6model [14288cf]ForkServerUnavailableerror class and configured fork server request timeouts to use it [10aef5c]KernelManager.shutdownmethod to track cleanup execution with a boolean flag and determine ownership before performing cleanup operations [940bc35]kernel-parent-watchdog.test.tsandkernel-shutdown.test.tsto verify the new shutdown return value contract [940bc35]Macroscope summarized 3393591.
Note
High Risk
Changes how IPython kernels and the forkserver are signaled, reaped, and journaled for supervisor recovery. A bug here can leak processes or, on pid reuse, kill an unrelated process.
Overview
Stops leaked IPython kernels after a hard crash (SIGKILL/OOM) and closes a pid-reuse race on forked kernels.
Direct spawns set
JPY_PARENT_PIDso ipykernel’s parent poller exits with Node. The forkserver pollsgetppid()and dies with the worker; forked children watch the forkserver viaparent_handle. Kernel and forkserver pids are journaled as orphans on spawn; inactive is written only on a confirmed kill or observed exit so a reused pid cannot mask a sibling’s record.forkKernelnow returns aForkedKernelHandle. Kill and liveness are keyed by never-reused fork request ids (not rawprocess.kill), with SIGCHLD-blocked check+kill and a bounded registry that evicts only exited entries.KernelManagerusesstartGenerationso concurrent start/shutdown cannot tear down a successor kernel.Reviewed by Cursor Bugbot for commit 10aef5c. Bugbot is set up for automated code reviews on this repo. Configure here.