feat(opencode): never leave orphaned OpenCode server processes

OpenChamber spawns the OpenCode server as an external child binary (detached
on Unix), so a hard crash, SIGKILL, or Ctrl+C of the host before graceful
teardown could leave it running. Orphaned servers then accumulate and contend
on the shared SQLite DB, causing severe startup slowdowns.

Add a per-process registry plus a startup reaper, mirroring the pattern
OpenCode's own CLI daemon uses for its detached server:

- One file per spawned process at
  ~/.config/openchamber/managed-opencode/<pid>.json. Per-process files avoid
  the read-modify-write clobber race between concurrent runtimes/windows that a
  single shared file would suffer.
- On spawn, record the child (pid, owner pid, port, binary, host runtime).
- On graceful close/restart, delete the record.
- On startup, reap only our own, verified, genuinely-orphaned processes:
  recorded by us AND still a live `opencode serve` on the recorded port AND
  whose spawner is provably gone (reparented to pid 1, or recorded owner dead).
  It never touches a process a live instance is using, the user's standalone
  server, the official desktop app, or the TUI.

Wire it into every runtime that spawns the server:

- web/desktop via the OpenCode lifecycle (register on spawn, unregister on
  close/restart, reap at startup). The restart-for-config-change flow inherits
  this automatically through the same kill/spawn paths.
- VS Code carries a parity implementation (it does not bundle the web package)
  that reads/writes the same registry directory and uses the same algorithm.
- Tag the actual host runtime (desktop/web/ssh-remote/vscode) for observability.

Also tighten teardown so the registry stays accurate and orphans die promptly
instead of only on the next start:

- The web server now also handles SIGHUP and SIGUSR2 (terminal close and the
  nodemon restart used by dev:server:watch / dev:web:hmr).
- Electron now installs SIGINT/SIGTERM/SIGHUP handlers that run the same
  background teardown as a normal quit, covering Ctrl+C on electron:dev.

External OpenCode servers (OPENCODE_SKIP_START) are intentionally excluded: we
never manage or kill processes we did not spawn.
This commit is contained in:
Bohdan Triapitsyn
2026-06-24 16:51:17 +03:00
parent a9dfd32347
commit 2ff5428c69
6 changed files with 566 additions and 1 deletions
+16
View File
@@ -285,6 +285,22 @@ const performConfirmedQuit = () => {
app.exit(0);
};
// Hard-stop signals (`Ctrl+C` on `electron:dev`, an external `kill`/SIGTERM,
// terminal close) bypass the normal app-quit flow — which would orphan the
// in-process web server's managed OpenCode child. Run the same background
// teardown the quit path uses (which kills the sidecar), then exit. The startup
// reaper remains the backstop for an unhandled hard crash (SIGKILL).
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
process.on(signal, () => {
try {
shutdownBackgroundServices();
} catch (error) {
log.warn(`[electron] ${signal} shutdown failed:`, error);
}
app.exit(0);
});
}
const requestQuitWithConfirmation = async () => {
await refreshQuitRiskFlags();