fix(browse): keep headed daemon alive when parent shell dies (handoff dies ~15s in) - #2405
Open
davidani-davi wants to merge 1 commit into
Open
fix(browse): keep headed daemon alive when parent shell dies (handoff dies ~15s in)#2405davidani-davi wants to merge 1 commit into
davidani-davi wants to merge 1 commit into
Conversation
The parent-process watchdog polls the spawning shell every 15s and, on parent death, shut the daemon down whenever connectionMode was 'headed'. Claude Code's Bash tool kills the parent shell after every tool invocation, so the parent is always dead within seconds. `handoff` flips an already-running headless daemon to headed at runtime. That daemon was spawned with a real BROWSE_PARENT_PID belonging to a long-dead shell, so the next tick tore down the browser ~15s after every handoff — including while the user was being asked to log in. The branch never protected its stated case: `$B connect` / /pair-agent launch with BROWSE_HEADED=1 + BROWSE_PARENT_PID=0, which skips watchdog registration entirely. garrytan#994 already inverted this for headless mode for exactly the same reason; the headed branch was left behind. Headed mode now stays alive on parent death, matching the BROWSE_HEADED=1 launch path. Cleanup is unchanged: handleChromiumDisconnect exits the daemon when the window closes, or `$B disconnect`. Tunnel mode still shuts down — a remote peer has no window to close, so parent death is its only cleanup signal. Extract the watchdog body into parentWatchdogTick() exposed via __testInternals__, mirroring the existing idleCheckTick seam, so the branches can be driven deterministically instead of waiting on a 15s interval. Verified end-to-end: before, daemon + Chromium dead at T+20; after, both alive past T+45 with session URL intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
$B handoffopens a visible browser for the user to take over — CAPTCHA, MFA, login. Under Claude Code that window is destroyed ~15 seconds later, every time, usually while the user is still typing their password.Root cause is the parent-process watchdog in
browse/src/server.ts. It polls the spawning shell every 15s and, on parent death, shuts the daemon down wheneverconnectionModeis'headed'. Claude Code's Bash tool kills the parent shell after every tool invocation, so the parent is always dead within seconds.handoffflips an already-running headless daemon to headed at runtime. That daemon was spawned with a realBROWSE_PARENT_PIDbelonging to a long-dead shell, so the next tick tears down the browser.Why the headed branch was never load-bearing
Its stated purpose is avoiding orphan daemons after
/pair-agentand/open-gstack-browser. But those paths launch withBROWSE_HEADED=1+BROWSE_PARENT_PID=0, which skips watchdog registration entirely at the outer gate:So the branch never fired for the case it was written for, and only ever fired for
handoff.This is also a re-run of #994, which inverted "kill on parent death" for headless mode for exactly this reason. The headed branch was left behind, and
handoff's runtime transition walks straight into it.Change
BROWSE_HEADED=1launch path. Cleanup is unchanged:handleChromiumDisconnectexits the daemon when the user closes the window, or$B disconnect.parentWatchdogTick()behind__testInternals__, mirroring the existingidleCheckTickseam, so the branches can be driven deterministically instead of waiting on a 15s interval.Verification
Reproduced end-to-end before the fix:
After:
Four new tests in
browse/test/server-factory.test.tscovering headed / headless / tunnel / live-parent. The headed test fails onmain(Received number of calls: 1) and passes here.browsesuite: 0 failures across two full runs. Repo-rootbun testshows 6 failures ingbrain-detect-installanduser-slug-fallback(status 127under restricted PATH) — reproduced identically on a cleanorigin/mainworktree, so they are pre-existing and environmental, unrelated to this change.Found while using
/browsefor real work, per the CONTRIBUTING workflow.🤖 Generated with Claude Code