import fs from "node:fs"; import path from "node:path"; import { resolvePaperclipInstanceRoot } from "../home-paths.js"; import { sanitizeMemoryFact } from "./memory-sanitizer.js"; const MAX_FACTS = 50; interface AssistantMemory { facts: string[]; updatedAt: string | null; } function resolveMemoryPath(companyId: string): string { return path.resolve( resolvePaperclipInstanceRoot(), "data", "assistant-memory", `${companyId}.json`, ); } function readMemory(companyId: string): AssistantMemory { const filePath = resolveMemoryPath(companyId); try { const raw = fs.readFileSync(filePath, "utf-8"); const parsed = JSON.parse(raw) as unknown; if ( typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && Array.isArray((parsed as Record).facts) && ((parsed as Record).facts as unknown[]).every((f) => typeof f === "string") ) { const p = parsed as Record; const updatedAt = typeof p.updatedAt === "string" ? p.updatedAt : null; return { facts: p.facts as string[], updatedAt }; } } catch { // File does not exist or is corrupt — return default } return { facts: [], updatedAt: null }; } function writeMemory(companyId: string, memory: AssistantMemory): void { const filePath = resolveMemoryPath(companyId); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(memory, null, 2), "utf-8"); } export function assistantMemoryService() { async function get(companyId: string): Promise { return readMemory(companyId); } async function append(companyId: string, rawFact: string): Promise { const sanitized = sanitizeMemoryFact(rawFact); const current = readMemory(companyId); const facts = [...current.facts, sanitized]; // FIFO eviction: cap at MAX_FACTS while (facts.length > MAX_FACTS) { facts.shift(); } const updated: AssistantMemory = { facts, updatedAt: new Date().toISOString(), }; writeMemory(companyId, updated); return updated; } async function clear(companyId: string): Promise { writeMemory(companyId, { facts: [], updatedAt: null }); } return { get, append, clear }; }