Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/report-dd5c8c62.md
T

9.1 KiB

PR #62 리뷰 — Issue #60 Linux 확인 후속 (job dd5c8c62)

  • 리뷰어: planner-reviewer-claude-01
  • 검증 경로: /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261019T000805Z-975c4c/1
  • identity: HEAD 6a4e385a78c8a561d5cf3644336de6a352ea9271, tree afa2c55925f93820337b69699c670c0b0ca90980 일치. 작업 트리 clean, .mam 없음. base 1d7b7da..head 8커밋. 판정은 base..head 전체에 대한 것이다.
  • 환경: macOS, /usr/bin/python3 3.9.6 와 .venv python 3.9.6, 둘 다 sqlite 3.51.0. 실제 opencode 데이터는 ls·grep -c 로만 봤다. 실제 DB 는 sqlite 로 열지 않았다.

결론

제품 코드에서는 차단 결함을 찾지 못했다. 다만 새 테스트 5개가 이 Mac 에서 항상 실패한다(B1). 이 리뷰의 중점이 macOS 정합성이고, 오케스트레이터의 전체 스위트도 이 Mac 에서 돈다. 그래서 NOT PASS 로 둔다. 고칠 곳은 fixture 몇 줄이고 재설계는 필요 없다.

차단

B1. test_a_query_reads_right_and_leaves_the_sidecars_alone[clean-*] 5개가 macOS 에서 실패한다.

  • 위치: tests/test_issue60_linux_fixes.py:153-154(make("clean"))과 :175-181.
  • 원인: macOS 시스템 SQLite 는 persistent WAL 로 동작해서, 마지막 연결을 닫아도 -wal·-shm 을 지우지 않는다. 임시 DB 에서 쓰기 연결을 닫은 뒤의 목록이 ['o.db', 'o.db-shm', 'o.db-wal'] 였다. 그래서 _wal_writer(db, ws).close() 뒤의 목록이 기대값 ["opencode.db"] 와 다르고, 첫 assert(make(state) == listing)에서 실패한다. 제품 코드까지 가지도 않는다.
  • 재현: 검증 경로에서 env -i … 로 tests/test_issue60_linux_fixes.py 를 단독 실행하면 5 failed, 40 passed. 관련 18개 모듈 실행(아래)에서도 같은 5개만 실패했다.
  • 제품 코드는 맞다. 이 테스트의 clean 상태는 Linux 에서 닫힌 DB 의 모습(사이드카 없음)이다. git archive 사본에서 이 상태를 만들도록 close 뒤에 -wal·-shm 을 지워 보았다. 그러자 해당 서브셋 33개가 모두 통과했고, 아래 뮤테이션 6개도 모두 잡혔다.
  • 권고: clean 분기에서 close 뒤 남은 -wal·-shm 을 지운다. close 가 이미 checkpoint 하므로 행은 본 파일에 있다. 또는 Mac 의 실제 상태인 "쓰는 쪽 없이 사이드카 둘 다"는 이미 leftover 가 다루므로, 그 사실을 주석으로 남긴다.
  • 주의: 지금 상태의 Mac 에서는 clean 이 첫 assert 에서 멈춘다. 그래서 immutable 분기를 없애는 뮤테이션(M1)이 이 Mac 에서는 드러나지 않는다.

중점 항목 확인

1. _ro_connect 세 갈래 (opencode.py:13-36). 검증 경로 코드를 3.9 로 import 해 임시 DB 에서 확인했다. 사이드카를 직접 지워 Linux 와 같은 상태를 만들었다.

상태 결과
사이드카 없음 같은 상태에서 base 의 평범한 mode=ro 는 unable to open database file 로 실패한다. 그래서 base 의 _descendants·db_rows_naming 은 Mac 에서도 빈 결과였다. _ro_connect(immutable)는 행을 읽고 아무 파일도 만들지 않는다.
writer 가 살아 있음(둘 다 있음) WAL 에만 있는 행까지 읽는다. 목록은 그대로다.
-shm 없는 -wal 커밋된 행을 읽는다. 원본 두 파일의 mtime 은 그대로였고 -shm 도 생기지 않았다. 같은 상태에서 평범한 mode=ro 는 -shm 을 만들었다. 디렉터리가 읽기 전용(0555)이어도 읽힌다.
DB 없음 OperationalError. 파일은 생기지 않는다. base 의 sqlite3.connect(db) 는 빈 DB 를 만들었으므로 개선이다.
  • 실제 Mac 의 상태: 실제 디렉터리에는 서비스가 떠 있어 opencode.db·-shm·-wal 이 모두 있다(ls). persistent WAL 이 기본인 Mac 에서는 "둘 다 있음 → mode=ro" 갈래가 대부분일 것이다. 다만 bun 이 시스템 SQLite 를 쓰는지는 확인하지 않았다.

2. _DB_PATHS 캐시 (opencode.py:39-40, 139-161).

  • 키는 (bin, ctx.home_dir, _data_home(ctx)) 다. _run 이 넘기는 HOME·XDG_DATA_HOME 과 같은 값이라, 다른 HOME 이나 XDG 에 다른 경로를 돌려주지 않는다.
  • 실패한 debug paths 는 저장되지 않는다. 바이너리가 없으면(bin_path None) 캐시를 쓰지 않는다.
  • 테스트 간 누출: 이 모듈은 autouse fixture 로 캐시를 비운다. 다른 모듈은 tmp_path 별 HOME 이라 키가 겹치지 않는다. 18개 모듈을 한 프로세스에서 실행했을 때 B1 외의 실패는 없었다.

