Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
9.0 KiB
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_toofailed 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_sessionfailed in 0.96 s (Truewhere the service-shaped holder must leave the session free). - Drift C without
parent_id IS NULL:test_comp_opencode_create_and_reconcile_and_stopfailed in 17.08 s.opencode_session_id_ownstayed 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]