Memory

Das OpenClaw-Gedaechtnis besteht aus purem Markdown im Agent-Workspace. Die Dateien sind die Wahrheitsquelle; das Modell “erinnert” sich nur an das, was auf die Festplatte geschrieben wird.

Memory-Such-Tools werden vom aktiven Memory-Plugin bereitgestellt (Standard: memory-core). Deaktiviere Memory-Plugins mit plugins.slots.memory = "none".

Memory-Dateien (Markdown)

Das Standard-Workspace-Layout nutzt zwei Gedaechtnis-Schichten:

  • memory/YYYY-MM-DD.md
    • Tagesprotokoll (nur anfuegen).
    • Lies heute und gestern beim Sitzungsstart.
  • MEMORY.md (optional)
    • Kuratiertes Langzeitgedaechtnis.
    • Nur in der privaten Hauptsitzung laden (nie in Gruppenkontexten).

Diese Dateien liegen im Workspace (agents.defaults.workspace, Standard ~/.openclaw/workspace). Siehe Agent-Workspace fuer den vollstaendigen Aufbau.

Memory-Tools

OpenClaw stellt dem Agenten zwei Tools fuer diese Markdown-Dateien bereit:

  • memory_search — semantische Suche ueber indizierte Snippets.
  • memory_get — gezieltes Lesen einer bestimmten Markdown-Datei/Zeilenbereich.

memory_get reagiert jetzt graceful, wenn eine Datei nicht existiert (zum Beispiel das heutige Tagesprotokoll vor dem ersten Schreiben). Sowohl der eingebaute Manager als auch das QMD-Backend geben { text: "", path } zurueck, statt ENOENT zu werfen, damit Agenten “noch nichts aufgezeichnet” handhaben und ihren Workflow fortsetzen koennen, ohne den Tool-Call in try/catch-Logik zu verpacken.

Wann Memory schreiben

  • Entscheidungen, Praeferenzen und dauerhafte Fakten kommen in MEMORY.md.
  • Tagesnotizen und laufender Kontext kommen in memory/YYYY-MM-DD.md.
  • Wenn jemand sagt “merk dir das”, schreibe es auf (behalte es nicht im RAM).
  • Dieser Bereich entwickelt sich noch weiter. Es hilft, das Modell daran zu erinnern, Erinnerungen zu speichern; es weiss dann, was zu tun ist.
  • Wenn etwas haften bleiben soll, bitte den Bot, es ins Memory zu schreiben.

Automatischer Memory-Flush (Pre-Compaction-Ping)

Wenn eine Sitzung kurz vor der Auto-Compaction steht, loest OpenClaw einen stillen, agentischen Turn aus, der das Modell daran erinnert, dauerhaftes Memory zu schreiben, bevor der Kontext komprimiert wird. Die Standard-Prompts sagen explizit, dass das Modell antworten darf, aber normalerweise ist NO_REPLY die richtige Antwort, damit der Nutzer diesen Turn nie sieht.

Das wird gesteuert durch agents.defaults.compaction.memoryFlush:

{
  agents: {
    defaults: {
      compaction: {
        reserveTokensFloor: 20000,
        memoryFlush: {
          enabled: true,
          softThresholdTokens: 4000,
          systemPrompt: "Session nearing compaction. Store durable memories now.",
          prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
        },
      },
    },
  },
}

Details:

  • Soft Threshold: Der Flush wird ausgeloest, wenn die geschaetzte Token-Anzahl der Sitzung contextWindow - reserveTokensFloor - softThresholdTokens ueberschreitet.
  • Standardmaessig still: Die Prompts enthalten NO_REPLY, sodass nichts zugestellt wird.
  • Zwei Prompts: Ein User-Prompt plus ein System-Prompt haengen die Erinnerung an.
  • Ein Flush pro Compaction-Zyklus (getrackt in sessions.json).
  • Workspace muss beschreibbar sein: Wenn die Sitzung in einer Sandbox mit workspaceAccess: "ro" oder "none" laeuft, wird der Flush uebersprungen.

Fuer den vollstaendigen Compaction-Lebenszyklus siehe Sitzungsverwaltung und Compaction.

Vektor-Memory-Suche

OpenClaw kann einen kleinen Vektorindex ueber MEMORY.md und memory/*.md aufbauen, damit semantische Abfragen verwandte Notizen finden koennen, auch wenn die Formulierung abweicht.

Standardwerte:

  • Standardmaessig aktiviert.
  • Ueberwacht Memory-Dateien auf Aenderungen (mit Debounce).
  • Konfiguriere die Memory-Suche unter agents.defaults.memorySearch (nicht auf Top-Level memorySearch).
  • Nutzt standardmaessig Remote-Embeddings. Wenn memorySearch.provider nicht gesetzt ist, waehlt OpenClaw automatisch:
    1. local, wenn ein memorySearch.local.modelPath konfiguriert ist und die Datei existiert.
    2. openai, wenn ein OpenAI-Schluessel aufgeloest werden kann.
    3. gemini, wenn ein Gemini-Schluessel aufgeloest werden kann.
    4. voyage, wenn ein Voyage-Schluessel aufgeloest werden kann.
    5. mistral, wenn ein Mistral-Schluessel aufgeloest werden kann.
    6. Andernfalls bleibt die Memory-Suche deaktiviert, bis sie konfiguriert wird.
  • Der lokale Modus nutzt node-llama-cpp und erfordert moeglicherweise pnpm approve-builds.
  • Nutzt sqlite-vec (wenn verfuegbar) zur Beschleunigung der Vektorsuche in SQLite.
  • memorySearch.provider = "ollama" wird ebenfalls unterstuetzt fuer lokale/selbstgehostete Ollama-Embeddings (/api/embeddings), wird aber nicht automatisch gewaehlt.

Remote-Embeddings erfordern einen API-Schluessel fuer den Embedding-Provider. OpenClaw loest Schluessel aus Auth-Profilen, models.providers.*.apiKey oder Umgebungsvariablen auf. Codex-OAuth deckt nur Chat/Completions ab und genuegt nicht fuer Embeddings bei der Memory-Suche. Fuer Gemini verwende GEMINI_API_KEY oder models.providers.google.apiKey. Fuer Voyage verwende VOYAGE_API_KEY oder models.providers.voyage.apiKey. Fuer Mistral verwende MISTRAL_API_KEY oder models.providers.mistral.apiKey. Ollama braucht typischerweise keinen echten API-Schluessel (ein Platzhalter wie OLLAMA_API_KEY=ollama-local reicht, wenn es von der lokalen Policy verlangt wird). Bei einem benutzerdefinierten OpenAI-kompatiblen Endpunkt setze memorySearch.remote.apiKey (und optional memorySearch.remote.headers).

QMD-Backend (experimentell)

Setze memory.backend = "qmd", um den eingebauten SQLite-Indexer durch QMD zu ersetzen: einen lokalen Such-Sidecar, der BM25 + Vektoren + Reranking kombiniert. Markdown bleibt die Wahrheitsquelle; OpenClaw nutzt QMD per Shell fuer den Abruf. Wichtige Punkte:

Voraussetzungen

  • Standardmaessig deaktiviert. Per Config aktivieren (memory.backend = "qmd").
  • Installiere die QMD-CLI separat (bun install -g https://github.com/tobi/qmd oder ein Release herunterladen) und stelle sicher, dass die qmd-Binary im PATH des Gateways liegt.
  • QMD braucht einen SQLite-Build, der Extensions erlaubt (brew install sqlite auf macOS).
  • QMD laeuft vollstaendig lokal ueber Bun + node-llama-cpp und laedt GGUF-Modelle automatisch von HuggingFace beim ersten Gebrauch herunter (kein separater Ollama-Daemon noetig).
  • Das Gateway betreibt QMD in einem eigenstaendigen XDG-Home unter ~/.openclaw/agents/<agentId>/qmd/, indem es XDG_CONFIG_HOME und XDG_CACHE_HOME setzt.
  • OS-Unterstuetzung: macOS und Linux funktionieren sofort nach Installation von Bun + SQLite. Windows wird am besten ueber WSL2 unterstuetzt.

Wie der Sidecar laeuft

  • Das Gateway schreibt ein eigenstaendiges QMD-Home unter ~/.openclaw/agents/<agentId>/qmd/ (Config + Cache + SQLite-DB).
  • Collections werden ueber qmd collection add aus memory.qmd.paths (plus Standard-Workspace-Memory-Dateien) erstellt, dann laufen qmd update + qmd embed beim Start und in einem konfigurierbaren Intervall (memory.qmd.update.interval, Standard 5 Min.).
  • Das Gateway initialisiert den QMD-Manager jetzt beim Start, sodass periodische Update-Timer auch vor dem ersten memory_search-Aufruf aktiv sind.
  • Boot-Refresh laeuft jetzt standardmaessig im Hintergrund, damit der Chat-Start nicht blockiert wird; setze memory.qmd.update.waitForBootSync = true, um das vorherige blockierende Verhalten beizubehalten.
  • Suchen laufen ueber memory.qmd.searchMode (Standard qmd search --json; unterstuetzt auch vsearch und query). Wenn der gewaehlte Modus Flags auf deinem QMD-Build ablehnt, versucht OpenClaw es erneut mit qmd query. Wenn QMD fehlschlaegt oder die Binary fehlt, faellt OpenClaw automatisch auf den eingebauten SQLite-Manager zurueck, damit die Memory-Tools weiter funktionieren.
  • OpenClaw bietet heute kein Tuning der QMD-Embed-Batch-Groesse an; das Batch-Verhalten wird von QMD selbst gesteuert.
  • Die erste Suche kann langsam sein: QMD laedt moeglicherweise lokale GGUF-Modelle (Reranker/Query Expansion) beim ersten qmd query-Durchlauf herunter.
    • OpenClaw setzt XDG_CONFIG_HOME/XDG_CACHE_HOME automatisch, wenn es QMD ausfuehrt.

    • Wenn du Modelle manuell vorab herunterladen (und denselben Index aufwaermen) willst, den OpenClaw nutzt, fuehre eine einmalige Query mit den XDG-Dirs des Agenten aus.

      OpenClaws QMD-State liegt unter deinem State-Dir (Standard ~/.openclaw). Du kannst qmd auf exakt denselben Index zeigen lassen, indem du dieselben XDG-Variablen exportierst, die OpenClaw nutzt:

      # Dasselbe State-Dir wie OpenClaw verwenden
      STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
      
      export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config"
      export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache"
      
      # (Optional) Index-Refresh + Embeddings erzwingen
      qmd update
      qmd embed
      
      # Aufwaermen / erstmaliges Herunterladen der Modelle ausloesen
      qmd query "test" -c memory-root --json >/dev/null 2>&1

Config-Oberflaeche (memory.qmd.*)

  • command (Standard qmd): ueberschreibt den Pfad zur Executable.
  • searchMode (Standard search): waehlt, welcher QMD-Befehl hinter memory_search steht (search, vsearch, query).
  • includeDefaultMemory (Standard true): indiziert automatisch MEMORY.md + memory/**/*.md.
  • paths[]: zusaetzliche Verzeichnisse/Dateien hinzufuegen (path, optionales pattern, optionaler stabiler name).
  • sessions: Session-JSONL-Indizierung aktivieren (enabled, retentionDays, exportDir).
  • update: steuert Refresh-Kadenz und Wartungsausfuehrung: (interval, debounceMs, onBoot, waitForBootSync, embedInterval, commandTimeoutMs, updateTimeoutMs, embedTimeoutMs).
  • limits: begrenzt den Recall-Payload (maxResults, maxSnippetChars, maxInjectedChars, timeoutMs).
  • scope: gleiches Schema wie session.sendPolicy. Standard ist DM-only (deny alle, allow Direktchats); lockere es, um QMD-Treffer in Gruppen/Kanaelen anzuzeigen.
    • match.keyPrefix matcht den normalisierten Sitzungsschluessel (Kleinbuchstaben, fuehrendes agent:<id>: entfernt). Beispiel: discord:channel:.
    • match.rawKeyPrefix matcht den rohen Sitzungsschluessel (Kleinbuchstaben), einschliesslich agent:<id>:. Beispiel: agent:main:discord:.
    • Legacy: match.keyPrefix: "agent:..." wird weiterhin als Raw-Key-Praefix behandelt, aber bevorzuge rawKeyPrefix fuer Klarheit.
  • Wenn scope eine Suche verweigert, loggt OpenClaw eine Warnung mit dem abgeleiteten channel/chatType, damit leere Ergebnisse leichter zu debuggen sind.
  • Snippets von ausserhalb des Workspaces erscheinen als qmd/<collection>/<relativer-pfad> in memory_search-Ergebnissen; memory_get versteht diesen Praefix und liest aus dem konfigurierten QMD-Collection-Root.
  • Wenn memory.qmd.sessions.enabled = true, exportiert OpenClaw bereinigte Sitzungstranskripte (User/Assistant-Turns) in eine dedizierte QMD-Collection unter ~/.openclaw/agents/<id>/qmd/sessions/, damit memory_search kuerzliche Konversationen abrufen kann, ohne den eingebauten SQLite-Index zu beruehren.
  • memory_search-Snippets enthalten jetzt einen Source: <pfad#zeile>-Footer, wenn memory.citations auf auto/on steht; setze memory.citations = "off", um die Pfad-Metadaten intern zu halten (der Agent empfaengt den Pfad weiterhin fuer memory_get, aber der Snippet-Text laesst den Footer weg und der System-Prompt warnt den Agenten, ihn nicht zu zitieren).

