From d50610c1abb840cdabf94c5684f41ed4e8edc5cb Mon Sep 17 00:00:00 2001 From: Godopu Date: Sun, 11 Oct 2026 06:28:01 +0000 Subject: [PATCH] fix(issue-50): a waiter whose own attempt is already running answers clear, not superseded The registry wait judged its record by one rule: not this attempt's pending record in this session -> superseded (redispatched, moved or closed by someone else). That also caught the record's own `started` landing before the prompt went out - the Issue #14 registry ack winning over an unreachable broker, as test_issue30_stall_watch_stop acknowledges at registration - and the wrapper left on exit 5 instead of its broker_unavailable path (rc 3). A record that is running under this attempt's token in this session is this attempt's own progress: the waiter answers `clear` at once, writes no admission and starts no hold. Another token, another session or a terminal status stay `superseded` (review 55219be1 R1 unchanged). Co-Authored-By: Claude Fable 5.1 --- .agents/skills/lib_py/delegation/wrapper.py | 19 ++++++++++++--- tests/test_issue50_dispatch.py | 26 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.agents/skills/lib_py/delegation/wrapper.py b/.agents/skills/lib_py/delegation/wrapper.py index 0f57a72..16efecb 100644 --- a/.agents/skills/lib_py/delegation/wrapper.py +++ b/.agents/skills/lib_py/delegation/wrapper.py @@ -334,6 +334,16 @@ def _own(rec: Optional[dict], token: Optional[str], session: str) -> bool: and _bare(str(rec.get("agent_session") or "")) == _bare(session) +def _started_own(rec: Optional[dict], token: Optional[str], session: str) -> bool: + """This attempt's record, in this session, already `running`: its own + `started` landed before the prompt went out (the registry ack of Issue + #14 wins over an unreachable broker; a test that acknowledges at + registration). Nothing was redispatched, moved or closed by someone else + - there is just nothing left to wait for.""" + return bool(rec) and bool(token) and rec.get("status") == "running" and rec.get("dispatch_token") == token \ + and _bare(str(rec.get("agent_session") or "")) == _bare(session) + + def _merge_own(registry_dir: str, job_id: str, token: Optional[str], session: str, source: str, fields: dict, guard=None) -> bool: """One merge through the engine, committed only while the record re-read @@ -434,7 +444,9 @@ def wait_open_jobs(a: argparse.Namespace) -> str: and its admission is committed (_admit); `busy ` when the bound runs out first; `superseded` once the record is not this attempt's pending one in this session any more (redispatched, moved, closed by someone else) - - nothing is written or closed then, the record is its owner's. The bound + nothing is written or closed then, the record is its owner's; `clear` + at once, with no admission, when it is this attempt's and already + running (_started_own). The bound is min(--wait-s, this job's remaining wall clock). The identity is pinned at the start: --dispatch-token (the token the wrapper read for this attempt), else the record's token read once. Every DISPATCH_SIGNAL_EVERY s @@ -450,8 +462,9 @@ def wait_open_jobs(a: argparse.Namespace) -> str: last_signal = None while True: now = _now() - if not _own(_load_record(a.registry_dir, a.job), token, a.agent_session): - return "superseded" + rec = _load_record(a.registry_dir, a.job) + if not _own(rec, token, a.agent_session): + return "clear" if _started_own(rec, token, a.agent_session) else "superseded" live, stale = open_jobs(a.registry_dir, a.agent_session, a.job, now) for other, reason in stale: if other not in reported: diff --git a/tests/test_issue50_dispatch.py b/tests/test_issue50_dispatch.py index 83180c8..8940d0c 100644 --- a/tests/test_issue50_dispatch.py +++ b/tests/test_issue50_dispatch.py @@ -422,6 +422,32 @@ def test_u3g_a_waiter_whose_attempt_changed_in_place_leaves_the_admission_to_the proc.communicate() +def test_u3i_an_attempt_already_running_under_its_own_token_is_clear_not_superseded(tmp_path): + # the Issue #14 registry ack: the agent's `started` for this attempt lands + # before the prompt goes out (test_issue30_stall_watch_stop). Nothing to + # wait for, nothing foreign: `clear` at once, no admission written + a = _register(tmp_path, status="running") + _event(tmp_path, a, time.time()) + me = _register(tmp_path) + tok = _rec(tmp_path, me)["dispatch_token"] + _edit(tmp_path, me, status="running") + before = _rec(tmp_path, me) + t0 = time.time() + assert decide.wait_open_jobs(_ns(tmp_path, me, wait_s=30)) == "clear" + assert time.time() - t0 < 5 and _rec(tmp_path, me) == before, "no wait, no write" + res = _cli(tmp_path, "wait-open-jobs", "--job", me, "--agent-session", "herdr:s", "--registry-dir", str(tmp_path), + "--wait-s", "30", "--timeout", "3600", "--dispatch-token", tok, "--hold") + assert res.stdout.strip() == "clear" and not _hold_pids(me), res.stderr + # running under another attempt's token, or in another session, is still someone else's + assert decide.wait_open_jobs(_ns(tmp_path, me, session="herdr:t")) == "superseded" + _edit(tmp_path, me, dispatch_token="another-attempt") + res = _cli(tmp_path, "wait-open-jobs", "--job", me, "--agent-session", "herdr:s", "--registry-dir", str(tmp_path), + "--wait-s", "30", "--timeout", "3600", "--dispatch-token", tok) + assert res.stdout.strip() == "superseded", res.stderr + _edit(tmp_path, me, dispatch_token=tok, status="completed") + assert decide.wait_open_jobs(_ns(tmp_path, me)) == "superseded" + + def test_u3h_the_pinned_token_is_the_wrappers_not_the_records(tmp_path): # the record's token changed before the waiter started (a redispatch raced # the wrapper): the waiter given the old token is superseded at once