fix(agents): open agy, codex and hermes databases read-only (Issue #64)

This commit is contained in:
Godopu committed 2026-10-19 23:12:55 +09:00
1 parent 3ce5d5fdc1
commit b0eccc5301
8 files changed
+461 -56

No files matched your search

+3 -3
View File
@@ -1,6 +1,6 @@
import os, sys, json, sqlite3, shutil
import os, sys, json, shutil
from typing import Optional, Any
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, conversation_in_use
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, _ro_connect, conversation_in_use
class AgyAgentAdapter(BaseAgentAdapter):
@property
@@ -71,7 +71,7 @@ class AgyAgentAdapter(BaseAgentAdapter):
if uuid in (ctx.row.get("_sibling_claimed_uuids") or []):
return False
try:
conn = sqlite3.connect(path)
conn = _ro_connect(path)
r = conn.execute("SELECT count(*) FROM steps").fetchone()
conn.close()
if not r or r[0] < 1:
@@ -1,6 +1,6 @@
import os, json, glob, sqlite3, subprocess
from typing import Optional, Any, List
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, scan_names, db_rows_naming
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, _ro_connect, scan_names, db_rows_naming
from lib_py.verify_session import workspace_key
class CodexAgentAdapter(BaseAgentAdapter):
@@ -77,7 +77,7 @@ class CodexAgentAdapter(BaseAgentAdapter):
if not db or not os.path.exists(db):
return None, None
try:
conn = sqlite3.connect(db)
conn = _ro_connect(db)
row = conn.execute("SELECT rollout_path, cwd FROM threads WHERE id=?", (uuid,)).fetchone()
conn.close()
if row and row[0] and os.path.exists(row[0]):
@@ -192,7 +192,7 @@ class CodexAgentAdapter(BaseAgentAdapter):
if db and os.path.exists(db):
try:
conn = sqlite3.connect(db)
conn = _ro_connect(db)
cur = conn.cursor()
cur.execute("PRAGMA table_info(threads)")
cols = {r[1] for r in cur.fetchall()}
@@ -1,6 +1,6 @@
import os, sys, sqlite3
from typing import Optional, Any
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, _ro_connect
from lib_py.verify_session import workspace_key
class HermesAgentAdapter(BaseAgentAdapter):
@@ -48,7 +48,7 @@ class HermesAgentAdapter(BaseAgentAdapter):
if not os.path.exists(hdb):
return False
try:
conn = sqlite3.connect(hdb)
conn = _ro_connect(hdb)
r = conn.execute("SELECT cwd, started_at FROM sessions WHERE id=?", (uuid,)).fetchone()
conn.close()
if not r:
@@ -97,7 +97,7 @@ class HermesAgentAdapter(BaseAgentAdapter):
if not os.path.exists(hdb):
return []
try:
conn = sqlite3.connect(hdb)
conn = _ro_connect(hdb)
if ctx.epoch:
rows = conn.execute(
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
@@ -1,7 +1,6 @@
import os, sys, sqlite3, shutil, subprocess, tempfile
from urllib.parse import quote
import os, sys, sqlite3, shutil, subprocess
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, _ro_connect, scan_names, db_rows_naming, conversation_in_use
from lib_py.verify_session import workspace_key
# Tables a 1.x opencode wrote and a migration to 2.x keeps but no longer
@@ -10,31 +9,6 @@ _LEGACY_TABLES = (("message", "session_id"), ("messages", "session_id"), ("part"
("parts", "session_id"), ("todo", "session_id"), ("session_share", "session_id"),
("session", "id"), ("sessions", "id"))
def _ro_connect(db: str):
"""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)
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.
_DB_PATHS = {}