Beispiel

memory: {
  backend: "qmd",
  citations: "auto",
  qmd: {
    includeDefaultMemory: true,
    update: { interval: "5m", debounceMs: 15000 },
    limits: { maxResults: 6, timeoutMs: 4000 },
    scope: {
      default: "deny",
      rules: [
        { action: "allow", match: { chatType: "direct" } },
        // Normalisierter Sitzungsschluessel-Praefix (entfernt `agent:<id>:`).
        { action: "deny", match: { keyPrefix: "discord:channel:" } },
        // Roher Sitzungsschluessel-Praefix (einschliesslich `agent:<id>:`).
        { action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } },
      ]
    },
    paths: [
      { name: "docs", path: "~/notes", pattern: "**/*.md" }
    ]
  }
}

Zitate und Fallback

  • memory.citations gilt unabhaengig vom Backend (auto/on/off).
  • Wenn qmd laeuft, setzen wir status().backend = "qmd", damit die Diagnose zeigt, welche Engine die Ergebnisse geliefert hat. Wenn der QMD-Subprocess beendet wird oder die JSON-Ausgabe nicht geparst werden kann, loggt der Search-Manager eine Warnung und gibt den eingebauten Provider (bestehende Markdown-Embeddings) zurueck, bis QMD sich erholt.

