mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
Root cause: bounded teardown (endPoolBounded, #2015) RESOLVES, but lingering sockets — embedding-provider fetch keep-alive, PgBouncer txn-mode sockets the bound raced past — keep Bun's event loop alive, so every `gbrain query` paid a flat 10s tax exiting via the hard-deadline force-exit banner. Three changes, one contract: - flushStdoutThenExit (cli-force-exit.ts): when main() resolves and the command is not a daemon, exit deliberately — after stdout AND stderr drain (writableLength===0, 'drain'-event + poll loop, 2s unref'd guard for a blocked pipe). Incident #1959 (force-exit truncating piped stdout) is the regression class; pinned by a 256KB real-pipe subprocess test. - drainThenDisconnect (cli.ts): ONE owner-disconnect helper at all 8 sites (op-dispatch, CLI_ONLY fall-through, search dashboard, doctor remediation x3, ze-switch, dream, read-only timeout path). Drains the background-work registry, then disconnect (best-effort), bounded by the 10s unref'd hard-deadline — which is now armed around the TEARDOWN window only, not before the op handler (the old placement would have force-killed any op slower than 10s). Closes the filed TODOS P3 drain-hoist: six sites previously skipped the drain entirely and had no hang timer at all. - Inner process.exit sweep: mid-handler exits in engine-owning/output-bearing paths (status, friction, claw-test, smoke-test, eval cross-modal / takes-quality replay / conversation-parser / whoknows-thin, status-thin) become process.exitCode + return so they flow through the drains and the flush-exit. Pre-engine usage/parse/refusal exits stay as-is. BrainRegistry.disconnectAll deliberately unchanged: zero production callers in src/, per-engine disconnects already bounded, and the kernel reclaims sockets on exit (src/core/timeout.ts doctrine). DAEMON_COMMANDS gains 'watch' ahead of the #2095 push transport. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>