From b3461377ae61e69cdd78c5d0e7e9a43095317a54 Mon Sep 17 00:00:00 2001 From: Godopu Date: Thu, 15 Oct 2026 15:16:06 +0000 Subject: [PATCH] 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 --- .../skills/lib_py/agents/adapters/codex.py | 13 ++++++- .agents/skills/lib_py/agents/adapters/grok.py | 7 +++- .../skills/lib_py/agents/adapters/opencode.py | 9 ++++- .agents/skills/lib_py/agents/base.py | 35 +++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/.agents/skills/lib_py/agents/adapters/codex.py b/.agents/skills/lib_py/agents/adapters/codex.py index b26d719..0c038da 100644 --- a/.agents/skills/lib_py/agents/adapters/codex.py +++ b/.agents/skills/lib_py/agents/adapters/codex.py @@ -1,6 +1,6 @@ import os, json, glob, sqlite3, subprocess 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 class CodexAgentAdapter(BaseAgentAdapter): @@ -158,6 +158,17 @@ class CodexAgentAdapter(BaseAgentAdapter): pass 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: return f"{binary} --dangerously-bypass-approvals-and-sandbox" diff --git a/.agents/skills/lib_py/agents/adapters/grok.py b/.agents/skills/lib_py/agents/adapters/grok.py index a9a0503..72f0270 100644 --- a/.agents/skills/lib_py/agents/adapters/grok.py +++ b/.agents/skills/lib_py/agents/adapters/grok.py @@ -1,7 +1,7 @@ import os, json, glob, shutil from typing import Optional, Any 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 @@ -108,6 +108,11 @@ class GrokAgentAdapter(BaseAgentAdapter): purged.append(self.artifact_path(uuid, ctx)) 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: if session_uuid: return f"{binary} --session-id {session_uuid} --permission-mode bypassPermissions" diff --git a/.agents/skills/lib_py/agents/adapters/opencode.py b/.agents/skills/lib_py/agents/adapters/opencode.py index 8338ca2..f1dd484 100644 --- a/.agents/skills/lib_py/agents/adapters/opencode.py +++ b/.agents/skills/lib_py/agents/adapters/opencode.py @@ -1,6 +1,6 @@ import os, sys, sqlite3, shutil 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 _PROVIDER_ENV_VARS = ( @@ -194,6 +194,13 @@ class OpenCodeAgentAdapter(BaseAgentAdapter): sys.stderr.write(f"WARN: purge opencode db records failed: {e}\n") 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: return f"{binary} --auto --agent build" diff --git a/.agents/skills/lib_py/agents/base.py b/.agents/skills/lib_py/agents/base.py index 50ccc00..dff7daf 100644 --- a/.agents/skills/lib_py/agents/base.py +++ b/.agents/skills/lib_py/agents/base.py @@ -2,6 +2,7 @@ import os, json, sqlite3, shutil, subprocess from typing import Optional, Dict, Any, List +from urllib.parse import quote from lib_py.paths import resolve_home 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) +def db_rows_naming(db: str, uuid: str, columns=(), id_tables=()) -> list: + """" (.: 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: def __init__(self, agent_name: str, session_name: str, command: str, env: Optional[Dict[str, str]] = None): self.agent_name = agent_name