From 2de484d8f6ba7bf8ffdb33668debf6eb8182034c Mon Sep 17 00:00:00 2001 From: VectorShell Date: Sat, 13 Jun 2026 02:16:02 +0000 Subject: [PATCH] fix(memory): harden memory capture and retrieval - Catch and log memory capture failures in openapi-chat. - Add recall-intent fallback for self-referential questions. - Load memories when Settings view mounts. --- apps/desktop/src/openapi-chat.ts | 11 ++++++-- apps/desktop/src/openpets-memory.ts | 37 ++++++++++++++++++-------- apps/desktop/src/renderer/src/main.tsx | 2 ++ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/openapi-chat.ts b/apps/desktop/src/openapi-chat.ts index 25673c1b..e49ea70c 100644 --- a/apps/desktop/src/openapi-chat.ts +++ b/apps/desktop/src/openapi-chat.ts @@ -4,7 +4,7 @@ import { dirname, join } from "node:path"; import { defaultOpenApiChatEndpoint, getAppStateSnapshot, normalizeOpenApiChatEndpoint } from "./app-state.js"; import { applyExternalPetReaction, applyInternalPetMessage, showDefaultPet } from "./default-pet-controller.js"; -import { info, warn } from "./logger.js"; +import { error, info, warn } from "./logger.js"; import type { OpenPetsReaction } from "./local-ipc-protocol.js"; import { buildRelevantMemoryContext, capturePromptMemories } from "./openpets-memory.js"; import { getMcpChatClientManager, type McpChatToolDefinition } from "./mcp-chat-client.js"; @@ -274,7 +274,14 @@ export async function sendOpenApiChatPrompt(prompt: string): Promise 0) { + info("app", "memory captured", { component: "openapi-chat", count: captured.length, kinds: captured.map((e) => e.kind).join(",") }); + } + } catch (err) { + error("app", "memory capture failed", { component: "openapi-chat", error: err instanceof Error ? err.message : String(err) }); + } appendTranscriptEntry("user", trimmedPrompt); if (isVanillaChatMcpEnabled()) { diff --git a/apps/desktop/src/openpets-memory.ts b/apps/desktop/src/openpets-memory.ts index d4b161f5..5da7738e 100644 --- a/apps/desktop/src/openpets-memory.ts +++ b/apps/desktop/src/openpets-memory.ts @@ -234,39 +234,54 @@ export interface ChatHistorySearchEntry { readonly createdAt: number; } +const recallIntentPattern = /(^|[^a-z0-9])(remember|recall|what do you know|who am i|what is my name|what's my name|tell me about me|about myself|myself|do you remember)($|[^a-z0-9])/i; + export function buildRelevantMemoryContext(prompt: string, chatHistory?: readonly ChatHistorySearchEntry[]): string | undefined { const memoryHits = searchOpenPetsMemories(prompt, maxRelevantMemoryItems); const chatHits = chatHistory && chatHistory.length > 0 ? searchChatHistory(prompt, chatHistory, maxRelevantMemoryItems) : []; - if (memoryHits.length === 0 && chatHits.length === 0) { + const hasRecallIntent = recallIntentPattern.test(normalizeMemoryComparisonValue(prompt)); + const fallbackMemories = memoryHits.length === 0 || hasRecallIntent + ? listOpenPetsMemories(3).filter((entry) => !memoryHits.some((hit) => hit.entry.id === entry.id)) + : []; + + if (memoryHits.length === 0 && chatHits.length === 0 && fallbackMemories.length === 0) { return undefined; } const lines: string[] = []; let usedChars = 0; - // Include memory hits first - for (const hit of memoryHits) { - const tagSuffix = hit.entry.tags.length > 0 ? ` tags=${hit.entry.tags.join(",")}` : ""; - const line = `- [${hit.entry.kind} importance=${hit.entry.importance}${tagSuffix}] ${hit.entry.text}`; + const addLine = (line: string): boolean => { if (usedChars > 0 && usedChars + line.length + 1 > maxRelevantMemoryChars) { - break; + return false; } lines.push(line); usedChars += line.length + 1; + return true; + }; + + // Include scored memory hits first + for (const hit of memoryHits) { + const tagSuffix = hit.entry.tags.length > 0 ? ` tags=${hit.entry.tags.join(",")}` : ""; + const line = `- [${hit.entry.kind} importance=${hit.entry.importance}${tagSuffix}] ${hit.entry.text}`; + if (!addLine(line)) break; + } + + // Add recent memories as a fallback when the user is asking about themselves or no scored hits exist + for (const entry of fallbackMemories) { + const tagSuffix = entry.tags.length > 0 ? ` tags=${entry.tags.join(",")}` : ""; + const line = `- [${entry.kind} importance=${entry.importance}${tagSuffix}] ${entry.text}`; + if (!addLine(line)) break; } // Include relevant chat history excerpts for (const hit of chatHits) { const prefix = hit.entry.role === "user" ? "User said" : "Assistant said"; const line = `- [chat history] ${prefix}: ${hit.entry.text}`; - if (usedChars > 0 && usedChars + line.length + 1 > maxRelevantMemoryChars) { - break; - } - lines.push(line); - usedChars += line.length + 1; + if (!addLine(line)) break; } if (lines.length === 0) { diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx index ada7b017..c3567476 100644 --- a/apps/desktop/src/renderer/src/main.tsx +++ b/apps/desktop/src/renderer/src/main.tsx @@ -1226,6 +1226,8 @@ function SettingsView({ onThemeModeChange }: { onThemeModeChange: (mode: ThemeMo useEffect(() => { void loadSettings().catch((err) => setError(String(err?.message ?? err))); }, []); + useEffect(() => { void loadMemories().catch((err) => setError(String(err?.message ?? err))); }, []); + useEffect(() => { if (!message) return; const timeout = window.setTimeout(() => setMessage(""), 2200); -- 2.45.3