Skip to content

Memory-Dateien

源码版本v2026.6.11

Verantwortung

„Memory-Dateien" (memory files) sind die langfristig persistenten Nutzer-Präferenzen, Verhaltensrichtlinien und Projektkontexte eines OpenClaw-Agenten. Sie unterscheiden sich vom Session-Transkript: Eine Session ist Nachrichten einer Konversation; Memory sind sessonübergreifend wiederverwendbare Notizen der Form „diese Regel gilt für immer". OpenClaw erkennt standardmäßig MEMORY.md im Workspace-Wurzelverzeichnis als Root-Memory-Datei (root memory file) und alle .md unter dem memory/-Unterverzeichnis als Hilfs-Memory; beim Agent-Start liest es sie in den System-Prompt, danach gilt die ganze Konversation nach diesen Regeln.

Das Memory-System unterstützt auch Multimodal: Unter memory/ liegen nicht nur .md, sondern auch Dateien mit Bild-/Audioerweiterungen, die MemoryMultimodalSettings erlaubt; memory-host-sdk kümmert sich um Klassifikation und Indizierung.

Designmotivation

Warum nicht alle Regeln direkt in den System-Prompt schreiben? Drei Gründe:

  1. Sessonübergreifend wiederverwendbar: Der System-Prompt verbraucht bei jedem Modelcall Token; Memory-Dateien werden nur einmal beim Agent-Start geladen und danach in der ganzen Session wiederverwendet. Langfristige Präferenzen in einer Datei sind günstiger als im Prompt.
  2. Nutzer editierbar: Memory-Dateien sind normales Markdown; ändert der Nutzer sie in einem Editor, sind sie beim nächsten Start wirksam — kein config-change oder gateway-restart. openclaw doctor --fix migriert zudem legacy-Dateien und vereinigt gespaltene canonical/legacy-Kopien.
  3. Plugin indizierbar: memory-host-sdk registriert Memory-Dateien als QMD-Collection; Plugins können Vektorindizes bauen und semantische Retrieval machen — so muss ein großer Memory-Block nicht als Ganzes in den Prompt, sondern wird per Query dynamisch relevanten Fragmente abgerufen.

Der Preis: Der Dateiname der Root-Memory hat eine legacy Kompatibilitätsgeschichte, ist case-sensitiv, und Symlinks werden ignoriert — diese Details verwaltet root-memory-files.ts zentral.

Schlüsseldateien

Datenfluss

listMemoryFiles (listMemoryFiles:150) ist der zentrale Scan-Eintritt:

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 {}

Drei Schichten: canonical-root-Datei — resolveCanonicalRootMemoryFile liefert nur echte (nicht symlink) Dateien zurück und verhindert, dass Symlinks externe Verzeichnisse hereinschleppen; das memory/-Verzeichnis läuft rekursiv durch collectMemoryFilesFromDir, dessen descend-Hook .openclaw-repair ablehnt und dessen include-Hook isAllowedMemoryFilePath Extensions filtert; extra paths kommen aus der config, jeder Pfad durch shouldSkipRootMemoryAuxiliaryPath, damit legacy-Dateien nicht doppelt gezählt werden.