Zusaetzliche Memory-Pfade

Wenn du Markdown-Dateien ausserhalb des Standard-Workspace-Layouts indizieren willst, fuege explizite Pfade hinzu:

agents: {
  defaults: {
    memorySearch: {
      extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
    }
  }
}

Hinweise:

  • Pfade koennen absolut oder workspace-relativ sein.
  • Verzeichnisse werden rekursiv nach .md-Dateien durchsucht.
  • Standardmaessig werden nur Markdown-Dateien indiziert.
  • Wenn memorySearch.multimodal.enabled = true, indiziert OpenClaw auch unterstuetzte Bild-/Audiodateien unter extraPaths. Standard-Memory-Roots (MEMORY.md, memory.md, memory/**/*.md) bleiben rein Markdown.
  • Symlinks werden ignoriert (Dateien oder Verzeichnisse).

Multimodale Memory-Dateien (Gemini Bild + Audio)

OpenClaw kann Bild- und Audiodateien aus memorySearch.extraPaths indizieren, wenn Gemini Embedding 2 verwendet wird:

agents: {
  defaults: {
    memorySearch: {
      provider: "gemini",
      model: "gemini-embedding-2-preview",
      extraPaths: ["assets/reference", "voice-notes"],
      multimodal: {
        enabled: true,
        modalities: ["image", "audio"], // oder ["all"]
        maxFileBytes: 10000000
      },
      remote: {
        apiKey: "YOUR_GEMINI_API_KEY"
      }
    }
  }
}

Hinweise:

  • Multimodales Memory wird aktuell nur fuer gemini-embedding-2-preview unterstuetzt.
  • Multimodale Indizierung gilt nur fuer Dateien, die ueber memorySearch.extraPaths gefunden werden.
  • Unterstuetzte Modalitaeten in dieser Phase: Bild und Audio.
  • memorySearch.fallback muss auf "none" bleiben, solange multimodales Memory aktiviert ist.
  • Passende Bild-/Audio-Datei-Bytes werden beim Indizieren zum konfigurierten Gemini-Embedding-Endpunkt hochgeladen.
  • Unterstuetzte Bildformate: .jpg, .jpeg, .png, .webp, .gif, .heic, .heif.
  • Unterstuetzte Audioformate: .mp3, .wav, .ogg, .opus, .m4a, .aac, .flac.
  • Suchanfragen bleiben Text, aber Gemini kann diese Textanfragen mit indizierten Bild-/Audio-Embeddings vergleichen.
  • memory_get liest weiterhin nur Markdown; Binaerdateien sind durchsuchbar, aber werden nicht als Rohdatei-Inhalte zurueckgegeben.

Gemini-Embeddings (nativ)

Setze den Provider auf gemini, um die Gemini-Embeddings-API direkt zu nutzen:

agents: {
  defaults: {
    memorySearch: {
      provider: "gemini",
      model: "gemini-embedding-001",
      remote: {
        apiKey: "YOUR_GEMINI_API_KEY"
      }
    }
  }
}

Hinweise:

  • remote.baseUrl ist optional (Standard ist die Gemini-API-Basis-URL).
  • remote.headers erlaubt zusaetzliche Header bei Bedarf.
  • Standard-Modell: gemini-embedding-001.
  • gemini-embedding-2-preview wird ebenfalls unterstuetzt: 8192 Token-Limit und konfigurierbare Dimensionen (768 / 1536 / 3072, Standard 3072).

Gemini Embedding 2 (Preview)

agents: {
  defaults: {
    memorySearch: {
      provider: "gemini",
      model: "gemini-embedding-2-preview",
      outputDimensionality: 3072,  // optional: 768, 1536 oder 3072 (Standard)
      remote: {
        apiKey: "YOUR_GEMINI_API_KEY"
      }
    }
  }
}

Achtung — Neuindizierung erforderlich: Der Wechsel von gemini-embedding-001 (768 Dimensionen) zu gemini-embedding-2-preview (3072 Dimensionen) aendert die Vektorgroesse. Das gilt auch, wenn du outputDimensionality zwischen 768, 1536 und 3072 aenderst. OpenClaw indiziert automatisch neu, wenn eine Modell- oder Dimensionsaenderung erkannt wird.

Wenn du einen benutzerdefinierten OpenAI-kompatiblen Endpunkt nutzen willst (OpenRouter, vLLM oder einen Proxy), kannst du die remote-Konfiguration mit dem OpenAI-Provider verwenden:

agents: {
  defaults: {
    memorySearch: {
      provider: "openai",
      model: "text-embedding-3-small",
      remote: {
        baseUrl: "https://api.example.com/v1/",
        apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
        headers: { "X-Custom-Header": "value" }
      }
    }
  }
}

Wenn du keinen API-Schluessel setzen willst, verwende memorySearch.provider = "local" oder setze memorySearch.fallback = "none".

