release(v4.9.5): a claude purge removes every path the session owns (Issue #31, PR #59) - archive the review reports, bump the lockstep version, load logroot eagerly

Review loop (--review-only, --all-reviewer, focused on this Mac:
macOS, /usr/bin/python3 3.9.6, bash 3.2): round 1 (@6f4f40a) agy and
grok PASS, claude and codex NOT PASS (a non-uuid id `memory` purged the
auto-memory dir, a linked dir below a root was followed, a NaN
startedAt counted as dead), sent as a PR comment and fixed by the
author in eleven commits; round 2 (@2a8d738) confirmed all three fixed
but 4/4 NOT PASS on one test holder (`sh -c "sleep 120" <uuid>` loses
the uuid on macOS /bin/sh), fixed in de6f3fc; round 3 (@de6f3fc) 4/4
PASS.

Full suite on macOS / Python 3.9.6 @de6f3fc: 1934 passed, 56 skipped,
1 error (the known #55 false positive: agy conversations of another
workspace). The real ~/.claude, /private/tmp/claude-<uid> and herdr
panes unchanged.

Also names lib_py.delegation.logroot in eager.CORE_MODULES (the v4.9.4
review's non-blocking note).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-18 01:25:15 +09:00
1 parent de6f3fc81f
commit 6fd417a7b7
32 files changed
+916 -26

No files matched your search

@@ -0,0 +1,76 @@
# PR #59 2차 Mac 리뷰 — NOT PASS
- Job: `a59e709d`; loop: `loop-20261017T135731Z-e93f0a`
- Base: `26e2c43753169671b600d03274901dbe203988dd`
- Head: `2a8d73825b3832376aeaf96b09548359dc3cb887`
- Tree: `d146f1ac68bcc3485d24fb92edb87264e4d1e8cb`
- 검증 트리: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T135731Z-e93f0a/1`
- 환경: macOS, 시스템 Python 3.9.6, bash 3.2, BSD ps. pytest는 지정된 제어 저장소의 `.venv/bin/python`으로 실행.
기존 3건의 결함은 이번 HEAD에서 해결됐고 수정 전 실패도 확인했다. 다만 새 UUID 대소문자 회귀 테스트 2건이 macOS에서 실패한다. 원인은 제품의 UUID 정규화가 아니라 테스트가 UUID 없는 프로세스를 생성하는 데 있다. 현재 HEAD를 이 Mac에서 통과한 상태로 승인할 수 없으므로 NOT PASS다. 수정 범위는 테스트 픽스처이며 재계획은 필요하지 않다.
## 수정 필요: [P2] UUID 명령행 보유자 픽스처가 macOS에서 UUID를 잃음
**근거:** `tests/test_issue31_purge_residual.py:315`의 `subprocess.Popen(["sh", "-c", "sleep 120", conv])`, 이어지는 `:318`의 live 단언. `upper`와 `mixed` 두 매개변수에서 동일하게 실패한다.
macOS의 sh는 마지막 명령을 실행하면서 동일 PID의 명령행을 `sleep 120`으로 바꾼다. `conv`는 셸의 `$0`였으며 sleep 인자로 전달되지 않는다. 앞서 `live.kill()` / `live.wait()`로 세션 파일의 PID도 죽였기 때문에 이 시점에는 세션 파일이나 명령행 어디에도 살아 있는 보유자가 없다. 어댑터가 False를 돌려주는 것은 이 픽스처 상태에서 올바르다.
**재현 절차:**
1. 검증 트리에서 아래 테스트 노드를 실행한다. 임시 HOME/HOME_DIR, CLAUDE_PROJECT_DIR, CLAUDE_CODE_TMPDIR, TMPDIR를 사용하고 브리프의 HERDR/MAM/WORKSPACE 환경 변수들을 제거한다. 테스트의 `home` 픽스처는 CLAUDE_CONFIG_DIR를 제거하여 임시 HOME/.claude를 쓰며, `own_tmp_roots`는 고정 /tmp fallback까지 임시 경로로 대체한다.
```sh
PYTHONDONTWRITEBYTECODE=1 /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python -m pytest -p no:cacheprovider --basetemp=<검증트리밖의새임시경로> -q tests/test_issue31_purge_residual.py::test_any_spelling_of_the_uuid_is_the_lowercase_session
```
2. 독립적으로 새 UUID를 생성하고 `Popen(["sh", "-c", "sleep 120", uuid])`를 띄운 뒤 0.1초 후 `ps -p <그 PID> -o args=`를 읽는다. 관측 결과는 `sleep 120`으로 UUID가 없다. 종료 시 생성한 프로세스만 kill/wait한다.
3. 검증 트리 HEAD의 git archive 임시 복사본에서 해당 한 줄만 `Popen([sys.executable, "-c", "import time; time.sleep(120)", conv])`로 대체하고 같은 두 테스트를 실행한다.
**기대/실제:** 원래 테스트는 lowercase UUID를 명령행에 가진 살아 있는 프로세스를 만들고 upper/mixed ID 모두 in-use=True임을 검증해야 한다. 실제로는 UUID 없는 sleep을 만들어 두 건 모두 `:318`에서 실패했다. 임시 복사본의 한 줄 대체 대조군에서는 **2 passed in 2.36s**, 종료 코드 0이었다. 별도 ps 확인에서도 Python 프로세스는 UUID 인자를 유지했다.
**수정 방향:** 셸의 마지막 명령 최적화에 의존하지 않는 프로세스를 생성한다. 이미 import한 `sys.executable`을 사용한 위 Python sleep 픽스처가 최소 수정이다. 생성 PID의 명령행에 UUID가 실제로 남아 있는지 확인하고 기존 finally의 kill/wait 정리는 유지한다. 원본 검증 트리나 creator 트리는 수정하지 않았다.
## 기존 지적의 해결 및 수정 전 실패 검증
현재 테스트 파일을 각 수정 직전 커밋의 격리된 git archive 복사본에 놓고 해당 노드를 실행했다. 테스트 임시 루트 격리 픽스처도 함께 사용했다. 이는 예전 테스트가 아니라 새 회귀 단언이 예전 구현을 실제로 거부하는지 확인하기 위한 것이다.
| 기존 지적 | 현재 구현 및 HEAD 결과 | 수정 전 결과 |
| --- | --- | --- |
| UUID 아닌 `memory`가 공유 폴더를 삭제 | `claude.py:10` 정규 UUID 확인/소문자 변환, `:301` 후보 경로 구성 및 `:368`, `:390` purge/residual 진입 가드. malformed/non-UUID 보존 테스트 통과. upper/mixed의 세션 파일 기반 in-use 및 CLI exit 4 단계도 통과 | `bd09c6a^`에서 `test_claude_rejects_malformed_uuid[memory]` 실패: 기대 []와 달리 memory 관련 경로 3개 삭제 |
| 상위 심볼릭 링크를 통한 범위 밖 삭제 | `claude.py:105`의 `_inside`가 루트 아래 상위 구성요소를 lstat하고 `:336`의 `_owned_paths`가 필터링. tmp key/project key/file-history 링크 뒤 파일 보존, leaf 링크만 삭제, 루트 자체 링크 허용 테스트 통과 | `6e0361c^`에서 `test_claude_purge_never_follows_a_linked_dir_below_a_root` 실패: 링크 뒤 경로 4개 삭제 |
| NaN/inf/과대 정수 startedAt이 죽음으로 판정 | `claude.py:69`의 `_session_file_live`: bool/type/finite/positive 검사 및 float 변환 OverflowError 처리로 판정 불가를 live로 유지. 숫자 경계값 및 CLI 테스트 통과 | `f943b0d^`에서 `[NaN]` 테스트 실패: 살아 있는 PID인데 False. 추가로 `60779ba^`에서 `[10**400]` 테스트 실패: OverflowError |
각 수정 전 실행은 1 failed였으며, 동일 노드들이 현재 HEAD의 관련 모듈 실행에서는 통과했다. 기존 3건을 계속 미해결인 제품 결함으로 보고하는 것은 아니다.
## 검증 결과와 범위
다음 네 모듈을 함께 실행: `tests/test_issue31_purge_residual.py`, `tests/test_issue42_skill_blocks.py`, `tests/test_issue55_agent_homes.py`, `tests/test_a4_adapter_contract.py`.
- 원본 HEAD: **146 passed, 2 failed, 1 skipped in 37.80s**. 위 P2의 upper/mixed 두 건만 실패. 전체 스위트는 실행하지 않았다.
- 임시 복사본의 픽스처 대조군: 위 두 건만 **2 passed**. 이를 원본 HEAD 전체 통과로 합산하지 않는다.
- 변경 Python 파일의 시스템 Python 3.9 구문 검사, 변경 셸 파일의 bash 구문 검사, `git diff --check` 통과.
- 종료 전 HEAD/tree 재확인 일치, 검증 트리 `git status --short` 출력 없음.
- 실제 Claude 실행/실제 세션 purge·stop·in-use는 수행하지 않았다. destructive 테스트는 임시 홈/프로젝트/config/tmp에서만 실행. issue31 외 모듈에도 실제 `/tmp/claude-<uid>`에 접근하지 않도록 별도 테스트 프로세스용 가드를 적용했다.
## 추가 검토 사항
- BSD ps 시작 시각: `LC_ALL=C`, `TZ=UTC0`로 읽어 `calendar.timegm`과 epoch-ms를 비교한다. 실제 임시 sleep 프로세스의 live 판정, 재사용 PID 모형, 판정 불가, UTC/DST 회귀 테스트를 확인했다. startedAt+2초 비교는 유지됐다.
- tmp 레이아웃: 실제 경로는 디렉터리 이름/종류 메타데이터만 확인했다. 이 Mac의 `/tmp`는 `/private/tmp`로 연결되고 실제 per-uid scratchpad는 workspace key 아래 UUID 디렉터리 구조였다. TMPDIR는 별도의 `/var/folders/.../T/`로 해석된다. override/base/per-uid/realpath 중복 제거 테스트 통과.
- 실제 ~/.claude의 projects, file-history, session-env, sessions, jobs 디렉터리 구조는 구현이 가정한 배치와 부합했다. 실제 세션 파일 및 key 내용은 읽지 않았다. 관측된 metadata 수: projects 디렉터리 16개, file-history 19개, session-env 21개, sessions JSON 6개와 key 6개, jobs 디렉터리 2개와 JSON 파일 1개. 이는 구조 확인이지 모든 실제 파일의 소유권을 검증했다는 뜻은 아니다.
- `dd292c1`: 삭제 가능한 `_owned_paths` 대신 전체 `_candidates`로 has_artifacts가 Claude를 선택하므로, 링크 뒤 잔여물만 있어도 auto purge는 Claude로 선택하여 보존 및 residual을 보고한다. `test_purge_cli_reports_a_linked_orphan_it_will_not_remove` 통과.
- 공유 파일/다른 UUID scratchpad/동일 접두사 jobs/다른 세션 metadata 보존 및 실패 후 ownership evidence 유지 검증을 관련 모듈에서 확인했다. key 경로는 보고 시 hex를 마스킹한다.
- `base.py:311`, `__main__.py:186`, `stop_session.sh:292`의 안내 전달을 검토했다. stdout JSON과 stderr 힌트를 분리하고 stop은 in-use gate의 실패를 exit 4로 처리한다. exit 4 및 purge 시 registry 행 삭제 설명이 stop SKILL에 반영됐다. skill code block 관련 검사 통과.
- agy에도 in-use gate가 적용되는 점은 공통 어댑터 계약의 의도된 동작이며 이 자체를 결함으로 판정하지 않았다.
- `test_tests_never_reach_the_real_tmp_root` 및 자식 Python의 fallback 주입 검증 통과. #55 관련 외부 agy 아티팩트를 결함 근거로 사용하지 않았다.
- 상위 경로 링크 검사는 검사 시점의 디렉터리 구조에 관한 보장이다. 동시 파일시스템 변경을 포함한 원자적 삭제 보장까지 검증한 것은 아니다.
## 로컬 증거
임시 증거 루트: `/tmp/review-a59e709d-4yb9c6z4` (macOS 실경로 `/private/tmp/...`).
- `head.log`: 원본 HEAD 네 모듈 실행 결과.
- `before-id.log`, `before-links.log`, `before-nan.log`, `before-overflow.log`: 수정 전 실패 결과.
- `fixture-control.log`: 한 줄 픽스처 대체 대조군의 2건 통과.
- `fixture-control/`: HEAD archive의 임시 대조군. 제품 코드 변경 없음.
[VERDICT: NOT PASS]