Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
9.8 KiB
9.8 KiB
PR #82 (Issue #78) 리뷰 - 새 루프 3라운드 (b49314c) - planner-reviewer-claude-01
- 검증 경로:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261029T184637Z-6f4228/3 - HEAD
b49314c3d039027d3f9478cacecfda587fdc8abc, treed49ff60bbf89437e997f7da21e2fae8df570456b, basea3364e8. - 리뷰 시작과 끝에서 확인한 것: HEAD·tree 가 [리뷰 identity] 와 같고,
git status --porcelain --ignored가 비어 있고,.mam이 없다. - 브리프는 이전 라운드(4b82e70c)와 identity(head·tree·검증 경로)와 커밋 수만 다르다. diff 는 잘려 있어 스냅샷을 직접 봤다.
- 2라운드(
568385c, 내가 PASS) 뒤 새 커밋은b49314c하나다. 6개 파일, +213/-74.lib_py/delegation/registry.py,lib_py/reset.py,lib_py/update.py,SKILL.md,reset.sh,tests/test_issue78_reset.py- 커밋 메시지에 따르면 codex 4라운드의 R1(다른 reset 의 fence 를 덮어씀, 내 N8 과 같은 지점)과 R2(없는 행 레지스트리를 fence 하지 않음)를 고친 것이다.
- 그 밖의 파일은 바뀌지 않았다. 이전 리포트(57b76c9a, ee7ae43d, c15c85f2, 4b82e70c)의 확인이 그대로 유효하다.
이번 변경
| 확인 | 근거(파일:줄) | 결과 |
|---|---|---|
| R1: 다른 live reset 의 fence 를 덮어쓰지 않는가 | reset.py:492-519 fence_registries: registry lock 안에서 reset_marker(d) 를 읽고(registry.py:97-117), live·unknown 이면서 pid 가 자기 것이 아니면 FenceBusy 를 던진다(reset.py:510-514). stale 마커만 교체한다. |
덮어쓰지 않는다. 확인과 쓰기가 같은 lock 안이라 두 reset 이 동시에 같은 레지스트리를 잡을 수 없다. 2라운드에서 내가 재현한 N8(B 가 A 의 마커를 덮어쓰고 지움)은 B 가 거부되는 것으로 막힌다. |
| 거부 시 "nothing was changed" 가 맞는가 | reset.sh:150-164: fenced <path> 줄은 FENCED 에 넣고, refused <msg> 줄은 메시지로 쓴 뒤 exit 1. EXIT trap(reset.sh:118-125)이 FENCED 중 pid 가 $$ 인 마커만 지운다. |
거부 전에 fence 한 자기 레지스트리는 trap 이 지운다. 다른 reset 의 마커는 pid 가 달라 남는다. 새 테스트가 B 의 loop marker·reset marker 도 남지 않음을 본다. |
| 지울 때 경합 | reset.sh:118-125 의 확인과 rm 은 lock 밖이다. |
내 마커가 live 인 동안 다른 fence 는 그것을 교체하지 못하므로(R1), 확인과 rm 사이에 남의 마커로 바뀌지 않는다. 문제없다. |
| 교착·양쪽 거부 | 디렉터리마다 짧게 lock 을 잡는다. | A 가 D1→D2, B 가 D2→D1 순서로 잡으면 둘 다 FenceBusy 로 거부될 수 있다. 교착은 없고 둘 다 아무것도 바꾸지 않은 채 끝나므로 다시 실행하면 된다. 비차단. |
reset_refusal 리팩터 |
registry.py:120-135 |
판정이 2라운드와 같다. absent·stale 이면 통과하고, sessions 필터, owner pid, unknown 이면 거부, live 이면 거부. 바뀐 것은 owner 확인이 ps 뒤로 간 것뿐이다. reset 자신의 create 에서만 ps 를 두 번 더 부른다. 마커가 없는 일반 등록은 여전히 ps 를 부르지 않는다. |
| R2: 없는 레지스트리도 fence 하는가 | update.py:150-163 jobs_dirs 가 존재 여부와 무관하게 (dir, addressed_only) 를 realpath 로 한 번씩 낸다. reset.py:507-508 이 각 dir 을 os.makedirs 한 뒤 fence 한다. |
등록도 registry_lock 이 mkdir(parents=True) 로 디렉터리를 만든다(mqtt_common.py:359). 그러므로 없는 레지스트리를 만들고 fence 하는 것이 맞다. 만들 수 없으면 OSError → refused ... → reset 거부(fail-closed). |
open_jobs 동작이 바뀌었나 |
update.py:166-185: jobs_dirs 로 열거한 뒤 _is_jobs_dir 가 False 면 건너뛴다. |
이전과 같은 디렉터리 집합·순서·중복 제거다(전에는 _is_jobs_dir 를 중복 제거 전에 봤지만, 없는 경로끼리 realpath 가 같아도 결과는 같다). update.sh 의 precheck 도 이 함수를 쓰므로 test_issue72_update.py 를 함께 돌렸다(아래, 통과). |
| SKILL.md 대조 | SKILL.md:26의 "created when absent", "다른 작업 공간의 live reset 이 이미 fence 한 공유 레지스트리면 아무것도 바꾸지 않고 거부", "다른 reset 의 마커를 교체·삭제하지 않음" |
reset.py:507-519, reset.sh:118-125,150-164 와 맞다. SKILL.md 에만 있고 코드로 강제되지 않는 규칙은 새로 생기지 않았다. |
실행한 테스트 (이 Mac, Python 3.9.6, /bin/bash 3.2)
명령: env -u HERDR_SOCKET_PATH -u HERDR_PANE_ID -u HERDR_TAB_ID -u HERDR_WORKSPACE_ID -u HERDR_SESSION_NAME -u HERDR_SESSION -u WORKSPACE_ROOT -u MAM_ENV_FILE -u MAM_JOBS_DIR -u DELEGATE_JOB_LOGS_DIR PYTHONDONTWRITEBYTECODE=1 <본 체크아웃 .venv/bin/python> -m pytest -p no:cacheprovider -q --basetemp=$TMPDIR/j18bt.*/…
- 리뷰 트리에는 .venv 가 없어 본 체크아웃의 .venv 인터프리터를 썼다.
&없이 띄웠다(SIGINT 상속 문제 회피). - 처음에 파일 이름을 잘못 넣어(
test_issue55_update.py, 없는 파일) A 묶음이 0건으로 끝났다. 올바른test_issue72_update.py로 다시 돌린 결과가 아래다. 다시 돌린 A 는 B 와 일부 시간이 겹쳤지만 모두 통과했다.
| 대상 | 결과 |
|---|---|
A: test_issue78_reset.py + test_issue78_hints_codex.py + test_issue42_skill_blocks.py + test_issue72_update.py (시그널 테스트, 새 [row-absent], test_other_reset_cannot_take_a_live_fence[False/True] 포함) |
134 passed, 1 skipped (819 s) |
B: delegation 8개 파일 (test_issue5_phase1_delegation.py test_issue12_registry_secrets.py test_issue14_delivery_ack.py test_issue18_liveness_contract.py test_issue34_job_tree.py test_issue50_onboard.py test_tier1_unit.py test_issue6_fail_closed.py) |
249 passed (635 s) |
git archive 사본 mutation (모두 대상 테스트가 실패한다):
| mutation | 실패한 테스트 |
|---|---|
| m1 fence 의 FenceBusy 검사 제거(남의 live 마커를 덮어씀) | test_other_reset_cannot_take_a_live_fence[False], [True] (2 failed) |
| m3 없는 행 레지스트리를 건너뜀 | test_registry_cli_is_refused_after_the_target_recheck[row-absent], test_fence_waits_for_a_registration_and_covers_every_registry_read (2 failed) |
| m4 stale 마커도 busy 로 봄 | test_fence_waits_… (1 failed, stale 교체 검증) |
m5 reset.sh 가 fenced 접두어를 벗기지 않음(종료 시 마커가 안 지워짐) |
test_registry_cli_is_refused_after_the_target_recheck[canonical] (1 failed) |
추가 probe (임시 디렉터리, lib_py.reset fence 직접 실행):
- terminated 행의
control_plane.mam_jobs_dir가 지워진(쓸 수 있는) 경로를 가리키면: rc 0. 그 경로(gone-ws/.mam/jobs, 부모 포함)가 새로 만들어지고, reset 이 끝난 뒤에도 빈 디렉터리로 남는다. - 만들 수 없는 경로(
/nonexistent-root/...)면: rc 1,refused the job registries cannot be fenced against new registrations ([Errno 30] Read-only file system: '/nonexistent-root'). 먼저 fence 한 자기.mam/jobs는 출력되어 trap 이 지운다.
프로세스: 테스트 앞뒤로 reconcile.sh(본 체크아웃 모니터 3714 하나), mock herdr, 구독자 목록이 같다. 바뀐 것은 오케스트레이터의 실제 herdr agent wait planner-reviewer-claude-01(70490 → 82377)뿐이며 테스트와 무관하다. 내 probe 의 holder 는 스크립트 안에서 정리했다.
발견 사항
차단
없음.
비차단
- N11. fence 가 모든 행(stopped·terminated 포함)의
control_plane.mam_jobs_dir를 만들고, 만들 수 없으면 reset 전체를 거부한다.jobs_dirs(update.py:150-163)는 상태와 무관하게 모든 행을 본다.fence_registries(reset.py:508)는 각 경로를makedirs한다.- 지워진 작업 공간을 가리키는 오래된 행이 있으면, 그 디렉터리 트리가 다시 생기고 남는다(probe 로 확인).
- 만들 수 없는 경로를 가리키는 행이 하나라도 있으면 reset 이 거부된다. 메시지가 어느 행 때문인지 말하지 않는다. 사용자가 그 행을 고치거나 지워야 한다.
- 안전한 쪽(fail-closed)이라 차단으로 보지 않는다. 다만 이 디렉터리에 등록할 수 없는 경우(만들 수 없음)는 등록도 생길 수 없으므로, 거부 대신 건너뛰어도 안전하다. 또 다른 control plane 레지스트리는 대상 행(running·control root)의 것만 fence 해도 대상 보호에는 충분하다.
- N12. 두 reset 이 서로 다른 순서로 공유 레지스트리를 잡으면 둘 다 거부될 수 있다. 교착은 없고, 둘 다 아무것도 바꾸지 않는다. 다시 실행하면 된다.
- 2라운드의 N8 은 해소됐다. N9(sessions 필터의
herdr:접두어 정확 비교)는 그대로다. N10(죽은 reset 의 마커)은 이제 어느 작업 공간의 다음 fence 든 stale 마커를 교체하므로 범위가 줄었다. - 이전 리포트의 N1, N2, N4, N5, N6 은 그대로다.
- 코드 양: reset.py 934줄, reset.sh 349줄. 이번 변경은
jobs_dirs추출로open_jobs와 fence 의 열거를 하나로 합쳐, 오히려 중복을 줄였다. 불필요한 코드는 찾지 못했다.
사용자 확인 필요
- N11: fence 와 open-job 검사가 stopped·terminated 행의 control plane 레지스트리까지 보고 만드는 것이 의도인지. 아니면 만들 수 없는 경로는 건너뛰거나 대상 행으로 좁힐지.
- 수동 stop 이 pre-assigned 행의 id 를 폴더 전체에서 추정하는 동작(이전 리포트 3번)을 별도 이슈로 다룰지.
- N6: agy 에도 폴더 소유 확인이 필요한지.
- 병합 뒤 main 에
.claude/skills/multi-agent-mux-reset링크를 손으로 만들어야 한다.
[VERDICT: PASS]