Memory files
Responsibilities
"Memory files" are OpenClaw agent's long-term persisted user preferences, behavioral guidelines, and project context. They differ from session transcript: a session is per-conversation messages; memory is "this rule always applies" notes reused across sessions. OpenClaw treats MEMORY.md at the workspace root as the canonical root memory file, and all .md files under the memory/ subdirectory as auxiliary memory. The agent reads them into the system prompt at startup and follows these rules for the rest of the conversation.
The memory system also supports multimodal: memory/ isn't limited to .md; it can hold images, audio, and other file types with extensions allowed by MemoryMultimodalSettings, classified and indexed by the memory-host-sdk.
Design motivation
Why not just write all rules into the system prompt? Three reasons:
- Reuse across sessions: the system prompt consumes tokens on every model call; memory files are loaded once at agent startup and reused across the whole session. Long-term preferences are cheaper written to a file than to the prompt.
- User-editable: memory files are plain markdown; users can edit them in any editor and the change takes effect on the next startup — no config change or gateway restart needed.
openclaw doctor --fixcan also auto-migrate legacy files and merge split canonical/legacy copies. - Plugin-indexable:
memory-host-sdkregisters memory files as a QMD collection; plugins can build vector indexes and do semantic retrieval — so large memory doesn't have to be stuffed into the prompt; relevant fragments are recalled dynamically by query.
The cost is that the "root memory" filename has legacy compatibility history, is case-sensitive, and symlinks are ignored — all these details are handled uniformly by root-memory-files.ts.
Key files
CANONICAL_ROOT_MEMORY_FILENAME:L6-L7— constant"MEMORY.md"; legacy is lowercase"memory.md".resolveCanonicalRootMemoryPath:L13-L15— joins${workspaceDir}/MEMORY.md.resolveLegacyRootMemoryPath:L17-L19— joins${workspaceDir}/memory.md.resolveRootMemoryRepairDir:L22-L24— doctor repair dir${workspaceDir}/.openclaw-repair/root-memory/.resolveCanonicalRootMemoryFile:L41-L54— scans workspace directory entries and returns the path of a real file (not symlink)MEMORY.md; returns null if absent.shouldSkipRootMemoryAuxiliaryPath:L56-L72— when scanning auxiliary files, skips legacy files and the repair directory.normalizeExtraMemoryPaths:L91-L101— tilde-expands + dedupes + absolutizes extra paths declared in config.isMemoryPath:L103-L112— checks whether a relative path is memory: rootMEMORY.md,memory/subdirectory, ordreams.md.listMemoryFiles:L150-L210— lists all memory files: canonical root → memory/ recursive → extra paths.resolveDefaultCollections:L409-L418— default QMD collections:memory-root(exact matchMEMORY.md) +memory-dir(memory/**/*.md).readMemoryFile:L67-L158— reads a single memory file; supportsfrom/linespagination, withsuggestReadFallbackhint.readAgentMemoryFile:L161-L182— resolves workspace by agent id, then delegates toreadMemoryFile.openclaw-runtime-memory.ts— stable facade for the plugin SDK; re-exports internal seams likeresolveCanonicalRootMemoryFile.CANONICAL_ROOT_MEMORY_FILENAME copy:L168— SDK side keeps the same-named constant to avoid circular deps back to core.system-prompt guidance:L229— system prompt tells the modelMEMORY.mdis "persistent user preferences and behavioral guidelines, followed throughout the session".workspace import:L15-L18— workspace module importsCANONICAL_ROOT_MEMORY_FILENAMEconstant for initialization detection.detectRootMemoryFiles:L97-L121— doctor scans both canonical + legacy files, returns existence and byte size.formatRootMemoryFilesWarning:L128-L140— gives a merge hint when both exist.RootMemoryMigrationResult:L142-L150— migration result type:mergedLegacy/removedLegacy/archivedLegacyPath.
Data flow
listMemoryFiles (listMemoryFiles:L150) is the core scan entry:
export async function listMemoryFiles(
workspaceDir: string,
extraPaths?: string[],
multimodal?: MemoryMultimodalSettings,
): Promise<string[]> {
const result: string[] = [];
const memoryDir = path.join(workspaceDir, "memory");
const shouldSkipWorkspaceMemoryPath = (absPath: string): boolean =>
shouldSkipRootMemoryAuxiliaryPath({ workspaceDir, absPath });
const addMarkdownFile = async (absPath: string) => {
try {
const stat = await statRegularFile(absPath);
if (stat.missing) {
return;
}
if (!absPath.endsWith(".md")) {
return;
}
result.push(absPath);
} catch {}
};
const memoryFile = await resolveCanonicalRootMemoryFile(workspaceDir);
if (memoryFile) {
await addMarkdownFile(memoryFile);
}
try {
const dirStat = await fs.lstat(memoryDir);
if (!dirStat.isSymbolicLink() && dirStat.isDirectory()) {
await collectMemoryFilesFromDir(memoryDir, result, multimodal, shouldSkipWorkspaceMemoryPath);
}
} catch {}Three layers: canonical root file — resolveCanonicalRootMemoryFile only returns a real file (not symlink), preventing symlinks from pulling in external directories; memory/ directory goes through collectMemoryFilesFromDir recursively — the descend hook rejects the .openclaw-repair directory, and the include hook filters extensions with isAllowedMemoryFilePath; extra paths come from config, and each path goes through shouldSkipRootMemoryAuxiliaryPath to prevent double-counting legacy files.
backend-config registers these files as QMD collections (resolveDefaultCollections:L409) for vector indexing:
function resolveDefaultCollections(
include: boolean,
workspaceDir: string,
existing: Set<string>,
agentId: string,
): ResolvedQmdCollection[] {
if (!include) {
return [];
}
const entries: Array<{ path: string; pattern: string; base: string }> = [
{ path: workspaceDir, pattern: CANONICAL_ROOT_MEMORY_FILENAME, base: "memory-root" },
{ path: path.join(workspaceDir, "memory"), pattern: "**/*.md", base: "memory-dir" },
];
return entries.map((entry) => ({
name: ensureUniqueName(scopeCollectionBase(entry.base, agentId), existing),
path: entry.path,
pattern: entry.pattern,
kind: "memory",
}));
}memory-root matches exact filename, non-recursive; memory-dir is glob **/*.md, recursing the whole memory/ subtree. scopeCollectionBase prefixes each collection name with the agent id, so when multiple agents share a workspace, their collections don't collide.
The doctor's split-file scan logic (detectRootMemoryFiles:L97) uses exactWorkspaceEntryExists to check both MEMORY.md and memory.md:
export async function detectRootMemoryFiles(
workspaceDir: string,
): Promise<RootMemoryFilesDetection> {
const resolvedWorkspace = path.resolve(workspaceDir);
const canonicalPath = resolveCanonicalRootMemoryPath(resolvedWorkspace);
const legacyPath = resolveLegacyRootMemoryPath(resolvedWorkspace);
const entries = await listWorkspaceEntries(resolvedWorkspace);
const [canonical, legacy] = await Promise.all([
entries.has(CANONICAL_ROOT_MEMORY_FILENAME)
? statIfExists(canonicalPath)
: Promise.resolve<RootMemoryStatResult>({ exists: false }),
entries.has(LEGACY_ROOT_MEMORY_FILENAME)
? statIfExists(legacyPath)
: Promise.resolve<RootMemoryStatResult>({ exists: false }),
]);Note listWorkspaceEntries uses readdir to get exact directory entries (case-sensitive). macOS's default filesystem is case-insensitive, but OpenClaw still treats it case-sensitively: canonical is always uppercase MEMORY.md, legacy is lowercase memory.md. When both exist, canonical wins and legacy is archived to .openclaw-repair/root-memory/<timestamp>/memory.md.
The whole read-write chain:
Boundaries and failures
- symlinks ignored:
resolveCanonicalRootMemoryFileusesentry.isFile() && !entry.isSymbolicLink()double-check (src/memory/root-memory-files.ts:L41-L54); thememory/directory also gates with!dirStat.isSymbolicLink(). This is a security boundary, preventing users (or malicious plugins) from pulling files outside the workspace into the memory index via symlinks. - case distinction: on case-insensitive filesystems (macOS HFS+/APFS default),
MEMORY.mdandmemory.mdare the same file; but OpenClaw usesexactWorkspaceEntryExiststo get the exactreaddirentry name and matches literally. This means writing both on macOS will collide, and doctor treats them as legacy-only. - legacy migration loses no content:
moveLegacyRootMemoryFileToArchivefirst renames; on failure (EXDEV cross-device) it falls back to copy + unlink (src/commands/doctor-workspace.ts:L152-L174). The archive path is timestamped; running doctor multiple times won't overwrite each other. - extra paths cross workspace: extra paths declared in config can be absolute or workspace-relative.
normalizeExtraMemoryPaths(packages/memory-host-sdk/src/host/internal.ts:L91-L101) does tilde expansion + dedup, but doesn't remove them from the QMD collection — plugins dedupe by absolute path at index time, avoiding indexing the same file twice. dreams.mdspecial path:isMemoryPathalso treatsdreams.mdas a memory file (packages/memory-host-sdk/src/host/internal.ts:L108-L110) — this legacy convention is preserved for backward compatibility with old users. Canonical is alwaysMEMORY.md;dreams.mdis just auxiliary memory.- multimodal extensions:
isAllowedMemoryFilePathby default only accepts.md, but whenMemoryMultimodalSettingsis enabled, it allows image/audio extensions classified byclassifyMemoryMultimodalPath. This is a plugin extension point; core doesn't ship multimodal indexing. - readMemoryFile pagination: reading a memory file supports
from/linesparameters (packages/memory-host-sdk/src/host/read-file.ts:L67).defaultLinesandmaxCharsare decided byresolveAgentContextLimitsbased on the agent config, preventing a single read from stuffing an oversized memory file into the prompt. - doctor detects split:
formatRootMemoryFilesWarningwarns when canonical and legacy both exist (src/commands/doctor-workspace.ts:L128-L140): "Dreaming writes durable promotions to MEMORY.md; legacy facts will be shadowed." This is the conflict reminder between OpenClaw's built-in "dreaming" mechanism (auto-promoting memory to MEMORY.md) and legacy files. - plugin SDK doesn't directly import core internals:
openclaw-runtime-memory.ts(packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts) only re-exports stable seams (resolveCanonicalRootMemoryFile/shouldSkipRootMemoryAuxiliaryPath/buildActiveMemoryPromptSection). Plugins call these through the SDK, not by directly importingsrc/memory/— so core refactors don't break plugins. - workspace attestation: the workspace directory is attested on first use (
src/agents/workspace.ts:L44WORKSPACE_ATTESTATION_DIRNAME). The attestation records inode/dev, preventing the user from swapping the filesystem mid-session and causing memory file path drift.
Summary
Memory files are OpenClaw agent's cross-session persistent preference layer. MEMORY.md is the canonical root file; memory/ subdirectory holds auxiliary memory; extra paths let users reference across workspaces. memory-host-sdk handles scanning, QMD collection registration, and paginated reads; plugins access through the SDK facade; the doctor command handles legacy migration and split-file repair. How memory is synced by the context engine during compaction is in Context Engine; which session the memory loads into is in the session lifecycle Agent main loop.
Official references: Memory docs · README.