fix(opencode): a -wal left without its -shm is read, not ignored (Issue #60, Linux item 5, review)

_ro_connect read every database without both sidecars as immutable, which
skips a -wal's committed rows: a recovered DB+WAL with no -shm showed no
sessions (planner-reviewer-codex-01). That state - no writer runs without
a -shm - is now read from a private DB+WAL copy, where SQLite may rebuild
the index, loaded into memory; nothing beside the source is touched. The
query matrix gains the walonly state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-18 13:36:41 +00:00
1 parent 463817ee34
commit 7803d47026
2 files changed
+28 -9

No files matched your search

@@ -1,4 +1,4 @@
import os, sys, sqlite3, shutil, subprocess import os, sys, sqlite3, shutil, subprocess, tempfile
from urllib.parse import quote from urllib.parse import quote
from typing import Optional, Any, List from typing import Optional, Any, List
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, scan_names, db_rows_naming, conversation_in_use from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, scan_names, db_rows_naming, conversation_in_use
@@ -11,12 +11,29 @@ _LEGACY_TABLES = (("message", "session_id"), ("messages", "session_id"), ("part"
("session", "id"), ("sessions", "id")) ("session", "id"), ("sessions", "id"))
def _ro_connect(db: str): def _ro_connect(db: str):
"""A read that neither creates nor removes anything beside db (Issue #60): """A read that neither creates nor removes anything beside db (Issue #60).
the WAL sidecars are used only when both already exist (a live writer Both WAL sidecars (a live writer keeps them): read through them. No -wal:
keeps them); otherwise the file is read as immutable.""" nothing to apply, so the file is read as immutable. A -wal without its
-shm (left behind - no writer runs without one) still holds committed
rows: they are read from a private copy, where SQLite may rebuild the
index, loaded into memory."""
path = os.path.abspath(db) path = os.path.abspath(db)
both = os.path.exists(path + "-wal") and os.path.exists(path + "-shm") wal, shm = os.path.exists(path + "-wal"), os.path.exists(path + "-shm")
return sqlite3.connect(f"file:{quote(path)}?mode=ro" + ("" if both else "&immutable=1"), uri=True) if wal and shm:
return sqlite3.connect(f"file:{quote(path)}?mode=ro", uri=True)
if not wal:
return sqlite3.connect(f"file:{quote(path)}?mode=ro&immutable=1", uri=True)
mem = sqlite3.connect(":memory:")
with tempfile.TemporaryDirectory(prefix="mam-opencode-") as tmp:
copy = os.path.join(tmp, "opencode.db")
shutil.copyfile(path, copy)
shutil.copyfile(path + "-wal", copy + "-wal")
src = sqlite3.connect(copy)
try:
src.backup(mem)
finally:
src.close()
return mem
# (binary, HOME, XDG_DATA_HOME) -> the db `debug paths` printed for them. # (binary, HOME, XDG_DATA_HOME) -> the db `debug paths` printed for them.
+5 -3
View File
@@ -135,8 +135,9 @@ def _wal_writer(db: Path, ws: Path) -> sqlite3.Connection:
@pytest.fixture @pytest.fixture
def wal_home(tmp_path, monkeypatch): def wal_home(tmp_path, monkeypatch):
"""(adapter, ctx, db, ws, make(state)) - make builds the database in one """(adapter, ctx, db, ws, make(state)) - make builds the database in one
of three states: `clean` (closed: no sidecars), `leftover` (both sidecars of four states: `clean` (closed: no sidecars), `leftover` (both sidecars
left behind by a writer that is gone, the rows only in the -wal), left behind by a writer that is gone, the rows only in the -wal),
`walonly` (the same without its -shm: the committed rows still count),
`writer` (a live writer whose rows are only in the -wal).""" `writer` (a live writer whose rows are only in the -wal)."""
home, ws = tmp_path / "home", tmp_path / "ws" home, ws = tmp_path / "home", tmp_path / "ws"
ws.mkdir() ws.mkdir()
@@ -150,11 +151,11 @@ def wal_home(tmp_path, monkeypatch):
def make(state): def make(state):
if state == "clean": if state == "clean":
_wal_writer(db, ws).close() _wal_writer(db, ws).close()
elif state == "leftover": elif state in ("leftover", "walonly"):
src = tmp_path / "src" / "opencode.db" src = tmp_path / "src" / "opencode.db"
src.parent.mkdir() src.parent.mkdir()
conn = _wal_writer(src, ws) conn = _wal_writer(src, ws)
for suffix in ("", "-wal", "-shm"): for suffix in ("", "-wal", "-shm")[:3 if state == "leftover" else 2]:
shutil.copyfile(str(src) + suffix, str(db) + suffix) shutil.copyfile(str(src) + suffix, str(db) + suffix)
conn.close() conn.close()
else: else:
@@ -170,6 +171,7 @@ def wal_home(tmp_path, monkeypatch):
@pytest.mark.parametrize("state,listing", [ @pytest.mark.parametrize("state,listing", [
("clean", ["opencode.db"]), ("clean", ["opencode.db"]),
("leftover", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]), ("leftover", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]),
("walonly", ["opencode.db", "opencode.db-wal"]),
("writer", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]), ("writer", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]),
]) ])
def test_a_query_reads_right_and_leaves_the_sidecars_alone(wal_home, state, listing, query): def test_a_query_reads_right_and_leaves_the_sidecars_alone(wal_home, state, listing, query):