Fallbacks:

  • memorySearch.fallback kann openai, gemini, voyage, mistral, ollama, local oder none sein.
  • Der Fallback-Provider wird nur genutzt, wenn der primaere Embedding-Provider fehlschlaegt.

Batch-Indizierung (OpenAI + Gemini + Voyage):

  • Standardmaessig deaktiviert. Setze agents.defaults.memorySearch.remote.batch.enabled = true zum Aktivieren fuer grosse Korpora (OpenAI, Gemini und Voyage).
  • Das Standardverhalten wartet auf den Batch-Abschluss; passe remote.batch.wait, remote.batch.pollIntervalMs und remote.batch.timeoutMinutes nach Bedarf an.
  • Setze remote.batch.concurrency, um zu steuern, wie viele Batch-Jobs parallel eingereicht werden (Standard: 2).
  • Der Batch-Modus greift, wenn memorySearch.provider = "openai" oder "gemini" und nutzt den entsprechenden API-Schluessel.
  • Gemini-Batch-Jobs nutzen den asynchronen Embeddings-Batch-Endpunkt und erfordern Gemini-Batch-API-Verfuegbarkeit.

Warum OpenAI-Batch schnell und guenstig ist:

  • Fuer grosse Backfills ist OpenAI typischerweise die schnellste Option, die wir unterstuetzen, weil wir viele Embedding-Requests in einem einzigen Batch-Job einreichen und OpenAI sie asynchron verarbeiten lassen koennen.
  • OpenAI bietet vergueenstigte Preise fuer Batch-API-Workloads, sodass grosse Indizierungslaeufe normalerweise guenstiger sind als das synchrone Senden derselben Requests.
  • Siehe die OpenAI-Batch-API-Dokumentation und Preise fuer Details:

Config-Beispiel:

agents: {
  defaults: {
    memorySearch: {
      provider: "openai",
      model: "text-embedding-3-small",
      fallback: "openai",
      remote: {
        batch: { enabled: true, concurrency: 2 }
      },
      sync: { watch: true }
    }
  }
}

Tools:

  • memory_search — gibt Snippets mit Datei- und Zeilenbereichen zurueck.
  • memory_get — liest Memory-Datei-Inhalt nach Pfad.

Lokaler Modus:

  • Setze agents.defaults.memorySearch.provider = "local".
  • Gib agents.defaults.memorySearch.local.modelPath an (GGUF oder hf:-URI).
  • Optional: Setze agents.defaults.memorySearch.fallback = "none", um Remote-Fallback zu vermeiden.

Wie die Memory-Tools funktionieren

  • memory_search durchsucht semantisch Markdown-Chunks (~400 Token Ziel, 80-Token Ueberlappung) aus MEMORY.md + memory/**/*.md. Es gibt Snippet-Text (begrenzt auf ~700 Zeichen), Dateipfad, Zeilenbereich, Score, Provider/Modell und ob von local auf Remote-Embeddings zurueckgegriffen wurde, zurueck. Kein vollstaendiger Datei-Payload wird zurueckgegeben.
  • memory_get liest eine bestimmte Memory-Markdown-Datei (workspace-relativ), optional ab einer Startzeile und fuer N Zeilen. Pfade ausserhalb von MEMORY.md / memory/ werden abgelehnt.
  • Beide Tools sind nur aktiviert, wenn memorySearch.enabled fuer den Agenten true ergibt.

Was indiziert wird (und wann)

  • Dateityp: nur Markdown (MEMORY.md, memory/**/*.md).
  • Index-Speicher: pro Agent SQLite unter ~/.openclaw/memory/<agentId>.sqlite (konfigurierbar ueber agents.defaults.memorySearch.store.path, unterstuetzt {agentId}-Token).
  • Aktualitaet: Watcher auf MEMORY.md + memory/ markiert den Index als dirty (Debounce 1,5s). Sync wird beim Sitzungsstart, bei der Suche oder in einem Intervall geplant und laeuft asynchron. Sitzungstranskripte nutzen Delta-Schwellenwerte, um die Hintergrund-Synchronisation auszuloesen.
  • Neuindizierungs-Trigger: Der Index speichert den Embedding-Provider/Modell- + Endpunkt-Fingerprint + Chunking-Parameter. Wenn sich einer davon aendert, setzt OpenClaw den gesamten Store automatisch zurueck und indiziert neu.

Hybridsuche (BM25 + Vektor)

Wenn aktiviert, kombiniert OpenClaw:

  • Vektor-Aehnlichkeit (semantischer Abgleich, Formulierung kann abweichen)
  • BM25-Keyword-Relevanz (exakte Tokens wie IDs, Env-Vars, Code-Symbole)

Wenn Volltextsuche auf deiner Plattform nicht verfuegbar ist, faellt OpenClaw auf reine Vektorsuche zurueck.

Warum Hybrid?

Vektorsuche ist grossartig bei “das bedeutet dasselbe”:

  • “Mac Studio Gateway Host” vs. “die Maschine, auf der das Gateway laeuft”
  • “Dateiaktualisierungen entprellen” vs. “nicht bei jedem Schreiben indizieren”

