Skip to content

Fichiers de mémoire

源码版本v2026.6.11

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:

  1. 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.
  2. É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 --fix sait aussi migrer les fichiers legacy, fusionner les copies canoniques/legacy divisées.
  3. Indexable par plugin: memory-host-sdk enregistre 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

Flux de données

listMemoryFiles (listMemoryFiles:150) est l'entrée de scan centrale:

typescript
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:

typescript
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:

typescript
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: resolveCanonicalRootMemoryFile utilise entry.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.md et memory.md sont le même fichier; mais OpenClaw utilise exactWorkspaceEntryExists pour obtenir l'entrée exacte de readdir et 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: moveLegacyRootMemoryFileToArchive rename 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: isMemoryPath considère aussi dreams.md comme un fichier mémoire (packages/memory-host-sdk/src/host/internal.ts:108-110), cette convention legacy est conservée pour compat. Le canonical reste MEMORY.md, dreams.md n'est que mémoire auxiliaire.
  • Extensions multimodales: isAllowedMemoryFilePath n'accepte par défaut que .md, mais MemoryMultimodalSettings activé autorise images/audio extensions via classifyMemoryMultimodalPath. 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), defaultLines et maxChars viennent de resolveAgentContextLimits selon 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 directement src/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:44 WORKSPACE_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.