From c8270dc42932a4a177996a33b3549187031d5117 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Wed, 10 Jun 2026 15:18:05 -0400 Subject: [PATCH] fix(dev): reap orphaned agent processes on dev/staging exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ctrl+C on `just dev`/`just staging` tears down the Tauri app before its in-process system sweep can finish, so agent workers spawned in their own process groups survive as orphans and accumulate across sessions. Add an EXIT trap that reaps this instance's agents via the PID-file receipts the desktop already writes (one PGID per agent under `/agents/agent-pids/`). The app-data dir is keyed by the bundle identifier, so the cleanup is scoped exactly to the instance that ran the recipe — the main checkout never reaps a worktree's agents, or vice versa. PID files are matched instead of the `SPROUT_MANAGED_AGENT` env marker because macOS `pkill -f` matches only argv, not the environment, so an env-marker match would silently reap nothing. Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 --- justfile | 8 ++++ scripts/cleanup-instance-agents.sh | 59 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100755 scripts/cleanup-instance-agents.sh diff --git a/justfile b/justfile index b7d02a5c3..a89778f40 100644 --- a/justfile +++ b/justfile @@ -277,6 +277,10 @@ dev *ARGS: _ensure-sidecar-stubs cd {{desktop_dir}} [[ -d node_modules ]] || pnpm install source ../scripts/instance-env.sh + # Ctrl+C kills the Tauri app before its in-process sweep finishes, leaking + # agent workers. Reap this instance's agents on exit as a backstop. + INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.SPROUT_TAURI_CONFIG).identifier)") + trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID"' EXIT echo "Starting on Vite port ${SPROUT_VITE_PORT}, relay ${SPROUT_RELAY_URL}" pnpm exec tauri dev --features mesh-llm --config "$SPROUT_TAURI_CONFIG" {{ARGS}} @@ -294,6 +298,10 @@ staging *ARGS: _ensure-sidecar-stubs cd {{desktop_dir}} source ../scripts/instance-env.sh export SPROUT_RELAY_URL="wss://sprout-oss.stage.blox.sqprod.co" + # Ctrl+C kills the Tauri app before its in-process sweep finishes, leaking + # agent workers. Reap this instance's agents on exit as a backstop. + INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.SPROUT_TAURI_CONFIG).identifier)") + trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID"' EXIT echo "Starting staging on Vite port ${SPROUT_VITE_PORT}, relay ${SPROUT_RELAY_URL}" pnpm exec tauri dev --features mesh-llm --config "$SPROUT_TAURI_CONFIG" {{ARGS}} diff --git a/scripts/cleanup-instance-agents.sh b/scripts/cleanup-instance-agents.sh new file mode 100755 index 000000000..a6bc52f98 --- /dev/null +++ b/scripts/cleanup-instance-agents.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Reap the agent processes belonging to a single desktop instance. +# +# `tauri dev` Ctrl+C tears down the Rust app before its in-process system sweep +# can finish, so agent workers (goose, sprout-agent, ...) it spawned in their +# own process groups survive as orphans. This script is the shell-side backstop: +# run it from an EXIT trap in the `just dev`/`just staging` recipes. +# +# It reads the PID-file receipts the desktop already writes — one file per agent +# under `/agents/agent-pids/.pid`, each containing the agent's +# PGID (agents are spawned with `process_group(0)`, so PID == PGID). Killing by +# PGID reaches the whole agent subtree. We deliberately do NOT match the +# `SPROUT_MANAGED_AGENT` env var from the shell: on macOS `pkill -f` matches only +# argv, not the environment, so an env-marker match silently reaps nothing. +# +# Scoping is exact because the app-data directory is keyed by the instance's +# bundle identifier, so this only ever touches the receipts this instance wrote +# (the main checkout never reaps a worktree's agents, or vice versa). +# +# Usage: cleanup-instance-agents.sh +# is the desktop bundle identifier, e.g. `xyz.block.sprout.app.dev` +# (main checkout) or `xyz.block.sprout.app.dev.my-branch` (a worktree). + +set -euo pipefail + +instance_id="${1:-}" +if [[ -z "$instance_id" ]]; then + echo "cleanup-instance-agents: no instance id given, skipping" >&2 + exit 0 +fi + +case "$(uname -s)" in + Darwin) app_data="$HOME/Library/Application Support/$instance_id" ;; + *) app_data="${XDG_DATA_HOME:-$HOME/.local/share}/$instance_id" ;; +esac + +pids_dir="$app_data/agents/agent-pids" +[[ -d "$pids_dir" ]] || exit 0 + +shopt -s nullglob +pgids=() +for pid_file in "$pids_dir"/*.pid; do + pgid="$(<"$pid_file")" + pgid="${pgid//[$'\t\r\n ']/}" + [[ "$pgid" =~ ^[0-9]+$ ]] || continue + pgids+=("$pgid") +done +[[ ${#pgids[@]} -gt 0 ]] || exit 0 + +# SIGTERM the whole group first, give it a moment, then SIGKILL survivors. +# `kill -- -` targets the process group. Failures are expected and fine +# (already-dead group, recycled PGID owned by someone else we can't signal). +for pgid in "${pgids[@]}"; do + kill -TERM -- "-$pgid" 2>/dev/null || true +done +sleep 0.2 +for pgid in "${pgids[@]}"; do + kill -KILL -- "-$pgid" 2>/dev/null || true +done