fix(issue-19): close the trap race, select daemons by argv shape, align docs with the polling fallback
Review round 1 (claude PASS, agy PASS) recommendations: - reconcile.sh: `_sub_stop` falls back to `$!` for the window before `_sub_pid=$!` (R-1); lock identity compared as (st_dev, st_ino). - lib.sh legacy start_watchdog prints nothing when opted out and no daemon runs, byte-identical to the python body. - conftest/test: `_is_subscriber_daemon_argv` selects only `bash|sh <root>/.../reconcile.sh --subscribe`, never a brief-carrying wrapper that quotes the words or a prefix-colliding sibling root (R-2); one new unit test for the selector. - monitor SKILL.md / VERSIONS.md: the polling fallback checks the lock file's presence only and does not exclude a second daemon; measurement wording uses the argv-shape criterion instead of raw pgrep. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
4da4a50682
commit
c096cb455b
6 files changed
+69
-16
No files matched your search
+18
-1
@@ -29,6 +29,23 @@ SKILLS_DIR = str(Path(__file__).resolve().parent.parent / ".agents" / "skills")
|
||||
if SKILLS_DIR not in sys.path:
|
||||
sys.path.insert(0, SKILLS_DIR)
|
||||
|
||||
def _is_subscriber_daemon_argv(args, root):
|
||||
"""True only for the daemon's own command line: `bash|sh <root>/.../reconcile.sh
|
||||
--subscribe ...`. A process that merely *quotes* those words (a loop driver
|
||||
or delegate wrapper carrying a brief in argv) and a sibling root that is a
|
||||
string prefix of `root` (`.../pytest-9` vs `.../pytest-99`) are excluded
|
||||
(review R-2) - the reaper kills what it selects."""
|
||||
argv = args.split()
|
||||
if len(argv) < 3:
|
||||
return False
|
||||
if os.path.basename(argv[0]) not in ("bash", "sh"):
|
||||
return False
|
||||
script = argv[1]
|
||||
return (script.startswith(str(root).rstrip(os.sep) + os.sep)
|
||||
and script.endswith(os.sep + "reconcile.sh")
|
||||
and argv[2] == "--subscribe")
|
||||
|
||||
|
||||
def _subscriber_daemon_pids(root):
|
||||
"""PIDs of `reconcile.sh --subscribe` daemons whose command line lives under
|
||||
`root`, plus their children (the python subscriber the bash wrapper waits on).
|
||||
@@ -40,7 +57,7 @@ def _subscriber_daemon_pids(root):
|
||||
root = str(root)
|
||||
found = []
|
||||
for pid in _run_real(["pgrep", "-f", "reconcile.sh --subscribe"]).split():
|
||||
if root in _run_real(["ps", "-o", "args=", "-p", pid]):
|
||||
if _is_subscriber_daemon_argv(_run_real(["ps", "-o", "args=", "-p", pid]), root):
|
||||
found.append(int(pid))
|
||||
kids = []
|
||||
for pid in found:
|
||||
|
||||
@@ -58,11 +58,14 @@ def _run_helper(sandbox: Path, snippet: str, env=None) -> subprocess.CompletedPr
|
||||
|
||||
|
||||
def _daemons_for(sandbox: Path) -> list:
|
||||
"""Same argv-token selection as the conftest reaper (review R-2)."""
|
||||
from conftest import _is_subscriber_daemon_argv
|
||||
|
||||
out = subprocess.run(["pgrep", "-f", "reconcile.sh --subscribe"], capture_output=True, text=True).stdout.split()
|
||||
hits = []
|
||||
for pid in out:
|
||||
args = subprocess.run(["ps", "-o", "args=", "-p", pid], capture_output=True, text=True).stdout
|
||||
if str(sandbox) in args:
|
||||
if _is_subscriber_daemon_argv(args, str(sandbox)):
|
||||
hits.append(int(pid))
|
||||
return hits
|
||||
|
||||
@@ -210,6 +213,25 @@ def test_create_session_without_opt_out_spawns_a_daemon_the_reaper_removes(mam_s
|
||||
# 2. the sandbox reaper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_reaper_selects_only_the_daemon_argv_shape(tmp_path):
|
||||
"""Review R-2: a process that quotes the words in a brief, or lives under
|
||||
a sibling root that is a string prefix, must never be selected."""
|
||||
from conftest import _is_subscriber_daemon_argv
|
||||
|
||||
root = str(tmp_path / "sbx1")
|
||||
daemon = "bash {}/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --subscribe --idle-timeout 0".format(root)
|
||||
assert _is_subscriber_daemon_argv(daemon, root)
|
||||
assert _is_subscriber_daemon_argv("/bin/sh " + daemon.split(" ", 1)[1], root)
|
||||
# sibling root that is a prefix of this one
|
||||
assert not _is_subscriber_daemon_argv(daemon, root[:-1])
|
||||
assert not _is_subscriber_daemon_argv(daemon, root + "0")
|
||||
# brief-carrying wrapper that merely quotes the words and the path
|
||||
quoting = "python3 -m lib_py.delegation.cli submit --prompt 'note: reconcile.sh --subscribe under {}/.agents/skills/x'".format(root)
|
||||
assert not _is_subscriber_daemon_argv(quoting, root)
|
||||
# the real reconcile.sh in a different mode
|
||||
assert not _is_subscriber_daemon_argv(daemon.replace("--subscribe", "--once"), root)
|
||||
assert not _is_subscriber_daemon_argv("", root)
|
||||
|
||||
def test_sandbox_reaper_terminates_daemons_anchored_in_the_sandbox(mam_sandbox, monkeypatch):
|
||||
from conftest import _reap_subscriber_daemons, _subscriber_daemon_pids
|
||||
from lib_py.delegation import daemon
|
||||
|
||||
Reference in new issue
Block a user