Skip to content

Instantly share code, notes, and snippets.

@SamSaffron
Created February 1, 2026 07:20
Show Gist options
  • Select an option

  • Save SamSaffron/9fd74d7acbe22ec5ec372e1a52cbdb5c to your computer and use it in GitHub Desktop.

Select an option

Save SamSaffron/9fd74d7acbe22ec5ec372e1a52cbdb5c to your computer and use it in GitHub Desktop.
term-llm session: 260201-1816

Session: 260201-1816

Exported from term-llm

Setup

Agent codebase
Provider Claude CLI (opus)
Model opus
Mode chat
Created 2026-02-01 07:16 UTC
Working Directory /home/sam/Source/openclaw
Tools

read_file,glob,grep,shell

Metrics

Turns Tool Calls Tokens
4 user / 8 LLM 11 241.7K in / 3.7K out

Conversation

User

How does memory work?


Assistant

grep
{
  "include": "*.ts",
  "max_results": 50,
  "pattern": "memory"
}
glob
{
  "pattern": "**/*memory*"
}
grep
{
  "include": "*.ts",
  "pattern": "class.*Memory|interface.*Memory|type.*Memory"
}

grep
{
  "include": "*.ts",
  "max_results": 50,
  "pattern": "memory"
}

Result:

/home/sam/Source/openclaw/src/auto-reply/templating.ts:121
  118: import { getBearerToken, getHeader } from "./http-utils.js";
  119: 
  120: const DEFAULT_BODY_BYTES = 2 * 1024 * 1024;
> 121:    * Used for hook confirmation messages like "Session context saved to memory".
  122:    */
  123:   HookMessages?: string[];
  124: };

---
/home/sam/Source/openclaw/src/gateway/tools-invoke-http.ts:35
> 35: const MEMORY_TOOL_NAMES = new Set(["memory_search", "memory_get"]);
  36: 
  37: type ToolsInvokeBody = {
  38:   tool?: unknown;
  39:   }
  40:   const reasons: string[] = [];
  41:   const plugins = cfg.plugins;

