Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/report-cc418c3b.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

11 KiB
Raw Blame History

🔍 Review Report — PR #41 델타 리뷰 3회차 (job cc418c3b)

  • Reviewer: planner-reviewer-claude-01 (role: planner,reviewer) · 2026-09-26
  • 대상 델타: af0a2a2..9200562 (1커밋, 4파일 +144/−31, git diff --check 클린), base main 8ea65bb (v4.7.4).
  • 검증 경로: /var/folders/.../T/mam-review/loop-20260926T062809Z-914379/1. identity 확인: HEAD = 9200562c…b25d, HEAD^{tree} = c8425e22…325c — 브리프와 일치.
  • 2회차 내 판정: PASS + P1·P2 (job 2cc309ea; 1회차 N1 은 그때 닫힘). 이번에는 grok N1(TERM 5 s 예산)·codex D1/D2 의 반영을 본다.
  • 안전 규칙 준수: 검증 경로·소스 워크트리·본 체크아웃(main 8ea65bb) 모두 수정·커밋·checkout 없음. 실험은 scratchpad 하네스(stop_stall_watch 추출, 대역은 내가 만들고 KILL 로 정리)와 되돌린 사본에서만. 실제 세션에 stall_watch 를 붙이지 않았고 실제 .mam·데몬·herdr 서버 조작 없음, push·merge·PR 상태 변경 없음.

결론 요약

세 정정은 지적을 닫는다. KILL 기반 stop 은 TERM 을 무시하는 대역에 대해서도 0.00 s 에 돌아오고(실측), prompt 앞의 마지막 게이트(_may_prompt: 래퍼 생존 + strict 재읽기)와 wait 직후의 _wrapper_gone 이 D1/D2 의 창을 닫는다. 남는 마이크로초 경합은 레지스트리 락 없이 닫을 수 없고, 그 결과(닫힌 잡에 한 줄 nudge)는 무해하므로 받아들일 수 있다. 비차단 관찰 2건(Q1·Q2).

1. grok N1 — KILL 로 바꾼 것 (검토 요청 1)

multi-agent-mux-delegate-job:711-722 stop_stall_watch: kill -0 → kill -9 → wait. 하네스 실측(함수 추출):

대역 결과
TERM 무시(trap "" TERM; exec sleep 300) rc 0, 0.00 s, 대역 사망
정상 rc 0, 0.00 s
STALL_PID 비어 있음 즉시 return 0
이미 종료(rc 2) 즉시, stderr 에 stall watch for j1 exited rc=2: + 로그 꼬리(보고 유지)

타당하다. 감시자에 정리할 상태가 없다: 파일·락·소켓을 열어 두지 않고(레코드는 매번 열고 닫음), 유일한 부수효과는 두 하위 프로세스 — publish_event(알림, --attempts 1, 자체 timeout 120 s)와 herdr agent prompt(nudge, herdr 자체 --timeout) — 뿐이라 KILL 순간 진행 중이던 것은 각자 끝난다. TERM 핸들러도 os._exit 하나였으므로 KILL 로 잃는 동작이 없다. 2회차의 5 s 폴링은 래퍼의 close 시계 앞에 붙어 deadline_timeout 을 늦추던 부작용이 있었고(grok 지적), 그 예산이 사라졌다. KILL 은 fork-exec 창에서도 삼켜지지 않는다.

한 가지 기록(Q1): KILL 순간 비행 중이던 publish_event 는 래퍼가 잡을 닫은 뒤 에 progress 를 남길 수 있다. 종결 뒤 이벤트는 등록부 CAS/종단 규칙이 거르거나 한 줄 알림으로 남을 뿐이라 무해하지만, 래퍼의 디스크 liveness 스캔은 events.log 의 모든 최근 이벤트를 생존 증거로 세므로 감시자의 stall_watch:*/stalled:* 알림도 idle 유예를 연장한다(벽시계가 상한이라 무한하지는 않다). "멈춤 알림이 생존 증거가 된다"는 이 긴장은 이 델타가 만든 것이 아니며, 후속에서 스캔이 progress+stall_watch: 접두 detail 을 제외하면 사라진다.

2. codex D1/D2 — prompt 앞 게이트 (검토 요청 2·3)

  • _dispatch_open(strict=True)(stall_watch.py:266-288): strict 에서는 읽기 오류도 "아니오". _may_prompt(:296-301) = 래퍼 생존 ∧ strict 재읽기 — _notify 가 돌아온 뒤, prompt() 직전 에 호출된다(:408-412). 발행 rc 는 상태 판단에 쓰지 않는다(rc 0 은 "그 순간의 CAS 통과" 이지 "지금도 열려 있음" 이 아니다 — 맞는 태도).
  • _wrapper_gone(:290-294)은 루프 머리(:313), 매 wait 직후(:329, 어떤 알림·판정보다 먼저), _may_prompt 안 — 세 곳. D2 의 "대기 중 부모 소멸 뒤 알림·재주입 1회" 창은 wait 직후 검사로 닫힌다.
  • 남는 경합: _may_prompt 가 참을 돌려준 뒤 self.src.prompt() 의 herdr agent prompt 가 실행되기까지의 마이크로초. 이 안에 잡이 닫히면 닫힌 잡의 pane 에 "끝났으면 completed 를 발행하고 아니면 이어서 하라" 한 줄이 들어간다. 에이전트가 그에 따라 completed 를 다시 발행해도 등록부가 종단 뒤 종단을 거부하므로 상태가 바뀌지 않는다. 레지스트리 락으로만 닫을 수 있는 창이고 비용이 이득보다 크다 — 받아들일 수 있다.

3. 문서

SKILL.md :51-54: KILL 즉시, 래퍼 소멸 뒤 최대 wait 한 번(≤ 60 s), 소멸을 본 뒤에는 알림·nudge 없음 — 코드와 일치.

4. 테스트 (검토 요청 4 + 검증 방법)

4-1. 검증 경로에서의 실행 (브리프의 명령 그대로, pr-41 .venv)

tests/test_issue30_stall_watch_stop.py tests/test_issue30_stall_watch.py tests/test_issue14_delivery_ack.py
→ 3 failed, 67 passed, 1 skipped in 511.26s (0:08:31)
FAILED test_issue30_stall_watch.py::test_s9_the_wrapper_runs_the_watcher_for_the_jobs_life[1]
FAILED test_issue30_stall_watch.py::test_s10_remote_and_human_jobs_are_not_watched[human-…]
FAILED test_issue14_delivery_ack.py::test_wrapper_idle_timeout_grace_ignores_pane_busy_token
SKIPPED test_issue30_stall_watch.py:331 (S6, /proc)
  • stop 테스트 11건(기존 7 + 새 4) 전부 통과.
  • 세 실패는 모두 공개 브로커 의존이다. S9[1]·S10[human] 의 실패 지점은 t18._publish(reg, jid, "started", env).returncode == 0 이고 stderr 는 publish failed after 2 attempts: no CONNACK from broker(4회) — 두 시험은 _live_broker() 로 일부러 공개 브로커를 쓴다(:533-537). 단독 재실행에서 S9[1]·pane_busy 는 통과했고 S10[human] 만 같은 no CONNACK 로 다시 실패했다. 같은 시각 scratch 등록부로 공개 브로커에 started 를 세 번 보내자 1/3 이 21 s 뒤 rc 2 였다 — 브로커가 이 시간대에 불안정하다는 직접 증거. 이 델타는 human/remote 분기(start_stall_watch 의 제외 로직)를 한 줄도 바꾸지 않았고(git diff … | grep -c 'human\|_is_remote' = 0), 회차마다 실패하는 케이스가 바뀐다(2회차 S9[0]→R2[False], 3회차 S9[1]→S10). 2회차 P2 그대로.
  • grok 이 인용한 idle-grace 시험의 분류(검토 요청 1 의 참고): 동의한다. test_issue14 의 _base_env 는 브로커를 고정하지 않아 기본값(공개 브로커)을 쓰고, PR 이 인용한 로그(subscribe handshake timed out, 등록 후 ~14 s 무배달 → idle 시계 늦게 시작 → 14 s 의 completed 가 열린 잡에 들어감)는 감시자와 무관한 구독자 핸드셰이크 지연이다. 이 시험은 PR #29·#37 리뷰 때도 base 와 동일한 코드에서 같은 방식으로 실패·재실행 통과했었고(이 호스트의 T-1 부류), KILL 로 바뀐 뒤에는 감시자가 close 시계 앞에 넣는 예산 자체가 0 이다. 다만 Q1 처럼 감시자의 알림이 디스크 liveness 로 세어지는 경로는 남아 있어, 그 시험이 감시자 켜진 채로 도는 한 "완전히 무관"은 아니다 — 시험 쪽에서 MAM_STALL_WATCH=0 을 주거나(감시자는 그 시험의 대상이 아니다) 스캔이 감시자 알림을 제외하면 깔끔해진다(Q2).