Aber sie kann schwach sein bei exakten, hochsignifikanten Tokens:

  • IDs (a828e60, b3b9895a...)
  • Code-Symbole (memorySearch.query.hybrid)
  • Fehlerstrings (“sqlite-vec unavailable”)

BM25 (Volltext) ist das Gegenteil: stark bei exakten Tokens, schwaecher bei Umschreibungen. Hybridsuche ist der pragmatische Mittelweg: beide Retrieval-Signale nutzen, damit du gute Ergebnisse sowohl fuer “natuerlichsprachige” Anfragen als auch fuer “Nadel im Heuhaufen”-Anfragen bekommst.

Wie wir Ergebnisse zusammenfuehren (aktuelles Design)

Implementierungsskizze:

  1. Einen Kandidatenpool von beiden Seiten abrufen:
  • Vektor: Top maxResults * candidateMultiplier nach Kosinus-Aehnlichkeit.
  • BM25: Top maxResults * candidateMultiplier nach FTS5-BM25-Rang (niedriger ist besser).
  1. BM25-Rang in einen 0..1-Score umrechnen:
  • textScore = 1 / (1 + max(0, bm25Rank))
  1. Kandidaten nach Chunk-ID vereinigen und einen gewichteten Score berechnen:
  • finalScore = vectorWeight * vectorScore + textWeight * textScore

Hinweise:

  • vectorWeight + textWeight wird in der Config-Aufloesung auf 1.0 normalisiert, sodass Gewichte als Prozente funktionieren.
  • Wenn Embeddings nicht verfuegbar sind (oder der Provider einen Null-Vektor liefert), fuehren wir trotzdem BM25 aus und geben Keyword-Treffer zurueck.
  • Wenn FTS5 nicht erstellt werden kann, behalten wir reine Vektorsuche bei (kein harter Fehler).

Das ist nicht “IR-theoretisch perfekt”, aber es ist einfach, schnell und verbessert tendenziell Recall/Precision bei echten Notizen. Wenn wir spaeter anspruchsvoller werden wollen, sind gaengige naechste Schritte Reciprocal Rank Fusion (RRF) oder Score-Normalisierung (Min/Max oder z-Score) vor dem Mischen.

Nachbearbeitungs-Pipeline

Nach dem Zusammenfuehren von Vektor- und Keyword-Scores verfeinern zwei optionale Nachbearbeitungsstufen die Ergebnisliste, bevor sie den Agenten erreicht:

Vektor + Keyword -> Gewichtetes Merging -> Temporal Decay -> Sortierung -> MMR -> Top-K-Ergebnisse

Beide Stufen sind standardmaessig deaktiviert und koennen unabhaengig voneinander aktiviert werden.

MMR-Re-Ranking (Diversitaet)

Wenn die Hybridsuche Ergebnisse liefert, koennen mehrere Chunks aehnlichen oder ueberlappenden Inhalt enthalten. Zum Beispiel koennte die Suche nach “Heimnetzwerk-Setup” fuenf nahezu identische Snippets aus verschiedenen Tagesnotizen liefern, die alle dieselbe Router-Konfiguration erwaehnen.

MMR (Maximal Marginal Relevance) rankt die Ergebnisse neu, um Relevanz mit Diversitaet auszubalancieren, und stellt sicher, dass die Top-Ergebnisse verschiedene Aspekte der Anfrage abdecken, statt dieselben Informationen zu wiederholen.

Funktionsweise:

  1. Ergebnisse werden nach ihrem urspruenglichen Relevanz-Score bewertet (Vektor + BM25 gewichteter Score).
  2. MMR waehlt iterativ Ergebnisse, die maximieren: lambda x Relevanz - (1-lambda) x max_Aehnlichkeit_zu_Ausgewaehlten.
  3. Die Aehnlichkeit zwischen Ergebnissen wird mittels Jaccard-Text-Aehnlichkeit auf tokenisiertem Inhalt gemessen.

Der lambda-Parameter steuert den Kompromiss:

  • lambda = 1.0 -> reine Relevanz (keine Diversitaetsstrafe)
  • lambda = 0.0 -> maximale Diversitaet (ignoriert Relevanz)
  • Standard: 0.7 (ausgewogen, leichte Relevanz-Tendenz)

Beispiel — Anfrage: “home network setup”

Gegeben diese Memory-Dateien:

memory/2026-02-10.md  -> "Configured Omada router, set VLAN 10 for IoT devices"
memory/2026-02-08.md  -> "Configured Omada router, moved IoT to VLAN 10"
memory/2026-02-05.md  -> "Set up AdGuard DNS on 192.168.10.2"
memory/network.md     -> "Router: Omada ER605, AdGuard: 192.168.10.2, VLAN 10: IoT"

Ohne MMR — Top 3 Ergebnisse:

