Exported from term-llm
| 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
| Turns | Tool Calls | Tokens |
|---|---|---|
| 4 user / 8 LLM | 11 | 241.7K in / 3.7K out |
How does memory work?
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" };
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:
OpenClaw memory is plain Markdown files on disk — the agent only "remembers" what's written to files. There are two main components:
- Memory files (source of truth)
- Vector search index (enables semantic search over those 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.
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".
Location: src/memory/manager.ts (MemoryIndexManager class, line 117+)
- Chunking: Markdown files are split into ~400-token chunks with 80-token overlap
- Embedding: Chunks are converted to vectors using OpenAI, Gemini, or local embeddings
- Storage: SQLite database at
~/.openclaw/memory/<agentId>.sqlite - Search: Hybrid search combining:
- Vector similarity (semantic match)
- BM25 keyword relevance (exact tokens like IDs, code symbols)
localif model path configuredopenaiif OpenAI key availablegeminiif Gemini key available
- File watcher on memory directories (debounced 1.5s)
- Session start
- On search request
- Periodic interval
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/..."
}
}| 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 |
how is stuff vectorized, what are the options here?
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:
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 |
When set to "auto" (the default), OpenClaw tries in order:
- Local — only if
local.modelPathis configured AND the file exists on disk - OpenAI — if an API key is available
- 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
}
}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"
}
}
}
}- Uses OpenAI SDK
- Default model:
text-embedding-3-small - Supports batch API for large indexing jobs (cheaper pricing)
- Uses Gemini embeddings API directly
- Default model:
gemini-embedding-001 - Supports async batch endpoint
- Uses
node-llama-cppwith GGUF models - Default model:
embeddinggemma-300M-Q8_0.gguf(~0.6GB, auto-downloads) - Requires native build:
pnpm approve-builds→ selectnode-llama-cpp
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.
| 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 |
can I configure chunking on my bot
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:
overlapis 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
how many tokens total are injected from memory? are they injected always? via tools on demand? some other way?
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:
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
There's no fixed token budget for memory. The model decides when/how much to retrieve:
memory_searchreturns up tomaxResultssnippets (default: 6)- Each snippet is capped at ~700 characters (
SNIPPET_MAX_CHARSinsrc/memory/manager.ts:91) memory_getcan read more lines if needed (model controlsfrom/linesparams)
// src/agents/memory-search.ts:80-81
const DEFAULT_MAX_RESULTS = 6;
const DEFAULT_MIN_SCORE = 0.35;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.",
];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>`,
};| 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
}
}
}
}