-
Notifications
You must be signed in to change notification settings - Fork 4.4k
09.1 Memory Backends
Relevant source files
The following files were used as context for generating this wiki page:
This page documents the pluggable memory backend system in ZeroClaw. It covers the available backend implementations (SQLite, PostgreSQL, Lucid, Markdown, None), their configuration options, and guidelines for selecting the appropriate backend for your deployment.
For information about hybrid search architecture and vector/keyword retrieval, see Hybrid Search. For memory snapshot and export functionality, see Memory Snapshot.
ZeroClaw implements memory as a trait-based pluggable system. The Memory trait defines the interface for storing, recalling, and managing conversational context. Different backend implementations provide varying trade-offs between performance, features, deployment complexity, and resource usage.
Key Concepts:
- Backend: The underlying storage mechanism (SQLite, PostgreSQL, Lucid, Markdown, or None)
- Auto-save: Automatic persistence of conversation turns to memory
- Hybrid Search: Combined vector similarity + keyword search (SQLite only)
- Hygiene: Automatic archival and purging of old memory entries (SQLite only)
The memory system is initialized during configuration loading and injected into the agent/channel runtime context as Arc<dyn Memory>.
Sources: src/channels/mod.rs:36, src/config/schema.rs:99-103
graph TB
subgraph "Memory Trait Interface"
MemoryTrait["Memory trait<br/>(store, recall, forget, list)"]
end
subgraph "Backend Implementations"
SQLiteBackend["SqliteMemory<br/>(hybrid search, hygiene,<br/>embedding cache)"]
PostgresBackend["PostgresMemory<br/>(remote storage)"]
LucidBackend["LucidMemory<br/>(external CLI bridge)"]
MarkdownBackend["MarkdownMemory<br/>(file-based)"]
NoneBackend["NoopMemory<br/>(no persistence)"]
end
subgraph "Runtime Context"
ChannelContext["ChannelRuntimeContext"]
AgentLoop["Agent Turn Loop"]
end
subgraph "Configuration"
MemoryConfig["[memory] in config.toml<br/>backend = 'sqlite' | 'postgres' | 'lucid' | 'markdown' | 'none'"]
StorageConfig["[storage.provider.config]<br/>(PostgreSQL connection)"]
end
MemoryConfig --> MemoryTrait
StorageConfig --> PostgresBackend
MemoryTrait --> SQLiteBackend
MemoryTrait --> PostgresBackend
MemoryTrait --> LucidBackend
MemoryTrait --> MarkdownBackend
MemoryTrait --> NoneBackend
SQLiteBackend --> ChannelContext
PostgresBackend --> ChannelContext
LucidBackend --> ChannelContext
MarkdownBackend --> ChannelContext
NoneBackend --> ChannelContext
ChannelContext --> AgentLoop
AgentLoop --> |"recall(query)"| ChannelContext
AgentLoop --> |"store(key, content)"| ChannelContext
Diagram: Memory Backend Architecture
The Memory trait provides a uniform interface. Configuration determines which concrete implementation is instantiated at runtime. The agent and channel systems interact only with the trait interface, enabling zero-code backend swapping.
Sources: src/channels/mod.rs:36, src/channels/mod.rs:106, src/config/schema.rs:99-103
Backend Key: sqlite
The default and most feature-complete backend. Provides full-stack search capabilities with no external dependencies.
Features:
- Hybrid search: Vector (cosine similarity) + keyword (FTS5 BM25) search
- Embedding cache: Caches embeddings with LRU eviction to reduce API calls
-
Hygiene: Automatic archival (
archive_after_days) and purging (purge_after_days) - Response cache: Optional caching of LLM responses with TTL
- Snapshot support: Export/import memory state for backup and recovery
- Transactional: ACID guarantees for all operations
Storage Location: ~/.zeroclaw/workspace/memory.db
Use Cases:
- Default choice for most deployments
- Single-node agents where local disk persistence is acceptable
- Scenarios requiring full-text and vector search without external services
Configuration Example:
[memory]
backend = "sqlite"
auto_save = true
hygiene_enabled = true
archive_after_days = 7
purge_after_days = 30
embedding_provider = "openai" # or "none", "custom:https://..."
embedding_model = "text-embedding-3-small"
embedding_dimensions = 1536
vector_weight = 0.7
keyword_weight = 0.3
embedding_cache_size = 10000
response_cache_enabled = false
response_cache_ttl_minutes = 60
sqlite_open_timeout_secs = 30 # optional timeout for locked DBSources: src/onboard/wizard.rs:263-298, src/config/schema.rs:99-103
Backend Key: postgres
Remote database backend for multi-agent deployments requiring shared memory state.
Features:
- Remote storage: Centralized memory across multiple agent instances
- Schema isolation: Configurable schema and table names
- Connection pooling: Built-in connection management
- JSONB storage: Native PostgreSQL JSON support for flexible memory entries
Limitations:
- No built-in vector search (requires PostgreSQL extensions like pgvector)
- No FTS5-style keyword search (requires PostgreSQL extensions like pg_trgm)
- Hygiene and response caching are not implemented
Use Cases:
- Multi-agent systems with shared memory requirements
- Cloud deployments where local disk is ephemeral
- Scenarios requiring centralized audit/compliance logs
Configuration Example:
[memory]
backend = "postgres"
auto_save = true
[storage.provider.config]
provider = "postgres"
db_url = "postgres://user:password@host:5432/zeroclaw"
schema = "public"
table = "memories"
connect_timeout_secs = 15Aliases: The db_url key also accepts dbURL for backward compatibility.
Sources: src/config/schema.rs:99-103, README.md:355-365
Backend Key: lucid
Bridge to an external lucid CLI binary for memory operations. Designed for integration with Lucid AI's memory service.
Features:
-
External CLI bridge: Delegates memory operations to
lucidcommand - Local hit threshold: Caches frequently accessed keys locally before querying external service
- Timeout controls: Separate timeouts for recall (low-latency) and store (async sync)
- Failure cooldown: Backs off after failures to avoid repeated slow attempts
Limitations:
- Requires
lucidbinary in PATH (or set viaZEROCLAW_LUCID_CMD) - Network/CLI latency overhead
- No built-in vector search or hygiene
Use Cases:
- Integration with Lucid AI's memory infrastructure
- Scenarios requiring external memory governance or compliance
- Hybrid deployments where Lucid manages long-term memory
Configuration Example:
[memory]
backend = "lucid"
auto_save = trueEnvironment Variables:
| Variable | Default | Description |
|---|---|---|
ZEROCLAW_LUCID_CMD |
lucid |
Path to lucid binary |
ZEROCLAW_LUCID_BUDGET |
200 |
Token budget for context recall |
ZEROCLAW_LUCID_LOCAL_HIT_THRESHOLD |
3 |
Local hit count to skip external recall |
ZEROCLAW_LUCID_RECALL_TIMEOUT_MS |
120 |
Low-latency recall timeout |
ZEROCLAW_LUCID_STORE_TIMEOUT_MS |
800 |
Async store timeout |
ZEROCLAW_LUCID_FAILURE_COOLDOWN_MS |
15000 |
Cooldown after failure |
Sources: README.md:370-377
Backend Key: markdown
File-based backend storing each memory entry as a separate .md file in the workspace.
Features:
-
Human-readable: Plain markdown files in
~/.zeroclaw/workspace/memory/ - Git-friendly: Easy to version control and inspect
- No dependencies: No database setup required
Limitations:
- No search capabilities (recall returns empty results)
- No hygiene or caching
- Poor performance at scale (O(n) file scans)
- No transactional guarantees
Use Cases:
- Debugging and inspection workflows
- Simple single-agent setups where search is not required
- Scenarios prioritizing auditability over performance
Configuration Example:
[memory]
backend = "markdown"
auto_save = trueStorage Location: ~/.zeroclaw/workspace/memory/*.md
Sources: src/onboard/wizard.rs:263-298
Backend Key: none
Explicit no-op backend that disables memory persistence entirely.
Features:
- Zero overhead: No disk I/O or database operations
- Stateless: Each agent turn starts with empty context
- Deterministic: No side effects from prior conversations
Limitations:
- No memory persistence or recall
- Auto-save is ignored
Use Cases:
- Stateless API endpoints where context is passed explicitly
- Testing and benchmarking scenarios requiring deterministic behavior
- Privacy-sensitive deployments where conversation history must not persist
Configuration Example:
[memory]
backend = "none"
auto_save = false # ignored, but semantically correctSources: src/onboard/wizard.rs:263-298, README.md:354-355
| Backend | Search | Hygiene | Remote | Setup Complexity | Performance | Use Case |
|---|---|---|---|---|---|---|
| SQLite | Hybrid (vector + keyword) | Yes | No | Low (bundled) | High (local) | Default for most deployments |
| PostgreSQL | No (requires extensions) | No | Yes | Medium (DB setup) | Medium (network) | Multi-agent, shared memory |
| Lucid | External | No | Yes | Medium (CLI binary) | Medium (network) | Lucid AI integration |
| Markdown | No | No | No | Low (filesystem) | Low (O(n) scans) | Debugging, inspection |
| None | No | No | No | None | N/A (no-op) | Stateless APIs, testing |
Sources: src/onboard/wizard.rs:263-298
sequenceDiagram
participant User
participant Wizard as "Onboarding Wizard"
participant ConfigLoader as "Config::load_or_init"
participant MemoryFactory as "memory::create_backend"
participant Backend as "Concrete Backend<br/>(e.g., SqliteMemory)"
participant Runtime as "ChannelRuntimeContext"
User->>Wizard: zeroclaw onboard --interactive
Wizard->>Wizard: setup_memory() prompts user
Wizard->>ConfigLoader: Save memory.backend = "sqlite"
User->>Runtime: zeroclaw daemon
Runtime->>ConfigLoader: Config::load_or_init()
ConfigLoader->>MemoryFactory: create_backend(config.memory)
alt backend = "sqlite"
MemoryFactory->>Backend: SqliteMemory::new(workspace/memory.db)
Backend->>Backend: Initialize FTS5, embedding cache
else backend = "postgres"
MemoryFactory->>Backend: PostgresMemory::new(storage.provider.config)
Backend->>Backend: Connect to remote DB
else backend = "lucid"
MemoryFactory->>Backend: LucidMemory::new()
Backend->>Backend: Verify lucid CLI in PATH
else backend = "markdown"
MemoryFactory->>Backend: MarkdownMemory::new(workspace/memory/)
Backend->>Backend: Ensure directory exists
else backend = "none"
MemoryFactory->>Backend: NoopMemory::new()
end
MemoryFactory-->>Runtime: Arc<dyn Memory>
Runtime->>Runtime: Store in ChannelRuntimeContext
Diagram: Memory Backend Initialization Flow
The wizard collects user preferences for the memory backend. During runtime initialization, the configuration loader invokes the memory factory to instantiate the appropriate backend. The factory returns an Arc<dyn Memory> trait object, which is stored in the runtime context and shared across all agent/channel operations.
Sources: src/onboard/wizard.rs:94, src/channels/mod.rs:106
Before each LLM call, the agent recalls relevant context from memory to enrich the user's message.
Code Flow:
-
build_memory_context: src/channels/mod.rs:443-469
- Calls
memory.recall(user_msg, 5, None)to retrieve top 5 relevant entries - Filters entries by
min_relevance_scorethreshold - Formats results as
[Memory context]\n- key: content\n\n
- Calls
-
process_channel_message: src/channels/mod.rs:589
- Invokes
build_memory_contextwith user message - Prepends memory context to user message before LLM call
- Invokes
Example:
// [src/channels/mod.rs:443-469]
async fn build_memory_context(
mem: &dyn Memory,
user_msg: &str,
min_relevance_score: f64,
) -> String {
let mut context = String::new();
if let Ok(entries) = mem.recall(user_msg, 5, None).await {
let relevant: Vec<_> = entries
.iter()
.filter(|e| match e.score {
Some(score) => score >= min_relevance_score,
None => true,
})
.collect();
if !relevant.is_empty() {
context.push_str("[Memory context]\n");
for entry in &relevant {
let _ = writeln!(context, "- {}: {}", entry.key, entry.content);
}
context.push('\n');
}
}
context
}Sources: src/channels/mod.rs:443-469, src/channels/mod.rs:589
When auto_save = true, each channel message is automatically persisted to memory.
Code Flow:
-
process_channel_message: src/channels/mod.rs:591-602
- Checks
ctx.auto_save_memoryflag - Generates memory key:
conversation_memory_key(&msg)→{channel}_{sender}_{message_id} - Calls
memory.store(key, content, Conversation, None)
- Checks
Example:
// [src/channels/mod.rs:591-602]
if ctx.auto_save_memory {
let autosave_key = conversation_memory_key(&msg);
let _ = ctx
.memory
.store(
&autosave_key,
&msg.content,
crate::memory::MemoryCategory::Conversation,
None,
)
.await;
}Key Format: {channel}_{sender}_{message_id}
Sources: src/channels/mod.rs:591-602, src/channels/mod.rs:125-127
graph TB
subgraph "Agent Turn"
UserMsg["User Message"]
RecallCtx["Recall Context<br/>(build_memory_context)"]
LLMCall["LLM Call<br/>(enriched prompt)"]
AutoSave["Auto-Save<br/>(if enabled)"]
end
subgraph "SQLite Backend Behavior"
SQLiteRecall["FTS5 + Vector Search<br/>(hybrid merge)"]
SQLiteStore["Insert into memories table<br/>(trigger FTS5 + embedding)"]
SQLiteHygiene["Background hygiene<br/>(archive/purge old entries)"]
end
subgraph "PostgreSQL Backend Behavior"
PGRecall["SELECT with JSONB filter<br/>(no vector search)"]
PGStore["INSERT into remote table<br/>(JSONB payload)"]
end
subgraph "Lucid Backend Behavior"
LucidRecall["lucid recall --budget=200<br/>(CLI subprocess)"]
LucidStore["lucid store<br/>(async timeout)"]
LucidCooldown["Failure cooldown<br/>(15s after error)"]
end
subgraph "Markdown Backend Behavior"
MarkdownRecall["Return empty results<br/>(no search)"]
MarkdownStore["Write {key}.md file<br/>(append mode)"]
end
subgraph "None Backend Behavior"
NoneRecall["Return empty immediately"]
NoneStore["No-op (discard)"]
end
UserMsg --> RecallCtx
RecallCtx --> |"backend = 'sqlite'"| SQLiteRecall
RecallCtx --> |"backend = 'postgres'"| PGRecall
RecallCtx --> |"backend = 'lucid'"| LucidRecall
RecallCtx --> |"backend = 'markdown'"| MarkdownRecall
RecallCtx --> |"backend = 'none'"| NoneRecall
RecallCtx --> LLMCall
LLMCall --> AutoSave
AutoSave --> |"backend = 'sqlite'"| SQLiteStore
AutoSave --> |"backend = 'postgres'"| PGStore
AutoSave --> |"backend = 'lucid'"| LucidStore
AutoSave --> |"backend = 'markdown'"| MarkdownStore
AutoSave --> |"backend = 'none'"| NoneStore
SQLiteStore --> SQLiteHygiene
LucidStore --> LucidCooldown
Diagram: Backend-Specific Runtime Behavior
Each backend implements recall and store with different strategies. SQLite performs hybrid search and triggers hygiene. PostgreSQL uses JSONB queries. Lucid delegates to an external CLI. Markdown writes files. None is a complete no-op.
Sources: src/channels/mod.rs:443-469, src/channels/mod.rs:591-602
Choose SQLite if:
- You need vector + keyword hybrid search
- Your deployment is single-node (local disk persistence is acceptable)
- You want automatic hygiene (archival/purging)
- You require snapshot/export functionality
Choose PostgreSQL if:
- You need shared memory across multiple agent instances
- Your deployment is cloud-native with ephemeral local disk
- You have an existing PostgreSQL infrastructure
- You can add extensions for vector search (e.g., pgvector)
Choose Lucid if:
- You are integrating with Lucid AI's memory service
- You need external memory governance or compliance
- You want centralized memory management across multiple systems
Choose Markdown if:
- You need human-readable memory inspection
- Your use case is single-agent with low message volume
- You prioritize git versioning and auditability over search performance
- You are debugging memory issues and need file-level visibility
Choose None if:
- Your agent is stateless (context passed explicitly per request)
- You are testing deterministic behavior without side effects
- Privacy requirements prohibit conversation history persistence
- You are benchmarking and want to eliminate memory I/O overhead
Sources: src/onboard/wizard.rs:263-298, README.md:330-365
The wizard and quick setup use backend-specific defaults optimized for each backend's capabilities.
Function: src/onboard/wizard.rs:268-298
fn memory_config_defaults_for_backend(backend: &str) -> MemoryConfig {
let profile = memory_backend_profile(backend);
MemoryConfig {
backend: backend.to_string(),
auto_save: profile.auto_save_default,
hygiene_enabled: profile.uses_sqlite_hygiene,
archive_after_days: if profile.uses_sqlite_hygiene { 7 } else { 0 },
purge_after_days: if profile.uses_sqlite_hygiene { 30 } else { 0 },
conversation_retention_days: 30,
embedding_provider: "none".to_string(),
embedding_model: "text-embedding-3-small".to_string(),
embedding_dimensions: 1536,
vector_weight: 0.7,
keyword_weight: 0.3,
min_relevance_score: 0.4,
embedding_cache_size: if profile.uses_sqlite_hygiene { 10000 } else { 0 },
chunk_max_tokens: 512,
response_cache_enabled: false,
response_cache_ttl_minutes: 60,
response_cache_max_entries: 5_000,
snapshot_enabled: false,
snapshot_on_hygiene: false,
auto_hydrate: true,
sqlite_open_timeout_secs: None,
}
}Backend Profiles:
| Backend | auto_save |
hygiene_enabled |
embedding_cache_size |
|---|---|---|---|
sqlite |
true |
true |
10000 |
postgres |
true |
false |
0 |
lucid |
true |
false |
0 |
markdown |
true |
false |
0 |
none |
false |
false |
0 |
Sources: src/onboard/wizard.rs:268-298
PostgreSQL backend supports environment variable overrides for connection strings:
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
ZEROCLAW_DB_URL |
ZeroClaw-specific override |
ZEROCLAW_POSTGRES_URL |
PostgreSQL-specific override |
Priority: ZEROCLAW_POSTGRES_URL > ZEROCLAW_DB_URL > DATABASE_URL > config.toml
Sources: README.md:355-365
ZeroClaw's memory system provides five backend implementations, each optimized for different deployment scenarios:
- SQLite — Default choice with hybrid search, hygiene, and zero external dependencies
- PostgreSQL — Remote storage for multi-agent deployments requiring shared memory
- Lucid — External CLI bridge for integration with Lucid AI's memory service
- Markdown — File-based backend for human-readable inspection and git versioning
- None — Explicit no-op for stateless APIs and deterministic testing
All backends implement the Memory trait, enabling configuration-driven swapping with zero code changes. The runtime context stores Arc<dyn Memory>, providing uniform access across agent and channel subsystems.
Sources: src/channels/mod.rs:36, src/channels/mod.rs:106, src/config/schema.rs:99-103, src/onboard/wizard.rs:263-298