Files
multi-agent-mux/.agents/reports/reviewer-grok-01/report-366a4d58.md
T

11 KiB
Raw Blame History

Review 366a4d58 — Issue #64

PASS. Commit b0eccc5 moves _ro_connect into base.py and uses it for every query-only open of an agy, codex, or hermes database found in the skills tree. The other-workspace agy sidecars come from reconcile drift C: it walks every conversation *.db and verify_artifact used to open each one read-write. On this host that open leaves -wal and -shm after close, which is what the #55 name guard counts. Head related tests: 95 passed. The new file on base: 23 failed, 20 passed.

Reviewed only the snapshot /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261019T134017Z-d90c23/1 (loop-20261019T134017Z-d90c23). Head b0eccc530149bc6e9211727e19921665c7b80492, tree f5a412a3ee96465db7c4b3223d2d76e95716ddfa, base 3ce5d5fdc108b0435e933a251fe1c1256b5d7973 is an ancestor and is the control-plane HEAD. One commit, 8 files, +461/−56. git diff --check is clean. Snapshot porcelain is empty and the snapshot has no .mam. Control-plane porcelain is empty. No product file was edited. The source worktree was not run.

The commit is b0eccc5 fix(agents): open agy, codex and hermes databases read-only (Issue #64). Files: agy.py, codex.py, hermes.py, opencode.py, base.py, reconcile.sh, status.sh, and new tests/test_issue64_ro_connect.py.

Host tools: control-plane venv Python 3.9.6, sqlite 3.51.0, bash 3.2.57(1)-release. bash -n accepts status.sh and reconcile.sh. The changed modules imported and ran under pytest. The opencode binary was not launched. Linux was not executed here. The real database was not opened.

Cause

Reconcile drift C for an unpinned running agy row (reconcile.sh:786-817) lists every <HOME_DIR>/.gemini/antigravity-cli/conversations/*.db (:800) and calls verify_session_uuid(cwd, 'agy', uuid, s_eval, mode="discover") (:816) for each basename. home in that script is os.environ['HOME_DIR'] (:437). verify_session_uuid (verify_session.py:83-108) builds a DiscoveryContext and returns adapter.verify_artifact. DiscoveryContext fills a missing home_dir through resolve_home (paths.py:11): the argument, then HOME_DIR, then HOME, then ~. The reconcile call omits home_dir, so the open uses the same home as the glob.

On base, AgyAgentAdapter.verify_artifact did sqlite3.connect(path) and SELECT count(*) FROM steps once the file existed and was new enough for the epoch. A discover-mode cache miss returns false only when the uuid is in _sibling_claimed_uuids (siblings on the same pane cwd). last_conversations.json mapping that uuid to a different workspace does not skip the open. A read-write open of a WAL database on macOS sqlite 3.51.0 creates -wal and -shm when they are absent and leaves those names after close. The #55 guard snapshots entry names at session start and fails when a new name appears. At check time no process held the database because the connection had already closed. That matches the v4.9.7 timing: the review loop and the suite were running reconcile against the real home.

status.sh agy (:78-81 and :278-281) returns from os.path.exists of the .db. It does not open sqlite.

Head agy.py:74 uses _ro_connect(path) for the same SELECT count(*) FROM steps. With no -wal, that open is mode=ro&immutable=1 and does not create sidecars.

test_unpinned_agy_reconcile_leaves_other_workspace_clean_db_without_sidecars reproduces this on a temporary home. last_conversations.json maps the conversation to workspace B, the row is an unpinned agy session whose pane cwd is workspace A, and the test calls verify_session_uuid (the function drift C calls). The function returns true and the directory stays one file. On base the same test failed because the listing gained -wal and -shm. The runtime test also spawns reconcile.sh --once --emit-diff --dry-run for a same-workspace unpinned agy session, with HOME and HOME_DIR pointed at the sandbox, and that listing stays unchanged.

A cache miss still accepts the other workspace's database when it has at least one step and is not sibling-claimed. The new test asserts that true return. Drift C can still treat that file as a candidate. The open no longer creates names.

Changed opens

_ro_connect is base.py:111-134. Both sidecars use file:<abspath>?mode=ro. No -wal uses mode=ro&immutable=1. A -wal without -shm is copied into tempfile.TemporaryDirectory(prefix="mam-ro-"), opened read-write on that copy only, backed into a memory connection, and the temp directory is removed. The copy name is os.path.basename(path) or state.db, so <uuid>.db, state.db, and state_5.sqlite keep their -wal sibling. The temp prefix was mam-opencode- while the helper lived in opencode.py.

db_rows_naming (base.py:137-145) now defaults to _ro_connect. A caller can still pass connect.

Query sites on the helper:

  • agy.py:74 verify_artifact (SELECT count(*) FROM steps). discover (agy.py:165-175) reads last_conversations.json and calls verify_artifact.
  • codex.py:80 _find_rollout_via_db. codex.py:195 discover (PRAGMA table_info(threads) and SELECT id, cwd FROM threads). codex.py:167-169 residual_artifacts goes through db_rows_naming for state_*.sqlite and thread_history_*.sqlite.
  • hermes.py:51 verify_artifact. hermes.py:100 discover.
  • opencode.py imports _ro_connect from base (:3). Query sites stay :149, :212, :234, :249, :316, and connect=_ro_connect at :249. The identity test asserts opencode_mod._ro_connect is base_mod._ro_connect.
  • status.sh: _ro_connect(hdb) at :91 and :291, _ro_connect(odb) at :116 and :316, _ro_connect(db) at :132 and :332. The static test requires each of those counts to be 2.
  • reconcile.sh: import at :423. Hermes drift _ro_connect(hdb) at :865, opencode _ro_connect(odb) at :954, codex drift _ro_connect(state_db) at :1048. The static test requires each count to be 1.

status.sh and reconcile.sh contain no sqlite3.connect literal.

Left as read-write

These opens write, or they are MAM's own databases. They stay sqlite3.connect:

  • opencode.py:193 deletes 1.x rows after session delete.
  • hermes.py:74 purge DELETE.
  • codex.py:152 purge DELETE FROM threads.
  • lib.sh:1599 and lib.sh:2068 (timeout 60).
  • lib_py/state.py:17.
  • lib_py/verify_session.py:41 opens the state database beside AGENT_SESSIONS_YAML or YAML_PATH (<yaml stem>.db, SELECT data FROM state, timeout 5). That is MAM's agent-sessions state, not an agent conversation database.
  • lib_py/loop/runner.py:416.
  • lib_py/atomic_yaml.py:61.

Agy purge removes files and does not open sqlite. Claude and grok adapters have no sqlite3.connect. The three sqlite3.connect calls inside _ro_connect are the helper: the mode=ro URI, the immutable URI, and the read-write open of the private copy.

VERSIONS.md and the skill guides are unchanged. Version bumps stay with the orchestrator.

Tests

Pytest used the control-plane venv, PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider, and a --basetemp outside the snapshot. The basetemp path contains neither mam-review nor shim. Inherited HERDR_*, MAM_*, MQTT_*, SKS_*, DELEGATE_JOB_LOGS_DIR, HOME_DIR, CLAUDE_PROJECT_DIR, CLAUDE_CONFIG_DIR, CLAUDE_CODE_TMPDIR, CODEX_HOME, PYTHONPATH, WORKSPACE_ROOT, MAM_ENV_FILE, AGENT_SESSIONS_YAML, XDG_STATE_HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME, and XDG_DATA_HOME were dropped. WORKSPACE_ROOT was not pointed at the snapshot.

Head, cwd the snapshot, basetemp /tmp/mam366-366a4d58-pybase, files tests/test_issue64_ro_connect.py and tests/test_issue60_linux_fixes.py: 95 passed in 72.05 s. Exit 0. No skip and no error. The new file is 43 of those (helper identity, agy, codex including db_rows_naming, hermes, one static script check, four runtime script states, and the cross-workspace case). The issue #60 file is the other 52, so the move still passes the existing opencode read-only tests.

Base proof used a throwaway git clone --shared of the snapshot, checked out at 3ce5d5f, with only tests/test_issue64_ro_connect.py taken from b0eccc5. Cwd was the clone. Basetemp was /tmp/mam366-366a4d58-pybase-base. Result: 23 failed, 20 passed in 10.84 s. Exit 1. The failures are the helper-location test, every clean and wal-only adapter query, the static script count (_ro_connect(hdb) was 0), runtime clean and wal-only, and the cross-workspace clean listing. Clean db_rows_naming returned [] and clean residual_artifacts returned only the rollout file; on base that helper's default open is a mode=ro URI (base.py at 3ce5d5f). The other clean and wal-only failures are listings that gained -wal and -shm after a query that still returned the row. The 20 passes are the leftover and live-writer states, where those names were already present, so a read-write open did not change the listing.

The four states in the new file are clean (close, then unlink -wal and -shm when they exist), leftover sidecars, -wal without -shm, and a connection left open. Each query asserts the right row and an unchanged directory listing. Runtime covers reconcile.sh --once --emit-diff --dry-run and status.sh --json plus text mode. Agy, hermes, and codex resume_state are yes.

Remaining risk

The check for -wal and the immutable connect are two steps. A wal created in that gap is invisible to the immutable open. A writer that already has both sidecars is read with mode=ro.

mode=ro with both sidecars rewrites the existing -shm bytes (the reader index). The -wal bytes and the file names stay. That rewrite was measured on the Issue #60 review and left in place.

Purge DELETE opens stay read-write, so a purge can still create or remove sidecars. That is the write the brief left out.

Drift C can still accept another workspace's agy database when the file has steps and the uuid is not sibling-claimed. The candidate decision is unchanged. The open no longer adds names.

verify_session.py:41 still opens MAM's own state database read-write.

The user's background service stayed up. Its pid is 23630 and the command is /Users/godopu16/.opencode/bin/opencode serve --service. The real database mtime is 1792420196 and its size is 36712448. ~/.config/opencode/service.json mtime is 1792246838. ~/.local/state/opencode/service.json mtime is 1792365227. The real opencode.log mtime is 1792420391 and its size is 2699345. The herdr config mtime is 1784456394.

After the report, snapshot porcelain is still empty, the snapshot still has no .mam, and control-plane porcelain is still empty at 3ce5d5fdc108b0435e933a251fe1c1256b5d7973. The herdr config mtime is still 1784456394. The opencode database mtime is still 1792420196 and its size is still 36712448. The opencode log mtime is still 1792420391 and its size is still 2699345. Service pid 23630 is still opencode serve --service.

[VERDICT: PASS]