Merge-workspace review: round 1 (@518beda) claude agy grok PASS, codex NOT PASS (finalize wait measured on the wall clock); fixed in4c86107. 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 at4c86107: 278 passed. Full suite at518bedaon 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
11 KiB
PR #51 — Codex review, job aa885e48
판정: NOT PASS
일반 입력과 안정적인 시스템 시각에서는 기존/새 판정의 비교 검사들이 일치했다. 그러나 새 _sleep이 경과 시간 측정에 wall clock을 사용해, 시스템 시각이 뒤로 이동할 때 두 finalize 재관측 간격이 달라진다. 핵심 계약인 동작 불변을 충족하려면 R1을 수정해야 한다. 수정 범위는 작은 helper와 회귀 검사이며 재계획은 필요하지 않다.
Identity / 범위
- loop:
loop-20261007T142403Z-10d77f - base:
7209750ac7e3ab9a7f6b0abc3670dbe0b1060f88 - head:
518bedac5d8ab832347bfc6682b146f3201021f1 - tree:
137b4e46c58b4f27e03dc19e6f9c6b42f574baa3 - 검증 cwd:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T142403Z-10d77f/1 - base..head 7개 커밋의 변경 전체를 검토했다. 제품 소스 수정·커밋·브랜치 변경은 하지 않았다.
- 검증 중 별도 탐침의 workspace 격리 실수가 있었다. 아래 “검증 과정의 격리 오류”에 별도로 기록한다.
R1 — P2: wall-clock 역행이 짧은 sleep까지 늘려 완료/heartbeat 재관측을 지연시킨다
근거: .agents/skills/lib_py/delegation/wrapper.py:145–153은 end = time.time() + seconds와 end - time.time()으로 sleep의 남은 시간을 계산한다. 이를 broker 재관측(:208, 최대 3초)과 디스크 heartbeat 유예(:236, 1초)가 모두 사용한다.
기존 bash는 해당 위치에서 sleep "$left" 또는 sleep 1을 호출한 다음 디스크를 다시 읽는다. 새 구현은 그 짧은 sleep 도중 wall clock이 뒤로 이동하면 되돌아간 시간까지 기다린 후에야 레코드를 읽는다. 잡의 wall-clock deadline이 이동된 시각에 맞춰 판단되는 기존 정책과, 개별 sleep 자체가 늘어나는 것은 별개의 동작이다.
재현 조건/절차:
tests/data/issue17_item4_old_decisions.sh의 실제 옛finalize_unfinished를 실행한다. 이 파일의 함수 9개는git show 7209750의 대응 함수와 바이트 단위로 일치함을 확인했다.- 실제 시스템 시각 대신 모의 시각을 사용한다. 시작 wall time=100, START_TIME=100, TIMEOUT=60, IDLE_TIMEOUT=10. 첫 sleep 중 wall clock만 10초 뒤로 이동하고, elapsed time은 정상 증가시킨다.
- 디스크 레코드는 elapsed 1초부터 completed를 반환하게 한다.
- 같은 입력으로 새
finalize_end를 실행한다. 두 경로 모두 실제 레지스트리/브로커/서버에 접근하지 않는다.
| 경로 | 옛 bash가 completed를 확인한 elapsed | 새 Python이 확인한 elapsed |
|---|---|---|
| broker_unavailable, 3초 재관측 | 3초 | 13초 |
| timeout, 1초 디스크 유예 재관측 | 1초 | 11초 |
기대: 기존과 같이 짧은 sleep의 경과 시간 뒤 디스크를 다시 읽고, 그 시점의 wall clock으로 잡 deadline/evidence를 판단한다.
실제: 이미 completed인 잡을 새 코드만 추가 10초 기다린다. 시각 보정 폭이 커지면 지연도 그만큼 커진다. 기존 비교 테스트는 안정적인 시각만 사용하므로 이 차이를 잡지 않는다.
수정 방향: _sleep의 내부 end/remaining만 time.monotonic()으로 계산한다. _now(), START_TIME+TIMEOUT, 이벤트 timestamp, evidence_until의 wall-clock 정책은 그대로 유지한다. 모의 wall clock 역행 중에도 1초/3초 간격으로 디스크를 재관측하는 비교 회귀 검사를 추가한다.
재현 파일: /tmp/aa885e48-clock.py
결과: /tmp/aa885e48-clock.log
검증 cwd에서 재실행:
PYTHONDONTWRITEBYTECODE=1 /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-51/.venv/bin/python /tmp/aa885e48-clock.py
호스트 시계는 변경하지 않았다. 표의 시간은 모의 elapsed time이다.
브리프 검토 항목
1. 옛/새 판정과 finalize
- fixture의 mam_int, mam_int_knob, start_stall_watch, _disk_status, _recent_disk_heartbeat, _map_sub_rc, _rc_for, finalize_unfinished, cmd_wait 총 9개 함수를 실제 base와 비교해 일치를 확인했다.
- 일반 finalize 라벨·phase별 cancelled 이유·inject의 agent_blocked·direct만 이벤트 발행·loop/discuss의 이벤트 억제·dispatch token mismatch rc=4 시 후속 이벤트 억제는 비교 검사에서 보존됐다.
- FAIL_CLOSED=0, completed 레코드 조기 종료, terminal error/cancelled, remote/human 분기도 확인했다.
- 기존 스위트 외에 preflight detail의 공백/따옴표/줄바꿈/빈 값 4건과 broker 대기 중 error/cancelled/completed 도착 3건을 옛/새로 직접 비교했다. stdout, stderr, FINAL_JOB_STATUS, registry/publish 호출이 모두 일치했다.
- 단 두 대기의 시계 역행 동작은 R1처럼 달라진다.
2. bash ↔ Python 경계
multi-agent-mux-delegate-job:97–101의 _decide는 인자를 "$@"로 넘긴다. 일반 값의 공백과 따옴표는 유지되며, :805–831의 begin 단일 행과 end fd 3 결과 분리는 비교 검사에서 close/publish stdout과 상태가 섞이지 않았다.
Python 경로를 존재하지 않는 경로로 바꾼 추가 탐침에서 exit-code, wait-rc, finalize는 각각 nonzero(이 호스트 bash에서 255/127/127)로 끝나 성공으로 바뀌지 않았다. begin의 command substitution 실패는 read 자체에 가려질 수 있지만, 뒤의 end 실행 실패로 정상 성공 상태를 반환하지 않았다. start_stall_watch의 판정 실패는 기존의 notify-only 감시 기동 실패와 마찬가지로 감시 없이 반환하므로 모든 보조 경로가 job을 중단하는 정책은 아니다.
판정은 wrapper.py로 이동했고 bash에는 실행/대기/프로세스 정리, diagnostic probe, CLI 흐름이 남았다. 새 int 모듈의 직접 실행은 -I로 격리되며 lib.sh 없이 lib_int.sh만 source한 실행도 확인했다.
3. watch-pid와 종료
wrapper.py:122–153,256,267 부근의 owner 검사는 kill(0) 뒤 ps stat의 Z 접두사를 검사하고, 대기 중과 부작용 직전에 owner가 사라지면 종료한다. 제공된 TERM/미회수 zombie 검사와 추가 KILL 비교를 확인했다.
추가 KILL 탐침은 broker 대기와 timeout 유예 각각에서 옛/새 wrapper만 종료했다. 양쪽 모두 deadline 이후에도 close/publish 호출이 없고 레코드가 그대로였다. 새 코드의 캡처 파이프 종료는 약 0.04초/0.18초, 옛 코드는 약 2.20초/0.39초였다. 옛 코드의 남은 sleep 때문에 최초 자체 탐침의 2초 기준은 너무 짧았으며, 기존 비교 검사의 4.5초 기준으로 바로잡아 재확인했다. 이는 제품 실패로 집계하지 않았다.
실제 polling 구현은 브리프의 0.2초가 아니라 최대 0.5초 간격이다. 이 간격 자체에서는 차단 결함을 찾지 않았다. macOS에서 실행 검증했고 Linux는 직접 실행하지 않았으므로 Linux 실측 통과를 주장하지 않는다.
4. 정수 규칙
ints.positive_int는 이전 runner의 구현과 같은 ASCII 공백/숫자, 선행 0 제거, 유효 9자리 제한, minimum 검사를 유지한다. 옛 bash와 새 adapter의 C/en_US.UTF-8, 유니코드 공백/숫자, 0 허용 여부 및 경계 입력 비교를 확인했다. lib_int.sh의 두 함수는 Python 호출로 얇아졌고, runner도 같은 모듈을 사용한다. minimum/default가 계약 범위의 값일 때 추가 차이를 찾지 못했다.
5. 정적 테스트 변경
- issue5: 옛 bash 문자열 대신 호출 연결, Python EXIT_CODES, 상태 매핑, subscriber 상수 일치를 검사한다.
- tier1 G10: broker unavailable 및 디스크 completed 우선 동작을 실행 검사해 기존 문자열 검사보다 의미를 직접 고정한다.
- issue26: Python으로 옮긴 두 환경변수를 shell-only 분류표에서 제거했으며 새 단위/비교 테이블이 해당 정책을 확인한다.
- issue21 L15: 독립 복사 fixture에 ints.py를 같이 복사한다. 기존 diff-only 거부/로컬 허용 assertion은 유지됐고 이 테스트를 별도로 실행했다.
- #42 인벤토리는 근거 줄 번호만 바뀌었다. 정책 분류 변경은 없다.
6. 성능
이 호스트에서 정수 판정 100회의 경과 시간은 옛 bash 약 0.0056초, 새 Python adapter 약 4.392초였다. _decide exit-code completed 20회의 프로세스 실행 중앙값은 0.0474초(0.0459–0.0493초)였다. 기존 shell-only 판단보다 시작 비용은 분명히 늘었다.
기본 direct 완료 경로는 초기 knob 4회 + 기본 정수 flag 검증 4회 + stall-watch/sub-status/finalize-begin/exit-code 4회로 약 12회의 새 Python 실행이 발생한다. explicit idle-timeout, 조기 subscriber 종료 판정, 미완료 finalize-end, delivery/preflight knob 경로에서 더 늘어난다. 기본 구간만 대략 0.5–0.6초 추가이며 1초 안팎의 짧은 잡에서는 비율이 크다. 반면 긴 finalize 대기에서는 기존의 반복 Python 디스크 probe/date 프로세스를 인프로세스 읽기로 바꿨으므로 비용이 단순히 추가되기만 하는 것은 아니다. 지정 예산/통합 검사 결과를 함께 보았고, 이 측정 자체를 차단 사유로 삼지는 않았다.
7. 이식성/검증
실제 macOS, Python 3.9, bash 3.2에서 검증했다. 변경된 Python core는 해당 인터프리터 AST parse 통과, 두 변경 셸 파일 bash -n 통과, git diff --check 통과다.
테스트 결과
| 검증 | 결과 |
|---|---|
| 브리프의 9개 파일, 지정 Python 3.9 환경 | 604 passed, 1 skipped, 1005.83초 |
| 수정된 issue21 L15 검사 추가 실행 | 1 passed, 1.08초 |
| 옛 함수 원본 일치 확인 | 9개 함수 바이트 일치 |
| 추가 정상 입력/늦은 종결 비교 | 7건 옛/새 일치 |
| KILL 시 broker/timeout 두 경로 | 옛/새 모두 close·발행 없음 |
| Python 실행 불가 경계 | 상태/종료 판정 3경로 모두 nonzero |
| 시계 역행 비교 | 두 경로의 재관측 지연 차이 재현 — R1 |
| Python AST / bash -n / diff --check | 통과 |
검증 종료 후 HEAD/tree가 브리프와 일치했고 추적 파일의 git status --porcelain 출력은 없었다. 전체 저장소 스위트는 요청대로 다시 실행하지 않았다.
추가 자료:
- 전체 지정 선택 검사:
/tmp/aa885e48-selected.log - 변경된 L15 추가 검사:
/tmp/aa885e48-l15.log - KILL/실행 실패/정수 성능 탐침:
/tmp/aa885e48-boundary.py,/tmp/aa885e48-boundary-v2.log - 시계 역행 비교:
/tmp/aa885e48-clock.py,/tmp/aa885e48-clock.log
검증 과정의 격리 오류
추가 7개 옛/새 비교를 pytest 밖에서 호출한 첫 탐침에서, 상속된 WORKSPACE_ROOT를 제거하지 않은 제 실수가 있었다. 그 값은 검증 저장소가 아닌 /Users/godopu16/PuKi/lac/curriculum/00_entry였고, wrapper source 시 lib.sh의 자동 초기화가 /Users/godopu16/PuKi/lac/curriculum/00_entry/.mam/shim/herdr 생성 파일을 다시 썼다. 이 경로 변경은 사용자에게 즉시 알렸다.
해당 탐침의 registry/publish는 stand-in으로 가로챘고 herdr 진단 함수도 stub이었다. 실제 에이전트/서버 작업은 하지 않았다. 다만 그 생성 파일의 실행 전 사본을 확보하지 않았으므로 이전 내용과 동일했는지, 원복됐는지는 주장하지 않는다. 공유 파일을 추측으로 덮어써 복구하지 않았다. 이후 탐침은 HERDR/MAM/MQTT 환경을 제거하고 임시 WORKSPACE_ROOT/HOME을 명시했다. 이 오류는 PR 제품 결함 R1과 별개의 검증 절차 문제다.
[VERDICT: NOT PASS]