fix(agents): codex, grok and opencode report their residual (Issue #31)

Their purge is unchanged; what it leaves is now reported: codex names and
thread rows (state_*, thread_history_*; logs_* is not scanned), grok
memory-v2 observations, opencode event/todo rows. db_rows_naming opens the
DB read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-15 15:16:06 +00:00
1 parent ae98cf18c4
commit b3461377ae
4 files changed
+61 -3

No files matched your search

+12 -1
View File
@@ -1,6 +1,6 @@
import os, json, glob, sqlite3, subprocess import os, json, glob, sqlite3, subprocess
from typing import Optional, Any, List from typing import Optional, Any, List
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, scan_names, db_rows_naming
from lib_py.verify_session import workspace_key from lib_py.verify_session import workspace_key
class CodexAgentAdapter(BaseAgentAdapter): class CodexAgentAdapter(BaseAgentAdapter):
@@ -158,6 +158,17 @@ class CodexAgentAdapter(BaseAgentAdapter):
pass pass
return purged return purged
def residual_artifacts(self, uuid: str, ctx: DiscoveryContext) -> List[str]:
# Issue #31: names under CODEX_HOME plus thread rows. logs_*.sqlite is
# not scanned (no thread index: a full scan of a large file).
root = self._codex_home(ctx)
found = scan_names(root, uuid)
for db in sorted(glob.glob(f"{root}/state_*.sqlite")):
found += db_rows_naming(db, uuid, ("thread_id",), id_tables=(("threads", "id"),))
for db in sorted(glob.glob(f"{root}/thread_history_*.sqlite")):
found += db_rows_naming(db, uuid, ("thread_id",))
return found
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str: def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
return f"{binary} --dangerously-bypass-approvals-and-sandbox" return f"{binary} --dangerously-bypass-approvals-and-sandbox"
@@ -1,7 +1,7 @@
import os, json, glob, shutil import os, json, glob, shutil
from typing import Optional, Any from typing import Optional, Any
from urllib.parse import quote from urllib.parse import quote
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, scan_names
from lib_py.verify_session import workspace_key from lib_py.verify_session import workspace_key
@@ -108,6 +108,11 @@ class GrokAgentAdapter(BaseAgentAdapter):
purged.append(self.artifact_path(uuid, ctx)) purged.append(self.artifact_path(uuid, ctx))
return purged return purged
def residual_artifacts(self, uuid: str, ctx: DiscoveryContext) -> list:
# Issue #31: reported, not removed - memory-v2 observations are the
# shared store of grok's memory pipeline.
return scan_names(f"{ctx.home_dir}/.grok", uuid)
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str: def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
if session_uuid: if session_uuid:
return f"{binary} --session-id {session_uuid} --permission-mode bypassPermissions" return f"{binary} --session-id {session_uuid} --permission-mode bypassPermissions"
@@ -1,6 +1,6 @@
import os, sys, sqlite3, shutil import os, sys, sqlite3, shutil
from typing import Optional, Any, List from typing import Optional, Any, List
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, scan_names, db_rows_naming
from lib_py.verify_session import workspace_key from lib_py.verify_session import workspace_key
_PROVIDER_ENV_VARS = ( _PROVIDER_ENV_VARS = (
@@ -194,6 +194,13 @@ class OpenCodeAgentAdapter(BaseAgentAdapter):
sys.stderr.write(f"WARN: purge opencode db records failed: {e}\n") sys.stderr.write(f"WARN: purge opencode db records failed: {e}\n")
return purged return purged
def residual_artifacts(self, uuid: str, ctx: DiscoveryContext) -> list:
# Issue #31: the purge leaves event-sourced rows (event, todo, ...);
# deleting those is not known to be safe, so they are reported.
found = scan_names(f"{ctx.home_dir}/.local/share/opencode", uuid)
return found + db_rows_naming(self._resolve_db(ctx), uuid, ("session_id", "aggregate_id"),
id_tables=(("session", "id"),))
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str: def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
return f"{binary} --auto --agent build" return f"{binary} --auto --agent build"
+35
View File
@@ -2,6 +2,7 @@
import os, json, sqlite3, shutil, subprocess import os, json, sqlite3, shutil, subprocess
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from urllib.parse import quote
from lib_py.paths import resolve_home from lib_py.paths import resolve_home
from lib_py.verify_session import verify_session_uuid, workspace_key from lib_py.verify_session import verify_session_uuid, workspace_key
@@ -107,6 +108,40 @@ def scan_names(root: str, uuid: str, skip=()) -> list:
return sorted(found) return sorted(found)
def db_rows_naming(db: str, uuid: str, columns=(), id_tables=()) -> list:
""""<db> (<table>.<col>: <n> rows)" for every table holding rows of this
conversation in one of `columns`, or in the id column of an `id_tables`
(table, column) pair (Issue #31). Read-only; unreadable tables are skipped."""
if not _plausible_id(uuid) or not db or not os.path.isfile(db):
return []
try:
conn = sqlite3.connect(f"file:{quote(os.path.abspath(db))}?mode=ro", uri=True)
except Exception:
return []
found = []
try:
tables = [r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")]
for table in tables:
qt = table.replace('"', '""')
try:
cols = {r[1] for r in conn.execute(f'PRAGMA table_info("{qt}")')}
except Exception:
continue
wanted = [c for c in columns if c in cols]
wanted += [c for t, c in id_tables if t == table and c in cols and c not in wanted]
for col in wanted:
try:
n = conn.execute(f'SELECT count(*) FROM "{qt}" WHERE "{col}"=?', (uuid,)).fetchone()[0]
except Exception:
continue
if n:
found.append(f"{db} ({table}.{col}: {n} rows)")
except Exception:
pass
finally:
conn.close()
return found
class SpawnSpec: class SpawnSpec:
def __init__(self, agent_name: str, session_name: str, command: str, env: Optional[Dict[str, str]] = None): def __init__(self, agent_name: str, session_name: str, command: str, env: Optional[Dict[str, str]] = None):
self.agent_name = agent_name self.agent_name = agent_name