---
/home/sam/Source/openclaw/src/gateway/tools-invoke-http.ts:58
> 58:   const slotRaw = plugins?.slots?.memory;
  59:   const slotDisabled =
  60:     slotRaw === null || (typeof slotRaw === "string" && slotRaw.trim().toLowerCase() === "none");
  61:   const pluginsDisabled = plugins?.enabled === false;
  62:     reasons.push("plugins.enabled=false");
  63:   }
  64:   if (slotDisabled) {

---
/home/sam/Source/openclaw/src/gateway/tools-invoke-http.ts:68
> 68:     reasons.push(slotRaw === null ? "plugins.slots.memory=null" : 'plugins.slots.memory="none"');
  69:   }
  70:   if (!pluginsDisabled && !slotDisabled && defaultDisabled) {

---
/home/sam/Source/openclaw/src/gateway/tools-invoke-http.ts:71
> 71:     reasons.push("memory plugin disabled by test default");
  72:   }
  73:   return reasons;
  74: }
  75:         error: {
  76:           type: "invalid_request",
  77:           message:

---
/home/sam/Source/openclaw/src/gateway/tools-invoke-http.ts:151
> 151:             `memory tools are disabled in tests${suffix}. ` +

---
/home/sam/Source/openclaw/src/gateway/tools-invoke-http.ts:152
  149:   /**
  150:    * Delete stored TwiML for a given `callId`.
  151:    *
> 152:             'Enable by setting plugins.slots.memory="memory-core" (and ensure plugins.enabled is not false).',
  153:         },
  154:       });
  155:       return true;

---
/home/sam/Source/openclaw/extensions/voice-call/src/providers/twilio.ts:72
  71: /**
> 72:    * We keep TwiML in-memory only long enough to satisfy the initial Twilio
  73:    * webhook request (notify mode). Subsequent webhooks should not reuse it.
  74:    */
  75:   private deleteStoredTwiml(callId: string): void {

---
/home/sam/Source/openclaw/extensions/msteams/src/pending-uploads.ts:2
  1: import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
> 2:  * In-memory storage for files awaiting user consent in the FileConsentCard flow.
  3:  *
  4:  * When sending large files (>=4MB) in personal chats, Teams requires user consent
  5:  * before upload. This module stores the file data temporarily until the user
  6: import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
  7: 

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:4
> 4: const memoryCorePlugin = {

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:5
> 5:   id: "memory-core",
  6:   name: "Memory (Core)",

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:7
> 7:   description: "File-backed memory search tools and CLI",

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:8
> 8:   kind: "memory",
  9:   configSchema: emptyPluginConfigSchema(),
  10:   register(api: OpenClawPluginApi) {
  11:     api.registerTool(
  12:       (ctx) => {

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:13
> 13:         const memorySearchTool = api.runtime.tools.createMemorySearchTool({
  14:           config: ctx.config,
  15:           agentSessionKey: ctx.sessionKey,
  16:         });

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:17
> 17:         const memoryGetTool = api.runtime.tools.createMemoryGetTool({
  18:           config: ctx.config,
  19:           agentSessionKey: ctx.sessionKey,
  20:         });

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:21
> 21:         if (!memorySearchTool || !memoryGetTool) {
  22:           return null;
  23:         }

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:24
> 24:         return [memorySearchTool, memoryGetTool];
  25:       },

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:26
> 26:       { names: ["memory_search", "memory_get"] },
  27:     );
  28: 
  29:     api.registerCli(
  30:       ({ program }) => {
  31:         api.runtime.tools.registerMemoryCli(program);
  32:       },

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:33
> 33:       { commands: ["memory"] },
  34:     );
  35:   },
  36: };
  37: 

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:38
  36: /**
  37:  * LRU-based seen event tracker with TTL support.
> 38: export default memoryCorePlugin;

---
/home/sam/Source/openclaw/extensions/nostr/src/seen-tracker.ts:3
  2: import os from "node:os";
> 3:  * Prevents unbounded memory growth under high load or abuse.
  4:  */
  5: 
  6: export interface SeenTrackerOptions {
  7: import path from "node:path";
  8: import { describe, expect, it } from "vitest";

---
/home/sam/Source/openclaw/extensions/msteams/src/polls-store.test.ts:5
> 5: import { createMSTeamsPollStoreMemory } from "./polls-store-memory.js";
  6: import { createMSTeamsPollStoreFs } from "./polls.js";
  7: 
  8: const createFsStore = async () => {
  9: const createMemoryStore = () => createMSTeamsPollStoreMemory();
  10: 
  11: describe.each([

---
/home/sam/Source/openclaw/extensions/msteams/src/polls-store.test.ts:16
> 16:   { name: "memory", createStore: createMemoryStore },
  17:   { name: "fs", createStore: createFsStore },
  18: ])("$name poll store", ({ createStore }) => {
  19:   it("stores polls and records normalized votes", async () => {
  20:   const baseSince = computeSinceTimestamp(state, gatewayStartedAt);
  21:   const since = Math.max(0, baseSince - STARTUP_LOOKBACK_SEC);
  22: 

---
/home/sam/Source/openclaw/extensions/nostr/src/nostr-bus.ts:361
> 361:   // Seed in-memory dedupe with recent IDs from disk (prevents restart replay)
  362:   if (state?.recentEventIds?.length) {
  363:     seen.seed(state.recentEventIds);
  364:   }
  365: 
  366:       // Mark seen AFTER verify (don't cache invalid IDs)
  367:       seen.add(event.id);

---
/home/sam/Source/openclaw/extensions/nostr/src/nostr-bus.ts:447
  444:       expect(snapshot.decrypt.failure).toBe(1);
  445:     });
  446: 
> 447:       metrics.emit("memory.seen_tracker_size", seen.size());
  448: 
  449:       // Decrypt the message
  450:       let plaintext: string;

---
/home/sam/Source/openclaw/extensions/nostr/src/nostr-bus.integration.test.ts:329
> 329:     it("tracks memory gauges (replaces rather than accumulates)", () => {
  330:       const metrics = createMetrics();
  331: 

---
/home/sam/Source/openclaw/extensions/nostr/src/nostr-bus.integration.test.ts:332
> 332:       metrics.emit("memory.seen_tracker_size", 100);

---
/home/sam/Source/openclaw/extensions/nostr/src/nostr-bus.integration.test.ts:333
> 333:       metrics.emit("memory.seen_tracker_size", 150);

---
/home/sam/Source/openclaw/extensions/nostr/src/nostr-bus.integration.test.ts:334
> 334:       metrics.emit("memory.seen_tracker_size", 125);
  335: 
  336:       const snapshot = metrics.getSnapshot();

---
/home/sam/Source/openclaw/extensions/nostr/src/nostr-bus.integration.test.ts:337
  334: 
  335: export type DecryptMetricName = "decrypt.success" | "decrypt.failure";
  336: 
> 337:       expect(snapshot.memory.seenTrackerSize).toBe(125); // Last value, not sum
  338:     });
  339: 
  340:     it("reset clears all counters", () => {

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:44
> 44: export type MemoryMetricName = "memory.seen_tracker_size" | "memory.rate_limiter_entries";
  45: 
  46: export type MetricName =
  47:   | EventMetricName
  48:   };
  49: 
  50:   /** Memory/capacity stats */

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:130
> 130:   memory: {
  131:     seenTrackerSize: number;
  132:     rateLimiterEntries: number;
  133:   };
  134:   };
  135: 
  136:   // Memory stats (updated via gauge-style metrics)

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:211
> 211:   const memory = {
  212:     seenTrackerSize: 0,
  213:     rateLimiterEntries: 0,
  214:   };
  215:         break;
  216: 
  217:       // Memory (gauge-style - value replaces, not adds)

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:389
> 389:       case "memory.seen_tracker_size":

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:390
> 390:         memory.seenTrackerSize = value;
  391:         break;

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:392
> 392:       case "memory.rate_limiter_entries":

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:393
> 393:         memory.rateLimiterEntries = value;
  394:         break;
  395:     }
  396:   }
  397:       relays: relaysObj,
  398:       rateLimiting: { ...rateLimiting },
  399:       decrypt: { ...decrypt },

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:413
> 413:       memory: { ...memory },
  414:       snapshotAt: Date.now(),
  415:     };
  416:   }
  417:     rateLimiting.globalHits = 0;
  418:     decrypt.success = 0;
  419:     decrypt.failure = 0;

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:439
> 439:     memory.seenTrackerSize = 0;

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:440
> 440:     memory.rateLimiterEntries = 0;
  441:   }
  442: 
  443:   return { emit, getSnapshot, reset };
  444:     relays: {},
  445:     rateLimiting: { perSenderHits: 0, globalHits: 0 },
  446:     decrypt: { success: 0, failure: 0 },

---
/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:469
  466:   deny: string[];
  467:   loadPaths: string[];
  468:   slots: {
> 469:     memory: { seenTrackerSize: 0, rateLimiterEntries: 0 },
  470:     snapshotAt: 0,
  471:   };
  472: 

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:11
> 11:     memory?: string | null;
  12:   };
  13:   entries: Record<string, { enabled?: boolean; config?: unknown }>;
  14: };
  15: export const normalizePluginsConfig = (
  16:   config?: OpenClawConfig["plugins"],
  17: ): NormalizedPluginsConfig => {

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:64
> 64:   const memorySlot = normalizeSlotValue(config?.slots?.memory);
  65:   return {
  66:     enabled: config?.enabled !== false,
  67:     allow: normalizeList(config?.allow),
  68:     deny: normalizeList(config?.deny),
  69:     loadPaths: normalizeList(config?.load?.paths),
  70:     slots: {

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:71
> 71:       memory: memorySlot === undefined ? defaultSlotIdForKey("memory") : memorySlot,
  72:     },
  73:     entries: normalizePluginEntries(config?.entries),
  74:   };
  75: };
  76: 
  77: const hasExplicitMemorySlot = (plugins?: OpenClawConfig["plugins"]) =>

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:78
> 78:   Boolean(plugins?.slots && Object.prototype.hasOwnProperty.call(plugins.slots, "memory"));
  79: 
  80: const hasExplicitMemoryEntry = (plugins?: OpenClawConfig["plugins"]) =>

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:81
> 81:   Boolean(plugins?.entries && Object.prototype.hasOwnProperty.call(plugins.entries, "memory-core"));
  82: 
  83: const hasExplicitPluginConfig = (plugins?: OpenClawConfig["plugins"]) => {
  84:   if (!plugins) {
  85:         ...plugins,
  86:         slots: {
  87:           ...plugins?.slots,

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:127
> 127:           memory: "none",
  128:         },
  129:       },
  130:     };
  131:       enabled: false,
  132:       slots: {
  133:         ...plugins?.slots,

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:140
> 140:         memory: "none",
  141:       },
  142:     },
  143:   };
  144:   if (config.allow.length > 0 && !config.allow.includes(id)) {
  145:     return { enabled: false, reason: "not in allowlist" };
  146:   }

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:174
> 174:   if (config.slots.memory === id) {
  175:     return { enabled: true };
  176:   }
  177:   const entry = config.entries[id];
  178:   slot: string | null | undefined;
  179:   selectedId: string | null;
  180: }): { enabled: boolean; reason?: string; selected?: boolean } {

---
/home/sam/Source/openclaw/src/plugins/config-state.ts:199
> 199:   if (params.kind !== "memory") {
  200:     return { enabled: true };
  201:   }
  202:   if (params.slot === null) {

[Results truncated at limit]
glob
{
  "pattern": "**/*memory*"
}

Result:

[f]    3K  2026-02-01 17:57  /home/sam/Source/openclaw/src/memory/sync-memory-files.ts
[f]    3K  2026-02-01 17:57  /home/sam/Source/openclaw/src/memory/memory-schema.ts
[d]    4K  2026-02-01 17:57  /home/sam/Source/openclaw/src/memory
[d]    4K  2026-02-01 17:57  /home/sam/Source/openclaw/src/hooks/bundled/session-memory
[f]   25K  2026-02-01 17:57  /home/sam/Source/openclaw/src/cli/memory-cli.ts
[f]   12K  2026-02-01 17:57  /home/sam/Source/openclaw/src/cli/memory-cli.test.ts
[f]    3K  2026-02-01 17:57  /home/sam/Source/openclaw/src/auto-reply/reply/memory-flush.test.ts
[f]    4K  2026-02-01 17:57  /home/sam/Source/openclaw/src/auto-reply/reply/memory-flush.ts
[f]    8K  2026-02-01 17:57  /home/sam/Source/openclaw/src/auto-reply/reply/agent-runner-memory.ts
[f]    5K  2026-02-01 17:57  /home/sam/Source/openclaw/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.increments-compaction-count-flush-compaction-completes.test.ts
[f]    7K  2026-02-01 17:57  /home/sam/Source/openclaw/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.uses-configured-prompts-memory-flush-runs.test.ts
[f]    7K  2026-02-01 17:57  /home/sam/Source/openclaw/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-sandbox-workspace-is-read.test.ts
[f]    5K  2026-02-01 17:57  /home/sam/Source/openclaw/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.skips-memory-flush-cli-providers.test.ts
[f]    7K  2026-02-01 17:57  /home/sam/Source/openclaw/src/auto-reply/reply/agent-runner.memory-flush.runreplyagent-memory-flush.runs-memory-flush-turn-updates-session-metadata.test.ts
[f]    4K  2026-02-01 17:57  /home/sam/Source/openclaw/src/agents/tools/memory-tool.ts
[f]    2K  2026-02-01 17:57  /home/sam/Source/openclaw/src/agents/tools/memory-tool.does-not-crash-on-errors.test.ts
[f]   10K  2026-02-01 17:57  /home/sam/Source/openclaw/src/agents/memory-search.ts
[f]    6K  2026-02-01 17:57  /home/sam/Source/openclaw/src/agents/memory-search.test.ts
[f]    4K  2026-02-01 17:57  /home/sam/Source/openclaw/extensions/open-prose/skills/prose/lib/project-memory.prose
[f]    3K  2026-02-01 17:57  /home/sam/Source/openclaw/extensions/open-prose/skills/prose/lib/user-memory.prose
[f]    5K  2026-02-01 17:57  /home/sam/Source/openclaw/extensions/open-prose/skills/prose/examples/31-captains-chair-with-memory.prose
[f]  951B  2026-02-01 17:57  /home/sam/Source/openclaw/extensions/msteams/src/polls-store-memory.ts
[f]    1K  2026-02-01 17:57  /home/sam/Source/openclaw/extensions/msteams/src/conversation-store-memory.ts
[d]    4K  2026-02-01 17:57  /home/sam/Source/openclaw/extensions/memory-lancedb
[d]    4K  2026-02-01 17:57  /home/sam/Source/openclaw/extensions/memory-core
[f]    9K  2026-02-01 17:57  /home/sam/Source/openclaw/docs/experiments/research/memory.md
[f]   15K  2026-02-01 17:57  /home/sam/Source/openclaw/docs/concepts/memory.md
[f]    1K  2026-02-01 17:57  /home/sam/Source/openclaw/docs/cli/memory.md
grep
{
  "include": "*.ts",
  "pattern": "class.*Memory|interface.*Memory|type.*Memory"
}

Result:

/home/sam/Source/openclaw/extensions/nostr/src/metrics.ts:44
  41: import { stringEnum } from "openclaw/plugin-sdk";
  42: import {
  43:   MEMORY_CATEGORIES,
> 44: export type MemoryMetricName = "memory.seen_tracker_size" | "memory.rate_limiter_entries";
  45: 
  46: export type MetricName =
  47:   | EventMetricName

---
/home/sam/Source/openclaw/extensions/memory-lancedb/index.ts:17
> 17:   type MemoryCategory,
  18:   memoryConfigSchema,
  19:   vectorDimsForModel,
  20: } from "./config.js";
  21: // Types
  22: // ============================================================================
  23: 

---
/home/sam/Source/openclaw/extensions/memory-lancedb/index.ts:26
> 26: type MemoryEntry = {
  27:   id: string;
  28:   text: string;
  29:   vector: number[];
  30:   createdAt: number;
  31: };
  32: 

---
/home/sam/Source/openclaw/extensions/memory-lancedb/index.ts:35
> 35: type MemorySearchResult = {
  36:   entry: MemoryEntry;
  37:   score: number;
  38: };
  39: 
  40: const TABLE_NAME = "memories";
  41: 

---
/home/sam/Source/openclaw/extensions/memory-lancedb/index.ts:46
> 46: class MemoryDB {
  47:   private db: lancedb.Connection | null = null;
  48:   private table: lancedb.Table | null = null;
  49:   private initPromise: Promise<void> | null = null;
  50:           if (memoryId) {
  51:             await db.delete(memoryId);
  52:             return {

---
/home/sam/Source/openclaw/extensions/memory-lancedb/index.ts:370
  367: import { homedir } from "node:os";
  368: import { join } from "node:path";
  369: 
> 370:               content: [{ type: "text", text: `Memory ${memoryId} forgotten.` }],
  371:               details: { action: "deleted", id: memoryId },
  372:             };
  373:           }

---
/home/sam/Source/openclaw/extensions/memory-lancedb/config.ts:5
> 5: export type MemoryConfig = {
  6:   embedding: {
  7:     provider: "openai";
  8:     model?: string;
  9: };
  10: 
  11: export const MEMORY_CATEGORIES = ["preference", "fact", "decision", "entity", "other"] as const;

---
/home/sam/Source/openclaw/extensions/memory-lancedb/config.ts:17
> 17: export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number];
  18: 
  19: const DEFAULT_MODEL = "text-embedding-3-small";
  20: const LEGACY_STATE_DIRS: string[] = [];
  21:   `You may reply, but usually ${SILENT_REPLY_TOKEN} is correct.`,
  22: ].join(" ");
  23: 

---
/home/sam/Source/openclaw/src/auto-reply/reply/memory-flush.ts:22
  19: import fsSync from "node:fs";
  20: import fs from "node:fs/promises";
  21: import path from "node:path";
> 22: export type MemoryFlushSettings = {
  23:   enabled: boolean;
  24:   softThresholdTokens: number;
  25:   prompt: string;

---
/home/sam/Source/openclaw/src/memory/manager.ts:7
> 7: import type { ResolvedMemorySearchConfig } from "../agents/memory-search.js";
  8: import type { OpenClawConfig } from "../config/config.js";
  9: import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
  10: import { resolveMemorySearchConfig } from "../agents/memory-search.js";
  11:   isMemoryPath,
  12:   listMemoryFiles,
  13:   normalizeExtraMemoryPaths,

---
/home/sam/Source/openclaw/src/memory/manager.ts:39
> 39:   type MemoryChunk,

---
/home/sam/Source/openclaw/src/memory/manager.ts:40
> 40:   type MemoryFileEntry,
  41:   parseEmbedding,
  42: } from "./internal.js";
  43: import { searchKeyword, searchVector } from "./manager-search.js";
  44: import { loadSqliteVecExtension } from "./sqlite-vec.js";
  45: import { requireNodeSqlite } from "./sqlite.js";
  46: 

---
/home/sam/Source/openclaw/src/memory/manager.ts:48
> 48: type MemorySource = "memory" | "sessions";
  49: 

---
/home/sam/Source/openclaw/src/memory/manager.ts:50
> 50: export type MemorySearchResult = {
  51:   path: string;
  52:   startLine: number;
  53:   endLine: number;
  54:   source: MemorySource;
  55: };
  56: 

---
/home/sam/Source/openclaw/src/memory/manager.ts:59
> 59: type MemoryIndexMeta = {
  60:   model: string;
  61:   provider: string;
  62:   providerKey?: string;
  63:   content: string;
  64: };
  65: 

---
/home/sam/Source/openclaw/src/memory/manager.ts:77
> 77: type MemorySyncProgressUpdate = {
  78:   completed: number;
  79:   total: number;
  80:   label?: string;
  81: };
  82: 

---
/home/sam/Source/openclaw/src/memory/manager.ts:83
> 83: type MemorySyncProgressState = {
  84:   completed: number;
  85:   total: number;
  86:   label?: string;
  87: const vectorToBlob = (embedding: number[]): Buffer =>
  88:   Buffer.from(new Float32Array(embedding).buffer);
  89: 

---
/home/sam/Source/openclaw/src/memory/manager.ts:117
  114: import os from "node:os";
  115: import path from "node:path";
  116: import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
> 117: export class MemoryIndexManager {
  118:   private readonly cacheKey: string;
  119:   private readonly cfg: OpenClawConfig;
  120:   private readonly agentId: string;

---
/home/sam/Source/openclaw/src/memory/manager.async-search.test.ts:5
  2: import os from "node:os";
  3: import path from "node:path";
  4: import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
> 5: import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
  6: 
  7: const embedBatch = vi.fn(async () => []);
  8: const embedQuery = vi.fn(async () => [0.2, 0.2, 0.2]);

---
/home/sam/Source/openclaw/src/memory/manager.atomic-reindex.test.ts:5
> 5: import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
  6: 
  7: let shouldFail = false;
  8: 

---
/home/sam/Source/openclaw/src/memory/index.ts:1
> 1: export type { MemoryIndexManager, MemorySearchResult } from "./manager.js";

---
/home/sam/Source/openclaw/src/memory/index.ts:2
> 2: export { getMemorySearchManager, type MemorySearchManagerResult } from "./search-manager.js";

---
/home/sam/Source/openclaw/src/memory/manager-cache-key.ts:1
> 1: import type { ResolvedMemorySearchConfig } from "../agents/memory-search.js";
  2: import { fingerprintHeaderNames } from "./headers-fingerprint.js";
  3: import { hashText } from "./internal.js";
  4: 
  5: import os from "node:os";
  6: import path from "node:path";
  7: import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

---
/home/sam/Source/openclaw/src/memory/manager.batch.test.ts:5
  3: import type { DatabaseSync } from "node:sqlite";
  4: import { createSubsystemLogger } from "../logging/subsystem.js";
> 5: import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
  6: 
  7: const embedBatch = vi.fn(async () => []);
  8: const embedQuery = vi.fn(async () => [0.5, 0.5, 0.5]);

---
/home/sam/Source/openclaw/src/memory/sync-memory-files.ts:3
  2: import os from "node:os";
> 3: import { buildFileEntry, listMemoryFiles, type MemoryFileEntry } from "./internal.js";
  4: 
  5: const log = createSubsystemLogger("memory");
  6: 
  7: import path from "node:path";
  8: import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

---
/home/sam/Source/openclaw/src/memory/manager.embedding-batches.test.ts:5
  2: import os from "node:os";
  3: import path from "node:path";
  4: import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
> 5: import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
  6: 
  7: const embedBatch = vi.fn(async (texts: string[]) => texts.map(() => [0, 1, 0]));
  8: const embedQuery = vi.fn(async () => [0, 1, 0]);

---
/home/sam/Source/openclaw/src/memory/manager.sync-errors-do-not-crash.test.ts:5
  4: import type { OpenClawConfig } from "../config/config.js";
> 5: import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
  6: 
  7: vi.mock("chokidar", () => ({
  8:   default: {

---
/home/sam/Source/openclaw/src/memory/search-manager.ts:2
> 2: import type { MemoryIndexManager } from "./manager.js";
  3: 

---
/home/sam/Source/openclaw/src/memory/search-manager.ts:4
  3: import fs from "node:fs/promises";
> 4: export type MemorySearchManagerResult = {
  5:   manager: MemoryIndexManager | null;
  6:   error?: string;
  7: };
  8: import path from "node:path";
  9: 

---
/home/sam/Source/openclaw/src/memory/internal.ts:6
> 6: export type MemoryFileEntry = {
  7:   path: string;
  8:   absPath: string;
  9:   mtimeMs: number;
  10:   hash: string;
  11: };
  12: 

---
/home/sam/Source/openclaw/src/memory/internal.ts:14
  11: import os from "node:os";
  12: import path from "node:path";
  13: import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
> 14: export type MemoryChunk = {
  15:   startLine: number;
  16:   endLine: number;
  17:   text: string;

---
/home/sam/Source/openclaw/src/memory/index.test.ts:5
  2: import os from "node:os";
  3: import path from "node:path";
  4: import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
> 5: import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
  6: 
  7: let embedBatchCalls = 0;
  8: let failEmbeddings = false;

---
/home/sam/Source/openclaw/src/memory/manager.vector-dedupe.test.ts:5
> 5: import { getMemorySearchManager, type MemoryIndexManager } from "./index.js";
  6: import { buildFileEntry } from "./internal.js";
  7: 
  8: vi.mock("./embeddings.js", () => {
  9: type IsVoiceCompatibleAudio = typeof import("../../media/audio.js").isVoiceCompatibleAudio;
  10: type GetImageMetadata = typeof import("../../media/image-ops.js").getImageMetadata;
  11: type ResizeToJpeg = typeof import("../../media/image-ops.js").resizeToJpeg;

---
/home/sam/Source/openclaw/src/plugins/runtime/types.ts:84
> 84: type CreateMemoryGetTool = typeof import("../../agents/tools/memory-tool.js").createMemoryGetTool;

---
/home/sam/Source/openclaw/src/plugins/runtime/types.ts:85
> 85: type CreateMemorySearchTool =

---
/home/sam/Source/openclaw/src/plugins/runtime/types.ts:86
> 86:   typeof import("../../agents/tools/memory-tool.js").createMemorySearchTool;

---
/home/sam/Source/openclaw/src/plugins/runtime/types.ts:87
> 87: type RegisterMemoryCli = typeof import("../../cli/memory-cli.js").registerMemoryCli;
  88: type DiscordMessageActions =
  89:   typeof import("../../channels/plugins/actions/discord.js").discordMessageActions;
  90: type AuditDiscordChannelPermissions =
  91:   };
  92: };
  93: 

---
/home/sam/Source/openclaw/src/config/types.tools.ts:224
  221:   SandboxDockerSettings,
  222:   SandboxPruneSettings,
  223: } from "./types.sandbox.js";
> 224: export type MemorySearchConfig = {
  225:   /** Enable vector memory search (default: true). */
  226:   enabled?: boolean;
  227:   /** Sources to index and search (default: ["memory"]). */

---
/home/sam/Source/openclaw/src/config/types.agent-defaults.ts:13
> 13: import type { MemorySearchConfig } from "./types.tools.js";
  14: 
  15: export type AgentModelEntryConfig = {
  16:   alias?: string;
  17:   memoryFlush?: AgentCompactionMemoryFlushConfig;
  18: };
  19: 

---
/home/sam/Source/openclaw/src/config/types.agent-defaults.ts:253
  250: import { resolveStateDir } from "../config/paths.js";
  251: import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
  252: import { setVerbose } from "../globals.js";
> 253: export type AgentCompactionMemoryFlushConfig = {
  254:   /** Enable the pre-compaction memory flush (default: true). */
  255:   enabled?: boolean;
  256:   /** Run the memory flush when context is within this many tokens of the compaction threshold. */

---
/home/sam/Source/openclaw/src/cli/memory-cli.ts:11
> 11: import { getMemorySearchManager, type MemorySearchManagerResult } from "../memory/index.js";
  12: import { listMemoryFiles, normalizeExtraMemoryPaths } from "../memory/internal.js";
  13: import { defaultRuntime } from "../runtime.js";
  14: import { formatDocsLink } from "../terminal/links.js";
  15: import { formatErrorMessage, withManager } from "./cli-utils.js";
  16: import { withProgress, withProgressTotals } from "./progress.js";
  17: 

---
/home/sam/Source/openclaw/src/cli/memory-cli.ts:20
> 20: type MemoryCommandOptions = {
  21:   agent?: string;
  22:   json?: boolean;
  23:   deep?: boolean;
  24:   verbose?: boolean;
  25: };
  26: 

---
/home/sam/Source/openclaw/src/cli/memory-cli.ts:28
> 28: type MemoryManager = NonNullable<MemorySearchManagerResult["manager"]>;
  29: 

---
/home/sam/Source/openclaw/src/cli/memory-cli.ts:30
> 30: type MemorySourceName = "memory" | "sessions";
  31: 
  32: type SourceScan = {
  33:   source: MemorySourceName;
  34:   issues: string[];
  35: };
  36: 

---
/home/sam/Source/openclaw/src/cli/memory-cli.ts:38
  35:   SandboxDockerSettings,
  36:   SandboxPruneSettings,
  37: } from "./types.sandbox.js";
> 38: type MemorySourceScan = {
  39:   sources: SourceScan[];
  40:   totalFiles: number | null;
  41:   issues: string[];

---
/home/sam/Source/openclaw/src/config/types.agents.ts:9
  7: import os from "node:os";
  8: import path from "node:path";
> 9: import type { AgentToolsConfig, MemorySearchConfig } from "./types.tools.js";
  10: 
  11: export type AgentModelConfig =
  12:   | string

---
/home/sam/Source/openclaw/src/agents/memory-search.ts:3
> 3: import type { OpenClawConfig, MemorySearchConfig } from "../config/config.js";
  4: import { resolveStateDir } from "../config/paths.js";
  5: import { clampInt, clampNumber, resolveUserPath } from "../utils.js";
  6: import { resolveAgentConfig } from "./agent-scope.js";
  7: 

---
/home/sam/Source/openclaw/src/agents/memory-search.ts:8
> 8: export type ResolvedMemorySearchConfig = {
  9:   enabled: boolean;
  10:   sources: Array<"memory" | "sessions">;
  11:   extraPaths: string[];

---
/home/sam/Source/openclaw/src/commands/status.scan.ts:1
> 1: import type { MemoryIndexManager } from "../memory/manager.js";
  2: import type { RuntimeEnv } from "../runtime.js";
  3: import { withProgress } from "../cli/progress.js";
  4: import { loadConfig } from "../config/config.js";
  5: import { getStatusSummary } from "./status.summary.js";
  6: import { getUpdateCheckResult } from "./status.update.js";
  7: 

---
/home/sam/Source/openclaw/src/commands/status.scan.ts:18
> 18: type MemoryStatusSnapshot = ReturnType<MemoryIndexManager["status"]> & {
  19:   agentId: string;
  20: };
  21: 

---
/home/sam/Source/openclaw/src/commands/status.scan.ts:22
> 22: type MemoryPluginStatus = {
  23:   enabled: boolean;
  24:   slot: string | null;
  25:   reason?: string;
  26: };
  27: 

---
/home/sam/Source/openclaw/src/commands/status.scan.ts:28
> 28: function resolveMemoryPluginStatus(cfg: ReturnType<typeof loadConfig>): MemoryPluginStatus {
  29:   const pluginsEnabled = cfg.plugins?.enabled !== false;
  30:   if (!pluginsEnabled) {
  31:     return { enabled: false, slot: null, reason: "plugins disabled" };

Assistant

read_file
{
  "file_path": "docs/concepts/memory.md"
}
read_file
{
  "end_line": 200,
  "file_path": "src/memory/manager.ts",
  "start_line": 1
}
read_file
{
  "file_path": "extensions/memory-core/index.ts"
}

read_file
{
  "file_path": "docs/concepts/memory.md"
}

Result:

1: ---
2: summary: "How OpenClaw memory works (workspace files + automatic memory flush)"
3: read_when:
4:   - You want the memory file layout and workflow
5:   - You want to tune the automatic pre-compaction memory flush
6: title: "Memory"
7: ---
8: 
9: # Memory
10: 
11: OpenClaw memory is **plain Markdown in the agent workspace**. The files are the
12: source of truth; the model only "remembers" what gets written to disk.
13: 
14: Memory search tools are provided by the active memory plugin (default:
15: `memory-core`). Disable memory plugins with `plugins.slots.memory = "none"`.
16: 
17: ## Memory files (Markdown)
18: 
19: The default workspace layout uses two memory layers:
20: 
21: - `memory/YYYY-MM-DD.md`
22:   - Daily log (append-only).
23:   - Read today + yesterday at session start.
24: - `MEMORY.md` (optional)
25:   - Curated long-term memory.
26:   - **Only load in the main, private session** (never in group contexts).
27: 
28: These files live under the workspace (`agents.defaults.workspace`, default
29: `~/.openclaw/workspace`). See [Agent workspace](/concepts/agent-workspace) for the full layout.
30: 
31: ## When to write memory
32: 
33: - Decisions, preferences, and durable facts go to `MEMORY.md`.
34: - Day-to-day notes and running context go to `memory/YYYY-MM-DD.md`.
35: - If someone says "remember this," write it down (do not keep it in RAM).
36: - This area is still evolving. It helps to remind the model to store memories; it will know what to do.
37: - If you want something to stick, **ask the bot to write it** into memory.
38: 
39: ## Automatic memory flush (pre-compaction ping)
40: 
41: When a session is **close to auto-compaction**, OpenClaw triggers a **silent,
42: agentic turn** that reminds the model to write durable memory **before** the
43: context is compacted. The default prompts explicitly say the model _may reply_,
44: but usually `NO_REPLY` is the correct response so the user never sees this turn.
45: 
46: This is controlled by `agents.defaults.compaction.memoryFlush`:
47: 
48: ```json5
49: {
50:   agents: {
51:     defaults: {
52:       compaction: {
53:         reserveTokensFloor: 20000,
54:         memoryFlush: {
55:           enabled: true,
56:           softThresholdTokens: 4000,
57:           systemPrompt: "Session nearing compaction. Store durable memories now.",
58:           prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
59:         },
60:       },
61:     },
62:   },
63: }
64: ```
65: 
66: Details:
67: 
68: - **Soft threshold**: flush triggers when the session token estimate crosses
69:   `contextWindow - reserveTokensFloor - softThresholdTokens`.
70: - **Silent** by default: prompts include `NO_REPLY` so nothing is delivered.
71: - **Two prompts**: a user prompt plus a system prompt append the reminder.
72: - **One flush per compaction cycle** (tracked in `sessions.json`).
73: - **Workspace must be writable**: if the session runs sandboxed with
74:   `workspaceAccess: "ro"` or `"none"`, the flush is skipped.
75: 
76: For the full compaction lifecycle, see
77: [Session management + compaction](/reference/session-management-compaction).
78: 
79: ## Vector memory search
80: 
81: OpenClaw can build a small vector index over `MEMORY.md` and `memory/*.md` (plus
82: any extra directories or files you opt in) so semantic queries can find related
83: notes even when wording differs.
84: 
85: Defaults:
86: 
87: - Enabled by default.
88: - Watches memory files for changes (debounced).
89: - Uses remote embeddings by default. If `memorySearch.provider` is not set, OpenClaw auto-selects:
90:   1. `local` if a `memorySearch.local.modelPath` is configured and the file exists.
91:   2. `openai` if an OpenAI key can be resolved.
92:   3. `gemini` if a Gemini key can be resolved.
93:   4. Otherwise memory search stays disabled until configured.
94: - Local mode uses node-llama-cpp and may require `pnpm approve-builds`.
95: - Uses sqlite-vec (when available) to accelerate vector search inside SQLite.
96: 
97: Remote embeddings **require** an API key for the embedding provider. OpenClaw
98: resolves keys from auth profiles, `models.providers.*.apiKey`, or environment
99: variables. Codex OAuth only covers chat/completions and does **not** satisfy
100: embeddings for memory search. For Gemini, use `GEMINI_API_KEY` or
101: `models.providers.google.apiKey`. When using a custom OpenAI-compatible endpoint,
102: set `memorySearch.remote.apiKey` (and optional `memorySearch.remote.headers`).
103: 
104: ### Additional memory paths
105: 
106: If you want to index Markdown files outside the default workspace layout, add
107: explicit paths:
108: 
109: ```json5
110: agents: {
111:   defaults: {
112:     memorySearch: {
113:       extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
114:     }
115:   }
116: }
117: ```
118: 
119: Notes:
120: 
121: - Paths can be absolute or workspace-relative.
122: - Directories are scanned recursively for `.md` files.
123: - Only Markdown files are indexed.
124: - Symlinks are ignored (files or directories).
125: 
126: ### Gemini embeddings (native)
127: 
128: Set the provider to `gemini` to use the Gemini embeddings API directly:
129: 
130: ```json5
131: agents: {
132:   defaults: {
133:     memorySearch: {
134:       provider: "gemini",
135:       model: "gemini-embedding-001",
136:       remote: {
137:         apiKey: "YOUR_GEMINI_API_KEY"
138:       }
139:     }
140:   }
141: }
142: ```
143: 
144: Notes:
145: 
146: - `remote.baseUrl` is optional (defaults to the Gemini API base URL).
147: - `remote.headers` lets you add extra headers if needed.
148: - Default model: `gemini-embedding-001`.
149: 
150: If you want to use a **custom OpenAI-compatible endpoint** (OpenRouter, vLLM, or a proxy),
151: you can use the `remote` configuration with the OpenAI provider:
152: 
153: ```json5
154: agents: {
155:   defaults: {
156:     memorySearch: {
157:       provider: "openai",
158:       model: "text-embedding-3-small",
159:       remote: {
160:         baseUrl: "https://api.example.com/v1/",
161:         apiKey: "YOUR_OPENAI_COMPAT_API_KEY",
162:         headers: { "X-Custom-Header": "value" }
163:       }
164:     }
165:   }
166: }
167: ```
168: 
169: If you don't want to set an API key, use `memorySearch.provider = "local"` or set
170: `memorySearch.fallback = "none"`.
171: 
172: Fallbacks:
173: 
174: - `memorySearch.fallback` can be `openai`, `gemini`, `local`, or `none`.
175: - The fallback provider is only used when the primary embedding provider fails.
176: 
177: Batch indexing (OpenAI + Gemini):
178: 
179: - Enabled by default for OpenAI and Gemini embeddings. Set `agents.defaults.memorySearch.remote.batch.enabled = false` to disable.
180: - Default behavior waits for batch completion; tune `remote.batch.wait`, `remote.batch.pollIntervalMs`, and `remote.batch.timeoutMinutes` if needed.
181: - Set `remote.batch.concurrency` to control how many batch jobs we submit in parallel (default: 2).
182: - Batch mode applies when `memorySearch.provider = "openai"` or `"gemini"` and uses the corresponding API key.
183: - Gemini batch jobs use the async embeddings batch endpoint and require Gemini Batch API availability.
184: 
185: Why OpenAI batch is fast + cheap:
186: 
187: - For large backfills, OpenAI is typically the fastest option we support because we can submit many embedding requests in a single batch job and let OpenAI process them asynchronously.
188: - OpenAI offers discounted pricing for Batch API workloads, so large indexing runs are usually cheaper than sending the same requests synchronously.
189: - See the OpenAI Batch API docs and pricing for details:
190:   - https://platform.openai.com/docs/api-reference/batch
191:   - https://platform.openai.com/pricing
192: 
193: Config example:
194: 
195: ```json5
196: agents: {
197:   defaults: {
198:     memorySearch: {
199:       provider: "openai",
200:       model: "text-embedding-3-small",
201:       fallback: "openai",
202:       remote: {
203:         batch: { enabled: true, concurrency: 2 }
204:       },
205:       sync: { watch: true }
206:     }
207:   }
208: }
209: ```
210: 
211: Tools:
212: 
213: - `memory_search` — returns snippets with file + line ranges.
214: - `memory_get` — read memory file content by path.
215: 
216: Local mode:
217: 
218: - Set `agents.defaults.memorySearch.provider = "local"`.
219: - Provide `agents.defaults.memorySearch.local.modelPath` (GGUF or `hf:` URI).
220: - Optional: set `agents.defaults.memorySearch.fallback = "none"` to avoid remote fallback.
221: 
222: ### How the memory tools work
223: 
224: - `memory_search` semantically searches Markdown chunks (~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped ~700 chars), file path, line range, score, provider/model, and whether we fell back from local → remote embeddings. No full file payload is returned.
225: - `memory_get` reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are allowed only when explicitly listed in `memorySearch.extraPaths`.
226: - Both tools are enabled only when `memorySearch.enabled` resolves true for the agent.
227: 
228: ### What gets indexed (and when)
229: 
230: - File type: Markdown only (`MEMORY.md`, `memory/**/*.md`, plus any `.md` files under `memorySearch.extraPaths`).
231: - Index storage: per-agent SQLite at `~/.openclaw/memory/<agentId>.sqlite` (configurable via `agents.defaults.memorySearch.store.path`, supports `{agentId}` token).
232: - Freshness: watcher on `MEMORY.md`, `memory/`, and `memorySearch.extraPaths` marks the index dirty (debounce 1.5s). Sync is scheduled on session start, on search, or on an interval and runs asynchronously. Session transcripts use delta thresholds to trigger background sync.
233: - Reindex triggers: the index stores the embedding **provider/model + endpoint fingerprint + chunking params**. If any of those change, OpenClaw automatically resets and reindexes the entire store.
234: 
235: ### Hybrid search (BM25 + vector)
236: 
237: When enabled, OpenClaw combines:
238: 
239: - **Vector similarity** (semantic match, wording can differ)
240: - **BM25 keyword relevance** (exact tokens like IDs, env vars, code symbols)
241: 
242: If full-text search is unavailable on your platform, OpenClaw falls back to vector-only search.
243: 
244: #### Why hybrid?
245: 
246: Vector search is great at “this means the same thing”:
247: 
248: - “Mac Studio gateway host” vs “the machine running the gateway”
249: - “debounce file updates” vs “avoid indexing on every write”
250: 
251: But it can be weak at exact, high-signal tokens:
252: 
253: - IDs (`a828e60`, `b3b9895a…`)
254: - code symbols (`memorySearch.query.hybrid`)
255: - error strings (“sqlite-vec unavailable”)
256: 
257: BM25 (full-text) is the opposite: strong at exact tokens, weaker at paraphrases.
258: Hybrid search is the pragmatic middle ground: **use both retrieval signals** so you get
259: good results for both “natural language” queries and “needle in a haystack” queries.
260: 
261: #### How we merge results (the current design)
262: 
263: Implementation sketch:
264: 
265: 1. Retrieve a candidate pool from both sides:
266: 
267: - **Vector**: top `maxResults * candidateMultiplier` by cosine similarity.
268: - **BM25**: top `maxResults * candidateMultiplier` by FTS5 BM25 rank (lower is better).
269: 
270: 2. Convert BM25 rank into a 0..1-ish score:
271: 
272: - `textScore = 1 / (1 + max(0, bm25Rank))`
273: 
274: 3. Union candidates by chunk id and compute a weighted score:
275: 
276: - `finalScore = vectorWeight * vectorScore + textWeight * textScore`
277: 
278: Notes:
279: 
280: - `vectorWeight` + `textWeight` is normalized to 1.0 in config resolution, so weights behave as percentages.
281: - If embeddings are unavailable (or the provider returns a zero-vector), we still run BM25 and return keyword matches.
282: - If FTS5 can’t be created, we keep vector-only search (no hard failure).
283: 
284: This isn’t “IR-theory perfect”, but it’s simple, fast, and tends to improve recall/precision on real notes.
285: If we want to get fancier later, common next steps are Reciprocal Rank Fusion (RRF) or score normalization
286: (min/max or z-score) before mixing.
287: 
288: Config:
289: 
290: ```json5
291: agents: {
292:   defaults: {
293:     memorySearch: {
294:       query: {
295:         hybrid: {
296:           enabled: true,
297:           vectorWeight: 0.7,
298:           textWeight: 0.3,
299:           candidateMultiplier: 4
300:         }
301:       }
302:     }
303:   }
304: }
305: ```
306: 
307: ### Embedding cache
308: 
309: OpenClaw can cache **chunk embeddings** in SQLite so reindexing and frequent updates (especially session transcripts) don't re-embed unchanged text.
310: 
311: Config:
312: 
313: ```json5
314: agents: {
315:   defaults: {
316:     memorySearch: {
317:       cache: {
318:         enabled: true,
319:         maxEntries: 50000
320:       }
321:     }
322:   }
323: }
324: ```
325: 
326: ### Session memory search (experimental)
327: 
328: You can optionally index **session transcripts** and surface them via `memory_search`.
329: This is gated behind an experimental flag.
330: 
331: ```json5
332: agents: {
333:   defaults: {
334:     memorySearch: {
335:       experimental: { sessionMemory: true },
336:       sources: ["memory", "sessions"]
337:     }
338:   }
339: }
340: ```
341: 
342: Notes:
343: 
344: - Session indexing is **opt-in** (off by default).
345: - Session updates are debounced and **indexed asynchronously** once they cross delta thresholds (best-effort).
346: - `memory_search` never blocks on indexing; results can be slightly stale until background sync finishes.
347: - Results still include snippets only; `memory_get` remains limited to memory files.
348: - Session indexing is isolated per agent (only that agent’s session logs are indexed).
349: - Session logs live on disk (`~/.openclaw/agents/<agentId>/sessions/*.jsonl`). Any process/user with filesystem access can read them, so treat disk access as the trust boundary. For stricter isolation, run agents under separate OS users or hosts.
350: 
351: Delta thresholds (defaults shown):
352: 
353: ```json5
354: agents: {
355:   defaults: {
356:     memorySearch: {
357:       sync: {
358:         sessions: {
359:           deltaBytes: 100000,   // ~100 KB
360:           deltaMessages: 50     // JSONL lines
361:         }
362:       }
363:     }
364:   }
365: }
366: ```
367: 
368: ### SQLite vector acceleration (sqlite-vec)
369: 
370: When the sqlite-vec extension is available, OpenClaw stores embeddings in a
371: SQLite virtual table (`vec0`) and performs vector distance queries in the
372: database. This keeps search fast without loading every embedding into JS.
373: 
374: Configuration (optional):
375: 
376: ```json5
377: agents: {
378:   defaults: {
379:     memorySearch: {
380:       store: {
381:         vector: {
382:           enabled: true,
383:           extensionPath: "/path/to/sqlite-vec"
384:         }
385:       }
386:     }
387:   }
388: }
389: ```
390: 
391: Notes:
392: 
393: - `enabled` defaults to true; when disabled, search falls back to in-process
394:   cosine similarity over stored embeddings.
395: - If the sqlite-vec extension is missing or fails to load, OpenClaw logs the
396:   error and continues with the JS fallback (no vector table).
397: - `extensionPath` overrides the bundled sqlite-vec path (useful for custom builds
398:   or non-standard install locations).
399: 
400: ### Local embedding auto-download
401: 
402: - Default local embedding model: `hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf` (~0.6 GB).
403: - When `memorySearch.provider = "local"`, `node-llama-cpp` resolves `modelPath`; if the GGUF is missing it **auto-downloads** to the cache (or `local.modelCacheDir` if set), then loads it. Downloads resume on retry.
404: - Native build requirement: run `pnpm approve-builds`, pick `node-llama-cpp`, then `pnpm rebuild node-llama-cpp`.
405: - Fallback: if local setup fails and `memorySearch.fallback = "openai"`, we automatically switch to remote embeddings (`openai/text-embedding-3-small` unless overridden) and record the reason.
406: 
407: ### Custom OpenAI-compatible endpoint example
408: 
409: ```json5
410: agents: {
411:   defaults: {
412:     memorySearch: {
413:       provider: "openai",
414:       model: "text-embedding-3-small",
415:       remote: {
416:         baseUrl: "https://api.example.com/v1/",
417:         apiKey: "YOUR_REMOTE_API_KEY",
418:         headers: {
419:           "X-Organization": "org-id",
420:           "X-Project": "project-id"
421:         }
422:       }
423:     }
424:   }
425: }
426: ```
427: 
428: Notes:
429: 
430: - `remote.*` takes precedence over `models.providers.openai.*`.
431: - `remote.headers` merge with OpenAI headers; remote wins on key conflicts. Omit `remote.headers` to use the OpenAI defaults.
432: 
read_file
{
  "end_line": 200,
  "file_path": "src/memory/manager.ts",
  "start_line": 1
}

Result:

1: import type { DatabaseSync } from "node:sqlite";
2: import chokidar, { type FSWatcher } from "chokidar";
3: import { randomUUID } from "node:crypto";
4: import fsSync from "node:fs";
5: import fs from "node:fs/promises";
6: import path from "node:path";
7: import type { ResolvedMemorySearchConfig } from "../agents/memory-search.js";
8: import type { OpenClawConfig } from "../config/config.js";
9: import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
10: import { resolveMemorySearchConfig } from "../agents/memory-search.js";
11: import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
12: import { createSubsystemLogger } from "../logging/subsystem.js";
13: import { onSessionTranscriptUpdate } from "../sessions/transcript-events.js";
14: import { resolveUserPath } from "../utils.js";
15: import { runGeminiEmbeddingBatches, type GeminiBatchRequest } from "./batch-gemini.js";
16: import {
17:   OPENAI_BATCH_ENDPOINT,
18:   type OpenAiBatchRequest,
19:   runOpenAiEmbeddingBatches,
20: } from "./batch-openai.js";
21: import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
22: import { DEFAULT_OPENAI_EMBEDDING_MODEL } from "./embeddings-openai.js";
23: import {
24:   createEmbeddingProvider,
25:   type EmbeddingProvider,
26:   type EmbeddingProviderResult,
27:   type GeminiEmbeddingClient,
28:   type OpenAiEmbeddingClient,
29: } from "./embeddings.js";
30: import { bm25RankToScore, buildFtsQuery, mergeHybridResults } from "./hybrid.js";
31: import {
32:   buildFileEntry,
33:   chunkMarkdown,
34:   ensureDir,
35:   hashText,
36:   isMemoryPath,
37:   listMemoryFiles,
38:   normalizeExtraMemoryPaths,
39:   type MemoryChunk,
40:   type MemoryFileEntry,
41:   parseEmbedding,
42: } from "./internal.js";
43: import { searchKeyword, searchVector } from "./manager-search.js";
44: import { ensureMemoryIndexSchema } from "./memory-schema.js";
45: import { loadSqliteVecExtension } from "./sqlite-vec.js";
46: import { requireNodeSqlite } from "./sqlite.js";
47: 
48: type MemorySource = "memory" | "sessions";
49: 
50: export type MemorySearchResult = {
51:   path: string;
52:   startLine: number;
53:   endLine: number;
54:   score: number;
55:   snippet: string;
56:   source: MemorySource;
57: };
58: 
59: type MemoryIndexMeta = {
60:   model: string;
61:   provider: string;
62:   providerKey?: string;
63:   chunkTokens: number;
64:   chunkOverlap: number;
65:   vectorDims?: number;
66: };
67: 
68: type SessionFileEntry = {
69:   path: string;
70:   absPath: string;
71:   mtimeMs: number;
72:   size: number;
73:   hash: string;
74:   content: string;
75: };
76: 
77: type MemorySyncProgressUpdate = {
78:   completed: number;
79:   total: number;
80:   label?: string;
81: };
82: 
83: type MemorySyncProgressState = {
84:   completed: number;
85:   total: number;
86:   label?: string;
87:   report: (update: MemorySyncProgressUpdate) => void;
88: };
89: 
90: const META_KEY = "memory_index_meta_v1";
91: const SNIPPET_MAX_CHARS = 700;
92: const VECTOR_TABLE = "chunks_vec";
93: const FTS_TABLE = "chunks_fts";
94: const EMBEDDING_CACHE_TABLE = "embedding_cache";
95: const SESSION_DIRTY_DEBOUNCE_MS = 5000;
96: const EMBEDDING_BATCH_MAX_TOKENS = 8000;
97: const EMBEDDING_APPROX_CHARS_PER_TOKEN = 1;
98: const EMBEDDING_INDEX_CONCURRENCY = 4;
99: const EMBEDDING_RETRY_MAX_ATTEMPTS = 3;
100: const EMBEDDING_RETRY_BASE_DELAY_MS = 500;
101: const EMBEDDING_RETRY_MAX_DELAY_MS = 8000;
102: const BATCH_FAILURE_LIMIT = 2;
103: const SESSION_DELTA_READ_CHUNK_BYTES = 64 * 1024;
104: const VECTOR_LOAD_TIMEOUT_MS = 30_000;
105: const EMBEDDING_QUERY_TIMEOUT_REMOTE_MS = 60_000;
106: const EMBEDDING_QUERY_TIMEOUT_LOCAL_MS = 5 * 60_000;
107: const EMBEDDING_BATCH_TIMEOUT_REMOTE_MS = 2 * 60_000;
108: const EMBEDDING_BATCH_TIMEOUT_LOCAL_MS = 10 * 60_000;
109: 
110: const log = createSubsystemLogger("memory");
111: 
112: const INDEX_CACHE = new Map<string, MemoryIndexManager>();
113: 
114: const vectorToBlob = (embedding: number[]): Buffer =>
115:   Buffer.from(new Float32Array(embedding).buffer);
116: 
117: export class MemoryIndexManager {
118:   private readonly cacheKey: string;
119:   private readonly cfg: OpenClawConfig;
120:   private readonly agentId: string;
121:   private readonly workspaceDir: string;
122:   private readonly settings: ResolvedMemorySearchConfig;
123:   private provider: EmbeddingProvider;
124:   private readonly requestedProvider: "openai" | "local" | "gemini" | "auto";
125:   private fallbackFrom?: "openai" | "local" | "gemini";
126:   private fallbackReason?: string;
127:   private openAi?: OpenAiEmbeddingClient;
128:   private gemini?: GeminiEmbeddingClient;
129:   private batch: {
130:     enabled: boolean;
131:     wait: boolean;
132:     concurrency: number;
133:     pollIntervalMs: number;
134:     timeoutMs: number;
135:   };
136:   private batchFailureCount = 0;
137:   private batchFailureLastError?: string;
138:   private batchFailureLastProvider?: string;
139:   private batchFailureLock: Promise<void> = Promise.resolve();
140:   private db: DatabaseSync;
141:   private readonly sources: Set<MemorySource>;
142:   private providerKey: string;
143:   private readonly cache: { enabled: boolean; maxEntries?: number };
144:   private readonly vector: {
145:     enabled: boolean;
146:     available: boolean | null;
147:     extensionPath?: string;
148:     loadError?: string;
149:     dims?: number;
150:   };
151:   private readonly fts: {
152:     enabled: boolean;
153:     available: boolean;
154:     loadError?: string;
155:   };
156:   private vectorReady: Promise<boolean> | null = null;
157:   private watcher: FSWatcher | null = null;
158:   private watchTimer: NodeJS.Timeout | null = null;
159:   private sessionWatchTimer: NodeJS.Timeout | null = null;
160:   private sessionUnsubscribe: (() => void) | null = null;
161:   private intervalTimer: NodeJS.Timeout | null = null;
162:   private closed = false;
163:   private dirty = false;
164:   private sessionsDirty = false;
165:   private sessionsDirtyFiles = new Set<string>();
166:   private sessionPendingFiles = new Set<string>();
167:   private sessionDeltas = new Map<
168:     string,
169:     { lastSize: number; pendingBytes: number; pendingMessages: number }
170:   >();
171:   private sessionWarm = new Set<string>();
172:   private syncing: Promise<void> | null = null;
173: 
174:   static async get(params: {
175:     cfg: OpenClawConfig;
176:     agentId: string;
177:   }): Promise<MemoryIndexManager | null> {
178:     const { cfg, agentId } = params;
179:     const settings = resolveMemorySearchConfig(cfg, agentId);
180:     if (!settings) {
181:       return null;
182:     }
183:     const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
184:     const key = `${agentId}:${workspaceDir}:${JSON.stringify(settings)}`;
185:     const existing = INDEX_CACHE.get(key);
186:     if (existing) {
187:       return existing;
188:     }
189:     const providerResult = await createEmbeddingProvider({
190:       config: cfg,
191:       agentDir: resolveAgentDir(cfg, agentId),
192:       provider: settings.provider,
193:       remote: settings.remote,
194:       model: settings.model,
195:       fallback: settings.fallback,
196:       local: settings.local,
197:     });
198:     const manager = new MemoryIndexManager({
199:       cacheKey: key,
200:       cfg,
read_file
{
  "file_path": "extensions/memory-core/index.ts"
}

Result:

1: import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2: import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3: 
4: const memoryCorePlugin = {
5:   id: "memory-core",
6:   name: "Memory (Core)",
7:   description: "File-backed memory search tools and CLI",
8:   kind: "memory",
9:   configSchema: emptyPluginConfigSchema(),
10:   register(api: OpenClawPluginApi) {
11:     api.registerTool(
12:       (ctx) => {
13:         const memorySearchTool = api.runtime.tools.createMemorySearchTool({
14:           config: ctx.config,
15:           agentSessionKey: ctx.sessionKey,
16:         });
17:         const memoryGetTool = api.runtime.tools.createMemoryGetTool({
18:           config: ctx.config,
19:           agentSessionKey: ctx.sessionKey,
20:         });
21:         if (!memorySearchTool || !memoryGetTool) {
22:           return null;
23:         }
24:         return [memorySearchTool, memoryGetTool];
25:       },
26:       { names: ["memory_search", "memory_get"] },
27:     );
28: 
29:     api.registerCli(
30:       ({ program }) => {
31:         api.runtime.tools.registerMemoryCli(program);
32:       },
33:       { commands: ["memory"] },
34:     );
35:   },
36: };
37: 
38: export default memoryCorePlugin;
39: 

Assistant

How Memory Works in OpenClaw

Summary

OpenClaw memory is plain Markdown files on disk — the agent only "remembers" what's written to files. There are two main components:

  1. Memory files (source of truth)
  2. Vector search index (enables semantic search over those files)

Memory Files

Location: Agent workspace (~/.openclaw/workspace by default)

File Purpose
memory/YYYY-MM-DD.md Daily log (append-only), today + yesterday loaded at session start
MEMORY.md Curated long-term memory (optional)

Key principle: If you want the agent to remember something, it must be written to these files.


Memory Plugins

Location: extensions/memory-core/index.ts

The default memory-core plugin registers two tools:

  • memory_search — semantic search over memory chunks (returns snippets + line ranges)
  • memory_get — read a specific memory file by path

Disable with plugins.slots.memory = "none".


Vector Memory Search

Location: src/memory/manager.ts (MemoryIndexManager class, line 117+)

How it works:

  1. Chunking: Markdown files are split into ~400-token chunks with 80-token overlap
  2. Embedding: Chunks are converted to vectors using OpenAI, Gemini, or local embeddings
  3. Storage: SQLite database at ~/.openclaw/memory/<agentId>.sqlite
  4. Search: Hybrid search combining:
    • Vector similarity (semantic match)
    • BM25 keyword relevance (exact tokens like IDs, code symbols)

Embedding providers (auto-selected):

  1. local if model path configured
  2. openai if OpenAI key available
  3. gemini if Gemini key available

Indexing triggers:

  • File watcher on memory directories (debounced 1.5s)
  • Session start
  • On search request
  • Periodic interval

Automatic Memory Flush

Location: src/auto-reply/reply/memory-flush.ts

When a session approaches auto-compaction, OpenClaw runs a silent agentic turn prompting the model to write durable memories before context is compacted.

compaction: {
  memoryFlush: {
    enabled: true,
    softThresholdTokens: 4000,  // triggers when this close to compaction
    prompt: "Write lasting notes to memory/..."
  }
}

Key Files

Path Purpose
src/memory/manager.ts MemoryIndexManager - indexing, search, file watching
src/memory/internal.ts Chunking, file listing, hashing utilities
src/agents/tools/memory-tool.ts Tool definitions (memory_search, memory_get)
src/auto-reply/reply/memory-flush.ts Pre-compaction memory flush logic
extensions/memory-core/index.ts Plugin that registers tools + CLI
docs/concepts/memory.md Full documentation

User

how is stuff vectorized, what are the options here?


Assistant

read_file
{
  "file_path": "src/memory/embeddings.ts"
}
glob
{
  "pattern": "src/memory/embeddings*.ts"
}
read_file
{
  "file_path": "src/agents/memory-search.ts"
}

read_file
{
  "file_path": "src/memory/embeddings.ts"
}

Result:

1: import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp";
2: import fsSync from "node:fs";
3: import type { OpenClawConfig } from "../config/config.js";
4: import { resolveUserPath } from "../utils.js";
5: import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js";
6: import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js";
7: import { importNodeLlamaCpp } from "./node-llama.js";
8: 
9: export type { GeminiEmbeddingClient } from "./embeddings-gemini.js";
10: export type { OpenAiEmbeddingClient } from "./embeddings-openai.js";
11: 
12: export type EmbeddingProvider = {
13:   id: string;
14:   model: string;
15:   embedQuery: (text: string) => Promise<number[]>;
16:   embedBatch: (texts: string[]) => Promise<number[][]>;
17: };
18: 
19: export type EmbeddingProviderResult = {
20:   provider: EmbeddingProvider;
21:   requestedProvider: "openai" | "local" | "gemini" | "auto";
22:   fallbackFrom?: "openai" | "local" | "gemini";
23:   fallbackReason?: string;
24:   openAi?: OpenAiEmbeddingClient;
25:   gemini?: GeminiEmbeddingClient;
26: };
27: 
28: export type EmbeddingProviderOptions = {
29:   config: OpenClawConfig;
30:   agentDir?: string;
31:   provider: "openai" | "local" | "gemini" | "auto";
32:   remote?: {
33:     baseUrl?: string;
34:     apiKey?: string;
35:     headers?: Record<string, string>;
36:   };
37:   model: string;
38:   fallback: "openai" | "gemini" | "local" | "none";
39:   local?: {
40:     modelPath?: string;
41:     modelCacheDir?: string;
42:   };
43: };
44: 
45: const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
46: 
47: function canAutoSelectLocal(options: EmbeddingProviderOptions): boolean {
48:   const modelPath = options.local?.modelPath?.trim();
49:   if (!modelPath) {
50:     return false;
51:   }
52:   if (/^(hf:|https?:)/i.test(modelPath)) {
53:     return false;
54:   }
55:   const resolved = resolveUserPath(modelPath);
56:   try {
57:     return fsSync.statSync(resolved).isFile();
58:   } catch {
59:     return false;
60:   }
61: }
62: 
63: function isMissingApiKeyError(err: unknown): boolean {
64:   const message = formatError(err);
65:   return message.includes("No API key found for provider");
66: }
67: 
68: async function createLocalEmbeddingProvider(
69:   options: EmbeddingProviderOptions,
70: ): Promise<EmbeddingProvider> {
71:   const modelPath = options.local?.modelPath?.trim() || DEFAULT_LOCAL_MODEL;
72:   const modelCacheDir = options.local?.modelCacheDir?.trim();
73: 
74:   // Lazy-load node-llama-cpp to keep startup light unless local is enabled.
75:   const { getLlama, resolveModelFile, LlamaLogLevel } = await importNodeLlamaCpp();
76: 
77:   let llama: Llama | null = null;
78:   let embeddingModel: LlamaModel | null = null;
79:   let embeddingContext: LlamaEmbeddingContext | null = null;
80: 
81:   const ensureContext = async () => {
82:     if (!llama) {
83:       llama = await getLlama({ logLevel: LlamaLogLevel.error });
84:     }
85:     if (!embeddingModel) {
86:       const resolved = await resolveModelFile(modelPath, modelCacheDir || undefined);
87:       embeddingModel = await llama.loadModel({ modelPath: resolved });
88:     }
89:     if (!embeddingContext) {
90:       embeddingContext = await embeddingModel.createEmbeddingContext();
91:     }
92:     return embeddingContext;
93:   };
94: 
95:   return {
96:     id: "local",
97:     model: modelPath,
98:     embedQuery: async (text) => {
99:       const ctx = await ensureContext();
100:       const embedding = await ctx.getEmbeddingFor(text);
101:       return Array.from(embedding.vector);
102:     },
103:     embedBatch: async (texts) => {
104:       const ctx = await ensureContext();
105:       const embeddings = await Promise.all(
106:         texts.map(async (text) => {
107:           const embedding = await ctx.getEmbeddingFor(text);
108:           return Array.from(embedding.vector);
109:         }),
110:       );
111:       return embeddings;
112:     },
113:   };
114: }
115: 
116: export async function createEmbeddingProvider(
117:   options: EmbeddingProviderOptions,
118: ): Promise<EmbeddingProviderResult> {
119:   const requestedProvider = options.provider;
120:   const fallback = options.fallback;
121: 
122:   const createProvider = async (id: "openai" | "local" | "gemini") => {
123:     if (id === "local") {
124:       const provider = await createLocalEmbeddingProvider(options);
125:       return { provider };
126:     }
127:     if (id === "gemini") {
128:       const { provider, client } = await createGeminiEmbeddingProvider(options);
129:       return { provider, gemini: client };
130:     }
131:     const { provider, client } = await createOpenAiEmbeddingProvider(options);
132:     return { provider, openAi: client };
133:   };
134: 
135:   const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini") =>
136:     provider === "local" ? formatLocalSetupError(err) : formatError(err);
137: 
138:   if (requestedProvider === "auto") {
139:     const missingKeyErrors: string[] = [];
140:     let localError: string | null = null;
141: 
142:     if (canAutoSelectLocal(options)) {
143:       try {
144:         const local = await createProvider("local");
145:         return { ...local, requestedProvider };
146:       } catch (err) {
147:         localError = formatLocalSetupError(err);
148:       }
149:     }
150: 
151:     for (const provider of ["openai", "gemini"] as const) {
152:       try {
153:         const result = await createProvider(provider);
154:         return { ...result, requestedProvider };
155:       } catch (err) {
156:         const message = formatPrimaryError(err, provider);
157:         if (isMissingApiKeyError(err)) {
158:           missingKeyErrors.push(message);
159:           continue;
160:         }
161:         throw new Error(message, { cause: err });
162:       }
163:     }
164: 
165:     const details = [...missingKeyErrors, localError].filter(Boolean) as string[];
166:     if (details.length > 0) {
167:       throw new Error(details.join("\n\n"));
168:     }
169:     throw new Error("No embeddings provider available.");
170:   }
171: 
172:   try {
173:     const primary = await createProvider(requestedProvider);
174:     return { ...primary, requestedProvider };
175:   } catch (primaryErr) {
176:     const reason = formatPrimaryError(primaryErr, requestedProvider);
177:     if (fallback && fallback !== "none" && fallback !== requestedProvider) {
178:       try {
179:         const fallbackResult = await createProvider(fallback);
180:         return {
181:           ...fallbackResult,
182:           requestedProvider,
183:           fallbackFrom: requestedProvider,
184:           fallbackReason: reason,
185:         };
186:       } catch (fallbackErr) {
187:         // oxlint-disable-next-line preserve-caught-error
188:         throw new Error(
189:           `${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`,
190:           { cause: fallbackErr },
191:         );
192:       }
193:     }
194:     throw new Error(reason, { cause: primaryErr });
195:   }
196: }
197: 
198: function formatError(err: unknown): string {
199:   if (err instanceof Error) {
200:     return err.message;
201:   }
202:   return String(err);
203: }
204: 
205: function isNodeLlamaCppMissing(err: unknown): boolean {
206:   if (!(err instanceof Error)) {
207:     return false;
208:   }
209:   const code = (err as Error & { code?: unknown }).code;
210:   if (code === "ERR_MODULE_NOT_FOUND") {
211:     return err.message.includes("node-llama-cpp");
212:   }
213:   return false;
214: }
215: 
216: function formatLocalSetupError(err: unknown): string {
217:   const detail = formatError(err);
218:   const missing = isNodeLlamaCppMissing(err);
219:   return [
220:     "Local embeddings unavailable.",
221:     missing
222:       ? "Reason: optional dependency node-llama-cpp is missing (or failed to install)."
223:       : detail
224:         ? `Reason: ${detail}`
225:         : undefined,
226:     missing && detail ? `Detail: ${detail}` : null,
227:     "To enable local embeddings:",
228:     "1) Use Node 22 LTS (recommended for installs/updates)",
229:     missing
230:       ? "2) Reinstall OpenClaw (this should install node-llama-cpp): npm i -g openclaw@latest"
231:       : null,
232:     "3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp",
233:     'Or set agents.defaults.memorySearch.provider = "openai" (remote).',
234:   ]
235:     .filter(Boolean)
236:     .join("\n");
237: }
238: 
glob
{
  "pattern": "src/memory/embeddings*.ts"
}

Result:

[f]    5K  2026-02-01 17:57  /home/sam/Source/openclaw/src/memory/embeddings-gemini.ts
[f]    3K  2026-02-01 17:57  /home/sam/Source/openclaw/src/memory/embeddings-openai.ts
[f]   10K  2026-02-01 17:57  /home/sam/Source/openclaw/src/memory/embeddings.test.ts
[f]    7K  2026-02-01 17:57  /home/sam/Source/openclaw/src/memory/embeddings.ts
read_file
{
  "file_path": "src/agents/memory-search.ts"
}

Result:

1: import os from "node:os";
2: import path from "node:path";
3: import type { OpenClawConfig, MemorySearchConfig } from "../config/config.js";
4: import { resolveStateDir } from "../config/paths.js";
5: import { clampInt, clampNumber, resolveUserPath } from "../utils.js";
6: import { resolveAgentConfig } from "./agent-scope.js";
7: 
8: export type ResolvedMemorySearchConfig = {
9:   enabled: boolean;
10:   sources: Array<"memory" | "sessions">;
11:   extraPaths: string[];
12:   provider: "openai" | "local" | "gemini" | "auto";
13:   remote?: {
14:     baseUrl?: string;
15:     apiKey?: string;
16:     headers?: Record<string, string>;
17:     batch?: {
18:       enabled: boolean;
19:       wait: boolean;
20:       concurrency: number;
21:       pollIntervalMs: number;
22:       timeoutMinutes: number;
23:     };
24:   };
25:   experimental: {
26:     sessionMemory: boolean;
27:   };
28:   fallback: "openai" | "gemini" | "local" | "none";
29:   model: string;
30:   local: {
31:     modelPath?: string;
32:     modelCacheDir?: string;
33:   };
34:   store: {
35:     driver: "sqlite";
36:     path: string;
37:     vector: {
38:       enabled: boolean;
39:       extensionPath?: string;
40:     };
41:   };
42:   chunking: {
43:     tokens: number;
44:     overlap: number;
45:   };
46:   sync: {
47:     onSessionStart: boolean;
48:     onSearch: boolean;
49:     watch: boolean;
50:     watchDebounceMs: number;
51:     intervalMinutes: number;
52:     sessions: {
53:       deltaBytes: number;
54:       deltaMessages: number;
55:     };
56:   };
57:   query: {
58:     maxResults: number;
59:     minScore: number;
60:     hybrid: {
61:       enabled: boolean;
62:       vectorWeight: number;
63:       textWeight: number;
64:       candidateMultiplier: number;
65:     };
66:   };
67:   cache: {
68:     enabled: boolean;
69:     maxEntries?: number;
70:   };
71: };
72: 
73: const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
74: const DEFAULT_GEMINI_MODEL = "gemini-embedding-001";
75: const DEFAULT_CHUNK_TOKENS = 400;
76: const DEFAULT_CHUNK_OVERLAP = 80;
77: const DEFAULT_WATCH_DEBOUNCE_MS = 1500;
78: const DEFAULT_SESSION_DELTA_BYTES = 100_000;
79: const DEFAULT_SESSION_DELTA_MESSAGES = 50;
80: const DEFAULT_MAX_RESULTS = 6;
81: const DEFAULT_MIN_SCORE = 0.35;
82: const DEFAULT_HYBRID_ENABLED = true;
83: const DEFAULT_HYBRID_VECTOR_WEIGHT = 0.7;
84: const DEFAULT_HYBRID_TEXT_WEIGHT = 0.3;
85: const DEFAULT_HYBRID_CANDIDATE_MULTIPLIER = 4;
86: const DEFAULT_CACHE_ENABLED = true;
87: const DEFAULT_SOURCES: Array<"memory" | "sessions"> = ["memory"];
88: 
89: function normalizeSources(
90:   sources: Array<"memory" | "sessions"> | undefined,
91:   sessionMemoryEnabled: boolean,
92: ): Array<"memory" | "sessions"> {
93:   const normalized = new Set<"memory" | "sessions">();
94:   const input = sources?.length ? sources : DEFAULT_SOURCES;
95:   for (const source of input) {
96:     if (source === "memory") {
97:       normalized.add("memory");
98:     }
99:     if (source === "sessions" && sessionMemoryEnabled) {
100:       normalized.add("sessions");
101:     }
102:   }
103:   if (normalized.size === 0) {
104:     normalized.add("memory");
105:   }
106:   return Array.from(normalized);
107: }
108: 
109: function resolveStorePath(agentId: string, raw?: string): string {
110:   const stateDir = resolveStateDir(process.env, os.homedir);
111:   const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`);
112:   if (!raw) {
113:     return fallback;
114:   }
115:   const withToken = raw.includes("{agentId}") ? raw.replaceAll("{agentId}", agentId) : raw;
116:   return resolveUserPath(withToken);
117: }
118: 
119: function mergeConfig(
120:   defaults: MemorySearchConfig | undefined,
121:   overrides: MemorySearchConfig | undefined,
122:   agentId: string,
123: ): ResolvedMemorySearchConfig {
124:   const enabled = overrides?.enabled ?? defaults?.enabled ?? true;
125:   const sessionMemory =
126:     overrides?.experimental?.sessionMemory ?? defaults?.experimental?.sessionMemory ?? false;
127:   const provider = overrides?.provider ?? defaults?.provider ?? "auto";
128:   const defaultRemote = defaults?.remote;
129:   const overrideRemote = overrides?.remote;
130:   const hasRemoteConfig = Boolean(
131:     overrideRemote?.baseUrl ||
132:     overrideRemote?.apiKey ||
133:     overrideRemote?.headers ||
134:     defaultRemote?.baseUrl ||
135:     defaultRemote?.apiKey ||
136:     defaultRemote?.headers,
137:   );
138:   const includeRemote =
139:     hasRemoteConfig || provider === "openai" || provider === "gemini" || provider === "auto";
140:   const batch = {
141:     enabled: overrideRemote?.batch?.enabled ?? defaultRemote?.batch?.enabled ?? true,
142:     wait: overrideRemote?.batch?.wait ?? defaultRemote?.batch?.wait ?? true,
143:     concurrency: Math.max(
144:       1,
145:       overrideRemote?.batch?.concurrency ?? defaultRemote?.batch?.concurrency ?? 2,
146:     ),
147:     pollIntervalMs:
148:       overrideRemote?.batch?.pollIntervalMs ?? defaultRemote?.batch?.pollIntervalMs ?? 2000,
149:     timeoutMinutes:
150:       overrideRemote?.batch?.timeoutMinutes ?? defaultRemote?.batch?.timeoutMinutes ?? 60,
151:   };
152:   const remote = includeRemote
153:     ? {
154:         baseUrl: overrideRemote?.baseUrl ?? defaultRemote?.baseUrl,
155:         apiKey: overrideRemote?.apiKey ?? defaultRemote?.apiKey,
156:         headers: overrideRemote?.headers ?? defaultRemote?.headers,
157:         batch,
158:       }
159:     : undefined;
160:   const fallback = overrides?.fallback ?? defaults?.fallback ?? "none";
161:   const modelDefault =
162:     provider === "gemini"
163:       ? DEFAULT_GEMINI_MODEL
164:       : provider === "openai"
165:         ? DEFAULT_OPENAI_MODEL
166:         : undefined;
167:   const model = overrides?.model ?? defaults?.model ?? modelDefault ?? "";
168:   const local = {
169:     modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath,
170:     modelCacheDir: overrides?.local?.modelCacheDir ?? defaults?.local?.modelCacheDir,
171:   };
172:   const sources = normalizeSources(overrides?.sources ?? defaults?.sources, sessionMemory);
173:   const rawPaths = [...(defaults?.extraPaths ?? []), ...(overrides?.extraPaths ?? [])]
174:     .map((value) => value.trim())
175:     .filter(Boolean);
176:   const extraPaths = Array.from(new Set(rawPaths));
177:   const vector = {
178:     enabled: overrides?.store?.vector?.enabled ?? defaults?.store?.vector?.enabled ?? true,
179:     extensionPath:
180:       overrides?.store?.vector?.extensionPath ?? defaults?.store?.vector?.extensionPath,
181:   };
182:   const store = {
183:     driver: overrides?.store?.driver ?? defaults?.store?.driver ?? "sqlite",
184:     path: resolveStorePath(agentId, overrides?.store?.path ?? defaults?.store?.path),
185:     vector,
186:   };
187:   const chunking = {
188:     tokens: overrides?.chunking?.tokens ?? defaults?.chunking?.tokens ?? DEFAULT_CHUNK_TOKENS,
189:     overlap: overrides?.chunking?.overlap ?? defaults?.chunking?.overlap ?? DEFAULT_CHUNK_OVERLAP,
190:   };
191:   const sync = {
192:     onSessionStart: overrides?.sync?.onSessionStart ?? defaults?.sync?.onSessionStart ?? true,
193:     onSearch: overrides?.sync?.onSearch ?? defaults?.sync?.onSearch ?? true,
194:     watch: overrides?.sync?.watch ?? defaults?.sync?.watch ?? true,
195:     watchDebounceMs:
196:       overrides?.sync?.watchDebounceMs ??
197:       defaults?.sync?.watchDebounceMs ??
198:       DEFAULT_WATCH_DEBOUNCE_MS,
199:     intervalMinutes: overrides?.sync?.intervalMinutes ?? defaults?.sync?.intervalMinutes ?? 0,
200:     sessions: {
201:       deltaBytes:
202:         overrides?.sync?.sessions?.deltaBytes ??
203:         defaults?.sync?.sessions?.deltaBytes ??
204:         DEFAULT_SESSION_DELTA_BYTES,
205:       deltaMessages:
206:         overrides?.sync?.sessions?.deltaMessages ??
207:         defaults?.sync?.sessions?.deltaMessages ??
208:         DEFAULT_SESSION_DELTA_MESSAGES,
209:     },
210:   };
211:   const query = {
212:     maxResults: overrides?.query?.maxResults ?? defaults?.query?.maxResults ?? DEFAULT_MAX_RESULTS,
213:     minScore: overrides?.query?.minScore ?? defaults?.query?.minScore ?? DEFAULT_MIN_SCORE,
214:   };
215:   const hybrid = {
216:     enabled:
217:       overrides?.query?.hybrid?.enabled ??
218:       defaults?.query?.hybrid?.enabled ??
219:       DEFAULT_HYBRID_ENABLED,
220:     vectorWeight:
221:       overrides?.query?.hybrid?.vectorWeight ??
222:       defaults?.query?.hybrid?.vectorWeight ??
223:       DEFAULT_HYBRID_VECTOR_WEIGHT,
224:     textWeight:
225:       overrides?.query?.hybrid?.textWeight ??
226:       defaults?.query?.hybrid?.textWeight ??
227:       DEFAULT_HYBRID_TEXT_WEIGHT,
228:     candidateMultiplier:
229:       overrides?.query?.hybrid?.candidateMultiplier ??
230:       defaults?.query?.hybrid?.candidateMultiplier ??
231:       DEFAULT_HYBRID_CANDIDATE_MULTIPLIER,
232:   };
233:   const cache = {
234:     enabled: overrides?.cache?.enabled ?? defaults?.cache?.enabled ?? DEFAULT_CACHE_ENABLED,
235:     maxEntries: overrides?.cache?.maxEntries ?? defaults?.cache?.maxEntries,
236:   };
237: 
238:   const overlap = clampNumber(chunking.overlap, 0, Math.max(0, chunking.tokens - 1));
239:   const minScore = clampNumber(query.minScore, 0, 1);
240:   const vectorWeight = clampNumber(hybrid.vectorWeight, 0, 1);
241:   const textWeight = clampNumber(hybrid.textWeight, 0, 1);
242:   const sum = vectorWeight + textWeight;
243:   const normalizedVectorWeight = sum > 0 ? vectorWeight / sum : DEFAULT_HYBRID_VECTOR_WEIGHT;
244:   const normalizedTextWeight = sum > 0 ? textWeight / sum : DEFAULT_HYBRID_TEXT_WEIGHT;
245:   const candidateMultiplier = clampInt(hybrid.candidateMultiplier, 1, 20);
246:   const deltaBytes = clampInt(sync.sessions.deltaBytes, 0, Number.MAX_SAFE_INTEGER);
247:   const deltaMessages = clampInt(sync.sessions.deltaMessages, 0, Number.MAX_SAFE_INTEGER);
248:   return {
249:     enabled,
250:     sources,
251:     extraPaths,
252:     provider,
253:     remote,
254:     experimental: {
255:       sessionMemory,
256:     },
257:     fallback,
258:     model,
259:     local,
260:     store,
261:     chunking: { tokens: Math.max(1, chunking.tokens), overlap },
262:     sync: {
263:       ...sync,
264:       sessions: {
265:         deltaBytes,
266:         deltaMessages,
267:       },
268:     },
269:     query: {
270:       ...query,
271:       minScore,
272:       hybrid: {
273:         enabled: Boolean(hybrid.enabled),
274:         vectorWeight: normalizedVectorWeight,
275:         textWeight: normalizedTextWeight,
276:         candidateMultiplier,
277:       },
278:     },
279:     cache: {
280:       enabled: Boolean(cache.enabled),
281:       maxEntries:
282:         typeof cache.maxEntries === "number" && Number.isFinite(cache.maxEntries)
283:           ? Math.max(1, Math.floor(cache.maxEntries))
284:           : undefined,
285:     },
286:   };
287: }
288: 
289: export function resolveMemorySearchConfig(
290:   cfg: OpenClawConfig,
291:   agentId: string,
292: ): ResolvedMemorySearchConfig | null {
293:   const defaults = cfg.agents?.defaults?.memorySearch;
294:   const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch;
295:   const resolved = mergeConfig(defaults, overrides, agentId);
296:   if (!resolved.enabled) {
297:     return null;
298:   }
299:   return resolved;
300: }
301: 

Assistant

Vectorization / Embedding Options

Summary

Location: src/memory/embeddings.ts (provider factory), src/agents/memory-search.ts (config resolution)

OpenClaw supports 3 embedding providers with automatic fallback:

Provider Model Notes
OpenAI text-embedding-3-small (default) Remote API, supports batch mode
Gemini gemini-embedding-001 (default) Remote API, supports batch mode
Local embeddinggemma-300M-Q8_0.gguf (~0.6GB) Uses node-llama-cpp, runs on-device

Provider Selection (provider: "auto")

When set to "auto" (the default), OpenClaw tries in order:

  1. Local — only if local.modelPath is configured AND the file exists on disk
  2. OpenAI — if an API key is available
  3. Gemini — if a Gemini API key is available
// src/memory/embeddings.ts:138-169
if (requestedProvider === "auto") {
  if (canAutoSelectLocal(options)) {
    // try local first
  }
  for (const provider of ["openai", "gemini"] as const) {
    // try remote providers
  }
}

Configuration

agents: {
  defaults: {
    memorySearch: {
      provider: "openai",        // "openai" | "gemini" | "local" | "auto"
      model: "text-embedding-3-small",
      fallback: "gemini",        // "openai" | "gemini" | "local" | "none"

      // Remote options (OpenAI/Gemini)
      remote: {
        baseUrl: "https://api.example.com/v1/",  // custom endpoint
        apiKey: "YOUR_KEY",
        headers: { "X-Custom": "value" },
        batch: {
          enabled: true,         // async batch API (cheaper/faster for large indexes)
          concurrency: 2
        }
      },

      // Local options
      local: {
        modelPath: "hf:ggml-org/embeddinggemma-300M-GGUF/...",  // or local path
        modelCacheDir: "/path/to/cache"
      }
    }
  }
}

Provider Implementations

OpenAI (src/memory/embeddings-openai.ts)

  • Uses OpenAI SDK
  • Default model: text-embedding-3-small
  • Supports batch API for large indexing jobs (cheaper pricing)

Gemini (src/memory/embeddings-gemini.ts)

  • Uses Gemini embeddings API directly
  • Default model: gemini-embedding-001
  • Supports async batch endpoint

Local (src/memory/embeddings.ts:68-114)

  • Uses node-llama-cpp with GGUF models
  • Default model: embeddinggemma-300M-Q8_0.gguf (~0.6GB, auto-downloads)
  • Requires native build: pnpm approve-builds → select node-llama-cpp

Fallback Behavior

If the primary provider fails, OpenClaw can fall back:

// src/memory/embeddings.ts:172-195
try {
  const primary = await createProvider(requestedProvider);
  return { ...primary, requestedProvider };
} catch (primaryErr) {
  if (fallback && fallback !== "none") {
    const fallbackResult = await createProvider(fallback);
    return { ...fallbackResult, fallbackFrom: requestedProvider, fallbackReason: ... };
  }
}

The result tracks fallbackFrom and fallbackReason so you know when fallback was used.


Key Files

File Purpose
src/memory/embeddings.ts Provider factory + local provider
src/memory/embeddings-openai.ts OpenAI provider
src/memory/embeddings-gemini.ts Gemini provider
src/agents/memory-search.ts Config resolution + defaults

User

can I configure chunking on my bot


Assistant

Yes! Chunking is configurable via agents.defaults.memorySearch.chunking:

agents: {
  defaults: {
    memorySearch: {
      chunking: {
        tokens: 400,    // target chunk size (default: 400)
        overlap: 80     // overlap between chunks (default: 80)
      }
    }
  }
}

Location: src/agents/memory-search.ts:73-76, 187-189

const DEFAULT_CHUNK_TOKENS = 400;
const DEFAULT_CHUNK_OVERLAP = 80;

// ...
chunking: {
  tokens: overrides?.chunking?.tokens ?? defaults?.chunking?.tokens ?? DEFAULT_CHUNK_TOKENS,
  overlap: overrides?.chunking?.overlap ?? defaults?.chunking?.overlap ?? DEFAULT_CHUNK_OVERLAP,
}

Notes:

  • overlap is clamped to [0, tokens - 1]
  • Changing chunking params triggers a full reindex (the index stores chunking config and detects changes)
  • Per-agent overrides are supported via agents.entries.<agentId>.memorySearch.chunking

User

how many tokens total are injected from memory? are they injected always? via tools on demand? some other way?


Assistant

grep
{
  "include": "*.ts",
  "max_results": 50,
  "pattern": "memory_search|memorySearch.*inject|loadMemory|memory.*context|inject.*memory"
}
read_file
{
  "file_path": "src/agents/tools/memory-tool.ts"
}

grep
{
  "include": "*.ts",
  "max_results": 50,
  "pattern": "memory_search|memorySearch.*inject|loadMemory|memory.*context|inject.*memory"
}

Result:

/home/sam/Source/openclaw/extensions/memory-lancedb/index.ts:188
> 188:   // Skip injected context from memory recall
  189:   if (text.includes("<relevant-memories>")) {
  190:     return false;
  191:   }
  192:             .map((r) => `- [${r.entry.category}] ${r.entry.text}`)
  193:             .join("\n");
  194: 

---
/home/sam/Source/openclaw/extensions/memory-lancedb/index.ts:495
  492: import { getBearerToken, getHeader } from "./http-utils.js";
  493: 
  494: const DEFAULT_BODY_BYTES = 2 * 1024 * 1024;
> 495:           api.logger.info?.(`memory-lancedb: injecting ${results.length} memories into context`);
  496: 
  497:           return {
  498:             prependContext: `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,

---
/home/sam/Source/openclaw/src/gateway/tools-invoke-http.ts:35
  32:         }
  33:         return [memorySearchTool, memoryGetTool];
  34:       },
> 35: const MEMORY_TOOL_NAMES = new Set(["memory_search", "memory_get"]);
  36: 
  37: type ToolsInvokeBody = {
  38:   tool?: unknown;

---
/home/sam/Source/openclaw/extensions/memory-core/index.ts:26
> 26:       { names: ["memory_search", "memory_get"] },
  27:     );
  28: 
  29:     api.registerCli(
  30: 
  31:     // Log completion (but don't send user-visible confirmation - it's internal housekeeping)
  32:     const relPath = memoryFilePath.replace(os.homedir(), "~");

---
/home/sam/Source/openclaw/src/hooks/bundled/session-memory/handler.ts:173
> 173:     console.log(`[session-memory] Session context saved to ${relPath}`);
  174:   } catch (err) {
  175:     console.error(
  176:       "[session-memory] Failed to save session memory:",
  177: export type AgentCompactionMemoryFlushConfig = {
  178:   /** Enable the pre-compaction memory flush (default: true). */
  179:   enabled?: boolean;

---
/home/sam/Source/openclaw/src/config/types.agent-defaults.ts:256
  253: 
  254: export const TOOL_GROUPS: Record<string, string[]> = {
  255:   // NOTE: Keep canonical (lowercase) tool names here.
> 256:   /** Run the memory flush when context is within this many tokens of the compaction threshold. */
  257:   softThresholdTokens?: number;
  258:   /** User prompt used for the memory flush turn (NO_REPLY is enforced if missing). */
  259:   prompt?: string;

---
/home/sam/Source/openclaw/src/agents/tool-policy.ts:15
> 15:   "group:memory": ["memory_search", "memory_get"],
  16:   "group:web": ["web_search", "web_fetch"],
  17:   // Basic workspace/file tools
  18:   "group:fs": ["read", "write", "edit", "apply_patch"],
  19:     "sessions_send",
  20:     "sessions_spawn",
  21:     "session_status",

---
/home/sam/Source/openclaw/src/agents/tool-policy.ts:51
  48:       skillsPrompt:
  49:         "<available_skills>\n  <skill>\n    <name>demo</name>\n  </skill>\n</available_skills>",
  50:       heartbeatPrompt: "ping",
> 51:     "memory_search",
  52:     "memory_get",
  53:     "web_search",
  54:     "web_fetch",

---
/home/sam/Source/openclaw/src/agents/system-prompt.test.ts:34
> 34:       toolNames: ["message", "memory_search"],
  35:       docsPath: "/tmp/openclaw/docs",
  36:       extraSystemPrompt: "Subagent details",
  37:       ttsHint: "Voice (TTS) is enabled.",
  38: 
  39:     const policy = resolveSandboxToolPolicyForAgent(cfg, "work");
  40:     expect(policy.allow).toEqual([

---
/home/sam/Source/openclaw/src/agents/sandbox-explain.test.ts:58
> 58:       "memory_search",
  59:       "memory_get",
  60:       "read",
  61:       "write",
  62:     };
  63: 
  64:     const policy = resolveSandboxToolPolicyForAgent(cfg, "main");

---
/home/sam/Source/openclaw/src/agents/sandbox-explain.test.ts:86
  83: /**
  84:  * Opt-in context pruning (“microcompact”-style) for Pi sessions.
  85:  *
> 86:     expect(policy.allow).toContain("memory_search");
  87:     expect(policy.allow).toContain("memory_get");
  88:     expect(policy.deny).toContain("memory_get");
  89:   });

---
/home/sam/Source/openclaw/src/agents/pi-extensions/context-pruning.ts:4
> 4:  * This only affects the in-memory context for the current request; it does not rewrite session
  5:  * history persisted on disk.
  6:  */
  7: 
  8:   }
  9:   return {
  10:     label: "Memory Search",

---
/home/sam/Source/openclaw/src/agents/tools/memory-tool.ts:38
> 38:     name: "memory_search",
  39:     description:
  40:       "Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines.",
  41:     parameters: MemorySearchSchema,
  42:     label: "Memory Get",
  43:     name: "memory_get",
  44:     description:

---
/home/sam/Source/openclaw/src/agents/tools/memory-tool.ts:93
  90: import { createMemoryGetTool, createMemorySearchTool } from "./memory-tool.js";
  91: 
  92: describe("memory tools", () => {
> 93:       "Safe snippet read from MEMORY.md, memory/*.md, or configured memorySearch.extraPaths with optional from/lines; use after memory_search to pull only the needed lines and keep context small.",
  94:     parameters: MemoryGetSchema,
  95:     execute: async (_toolCallId, params) => {
  96:       const relPath = readStringParam(params, "path", { required: true });

---
/home/sam/Source/openclaw/src/agents/tools/memory-tool.does-not-crash-on-errors.test.ts:33
> 33:   it("does not throw when memory_search fails (e.g. embeddings 429)", async () => {
  34:     const cfg = { agents: { list: [{ id: "main", default: true }] } };
  35:     const tool = createMemorySearchTool({ config: cfg });
  36:     expect(tool).not.toBeNull();
  37:   if (params.isMinimal) {
  38:     return [];
  39:   }

---
/home/sam/Source/openclaw/src/agents/system-prompt.ts:43
> 43:   if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
  44:     return [];
  45:   }
  46:   return [
  47:     "## Memory Recall",

---
/home/sam/Source/openclaw/src/agents/system-prompt.ts:48
> 48:     "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
  49:     "",
  50:   ];
  51: }
  52:   "session_status",
  53:   "cron",
  54:   // Memory - pass relevant info in spawn prompt instead

---
/home/sam/Source/openclaw/src/agents/pi-tools.policy.ts:94
> 94:   "memory_search",
  95:   "memory_get",
  96: ];
  97: 

read_file
{
  "file_path": "src/agents/tools/memory-tool.ts"
}

Result:

1: import { Type } from "@sinclair/typebox";
2: import type { OpenClawConfig } from "../../config/config.js";
3: import type { AnyAgentTool } from "./common.js";
4: import { getMemorySearchManager } from "../../memory/index.js";
5: import { resolveSessionAgentId } from "../agent-scope.js";
6: import { resolveMemorySearchConfig } from "../memory-search.js";
7: import { jsonResult, readNumberParam, readStringParam } from "./common.js";
8: 
9: const MemorySearchSchema = Type.Object({
10:   query: Type.String(),
11:   maxResults: Type.Optional(Type.Number()),
12:   minScore: Type.Optional(Type.Number()),
13: });
14: 
15: const MemoryGetSchema = Type.Object({
16:   path: Type.String(),
17:   from: Type.Optional(Type.Number()),
18:   lines: Type.Optional(Type.Number()),
19: });
20: 
21: export function createMemorySearchTool(options: {
22:   config?: OpenClawConfig;
23:   agentSessionKey?: string;
24: }): AnyAgentTool | null {
25:   const cfg = options.config;
26:   if (!cfg) {
27:     return null;
28:   }
29:   const agentId = resolveSessionAgentId({
30:     sessionKey: options.agentSessionKey,
31:     config: cfg,
32:   });
33:   if (!resolveMemorySearchConfig(cfg, agentId)) {
34:     return null;
35:   }
36:   return {
37:     label: "Memory Search",
38:     name: "memory_search",
39:     description:
40:       "Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines.",
41:     parameters: MemorySearchSchema,
42:     execute: async (_toolCallId, params) => {
43:       const query = readStringParam(params, "query", { required: true });
44:       const maxResults = readNumberParam(params, "maxResults");
45:       const minScore = readNumberParam(params, "minScore");
46:       const { manager, error } = await getMemorySearchManager({
47:         cfg,
48:         agentId,
49:       });
50:       if (!manager) {
51:         return jsonResult({ results: [], disabled: true, error });
52:       }
53:       try {
54:         const results = await manager.search(query, {
55:           maxResults,
56:           minScore,
57:           sessionKey: options.agentSessionKey,
58:         });
59:         const status = manager.status();
60:         return jsonResult({
61:           results,
62:           provider: status.provider,
63:           model: status.model,
64:           fallback: status.fallback,
65:         });
66:       } catch (err) {
67:         const message = err instanceof Error ? err.message : String(err);
68:         return jsonResult({ results: [], disabled: true, error: message });
69:       }
70:     },
71:   };
72: }
73: 
74: export function createMemoryGetTool(options: {
75:   config?: OpenClawConfig;
76:   agentSessionKey?: string;
77: }): AnyAgentTool | null {
78:   const cfg = options.config;
79:   if (!cfg) {
80:     return null;
81:   }
82:   const agentId = resolveSessionAgentId({
83:     sessionKey: options.agentSessionKey,
84:     config: cfg,
85:   });
86:   if (!resolveMemorySearchConfig(cfg, agentId)) {
87:     return null;
88:   }
89:   return {
90:     label: "Memory Get",
91:     name: "memory_get",
92:     description:
93:       "Safe snippet read from MEMORY.md, memory/*.md, or configured memorySearch.extraPaths with optional from/lines; use after memory_search to pull only the needed lines and keep context small.",
94:     parameters: MemoryGetSchema,
95:     execute: async (_toolCallId, params) => {
96:       const relPath = readStringParam(params, "path", { required: true });
97:       const from = readNumberParam(params, "from", { integer: true });
98:       const lines = readNumberParam(params, "lines", { integer: true });
99:       const { manager, error } = await getMemorySearchManager({
100:         cfg,
101:         agentId,
102:       });
103:       if (!manager) {
104:         return jsonResult({ path: relPath, text: "", disabled: true, error });
105:       }
106:       try {
107:         const result = await manager.readFile({
108:           relPath,
109:           from: from ?? undefined,
110:           lines: lines ?? undefined,
111:         });
112:         return jsonResult(result);
113:       } catch (err) {
114:         const message = err instanceof Error ? err.message : String(err);
115:         return jsonResult({ path: relPath, text: "", disabled: true, error: message });
116:       }
117:     },
118:   };
119: }
120: 

Assistant

Memory Injection: Tools On-Demand (Not Auto-Injected)

How It Works

Memory in memory-core (the default plugin) is not automatically injected into context. Instead, it's accessed on-demand via tools:

Tool Purpose
memory_search Semantic search, returns snippets (~700 chars max each)
memory_get Read specific file/lines after search

Location: src/agents/tools/memory-tool.ts


Token Budget

There's no fixed token budget for memory. The model decides when/how much to retrieve:

  1. memory_search returns up to maxResults snippets (default: 6)
  2. Each snippet is capped at ~700 characters (SNIPPET_MAX_CHARS in src/memory/manager.ts:91)
  3. memory_get can read more lines if needed (model controls from/lines params)
// src/agents/memory-search.ts:80-81
const DEFAULT_MAX_RESULTS = 6;
const DEFAULT_MIN_SCORE = 0.35;

System Prompt Guidance

The system prompt tells the model when to use memory tools:

// src/agents/system-prompt.ts:43-50
if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
  return [];
}
return [
  "## Memory Recall",
  "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
];

Alternative: memory-lancedb Plugin (Auto-Injection)

The memory-lancedb extension (not default) does auto-inject memories:

// extensions/memory-lancedb/index.ts:495-498
api.logger.info?.(`memory-lancedb: injecting ${results.length} memories into context`);
return {
  prependContext: `<relevant-memories>\nThe following memories may be relevant...\n</relevant-memories>`,
};

Summary

Plugin Injection Style Token Control
memory-core (default) On-demand via tools Model decides (default 6 snippets × ~700 chars)
memory-lancedb Auto-prepended to context Plugin-controlled

To tune memory-core:

agents: {
  defaults: {
    memorySearch: {
      query: {
        maxResults: 6,    // max snippets returned
        minScore: 0.35    // similarity threshold
      }
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment