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
9.4 KiB
PR #41 3회차 델타 리뷰 — ee9f0555
PASS. 2회차 D1·D2를 재현했던 독립 탐침이 이제 prompt를 보내지 않는다. 감시자 정리의 5초 유예도 제거됐다. 이번 delta에서 새 병합 차단 결함을 발견하지 않았다.
검증 identity 및 범위
- base:
af0a2a2557768fa316203b177e1b90e9f7283a1a - head:
9200562c914ab4f6e6d8a599cac371a07f99b25d - tree:
c8425e2259544c3a34210858f331b0563f15325c - loop:
loop-20260926T062809Z-914379 - 검증 cwd:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260926T062809Z-914379/1
4개 파일(+144/−31)의 delta를 검토했다. 검증 트리는 수정하지 않았으며 HEAD/tree 일치와 깨끗한 git 상태를 확인했다. 실제 에이전트/herdr 세션을 조작하지 않았다. 테스트와 추가 탐침의 레지스트리·HOME·프로세스는 임시 경로에 격리했다.
사용자의 후속 지시에 따라 본 체크아웃의 업데이트된 .mam.env를 확인했다. 비밀값을 출력하지 않고 새 MQTT 연결 설정만 추가 테스트 환경에 전달했다. 실제 세션 등록부 경로나 herdr 설정은 테스트에 새로 전달하지 않았다.
이전 차단 항목 처리
| 항목 | 판정 | 근거 |
|---|---|---|
| D1: 알림 발행 중 완료/재디스패치 후 prompt | 닫힘 | stall_watch.py:408의 _may_prompt()가 블로킹 _notify() 이후에 최신 레코드와 부모를 확인한다. 실제 publisher 로직을 사용한 이전 탐침에서 completed/attempt 2 모두 prompt 0회. |
| D1: 마지막 읽기 오류가 재주입 허가로 취급됨 | 닫힘 | stall_watch.py:278~:279가 strict 검사에서는 오류를 False로 처리한다. 깨진 JSON·없는 레코드·PermissionError 모두 최종 게이트에서 보류. 관측 루프의 일시 오류 허용과 분리됐다. |
| D2: herdr 대기 중 부모 소멸 뒤 알림·prompt | 닫힘 | stall_watch.py:329에서 wait 반환 직후, 오류 경고보다 먼저 부모를 확인한다. 독립 TIMEOUT/TRANSPORT_ERROR 탐침에서 알림·prompt 모두 0회. 발행 중 부모 소멸도 마지막 게이트에서 차단됐다. |
| grok N1: TERM 유예 5초가 종결 경로를 지연 | 닫힘 | 래퍼 :715~:716이 KILL+wait로 즉시 회수한다. 새 즉시 반환 테스트는 head에서 통과하고 base의 5초 유예 함수에서 assertion으로 실패했다. |
표의 Python 경로는 .agents/skills/lib_py/stall_watch.py, 래퍼 경로는 .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job이다.
KILL 전환 판단
전환은 타당하다. 이 감시자는 실제 작업 에이전트가 아니라 관측·알림 보조 프로세스다. 자체 registry 트랜잭션이나 종료 때 반드시 기록해야 할 체크포인트를 보유하지 않는다. 이전 TERM 핸들러도 os._exit()였으므로 정상 finally 정리나 버퍼 flush를 보장하던 동작을 새로 잃는 변경은 아니다. 빈 PID와 이미 종료한 자식의 처리·오류 보고 분기도 유지된다. 자기 자식을 KILL한 뒤 wait로 회수하는 경로에 종전의 TERM 무시/5초 유예 문제는 없다.
다만 KILL은 프로세스 그룹 전체를 죽이지 않는다. 실행 중이던 herdr/publish 자식은 잠시 남을 수 있다. agent wait에는 요청한 timeout이 있고 publisher는 자체 CONNACK 10초/PUBACK 5초 제한을 갖는다(lib_py/delegation/publisher.py:23~:24). 일반적인 정상 CLI/네트워크 실패 경로에서는 이 자식들이 끝난다. 반면 HerdrSource._herdr의 30초, _notify의 120초는 감시자 측 subprocess.run timeout이므로 부모가 KILL되면 그 타이머까지 살아남는다고 주장할 수는 없다. 따라서 외부 CLI 자체가 영구 정지하는 상황까지 “모든 자식의 종료 보장”으로 확대하지 않는다. 이는 이전 os._exit() 종료에도 있던 한계이며 이번 delta의 새 차단 결함으로 분류하지 않았다. 이번 실행 후 테스트 감시자·heartbeat 잔류 여부를 별도로 확인했다.
마지막 게이트와 경합 판단
_may_prompt()(stall_watch.py:296~:301)가 발행 뒤에 있고, 다음 줄의 실제 prompt 호출까지 추가 네트워크 대기를 두지 않는 점이 핵심이다. 이전의 최대 120초 발행 대기를 사이에 둔 오래된 관측 문제는 해결됐다. 잡 상태를 읽지 못한 회차에는 prompt를 보류하며, 발행 rc 0/2를 최신 dispatch의 생존 증명으로 사용하지 않는다.
최종 확인과 herdr 입력 사이에 다른 actor가 완료/재디스패치를 수행하는 짧은 경합은 여전히 가능하다. 이는 원자적 입력 프로토콜이 아니므로 절대적인 선형화 보장은 아니다. 그러나 이번 기능은 잡당 한 번의 계속 요청과 알림을 제공하며 잡 종결·자동 승인을 수행하지 않는다. 이 범위에서 블로킹 I/O 뒤 최신 상태를 확인하는 현재 게이트는 수용 가능하다. registry lock을 외부 RPC 동안 붙잡는 확장 설계를 이번 PR에 요구하지 않는다.
독립 탐침에서는 _notify=True 대역만 쓰지 않았다. 이전 회차의 탐침을 그대로 head에 적용해 실제 publish_event.main()의 wire 단계에 completed/재디스패치를 끼워 넣었다. publisher rc는 여전히 0이었으나 두 경우 모두 prompt가 0회였다. 부모 소멸도 wait 내부에서 실제로 조건을 바꾸는 별도 모의 관측으로 확인했다.
브로커 지연 분류
브리프에 기술된 subscribe 지연 사례를 브로커 의존 테스트의 타이밍 플레이크로 분류하는 것은 타당하다. job_subscriber.py:489~:491은 구독이 확인되기 전에는 idle 기준 시각을 계속 갱신하며, 최초 이벤트 전에는 ack 여유도 포함한다. 따라서 등록 후 14초에 completed를 쓰는 테스트는 구독 시작 자체가 늦으면 아직 열린 잡에 completed가 들어갈 수 있다. 이를 곧바로 감시자 stop 지연의 증거로 삼을 수 없다. 단순히 “ignored TERM 로그가 없다”는 사실만이 아니라 구독/이벤트 시각과 이 제어 흐름을 함께 봐야 한다.
이번 원본 선택 suite는 그대로 통과했다. 이어 사용자가 업데이트한 사설 브로커 설정으로 test_wrapper_idle_timeout_grace_ignores_pane_busy_token을 추가 실행했고 17.00초에 통과했다. 이전 회차에는 같은 모듈에서 공개 브로커 CONNACK 실패도 직접 관측했다. 이 증거들은 브리프의 분류와 부합한다. 다만 다른 호스트의 과거 세 실행 로그 전체를 이번에 독립 재현했다고 주장하지 않는다.
테스트 평가 및 비차단 메모
추가 테스트는 중요한 회귀를 잡는다. 다만 다음 정밀도 개선은 권한다.
tests/test_issue30_stall_watch_stop.py:421~:423의 부모 생존 대역은 호출 횟수 기준이다. head에서는 첫 루프 머리·첫 wait 뒤의 두 호출 다음, 두 번째 루프 머리에서 이미 False가 되어 종료한다. 이름 그대로 “두 번째 wait 도중 소멸”을 강제하려면 wait 대역 안에서 별도 alive 상태를 바꾸는 것이 낫다. 이번 리뷰의 독립 탐침은 그렇게 구성해 실제 wait 뒤 검사를 확인했다.- unreadable 테스트를 base에 그대로 연결하면
_dispatch_open(strict)시그니처 차이로 TypeError가 먼저 난다. 이를 의도한 행동 검증으로 세지 않았다. 대신 임시 head 사본에서return not strict만return True로 되돌려 새 API를 유지한 채 테스트를 실행했고, 실제 prompt 발생에 따른 AssertionError를 확인했다. - SKILL.md
:53의 “최대 한 wait, ≤60초”는 herdr wait 도중 부모가 사라진 경우의 설명으로 한정하는 편이 정확하다. 발행 중 부모가 사라지면 진행 중인 발행 호출이 끝날 때까지도 기다릴 수 있다. 현재 코드는 그 뒤 prompt를 보내지 않으므로 D1·D2는 해결됐지만, 60초를 모든 외부 호출에 대한 전역 상한으로 읽으면 과장이다.
2회차에 지적했던 테스트 실패 경로의 최종 cleanup 보강 및 이전 회차의 #17 관련 비차단 부채는 이번 delta 밖의 후속 항목으로 유지한다. 새 차단 조건으로 올리지 않았다.
실행 결과
- 브리프의 worktree
.venv/bin/python,AGENT_PYTHON_BIN,PYTHONDONTWRITEBYTECODE=1,-p no:cacheprovider, 외부--basetemp=/tmp/ee9f0555-suite사용. - 지정된
test_issue30_stall_watch_stop.py,test_issue30_stall_watch.py,test_issue14_delivery_ack.py: 70 passed, 1 skipped, 426.01초. skip은 Linux /proc 전용 S6이다. - 업데이트된 MQTT 설정을 적용한 위 idle-grace 사례: 1 passed, 17.00초. 기존 suite와 중복된 사례이며 별도 고유 테스트 수로 더하지 않았다.
- 독립 추가 확인: 실제 publisher 완료/재디스패치 2건, wait 중 부모 소멸 1건, 최종 게이트 정상/깨진 JSON/레코드 없음/권한 오류 4건, 발행 중 부모 소멸 및 오류 wait 중 부모 소멸 2건 통과.
- 부정 검증: base에서 완료·재디스패치·부모 소멸 테스트 3건 및 즉시 stop 테스트 1건이 예상한 assertion으로 실패. strict 정책만 되돌린 임시 사본의 unreadable 테스트 1건도 예상한 assertion으로 실패. 검증 트리 파일은 변경하지 않았다.
git diff --check af0a2a2, 래퍼bash -n통과. HEAD/tree 및 깨끗한 git 상태 재확인.
[VERDICT: PASS]