Skip to content

User data retention regression guard

Owner rule: never lose user data. Durable bridge messages, job records, run logs, metadata, approvals, completed wait records and CLI transcripts must survive maintenance and overflow. Archive destinations have unique names or refuse an existing destination. A rename to an archive preserves the same file; SQL removal requires a committed archive copy first. Archive failures leave originals intact. OpenCode uninstall archives installed files and directories to preserve local edits.

test/data-retention.test.ts scans every production TypeScript file for filesystem removals and destructive SQL. Its exact-expression allowlist counts each reviewed call, so new calls, changed arguments and aliases fail the guard. This is a static regression check for the APIs in use, not a proof about arbitrary computed code or shell commands. Existing storage tests exercise run pruning, job overflow, migration rollback and archive failure. Recovery HTTP tests use temporary homes and CLI fixtures, including a temporary OpenCode database, and compare original transcript bytes after reads. No automated test uses the owner’s actual data roots.

Source Permitted removal and why it contains no unique user data
core/json-store.ts Unpublished atomic-write .tmp file only.
core/storage-lock.ts Maintenance lock and per-process lease files (PID, start time and nonce only). A stale lock is moved aside and removed only while its nonce still matches; a lease only after its owner process is gone.
core/token.ts Only the exclusively created, unpublished token staging file (or its duplicate hard link after publication). The active token is never removed.
core/history-store.ts A failed history copy attempt: every row moves into retained_<stamp>_* tables in the same transaction before the copy tables are emptied. Derived search documents are cleared with FTS delete-all for that retry or a reindex.
core/history.ts Derived search index only (pending queue, tags, cursors, files); a reindex rebuilds it from the read-only sources.
core/finished-run-bundles.ts The packing failure record of a run that was packed successfully afterwards.
core/storage-absorb.ts The absorb cursor of a table with conflicts, so the next run scans it again. Conflicting rows are kept.
core/storage-finalize.ts Owner decision 2026-10-09: only the explicit, confirmed agent-bridge storage finalize command, after the new format is migrated and verified. Each legacy table, file or bundled original is proven redundant (row by row, or byte-exact in a verified bundle) immediately before removal; anything unproven is kept and reported.
core/migration-lock.ts Exclusive migration lock (PID and nonce only) on release, failed lock publication or confirmed exited owner; empty recovery coordination file. Never the database or snapshot.
core/notifications.ts Empty notification lock directory.
core/node.ts Abandoned Unix-domain socket path.
mcp/rewake.ts Socket endpoint registrations containing only port, secret and PID. No conversations or prompts.
core/resource-slots.ts Expired/released process concurrency leases in slots, not messages.
network/files.ts Unpublished .partial-* transfer staging directory after a failed receive. The sender’s original is retained.
cli/smoke.ts, cli/reliability.ts, cli/reliability-live.ts Explicitly created temporary test homes/repositories only.
cli/dashboard-key.ts Only the random staging file successfully created by this call with exclusive creation. It is an unpublished credential or a duplicate hard link after atomic publication. The active dashboard-key is never removed; replacement requires the owner’s explicit ui --reset-key command.
core/worktree-links.ts Symlink/junction itself only; never its destination. Used by explicitly authorized cleanup; traversal through a linked root/container or linked components inside that boundary is refused; leaf link entries are detached without traversal. System/home aliases above that boundary are resolved before removal. The private ab-cache-ignore-* mkdtemp view contains only empty directories and copies of ignore rules; its exact removal is permitted and never reaches linked caches.
core/worktree.ts A disposable worktree copy after its callers verify clean, merged Git content. Failed creation retains existing branches and partial checkout data. The opt-in job-close caller additionally verifies all checkout/reflog commits on remote branches before non-force Git removal. Its only cache-removal call covers physical, untracked Unity Libraries proven absent at job creation; these are job-created regenerable caches. Linked/shared caches, unknown ignored files, uncommitted work, unknown provenance and unproven process shutdown are retained. See docs/worktree-lifecycle.md. No new removal expression is allowlisted.
core/sqlite-maintenance.ts Exact archived row selection, after archive commit; copy failure rolls the source transaction back.
core/sqlite-migrations.ts Backup recovery under the migration writer lock, after reading original rows from the durable pre-migration snapshot. Failure rolls back and retains the snapshot.

