-
Notifications
You must be signed in to change notification settings - Fork 3.6k
09 Memory System
Relevant source files
The following files were used as context for generating this wiki page:
This page documents ZeroClaw's memory system — the persistent storage layer that allows agents to recall and save information across conversations. The Memory trait provides a pluggable architecture where different storage backends can be swapped through configuration.
For detailed comparison of specific backend implementations, see Memory Backends. For the technical details of hybrid search (vector + keyword), see Hybrid Search. For the export/import system that preserves core memories in Git-visible Markdown, see Memory Snapshot.
The memory system is a trait-driven abstraction that decouples the agent's recall/storage logic from the underlying persistence mechanism. Every backend implements the same Memory trait, allowing zero-code-change swaps between SQLite, PostgreSQL, Lucid, Markdown files, or a no-op backend.
graph TB
Agent["Agent Core<br/>loop_.rs"]
MemoryTrait["Memory Trait<br/>src/memory/traits.rs"]
subgraph "Backend Implementations"
SQLite["SqliteMemory<br/>Full hybrid search<br/>brain.db"]
Postgres["PostgresMemory<br/>Remote storage"]
Lucid["LucidMemory<br/>Lucid CLI bridge"]
Markdown["MarkdownMemory<br/>workspace/*.md"]
None["NoneMemory<br/>No persistence"]
end
subgraph "SQLite Subsystems"
FTS5["FTS5 Virtual Table<br/>memories_fts<br/>BM25 keyword search"]
VectorDB["Embedding BLOB<br/>memories.embedding<br/>Cosine similarity"]
Cache["Embedding Cache<br/>embedding_cache<br/>LRU eviction"]
Snapshot["Memory Snapshot<br/>MEMORY_SNAPSHOT.md<br/>Git-visible soul"]
end
Agent -->|"recall(query)<br/>store(key, content)"| MemoryTrait
MemoryTrait --> SQLite
MemoryTrait --> Postgres
MemoryTrait --> Lucid
MemoryTrait --> Markdown
MemoryTrait --> None
SQLite --> FTS5
SQLite --> VectorDB
SQLite --> Cache
SQLite --> Snapshot
FTS5 -.->|"Hybrid merge<br/>weighted scoring"| Agent
VectorDB -.->|"Hybrid merge<br/>weighted scoring"| Agent
Key Design Principles:
-
Zero External Dependencies: No Pinecone, no Elasticsearch, no LangChain. The SQLite backend implements a full-stack search engine (vector DB + keyword search + hybrid merge) in pure Rust with only rusqlite as a dependency.
-
Trait-Driven Pluggability:
backend = "sqlite"in config.toml instantly switches to a different implementation. No code changes, no recompilation. -
Automatic Context Enrichment: The agent automatically calls
recall()before each LLM request to inject relevant historical context into the prompt. -
Atomic Soul Export: Core memories are continuously exported to
MEMORY_SNAPSHOT.mdso the agent's "soul" is always Git-visible and survives database loss.
Sources: README.md:330-343, src/memory/snapshot.rs:1-8
The Memory trait defines five core operations that all backends must implement:
| Method | Purpose | Example |
|---|---|---|
recall(query, limit) |
Semantic search for relevant memories | Agent recalls context before LLM request |
store(key, content, category) |
Persist a new memory or update existing | Agent stores user preferences, facts |
forget(key) |
Delete a memory by key | User requests data deletion |
list_keys() |
Enumerate all stored keys | Debugging, memory management tools |
get(key) |
Retrieve exact memory by key | Direct lookup for known keys |
Trait Contract:
#[async_trait]
pub trait Memory: Send + Sync {
async fn recall(&self, query: &str, limit: usize) -> Result<Vec<MemoryEntry>>;
async fn store(&self, key: &str, content: &str, category: MemoryCategory) -> Result<()>;
async fn forget(&self, key: &str) -> Result<bool>;
async fn list_keys(&self) -> Result<Vec<String>>;
async fn get(&self, key: &str) -> Result<Option<MemoryEntry>>;
}The MemoryEntry struct contains:
-
key: Unique identifier (e.g.,"user_preference_lang","fact_rust_memory_safety") -
content: The actual memory text (plain text or markdown) -
category: Enum ofCore,Conversation,Fact,UserPreference,Other -
similarity_score: Optional relevance score from hybrid search (0.0-1.0)
Sources: Referenced from architecture diagrams and README.md memory configuration
Backends are selected via [memory] section in config.toml:
[memory]
backend = "sqlite" # "sqlite", "postgres", "lucid", "markdown", "none"
auto_save = true # Agent automatically stores important facts
embedding_provider = "none" # "none", "openai", "custom:https://..."
vector_weight = 0.7 # Hybrid search: vector importance
keyword_weight = 0.3 # Hybrid search: keyword importanceBackend Dispatch Logic:
flowchart TB
Start["Config::load()"]
ReadBackend["Read memory.backend"]
Decision{"backend value?"}
CreateSQLite["SqliteMemory::new()<br/>Path: workspace/memory/brain.db"]
CreatePostgres["PostgresMemory::new()<br/>storage.provider.config.db_url"]
CreateLucid["LucidMemory::new()<br/>Spawns lucid CLI subprocess"]
CreateMarkdown["MarkdownMemory::new()<br/>Reads workspace/*.md"]
CreateNone["NoneMemory::new()<br/>No-op stub"]
CheckHydrate{"brain.db missing<br/>but MEMORY_SNAPSHOT.md<br/>exists?"}
Hydrate["hydrate_from_snapshot()<br/>Import from Markdown"]
Start --> ReadBackend
ReadBackend --> Decision
Decision -->|"'sqlite'"| CheckHydrate
Decision -->|"'postgres'"| CreatePostgres
Decision -->|"'lucid'"| CreateLucid
Decision -->|"'markdown'"| CreateMarkdown
Decision -->|"'none'"| CreateNone
CheckHydrate -->|Yes| Hydrate
CheckHydrate -->|No| CreateSQLite
Hydrate --> CreateSQLite
For PostgreSQL remote storage, add:
[storage.provider.config]
provider = "postgres"
db_url = "postgres://user:password@host:5432/zeroclaw"
schema = "public"
table = "memories"
connect_timeout_secs = 15Sources: README.md:346-376
The SQLite backend implements a dual-index hybrid search system that combines vector similarity (semantic) with keyword matching (lexical) for optimal recall:
graph TB
Query["User Query:<br/>'What did I say about Rust?'"]
subgraph "Dual Search Paths"
VectorPath["Vector Search Path"]
KeywordPath["Keyword Search Path"]
end
subgraph "Vector Pipeline"
Embed["Embed query via<br/>EmbeddingProvider"]
CheckCache["Check embedding_cache<br/>for cached embeddings"]
Cosine["Cosine similarity<br/>against memories.embedding"]
end
subgraph "Keyword Pipeline"
FTS["FTS5 query on<br/>memories_fts"]
BM25["BM25 scoring"]
end
Merge["Weighted Merge<br/>vector_weight * vec_score +<br/>keyword_weight * kw_score"]
Sort["Sort by combined score"]
Return["Return top N entries"]
Query --> VectorPath
Query --> KeywordPath
VectorPath --> Embed
Embed --> CheckCache
CheckCache --> Cosine
KeywordPath --> FTS
FTS --> BM25
Cosine --> Merge
BM25 --> Merge
Merge --> Sort
Sort --> Return
Why Hybrid?
-
Vector search handles synonyms and semantic similarity (
"Rust memory safety"matches"zero-cost abstractions") -
Keyword search handles exact terms and rare words (
"tokio::spawn"matches code snippets) - Weighted merge balances both approaches (default: 70% vector, 30% keyword)
The embedding cache prevents redundant API calls for repeated queries — cached embeddings are stored as (content_hash, embedding, created_at) with LRU eviction.
Sources: README.md:330-343, src/memory/snapshot.rs:117-140
Memories are tagged with categories to organize information and control export behavior:
| Category | Purpose | Exported to Snapshot? |
|---|---|---|
Core |
Agent identity, core values, user instructions | ✅ Yes |
UserPreference |
User preferences (language, style, tools) | ✅ Yes |
Fact |
Long-term factual knowledge | ✅ Yes |
Conversation |
Dialogue history, ephemeral context | ❌ No |
Other |
Uncategorized memories | ❌ No |
Only Core category memories are exported to MEMORY_SNAPSHOT.md (the agent's "soul"). This ensures Git commits remain small and focused on identity/preferences rather than full conversation logs.
graph LR
Agent["Agent stores memory<br/>with category tag"]
Core["Core:<br/>Identity, values, rules<br/>→ MEMORY_SNAPSHOT.md"]
UserPref["UserPreference:<br/>Language, style<br/>→ MEMORY_SNAPSHOT.md"]
Fact["Fact:<br/>Learned knowledge<br/>→ MEMORY_SNAPSHOT.md"]
Conv["Conversation:<br/>Ephemeral dialogue<br/>→ brain.db only"]
Other["Other:<br/>Uncategorized<br/>→ brain.db only"]
Agent --> Core
Agent --> UserPref
Agent --> Fact
Agent --> Conv
Agent --> Other
Sources: src/memory/snapshot.rs:37-56
When auto_save = true, the agent automatically integrates memory into the turn cycle:
sequenceDiagram
participant User
participant Agent as Agent Core
participant Memory as Memory Backend
participant LLM as Provider
User->>Agent: Send message
Agent->>Memory: recall(user_message, limit=5)
Memory-->>Agent: [Relevant memories with scores]
Agent->>Agent: Build system prompt<br/>+ inject recalled context
Agent->>LLM: chat(messages, tools)
LLM-->>Agent: Response + tool calls
opt Auto-save important facts
Agent->>Memory: store(key, content, category)
Memory-->>Agent: OK
end
Agent->>Memory: Auto-export snapshot<br/>if Core memories changed
Memory-->>Agent: Exported N entries
Agent-->>User: Final response
Recall Timing: Before every LLM request, the agent calls recall() with the user's message as the query. The top-N relevant memories are injected into the system prompt as context.
Store Timing: The agent uses memory tools (memory_store, memory_recall, memory_forget) to explicitly manage memories. Additionally, the backend may auto-save certain categories (configured per-backend).
Sources: Referenced from agent workflow in high-level diagrams
The snapshot system provides disaster recovery and Git visibility for core memories:
flowchart TB
Start["Agent starts"]
CheckDB{"brain.db exists<br/>and populated?"}
CheckSnapshot{"MEMORY_SNAPSHOT.md<br/>exists?"}
Hydrate["hydrate_from_snapshot()<br/>Parse Markdown → SQLite"]
NormalBoot["Normal boot<br/>Use existing brain.db"]
Export["On shutdown or<br/>periodic timer"]
ExportSnapshot["export_snapshot()<br/>SELECT * WHERE category='core'<br/>→ MEMORY_SNAPSHOT.md"]
Start --> CheckDB
CheckDB -->|No| CheckSnapshot
CheckDB -->|Yes| NormalBoot
CheckSnapshot -->|Yes| Hydrate
CheckSnapshot -->|No| NormalBoot
Hydrate --> NormalBoot
NormalBoot --> Export
Export --> ExportSnapshot
File Format:
# 🧠 ZeroClaw Memory Snapshot
> Auto-generated. Do not edit manually unless you know what you're doing.
> This file is the "soul" of your agent.
**Last exported:** 2025-01-15 14:30:00
**Total core memories:** 3
---
### 🔑 `identity`
I am ZeroClaw, a self-preserving AI agent built with Rust.
*Created: 2025-01-15 | Updated: 2025-01-15*
---
### 🔑 `user_preference_lang`
The user prefers Rust for systems programming.
*Created: 2025-01-14 | Updated: 2025-01-15*
---Auto-Hydration: If brain.db is missing or empty but MEMORY_SNAPSHOT.md exists, the agent automatically re-indexes all entries back into a fresh SQLite database on startup. This "atomic soul recovery" ensures the agent never loses its identity.
See Memory Snapshot for implementation details.
Sources: src/memory/snapshot.rs:1-90, src/memory/snapshot.rs:180-199
Vector search requires embeddings (dense numerical representations of text). ZeroClaw supports:
| Provider | Configuration | Use Case |
|---|---|---|
none |
embedding_provider = "none" |
No vector search (keyword-only) |
openai |
embedding_provider = "openai"OPENAI_API_KEY
|
Production deployments |
custom |
embedding_provider = "custom:https://..." |
Self-hosted embedding models |
Embedding Cache:
The SQLite backend includes a transparent cache layer:
CREATE TABLE IF NOT EXISTS embedding_cache (
content_hash TEXT PRIMARY KEY,
embedding BLOB NOT NULL,
created_at TEXT NOT NULL
);When the same query is recalled multiple times, the embedding is fetched from the cache rather than calling the API again. This dramatically reduces latency and API costs for repeated searches.
Sources: README.md:346-376, src/memory/snapshot.rs:136-139
| Feature | SQLite | PostgreSQL | Lucid | Markdown | None |
|---|---|---|---|---|---|
| Hybrid Search | ✅ Full | ❌ No | ✅ Via CLI | ❌ No | ❌ No |
| Vector DB | ✅ Embedded | ✅ pgvector | ✅ External | ❌ No | ❌ No |
| Keyword Search | ✅ FTS5 | ✅ SQL LIKE | ✅ External | ✅ grep | ❌ No |
| Multi-User | ❌ Single | ✅ Yes | ✅ Yes | ❌ Single | ❌ No |
| Git-Friendly | ❌ Remote | ❌ Remote | ✅ Text | ✅ N/A | |
| Snapshot Export | ✅ Yes | ❌ No | ❌ No | ✅ Native | ❌ N/A |
| Setup Complexity | Low | Medium | High | Low | None |
For detailed analysis and usage guidelines for each backend, see Memory Backends.
Sources: README.md:330-376
[memory]
backend = "sqlite"
auto_save = true
embedding_provider = "openai"
vector_weight = 0.7
keyword_weight = 0.3
sqlite_open_timeout_secs = 30 # Optional: handle locked database[memory]
backend = "postgres"
auto_save = true
embedding_provider = "openai"
[storage.provider.config]
provider = "postgres"
db_url = "postgres://user:password@host:5432/zeroclaw"
schema = "public"
table = "memories"
connect_timeout_secs = 15[memory]
backend = "lucid"
auto_save = true
# Optional environment variables:
# ZEROCLAW_LUCID_CMD=/usr/local/bin/lucid
# ZEROCLAW_LUCID_BUDGET=200
# ZEROCLAW_LUCID_RECALL_TIMEOUT_MS=120
# ZEROCLAW_LUCID_STORE_TIMEOUT_MS=800[memory]
backend = "markdown"
auto_save = true
# Reads/writes workspace/*.md files directly[memory]
backend = "none"
auto_save = false
# All recall() calls return empty results
# All store() calls are no-opsSources: README.md:346-376
Next Steps:
- Memory Backends — Detailed comparison and usage guidelines for each backend
- Hybrid Search — Deep dive into vector + keyword merge algorithm
- Memory Snapshot — Export/import system implementation details