4-2. 수정 없이는 실패하는가 — 확인함

래퍼와 stall_watch.py 만 2회차 head(af0a2a2)로 되돌린 사본에서 test_issue30_stall_watch_stop.py → 5 failed / 6 passed (33.8 s): (a) returns_at_once(2회차 코드는 5 s 예산으로 took≈6), 그리고 새 4건(발행 중 completed / 재디스패치 / 깨진 레코드 / 대기 중 래퍼 소멸) 이 정확히 실패. 잔류 프로세스 0.

4-3. 결정성·잔류 (검토 요청 4)

새 4건은 in-process(wait_once 주입, _notify 는 발행 중 다른 행위자가 레코드를 바꾸는 대역, _alive 주입)라 브로커·herdr·시계에 의존하지 않고 3회 실행(전체·되돌린 사본·단독)에서 결과가 같았다. 세 실행 뒤 호스트에 stall_watch --job/sleep 300 잔류 0. 실제 .mam/jobs 는 이 잡 자신의 파일 외 추가 0, 검증 경로 tracked 파일 변화 없음(HEAD 9200562), 본 체크아웃 main 8ea65bb.

5. 2회차 판정 항목의 처리

# 2회차 항목 이번 델타
(1회차 N1) 래퍼 영구 정지 2회차에 닫힘 유지 — 이제 KILL 즉시(0.00 s 실측)
grok N1 TERM 5 s 예산이 close 시계를 늦춤 닫힘 — 예산 제거
codex D1 알림 발행 중 상태 변화에도 prompt 닫힘 — _may_prompt(strict 재읽기 + 래퍼 생존), 테스트 3건
codex D2 대기 중 부모 소멸 뒤 알림·재주입 닫힘 — wait 직후 _wrapper_gone, 테스트 1건, SKILL.md 문구
P1 부모 소멸 감지 지연의 상태별 상한(정보) 문서에 "≤ 60 s" 로 명시됨 — 종결
P2 S9/R2(이제 S10 도) 의 공개 브로커 의존 열림 — 이번 실행에서도 3건

6. 판정

세 정정은 지적을 닫고 부작용이 없다(KILL 로 잃는 상태 없음, 게이트가 prompt 의 유일한 경로 앞에 있음). 남는 마이크로초 경합은 결과가 무해해 받아들인다. 버전 락스텝 bump 는 판정에서 제외한다.

# 비차단 관찰 근거
Q1 래퍼의 디스크 liveness 스캔은 events.log 마지막 줄의 timestamp 만 보므로 감시자의 progress(stall_watch:*/stalled:*) 도 생존 증거가 된다 — 멈춤 알림이 idle 유예를 연장하는 긴장(벽시계가 상한) 래퍼 :810-825 의 스캔; 델타 이전부터
Q2 test_issue14 의 liveness 시험들은 감시자를 끄지 않고 돈다 — 시험 대상이 아니므로 MAM_STALL_WATCH=0 을 주는 편이 결정적 _base_env 에 MAM_STALL_WATCH 없음

7. 교차검증 가이드

RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260926T062809Z-914379/1
PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-41/.venv/bin/python
git -C "$RP" rev-parse HEAD 'HEAD^{tree}'                     # 9200562… / c8425e2…
# stop_stall_watch 추출 → TERM 무시 대역: rc 0, 0.00 s, 대역 사망 (§1)
# 되돌린 사본(래퍼·stall_watch.py 를 af0a2a2 판으로) → tests/test_issue30_stall_watch_stop.py → 5 failed / 6 passed
# 브로커 건강: scratch 등록부에 started 3회 발행 → 1/3 rc 2 (no CONNACK) — S9/S10 실패와 같은 원인

[VERDICT: PASS]