0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【#9】 OpenClaw を読み解く — 記憶のスロットの限定、二重記憶しないための仕組み

0
Last updated at Posted at 2026-06-27

【#9】 OpenClaw を読み解く — 記憶のスロットの限定、二重記憶しないための仕組み

本記事のコード参照は OpenClaw maincee2aca409(version 2026.6.10)時点。行番号は更新でズレ得ます。

#08 のセッションは「ひとつの会話」の記憶でした。今回は会話を越えて持続する長期メモリです。OpenClaw のメモリは複数のプラグインで実装されますが、「同時に有効化できるのは1つだけ」という独特の排他スロットを持ちます。src/memory/packages/memory-host-sdk、そして extensions/memory-* を読み解きます。

ただ一つの席 — 記憶が二重化しないための排他則

VISION.md:84 が宣言します。

Memory is a special plugin slot where only one memory plugin can be active at a time.

複数のメモリバックエンドが同時に走ると、記憶の所在が二重化して破綻します。だから排他スロット。設定では plugins.slots.memorysrc/config/types.plugins.ts:43)で「どのプラグインがメモリスロットを所有するか」を選びます("none" で無効化)。

強制は src/plugins/memory-runtime.ts:11resolveMemoryRuntimePluginIds() です。

function resolveMemoryRuntimePluginIds(config): string[] {
  const plugins = normalizePluginsConfig(config.plugins);
  const memorySlot = plugins.slots.memory;
  if (!plugins.enabled || typeof memorySlot !== "string" || memorySlot.trim().length === 0)
    return [];
  const pluginId = memorySlot.trim();
  if (plugins.deny.includes(pluginId) || plugins.entries[pluginId]?.enabled === false)
    return [];
  return [pluginId];   // 必ず1つだけ
}

返り値は最大でも要素1つの配列。createPluginRegistry(#04)の registerMemoryCapabilitysrc/plugins/types.ts:2882)が、この排他 capability を登録します。

記憶への問いかけ方 — MemorySearchManager という契約

メモリプラグインが実装すべき契約は MemorySearchManagerpackages/memory-host-sdk/src/host/types.ts:144)です。

export interface MemorySearchManager {
  search(query: string, opts?: {
    maxResults?: number; minScore?: number; sessionKey?: string;
    qmdSearchModeOverride?: "query" | "search" | "vsearch";
    sources?: MemorySource[]; signal?: AbortSignal;
  }): Promise<MemorySearchResult[]>;

  readFile(params: { relPath: string; from?: number; lines?: number }): Promise<MemoryReadResult>;
  status(): MemoryProviderStatus;
  sync?(params?: { reason?: string; force?: boolean; sessionFiles?: string[] }): Promise<void>;

  probeEmbeddingAvailability(): Promise<MemoryEmbeddingProbeResult>;
  probeVectorAvailability(): Promise<boolean>;
  close?(): Promise<void>;
}

search(検索)、readFile(読み取り)、status(状態)が必須。sync(同期)や埋め込み可用性のプローブはオプション。コアはこの契約だけを知り、背後が markdown ファイルか LanceDB かを問いません——#04 の「capability 駆動」がメモリでも貫かれています。capability 自体は MemoryPluginCapabilitysrc/plugins/memory-state.ts:145)で、promptBuilder / runtimeMemorySearchManager を包む)/ flushPlanResolver / publicArtifacts を持ちます。

四つの記憶のかたち — 役割の分担

スロットを所有できるのは memory-core か memory-lancedb。wiki と active-memory はそれを補完する非排他プラグインです。

