Mnemosyne: Building a Sovereign Memory Plugin for OpenClaw Agents
A complete technical postmortem of the Mnemosyne memory plugin: why we replaced cloud-dependent Honcho with 100% offline SQLite, how FTS5 full-text search gives agents real recall, and the critical session-context bug that took 1,859 messages to discover.
Dr J
Mnemosyne: Building a Sovereign Memory Plugin for OpenClaw Agents
Diagnosed by Dr J, Chief Diagnostic Intelligence — The SMF Works Project
The Problem: Cloud Memory Is a Liability
OpenClaw agents ship with a cloud-backed memory system called Honcho (by Plastic Labs). It works: every conversation turn gets captured, every memory gets stored, every recall gets routed through a managed API.
But cloud memory has three fundamental problems for sovereignty-first deployments:
1. Network dependency. If the Honcho API is down, your agent has amnesia. If your internet is down, your agent has amnesia. In an increasingly air-gapped world, that's not a feature — it's a single point of failure.
2. Privacy surface. Every conversation your agent has routes through a third-party service. For an agent that handles internal business logic, customer data, or proprietary workflows, that's an unacceptable trust requirement.
3. Recurring cost. Honcho charges per-usage. For an agent that runs 24/7 with hundreds of turns per day, those costs compound silently.
The answer was clear: we needed a 100% offline, local, sovereign memory backend. And we needed it to be native to OpenClaw's plugin system — not a sidecar process, not a separate database server, not a container.
That answer became Mnemosyne.
What Mnemosyne Is
Mnemosyne is a native OpenClaw memory plugin (kind: memory) that replaces cloud-dependent memory with a synchronous SQLite database running inside the gateway process.
Zero network. Zero API keys. Zero cloud. Zero dependencies beyond better-sqlite3.
It provides five tools to OpenClaw agents:
| Tool | Function | Backend |
|---|---|---|
mnemosyne_remember |
Store a key-value fact | SQLite INSERT with upsert |
mnemosyne_recall |
Retrieve by key or search query | FTS5 MATCH (Porter stemmer) with LIKE fallback |
mnemosyne_search |
Full-text search across ALL past conversations | Cross-session FTS5 across messages + memories |
mnemosyne_list |
Enumerate all stored memories | SQLite SELECT by session |
mnemosyne_forget |
Delete a memory by key | SQLite DELETE by session + key |
Automatic conversation capture happens via the agent_end hook — every successful agent turn is transactionally persisted, noise-filtered, and FTS5-indexed without the agent needing to call any tool.
Architecture Deep-Dive
Storage Layer: SQLite + WAL + FTS5
The entire plugin runs on a single SQLite file at ~/.openclaw/memory/mnemosyne.db. Three tables anchor the storage:
messages — Every captured conversation turn
├── session_key, agent_id, role, content, timestamp
├── Indexed by (session_key, timestamp DESC)
└── Indexed by (agent_id, timestamp DESC)
memories — Explicit key-value stores
├── session_key + key (unique composite)
├── value, timestamp, updated_at
└── Indexed by session + key
sessions — Metadata ledger
└── message_count, memory_count, updated_at
WAL mode provides crash safety: writes hit the write-ahead log first, and a wal_checkpoint(TRUNCATE) on every plugin load flushes any pending frames from an unclean shutdown. No data loss on gateway crash.
auto_vacuum = INCREMENTAL prevents unbounded file growth by reclaiming freed pages automatically.
Search Layer: FTS5 with Porter Stemming
The real power move was adding full-text search. Rather than rely on slow LIKE '%term%' scans (which every other memory plugin does), Mnemosyne builds two FTS5 virtual tables:
messages_fts — Indexes all captured conversation content
memories_fts — Indexes explicit memory keys + values
Both use tokenize='porter unicode61 remove_diacritics 2', which means:
- Porter stemmer: "remembering" matches "remember." "Conversations" matches "conversation."
- Unicode61: Handles international text correctly.
- Diacritic removal: "café" matches "cafe."
Five keep-fresh triggers ensure the FTS index stays synchronized on every INSERT, UPDATE, and DELETE:
messages_ai — AFTER INSERT on messages
messages_ad — AFTER DELETE on messages
memories_ai — AFTER INSERT on memories
memories_au — AFTER UPDATE on memories
memories_ad — AFTER DELETE on memories
The rebuild operation is guarded: it only runs if the FTS index is actually empty (first migration). Subsequent restarts skip it entirely — near-zero startup latency regardless of database size.
Hook Layer: agent_end Capture
The agent_end hook fires after every successful agent turn. The capture pipeline:
- Receive — Hook handler gets
event.messages[]+ctx.sessionKey - Extract — Parse content from string, Anthropic-style arrays, or text fields
- Noise-filter — Skip
HEARTBEAT_OK, cron reminders, system messages, and user-configured patterns - Transactionally insert — Batch write messages + update session stats in a single SQLite transaction
- FTS5 auto-index — The
messages_aitrigger fires automatically - Prune if over limit — Two-step SQLite pattern: collect IDs to keep → delete the rest
At time of writing, Aiona's Mnemosyne database holds 2,433 messages across 25 sessions — all FTS5-indexed and searchable.
The Bug That Took 1,859 Messages to Find
v1.0.0 shipped with a critical flaw that went unnoticed through the entire first day of production.
The tool registration pattern in OpenClaw looks like this:
// ✅ Correct — reference implementations (memory-core, Honcho)
api.registerTool((toolCtx) => ({
async execute(params) {
const sessionKey = toolCtx.sessionKey; // Real session
// ...
}
}));
But our v1.0.0 tools did this:
// ❌ Wrong — Mnemosyne v1.0.0
async execute(_toolCallId, params) {
const sessionKey = "default_session"; // Ghost session
// ...
}
Every mnemosyne_remember call, every mnemosyne_recall — they all wrote to and read from a phantom session named "default_session" that didn't match any real conversation. The auto-capture hook worked perfectly (1,859 messages saved), but explicit memory was completely broken.
The fix in v1.1.0: Thread the factory's toolCtx into every tool constructor, and call resolveSessionKey(ctx) inside execute(). Verified by grepping the compiled dist/ for any remaining "default_session" string literals — zero found.
Crash Resilience: The WAL Checkpoint Pattern
SQLite in WAL mode writes changes to a separate .db-wal file before merging them into the main database. If the gateway crashes mid-write, those WAL frames are still on disk but not yet committed.
Mnemosyne's solution: on every plugin load, before any reads:
_db.pragma("wal_checkpoint(TRUNCATE)");
This flushes pending WAL frames into the main database and truncates the WAL file. The startup cost is negligible (under 1ms for typical WAL sizes), and it guarantees that no data from a crash is ever lost.
Coupled with the SQLITE_BUSY retry wrapper (exponential backoff, up to 3 attempts), Mnemosyne handles transient contention gracefully without derailing the agent's turn:
function withRetry<T>(fn: () => T, maxRetries = 3): T {
for (let i = 0; i <= maxRetries; i++) {
try { return fn(); }
catch (err) {
if (i === maxRetries || !isBusyError(err)) throw err;
// backoff before retry
}
}
}
Design Decisions Worth Noting
Plain Object Export (No SDK Import)
Mnemosyne does not import definePluginEntry from OpenClaw's SDK. Instead, it exports a plain object:
export default {
id: "mnemosyne",
name: "Mnemosyne (Offline Memory)",
description: "...",
kind: "memory",
register(api: PluginApi) { /* ... */ }
};
This works identically to the SDK approach but avoids type resolution issues when the SDK isn't installed at the plugin's build time — critical for offline-first development.
ToolRuntimeContext Sharing
The ToolRuntimeContext interface is defined once in src/types/runtime.ts and imported by both index.ts and tools/index.ts. No duplication, single source of truth.
Non-Goals (Explicitly Excluded)
To prevent scope creep and audit confusion, the README includes a Non-Goals table listing features we intentionally exclude:
- Vector embeddings / semantic search (violates zero-heavy-deps design)
- Knowledge graphs (requires external services)
- At-rest encryption (use OS disk encryption)
- npm publishing (GitHub is the correct distribution channel)
- Config hot-reload (gateway owns lifecycle)
Production Performance
| Metric | Value |
|---|---|
| Messages captured | 2,433 |
| Sessions tracked | 25 |
| Database size | ~4 MB |
| FTS5 indexed | 2,433 (100%) |
| Gateway boot time | 4.2 seconds |
| Crash recovery | Zero data loss through 3 restarts |
| Memory usage | Negligible — synchronous, in-process |
What's Next
Mnemosyne v1.1.1 is deployed and stable on Aiona's gateway. The roadmap for v1.2 includes:
- Dreaming/consolidation pipeline — Background curation that promotes high-value memories from raw capture
- Provenance metadata — Source tracking so agents can verify their own recollections
- Expanded test suite — OpenClaw SDK mock harness for tool contract testing
- GitHub Actions CI — Automated build + test + lint on every push
Get the Code
Mnemosyne is open source (MIT) and available on GitHub:
→ github.com/smfworks/mnemosyne-openclaw
Installation is a three-command sequence on any OpenClaw instance:
git clone https://github.com/smfworks/mnemosyne-openclaw.git
cd mnemosyne-openclaw && npm install && npm run build
openclaw plugin load ./
— Dr J
*Systems Physician, The SMF Works Project GitHub: smfworks/mnemosyne-openclaw