Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/report-e93cf9d4.md
T
GodopuandClaude Opus 5.5 8fde39be33 release(v4.8.6): Issue #17 item 4 - archive PR #51 reviews and bump the lockstep version
Merge-workspace review: round 1 (@518beda) claude agy grok PASS, codex
NOT PASS (finalize wait measured on the wall clock); fixed in 4c86107.
Round 2 delta (@4c86107) claude agy grok PASS; codex (24b21b9c, after
817d1650 closed deadline_timeout with no report) found R1 closed and no
new defect, but withheld PASS only because its sandbox could not bind
the suite's loopback broker. The same two-file suite run here at
4c86107: 278 passed. Full suite at 518beda on macOS / Python 3.9:
1711 passed / 1 failed / 38 skipped; the failure
(test_issue45_restore, herdr 0.9.3 cwd restore) also fails on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-08 01:37:48 +09:00

66 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 리뷰 리포트 — Job e93cf9d4 (PR #51, refactor/issue-17-item4)
- 리뷰어: planner-reviewer-claude-01 (claude)
- 리뷰 identity: loop-20261007T142403Z-10d77f / base 7209750 / head 518beda / tree 137b4e4 — 검증 경로에서 `git rev-parse HEAD 'HEAD^{tree}'` 일치, 미커밋 변경 없음
## 결론 요약
동작 불변 계약이 지켜졌다. 옛 bash 판정(tests/data/issue17_item4_old_decisions.sh = 7209750 의 함수)과 새 wrapper.py 를 경로별로 대조해 차이를 찾지 못했고(옛 코드의 따옴표 경로 버그 하나는 새 코드에서 고쳐짐 — 개선), 지정 9개 파일이 이 macOS·Python 3.9·bash 3.2 에서 전부 통과한다. 차단 결함 없음. 비차단 관찰 5건(§8).
## 1. 동작 불변 — 경로별 대조
| 옛 bash (old_decisions.sh) | 새 Python (wrapper.py) | 판단 |
|---|---|---|
| _map_sub_rc :72-90 | sub_status :58-66 | 같음. rc 3 은 디스크 종결 상태가 이김, 그 외 코드는 timeout |
| 구독자 조기 종료 판정 `[ $sub_exit -eq 0 ] \|\| [ … -eq 3 ]` | subscriber_up :69-70 | 같음 |
| _rc_for :92-101 + 호출부의 `MAM_DELEGATE_FAIL_CLOSED=0 → exit 0` | exit_code :77-78, fail_closed :73-74 | 같음. 빈 값은 1 로 보는 `${…:-1}` 와 `or "1"` 일치 |
| cmd_wait rc 4→2 :261 | wait_rc :81-82 | 같음 |
| start_stall_watch 의 감시 여부 :30-36 | stall_watch :89-99 | 같음. 메시지도 동일 |
| finalize_unfinished 앞부분: FAIL_CLOSED=0·completed 면 반환, stall.out 꼬리 3줄, 디스크 종결이면 FINAL=디스크(completed 면 반환) :105-123 | finalize_begin :161-187 (`keep` / `done completed` / `go\|probe <final> <evidence_until> <deadline>`) | 같음 |
| broker_unavailable: 마감까지 ≤3 s 간격 재관측, 매 회 시계로 마감 계산, 마감 도달 시 wall :128-146 | finalize_end :201-216 | 같음(`_sleep` 이 0.5 s 단위로 쪼개 소유자를 확인할 뿐, 대기 총량은 같다) |
| timeout: evidence_until 는 진단 **전**에 계산, 마감까지 디스크 재관측, 디스크 heartbeat 가 창을 다시 열고, 창이 지나면 종료, 마감 도달 시 wall :149-194 | finalize_begin 이 evidence_until 계산(:181) → bash 가 진단 프로브(래퍼 :137-142) → finalize_end :217-238 | 같음. 진단 순서도 같다 |
| 라벨: sub_rc 4 → wall, wall 이고 phase≠inject 면 wall_clock_timeout, preflight/idle_wait 는 cancelled + session_busy 또는 PREFLIGHT_REASON :199-209 | :242-251 | 같음 |
| close rc 4(토큰 불일치) → 이벤트 억제, FINAL 그대로 :216-224 | :256-262 | 같음 |
| close 뒤 디스크 completed 면 completed :226-232 | :263-264 | 같음 |
| direct 만 이벤트: cancelled 또는 (sub_rc≠1 또는 inject) 이면 error(inject 는 agent_blocked) :234-245 | :266-272 | 같음. 토큰 인자도 동일 |
| registry.py close 출력 `2>&1` 을 stdout 으로, publish_event stderr 는 버림 | _run merge=True/False :190-193, 답은 fd 3 | 같음 — 래퍼가 `3>&1 1>&4` 로 stdout 을 원래 stdout 으로 보낸다(래퍼 :145-149). subprocess 기본 close_fds 로 fd 3 은 자식에 새지 않는다 |
- 비교 테스트(test_c_*)가 status×sub_rc×phase 전 조합, 토큰 불일치(CLOSE_RC=4), FAIL_CLOSED=0, remote·human·loop·discuss, 늦은 completed, heartbeat 유예가 마감까지 가는 경우, 래퍼 사망(KILL·미회수 좀비)을 옛·새 양쪽으로 돌린다.
- 직접 돌려 본 미포함 경로: registry 경로에 작은따옴표(`reg it's`)가 있을 때 옛 `_disk_status` 는 경로를 Python 소스에 끼워 넣어 `""` 를 내고(rc 3 + 디스크 completed 를 broker_unavailable 로 오판), 새 sub_status 는 `completed`. 옛 버그가 고쳐진 차이이며 계약 위반이 아니다.
## 2. bash ↔ Python 경계
- `_decide`(래퍼 :97-99)는 `"$@"` 로 인자를 그대로 넘긴다. 빈 값(`--dispatch-token ""`, `--phase ""`, `--preflight-reason ""`)은 argparse 기본값과 같다. `-` 로 시작하는 값이 들어갈 수 있는 인자는 PREFLIGHT_REASON 뿐인데 값은 `target_not_found` 하나(래퍼 :850). `${START_TIME:+--start-time "$START_TIME"}` 는 빈 START_TIME 이면 인자를 빼고 Python 이 now 를 쓴다 — 옛 `${START_TIME:-$(date +%s)}` 와 같다.
- 한 줄 출력: finalize-begin 은 stdout 에 답 한 줄만, 진단은 stderr. finalize-end 는 fd 3 으로 답(fd 3 이 닫혀 있으면 stdout).
- 래퍼에 판정이 남지 않았다: test_s_the_wrapper_carries_no_moved_decision(주석 제외 소스에 _map_sub_rc·_rc_for·FINALIZE_CLOCK·`${MAM_DELEGATE_FAIL_CLOSED`·SUB_RC 비교·라벨 문자열 없음, lib_int.sh 에 BASH_REMATCH 없음).
- knob 환경변수: 옛 bash 는 셸 변수로, 새 Python 은 환경으로 읽는다. 래퍼가 이 두 knob 을 대입하지 않고 .mam.env 는 `export` 줄로 들어오므로(래퍼 :37-45, envfile.py :116) 차이가 없다.
- Python 실패 시: 관찰 N2.
## 3. finalize-end 의 --watch-pid
- `_check_owner` 가 두 대기의 매 반복(0.5 s 단위)과 close 직전(:256), 이벤트 직전(:267)에 래퍼 생존을 본다. 사라졌으면 OwnerGone → 닫기·발행 없이 종료(:338-339).
- `_alive`(:122-137): kill(0) 실패면 죽음, 성공이면 `ps -o stat= -p` 가 `Z…` 이거나 빈 출력이면 죽음. macOS 와 Linux 모두 좀비는 `Z`(macOS `Z`, Linux `Z`/`Z+`)로 시작하고, 사라진 pid 는 빈 출력이다. ps 를 못 돌리면 kill(0) 결과를 믿는다(살아 있음) — 닫는 쪽이 아니라 기다리는 쪽이지만, 래퍼가 사라진 뒤라면 대기는 마감에서 끝나고 닫기가 일어날 수 있다(ps 가 없을 때만, 비현실적).
- 래퍼에 TERM trap 이 없으므로(trap 은 부트스트랩 EXIT 용 :916-942 뿐) TERM 이 `$(…)` 대기 중 지연되는 문제는 없다. 래퍼가 TERM/KILL 로 사라지면 고아가 된 Python 이 0.5 s 안에 알아채고 끝난다 — 옛 bash 루프가 래퍼와 함께 끝나던 것과 같은 결과. 테스트 test_c_finalize_ends_with_its_wrapper / _with_an_unreaped_wrapper / test_c_an_unreaped_wrapper_leaves_the_real_record_alone 통과.
- 정상 경로 지연: 확인마다 ps 한 번(수 ms). 대기의 총길이는 옛과 같다.
## 4. 정수 규칙 (lib_py/ints.py)
- positive_int 는 runner.py 의 옛 _positive_int 를 그대로 옮긴 것이고(diff 상 동일 본문), 옛 bash mam_int 와도 같은 규칙이다: ASCII 공백 6종만 앞뒤 허용, ASCII 숫자만(`٥` 거부, 로케일 무관), 선행 0 제거 뒤 9자리 이하, 최솟값. knob 의 경고 문구도 동일. test_c_mam_int_and_knob 이 C·en_US.UTF-8 두 로케일에서 옛·새를 비교하고 통과.
- lib_int.sh 를 lib.sh 없이 source 하는 diff_collect.sh·loop_lock.sh: `_MAM_INTS_PY` 를 BASH_SOURCE 기준으로 잡으므로 동작한다(test_issue21_loop_b l15 는 그 배치를 재현하도록 lib_py/ints.py 도 복사). 비용·실패는 N3.
## 5. 수정된 정적 테스트 4개
약해지지 않았고 오히려 강해졌다: 각 테스트가 래퍼 소스의 `_decide …` 호출 문자열을 계속 고정하면서, 옮겨진 표 자체(EXIT_CODES, rc 0~5 의 sub_status, rc 3 의 디스크 우선, SUB_RC_* 와 래퍼 상수의 일치)를 실제 함수로 검사한다. test_issue26_ints 의 NOT_ARITHMETIC 에서 두 knob 을 뺀 것은 그 knob 들이 셸에서 사라졌기 때문이며 정적 가드가 재유입을 막는다.
## 6. 성능
- `_decide` 한 번 ≈ 50 ms(5회 0.25 s), 그중 약 29 ms 는 `lib_py.delegation` 패키지 import(N1). 잡 하나당 호출은 정상 경로에서 3~5회(sub-status, exit-code, stall-watch, 미완료 시 finalize-begin/-end).
- `mam_int`/`mam_int_knob` 는 호출당 ≈ 47 ms(10회 0.47 s; 옛 bash 는 ≈ 0.1 ms). 래퍼 기동 시 knob 4~5개 + 플래그, send_keys 경로의 knob 몇 개. 모두 폴링 루프 밖(마감 계산은 1회)이라 잡당 1 s 미만이며 에이전트 턴 시간에 비해 무시할 수준.
## 7. 이식성
이 macOS·/usr/bin/python3 3.9.6·bash 3.2 에서 지정 9개 파일 통과. wrapper.py·ints.py 는 3.9 문법(`from __future__ import annotations`, typing.List), /proc 없음(ps 사용), str.isascii(3.7+).
## 8. 비차단 관찰
- **N1** wrapper.py 머리말의 "Stdlib only" 와 달리 `python -m lib_py.delegation.wrapper` 는 패키지 `lib_py/delegation/__init__.py` 를 먼저 import 해 service → mqtt_common → **paho** 를 싣는다(paho 없는 python3 에서 `ModuleNotFoundError: paho` 확인). 래퍼의 `$PY` 는 원래 paho 가 필요하므로 동작에는 문제없지만, 문서 정정이나 패키지 밖 모듈(ints.py 처럼 경로 실행)로 두면 호출당 ~29 ms 도 줄어든다.
- **N2** `_decide` 가 실패하면(모듈 import 오류 등) `exit "$(_decide exit-code …)"` 는 `exit ""` → bash 3.2 에서 rc 255("numeric argument required"), finalize 도 실패해 잡이 열린 채 남는다. 옛 코드도 같은 `$PY` 로 registry.py 를 돌렸으므로 실질 회귀는 아니지만, `exit "${rc:-1}"` 같은 기본값이 있으면 fail-closed 코드(1)가 보장된다.
- **N3** lib_int.sh 는 `$PY` 가 아니라 PATH 의 `python3 -I` 를 쓴다. python3 가 없으면 mam_int_knob 이 아무것도 출력하지 않아(옛 bash 는 항상 기본값 출력) `$(( $(date +%s) + $(mam_int_knob …) ))`(lib.sh:2783 등)가 산술 오류가 된다. MAM 은 python3 를 이미 필수로 요구하므로 현실적 위험은 낮다.
- **N4** 옛 `_disk_status` 의 작은따옴표 경로 버그(§1)가 새 코드에서 고쳐졌다 — 동작 차이지만 개선.
- **N5** 브리프·PR 설명의 "0.2 s 마다 확인"과 달리 코드는 0.5 s(`_sleep`, :145-153). 설명 정정만 필요.
## 9. 테스트
검증 경로에서 지정 9개 파일(pr-51 .venv, Python 3.9.6): **604 passed / 1 skipped / 0 failed** (1009 s, rc 0). skip = test_issue30_stall_watch.py:340 "real process trees via /proc"(Linux 전용, 기존). MQTT_* unset, .mam.env 미export. 실제 .mam/jobs 항목 수 변화 없음(1052 → 1052).
[VERDICT: PASS]