1. memory/2026-02-10.md  (score: 0.92)  <- Router + VLAN
2. memory/2026-02-08.md  (score: 0.89)  <- Router + VLAN (fast-Duplikat!)
3. memory/network.md     (score: 0.85)  <- Referenzdokument

Mit MMR (lambda=0.7) — Top 3 Ergebnisse:

1. memory/2026-02-10.md  (score: 0.92)  <- Router + VLAN
2. memory/network.md     (score: 0.85)  <- Referenzdokument (divers!)
3. memory/2026-02-05.md  (score: 0.78)  <- AdGuard DNS (divers!)

Das Fast-Duplikat vom 8. Februar faellt heraus, und der Agent bekommt drei verschiedene Informationsstuecke.

Wann aktivieren: Wenn memory_search redundante oder fast-doppelte Snippets zurueckgibt, besonders bei Tagesnotizen, die oft aehnliche Informationen ueber Tage hinweg wiederholen.

Temporal Decay (Aktualitaetsboost)

Agenten mit Tagesnotizen sammeln im Laufe der Zeit hunderte datierte Dateien an. Ohne Decay kann eine gut formulierte Notiz von vor sechs Monaten das gestrige Update zum selben Thema uebertrumpfen.

Temporal Decay wendet einen exponentiellen Multiplikator auf Scores basierend auf dem Alter jedes Ergebnisses an, sodass neuere Erinnerungen natuerlich hoeher ranken, waehrend alte verblassen:

decayedScore = score x e^(-lambda x ageInDays)

wobei lambda = ln(2) / halfLifeDays.

Mit der Standard-Halbwertszeit von 30 Tagen:

  • Heutige Notizen: 100% des Original-Scores
  • Vor 7 Tagen: ~84%
  • Vor 30 Tagen: 50%
  • Vor 90 Tagen: 12,5%
  • Vor 180 Tagen: ~1,6%

Evergreen-Dateien werden nie gealtert:

  • MEMORY.md (Root-Memory-Datei)
  • Nicht-datierte Dateien in memory/ (z.B. memory/projects.md, memory/network.md)
  • Diese enthalten dauerhafte Referenzinformationen, die immer normal ranken sollen.

Datierte Tagesdateien (memory/YYYY-MM-DD.md) verwenden das aus dem Dateinamen extrahierte Datum. Andere Quellen (z.B. Sitzungstranskripte) greifen auf die Dateimodifikationszeit (mtime) zurueck.

Beispiel — Anfrage: “was ist Rods Arbeitsplan?”

Gegeben diese Memory-Dateien (heute ist der 10. Feb):

memory/2025-09-15.md  -> "Rod works Mon-Fri, standup at 10am, pairing at 2pm"  (148 Tage alt)
memory/2026-02-10.md  -> "Rod has standup at 14:15, 1:1 with Zeb at 14:45"    (heute)
memory/2026-02-03.md  -> "Rod started new team, standup moved to 14:15"        (7 Tage alt)

Ohne Decay:

1. memory/2025-09-15.md  (score: 0.91)  <- bester semantischer Match, aber veraltet!
2. memory/2026-02-10.md  (score: 0.82)
3. memory/2026-02-03.md  (score: 0.80)

Mit Decay (halfLife=30):

1. memory/2026-02-10.md  (score: 0.82 x 1.00 = 0.82)  <- heute, kein Decay
2. memory/2026-02-03.md  (score: 0.80 x 0.85 = 0.68)  <- 7 Tage, milder Decay
3. memory/2025-09-15.md  (score: 0.91 x 0.03 = 0.03)  <- 148 Tage, fast verschwunden

Die veraltete September-Notiz rutscht trotz des besten rohen semantischen Matches ans Ende.

Wann aktivieren: Wenn dein Agent Monate von Tagesnotizen hat und alte, veraltete Informationen neueren Kontext uebertrumpfen. Eine Halbwertszeit von 30 Tagen funktioniert gut fuer tagesnotiz-lastige Workflows; erhoehe sie (z.B. 90 Tage), wenn du oefter auf aeltere Notizen zurueckgreifst.

Konfiguration

Beide Features werden unter memorySearch.query.hybrid konfiguriert:

agents: {
  defaults: {
    memorySearch: {
      query: {
        hybrid: {
          enabled: true,
          vectorWeight: 0.7,
          textWeight: 0.3,
          candidateMultiplier: 4,
          // Diversitaet: redundante Ergebnisse reduzieren
          mmr: {
            enabled: true,    // Standard: false
            lambda: 0.7       // 0 = max. Diversitaet, 1 = max. Relevanz
          },
          // Aktualitaet: neuere Erinnerungen boosten
          temporalDecay: {
            enabled: true,    // Standard: false
            halfLifeDays: 30  // Score halbiert sich alle 30 Tage
          }
        }
      }
    }
  }
}

Du kannst jedes Feature unabhaengig aktivieren:

  • Nur MMR — nuetzlich, wenn du viele aehnliche Notizen hast, aber das Alter egal ist.
  • Nur Temporal Decay — nuetzlich, wenn Aktualitaet wichtig ist, aber deine Ergebnisse bereits divers sind.
  • Beide — empfohlen fuer Agenten mit grossen, langlebigen Tagesnotiz-Historien.

