Skip to content

state.db — Session State Store

state.db is the main SQLite database for Hermes Agent on the Agent Host. It is the single source of truth for session history, message logs, and full-text search. A separate file sessions.db exists but is a 0-byte placeholder — all data lives in state.db.

~/.hermes/state.db # Live database (WAL mode)
~/.hermes/state-snapshots/ # Auto-snapshots (pre-update, etc.)
Table Purpose ~Size
sessions One row per conversation session. Stores session ID, source platform (slack/telegram/cli), model config, token usage, cost tracking, timestamps, parent session (for handoff chains), and title. 300+ rows
messages Individual messages per session. Stores role (user/assistant/tool/system), content, tool call metadata, timestamps, reasoning outputs, and platform message IDs. 17 000+ rows
messages_fts FTS5 virtual table over messages.content with trigram tokenizer. Powers the session_search tool for fast full-text retrieval across all sessions. 17 000+ docs
schema_version Migration version counter. Used by Hermes to apply schema upgrades on startup. 1 row
state_meta Generic key-value metadata store. 2 rows
compression_locks Advisory locks for session compression. Prevents concurrent compression of the same session. 0 rows
sqlite_sequence Auto-updated by SQLite for AUTOINCREMENT on messages.id. 1 row

The trigram FTS5 index creates supporting tables automatically: messages_fts_config, messages_fts_content, messages_fts_data, messages_fts_docsize, messages_fts_idx, plus messages_fts_trigram_*. These are all internal to the FTS5 virtual table and should not be queried directly.

CREATE INDEX idx_sessions_source ON sessions(source);
CREATE INDEX idx_sessions_parent ON sessions(parent_session_id);
CREATE INDEX idx_sessions_started ON sessions(started_at DESC);
CREATE INDEX idx_messages_session ON messages(session_id, timestamp);
CREATE INDEX idx_compression_locks_expires ON compression_locks(expires_at);
Column Purpose
id Primary key, e.g. slack:C0B7C9ZFY4R:1780260332.893459
source Platform: slack, telegram, discord, cli, …
parent_session_id Self-referential FK for compression handoff chains
model, billing_provider, billing_base_url Model/billing config at session creation time
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens Token consumption counters
estimated_cost_usd, actual_cost_usd Cost tracking (estimated vs. confirmed)
title Auto-generated or user-edited session title
handoff_state, handoff_platform Cross-platform handoff support
Column Purpose
id AUTOINCREMENT primary key
session_id FK → sessions.id
role user, assistant, tool, system
content Message text (indexed by FTS5)
tool_calls, tool_call_id, tool_name Tool invocation metadata
reasoning, reasoning_content, reasoning_details Thinking/reasoning output
platform_message_id Cross-platform message correlation (e.g. Slack TS)
observed Flag for deduplication

The scheduled daily backup (backup.sh, see backup) deliberately excludes state.db — too large for the GitHub-hosted backup repo. To close that gap, a lightweight daily state.db snapshot now runs at 02:00 UTC (state_db_backup.sh, cron “Hermes state.db Daily Snapshot”, no_agent): WAL-safe sqlite3 .backup → gzip → ~/.hermes/state-db-snapshots/state.db-YYYYMMDD.gz, local-only (not git), 7-day rotation. Every incident can now fall back to at worst a yesterday state.

Sources of state.db recovery, with what they restore and typical freshness:

Source Restores Typical freshness Access required
Daily snapshot (~/.hermes/state-db-snapshots/) full state.db yesterday at worst local file only
Pre-update snapshot (~/.hermes/state-snapshots/<ts>/) full state.db only at each hermes update — can be weeks/months stale local file only
Manual copies (state.db.bak-*, state.db.malformed-backup-*) full state.db when someone made a copy before a repair local file only
hermes backup zip (manual) full state.db (via _safe_copy_db sqlite backup) only when run manually local file only
backup.sh daily repo (hermes-backup) does NOT include state.db; restores SOUL/CONTEXT/memory/config/skills/cron/scripts/plugins daily git repo
backup.sh repo secrets (.env, auth.json) excluded by design (Bitwarden) — a full restore needs manual secret recovery — Bitwarden access

Reading the signals: a state.db.bak-* / state.db.malformed-backup-* file never appears on its own — it is only created when someone starts repairing state.db. Seeing one means a problem has begun: run hermes doctor and check the store. backup.sh and the daily snapshot are complementary: the first protects the irreplaceable (SOUL, memory, skills, cron, config, profiles) but excludes state.db; the second protects full session history. A complete restore needs both (plus secrets from Bitwarden).

Residual risk: the daily snapshots live on the same disk as state.db — they protect against corruption, not against losing the VPS disk. Off-host shipping of the snapshot is not sized for the GitHub repo (~56 MB/day gz) and remains a documented future option; backup.sh is the only off-host copy (excluding state.db by design).

Health monitoring: four read-only cron jobs cover different failure classes:

Job Frequency Detects Delivery
state.db Corruption & Write-Failure Watchdog every 5 min FTS deferrals/corruption and file is not a database write failures in the journal/log Slack on new deduplicated evidence
state.db Health Watchdog hourly full integrity_check + foreign_key_check on the main and profile databases; deleted state.db/WAL/SHM descriptors local cron output only; no routine Slack message
state.db Snapshot Freshness Watchdog every 6 h missing or older-than-48h daily snapshot Slack on anomaly
Hermes state.db Daily Snapshot daily at 02:00 UTC validates the staged snapshot before atomic publication Slack only when the no-agent job emits a failure

The freshness watchdog (~/.hermes/scripts/state_db_snapshot_freshness.sh) is deliberately separate from snapshot validity: a fresh file can still be invalid, while a dead snapshot job can otherwise remain silent. The full health watchdog (~/.hermes/scripts/state_db_health_watchdog.py) is read-only and never repairs or restarts services. The scripts and current schedules are documented in the state.db recurrence postmortem. If the watchdog name differs in an existing installation, verify the live cron definition before recreating it; the script path and behavior are the source of truth.

Verifying backup includes state.db:

Terminal window
# List files in the latest full backup
unzip -l ~/.hermes/backups/hermes-backup-*.zip | grep state.db
# Check latest quick snapshot
ls ~/.hermes/state-snapshots/*/state.db
# Check daily state.db snapshot
ls -lh ~/.hermes/state-db-snapshots/

The file ~/.hermes/sessions.db exists as a 0-byte file. It is a legacy placeholder — all session data has been stored in state.db since the hermes_state.py SQLite store replaced the per-session JSONL approach. The backup system targets state.db explicitly and does not need sessions.db.