Persistence
AgentMux persists state across four files, owned by two processes (sidecar + launcher). This page enumerates them and explains the boundary.
At a glance
Section titled “At a glance”| File | Format | Writer | Owns |
|---|---|---|---|
objects.db | SQLite | sidecar | Workspaces, tabs, blocks, windows, layouts, controllers + the agent-definition / identity / memory / drone catalog — the app-domain reducer state of record |
filestore.db | SQLite | sidecar | Per-block content blobs (agent configs, snapshots, file-pane content) |
sagas.db | SQLite | sidecar | Saga step + lifecycle rows for the srv sagas (compensated after a crash) |
launcher-events.log | JSONL | launcher | Append-only Layer 1 reducer event log (durable, OS-level facts) |
All four live under the version-scoped data dir at <data-dir>/data/. For installed/portable builds the data dir is version-scoped — ~/.agentmux/channels/<channel>/versions/<version>/data/ — so two concurrent release versions never share SQLite DBs or caches. Dev builds use ~/.agentmux/dev/<branch>/data/. SQLite files are inside data/db/; the JSONL log is directly in data/. See Data layout for how <channel> and <version> are derived.
Not everything is version-scoped. Alongside
data/, the version dir also holdslogs/,cef-cache/, andruntime/(ipc-port, lock). Butconfig/(settings, including shortcut overrides) andagents/(agent working dirs) live one level up, at the channel-wide root~/.agentmux/channels/<channel>/— so settings and agent working dirs survive version upgrades. Path resolution is indata_paths.rs.
Resolution is centralized in agentmux-common::DataPaths — the launcher resolves once and exports AGENTMUX_DATA_DIR; host + sidecar read it from env.
objects.db — sidecar reducer state
Section titled “objects.db — sidecar reducer state”Owned by agentmux-srv. Opened via WaveStore::open(<data-dir>/db/objects.db) in crates/srv/src/main.rs. The historical type name WaveStore predates the AgentMux rebrand — the file itself is objects.db.
Stores every:
- Workspace — top-level container, identity + window membership
- Tab — per-window tab list with layout root pointer
- Block — every pane (terminal, browser, agent, file, system metrics, …) including its meta map (controller type, agent id, URL, scroll position, …)
- Window — OS-window record; the host reads this on launch to restore window geometry
- Layout — per-tab pane tree (split orientation, focus path, sizes)
- Client — per-CEF-session client record
- Controller — block controller registration (shell, command runner, web view)
It also holds the agent domain: agent definitions (the launchable agent catalog — formerly “Forge”), accounts and bundles, per-launch agent instances, and drone definitions/runs.
The schema is a single flat table set built by run_object_schema
(crates/srv/src/backend/storage/migrations.rs) — no migration ladder. A
PRAGMA user_version stamp acts as a downgrade tripwire.
This is where “I closed the app and reopened it; my tabs are back” comes from. If you delete objects.db, the next launch comes up with a single empty workspace.
Bootstrap + persist subscriber
Section titled “Bootstrap + persist subscriber”At startup the sidecar’s reducer loads its in-memory state from objects.db (bootstrap_state_from_wstore). During the session, HTTP/WS RPC writes still go directly to SQLite via wcore — SQLite stays authoritative even if the reducer’s session-only projection diverges.
A separate persist subscriber (crates/srv/src/persist_subscriber.rs) consumes the reducer’s broadcast bus and mirrors lifecycle events back to SQLite for entities the reducer owns. On a Lagged error it does a scoped full-resync (insert/update only, never delete — there’s a code comment explaining the migration-window reason).
Migration status: workspaces are reducer-driven (E.2c.2 onward); tab + block writes are still mostly RPC-direct.
Cross-channel agent persistence
Section titled “Cross-channel agent persistence”objects.db is authoritative per channel, but user agents are meant to follow
you everywhere — an agent created in one channel (or one release version) should
appear, fully configured, in all of them. That globalization is implemented by a
definition-registry mirror that shadows local objects.db agent mutations into
an account-wide store. See
SPEC_CROSS_CHANNEL_AGENT_PERSISTENCE
and def_registry_mirror.rs.
The two global stores
Section titled “The two global stores”Both live under ~/.agentmux/shared/agents/ (account-wide, channel- and
version-independent), as siblings resolved in
registry/paths.rs:
- Definition registry —
~/.agentmux/shared/agents/definitions/— the global copy of each user agent’s definition (the launchable catalog entry). - Instances registry —
~/.agentmux/shared/agents/registry/— the named cross-channel instances (a running/launched agent’s identity).
What travels with the definition
Section titled “What travels with the definition”A global definition record is not just the row from db_agent_definitions. The
mirror (agent_definition_to_record) folds the agent’s content blobs (e.g.
instructions, the per-agent ui:zoom) and its skills into the same record,
because both live in separate local tables and must travel together for a
cross-channel agent to launch with its instructions intact.
Authority and read direction
Section titled “Authority and read direction”- Local SQLite stays authoritative for writes within its channel. The global store is a cross-channel read view, never a write target that overrides local truth.
- Reads are global-first for the roster.
agent_def_listoverlays the global user-agent roster so an agent that exists only in another channel still shows up. - Content + skills fall back to the global record when the agent has no local SQLite row (the cross-channel case), so it launches with its instructions.
Symmetric mirroring (set and delete)
Section titled “Symmetric mirroring (set and delete)”Every local mutation re-mirrors via registry_def_upsert:
agent_content_setandagent_content_deleteboth re-mirror — so a reset-to-default that deletes a local content row propagates the deletion globally, not just the addition.- Skills mirror the same way (insert and delete).
To make that safe, the mirror reads the local row with local-only accessors
(agent_content_get_all_local / agent_skill_list_local). If it read through the
global-fallback accessors, deleting a local blob would re-read the still-present
global copy and re-mirror it straight back, defeating the deletion.
Deletes are global tombstones
Section titled “Deletes are global tombstones”Deleting a user agent calls registry_def_retire, which writes a retired/
tombstone into the definition store rather than just removing the active
record. The tombstone is what stops a stale objects.db in another channel from
resurrecting a deleted agent on its next registry_def_upsert — upsert refuses
to revive a tombstoned id. A delete works even for an agent that exists only
globally (no local SQLite row): the retire path still tombstones and reports
success.
Resume across channels
Section titled “Resume across channels”The instances registry carries a session_id (added in schema v1→v2, see
registry/schema.rs)
so a cross-channel agent’s conversation can resume rather than starting cold
when it’s opened from a different channel or version.
Seeded vs user agents
Section titled “Seeded vs user agents”Only user agents go global. Seeded template agents stay local (per-channel):
they’re manifest-managed and re-seeded into each channel, and their hide flag is
per-channel UI state — so the mirror skips any definition with is_seeded != 0.
filestore.db — content blobs
Section titled “filestore.db — content blobs”Same writer (agentmux-srv), opened via FileStore::open(<data-dir>/db/filestore.db). Where blob content lives: agents’ generated config files, session archives, file-pane buffers, captured screenshots. objects.db holds the meta — filestore.db holds the bytes.
You can rebuild filestore.db from scratch (you’ll lose snapshots) without losing your workspace structure. The reverse is not true — objects.db references blobs by hash; deleting filestore.db while keeping objects.db leaves dangling references that the file pane will report as missing.
sagas.db — sidecar saga coordinator
Section titled “sagas.db — sidecar saga coordinator”Per SPEC_PHASE_E_SAGAS_2026-04-30.md (Path A — chosen during Phase E.5), srv’s sagas run multi-step workspace, tab and block commands such as tear-off, redock and delete (see How sagas fit). sagas.db records each saga’s lifecycle and each step’s command and outcome (crates/srv/src/sagas/log.rs).
After a crash, srv doesn’t resume an interrupted saga, it undoes it. At startup, before the API server accepts requests, compensate_unresolved (crates/srv/src/sagas/recovery.rs) walks each unresolved saga’s succeeded steps in reverse and dispatches their inverse commands. A step with no inverse it can derive is logged for review, and the saga is marked failed_compensation.
Launcher sagas aren’t persisted
Section titled “Launcher sagas aren’t persisted”The launcher’s two sagas, pool_respawn and window_cleanup (crates/launcher/src/saga/), keep their state in an in-memory registry. Both narrate cleanup the host does on its own, and srv rebuilds windows from its own state after a crash, so an interrupted launcher saga leaves nothing to recover. Their transitions are in the launcher log, prefixed [saga]. Older versions kept a launcher-sagas.db; the launcher deletes it at startup.
launcher-events.log — Layer 1 event log
Section titled “launcher-events.log — Layer 1 event log”JSONL, append-only. Written by the launcher’s event_log::run_disk_writer task to <data-dir>/launcher-events.log. Holds every command + event the launcher reducer processed: process spawn/exit, window create/destroy, monitor changes, drag operations, tear-off events. Includes WRR (Window Reality Reconciliation) drift records.
In-memory ring buffer (4096 entries) backs disk overflow — recent events are queryable from a running instance without touching disk.
This is the canonical forensic source when something feels off at the OS level: a window vanished, a pane drifted, a monitor disconnect lost focus. Search recipes:
# Windows / Git Bash / muxlog-stylegrep -E "HiddenSinceOpen|HwndWithoutBrowser|WRR-DRIFT|wfr:gate|wfr:runner|pending=" \ "$AGENTMUX_DATA_DIR/launcher-events.log"Backup + portability
Section titled “Backup + portability”Each file is independently restorable:
- Cold-copy the whole
<data-dir>/data/directory while AgentMux is closed — that’s the simplest, most reliable backup. - Hot copy of an individual SQLite file while AgentMux is open is supported because the writer holds short transactions; use
sqlite3 .backup. JSONL log is append-only and safe to copy at any time.
Re-importing into a fresh install: stop AgentMux, place the files at <data-dir>/data/db/ (and the log at <data-dir>/data/), and launch. Bootstrap reads them.
What’s NOT here
Section titled “What’s NOT here”For completeness, things that are NOT stored in these four files:
- Settings — JSON at the channel-wide
~/.agentmux/channels/<channel>/config/settings.json; not underversions/<version>/, so settings survive version upgrades - Agent working directories — per-agent workspace dirs under the channel-wide
~/.agentmux/channels/<channel>/agents/, again one level above the version dir (the agent definitions themselves live inobjects.dblocally and in the global registry — see below) - Cookies / OAuth tokens / dictionary downloads —
~/.agentmux/shared/(account-wide, version- and channel-independent) - Global agent registry — user-agent definitions + named instances live account-wide under
~/.agentmux/shared/agents/(see Cross-channel agent persistence) - Cross-process session-ownership leases —
~/.agentmux/shared/agents/registry/leases/<instance_id>.lease.json, a sibling directory of the instances registry above, not part of any per-channel/per-version<data-dir>/data/tree at all. One JSON file per agentinstance_id: TTL-based (renewed every 5s, reclaimable once ~3 renewals are missed —LEASE_TTL_MS/RENEW_INTERVAL_MS) and scoped to aboot_id, so at most one process can hold the right to drive a given agent’s turns at a time in host-mode. Guarded by a real OS advisory lock (flock/LockFileEx) for the claim/renew/release critical section, not the atomic-rename-only patternRegistry::upsertuses elsewhere. Seecrates/srv/src/registry/leases.rs. - Chromium cache — version-scoped
~/.agentmux/channels/<channel>/versions/<version>/cef-cache/ - CLI provider configs — auth-config-dir managed by each provider’s CLI
See also
Section titled “See also”- Architecture overview — process topology
- Multi-instance & dev mode — full data-dir layout
- Reducer stack — how persistence relates to the reducer model
- Debugging — log discovery + diagnostic recipes