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>
79 lines
2.3 KiB
TypeScript
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 };
|
|
}
|