Embedding-Cache

OpenClaw kann Chunk-Embeddings in SQLite cachen, damit Neuindizierung und haeufige Updates (besonders Sitzungstranskripte) unveraenderten Text nicht erneut embedden.

Config:

agents: {
  defaults: {
    memorySearch: {
      cache: {
        enabled: true,
        maxEntries: 50000
      }
    }
  }
}

Session-Memory-Suche (experimentell)

Du kannst optional Sitzungstranskripte indizieren und ueber memory_search verfuegbar machen. Das ist hinter einem experimentellen Flag geschuetzt.

agents: {
  defaults: {
    memorySearch: {
      experimental: { sessionMemory: true },
      sources: ["memory", "sessions"]
    }
  }
}

Hinweise:

  • Session-Indizierung ist opt-in (standardmaessig deaktiviert).
  • Session-Updates werden entprellt und asynchron indiziert, sobald sie Delta-Schwellenwerte ueberschreiten (Best-Effort).
  • memory_search blockiert nie auf Indizierung; Ergebnisse koennen leicht veraltet sein, bis die Hintergrund-Synchronisation abgeschlossen ist.
  • Ergebnisse enthalten weiterhin nur Snippets; memory_get bleibt auf Memory-Dateien beschraenkt.
  • Session-Indizierung ist pro Agent isoliert (nur die Sitzungsprotokolle dieses Agenten werden indiziert).
  • Sitzungsprotokolle liegen auf der Festplatte (~/.openclaw/agents/<agentId>/sessions/*.jsonl). Jeder Prozess/Nutzer mit Dateisystemzugriff kann sie lesen, daher behandle Festplattenzugriff als Vertrauensgrenze. Fuer strengere Isolation fuehre Agenten unter separaten OS-Nutzern oder Hosts aus.

Delta-Schwellenwerte (Standardwerte gezeigt):

agents: {
  defaults: {
    memorySearch: {
      sync: {
        sessions: {
          deltaBytes: 100000,   // ~100 KB
          deltaMessages: 50     // JSONL-Zeilen
        }
      }
    }
  }
}

SQLite-Vektor-Beschleunigung (sqlite-vec)

Wenn die sqlite-vec-Extension verfuegbar ist, speichert OpenClaw Embeddings in einer SQLite-virtuellen Tabelle (vec0) und fuehrt Vektor-Distanz-Abfragen in der Datenbank aus. Das haelt die Suche schnell, ohne jedes Embedding in JS laden zu muessen.

Konfiguration (optional):

agents: {
  defaults: {
    memorySearch: {
      store: {
        vector: {
          enabled: true,
          extensionPath: "/path/to/sqlite-vec"
        }
      }
    }
  }
}

Hinweise:

  • enabled ist standardmaessig true; wenn deaktiviert, faellt die Suche auf In-Process-Kosinus-Aehnlichkeit ueber gespeicherte Embeddings zurueck.
  • Wenn die sqlite-vec-Extension fehlt oder nicht geladen werden kann, loggt OpenClaw den Fehler und faehrt mit dem JS-Fallback fort (keine Vektortabelle).
  • extensionPath ueberschreibt den mitgelieferten sqlite-vec-Pfad (nuetzlich fuer eigene Builds oder nicht-standardmaessige Installationsorte).

Lokaler Embedding-Auto-Download

  • Standard lokales Embedding-Modell: hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf (~0,6 GB).
  • Wenn memorySearch.provider = "local", loest node-llama-cpp den modelPath auf; wenn das GGUF fehlt, wird es automatisch heruntergeladen in den Cache (oder local.modelCacheDir, wenn gesetzt), dann geladen. Downloads werden beim Retry fortgesetzt.
  • Native-Build-Anforderung: Fuehre pnpm approve-builds aus, waehle node-llama-cpp, dann pnpm rebuild node-llama-cpp.
  • Fallback: Wenn das lokale Setup fehlschlaegt und memorySearch.fallback = "openai", wechseln wir automatisch zu Remote-Embeddings (openai/text-embedding-3-small, sofern nicht ueberschrieben) und protokollieren den Grund.

Beispiel fuer benutzerdefinierten OpenAI-kompatiblen Endpunkt

agents: {
  defaults: {
    memorySearch: {
      provider: "openai",
      model: "text-embedding-3-small",
      remote: {
        baseUrl: "https://api.example.com/v1/",
        apiKey: "YOUR_REMOTE_API_KEY",
        headers: {
          "X-Organization": "org-id",
          "X-Project": "project-id"
        }
      }
    }
  }
}

Hinweise:

  • remote.* hat Vorrang vor models.providers.openai.*.
  • remote.headers werden mit OpenAI-Headern zusammengefuehrt; Remote gewinnt bei Schluesselkonflikten. Lasse remote.headers weg, um die OpenAI-Standards zu nutzen.