You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bug: browse stop exits 1 and retries after successful shutdown #2254
Reproduction\n\nEnvironment: macOS arm64, origin/main 83238f9ba28b9a5dfa3405fa625b70f7b5b99d39.\n\n1. Run the generated browse binary with status. It starts and reports healthy.\n2. Run the same binary with stop.\n\nObserved output:\n\n [browse] Server connection lost. Restarting...\n [browse] [browse] Server crashed twice in a row — aborting\n\nThe stop command exits 1, although the daemon is actually gone afterward and no matching process remains.\n\n### Expected\n\nAn intentional stop should not enter the crash-restart path. It should stop the daemon and exit 0.
Confirming this on Windows 11 (gstack v1.60.1.0, fresh ./setup install), so it isn't macOS-specific. One difference from the original report: on Windows stop exits 0, not 1, while printing the same two lines. The daemon is genuinely gone afterward and no chrome-headless-shell processes are left behind.
Root cause
stop is dispatched as an ordinary command, and the handler exits the process before the response is ever written.
shutdown() ends with process.exit(exitCode) (browse/src/server.ts, end of the async function shutdown body). So return 'Server stopped' is unreachable and the HTTP response is never sent — the client always sees the socket drop.
The CLI has no special case for stop (grepping cli.ts turns up only comments and help text), so the dead socket falls into the generic crash-recovery branch in sendCommand, which is guarded on ECONNREFUSED | ECONNRESET | 'fetch failed':
stop → server exits → client sees the connection drop
Client prints Server connection lost. Restarting... and launches a fresh daemon + Chromium
It re-sends stop to the new daemon, which exits the same way
retries >= 1 → Server crashed twice in a row — aborting
So the CLI resurrects the exact daemon the user asked it to kill, then kills it again. Beyond the misleading message, the practical cost is a full Chromium cold start on every stop.
Note restart has the same shape but wants this — its comment says "Signal that we want a restart — the CLI will detect exit and restart". stop inherits that behavior without wanting it, which is probably why this went unnoticed.
Possible fixes
Client-side (most robust): special-case stop in sendCommand so a connection drop after dispatching stop counts as success. The server is guaranteed to die, so a dropped socket is the expected outcome rather than an error.
Server-side: write the response before tearing down — e.g. schedule shutdown() on the next tick / after flush so 'Server stopped' reaches the client first. Riskier, since it introduces a window where the daemon is alive but committed to exiting.
Option 1 avoids depending on shutdown ordering at all.
Activity
time-attack commentedon Jul 15, 2026
@16francej Close this because it duplicates or has been superseded by the selected issue covering the same shutdown retry behavior.
p20240440-cpu commentedon Aug 6, 2026
Confirming this on Windows 11 (gstack v1.60.1.0, fresh
./setupinstall), so it isn't macOS-specific. One difference from the original report: on Windowsstopexits 0, not 1, while printing the same two lines. The daemon is genuinely gone afterward and nochrome-headless-shellprocesses are left behind.Root cause
stopis dispatched as an ordinary command, and the handler exits the process before the response is ever written.browse/src/meta-commands.ts:shutdown()ends withprocess.exit(exitCode)(browse/src/server.ts, end of theasync function shutdownbody). Soreturn 'Server stopped'is unreachable and the HTTP response is never sent — the client always sees the socket drop.The CLI has no special case for
stop(greppingcli.tsturns up only comments and help text), so the dead socket falls into the generic crash-recovery branch insendCommand, which is guarded onECONNREFUSED | ECONNRESET | 'fetch failed':stop→ server exits → client sees the connection dropisProcessAlive(oldState.pid)is now false, so the busy-vs-dead probe from Headed browser "keeps crashing": HTTP-unresponsive daemon → orphaned Chromium → SingletonLock → restart crash-loop, and silent headless respawns #1781 correctly concludes "dead" and takes the restart pathServer connection lost. Restarting...and launches a fresh daemon + Chromiumstopto the new daemon, which exits the same wayretries >= 1→Server crashed twice in a row — abortingSo the CLI resurrects the exact daemon the user asked it to kill, then kills it again. Beyond the misleading message, the practical cost is a full Chromium cold start on every
stop.Note
restarthas the same shape but wants this — its comment says "Signal that we want a restart — the CLI will detect exit and restart".stopinherits that behavior without wanting it, which is probably why this went unnoticed.Possible fixes
stopinsendCommandso a connection drop after dispatchingstopcounts as success. The server is guaranteed to die, so a dropped socket is the expected outcome rather than an error.shutdown()on the next tick / after flush so'Server stopped'reaches the client first. Riskier, since it introduces a window where the daemon is alive but committed to exiting.Option 1 avoids depending on shutdown ordering at all.
fix(browse): stop --force-restart kills the live daemon directly inst…
v1.68.2.0 fix: tunnel revoke exists and revokes everything — setup ke…