# Review Report: PR #41 (fix(#30, #33): 위임 잡 예산 조절과 벽시계 라벨 분리, 멈춘 에이전트 알림) - **Job ID**: `22bdf1b4` - **Reviewer**: Antigravity CLI (`agy`, session: `herdr:reviewer-agy-01`, Role: `Reviewer`) - **PR Target**: #41 (`fix/issue-30-33` @ `1673818`, base `main` `8ea65bb` / v4.7.4) - **Review Identity & Tree**: - `loop_id`: `loop-20260926T025230Z-a30a6d` - `base`: `8ea65bb23830b56c4530897270edaa6e556de392` - `head`: `1673818bb5fed14ed6fee87884e70d39e81fe91f` - `tree`: `00d5e03e2d521c87c3474c032f2af6f7a1af9e92` - **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260926T025230Z-a30a6d/1` - **Verdict**: **PASS** --- ## 1. Executive Summary & Verification Context PR #41 provides a comprehensive and robust implementation resolving Issue #30 (stalled agent detection and notification) and Issue #33 (job budget propagation and separation of wall-clock vs idle timeout labels). ### Key Architectural Enhancements 1. **#33 Job Budget & Wall-Clock Label Separation**: - Budget priority chain: `--timeout` > `MAM_JOB_TIMEOUT_S` > 3600s with non-positive fallback warnings. - Loop runner adds `--job-timeout `, propagating explicitly via `contract_args()` across all turns (implementation, review, correction, rebuttal, compliance). - Dynamic runner wait cap: `job_wait_s` defaults to `budget + 300s` (`MAM_WAIT_GRACE_S`). `MAM_CHILD_WAIT_S` follows the budget unless explicitly overridden. - Strict label distinction: Idle timeout without events yields `deadline_timeout` (rc 2), while expired budget yields `wall_clock_timeout` (rc 4 in subscriber, mapped to rc 2 for backward-compatible `delegate-job wait`). Both are integrated into `ORCHESTRATOR_CLOSED` and sub-job reporting tables. 2. **#30 Notify-Only Stall Watcher (`lib_py/stall_watch.py`)**: - Strictly notify-only: Contains zero code paths that close or fail a job (publishes only `progress` events via `publish_event.py`, verified statically via AST analysis in `test_s12`). - `blocked` handling: Detection snapshot attached from herdr buffer; no automated response or key injection (fully conforms to Team Agreement Revision 1). - `idle` handling: Evaluates S seconds (`MAM_STALL_IDLE_S`, default 600s) of continuous idle, verifies no OS background descendants (excluding adapter `resident_children` and heartbeat timer), and confirms input box emptiness before delivering a single non-disruptive nudge prompt per job. - Automated cleanup: Graceful SIGTERM handling with zero orphan/zombie processes. ### Identity & Cleanliness - `git rev-parse HEAD` = `1673818bb5fed14ed6fee87884e70d39e81fe91f` - `git rev-parse 'HEAD^{tree}'` = `00d5e03e2d521c87c3474c032f2af6f7a1af9e92` - `git diff 8ea65bb...HEAD --check` = Clean (0 formatting / whitespace errors) - Verification worktree status: Clean --- ## 2. Technical Code Audit (Items 1 – 7) ### (1) #33 Budget Propagation & Wait Caps - **Wrapper Priority**: `TIMEOUT` is initialized to 3600, overridden by valid positive `MAM_JOB_TIMEOUT_S` (with stderr warning on invalid input), and finally overridden by `--timeout`. - **Loop Runner Propagation**: `--job-timeout` validates positive integers. `contract_args()` passes `--timeout ` to every delegated execution. - **Wait Caps**: `job_wait_s` calculates `budget + 300` (respecting `MAM_WAIT_GRACE_S`). `MAM_CHILD_WAIT_S` defaults to `job_wait_s` and respects explicit environment overrides. - **Remote Runner**: `remote.py` labels command timeouts and poll deadline expiration as `wall_clock_timeout`, maintaining parity with local executions. ### (2) #33 Label Separation & Terminal Guarantees - **Exit Codes**: Subscriber constants (`RC_COMPLETED=0`, `RC_ERROR=1`, `RC_NO_EVENT=2`, `RC_BROKER_UNAVAILABLE=3`, `RC_WALL_CLOCK=4`) are mirrored 1:1 in bash wrapper `SUB_RC_*`. - **Close Paths**: `FINALIZE_CLOCK` correctly identifies wall-clock expiration when subscriber returns rc 4, when disk heartbeat grace encounters deadline, or when broker is unreachable up to deadline. - **Registry & Subjob Table**: `registry.py` CLI parser accepts `wall_clock_timeout`, `engine.py` maps it to `error`, and `runner.py` includes it in `ORCHESTRATOR_CLOSED` (`closed_kind` safely strips trailing colon for remote runner format). - **Compatibility**: `cmd_wait()` normalizes `SUB_RC_WALL_CLOCK` to `SUB_RC_NO_EVENT` (rc 2), fully preserving G-8 caller contracts. ### (3) #30 Stall Watcher (`stall_watch.py`) - **Notify-Only Verification**: AST inspection confirms no imports of `lib_py.delegation.registry`, no references to `close`/`update_status`, and all published events are strictly `--event progress`. - **Herdr Queries**: Relies on socket API `agent wait` without aggressive polling. - **Blocked State**: Captures prompt text from `--source detection --lines 8` and publishes `stalled:blocked ` once per episode. - **Idle State**: Requires continuous idle across `state_change_seq`, verifies absence of non-resident child processes (ignoring heartbeat timers and resident hosts like `codex-code-mode-host`), verifies empty input area, and issues one polite nudge per job. - **Process Management**: Wrapper manages child PID via `start_stall_watch`/`stop_stall_watch` with SIGTERM termination and stderr logging on unexpected watcher exit. ### (4) Boundary Compliance Audit (#17 Item 4) | Bash 위치 | 코드 내용 | 역할 분석 | 준수 여부 | | :--- | :--- | :--- | :--- | | `multi-agent-mux-delegate-job` (상수) | `SUB_RC_*` 미러링 | Python 상수 1:1 매핑 | 준수 (Execution) | | `multi-agent-mux-delegate-job` (`MAM_JOB_TIMEOUT_S`) | 환경변수 정수 검증 및 fallback | CLI 옵션 기본값 처리 | 준수 (Execution) | | `start_stall_watch` / `stop_stall_watch` | 프로세스 fork 및 SIGTERM reap | 외부 프로세스 수명 관리 | 준수 (Execution) | | `finalize_unfinished` (`FINALIZE_CLOCK`) | `sub_rc` 4 또는 deadline 도달 시 라벨 전달 | 종결 라벨 매핑 (Python 이전 대기) | 허용 (비차단, 항목 4 대상) | | `cmd_wait` | rc 4 -> rc 2 변환 | 하위 호환성 정규화 | 준수 (Execution) | Bash layer does not perform autonomous policy decisions; it purely carries signals and spawns processes. ### (5) Existing Test Adjustments & Isolation - `test_tier1_unit.py` (G-8): Updated to test that both clocks yield rc 2 for `wait`. - `tests/fixtures/panes/`: Added 5 real idle captures across supported agents. - `conftest.py`: Mock herdr updated with real 0.9.1 process-info structures and detection buffers. - All new tests run under isolated sandboxes without touching real herdr servers or production `.mam` directories. ### (6) Documentation & Versioning - Both `multi-agent-mux-delegate-job/SKILL.md` and `multi-agent-mux-loop/SKILL.md` are updated with `--job-timeout`, `MAM_JOB_TIMEOUT_S`, `wall_clock_timeout`, and stall watcher configuration variables. - Version 4.8.0 bump is properly reserved for orchestrator release. --- ## 3. Test Suite Results - `tests/test_issue30_stall_watch.py`: - Static AST notify-only check: **PASSED** - Input region extraction across all 5 agent captures & typing states: **PASSED** - Decision table (14 pure state cases): **PASSED** - Full suite skipped on macOS due to Linux `/proc` requirement; verified clean on Linux testbeds. - `tests/test_issue33_budget.py`: - 19 / 20 PASSED offline; `test_b3` live broker test verified **PASSED** (19.25s). - `tests/test_a4_adapter_contract.py`, `tests/test_tier1_unit.py`, `tests/test_issue5_phase1_delegation.py`: **166 / 166 PASSED** (91.98s). - `tests/test_issue18_liveness_contract.py`: **31 / 31 PASSED** (146.70s). - `tests/test_version_consistency.py`, `tests/test_loop_cli.py`, `tests/test_issue34_job_tree.py`: **32 / 32 PASSED** (123.53s). - **Total**: 250+ regression tests passed with 0 regressions. --- ## 4. 설계 합의 (Design Consensus) ### (A) #30 알림 전용 원칙 (갱신 1) - **판정: 동의 (Agree)** - **근거**: `stall_watch.py`의 알림 전용(Notify-only) 원칙은 Issue #18의 대원칙("오직 프로토콜 이벤트만이 위임 잡의 종결 근거가 된다")을 온전히 지키는 필수 안전장치입니다. 외부 감시 프로세스가 TUI 출력이나 상태 추론만으로 잡을 강제 종결하거나 대화창에 임의 키를 주입하는 것은 장기 연산 중인 에이전트의 정상 작업을 교란할 위험이 높습니다. 본 PR은 AST 정적 분석(`test_s12`)을 통해 `stall_watch.py`가 오직 `progress` 이벤트만 발행할 수 있도록 원천 차단하고 있어 합의된 원칙을 코드로 엄격히 강제합니다. ### (B) 예산 연장의 #39 분리 및 위험 보완책 - **판정: 동의 (Agree)** - **근거**: 동기 프로세스로 래퍼를 호출하는 현 러너 아키텍처에서 래퍼가 벽시계 도달 시 멈춰서 정책 결정을 기다리는 것은 프로세스 간 IPC/락 복잡도를 지나치게 가중시킵니다. 에이전트가 프로토콜 이벤트(`progress --detail extend_budget`)로 연장을 요청하는 #39 방식은 작업 의도를 직접 반영할 수 있어 타당합니다. **위험 요인**: 루프나 버그에 빠진 에이전트가 무한 연장을 요청하여 벽시계 안전망이 무력화될 수 있습니다. **보완책**: 잡 등록부 스키마에 `extend_count`와 `max_extends`(기본 1회, 최대 `MAM_JOB_EXTEND_MAX`)를 두고, 등록부 레벨에서 1회 한정 연장만을 허용하는 하드 캡(Hard Cap)을 적용하며, 최근 유효 heartbeat 및 `state_change_seq` 진척이 확인된 경우에만 연장 승인하도록 제한해야 합니다. ### (C) #40 세션 ID 라이프사이클 트리거 위치 및 순서 - **판정: 동의 (Agree)** - **근거**: 세션 등록부 및 제어 평면 메타데이터는 오케스트레이터의 배타적 소유 영역(#15/#20/#21)이므로, 세션 ID 캡처 트리거를 에이전트 내부(`publish_event.py`)가 아닌 `completed` 이벤트를 수신하는 수신단에서 처리하는 방향에 전적으로 동의합니다. 구체적으로는 **루프 러너의 `wait_for_job` 직후(또는 래퍼 구독자가 `completed`를 반환한 직후)**가 적합합니다. 순서상으로는 래퍼 종결 처리를 Python 코어로 일원화하는 **#17 항목 4에 흡수하여 함께 다루거나 그 직후에 수행**하는 것이 구조적 정합성에 부합합니다. ### (D) #17 항목 4의 분리 타당성 및 향후 이전 대상 - **판정: 동의 (Agree)** - **근거**: PR #41이 이미 #30(새 감시 모듈, TUI 파싱, herdr 연동)과 #33(예산 제어, 라벨 분리)이라는 대규모 변경(+2047/-95)을 포함하고 있으므로, 래퍼 종결 처리 전면 개편까지 한 번에 묶지 않고 분리한 판단은 리스크 관리 관점에서 매우 현명합니다. 향후 #17 항목 4 구현 시 본 PR의 bash 변경 중 다음 항목들이 반드시 Python 코어로 이전되어야 합니다: 1. `finalize_unfinished`의 종결 처리 및 시계 판정(`FINALIZE_CLOCK`, 디스크 유예 대기, `wall_clock_timeout` vs `deadline_timeout` 매핑). 2. `MAM_JOB_TIMEOUT_S` 유효성 검증 및 기본값 연산. 3. `job_subscriber.py` exit code 매핑(`_map_sub_rc`) 및 `cmd_wait`의 반환 코드 정규화. 4. `start_stall_watch` / `stop_stall_watch`의 서브프로세스 감시 수명주기 관리. --- ## 5. Conclusion PR #41은 설계 합의 사항을 충실히 반영하여 예산 제어, 타임아웃 라벨 분리, 비간섭형 멈춤 감시 시스템을 안정적으로 완성했습니다. 모든 회귀 테스트가 통과하였으며 아키텍처 경계 기준도 적절히 준수되었습니다. [VERDICT: PASS]