Repository navigation
fix(browser): bound the agent-browser daemon's life instead of hoping teardown runs (#16367) - #16588
Conversation
`agent-browser` is a client/daemon CLI. Orca only ever spawns the short-lived client; that client forks a daemon Orca holds no handle on, which reparents to pid 1 immediately. Nothing in Orca reclaimed it, so a crashed or SIGKILL'd run left one daemon per browser tab alive forever — two of them at ~25.6 GiB and ~7.2 GiB RSS saturated a 64 GiB cgroup under headless `orca serve`. Three fixes, in order of how much they cover: 1. Set `AGENT_BROWSER_IDLE_TIMEOUT_MS` on both spawn paths (the bundled-binary bridge and the orcad external-Chromium provider). This is the only bound that survives every way Orca can die, including SIGKILL, where no teardown code ever runs. 10 minutes: >6x the bridge's 90s `EXEC_TIMEOUT_MS`, so it can never cut a command, a retry chain, or an ordinary gap between two user commands, while capping an abandoned daemon at minutes instead of days. Verified against agent-browser 0.27.0: an idle daemon exits and takes its Chromium tree and socket sidecar files with it, and a daemon attached over `--cdp` (the bridge's case) leaves the attached browser running, so an Orca tab is never closed by its daemon idling out. 2. Await `destroyAllSessions()` in the will-quit teardown barrier. It was fire-and-forget and the only browser member missing from `settleTeardownWithinDeadline`; each session's close is its own agent-browser child taking hundreds of ms, so `app.quit()` won. 3. Sweep daemons a previous run left behind, using agent-browser's own `session list` / `close` rather than a pid walk (see `windows-pty-job.ts` for why walking your own orphans is guesswork). `closeStaleAgentBrowserSession` only ever reset the one name a new tab was about to reuse. The sweep runs only when `AGENT_BROWSER_SOCKET_DIR` is set, because that private per-profile directory is what proves the enumeration can only see this Orca profile's daemons; it is never set on Windows, so Windows gets no enumeration rather than a machine-wide sweep that could close a daemon Orca does not own. Windows stays bounded by the idle timeout, which needs no ownership proof. orcad's session name is stable across runs, so it closes that one name at start instead — a killed orcad's daemon would otherwise be reused while still holding the previous run's Chromium on a dead serve port. Where the 25 GiB went is inference from code, not a measurement: `captureStart` sets `activeCapture` and only an explicit `captureStop` ends it, so a HAR capture in a daemon living for days is unbounded. Not claimed as proven; the idle bound caps it either way. Not re-landed: the queue bounds from #10179 (reverted by #10255) bound Orca's own main-process heap, not the daemon's RSS, so they do not address this report. Also true but left alone: the 3-strike breaker's `destroySession` is an unawaited call whose `close` is `catch {}`-swallowed, and it only fires while a command is in flight — an idle-but-bloated daemon is never noticed. The idle timeout now bounds that case. `getOffscreenBrowserBackend()?.destroyAll?.()` is declared `void` and fully synchronous, so unlike `destroyAllSessions` it has no promise to lose and needs no barrier entry.
… owns Review follow-ups on the agent-browser orphan fix. - Never idle-bound the orcad external-Chromium daemon: it owns the user's remote browser, so the 10-minute bound closed a live session and every tab in it. Per-tab helper daemons keep the bound; the stable session name plus the `close` in start() is what reclaims a killed orcad's Chromium tree. - Retire a page's daemon from the headless offscreen backend, which is the only place `orca serve` closes a page and never reached the bridge. Credit to @Jinwoo-H (#16564) for identifying this owner-boundary gap. - Bound the teardown close at 5s so the will-quit barrier member cannot inherit the 90s exec timeout, and close sessions still being created. - Gate the startup sweep on a socket directory Orca derived itself; an inherited AGENT_BROWSER_SOCKET_DIR is no proof of per-profile ownership. - Replay a session's network routes when the daemon idled out between two commands, instead of silently serving unstubbed requests.
Of the three behaviours this PR adds, two are already recoverable in the field without a build: the idle bound is an env passthrough an operator can raise, and the quit close is bounded by its own timeout inside the teardown deadline. The startup sweep was the exception — it fires unconditionally, and if it closes a daemon it should not, or spawns one process per stale name on a profile holding hundreds, the only remedy was a revert. ORCA_DISABLE_AGENT_BROWSER_SWEEP=1 turns it off, matching the existing ORCA_DISABLE_CODEX_TRUST_RPC / ORCA_DISABLE_HTTP2 convention. Note for anyone reaching for it on macOS: a Finder-launched Orca does not see shell env, so it needs launchctl setenv or a terminal launch.
De-risking review: one blocking question, one flag added1. BLOCKING —
|
…user's start() closed the daemon before every open, killing the Chromium tree with it. That runs on every provider start, not just after a crash — so an `orca serve` restart took the remote user's browser and every tab in it. The justification was borrowed from the pane bridge, which passes --cdp and so really does hold a port that dies with its Orca. This session passes only --session and --profile: nothing binds it to the old process, and the daemon owns its Chromium independently. A survivor is reusable as-is. start() now probes for an active tab first and returns it untouched. Only a name that answers nothing gets closed and reopened — which is still the killed-orcad case the stable session name exists to recover. This matters more as orcad becomes the backend the remote host runs on: the browser it manages belongs to a user, not to the process that happens to be driving it this minute. It is also the same principle that already exempts this path from AGENT_BROWSER_IDLE_TIMEOUT_MS.
📝 WalkthroughWalkthroughThe changes add Agent Browser idle-timeout configuration and socket-directory ownership metadata. The bridge now tracks command activity, restores interception after daemon expiry, reclaims orphaned sessions, and closes sessions during shutdown. External Chromium sessions reuse active tabs and recover unresponsive sessions. Application startup invokes orphan sweeping, and shutdown awaits session cleanup. Tests cover environment propagation, idle handling, orphan filtering, session recovery, teardown timing, and guest-page unregistration. Merge Risk: 🔵 Low · up to Idle browser daemon replacement can currently stop an active HAR capture from recording subsequent traffic, potentially producing incomplete or empty HAR files. The PR is otherwise mergeable, but HAR capture restoration should be addressed or explicitly accepted by the owner. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation The changes address issue
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/browser/agent-browser-process-environment.ts (1)
7-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReduce this comment. Keep the lifetime rationale concise and move operational detail to issue documentation. As per coding guidelines: “DO NOT: be verbose” and “BE CONCISE. 1 LINE if possible.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 617e0dec-44ca-4d33-9cd2-6d39cff593f6
📒 Files selected for processing (12)
src/main/browser/agent-browser-bridge-command-transport.test.tssrc/main/browser/agent-browser-bridge-session-lifecycle.test.tssrc/main/browser/agent-browser-bridge.tssrc/main/browser/agent-browser-orphan-sweep.test.tssrc/main/browser/agent-browser-orphan-sweep.tssrc/main/browser/agent-browser-process-environment.test.tssrc/main/browser/agent-browser-process-environment.tssrc/main/browser/offscreen-browser-backend-lifecycle.test.tssrc/main/index.tssrc/main/orcad/external-chromium-browser-session.test.tssrc/main/orcad/external-chromium-browser-session.tssrc/main/quit-teardown-agent-browser-daemons.test.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.
| private reinitializeIfDaemonIdledOut(sessionName: string, session: SessionState): void { | ||
| if ( | ||
| this.agentBrowserIdleTimeoutMs === null || | ||
| Date.now() - session.lastCommandAt < this.agentBrowserIdleTimeoutMs | ||
| ) { | ||
| return | ||
| } | ||
| session.initialized = false | ||
| if (session.activeInterceptPatterns.length > 0) { | ||
| this.pendingInterceptRestore.set(sessionName, [...session.activeInterceptPatterns]) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- repository conventions ---'
find /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- target outline ---'
ast-grep outline src/main/browser/agent-browser-bridge.ts --match 'reinitializeIfDaemonIdledOut' --view expanded
printf '%s\n' '--- target lifecycle references ---'
rg -n -C 10 'reinitializeIfDaemonIdledOut|activeCapture|network har|captureStart|captureStop|pendingInterceptRestore|lastCommandAt' src/main/browser/agent-browser-bridge.tsRepository: stablyai/orca
Length of output: 16222
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- applicable conventions ---'
cat /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions/src-main-daemon.md
cat /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions/src.md
printf '%s\n' '--- daemon execution and restoration ---'
sed -n '2575,2675p' src/main/browser/agent-browser-bridge.ts
printf '%s\n' '--- capture API and version references ---'
rg -n -C 8 'captureStart|captureStop|network.*har|agent-browser|0\.27\.0' package.json package-lock.json pnpm-lock.yaml yarn.lock src tests 2>/dev/null || trueRepository: stablyai/orca
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- applicable daemon convention ---'
sed -n '1,220p' /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions/src-main-daemon.md
printf '%s\n' '--- bridge execution path ---'
sed -n '2575,2675p' src/main/browser/agent-browser-bridge.ts
printf '%s\n' '--- package manifests containing agent-browser ---'
rg -n -C 3 'agent-browser|0\.27\.0' package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb 2>/dev/null || true
printf '%s\n' '--- targeted capture tests ---'
rg -n -C 8 'capture(Start|Stop)|network.*har|activeCapture|idle' src/main/browser --glob '*.{ts,tsx}' 2>/dev/null || trueRepository: stablyai/orca
Length of output: 49853
🌐 Web query:
agent-browser 0.27.0 "network har start" daemon session implementation
💡 Result:
In agent-browser v0.27.0, the network HAR capture functionality is implemented as part of the tool's client-daemon architecture [1][2]. The daemon, which manages the browser state over the Chrome DevTools Protocol (CDP), handles the network events required to generate HAR 1.2 files [1][2]. Network HAR recording is managed via the network har CLI commands, which communicate specific actions to the daemon [1]: 1. network har start [--content ]: This command tells the daemon to begin tracking network traffic [3][4]. It supports configurable content modes: - text (default): Embeds text-based response bodies (JSON, HTML, JS, etc.) up to 2 MB per body [3]. - all: Embeds all response bodies, with binary content base64-encoded [3][4]. - none: Records only metadata, excluding all response bodies [3][4]. 2. network har stop [path]: This command instructs the daemon to stop tracking, finalize the HAR 1.2 data, and save it to the specified path [1]. If no path is provided, the daemon writes the file to a default location (typically ~/.agent-browser/tmp/har/) [1]. Implementation Details: - Daemon Integration: The har_start and har_stop actions are processed by the Rust daemon, which uses CDP events (such as Network.requestWillBeSent and responseReceived) to collect accurate timing, headers, and body data [1]. - Session Persistence: Because the agent-browser uses a persistent daemon-client model, HAR recording remains active across navigation events within a session [1][2]. The har_stop action is explicitly handled to avoid triggering redundant browser launches [1]. - Utility: These HAR files are often used in conjunction with skills like derive-client to reverse-engineer APIs by analyzing the captured request/response data offline [5][6].
Citations:
- 1: feat: add
network har start/stopcommand for HAR 1.2 export vercel-labs/agent-browser#874 - 2: https://github.com/vercel-labs/agent-browser/blob/90050f2913159875e2c3719e424746396ccb3cbf/README.md
- 3: https://agent-browser.dev/network
- 4: https://github.com/vercel-labs/agent-browser?tab=readme-ov-file
- 5: https://agent-browser.dev/changelog
- 6: https://github.com/vercel-labs/agent-browser/blob/main/skill-data/derive-client/SKILL.md
🌐 Web query:
site:github.com/vercel-labs/agent-browser "har_stop" "har_start" Rust daemon
💡 Result:
The terms har_start and har_stop refer to specific command actions within the Vercel Labs agent-browser project, a tool that uses a Rust-based daemon to orchestrate browser automation [1][2][3]. These commands are used to manage the recording of network traffic in the HAR (HTTP Archive) 1.2 format: 1. har_start: This command initiates the recording of network requests during a browser session [1][2][3]. It sets a flag within the daemon to begin tracking network activity [2][3]. 2. har_stop: This command concludes the recording session and, in current implementations, is designed to save the captured network traffic to a specified file path [1][4]. If no path is provided, it may default to a temporary directory [1]. The daemon infrastructure routes these actions through specific handlers (e.g., handleHarStart and handleHarStop in the codebase) that interact with the underlying browser context, often utilizing Playwright-based browser management [2][5]. While these actions are defined in the project's protocol and CLI interface, functionality has evolved through development cycles, moving from initial stubs to integrated features that support exporting full HAR 1.2 files with request and response metadata [1][5]. You can typically invoke these through the CLI via commands such as agent-browser network har start and agent-browser network har stop [path] [1].
Citations:
- 1: feat: add
network har start/stopcommand for HAR 1.2 export vercel-labs/agent-browser#874 - 2: https://github.com/vercel-labs/agent-browser/blob/139dd0ec/src/actions.ts
- 3: https://github.com/vercel-labs/agent-browser/blob/4572acf0/cli/src/native/parity_tests.rs
- 4: https://github.com/vercel-labs/agent-browser/blob/82eadcee/cli/src/output.rs
- 5: har start / har stop doesn't actually save a .har file vercel-labs/agent-browser#484
🏁 Script executed:
#!/bin/bash
set -e
tmp=$(mktemp)
curl -fsSL 'https://api.github.com/repos/vercel-labs/agent-browser/git/trees/v0.27.0?recursive=1' > "$tmp"
printf '%s\n' '--- files related to HAR ---'
jq -r '.tree[] | select(.path | test("har|action|daemon|network"; "i")) | .path' "$tmp" | head -100
printf '%s\n' '--- tag metadata ---'
curl -fsSL 'https://api.github.com/repos/vercel-labs/agent-browser/git/ref/tags/v0.27.0' |
jq '{ref: .ref, type: .object.type, sha: .object.sha}'Repository: stablyai/orca
Length of output: 595
🏁 Script executed:
#!/bin/bash
set -e
base='https://raw.githubusercontent.com/vercel-labs/agent-browser/v0.27.0'
for file in cli/src/native/network.rs cli/src/native/daemon.rs cli/src/native/actions.rs; do
printf '\n--- %s ---\n' "$file"
curl -fsSL "$base/$file" |
nl -ba |
rg -n -C 12 'har|HAR|Network\.enable|requestWillBeSent|responseReceived' || true
doneRepository: stablyai/orca
Length of output: 585
🏁 Script executed:
#!/bin/bash
set -e
base='https://raw.githubusercontent.com/vercel-labs/agent-browser/v0.27.0'
for file in cli/src/native/network.rs cli/src/native/daemon.rs cli/src/native/actions.rs; do
printf '\n--- %s ---\n' "$file"
curl -fsSL "$base/$file" |
rg -n -C 12 'har|HAR|Network\.enable|requestWillBeSent|responseReceived' || true
doneRepository: stablyai/orca
Length of output: 50369
Restore HAR capture after idle replacement. When activeCapture is true, the replacement agent-browser v0.27.0 daemon starts with har_recording = false; post-replacement traffic is ignored, and network har stop writes an empty HAR. Restore network har start before processing the next command.
… teardown runs (stablyai#16367) (stablyai#16588) * fix(browser): bound the agent-browser daemon lifetime (stablyai#16367) `agent-browser` is a client/daemon CLI. Orca only ever spawns the short-lived client; that client forks a daemon Orca holds no handle on, which reparents to pid 1 immediately. Nothing in Orca reclaimed it, so a crashed or SIGKILL'd run left one daemon per browser tab alive forever — two of them at ~25.6 GiB and ~7.2 GiB RSS saturated a 64 GiB cgroup under headless `orca serve`. Three fixes, in order of how much they cover: 1. Set `AGENT_BROWSER_IDLE_TIMEOUT_MS` on both spawn paths (the bundled-binary bridge and the orcad external-Chromium provider). This is the only bound that survives every way Orca can die, including SIGKILL, where no teardown code ever runs. 10 minutes: >6x the bridge's 90s `EXEC_TIMEOUT_MS`, so it can never cut a command, a retry chain, or an ordinary gap between two user commands, while capping an abandoned daemon at minutes instead of days. Verified against agent-browser 0.27.0: an idle daemon exits and takes its Chromium tree and socket sidecar files with it, and a daemon attached over `--cdp` (the bridge's case) leaves the attached browser running, so an Orca tab is never closed by its daemon idling out. 2. Await `destroyAllSessions()` in the will-quit teardown barrier. It was fire-and-forget and the only browser member missing from `settleTeardownWithinDeadline`; each session's close is its own agent-browser child taking hundreds of ms, so `app.quit()` won. 3. Sweep daemons a previous run left behind, using agent-browser's own `session list` / `close` rather than a pid walk (see `windows-pty-job.ts` for why walking your own orphans is guesswork). `closeStaleAgentBrowserSession` only ever reset the one name a new tab was about to reuse. The sweep runs only when `AGENT_BROWSER_SOCKET_DIR` is set, because that private per-profile directory is what proves the enumeration can only see this Orca profile's daemons; it is never set on Windows, so Windows gets no enumeration rather than a machine-wide sweep that could close a daemon Orca does not own. Windows stays bounded by the idle timeout, which needs no ownership proof. orcad's session name is stable across runs, so it closes that one name at start instead — a killed orcad's daemon would otherwise be reused while still holding the previous run's Chromium on a dead serve port. Where the 25 GiB went is inference from code, not a measurement: `captureStart` sets `activeCapture` and only an explicit `captureStop` ends it, so a HAR capture in a daemon living for days is unbounded. Not claimed as proven; the idle bound caps it either way. Not re-landed: the queue bounds from stablyai#10179 (reverted by stablyai#10255) bound Orca's own main-process heap, not the daemon's RSS, so they do not address this report. Also true but left alone: the 3-strike breaker's `destroySession` is an unawaited call whose `close` is `catch {}`-swallowed, and it only fires while a command is in flight — an idle-but-bloated daemon is never noticed. The idle timeout now bounds that case. `getOffscreenBrowserBackend()?.destroyAll?.()` is declared `void` and fully synchronous, so unlike `destroyAllSessions` it has no promise to lose and needs no barrier entry. * fix(browser): scope the daemon idle bound and close every daemon Orca owns Review follow-ups on the agent-browser orphan fix. - Never idle-bound the orcad external-Chromium daemon: it owns the user's remote browser, so the 10-minute bound closed a live session and every tab in it. Per-tab helper daemons keep the bound; the stable session name plus the `close` in start() is what reclaims a killed orcad's Chromium tree. - Retire a page's daemon from the headless offscreen backend, which is the only place `orca serve` closes a page and never reached the bridge. Credit to @Jinwoo-H (stablyai#16564) for identifying this owner-boundary gap. - Bound the teardown close at 5s so the will-quit barrier member cannot inherit the 90s exec timeout, and close sessions still being created. - Gate the startup sweep on a socket directory Orca derived itself; an inherited AGENT_BROWSER_SOCKET_DIR is no proof of per-profile ownership. - Replay a session's network routes when the daemon idled out between two commands, instead of silently serving unstubbed requests. * fix(browser): give the orphan sweep a kill switch Of the three behaviours this PR adds, two are already recoverable in the field without a build: the idle bound is an env passthrough an operator can raise, and the quit close is bounded by its own timeout inside the teardown deadline. The startup sweep was the exception — it fires unconditionally, and if it closes a daemon it should not, or spawns one process per stale name on a profile holding hundreds, the only remedy was a revert. ORCA_DISABLE_AGENT_BROWSER_SWEEP=1 turns it off, matching the existing ORCA_DISABLE_CODEX_TRUST_RPC / ORCA_DISABLE_HTTP2 convention. Note for anyone reaching for it on macOS: a Finder-launched Orca does not see shell env, so it needs launchctl setenv or a terminal launch. * fix(orcad): reuse a surviving browser session instead of closing the user's start() closed the daemon before every open, killing the Chromium tree with it. That runs on every provider start, not just after a crash — so an `orca serve` restart took the remote user's browser and every tab in it. The justification was borrowed from the pane bridge, which passes --cdp and so really does hold a port that dies with its Orca. This session passes only --session and --profile: nothing binds it to the old process, and the daemon owns its Chromium independently. A survivor is reusable as-is. start() now probes for an active tab first and returns it untouched. Only a name that answers nothing gets closed and reopened — which is still the killed-orcad case the stable session name exists to recover. This matters more as orcad becomes the backend the remote host runs on: the browser it manages belongs to a user, not to the process that happens to be driving it this minute. It is also the same principle that already exempts this path from AGENT_BROWSER_IDLE_TIMEOUT_MS.
… teardown runs (stablyai#16367) (stablyai#16588) * fix(browser): bound the agent-browser daemon lifetime (stablyai#16367) `agent-browser` is a client/daemon CLI. Orca only ever spawns the short-lived client; that client forks a daemon Orca holds no handle on, which reparents to pid 1 immediately. Nothing in Orca reclaimed it, so a crashed or SIGKILL'd run left one daemon per browser tab alive forever — two of them at ~25.6 GiB and ~7.2 GiB RSS saturated a 64 GiB cgroup under headless `orca serve`. Three fixes, in order of how much they cover: 1. Set `AGENT_BROWSER_IDLE_TIMEOUT_MS` on both spawn paths (the bundled-binary bridge and the orcad external-Chromium provider). This is the only bound that survives every way Orca can die, including SIGKILL, where no teardown code ever runs. 10 minutes: >6x the bridge's 90s `EXEC_TIMEOUT_MS`, so it can never cut a command, a retry chain, or an ordinary gap between two user commands, while capping an abandoned daemon at minutes instead of days. Verified against agent-browser 0.27.0: an idle daemon exits and takes its Chromium tree and socket sidecar files with it, and a daemon attached over `--cdp` (the bridge's case) leaves the attached browser running, so an Orca tab is never closed by its daemon idling out. 2. Await `destroyAllSessions()` in the will-quit teardown barrier. It was fire-and-forget and the only browser member missing from `settleTeardownWithinDeadline`; each session's close is its own agent-browser child taking hundreds of ms, so `app.quit()` won. 3. Sweep daemons a previous run left behind, using agent-browser's own `session list` / `close` rather than a pid walk (see `windows-pty-job.ts` for why walking your own orphans is guesswork). `closeStaleAgentBrowserSession` only ever reset the one name a new tab was about to reuse. The sweep runs only when `AGENT_BROWSER_SOCKET_DIR` is set, because that private per-profile directory is what proves the enumeration can only see this Orca profile's daemons; it is never set on Windows, so Windows gets no enumeration rather than a machine-wide sweep that could close a daemon Orca does not own. Windows stays bounded by the idle timeout, which needs no ownership proof. orcad's session name is stable across runs, so it closes that one name at start instead — a killed orcad's daemon would otherwise be reused while still holding the previous run's Chromium on a dead serve port. Where the 25 GiB went is inference from code, not a measurement: `captureStart` sets `activeCapture` and only an explicit `captureStop` ends it, so a HAR capture in a daemon living for days is unbounded. Not claimed as proven; the idle bound caps it either way. Not re-landed: the queue bounds from stablyai#10179 (reverted by stablyai#10255) bound Orca's own main-process heap, not the daemon's RSS, so they do not address this report. Also true but left alone: the 3-strike breaker's `destroySession` is an unawaited call whose `close` is `catch {}`-swallowed, and it only fires while a command is in flight — an idle-but-bloated daemon is never noticed. The idle timeout now bounds that case. `getOffscreenBrowserBackend()?.destroyAll?.()` is declared `void` and fully synchronous, so unlike `destroyAllSessions` it has no promise to lose and needs no barrier entry. * fix(browser): scope the daemon idle bound and close every daemon Orca owns Review follow-ups on the agent-browser orphan fix. - Never idle-bound the orcad external-Chromium daemon: it owns the user's remote browser, so the 10-minute bound closed a live session and every tab in it. Per-tab helper daemons keep the bound; the stable session name plus the `close` in start() is what reclaims a killed orcad's Chromium tree. - Retire a page's daemon from the headless offscreen backend, which is the only place `orca serve` closes a page and never reached the bridge. Credit to @Jinwoo-H (stablyai#16564) for identifying this owner-boundary gap. - Bound the teardown close at 5s so the will-quit barrier member cannot inherit the 90s exec timeout, and close sessions still being created. - Gate the startup sweep on a socket directory Orca derived itself; an inherited AGENT_BROWSER_SOCKET_DIR is no proof of per-profile ownership. - Replay a session's network routes when the daemon idled out between two commands, instead of silently serving unstubbed requests. * fix(browser): give the orphan sweep a kill switch Of the three behaviours this PR adds, two are already recoverable in the field without a build: the idle bound is an env passthrough an operator can raise, and the quit close is bounded by its own timeout inside the teardown deadline. The startup sweep was the exception — it fires unconditionally, and if it closes a daemon it should not, or spawns one process per stale name on a profile holding hundreds, the only remedy was a revert. ORCA_DISABLE_AGENT_BROWSER_SWEEP=1 turns it off, matching the existing ORCA_DISABLE_CODEX_TRUST_RPC / ORCA_DISABLE_HTTP2 convention. Note for anyone reaching for it on macOS: a Finder-launched Orca does not see shell env, so it needs launchctl setenv or a terminal launch. * fix(orcad): reuse a surviving browser session instead of closing the user's start() closed the daemon before every open, killing the Chromium tree with it. That runs on every provider start, not just after a crash — so an `orca serve` restart took the remote user's browser and every tab in it. The justification was borrowed from the pane bridge, which passes --cdp and so really does hold a port that dies with its Orca. This session passes only --session and --profile: nothing binds it to the old process, and the daemon owns its Chromium independently. A survivor is reusable as-is. start() now probes for an active tab first and returns it untouched. Only a name that answers nothing gets closed and reopened — which is still the killed-orcad case the stable session name exists to recover. This matters more as orcad becomes the backend the remote host runs on: the browser it manages belongs to a user, not to the process that happens to be driving it this minute. It is also the same principle that already exempts this path from AGENT_BROWSER_IDLE_TIMEOUT_MS.
… teardown runs (stablyai#16367) (stablyai#16588) * fix(browser): bound the agent-browser daemon lifetime (stablyai#16367) `agent-browser` is a client/daemon CLI. Orca only ever spawns the short-lived client; that client forks a daemon Orca holds no handle on, which reparents to pid 1 immediately. Nothing in Orca reclaimed it, so a crashed or SIGKILL'd run left one daemon per browser tab alive forever — two of them at ~25.6 GiB and ~7.2 GiB RSS saturated a 64 GiB cgroup under headless `orca serve`. Three fixes, in order of how much they cover: 1. Set `AGENT_BROWSER_IDLE_TIMEOUT_MS` on both spawn paths (the bundled-binary bridge and the orcad external-Chromium provider). This is the only bound that survives every way Orca can die, including SIGKILL, where no teardown code ever runs. 10 minutes: >6x the bridge's 90s `EXEC_TIMEOUT_MS`, so it can never cut a command, a retry chain, or an ordinary gap between two user commands, while capping an abandoned daemon at minutes instead of days. Verified against agent-browser 0.27.0: an idle daemon exits and takes its Chromium tree and socket sidecar files with it, and a daemon attached over `--cdp` (the bridge's case) leaves the attached browser running, so an Orca tab is never closed by its daemon idling out. 2. Await `destroyAllSessions()` in the will-quit teardown barrier. It was fire-and-forget and the only browser member missing from `settleTeardownWithinDeadline`; each session's close is its own agent-browser child taking hundreds of ms, so `app.quit()` won. 3. Sweep daemons a previous run left behind, using agent-browser's own `session list` / `close` rather than a pid walk (see `windows-pty-job.ts` for why walking your own orphans is guesswork). `closeStaleAgentBrowserSession` only ever reset the one name a new tab was about to reuse. The sweep runs only when `AGENT_BROWSER_SOCKET_DIR` is set, because that private per-profile directory is what proves the enumeration can only see this Orca profile's daemons; it is never set on Windows, so Windows gets no enumeration rather than a machine-wide sweep that could close a daemon Orca does not own. Windows stays bounded by the idle timeout, which needs no ownership proof. orcad's session name is stable across runs, so it closes that one name at start instead — a killed orcad's daemon would otherwise be reused while still holding the previous run's Chromium on a dead serve port. Where the 25 GiB went is inference from code, not a measurement: `captureStart` sets `activeCapture` and only an explicit `captureStop` ends it, so a HAR capture in a daemon living for days is unbounded. Not claimed as proven; the idle bound caps it either way. Not re-landed: the queue bounds from stablyai#10179 (reverted by stablyai#10255) bound Orca's own main-process heap, not the daemon's RSS, so they do not address this report. Also true but left alone: the 3-strike breaker's `destroySession` is an unawaited call whose `close` is `catch {}`-swallowed, and it only fires while a command is in flight — an idle-but-bloated daemon is never noticed. The idle timeout now bounds that case. `getOffscreenBrowserBackend()?.destroyAll?.()` is declared `void` and fully synchronous, so unlike `destroyAllSessions` it has no promise to lose and needs no barrier entry. * fix(browser): scope the daemon idle bound and close every daemon Orca owns Review follow-ups on the agent-browser orphan fix. - Never idle-bound the orcad external-Chromium daemon: it owns the user's remote browser, so the 10-minute bound closed a live session and every tab in it. Per-tab helper daemons keep the bound; the stable session name plus the `close` in start() is what reclaims a killed orcad's Chromium tree. - Retire a page's daemon from the headless offscreen backend, which is the only place `orca serve` closes a page and never reached the bridge. Credit to @Jinwoo-H (stablyai#16564) for identifying this owner-boundary gap. - Bound the teardown close at 5s so the will-quit barrier member cannot inherit the 90s exec timeout, and close sessions still being created. - Gate the startup sweep on a socket directory Orca derived itself; an inherited AGENT_BROWSER_SOCKET_DIR is no proof of per-profile ownership. - Replay a session's network routes when the daemon idled out between two commands, instead of silently serving unstubbed requests. * fix(browser): give the orphan sweep a kill switch Of the three behaviours this PR adds, two are already recoverable in the field without a build: the idle bound is an env passthrough an operator can raise, and the quit close is bounded by its own timeout inside the teardown deadline. The startup sweep was the exception — it fires unconditionally, and if it closes a daemon it should not, or spawns one process per stale name on a profile holding hundreds, the only remedy was a revert. ORCA_DISABLE_AGENT_BROWSER_SWEEP=1 turns it off, matching the existing ORCA_DISABLE_CODEX_TRUST_RPC / ORCA_DISABLE_HTTP2 convention. Note for anyone reaching for it on macOS: a Finder-launched Orca does not see shell env, so it needs launchctl setenv or a terminal launch. * fix(orcad): reuse a surviving browser session instead of closing the user's start() closed the daemon before every open, killing the Chromium tree with it. That runs on every provider start, not just after a crash — so an `orca serve` restart took the remote user's browser and every tab in it. The justification was borrowed from the pane bridge, which passes --cdp and so really does hold a port that dies with its Orca. This session passes only --session and --profile: nothing binds it to the old process, and the daemon owns its Chromium independently. A survivor is reusable as-is. start() now probes for an active tab first and returns it untouched. Only a name that answers nothing gets closed and reopened — which is still the killed-orcad case the stable session name exists to recover. This matters more as orcad becomes the backend the remote host runs on: the browser it manages belongs to a user, not to the process that happens to be driving it this minute. It is also the same principle that already exempts this path from AGENT_BROWSER_IDLE_TIMEOUT_MS. (cherry picked from commit 88ecc73)
… teardown runs (stablyai#16367) (stablyai#16588) * fix(browser): bound the agent-browser daemon lifetime (stablyai#16367) `agent-browser` is a client/daemon CLI. Orca only ever spawns the short-lived client; that client forks a daemon Orca holds no handle on, which reparents to pid 1 immediately. Nothing in Orca reclaimed it, so a crashed or SIGKILL'd run left one daemon per browser tab alive forever — two of them at ~25.6 GiB and ~7.2 GiB RSS saturated a 64 GiB cgroup under headless `orca serve`. Three fixes, in order of how much they cover: 1. Set `AGENT_BROWSER_IDLE_TIMEOUT_MS` on both spawn paths (the bundled-binary bridge and the orcad external-Chromium provider). This is the only bound that survives every way Orca can die, including SIGKILL, where no teardown code ever runs. 10 minutes: >6x the bridge's 90s `EXEC_TIMEOUT_MS`, so it can never cut a command, a retry chain, or an ordinary gap between two user commands, while capping an abandoned daemon at minutes instead of days. Verified against agent-browser 0.27.0: an idle daemon exits and takes its Chromium tree and socket sidecar files with it, and a daemon attached over `--cdp` (the bridge's case) leaves the attached browser running, so an Orca tab is never closed by its daemon idling out. 2. Await `destroyAllSessions()` in the will-quit teardown barrier. It was fire-and-forget and the only browser member missing from `settleTeardownWithinDeadline`; each session's close is its own agent-browser child taking hundreds of ms, so `app.quit()` won. 3. Sweep daemons a previous run left behind, using agent-browser's own `session list` / `close` rather than a pid walk (see `windows-pty-job.ts` for why walking your own orphans is guesswork). `closeStaleAgentBrowserSession` only ever reset the one name a new tab was about to reuse. The sweep runs only when `AGENT_BROWSER_SOCKET_DIR` is set, because that private per-profile directory is what proves the enumeration can only see this Orca profile's daemons; it is never set on Windows, so Windows gets no enumeration rather than a machine-wide sweep that could close a daemon Orca does not own. Windows stays bounded by the idle timeout, which needs no ownership proof. orcad's session name is stable across runs, so it closes that one name at start instead — a killed orcad's daemon would otherwise be reused while still holding the previous run's Chromium on a dead serve port. Where the 25 GiB went is inference from code, not a measurement: `captureStart` sets `activeCapture` and only an explicit `captureStop` ends it, so a HAR capture in a daemon living for days is unbounded. Not claimed as proven; the idle bound caps it either way. Not re-landed: the queue bounds from stablyai#10179 (reverted by stablyai#10255) bound Orca's own main-process heap, not the daemon's RSS, so they do not address this report. Also true but left alone: the 3-strike breaker's `destroySession` is an unawaited call whose `close` is `catch {}`-swallowed, and it only fires while a command is in flight — an idle-but-bloated daemon is never noticed. The idle timeout now bounds that case. `getOffscreenBrowserBackend()?.destroyAll?.()` is declared `void` and fully synchronous, so unlike `destroyAllSessions` it has no promise to lose and needs no barrier entry. * fix(browser): scope the daemon idle bound and close every daemon Orca owns Review follow-ups on the agent-browser orphan fix. - Never idle-bound the orcad external-Chromium daemon: it owns the user's remote browser, so the 10-minute bound closed a live session and every tab in it. Per-tab helper daemons keep the bound; the stable session name plus the `close` in start() is what reclaims a killed orcad's Chromium tree. - Retire a page's daemon from the headless offscreen backend, which is the only place `orca serve` closes a page and never reached the bridge. Credit to @Jinwoo-H (stablyai#16564) for identifying this owner-boundary gap. - Bound the teardown close at 5s so the will-quit barrier member cannot inherit the 90s exec timeout, and close sessions still being created. - Gate the startup sweep on a socket directory Orca derived itself; an inherited AGENT_BROWSER_SOCKET_DIR is no proof of per-profile ownership. - Replay a session's network routes when the daemon idled out between two commands, instead of silently serving unstubbed requests. * fix(browser): give the orphan sweep a kill switch Of the three behaviours this PR adds, two are already recoverable in the field without a build: the idle bound is an env passthrough an operator can raise, and the quit close is bounded by its own timeout inside the teardown deadline. The startup sweep was the exception — it fires unconditionally, and if it closes a daemon it should not, or spawns one process per stale name on a profile holding hundreds, the only remedy was a revert. ORCA_DISABLE_AGENT_BROWSER_SWEEP=1 turns it off, matching the existing ORCA_DISABLE_CODEX_TRUST_RPC / ORCA_DISABLE_HTTP2 convention. Note for anyone reaching for it on macOS: a Finder-launched Orca does not see shell env, so it needs launchctl setenv or a terminal launch. * fix(orcad): reuse a surviving browser session instead of closing the user's start() closed the daemon before every open, killing the Chromium tree with it. That runs on every provider start, not just after a crash — so an `orca serve` restart took the remote user's browser and every tab in it. The justification was borrowed from the pane bridge, which passes --cdp and so really does hold a port that dies with its Orca. This session passes only --session and --profile: nothing binds it to the old process, and the daemon owns its Chromium independently. A survivor is reusable as-is. start() now probes for an active tab first and returns it untouched. Only a name that answers nothing gets closed and reopened — which is still the killed-orcad case the stable session name exists to recover. This matters more as orcad becomes the backend the remote host runs on: the browser it manages belongs to a user, not to the process that happens to be driving it this minute. It is also the same principle that already exempts this path from AGENT_BROWSER_IDLE_TIMEOUT_MS.
ELI5
When Orca opens a browser for an agent, it starts a little background helper program that actually drives the browser. Orca was never able to shut those helpers down reliably — they detached from Orca completely, so if Orca quit, crashed, or was force-killed, the helpers just kept running forever, and two of them had grown to about 25 GB and 7 GB of memory, which filled the machine until everything froze. Now every helper turns itself off after ten minutes of doing nothing, Orca actually waits for them to close when you quit, and on start-up Orca cleans up any leftovers from a previous run.
The one helper that runs a remote user's own browser is deliberately left alone. A review round found this PR was doing the opposite in one place: it closed that helper — and the whole browser hanging off it — every single time Orca started, not just after a crash. That is fixed: Orca now looks for a live tab first and, if it finds one, leaves the browser exactly as the user left it.
What Changed
Original commit (
a12a2fd171f) plus three review rounds (294fdeb6d73,c9a065be2e1,a73395ba43c) — 13 files, +864/−51.1. A self-imposed idle timeout — the only bound that survives SIGKILL.
agent-browser-process-environment.tsnow setsAGENT_BROWSER_IDLE_TIMEOUT_MS = 10 minon every platform (previously it returned the inherited env untouched on win32 and whenever a caller had already setAGENT_BROWSER_SOCKET_DIR, so those paths got nothing). An operator-set value passes through untouched.2. The bound is asymmetric, on purpose — and the orcad daemon is now reused, not killed. The first fix round removed the timeout from the orcad external-Chromium daemon (
external-chromium-browser-session.ts): that daemon owns a remote user's whole Chromium tree, so idling it out would have closed their live browser and every tab in it.The same round then contradicted that reasoning:
start()issued aclosebefore everyopen, which kills the daemon and its Chromium tree — andstart()runs on every provider start, not only after a crash, so anorca serverestart took the remote user's browser and every tab in it. The justification for thatclosewas borrowed from the pane bridge, which passes--cdpand so genuinely holds a port that dies with its Orca. This session passes only--sessionand--profile: nothing binds it to the old process, and the daemon owns its Chromium independently, so a survivor is reusable as-is.Fixed in
a73395ba43c:start()probes for an active tab and returns it untouched; only a session name that answers nothing is closed and reopened — which is still the killed-orcad case the stable session name exists to recover. This matters more as orcad becomes the backend the remote host runs on: the browser it manages belongs to a user, not to the process driving it this minute.3. Quit actually waits.
destroyAllSessions()was fire-and-forget and was the only teardown step not insettleTeardownWithinDeadline, soapp.quit()was reached long before any daemon closed. It is now a barrier member (index.ts:3545) — and each per-session close runs with an explicitTEARDOWN_CLOSE_TIMEOUT_MS = 5_000instead of inheritingEXEC_TIMEOUT_MS = 90_000, so the member is individually bounded well inside the 20 s deadline. The loop also walks the union ofsessions/pendingSessionCreation/pendingSessionDestruction, so a daemon spawned by a session still being created no longer outlives quit.4. Headless owner boundary. In
orca serve,OffscreenBrowserBackend.closeTabdestroyed the page without ever retiring its daemon, and it unregistered the guest first — which would have defeatedonTabClosed'swebContentsIdlookup even if it had been wired up. The backend now takes agetAgentBrowserBridgehook and awaits the new page-id-keyedAgentBrowserBridge.onPageClosed(browserPageId)beforeunregisterGuest, on bothcloseTaband the unexpectedwebContents 'destroyed'event.destroyAlldrops each entry before destroying so the destroyed handler cannot queue a second, unawaited retirement outside the quit barrier.5. Start-up sweep for daemons left by a previous run.
closeStaleAgentBrowserSessiononly ever reset the one name a new tab was about to reuse, so a crash with 10 open tabs left 9 daemons untouched forever. Newagent-browser-orphan-sweep.tsusessession list --json+--session X close— no pid-walking, per the argument inwindows-pty-job.ts:4-24— filtered to theorca-tab-prefix and guarded byisSessionLive.6. Ownership is proven, not assumed. The sweep originally ran whenever
AGENT_BROWSER_SOCKET_DIRwas merely set, which an inherited value satisfies — two Orca profiles sharing one socket dir would sweep each other's live daemons.createAgentBrowserProcessEnvironmentnow returns{ env, ownsSocketDirectory }and the sweep gates onownsSocketDirectory. Windows deliberately gets no enumeration at all (named pipes make the directory moot) rather than a machine-wide sweep that could close another process's daemons; it stays bounded by the idle timeout, which needs no ownership proof.7. Idle-out is no longer silent. The bridge tracks
session.lastCommandAt; when the gap reaches the configured idle timeout it resetssession.initializedand re-seedspendingInterceptRestore, so configured network routes are replayed onto the replacement daemon instead of requests silently hitting the real network. Detection is skipped entirely when no idle bound is configured.8. The sweep has a kill switch (
c9a065be2e1).ORCA_DISABLE_AGENT_BROWSER_SWEEP=1disables it, matching the existingORCA_DISABLE_CODEX_TRUST_RPC/ORCA_DISABLE_HTTP2convention. Of the three new behaviours, the other two were already field-recoverable without a build — the idle bound is an env passthrough an operator can raise (use a large value, not0), and the quit close is bounded atTEARDOWN_CLOSE_TIMEOUT_MS = 5_000inside the 20 s teardown deadline. The sweep was the only one whose failure modes (closing a daemon it should not, or spawning one process per stale name on a profile holding hundreds) were revert-only. macOS caveat: a Finder-launched Orca does not see shell env, so both this flag and anAGENT_BROWSER_IDLE_TIMEOUT_MSoverride needlaunchctl setenvor a terminal launch.Cleanups from the same rounds: dropped the dead
.catch(() => {})onsweepOrphanedSessions, collapsed the two identical sweep timeouts into oneSWEEP_TIMEOUT_MS, dropped the never-readsuccess?: unknownfromSessionListEnvelope, and deleted two tests that only re-exercisedsettleTeardownWithinDeadline(already pinned inquit-teardown-deadline.test.ts).Why
agent-browseris a client/daemon CLI. Orca only ever spawns the short-lived client; the client forks a persistent daemon Orca holds no handle on. Reproduced directly against agent-browser 0.27.0: a freshly spawned daemon is PPID 1 immediately, with the whole Chrome tree under it. Per its README the daemon self-terminates only whenAGENT_BROWSER_IDLE_TIMEOUT_MSis set — and Orca set it nowhere. So nothing bounded a daemon's life: no idle timeout, no max lifetime, no memory cap, no health check, no reaper.Evidence for each leak path:
agent-browser-process-environment.tsreturnedinheritedEnvuntouched on win32 and on the socket-dir-override path.index.ts:3487(pre-fix) calleddestroyAllSessions()unawaited while every neighbour was asettleTeardownWithinDeadlinemember;destroySessionspawns a freshagent-browser --session X closechild and waits on it (agent-browser-bridge.ts:2465), so every open tab's daemon survived a normal quit.orca-runtime-browser.ts:2062takes the offscreen branch and calls onlyoffscreen.closeTab, which never reached the bridge. A long-livedorca serveopening and closing 50 pages a day leaked all 50 daemons; nothing in the original commit closed them.closeStaleAgentBrowserSessiononly reset the single name a new tab was reusing.Reported impact (#16367): two orphans at ~25.6 GiB and ~7.2 GiB RSS drove a 64 GiB cgroup to its limit until Orca, Claude, Codex and Chrome all entered uninterruptible
Dstate under headlessorca serve.10 minutes for the idle bound is >6× the bridge's 90 s
EXEC_TIMEOUT_MS, so it cannot cut a command, a retry chain, or a normal think-gap between two user commands, while capping an abandoned daemon at minutes instead of days.Corrections to the earlier analysis
close-before-openwas justified from the wrong path, and is gone. It was reasoned about as if this session behaved like the pane bridge, which passes--cdpand therefore really does hold a port that dies with its Orca. The orcad session passes only--sessionand--profile, so a surviving daemon is not bound to the dead process and is reusable as-is. Closing it unconditionally destroyed a live remote browser on every provider start — the exact outcome exempting this path from the idle bound was meant to prevent.getOffscreenBrowserBackend()?.destroyAll?.()does not have the same bug: it is declaredvoidand is fully synchronous. No barrier entry needed./tmp/orca-ab-*does not grow unboundedly: the key issha256(userDataPath), one stable dir per profile.Risk & Trade-offs
Merge risk: Medium-high. The one change that could destroy a remote user's live browser is fixed and pinned by tests, so what remains is an unexercised Windows path and a headline memory number attributed to HAR capture by reading code rather than by measuring the leaking process. (Was High; what moved it: the orcad close-on-every-start bug is fixed in
a73395ba43c, and the sweep — the only new behaviour whose failure modes were revert-only — now has theORCA_DISABLE_AGENT_BROWSER_SWEEP=1kill switch.)What could go wrong
The orcad daemon is closed on every provider start, taking a remote user's live browser and every tab with it.Fixed and pinned (a73395ba43c).start()now probes for an active tab and returns it untouched; only a name that answers nothing is closed and reopened. Both new tests were verified to fail against the old behaviour — one asserts nocloseand noopenis issued when a session answers, the other that a silent name is still reclaimed.agent-browser's notion of "idle" is not what we think it is, and long sessions get cut. The 10-minute bound is built on the tool's README plus one manual run against 0.27.0. If the daemon measures from process start rather than last command, any session older than 10 minutes gets its daemon killed and respawned. Bounded and self-healing —execAgentBrowserreasserts--cdpon every call, so the session comes back — but a user would see a browser tab reset mid-task. No automated test can pin this without the real binary; the Windows run below would settle it as a side effect.orca serve. What would surface it: an empty or truncated HAR with no error.destroyAllSessions()used to be fire-and-forget; quit now waits on it. Each close is capped at 5 s and the whole thing sits inside the existing 20 s teardown barrier, so the worst case is bounded — but quit is measurably slower when a daemon will not close.isSessionLive()and the close spawn, and a profile with hundreds of staleorca-tab-names producing hundreds of sequential subprocess spawns after launch. Both are low-impact (the session respawns; the sweep is unawaited and off the critical path), and both are now switchable off in the field withORCA_DISABLE_AGENT_BROWSER_SWEEP=1rather than needing a revert — but the spawn count is still uncapped when the sweep is on.What a user gets in exchange
The reported failure in #16367 stops happening: two orphaned daemons at ~25.6 GiB and ~7.2 GiB RSS drove a 64 GiB cgroup to its limit until Orca, Claude, Codex and Chrome all went into uninterruptible
Dstate. Every orphan class named in that issue is now bounded at 10 minutes, and three of the four are closed deterministically instead — daemons die on quit, on headless page close, and on the next start-up after a crash. Before this, anorca serveopening and closing 50 pages a day leaked all 50 daemons, forever, with nothing in the codebase capable of reclaiming them. The idle bound is the only one that survives SIGKILL, which matters precisely because a cgroup at its memory limit kills Orca outright and no teardown code runs at all.Trade-offs accepted
process.oncemeans abeforeunloadveto on the first signal makes a second SIGTERM a no-op. Real, unfixed, owed as a separate PR.createAgentBrowserProcessEnvironmentnow returns a shallow copy on win32 and on the mkdir-failure path, where it previously returned the liveprocess.envreference. The bridge snapshots once in its constructor, so post-construction PATH/DISPLAY mutations are not picked up on those two paths. Both mutators are macOS/Linux start-up steps, so this is likely inert — but the ordering was never verified.orca servetopology from the issue was never run against the built app. The owner-boundary fix that closes that exact leak is covered by unit tests only.How to back it out
Clean revert, and the riskiest single behaviour can be disabled without one (
ORCA_DISABLE_AGENT_BROWSER_SWEEP=1; on macOS vialaunchctl setenvor a terminal launch, since a Finder-launched Orca does not see shell env). No schema, no migration, no persisted state — the only durable artifact is the per-profile socket directory under/tmp/orca-ab-*, which is keyed onsha256(userDataPath)and was already there before this PR. Reverting restores the previous (leaking) behaviour immediately with no cleanup step.Linked Issue
Fixes #16367
This closes the reported failure: every class of orphan named in the issue is now bounded (idle timer), and three of the four are also closed deterministically (quit barrier, headless close, start-up sweep). Two known gaps are documented under Review rather than left implied — Windows has no deterministic reclamation, only the 10-minute timer; and an in-flight HAR capture lost to an idle-out is still lost silently.
Visual Proof
N/A — every change is in the main process and in a detached CLI daemon's lifetime (process env, quit teardown, page-close wiring, start-up sweep); nothing renders and no user-facing copy, layout, or interaction changes.
Testing
Real output from the fix rounds (worktree
/tmp/fixwt-16588, heada73395ba43c):The 1 skip is pre-existing: an integration test that needs a real operator executable.
Non-vacuity — each new test was re-run against the pre-fix source and fails there:
The two
a73395ba43ctests are the proof for the orcad fix: the first fails against the previous behaviour becausecloseandopenwere both issued against a session that already had a live tab; the second pins that a silent name is still reclaimed, so the killed-orcad recovery the stable session name exists for is not lost. The kill switch adds two more (enumerates nothing when disabled, even when Orca owns the socket directory,still sweeps when the flag holds any other value) — new-code coverage rather than regression proofs, since the flag did not exist before.The original commit's
it.eachidle-timeout test also fails pre-fix on all three platforms, and the quit-teardown source-shape tests fail pre-fix becauseindex.tshad noagentBrowserShutdownbinding anddestroyAllSessionswas not a barrier member. The sweep, orcad-env and offscreen-retirement tests cover new modules, so they are new-code coverage rather than regression proofs.Reviewer re-ran independently on a clean checkout of the branch:
pnpm tcclean andpnpm test src/main/browser src/main/orcadgreen.Manual verification against the real agent-browser 0.27.0 binary (macOS):
--cdp(the bridge's mode) idles out without closing the attached browser, so an Orca tab is never killed by its own daemon expiring.session list --json→{"data":{"sessions":["orca-tab-sweepprobe"]}};--session X closekilled the PPID-1 orphan.Recommended before merge — still not done. Launch Orca on Windows with
AGENT_BROWSER_IDLE_TIMEOUT_MS=20000, open a browser pane, run a command, idle 30 s, confirm the daemon is gone, then run another command in the same pane and confirm it succeeds with the tab still alive. That single run converts three unknowns at once: that the changed win32 env path actually delivers the variable, that "idle" is measured from last command rather than process start, and that an idled-out daemon does not take the Electron webview with it.Also not verified end-to-end: the Linux headless
orca servetopology from the issue (a long-lived serve opening/closing many pages) was never run against the built app — the headless owner-boundary fix is covered by unit tests only.AI Disclosure
Review
Credit
Jinwoo Hong (@Jinwoo-H), #16564 identified the headless owner-boundary gap this PR originally missed entirely, and he was first and right on three separate points:
orca serve,OffscreenBrowserBackend.closeTabdestroys the page without ever retiring itsorca-tab-<pageId>daemon, and it unregisters the guest first, which would defeatonTabClosed'swebContentsIdlookup even if it were wired up. Our original commit reclaimed that leak only via the 10-minute idle timer. The fix here is inherited from @Jinwoo-H's fix(browser): retire helpers with page owners #16564: thegetAgentBrowserBridgehook and awaiting the bridge beforeunregisterGuestare his design.agent-browser closeat 5 s rather than letting it inherit the bridge's 90 s exec timeout inside a 20 s quit barrier. Our original commit moveddestroyAllSessionsinto the barrier while leaving the 90 s timeout — a wedged daemon would have hung quit for the whole deadline.TEARDOWN_CLOSE_TIMEOUT_MS = 5_000is taken from fix(browser): retire helpers with page owners #16564.destroyAllSessionsmust cover sessions still being created. The union oversessions/pendingSessionCreation/pendingSessionDestructionis his fix, adopted here.His judgement that a PPID-1 helper is normal, and that cleanup must key on session identity rather than pid or process name, matches the conclusion our sweep reached independently — he did not need our analysis to get there. The two PRs are complementary rather than competing: #16564 is the deterministic in-run retirement; this one adds the bound that survives a SIGKILL (which #16564 explicitly declines: "deliberately adds no … idle reaper"). A cgroup at its memory limit kills Orca outright and no teardown code of either PR runs.
Fixed this round
close-before-opendestroyed a live remote browser on every provider start. Not a crash-only path —start()runs on every provider start. Fixed ina73395ba43cby probing for an active tab and reusing it; only a name that answers nothing is reclaimed. This was the finding that had held the merge risk at High.c9a065be2e1withORCA_DISABLE_AGENT_BROWSER_SWEEP=1.Declined, with reasons
registerServeSignalHandlers). Adjacent and worth taking separately — today'sprocess.oncemeans abeforeunloadveto on the first signal makes a second SIGTERM a no-op. It touches serve process lifecycle rather than the daemon-orphan path, so it belongs in its own PR, not in this diff. Not fixed by this PR.resolveTargetPagefor the orcad path. Removing the bound from that daemon removes the failure mode outright with strictly less code; a periodictablist would add unbounded subprocess churn per registered page.createAgentBrowserProcessEnvironmentto a liveprocess.envreference on win32 and the mkdir-failure path. See residual risk 2 below; reverting would reintroduce cross-platform inconsistency for no observed defect.Residual risk
agent-browser's idle definition comes from its README plus one manual run against 0.27.0. If "idle" were measured from daemon start rather than last command, the 10-minute bound would cut long sessions. No automated coverage can pin this without the real binary — the Windows run recommended under Testing would settle it.process.envreference. The bridge snapshots it once in its constructor, so post-construction mutations (hydrate-shell-path.ts:395sets PATH,ensure-virtual-display.ts:129/165sets DISPLAY) are not picked up on those two paths. Both mutators are macOS/Linux start-up steps and the Linux success path already copied, so this is likely inert — but the ordering was not verified.agent-browserspawns after launch. Off the main thread and unawaited, so latency-invisible, but unbounded subprocess churn. Disableable withORCA_DISABLE_AGENT_BROWSER_SWEEP=1; still uncapped when on.isSessionLive()and the close spawn: a tab created in that window could have its brand-new daemon closed. Low impact —execAgentBrowserreasserts--cdpon every invocation, so the session simply respawns.tabbut cannot drive its Chromium is reused rather than replaced. Deliberate: a false "dead" verdict costs the user their entire browser, a false "healthy" one costs a failed command.Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
child_processuse — the sweep goes throughrunProcess.ORCA_DISABLE_AGENT_BROWSER_SWEEP=1disables the start-up sweep. On macOS a Finder-launched Orca does not see shell env, so this flag (and anyAGENT_BROWSER_IDLE_TIMEOUT_MSoverride) needslaunchctl setenvor a terminal launch. To relax the idle bound rather than remove it, set a large value — not0.a73395ba43c, never closed while it still answers, so a remote user's live browser is not closed by an Orca restart. No wire-format change, so no mixed-version concern.AGENT_BROWSER_IDLE_TIMEOUT_MSorAGENT_BROWSER_SOCKET_DIRis preserved untouched.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm testpass locally;pnpm buildleft to CI