diff --git a/.agents/skills/multi-agent-mux-delegate-job/SKILL.md b/.agents/skills/multi-agent-mux-delegate-job/SKILL.md index 230cf36..72e4a19 100644 --- a/.agents/skills/multi-agent-mux-delegate-job/SKILL.md +++ b/.agents/skills/multi-agent-mux-delegate-job/SKILL.md @@ -50,8 +50,9 @@ multi-agent-mux-delegate-job submit \ # MAM_STALL_IDLE_S (600) / MAM_STALL_HERDR_FAIL_S (120) / MAM_STALL_WATCH=0: the stall watcher (Issue #30). # The watcher lives as long as the job's wrapper: the wrapper stops it (KILL, at once) # when its subscriber ends, and the watcher exits on its own once the wrapper pid is gone - # (checked after every herdr wait, so it can outlive a killed wrapper by one wait, <= 60 s, - # and never sends a notice or a nudge after it saw the wrapper gone). + # (checked after every herdr wait, so it can outlive a killed wrapper by one herdr wait, + # <= 60 s - or, when the wrapper goes while a notice is being published, until that + # publish ends - and never sends a notice or a nudge after it saw the wrapper gone). # --parent-job (Issue #34): this job is delegated from job , which # must exist in the registry (else exit 1). A loop waiting on waits for # it too and shows it to reviewers. Default: $MAM_JOB_ID (the remote runner diff --git a/tests/test_issue30_stall_watch_stop.py b/tests/test_issue30_stall_watch_stop.py index b2b9d5c..581c2c7 100644 --- a/tests/test_issue30_stall_watch_stop.py +++ b/tests/test_issue30_stall_watch_stop.py @@ -329,9 +329,10 @@ def test_a_refused_idle_notice_withholds_the_nudge(tmp_path, monkeypatch): # round-2 review (codex D1/D2) - the last gate sits between the notice and the keys # --------------------------------------------------------------------------- -def _during_notice(tmp_path, monkeypatch, change, alive=lambda pid: True): +def _during_notice(tmp_path, monkeypatch, change, alive=lambda pid: True, in_wait=lambda tick: None): """Idle past S on the second tick; `change(rec)` runs inside the notice's - publish, as another actor would act while it waits on the wire.""" + publish, as another actor would act while it waits on the wire, and + `in_wait(tick)` inside each herdr wait.""" from lib_py import stall_watch import argparse import uuid @@ -343,6 +344,7 @@ def _during_notice(tmp_path, monkeypatch, change, alive=lambda pid: True): def fake_wait_once(target, until=(), timeout_ms=None, session=None, run=None): from lib_py.loop import watcher as w ticks.append(1) + in_wait(len(ticks)) if len(ticks) == 1: return w.WaitOutcome(w.MATCHED, agent={"agent_status": "idle", "state_change_seq": 1, "pane_id": "w1:p1"}) if len(ticks) > 2: # a watcher that did not stop: end the run through the record @@ -416,10 +418,19 @@ def test_an_unreadable_record_at_the_last_gate_withholds_the_nudge(tmp_path, mon def test_a_wrapper_gone_during_the_wait_gets_no_notice_and_no_nudge(tmp_path, monkeypatch): """Idle past S and the wrapper vanished during the same wait: the watcher leaves before judging - no notice, no keys, the record untouched.""" - calls = [] + gone, seen = [], [] def alive(pid): - calls.append(pid) - return len(calls) <= 2 # alive at the first loop head and after the first wait, gone after - rc, prompts, notes = _during_notice(tmp_path, monkeypatch, lambda rec: None, alive=alive) + seen.append((len(ticks), not gone)) + return not gone + + ticks = [] + + def in_wait(tick): + ticks.append(tick) + if tick == 2: + gone.append(True) # inside the second wait, while the watcher is blocked in herdr + rc, prompts, notes = _during_notice(tmp_path, monkeypatch, lambda rec: None, alive=alive, in_wait=in_wait) assert rc == 0 and notes == [] and prompts == [], (prompts, notes) + assert ticks == [1, 2], "the wrapper must vanish inside the second wait, not before it" + assert seen[-1] == (2, False) and all(ok for _, ok in seen[:-1]), seen