Repository navigation
fix(proxy): reap mitmproxy children on stop, quit, exit and startup - #777
Merged
Merged
Conversation
Record why per-session mitmdump children outlive the app (undetached spawn, no parent-death guard, a stop() that hangs when the child already died, no startup sweep) and the src-side design: an argv ownership marker we already control, ppid-based ownership, bounded teardown, a will-quit sweep, and a startup reaper that journals its counts. Refs #767 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
Every Claude proxy-streaming session spawns its own mitmdump undetached, and any death of the Electron main that skips will-quit (Force Quit, native crash, the crash hooks' process.exit(1)) reparents the child to launchd: the 2026-09-03 audit found 20 orphaned mitmdumps (ppid 1, two days old) next to 12 live ones, each holding ~60-100 MB RSS and a port on a swapping 16 GB machine. ProxyServer.stop() also never resolves when the child already died, hanging quit and forcing the Force Quit that orphans the rest. Host-side process hygiene, no submodule changes: identify our children by the confdir argv token we control, decide ownership by kernel ppid (conservative: only kill when the owner is provably dead), terminate with a TERM/KILL grace sequence, bound each session teardown with a stop deadline, sweep owned children after killAll, SIGKILL synchronously on process exit, and reap stale processes from previous runs at startup with journaled counts. Refs #767 (item 5, process-hygiene half)
Integrate current main. Check kernel command, creation timestamp, parent and unchanged argv before TERM and delayed KILL; fail closed on changed or unreadable identity. Bound inspection and synchronous exit work. Retain the documented non-atomic POSIX check-to-signal limitation. Refs #767
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.
Problem
Every Claude proxy-streaming session spawns its own
mitmdumpundetached (claudeSession.ts→createProxyServerin theclaude-code-headlesssubmodule). Any death of the Electron main that skips will-quit — Force Quit/SIGKILL, native crash,process.exit(1)from the crash hooks,app.exit(1)— reparents the child to launchd and it keeps running (mitmdump only writes on traffic; an orphan gets none). The 2026-09-03 audit of a two-day run found 20 orphaned mitmdumps (ppid 1, two days old) next to 12 live ones — each a Python process holding ~60–100 MB RSS and a bound port on a 16 GB laptop that already swaps (3.7 GB of proxy state on disk).Second leak, same launcher:
ProxyServer.stop()awaitschild.once('exit')after SIGTERM/SIGKILL and never resolves if the child already exited on its own — one dead proxy hangsclaudeSession.stop()→killAll()→ the will-quit gate, and the user's only exit is Force Quit, which orphans every other session's proxy.Refs #767 (expected item 5, process-hygiene half). Plan:
docs/superpowers/plans/2026-09-03-mitmproxy-reaping.md.Implemented behavior
--set confdir=<STATE_DIR>/proxy/_shared-conf), now exported asMITMPROXY_SHARED_CONF_DIRand passed byclaudeSession.ts, so spawn and search cannot drift. A user's own Homebrew mitmproxy never matches.ppid === process.pidours-alive;ppid <= 1or dead owner → stale, kill; live foreign owner → keep and journal (conservative side of perf(diagnostics): default-off and off-thread the always-on diagnostic subsystems #767(d): only act when the owner is provably gone; sound because the state process lock guarantees one main per state dir).--listen-port+ ppid-self so a wedged/dead child can never hang quit; (2) will-quit sweep afterkillAll()(composed inindex.ts, gate stays proxy-agnostic); (3) synchronous SIGKILL onprocess.on('exit'); (4) startup reaper for the previous run's leftovers, journaled asproxy.mitmdump.stale_reapedwith scanned/stale/reaped/survived/kept counts, fire-and-forget so boot is never delayed.isPidRunningexported fromprocessLock.tsso the reaper and the state lock share one liveness answer (same EPERM semantics).Design decisions and tradeoffs
packages/changes in this PR: the parent-side fix (detached: true+kill(-pgid)) and an addon-sideos.getppid()watchdog are submodule follow-ups; everything here is compatible with both landing later.Links
Refs #767. Refs #495 (A9 rollback), #119.
Tests and verification
mitmproxyReaper.test.ts23/23: stale-filter matrix (ppid 1 / dead owner kill; live-foreign-owner, ppid-self, unmarked, marker-prefix keep);psrow parsing with spaces; grace sequence under fake timers (TERM-only, TERM→KILL boundary, already-gone sends nothing);stopProxyServerWithDeadline(resolves clean; escalates by port; not-found when only a previous run's port match exists); startup reaper end-to-end with injected process list and journaled counts.processLock+claude/runtimesuites 44/44;npm run typecheckclean.Known limitations / follow-ups
Signal-safety review repair (2026-09-04)
The marker/PPID scan now only discovers candidates. Immediately before TERM, delayed KILL and synchronous exit signalling, revalidate kernel command, start timestamp, parent and unchanged argv. Changed/unreadable/unknown identity is retained and is not counted as reaped. Bound process inspection and exit-loop work. Normal native mitmdump and Python launchers are supported; unknown interpreter layouts fail closed.
33 focused tests pass, including identity replacement during the grace period, unrelated executables and synchronous PID reuse; TypeScript and both full host CI gates pass on final head f62fd22 (run 33938037840). POSIX ps checks are not atomic pidfds: second-resolution timestamps and a small check-to-signal race remain. No real live process was signalled and force-quit qualification is still unperformed. No claim of currently finding the historical 20 orphans.