| summary | How OpenClaw Manages Memory: from writes to indexing and retrieval | ||
|---|---|---|---|
| read_when |
|
To understand OpenClaw memory internals, this article starts from the source code and breaks down the full OpenClaw memory pipeline: memory writes -> file discovery -> chunking and embeddings -> SQLite index -> hybrid retrieval -> tool re-injection.
OpenClaw memory is a layered system, not "dump all history back into context."
- Source data layer: Markdown files in the workspace, always readable and auditable.
- Derived index layer: SQLite index, always rebuildable.
- Retrieval layer: Hybrid vector + keyword search, returning snippets instead of whole files.
Key principles:
- Auditable: the source of truth is Markdown.
- Recoverable: indexes can be deleted and rebuilt without touching source data.
- Controllable and tunable:
memorySearchexposes fine-grained tuning.
Memory source data lives in Markdown files with the default layout:
MEMORY.mdormemory.md- Long-term stable memory
memory/YYYY-MM-DD.md- Daily notes
memory/YYYY-MM-DD-<slug>.md- Session archives
These files live at the workspace root (agents.defaults.workspace). Docs use <workspace> as a placeholder.
listMemoryFiles() in src/memory/internal.ts scans:
<workspace>/MEMORY.mdand<workspace>/memory.md<workspace>/memory/**/*.md- extra directories or files configured in
memorySearch.extraPaths
Limits and details:
- Only
.mdfiles are indexed. - All symlinks are ignored.
- Extra paths can be absolute or workspace-relative.
- Real paths are de-duplicated to avoid double indexing.
OpenClaw memory writes are not a single entry point, but multiple cooperating mechanisms:
Users or models explicitly write to MEMORY.md or memory/YYYY-MM-DD.md. This is the most direct and reliable path.
When a user runs /new, the session-memory hook (if enabled) saves recent conversation to a standalone file:
- File name:
memory/YYYY-MM-DD-<slug>.md - Content: session metadata + recent conversation summary
Implementation: src/hooks/bundled/session-memory/handler.ts
Core behavior:
- Read session JSONL and extract user + assistant text.
- Use an LLM to generate a slug; fall back to a timestamp on failure.
- Write the Markdown file.
When a session approaches compaction, an implicit agent turn is triggered to prompt the model to write durable memory to files.
Implementation:
src/auto-reply/reply/memory-flush.tssrc/auto-reply/reply/agent-runner-memory.ts
Core trigger condition:
threshold = contextWindow - reserveTokensFloor - softThresholdTokens
When totalTokens >= threshold and the current compaction cycle has not flushed, the flush runs.
Key traits:
- Default prompt includes
NO_REPLYto keep it hidden from users. - Skips when the sandbox workspace is read-only or unavailable.
- Skips for the CLI provider.
- Runs at most once per compaction cycle.
Config entry: agents.defaults.compaction.memoryFlush.
Core class: MemoryIndexManager (src/memory/manager.ts).
It owns indexing orchestration, sync, retrieval entry points, caches, and fallbacks.
Indexes are per-agent SQLite files:
<state-dir>/memory/<agentId>.sqlite
<state-dir> is determined by OPENCLAW_STATE_DIR.
If unset, default state-dir resolution rules apply, including legacy path selection.
Initialization lives in src/memory/memory-schema.ts.
Key tables:
files: file metadata (hash, mtime, size, source)chunks: chunk text, line ranges, embedding JSONembedding_cache: embedding cache (provider model key hash)chunks_vec: sqlite-vec vector index (optional)chunks_fts: FTS5 keyword index (optional)
- If
store.vector.enabledis true, OpenClaw tries to load the sqlite-vec extension. - If loading fails, it falls back to in-memory similarity computation.
- Vector dimension changes trigger vector table rebuilds.
Each file is read and hashed with SHA-256 to decide whether reindexing is needed. If the hash is unchanged, the file is skipped.
Default chunking parameters:
tokens = 400overlap = 80
Implementation logic:
- Split by lines and record line ranges.
- Estimate chunk size as
tokens * 4characters. - Keep overlap with a sliding window.
Each chunk's unique ID hashes:
source + path + startLine + endLine + chunkHash + model
This ensures new chunk IDs when source, model, or line positions change.
resolveMemorySearchConfig() resolves unspecified providers to auto.
The auto order in createEmbeddingProvider() is:
- Local model available and path exists
- OpenAI API key available
- Gemini API key available
- Otherwise memory search is disabled
- OpenAI uses
POST /embeddings - Gemini uses
embedContentandbatchEmbedContents
Both respect memorySearch.remote baseUrl and headers.
The local provider uses node-llama-cpp.
OpenClaw only calls resolveModelFile() and createEmbeddingContext(); model resolution and caching are handled by the library.
Indexing uses batches by default:
- OpenAI and Gemini prefer batch APIs.
- Max batch size is 50k requests.
- OpenAI uses
/files+/batcheswith a 24h completion window. - Gemini uses the upload endpoint and calls
asyncBatchEmbedContent.
Resilience:
- Batch timeouts retry once.
- Repeated failures disable batch and fall back to standard embeddings.
- Some provider errors force batch to disable.
embedding_cache keys on provider + model + providerKey + hash:
- providerKey includes baseUrl, model, and header fingerprints to avoid cross-endpoint pollution.
- Cache eviction is ordered by
updated_at. - Full reindex attempts to seed cache from previous indexes.
Index sync runs asynchronously and does not block search requests. Core mechanisms:
- Session start: warm sync
- On search: background sync when dirty
- Watcher: file change trigger
- Interval: periodic trigger
- Uses chokidar
- Default debounce is 1500ms
- Watches
MEMORY.md,memory/, and extraPaths
When session indexing is enabled:
- Watches JSONL file changes
- Uses added bytes or line count as thresholds
- Extracts user + assistant text only
- Compresses text to a single line
Indexed file path form:
<state-dir>/agents/<agentId>/sessions/*.jsonl
memory_search does not wait for sync to complete, so results can be slightly stale.
This is a latency-first design choice.
Vector retrieval path:
- If sqlite-vec is available: use
vec_distance_cosine - Otherwise: load all chunk embeddings and compute cosine similarity
Final score:
score = 1 - cosine_distance
If FTS5 is available:
- Rank by BM25
- Tokens include only alphanumerics and underscores
- Multiple tokens are combined with AND
BM25 rank converts to:
textScore = 1 / (1 + rank)
Hybrid merge formula:
finalScore = vectorWeight * vectorScore + textWeight * textScore
Default parameters:
vectorWeight = 0.7textWeight = 0.3candidateMultiplier = 4- Max candidate pool 200
Each result includes:
- path
- startLine, endLine
- snippet (max 700 chars)
- score
- source (memory or sessions)
OpenClaw exposes two memory tools when memory search is enabled; the tool definitions live in
src/agents/tools/memory-tool.ts and are included in the runtime tool list when allowed by policy:
memory_search- Parameters:
query, optionalmaxResults, optionalminScore - Uses
getMemorySearchManager(...)to run semantic search overMEMORY.md,memory/**/*.md, and optional session transcripts (when enabled inmemorySearch.sources). - Returns top snippets with
pathand line ranges, plus provider/model metadata.
- Parameters:
memory_get- Parameters:
path, optionalfrom, optionallines - Uses
MemorySearchManager.readFile(...)to fetch only the needed lines. - Allowed paths are constrained to
MEMORY.md,memory/**/*.md, and configuredmemorySearch.extraPaths.
- Parameters:
These tools are only available when resolveMemorySearchConfig(...) returns enabled config.
When memory_search or memory_get is available, buildAgentSystemPrompt(...) injects a Memory Recall
section that instructs the model to:
- Run
memory_searchbefore answering about past work, decisions, dates, people, preferences, or todos. - Follow up with
memory_getto pull only the relevant lines.
Implementation: src/agents/system-prompt.ts.
MEMORY.md or memory.md may also appear in Project Context because bootstrap files are injected
into the system prompt (with truncation if they exceed the max chars limit).
However:
- The
memory/directory is not injected into Project Context. memory_search/memory_getare the canonical retrieval path for durable memory, especially for long history or when Project Context was truncated.
OpenClaw also provides CLI commands:
openclaw memory statusopenclaw memory status --deepopenclaw memory indexopenclaw memory search "..."
status --deep probes:
- sqlite-vec availability
- embeddings availability
References:
- Increase
chunking.tokensfor better semantic coherence, but worse pinpoint accuracy. - Tune
query.minScoreto control recall threshold. - Increase
query.hybrid.textWeightto improve hits for code symbols and IDs.
- Batch APIs are good for large backfills.
cache.maxEntriesreduces repeated embedding cost.- When the vector extension is unavailable, search slows down; prioritize fixing sqlite-vec.
- For fast-moving sessions, increase
sync.sessions.deltaBytesordeltaMessages. - If you care more about recent dialog, enable
sources: ["memory", "sessions"].
-
No results from memory_search
- Check whether the index is dirty
- Run
openclaw memory status --deep --index
-
Vector search unavailable
- sqlite-vec load failure falls back automatically
- Check vector status in
status --deep
-
Embedding errors
- Confirm API key resolution paths
- Check whether the provider was fallen back
-
Memory flush not executed
- Workspace may be read-only or inaccessible
- A flush already ran in the current compaction cycle
OpenClaw memory is an auditable, rebuildable, and tunable engineering memory system:
- Markdown is the only source of truth
- SQLite indexes provide a rebuildable retrieval layer
- Hybrid vector + keyword retrieval balances semantics and precision
- Automated flush and hooks keep important memory from being lost
This design is ideal for long-term personal or team assistant scenarios, balancing control and extensibility.