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
123 lines
7.1 KiB
Markdown
123 lines
7.1 KiB
Markdown
# 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]
|