chat-dev / server /memoryStore.js
incognitolm
Migration to PostgreSQL
bff1056
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;
},
};