Memory
How OpenClaw memory works (workspace files + automatic memory refresh).
OpenClaw memory is plain Markdown in agent workspaces. These files are
the source of truth; the model only "remembers" what is written to disk.
Memory search tools are provided by the active memory plugin (default:
''memory-core''). Use ''plugins.slots.memory = "none"'' to disable the memory plugin.
Memory Files (Markdown)
Default workspace layout uses two memory layers:
- ''memory/YYYY-MM-DD.md''
- Daily logs (append-only).
- Today + yesterday are read at the start of a turn.
- ''MEMORY.md'' (optional)
- Curated long-term memory.
- Only loaded in the main private session (never in group context).
These files live under the workspace (''agents.defaults.workspace'', default
When to Write to Memory
- Decisions, preferences, and persistent facts go to ''MEMORY.md''.
- Daily notes and runtime environment go to ''memory/YYYY-MM-DD.md''.
- If someone says "remember this", write it down (don't keep it in RAM).
- This area is still evolving. Helpful to remind the model to store memories; it will know what to do.
- If you want something persisted, ask the bot to write it to memory.
Automatic Memory Flush (Pre-Compaction Ping)
When a session nears automatic compaction, OpenClaw triggers a **silent,
Agent turn that reminds the model to **write durable memory
before the'' context is compacted. The default prompt explicitly states the model _may reply_,
but usually ''NO_REPLY'' is the correct response, so the user never sees this turn.
This is controlled by ''agents.defaults.compaction.memoryFlush'':
{
agents: {
defaults: {
compaction: {
reserveTokensFloor: 20000,
memoryFlush: {
enabled: true,
softThresholdTokens: 4000,
systemPrompt: "Session nearing compaction. Store durable memories now.",
prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
},
},
},
},
}Details:
- Soft threshold: Flush triggers when the session token estimate exceeds
''contextWindow - reserveTokensFloor - softThresholdTokens''.
- ''Silent by default'': The prompt includes ''NO_REPLY'', so nothing is delivered.
- Two prompts: User prompt plus system prompt with the reminder attached.
- ''One flush per compaction cycle'' (tracked in ''sessions.json'').
- Workspace must be writable: If the session is running in a sandbox with
''workspaceAccess: "ro"'' or ''"none"'', the flush is skipped.
For the full compaction lifecycle, see
Vector Memory Search
OpenClaw can build a small vector index over ''MEMORY.md'' and ''memory/*.md'' (plus
any extra directories or files you opt into), so semantic queries can find relevant
notes even when the wording is different.
Defaults:
- Enabled by default.
- Watches memory files for changes (debounced).
- Uses remote embeddings by default. If ''memorySearch.provider'' is not set, OpenClaw auto-selects:
1. ''local'' (if ''memorySearch.local.modelPath'' is configured and the file exists).
2. ''openai'' if an OpenAI key can be resolved.
3. ''gemini'' if a Gemini key can be resolved.
4. Otherwise, memory search remains disabled until configured.
- Local mode uses node-llama-cpp and may require ''pnpm approve-builds''.
- Uses sqlite-vec (if available) to accelerate vector search within SQLite.
Remote embeddings require an API key for the embedding provider. OpenClaw
resolves keys from auth profiles, ''models.providers.*.apiKey'', or environment
#
Additional Memory Paths
If you want to index Markdown files outside the default workspace layout, add
agents: {
defaults: {
memorySearch: {
extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
}
}
}explicit paths:
Notes:
- Paths can be absolute or relative to the workspace.
- Recursively scans directories for ''.md'' files.
#
Gemini Embeddings (Native)
Set provider to ''gemini'' to use Gemini embedding API directly:
agents: {
defaults: {
memorySearch: {
provider: "gemini",
model: "gemini-embedding-001",
remote: {
apiKey: "YOUR_GEMINI_API_KEY"
}
}
}
}Notes:
- ''remote.baseUrl'' is optional (defaults to Gemini API base URL).
- ''remote.headers'' allows you to add extra headers as needed.
- Default model: ''gemini-embedding-001''.
If you want to use a custom OpenAI-compatible endpoint (OpenRouter, vLLM, or a proxy),
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
headers: { "X-Custom-Header": "value" }
}
}
}
}you can use the ''remote'' config with the OpenAI provider:
If you don't want to set an API key, use ''memorySearch.provider = "local"'' or set
''memorySearch.fallback = "none"''.
Fallback:
- ''memorySearch.fallback'' can be ''openai'', ''gemini'', ''local'', or ''none''.
- The fallback provider is only used if the primary embedding provider fails.
Batch Indexing (OpenAI + Gemini):
- OpenAI and Gemini embeddings are enabled by default. Set ''agents.defaults.memorySearch.remote.batch.enabled = false'' to disable.
- Default behavior waits for batch completion; adjust ''remote.batch.wait'', ''remote.batch.pollIntervalMs'', and ''remote.batch.timeoutMinutes'' if needed.
- Set ''remote.batch.concurrency'' to control how many batch jobs we submit in parallel (default: 2).
- Batch mode works with ''memorySearch.provider = "openai"'' or ''"gemini"'' and uses the corresponding API key.
- Gemini batch jobs use the async embedding batch endpoint and require Gemini Batch API availability.
Why OpenAI Batch is fast and cheap:
- For large backfills, OpenAI is often our fastest option because we can submit many embedding requests in a single batch job and let OpenAI process them asynchronously.
- OpenAI offers discounted pricing for Batch API workloads, so large index runs are often cheaper than sending the same requests synchronously.
How Memory Tools Work
- ''memory_search'' searches Markdown chunks from ''MEMORY.md'' + ''memory/**/*.md'' semantically (~400 token target, 80 token overlap). It returns snippet text (capped at 700 chars), file path, line range, score, provider/model, and whether we fell back from local → remote embedding. It does not return the full file payload.
- ''memory_get'' reads a specific memory Markdown file (relative to workspace), optionally reading N lines starting from a start line. Paths outside ''MEMORY.md'' / ''memory/'' are only allowed if explicitly listed in ''memorySearch.extraPaths''.
- Both tools are only enabled if the agent's ''memorySearch.enabled'' resolves to true.
#
What Gets Indexed (and When)
- File types: Markdown only (''MEMORY.md'', ''memory/**/*.md'', and any ''.md'' files under ''memorySearch.extraPaths'').
- Index storage: Per-agent SQLite at ''~/.openclaw/memory/<agentId>.sqlite'' (configurable via ''agents.defaults.memorySearch.store.path'', supports ''{agentId}'' token).
- Freshness: Watchers on ''MEMORY.md'', ''memory/'', and ''memorySearch.extraPaths'' mark the index as dirty (debounced 1.5s). Sync is scheduled at session start, on search, or at intervals, and runs asynchronously. Session transcripts use delta thresholds to trigger background sync.
- Re-index triggers: Index stores embedding provider/model + endpoint fingerprint + chunking params. If any of these change, OpenClaw automatically resets the entire store and re-indexes.
#
Hybrid Search (BM25 + Vector)
When enabled, OpenClaw combines:
- Vector similarity (semantic matching, wording may differ)
- BM25 keyword relevance (exact tokens like IDs, env vars, code symbols)
If full-text search is not supported on your platform, OpenClaw falls back to pure vector search.
##
Why Hybrid?
Vector search is good at "this means the same thing":
- "Mac Studio gateway host" vs "computer running gateway"
- "debounce file updates" vs "avoid indexing on every write"
But it can be weak on precise, high-signal tokens:
- IDs (''a828e60'', ''b3b9895a…'')
- Code symbols (''memorySearch.query.hybrid'')
- Error strings ("sqlite-vec not available")
BM25 (full-text) is the opposite: strong on exact tokens, weak on paraphrasing.
Hybrid search is a pragmatic middle ground: use both retrieval signals so you get
good results for both "natural language" queries and "needle in a haystack" queries.
##
How We Merge Results (Current Design)
Implementation sketch:
1. Retrieve candidate pools from both sides:
- ''Vector'': Top ''maxResults * candidateMultiplier'' by cosine similarity.
- ''BM25'': Top ''maxResults * candidateMultiplier'' by FTS5 BM25 rank (lower is better).
2. Convert BM25 rank to a ~0..1 score:
- ''textScore = 1 / (1 + max(0, bm25Rank))''
3. Union candidates by chunk id and compute weighted score:
- ''finalScore = vectorWeight * vectorScore + textWeight * textScore''
Notes:
- ''vectorWeight'' + ''textWeight'' are normalized to 1.0 at config resolution, so weights behave as percentages.
agents: {
defaults: {
memorySearch: {
query: {
hybrid: {
enabled: true,
vectorWeight: 0.7,
textWeight: 0.3,
candidateMultiplier: 4
}
}
}
}
}#
Embedding Cache
OpenClaw can cache chunk embeddings in SQLite, so re-indexing and frequent updates (especially session transcripts) don't re-embed unchanged text.
Configuration:
agents: {
defaults: {
memorySearch: {
cache: {
enabled: true,
maxEntries: 50000
}
}
}
}#
Session Memory Search (Experimental)
You can opt into indexing ''session transcripts'' and surfacing them via ''memory_search''.
This is behind an experimental flag.
agents: {
defaults: {
memorySearch: {
experimental: { sessionMemory: true },
sources: ["memory", "sessions"]
}
}
}Notes:
- Session indexing is opt-in (off by default).
- Session updates are debounced and indexed asynchronously once they exceed a delta threshold (best effort).
- ''memory_search'' never blocks on indexing; results may be slightly stale until background sync completes.
- Results still only contain snippets; ''memory_get'' is still limited to memory files.
- Session indexes are isolated per-agent (only indexes that agent's session logs).
- Session logs are stored on disk (''~/.openclaw/agents/<agentId>/sessions/*.jsonl''). Any process/user with filesystem access can read them, so treat disk access as a trust boundary. For stricter isolation, run agents under separate OS users or hosts.
agents: {
defaults: {
memorySearch: {
sync: {
sessions: {
deltaBytes: 100000, // ~100 KB
deltaMessages: 50 // JSONL lines
}
}
}
}
}#
SQLite Vector Acceleration (sqlite-vec)
When the sqlite-vec extension is available, OpenClaw stores embeddings in
SQLite virtual tables (''vec0'') and performs vector distance queries
inside the database. This keeps search fast without loading every embedding into JS.
agents: {
defaults: {
memorySearch: {
store: {
vector: {
enabled: true,
extensionPath: "/path/to/sqlite-vec"
}
}
}
}
}Configuration (optional):
Notes:
- ''enabled'' defaults to true; when disabled, search falls back to in-process
cosine similarity over stored embeddings.
#
Local Embedding Auto-Download
- Default local embedding model: ''hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf'' (~0.6 GB).
- When ''memorySearch.provider = "local"'', ''node-llama-cpp'' resolves ''modelPath''; if the GGUF is missing, it ''auto-downloads'' it to cache (or ''local.modelCacheDir'' if set), then loads it. Downloads resume on retry.
- Local build requirement: Run ''pnpm approve-builds'', select ''node-llama-cpp'', then ''pnpm rebuild node-llama-cpp''.
- Fallback: If local setup fails and ''memorySearch.fallback = "openai"'', we auto-switch to remote embeddings (''openai/text-embedding-3-small'' unless overridden) and log the reason.
#
Custom OpenAI-Compatible Endpoint Example
agents: {
defaults: {
memorySearch: {
provider: "openai",
model: "text-embedding-3-small",
remote: {
baseUrl: "https://api.example.com/v1/",
apiKey: "YOUR_REMOTE_API_KEY",
headers: {
"X-Organization": "org-id",
"X-Project": "project-id"
}
}
}
}
}Notes:
- ''remote.*'' takes precedence over ''models.providers.openai.*''.
- ''remote.headers'' merges with OpenAI headers; remote wins on key conflicts. Omit ''remote.headers'' to use OpenAI defaults.