Skip to content

fix(browser): cross-platform reaper for Chromium orphaned by abnormal daemon death - #70002

Open
sablea wants to merge 1 commit into
NousResearch:mainfrom
sablea:fix/browser-orphan-chrome-reap
Open

fix(browser): cross-platform reaper for Chromium orphaned by abnormal daemon death#70002
sablea wants to merge 1 commit into
NousResearch:mainfrom
sablea:fix/browser-orphan-chrome-reap

Conversation

@sablea

@sablea sablea commented Jul 23, 2026

Copy link
Copy Markdown

What does this PR do?

Reaps orphaned Chromium processes that accumulate when the agent-browser daemon dies abnormally — OOM kill / crash / SIGKILL, the common failure mode on memory-starved hosts. A single psutil-based reaper that works on both POSIX and Windows.

The leak

agent-browser launches Chromium with a --user-data-dir=<tmp>/agent-browser-chrome-* profile. When the daemon is killed without a clean shutdown, the browser is cut loose and keeps running — on POSIX it reparents to init, on Windows its PPID is invalidated — and every existing cleanup path then goes blind:

  • _cleanup_single_browser_session kills via the daemon PID's child tree; _terminate_host_pid returns silently once that PID is dead, and the socket dir (the only pid breadcrumb) is then removed.
  • _reap_orphaned_browser_sessions only globs daemon socket dirs (agent-browser-h_*/cdp_*/hermes_*), never the browser's agent-browser-chrome-* user-data dir.

After that nothing can find the browser again, so it runs until reboot.

Motivating case: on a 2 GB RAM Ubuntu VPS running a long-lived gateway, 7 Chromium processes survived 6 days (daemon dead, socket dirs gone, agent-browser-chrome-* still under /tmp) until killed by hand. On a box that small the orphans themselves make the next OOM kill — and the next orphan — more likely.

Relationship to #43577

#43577 already tackles this exact leak and reaps orphans correctly on Windows. It's currently scoped to Windows (if os.name != "nt": return 0), on the reasoning that POSIX's SIGTERM cascade handles the Unix side.

While testing on Linux I found the POSIX side isn't actually covered: the cascade only fires while a live process walks the tree, so a daemon that's killed abnormally (OOM / SIGKILL) leaves its whole Chromium tree behind — POSIX doesn't signal a process's children when it dies. Repro: kill -9 the daemon after a successful navigate and the full 12-process tree survives session cleanup and the inactivity reaper indefinitely (logs below).

Rather than add a second platform-specific reaper, this PR detects orphans cross-platform via psutil (already a hard dependency) and reaps through the existing ProcessRegistry._terminate_host_pid (taskkill /T /F on Windows, psutil tree-walk on POSIX), so one code path needs no per-OS branch:

step #43577 (Windows) this PR
enumerate wmic psutil.process_iter
command line parse WMIC CSV structured proc.cmdline()
orphan check ParentProcessId proc.parent() (dead/None on all OSes)
kill new taskkill code existing _terminate_host_pid
cadence startup only every cleanup tick + startup/atexit

Scope note / verification: the POSIX path is verified live (repro below). The Windows path is by construction — it reuses psutil plus the existing taskkill primitive — but I haven't verified it on a real Windows host. So this could either (a) supersede #43577 as a unified cross-platform reaper, or (b) be trimmed to POSIX-only to land alongside #43577's tested Windows path. Happy to go whichever way the maintainers and @bighamx prefer — glad to coordinate.

Related Issue

Fixes #32047. Also covers the macOS case in #17388 (closed not-planned; process-tree cleanup exists, but no call chain reaches a browser whose daemon PID is already gone).

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • tools/browser_tool.py: add _find_orphaned_chrome_processes() (cross-platform psutil scan matching the agent-browser-chrome-* user-data-dir signature; skips --type= helpers and browsers whose parent is a live agent-browser daemon) and _reap_orphaned_chrome_processes() (tree-kill via ProcessRegistry._terminate_host_pid + stale profile-dir removal). Wire the sweep into the cleanup-thread tick and both branches of _reap_orphaned_browser_sessions (normal path + empty-socket-dir early return).
  • tests/tools/test_browser_cleanup.py: 4 regression tests — orphan reaped (+ profile dir removed), daemon-owned browser left alone, --type= helpers / foreign Chrome installs skipped, sweep not short-circuited by the empty-socket-dir return.

How to Test

  1. On current main (local backend): browser_navigate to a page, then kill -9 the agent-browser daemon. The full Chromium tree (12+ processes) survives session cleanup and the inactivity reaper forever; the socket dir is removed so nothing can find it again.
  2. With this PR: the next cleanup tick tree-kills the orphaned Chromium and removes its agent-browser-chrome-* user-data dir.
  3. pytest tests/tools/test_browser_cleanup.py -q → 10 passed.

Checklist

