From 4050dd9dce0904aaf7de7ee34f07b2a0b738ec7e Mon Sep 17 00:00:00 2001 From: Vikas Singhal Date: Fri, 17 Jul 2026 23:21:42 +0530 Subject: [PATCH] fix(terminal): agent-browser cleanup handles agents that relocate the socket dir (v0.227.2) Follow-up to v0.227.1. End-to-end testing on a real qa session showed the exit trap was ineffective for the agents that actually leak: `agent-browser close --all` keys off the SOCKET DIR, not the namespace, and qa/engineer/site-porter/website-bot export a custom AGENT_BROWSER_SOCKET_DIR to a writable per-session dir (real HOME is read-only under systemd ProtectHome). So the trap's plain close --all (which only had the server's default socket dir) found "No active sessions" and left the daemon running. The trap now finds THIS session's daemons by the AGENT_BROWSER_NAMESPACE they reliably inherit, recovers each one's own AGENT_BROWSER_SOCKET_DIR from its /proc environ, and closes it scoped to that (SIGTERM straggler fallback). Namespace-scoped, so it never touches another session's browser; the plain close --all runs first for the default-socket-dir / macOS case. Verified against a real custom-socket-dir daemon. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019zMSbnPEbqSVPGLasTKENp --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- terminal/claude-launch.sh | 39 ++++++++++++++++++++++++++++++++------- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e78c2..1fd74a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ new version heading in the same commit. ## [Unreleased] +## [0.227.2] — 2026-07-17 +### Fixed +- **`agent-browser` cleanup now handles agents that relocate the socket dir (follow-up to 0.227.1).** + End-to-end testing on a real `qa` session revealed 0.227.1's exit trap was ineffective for the very + agents that leak: `agent-browser close --all` keys off the **socket dir**, not the namespace, and + several agents (`qa`/`engineer`/`site-porter`/`website-bot`) export a custom + **`AGENT_BROWSER_SOCKET_DIR`** to a writable per-session dir (their HOME is read-only under systemd + `ProtectHome`), so the trap's plain `close --all` — which only had the server's default socket dir — + found "No active sessions" and left the daemon running. Fix (`terminal/claude-launch.sh`): the trap + now finds **this session's** daemons by the `AGENT_BROWSER_NAMESPACE` env they reliably inherit, + **recovers each one's own `AGENT_BROWSER_SOCKET_DIR` from its `/proc` environ**, and runs `close --all` + scoped to it (with a `SIGTERM` straggler fallback). Still namespace-scoped, so it never touches another + session's browser; the plain `close --all` runs first to cover the default-socket-dir / macOS case. + ## [0.227.1] — 2026-07-17 ### Fixed - **`agent-browser` daemon leak — sessions now clean up their own browser (root-cause fix, not a GC).** diff --git a/package-lock.json b/package-lock.json index 6bb4e94..a19903d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.227.1", + "version": "0.227.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.227.1", + "version": "0.227.2", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index ee3d958..61c9286 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.227.1", + "version": "0.227.2", "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/terminal/claude-launch.sh b/terminal/claude-launch.sh index d6a49bd..24679be 100755 --- a/terminal/claude-launch.sh +++ b/terminal/claude-launch.sh @@ -38,14 +38,39 @@ cd "$AGENT_DIR" 2>/dev/null || { red "agent folder not found: $AGENT_DIR"; exec # 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. +# + RAM — until reboot/OOM. Fix: this session OWNS its browser and shuts it down when the launcher exits. +# The signal traps make cleanup 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. +# +# Scoping is by the per-session AGENT_BROWSER_NAMESPACE (set by the server; re-derived from SESSION here +# so it's right on the RESUME path too). Note `agent-browser close --all` keys off the *socket dir*, not +# the namespace — and several agents (qa/engineer/site-porter/…) relocate AGENT_BROWSER_SOCKET_DIR to a +# writable per-session dir because their real HOME is read-only under systemd ProtectHome — so a plain +# `close --all` here (which has the server's default socket dir, not the agent's) would miss those. So we +# find THIS session's daemons by the namespace env they inherit, recover each one's OWN socket dir from +# its /proc environ, and close it there (with a force-kill fallback). Namespace-scoped, so it never +# touches another live session's browser. /proc is Linux-only; the plain close first covers the +# default-socket-dir case (incl. macOS). 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; } +aos_browser_cleanup() { + command -v agent-browser >/dev/null 2>&1 || return 0 + local ns="${AGENT_BROWSER_NAMESPACE:-}" + agent-browser close --all >/dev/null 2>&1 # default-socket-dir daemons (namespace-scoped) + [ -n "$ns" ] || return 0 + # Custom-socket-dir daemons (Linux): match this session's namespace, recover its socket dir, close there. + local pid envf sd hm + for pid in $(pgrep -f 'agent-browser-' 2>/dev/null); do + envf="/proc/$pid/environ" + [ -r "$envf" ] || continue + tr '\0' '\n' < "$envf" 2>/dev/null | grep -qx "AGENT_BROWSER_NAMESPACE=$ns" || continue + sd=$(tr '\0' '\n' < "$envf" 2>/dev/null | sed -n 's/^AGENT_BROWSER_SOCKET_DIR=//p' | head -1) + hm=$(tr '\0' '\n' < "$envf" 2>/dev/null | sed -n 's/^HOME=//p' | head -1) + env AGENT_BROWSER_NAMESPACE="$ns" ${sd:+AGENT_BROWSER_SOCKET_DIR="$sd"} ${hm:+HOME="$hm"} agent-browser close --all >/dev/null 2>&1 + kill -0 "$pid" 2>/dev/null && kill -TERM "$pid" 2>/dev/null # straggler fallback + done + return 0 +} trap aos_browser_cleanup EXIT trap 'exit 129' HUP trap 'exit 143' TERM