fix(browse): skip crash-retry when user invokes stop or restart

sendCommand's ECONNRESET handler treats every dropped connection as a
crash and restarts the daemon. But stop and restart are user-initiated
shutdowns: the daemon intentionally closes the HTTP connection before
the response body flushes (meta-commands.ts:423-433). The restart
attempt sends stop again, the new daemon exits again, and retries >= 1
throws 'Server crashed twice in a row — aborting.'

Fix: add a short-circuit check for command === 'stop' || command ===
'restart' at the top of the ECONNRESET handler. When user-intended,
call process.exit(0) to skip main()'s surrounding .catch(), which
would print '[browse] fetch failed' and exit 1.

'command' is already in closure from sendCommand's signature (cli.ts:527).
The recursive calls at cli.ts:596 and :613 pass 'command' explicitly.

Daemon-side handlers (meta-commands.ts) are unchanged. No 'chore:
rebuild' commit — CI's windows-setup-e2e.yml rebuilds browse binary
on every push touching browse/src/cli.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
TTmo123 2026-06-28 22:01:35 +08:00
parent 11de390be1
commit 897f41cfd9
1 changed files with 9 additions and 0 deletions

View File

@ -585,6 +585,15 @@ async function sendCommand(state: ServerState, command: string, args: string[],
}
// Connection error — server may have crashed, OR may just be busy.
if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET' || err.message?.includes('fetch failed')) {
// User-initiated shutdown (stop / restart) intentionally closes the daemon.
// Both meta-commands.ts:423 ('stop') and :428 ('restart') call shutdown(),
// which closes the HTTP connection before the response body reaches the
// client. ECONNRESET here is the expected exit signal, not a crash.
// 'command' is already in closure from sendCommand's signature — no new
// param passing.
if (command === 'stop' || command === 'restart') {
process.exit(0);
}
const oldState = readState();
// #1781 busy-vs-dead: a single-threaded daemon under beacon/extension load
// can briefly stop answering HTTP while still alive. Before declaring a