From 068e9085b16a89ea11f7f3e1554439bfe1d4e560 Mon Sep 17 00:00:00 2001 From: Mike Nott Date: Wed, 28 Jan 2026 15:07:24 +0000 Subject: [PATCH] fix(memory-lancedb): improve autoCapture with turn-by-turn processing Two fixes for autoCapture reliability: 1. Strip injected memory context before capture filtering - autoRecall prepends to user messages - This was causing shouldCapture() to skip all user messages - Now strips the context before evaluating capture criteria 2. Process only current turn instead of full history - Previously scanned all messages and picked arbitrary 3 - In long sessions (100+ messages), recent content was missed - Now captures only last user + last assistant message - Previous turns were already captured when they occurred These fixes ensure that autoCapture reliably stores each conversation turn as it happens, rather than missing recent exchanges. --- extensions/memory-lancedb/index.ts | 85 ++++++++++++++++++------------ 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/extensions/memory-lancedb/index.ts b/extensions/memory-lancedb/index.ts index e7daab6f5..59e198d18 100644 --- a/extensions/memory-lancedb/index.ts +++ b/extensions/memory-lancedb/index.ts @@ -185,18 +185,28 @@ const MEMORY_TRIGGERS = [ /always|never|important/i, ]; +// Strip injected memory context from message text before processing +function stripMemoryContext(text: string): string { + const memoryBlockEnd = text.indexOf(""); + if (memoryBlockEnd !== -1) { + return text.slice(memoryBlockEnd + "".length).trim(); + } + return text; +} + function shouldCapture(text: string): boolean { - if (text.length < 10 || text.length > 500) return false; - // Skip injected context from memory recall - if (text.includes("")) return false; - // Skip system-generated content - if (text.startsWith("<") && text.includes(" 500) return false; + // Skip system-generated content (pure XML) + if (cleanText.startsWith("<") && cleanText.includes(" 3) return false; - return MEMORY_TRIGGERS.some((r) => r.test(text)); + return MEMORY_TRIGGERS.some((r) => r.test(cleanText)); } function detectCategory(text: string): MemoryCategory { @@ -491,7 +501,8 @@ const memoryPlugin = { }); } - // Auto-capture: analyze and store important information after agent ends + // Auto-capture: store important information from the current turn + // Only processes the last user message and last assistant message (not full history) if (cfg.autoCapture) { api.on("agent_end", async (event) => { if (!event.success || !event.messages || event.messages.length === 0) { @@ -499,26 +510,9 @@ const memoryPlugin = { } try { - // Extract text content from messages (handling unknown[] type) - const texts: string[] = []; - for (const msg of event.messages) { - // Type guard for message object - if (!msg || typeof msg !== "object") continue; - const msgObj = msg as Record; - - // Only process user and assistant messages - const role = msgObj.role; - if (role !== "user" && role !== "assistant") continue; - - const content = msgObj.content; - - // Handle string content directly - if (typeof content === "string") { - texts.push(content); - continue; - } - - // Handle array content (content blocks) + // Helper to extract text from message content + const extractText = (content: unknown): string | null => { + if (typeof content === "string") return content; if (Array.isArray(content)) { for (const block of content) { if ( @@ -529,21 +523,46 @@ const memoryPlugin = { "text" in block && typeof (block as Record).text === "string" ) { - texts.push((block as Record).text as string); + return (block as Record).text as string; } } } + return null; + }; + + // Find the LAST user message and LAST assistant message (current turn only) + // Previous turns were already captured when they happened + let lastUserText: string | null = null; + let lastAssistantText: string | null = null; + + for (const msg of event.messages) { + if (!msg || typeof msg !== "object") continue; + const msgObj = msg as Record; + const role = msgObj.role; + const text = extractText(msgObj.content); + + if (role === "user" && text) lastUserText = text; + if (role === "assistant" && text) lastAssistantText = text; } + // Collect texts from this turn only + const turnTexts: string[] = []; + if (lastUserText) turnTexts.push(lastUserText); + if (lastAssistantText) turnTexts.push(lastAssistantText); + // Filter for capturable content - const toCapture = texts.filter( + const toCapture = turnTexts.filter( (text) => text && shouldCapture(text), ); if (toCapture.length === 0) return; - // Store each capturable piece (limit to 3 per conversation) + // Store each capturable piece from this turn let stored = 0; - for (const text of toCapture.slice(0, 3)) { + for (const rawText of toCapture) { + // Clean the text before storing (strip injected memory context) + const text = stripMemoryContext(rawText); + if (text.length < 10) continue; // Re-check length after cleaning + const category = detectCategory(text); const vector = await embeddings.embed(text);