Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
9.0 KiB
Issue #64 리뷰 — agy·codex·hermes DB 읽기 전용 열기 (job b78af5ad)
- 리뷰어: planner-reviewer-claude-01
- 검증 경로:
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261019T134017Z-d90c23/1 - identity: HEAD
b0eccc530149bc6e9211727e19921665c7b80492, treef5a412a3ee96465db7c4b3223d2d76e95716ddfa가 일치한다. 작업 트리는 clean 이고.mam은 없다. - 범위: base
3ce5d5f(v4.9.7)..head 1커밋. 판정은 base..head 전체에 대한 것이다. - 참고 자료: creator(agy) 리포트
.mam/jobs/c721e4f5/agy-reports/report-final.md - 환경: macOS, Python 3.9.6, sqlite 3.51.0. 실제 에이전트 홈은
ls·lsof·ps로만 봤고, sqlite 로 연 적은 없다.
결론
이슈 범위는 빠짐없이 바뀌었다. 조회 전용 열기는 모두 _ro_connect 로 바뀌었고, 쓰기와 MAM 자신의 DB 는 그대로다. 새 테스트는 base 에서 실패하고 head 에서 통과한다. 원인도 직접 재현해 확인했다. 차단 결함은 없다. 다만 새 런타임 테스트의 reconcile 단계가 실제로는 drift C 를 실행하지 않는다(N1). 또 기존부터 있던 workspace 간 오고정 문제를 새 테스트가 기대값으로 고정해 버렸다(N2). 둘 다 테스트를 보강하라는 권고다.
1. 원인 확인 — 직접 재현함
creator 의 결론: reconcile.sh drift C(agy, :786-839)는 고정되지 않은(unpinned) 실행 중 agy 행이 있으면 호스트 공용 conversations/*.db 를 전부 훑는다. 각 DB 에 verify_session_uuid 를 부르고, 이어서 AgyAgentAdapter.verify_artifact 가 sqlite3.connect(path) 로 연다. last_conversations.json 이 그 대화를 다른 workspace 의 것으로 적어 두어도 cache_match=False 는 _sibling_claimed_uuids 만 보고 거르지 않는다(agy.py:60-72). 그러면 macOS persistent WAL 때문에 -wal·-shm 이 남는다.
재현 방법:
- git archive 사본에 임시 테스트를 넣었다(mam_sandbox, 임시 HOME, mock herdr).
- 사이드카 없는 WAL agy DB 하나를 만들고,
last_conversations.json은 그 대화를 다른 workspacews_b에 매핑했다. - ws 의 고정되지 않은 running 행을 mock herdr 에 살아 있는 pane 으로 등록했다.
reconcile.sh --once --emit-diff --dry-run을 실행했다.
| 사본 | DB 디렉터리 결과 |
|---|---|
| base | <uuid>.db, .db-shm, .db-wal — 사이드카가 생긴다 |
| head | <uuid>.db 만 — 바뀌지 않는다 |
이로써 v4.9.7 리뷰 때 #55 가드가 다른 workspace 의 agy DB 옆에서 잡은 사이드카는 MAM 의 reconcile drift C(status·create 가 부르는 경로 포함)가 만든 것으로 설명된다. 다만 그때 그 시각에 고정되지 않은 agy 행이 실제로 있었는지는 기록이 없어 확인하지 못했다.
2. 바뀐 곳 확인
- 공용 helper.
_ro_connect가base.py:111-135로 옮겨졌다. 본문은 그대로이고, 임시 복사본 이름만os.path.basename(path)를 쓴다.- opencode 는 base 의 것을 import 한다(
opencode._ro_connect is base._ro_connect, 테스트로 고정). - opencode 에서 쓰지 않게 된
quote·tempfileimport 는 지워졌다.shutil·sqlite3는 아직 쓰고 있다(which, purge).
- opencode 는 base 의 것을 import 한다(
db_rows_naming기본 연결. 기본값이_ro_connect가 됐다(base.py:145). 그래서 codex·opencode 의 residual 확인도 clean WAL DB 를 읽는다. base 에서는mode=ro가 Mac 에서unable to open으로 실패해 빈 결과를 냈다.- 조회 전용 열기를 바꾼 곳:
- agy
verify_artifact(agy.py:74) - codex
_find_rollout_via_db(codex.py:80),discover(:195) - hermes
verify_artifact(hermes.py:51),discover(:100) - status.sh hermes·codex 두 모드(
:90-91,:131-132,:290-291,:331-332) - reconcile.sh hermes drift(
:865), codex drift(:1048)
- agy
- 남은
sqlite3.connect(grep 으로 전수 확인). 모두 대상이 아니라는 판단이 맞다.- 의도된 쓰기: codex·hermes·opencode purge 의 DELETE
- MAM 자신의 DB:
lib.sh:1599,2068,atomic_yaml.py:61,state.py:17,verify_session.py:41(agent-sessions.db),loop/runner.py:416
- import 정리. agy 에서
sqlite3import 를 지웠고 다른 사용처는 없다. status.sh 의 opencode 블록은 이제lib_py.agents.base에서 import 한다.
3. 테스트 실행
조건: env -i, 임시 HOME, XDG_DATA_HOME=<scratchpad>, basetemp 는 scratchpad, -p no:cacheprovider, PYTHONDONTWRITEBYTECODE=1.
새 모듈 tests/test_issue64_ro_connect.py
- head: 43 passed.
- base 사본에 이 파일만 넣으면: 23 failed, 20 passed. creator 의 수치와 같다.
- base 에서 실패한 것은 모든 clean·walonly 경우, 정적 검사, workspace 간 테스트다. 실패 메시지는 사이드카
-shm·-wal이 생겼다는 것이다.
관련 13개 모듈
- 대상: issue64, issue60_linux_fixes, issue60_opencode_v2, a4, issue10_13_purge, issue31, orc_onboard, tier1, tier2, uuid_target, issue55, issue57, issue21_worktree_prereqs.
- 결과: 461 passed, 1 skipped, 실패 0, 15분. skip 은 PATH 에 opencode 가 없는 실바이너리 계약 테스트다. #55 가드 오탐은 없었다.
뮤테이션 (git archive 사본, issue64 모듈)
| 뮤테이션 | 결과 |
|---|---|
agy verify_artifact 를 sqlite3.connect 로 |
5 failed(workspace 간 테스트 포함) |
db_rows_naming 기본값을 옛 mode=ro URI 로 |
4 failed |
hermes discover 를 sqlite3.connect 로 |
2 failed |
reconcile hermes drift 를 sqlite3.connect 로 |
1 failed — 정적 검사만 잡음 |
reconcile codex drift 를 sqlite3.connect 로 |
1 failed — 정적 검사만 잡음 |
린트
git diff --check는 깨끗하다.- 바뀐 Python 6개 파일이 3.9 로 파싱된다.
- status.sh·reconcile.sh 는
/bin/bash -n(3.2)을 통과한다.
비차단
- N1. 런타임 테스트의 reconcile 단계는 drift C 를 실행하지 않는다.
- 위치:
test_status_and_reconcile_runtime_leave_sidecars_alone(:241-...). - 원인: 세 행을 mock herdr 에 등록하지 않는다. 그래서 drift A 가 먼저
herdr gone … Marked terminated로 처리하고 drift C 를 건너뛴다. base 에서 같은 상황을 실행해 drift 결과로 확인했다. - 결과: reconcile 의 hermes·codex 변경은 정적 문자열 검사로만 고정된다(위 뮤테이션 2개).
- 권고: 이슈 64 요구는 "실제 실행 또는 최소한 정적 검사"라 충족은 된다. 그래도 mock herdr state 에 세 세션을
running으로 넣으면 실제 drift C 경로가 검증된다(내 재현처럼state["agents"][name] = {...}). status 단계는 실제로 열린다. base 에서 hermesstate.db사이드카로 실패한다.
- 위치:
- N2. 기존 결함: drift C(agy)가 다른 workspace 의 대화를 고정한다.
- 내 재현에서
last_conversations.json이ws_b의 것으로 적은 대화가 epoch 뒤 유일한 후보였다. 그러자 base·head 모두 ws 행에updated conversation id: <uuid>로 고정했다(dry-run 기준). - 이 PR 이 만든 문제는 아니고 #64 범위 밖이다. 다만 새 테스트
test_unpinned_agy_reconcile_…(:288)가verify_session_uuid(ws_a, …, uuid_b) is True를 단언해 이 동작을 기대값으로 고정한다. - 권고: 이 단언은 빼고 디렉터리 목록만 본다. 오고정은 별도 이슈로 다룬다. 예를 들어
cache_match가 아니면서 lc 가 그 uuid 를 다른 cwd 에 매핑하면 거절한다.
- 내 재현에서
- N3.
_ro_connect의 immutable 경쟁 창(ms 수준)은 #60 에서 남긴 것이 공용 helper 를 통해 agy·codex·hermes 로도 넓어졌다. 세 에이전트의 실제 DB 는 모두 WAL 형태다(ls: hermes·codex 는 사이드카 쌍이 있고, agy 대화 72개 중 7개에 사이드카가 있음). 실행 중에는 사이드카가 있으므로mode=ro갈래를 탄다.
관찰·공개
- 실제 codex DB 의 사이드카. 관련 모듈 실행 전후로
~/.codex/목록을 비교했더니state_5.sqlite-shm·-wal이 다시 나타나 있었다(mtime 23:29/23:35, 지금은 보유 프로세스 없음).- 이 두 파일은 잡 시작 직후(23:04)에도 있었다가 사라졌던 것이다.
- 같은 시간대에 실제
~/.codex를 쓰는 codex 프로세스들이 떠 있다:codex resume(이 루프의 리뷰어), app-server daemon, IDE 확장. - 내 실행은
env -i·임시 HOME 이었고, head 코드는 사이드카를 만들지 않으며, #55 가드도 아무것도 잡지 않았다. 그래서 테스트 산출물이 아니라 실제 codex 활동으로 본다. 판정 근거로는 쓰지 않았다.
- 검증 트리에 임시 파일이 들어갔다 지워짐. 재현 중 내 셸 명령 실수로 임시 테스트 파일(
tests/test_zz_review_agy.py)이 검증 트리에 몇 초간 복사됐다. 그 파일로 아무것도 실행하지 않았고 즉시 지웠다. 이후git status --porcelain --ignored는 비어 있고 HEAD·tree 는 그대로다. 모든 재현은 scratchpad 사본에서 했다.
[VERDICT: PASS]