From 897f41cfd9debbbd0f7c5120b48b90cd620045f0 Mon Sep 17 00:00:00 2001 From: TTmo123 Date: Sun, 28 Jun 2026 22:01:35 +0800 Subject: [PATCH] fix(browse): skip crash-retry when user invokes stop or restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- browse/src/cli.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 59327b792..7e41ca1d1 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -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