Skip to content

記憶檔案

源码版本v2026.6.11

職責

「記憶檔案」(memory files) 是 OpenClaw agent 長期持久化的 user 偏好、行為指引、專案上下文。它和 session transcript 不一樣:session 是一段對話的逐條訊息,記憶 (memory) 是跨 session 複用的「這條規則永遠生效」式筆記。OpenClaw 預設認 workspace 根目錄下的 MEMORY.md 作為根記憶檔案 (root memory file),memory/ 子目錄下的所有 .md 作為輔助記憶,agent 啟動時把它們讀進 system prompt,之後整段對話都按這些規則走。

記憶系統同時支援 multimodal:memory/ 下不只有 .md,還可以放圖片、音訊這類被 MemoryMultimodalSettings 允許的副檔名檔案,由 memory-host-sdk 做分類和索引。

設計動機

為什麼不直接把所有規則寫進 system prompt?三個理由:

  1. 跨 session 複用:system prompt 每次模型呼叫都消耗 token,記憶檔案只在 agent 啟動時載入一次,之後整個 session 複用;長期偏好寫進檔案比寫進 prompt 划算。
  2. 使用者可編輯:記憶檔案就是普通 markdown,使用者用任何編輯器改完下次啟動就生效,不必改 config 或重啟閘道。openclaw doctor --fix 還能自動遷移 legacy 檔案、合併分裂的 canonical/legacy 副本。
  3. plugin 可索引:memory-host-sdk 把記憶檔案註冊成 QMD collection,plugin 可以建向量索引、做語意檢索——這樣大段記憶不必整段塞 prompt,而是按 query 動態召回相關片段。

代價是「根記憶」檔名有 legacy 相容歷史,大小寫敏感,且 symlink 會被忽略——這些細節都由 root-memory-files.ts 統一管理。

關鍵檔案

資料流

listMemoryFiles(listMemoryFiles:150)是核心掃描入口:

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

三層: canonical root 檔案,resolveCanonicalRootMemoryFile 只回傳真實檔案(非 symlink),避免軟連結把外部目錄拖進來; memory/ 目錄走 collectMemoryFilesFromDir 遞迴,descend 鉤子拒絕 .openclaw-repair 目錄,include 鉤子用 isAllowedMemoryFilePath 過濾副檔名; extra paths 來自 config 宣告,每個路徑都過 shouldSkipRootMemoryAuxiliaryPath 防止把 legacy 檔案重複算進來。

backend-config 把這些檔案註冊成 QMD collections(resolveDefaultCollections:409)給向量索引用:

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 是精確檔名匹配,不遞迴;memory-dir 是 glob **/*.md,遞迴整個 memory/ 子樹。scopeCollectionBase 給每個 collection name 加上 agent id 前綴,這樣多 agent 共享一個 workspace 時各自的 collection 不會撞名。

doctor 掃描分裂檔案的邏輯(detectRootMemoryFiles:97)用 exactWorkspaceEntryExists 同時檢查 MEMORY.mdmemory.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 }),
  ]);

注意 listWorkspaceEntriesreaddir 拿到精確目錄條目(大小寫敏感),macOS 預設檔案系統大小寫不敏感,但 OpenClaw 還是按大小寫敏感處理:canonical 永遠是大寫 MEMORY.md,legacy 是小寫 memory.md,兩份同時存在時 canonical 優先,legacy 被 archive 到 .openclaw-repair/root-memory/<timestamp>/memory.md

整體讀寫鏈路:

邊界與失敗

  • symlink 被忽略:resolveCanonicalRootMemoryFileentry.isFile() && !entry.isSymbolicLink() 雙重判斷(src/memory/root-memory-files.ts:41-54),memory/ 目錄也用 !dirStat.isSymbolicLink() 守門。這是安全邊界,防止使用者(或惡意 plugin)用軟連結把 workspace 外的檔案拖進記憶索引。
  • 大小寫區分:檔案系統大小寫不敏感(macOS HFS+/APFS 預設)時,MEMORY.mdmemory.md 是同一個檔案;但 OpenClaw 用 exactWorkspaceEntryExists 拿到 readdir 的精確條目名,按字面匹配。這意味著 macOS 上兩份都寫會撞檔案,doctor 會把它們當 legacy-only 處理。
  • legacy 遷移不丟內容:moveLegacyRootMemoryFileToArchive 先 rename,失敗(EXDEV 跨裝置)退化為 copy + unlink(src/commands/doctor-workspace.ts:152-174)。archive 路徑帶時間戳,多次跑 doctor 不會互相覆蓋。
  • extra paths 跨 workspace:config 宣告的 extra paths 可以是絕對路徑或 workspace 相對路徑,normalizeExtraMemoryPaths(packages/memory-host-sdk/src/host/internal.ts:91-101)做 tilde 展開 + 去重,但不會把它們從 QMD collection 移出——plugin 索引時會按 absolute path 去重,避免同一檔案被索引兩次。
  • dreams.md 特殊路徑:isMemoryPathdreams.md 也認作記憶檔案(packages/memory-host-sdk/src/host/internal.ts:108-110),這個 legacy 約定保留是為了相容老使用者。canonical 永遠是 MEMORY.md,dreams.md 只是輔助記憶。
  • multimodal 副檔名:isAllowedMemoryFilePath 預設只接受 .md,但 MemoryMultimodalSettings 啟用後允許圖片/音訊副檔名走 classifyMemoryMultimodalPath 分類。這是 plugin 擴展點,core 不內建 multimodal 索引。
  • readMemoryFile 分頁:讀記憶檔案支援 from / lines 參數(packages/memory-host-sdk/src/host/read-file.ts:67),defaultLinesmaxCharsresolveAgentContextLimits 按 agent 設定決定,防捲單次讀把超大記憶檔案全塞進 prompt。
  • doctor 檢測分裂:formatRootMemoryFilesWarning 在 canonical 和 legacy 同時存在時給出警告(src/commands/doctor-workspace.ts:128-140):「Dreaming 寫 durable promotions 到 MEMORY.md,legacy 裡舊事實會被 shadow」。這是 OpenClaw 內建的「dreaming」機制(自動 promote 記憶到 MEMORY.md)和 legacy 檔案衝突的提醒。
  • plugin SDK 不直接匯 core 內部:openclaw-runtime-memory.ts(packages/memory-host-sdk/src/host/openclaw-runtime-memory.ts)只 re-export 穩定 seam(resolveCanonicalRootMemoryFile / shouldSkipRootMemoryAuxiliaryPath / buildActiveMemoryPromptSection),plugin 透過 SDK 呼叫這些函式,不直接 import src/memory/——這樣 core 重構時 plugin 不破。
  • workspace attestation:workspace 目錄在首次使用時會被 attest(src/agents/workspace.ts:44 WORKSPACE_ATTESTATION_DIRNAME),attestation 裡記錄 inode/dev,防止使用者在工作區中途換檔案系統導致記憶檔案路徑漂移。

小結

記憶檔案是 OpenClaw agent 跨 session 的持久化偏好層。MEMORY.md 是 canonical 根檔案,memory/ 子目錄是輔助記憶,extra paths 讓使用者跨 workspace 引用。memory-host-sdk 負責掃描、註冊 QMD collection、分頁讀取,plugin 透過 SDK facade 接入;doctor 指令處理 legacy 檔案遷移和分裂修復。memory 怎麼被 context engine 在壓縮時同步看 Context Engine;記憶載入進哪個 session 看工作階段生命週期 Agent 主循環

對照官方資料:Memory 文件 · README