Repository navigation
fix(orphans): reclaim dead helper trees that leaked behind dead sessions - #87
Merged
Merged
Conversation
Leaked Codex plugin broker trees (up to 300 processes each) were never offered for cleanup. Several rules each blocked them: - Any non-zero CPU sample counted as work, so idle MCP servers ticking timers and GC hid every tree. A member now counts as idle below 5% of one core and the whole tree below 25%, in both detection and the kill-time recheck. - A process from a live app's bundle protected its tree even when an agent helper only borrowed the bundle's runtime (ChatGPT.app's cua_node under codex app-server). It still protects the tree unless its parent is a known agent helper. - macOS lsof prints a Unix socket's own address in field d, not D, so no peer ever resolved and every connection, even to the tree's own children, read as external. The probe now requests and parses d. - Socket probes now report a PeerState: none, listening, system (peers only another user owns), or connected. A peer shown as (none) has closed, and a bound UDP socket on Linux counts as connected, as on Windows. - Signatures can set idle_without_clients. A flagged helper's own listeners no longer keep its tree, and under a flagged root neither do system-service sockets. Other processes' listeners, connections to the user's processes and network connections still do. The Codex broker and claude mcp serve set it, since the plugin starts a new broker when the old endpoint stops answering. - On macOS, PeerResolver takes the tree's Unix sockets from one lsof pass over all of the user's processes, so a listener's clients (lsof shows an accepted connection only on the client's side) and the owners of outside peers come from one snapshot. It runs only for trees with a flagged helper and fails closed on any lsof error. - The macOS probe deadline scales with the number of 200-pid batches. - The TUI kill path takes the configured signature set instead of a second copy of the closure.
6 of 7 tasks
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.
What changed
Driga now finds and can kill leaked Codex plugin broker trees, which were never offered for cleanup. Idle means under 5% of a core per process and 25% per tree, not exactly zero. Socket probes report one of four states (none, listening, system, connected). A new signature flag,
idle_without_clients, lets the Codex broker andclaude mcp servecount as idle when no real client is connected. It also fixes a macOSlsofparsing bug that made every Unix socket look externally connected.Why
On one machine, 27 orphaned
app-server-broker.mjstrees held about 1,500 processes and 5.8 GB, anddriga orphans listshowed none of them. Five rules each blocked them:cua_node.lsof -Fprints a Unix socket's own address ind, notD. So no peer ever resolved, and even the broker's connection to its own child counted as external. The fixture was hand-written withD, so tests never caught it.broker.sockby design, and any listener counted as use. The plugin reuses a broker while the socket answers and starts a new one when it doesn't, so a broker with no client is idle.lsofshows a connection a server accepted only on the client's side. Finding a listener's clients takes one pass over all of the user's processes (PeerResolver). Using that pass's snapshot for the tree's own Unix sockets avoids a gap between two scans.Rejected alternatives:
claude mcp servewould become killable.Risk and rollout
This changes when Driga can signal or kill a process. The guards:
lsofpass runs only when a tree contains a flagged helper. On any error, including exit 1, every process in the tree counts as connected.Contract changes
platform.SocketProber.ExternalPeersnow returnsmap[int32]model.PeerStateinstead ofmap[int32]bool. New optionalplatform.PeerResolverinterface.idle_without_clients, documented indocs/usage.md. Extra signature files without it keep the strict behavior.Verified
Run on macOS (Darwin 27, arm64).
go test ./...andgo test -race ./...: all pass exceptTestPermissionHookBuiltBinary(TempDir cleanup), which fails the same way onmainwithout this change.GOOS=linux go build ./...;go vetwithGOOS=linuxandGOOS=windows;just _fmt-check tidy-check vet generate-check docs-check.driga orphans kill --yesrefused 3 times in a row.codex app-serverholds an HTTPS connection for a few seconds, and Driga correctly refuses during that time./code-reviewat low and high effort. All 11 findings are fixed, and each has a test.