From 3e00460cdc93c171a2b256ac612fae33a5c97396 Mon Sep 17 00:00:00 2001 From: fan Date: Sun, 15 Feb 2026 07:38:29 +0800 Subject: [PATCH] feat(memory-lancedb): make auto-capture max length configurable --- extensions/memory-lancedb/config.ts | 24 ++++++++++++++++++- extensions/memory-lancedb/index.test.ts | 17 +++++++++++++ extensions/memory-lancedb/index.ts | 9 ++++--- .../memory-lancedb/openclaw.plugin.json | 11 +++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/extensions/memory-lancedb/config.ts b/extensions/memory-lancedb/config.ts index d3ab87d20d..4caa48cdce 100644 --- a/extensions/memory-lancedb/config.ts +++ b/extensions/memory-lancedb/config.ts @@ -11,12 +11,14 @@ export type MemoryConfig = { dbPath?: string; autoCapture?: boolean; autoRecall?: boolean; + captureMaxChars?: number; }; export const MEMORY_CATEGORIES = ["preference", "fact", "decision", "entity", "other"] as const; export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number]; const DEFAULT_MODEL = "text-embedding-3-small"; +const DEFAULT_CAPTURE_MAX_CHARS = 1500; const LEGACY_STATE_DIRS: string[] = []; function resolveDefaultDbPath(): string { @@ -89,7 +91,11 @@ export const memoryConfigSchema = { throw new Error("memory config required"); } const cfg = value as Record; - assertAllowedKeys(cfg, ["embedding", "dbPath", "autoCapture", "autoRecall"], "memory config"); + assertAllowedKeys( + cfg, + ["embedding", "dbPath", "autoCapture", "autoRecall", "captureMaxChars"], + "memory config", + ); const embedding = cfg.embedding as Record | undefined; if (!embedding || typeof embedding.apiKey !== "string") { @@ -99,6 +105,15 @@ export const memoryConfigSchema = { const model = resolveEmbeddingModel(embedding); + const captureMaxChars = + typeof cfg.captureMaxChars === "number" ? Math.floor(cfg.captureMaxChars) : undefined; + if ( + typeof captureMaxChars === "number" && + (captureMaxChars < 100 || captureMaxChars > 10_000) + ) { + throw new Error("captureMaxChars must be between 100 and 10000"); + } + return { embedding: { provider: "openai", @@ -108,6 +123,7 @@ export const memoryConfigSchema = { dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH, autoCapture: cfg.autoCapture !== false, autoRecall: cfg.autoRecall !== false, + captureMaxChars: captureMaxChars ?? DEFAULT_CAPTURE_MAX_CHARS, }; }, uiHints: { @@ -135,5 +151,11 @@ export const memoryConfigSchema = { label: "Auto-Recall", help: "Automatically inject relevant memories into context", }, + captureMaxChars: { + label: "Capture Max Chars", + help: "Maximum message length eligible for auto-capture", + advanced: true, + placeholder: String(DEFAULT_CAPTURE_MAX_CHARS), + }, }, }; diff --git a/extensions/memory-lancedb/index.test.ts b/extensions/memory-lancedb/index.test.ts index d51eb66ad7..b5261f848a 100644 --- a/extensions/memory-lancedb/index.test.ts +++ b/extensions/memory-lancedb/index.test.ts @@ -61,6 +61,7 @@ describe("memory plugin e2e", () => { expect(config).toBeDefined(); expect(config?.embedding?.apiKey).toBe(OPENAI_API_KEY); expect(config?.dbPath).toBe(dbPath); + expect(config?.captureMaxChars).toBe(1500); }); test("config schema resolves env vars", async () => { @@ -92,6 +93,18 @@ describe("memory plugin e2e", () => { }).toThrow("embedding.apiKey is required"); }); + test("config schema validates captureMaxChars range", async () => { + const { default: memoryPlugin } = await import("./index.js"); + + expect(() => { + memoryPlugin.configSchema?.parse?.({ + embedding: { apiKey: OPENAI_API_KEY }, + dbPath, + captureMaxChars: 99, + }); + }).toThrow("captureMaxChars must be between 100 and 10000"); + }); + test("shouldCapture applies real capture rules", async () => { const { shouldCapture } = await import("./index.js"); @@ -104,6 +117,10 @@ describe("memory plugin e2e", () => { expect(shouldCapture("injected")).toBe(false); expect(shouldCapture("status")).toBe(false); expect(shouldCapture("Here is a short **summary**\n- bullet")).toBe(false); + const longButAllowed = `I always prefer this style. ${"x".repeat(1200)}`; + const tooLong = `I always prefer this style. ${"x".repeat(1600)}`; + expect(shouldCapture(longButAllowed, { maxChars: 1500 })).toBe(true); + expect(shouldCapture(tooLong, { maxChars: 1500 })).toBe(false); }); test("detectCategory classifies using production logic", async () => { diff --git a/extensions/memory-lancedb/index.ts b/extensions/memory-lancedb/index.ts index 64f557ea95..aa53d834dd 100644 --- a/extensions/memory-lancedb/index.ts +++ b/extensions/memory-lancedb/index.ts @@ -194,8 +194,9 @@ const MEMORY_TRIGGERS = [ /always|never|important/i, ]; -export function shouldCapture(text: string): boolean { - if (text.length < 10 || text.length > 500) { +export function shouldCapture(text: string, options?: { maxChars?: number }): boolean { + const maxChars = options?.maxChars ?? 1500; + if (text.length < 10 || text.length > maxChars) { return false; } // Skip injected context from memory recall @@ -570,7 +571,9 @@ const memoryPlugin = { } // Filter for capturable content - const toCapture = texts.filter((text) => text && shouldCapture(text)); + const toCapture = texts.filter( + (text) => text && shouldCapture(text, { maxChars: cfg.captureMaxChars }), + ); if (toCapture.length === 0) { return; } diff --git a/extensions/memory-lancedb/openclaw.plugin.json b/extensions/memory-lancedb/openclaw.plugin.json index de25c49529..007ea3d63d 100644 --- a/extensions/memory-lancedb/openclaw.plugin.json +++ b/extensions/memory-lancedb/openclaw.plugin.json @@ -25,6 +25,12 @@ "autoRecall": { "label": "Auto-Recall", "help": "Automatically inject relevant memories into context" + }, + "captureMaxChars": { + "label": "Capture Max Chars", + "help": "Maximum message length eligible for auto-capture", + "advanced": true, + "placeholder": "1500" } }, "configSchema": { @@ -53,6 +59,11 @@ }, "autoRecall": { "type": "boolean" + }, + "captureMaxChars": { + "type": "number", + "minimum": 100, + "maximum": 10000 } }, "required": ["embedding"]