Orphans and the registry
Zombie processes accumulate
Section titled “Zombie processes accumulate”Cause. Hosts that were hard-killed. Every in-process guard — job objects, process groups, watchdogs — needs some of our code to run, and a SIGKILL runs none of it. The CLI survives, invisible to any later host.
Fix. This is what CliRegistry exists for. Register at spawn, unregister at exit, and sweep at startup:
const registry = new CliRegistry(stateDirectory);
// At startup, before spawning anything.registry.prune();for (const orphan of registry.orphans()) { console.warn('left behind by a dead host:', orphan.pid, orphan.sessionId); // Kill it, or offer to — the decision is the host's.}
// Around each session.registry.register(childProcess, sessionId, workingDirectory);session.onClose(() => registry.unregister(session.pid));Arm a watchdog too, so the common case — the host closing without running cleanup — is caught in-process:
new ParentWatchdog().start(() => { session.stop('SIGKILL'); process.exit(0);});The registry reports nothing
Section titled “The registry reports nothing”| Cause | Detail |
|---|---|
| The state directory is unwritable | register() swallows write failures by design — a host that cannot write its state directory should still be able to run a CLI |
| The pid came back as something else | liveEntries() requires the session id to still appear in the process’s command line, so a reused pid is correctly excluded |
| Identity could not be read | commandLine() returning null means unknown, not dead — and unknown is deliberately not a match, because the alternative is killing a stranger’s process |
Contradictory answers in one conversation
Section titled “Contradictory answers in one conversation”Cause. Two CLIs are appending to the same conversation, which branches its history.
Fix. Ask before resuming:
const existing = registry.findLive(sessionId);if (existing) { throw new Error(`session ${sessionId} is already driven by pid ${existing.pid}`);}This is the second reason the registry exists, alongside orphan cleanup.
Why several mechanisms overlap
Section titled “Why several mechanisms overlap”Each guard fails in a different way, which is why none of them is the only one. See Architecture for the full table; the short version is that the registry catches whatever all the in-process guards missed, after the fact, because writing a file at spawn and removing it at exit is the only record that survives a host being killed outright.