From 4b48f8116f5b735a5499199ab9eed7bd9eab69ed Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 16 Apr 2026 13:07:13 -0700 Subject: [PATCH] fix: headed browser no longer auto-shuts down after 15 seconds The parent-process watchdog in server.ts polls the spawning CLI's PID every 15s and self-terminates if it is gone. The connect command in cli.ts exits with process.exit(0) immediately after launching the server, so the watchdog would reliably kill the headed browser within ~15s. This contradicted the idle timer's own design: server.ts:745 explicitly skips headed mode because "the user is looking at the browser. Never auto-die." The watchdog had no such exemption. Two-layer fix: 1. CLI layer: connect handler always sets BROWSE_PARENT_PID=0 (was only pass-through for pair-agent subprocesses). The user owns the headed browser lifecycle; cleanup happens via browser disconnect event or $B disconnect. 2. CLI layer: startServer() honors caller's BROWSE_PARENT_PID=0 in the headless spawn path too. Lets CI, non-interactive shells, and Claude Code Bash calls opt into persistent servers across short-lived CLI invocations. 3. Server layer: defense-in-depth. Watchdog now also skips when BROWSE_HEADED=1, so even if a future launcher forgets PID=0, headed browsers won't die. Adds log lines when the watchdog is disabled so lifecycle debugging is easier. Four community contributors diagnosed variants of this bug independently. Thanks for the clear analyses and reproductions. Closes #1020 (rocke2020) Closes #1018 (sanghyuk-seo-nexcube) Closes #1012 (rodbland2021) Closes #986 (jbetala7) Closes #1006 Closes #943 Co-Authored-By: rocke2020 Co-Authored-By: sanghyuk-seo-nexcube Co-Authored-By: rodbland2021 Co-Authored-By: jbetala7 Co-Authored-By: Claude Opus 4.7 (1M context) --- browse/src/cli.ts | 20 +++++++++++++------- browse/src/server.ts | 14 +++++++++++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index ae287515..e0906f04 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -210,12 +210,18 @@ async function startServer(extraEnv?: Record): Promise): Promise { // server can become an orphan — keeping chrome-headless-shell alive and // causing console-window flicker on Windows. Poll the parent PID every 15s // and self-terminate if it is gone. +// +// Headed mode (BROWSE_HEADED=1 or BROWSE_PARENT_PID=0): The user controls +// the browser window lifecycle. The CLI exits immediately after connect, +// so the watchdog would kill the server prematurely. Disabled in both cases +// as defense-in-depth — the CLI sets PID=0 for headed mode, and the server +// also checks BROWSE_HEADED in case a future launcher forgets. +// Cleanup happens via browser disconnect event or $B disconnect. const BROWSE_PARENT_PID = parseInt(process.env.BROWSE_PARENT_PID || '0', 10); -if (BROWSE_PARENT_PID > 0) { +const IS_HEADED_WATCHDOG = process.env.BROWSE_HEADED === '1'; +if (BROWSE_PARENT_PID > 0 && !IS_HEADED_WATCHDOG) { setInterval(() => { try { process.kill(BROWSE_PARENT_PID, 0); // signal 0 = existence check only, no signal sent @@ -767,6 +775,10 @@ if (BROWSE_PARENT_PID > 0) { shutdown(); } }, 15_000); +} else if (IS_HEADED_WATCHDOG) { + console.log('[browse] Parent-process watchdog disabled (headed mode)'); +} else if (BROWSE_PARENT_PID === 0) { + console.log('[browse] Parent-process watchdog disabled (BROWSE_PARENT_PID=0)'); } // ─── Command Sets (from commands.ts — single source of truth) ───