test(issue-18): acknowledge with started in the remaining working-agent scenarios; refresh the subscriber help capture

T1-14 and T1-20 model an agent that goes `working` and completes late. Under
the liveness contract an agent that never publishes `started` is a delivery
failure, so the watcher threads now acknowledge (non-blocking: the record
lands in events.log before the wire publish) and the late completion is
honoured inside the K x N grace as before. help-job_subscriber.txt records
the intended --ack-timeout option.

Full suite at 66f23c7: 888 passed, 3 failed (these), 0 leaked daemons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-09-13 19:43:16 +09:00
1 parent 66f23c7cbd
commit c17c87439d
2 files changed
+36 -24

No files matched your search

+6 -1
View File
@@ -1,5 +1,6 @@
usage: job_subscriber.py [-h] (--job JOB | --wait-any) [--timeout TIMEOUT] usage: job_subscriber.py [-h] (--job JOB | --wait-any) [--timeout TIMEOUT]
[--idle-timeout IDLE_TIMEOUT] [--expect-retention] [--idle-timeout IDLE_TIMEOUT]
[--ack-timeout ACK_TIMEOUT] [--expect-retention]
[--expect-dispatch-token EXPECT_DISPATCH_TOKEN] [--expect-dispatch-token EXPECT_DISPATCH_TOKEN]
[--attempt-id ATTEMPT_ID] [--attempt-id ATTEMPT_ID]
[--registry-dir REGISTRY_DIR] [--remote] [-v] [--registry-dir REGISTRY_DIR] [--remote] [-v]
@@ -15,6 +16,10 @@ optional arguments:
--idle-timeout IDLE_TIMEOUT --idle-timeout IDLE_TIMEOUT
max seconds with no new event (default: max seconds with no new event (default:
job.idle_timeout_sec or 120) job.idle_timeout_sec or 120)
--ack-timeout ACK_TIMEOUT
extra idle budget before the first event, for the
injection to produce `started` (default:
job.ack_timeout_sec; Issue #18)
--expect-retention warn if no retained terminal event arrives promptly --expect-retention warn if no retained terminal event arrives promptly
--expect-dispatch-token EXPECT_DISPATCH_TOKEN --expect-dispatch-token EXPECT_DISPATCH_TOKEN
expected dispatch token for CAS verification expected dispatch token for CAS verification
+30 -23
View File
@@ -522,6 +522,34 @@ def test_t3_3_runner_delegate_job_session_busy_retry(tmp_path, monkeypatch):
assert jid == "jid_busy_2" assert jid == "jid_busy_2"
def _publish_started_for_registered_job(reg_dir, cwd, env=None):
"""Issue #18: a working agent that never publishes `started` is a delivery
failure (rc 1) under the liveness contract. The R1 re-observation cases
below are about an agent that *did* start and then went silent, so the
watcher threads acknowledge the job the way the injected command does."""
deadline = time.time() + 20
while time.time() < deadline:
files = list(Path(reg_dir).glob("*.json"))
if files:
jid = files[0].stem
tok = json.loads(files[0].read_text()).get("dispatch_token") or ""
args = [sys.executable, str(SKILLS / "multi-agent-mux-delegate-job" / "scripts" / "publish_event.py"),
"--registry-dir", str(reg_dir), "--job", jid, "--event", "started", "--attempts", "1"]
if tok:
args += ["--expect-dispatch-token", tok]
run_env = dict(os.environ)
run_env.update(env or {})
# Fire and forget: the record lands in events.log before the wire
# publish, and blocking here on the broker ACK (seconds) would
# delay the scenario steps that follow in the watcher thread.
subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
cwd=str(cwd), env=run_env)
return jid
time.sleep(0.1)
return None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# T1-14: R1 working 중 subscriber 타임아웃 -> 디스크 completed 재관측 수용 (rc=0) # T1-14: R1 working 중 subscriber 타임아웃 -> 디스크 completed 재관측 수용 (rc=0)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -547,6 +575,7 @@ def test_t1_14_working_agent_reobserve_completed(tmp_path, mock_herdr):
if any("agent" in c and "prompt" in c for c in st.get("calls", [])): if any("agent" in c and "prompt" in c for c in st.get("calls", [])):
st["agents"]["agt_w1"]["agent_status"] = "working" st["agents"]["agt_w1"]["agent_status"] = "working"
state_file.write_text(json.dumps(st, indent=2)) state_file.write_text(json.dumps(st, indent=2))
_publish_started_for_registered_job(reg_dir, tmp_path) # Issue #18: acknowledge
break break
except Exception: except Exception:
pass pass
@@ -603,29 +632,6 @@ def test_t1_14_working_agent_reobserve_completed(tmp_path, mock_herdr):
assert len(agent_gets) >= 1, "R1 loop must query agent get" assert len(agent_gets) >= 1, "R1 loop must query agent get"
def _publish_started_for_registered_job(reg_dir, cwd, env=None):
"""Issue #18: a working agent that never publishes `started` is a delivery
failure (rc 1) under the liveness contract. The R1 re-observation cases
below are about an agent that *did* start and then went silent, so the
watcher threads acknowledge the job the way the injected command does."""
deadline = time.time() + 20
while time.time() < deadline:
files = list(Path(reg_dir).glob("*.json"))
if files:
jid = files[0].stem
tok = json.loads(files[0].read_text()).get("dispatch_token") or ""
args = [sys.executable, str(SKILLS / "multi-agent-mux-delegate-job" / "scripts" / "publish_event.py"),
"--registry-dir", str(reg_dir), "--job", jid, "--event", "started", "--attempts", "1"]
if tok:
args += ["--expect-dispatch-token", tok]
run_env = dict(os.environ)
run_env.update(env or {})
subprocess.run(args, capture_output=True, text=True, cwd=str(cwd), env=run_env, timeout=60)
return jid
time.sleep(0.1)
return None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# T1-15: R1 working 고정 + 미발행 -> Issue #18: K×N 후 rc=2 error (working 은 진단일 뿐) # T1-15: R1 working 고정 + 미발행 -> Issue #18: K×N 후 rc=2 error (working 은 진단일 뿐)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -886,6 +892,7 @@ def test_t1_20_loop_worker_late_completed_continues_to_reviewer(tmp_path, mock_h
if any("agent" in c and "prompt" in c for c in st.get("calls", [])): if any("agent" in c and "prompt" in c for c in st.get("calls", [])):
st["agents"]["agt_worker"]["agent_status"] = "working" st["agents"]["agt_worker"]["agent_status"] = "working"
state_file.write_text(json.dumps(st, indent=2)) state_file.write_text(json.dumps(st, indent=2))
_publish_started_for_registered_job(reg_dir, tmp_path) # Issue #18: acknowledge
break break
except Exception: except Exception:
pass pass