3. auth_ok (opencode.py:317-321, __main__.py:120-127, create_session.sh:145-151).

  • 다른 에이전트의 분기와 adapter 는 바뀌지 않았다. auth-ok 를 부르는 곳은 opencode 분기뿐이다.
  • python -m lib_py.agents auth-ok opencode 를 /usr/bin/python3 3.9 로 실행했다. PATH 에 opencode 가 없으면 rc 1, ~/.opencode/bin 이 있으면 rc 0, 인자가 없으면 rc 1(unknown agent '').
  • PYTHONPATH 는 lib.sh 가 export 한다(lib.sh:25). 같은 파일의 facts·spawn-spec 호출과 같은 방식이다.
  • create 의 사전 검사로 충분하다. 바로 위 command -v "$AGENT"(:117)가 이미 PATH 의 바이너리를 요구한다.

4. input_placeholder_pattern (opencode.py:75-79, input_region.py:83-85).

  • 홈 화면 fixture 두 개(140열, 잘린 예시가 있는 60열)는 모두 빈 입력으로 읽힌다. 1.x 캡처도 그대로 빈 입력이다. 다른 adapter 는 None 이다(테스트로 고정).
  • fullmatch 라 Ask anything… "x" and fix it 처럼 뒤에 글이 붙은 입력은 그대로 읽힌다.

5. stop 문구 (stop_session.sh:242). 메시지만 바뀌었고 동작은 같다. 테스트 2개(exit 됨 / 살아 있어 kill)가 통과했다.

6. Python 3.9 와 셸.

  • 바뀐 Python 5개 파일이 3.9 에서 파싱되고, 제품 모듈은 3.9 로 import 해 실행했다. 새 테스트는 from __future__ import annotations 를 쓴다.
  • create_session.sh·stop_session.sh 가 /bin/bash -n(3.2)을 통과한다. git diff --check 1d7b7da..HEAD 는 깨끗하다.

테스트 실행 (검증 경로)

조건: env -i, 임시 HOME, XDG_DATA_HOME=<scratchpad>, basetemp 는 scratchpad, -p no:cacheprovider, PYTHONDONTWRITEBYTECODE=1.

대상 18개 모듈: issue60_linux_fixes, issue60_opencode_v2, a4, issue50_dispatch, issue30_stall_watch(+_stop), issue31, tier1, tier2, c1_tui_readiness, issue5_phase0/phase2b, issue24, issue6, uuid_target, issue55, issue57, issue21_worktree_prereqs.

  • 결과: 5 failed(B1 의 5개뿐), 623 passed, 2 skipped, 29분.
  • skip 2개: /proc 전용 테스트와, PATH 에 opencode 가 없어 건너뛴 실바이너리 계약 테스트.
  • 부작용 확인:
    • scratchpad XDG 디렉터리는 실행 뒤에도 비어 있었다.
    • 실제 ~/.local/share/opencode 목록은 실행 전후가 같았다.
    • 실제 log 의 debug 줄 수는 2 → 2.
    • 남은 opencode 프로세스는 없었다(사용자 서비스 제외).
    • #55 가드 오탐도 없었다.

뮤테이션: git archive 사본에 B1 fixture 수정을 넣고 진행했다. 대상은 issue60_linux_fixes 와 a4 이며, 실제 프로세스를 띄우는 stop·create 2개는 뺐다. 모두 잡혔다.

뮤테이션 결과
M1 immutable 분기를 mode=ro 로 5 failed
M2 wal and shm 를 wal 로 5 failed
M3 캐시 사용 끔 1 failed
M4 잘린 예시 허용("?) 제거 2 failed
M5 fullmatch 를 match 로 4 failed
M6 auth_ok 를 항상 True 로 3 failed

비차단

  • N1. immutable 경쟁. -wal 없음을 판단한 뒤 연결이 쓰이는 동안 writer 가 생기면 결과가 틀릴 수 있다. 임시 DB 에서 재현했다.
    • (a) 판단 직후 writer 가 한 행을 커밋하면, immutable 연결은 1/2 만 본다.
    • (b) 열린 immutable 커서 아래에서 writer 가 지우고 다시 쓰고 checkpoint 하면, 예외 없이 옛 행과 새 행이 섞인 20119행이 나왔다(옛 20000행, 새 30000행).
    • 쿼리가 짧아(LIMIT 20, 단건 조회) 실제 창은 ms 수준이다. persistent WAL 인 Mac 에서는 이 갈래 자체가 드물다.
    • 원하면 이렇게 줄일 수 있다: 쿼리 뒤 -wal 이 여전히 없고 본 파일의 mtime·size 가 그대로인지 확인하고, 아니면 mode=ro 로 한 번 다시 읽는다.
  • N2. 캐시는 프로세스 수명 동안 유지된다. 같은 경로에서 바이너리를 업그레이드한 경우, 오래 도는 reconcile --subscribe 는 예전 debug paths 결과를 계속 쓴다. 경로 규칙이 바뀌는 업그레이드에서만 문제가 된다.
  • N3. 사전 검사가 약해졌다. create 의 opencode 검사는 사실상 command -v 와 같아졌다. 깨진 바이너리도 통과하고, 실패는 이후 준비 대기 단계에서 드러난다. 2.x 에 로그인이 없다는 전제와 맞는 완화로 본다.
  • N4. 안내 문구와 같은 모양의 초안. 사용자가 정확히 그 모양으로 친 입력(Ask anything..., Ask anything… "fix the tests")은 빈 입력으로 읽힌다. 확인한 결과 둘 다 '' 였다. 실사용에서 생길 가능성은 매우 낮다.

[VERDICT: NOT PASS]