Skip to content

fix(browser): browser tools unusable after Hermes restart — zombie daemon holds port - #65701

Open
LiteSoul wants to merge 1 commit into
NousResearch:mainfrom
LiteSoul:fix/browser-daemon-zombie-retry
Open

fix(browser): browser tools unusable after Hermes restart — zombie daemon holds port#65701
LiteSoul wants to merge 1 commit into
NousResearch:mainfrom
LiteSoul:fix/browser-daemon-zombie-retry

Conversation

@LiteSoul

@LiteSoul LiteSoul commented Jul 16, 2026

Copy link
Copy Markdown

Problem

On Windows, when Hermes is force-killed (taskkill, power loss, app close and reopen), the agent-browser daemon holds its deterministic TCP port as an orphan. The next Hermes session's first browser command hits EADDRINUSE before the background reaper thread has had a chance to sweep, surfacing as:

✗ Daemon failed to start (port: 127.0.0.1:55478)

At that point every browser tool is unusablebrowser_navigate, browser_click, browser_snapshot etc. all return the error — until the user manually finds and kills the zombie node.exe in Task Manager.

Reproduced live on Windows (this session): browser_navigate returned "Daemon failed to start (port: 127.0.0.1:55478)" with a zombie daemon.js (PID 39924) holding the port. After killing the zombie via Stop-Process -Id 39924 -Force, browser_navigate succeeded. PID 39924's daemon was spawned from C:\Users\LiteSoul\AppData\Roaming\nvm\v24.13.1\node_modules\agent-browser\dist\daemon.js — agent-browser v0.17.1.

