import { createMemoryV2PluginBackends } from './memory-v2-plugin-backends.mjs'; const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']); const FALSE_VALUES = new Set(['0', 'false', 'no', 'off']); const MAX_MEMORY_TEXT_LENGTH = 2000; const MAX_CONTEXT_ITEMS = 50; function normalizeText(value, maxLength = MAX_MEMORY_TEXT_LENGTH) { const text = String(value ?? '').trim(); if (!text) return null; return text.length > maxLength ? text.slice(0, maxLength) : text; } function readFlag(env, key, fallback) { const raw = env?.[key]; if (raw == null || raw === '') return fallback; const normalized = String(raw).trim().toLowerCase(); if (TRUE_VALUES.has(normalized)) return true; if (FALSE_VALUES.has(normalized)) return false; return fallback; } function emptyResolveResult({ enabled, skipped = false, reason = null, degraded = false, source = null, } = {}) { return { ok: !degraded, enabled: Boolean(enabled), skipped, reason, degraded, source, profile: null, semanticMemories: [], behaviorSummary: null, activeGoals: [], contextGoals: [], memories: [], }; } function skippedWriteResult(reason) { return { ok: true, enabled: false, skipped: true, reason, source: null, saved: 0, analyzed: 0, memories: 0, }; } export function resolveMemoryV2Policy({ env = process.env, overrides = {} } = {}) { const legacyDefault = readFlag(env, 'USER_CONVERSATION_MEMORY_ENABLED', true); const enabled = readFlag(env, 'MEMORY_ENABLED', legacyDefault); return { enabled, profileEnabled: readFlag(env, 'MEMORY_PROFILE_ENABLED', enabled), eventLogEnabled: readFlag(env, 'MEMORY_EVENT_LOG_ENABLED', enabled), vectorEnabled: readFlag(env, 'MEMORY_VECTOR_ENABLED', false), backend: String(env?.MEMORY_BACKEND ?? 'legacy').trim() || 'legacy', failOpen: readFlag(env, 'MEMORY_FAIL_OPEN', true), ...overrides, }; } export function createLegacyMemoryBackend(conversationMemoryService) { return { name: 'legacy-conversation-memory', isAvailable() { return Boolean(conversationMemoryService); }, async resolve({ userId, limit = 40 } = {}) { if (!conversationMemoryService?.listMemories || !userId) { return { memories: [] }; } const memories = await conversationMemoryService.listMemories(userId, { limit }); return { memories }; }, async write({ userId, sessionId, messages = [] } = {}) { if (!conversationMemoryService?.saveAndAnalyze || !userId || !sessionId) { return { saved: 0, analyzed: 0, memories: 0 }; } return conversationMemoryService.saveAndAnalyze(sessionId, userId, messages); }, async compact({ userId } = {}) { if (!conversationMemoryService?.analyzeUser || !userId) { return { analyzed: 0, memories: 0 }; } return conversationMemoryService.analyzeUser(userId); }, }; } function normalizeMemoryItem(item) { if (typeof item === 'string') { const text = normalizeText(item); return text ? { label: 'memory', text } : null; } if (!item || typeof item !== 'object') return null; const text = normalizeText( item.text ?? item.content ?? item.memoryText ?? item.memory_text ?? '', ); if (!text) return null; const normalized = { label: normalizeText(item.label ?? item.type ?? 'memory', 80) ?? 'memory', text, }; if (item.id != null) normalized.id = String(item.id); const hasCreatedAt = Object.prototype.hasOwnProperty.call(item, 'createdAt') || Object.prototype.hasOwnProperty.call(item, 'created_at'); if (hasCreatedAt) normalized.createdAt = item.createdAt ?? item.created_at ?? null; const score = item.score ?? item.confidence; if (score != null && Number.isFinite(Number(score))) normalized.score = Number(score); return normalized; } function normalizeStringList(values) { if (!Array.isArray(values)) return []; return values .map((value) => ( typeof value === 'string' ? normalizeText(value) : normalizeText(value?.text ?? value?.content ?? value?.summary ?? '') )) .filter(Boolean) .slice(0, MAX_CONTEXT_ITEMS); } function normalizeMemoryList(values) { if (!Array.isArray(values)) return []; return values .map((item) => normalizeMemoryItem(item)) .filter(Boolean) .slice(0, MAX_CONTEXT_ITEMS); } function normalizeResolvePayload(payload, source, policy) { const memories = normalizeMemoryList(payload?.memories); const semanticMemories = Array.isArray(payload?.semanticMemories) ? normalizeStringList(payload.semanticMemories) : memories; const contextGoals = normalizeStringList( Array.isArray(payload?.contextGoals) ? payload.contextGoals : (Array.isArray(payload?.userLongTermGoals) ? payload.userLongTermGoals : payload?.activeGoals), ); return { ok: true, enabled: true, skipped: false, reason: null, degraded: false, source, profile: policy.profileEnabled ? (payload?.profile ?? null) : null, semanticMemories, behaviorSummary: normalizeText(payload?.behaviorSummary, 4000), activeGoals: contextGoals, contextGoals, memories, }; } function supportsOperation(backend, operation) { if (!operation) return true; return typeof backend?.[operation] === 'function'; } function isBackendAvailable(backend) { try { return backend?.isAvailable?.() !== false; } catch { return false; } } function selectBackend(backends, policy, operation = null) { if (!Array.isArray(backends) || backends.length === 0) return null; const preferred = policy.backend; if (preferred && preferred !== 'legacy') { const exact = backends.find((backend) => backend?.name === preferred); if (exact && isBackendAvailable(exact) && supportsOperation(exact, operation)) { return exact; } } return backends.find((backend) => ( isBackendAvailable(backend) && supportsOperation(backend, operation) )) ?? null; } function backendStatus(backend) { const name = String(backend?.name ?? 'unknown'); let available = true; try { available = backend?.isAvailable?.() !== false; } catch { available = false; } const status = { name, available, supports: { resolve: typeof backend?.resolve === 'function', write: typeof backend?.write === 'function', compact: typeof backend?.compact === 'function', }, }; if (backend?.category) status.category = String(backend.category); if (backend?.role) status.role = String(backend.role); if (backend?.flag) status.flag = String(backend.flag); const reason = typeof backend?.getUnavailableReason === 'function' ? backend.getUnavailableReason() : backend?.unavailableReason; if (!available && reason) status.reason = String(reason); return status; } export function createMemoryV2({ legacyMemoryService = null, backends = null, policy = null, env = process.env, logger = console, } = {}) { const resolvedPolicy = policy ?? resolveMemoryV2Policy({ env }); const resolvedBackends = backends ?? [ createLegacyMemoryBackend(legacyMemoryService), ...createMemoryV2PluginBackends(), ]; async function failOpen(operation, err, fallback) { logger?.warn?.( `[memory-v2] ${operation} degraded: ${err instanceof Error ? err.message : err}`, ); if (resolvedPolicy.failOpen) return fallback; throw err; } async function resolve(input = {}) { if (!resolvedPolicy.enabled) { return emptyResolveResult({ enabled: false, skipped: true, reason: 'disabled' }); } const backend = selectBackend(resolvedBackends, resolvedPolicy, 'resolve'); if (!backend?.resolve) { return emptyResolveResult({ enabled: true, skipped: true, reason: 'no_backend' }); } try { const payload = await backend.resolve(input); return normalizeResolvePayload(payload, backend.name ?? 'unknown', resolvedPolicy); } catch (err) { return failOpen( 'resolve', err, emptyResolveResult({ enabled: true, degraded: true, reason: 'backend_failure', source: backend.name ?? 'unknown', }), ); } } async function write(input = {}) { if (!resolvedPolicy.enabled) return skippedWriteResult('disabled'); if (!resolvedPolicy.eventLogEnabled) return skippedWriteResult('event_log_disabled'); const backend = selectBackend(resolvedBackends, resolvedPolicy, 'write'); if (!backend?.write) return skippedWriteResult('no_backend'); try { const result = await backend.write(input); return { ok: true, enabled: true, skipped: false, reason: null, source: backend.name ?? 'unknown', saved: Number(result?.saved ?? 0), analyzed: Number(result?.analyzed ?? 0), memories: Number(result?.memories ?? 0), }; } catch (err) { return failOpen('write', err, { ok: false, enabled: true, skipped: true, reason: 'backend_failure', source: backend.name ?? 'unknown', saved: 0, analyzed: 0, memories: 0, }); } } async function compact(input = {}) { if (!resolvedPolicy.enabled) { return { ok: true, enabled: false, skipped: true, reason: 'disabled' }; } const backend = selectBackend(resolvedBackends, resolvedPolicy, 'compact'); if (!backend?.compact) { return { ok: true, enabled: true, skipped: true, reason: 'no_backend' }; } try { const result = await backend.compact(input); return { ok: true, enabled: true, skipped: false, reason: null, source: backend.name ?? 'unknown', analyzed: Number(result?.analyzed ?? 0), memories: Number(result?.memories ?? 0), }; } catch (err) { return failOpen('compact', err, { ok: false, enabled: true, skipped: true, reason: 'backend_failure', source: backend.name ?? 'unknown', analyzed: 0, memories: 0, }); } } function getStatus() { const backends = resolvedBackends.map((backend) => backendStatus(backend)); const selected = selectBackend(resolvedBackends, resolvedPolicy, 'resolve'); return { enabled: Boolean(resolvedPolicy.enabled), backend: resolvedPolicy.backend, selectedBackend: selected?.name ?? null, profileEnabled: Boolean(resolvedPolicy.profileEnabled), eventLogEnabled: Boolean(resolvedPolicy.eventLogEnabled), vectorEnabled: Boolean(resolvedPolicy.vectorEnabled), failOpen: Boolean(resolvedPolicy.failOpen), backends, }; } return { policy: resolvedPolicy, getStatus, resolve, write, compact, }; }