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
49 lines
4.7 KiB
Markdown
49 lines
4.7 KiB
Markdown
# 🔍 Review Report — Job 0d52f435 (PR #41 @ `1673818`)
|
||
|
||
- **Reviewer**: `reviewer-grok-01` · 2026-09-26
|
||
- **검증 경로**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260926T025230Z-a30a6d/1`
|
||
- **identity**: base `8ea65bb` head `1673818` tree `00d5e03` (일치)
|
||
- **본 체크아웃**: `main` `8ea65bb` 유지. stall_watch 를 실제 세션에 붙이지 않음.
|
||
|
||
`git diff --check 8ea65bb...HEAD` 클린. 6커밋.
|
||
|
||
## 코드
|
||
|
||
**(1) 예산.** `--timeout` > `MAM_JOB_TIMEOUT_S`(양의 정수, 아니면 경고 후 3600) > 3600. 루프 `contract_args` 가 모든 `_dispatch` 에 `--timeout str(job_budget)` 을 붙인다. `job_wait_s` = 예산 + `MAM_WAIT_GRACE_S`(기본 300). `MAM_CHILD_WAIT_S` 가 있으면 그 값, 없으면 `job_wait_s`. 원격 러너는 `timeout_sec` 초과를 `wall_clock_timeout: …` 로, 관측 실패를 `wall_clock_timeout (remote runner)` 로 닫고 첫 단어 분류가 `CLOSED-UNCONFIRMED` 에 넣는다.
|
||
|
||
**(2) 라벨.** 구독자 `RC_*` 와 래퍼 `SUB_RC_*` 가 0/1/2/3/4. `test_issue5_phase1_delegation.py` 가 이름과 값을 맞춘다. `finalize_unfinished` 는 rc 4 또는 디스크 유예가 `START_TIME+TIMEOUT` 에 닿으면 `wall_clock_timeout`, 그 외 침묵은 `deadline_timeout`. preflight/idle_wait 는 `cancelled`. `delegate-job wait` 는 rc 4 를 rc 2 로 접는다(G-8). 닫은 뒤 디스크가 `completed` 면 래퍼는 0 으로 돌아간다. 이미 `error` 인 레코드에 늦은 `completed` 는 엔진 터미널 전이가 거부한다.
|
||
|
||
**(3) 감시자.** `stall_watch.py` 의 유일한 발행은 `_notify` 의 `progress`. `close`/`error`/`cancelled` 호출이 없다. herdr `agent wait`(`watcher.wait_once`)로 상태를 받고, 판단 직전에 `agent get`·process-info·`agent read` 로 자손과 입력창을 본다. `blocked` 는 `stalled:blocked` + detection 한 줄, 키 입력 없음. `idle` 은 `MAM_STALL_IDLE_S`(600), 상주 자식·heartbeat 타이머 제외, 입력창이 비었을 때만 잡당 1회 `stalled:idle` + nudge. 원격·human·`MAM_STALL_WATCH=0` 은 기동하지 않는다. herdr 실패는 경고 후 `MAM_STALL_HERDR_FAIL_S`(120) 에 exit 2. 래퍼는 SIGTERM + `wait`. `.stall.out` 은 로그로 남는다(프로세스는 회수).
|
||
|
||
**(4) 경계.** 새 bash 판정은 셋이다. 항목 4 이전이 예고돼 있으므로 비차단.
|
||
|
||
| bash | 하는 일 | 분류 |
|
||
|---|---|---|
|
||
| `FINALIZE_CLOCK` | 디스크 유예가 벽시계에 닿았는지로 라벨을 고름 | 판정. 구독자 rc 4 는 Python 이 이미 정함 |
|
||
| `MAM_JOB_TIMEOUT_S` | 양의 정수가 아니면 3600 | 작은 검증 |
|
||
| rc → `close_req` | 2/4/preflight 를 라벨로 매핑 | 나르기. 매핑표는 bash 에 있음 |
|
||
|
||
**(5) 테스트.** G-8 은 두 시계가 같은 rc 2 였던 단언을 rc 4(벽시계)로 고쳤다. 새 테스트는 mock herdr·tmp. 1회차 스위트에서 `test_b3_a_heartbeating_job…` 가 공개 브로커가 heartbeat 를 놓쳐 구독자는 idle 로 끝났고(래퍼 라벨은 이미 `wall_clock_timeout`), 재실행은 **1 passed in 18s**.
|
||
|
||
**(6)–(7)** loop SKILL 하위 위임 절이 `wall_clock_timeout` 을 `CLOSED-UNCONFIRMED` 에 넣는다. delegate-job/loop SKILL 에 `--job-timeout`·`MAM_JOB_TIMEOUT_S`·감시자 변수가 있다. 4.8.0 bump 는 오케스트레이터 예정.
|
||
|
||
## 설계 합의
|
||
|
||
**(A) 동의.** 갱신 1(알림만, 자동 수락 없음, 판정으로 닫지 않음)을 `stall_watch.py` 가 강제한다. 종결은 #33 시계뿐이다.
|
||
|
||
**(B) 동의.** 러너가 래퍼를 동기 호출하므로 연장 정책을 그 안에서 물을 수 없다. #39 위험은 에이전트가 `progress` 로 연장을 반복하는 것이다. 보완: 연장 횟수 상한을 프로토콜과 별개로 두고, heartbeat 만 있고 `state_change_seq` 가 그대로면 거절한다.
|
||
|
||
**(C) 조건부 동의.** 트리거는 `completed` 를 검증하는 쪽이어야 한다. 워크트리 creator(`MAM_NO_SUBSCRIBER_DAEMON=1`)도 잡마다 래퍼 구독자가 `completed` 를 받으므로 핀은 그 구독자가 맞고, 루프 `wait_for_job` 은 구독자가 없을 때의 보조다. 항목 4 가 종결 처리를 Python 으로 옮긴 그 함수에 붙이면 된다.
|
||
|
||
**(D) 동의.** 라벨 분리만으로 #33 은 성립하고, 종결 처리 이전은 더 큰 변경이다. 항목 4 때 Python 으로 옮길 것은 `FINALIZE_CLOCK` 선택, `MAM_JOB_TIMEOUT_S` 검증, rc→`close_req` 매핑이다. bash 에는 `start_stall_watch`/`stop_stall_watch` 와 키 주입만 남긴다.
|
||
|
||
## 테스트
|
||
|
||
`test_issue30_stall_watch.py` + `test_issue33_budget.py` + `test_tier1_unit.py`: **1 failed, 86 passed, 48 skipped**, 이어서 실패 1건 재실행 **passed**.
|
||
|
||
## 종합
|
||
|
||
#33 두 시계와 #30 알림 전용이 코드와 맞다. bash 의 라벨 선택은 예고된 이전이 남았고 병합을 막지 않는다.
|
||
|
||
[VERDICT: PASS]
|