Code

  • I've read the Contributing Guide
  • Commit messages follow Conventional Commits
  • Searched existing PRs for duplicates (found fix(browser): reap orphaned Chrome processes after daemon exit on Windows #43577 — see above)
  • PR contains only changes related to this fix
  • I've run pytest tests/ -q and all tests pass — the full suite is red on main independently of this PR (e.g. tip commit fix(desktop): place steer messages before redirected replies #69739 shows failing CI), in unrelated modules. This PR's own tests pass (pytest tests/tools/test_browser_cleanup.py -q → 10 passed) and no browser/chrome/cleanup test is in the suite's failure set.
  • I've added tests for my changes
  • Tested on my platform: Ubuntu 24.04 (2 GB VPS + container repro)

Documentation & Housekeeping

  • Docstrings updated — or N/A
  • cli-config.yaml.example — N/A (no config keys)
  • CONTRIBUTING.md/AGENTS.md — N/A
  • Cross-platform impact considered (Windows taskkill + POSIX psutil)
  • Tool descriptions/schemas — N/A

Screenshots / Logs

Without the fix (daemon SIGKILLed at t≈2s; reaper ran at t≈62s):

=== SAMPLE 8 @ t= 82.4s ===
  active_sessions   : []          <- session cleanup "succeeded"
  socket dirs       : []          <- pid breadcrumb destroyed
  daemon pids       : []
  chrome pids       : [15990, 15992, 15995, ... 16100]  <- leaked forever

With the fix (same scenario):

FINAL VERDICT: chrome pids still alive after reaper ran: []

…platform)

When the agent-browser daemon dies without shutting Chromium down (OOM
kill / crash / SIGKILL — the common failure mode on memory-starved
hosts), the browser is cut loose and keeps running: on POSIX it
reparents to init, on Windows its PPID is invalidated. Reproduced live
on Linux — SIGKILL the daemon after a successful navigate and the full
12-process Chromium tree survives session cleanup indefinitely.

Every existing cleanup path goes blind in that state:

* _cleanup_single_browser_session kills via the daemon PID's child
  tree — _terminate_host_pid returns silently when that PID is already
  dead — then removes the socket dir, destroying the only pid
  breadcrumb.
* _reap_orphaned_browser_sessions only globs daemon socket dirs
  (agent-browser-h_*/cdp_*/hermes_*), never the browser's
  agent-browser-chrome-* user-data dir.

On a long-running gateway this accumulates orphaned Chromium processes:
7 chrome processes surviving 6 days on a 2 GB Ubuntu VPS (the NousResearch#32047
symptom; also the macOS case in NousResearch#17388, closed not-planned).

Relationship to NousResearch#43577: that open PR reaps the same orphans but is gated
Windows-only (`if os.name != "nt": return 0`) on the assumption that
"POSIX SIGTERM cascades reliably." The live repro above disproves that
assumption — the cascade needs a live process to walk the tree, which an
abnormally-killed daemon cannot do (POSIX does not signal children when
a parent dies). This change detects orphans cross-platform via psutil
(already a hard dependency) instead of WMIC, and reaps via the existing
ProcessRegistry._terminate_host_pid (taskkill /T /F on Windows, psutil
tree-walk on POSIX), so a single reaper needs no per-OS branch. The
POSIX path is verified live; the Windows path is by construction only
(cross-platform primitives + the existing taskkill path) and not yet
verified on Windows.

Fix: sweep for main Chromium processes whose cmdline carries an
agent-browser-chrome-* --user-data-dir but whose parent is no longer a
live agent-browser daemon; tree-kill them and drop the stale profile
dir. The sweep runs on every cleanup-thread tick and from the
startup/atexit orphan reap — including when no socket dirs remain, which
is exactly the orphan's state — so a long session self-heals instead of
leaking until restart.

Daemon-owned browsers and foreign Chrome installs are untouched
(covered by unit tests).

Refs: NousResearch#17388, NousResearch#32047, NousResearch#43577
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets tool/browser Browser automation (CDP, Playwright) labels Jul 23, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #62615, #60152, #64383, and #43577: this patch uses a cross-platform cmdline/parent-based sweep after abnormal daemon death, while the existing proposals use profile-directory, socket-session, ownership, or Windows-specific mechanisms. Maintainers should select a canonical orphan-reaper design.

@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 tracing the already-dead-daemon case; current main still returns without any Chromium scan when no socket directory remains (tools/browser_tool.py:1787-1788), so this addresses a real gap.

Problems

  • tools/browser_tool.py:1856-1859 treats any process with an agent-browser-chrome-* argument and a non-agent-browser immediate parent as garbage. It does not verify process identity or bind the process/profile to a known Hermes daemon. Current main explicitly requires identity plus session binding before tree-killing from predictable temporary-path state (tools/browser_tool.py:1672-1751) to avoid arbitrary-process DoS.
  • The new tests cover only null and agent-browser-named parents. They do not cover a matching profile argument without verified Hermes ownership.

Suggested changes

  • Preserve or derive durable session provenance and fail closed until a candidate is tied to a known dead agent-browser daemon; then add a negative ownership test before tree-killing.
  • Add platform integration coverage for the parent topology rather than relying only on mocked parent names.

Automated hermes-sweeper review.

Comment thread tools/browser_tool.py
parent = proc.parent()
if parent is not None and "agent-browser" in (parent.name() or ""):
continue # daemon alive and owning this browser — not an orphan
orphans.append((proc, user_data_dir))

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.

This appends any process with the matching argument unless its immediate parent name contains agent-browser. Current main requires both process identity and binding to a specific session before a tree kill (_verify_reapable_browser_daemon), because predictable temporary-path state otherwise permits same-user arbitrary-process DoS. Please require durable evidence that this profile/process belongs to a known dead Hermes daemon, and add a negative test for a matching command line without that association.

@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:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
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 P3 Low — cosmetic, nice to have 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.

[Bug] agent-browser leaves 200+ orphaned Chrome processes after task completion (Windows)

3 participants