Fichiers de mémoire
Responsabilités
Les « fichiers de mémoire » (memory files) sont la persistance à long terme des préférences utilisateur, des directives de comportement, du contexte de projet pour l'agent OpenClaw. À la différence du session transcript: une session est une séquence message-à-message d'une conversation, la mémoire est une note « cette règle s'applique toujours » réutilisée cross-session. OpenClaw considère par défaut le fichier MEMORY.md à la racine du workspace comme fichier de mémoire racine (root memory file), et tous les .md sous memory/ comme mémoire auxiliaire; au démarrage agent, ils sont lus dans le system prompt, puis toute la conversation suit ces règles.
Le système de mémoire supporte aussi multimodal: sous memory/ il n'y a pas que des .md, on peut aussi mettre des images, audio etc. tant que les extensions sont autorisées par MemoryMultimodalSettings, classification et indexation par memory-host-sdk.
Motivation de conception
Pourquoi ne pas tout mettre dans le system prompt? Trois raisons:
- Réutilisation cross-session: le system prompt consomme des tokens à chaque appel modèle; les fichiers mémoire ne sont chargés qu'au démarrage de l'agent, puis réutilisés pour toute la session; une préférence long-terme vaut mieux dans un fichier que dans le prompt.
- Éditable par l'utilisateur: la mémoire est un markdown normal, l'utilisateur peut la modifier dans n'importe quel éditeur et le prochain démarrage prend effet, sans changer config ni relancer gateway.
openclaw doctor --fixsait aussi migrer les fichiers legacy, fusionner les copies canoniques/legacy divisées. - Indexable par plugin:
memory-host-sdkenregistre les fichiers mémoire comme une QMD collection, les plugins peuvent construire un index vectoriel, faire de la retrieval sémantique — ainsi de larges blocs de mémoire n'ont pas besoin d'entrer entièrement dans le prompt, les fragments pertinents sont rappelés dynamiquement par query.
Le coût est que le fichier « racine mémoire » a un historique de compat legacy, est sensible à la casse, et les symlinks sont ignorés — ces détails sont gérés uniformément par root-memory-files.ts.
Fichiers clés
CANONICAL_ROOT_MEMORY_FILENAME:6-7— Constante"MEMORY.md", legacy est"memory.md"en minuscules.resolveCanonicalRootMemoryPath:13-15— Construit${workspaceDir}/MEMORY.md.resolveLegacyRootMemoryPath:17-19— Construit${workspaceDir}/memory.md.resolveRootMemoryRepairDir:22-24— Répertoire de réparation doctor${workspaceDir}/.openclaw-repair/root-memory/.resolveCanonicalRootMemoryFile:41-54— Scanne les entrées du répertoire workspace, renvoie le cheminMEMORY.mdd'un fichier réel (non symlink), null si absent.shouldSkipRootMemoryAuxiliaryPath:56-72— En scannant les fichiers auxiliaires, on saute le fichier legacy et le répertoire repair.normalizeExtraMemoryPaths:91-101— Tilde-expand + dédoublonne + convertit en absolu les extra paths déclarés dans la config.isMemoryPath:103-112— Détermine si un chemin relatif appartient à la mémoire: racineMEMORY.md, sous-répertoirememory/, oudreams.md.listMemoryFiles:150-210— Liste tous les fichiers mémoire: canonical root →memory/récursif → extra paths.resolveDefaultCollections:409-418— QMD collections par défaut:memory-root(match exactMEMORY.md) +memory-dir(memory/**/*.md).readMemoryFile:67-158— Lit un fichier mémoire unique, supporte paginationfrom/lines, avec suggestionsuggestReadFallback.readAgentMemoryFile:161-182— Résout le workspace par agent id puis délègue àreadMemoryFile.openclaw-runtime-memory.ts— Facade stable pour le plugin SDK, re-export des seams internes commeresolveCanonicalRootMemoryFile.CANONICAL_ROOT_MEMORY_FILENAME copy:168— Le SDK garde aussi la constante homonyme, pour éviter une dépendance circulaire vers le core.system-prompt guidance:229— Le system prompt indique au modèle queMEMORY.mdest « préférence utilisateur persistante et directives comportementales, à respecter durant toute la session ».workspace import:15-18— Le module workspace importe la constanteCANONICAL_ROOT_MEMORY_FILENAMEpour le init check.detectRootMemoryFiles:97-121— doctor scanne les deux fichiers canonical + legacy, retourne existence + taille en octets.formatRootMemoryFilesWarning:128-140— En cas de coexistence des deux, donne un hint de fusion.RootMemoryMigrationResult:142-150— Type de résultat de migration:mergedLegacy/removedLegacy/archivedLegacyPath.
Flux de données
listMemoryFiles (listMemoryFiles:150) est l'entrée de scan centrale:
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 {}Trois couches: le fichier canonical root, resolveCanonicalRootMemoryFile ne renvoie qu'un fichier réel (non symlink), pour empêcher un lien symbolique de traîner un répertoire externe; le répertoire memory/ passe par collectMemoryFilesFromDir récursif, le hook descend refuse le répertoire .openclaw-repair, le hook include filtre les extensions via isAllowedMemoryFilePath; les extra paths viennent de la config, chaque chemin passe par shouldSkipRootMemoryAuxiliaryPath pour éviter de recompter le fichier legacy.
backend-config enregistre ces fichiers comme QMD collections (resolveDefaultCollections:409) pour l'index vectoriel:
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 match un nom de fichier exact, non récursif; memory-dir est un glob **/*.md récursif sur tout le sous-arbre memory/. scopeCollectionBase préfixe chaque nom de collection avec l'agent id, ainsi plusieurs agents partageant un workspace n'entrent pas en collision.
La logique de doctor qui scanne les fichiers divisés (detectRootMemoryFiles:97) utilise exactWorkspaceEntryExists pour vérifier simultanément MEMORY.md et 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 }),
]);Notez que listWorkspaceEntries utilise readdir pour obtenir les entrées exactes du répertoire (sensible à la casse); macOS a un système de fichiers insensible à la casse par défaut, mais OpenClaw reste sensible à la casse: canonical est toujours MEMORY.md majuscule, legacy memory.md minuscule; si les deux coexistent, canonical prend precedence, legacy est archivé dans .openclaw-repair/root-memory/<timestamp>/memory.md.
Toute la chaîne lecture/écriture:
Limites et modes d'échec
- Symlinks ignorés:
resolveCanonicalRootMemoryFileutiliseentry.isFile() && !entry.isSymbolicLink()double check (src/memory/root-memory-files.ts:41-54),memory/utilise aussi!dirStat.isSymbolicLink()comme gate. C'est la frontière de sécurité, empêchant un utilisateur (ou un plugin malveillant) d'utiliser un lien symbolique pour traîner un fichier externe au workspace dans l'index mémoire. - Sensibilité à la casse: sur un système de fichiers insensible à la casse (macOS HFS+/APFS par défaut),
MEMORY.mdetmemory.mdsont le même fichier; mais OpenClaw utiliseexactWorkspaceEntryExistspour obtenir l'entrée exacte dereaddiret matche littéralement. Cela signifie que sur macOS écrire les deux fichiers crée une collision, doctor les traitera comme legacy-only. - Migration legacy sans perte de contenu:
moveLegacyRootMemoryFileToArchiverename d'abord, en cas d'échec (EXDEV cross-device) repli en copy + unlink (src/commands/doctor-workspace.ts:152-174). Le chemin d'archive porte un timestamp, des runs répétés de doctor ne s'écrasent pas mutuellement. - Extra paths cross-workspace: les extra paths déclarés dans config peuvent être des chemins absolus ou relatifs au workspace;
normalizeExtraMemoryPaths(packages/memory-host-sdk/src/host/internal.ts:91-101) fait tilde-expand + dédoublonnage, mais ne les retire pas de la QMD collection — le plugin index par chemin absolu et dédoublonne, pour éviter qu'un même fichier soit indexé deux fois. - Chemin spécial
dreams.md:isMemoryPathconsidère aussidreams.mdcomme un fichier mémoire (packages/memory-host-sdk/src/host/internal.ts:108-110), cette convention legacy est conservée pour compat. Le canonical resteMEMORY.md,dreams.mdn'est que mémoire auxiliaire. - Extensions multimodales:
isAllowedMemoryFilePathn'accepte par défaut que.md, maisMemoryMultimodalSettingsactivé autorise images/audio extensions viaclassifyMemoryMultimodalPath. C'est un point d'extension plugin, le core n'embarque pas d'indexation multimodale. - Pagination readMemoryFile: la lecture supporte
from/lines(packages/memory-host-sdk/src/host/read-file.ts:67),defaultLinesetmaxCharsviennent deresolveAgentContextLimitsselon la config agent, pour éviter qu'une lecture unique d'un très gros fichier mémoire ne bourre le prompt. - Détection de division par doctor:
formatRootMemoryFilesWarningémet un warning si canonical et legacy coexistent (src/commands/doctor-workspace.ts:128-140): « Dreaming écrit durable promotions dans MEMORY.md, les vieux faits dans legacy seront shadowed ». C'est le rappel que le mécanisme « dreaming » interne à OpenClaw (auto-promote memory vers MEMORY.md) entre en conflit avec le fichier legacy. - Plugin SDK ne touche pas aux internals core:
openclaw-runtime-memory.ts(packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts) ne re-export que des seams stables (resolveCanonicalRootMemoryFile/shouldSkipRootMemoryAuxiliaryPath/buildActiveMemoryPromptSection); les plugins utilisent le SDK pour appeler ces fonctions, sans importer directementsrc/memory/— ainsi une refonte core ne casse pas les plugins. - Workspace attestation: le répertoire workspace est attesté à la première utilisation (
src/agents/workspace.ts:44WORKSPACE_ATTESTATION_DIRNAME), l'attestation enregistre inode/dev, pour empêcher qu'un utilisateur change de système de fichiers en cours et que le chemin du fichier mémoire dérive.
Résumé
Les fichiers mémoire sont la couche de persistance cross-session de l'agent OpenClaw. MEMORY.md est le fichier racine canonical, memory/ la mémoire auxiliaire, extra paths pour référencer cross-workspace. memory-host-sdk gère scan, enregistrement QMD collection, lecture paginée; les plugins se branchent via la facade SDK; la commande doctor gère la migration legacy et la réparation des fichiers divisés. Comment la memory est synchronisée par le context engine à la compression dans Context Engine; dans quelle session la mémoire est chargée via le cycle de vie de session boucle principale de l'agent.
Pour comparer avec la documentation officielle: Memory docs · README.