import crypto from 'crypto'; import path from 'path'; import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js'; import { isPostgresStorageMode } from './dataPaths.js'; import { decryptJsonPayload, encryptJsonPayload, makeOwnerLookup, pgQuery, } from './postgres.js'; const DATA_ROOT = '/data/memories'; const INDEX_FILE = path.join(DATA_ROOT, 'index.json'); const MAX_MEMORY_LENGTH = 220; const state = { loaded: false, index: { memories: {}, }, }; function ensureOwner(owner) { if (!owner?.type || !owner?.id) throw new Error('Invalid memory owner'); return owner; } function nowIso() { return new Date().toISOString(); } function sanitizeText(text) { return String(text || '').replace(/\s+/g, ' ').trim().slice(0, MAX_MEMORY_LENGTH); } function memoryAad(memoryId) { return `memory:${memoryId}`; } async function ensureLoaded() { if (state.loaded || isPostgresStorageMode()) return; const stored = await loadEncryptedJson(INDEX_FILE); state.index = { memories: stored?.memories || {}, }; state.loaded = true; } async function saveIndex() { await saveEncryptedJson(INDEX_FILE, state.index); } function matchesOwner(memory, owner) { return memory.ownerType === owner.type && memory.ownerId === owner.id; } function sanitize(memory) { return { id: memory.id, content: memory.content, source: memory.source || 'assistant', sessionId: memory.sessionId || null, createdAt: memory.createdAt, updatedAt: memory.updatedAt, }; } async function listSql(owner) { const { rows } = await pgQuery( 'SELECT id, payload, updated_at FROM memories WHERE owner_lookup = $1 ORDER BY updated_at DESC', [makeOwnerLookup(owner)] ); return rows .map((row) => decryptJsonPayload(row.payload, memoryAad(row.id))) .filter(Boolean) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) .map(sanitize); } async function upsertSql(memory) { await pgQuery( `INSERT INTO memories (id, owner_lookup, created_at, updated_at, payload) VALUES ($1, $2, $3, $4, $5::jsonb) ON CONFLICT (id) DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`, [ memory.id, makeOwnerLookup({ type: memory.ownerType, id: memory.ownerId }), memory.createdAt, memory.updatedAt, JSON.stringify(encryptJsonPayload(memory, memoryAad(memory.id))), ] ); } export const memoryStore = { async list(owner) { ensureOwner(owner); if (isPostgresStorageMode()) return listSql(owner); await ensureLoaded(); return Object.values(state.index.memories) .filter((memory) => matchesOwner(memory, owner)) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) .map(sanitize); }, async create(owner, { content, sessionId = null, source = 'assistant' }) { ensureOwner(owner); const normalized = sanitizeText(content); if (!normalized) return null; const memory = { id: crypto.randomUUID(), ownerType: owner.type, ownerId: owner.id, content: normalized, sessionId, source, createdAt: nowIso(), updatedAt: nowIso(), }; if (isPostgresStorageMode()) { await upsertSql(memory); return sanitize(memory); } await ensureLoaded(); state.index.memories[memory.id] = memory; await saveIndex(); return sanitize(memory); }, async update(owner, id, content) { ensureOwner(owner); const normalized = sanitizeText(content); if (!normalized) return null; if (isPostgresStorageMode()) { const { rows } = await pgQuery( 'SELECT payload FROM memories WHERE id = $1 AND owner_lookup = $2', [id, makeOwnerLookup(owner)] ); const memory = rows[0] ? decryptJsonPayload(rows[0].payload, memoryAad(id)) : null; if (!memory || !matchesOwner(memory, owner)) return null; memory.content = normalized; memory.updatedAt = nowIso(); await upsertSql(memory); return sanitize(memory); } await ensureLoaded(); const memory = state.index.memories[id]; if (!memory || !matchesOwner(memory, owner)) return null; memory.content = normalized; memory.updatedAt = nowIso(); await saveIndex(); return sanitize(memory); }, async delete(owner, id) { ensureOwner(owner); if (isPostgresStorageMode()) { const result = await pgQuery( 'DELETE FROM memories WHERE id = $1 AND owner_lookup = $2', [id, makeOwnerLookup(owner)] ); return result.rowCount > 0; } await ensureLoaded(); const memory = state.index.memories[id]; if (!memory || !matchesOwner(memory, owner)) return false; delete state.index.memories[id]; await saveIndex(); return true; }, };