nexus/server/src/services/assistant-memory.ts
Nexus Dev 7bb72a5a2f feat(33-01,33-02): memory service + sanitizer, personal assistant page
33-01: memory-sanitizer, assistant-memory service, REST routes, 17 tests
33-02: useNexusMode hook, PersonalAssistantPage, sidebar nav, route wiring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:49 +00:00

79 lines
2.3 KiB
TypeScript

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<string, unknown>).facts) &&
((parsed as Record<string, unknown>).facts as unknown[]).every((f) => typeof f === "string")
) {
const p = parsed as Record<string, unknown>;
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<AssistantMemory> {
return readMemory(companyId);
}
async function append(companyId: string, rawFact: string): Promise<AssistantMemory> {
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<void> {
writeMemory(companyId, { facts: [], updatedAt: null });
}
return { get, append, clear };
}