feat(memory-lancedb): make auto-capture max length configurable

This commit is contained in:
fan
2026-02-15 07:38:29 +08:00
committed by Vignesh
parent 82c1d9d3ef
commit 3e00460cdc
4 changed files with 57 additions and 4 deletions
+23 -1
View File
@@ -11,12 +11,14 @@ export type MemoryConfig = {
dbPath?: string; dbPath?: string;
autoCapture?: boolean; autoCapture?: boolean;
autoRecall?: boolean; autoRecall?: boolean;
captureMaxChars?: number;
}; };
export const MEMORY_CATEGORIES = ["preference", "fact", "decision", "entity", "other"] as const; export const MEMORY_CATEGORIES = ["preference", "fact", "decision", "entity", "other"] as const;
export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number]; export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number];
const DEFAULT_MODEL = "text-embedding-3-small"; const DEFAULT_MODEL = "text-embedding-3-small";
const DEFAULT_CAPTURE_MAX_CHARS = 1500;
const LEGACY_STATE_DIRS: string[] = []; const LEGACY_STATE_DIRS: string[] = [];
function resolveDefaultDbPath(): string { function resolveDefaultDbPath(): string {
@@ -89,7 +91,11 @@ export const memoryConfigSchema = {
throw new Error("memory config required"); throw new Error("memory config required");
} }
const cfg = value as Record<string, unknown>; const cfg = value as Record<string, unknown>;
assertAllowedKeys(cfg, ["embedding", "dbPath", "autoCapture", "autoRecall"], "memory config"); assertAllowedKeys(
cfg,
["embedding", "dbPath", "autoCapture", "autoRecall", "captureMaxChars"],
"memory config",
);
const embedding = cfg.embedding as Record<string, unknown> | undefined; const embedding = cfg.embedding as Record<string, unknown> | undefined;
if (!embedding || typeof embedding.apiKey !== "string") { if (!embedding || typeof embedding.apiKey !== "string") {
@@ -99,6 +105,15 @@ export const memoryConfigSchema = {
const model = resolveEmbeddingModel(embedding); 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 { return {
embedding: { embedding: {
provider: "openai", provider: "openai",
@@ -108,6 +123,7 @@ export const memoryConfigSchema = {
dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH, dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH,
autoCapture: cfg.autoCapture !== false, autoCapture: cfg.autoCapture !== false,
autoRecall: cfg.autoRecall !== false, autoRecall: cfg.autoRecall !== false,
captureMaxChars: captureMaxChars ?? DEFAULT_CAPTURE_MAX_CHARS,
}; };
}, },
uiHints: { uiHints: {
@@ -135,5 +151,11 @@ export const memoryConfigSchema = {
label: "Auto-Recall", label: "Auto-Recall",
help: "Automatically inject relevant memories into context", 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),
},
}, },
}; };
+17
View File
@@ -61,6 +61,7 @@ describe("memory plugin e2e", () => {
expect(config).toBeDefined(); expect(config).toBeDefined();
expect(config?.embedding?.apiKey).toBe(OPENAI_API_KEY); expect(config?.embedding?.apiKey).toBe(OPENAI_API_KEY);
expect(config?.dbPath).toBe(dbPath); expect(config?.dbPath).toBe(dbPath);
expect(config?.captureMaxChars).toBe(1500);
}); });
test("config schema resolves env vars", async () => { test("config schema resolves env vars", async () => {
@@ -92,6 +93,18 @@ describe("memory plugin e2e", () => {
}).toThrow("embedding.apiKey is required"); }).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 () => { test("shouldCapture applies real capture rules", async () => {
const { shouldCapture } = await import("./index.js"); const { shouldCapture } = await import("./index.js");
@@ -104,6 +117,10 @@ describe("memory plugin e2e", () => {
expect(shouldCapture("<relevant-memories>injected</relevant-memories>")).toBe(false); expect(shouldCapture("<relevant-memories>injected</relevant-memories>")).toBe(false);
expect(shouldCapture("<system>status</system>")).toBe(false); expect(shouldCapture("<system>status</system>")).toBe(false);
expect(shouldCapture("Here is a short **summary**\n- bullet")).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 () => { test("detectCategory classifies using production logic", async () => {
+6 -3
View File
@@ -194,8 +194,9 @@ const MEMORY_TRIGGERS = [
/always|never|important/i, /always|never|important/i,
]; ];
export function shouldCapture(text: string): boolean { export function shouldCapture(text: string, options?: { maxChars?: number }): boolean {
if (text.length < 10 || text.length > 500) { const maxChars = options?.maxChars ?? 1500;
if (text.length < 10 || text.length > maxChars) {
return false; return false;
} }
// Skip injected context from memory recall // Skip injected context from memory recall
@@ -570,7 +571,9 @@ const memoryPlugin = {
} }
// Filter for capturable content // 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) { if (toCapture.length === 0) {
return; return;
} }
@@ -25,6 +25,12 @@
"autoRecall": { "autoRecall": {
"label": "Auto-Recall", "label": "Auto-Recall",
"help": "Automatically inject relevant memories into context" "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": { "configSchema": {
@@ -53,6 +59,11 @@
}, },
"autoRecall": { "autoRecall": {
"type": "boolean" "type": "boolean"
},
"captureMaxChars": {
"type": "number",
"minimum": 100,
"maximum": 10000
} }
}, },
"required": ["embedding"] "required": ["embedding"]