Memory System
How the agent remembers things across conversations.
Overview
OpenClaw's memory system gives the AI agent persistent, searchable knowledge that survives across sessions. The source of truth is always plain Markdown files in the workspace. These files are indexed, chunked, embedded, and searchable via hybrid vector + keyword search.
Workspace Files Index Layer Agent Interface
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ MEMORY.md │───────────────►│ SQLite DB │◄────────────►│ memory_search│
│ memory/*.md │ chunk + embed │ - files │ hybrid │ memory_get │
│ sessions/*. │───────────────►│ - chunks │ search │ │
│ jsonl │ │ - embeddings │ │ (tools given │
└──────────────┘ │ - FTS5 index │ │ to the LLM) │
└──────────────┘ └──────────────┘
File Convention
MEMORY.md (Workspace Root)
Curated long-term memory. The agent reads and writes this file for durable facts, decisions, and preferences.
- Only loaded in main/private sessions (not group chats)
- Acts as the "brain" of the agent for a given workspace
memory/*.md (Daily and Evergreen Files)
| Pattern | Purpose | Temporal Decay |
|---|---|---|
memory/YYYY-MM-DD.md |
Daily append-only logs (running context, notes) | Yes — exponential decay with 30-day half-life |
memory/topics.md |
Evergreen knowledge files (no date in name) | No decay |
Session Transcripts (~/.openclaw/agents/<agentId>/sessions/*.jsonl)
Past conversations are also indexed as a secondary memory source. The system converts JSONL message transcripts into searchable text with line-number mappings back to the original format.
Data Model
SQLite Schema (src/memory/memory-schema.ts)
-- Tracked files
CREATE TABLE files (
path TEXT PRIMARY KEY,
source TEXT, -- "memory" or "sessions"
hash TEXT,
mtime INTEGER,
size INTEGER
);
-- Text chunks with embeddings
CREATE TABLE chunks (
id TEXT PRIMARY KEY,
path TEXT,
source TEXT,
startLine INTEGER,
endLine INTEGER,
hash TEXT,
model TEXT,
text TEXT,
embedding TEXT, -- JSON array of floats
updated_at INTEGER
);
-- Embedding cache (cross-session reuse)
CREATE TABLE embedding_cache (
provider TEXT,
model TEXT,
key TEXT,
hash TEXT,
embedding TEXT,
dimensions INTEGER
);
-- Full-text search (when available)
CREATE VIRTUAL TABLE chunks_fts USING fts5(text, content=chunks);Storage location: ~/.openclaw/memory/<agentId>.sqlite
Chunking
Markdown files are split into overlapping chunks for embedding:
- Chunk size: ~400 tokens (~1600 characters)
- Overlap: ~80 tokens (~320 characters) — preserves context across chunk boundaries
- Line tracking: Each chunk records
startLineandendLinefor citation accuracy - Session remapping: JSONL transcripts get a line-number map that translates flattened text positions back to original message indices
Source: chunkMarkdown() in src/memory/internal.ts
Embedding Providers
Auto-selection priority (provider: "auto"):
| Priority | Provider | Model | Dimensions | Condition |
|---|---|---|---|---|
| 1 | Local | embeddinggemma-300m-qat-q8_0.gguf |
Varies | memorySearch.local.modelPath configured |
| 2 | OpenAI | text-embedding-3-small |
1536 | API key found |
| 3 | Gemini | gemini-embedding-001 |
Varies | API key found |
| 4 | Voyage | voyage-4-large |
Varies | API key found |
OpenAI also supports text-embedding-3-large (3072 dimensions) via config.
Batch Processing
- Batch size: 8000 tokens per batch
- Concurrency: 2–4 concurrent batches
- Timeouts: 60s remote, 5min local
- Retries: Up to 3 attempts with exponential backoff (500ms → 8s)
- Caching: Embeddings cached by
(provider, model, key, hash)— skips re-embedding unchanged chunks
Source: src/memory/manager-embedding-ops.ts
Search
Hybrid Search (src/memory/hybrid.ts)
Combines two scoring signals:
final_score = vectorWeight × vectorScore + textWeight × textScore
| Component | Method | Default Weight |
|---|---|---|
| Vector | Cosine similarity (L2-normalized embeddings via sqlite-vec) |
0.7 |
| Keyword | FTS5 with BM25, normalized via 1 / (1 + rank) |
0.3 |
When embeddings are unavailable, falls back to keyword-only search with query expansion (stop-word removal, Chinese bigram tokenization).
Temporal Decay (src/memory/temporal-decay.ts)
Dated memory files (memory/YYYY-MM-DD.md) decay exponentially:
decayed_score = score × exp(-λ × age_in_days)
where λ = ln(2) / half_life_days (default half_life = 30 days)
Evergreen files (MEMORY.md, undated files) are not affected.
Maximal Marginal Relevance (MMR) — Optional
Diversity-aware re-ranking to avoid returning near-duplicate results:
MMR_score = λ × relevance - (1 - λ) × max_similarity_to_already_selected
Default: disabled. When enabled, λ = 0.7 (biased toward relevance over diversity). Uses Jaccard token similarity for efficiency.
Result Limits
| Parameter | Default |
|---|---|
| Max results | 6 |
| Min score threshold | 0.35 |
| Max snippet length | 700 chars |
| Max total injected chars (QMD) | 4000 |
Agent Tools
memory_search
Parameters: query (string), maxResults? (number), minScore? (number)
Returns: [{ path, startLine, endLine, score, snippet, source, citation? }]
Flow:
- Resolve config and agent ID from session key
- Get MemorySearchManager (builtin SQLite or QMD backend)
- Hybrid search with citations
- Clamp results to injection budget
- Return with provider/model metadata
memory_get
Parameters: path (string), from? (line number), lines? (count)
Returns: { path, text }
Used after memory_search to pull specific line ranges efficiently. This two-step pattern (search → get) keeps search results compact while allowing the agent to read full context when needed.
System Prompt Integration
From src/agents/system-prompt.ts:
## Memory Recall
Before answering anything about prior work, decisions, dates, people,
preferences, or todos: run memory_search on MEMORY.md + memory/*.md;
then use memory_get to pull only the needed lines.
If low confidence after search, say you checked.
The memory section only appears when:
- Prompt mode is
"full"(not subagent minimal mode) memory_searchormemory_gettools are registered
Citations
| Mode | Behavior |
|---|---|
"on" |
Always include Source: <path>#L<start>-L<end> |
"off" |
Never mention file paths or line numbers |
"auto" |
Citations in direct DMs; suppressed in groups/channels |
QMD Backend (Optional)
QMD (Qualified Memory Database) is a local-first vector search sidecar:
- Combines BM25 + vector search + reranking
- Runs as a separate Bun-based CLI tool
- State:
~/.openclaw/agents/<agentId>/qmd/ - Update cycle:
qmd update(index) +qmd embed(embeddings) on boot + every 5min
Search Modes
| Mode | Description |
|---|---|
search (default) |
Fast keyword + vector hybrid |
vsearch |
Vector-only |
query |
Full query expansion + reranking (slower, best recall) |
Fallback
FallbackMemoryManager wraps both backends:
- Primary: QMD (if configured)
- Fallback: Builtin SQLite
- Transparent switching on failure with reason tracking
LanceDB Extension (Alternative)
extensions/memory-lancedb/ provides an alternative vector backend using LanceDB:
type MemoryEntry = {
id: string;
text: string;
vector: number[];
importance: number;
category: "preference" | "fact" | "decision" | "entity" | "other";
createdAt: number;
};Uses OpenAI embeddings (text-embedding-3-small/large). Lazy-loads to keep startup fast.
Sync & Indexing
File Watching
- Watcher: chokidar with 1500ms debounce
- Hash-based change detection (skip unchanged files)
- Session files tracked separately with delta thresholds
Indexing Pipeline
File change detected
→ Debounce (1500ms)
→ Hash check (skip if unchanged)
→ chunkMarkdown() — split into overlapping chunks with line numbers
→ Load embedding cache
→ Request embeddings from provider (batch + retry)
→ Store in cache + DB chunks table
→ Update files table (hash, mtime, size)
Session Indexing
Session transcripts (JSONL) are indexed only when changes exceed thresholds:
deltaBytes: Minimum bytes changed since last syncdeltaMessages: Minimum messages added since last sync
This avoids re-indexing after every single message.
Architecture Diagram
┌─────────────────────────────────────────────────────────────┐
│ Memory System │
│ │
│ Sources Indexing Search │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │MEMORY.md │──┐ │Chunk │ │ Hybrid │ │
│ │memory/* │──┤ watch + │Markdown │ │ Search │ │
│ │sessions/ │──┤ hash │(400 tok │ │ │ │
│ │ *.jsonl │──┘ detect │ overlap) │ │ Vector │ │
│ └──────────┘ │ └────┬─────┘ │ (0.7) │ │
│ │ │ │ + │ │
│ ▼ ▼ │Keyword │ │
│ ┌──────────┐ ┌──────────┐ │ (0.3) │ │
│ │ File │ │ Embed │ └────┬────┘ │
│ │ Watcher │ │ Provider │ │ │
│ │(chokidar)│ │(OpenAI/ │ ▼ │
│ └──────────┘ │ Gemini/ │ ┌─────────┐ │
│ │ Local) │ │ Temporal│ │
│ └────┬─────┘ │ Decay │ │
│ │ └────┬────┘ │
│ ▼ │ │
│ ┌──────────┐ ▼ │
│ │ SQLite │ ┌─────────┐ │
│ │ + FTS5 │◄────────│ MMR │ │
│ │ + vec │ │(optional)│ │
│ └──────────┘ └─────────┘ │
│ │
│ Tools: memory_search (query → results) │
│ memory_get (path + lines → text) │
└─────────────────────────────────────────────────────────────┘