diff --git a/.agents/skills/lib_py/agents/adapters/opencode.py b/.agents/skills/lib_py/agents/adapters/opencode.py index 6c3d7ce..628e7ee 100644 --- a/.agents/skills/lib_py/agents/adapters/opencode.py +++ b/.agents/skills/lib_py/agents/adapters/opencode.py @@ -1,4 +1,4 @@ -import os, sys, sqlite3, shutil, subprocess +import os, sys, sqlite3, shutil, subprocess, tempfile from urllib.parse import quote from typing import Optional, Any, List 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")) def _ro_connect(db: str): - """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 - keeps them); otherwise the file is read as immutable.""" + """A read that neither creates nor removes anything beside db (Issue #60). + Both WAL sidecars (a live writer keeps them): read through them. No -wal: + 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) - both = os.path.exists(path + "-wal") and os.path.exists(path + "-shm") - return sqlite3.connect(f"file:{quote(path)}?mode=ro" + ("" if both else "&immutable=1"), uri=True) + wal, shm = os.path.exists(path + "-wal"), os.path.exists(path + "-shm") + 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. diff --git a/tests/test_issue60_linux_fixes.py b/tests/test_issue60_linux_fixes.py index 1277892..e03ffa3 100644 --- a/tests/test_issue60_linux_fixes.py +++ b/tests/test_issue60_linux_fixes.py @@ -135,8 +135,9 @@ def _wal_writer(db: Path, ws: Path) -> sqlite3.Connection: @pytest.fixture def wal_home(tmp_path, monkeypatch): """(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), + `walonly` (the same without its -shm: the committed rows still count), `writer` (a live writer whose rows are only in the -wal).""" home, ws = tmp_path / "home", tmp_path / "ws" ws.mkdir() @@ -150,11 +151,11 @@ def wal_home(tmp_path, monkeypatch): def make(state): if state == "clean": _wal_writer(db, ws).close() - elif state == "leftover": + elif state in ("leftover", "walonly"): src = tmp_path / "src" / "opencode.db" src.parent.mkdir() 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) conn.close() else: @@ -170,6 +171,7 @@ def wal_home(tmp_path, monkeypatch): @pytest.mark.parametrize("state,listing", [ ("clean", ["opencode.db"]), ("leftover", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]), + ("walonly", ["opencode.db", "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):