backend-config registriert diese Dateien als QMD-Collections (resolveDefaultCollections:409) für die Vektorindizierung:

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 ist exakter Dateiname-Match, nicht rekursiv; memory-dir ist glob **/*.md über den gesamten memory/-Subtree. scopeCollectionBase präfixt jeden collection-Namen mit der agent id, sodass mehrere Agenten in einem Workspace ihre Collections nicht kollidieren lassen.

doctor scannt gespaltene Dateien (detectRootMemoryFiles:97) mit exactWorkspaceEntryExists, das MEMORY.md und memory.md gleichzeitig prüft:

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 }),
  ]);

Beachten Sie: listWorkspaceEntries nutzt readdir für exakte Verzeichniseinträge (case-sensitive). macOS ist default case-insensitiv, aber OpenClaw behandelt es als case-sensitive: canonical ist immer MEMORY.md, legacy memory.md; bei Koexistenz gewinnt canonical, legacy wird nach .openclaw-repair/root-memory/<timestamp>/memory.md archiviert.

Die gesamte Lese-/Schreib-Kette:

Grenzen und Fehler

  • Symlinks ignoriert: resolveCanonicalRootMemoryFile prüft doppelt entry.isFile() && !entry.isSymbolicLink() (src/memory/root-memory-files.ts:41-54); auch das memory/-Verzeichnis wird von !dirStat.isSymbolicLink() bewacht. Das ist eine Sicherheitsgrenze, die verhindert, dass Nutzer (oder bösartige Plugins) per Symlink Dateien außerhalb des Workspace in den Memory-Index ziehen.
  • Case-Unterscheidung: Bei case-insensitivem Dateisystem (macOS HFS+/APFS default) sind MEMORY.md und memory.md dieselbe Datei; OpenClaw nutzt exactWorkspaceEntryExists, um den exakten Eintragsnamen von readdir zu matchen. Das bedeutet: Werden auf macOS beide geschrieben, kollidieren sie; doctor behandelt sie als legacy-only.
  • Legacy-Migration verliert nichts: moveLegacyRootMemoryFileToArchive benennt zuerst (rename); schlägt das fehl (EXDEV cross-device), degradiert es auf copy + unlink (src/commands/doctor-workspace.ts:152-174). Der Archivpfad trägt einen Zeitstempel; mehrfaches doctor-Laufen überschreibt nicht gegenseitig.
  • extra paths workspaceübergreifend: In config deklarierte extra paths können absolute Pfade oder workspace-relative Pfade sein; normalizeExtraMemoryPaths (packages/memory-host-sdk/src/host/internal.ts:91-101) rollt tilde aus und entdoppt, verschiebt sie aber nicht aus der QMD-Collection — das Plugin entdoppt beim Indizieren nach absolutem Pfad, damit dieselbe Datei nicht zweimal indiziert wird.
  • Sonderpfad dreams.md: isMemoryPath erkennt dreams.md ebenfalls als Memory-Datei (packages/memory-host-sdk/src/host/internal.ts:108-110) — diese legacy-Konvention bleibt für Altnutzer. Canonical bleibt MEMORY.md; dreams.md ist nur Hilfs-Memory.
  • Multimodal-Erweiterungen: isAllowedMemoryFilePath akzeptiert default nur .md; aktiviert MemoryMultimodalSettings Bilder-/Audio-Erweiterungen, die über classifyMemoryMultimodalPath klassifiziert werden. Das ist eine Plugin-Erweiterungsstelle; core bringt keine Multimodal-Indizierung mit.
  • readMemoryFile seitenweise: Lesen von Memory unterstützt from / lines-Parameter (packages/memory-host-sdk/src/host/read-file.ts:67); defaultLines und maxChars werden von resolveAgentContextLimits aus der Agent-Konfiguration bestimmt, damit ein einzelner Lesevorgang nicht eine riesige Memory-Datei komplett in den Prompt drückt.
  • doctor detektiert Spaltung: formatRootMemoryFilesWarning warnt bei Koexistenz von canonical und legacy (src/commands/doctor-workspace.ts:128-140): „Dreaming schreibt dauerhafte Promotions nach MEMORY.md; alte Fakten im legacy werden überschattet". Das ist der Hinweis auf den Konflikt zwischen dem eingebauten Dreaming-Mechanismus (automatisches Promoten von Memory nach MEMORY.md) und der legacy-Datei.
  • Plugin-SDK importiert core-intern nicht direkt: openclaw-runtime-memory.ts (packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts) re-exportiert nur stabile seams (resolveCanonicalRootMemoryFile / shouldSkipRootMemoryAuxiliaryPath / buildActiveMemoryPromptSection); Plugins rufen über das SDK auf, ohne src/memory/ direkt zu importieren — so bricht ein core-Refactor nicht die Plugins.
  • Workspace-Attestation: Das Workspace-Verzeichnis wird beim ersten Gebrauch attestiert (src/agents/workspace.ts:44 WORKSPACE_ATTESTATION_DIRNAME); die Attestation notiert inode/dev, damit ein späterer Dateisystemwechsel des Nutzers den Memory-Dateipfad nicht abdriftet.

Zusammenfassung

Memory-Dateien sind die sessonübergreifend persistente Präferenzschicht eines OpenClaw-Agenten. MEMORY.md ist die canonical Wurzeldatei, memory/-Unterverzeichnis das Hilfs-Memory, extra paths erlauben workspaceübergreifende Referenzen. memory-host-sdk übernimmt Scannen, QMD-Collection-Registrierung, seitenweises Lesen; Plugins setzen über die SDK-Fassade an; doctor behandelt legacy-Migration und Spaltungsreparatur. Wie memory von der context engine beim Komprimieren synchronisiert wird, siehe Context Engine; in welche Session das Memory geladen wird, siehe Sitzungslebenszyklus Agent-Hauptschleife.

Vergleich mit offiziellen Ressourcen: Memory-Doku · README.