When the user closed the headed browser window, the disconnect handler
in browser-manager.ts called process.exit(2) directly, bypassing the
server's shutdown() function entirely. That meant:
- sidebar-agent daemon kept polling a dead server
- session state wasn't saved
- Chromium profile locks (SingletonLock, SingletonSocket, SingletonCookie)
weren't cleaned — causing "profile in use" errors on next $B connect
- state file at .gstack/browse.json was left stale
Now the disconnect handler calls onDisconnect(), which server.ts wires
up to shutdown(2). Full cleanup runs first, then the process exits with
code 2 — preserving the existing semantic that distinguishes user-close
(exit 2) from crashes (exit 1).
shutdown() now accepts an optional exitCode parameter (default 0) so
the SIGTERM/SIGINT paths and the disconnect path can share cleanup code
while preserving their distinct exit codes.
Surfaced by Codex during /plan-eng-review of the watchdog fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>