From ae98cf18c4db573af9cc630ab2e2fe22ede3f31a Mon Sep 17 00:00:00 2001 From: Godopu Date: Thu, 15 Oct 2026 15:16:06 +0000 Subject: [PATCH] fix(stop): a purge waits for the agent process, not only the herdr session (Issue #31) A closed herdr session is not an exited agent, and the `herdr already dead` path ran no check at all, so stop could remove the files of a live claude and then its row. New `python -m lib_py.agents in-use [ws] [--wait s]` gives the purge CLI's own verdict; stop asks it outside the registry lock and, unless it answers free, exits 4 with nothing changed. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/lib_py/agents/__main__.py | 34 ++++++++++++++++++- .../scripts/stop_session.sh | 17 ++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.agents/skills/lib_py/agents/__main__.py b/.agents/skills/lib_py/agents/__main__.py index 9651a65..2dac388 100644 --- a/.agents/skills/lib_py/agents/__main__.py +++ b/.agents/skills/lib_py/agents/__main__.py @@ -1,6 +1,6 @@ # __main__.py — CLI bridge for lib_py.agents facts and resolution (N4) -import os, sys, json, shlex +import os, sys, json, shlex, time from lib_py.agents.registry import get_adapter, own_key, agent_of_row # `purge auto` probes adapters in this order; opencode last because its @@ -151,6 +151,38 @@ def main(): print(json.dumps({"agent": name, "purged": purged, "residual": residual, "in_use": False})) sys.exit(0) + elif cmd == 'in-use': + # in-use [workspace] [--wait ] (Issue #31) + # The purge CLI's own in-use verdict, for stop_session.sh to ask + # before it purges. Re-checks every 0.25 s for up to --wait seconds. + # Prints {"agent", "in_use"}. Exit 0 free; 4 still in use; 1 bad args. + args = sys.argv[2:] + wait = 0.0 + if '--wait' in args: + i = args.index('--wait') + try: + wait = float(args[i + 1]) + except (IndexError, ValueError): + print("ERROR: --wait requires seconds", file=sys.stderr) + sys.exit(1) + del args[i:i + 2] + agent_name = args[0] if args else '' + uuid = args[1] if len(args) > 1 else '' + workspace = args[2] if len(args) > 2 and args[2] else os.getcwd() + adapter = get_adapter(agent_name) + if not adapter or not uuid: + print("ERROR: in-use requires ", file=sys.stderr) + sys.exit(1) + from lib_py.agents.base import DiscoveryContext + ctx = DiscoveryContext(workspace, agent_name) + deadline = time.monotonic() + wait + in_use = adapter.conversation_in_use(uuid, ctx) + while in_use and time.monotonic() < deadline: + time.sleep(0.25) + in_use = adapter.conversation_in_use(uuid, ctx) + print(json.dumps({"agent": agent_name, "in_use": in_use})) + sys.exit(4 if in_use else 0) + else: print(f"ERROR: Unknown CLI command {cmd!r}", file=sys.stderr) sys.exit(1) diff --git a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh index fb0d44b..5fae625 100755 --- a/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh +++ b/.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh @@ -282,6 +282,23 @@ if [ "$PURGE" = "1" ] && [ "$HERDR_ALIVE" = "1" ]; then fi fi +# Purge process gate (Issue #31): a closed herdr session is not an exited +# agent - the process may still be shutting down, or may have outlived a dead +# herdr ("herdr already dead" above runs no gate at all). The purge asks the +# adapter's own in-use verdict, the one the purge CLI uses, outside the +# registry lock. Any answer but "free" (0) keeps everything. +if [ "$PURGE" = "1" ] && [ -n "$PURGE_UUID" ]; then + inuse_rc=0 + "$(_delegate_py_bin)" -m lib_py.agents in-use "$AGENT" "$PURGE_UUID" "$TARGET_CWD" --wait 5 >/dev/null || inuse_rc=$? + if [ "$inuse_rc" != "0" ]; then + echo "ERROR: conversation $PURGE_UUID is still held by a live agent process (rc=$inuse_rc)." >&2 + echo " Refusing purge — records preserved (no state was modified)." >&2 + echo " For claude, see the session files naming it under ~/.claude/sessions/." >&2 + delegate_publish_event "$DELEGATE_JOB_ID" cancelled "purge aborted: conversation still in use; no state was modified" "$DELEGATE_JOB_TOKEN" "$DELEGATE_JOB_ATTEMPT" + exit 4 + fi +fi + # INVARIANT: Terminal events for a job must be published AFTER atomic_dump_yaml updates the session status atomic_dump_yaml "$AGENT_SESSIONS_YAML" \ SESSION_NAME="$SESSION_NAME" AGENT="$AGENT" PURGE="$PURGE" \