/** * Factory for building a memory backend from environment variables. * * Called once at startup by host's singleton (`api/server/services/memoryStore.js`). * Returns `null` — never throws — when required env vars are missing, so * agents with `memory_enabled=true` still run, just without memory. Host * logs a single warning on boot instead of crashing. */ import { Pool } from 'pg'; import { DEFAULT_MEMORY_TABLE } from './constants'; import { PgvectorMemoryStore } from './pgvectorStore'; import { runMemoryMigration } from './migrate'; import { PgvectorRecallTracker, type RecallTracker } from './recallTracking'; import type { MemoryBackend } from './types'; export interface BuildMemoryBackendResult { backend: MemoryBackend; pool: Pool; /** Phase 2 — shared recall tracker bound to the same pool/migration. */ recallTracker: RecallTracker; } interface PgConfig { host: string; port: number; user: string; password: string; database: string; ssl: boolean; } function readPgConfig(): PgConfig | null { // Dev shortcut: single URL overrides discrete fields if present. const url = process.env.MEMORY_DATABASE_URL; if (url) { try { const u = new URL(url); return { host: u.hostname, port: Number(u.port || 5432), user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, ''), ssl: (process.env.MEMORY_POSTGRES_SSL ?? 'disable') === 'require', }; } catch { return null; } } const host = process.env.MEMORY_POSTGRES_HOST; const user = process.env.MEMORY_POSTGRES_USER; const password = process.env.MEMORY_POSTGRES_PASSWORD; const database = process.env.MEMORY_POSTGRES_DB; if (!host || !user || !password || !database) { return null; } return { host, port: Number(process.env.MEMORY_POSTGRES_PORT ?? 5432), user, password, database, ssl: (process.env.MEMORY_POSTGRES_SSL ?? 'disable') === 'require', }; } /** * Try to build a configured memory backend. Returns null when the required * env vars are absent — caller should log a single "memory disabled" warning * and continue. Never throws on missing config. */ export async function buildMemoryBackendFromEnv(): Promise { const cfg = readPgConfig(); if (!cfg) return null; const pool = new Pool({ host: cfg.host, port: cfg.port, user: cfg.user, password: cfg.password, database: cfg.database, ssl: cfg.ssl ? { rejectUnauthorized: false } : undefined, max: 10, }); const table = process.env.MEMORY_TABLE_NAME ?? DEFAULT_MEMORY_TABLE; try { await runMemoryMigration({ pool, table }); } catch (err) { // Migration failures ARE fatal — they mean dimension mismatch or // permissions wrong, and silently running past that would produce // corrupt or missing memory reads. await pool.end().catch(() => undefined); throw err; } const backend = new PgvectorMemoryStore({ pool, table }); // Phase 2 — recall tracker migration is best-effort at boot. If it fails, // tool wiring continues with a null tracker rather than crashing host. const recallTracker = new PgvectorRecallTracker(pool); try { await recallTracker.migrate(); } catch { // [phase2-recall-tracking] debug: migration skipped — non-fatal } return { backend, pool, recallTracker }; }