From 492fc5b9dd03aead5ee32268c95914dc7831233e Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 12:48:39 -0700 Subject: [PATCH] fix(browse): 'browse stop' no longer restarts the daemon it was asked to stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stop handler awaited shutdown() — which ends in process.exit — before returning, so the acknowledgement never egressed. The CLI's fetch reset, which its crash path reasonably interpreted as a dead daemon: it relaunched Chromium, re-sent stop, watched the daemon exit again, and errored 'Server crashed twice in a row'. Every stop cost a wasted Chromium launch and a nonzero exit. The ack now returns first; shutdown fires on a 25ms unref'd timer. Same fix for restart. Fork's test pins ack-before-teardown for both. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian Co-Authored-By: Claude Fable 5 --- browse/src/meta-commands.ts | 7 ++++-- browse/test/stop-ack-before-shutdown.test.ts | 23 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 browse/test/stop-ack-before-shutdown.test.ts diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index 4bd0faae7..4cd296492 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -421,14 +421,17 @@ export async function handleMetaCommand( } case 'stop': { - await shutdown(); + // Return the acknowledgement before closing the listener. Shutting down + // inline resets the CLI's fetch, which it reasonably interprets as a + // crash and then restarts the daemon it was asked to stop. + setTimeout(() => { void shutdown(); }, 25).unref?.(); return 'Server stopped'; } case 'restart': { // Signal that we want a restart — the CLI will detect exit and restart console.log('[browse] Restart requested. Exiting for CLI to restart.'); - await shutdown(); + setTimeout(() => { void shutdown(); }, 25).unref?.(); return 'Restarting...'; } diff --git a/browse/test/stop-ack-before-shutdown.test.ts b/browse/test/stop-ack-before-shutdown.test.ts new file mode 100644 index 000000000..05c761675 --- /dev/null +++ b/browse/test/stop-ack-before-shutdown.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'bun:test'; +import { handleMetaCommand } from '../src/meta-commands'; + +describe('server control acknowledgement ordering', () => { + for (const [command, acknowledgement] of [ + ['stop', 'Server stopped'], + ['restart', 'Restarting...'], + ] as const) { + test(`${command} acknowledges before closing the listener`, async () => { + let shutdownCalls = 0; + const manager = { getActiveSession: () => ({}) } as any; + + const result = await handleMetaCommand(command, [], manager, async () => { + shutdownCalls += 1; + }); + + expect(result).toBe(acknowledgement); + expect(shutdownCalls).toBe(0); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(shutdownCalls).toBe(1); + }); + } +});