Issue #34: the loop waits for the creator job's sub-jobs, reviewers get the sub-job table as evidence, a run with sub-jobs voids a round whose source changed during review, and a pane caller reaches an agent of another workspace on its own server. VERSIONS.md carries the entry; the merge-side reports (round 1 @ed293dc and round 2 @e80186f with identity sidecars, baseed293dc-> heade80186f, evidence review-tree) are archived under .agents/reports/<session>/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
11 KiB
PR #38 독립 리뷰 — c1dec920
판정: NOT PASS. 새 하위 잡 분류 코드가 실제 종결 이벤트 생산자/조회자의 계약과 맞지 않는 P2 결함 두 건이 재현됐다. 기존 테스트의 실패와 구분하여 아래에 기록한다. 설계 재계획이 필요한 사안은 아니며, 종결 사유 조회·정규화와 회귀 테스트 추가로 수정할 수 있다.
검증 대상
- 역할: planner,reviewer / 이번 잡은 Reviewer.
- base:
7a4a0c209195c20719a50e28398416ccd19004e2 - head:
ed293dcef4c346e0050a31cb46812bcdcf1d67cf - tree:
671b0e59eef915f89d1dce63e9c0796db132899c - 검증 경로:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T055502Z-a37cf1/1 - 아래 파일:라인은 이 HEAD 기준이다. 검증 전후 HEAD/tree가 일치하고
git status --short는 비어 있다. 제품 코드 수정, 커밋, 실제 herdr 세션 조작은 하지 않았다. 탐침·수정 방향 검증·역검증은 임시 자료 또는 일회성 Python 프로세스에서 수행했다.
R1 — P2: 취소 이벤트의 사유를 읽지 못해 NOT-DISPATCHED 분기가 작동하지 않음
근거
.agents/skills/lib_py/loop/runner.py:687:registry.get_feedback()에서 종결 사유를 가져온다.- 같은 파일
:693: cancelled + session_busy/target_not_found/rolled back 사유일 때만 NOT-DISPATCHED로 분류한다. .agents/skills/lib_py/delegation/registry.py:325,:327,:342: audit 및 로컬 events.log 경로 모두 completed/error만 읽고 cancelled는 제외한다.- 실제 생산자는
.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:926에서 cancelled를 발행한다. create rollback도.agents/skills/multi-agent-mux-create/scripts/create_session.sh:340에서 cancelled +rolled back by create_session을 발행한다.
재현 절차
- 임시 HOME/WORKSPACE_ROOT/registry/audit 경로를 만들고 브로커를 127.0.0.1:9로 설정한다.
- 실제 delegate-job 래퍼의
submit --agent human --agent-session herdr:img --dry-run --registry-dir <tmp-reg> --prompt probe로 부모를 등록한다. 같은 명령에--parent-job <parent>를 추가해 자식을 각각 등록한다. - 자식 레코드의 dispatch token과 attempt를 사용해 실제
publish_event.py --registry-dir <tmp-reg> --job <child> --expect-dispatch-token <token> --attempt-id 1 --event cancelled --detail <reason>를 실행한다. - 같은 registry/audit 환경에서
LoopRunner._subjob_state(registry.load_job(child, registry_dir))및_subjobs_block([parent])를 읽는다.
| 발행한 detail | 기대 상태·사유 | 실제 상태·사유 |
|---|---|---|
| session_busy | NOT-DISPATCHED / session_busy | cancelled / - |
| target_not_found | NOT-DISPATCHED / target_not_found | cancelled / - |
| rolled back by create_session | NOT-DISPATCHED / rolled back by create_session | cancelled / - |
세 경우 모두 레코드는 cancelled이고 events.log에 정확한 detail이 남아 있다. 브로커 연결 거절로 publisher rc=2지만 로컬 종결·로그 기록은 완료됐다. get_feedback()만 빈 문자열을 반환한다. 리뷰 표에서는 세 사유가 모두 사라진다.
영향 및 수정 방향
리뷰어가 실제 실행 후 실패와 애초에 전달되지 않은 잡을 구분할 수 없다. 현재는 둘 다 실패 건수에 포함되므로 이 문제만으로 자동 PASS가 되는 것은 아니다. 그러나 이번 PR이 명시한 상태/사유 근거 제공 계약을 충족하지 못한다.
종결 사유 조회가 cancelled도 처리하도록 하거나, 기존 completed/error feedback 계약을 유지해야 한다면 세 terminal 이벤트를 다루는 별도 조회 함수를 새 소비자에서 사용해야 한다. audit와 로컬 events.log fallback을 모두 검사하는 회귀 테스트를 추가한다. 새 T12는 cancelled 이벤트의 존재만 검사하고 그 이벤트가 리뷰 표에서 어떻게 분류되는지는 검사하지 않는다.
임시 Python 프로세스에서 get_feedback의 세 이벤트 필터에 cancelled를 포함시킨 뒤 같은 저장 레코드 세 건이 모두 기대한 NOT-DISPATCHED + 원래 사유로 분류됨을 확인했다. 소스 파일은 변경하지 않았다.
재현 자료: /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/c1dec920-wrapper-probe-_555g31h/review-block.md 및 인접 reg/, .mam-logs/.
R2 — P2: 원격 러너의 실제 deadline 사유를 CLOSED-UNCONFIRMED로 인식하지 않음
근거
.agents/skills/lib_py/loop/runner.py:36: 허용 사유는 정확히 deadline_timeout/agent_blocked 두 문자열이다.- 같은 파일
:690: 문자열 완전 일치로 미확인 종결을 판단한다. .agents/skills/lib_py/delegation/remote.py:352: 실행 명령 없이 외부 에이전트의 종료를 기다리는 원격 러너는 제한 시간이 지나면deadline_timeout (remote runner)라는 detail로 error를 발행한다. 외부 에이전트를 중단시키는 경로는 아니다.
재현 절차
- 임시 registry/audit와 127.0.0.1:9 브로커 환경에서 UUID4 job_id/dispatch_token, attempt_id=1, timeout_sec=1인 dispatch를 만든다.
- 실제
remote.handle_dispatch(dispatch, registry_dir, "probe", None)를 실행한다. 테스트에서는 heartbeat 타이머 생성만 생략하도록remote._publish를 감싸고, started/error 발행·로컬 기록은 실제 producer를 사용한다. 실제 에이전트는 실행하지 않는다. - 종료 레코드를
LoopRunner._subjob_state()에 전달한다.
실측:
runner rc: 2
record status: error
get_feedback: deadline_timeout (remote runner)
expected: CLOSED-UNCONFIRMED / deadline_timeout (remote runner)
actual: error / deadline_timeout (remote runner)
영향 및 수정 방향
이 원격 마감은 에이전트가 계속 작업할 수 있는 미확인 종결인데, 일반 error로 집계되어 미확인 종결 수와 해당 경고가 누락된다. error도 실패 규칙의 대상이고 하위 잡이 있는 경우 지문 가드는 켜지므로, 이 사례만으로 거짓 PASS가 발생했다고 주장하지는 않는다.
실제 생산자의 사유를 canonical reason으로 정규화하거나, 알려진 원격 마감 문자열을 분류기에 포함시킨다. 임의의 모든 error를 마감으로 취급해서는 안 된다. 실제 handle_dispatch(..., exec_cmd=None)의 마감 결과를 소비하는 회귀 테스트가 필요하다. 일회성 Python 프로세스에서 이 정확한 문자열을 인식하게 했을 때 기대한 CLOSED-UNCONFIRMED가 됨을 확인했다.
실제 producer 탐침 자료: /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/c1dec920-extra-uj1j3nhp/.
검증 결과 및 범위
실행 인터프리터는 브리프의 main venv Python이며, 검증 경로에서 AGENT_PYTHON_BIN=<해당 Python> PYTHONDONTWRITEBYTECODE=1 python -m pytest -q -p no:cacheprovider --basetemp=<검증 경로 밖 임시 경로>를 사용했다.
선택 파일:
test_issue34_job_tree.py— 신규 17개 사례 포함test_issue21_worktree_prereqs.py,test_issue21_phase2_loop_a.py,test_issue21_loop_b.py,test_issue21_acceptance.pytest_issue5_phase1_delegation.pytest_tier4_e2e.pytest_issue23_24_startup_path.pytest_issue7_workspace_packing.py,test_herdr_shim_contract.py
총 287개 서로 다른 선택 항목의 통과를 확인했다. 한 번의 무조건적인 전체 green 실행이라는 의미는 아니며, 실행 조건과 원시 결과는 다음과 같다.
| 실행 | 결과 |
|---|---|
| 선택 묶음 최초 집계 | 284 passed, 3 failed / 894.44s |
| Issue21 L11, A4 재실행 | 2 passed / 2.26s |
| Tier4 scenario5 별도 재실행 | 1 passed / 338.00s, MAM_INJECT_ACK_GRACE_S=1 |
| 변경 셸 bash -n, 변경 Python AST 구문 검사 | 통과 |
| git diff --check base..HEAD | 통과 |
L11/A4의 실패는 테스트가 mam-review 문자열이 없어야 한다고 검사하는데 basetemp 이름을 mam-review-c1dec920.*로 정했기 때문이다. 코드의 실제 경로 누출/정리 실패가 아니며, 이 문자열이 없는 /tmp/c1dec920-rerun.*에서 두 항목이 통과했다.
최초 Tier4 scenario5는 모의 에이전트가 started를 발행하지 않아 잡마다 180초 ACK 대기를 하는 것을 확인하고 해당 샌드박스 loop 프로세스 트리만 중단했다. 따라서 최초 실패의 rc=143은 제품 결함 판정에 사용하지 않았다. 같은 테스트를 ACK grace=1로 재실행해 실제 두 리뷰 회차와 최종 PASS까지 통과했다. 기본 180초 설정으로 끝까지 완료한 결과는 주장하지 않는다.
역검증은 임시 skills 사본에서 runner.py만 base 버전으로 교체하여 신규 T3와 T5b[True]를 실행했다. 2 failed / 10.45s로, 자손 대기와 CLOSED-UNCONFIRMED 리뷰 근거가 없다는 이유로 실패했다. 제품 파일에는 역변경을 적용하지 않았다.
추가 확인:
- 실제 래퍼/등록부로 부모 flag, 존재하지 않는 부모 거절, MAM_JOB_ID fallback, 직접 자식 목록을 검사하는 신규 테스트가 통과했다. 깊이는 조회 시 계산된다. 순환/자기 참조는 조회의 visited 집합으로 무한 탐색을 방지하며, Python register_job API 자체가 모든 잘못된 parent 입력을 거부하는 것은 아니다.
- 503개 임시 레코드(자손 502개) 탐침에서 깊이 1/2/3과 자기 참조 배제가 맞았고, 조회는 약 0.0098초였다. 매 폴링 전체 스캔은 남아 있다. MAM_JOB_TREE_MAX는 스캔 자체의 상한이 아니라 대기를 중단하는 상한이며, 이후 리뷰 표에서 열린 잡을 숨기지 않는다.
- 실행·수정·순응 경로의 wait_for_job_tree 연결과 이전 creator 잡 자손의 후속 회차 노출을 확인했다. 자손 대기 시간/개수 초과는 루프 즉시 실패가 아니라 리뷰 근거로 처리하는 설계다. 실제 signal handler는 checkpoint 후 exit한다.
- 자손과 귀속 불명 열린 잡이 모두 없으면 prompt 추가 블록은 빈 문자열이다. 자신의 created_jobs는 제외한다. 다른 루프의 부모 없는 새 열린 잡은 시간 기반 휴리스틱으로 나타날 수 있다. 문서의 기본 wait-open-jobs=off 및 무관한 잡에 대한 경고와 일치하지만, 이 목록을 해당 creator 소유임이 입증된 목록으로 해석해서는 안 된다.
- 임시 Git 저장소에서 .mam, jobs, 상대경로 audit 파일 변경은 fingerprint를 바꾸지 않고 소스 변경은 바꾸는 것을 확인했다. 소스 밖 제어 경로 제외와 identity 모드 분리는 코드로 확인했다.
- 같은 서버 다른 workspace 조회, 명시적 불일치/외부 pane scope 보존, 기존 교차 서버 규칙, workspace packing, shim 계약 테스트가 통과했다.
- parent 안내문 및 loop 문서는 주요 대기/리뷰/지문 동작을 설명한다. 위 R1/R2는 그 설명과 실제 terminal reason 소비 계약의 차이다.
- 예정된 4.7.4 버전 bump 부재는 거절 사유로 삼지 않았다.
두 종결 분류 결함을 수정하고 실제 producer → reason reader → 리뷰 표까지 잇는 회귀 테스트를 추가한 뒤 재리뷰해야 한다.
[VERDICT: NOT PASS]