Skip to content

Persistence

AgentMux persists state across four files, owned by two processes (sidecar + launcher). This page enumerates them and explains the boundary.

FileFormatWriterOwns
objects.dbSQLitesidecarWorkspaces, tabs, blocks, windows, layouts, controllers + the agent-definition / identity / memory / drone catalog — the app-domain reducer state of record
filestore.dbSQLitesidecarPer-block content blobs (agent configs, snapshots, file-pane content)
sagas.dbSQLitesidecarSaga step + lifecycle rows for the srv sagas (compensated after a crash)
launcher-events.logJSONLlauncherAppend-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 holds logs/, cef-cache/, and runtime/ (ipc-port, lock). But config/ (settings, including shortcut overrides) and agents/ (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 in data_paths.rs.

Resolution is centralized in agentmux-common::DataPaths — the launcher resolves once and exports AGENTMUX_DATA_DIR; host + sidecar read it from env.

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.

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.

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.

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).

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.

  • 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_list overlays 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.

Every local mutation re-mirrors via registry_def_upsert:

  • agent_content_set and agent_content_delete both 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.

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.

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.

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.

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.

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.

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.

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:

Terminal window
# Windows / Git Bash / muxlog-style
grep -E "HiddenSinceOpen|HwndWithoutBrowser|WRR-DRIFT|wfr:gate|wfr:runner|pending=" \
"$AGENTMUX_DATA_DIR/launcher-events.log"

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.

For completeness, things that are NOT stored in these four files:

  • Settings — JSON at the channel-wide ~/.agentmux/channels/<channel>/config/settings.json; not under versions/<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 in objects.db locally 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 agent instance_id: TTL-based (renewed every 5s, reclaimable once ~3 renewals are missed — LEASE_TTL_MS/RENEW_INTERVAL_MS) and scoped to a boot_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 pattern Registry::upsert uses elsewhere. See crates/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