Harden OpenPets memory capture and retrieval #1

Open
scriptoriumadmin wants to merge 1 commit from pr/memory-hardening into main
3 changed files with 37 additions and 13 deletions

View file

@ -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<OpenApiChat
throw new Error(`Prompt is too long. Keep it under ${maxPromptChars} characters.`);
}
capturePromptMemories(trimmedPrompt);
try {
const captured = capturePromptMemories(trimmedPrompt);
if (captured.length > 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()) {

View file

@ -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) {

View file

@ -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);