Issue #33 (job budget, wall_clock_timeout vs deadline_timeout) and Issue #30 (notify-only stall watcher). Reviews of PR #41 in the merge workspace: round 1 (@1673818) claude N1 / codex R1 R2 NOT PASS, agy grok PASS; round 2 (@af0a2a2) grok N1 / codex D1 D2 NOT PASS, claude agy PASS; round 3 (@9200562) 4/4 PASS. Reports and the round-3 identity sidecars go to .agents/reports/<session>/. The design consensus on items A-D is in the VERSIONS.md entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
7.1 KiB
Code Review Report (Round 3 Delta): PR #41 (fix/issue-30-33)
- Job ID:
27654d6f - Target / Session:
agy(herdr:reviewer-agy-01) - Role: Reviewer
- Review Identity:
loop_id:loop-20260926T062809Z-914379base:af0a2a2557768fa316203b177e1b90e9f7283a1ahead:9200562c914ab4f6e6d8a599cac371a07f99b25dtree:c8425e2259544c3a34210858f331b0563f15325c- Verification Path:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260926T062809Z-914379/1
1. Executive Summary
This is the Round 3 delta review for PR #41 (fix/issue-30-33: "위임 잡 예산 조절과 벽시계 라벨 분리, 멈춘 에이전트 알림 (알림 전용)").
Round 2 review resulted in: Claude PASS, Agy PASS, Grok NOT PASS (N1: 5s TERM budget delayed wrapper close clock), and Codex NOT PASS (D1: prompt sent even if job completed/re-dispatched/record unreadable during publish; D2: notification/prompt injected after wrapper vanished during herdr wait).
Commit 9200562 directly addresses all Round 2 blocking findings:
- Grok N1: Replaced the 5-second graceful SIGTERM polling loop in wrapper
stop_stall_watchwith immediatekill -9 "$STALL_PID"followed bywait "$STALL_PID". - Codex D1: Introduced
_may_prompt()with_dispatch_open(strict=True)install_watch.pyright before sending keystrokes to the target pane. Unreadable records, completed statuses, and re-dispatches strictly withhold the nudge prompt. - Codex D2: Added immediate
_wrapper_gone()check directly afterwatcher.wait_once(prior to any notification logic), and documented the lifecycle contract inSKILL.md. - Test Determinism: Added 4 in-process deterministic tests in
test_issue30_stall_watch_stop.pycovering completion, re-dispatch, torn record reads during notification publish, and wrapper death during wait.
All 11 tests in test_issue30_stall_watch_stop.py and all 47 tests in test_issue30_stall_watch.py passed cleanly in the isolated verification path.
2. Detailed Technical Review & Audit Questions
2.1. grok N1: Immediate KILL in stop_stall_watch
- Soundness & State Hygiene:
In
multi-agent-mux-delegate-job:stop_stall_watch() { [ -n "${STALL_PID:-}" ] || return 0 local rc=0 if kill -0 "$STALL_PID" 2>/dev/null; then kill -9 "$STALL_PID" 2>/dev/null || true wait "$STALL_PID" 2>/dev/null || true else wait "$STALL_PID" 2>/dev/null || rc=$? fi ... }stall_watch.pyis entirely stateless: it holds no open file locks, no pending transactions, and no uncommitted state to disk. Its SIGTERM handler was a triviallambda *_: os._exit(RC_TERMINAL).- Replacing SIGTERM with SIGKILL guarantees immediate termination without signal deferral by child bash subshells (resolving N1).
- Any child processes spawned by the watcher (e.g.
herdr ... waitorpublish_event.py) are bounded by explicit timeouts (timeout_ms/ socket keepalive) and terminate independently. - Verdict on
test_issue14 idle-graceflake: The flake observed intest_issue14("subscribe handshake timed out" / public broker ~14s delivery latency) had no stall watcher involvement (noignored TERMlogs). Classifying this as ambient public MQTT broker latency flake (M9) is sound and accurate.
2.2. codex D1 & D2: Pre-Prompt Gate and Post-Wait Checks
_dispatch_open(strict=True)and_may_prompt():def _dispatch_open(self, strict=False): try: with open(os.path.join(self.args.registry_dir, self.args.job + ".json")) as f: rec = json.load(f) except FileNotFoundError: return False except (OSError, ValueError): return not strict if rec.get("status") in TERMINAL_STATUSES: return False tok = self.args.expect_dispatch_token if tok and rec.get("dispatch_token") != tok: return False att = self.args.attempt_id if att is not None and rec.get("attempt_id") != att: return False return True def _may_prompt(self): return not self._wrapper_gone() and self._dispatch_open(strict=True)- In
_notify("stalled:idle"), publishing an MQTT event over the network can block or take significant time (up to 120s timeout). - Immediately upon
_notify()returning,_may_prompt()re-evaluates the wrapper liveness and re-reads the registry JSON file withstrict=True. - With
strict=True, any unreadable or torn write evaluates toFalse, withholding keystrokes.
- In
- Post-Wait Wrapper Liveness:
out = watcher.wait_once(self.args.target, until=until, timeout_ms=tmo, session=self.args.herdr_session) if self._wrapper_gone(): # round-2 review D2: before any notice return RC_TERMINAL- If the wrapper died while
wait_oncewas blocked (for up to 60s), the watcher exits cleanly withRC_TERMINALbefore publishing any notice or queuing any prompt.
- If the wrapper died while
- Acceptability of Residual Race Window:
- The window between
_may_prompt()returningTrueandself.src.prompt(...)executing is microsecond-level CPU instructions. - Adding multi-process distributed file locking would introduce deadlock risks and lock contention without practical benefit. The gate as implemented is fully robust and acceptable.
- The window between
2.3. Test Determinism & Verification Results
- The 4 newly added unit tests in
tests/test_issue30_stall_watch_stop.py:test_a_job_completed_while_the_notice_is_published_is_not_nudged: PASSEDtest_a_job_redispatched_while_the_notice_is_published_is_not_nudged: PASSEDtest_an_unreadable_record_at_the_last_gate_withholds_the_nudge: PASSEDtest_a_wrapper_gone_during_the_wait_gets_no_notice_and_no_nudge: PASSED
- Full suite execution results in verification path:
tests/test_issue30_stall_watch_stop.py: 11 passed in 30.34stests/test_issue30_stall_watch.py: 47 passed, 1 skipped in 274.70s- Git whitespace / formatting check (
git diff af0a2a2...HEAD --check): Clean (no issues). - No residual processes, zero memory/socket leaks.
3. Resolution Matrix for Prior Blocking Findings
| Reviewer / Item | Issue Description | Resolution in 9200562 |
Status |
|---|---|---|---|
| grok N1 | 5s graceful TERM budget in stop_stall_watch delayed wrapper exit & close clock |
Replaced with immediate kill -9 "$STALL_PID" + wait. Returns in 0s. |
CLOSED |
| codex D1 | Keystroke prompt sent after long publish even if job completed or was re-dispatched | Added _may_prompt() with _dispatch_open(strict=True) re-check. |
CLOSED |
| codex D2 | Wrapper vanished during herdr wait could still trigger 1 notice/prompt | Added _wrapper_gone() check immediately post-wait_once. |
CLOSED |
| agy | Round 2 Passed | Verified delta stability and test passes. | PASS |
| claude | Round 2 Passed | Verified delta stability and test passes. | PASS |
4. Conclusion
All requested delta fixes in commit 9200562 have been verified to be correct, minimal, well-tested, and conformant with repository rules. All blockers from Round 2 are successfully resolved.
[VERDICT: PASS]