Skip to content

fix(terminal): agent-browser daemon leak — sessions clean up their own browser (v0.227.1)#377

Merged
vikasprogrammer merged 1 commit into
mainfrom
feat/reap-orphan-browsers
Jul 17, 2026
Merged

fix(terminal): agent-browser daemon leak — sessions clean up their own browser (v0.227.1)#377
vikasprogrammer merged 1 commit into
mainfrom
feat/reap-orphan-browsers

Conversation

@vikasprogrammer

Copy link
Copy Markdown
Owner

The leak

The agent-browser skill (global skill, used by qa/docs-bot/researcher/… agents) starts a persistent headless-Chrome daemon that double-forks (setsid) out of the session's tmux process group. So tmux kill-session at teardown never reaches it — it reparents to PID 1 and survives, burning CPU (its --use-angle=swiftshader-webgl software renderer spins helpers at ~100%) and RAM, for days, until reboot or OOM.

Observed live on the instawp box: 6 orphaned daemons (the qa agent alone leaked 4), up to ~4 days old, driving load average to ~25. Reaping them dropped it to ~1 — the CPU was the bigger hit, not just memory. This is what surfaced as "lots of crashes" (OOM-killing claude/chrome mid-run).

Fix — root cause, not a GC

Each session now owns its browser lifecycle:

  • src/terminal.ts (sessionEnv) exports:
    • AGENT_BROWSER_NAMESPACE=aos-<session-id> — isolates each session's daemon, socket, and saved state, so cleanup targets exactly that session's browser.
    • AGENT_BROWSER_IDLE_TIMEOUT_MS (default 300000, operator-overridable) — the vendor's own idle self-shutdown, kept only as the last-resort net for the one exit a trap can't catch: an un-trappable SIGKILL (OOM).
  • terminal/claude-launch.sh adds an exit trap (EXIT/HUP/TERM/INT) that runs agent-browser close --all (the vendor's clean shutdown). So the session tears its browser down on any trappable exit — including the SIGHUP tmux kill-session sends. Namespace-scoped, so it can never touch another live session's browser. The launcher already stays claude's parent (never execs it — see the existing NOTE at the interactive lane), so the trap is live across interactive, unattended, and resident lanes.

Why not a reaper/GC: a process-scanning sweep can't reliably attribute a daemon to a session (agent-browser spawns it with a sanitised env — SESSION is inconsistently present), and ppid==1 is true for live daemons too. Fixing teardown at the source avoids both traps.

Upstream context

Acknowledged gap in vercel-labs/agent-browser (#885 idle-timeout, #1334 helpers not cleaned on session kill, #1371 orphaned helpers spin high CPU, #1401 failed-launch zombies): the daemon self-shuts-down on idle, but nothing cleans up after abnormal termination. Agent OS now does.

Verification

  • AGENT_BROWSER_IDLE_TIMEOUT_MS / AGENT_BROWSER_NAMESPACE env propagate to the daemon (grandchild of claude).
  • agent-browser close --all cleanly kills a namespaced daemon (verified on the box).
  • The exit trap fires cleanup on SIGHUP (what tmux kill-session sends), SIGTERM, and clean exit (verified on the box with process-group signalling).
  • npm run typecheck ✓ · npm run build ✓ · npm run test:governance68/68 ✓ · bash -n claude-launch.sh

Deploy note

Takes effect for new sessions after build + restart (existing running sessions keep the old launch env until they cycle). The interim stopgap cron on jump-server can be retired once this is deployed.

🤖 Generated with Claude Code

…n browser (v0.226.1)

The agent-browser skill starts a persistent headless-Chrome daemon that double-forks
(setsid) out of the session's tmux process group, so `tmux kill-session` at teardown
never reached it — it survived for days, burning CPU (its swiftshader software renderer
spins helpers at ~100%) + RAM until reboot/OOM (6 orphaned daemons drove the instawp
box's load average to ~25; reaping them dropped it to ~1).

Root-cause fix — each session owns its browser lifecycle, no process-scanning GC:
- src/terminal.ts sessionEnv exports AGENT_BROWSER_NAMESPACE=aos-<id> (isolates the
  daemon/socket/state per session) + AGENT_BROWSER_IDLE_TIMEOUT_MS (5m default, the
  vendor self-shutdown, kept only as the net for un-trappable SIGKILL/OOM).
- terminal/claude-launch.sh adds an exit trap (EXIT/HUP/TERM/INT) running
  `agent-browser close --all` — so the session tears its browser down on any trappable
  exit, including the SIGHUP kill-session sends. Namespace-scoped, so it never touches
  another live session's browser. The launcher stays claude's parent (never execs it),
  so the trap covers interactive, unattended, and resident lanes.

Upstream-acknowledged gap (vercel-labs/agent-browser #885/#1334/#1371/#1401): the
daemon self-shuts-down on idle but nothing cleaned up after abnormal termination.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019zMSbnPEbqSVPGLasTKENp
@vikasprogrammer
vikasprogrammer force-pushed the feat/reap-orphan-browsers branch from 4246840 to c929263 Compare July 17, 2026 14:29
@vikasprogrammer vikasprogrammer changed the title fix(terminal): agent-browser daemon leak — sessions clean up their own browser (v0.226.1) fix(terminal): agent-browser daemon leak — sessions clean up their own browser (v0.227.1) Jul 17, 2026
@vikasprogrammer
vikasprogrammer merged commit e688871 into main Jul 17, 2026
1 check passed
@vikasprogrammer
vikasprogrammer deleted the feat/reap-orphan-browsers branch July 17, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant