release(v4.9.7): opencode 2.x Linux follow-up (Issue #60, PR #62) - 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:
GodopuandClaude Opus 5.5 committed 2026-10-19 12:16:20 +09:00
1 parent 6a37c0019b
commit cffe279eb0
28 files changed
+568 -26

No files matched your search

@@ -0,0 +1,59 @@
# Review 85982c2d — PR #62, Issue #60 Linux follow-up
NOT PASS. Five new tests fail on this Mac before they call the query. The read-only open itself, measured on temporary databases, returns the committed rows and leaves the directory names as they were.
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261019T000805Z-975c4c/1` (`loop-20261019T000805Z-975c4c`). Head `6a4e385a78c8a561d5cf3644336de6a352ea9271`, tree `afa2c55925f93820337b69699c670c0b0ca90980`, base `1d7b7dab58a10fe86fd747b7290d47674ca93230` is an ancestor and is the control-plane HEAD. Eight commits, 11 files, +515/−31. `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.
Commits, newest first: `6a4e385` (narrow fixture ends at its last drawn line), `d17cd80` (auth is the binary; create uses `auth-ok`), `7803d47` (a `-wal` without its `-shm` is read from a private copy), `463817e` (unconfirmed `/exit` wording), `614ab14` (`debug paths` cached per binary and home), `ac5f115` (queries go through `_ro_connect`), `14d7a46` (2.x home placeholder), `4d49abb` (`DELEGATE_JOB_PYTHON` for the u1e wrapper).
Host tools: control-plane venv Python 3.9.6, sqlite 3.51.0. `bash -n` accepts `create_session.sh` and `stop_session.sh`. `py_compile` accepts `opencode.py`, `base.py`, `input_region.py`, `__main__.py`, and `tests/test_issue60_linux_fixes.py`. The opencode binary was not launched. Linux was not executed here.
## The five failures
`tests/test_issue60_linux_fixes.py::test_a_query_reads_right_and_leaves_the_sidecars_alone` with state `clean` fails for all five queries (`verify_artifact`, `discover`, `_has_session`, `_descendants`, `residual_artifacts`).
`make("clean")` closes a WAL connection and requires the directory to be exactly `["opencode.db"]` (`test_issue60_linux_fixes.py:154` and `:173-181`). The assertion is that listing, and it runs before the query. On sqlite 3.51.0 here, closing the last connection leaves `opencode.db-wal` at 0 bytes and `opencode.db-shm` at 32768 bytes. `PRAGMA wal_checkpoint(TRUNCATE)` then close leaves the same two names. The main file grows from 4096 to 8192 bytes on that close, which is the checkpoint into the main file. The pytest failure is `['opencode.db', 'opencode.db-shm', 'opencode.db-wal']` against `['opencode.db']`.
That is the state a just-closed WAL database has on this host. `_ro_connect` then sees both sidecars and takes the `mode=ro` branch. A temporary database in that state returned the inserted row, and the three names and their sizes were the same after the connection closed.
The no-wal branch was measured by removing the sidecars after the checkpoint. `_ro_connect` (`opencode.py:13-36`) opened `file:<path>?mode=ro&immutable=1`, returned the row, and the directory stayed a single file with the same size and hash. A path containing a space behaved the same way.
The fixture has to remove the sidecars this sqlite keeps before it can assert a one-file listing and exercise that branch. As written, the five clean cases are red on the review host, so the related file is red here.
The other three states of the same test passed: both sidecars left by a writer that is gone, a `-wal` copied without its `-shm`, and a live writer. Each returned the expected rows (`ses_main`, the child id, or the residual `session_v2` count the test asks for) and the directory names after the query matched the names before it.
## The three open modes, on temporary databases
Both `-wal` and `-shm` present: `mode=ro`. The live writer's uncheckpointed row was visible. The `-wal` size and hash stayed the same. The `-shm` hash changed while the reader was open and was still the new hash after the reader closed (32768 bytes, same inode). The directory names stayed the three sidecar names. In a second sample the writer then inserted another row; `wal_checkpoint(TRUNCATE)` returned `(0, 0, 0)`.
No `-wal`: `immutable=1`. Covered above. An existing `-shm` with no `-wal` was left in place, and the row in the main file was visible.
`-wal` present and `-shm` absent: the function copies the db and the wal into a `mam-opencode-` temporary directory, backups that copy into memory, and drops the temporary directory before returning the memory connection. Both rows that lived in the wal were visible. The original db and wal kept their size and hash. No `-shm` appeared beside the original.
`db_rows_naming` takes the optional `connect` and the opencode residual path passes `_ro_connect` (`base.py:111-119`, `opencode.py:273-274`). `verify_artifact`, `_descendants`, `_has_session`, and `discover` call `_ro_connect`. `purge_artifacts` still uses `sqlite3.connect(db)` for the 1.x `DELETE` statements (`opencode.py:218`). That line is unchanged in this range.
An immutable open that is issued after the no-wal check has already been recorded, and after a writer has then committed a new row into a new `-wal`, returns the rows that were in the main file. `_ro_connect` called once both sidecars exist returns the new row as well. The gap is between `os.path.exists` (`opencode.py:21`) and `sqlite3.connect` (`opencode.py:25`). A writer that already has both sidecars is read with `mode=ro`.
## The other five items
The 2.x placeholder pattern is `Ask anything(?:…|\.\.\.)(?: "[^"]*"?)?` (`opencode.py:76-79`). `extract_input_region` applies it with `re.fullmatch` to the first non-blank line of the box only (`input_region.py:76-86`). Both new fixtures, wide and narrow, read as an empty box. The narrow fixture's example line contains a quote and does not end with a quote. A first line that is exactly `Ask anything…` or `Ask anything… "<example>"` (ellipsis or three dots) reads as empty. A first line with text after the closing quote is kept, and a later line with the placeholder shape is kept. `input_placeholder` stays `''`, which is what `lib.sh` greps. The other adapters' `input_placeholder_pattern` is `None`. Those tests passed.
`auth_ok` for opencode is `shutil.which("opencode")` or `<HOME_DIR or HOME>/.opencode/bin/opencode` (`opencode.py:317-321`). Keys and `auth.json` are outside that check. `__main__.py` `auth-ok` exits 0 or 1 from the adapter, and exits 1 for an unknown agent. `create_session.sh` calls `auth-ok opencode` through `_delegate_py_bin`. The diff among adapters is only `opencode.py`. Claude, agy, grok, codex, and hermes keep their previous `auth_ok` bodies. `test_adapter_auth_ok`, the binary-only cases, and `test_create_decides_opencode_auth_without_running_it` passed. The create run records no `--version` call.
`_DB_PATHS` keys are `(binary, home, data home)` (`opencode.py:139-161`). A hit returns the stored path. A failed `debug paths` is not stored. The new tests replace the dict for each test (`test_issue60_linux_fixes.py:48-50`). `test_debug_paths_runs_once_per_home` and `test_a_failed_debug_paths_is_not_remembered` passed. The same key keeps its path for the life of the process, including after the binary at that path is replaced.
`stop_session.sh` still sends the exit key, waits, and runs `kill-session` when the session is still there (`stop_session.sh:242-250`). The unconfirmed-delivery line now says it is waiting for the session to exit. `test_the_graceful_stop_reports_what_happened` passed for a pane that exits on `/exit` and for one that stays up. The staying-up case still contains `still alive → kill-session`.
`test_u1e_a_dry_run_record_is_not_an_open_job` passes `DELEGATE_JOB_PYTHON=sys.executable`. That test passed.
## Suite
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mam859-85982c2d-pybase`. 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`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, and `XDG_DATA_HOME` were dropped.
5 failed, 42 passed in 59.38 s. The failures are the five `clean` cases named above. The rest of `tests/test_issue60_linux_fixes.py`, `tests/test_a4_adapter_contract.py::test_adapter_auth_ok`, and `tests/test_issue50_dispatch.py::test_u1e_a_dry_run_record_is_not_an_open_job` passed. No error from the session guards.
The user's background service stayed up. Its pid is 23630 and the command is `opencode serve --service`. The real database was not opened. Its mtime stayed 1792367390. `~/.config/opencode/service.json` stayed 1792246838 and `~/.local/state/opencode/service.json` stayed 1792365227. The real `opencode.log` mtime stayed 1792368889 and its size stayed 2636652. The herdr config mtime stayed 1784456394.
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `1d7b7dab`. The herdr config mtime is still 1784456394. The opencode database mtime is still 1792367390, the opencode log mtime is still 1792368889 and its size is still 2636652, and service pid 23630 is still `opencode serve --service`.
[VERDICT: NOT PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261019T015801Z-05463c",
"base_sha": "1d7b7dab58a10fe86fd747b7290d47674ca93230",
"head_sha": "6a37c0019baa31ee28b32a086e57b4c8239413c4",
"tree_sha": "87013e7ef7f5239f1bf6ea38a2cbda214257be3c",
"review_commit": "6a37c0019baa31ee28b32a086e57b4c8239413c4",
"reviewer": "reviewer-grok-01",
"job_id": "8f95cc77",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-19T02:37:22Z"
}
@@ -0,0 +1,41 @@
# Review 8f95cc77 — PR #62, second pass
PASS. Commit `6a37c00` fixes the five Mac failures from `6a4e385` and the two extra findings from that round. Related tests: 53 passed.
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261019T015801Z-05463c/1` (`loop-20261019T015801Z-05463c`). Head `6a37c0019baa31ee28b32a086e57b4c8239413c4`, tree `87013e7ef7f5239f1bf6ea38a2cbda214257be3c`, base `1d7b7dab58a10fe86fd747b7290d47674ca93230` is an ancestor and is the control-plane HEAD. Nine commits, 13 files, +553/−36. `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 new commit on top of `6a4e385` is `6a37c00`. It changes four files (+40/−7): `opencode.py` (placeholder pattern and `auth_ok`), `status.sh`, `reconcile.sh`, and `tests/test_issue60_linux_fixes.py`.
Host tools: control-plane venv Python 3.9.6, sqlite 3.51.0. `bash -n` accepts `status.sh`, `reconcile.sh`, `create_session.sh`, and `stop_session.sh`. The changed Python modules parse. The opencode binary was not launched. Linux was not executed here. The real database was not opened.
## The round-1 blocker
On this host, closing the last WAL connection leaves `opencode.db-wal` at 0 bytes and `opencode.db-shm` at 32768 bytes, and the checkpoint has already written the rows into the main file. `make("clean")` now removes those two sidecars when they exist (`test_issue60_linux_fixes.py:158-162`). The five `clean` cases of `test_a_query_reads_right_and_leaves_the_sidecars_alone` then see a directory of exactly `opencode.db`, read the expected rows through `mode=ro&immutable=1`, and the directory is still that one file afterward. Those five cases passed. The leftover, wal-only, and live-writer cases passed again.
On Linux the close already removes the sidecars, so the new `os.path.exists` unlink is a no-op there. The same rows are what the immutable open reads.
## Placeholder and auth
The pattern is now `Ask anything(?:…|\.\.\.) "[^"]*"?` (`opencode.py:79`). The quoted example is required, so a first line that is only `Ask anything…` or `Ask anything...` stays typed text. The wide and narrow home fixtures still read as an empty box, including the narrow line whose example has no closing quote. A first line with text after the closing quote stays, and so does a later line. `test_typed_text_is_read_as_typed` covers the two bare lines on both fixtures and passed. `input_placeholder` stays `''`. The other adapters' pattern property stays `None`.
A first line that is exactly `Ask anything… "<example>"` is still cleared. That is the home-screen shape.
`auth_ok` accepts `shutil.which("opencode")`, or `<home>/.opencode/bin/opencode` when that path is a file and `os.access(..., os.X_OK)` is true (`opencode.py:321-323`). A mode `0644` file and a directory named `opencode` both fail. `test_a_home_install_that_cannot_run_is_not_enough` and `test_adapter_auth_ok` passed. Keys and `auth.json` still do not count. Create still asks `auth-ok` and does not run `--version`. The other agents' `auth_ok` bodies are outside this commit.
## status and reconcile
Both copies of `resume_on_disk` in `status.sh` (`:117` and `:318`) and the opencode drift-C open in `reconcile.sh` (`:955`) call `_ro_connect(odb)`. `sqlite3.connect(odb)` is gone from both scripts. `test_status_and_reconcile_open_the_database_read_only` checks those counts and passed. `env_python` puts the skill directory on `PYTHONPATH`, which is the path `get_adapter` already uses a few lines above each open. A failed open still returns `MISSING` in status and is swallowed in reconcile. Hermes and codex in those scripts still open their own databases with `sqlite3.connect`.
## Suite
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mam8f95-8f95cc77-pybase`. 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`, `XDG_STATE_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, and `XDG_DATA_HOME` were dropped.
53 passed in 59.23 s: `tests/test_issue60_linux_fixes.py` and `tests/test_a4_adapter_contract.py::test_adapter_auth_ok`. No skip and no error.
The earlier commits are unchanged in this pass: `_ro_connect` still picks `mode=ro` when both sidecars exist, `immutable=1` when there is no `-wal`, and a private memory copy when a `-wal` has no `-shm`. The gap between the `exists` check and the immutable open is still there. `_DB_PATHS` still keeps a path for the life of the process, and each test replaces that dict. Create still treats an executable binary as configured. Those three were left in place on purpose.
The user's background service stayed up. Its pid is 23630 and the command is `opencode serve --service`. The real database mtime stayed 1792367390. `~/.config/opencode/service.json` stayed 1792246838 and `~/.local/state/opencode/service.json` stayed 1792365227. The real `opencode.log` mtime stayed 1792368889 and its size stayed 2636652. The herdr config mtime stayed 1784456394.
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `1d7b7dab`. The herdr config mtime is still 1784456394. The opencode database mtime is still 1792367390, the opencode log mtime is still 1792368889 and its size is still 2636652, and service pid 23630 is still `opencode serve --service`.
[VERDICT: PASS]