Fix (scoped, per teknium1's review)

Two pieces after a substantive revision responding to teknium1's Blocking feedback:

1. Retry on daemon-start failure

When _run_browser_command detects "Daemon failed to start":

  1. Detect the error via _is_daemon_start_failure(result).
  2. Reap — synchronously call _reap_orphaned_browser_sessions() to kill the Hermes-managed zombie holding the port. The reaper only globs agent-browser-h_* / agent-browser-cdp_* / agent-browser-hermes_* socket dirs in the system tmp dir; it does not touch ~/.agent-browser/.
  3. Reset_reset_session_for_retry(task_id) drops the failed session from _active_sessions and removes its socket dir, so the retry gets a fresh h_<uuid4().hex[:10]> session name → new port → no collision with the just-killed zombie.
  4. Retry — call _run_browser_command exactly once more with _daemon_retried=True. A second failure returns the error without retrying again.

Why this is safe: the retry path only fires after the daemon has already failed to start — a daemon that can't bind its port never opened Chromium, never accepted a CDP connection, so there is no active browser session to lose. _reap_orphaned_browser_sessions is the existing cross-process-safe reaper (owner_pid-based ownership check, #21561); the retry just calls it inline instead of waiting for the lazy background cleanup thread.

2. Dropped the ~/.agent-browser/ sweep entirely

The first iteration of this PR also swept ~/.agent-browser/ for zombies from direct CLI invocations outside Hermes. teknium1 marked this Blocking on review: app-dir sessions have no owner_pid file, so identity + session-name binding only proves the PID is an agent-browser daemon for that session name — not that it is orphaned or Hermes-owned. A live agent-browser open in a user's separate terminal would be terminated by the next Hermes restart. The app-dir machinery is removed entirely from this iteration. New regression test TestAppDirSessionsAreLeftAlone::test_live_app_dir_daemon_survives_reaper pins the removal.

The retry calls _reap_orphaned_browser_sessions unchanged — its glob misses ~/.agent-browser/ by design, so the retry can never reach live direct-CLI daemons either.

Revalidation on current main

teknium1's second ask was to revalidate the Windows repro on the upgraded dependency. Honest findings:

  • Upstream main installs agent-browser@^0.26.0 via Install-AgentBrowser in scripts/install.ps1 (commit 284e084bcc wired the daemon idle-timeout). On Linux/macOS the equivalent is ensure_browser() in scripts/install.sh.
  • On this Windows machine, the Hermes-managed bundled prefix at $HERMES_HOME/node/bin/agent-browser — the location Install-AgentBrowser populates — does not exist on disk. Hermes falls through to bare PATH, which resolves to a user-managed NVM install of agent-browser v0.17.1. The idle-timeout commit does NOT apply to a 0.17.1 daemon. The zombie-on-restart problem still reproduces here, exactly as the PR describes.
  • Why this is structural, not a quirk of this machine: the install-flow gap is documented in the discussion thread — short version is that install.ps1's auto-driven $InstallStages list does not include a browser stage, so the desktop Update button (which iterates install.ps1 -Stage <n> per-stage via bootstrap-runner.ts) never invokes Install-AgentBrowser. It's reachable only via install.ps1 -PostInstall (a one-off post-install bootstrapper) or install.ps1 -Ensure browser (an explicit manual invocation). Windows users installing/updating exclusively via the desktop app get whatever agent-browser their bare PATH provides. The zombie problem persists for that population as the production reality, not as a legacy edge — until a browser stage reaches the install manifest (a separate PR I'll file shortly).
  • I did not directly verify the repro on a healthily-installed agent-browser@^0.26.0 (that would need a separate install.ps1 -PostInstall run + force-restart repro). Flagging the gap rather than claiming false-green. The original submission's premise about AGENT_BROWSER_IDLE_TIMEOUT_MS being unimplemented in v0.17.1 was accurate for 0.17.1 but stale for current main — that paragraph is dropped from this body.
  • Implication for this PR: kept the scoped retry because for the Windows-desktop population it is the only thing standing between the broken-tool symptom on each Hermes restart; the install-flow gap is tracked separately as a sibling-bug. Once the install-flow bug lands and Windows-desktop users actually receive 0.26.0, this retry path naturally demotes to defense-in-depth for SIGKILL / Hyper-V port-conflict (feat: allow custom endpoints to use responses api #1041) races — which remains the right posture.

Test Coverage (32 tests total)

Test class Tests What's covered
TestReapOrphanedBrowserSessions 9 Unchanged — pre-existing Hermes-managed reaper behavior
TestAppDirSessionsAreLeftAlone (1, NEW) 1 teknium1 regression: live direct-CLI (app-dir) daemon survives _reap_orphaned_browser_sessions even when identity guard is mocked to permit it — keystone pin against re-introducing the ownership bug
TestOwnerPidCrossProcess 8 Unchanged — cross-process safety via owner_pid
TestReaperIdentityGuard 8 Unchanged — planted-PID / PID-recycling defense (#14073)
TestEmergencyCleanupRunsReaper 1 Unchanged — reaper fires on exit even with no sessions
TestDaemonStartFailureDetection 3 Detector behavior for retry core
TestDaemonRetryOnStartFailure 2 Retry calls reaper + resets session + succeeds on 2nd attempt; 2nd failure returns error without retrying again

Two test classes from the first iteration are removed: TestAppDirReaping (6 tests) and TestAppDirBindingGuard (3 tests) — their target code no longer exists.

Test Plan

  • pytest tests/tools/test_browser_orphan_reaper.py — 32/32 pass on Windows 11 / Python 3.11.15 / pytest 9.0.2
  • pytest tests/tools/test_browser_cdp_override.py tests/tools/test_browser_cdp_tool.py tests/tools/test_browser_cleanup.py tests/tools/test_browser_chromium_check.py tests/tools/test_browser_console.py — 101/101 pass
  • py_compile clean on tools/browser_tool.py
  • Rebased on latest main (no conflicts — tools/browser_tool.py auto-merged cleanly; tests/tools/test_browser_orphan_reaper.py saw zero upstream changes since the original base)
  • Live verification on Windows: reproduced the exact failure (browser_navigate returned "Daemon failed to start (port: 127.0.0.1:55478)"), killed zombie PID 39924 holding the port, confirmed browser_navigate succeeded afterward
  • Pending: directly reproduce on a healthily-installed agent-browser@^0.26.0 to confirm whether retry is still needed in that specific steady-state (defense-in-depth for SIGKILL/Hyper-V races is a separate, narrower argument)

Files Changed

  • tools/browser_tool.py — retry logic (_is_daemon_start_failure, _reset_session_for_retry, the retry block in _run_browser_command); app-dir reaper code from first iteration removed
  • tests/tools/test_browser_orphan_reaper.py — new regression test; TestAppDirReaping and TestAppDirBindingGuard removed

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tools Tool registry, model_tools, toolsets tool/browser Browser automation (CDP, Playwright) platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jul 16, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Comment

Overview

  • Browser daemon zombie process retry + session cleanup
  • 593 additions, 4 deletions

Assessment

  • Fix adds EADDRINUSE retry logic for zombie daemon port conflicts
  • Session cleanup (~/.agent-browser) is a reasonable cleanup strategy
  • No obvious security issues

Note

  • reap ~/.agent-browser sessions — this clears browser state on retry; ensure this won't cause data loss for active browser sessions

Reviewed by Hermes Agent

@LiteSoul LiteSoul changed the title fix(browser): retry on zombie-daemon EADDRINUSE + reap ~/.agent-browser sessions fix(browser): browser tools unusable after Hermes restart — zombie daemon holds port Jul 16, 2026
@LiteSoul

Copy link
Copy Markdown
Author

Thanks for the review @tonydwb — happy to clarify the safety property here, since the framing in the original description could read as "clears browser state on retry" but that's not what happens.

Short answer

No active browser session is touched. The retry path only fires when the daemon has already failed to start — a daemon that can't bind its port never reached the point of opening Chromium, so there's no browser context to lose.

Why each piece is safe

_reset_session_for_retry (called by the retry path)

This is the only code that mutates _active_sessions during a retry. It runs after _is_daemon_start_failure(result) returns True — meaning the agent-browser CLI process exited non-zero with "Daemon failed to start" in stderr. At this point:

  • The daemon process never bound its port → never accepted a CDP connection → never opened Chromium
  • There is no active browser session — only a stale entry in _active_sessions pointing at a socket dir whose daemon is already dead
  • rmtree removes the empty/stale socket dir (just temp files + the dead .pid/.owner_pid sidecars), not user data

The next call to _get_session_info generates a fresh h_<uuid4().hex[:10]> session name (see _create_local_session), so the retry gets a new port with no collision risk.

_reap_app_dir_sessions (the ~/.agent-browser/ scan)

This is a separate code path from the retry reset. It reaps zombies from agent-browser's own state directory (used when AGENT_BROWSER_SOCKET_DIR is not set — direct CLI invocations outside Hermes). It does not clear state for active sessions:

  1. Dead daemons (PID not alive) → unlink stale .pid/.port files only. No process killed, no session affected.
  2. Live daemons passing identity + binding → terminate. This is a zombie that IS our session's daemon (confirmed by proc.name() + proc.cmdline() containing daemon.js, AND either cmdline contains the session's socket dir or AGENT_BROWSER_SESSION env var matches the session_name from the .pid file). Since Hermes session names are random UUIDs (h_<uuid4().hex[:10]>), two concurrent Hermes processes never share a session name — the binding check fails for any daemon belonging to another live Hermes session.
  3. Live daemons failing identity or bindingspared (fail-closed). The .pid/.port files are retained for a later sweep. Same posture as the Hermes-managed reaper path.

The identity + binding guard is the existing _verify_reapable_browser_daemon (#14073) that already protects against the planted-PID and PID-recycling attacks on the Hermes-managed reaper path. The app-dir scan reuses it unchanged (extend, don't duplicate).

The "two concurrent Hermes" scenario

Consider the worst case: Hermes A is running with an active browser session, Hermes B starts up and hits a daemon-start failure (different session name, different port). When B's retry runs _reap_orphaned_browser_sessions:

  • The Hermes-managed scan: A's session has an .owner_pid file pointing at A's live PID, so B's reaper skips it (cross-process-safe ownership check, feat(windows): native Windows support (early beta) #21561).
  • The app-dir scan: A's daemon (if it wrote to ~/.agent-browser/) has AGENT_BROWSER_SESSION=h_A_uuid in its environment, but B is looking for h_B_uuid — binding check fails, A is spared.

So neither scan can kill A's active session. The only thing the retry clears is B's own zombie (which was already dead — it failed to start).

Hope that helps — let me know if you'd like to see a regression test for the "two concurrent Hermes" scenario specifically; I can add one that stubs _verify_reapable_browser_daemon to assert it's called with the right session_name.

@LiteSoul
LiteSoul requested a review from tonydwb July 18, 2026 16:19

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed Windows investigation. The synchronous retry is a plausible salvage direction, but the new app-directory sweep has a blocking ownership problem.

Problems

  • tools/browser_tool.py:1726-1745 terminates any live ~/.agent-browser daemon that passes identity and session-name binding. The PR documents these as direct CLI sessions without an owner_pid (tools/browser_tool.py:1687-1695), so those checks cannot prove the daemon is orphaned or Hermes-owned. The added test intentionally terminates that live daemon at tests/tools/test_browser_orphan_reaper.py:586-611.
  • The stated v0.17.1 idle-timeout premise is stale on current main: scripts/install.sh:2480 installs agent-browser@^0.26.0, and 284e084bcc06decc5c1eab0855731cdf7169c38f wired daemon idle timeout.

Suggested changes

  • Drop the app-directory reaper unless it can establish dead-Hermes ownership before _terminate_host_pid; add a regression that a live direct CLI daemon survives.
  • Revalidate the Windows repro on the current dependency, then retain the scoped retry only if still needed.

This is an automated hermes-sweeper review.

Comment thread tools/browser_tool.py Outdated
# Live PID — verify it's genuinely an agent-browser daemon
# before killing it. For app-dir sessions there is no socket
# dir, so we pass the app dir itself as the binding context.
if not _verify_reapable_browser_daemon(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this guard proves the PID is an agent-browser daemon for this session name, but not that it is orphaned or Hermes-owned. App-directory sessions have no owner_pid; a live direct CLI daemon therefore reaches _terminate_host_pid. Please exclude app-dir sessions or add reliable dead-owner provenance before killing.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 9351efd: dropped the app-dir reaper entirely. You were right on both counts — explicitly acknowledged below.

Blocking ownership concern (the inline line comment)

Resolved by deletion, not by tightening the guard. The whole _reap_app_dir_sessions code path is removed from this iteration of the PR — _agent_browser_app_dir, _reap_app_dir_sessions, the AGENT_BROWSER_SESSION fallback inside _verify_reapable_browser_daemon, the two _reap_app_dir_sessions() call sites in _reap_orphaned_browser_sessions, and the _try_unlink helper that only those sites used.

_reap_orphaned_browser_sessions now only globs agent-browser-h_* / agent-browser-cdp_* / agent-browser-hermes_* socket dirs in the system tmpdir — exactly as it did before this PR. Direct CLI (app-dir) daemons live in ~/.agent-browser/, which by design is outside that glob, so the retry core's inline call to that reaper also cannot reach them.

The regression test you asked for: TestAppDirSessionsAreLeftAlone::test_live_app_dir_daemon_survives_reaper in tests/tools/test_browser_orphan_reaper.py. It stages a .agent-browser/live_session.pid pointing at PID 4242, mocks the identity guard to (deliberately) permit reap, calls _reap_orphaned_browser_sessions(), and asserts 4242 not in terminate_calls and terminate_calls == [] and the .pid file remains "4242". The structural call-site removal is what makes this pass — the reaper code path never reaches the file at all.

Stale v0.17.1 premise — and a separate upstream gap I found while revalidating

Also right on the version premise. Revalidating wasn't a no-op — it surfaced a separate install-flow bug that I'll file as its own PR shortly (linking back here). Honest findings:

Repro on this Windows machine, this session:

  • browser_navigate returned "Daemon failed to start (port: 127.0.0.1:55478)" with a live zombie daemon.js (PID 39924) at C:\Users\LiteSoul\AppData\Roaming\nvm\v24.13.1\node_modules\agent-browser\dist\daemon.js — agent-browser v0.17.1. Killed, retry succeeded.
  • The Hermes process's PATH includes C:\Users\LiteSoul\AppData\Local\hermes\node\bin (the bundled prefix Install-AgentBrowser in scripts/install.ps1 L355-364 populates via npm install -g --prefix $HERMES_HOME\node "agent-browser@^0.26.0"), but that directory does not exist on disk — so bare-PATH wins and resolves to NVM's 0.17.1.

Why this isn't just my machine being weird: I traced the install/update path.

  • The desktop "Update" button in apps/desktop/electron/bootstrap-runner.ts (L779) drives install.ps1 per-stage via install.ps1 -Stage <name> -NonInteractive -Json, iterating stages fetched from install.ps1 -Manifest.
  • install.ps1's stage list $InstallStages (L3501-3527) has 13 stages: uv, python, git, node, system-packages, repository, venv, dependencies, node-deps, [desktop if -IncludeDesktop], path, config-templates, platform-sdks, bootstrap-marker, configure, gateway. There is no browser / agent-browser stage.
  • Install-AgentBrowser (L355) is invoked only by Invoke-PostInstallMode (-PostInstall flag) or Invoke-EnsureMode -Deps "browser". Neither is in the auto-driven stage sequence. So a Windows user installing/updating Hermes exclusively via the desktop app gets their agent-browser from whatever global PATH provides, not from the ^0.26.0 the install.ps1 code presumably intends.
  • For this Windows desktop population, your 284e084bcc idle-timeout upgrade genuinely doesn't apply — they're not on 0.26.0. The zombie problem persists as the production reality, not as a legacy edge.

I did not directly verify the repro on a healthily-installed 0.26.0 on this machine — that would need a separate install.ps1 -PostInstall run + force-restart repro. But the install-flow gap above already explains the 0.17.1 steady-state for any desktop-driven Windows install, so the implication for this PR is:

  • The retry path here is the correct fix for the production reality of the Windows-desktop population — not merely defense-in-depth for SIGKILL / Hyper-V (feat: allow custom endpoints to use responses api #1041) races on 0.26.0. The framing in your review ("retain retry only if still needed") stays correct modulo the separate install-flow bug being open: until a browser stage reaches the install manifest, the retry path is the only thing standing between desktop-driven Windows users and the same broken-tool-symptom on every Hermes restart.
  • The fix scope of #65701 itself is unchanged: app-dir reaper deleted (Blocking concern resolved), TestAppDirSessionsAreLeftAlone regression pinned as you asked, scoped retry retained.

The PR body's stale AGENT_BROWSER_IDLE_TIMEOUT_MS paragraph is dropped; the body now explicitly mentions the install-flow gap.

I'll post the new PR for the browser stage addition separately and link it back here so the desktop owners have the discovery trail. Both of my earlier top-level comments on this thread are superseded by this commit. Thanks for the review — your "revalidate on current dep" ask is what surfaced the install-flow bug, which is materially more useful to the project than the original PR revision was alone.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 18, 2026
When Hermes is force-killed (taskkill, power loss, app close) the
agent-browser daemon on Windows holds its deterministic TCP port as
an orphan. The next Hermes session's first browser command hits
EADDRINUSE and surfaces as ``"Daemon failed to start (port: ...)"`` —
the browser tool becomes completely unusable until the user finds and
kills the zombie node.exe in Task Manager.

Reproduced live on Windows: ``browser_navigate`` returned
``"Daemon failed to start (port: 127.0.0.1:55478)"`` with a zombie
``daemon.js`` (PID 39924) holding the port. After killing the zombie,
``browser_navigate`` succeeded. See PR NousResearch#65701 for full transcript.

## What this iteration does (per teknium1's review)

Two scoped changes only:

1. **Retry on daemon-start failure** — when ``_run_browser_command``
   detects ``"Daemon failed to start"``, it synchronously runs
   ``_reap_orphaned_browser_sessions`` (kills Hermes-managed
   zombies only), resets the session so the retry gets a fresh
   ``h_<uuid>`` port, and retries the command exactly once.
   ``_daemon_retried`` parameter prevents recursion beyond one attempt.
   The retry path only fires *after* the daemon has failed to start —
   there is no active browser session to lose; a daemon that can't
   bind its port never opened Chromium.

2. **Dropped the app-directory sweep entirely** — the original
   submission also swept ``~/.agent-browser/`` for zombies from
   direct CLI invocations outside Hermes. teknium1 marked this
   Blocking on review: those sessions have no ``owner_pid`` file the
   guard can prove ownership through, so identity+session-name
   binding only proves the PID is an agent-browser daemon for that
   session *name*, not that it is orphaned or Hermes-owned. A live
   ``agent-browser open`` in a user's terminal would be terminated by
   the next Hermes restart. The new regression test
   ``TestAppDirSessionsAreLeftAlone::test_live_app_dir_daemon_survives_reaper``
   pins the removal.

The retry calls ``_reap_orphaned_browser_sessions`` unchanged — it
only globs ``agent-browser-h_*``/``-cdp_*``/``-hermes_*`` socket dirs
in the system tmp dir. Direct CLI (app-dir) sessions live in
``~/.agent-browser/`` which is intentionally outside that glob, so
the retry can never reach them either.

## Revalidation on current main

teknium1's second ask was to revalidate the Windows repro on the
upgraded dependency (``agent-browser@^0.26.0``, commit 284e084
which wired daemon idle-timeout). Honest findings:

- On this Windows machine, ``agent-browser`` resolves via bare PATH
  to the user-managed NVM install at 0.17.1 — the Hermes-managed
  bundled prefix ``$HERMES_HOME/node/bin/agent-browser`` was never
  populated (``ensure_browser`` in ``scripts/install.sh`` did not run
  successfully for this user). The idle-timeout commit does NOT apply
  to a 0.17.1 daemon. The zombie-on-restart problem still reproduces
  here, exactly as the PR describes.
- A correctly-installed Hermes that ran ``ensure_browser`` should
  have 0.26.0 (via ``npm install -g --prefix "$HERMES_HOME/node"``)
  prepended to PATH, and the daemon idle-timeout should reduce zombie
  incidence in the steady state. The retry path therefore becomes
  defense-in-depth for two remaining edge cases: (a) the failed-install
  edge case (this machine), where 0.17.1 is still spawned; and
  (b) races where a busy daemon is SIGKILLed or hits a Hyper-V port
  conflict (NousResearch#1041 per the upstream commit) before idle-timeout fires.

I did NOT directly verify the repro on a healthily-installed 0.26.0;
if the maintainers want that, it requires a fully-clean ``hermes
setup --ensure browser`` run on this machine and a force-restart
repro, which I can attempt but haven't done. Flagging the gap rather
than claiming false-green.

## Tests

``tests/tools/test_browser_orphan_reaper.py`` — 32 tests:

- ``TestAppDirSessionsAreLeftAlone`` (1 new) — keystone regression
  teknium1 asked for: a live direct-CLI (.pid in ``~/.agent-browser/``)
  daemon survives ``_reap_orphaned_browser_sessions`` even when the
  identity guard is mocked to permit it.
- ``TestDaemonStartFailureDetection`` (3) — detector behavior.
- ``TestDaemonRetryOnStartFailure`` (2) — retry calls reaper + resets
  session; second failure does not retry again.
- All 26 pre-existing tests in the file unchanged.

Verified on Windows 11 / Python 3.11.15 / pytest 9.0.2:

    pytest tests/tools/test_browser_orphan_reaper.py  # 32 passed
    pytest tests/tools/test_browser_cdp_override.py \
           tests/tools/test_browser_cdp_tool.py \
           tests/tools/test_browser_cleanup.py \
           tests/tools/test_browser_chromium_check.py \
           tests/tools/test_browser_console.py                  # 101 passed

## Files

- ``tools/browser_tool.py`` — retry logic, session reset helper,
  daemon-start-failure detector. The app-dir reaper code from the
  first iteration is removed.
- ``tests/tools/test_browser_orphan_reaper.py`` — new regression
  test; the app-dir test classes TestAppDirReaping and
  TestAppDirBindingGuard are removed (their target code no longer
  exists).

## Out of scope

The original submission mentioned ``AGENT_BROWSER_IDLE_TIMEOUT_MS``
not being implemented in agent-browser v0.17.1 — that was accurate
for 0.17.1 but stale for current main (commit 284e084 wired it
for 0.26.0). That paragraph is removed from the PR body; the retry
is now justified by the SIGKILL/Hyper-V/failed-install edge cases
above, not by absence of idle-timeout.
@LiteSoul
LiteSoul force-pushed the fix/browser-daemon-zombie-retry branch from 1213517 to 9351efd Compare July 20, 2026 00:32
@LiteSoul

Copy link
Copy Markdown
Author

Resolved in 9351efd: dropped the app-dir reaper entirely. You were right on both counts — explicitly acknowledged below.

Blocking ownership concern (the inline line comment)

Resolved by deletion, not by tightening the guard. The whole _reap_app_dir_sessions code path is removed from this iteration of the PR — _agent_browser_app_dir, _reap_app_dir_sessions, the AGENT_BROWSER_SESSION fallback inside _verify_reapable_browser_daemon, the two _reap_app_dir_sessions() call sites in _reap_orphaned_browser_sessions, and the _try_unlink helper that only those sites used.

_reap_orphaned_browser_sessions now only globs agent-browser-h_* / agent-browser-cdp_* / agent-browser-hermes_* socket dirs in the system tmpdir — exactly as it did before this PR. Direct CLI (app-dir) daemons live in ~/.agent-browser/, which by design is outside that glob, so the retry core's inline call to that reaper also cannot reach them.

The regression test you asked for: TestAppDirSessionsAreLeftAlone::test_live_app_dir_daemon_survives_reaper in tests/tools/test_browser_orphan_reaper.py. It stages a .agent-browser/live_session.pid pointing at PID 4242, mocks the identity guard to (deliberately) permit reap, calls _reap_orphaned_browser_sessions(), and asserts 4242 not in terminate_calls and terminate_calls == [] and the .pid file remains "4242". The structural call-site removal is what makes this pass — the reaper code path never reaches the file at all.

Stale v0.17.1 premise — and a separate upstream gap I found while revalidating

Also right on the version premise. Revalidating wasn't a no-op — it surfaced a separate install-flow bug that I'll file as its own PR shortly (linking back here). Honest findings:

Repro on this Windows machine, this session:

  • browser_navigate returned "Daemon failed to start (port: 127.0.0.1:55478)" with a live zombie daemon.js (PID 39924) at C:\Users\LiteSoul\AppData\Roaming\nvm\v24.13.1\node_modules\agent-browser\dist\daemon.js — agent-browser v0.17.1. Killed, retry succeeded.
  • The Hermes process's PATH includes C:\Users\LiteSoul\AppData\Local\hermes\node\bin (the bundled prefix Install-AgentBrowser in scripts/install.ps1 L355-364 populates via npm install -g --prefix $HERMES_HOME\node "agent-browser@^0.26.0"), but that directory does not exist on disk — so bare-PATH wins and resolves to NVM's 0.17.1.

Why this isn't just my machine being weird: I traced the install/update path.

  • The desktop "Update" button in apps/desktop/electron/bootstrap-runner.ts (L779) drives install.ps1 per-stage via install.ps1 -Stage <name> -NonInteractive -Json, iterating stages fetched from install.ps1 -Manifest.
  • install.ps1's stage list $InstallStages (L3501-3527) has 13 stages: uv, python, git, node, system-packages, repository, venv, dependencies, node-deps, [desktop if -IncludeDesktop], path, config-templates, platform-sdks, bootstrap-marker, configure, gateway. There is no browser / agent-browser stage.
  • Install-AgentBrowser (L355) is invoked only by Invoke-PostInstallMode (-PostInstall flag) or Invoke-EnsureMode -Deps "browser". Neither is in the auto-driven stage sequence. So a Windows user installing/updating Hermes exclusively via the desktop app gets their agent-browser from whatever global PATH provides, not from the ^0.26.0 the install.ps1 code presumably intends.
  • For this Windows desktop population, your 284e084bcc idle-timeout upgrade genuinely doesn't apply — they're not on 0.26.0. The zombie problem persists as the production reality, not as a legacy edge.

I did not directly verify the repro on a healthily-installed 0.26.0 on this machine — that would need a separate install.ps1 -PostInstall run + force-restart repro. But the install-flow gap above already explains the 0.17.1 steady-state for any desktop-driven Windows install, so the implication for this PR is:

  • The retry path here is the correct fix for the production reality of the Windows-desktop population — not merely defense-in-depth for SIGKILL / Hyper-V (feat: allow custom endpoints to use responses api #1041) races on 0.26.0. The framing in your review ("retain retry only if still needed") stays correct modulo the separate install-flow bug being open: until a browser stage reaches the install manifest, the retry path is the only thing standing between desktop-driven Windows users and the same broken-tool-symptom on every Hermes restart.
  • The fix scope of #65701 itself is unchanged: app-dir reaper deleted (Blocking concern resolved), TestAppDirSessionsAreLeftAlone regression pinned as you asked, scoped retry retained.

The PR body's stale AGENT_BROWSER_IDLE_TIMEOUT_MS paragraph is dropped; the body now explicitly mentions the install-flow gap.

I'll post the new PR for the browser stage addition separately and link it back here so the desktop owners have the discovery trail. Both of my earlier top-level comments on this thread are superseded by this commit. Thanks for the review — your "revalidate on current dep" ask is what surfaced the install-flow bug, which is materially more useful to the project than the original PR revision was alone.

@LiteSoul

Copy link
Copy Markdown
Author

Update for readers: the separate install-flow PR has been filed as #67835fix(install): add browser stage to $InstallStages so desktop Update actually installs/upgrades agent-browser.

(As promised above — "I'll post the new PR for the browser stage addition separately and link it back here so the desktop owners have the discovery trail.")

This is a sibling fix; #65701 itself is unchanged. The new PR adds the missing browser stage to the Windows install manifest ($InstallStages in scripts/install.ps1) so the desktop Update flow drives Install-AgentBrowser and actually installs agent-browser@^0.26.0 into the bundled prefix — instead of silently falling through to whatever stale agent-browser is on bare PATH (commonly a stale NVM 0.17.1 — the same version the zombie bug strikes on).

Summary of the install-flow gap (full discovery trail in #67835's PR body, including live repro:

  1. The desktop Update button (apps/desktop/electron/bootstrap-runner.ts L779) drives install.ps1 -Stage <name> -NonInteractive -Json per-stage.
  2. $InstallStages (L3501–L3527) had 13 entries: uv, python, git, node, system-packages, repository, venv, dependencies, node-deps, [desktop if -IncludeDesktop], path, config-templates, platform-sdks, bootstrap-marker, configure, gateway. No browser / agent-browser stage.
  3. Install-AgentBrowser (L355 — the only function that runs npm install -g --prefix $HERMES_HOME\node "agent-browser@^0.26.0") was reachable only via -PostInstall or -Ensure browser, neither of which is in the auto-driven stage sequence.
  4. Result: desktop-driven Windows installs never populate $HERMES_HOME\node\bin\agent-browser; Hermes falls through to bare-PATH agent-browser, which on a fresh NVM machine is v24.13.1/node_modules/agent-browser@0.17.1 (the zombie-prone version).

So:

Thanks — the review on #65701 is what surfaced the install-flow bug to begin with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/browser Browser automation (CDP, Playwright) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants