?

Location and driver

The session store is a single SQLite database at state.path (defaults to ~/.local/share/rousseau/sessions.db, see internal/config/config.go setDefaults).

Rousseau uses modernc.org/sqlite — a pure-Go SQLite driver. There is no CGO or libsqlite3 dependency. The Go binary in bin/rousseau is fully static.

internal/state/sqlite/store.go Open() applies four pragmas on every open:

PRAGMA Purpose
journal_mode=WAL Write-ahead logging. Enables concurrent readers, safe live backups.
foreign_keys=ON Standard integrity guarantee.
busy_timeout=15000 15-second wait on lock contention — critical once multiple transports write concurrently.
EnsureSearch runs afterwards to install the FTS5 schema.

The store is opened once per process. Multiple daemons pointing at the same DB file are supported because of the busy-timeout + WAL combination — the WhatsApp bridge, rousseau mcp, and rousseau session list can share the file safely.

Schema tour

Table: sessions

Defined in internal/state/sqlite/schema.sql:

CREATE TABLE IF NOT EXISTS sessions (
    id             TEXT PRIMARY KEY,
    title          TEXT NOT NULL,
    payload        TEXT NOT NULL,        -- JSON blob of the full agent.Session
    message_count  INTEGER NOT NULL DEFAULT 0,
    created_at     TEXT NOT NULL,
    updated_at     TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_sessions_updated_at
    ON sessions(updated_at DESC);

Payload shape. The payload column stores the full agent.Session JSON — roles, content blocks, tool-use and tool-result blocks, timestamps. See Save/Load in internal/state/sqlite/store.go. Keeping the whole session as one JSON blob keeps schema migrations rare; queries against internals go through the FTS5 index below.

Timestamps are ISO-8601 with millisecond precision (2006-01-02T15:04:05.000Z in Go time syntax), UTC.

Ordering. idx_sessions_updated_at powers List and RecentSessions (both in store.go / search.go).

Virtual table: sessions_fts (FTS5)

Installed by searchSchema in internal/state/sqlite/search.go:

CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
    session_id UNINDEXED,
    title,
    body,
    tokenize = 'porter unicode61'
);

Three trigger-driven writes keep it consistent with sessions:

  • sessions_fts_ai — after INSERT on sessions, mirror the row.
  • sessions_fts_au — after UPDATE, delete + reinsert.
  • sessions_fts_ad — after DELETE, drop the FTS row.

Backfill. EnsureSearch runs a LEFT JOIN on every Open() to insert any sessions rows the FTS index doesn't already have. This makes the index safe to add to an existing database — no manual migration.

Tokenisation. porter unicode61 — Porter stemmer + Unicode-aware casefolding. Case-insensitive, handles English morphology (retry/retries/retried).

Ranking. Search() orders by bm25(sessions_fts) (lower is more relevant). SearchHit.Rank exposes it.

Query syntax. Passed to FTS5 verbatim. See Tutorial: Expose tools via MCP for the operator cheat sheet.

Table: jid_sessions

Persists platform-sender-to-session-id mappings; installed by NewJIDMap in internal/state/sqlite/jidmap.go:

CREATE TABLE IF NOT EXISTS jid_sessions (
    jid         TEXT PRIMARY KEY,
    session_id  TEXT NOT NULL,
    created_at  TEXT NOT NULL
);

Every long-running transport uses the JID map so the same phone number, Matrix user, or Slack user picks up the same conversation across restarts. Router.Handle (internal/transport/router.go) looks it up on inbound; Put writes it after Save.

The JID space is transport-specific — 447900123456@s.whatsapp.net for WhatsApp, @user:matrix.org for Matrix, U01ABC… for Slack. The transport is responsible for canonicalising.

Table: cron_jobs

Installed by NewCronStore in internal/state/sqlite/cron.go:

CREATE TABLE IF NOT EXISTS cron_jobs (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL UNIQUE,
    cron_expr   TEXT NOT NULL,
    prompt      TEXT NOT NULL,
    deliver_to  TEXT NOT NULL,
    enabled     INTEGER NOT NULL DEFAULT 1,
    created_at  TEXT NOT NULL,
    last_run_at TEXT
);

UNIQUE(name) prevents duplicates. rousseau cron add/list/remove/enable/disable (from internal/cli/cron.go) all round-trip through this table. The scheduler in internal/cron/scheduler.go reconciles from it every poll_interval. MCP exposes it read-only via rousseau_cron_list.

Concurrency posture

  • WAL allows unlimited concurrent readers alongside a single writer.
  • busy_timeout=15000 means a writer that hits contention waits up to 15 s rather than failing fast. In practice the WhatsApp bridge holds the writer role while rousseau mcp and rousseau session list are read-only visitors.
  • The store is not designed for cross-machine concurrency. Two hosts writing to the same file over NFS is undefined behaviour — use a single writer and rsync the DB elsewhere for read replicas.

Backing up

The safest approach is a live sqlite3 .backup:

sqlite3 ~/.local/share/rousseau/sessions.db ".backup '/backup/sessions.db.$(date -I).bak'"

.backup uses SQLite's online backup API and works while the primary is being written to. restic / borg snapshots on the raw file are also safe because of WAL — the backup gets a consistent snapshot as of the moment the file was read.

The whatsapp.db file (whatsmeow device credentials) is a separate database; back it up the same way if you want to avoid re-pairing after a restore.

Rebuilding the FTS index

If the FTS5 index goes out of sync (extremely rare — the triggers keep it consistent), rebuild it:

sqlite3 ~/.local/share/rousseau/sessions.db <<'SQL'
DELETE FROM sessions_fts;
INSERT INTO sessions_fts (session_id, title, body)
SELECT id, title, payload FROM sessions;
SQL

Rousseau's EnsureSearch will not undo this; the triggers just resume from a clean state.

Related

Type to search 150+ pages. Ranking: BM25 with title/description boost.