release(v4.9.6): opencode 2.x only (Issue #60) - archive the review reports, bump the lockstep version
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
0e05b1b677
commit
cb2c84ccd7
29 files changed
+835
-25
No files matched your search
@@ -0,0 +1,83 @@
|
||||
# Issue #60 리뷰 2차 — opencode 2.x 전용 지원 (job 8a485fa0)
|
||||
|
||||
- 리뷰어: planner-reviewer-claude-01
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T064723Z-79133f/1`
|
||||
- identity: HEAD `cb5f439cce41a2741a6b9974a308a765e50f00ad`, tree `cfec95403cbde10a6340b9007e1246c83be461cd` — 일치, 작업 트리 clean, `.mam` 없음. base..head 4커밋(a311c23, f0d1c00, 6b4b96f, cb5f439). 판정은 base..head 전체에 대해 내린다.
|
||||
- 1차(6b4b96f)에서 확인한 사항(측정 사실↔구현, 실바이너리 `session delete` 의 자식 삭제·cwd 무관·공유 행 보존, `--standalone` 수명, 1.x 가정 잔존 없음)은 cb5f439 가 건드리지 않은 부분이라 그대로 유효하다. 이번에는 cb5f439 를 중심으로 봤다.
|
||||
- 실제 opencode 는 `HOME`/`TMPDIR` 를 scratchpad 로, `XDG_*` 를 unset 하고 `api --standalone`·`session delete --standalone`·`debug paths` 로만 실행했다. 모델 호출 프롬프트는 보내지 않았다. 매 실험 뒤 남은 opencode 프로세스가 없음을 확인했다(사용자 서비스 pid 86524 제외). 실제 경로는 `ls`·`grep`(읽기)으로만 봤다.
|
||||
|
||||
## 결론
|
||||
|
||||
1차 지적 5건의 수정은 모두 동작하고, 테스트가 각각을 잡는다. 하지만 **N4 수정(XDG_DATA_HOME 따르기)이 테스트 격리를 깨는 새 회귀를 만들었다(B1).** 테스트 러너 환경에 `XDG_DATA_HOME` 이 있으면 sandbox 테스트가 sandbox 밖의 opencode 데이터 디렉터리(Linux 라면 사용자의 실제 DB)를 대상으로 삼고, 2개 테스트가 실패한다. 사용자가 다음에 Linux 호환성 시험을 할 예정이고 브리프가 실제 opencode 데이터 접근을 금지하므로 차단으로 본다. 수정은 fixture 두 줄이다(아래). 설계 변경은 필요 없다.
|
||||
|
||||
## B1 (차단). XDG_DATA_HOME 이 설정된 러너에서 sandbox 테스트가 sandbox 밖 DB 를 본다
|
||||
|
||||
원인: `opencode.py:87-94` `_data_home` 은 `ctx.home_dir` 이 `expanduser("~")` 와 같으면 호출자의 `XDG_DATA_HOME` 을 쓴다. 테스트 fixture `mam_sandbox`(`tests/conftest.py:259`)와 `home`(`tests/test_issue31_purge_residual.py:85-96`)는 `HOME` 을 tmp 로 바꾸지만 `XDG_DATA_HOME` 은 지우지 않는다(`conftest.py:307` 은 `XDG_CONFIG_HOME` 만 지움). 그래서 sandbox 의 HOME 이 "자기 홈" 으로 판정되어 러너의 `XDG_DATA_HOME/opencode/opencode.db` 가 선택된다. 1차 head 는 `_run` 이 `XDG_DATA_HOME=<home>/.local/share` 로 강제했으므로 이런 일이 없었다.
|
||||
|
||||
재현(검증 경로, `env -i … XDG_DATA_HOME=<scratchpad>/j8a/xdg`):
|
||||
```
|
||||
pytest tests/test_tier2_component.py tests/test_issue60_opencode_v2.py tests/test_a4_adapter_contract.py tests/test_issue31_purge_residual.py -k opencode
|
||||
FAILED tests/test_tier2_component.py::test_comp_opencode_create_and_reconcile_and_stop
|
||||
assert s_after.get("opencode_session_id_own") == parent → None (drift C 가 sandbox DB 가 아니라 XDG 쪽을 봄)
|
||||
FAILED tests/test_issue31_purge_residual.py::test_opencode_purge_leaves_no_rows
|
||||
'<sandbox>/home/.local/share/opencode/opencode.db (session_v2.id: 1 rows)' in [] (residual 이 XDG 쪽 DB 를 스캔)
|
||||
2 failed, 21 passed
|
||||
```
|
||||
XDG 위치에 미리 DB 를 둔 대조 실험에서도 residual 스캔과 purge 의 DB 해석이 그 DB 를 가리켰다. 이 실험에서는 sandbox 세션 id 가 무작위라 지워진 행은 없었다. 그래도 Linux 에서 `XDG_DATA_HOME` 을 둔 사용자가 스위트를 돌리면:
|
||||
- reconcile 은 실제 DB 를 읽고,
|
||||
- purge 는 실제 DB 에 `DELETE … WHERE id=?` 트랜잭션과 가짜 `session delete` 를 실행할 수 있다.
|
||||
- #55 가드(`tests/_agent_home_guard.py:48`)는 `$HOME/.local/share/opencode` 만 감시하므로 이것을 잡지 못한다.
|
||||
|
||||
수정 확인: 사본에서 `conftest.py` 의 `mam_sandbox` 에 `monkeypatch.delenv("XDG_DATA_HOME", raising=False)` 를, issue31 `home` fixture 에 같은 줄을 넣고 같은 조건으로 실행하자 **22 passed**, XDG 위치에 DB 접근 흔적이 없었다. 아래 N2 의 `--version` 로그만 남았다. `_agent_home_guard` 의 감시 대상에 `XDG_DATA_HOME` 쪽 opencode 디렉터리를 넣는 것도 권한다.
|
||||
|
||||
## 1차 지적 수정 확인
|
||||
|
||||
| 지적 | 수정 | 확인 |
|
||||
|---|---|---|
|
||||
| codex P2: 자식 세션의 1.x 행 잔존, 부모 residual 이 빔 | `_descendants`(`opencode.py:195-217`)가 삭제 **전에** session_v2·1.x session 의 `parent_id` 를 모든 깊이로 따라가고, `_LEGACY_TABLES` 8개를 부모+자손 id 로 정리한다. residual 은 자손을 `[child session <id>]` 로 보고한다 | **실바이너리 재현**: 임시 HOME 에서 `api --standalone` 으로 P→C→G(3단) + 무관 O 를 만들고, 1.x 형 `session(id,parent_id)`·message·part·todo·session_share 를 덧붙여 어댑터 purge 를 실행. `_descendants(P)==[C,G]`, purged `['opencode session (with its child sessions): P', 'sqlite rows for session: P and 2 child session(s)']`, residual P·C·G 모두 `[]`, O 는 7건 그대로. 순환 방지(`cid != uuid and cid not in found`), 읽기 전용 URI(`quote(abspath)`) 확인 |
|
||||
| codex 주의: 사용자 서비스가 DB 를 잡아 모든 purge 가 exit 4 | `conversation_in_use`(`:240-262`)가 argv 검사 후 lsof 보유자 중 `serve --service` 가 아닌 것만 in-use. lsof/ps 예외는 in-use(fail-closed) | in-use CLI 시뮬레이션(임시 HOME, 가짜 opencode): 보유자 없음 → rc 0. `… serve --service` 보유자 → rc 0. `… serve --stdio --port 0` 보유자 → rc 4. argv 에 id → rc 4. 힌트에 lsof 줄 추가됨 |
|
||||
| N4: XDG_DATA_HOME 무시 | `_data_home` | 제품 동작은 의도대로다(`test_the_data_dir_follows…`). 다만 B1, N1 |
|
||||
| agy: 상주 자식 | `resident_children=('opencode',)` | `stall_watch.background_children`(`:118-136`)는 상주 자식을 건너뛰되 그 자식들은 계속 순회하므로, 서버 아래 도구 프로세스는 여전히 작업으로 잡힌다. 1차에서 실측한 서버 자식 이름(`opencode serve --stdio --port 0`, argv0 basename `opencode`)과 맞음 |
|
||||
| claude N1·N2 | 실바이너리 테스트가 `_bin` 을 `REAL` 로 고정. tier2 가 더 늦은 자식 세션을 시드하고 부모 고정을 단언 | PATH 에 opencode 가 없는 `env -i` 로 `test_real_opencode_2x_contract` 통과(아래 테스트 결과) |
|
||||
|
||||
뮤테이션(git archive 사본, `-k "opencode and not real_opencode"`, issue60·issue31·tier2):
|
||||
|
||||
| 변형 | 실패 |
|
||||
|---|---|
|
||||
| 자손 제거(`ids=[uuid]`) | 1 (`test_purge_takes_the_1x_rows_of_the_child_sessions_too`) |
|
||||
| residual 의 자손 보고 제거 | 2 |
|
||||
| 서비스 제외 제거 | 1 (`test_the_background_service_alone…`) |
|
||||
| XDG 무시 | 1 |
|
||||
| resident `()` | 1 |
|
||||
| reconcile drift C `parent_id IS NULL` 제거 | 1 (tier2 — 1차 N2 해소) |
|
||||
| `_LEGACY_TABLES` 에서 todo·session_share 제거 | 2 |
|
||||
|
||||
## 테스트 실행 (검증 경로, `env -i`, PATH 에 opencode 없음, basetemp 는 scratchpad, `XDG_DATA_HOME` 미설정)
|
||||
|
||||
- 관련 12개 모듈(a4, issue31, issue10_13, issue60, tier1, tier2, issue5_phase0, issue23_24, issue6, c1, issue9, issue30_stall_watch): **473 passed, 1 skipped in 1010 s**, rc 0. skip 은 `/proc` 전용 테스트(macOS). 1차의 N1 이 해소되어 PATH 없이도 실바이너리 계약 테스트가 통과한다. #55 오탐 없음. 실행 뒤 남은 opencode 프로세스 없음.
|
||||
- 같은 테스트를 `XDG_DATA_HOME=<scratchpad>` 로 실행하면 2 failed — B1.
|
||||
|
||||
## 비차단
|
||||
|
||||
**N1. XDG 를 따르게 되면서 status.sh 가 어긋난다.** `status.sh:108`, `:318` 은 여전히 `{home}/.local/share/opencode` 고정이다(reconcile 의 `:917` 폴백도 같으나 `_resolve_db` 가 항상 경로를 돌려줘 닿지 않음). Linux 에서 `XDG_DATA_HOME` 을 둔 사용자라면 reconcile 은 XDG 쪽 DB 에서 id 를 고정하는데 status 는 같은 세션을 `MISSING` 으로 보인다. 표시만의 문제지만 N4 수정과 같은 범위이므로 status 도 어댑터의 `_resolve_db` 를 쓰는 편이 낫다. `auth_ok`·create_session.sh 의 auth.json 검사는 브리프대로 의도적 비변경으로 보았다.
|
||||
|
||||
**N2. (기존, f0d1c00 부터) `test_issue60_opencode_v2.py` 를 import 하기만 해도 실제 opencode 가 실제 HOME 으로 실행된다.** `_real_opencode()`(`:~300`)의 `--version` 호출이 모듈 import 시점에 러너 환경 그대로 돈다. 그 결과 실제 `~/.local/share/opencode/log/opencode.log` 에 `args="[\"--version\"]"` 줄이 붙는다. 읽기로 확인: 2026-10-18T06:46~06:47Z 에 여러 줄. 내 1차 리뷰 실행도 같은 줄을 남겼음을 밝혀 둔다. 데이터 손상은 아니지만 "실제 ~/.local/share/opencode 쓰기 금지" 에 걸리므로, 버전 확인도 임시 HOME/XDG 로 돌리거나 테스트 본문(임시 HOME 설정 뒤)으로 옮기기를 권한다.
|
||||
|
||||
**N3. 서비스 제외의 한계(의도된 설계로 보임).**
|
||||
- 다른 워크스페이스의 MAM opencode 세션(`--standalone` 서버)이 하나라도 떠 있으면, 그 세션과 무관한 purge 도 여전히 exit 4 다. 안전한 쪽의 오탐이다.
|
||||
- 반대로, 사용자가 서비스 모드 TUI 나 웹 UI 로 MAM 세션을 id 없이 열고 있으면 그 사용은 보이지 않는다. MAM 은 `--standalone` 으로만 띄우므로 실사용 위험은 낮다.
|
||||
- Linux 에 lsof 가 없으면 DB 보유자 검사가 통째로 빠진다. base 와 같다.
|
||||
|
||||
## Python 3.9 / bash / Linux
|
||||
|
||||
- 변경 파일은 Python 3.9 로 파싱된다(`/usr/bin/python3` 3.9.6, venv 3.9.6). f-string 안의 따옴표 중첩 없음, `from urllib.parse import quote` 는 표준.
|
||||
- `ps -o command= -p` 는 procps-ng 에서도 지원된다.
|
||||
- resident 테스트의 `exec -a opencode` 는 bash 기능이라 Linux 에서도 동작한다.
|
||||
- 셸 스크립트는 cb5f439 에서 바뀌지 않았다.
|
||||
- `git diff --check` 깨끗.
|
||||
|
||||
## 판정
|
||||
|
||||
B1 은 fixture 두 줄로 고쳐지고 수정 효과도 확인했다. 그러나 Linux 시험 직전에 사용자 실제 데이터 접근 가능성을 만드는 테스트 격리 회귀이므로 차단으로 둔다. 그 밖의 수정은 모두 PASS 수준이다.
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -0,0 +1,65 @@
|
||||
# Issue #60 리뷰 3차 — opencode 2.x 전용 지원 (job f1132474)
|
||||
|
||||
- 리뷰어: planner-reviewer-claude-01
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T073639Z-b5cb7f/1`
|
||||
- identity: HEAD `0e05b1b6777ceb837066454ac688e04a0f5d14a0`, tree `c5cb1b16af283b3fdb447bafb865ba1abbfbb5a8` — 일치, 작업 트리 clean, `.mam` 없음. base..head 5커밋(a311c23, f0d1c00, 6b4b96f, cb5f439, 0e05b1b). 판정은 base..head 전체에 대해 내린다.
|
||||
- 1차(6b4b96f)·2차(cb5f439)에서 실바이너리로 확인한 사항은 0e05b1b 가 건드리지 않은 제품 코드라 그대로 유효하다. 0e05b1b 는 status.sh, tests/_agent_home_guard.py, tests/test_issue60_opencode_v2.py 만 바꿨다. 확인한 사항:
|
||||
- 측정 사실과 구현의 일치
|
||||
- `session delete` 의 자식 삭제, cwd 무관, 공유 행 보존
|
||||
- 3단 자손 정리
|
||||
- 서비스 보유자 제외
|
||||
- `--standalone` 수명
|
||||
- resident children
|
||||
- 실제 opencode 데이터는 읽기(`grep -c`, `stat`)로만 봤다.
|
||||
|
||||
## 결론
|
||||
|
||||
2차 차단 지적(B1)과 비차단 N1·N2 가 모두 해결됐고 새 회귀는 찾지 못했다. base..head 전체를 병합 가능으로 판단한다. 남은 것은 테스트 보강 권고뿐이다.
|
||||
|
||||
## 2차 지적별 확인
|
||||
|
||||
**B1 (XDG_DATA_HOME 테스트 격리) — 해결.**
|
||||
- `_agent_home_guard.DROPPED_VARS` 에 `XDG_DATA_HOME` 이 추가됐다(`tests/_agent_home_guard.py:21`). 그래서 `conftest._isolate_agent_homes`(`conftest.py:210`)가 테스트마다 이를 지운다. mam_sandbox·issue31 `home` 처럼 HOME 만 바꾸는 fixture 도 이제 `<HOME>/.local/share` 를 본다.
|
||||
- 2차에서 실패한 두 테스트(tier2 opencode e2e, `test_opencode_purge_leaves_no_rows`)가 `XDG_DATA_HOME=<scratchpad>` 에서 통과한다(아래 실행 결과).
|
||||
- 뮤테이션: `DROPPED_VARS` 에서 다시 빼면 그 두 테스트와 `test_issue55::test_l1_every_path_variable_the_code_reads_is_accounted_for` 가 실패한다(3 failed). L1 은 환경과 무관하게 실패하므로, 이후 누가 지워도 잡힌다.
|
||||
- 세션 가드는 `XDG_DATA_HOME` 이 설정돼 있으면 `<XDG_DATA_HOME>/opencode` 를 감시한다(`:48-49`). `ambient` 는 conftest 가 drop 전에 기록한 pane 값(`_AGENT_HOME_VARS = ("HOME",) + DROPPED_VARS`)이라 올바른 위치다.
|
||||
|
||||
**N1 (status.sh 고정 경로) — 해결.**
|
||||
- 두 Python 블록(`status.sh:105-113`, `:305-313`) 모두 `get_adapter('opencode')._resolve_db(DiscoveryContext(workspace=cwd, agent_name='opencode', home_dir=home))` 를 쓴다.
|
||||
- `cwd`(`:59`/`:259`)와 `home`(`HOME_DIR`, `:41`/`:241`)은 각 블록에 정의돼 있다. `env_python` 이 `PYTHONPATH=$SKILL_DIR` 를 넣고 환경을 상속하므로(`lib.sh:1852-1871`) `lib_py` import 와 PATH·XDG 가 그대로 전달된다. 같은 파일이 이미 `lib_py.delegation.logroot` 등을 import 한다.
|
||||
- 행동 확인: git archive 사본에 임시 테스트를 넣고 실행했다(mam_sandbox+mock_herdr+mock_agents → create → reconcile 로 id 고정 → status). 1 passed.
|
||||
- `--json` 의 `resume_state == "yes"` 와 plain 출력 행의 `yes`, 둘 다 rc 0.
|
||||
- PATH 에서 opencode 를 빼도 폴백 경로로 `yes`.
|
||||
- DB 를 `XDG_DATA_HOME` 쪽으로 옮기면, XDG 없이 `MISSING`, XDG 를 주면 `yes`.
|
||||
- 행을 지우면 `MISSING`. 모두 rc 0.
|
||||
- status 는 이제 opencode 행마다 `opencode debug paths` 를 실행한다. 이 명령은 opencode 자신의 log 디렉터리에 기록한다. 하지만 status 가 이미 돌리는 `reconcile.sh --dry-run` 의 drift C 도 같은 `_resolve_db` 를 부르므로 새로운 종류의 부작용은 아니다.
|
||||
|
||||
**N2 (`--version` 탐침이 실제 로그에 기록) — 해결.**
|
||||
- `_real_opencode()`(`test_issue60_opencode_v2.py:290-303`)가 `TemporaryDirectory` 를 HOME·TMPDIR 로 하고 PATH 만 넘긴다. XDG_* 는 넘기지 않는다.
|
||||
- 실제 `~/.local/share/opencode/log/opencode.log` 의 `args="[\"--version\"]"` 줄 수를 읽기로 세었다. 아래 두 스위트 실행 전 50, 후 50(증가 없음) 이다.
|
||||
|
||||
## 테스트 실행 (검증 경로, `env -i`, basetemp 는 scratchpad)
|
||||
|
||||
관련 10개 모듈: a4, issue31, issue10_13, issue60, tier2, issue55, issue57(status), issue21_worktree_prereqs(status), issue30_stall_watch, tier1.
|
||||
|
||||
- `XDG_DATA_HOME=<scratchpad>/jf1/xdg`, PATH 에 opencode 없음: **363 passed, 1 skipped**, rc 0. 실행 뒤 그 XDG 디렉터리는 비어 있었다(아무 파일·디렉터리도 생기지 않음).
|
||||
- `XDG_DATA_HOME` 미설정, PATH 에 `~/.opencode/bin` 포함(실바이너리 계약 테스트 실행): **363 passed, 1 skipped**, rc 0.
|
||||
- skip 은 두 번 모두 `/proc` 전용 테스트(macOS)다. #55 가드 오탐은 없었고, 실행 뒤 남은 opencode 프로세스도 없었다.
|
||||
|
||||
## 비차단 (테스트 보강 권고)
|
||||
|
||||
- **T1.** 이번에 바뀐 두 동작은 회귀 테스트가 없다. 뮤테이션(`XDG_DATA_HOME` 설정 상태, issue55·issue60·issue31·tier2):
|
||||
- 가드의 XDG 감시를 원래대로 되돌림 → 28 passed, 실패 0
|
||||
- status.sh 를 고정 경로로 되돌림 → 28 passed, 실패 0
|
||||
|
||||
`test_issue55::test_g1` 에 `watch_dirs({"HOME": h, "XDG_DATA_HOME": x})["opencode"] == x/opencode` 한 줄을 넣고, 위 임시 테스트와 같은 status 행동 테스트(XDG 쪽 DB → `yes`)를 넣기를 권한다.
|
||||
- **T2 (기존 설계).** 가드는 opencode 데이터 디렉터리의 항목 이름을 비교한다. `XDG_DATA_HOME` 이 설정된 Linux 러너에서 사용자 opencode(서비스 포함)가 그 디렉터리에 새 항목을 만들면 #55 와 같은 종류의 오탐이 날 수 있다. 판정 근거로 쓰지 말고 보고만 할 종류다.
|
||||
- 1·2차 비차단 N3(다른 MAM opencode 세션이 떠 있으면 무관한 purge 도 exit 4, lsof 없는 Linux 에서는 DB 보유자 검사 생략)는 의도된 동작으로 보고 그대로 둔다.
|
||||
|
||||
## 병합 가능성
|
||||
|
||||
- base..head 전체에서 `git diff --check` 는 깨끗하다.
|
||||
- 변경 Python 파일은 Python 3.9(`/usr/bin/python3` 3.9.6)로 컴파일된다. 셸 스크립트는 `/bin/bash -n`(3.2)을 통과한다.
|
||||
- 1·2차의 실바이너리 검증과 뮤테이션 결과에 이번 검증이 더해져, 차단 사항이 없다.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,79 @@
|
||||
# Issue #60 리뷰 — opencode 2.x 전용 지원 (job f4cedbae)
|
||||
|
||||
- 리뷰어: planner-reviewer-claude-01
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T055915Z-abe16e/1`
|
||||
- identity 확인: HEAD `6b4b96fa1875862ef3bed830fe7b04166b1fad58`, tree `9cfb2106081eba96b58cc6b7573031de0acba399` — [리뷰 identity] 와 일치, 작업 트리 clean, `.mam` 없음. base..head 커밋 3개(a311c23, f0d1c00, 6b4b96f).
|
||||
- 실제 opencode 데이터·서비스는 건드리지 않았다. 실제 경로는 `ls`·`lsof -t`·`ps` 로만 봤다. 실제 바이너리(v2.0.32)는 전부 `HOME=<scratchpad>/j60/h*`, `TMPDIR=<scratchpad>/j60/t*`, `XDG_*` unset, `--standalone`(또는 `debug paths`·`--help`)로만 실행했고, 모델 호출 프롬프트는 보내지 않았다. 각 실험 뒤 `ps` 로 남은 opencode 프로세스가 없음을 확인했다(사용자 서비스 pid 86524 `opencode serve --service` 제외).
|
||||
|
||||
## 결론 요약
|
||||
|
||||
구현은 측정 사실과 맞고, 실제 2.0.32 바이너리로 직접 재현한 결과도 일치한다. 차단 결함은 찾지 못했다. 테스트는 회귀를 잘 잡는다(뮤테이션 13개 중 12개 검출). 아래 비차단 항목 중 N1(opt-in 실바이너리 테스트의 PATH 의존)은 Linux 호환성 시험 전에 고치기를 권한다.
|
||||
|
||||
## 테스트 실행 (검증 경로, `env -i` + PATH 에 `~/.opencode/bin` 포함, basetemp 는 scratchpad)
|
||||
|
||||
- 관련 11개 모듈(`test_a4_adapter_contract`, `test_issue31_purge_residual`, `test_issue10_13_purge`, `test_issue60_opencode_v2`, `test_tier1_unit`, `test_tier2_component`, `test_issue5_phase0_contracts`, `test_issue23_24_startup_path`, `test_issue6_fail_closed`, `test_c1_tui_readiness`, `test_issue9_server_env`): **420 passed in 723.50s**, rc 0 (opt-in 실바이너리 계약 테스트 포함; #55 가드 오탐도 이번에는 없음). 실행 뒤 남은 opencode 프로세스 없음.
|
||||
- `test_issue60_opencode_v2.py` 를 PATH 에 opencode 없이 실행: 1 failed, 9 passed — 아래 N1.
|
||||
|
||||
## 1. 측정 사실 ↔ 구현 대조 (실제 opencode 2.0.32, 임시 HOME 에서 재현)
|
||||
|
||||
| 항목 | 직접 측정 | 구현 |
|
||||
|---|---|---|
|
||||
| `debug paths` | exit 0, 0.04 s, `db <HOME>/.local/share/opencode/opencode.db` 줄 포함(공백 정렬). DB 는 만들지 않음(`log/`·`repos/` 디렉터리만 생성) | `opencode.py:93-98` 이 `split(None, 1)` 로 `db` 줄을 읽고 `strip()` — 경로의 공백도 보존됨. 실패·타임아웃(5 s) 시 기본 경로 폴백(`:101`) |
|
||||
| TUI 플래그 | `--help` 에 `--standalone`·`--auto`·`--session, -s` 있음, `--agent`·`-m` 없음. `--session` 은 "continue, or to create if it does not exist" | `spawn_spec`/`resume_spec`(`:198-204`), create/resume/update_yaml 의 셸 사본 모두 `--auto --standalone` |
|
||||
| 세션 저장소 | 새 2.x DB 에 `session`·`message`·`part` 테이블 없음, 세션은 `session_v2`(`parent_id`, `directory`=realpath, `time_created` ms) | verify/discover/_has_session/status/reconcile 모두 `session_v2` |
|
||||
| `session delete --standalone -- <id>` | 부모 P 삭제 시 자식 C 의 `session_v2`·`session_message`·`event_sequence` 행까지 삭제, 같은 디렉터리의 다른 세션 O 는 그대로. 프로젝트의 `event_sequence`(aggregate_id=project id) 같은 공유 행은 남음. **cwd 와 무관**(다른 프로젝트 cwd 에서 지워도 동작). 없는 id → stderr `Session not found: <id>`, exit 1 | `purge_artifacts`(`:136-150`) 가 `session_v2` 에 있을 때만 CLI 호출, `--` 사용, 실패 시 `WARN` + `residual` 에 `session_v2.id` 보고(`:192-193`). 가짜 CLI(`tests/_fake_opencode.py`)의 출력 스트림·exit 코드·자식 재귀 삭제가 실제와 같음 |
|
||||
| `--standalone` 수명 | pty 안에서 TUI(`opencode --auto --standalone`)가 자식 `opencode serve --stdio --port 0` 을 띄움. 시작만으로는 `session_v2` 에 행이 생기지 않음(첫 메시지 전). `/exit`(입력 후 1 s 뒤 Enter) → 종료, pty 닫힘 → 0.11 s 안에 종료, TUI SIGKILL → 서버 자식도 2 s 안에 종료. 모든 경우 남은 프로세스 0 | stop 의 graceful(`/exit` via send_keys_safe)→kill-session→SIGKILL 체인은 변경 없음이며 어느 단계에서도 프로세스가 남지 않는다 |
|
||||
|
||||
참고: 브리프의 "TUI 는 SIGTERM·SIGHUP 을 무시" 는 내 pty 실험에서는 재현되지 않았다(SIGTERM 한 번에 TUI·서버 모두 종료). 어느 쪽이든 stop 체인에는 유리한 방향이라 판정에 영향 없음.
|
||||
|
||||
## 2. 코드 리뷰
|
||||
|
||||
- `opencode.py:62-87` `_bin`/`_run`: 기존 `_resolve_db` 안의 탐색 로직을 그대로 뽑아낸 것. `_run` 은 HOME·XDG_DATA_HOME 을 ctx.home_dir 기준으로 덮어써 `debug paths` 와 `session delete` 가 같은 DB 를 보게 한다(일관). `cwd` 가 없으면 None — `session delete` 가 cwd 무관함을 실측했으므로 문제 없음.
|
||||
- `verify_artifact`(`:107-131`): `session_v2` 의 `directory, time_created` 고정 스키마. ms→s 변환, epoch 비교, workspace_key 비교 유지.
|
||||
- `discover`(`:213-239`): `parent_id IS NULL` — 실제 스키마에서 subagent 세션은 `parent_id` 가 채워지고 `directory` 가 같으며 더 늦게 생성됨을 확인(C 가 P 보다 161 ms 뒤). 필터가 없으면 reconcile 이 두 후보로 `C-ambiguous` 가 되거나 자식을 고를 수 있으므로 정당하다.
|
||||
- `purge_artifacts`: `_has_session` 이 거짓이면(1.x 에만 있는 세션, 없는 id) CLI 를 부르지 않는다. 1.x 테이블 SQL 삭제는 `session_id`/`id` 정확 일치만이라 다른 세션·공유 행을 지우지 않는다. 1.x 테이블이 없으면 "sqlite rows" 항목을 보고하지 않도록 바뀐 것(`:172-173`)도 맞다.
|
||||
- reconcile drift C(`reconcile.sh:952-994`): `PRAGMA table_info(session_v2)` + `parent_id IS NULL`(컬럼 있을 때만). `_resolve_db` 가 이제 항상 경로를 돌려주므로 `:916-930` 의 파일시스템 폴백은 사실상 죽은 코드(기존 코드, 비차단).
|
||||
- status.sh 두 곳 `session_v2`. Python 3.9(`/usr/bin/python3` 3.9.6, venv 3.9.6) 파싱 OK, 셸 5개 `/bin/bash -n`(3.2) OK, `git diff --check` 깨끗.
|
||||
- 남은 1.x 가정 검색(`agent build`, `db path`, `FROM session\b`, `table_info(session)`, `opencode -m`; VERSIONS.md·bug-report·.agents/reports 제외): 코드·문서에 남은 곳 없음(주석·테스트 docstring 의 설명적 언급만).
|
||||
|
||||
## 3. 테스트가 회귀를 잡는가 — 뮤테이션 (git archive 사본, `-k "opencode and not real_opencode"`, 3개 모듈)
|
||||
|
||||
| 뮤테이션 | 결과 |
|
||||
|---|---|
|
||||
| discover 를 `session` 테이블로 | 5 실패 |
|
||||
| discover 의 `parent_id IS NULL` 제거 | 1 실패(`test_discover_and_verify…skip_child_sessions`) |
|
||||
| verify_artifact 를 `session` 테이블로 | 6 실패 |
|
||||
| purge 에서 CLI 호출 제거 | 4 실패 |
|
||||
| `--` 제거 / `--standalone` 제거 | 각 1 실패 |
|
||||
| `_resolve_db` 를 `db path` 로 | 2 실패 |
|
||||
| residual 에서 `session_v2.id` 제거 | 3 실패 |
|
||||
| reconcile `session` 테이블로 / status `session` 으로 | 각 1 실패(구조 검사) |
|
||||
| create_session.sh·spawn_spec 을 `--agent build` 로 | 각 1 실패 |
|
||||
| **reconcile drift C 의 `parent_id IS NULL` 제거** | **0 실패** (N2) |
|
||||
|
||||
## 4. 비차단 지적
|
||||
|
||||
**N1. opt-in 실바이너리 테스트가 opencode 가 PATH 에 없으면 거짓 실패한다** (`tests/test_issue60_opencode_v2.py:182-228`).
|
||||
`_real_opencode()` 는 import 시점(실제 HOME)에 `~/.opencode/bin/opencode` 폴백으로 바이너리를 찾아 테스트를 켠다. 그런데 테스트 안에서 `HOME` 을 임시 디렉터리로 바꾼 뒤 어댑터의 `_bin()` 은 `shutil.which` → `ctx.home_dir/.opencode/bin`(임시 home) → `expanduser("~")`(역시 임시 home) 순이라 바이너리를 못 찾는다. 결과:
|
||||
```
|
||||
env -i PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin ... pytest tests/test_issue60_opencode_v2.py
|
||||
FAILED test_real_opencode_2x_contract - assert [] == ['opencode session (with its child sessions): ses_…']
|
||||
WARN: purge opencode: no opencode binary to delete session ses_…
|
||||
1 failed, 9 passed
|
||||
```
|
||||
PATH 에 `~/.opencode/bin` 을 넣으면 10 passed. 공식 설치기의 기본 위치가 `~/.opencode/bin` 이고 PATH 등록은 셸 rc 에 의존하므로, Linux 호환성 시험(CI·비로그인 셸)에서 제품 문제로 오인될 수 있다. 같은 이유로 `_resolve_db` 단언(`:218`)은 폴백 경로와 같아서 `debug paths` 파싱을 검증하지 못한 채 통과한다. 수정 예: 테스트에서 `monkeypatch.setattr(adapter, "_bin", lambda ctx: REAL)` 또는 `monkeypatch.setenv("PATH", os.path.dirname(REAL) + os.pathsep + os.environ["PATH"])`. 브리프가 정한 실행 방식(`env -u …`, PATH 상속)에서는 통과하므로 차단으로 보지 않았다.
|
||||
|
||||
**N2. reconcile drift C 의 `parent_id` 필터는 행동 테스트가 없다.** `test_status_and_reconcile_read_session_v2` 는 문자열 검사뿐이고, tier2 e2e(`test_comp_opencode_create_and_reconcile_and_stop`)는 자식 세션을 시드하지 않는다. 위 표처럼 필터를 지워도 아무 테스트도 실패하지 않는다. 어댑터 discover 쪽은 잡힌다.
|
||||
|
||||
**N3. (기존 동작, 이번 PR 이 바꾸지 않음) opencode 의 in-use 판정은 공유 DB 를 lsof 하므로, 어떤 opencode 프로세스든 DB 를 열고 있으면 항상 "사용 중"이다.** `base.py:307-309` 가 `artifact_path`(=모든 세션이 공유하는 `opencode.db`)를 lsof 한다. 이 Mac 에서는 사용자 서비스(pid 86524 `opencode serve --service`)가 실제 DB 를 열고 있다(`lsof -t ~/.local/share/opencode/opencode.db` → 86524). 임시 HOME 재현: DB 를 아무 프로세스가 열지 않으면 `in-use opencode <id>` → `in_use: false`, rc 0, 다른 python 이 같은 DB 를 열어 두면 → `in_use: true`, rc 4. 따라서 이 Mac 에서는 `stop --purge-conversation` 이 opencode 세션에 대해 항상 exit 4 로 거부되어, 이번에 새로 만든 `session delete` 경로는 stop 을 통해서는 닿지 않는다(안전한 쪽의 오탐). 힌트 문구도 명령줄만 언급하고 DB 를 연 프로세스는 말하지 않는다. base 의 `_resolve_db` 도 같은 DB 로 귀결되므로 회귀는 아니다. 세션 단위 판정(예: `--session <id>` argv 와 그 세션의 TUI pid)으로 바꿀지는 별도 이슈로 다룰 일이다.
|
||||
|
||||
**N4. (기존 동작) XDG_DATA_HOME 덮어쓰기.** `_run` 은 사용자의 `XDG_DATA_HOME` 을 무시하고 `~/.local/share` 로 강제하며, status.sh·reconcile 폴백도 `~/.local/share/opencode` 고정이다. Linux 에서 `XDG_DATA_HOME` 을 따로 둔 사용자라면 pane 의 TUI 는 다른 DB 에 쓴다. 기존 코드와 같은 동작이며 기본 설정에서는 문제 없다. Linux 시험 때 확인할 항목으로 남긴다.
|
||||
|
||||
**N5. (사소) `_resolve_db` 가 호출마다 opencode 를 실행한다.** discover 는 후보마다 `verify_artifact` → `_resolve_db` 를 다시 부른다(최대 21회 × 약 0.04 s). 기존 `db path` 호출과 같은 구조라 회귀는 아니다.
|
||||
|
||||
## 5. Linux 이식성
|
||||
|
||||
- 경로: `debug paths` 가 HOME·XDG 를 따르고, 어댑터는 그 출력을 쓴다. `/proc`·macOS 전용 명령은 새로 쓰지 않았다. in-use 는 기존 `ps -eo` + (있으면) `lsof`.
|
||||
- 테스트: 가짜 CLI 는 `#!/usr/bin/env python3`, 래퍼는 `#!/bin/sh` + `exec` — Linux 에서도 동작. 실바이너리 테스트는 `os.path.realpath(ws)` 로 디렉터리를 맞춘다(macOS `/private` 접두사도 처리). N1 만 Linux 에서 문제가 될 수 있다.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20261018T103110Z-8a6969",
|
||||
"base_sha": "74333f87d0c02888c2b5b281e912d97e74348c01",
|
||||
"head_sha": "0e05b1b6777ceb837066454ac688e04a0f5d14a0",
|
||||
"tree_sha": "c5cb1b16af283b3fdb447bafb865ba1abbfbb5a8",
|
||||
"review_commit": "0e05b1b6777ceb837066454ac688e04a0f5d14a0",
|
||||
"reviewer": "planner-reviewer-codex-01",
|
||||
"job_id": "598fe58e",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-10-18T10:34:16Z"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Issue #60, review 3 (head 0e05b1b)
|
||||
|
||||
**Scope and identity.** Reviewed base `74333f87d0c02888c2b5b281e912d97e74348c01` through head `0e05b1b6777ceb837066454ac688e04a0f5d14a0` in the assigned review-only worktree. Its HEAD/tree matched the brief and `git status --short` was empty. No code or branch changes were made.
|
||||
|
||||
**Review result.** The second-round isolation defect is addressed: `tests/_agent_home_guard.py:21` now drops inherited `XDG_DATA_HOME` for each test, and `:48-49` snapshots the OpenCode directory under the ambient XDG root so data written there is still detected. `tests/test_issue60_opencode_v2.py:297-304` probes `--version` with a temporary `HOME` and `TMPDIR`; the opt-in real 2.x contract test also uses its own temporary home and standalone API processes (`:311-352`). I ran that contract test here; it passed, with no service operation or model prompt. The worktree had no changes after testing, and process inspection showed only the pre-existing user `opencode serve --service` process (PID 86524), which was left untouched.
|
||||
|
||||
The status change preserves the prior `yes`/`MISSING`/`no` behavior while resolving the database through the adapter and querying `session_v2`: `.agents/skills/multi-agent-mux-status/scripts/status.sh:105-121`. The same branch exists in both embedded status paths. This avoids the stale fixed `~/.local/share` lookup and does not change status formatting or exit handling. Adapter path resolution at `.agents/skills/lib_py/agents/adapters/opencode.py:90-123` applies caller XDG data only to the caller's own home, runs `debug paths` with the requested home, and falls back to that home/XDG-derived 2.x database path.
|
||||
|
||||
No blocking defect found in the cumulative change. The prior data-safety and purge concerns remain covered: focused tests exercise parent/child deletion, legacy rows retained by migrated databases, preservation of unrelated sessions, and residual reporting after a failed delete. `discover` selects top-level `session_v2` rows and verifies child sessions independently. The fake CLI and real 2.x opt-in test both cover the expected v2 contract. Search of active skills found no remaining OpenCode assumptions on `db path`, `FROM session`, or an OpenCode `--agent` option; only explanatory comments and generic delegation syntax matched.
|
||||
|
||||
**Validation.** On macOS / Python 3.9.6, ran the related suite in the review worktree with a temporary `HOME`/`TMPDIR`, XDG variables unset, requested environment scrubbed, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and an external temporary basetemp:
|
||||
|
||||
`tests/test_issue60_opencode_v2.py tests/test_a4_adapter_contract.py tests/test_issue31_purge_residual.py tests/test_issue55_agent_homes.py tests/test_issue42_skill_blocks.py tests/test_issue10_13_purge.py tests/test_tier2_component.py::test_comp_opencode_create_and_reconcile_and_stop`
|
||||
|
||||
Result: **187 passed, 1 skipped** (118.14s). Also ran `git diff --check` over base..head and `bash -n` on the modified status, reconcile, create, and resume shell scripts; both passed. The full suite and Linux runtime were not rerun in this reviewer turn; the brief records the orchestrator's full-suite result (1949 passed, 56 skipped). No Linux-specific failure was observed, but this review's runtime validation was macOS-only.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,100 @@
|
||||
# Issue #60 — OpenCode 2.x Mac review
|
||||
|
||||
**NOT PASS.** 기본 2.x 실행·조회·삭제 계약과 테스트는 통과했다. 그러나 새 재귀 삭제 경로가 자식 세션의 구버전 데이터를 남긴 뒤 부모 purge의 residual을 빈 목록으로 보고하는 결함을 확인했다. 실제 OpenCode 2.0.32에서도 재현됐다. 국소 수정과 회귀 테스트로 해결할 수 있으므로 재계획 요청은 없다.
|
||||
|
||||
## Identity / 범위
|
||||
|
||||
- Job `614b6263`, loop `loop-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 하나만 조회한다.
|
||||
|
||||
**재현 절차:**
|
||||
|
||||
1. HOME/TMPDIR가 임시이고 XDG_*가 unset인 환경에서 실제 2.0.32의 `api --standalone POST /api/session -d ...`로 부모 P, `parentID=P`인 자식 C, 무관한 세션 O를 만든다. 모델 프롬프트는 보내지 않는다.
|
||||
2. 이 임시 DB에 마이그레이션 잔존 상태를 모형화하는 legacy `session(id,parent_id)`와 `message(id,session_id)` 테이블을 만들고 P/C/O에 해당하는 행을 넣는다. 실제 사용자 DB나 실제 migration 파일을 수정한 것이 아니다.
|
||||
3. HEAD 어댑터로 `purge_artifacts(P, ctx)` 후 `residual_artifacts(P, ctx)`를 호출한다.
|
||||
4. 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..head `git 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]
|
||||
@@ -0,0 +1,120 @@
|
||||
# Issue #60 2차 리뷰 — NOT PASS
|
||||
|
||||
이전 자식 legacy 데이터 누락은 실제 OpenCode 2.0.32에서도 해결됐다. 새 기능의 행동 테스트와 수정 전 실패도 확인했다. 그러나 새 XDG_DATA_HOME 읽기가 테스트의 환경 격리 목록에 반영되지 않아 원본 HEAD 검사 1건이 실패하며, 실제로 sandbox 밖 DB 경로를 선택하는 회귀가 있다. 국소 수정이 필요하고 재계획은 필요하지 않다.
|
||||
|
||||
## Identity
|
||||
|
||||
- Job: `c4e96018`; loop: `loop-20261018T064723Z-79133f`
|
||||
- Base: `74333f87d0c02888c2b5b281e912d97e74348c01`
|
||||
- Head: `cb5f439cce41a2741a6b9974a308a765e50f00ad`
|
||||
- Tree: `cfec95403cbde10a6340b9007e1246c83be461cd`
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T064723Z-79133f/1`
|
||||
|
||||
시작/종료의 HEAD/tree가 일치하고 검증 트리는 clean이다. creator 트리에서 실행·수정하지 않았다. 제품 원본과 원본 테스트는 수정하지 않았다. 아래 대조군만 검증 트리의 별도 git archive 복사본에서 변경했다. 판정은 base..head 누적 변경을 대상으로 한다.
|
||||
|
||||
## 수정 필요 — P1: 상속된 XDG_DATA_HOME이 테스트 sandbox 밖을 가리킴
|
||||
|
||||
**근거:** `.agents/skills/lib_py/agents/adapters/opencode.py:90`~`:93`은 HOME과 ctx.home_dir가 같으면 ambient XDG_DATA_HOME을 반환하도록 새로 변경됐다. 그런데 `tests/_agent_home_guard.py`의 `DROPPED_VARS`에는 XDG_DATA_HOME이 없고, `tests/conftest.py::_isolate_agent_homes`는 그 목록만 제거한다. `mam_sandbox`는 HOME/HOME_DIR를 임시 홈으로 바꾸지만 XDG_DATA_HOME은 제거하지 않는다. 따라서 이 시점에 own=True가 되면서 호출자의 외부 XDG data root가 그대로 테스트에 유입된다. `watch_dirs()` 역시 opencode의 기본 HOME/.local/share 경로만 감시한다.
|
||||
|
||||
**원본 HEAD에서 관측:** `tests/test_issue55_agent_homes.py:61`의 `test_l1_every_path_variable_the_code_reads_is_accounted_for`가 다음 값으로 실패했다.
|
||||
|
||||
```text
|
||||
path variables the adapters/lib.sh read but conftest._isolate_agent_homes does not drop: ['XDG_DATA_HOME']
|
||||
```
|
||||
|
||||
이는 새 파일 생성에 대한 외부 에이전트 활동 오탐이 아니라, 소스 코드가 읽는 변수와 격리 목록의 결정적 불일치다. 테스트 프로세스 시작 전에 XDG를 unset한 이번 리뷰 환경에서도 목록 검사는 실패한다.
|
||||
|
||||
**안전한 재현 절차:**
|
||||
|
||||
1. 검증 HEAD 복사본에 `mam_sandbox` fixture를 사용하는 짧은 pytest 테스트를 둔다.
|
||||
2. pytest 호출자의 HOME은 임시 H, XDG_DATA_HOME은 별개의 임시 D로 설정한다. 실제 사용자 경로는 사용하지 않는다.
|
||||
3. 테스트 안에서 OpenCode 어댑터의 `_bin`을 None으로 대체해 실제 CLI 실행을 막고, `DiscoveryContext(workspace=sandbox, agent_name='opencode', home_dir=sandbox)`로 `_resolve_db`를 호출한다.
|
||||
4. 결과가 sandbox 내부인지 검사한다.
|
||||
|
||||
**기대:** 각 테스트가 명시적으로 설정하지 않은 ambient XDG는 격리되어 `<sandbox>/.local/share/opencode/opencode.db`가 선택돼야 한다.
|
||||
|
||||
**실제:**
|
||||
|
||||
```text
|
||||
sandbox: /private/tmp/review-c4e96018-i02oz_61/pytest-isolation/test_inherited_data_home_does_0
|
||||
resolved_db: /tmp/review-c4e96018-i02oz_61/ambient-data/opencode/opencode.db
|
||||
```
|
||||
|
||||
추가 탐침은 **1 failed in 0.09s**. 실제 파일 쓰기나 삭제를 하지 않고 경로 선택만 확인했다. 일반 개발자 환경에서 D가 실제 XDG 디렉터리라면 어댑터/테스트가 그 DB를 읽거나 변경할 수 있으므로, 단순한 목록 테스트의 기대값 갱신으로 넘길 문제는 아니다. 실제 사용자 데이터 훼손이 관측됐다는 뜻은 아니다.
|
||||
|
||||
**수정 방향:** `DROPPED_VARS`에 XDG_DATA_HOME을 추가하여 테스트마다 제거하고, XDG 동작을 검증하는 테스트만 명시적으로 임시 XDG를 설정하게 한다. 세션 guard의 ambient 변수 기록 및 `watch_dirs()`도 새 data-root 계약을 반영하여 실제 XDG 경로를 감시하게 한다. `LEFT_ALONE`에만 추가하거나 목록 단언을 약화하면 sandbox 경로 문제는 남는다.
|
||||
|
||||
**대조군:** 임시 복사본에서 DROPPED_VARS에 XDG_DATA_HOME만 추가하자 위 경로 탐침과 기존 L1 검사 두 건이 **2 passed in 0.09s**로 바뀌었다. 이는 격리 누락 원인의 확인이며, 원본 수정이나 전체 검증 완료를 의미하지 않는다. 감시 경로 보완도 함께 필요하다.
|
||||
|
||||
## 이전 지적 및 이번 변경 확인
|
||||
|
||||
### 자식 legacy 데이터 삭제와 residual
|
||||
|
||||
`purge_artifacts`가 CLI 호출 전에 `_descendants`로 session_v2와 legacy session의 parent_id를 따라 모든 깊이의 ID를 모은다. 중복/순환은 found와 root 비교로 제한한다. legacy message/messages/part/parts/todo/session_share/session/sessions를 해당 ID 집합에 대해 삭제한다. 관련 없는 세션은 집합에 넣지 않는다.
|
||||
|
||||
이전 리뷰의 실제 재현을 새로운 임시 홈에서 반복했다. 실제 2.0.32 standalone API로 부모 P, 자식 C, 무관한 O를 생성하고, 임시 DB에 P/C/O의 legacy session/message를 추가한 뒤 현재 어댑터로 P를 purge했다.
|
||||
|
||||
- session_v2와 legacy session 모두 **O만 남았다**.
|
||||
- P와 C의 residual은 각각 `[]`였다.
|
||||
- purged는 부모/자식 네이티브 삭제와 `and 1 child session(s)` legacy 정리를 보고했다.
|
||||
- 원래 차단 지적이었던 C의 legacy 행 생존은 재현되지 않았다.
|
||||
|
||||
새 테스트는 손자 포함 삭제, 무관한 세션 보존, 삭제 실패 시 `[child session ...]` residual도 확인한다. todo의 변경된 계약은 issue31 테스트가 삭제됨을 단언하며 통과했다. session_share도 구현의 명시적 legacy 집합에 포함됐다.
|
||||
|
||||
### 서비스 보유자 / XDG / resident / drift C
|
||||
|
||||
- 공유 DB에 대한 서비스 보유자 제외, 일반 보유자 및 UUID 명령행 보유자 유지 테스트 통과.
|
||||
- 호출자 자신의 HOME에서 XDG_DATA_HOME 존중, 다른 HOME에서는 그 홈의 기본 data root 사용 테스트 통과. 제품 경로 계약은 의도대로 바뀌었으나 위 테스트 격리 갱신이 누락됐다.
|
||||
- resident_children=('opencode',) 테스트 통과. standalone 서버 자체는 idle 판단에서 제외하지만 그 아래 작업 프로세스는 계속 탐색하여 작업으로 본다.
|
||||
- `test_comp_opencode_create_and_reconcile_and_stop` 통과. 더 최신 자식이 있어도 실제 reconcile.sh 경로가 부모를 확정한다. 이는 문자열 검사만이 아니라 임시 fixture 안의 행동 검증이다.
|
||||
- 실제 바이너리 계약 테스트는 `_bin`을 발견한 바이너리로 고정하여 임시 HOME에서 PATH에 의존하지 않도록 바뀌었고 실제 2.0.32로 통과했다.
|
||||
|
||||
**수정 전 대조군:** 현재 `test_issue60_opencode_v2.py`를 이전 HEAD `6b4b96f`의 별도 archive에 넣어 아래 다섯 노드만 실행했다. 모두 수정 전 동작 때문에 실패했다(**5 failed, 10 deselected in 2.57s**).
|
||||
|
||||
1. test_purge_takes_the_1x_rows_of_the_child_sessions_too
|
||||
2. test_a_failed_delete_reports_the_child_sessions_left
|
||||
3. test_the_background_service_alone_does_not_hold_a_session
|
||||
4. test_the_data_dir_follows_the_callers_xdg_data_home_only_for_its_own_home
|
||||
5. test_the_standalone_server_is_a_resident_child_but_its_tools_are_work
|
||||
|
||||
같은 다섯 노드는 현재 HEAD에서 통과했다. drift-C 필터 제거 변형은 이번 리뷰에서 별도로 반복하지 않았으며 현재 행동 테스트 통과를 확인했다.
|
||||
|
||||
## 원본 HEAD 실행 결과
|
||||
|
||||
다음 6개 모듈과 tier2 노드 1개 실행:
|
||||
|
||||
- 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
|
||||
- tests/test_tier2_component.py::test_comp_opencode_create_and_reconcile_and_stop
|
||||
|
||||
**1 failed, 186 passed, 1 skipped in 91.16s**, pytest exit 1. 유일한 실패는 위 XDG 변수 목록 검사다. skip은 untracked `.claude/skills`가 검증 worktree에 없는 issue42 검사이며, 실제 OpenCode 테스트는 skip되지 않았다.
|
||||
|
||||
변경 Python 파일의 시스템 Python 3.9 AST 구문 검사, 변경 셸 파일의 `/bin/bash -n`, base..head `git diff --check` 모두 통과. 전체 스위트는 오케스트레이터가 별도로 실행하는 범위라 반복하지 않았다.
|
||||
|
||||
## 안전 범위와 남은 제한
|
||||
|
||||
- 지정 venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, 검증 트리 밖 basetemp로 실행했다. HERDR/MAM/WORKSPACE/CLAUDE_CONFIG_DIR 바인딩은 제거했다.
|
||||
- 실제 바이너리는 HOME/TMPDIR 임시화, XDG_* unset, 서버 사용 명령은 --standalone으로만 실행했다. `_run`이 설정하는 XDG_DATA_HOME도 해당 임시 홈 아래다. 모델 프롬프트나 실제 사용자 서비스/API 호출은 하지 않았다.
|
||||
- XDG ambient 재현에는 임시 경로와 `_bin=None`만 사용했다. 실제 opencode 데이터나 실제 DB는 건드리지 않았다.
|
||||
- 종료 후 OpenCode 프로세스는 기존 사용자 서비스 PID 86524만 남았다. 그 서비스는 제어하지 않았다. 시험 프로세스가 남지 않았다.
|
||||
- 1차 리뷰에서 확인한 create→drift C→status→stop→resume 연결 및 실제 PTY의 `/exit` 성공 근거는 `.mam/jobs/614b6263/codex-reports/report-final.md`에 있다. 이번에는 새 tier2 행동 테스트와 실제 API 삭제 계약을 재검증했고, 실제 모델 첫 메시지는 보내지 않았다.
|
||||
- status.sh의 `resume_on_disk`는 아직 기본 HOME 경로를 직접 찾는다. 사용자 지정 XDG 저장소에 대한 status 표시는 어댑터의 새 경로 지원과 다를 수 있다. 이는 1차 리뷰에서도 있던 비차단 후속 사항이며 이번 NOT PASS의 추가 사유로 삼지 않았다.
|
||||
- Linux 실제 실행 결과는 없다. 구문 및 플랫폼 코드 경로를 검토했고 Linux 런타임/서비스 동작 검증은 예정된 후속 작업으로 남는다.
|
||||
|
||||
## 증거 파일
|
||||
|
||||
`/tmp/review-c4e96018-i02oz_61` (실경로 `/private/tmp/...`):
|
||||
|
||||
- head.log — 원본 결과 및 유일한 실패.
|
||||
- before-fix.log — 이전 HEAD에서 새 회귀 검사 5건 실패.
|
||||
- real_probe.py, real-probe/result.json — 실제 2.0.32 부모/자식 legacy 정리 확인.
|
||||
- isolation.log — sandbox 밖 임시 XDG data root 선택 재현.
|
||||
- isolation-control.log — 임시 격리 목록 보완 후 2건 통과.
|
||||
- isolation-probe/tests/test_review_xdg_isolation.py — 읽기만 하는 재현 테스트. 이 복사본의 guard에는 대조군 변경이 적용돼 있으므로 원본 재현 시 HEAD의 guard를 사용해야 한다.
|
||||
- processes-after.json — 시험 후 OpenCode 프로세스 확인.
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -0,0 +1,56 @@
|
||||
# Review Report: Issue #60 (`fix/issue-60-opencode-v2`)
|
||||
|
||||
- **Job ID**: `1abe0a90`
|
||||
- **Reviewer**: `herdr:reviewer-agy-01` (`agy`)
|
||||
- **Commit Range**: `74333f87d0c02888c2b5b281e912d97e74348c01` .. `6b4b96fa1875862ef3bed830fe7b04166b1fad58` (3 commits across 13 files)
|
||||
- **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T055915Z-abe16e/1`
|
||||
- **Host / Runtime**: macOS `arm64-apple-darwin25` (Darwin 25.3.0), `/usr/bin/python3` 3.9.6 & `.venv/bin/python` 3.9.6, `/bin/bash` 3.2.57, `opencode` v2.0.32 (`/Users/godopu16/.opencode/bin/opencode`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Review Findings by Focus Area
|
||||
|
||||
### 1.1 OpenCode 2.x Schema, CLI Contract, and Purge Safety
|
||||
- **`_resolve_db` (`opencode debug paths`)**:
|
||||
- `.agents/skills/lib_py/agents/adapters/opencode.py:88-107` invokes `self._run(ctx, ["debug", "paths"], timeout=5)` with `HOME`, `HOME_DIR`, and `XDG_DATA_HOME=<ctx.home_dir>/.local/share`, parses the `db` line, and falls back to `f"{ctx.home_dir}/.local/share/opencode/opencode.db"`.
|
||||
- Verified against real `opencode v2.0.32` in an isolated temporary `HOME`: `debug paths` returns `<home>/.local/share/opencode/opencode.db` without creating the database file.
|
||||
- **`verify_artifact` and `discover` (`session_v2` & `parent_id IS NULL`)**:
|
||||
- `verify_artifact` (`.agents/skills/lib_py/agents/adapters/opencode.py:113-147`) queries `SELECT directory, time_created FROM session_v2 WHERE id=?`, converts millisecond `time_created` (`float(started_at) / 1000.0`) against `ctx.epoch`, and checks `workspace_key(found_cwd) == workspace_key(ctx.cwd)`.
|
||||
- `discover` (`.agents/skills/lib_py/agents/adapters/opencode.py:198-235`) queries `SELECT id FROM session_v2 WHERE parent_id IS NULL`, filtering out subagent child sessions (which share `directory` and have newer `time_created` than the top-level TUI session).
|
||||
- Verified against read-only inspection (`mode=ro`) of the host's migrated `opencode.db` and a fresh `2.0.32` DB in a temporary `HOME`: fresh 2.x databases contain `session_v2` (with `id`, `parent_id`, `directory`, `time_created`) and no `session`/`message`/`part`/`todo`/`session_share` tables.
|
||||
- **`purge_artifacts` and `residual_artifacts`**:
|
||||
- `purge_artifacts` (`.agents/skills/lib_py/agents/adapters/opencode.py:149-181`) checks `self._has_session(db, uuid)` (`SELECT 1 FROM session_v2 WHERE id=?`) before calling `self._run(ctx, ["session", "delete", "--standalone", "--", uuid], timeout=60)`.
|
||||
- Using `--` before `uuid` prevents option-flag injection if an ID ever starts with `-`, and `--standalone` avoids starting or attaching to a per-user background service.
|
||||
- In a fresh 2.0.32 DB, `session_inbox`, `session_message`, `session_pending`, `instruction_entry`, and `instruction_state` all have `ON DELETE CASCADE` foreign keys to `session_v2(id)`, and `opencode session delete` also removes `event` and `event_sequence` rows (`aggregate_id`) for the session and its child sessions while leaving other sessions untouched.
|
||||
- For databases migrated from 1.x, `purge_artifacts` subsequently deletes matching `uuid` rows from `part`/`parts`/`message`/`messages`/`session`/`sessions` (if those tables exist) and `residual_artifacts` reports any surviving `todo.session_id` or `session_v2.id` (e.g., if `session delete` failed or the binary was missing, in which case `purge_artifacts` emits a `WARN:` on `stderr`).
|
||||
|
||||
### 1.2 End-to-End Lifecycle (`create` → `reconcile` drift C → `status` → `stop` → `resume`)
|
||||
- **`create_session.sh`, `resume_session.sh`, `update_yaml_resumed.sh`**:
|
||||
- All three scripts and `OpenCodeAgentAdapter.spawn_spec` / `resume_spec` consistently use `opencode --auto --standalone` (and `opencode --session <uuid> --auto --standalone` on materialized resume), removing the defunct `--agent build` flag rejected by OpenCode 2.x TUI.
|
||||
- **`reconcile.sh` (drift C) and `status.sh`**:
|
||||
- `.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh:955-990` inspects `PRAGMA table_info(session_v2)`, filters by `parent_id IS NULL`, `directory = ?`, and `time_created >= ?` (ms), and validates candidates via `verify_session_uuid(cwd, 'opencode', uuid, s_eval, mode="discover")`.
|
||||
- `.agents/skills/multi-agent-mux-status/scripts/status.sh:127,337` checks `SELECT 1 FROM session_v2 WHERE id=?` in both human and JSON status paths.
|
||||
- **`--standalone` process lifecycle & `stop_session.sh`**:
|
||||
- Verified in an isolated PTY with real `opencode v2.0.32`: `opencode --auto --standalone` spawns `opencode serve --stdio --port 0` as a direct child of the TUI process; closing the PTY master terminates both the TUI and its `opencode serve --stdio --port 0` child cleanly within ~0.1s with zero lingering background processes.
|
||||
- `stop_session.sh`'s graceful `/exit` (`send_keys_safe` with echo wait before `Enter`) -> `herdr kill-session` (closes PTY) -> `SIGKILL` fallback and `in-use` gate (`ps` argv check for `--session <uuid>`) work cleanly with `--standalone`.
|
||||
|
||||
### 1.3 Linux Portability, Shell/Python Compatibility, and Completeness
|
||||
- **Linux & XDG portability**:
|
||||
- `OpenCodeAgentAdapter._run` explicitly sets `HOME`, `HOME_DIR`, and `XDG_DATA_HOME = os.path.join(ctx.home_dir, ".local/share")`, ensuring that even on Linux hosts where `XDG_DATA_HOME` is set in the ambient desktop environment, adapter CLI invocations remain isolated to `ctx.home_dir`.
|
||||
- `test_real_opencode_2x_contract` explicitly unsets `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, and `HOME_DIR` via `monkeypatch.delenv`.
|
||||
- **Python 3.9 & Bash 3.2 compatibility**:
|
||||
- Verified on macOS `/usr/bin/python3` / `.venv/bin/python` (3.9.6) and `/bin/bash` (3.2.57 with `set -u`).
|
||||
- **Repository-wide search for stale 1.x assumptions**:
|
||||
- Grepping `.agents/skills/`, `tests/`, and `docs/` for `db path`, `--agent build`, `FROM session\b`, `INTO session\b`, and `table_info(session)` confirms no remaining 1.x code or documentation assumptions exist outside explanatory comments.
|
||||
- **Test coverage & negative regression verification**:
|
||||
- `tests/test_issue60_opencode_v2.py` (10/10 passed, including `test_real_opencode_2x_contract` against real `opencode v2.0.32`), `tests/test_a4_adapter_contract.py`, `tests/test_issue31_purge_residual.py`, `tests/test_issue42_skill_blocks.py`, `tests/test_issue55_agent_homes.py`, and `tests/test_tier1_unit.py` all pass.
|
||||
- Reverting `SELECT id FROM session_v2 WHERE parent_id IS NULL` to `SELECT id FROM session` in an isolated `/tmp` copy immediately fails `test_discover_and_verify_read_session_v2_and_skip_child_sessions` and `test_a_fresh_2x_database_has_no_session_table_and_still_works`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Non-Blocking Observation (Out of Scope for Issue #60)
|
||||
|
||||
- **[Note — `stall_watch.py` `background_children` with `--standalone`]**:
|
||||
- Because `opencode --auto --standalone` keeps `opencode serve --stdio --port 0` alive as a child process of the TUI while idle, and `OpenCodeAgentAdapter.resident_children` is `()`, `lib_py.stall_watch.background_children(agent_pid, adapter.resident_children)` sees the `opencode serve --stdio --port 0` child process and conservatively withholds the optional idle nudge (`NOTIFY_IDLE_AND_NUDGE`). If idle nudging is desired for `--standalone` OpenCode in the future, `stall_watch.background_children` can exempt children whose `argv` matches `opencode serve --stdio` (while still walking any tool subprocesses spawned beneath that server). This does not affect any session lifecycle, discovery, or purge behavior in Issue #60.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,60 @@
|
||||
# Issue #60 Code Review Report — Round 3 (`0e05b1b`)
|
||||
|
||||
- **Job ID**: `5420eae1`
|
||||
- **Reviewer**: `agy` (`herdr:reviewer-agy-01`)
|
||||
- **Branch**: `fix/issue-60-opencode-v2`
|
||||
- **Base .. Head**: `74333f87d0c02888c2b5b281e912d97e74348c01` .. `0e05b1b6777ceb837066454ac688e04a0f5d14a0` (5 commits; Round 3 delta: `0e05b1b`)
|
||||
- **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T073639Z-b5cb7f/1`
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
Commit `0e05b1b` cleanly resolves all blocking and non-blocking findings raised in Round 2 (`cb5f439`) without introducing regressions:
|
||||
|
||||
1. **Test isolation & Issue #55 path-variable guard (`tests/_agent_home_guard.py`)**:
|
||||
- `XDG_DATA_HOME` is added to `DROPPED_VARS`, so `conftest._isolate_agent_homes` drops any ambient `XDG_DATA_HOME` for every test (preventing a sandbox that overrides `HOME` from resolving the runner's ambient `XDG_DATA_HOME` via `_data_home(ctx)`).
|
||||
- `watch_dirs(ambient)` now watches `os.path.join(ambient.get("XDG_DATA_HOME") or os.path.join(home, ".local", "share"), "opencode")` so the session leak guard monitors the real opencode data directory even when `XDG_DATA_HOME` is set in the ambient environment.
|
||||
- `tests/test_issue55_agent_homes.py::test_l1_every_path_variable_the_code_reads_is_accounted_for` passes.
|
||||
2. **`status.sh` DB path resolution (`.agents/skills/multi-agent-mux-status/scripts/status.sh:108-111, 308-311`)**:
|
||||
- Both `--json` and text modes now resolve the opencode DB path via `get_adapter('opencode')._resolve_db(DiscoveryContext(workspace=cwd, agent_name='opencode', home_dir=home))` instead of a hardcoded `~/.local/share/opencode` lookup, keeping `status.sh` consistent with `reconcile.sh` and the adapter's `debug paths` / `XDG_DATA_HOME` resolution.
|
||||
3. **Real binary `--version` probe isolation (`tests/test_issue60_opencode_v2.py:294-301`)**:
|
||||
- `_real_opencode()` now runs `opencode --version` inside a `tempfile.TemporaryDirectory()` with `HOME` and `TMPDIR` pointed at that temp dir and `XDG_*` omitted from `env`, preventing import-time log writes to the user's real `~/.local/share/opencode/log`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verification of Cumulative Changes (`74333f8..0e05b1b`)
|
||||
|
||||
| Area | Verification Result |
|
||||
| :--- | :--- |
|
||||
| **Adapter CLI & Schema (`opencode.py`)** | `spawn_spec` / `resume_spec` use `--auto --standalone` (and `--session <id>` when materialized); `_resolve_db` parses `opencode debug paths` with fallback to `<_data_home>/opencode/opencode.db`; `discover` (`WHERE parent_id IS NULL`) and `verify_artifact` query `session_v2`. |
|
||||
| **Purge & Residual (`opencode.py`)** | Collects `_descendants(db, uuid)` from `session_v2` and legacy `session` before invoking `opencode session delete --standalone -- <uuid>`, deletes `_LEGACY_TABLES` rows for `[uuid] + descendants`, and reports remaining `session_v2` / legacy rows (tagging child sessions with `[child session <id>]`). |
|
||||
| **In-Use & Stall Watch (`opencode.py`)** | `conversation_in_use` ignores `serve --service` holders of `opencode.db` while detecting `--standalone` / `--session` processes; `resident_children = ('opencode',)` prevents idle `opencode serve --stdio --port 0` from tripping `background_children` while still detecting tool subprocesses under it. |
|
||||
| **Shell Scripts (`create_session.sh`, `resume_session.sh`, `update_yaml_resumed.sh`, `reconcile.sh`, `status.sh`)** | All shell launch/resume commands use `--auto --standalone` without `--agent`; `reconcile.sh` drift C queries `session_v2` with `parent_id IS NULL`; `status.sh` uses the adapter's `_resolve_db` and queries `session_v2`. |
|
||||
| **Tests & Guard (`_agent_home_guard.py`, `_fake_opencode.py`, `test_issue60_opencode_v2.py`, `test_a4_adapter_contract.py`, `test_issue31_purge_residual.py`, `test_tier2_component.py`)** | `XDG_DATA_HOME` is in `DROPPED_VARS` and watched by `watch_dirs`; `_real_opencode()` runs `--version` in an isolated temporary `HOME`/`TMPDIR`. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Test Execution Summary
|
||||
|
||||
All tests were executed in the verification worktree (`/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T073639Z-b5cb7f/1`) with `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, external `--basetemp`, and isolated environment variables:
|
||||
|
||||
1. **Adapter, Guard, Purge/Residual, and Issue #60 Suites**:
|
||||
- `tests/test_issue60_opencode_v2.py` (15 passed, including `test_real_opencode_2x_contract` against real opencode v2.0.32 in isolated `HOME`/`TMPDIR`)
|
||||
- `tests/test_issue55_agent_homes.py` (11 passed, including `test_l1_every_path_variable_the_code_reads_is_accounted_for`)
|
||||
- `tests/test_a4_adapter_contract.py` (33 passed)
|
||||
- `tests/test_issue31_purge_residual.py` (67 passed)
|
||||
- **Total**: `126 passed in 48.31s`
|
||||
2. **Tier 2 Component Suite (`opencode or status`) with `XDG_DATA_HOME` Canary**:
|
||||
- Ran `tests/test_tier2_component.py -k "opencode or status"` with `XDG_DATA_HOME` pointed at a canary temp directory.
|
||||
- **Result**: `7 passed in 62.22s`, and `XDG_CANARY` remained completely empty.
|
||||
3. **Post-check**:
|
||||
- Verification worktree is clean (`git status -s` empty, no `.mam` directory), and no stray `opencode` test processes remain.
|
||||
|
||||
---
|
||||
|
||||
## 4. Findings
|
||||
|
||||
No blocking or non-blocking issues remaining. The branch `fix/issue-60-opencode-v2` (`74333f8..0e05b1b`) is ready to merge.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,62 @@
|
||||
# Review Report (Round 2): Issue #60 (`fix/issue-60-opencode-v2`)
|
||||
|
||||
- **Job ID**: `c9d06e85`
|
||||
- **Reviewer**: `herdr:reviewer-agy-01` (`agy`)
|
||||
- **Commit Range**: `74333f87d0c02888c2b5b281e912d97e74348c01` .. `cb5f439cce41a2741a6b9974a308a765e50f00ad` (4 commits; Round 2 delta: `6b4b96fa1875862ef3bed830fe7b04166b1fad58..cb5f439cce41a2741a6b9974a308a765e50f00ad`, 1 commit)
|
||||
- **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T064723Z-79133f/1`
|
||||
- **Host / Runtime**: macOS `arm64-apple-darwin25` (Darwin 25.3.0), `/usr/bin/python3` 3.9.6 & `.venv/bin/python` 3.9.6, `/bin/bash` 3.2.57, `opencode` v2.0.32 (`/Users/godopu16/.opencode/bin/opencode`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Round 1 Findings Verification (`6b4b96f..cb5f439`)
|
||||
|
||||
All five Round 1 items addressed in commit `cb5f439` work as designed and their new tests fail against pre-fix Round 1 code (`6b4b96f`):
|
||||
|
||||
1. **[Resolved — Child session 1.x row purge & residual reporting (`_descendants`, `_LEGACY_TABLES`)]**:
|
||||
- `.agents/skills/lib_py/agents/adapters/opencode.py:160-222` resolves all descendant session IDs (`_descendants(db, uuid)`, walking `parent_id` across both `session_v2` and 1.x `session` tables to arbitrary depth with cycle protection in read-only mode) before calling `opencode session delete --standalone -- <uuid>`.
|
||||
- `purge_artifacts` then deletes rows for `[uuid] + descendants` across all 1.x legacy tables (`_LEGACY_TABLES`: `message`, `messages`, `part`, `parts`, `todo`, `session_share`, `session`, `sessions`).
|
||||
- `residual_artifacts` (`.agents/skills/lib_py/agents/adapters/opencode.py:229-238`) checks `[uuid] + self._descendants(db, uuid)` and tags surviving descendant rows with `[child session <id>]`.
|
||||
2. **[Resolved — `conversation_in_use` ignores background `serve --service` daemon]**:
|
||||
- `.agents/skills/lib_py/agents/adapters/opencode.py:240-262` checks `conversation_in_use(uuid, [])` first, then inspects `lsof -t <db>` PIDs via `ps -o command= -p <pid>` and ignores processes with `serve --service` in their command line while still treating `--standalone` TUI/server holders as in-use (and failing safe to `True` on `lsof`/`ps` errors).
|
||||
3. **[Resolved — `XDG_DATA_HOME` handling in `_data_home(ctx)`]**:
|
||||
- `.agents/skills/lib_py/agents/adapters/opencode.py:89-99` uses the caller's `XDG_DATA_HOME` when `ctx.home_dir` resolves to `os.path.expanduser("~")` (or is empty), and `<ctx.home_dir>/.local/share` when `ctx.home_dir` is a separate directory.
|
||||
4. **[Resolved — `resident_children = ('opencode',)`]**:
|
||||
- `.agents/skills/lib_py/agents/adapters/opencode.py:72-76` declares `resident_children = ('opencode',)`, so `stall_watch.background_children` skips the idle `opencode serve --stdio --port 0` child while still walking any tool subprocesses spawned beneath it.
|
||||
5. **[Resolved — Test coverage & pre-fix failure check]**:
|
||||
- `test_real_opencode_2x_contract` binds `adapter._bin` to `REAL` (no `PATH` dependency), and `test_comp_opencode_create_and_reconcile_and_stop` (`tests/test_tier2_component.py:1252-1273`) verifies that `reconcile.sh` drift C pins the top-level session rather than a newer child session in the same directory.
|
||||
- Running the 5 new tests in `tests/test_issue60_opencode_v2.py` against Round 1 commit `6b4b96f` in an isolated `/tmp` copy fails all 5 (`5 failed, 10 deselected`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Issue Blocking Merge
|
||||
|
||||
### [P1] `tests/test_issue55_agent_homes.py::test_l1_every_path_variable_the_code_reads_is_accounted_for` fails (`XDG_DATA_HOME` missing from `tests/_agent_home_guard.py`)
|
||||
|
||||
- **Location**: `tests/_agent_home_guard.py:21` (triggered by `.agents/skills/lib_py/agents/adapters/opencode.py:94` reading `os.environ.get("XDG_DATA_HOME")`)
|
||||
- **Symptom**:
|
||||
```text
|
||||
FAILED tests/test_issue55_agent_homes.py::test_l1_every_path_variable_the_code_reads_is_accounted_for
|
||||
AssertionError: path variables the adapters/lib.sh read but conftest._isolate_agent_homes does not drop: ['XDG_DATA_HOME']
|
||||
```
|
||||
- **Root Cause & Linux Isolation Impact**:
|
||||
- `test_issue55_agent_homes.py` scans `.agents/skills/lib_py/agents/*.py`, `lib_py/paths.py`, and `lib.sh` for any `*_DIR` / `*_HOME` environment variable read by the adapters and requires every such variable to be accounted for in `tests/_agent_home_guard.py` (`DROPPED_VARS` or `LEFT_ALONE`).
|
||||
- Furthermore, `XDG_DATA_HOME` **must** be in `DROPPED_VARS` (not `LEFT_ALONE`): in `opencode.py:95`, `own = os.path.realpath(ctx.home_dir or "") == os.path.realpath(os.path.expanduser("~"))`. When a test isolates `HOME` via `monkeypatch.setenv("HOME", str(tmp_path))` (such as `mam_sandbox`), `os.path.expanduser("~")` equals `ctx.home_dir` (`own` is `True`). On a Linux machine where `XDG_DATA_HOME` is set in the outer shell environment, if `conftest._isolate_agent_homes` does not drop `XDG_DATA_HOME`, `_data_home(ctx)` would use the outer environment's real `XDG_DATA_HOME` instead of `<tmp_path>/.local/share`!
|
||||
- **Required Fix**:
|
||||
Add `"XDG_DATA_HOME"` to `DROPPED_VARS` in `tests/_agent_home_guard.py:21` (and optionally honour `ambient.get("XDG_DATA_HOME")` for `"opencode"` in `watch_dirs(ambient)` at `tests/_agent_home_guard.py:48`):
|
||||
```python
|
||||
DROPPED_VARS = ("HOME_DIR", "CLAUDE_PROJECT_DIR", "CLAUDE_CONFIG_DIR", "CODEX_HOME", "XDG_DATA_HOME", "LOCAL_BIN")
|
||||
```
|
||||
*(Verified in an isolated `/tmp` copy: adding `"XDG_DATA_HOME"` to `DROPPED_VARS` makes both `tests/test_issue55_agent_homes.py` (11/11) and `tests/test_issue60_opencode_v2.py` (15/15, including `test_the_data_dir_follows_the_callers_xdg_data_home_only_for_its_own_home` which sets `XDG_DATA_HOME` inside the test) pass cleanly).*
|
||||
|
||||
---
|
||||
|
||||
## 3. Verification Summary
|
||||
|
||||
- `tests/test_issue60_opencode_v2.py`: **15 passed** (including `test_real_opencode_2x_contract` against `opencode v2.0.32`)
|
||||
- `tests/test_a4_adapter_contract.py`: **33 passed**
|
||||
- `tests/test_tier2_component.py -k opencode`: **1 passed** (`test_comp_opencode_create_and_reconcile_and_stop`)
|
||||
- `tests/test_issue31_purge_residual.py -k opencode`: **1 passed** (`test_opencode_purge_leaves_no_rows`)
|
||||
- `tests/test_issue55_agent_homes.py`: **10 passed, 1 failed** (`test_l1_every_path_variable_the_code_reads_is_accounted_for`)
|
||||
- Verification worktree `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T064723Z-79133f/1` verified clean (`git status -s` empty, no `.mam` directory created).
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Review 4709b38f — Issue #60, opencode 2.x
|
||||
|
||||
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T055915Z-abe16e/1` (`loop-20261018T055915Z-abe16e`). Head `6b4b96fa1875862ef3bed830fe7b04166b1fad58`, tree `9cfb2106081eba96b58cc6b7573031de0acba399`, base `74333f87d0c02888c2b5b281e912d97e74348c01` is an ancestor. Three commits, 13 files, +460/−275. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is `74333f87d0c02888c2b5b281e912d97e74348c01`, porcelain empty. No product file was edited.
|
||||
|
||||
Host tools: `/usr/bin/python3` 3.9.6, the control-plane venv the same 3.9.6, `/bin/bash` 3.2.57. `bash -n` accepts `create_session.sh`, `resume_session.sh`, `update_yaml_resumed.sh`, `reconcile.sh`, and `status.sh`. `opencode --version` in a temporary home, with `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, and `XDG_CACHE_HOME` unset, printed `opencode v2.0.32`.
|
||||
|
||||
The user's background service stayed up for the whole review. Its pid is 86524, the command is `opencode serve --service`, and the service file mtime stayed 1792246838. The real database mtime stayed 1792247140. Nothing in this review called `session delete`, `service stop`, or an API against that database. After the tests the only `opencode` binary still running was that service.
|
||||
|
||||
## What this Mac's database already stores
|
||||
|
||||
A read-only open of `~/.local/share/opencode/opencode.db` shows a migrated database. `session_v2` and `session` each have 4 rows. Every `session_v2` row has `parent_id` NULL, and every id matches `[A-Za-z0-9_]`. The 2.x tables present are `session_v2`, `session_message`, `session_inbox`, `session_pending`, `event`, and `event_sequence`. The 1.x tables a migration keeps are also present: `session`, `message`, `part`, `todo`, and `session_share`. `todo` and `session_share` are keyed by `session_id`. Column names only were read. No row body was printed.
|
||||
|
||||
That matches the adapter. `discover` reads `session_v2` where `parent_id IS NULL` (`opencode.py:220`). A child shares the workspace directory and can be newer than the TUI session, so drift C in `reconcile.sh:966-968` uses the same filter. `verify_artifact` still accepts a known child id (`opencode.py:115`). `status.sh` checks `session_v2` in both copies of `resume_on_disk`.
|
||||
|
||||
## Purge scope
|
||||
|
||||
`purge_artifacts` calls `opencode session delete --standalone -- <id>` only when `session_v2` contains that id (`opencode.py:136-141`). The fake CLI, and the real 2.0.32 contract below, delete that id and its children from the 2.x tables and leave the other session alone. A missing id does not invoke the CLI (`test_purge_does_not_run_opencode_for_a_session_it_does_not_hold`). A missing binary or a non-zero delete writes a `WARN` and leaves the `session_v2` row for `residual_artifacts` (`opencode.py:143-149`, `test_a_failed_delete_is_warned_and_reported`).
|
||||
|
||||
The SQL that follows deletes `message`, `part`, and `session` (and the plural names) for that same id. It does not delete `todo` or `session_share`. `db_rows_naming` reports any remaining `session_id` or `aggregate_id` rows, so a migrated `todo` stays visible: `test_opencode_residual_reports_event_rows` expects residual `[<db> (todo.session_id: 1 rows)]` and eight entries for the other session. `session_share.session_id` is on that same report path. The append of `sqlite rows for session` happens when one of those 1.x tables exists (`opencode.py:172-173`), including when the delete matched no row. A fresh 2.x database has none of those tables, and the real contract asserts the purged list is only the session-delete line.
|
||||
|
||||
## Spawn, resume, and the real binary
|
||||
|
||||
`spawn_spec` is `opencode --auto --standalone`. `resume_spec` with a materialized id is `opencode --session <id> --auto --standalone` (`opencode.py:198-204`). The same strings are the `opencode` arms in `create_session.sh`, `resume_session.sh`, and `update_yaml_resumed.sh`. `test_spawn_and_resume_never_pass_agent_and_run_standalone` reads those three scripts and rejects a line that still contains `--agent`. Live skill files and `docs/` have no `opencode db path` and no `--agent build`. `VERSIONS.md`, `bug-report/`, and `.agents/reports/` still describe the 1.x command; the brief leaves those historical notes in place. `create_session.sh` still treats `auth.json` or a provider key as enough, and otherwise accepts `opencode --version`. That check was left as it was, and `--version` exits 0 on this install.
|
||||
|
||||
`_run` points `HOME` and `XDG_DATA_HOME` at the discovery home before `debug paths` or `session delete` (`opencode.py:80-87`). `_resolve_db` reads the `db` line. The fallback is `<home>/.local/share/opencode/opencode.db`.
|
||||
|
||||
The opt-in contract `test_real_opencode_2x_contract` ran here, because 2.0.32 is installed. It used a pytest temporary home and tmp, unset the four `XDG_*` variables, and called only `--help`, `api --standalone`, and `session delete --standalone`. `--help` contains `--standalone` and `--session`. Discover returned the parent id. `verify_artifact` on the child was true. Purge of the parent cleared parent and child residuals. A second delete exited 1 with `Session not found`. No model prompt was sent.
|
||||
|
||||
This review did not start a herdr pane, so it did not drive create, a first TUI message, drift C, stop, and resume as one live chain. The pieces of that chain are the command lines above, drift C's `session_v2` query, and `verify_session_uuid`, which calls the adapter. Stop's graceful `/exit` then kill-session then SIGKILL path is unchanged. `--standalone` is what makes the server a child of that TUI. The id charset on this database has no shell metacharacters, so the unquoted `$UUID` in `resume_session.sh` is safe for the ids stored here.
|
||||
|
||||
The new code uses `HOME`, `XDG_DATA_HOME`, sqlite, and `subprocess`. Nothing in it is macOS-only. This host is macOS, so Linux was not executed here.
|
||||
|
||||
## Tests
|
||||
|
||||
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mam4709-4709b38f-pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_TMPDIR`, `CODEX_HOME`, and `PYTHONPATH` were dropped.
|
||||
|
||||
On the snapshot, 44 tests passed in 7.56 s: `tests/test_issue60_opencode_v2.py` (including the real-binary contract), `tests/test_a4_adapter_contract.py`, and `tests/test_issue31_purge_residual.py::test_opencode_residual_reports_event_rows`. No skip and no error.
|
||||
|
||||
A separate clone whose `discover` query read `session` instead of `session_v2` failed both `test_discover_and_verify_read_session_v2_and_skip_child_sessions` and `test_a_fresh_2x_database_has_no_session_table_and_still_works` (`assert [] == ['ses_main']`), in 1.07 s. A fresh 2.x database has no `session` table, the query raises, and `discover` returns `[]`. The clone was discarded. Snapshot porcelain stayed empty.
|
||||
|
||||
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `74333f87`. The herdr config mtime is still 1784456394. The opencode database mtime is still 1792247140 and service pid 86524 is still `opencode serve --service`.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,53 @@
|
||||
# Review 5ab36507 — Issue #60, second pass
|
||||
|
||||
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T064723Z-79133f/1` (`loop-20261018T064723Z-79133f`). Head `cb5f439cce41a2741a6b9974a308a765e50f00ad`, tree `cfec95403cbde10a6340b9007e1246c83be461cd`, base `74333f87d0c02888c2b5b281e912d97e74348c01` is an ancestor. Four commits, 14 files, +689/−300. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is `74333f87d0c02888c2b5b281e912d97e74348c01`, porcelain empty. No product file was edited.
|
||||
|
||||
The new commit on top of the first-pass head `6b4b96f` is `cb5f439` (`fix(opencode): a purge takes its child sessions' 1.x rows, the background service alone holds no session, XDG_DATA_HOME is followed`). The checks below are against the whole base..head range.
|
||||
|
||||
Host tools: `/usr/bin/python3` 3.9.6, the control-plane venv the same 3.9.6, `/bin/bash` 3.2.57. `bash -n` accepts `create_session.sh`, `resume_session.sh`, `update_yaml_resumed.sh`, `reconcile.sh`, and `status.sh`. `opencode --version` in a temporary home, with `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, and `XDG_CACHE_HOME` unset, printed `opencode v2.0.32`.
|
||||
|
||||
The user's background service stayed up for the whole review. Its pid is 86524, the command is `opencode serve --service`, and both service-file mtimes stayed 1792246838. The real database mtime stayed 1792247140, and a read-only count stayed at 4 `session` rows and 4 `session_v2` rows. `lsof` on that database shows only pid 86524, and that command line contains `serve --service`. Nothing in this review called `session delete`, `service stop`, or an API against that database.
|
||||
|
||||
## What cb5f439 changes
|
||||
|
||||
`purge_artifacts` records descendants before `opencode session delete` (`opencode.py:162`). `_descendants` walks `parent_id` in `session_v2` and, when that column exists, in the 1.x `session` table, at every depth, through a read-only URI (`opencode.py:195-217`). The SQL that follows deletes `message`, `part`, `todo`, `session_share`, and `session` (and the plural names) for the parent and those descendants (`opencode.py:178-190`). `todo` and `session_share` are keyed by `session_id` on this machine's migrated database; the purge deletes those rows and the report path is what remains. A fresh 2.x database has none of those tables, and the real contract still expects the purged list to be only the session-delete line.
|
||||
|
||||
`residual_artifacts` asks `db_rows_naming` for the parent and for each descendant still on record, and suffixes the descendant lines with `[child session <id>]` (`opencode.py:229-237`). `test_purge_takes_the_1x_rows_of_the_child_sessions_too` seeds a parent, a child, a grandchild, and an unrelated session, and expects the unrelated session to keep six entries. `test_a_failed_delete_reports_the_child_sessions_left` expects the child `session_v2` row, with that suffix, when the binary is missing.
|
||||
|
||||
`conversation_in_use` still treats a command line that names the id as in use. A process that holds the shared database counts as well, except a command line that contains `serve --service` (`opencode.py:240-262`). On this Mac that is the only holder of the real database, so a purge of a session in that file is not refused solely because the user's service is running. Any other holder still returns true, and the purge CLI still exits 4 before it deletes (`agents/__main__.py:146-148`). `lsof` or `ps` raising is treated as in use. When `lsof` is absent the database-holder check returns false; the command-line check still runs. This host has `lsof`.
|
||||
|
||||
`_data_home` returns the caller's `XDG_DATA_HOME` when that variable is set and `ctx.home_dir` is the caller's own home (realpath), and `<home>/.local/share` for any other home (`opencode.py:87-94`). `_run` puts that value in the child environment (`opencode.py:103-106`). `test_the_data_dir_follows_the_callers_xdg_data_home_only_for_its_own_home` covers both sides and the unset case.
|
||||
|
||||
`resident_children` is `('opencode',)` (`opencode.py:70-73`). `background_children` records a process's children before it skips a resident name (`stall_watch.py:126-132`), so the `opencode serve --stdio` child is idle and a tool under that child still counts. `test_the_standalone_server_is_a_resident_child_but_its_tools_are_work` passed here. The Linux `_proc_table` reads `/proc`; the name check also uses `argv[0]`, so a process whose comm is the binary and whose argv0 is `opencode` still matches. This host took the `ps` path.
|
||||
|
||||
The real-binary test binds `_bin` to the absolute path it already resolved (`test_issue60_opencode_v2.py` `test_real_opencode_2x_contract`), so the later calls do not depend on `PATH` under the temporary home. Drift C in `reconcile.sh:966-968` keeps `parent_id IS NULL`. `test_comp_opencode_create_and_reconcile_and_stop` inserts a newer child in the same directory and requires the pinned id to be the parent. That test passed.
|
||||
|
||||
## Tests that catch a revert
|
||||
|
||||
On the snapshot, 50 tests passed in 65.40 s: `tests/test_issue60_opencode_v2.py` (including the real-binary contract), `tests/test_a4_adapter_contract.py`, `tests/test_issue31_purge_residual.py::test_opencode_purge_leaves_no_rows`, and `tests/test_tier2_component.py::test_comp_opencode_create_and_reconcile_and_stop`. No skip and no error. Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mam5ab3-5ab36507-pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_TMPDIR`, `CODEX_HOME`, `PYTHONPATH`, and the four `XDG_*` variables were dropped.
|
||||
|
||||
A separate clone, discarded afterward, failed three probes:
|
||||
|
||||
- SQL limited to the parent id: `test_purge_takes_the_1x_rows_of_the_child_sessions_too` failed in 0.66 s. The purged list was only the session-delete line.
|
||||
- Every database holder treated as in use: `test_the_background_service_alone_does_not_hold_a_session` failed in 0.96 s (`True` where the service-shaped holder must leave the session free).
|
||||
- Drift C without `parent_id IS NULL`: `test_comp_opencode_create_and_reconcile_and_stop` failed in 17.08 s. `opencode_session_id_own` stayed unset, so the parent was not pinned.
|
||||
|
||||
Snapshot porcelain stayed empty.
|
||||
|
||||
The real contract ran under a pytest temporary home and tmp, with the four `XDG_*` variables unset, and called `--help`, `api --standalone`, and `session delete --standalone`. `--help` contains `--standalone` and `--session`. Discover returned the parent. `verify_artifact` on the child was true. Purge cleared parent and child residuals. A second delete exited 1 with `Session not found`. No model prompt was sent. After that run the real database still had 4 and 4 rows.
|
||||
|
||||
## Boundaries
|
||||
|
||||
`status.sh` `resume_on_disk` queries `session_v2` once it has opened a database (`status.sh:127` and the second copy at `:337`). The path it opens is still `$home/.local/share/opencode` (then `storage.db`, then a `project/*.db` walk). Drift C asks the adapter, which follows `XDG_DATA_HOME` for the caller's own home. On this host `XDG_DATA_HOME` is unset, so both look at `~/.local/share/opencode/opencode.db`. A caller who sets `XDG_DATA_HOME` would see drift C and `resume_on_disk` use different files.
|
||||
|
||||
The append of `sqlite rows for session` happens when one of the 1.x tables exists (`opencode.py:188-190`), including when the delete matched no row. `_run` copies the rest of the environment and sets `HOME`, `HOME_DIR`, and `XDG_DATA_HOME`. The contract test clears `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, and `XDG_CACHE_HOME` before it starts the binary.
|
||||
|
||||
This review did not start a herdr pane, so it did not drive create, a first TUI message, drift C, stop, and resume as one live chain against a real TUI. The tier-2 test drives create, drift C, and stop against the mock herdr and the fake CLI, in a sandbox home. Stop's graceful `/exit` then kill-session then SIGKILL path is unchanged. `--standalone` is what makes the server a child of that TUI. The id charset on this database matches `[A-Za-z0-9_]`, so the unquoted `$UUID` in `resume_session.sh` is safe for the ids stored here.
|
||||
|
||||
Live skill files and `docs/` have no `opencode db path` and no `--agent build`. `VERSIONS.md`, `bug-report/`, and `.agents/reports/` still describe the 1.x command; the brief leaves those historical notes in place. `create_session.sh` still treats `auth.json` or a provider key as enough, and otherwise accepts `opencode --version`. That check was left as it was, and `--version` exits 0 on this install.
|
||||
|
||||
The new code uses `HOME`, `XDG_DATA_HOME`, sqlite, `subprocess`, `lsof`, and `ps`. `stall_watch` reads `/proc` when that directory exists. Nothing in the new adapter code is macOS-only. This host is macOS, so Linux was not executed here.
|
||||
|
||||
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `74333f87`. The herdr config mtime is still 1784456394. The opencode database mtime is still 1792247140 and service pid 86524 is still `opencode serve --service`.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,37 @@
|
||||
# Review d384cdc0 — Issue #60, third pass
|
||||
|
||||
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261018T073639Z-b5cb7f/1` (`loop-20261018T073639Z-b5cb7f`). Head `0e05b1b6777ceb837066454ac688e04a0f5d14a0`, tree `c5cb1b16af283b3fdb447bafb865ba1abbfbb5a8`, base `74333f87d0c02888c2b5b281e912d97e74348c01` is an ancestor. Five commits, 15 files, +705/−330. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is `74333f87d0c02888c2b5b281e912d97e74348c01`, porcelain empty. No product file was edited.
|
||||
|
||||
The new commit on top of `cb5f439` is `0e05b1b`. It changes three files: `tests/_agent_home_guard.py`, `.agents/skills/multi-agent-mux-status/scripts/status.sh` (both copies of `resume_on_disk`), and the `--version` probe in `tests/test_issue60_opencode_v2.py`.
|
||||
|
||||
Host tools: the control-plane venv is Python 3.9.6, `/bin/bash` is 3.2.57. `bash -n` accepts `status.sh` and `create_session.sh`. `opencode --version` in a temporary home, with an environment of only `PATH`, `HOME`, and `TMPDIR`, printed `opencode v2.0.32`.
|
||||
|
||||
The user's background service stayed up. Its pid is 86524, the command is `opencode serve --service`, and both service-file mtimes stayed 1792246838. The real database mtime stayed 1792247140, with 4 `session` rows and 4 `session_v2` rows. The real `opencode.log` mtime stayed 1792308195 and its size stayed the same. The herdr config mtime stayed 1784456394.
|
||||
|
||||
## What 0e05b1b fixes
|
||||
|
||||
`_agent_home_guard.DROPPED_VARS` now includes `XDG_DATA_HOME` (`_agent_home_guard.py:21`). `conftest._isolate_agent_homes` drops that tuple for every test, and `pytest_configure` records the value first so the session guard can watch it. `watch_dirs` uses `<XDG_DATA_HOME>/opencode` when the recorded environment has the variable, and `<home>/.local/share/opencode` otherwise (`_agent_home_guard.py:48-49`).
|
||||
|
||||
That matches the second-pass failure. The adapter follows the caller's `XDG_DATA_HOME` when `ctx.home_dir` is the caller's own home. A sandbox that only changed `HOME` still inherited the runner's `XDG_DATA_HOME`, so discovery and purge resolved the runner's data directory. Dropping the variable makes `HOME` govern, which is what `_data_home` does when the variable is unset.
|
||||
|
||||
`test_l1_every_path_variable_the_code_reads_is_accounted_for` scans adapters for `*_HOME` reads. `XDG_DATA_HOME` is one of them (`opencode.py` `os.environ.get("XDG_DATA_HOME")`). A clone whose `DROPPED_VARS` omitted it failed that test in 0.04 s with `['XDG_DATA_HOME']`. The suite below was started with `XDG_DATA_HOME` pointed at an empty directory `/tmp/mamd384-d384cdc0-xdg`. After 65 passed tests that directory was still empty.
|
||||
|
||||
`status.sh` `resume_on_disk`, in the `--json` block and in the text block, asks `get_adapter('opencode')._resolve_db(DiscoveryContext(..., home_dir=home))` (`status.sh:108-111` and `:305-308` in the second copy). `home` is `HOME_DIR`. `env_python` passes the parent environment through, so a set `XDG_DATA_HOME` reaches `_data_home` when `HOME_DIR` is the caller's home. A copy of the tree, with the fake opencode on `PATH` and the only database under a separate `XDG_DATA_HOME`, returned exit 0. `--json` `resume_state` was `yes` for `ses_probe` and `MISSING` for an id the database does not hold. Text mode exited 0 and the session's line contained `yes`. `~/.local/share/opencode/opencode.db` under that sandbox home was not created. The existing status checks in the same suite (`test_status_json_schema_fields`, `test_status_text_headers_presence`, and the claude and agy `resume_on_disk` helpers) also exited 0.
|
||||
|
||||
`_real_opencode` runs `--version` inside `tempfile.TemporaryDirectory` with `HOME` and `TMPDIR` set to that directory and no `XDG_*` variables (`test_issue60_opencode_v2.py:298-300`). The same shape, run on its own, wrote a log only under that temporary home. The real log was unchanged. The opt-in contract ran as part of the 65 (no skip).
|
||||
|
||||
## Suite
|
||||
|
||||
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mamd384-d384cdc0-pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_*`, `CODEX_HOME`, and `PYTHONPATH` were dropped. `XDG_DATA_HOME` was the empty sentinel above. `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, and `XDG_CACHE_HOME` were unset.
|
||||
|
||||
65 passed in 68.75 s: `tests/test_issue55_agent_homes.py`, `tests/test_issue60_opencode_v2.py` (including the real-binary contract), `tests/test_a4_adapter_contract.py`, `tests/test_issue31_purge_residual.py::test_opencode_purge_leaves_no_rows`, `tests/test_tier2_component.py::test_comp_opencode_create_and_reconcile_and_stop`, and the four status cases named above. No skip and no error. Snapshot porcelain stayed empty.
|
||||
|
||||
`opencode debug paths` in a temporary home prints the `db` line under that home's `.local/share/opencode` and creates `log/opencode.log` there. It does not create `opencode.db`. So an opencode row in `status.sh` now runs that command, and a real status check appends the log under the data directory `_resolve_db` is about to open. The RESUME value and the exit code measured above stay 0 / `yes` / `MISSING`. The database file is untouched by the path lookup.
|
||||
|
||||
The earlier commits are unchanged in this pass: purge still collects descendants before `session delete` and deletes the 1.x rows of parent and children, `conversation_in_use` still ignores only a `serve --service` holder, drift C still requires `parent_id IS NULL`, and spawn/resume stay `--auto --standalone`. The tier-2 test in this suite pins the parent when a newer child shares the directory, against the mock herdr. This review did not drive a live herdr pane through create, a first TUI message, stop, and resume.
|
||||
|
||||
The new code uses the environment, sqlite, and `subprocess`. Nothing added in `0e05b1b` is macOS-only. This host is macOS, so Linux was not executed here.
|
||||
|
||||
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `74333f87`. The herdr config mtime is still 1784456394. The opencode database mtime is still 1792247140, the opencode log mtime is still 1792308195, and service pid 86524 is still `opencode serve --service`.
|
||||
|
||||
[VERDICT: PASS]
|
||||
Reference in new issue
Block a user