Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
8.5 KiB
PR #82 (Issue #78) 리뷰
검증 경로 /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261029T184637Z-6f4228/3. 테스트 뒤 HEAD b49314c3d039027d3f9478cacecfda587fdc8abc, tree d49ff60bbf89437e997f7da21e2fae8df570456b, porcelain 0. base a3364e82e1f9e9c6ac504eef377e8dbefba02b8d 의 조상이다. 스냅샷에 .venv 가 없어 제어 평면 Python 3.9.6 으로, 작업 디렉터리는 스냅샷으로 실행했다. 실제 작업 공간에서 reset.sh, stop_session.sh --purge-conversation, --dry-run 은 실행하지 않았다. 체크아웃 모니터(pid 3714, reconcile.sh --subscribe --idle-timeout 0)는 그대로 두고 신호를 보내지 않았다. 제품 파일은 수정하지 않았다.
이번 스냅샷의 추가분 b49314c 은 이전 head 568385c 이후 6개 파일(+213/−74)이다. 두 작업 공간이 같은 잡 디렉터리를 동시에 reset 할 때 나중 fence 가 앞 마커를 지우던 겹침, 그리고 아직 없는 행 레지스트리를 fence 하지 않던 구멍을 막는지 코드를 확인했다. 브리프의 diff 는 잘려 있어 스냅샷을 직접 보았다. 브리프의 줄 수 812/286 은 현재 lib_py/reset.py 934줄, reset.sh 349줄과 다르다.
확인한 것
다른 살아있는 reset 의 펜스는 덮어쓰지 않는다
fence_registries (reset.py:492-519) 는 jobs_dirs 가 주는 디렉터리마다 registry_lock 안에서 reset_marker 를 본다. 상태가 live 또는 unknown 이고 pid 가 이 reset 이 아니면 FenceBusy 를 내고 그 파일은 그대로 둔다 (reset.py:510-514). 없거나 stale 이거나 pid 가 자기 것이면 자기 마커로 바꾼다 (:515-518).
reset_marker (registry.py:97-116):
- 파일이 없으면
absent.ps를 부르지 않는다 (:105-106). - 읽기 실패는
unknown(:107-108). - pid 가 숫자가 아니면
stale(:109-111). - 이 프로세스의 lstart 를 얻지 못하면
unknown(:112-113). - 홀더가 죽었거나 lstart 가 다르면
stale(:114-116). - 맞으면
live(:117).
reset.sh 는 fenced <경로> 만 정리 목록에 넣고, refused 줄은 거절 이유로 찍은 뒤 exit 1 한다 (reset.sh:152-163). 정리는 pid 가 이 셸일 때만 마커를 지운다 (reset.sh:120-121). 다른 reset 의 마커는 목록에 없고, 지워지지도 않는다. fence 는 spec 쓰기와 purge 보다 앞이다 (reset.sh:150-168). 거절된 reset 은 대화를 지우지 않는다.
아직 없는 행 레지스트리도 막는다
jobs_dirs (update.py:150-163) 는 .mam/jobs, MAM_JOBS_DIR, 행의 control_plane.mam_jobs_dir 를 디렉터리가 아직 없어도 물리 경로당 한 번 돌려준다. 앞의 둘은 모든 등록을 거부하고, 행이 가리키는 다른 제어 평면은 이 레지스트리의 세션만 거부한다. fence 는 그 경로를 만든 다음 마커를 쓴다 (reset.py:507-518). open_jobs 는 같은 목록을 쓰되, 디렉터리가 아니면 건너뛴다 (update.py:174-176). 잡 목록은 *.json 만 보므로 마커 파일은 잡이 아니다 (update.py:116-120).
등록 거부는 그대로다. 마커가 없고 stale 이면 통과한다. sessions 에 없는 세션은 통과한다. MAM_RESET_OWNER_PID 가 마커 pid 이면 통과한다. unknown 과 live 는 거부한다 (registry.py:120-135). 호출은 레지스트리 잠금 안이다 (registry.py:219-222). 마커가 없을 때 일반 등록의 추가 비용은 그 파일을 한 번 열다 실패하는 것이다. ps 는 마커가 있을 때만 돈다.
R2 와 이전 안전장치는 유지된다
post-discovered purge 는 target_id_owned 가 verify_session_uuid(..., mode="revalidate") 를 통과할 때만 한다 (reset.py:522-537). codex verify_artifact 는 rollout cwd 가 행의 폴더와 같을 때만 참이다 (adapters/codex.py:99-130). create 는 codex source 를 pending-discovery 로 둔다 (create_session.sh:620-625).
| SKILL.md | 내용 | 코드 |
|---|---|---|
| :26 | 공유 레지스트리가 다른 작업 공간의 살아있는 reset 에 막혀 있으면 아무것도 바꾸지 않고 거부한다. 그 마커를 바꾸거나 지우지 않는다 | reset.py:510-514, reset.sh:120-121, :160-163 |
| :26 | 아직 없는 행 레지스트리도 만들어 막는다. 다른 제어 평면은 이 작업 공간의 세션만 | update.py:150-163, reset.py:507-518 |
| :26 | 어떤 경로로 열어도 거부. 예외는 reset 자신의 create 온보딩 | 마커가 디렉터리 안에 있음. create 의 MAM_RESET_OWNER_PID=$$ |
| :47 | 죽인 reset 의 마커는 홀더가 죽으면 무시 | registry.py:114-116. fence 는 그 stale 마커를 자기 것으로 바꾼다 (reset.py:510-518) |
제안 1–6, 대상 선택, spec, 잠금 순서, lsof, codex capability, 패키징은 이번 여섯 파일 diff 가 지우지 않았고, 같은 테스트 묶음이 다시 통과했다. submit 이 .mam/loop-guard-active 자체를 보지 않는 것은 리뷰 루프에 대한 기존 한계다. reset 은 작업 공간 마커와 레지스트리 마커로 등록을 거부한다.
실행한 테스트
스냅샷에 .venv 가 없다. cwd 는 검증 경로, 인터프리터는 /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python (3.9.6). HERDR_SOCKET_PATH, HERDR_PANE_ID, HERDR_TAB_ID, HERDR_WORKSPACE_ID, HERDR_SESSION_NAME, HERDR_SESSION, WORKSPACE_ROOT, MAM_ENV_FILE, MAM_JOBS_DIR, DELEGATE_JOB_LOGS_DIR 는 해제했다. PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider, --basetemp=/tmp/mam-i78-30c01a28 (mam-review 와 shim 없음), -q --tb=line.
파일: tests/test_issue78_reset.py, tests/test_issue78_hints_codex.py, tests/test_issue42_skill_blocks.py, tests/test_issue10_13_purge.py, tests/test_issue31_purge_residual.py, tests/test_issue63_same_cwd_ids.py.
결과: 201 passed, 1 skipped in 994.63s. 로그의 종료 줄은 EXIT:0. 세션 가드 오류는 없었다. 건너뛴 항목은 tests/test_issue42_skill_blocks.py:263-266 이다. 스냅샷에 .claude/skills 가 없다.
이번 커밋의 테스트도 통과했다. test_registry_cli_is_refused_after_the_target_recheck (:1793, canonical/alias/custom/row-absent), test_fence_waits_for_a_registration_and_covers_every_registry_read (:1920, 없는 디렉터리와 stale 마커), test_other_reset_cannot_take_a_live_fence (:2031, 대상 있음/없음), test_post_discovered_id_is_purged_only_when_it_verifies_for_its_folder (:1984, own/foreign).
전체 스위트, tests/test_issue34_job_tree.py, tests/test_issue50_dispatch_herdr.py 는 돌리지 않았다. 테스트 뒤에 이 basetemp 의 pytest 프로세스는 남지 않았다. 테스트 뒤 HEAD·tree·porcelain 은 위와 같다.
발견 사항
차단: 없음. 한 reset 은 다른 살아있는 reset 의 마커를 바꾸거나 지우지 않고, 그 경우 purge 전에 거부한다. 아직 없는 행 레지스트리는 만들어 막는다. 마커가 없을 때 일반 등록은 ps 를 부르지 않는다.
비차단:
- 죽은 reset 의 stale 마커는 다음 fence 가 자기 마커로 바꾼다 (
reset.py:510-518, 테스트:1936-1937). SKILL.md:26 의 "다른 reset 의 마커를 바꾸지 않는다" 는 바로 앞의 살아있는 공유 펜스에 대한 문장이고, 죽은 홀더는 SKILL.md:47 과 같이 무시된다. ps가 이 프로세스의 lstart 를 주지 못하면 마커는unknown이다. 등록은 거부되고 (registry.py:131-133), 다른 reset 도 그 펜스를 가져가지 않는다 (reset.py:511).- fence 가 만들었어도 종료 시 지우는 것은
.reset-in-progress뿐이다 (reset.sh:120-121). 행이 가리키던 빈 디렉터리와.lock은 남는다. MAM_RESET_OWNER_PID는 herdrnew-session의 pane env 로 넘어가지 않는다. create 가 herdr 서버를 새로 띄우면lib.sh:276-290의 deny 목록에 이 변수가 없다. 기본 상주 경로는new-session이다.- 원격
handle_dispatch는 reset 거부로mirror_job이None이면 0 으로 돌아간다 (remote.py:307-310). --dry-runreset 의 파이썬은_delegate_py_bin과 다를 수 있다 (reset.sh:51-58).update.sh의 거절 문구는 reset 이 loop 마커를 잡고 있어도 review loop 라고 말한다. 줄 수는 934/349 이다.
사용자 확인 필요
create 와 resume 은 mam_reset_admit 을 부르지 않는다. 결정 변경은 필요하지 않다. reset 중에 새로 만든 행은 spec 에 없고, spec 에 있는 행은 purge 전에 agent·서버·열린 잡·ID 소유를 다시 본다.
[VERDICT: PASS]