Approvals and completed wait records are not exceptions: they are archived. Run pruning and job overflow are not exceptions: they are archived. No removal or mutation is allowed in src/core/transcripts/ or the history inspector.

Job-store locks and worktree leases are also archived. Their versioned metadata records a process creation identity and a unique nonce. Complete metadata is published with an exclusive hard link, so interrupted publication cannot leave an empty canonical lock. Confirmed dead owners are claimed by an exact nonce rename before the canonical lock moves into its owner directory and that directory moves to a unique archive under .metadata-leases/. Original metadata bytes are retained; worktree contents are untouched. Live, unverifiable, malformed and legacy owners stay protected. Recovery never expires a live owner by age. Filesystems without hard-link support fail closed.

scripts/audit-run-recovery.ts only reads bridge JSON/logs and uses the existing read-only CLI transcript readers. It does not start startUi, create tokens, repair malformed JSON, open a CLI database for writing or launch an agent. Build this script to a temporary .mjs outside the owner’s data roots with esbuild, then run it with Node; an optional first argument selects the bridge home. It reports archived runs, distinct durable jobs, recovered jobs and transcript availability per agent. Availability means a reader found a supported transcript, even if its first bounded page has no displayable messages. Reader discovery bounds are in docs/transcripts.md; no run/job history count limit is imposed.

Retention configuration and upgrade behavior

Section titled “Retention configuration and upgrade behavior”

History is archived, never automatically deleted: messages, jobs, run logs, completed approvals and wait records remain available after maintenance. agent-bridge doctor checks storage; doctor --backup creates a verified snapshot. See storage and recovery.

bridge.db uses SQLite PRAGMA user_version (currently schema 9), with ordered migrations for messages, decisions, legacy history tables, session identity and durable delivery. In 0.30.0, history and conversations move into independently versioned history.db v1 through a protected, verified copy; legacy tables remain intact. See conversation storage. Unversioned databases upgrade through version 1. Pending migrations run in order in one transaction, after a consistent SQLite backup (including committed WAL data). Opening a newer schema fails without changing it. Three recent .backup-* copies stay beside the store; older backups move into archive/ and are never deleted automatically.

JSON stores use version: 2. Legacy config, preferences, jobs and runner state remain readable; the first upgraded write backs up the old file. jobs.json is now { "version": 2, "jobs": [...] }; auto-wake.json keeps preferences under peers. Unknown fields survive updates. Writes replace files atomically and refuse newer versions. Corrupt JSON moves to .corrupt-<time>-<id> with a warning, preserving its original bytes. Other read errors prevent replacement rather than treating an unreadable file as empty.

Retention controls are environment variables (nonnegative integers; 0 disables pruning):

Env var Default Retention behavior
AGENT_BRIDGE_MESSAGE_TTL_MS 604800000 (7 days) Expired messages move into separate archive.db before removal from the primary
AGENT_BRIDGE_QUEUED_MAIL_MAX_AGE_MS 86400000 (1 day) Stale queued messages move into archive.db before a peer claims mail
AGENT_BRIDGE_JOB_STORE_LIMIT 200 Finished jobs beyond the limit move into archive/jobs.json.overflow.json-*; running and interrupted jobs stay active
AGENT_BRIDGE_RUN_LOG_LIMIT 50 Older finished logs and their metadata move into runs/archive/; unfinished feeds stay active
AGENT_BRIDGE_RUNNER_KEEP_MS 604800000 (7 days) Old finished runner state/spec files move into jobs/archive/; running state stays active

Archived messages remain visible in history and searchable through the authenticated history API. Archived jobs remain continuable, and archived run logs stay readable in the dashboard and watch. Legacy archived_messages rows move into archive.db without changing the primary schema; the archive has its own versioned migrations. Retain these archives with your normal backups; they have no automatic size cap. Runner specs are archived after consumption and earlier runner state is archived before a new turn starts. Stored job prompts are retained in full; only displayed previews and the in-memory recent-job list are bounded.