fix(terminal): agent-browser daemon leak — sessions clean up their own browser (v0.227.1)#377
Merged
Merged
Conversation
…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
force-pushed
the
feat/reap-orphan-browsers
branch
from
July 17, 2026 14:29
4246840 to
c929263
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The leak
The
agent-browserskill (global skill, used byqa/docs-bot/researcher/… agents) starts a persistent headless-Chrome daemon that double-forks (setsid) out of the session's tmux process group. Sotmux kill-sessionat teardown never reaches it — it reparents to PID 1 and survives, burning CPU (its--use-angle=swiftshader-webglsoftware renderer spins helpers at ~100%) and RAM, for days, until reboot or OOM.Observed live on the instawp box: 6 orphaned daemons (the
qaagent 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-killingclaude/chromemid-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(default300000, 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.shadds an exit trap (EXIT/HUP/TERM/INT) that runsagent-browser close --all(the vendor's clean shutdown). So the session tears its browser down on any trappable exit — including the SIGHUPtmux kill-sessionsends. Namespace-scoped, so it can never touch another live session's browser. The launcher already stays claude's parent (neverexecs 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 —
SESSIONis inconsistently present), andppid==1is 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_NAMESPACEenv propagate to the daemon (grandchild ofclaude).agent-browser close --allcleanly kills a namespaced daemon (verified on the box).tmux kill-sessionsends), SIGTERM, and clean exit (verified on the box with process-group signalling).npm run typecheck✓ ·npm run build✓ ·npm run test:governance→ 68/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