プラグイン 役割 特徴
memory-core 組み込みのファイルベース長期メモリ MEMORY.md + memory/*.md、セッション索引、FTS+ベクトルのハイブリッド検索(QMD)、"dreaming"(light/REM/deep)フェーズ
memory-lancedb ベクトル特化の排他メモリ LanceDB ベクトルストア、OpenAI/Anthropic 埋め込み、毎メッセージ自動 recall、自動 capture、カテゴリ別重要度スコア
memory-wiki 永続ナレッジ vault(別スロット) Obsidian 互換 markdown、決定論的索引とバックリンク、構造化クレーム+エビデンス、wiki_search/wiki_get
active-memory 返信前のブロッキング recall(非排他) 返信前にサブエージェントを走らせ、限定的なメモリ文脈を注入

extensions/memory-wiki/README.md が立ち位置を明言します。「This plugin is separate from the active memory plugin. The active memory plugin still handles recall, promotion, and dreaming.」wiki はスロットを取らず、active なメモリプラグインを補う supplement です。

エージェントに開かれた窓 — 記憶を呼ぶツール

メモリは ツールとしてエージェントに公開されます。スロットに何が乗っているかで注入されるツールが変わります。

  • memory-core: memory_search / memory_get
  • memory-lancedb: memory_recall / memory_store(排他)
  • memory-wiki: wiki_search / wiki_get(非排他の追加)
  • active-memory: ブロッキングサブエージェントが使うツールを設定(既定は memory_search + memory_get、lancedb なら memory_recall

memory_search の description が運用思想を物語ります(extensions/memory-core/src/tools.ts:393)。

return createMemoryTool({
  name: "memory_search",
  description: "Mandatory recall step: semantically search MEMORY.md + memory/*.md ... before answering questions about prior work, decisions, dates, people, preferences, or todos. Optional `corpus=wiki` or `corpus=all` ...",
  parameters: MemorySearchSchema,
});

「過去の作業・決定・日付・人・好み・TODO を答える前の必須の recall ステップ」。corpus パラメータで wiki やセッションも横断検索できます。active-memory はこのツールを「返信前に必ず走るサブエージェント」で包み、文脈を先読みさせる仕掛けです(extensions/active-memory/index.ts:61)。

ルート AGENTS.md のメモリ注意書きも実務的です。「Memory wiki prompt digest stays tiny; prefer wiki_search / wiki_get; verify contact data before use; source-class provenance for generated people facts.」wiki のプロンプトダイジェストは小さく保ち、生成された人物事実は出所を明示し使用前に検証せよ、と。

意味を数に変える — ローカルとリモートの埋め込み

ベクトル検索には埋め込みが要ります。OpenClaw は両対応です。

  • ローカル埋め込みpackages/memory-host-sdk/src/host/embeddings.ts:69): node-llama-cpp + GGUF モデル。ワーカースレッドまたはインプロセスで遅延ロード。local.modelPath 等で設定。
  • リモート埋め込み: memory-lancedb が OpenAI / Anthropic を使う。getMemoryEmbeddingProvider(providerId, cfg) でプロバイダを解決。

可用性は probeEmbeddingAvailability() で非同期に確認し、使えなければツールは disabled: true を返し、プローブ成功後にベクトル検索を再開します。LanceDB の recall はこうです(extensions/memory-lancedb/index.ts:1537)。

let vector: number[];
try {
  vector = await embeddings.embed(normalizeRecallQuery(query, currentCfg.recallMaxChars),
    { timeoutMs: DEFAULT_TOOL_RECALL_TIMEOUT_MS });
} catch (error) {
  throw new MemoryRecallEmbeddingError(error);
}
return await db.search(vector, limit + DEFAULT_TOOL_RECALL_OVERFETCH_EXTRA, 0.1);

クエリを埋め込みベクトルに変換し、LanceDB で近傍検索。埋め込みが落ちても、MemoryRecallEmbeddingError として明示的に扱われます。

待ちすぎない約束 — 必ず締め切りを切る

メモリ検索は外部依存(埋め込みプロバイダ・ベクトルストア)を含むため、デッドラインが厳格です(extensions/memory-core/src/tools.ts:126)。

async function runMemorySearchToolWithDeadline<T>(params): Promise<...> {
  const controller = new AbortController();
  const timeoutPromise = new Promise((resolve) => {
    setTimeout(() => {
      resolve("timeout");
      controller.abort(new Error(`memory_search timed out after ${...}s`));
    }, params.timeoutMs);
  });
  const task = params.run(controller.signal);
  return await Promise.race([task, timeoutPromise]);
}

Promise.race でタイムアウトとタスクを競わせ、超過すれば AbortController で中断。メモリ検索が遅いせいで返信全体が止まる、という事故を防ぎます。active-memory はさらに「preflight に 1500ms、abort 収束に 1500ms を予約」といった細かいバジェット管理を持ちます。

まとめ — 一つの席と、背後を問わぬ契約

  • メモリは 排他スロット: 同時にアクティブなのは1プラグインだけ(resolveMemoryRuntimePluginIds が強制)。
  • 契約は MemorySearchManager(search / readFile / status が必須)で、コアは背後の実装を問わない。
  • memory-core(ファイル+QMD)/ lancedb(ベクトル排他)がスロットを取り、wiki / active-memory が非排他で補完。
  • メモリは ツールとしてエージェントに公開され、active-memory は返信前ブロッキング recall を担う。
  • 埋め込みはローカル/リモート両対応、検索は必ずタイムアウトを切る

次回予告 — すべての土台、状態とストレージへ

#10 は、これらすべての土台、状態・ストレージ層です。「SQLite only」「Kysely で生 SQL を書かない」「マイグレーションは database-first」という #01 で見た原則が、二層 DB(共有 / エージェント単位)と doctor マイグレーションとしてどう実装されているのかを読み解きます。

図1.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?