Files
multi-agent-mux/.agents/reports/reviewer-agy-01/report-27654d6f.md
T
GodopuandClaude Opus 5.5 53f87e4560 release(v4.8.0): bump framework and eight skills to 4.8.0 in lockstep; archive PR #41 reviews (three rounds)
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
2026-09-26 16:08:37 +09:00

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-914379
    • base: af0a2a2557768fa316203b177e1b90e9f7283a1a
    • head: 9200562c914ab4f6e6d8a599cac371a07f99b25d
    • tree: 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:

  1. Grok N1: Replaced the 5-second graceful SIGTERM polling loop in wrapper stop_stall_watch with immediate kill -9 "$STALL_PID" followed by wait "$STALL_PID".
  2. Codex D1: Introduced _may_prompt() with _dispatch_open(strict=True) in stall_watch.py right before sending keystrokes to the target pane. Unreadable records, completed statuses, and re-dispatches strictly withhold the nudge prompt.
  3. Codex D2: Added immediate _wrapper_gone() check directly after watcher.wait_once (prior to any notification logic), and documented the lifecycle contract in SKILL.md.
  4. Test Determinism: Added 4 in-process deterministic tests in test_issue30_stall_watch_stop.py covering 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.py is entirely stateless: it holds no open file locks, no pending transactions, and no uncommitted state to disk. Its SIGTERM handler was a trivial lambda *_: 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 ... wait or publish_event.py) are bounded by explicit timeouts (timeout_ms / socket keepalive) and terminate independently.
    • Verdict on test_issue14 idle-grace flake: The flake observed in test_issue14 ("subscribe handshake timed out" / public broker ~14s delivery latency) had no stall watcher involvement (no ignored TERM logs). 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 with strict=True.
    • With strict=True, any unreadable or torn write evaluates to False, withholding keystrokes.
  • 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_once was blocked (for up to 60s), the watcher exits cleanly with RC_TERMINAL before publishing any notice or queuing any prompt.
  • Acceptability of Residual Race Window:
    • The window between _may_prompt() returning True and self.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.

2.3. Test Determinism & Verification Results

  • The 4 newly added unit tests in tests/test_issue30_stall_watch_stop.py:
    1. test_a_job_completed_while_the_notice_is_published_is_not_nudged: PASSED
    2. test_a_job_redispatched_while_the_notice_is_published_is_not_nudged: PASSED
    3. test_an_unreadable_record_at_the_last_gate_withholds_the_nudge: PASSED
    4. test_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.34s
    • tests/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]