# 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`: ```bash 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()`**: ```python 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**: ```python 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]