Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
11 KiB
Issue #60 — OpenCode 2.x Mac review
NOT PASS. 기본 2.x 실행·조회·삭제 계약과 테스트는 통과했다. 그러나 새 재귀 삭제 경로가 자식 세션의 구버전 데이터를 남긴 뒤 부모 purge의 residual을 빈 목록으로 보고하는 결함을 확인했다. 실제 OpenCode 2.0.32에서도 재현됐다. 국소 수정과 회귀 테스트로 해결할 수 있으므로 재계획 요청은 없다.
Identity / 범위
- Job
614b6263, looploop-20261018T055915Z-abe16e - Base
74333f87d0c02888c2b5b281e912d97e74348c01 - Head
6b4b96fa1875862ef3bed830fe7b04166b1fad58 - Tree
9cfb2106081eba96b58cc6b7573031de0acba399 - 검증 트리
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T055915Z-abe16e/1 - macOS, 시스템 Python 3.9.6, bash 3.2, 실제 OpenCode
v2.0.32.
시작/종료 HEAD와 tree가 일치하고 git status --short는 비어 있다. creator 트리 실행·수정, 제품 코드 수정, 브랜치/커밋 변경 없음. 추가 탐침과 의도적 회귀는 검증 HEAD의 임시 git archive 복사본에서만 수행했다. tea issues 60 --login pugit --repo tmpl/multi-agent-mux --comments로 본문과 정정 댓글도 읽었다.
수정 필요 — P2: 자식의 legacy 데이터가 남아도 부모 purge는 residual 없음
파일 근거: .agents/skills/lib_py/agents/adapters/opencode.py:141:145는 네이티브 CLI에 부모와 자식들의 삭제를 맡기고 이를 성공으로 보고한다. 이어지는 :158:169의 legacy SQL은 요청한 부모 uuid 하나만 삭제한다. :188~:193의 residual도 그 uuid 하나만 조회한다.
재현 절차:
- HOME/TMPDIR가 임시이고 XDG_*가 unset인 환경에서 실제 2.0.32의
api --standalone POST /api/session -d ...로 부모 P,parentID=P인 자식 C, 무관한 세션 O를 만든다. 모델 프롬프트는 보내지 않는다. - 이 임시 DB에 마이그레이션 잔존 상태를 모형화하는 legacy
session(id,parent_id)와message(id,session_id)테이블을 만들고 P/C/O에 해당하는 행을 넣는다. 실제 사용자 DB나 실제 migration 파일을 수정한 것이 아니다. - HEAD 어댑터로
purge_artifacts(P, ctx)후residual_artifacts(P, ctx)를 호출한다. - session_v2, legacy session/message, C의 residual을 읽는다.
기대: 삭제 범위를 자식까지 확장했으므로 C의 소유 legacy 데이터도 정리하거나, 최소한 남은 C 데이터를 부모 purge 결과의 residual에 포함해야 한다. 무관한 O는 유지해야 한다.
실제:
- session_v2에는 O만 남아 네이티브 부모/자식 삭제는 성공했다.
- legacy session에는 C와 O가 남았고 legacy message의 C 행도 남았다.
- purged에는
opencode session (with its child sessions): P,sqlite rows for session: P가 출력됐다. - **부모 residual은
[]**였다. C를 별도로 조회해야session.id: 1 rows,message.session_id: 1 rows가 드러났다. - 같은 상황을 저장소 fake CLI로 시험한 추가 테스트도 실패했다. 즉 fake와 실제 CLI의 불일치가 아니라 어댑터의 자식 범위 추적 누락이다.
실제 재현 ID와 결과는 /tmp/review-614b6263-c_7f_2xc/real-probe/result.json, 재현 스크립트는 real_probe.py에 있다. 스크립트는 위 재현에 이어 독립 TUI 종료 확인까지 수행한다. probe-copy/tests/test_issue60_opencode_v2.py::test_review_child_legacy_rows_are_not_silently_abandoned에도 작은 fake 기반 재현이 있다.
수정 방향: 네이티브 삭제 전에 session_v2의 해당 부모 아래 자손 ID 집합을 확보하고, 삭제 후 legacy 정리와 residual 검증을 그 정확한 집합에 적용한다. 보존 대상 테이블의 자식 잔여물도 누락하지 않아야 한다. 실패/부분 실패 시에도 범위를 잃지 않고 WARN/residual을 남긴다. prefix나 workspace만으로 범위를 넓히면 다른 세션을 지우므로 피한다. 부모·자식·손자·무관한 세션이 있는 migrated DB 회귀 테스트를 추가한다.
기존 코드에서 이어지는 호환성 주의점
다음 두 가지는 추가 탐침으로 확인했지만 이번 diff가 새로 도입한 결함으로 분류하지 않았다. 위 자식 residual 결함만으로 NOT PASS이며, 아래 사항은 Issue #60/Linux 후속 검증에서 명시적으로 다룰 필요가 있다.
XDG_DATA_HOME 지정 시 실제 세션 저장소를 놓침
opencode.py:80~:84에서 호출 환경의 XDG_DATA_HOME을 항상 <ctx.home_dir>/.local/share로 덮어쓴다. debug paths를 사용해도 이미 다른 환경으로 질의하므로 호출자의 XDG 경로를 발견하지 못한다. status.sh의 resume_on_disk도 기본 HOME 경로를 직접 쓴다.
임시 홈 H와 임시 XDG_DATA_HOME=D를 두고 fake CLI가 따르는 D/opencode/opencode.db에만 ses_xdg를 넣었다. 기대 discover는 [ses_xdg]이지만 실제 _resolve_db는 H/.local/share/opencode/opencode.db, discover는 []였다. 이 시험에서 custom XDG를 받은 실행 파일은 저장소 fake뿐이며 실제 바이너리는 실행하지 않았다. 원본 이전 구현도 같은 강제 override를 하므로 신규 회귀라고 주장하지 않는다.
수정 시 대상 홈 격리 계약을 유지하면서 같은 사용자 환경의 명시적 XDG_DATA_HOME을 존중하고, spawn/discovery/status/purge가 동일한 data root를 쓰도록 해야 한다. 별도 홈을 지정할 때 ambient XDG가 실제 홈으로 새는 문제도 함께 막아야 한다. 이슈 정정 댓글의 XDG 경로 요구와 현재 동작에는 이 차이가 남아 있다.
공유 DB의 열린 FD가 모든 세션을 in-use로 만듦
OpenCode는 base.py:307:309의 공통 판정을 상속하고, artifact_path가 세션 전용 파일이 아닌 전체 opencode.db다. base.py:78:83의 lsof 결과는 어떤 프로세스든 그 DB를 열면 True다.
임시 DB의 종료된 세션에 대해 먼저 in-use=False를 확인한 뒤, UUID 인자 없이 DB 파일만 읽기 모드로 열고 잠드는 Python 프로세스를 만들었다. 같은 세션의 in-use가 True가 됐다. holder는 finally에서 kill/wait했다. 따라서 다른 standalone TUI나 사용자 서비스가 공용 DB를 열고 있으면 종료된 세션도 purge gate에서 거절될 수 있다. 실제 사용자 서비스에 대해 in-use나 DB API를 호출해 검증하지는 않았다. 공유 파일 기반 판정은 base에도 있던 동작이며 이번 변경의 신규 회귀는 아니다.
후속 수정은 단순히 안전 검사를 제거하기보다 OpenCode 세션별 보유/실행 상태 증거를 사용해야 한다. --standalone은 서버 생명주기를 분리하지만 DB를 세션마다 분리하는 옵션은 아니라는 점이 중요하다.
검증 결과
원본 HEAD: 181 passed, 1 skipped
실행 모듈:
- tests/test_issue60_opencode_v2.py
- tests/test_a4_adapter_contract.py
- tests/test_issue31_purge_residual.py
- tests/test_issue42_skill_blocks.py
- tests/test_issue55_agent_homes.py
- tests/test_issue10_13_purge.py
181 passed, 1 skipped in 53.41s, pytest exit 0. skip은 worktree에 untracked .claude/skills가 없어 test_issue42_skill_blocks.py:266을 건너뛴 것이다. 실제 OpenCode 2.x 계약 테스트는 skip되지 않고 통과했다. 전체 스위트는 오케스트레이터 범위라 실행하지 않았다.
원본 검증은 지정 venv의 python -m pytest -p no:cacheprovider --basetemp=/tmp/review-614b6263-c_7f_2xc/pytest-head -q -ra ..., PYTHONDONTWRITEBYTECODE=1로 수행했다. HERDR/MAM/WORKSPACE 바인딩을 제거하고 HOME/TMPDIR를 임시화, 실제 바이너리 계약 테스트는 XDG_DATA_HOME/STATE_HOME/CONFIG_HOME/CACHE_HOME unset으로 수행했다. Claude 관련 테스트의 실제 /tmp fallback도 격리했다.
추가 검증
- 추가 edge 탐침 3 failed, 10 deselected in 2.23s: 자식 legacy residual 누락, XDG 경로 누락, 공유 DB FD false positive. 원본 제공 스위트의 실패로 합산하지 않는다.
- discover의 SQL만 임시 복사본에서 session_v2→session으로 되돌린 대조군: 2 failed, 11 deselected in 1.30s. 기존
test_discover_and_verify_read_session_v2_and_skip_child_sessions,test_a_fresh_2x_database_has_no_session_table_and_still_works가 빈 결과를 거부했다. 원본은 변경하지 않았고 대조군 수정도 복원했다. - 가짜 herdr/가짜 OpenCode를 쓰는 추가 전체 연결 시험: 1 passed in 45.07s. 실제 create_session.sh 실행 → mock DB에 parent/더 최신 child 구성 및 own ID 비우기 → reconcile.sh --once에서 drift C가 parent만 확정 → status.sh --json의 resume_state=yes → stop_session.sh로 stopped → resume_session.sh의
--session <동일 parent> --auto --standalone확인. 첫 모델 메시지 대신 mock DB seed를 사용했으며 실제 프롬프트나 실제 herdr 서버는 쓰지 않았다. fixture 안의 모의 온보딩 잡만 생성됐다. - 실제 2.0.32: 임시 홈에서 API 부모/자식 생성 및 삭제, 무관한 세션 보존 확인. 별도 PTY에서 살아남은 세션을
--session ... --auto --standalone으로 재개하고/exit입력 후 0.3초 간격을 두어 Enter 전송, exit 0 확인. 모델 프롬프트 없음. - 시험 후 프로세스 확인: 기존 사용자 OpenCode 서비스 PID 86524만 남았다. 그 프로세스에는 제어 명령을 보내지 않았으며 시험으로 만든 OpenCode 프로세스는 남지 않았다.
- 변경 Python 파일 모두 시스템 Python 3.9 AST 검사 통과. 변경 셸 파일 모두
/bin/bash -n통과. base..headgit diff --check통과.
코드/문서 검토 결론과 한계
- session_v2.directory/time_created(ms), discover의 parent_id IS NULL 필터는 새 subagent가 본 세션을 덮어쓰는 것을 피하며 새 DB와 migrated DB 가정에 맞는다.
debug paths의 db 줄 파싱,session delete --standalone -- <id>, 실패/바이너리 부재 WARN 및 부모 residual, 없는 ID에 대한 불필요 CLI 호출 회피는 관련 테스트에서 확인됐다.- create/resume/update_yaml의 플래그와 adapter 생성 명령은 일치한다. active
.agents/skills, docs, README 검색에서 운영 명령으로 남은opencode db path나 TUI--agent사용은 찾지 못했다. 설명용 과거 동작 언급, 의도적인 legacy DELETE,opencode run -m은 해당하지 않는다. VERSIONS/과거 리포트는 판단에서 제외했다. - fake CLI는 기본 db path 및 v2 재귀 삭제/legacy 보존을 실제 계약과 일치하게 모형화한다. 다만 mock lifecycle은 실제 TUI의 모든 시그널 동작을 증명하지 않는다. 실제 PTY에서는 정상
/exit경로를 확인했고 실제 SIGKILL/SIGHUP 전 조합을 반복 검증하지 않았다. - auth_ok와 auth.json 검사는 브리프의 의도된 제외 범위로 취급했다.
- Linux에서 실행한 결과는 없다. Python 3.9/bash 3.2 구문과 플랫폼 전용 새 의존성 여부를 검토했지만 Linux 런타임, 서비스/PTY 동작은 예정된 별도 검증이 필요하다.
증거
임시 루트 /tmp/review-614b6263-c_7f_2xc (실경로 /private/tmp/...): head.log, probes.log, mutation.log, flow.log, real_probe.py, real-probe/result.json, real-probe/tui-output.bin, processes-after.json. 추가 pytest 재현은 probe-copy/tests/test_issue60_opencode_v2.py 및 probe-copy/tests/test_review60_flow.py에 있다.
[VERDICT: NOT PASS]