PersistenceOne SQLite database, sixteen tables, newest-first migrations, and a baseline parity test.

Everything the agents service remembers is in one SQLite file (./data/agents.sqlite by default), plus per-agent state directories on disk for memory and attachments.

The tables

Schema: agents/src/sqlite-schema.sql. Nearly every table is WITHOUT ROWID and carries account_idaccount isolation is a schema property, not a convention.

Table

Holds

accounts

Account rows, created lazily as resources are written

account_authorizations

Which signer may act for an account, with role OWNER or AGENT

server_config

Server-wide values, including the AES-GCM secret key

model_providers

Provider records, config as CBOR

secrets

Encrypted API keys, OAuth blobs, and signing-identity seeds

agents

Agent definitions (CBOR), state dir, status

agent_drafts

Hypermedia write drafts

tool_documents

Tool documents, keyed by name, addressed by CID

sessions

Threads: title, status, parent, plan

session_events

The Log: CBOR payload per (session_id, seq)

runs

Runs — the execution tree and the dispatch queue

run_journal

Script effect log, (run_id, seq)

run_event_waits

What a parked run is listening for (Time)

agent_triggers

Trigger rules: source, prompt, continuation

trigger_firings

One row per firing, with the exactly-once unique key

activity_watermarks

Per-account, per-server feed cursors

action_idempotency

Stored request/response bytes per client request id

Two structural details carry most of the system's weight. runs.status/queue/not_before/created_at is indexed as runs_dispatch — that index is the queue. And trigger_firings has UNIQUE (account_id, trigger_id, activity_key) — that constraint is exactly-once trigger firing.

Migrations

agents/src/sqlite.ts holds a plain array of SQL strings with a convention that trips people up:

export const migrations: string[] = [ // ======= IMPORTANT: Add new migrations below this line. ======= … ].reverse()

The array is written newest-first and reversed at the end, so a new migration goes at the top of the literal. desiredVersion is simply migrations.length (currently 11).

Each migration runs inside its own SAVEPOINT within one outer transaction; a failure rolls back to that savepoint and aborts the batch, so a half-applied schema is not a state the server can boot into.

The version gate is strict. A database with no server_config, a legacy schema_version key, a null version, or a version newer than the binary is refused outright — and the server then serves a 500 on every route with the message Delete the database file (rm <dbPath>*) and restart the server. Rolling a binary back to before a migration means recreating the database.

The parity test that keeps the baseline honest

A fresh database is built from sqlite-schema.sql alone, never by replaying migrations — so every column a migration adds must also be added to the baseline file, or fresh installs and migrated installs diverge silently.

sqlite.test.ts asserts both directions: a fresh init has the columns migrations add (runs.parent_tool_call_id, runs.continued_from_run_id, the run_event_waits table, agent_triggers.continuation_cbor), and a synthetic old database built by regex-stripping those columns migrates back up to parity. The comment says it plainly: "this is the assertion that catches the two drifting apart."

Transaction policy

Never hold a write transaction across a provider or tool network call. CreateAgent and CreateSession use short idempotent transactions; MessageSession explicitly avoids long transactions because it does model and network work. The exception that proves the rule is signal delivery, which must be one transaction — journal append, run requeue, and wait deletion together — to be exactly-once.

What is not here

There is no retention or pruning policy for events, runs, or journals. A busy account grows monotonically. Journals are the one exception, bounded per run at 5,000 entries or 8 MiB, which is what makes continueAsNew necessary rather than optional for long loops.

Related

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime