diff --git a/CHANGELOG.md b/CHANGELOG.md index 36efb17..98e78c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,27 @@ new version heading in the same commit. ## [Unreleased] +## [0.227.1] — 2026-07-17 +### Fixed +- **`agent-browser` daemon leak — sessions now clean up their own browser (root-cause fix, not a GC).** + 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 and it + survived — burning CPU (its `swiftshader-webgl` software renderer spins helpers at ~100%) and RAM — for + **days**, until reboot or OOM. On the instawp box this had accumulated 6 orphaned daemons (one agent + leaked 4) driving load average to ~25; killing them dropped it to ~1. Two changes make each session own + its browser lifecycle: + - `src/terminal.ts` (`sessionEnv`) now exports **`AGENT_BROWSER_NAMESPACE=aos-`** (isolates + each session's daemon + socket + saved state) and **`AGENT_BROWSER_IDLE_TIMEOUT_MS`** (default 5 min, + operator-overridable — the vendor's 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**. Scoped to the session's + namespace, so it can never touch another live session's browser. The launcher already stays claude's + parent (never `exec`s it), so the trap is live for interactive, unattended, and resident lanes. + Upstream-acknowledged gap (vercel-labs/agent-browser #885/#1334/#1371/#1401): the daemon has idle + self-shutdown but nothing cleaned up after abnormal termination — now agent-os does, at the source. + ## [0.227.0] — 2026-07-17 ### Added - **Slack chat IDs now auto-link from a member's email.** When a notification (task assignment, approval, @@ -25,6 +46,7 @@ new version heading in the same commit. why and **scroll-jumps** to the relevant section below; the card hides once all four are done. This is the manual fallback for what auto-link can't cover — chiefly **Discord**, which has no email to resolve from — nudging members to link it themselves so task/approval DMs reach them. +## [0.226.0] — 2026-07-16 ### Added - **Distraction-free terminal + "Pop out" to its own tab.** The individual terminal view gets two new affordances in its top-right toolbar. **Focus** (⤢) lifts the pane to a full-viewport overlay (`fixed diff --git a/package-lock.json b/package-lock.json index 53ec4ff..6bb4e94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.227.0", + "version": "0.227.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.227.0", + "version": "0.227.1", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index 24b4e96..ee3d958 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.227.0", + "version": "0.227.1", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", diff --git a/src/terminal.ts b/src/terminal.ts index abc3618..feeb3be 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -1641,6 +1641,19 @@ export class TerminalManager { AGENT: agent, TASK_B64: Buffer.from(task, 'utf8').toString('base64'), AOS_SECRET: secret, + // The `agent-browser` skill starts a persistent headless-Chrome daemon that double-forks to init and + // so escapes the session's tmux process group — `tmux kill-session` at teardown can't reach it, and + // it (plus its swiftshader Chrome, which burns CPU) survives for days until reboot/OOM. Two env knobs + // let the SESSION clean up after ITSELF (root cause) instead of a process-scanning GC: + // • NAMESPACE per session isolates its daemon + socket + saved state, so the launcher's exit trap + // (`agent-browser close --all`, terminal/claude-launch.sh) shuts down THIS session's browser on + // any trappable exit — including the SIGHUP `tmux kill-session` sends — without touching another + // live session's browser. + // • IDLE_TIMEOUT is the last-resort net for the ONE exit the trap can't catch: an un-trappable + // SIGKILL (OOM). The daemon self-exits after this many ms with no commands. Operator-overridable; + // 5 min is long enough not to interrupt a multi-step browse (LLM think-time between commands). + AGENT_BROWSER_NAMESPACE: `aos-${id}`, + AGENT_BROWSER_IDLE_TIMEOUT_MS: process.env.AGENT_BROWSER_IDLE_TIMEOUT_MS || '300000', }; // Under the launcher, the systemd-run scope starts with a minimal PATH; seed it with the dir that // holds this app's node (claude is usually installed alongside it) plus the standard bins. Flag diff --git a/terminal/claude-launch.sh b/terminal/claude-launch.sh index 74b29a0..d6a49bd 100755 --- a/terminal/claude-launch.sh +++ b/terminal/claude-launch.sh @@ -35,6 +35,22 @@ red() { printf '\033[31m%s\033[0m\n' "$1"; } cd "$AGENT_DIR" 2>/dev/null || { red "agent folder not found: $AGENT_DIR"; exec bash; } +# Browser self-cleanup (root-cause fix for the agent-browser daemon leak). The `agent-browser` skill +# starts a persistent headless-Chrome daemon that double-forks (setsid) OUT of this pane's process +# group, so `tmux kill-session` at teardown can't reach it and it survives — burning CPU (swiftshader) +# + RAM — until reboot/OOM. Fix: this session OWNS its browser and shuts it down when the launcher +# exits. `close --all` is scoped to this session's AGENT_BROWSER_NAMESPACE (set per-session by the +# server; derived from SESSION here so it's right even on the RESUME path, whose recovered env may +# predate the var), so it can never touch another live session's browser. The signal traps make it run +# on the SIGHUP/SIGTERM `tmux kill-session` sends (both trappable) as well as a clean exit — only an +# un-trappable SIGKILL (OOM) slips past, and AGENT_BROWSER_IDLE_TIMEOUT_MS is the net for that. +export AGENT_BROWSER_NAMESPACE="${AGENT_BROWSER_NAMESPACE:-aos-${SESSION:-}}" +aos_browser_cleanup() { command -v agent-browser >/dev/null 2>&1 && agent-browser close --all >/dev/null 2>&1; return 0; } +trap aos_browser_cleanup EXIT +trap 'exit 129' HUP +trap 'exit 143' TERM +trap 'exit 130' INT + # Wire the gate as a project-local PreToolUse hook. claude inherits AOS_URL/SESSION/AGENT from # this shell's env, so the hook can reach the gateway and tag the right session. We regenerate # the settings each launch so the hook path is always correct (and portable across machines).