Merge Memory V2 runtime facade
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { createLangGraphHttpClient, createLangGraphMemoryBackend } from './memory-v2-langgraph.mjs';
|
||||
import { createLegacyMemoryBackend, createMemoryV2, resolveMemoryV2Policy } from './memory-v2.mjs';
|
||||
import { createLettaHttpClient, createLettaMemoryBackend } from './memory-v2-letta.mjs';
|
||||
import { createMemoryV2PluginBackends } from './memory-v2-plugin-backends.mjs';
|
||||
import { createMem0HttpClient, createMem0MemoryBackend } from './memory-v2-mem0.mjs';
|
||||
import { createNeo4jHttpClient, createNeo4jMemoryBackend } from './memory-v2-neo4j.mjs';
|
||||
import { createPgvectorMemoryBackend } from './memory-v2-pgvector.mjs';
|
||||
import { createQdrantHttpClient, createQdrantMemoryBackend } from './memory-v2-qdrant.mjs';
|
||||
import { createRedisStreamsClient, createRedisStreamsMemoryBackend } from './memory-v2-redis-streams.mjs';
|
||||
import { createWeaviateHttpClient, createWeaviateMemoryBackend } from './memory-v2-weaviate.mjs';
|
||||
|
||||
const DEFAULT_PGVECTOR_URL_ENV = 'MEMORY_PGVECTOR_DATABASE_URL';
|
||||
|
||||
function readFlag(env, key, fallback = false) {
|
||||
const raw = env?.[key];
|
||||
if (raw == null || raw === '') return fallback;
|
||||
const normalized = String(raw).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function resolveModuleSpecifier(specifier) {
|
||||
const value = String(specifier ?? '').trim();
|
||||
if (!value) return null;
|
||||
if (value.startsWith('.') || value.startsWith('/')) {
|
||||
return pathToFileURL(path.resolve(value)).href;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function loadEmbeddingModule(moduleSpecifier, importModule) {
|
||||
const resolved = resolveModuleSpecifier(moduleSpecifier);
|
||||
if (!resolved) return null;
|
||||
const imported = await importModule(resolved);
|
||||
const fn = imported?.embedQuery ?? imported?.default;
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(
|
||||
`Memory V2 pgvector embedding module must export embedQuery or default: ${moduleSpecifier}`,
|
||||
);
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
async function createPgPool(connectionString, { importPg, max }) {
|
||||
const imported = await importPg('pg');
|
||||
const PgPool = imported?.Pool ?? imported?.default?.Pool;
|
||||
if (typeof PgPool !== 'function') {
|
||||
throw new Error('pg module does not export Pool');
|
||||
}
|
||||
return new PgPool({ connectionString, max });
|
||||
}
|
||||
|
||||
export async function createMemoryV2Runtime({
|
||||
legacyMemoryService = null,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
importPg = (specifier) => import(specifier),
|
||||
importRedis = (specifier) => import(specifier),
|
||||
importModule = (specifier) => import(specifier),
|
||||
fetchImpl = globalThis.fetch,
|
||||
lettaClient = null,
|
||||
weaviateClient = null,
|
||||
neo4jClient = null,
|
||||
redisStreamsClient = null,
|
||||
langGraphClient = null,
|
||||
} = {}) {
|
||||
const vectorEnabled = readFlag(env, 'MEMORY_VECTOR_ENABLED', false);
|
||||
const pgvectorEnabled = readFlag(env, 'MEMORY_PGVECTOR_ENABLED', vectorEnabled);
|
||||
const connectionString = String(env?.[DEFAULT_PGVECTOR_URL_ENV] ?? '').trim();
|
||||
const embeddingModule = String(env?.MEMORY_PGVECTOR_EMBEDDING_MODULE ?? '').trim();
|
||||
const backends = [createLegacyMemoryBackend(legacyMemoryService)];
|
||||
const closers = [];
|
||||
|
||||
let pgvectorBackend = null;
|
||||
if (pgvectorEnabled && connectionString) {
|
||||
try {
|
||||
const embedQuery = await loadEmbeddingModule(embeddingModule, importModule);
|
||||
if (!embedQuery) {
|
||||
pgvectorBackend = createPgvectorMemoryBackend({
|
||||
enabled: false,
|
||||
tableName: env?.MEMORY_PGVECTOR_TABLE,
|
||||
unavailableReason: 'embedding_module_not_configured',
|
||||
});
|
||||
} else {
|
||||
const pool = await createPgPool(connectionString, {
|
||||
importPg,
|
||||
max: Math.max(1, Number(env?.MEMORY_PGVECTOR_POOL_MAX ?? 5) || 5),
|
||||
});
|
||||
closers.push(() => pool.end?.());
|
||||
pgvectorBackend = createPgvectorMemoryBackend({
|
||||
pool,
|
||||
enabled: pgvectorEnabled,
|
||||
tableName: env?.MEMORY_PGVECTOR_TABLE,
|
||||
embedQuery,
|
||||
defaultLimit: Number(env?.MEMORY_PGVECTOR_LIMIT ?? 8) || 8,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] pgvector backend unavailable: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
pgvectorBackend = createPgvectorMemoryBackend({
|
||||
enabled: false,
|
||||
unavailableReason: 'init_failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (pgvectorBackend) backends.push(pgvectorBackend);
|
||||
let qdrantBackend = null;
|
||||
const qdrantEnabled = readFlag(env, 'MEMORY_QDRANT_ENABLED', false);
|
||||
const qdrantUrl = String(env?.MEMORY_QDRANT_URL ?? '').trim();
|
||||
const qdrantCollection = String(env?.MEMORY_QDRANT_COLLECTION ?? '').trim();
|
||||
const qdrantEmbeddingModule = String(env?.MEMORY_QDRANT_EMBEDDING_MODULE ?? '').trim();
|
||||
if (qdrantEnabled || qdrantUrl || qdrantCollection) {
|
||||
try {
|
||||
const qdrantEmbedQuery = await loadEmbeddingModule(qdrantEmbeddingModule, importModule);
|
||||
const qdrantClient = qdrantEnabled && qdrantUrl && qdrantEmbedQuery
|
||||
? createQdrantHttpClient({
|
||||
url: qdrantUrl,
|
||||
apiKey: env?.MEMORY_QDRANT_API_KEY,
|
||||
fetchImpl,
|
||||
timeoutMs: Number(env?.MEMORY_QDRANT_TIMEOUT_MS ?? 3000) || 3000,
|
||||
})
|
||||
: null;
|
||||
qdrantBackend = createQdrantMemoryBackend({
|
||||
enabled: qdrantEnabled,
|
||||
url: qdrantUrl,
|
||||
collection: qdrantCollection || undefined,
|
||||
client: qdrantClient,
|
||||
embedQuery: qdrantEmbedQuery,
|
||||
defaultLimit: Number(env?.MEMORY_QDRANT_LIMIT ?? 8) || 8,
|
||||
});
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] qdrant backend unavailable: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
qdrantBackend = createQdrantMemoryBackend({
|
||||
enabled: false,
|
||||
unavailableReason: 'init_failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (qdrantBackend) backends.push(qdrantBackend);
|
||||
let weaviateBackend = null;
|
||||
const weaviateEnabled = readFlag(env, 'MEMORY_WEAVIATE_ENABLED', false);
|
||||
const weaviateUrl = String(env?.MEMORY_WEAVIATE_URL ?? '').trim();
|
||||
const weaviateCollection = String(env?.MEMORY_WEAVIATE_COLLECTION ?? '').trim();
|
||||
const weaviateEmbeddingModule = String(env?.MEMORY_WEAVIATE_EMBEDDING_MODULE ?? '').trim();
|
||||
if (weaviateEnabled || weaviateUrl || weaviateCollection) {
|
||||
try {
|
||||
const weaviateEmbedQuery = await loadEmbeddingModule(weaviateEmbeddingModule, importModule);
|
||||
const resolvedWeaviateClient = weaviateClient ?? (
|
||||
weaviateEnabled && weaviateUrl && weaviateEmbedQuery
|
||||
? createWeaviateHttpClient({
|
||||
url: weaviateUrl,
|
||||
apiKey: env?.MEMORY_WEAVIATE_API_KEY,
|
||||
fetchImpl,
|
||||
timeoutMs: Number(env?.MEMORY_WEAVIATE_TIMEOUT_MS ?? 3000) || 3000,
|
||||
})
|
||||
: null
|
||||
);
|
||||
weaviateBackend = createWeaviateMemoryBackend({
|
||||
enabled: weaviateEnabled,
|
||||
url: weaviateUrl,
|
||||
collection: weaviateCollection || undefined,
|
||||
client: resolvedWeaviateClient,
|
||||
embedQuery: weaviateEmbedQuery,
|
||||
defaultLimit: Number(env?.MEMORY_WEAVIATE_LIMIT ?? 8) || 8,
|
||||
});
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] weaviate backend unavailable: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
weaviateBackend = createWeaviateMemoryBackend({
|
||||
enabled: false,
|
||||
unavailableReason: 'init_failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (weaviateBackend) backends.push(weaviateBackend);
|
||||
let mem0Backend = null;
|
||||
const mem0Enabled = readFlag(env, 'MEMORY_MEM0_ENABLED', false);
|
||||
const mem0ApiKey = String(env?.MEMORY_MEM0_API_KEY ?? '').trim();
|
||||
const mem0ProjectId = String(env?.MEMORY_MEM0_PROJECT_ID ?? '').trim();
|
||||
const mem0BaseUrl = String(env?.MEMORY_MEM0_BASE_URL ?? 'https://api.mem0.ai').trim();
|
||||
const mem0ModelProviderKeyId = String(env?.MEMORY_MEM0_MODEL_PROVIDER_KEY_ID ?? '').trim();
|
||||
const mem0Model = String(env?.MEMORY_MEM0_MODEL ?? '').trim();
|
||||
const mem0ModelApiType = String(env?.MEMORY_MEM0_MODEL_API ?? '').trim();
|
||||
if (mem0Enabled || mem0ApiKey || mem0ProjectId) {
|
||||
try {
|
||||
const mem0Client = mem0Enabled && mem0ApiKey
|
||||
? createMem0HttpClient({
|
||||
apiKey: mem0ApiKey,
|
||||
projectId: mem0ProjectId || undefined,
|
||||
baseUrl: mem0BaseUrl,
|
||||
writePath: env?.MEMORY_MEM0_WRITE_PATH || undefined,
|
||||
compactPath: env?.MEMORY_MEM0_COMPACT_PATH || undefined,
|
||||
modelProviderKeyId: mem0ModelProviderKeyId || undefined,
|
||||
model: mem0Model || undefined,
|
||||
modelApiType: mem0ModelApiType || undefined,
|
||||
fetchImpl,
|
||||
timeoutMs: Number(env?.MEMORY_MEM0_TIMEOUT_MS ?? 3000) || 3000,
|
||||
})
|
||||
: null;
|
||||
mem0Backend = createMem0MemoryBackend({
|
||||
enabled: mem0Enabled,
|
||||
apiKey: mem0ApiKey,
|
||||
projectId: mem0ProjectId || undefined,
|
||||
client: mem0Client,
|
||||
});
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] mem0 backend unavailable: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
mem0Backend = createMem0MemoryBackend({
|
||||
enabled: false,
|
||||
unavailableReason: 'init_failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mem0Backend) backends.push(mem0Backend);
|
||||
let lettaBackend = null;
|
||||
const lettaEnabled = readFlag(env, 'MEMORY_LETTA_ENABLED', false);
|
||||
const lettaApiKey = String(env?.MEMORY_LETTA_API_KEY ?? '').trim();
|
||||
const lettaProjectId = String(env?.MEMORY_LETTA_PROJECT_ID ?? '').trim();
|
||||
const lettaAgentId = String(env?.MEMORY_LETTA_AGENT_ID ?? '').trim();
|
||||
const lettaBaseUrl = String(env?.MEMORY_LETTA_BASE_URL ?? 'https://api.letta.com').trim();
|
||||
const lettaModelProviderKeyId = String(env?.MEMORY_LETTA_MODEL_PROVIDER_KEY_ID ?? '').trim();
|
||||
const lettaModel = String(env?.MEMORY_LETTA_MODEL ?? '').trim();
|
||||
const lettaModelApiType = String(env?.MEMORY_LETTA_MODEL_API ?? '').trim();
|
||||
if (lettaEnabled || lettaApiKey || lettaProjectId || lettaAgentId) {
|
||||
try {
|
||||
const resolvedLettaClient = lettaClient ?? (
|
||||
lettaEnabled && lettaApiKey && lettaAgentId
|
||||
? createLettaHttpClient({
|
||||
apiKey: lettaApiKey,
|
||||
baseUrl: lettaBaseUrl,
|
||||
agentId: lettaAgentId,
|
||||
projectId: lettaProjectId || undefined,
|
||||
resolvePath: env?.MEMORY_LETTA_RESOLVE_PATH || undefined,
|
||||
writePath: env?.MEMORY_LETTA_WRITE_PATH || undefined,
|
||||
compactPath: env?.MEMORY_LETTA_COMPACT_PATH || undefined,
|
||||
modelProviderKeyId: lettaModelProviderKeyId || undefined,
|
||||
model: lettaModel || undefined,
|
||||
modelApiType: lettaModelApiType || undefined,
|
||||
fetchImpl,
|
||||
timeoutMs: Number(env?.MEMORY_LETTA_TIMEOUT_MS ?? 3000) || 3000,
|
||||
})
|
||||
: null
|
||||
);
|
||||
lettaBackend = createLettaMemoryBackend({
|
||||
enabled: lettaEnabled,
|
||||
apiKey: lettaApiKey,
|
||||
projectId: lettaProjectId || undefined,
|
||||
agentId: lettaAgentId || undefined,
|
||||
client: resolvedLettaClient,
|
||||
});
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] letta backend unavailable: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
lettaBackend = createLettaMemoryBackend({
|
||||
enabled: false,
|
||||
unavailableReason: 'init_failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (lettaBackend) backends.push(lettaBackend);
|
||||
let neo4jBackend = null;
|
||||
const neo4jEnabled = readFlag(env, 'MEMORY_NEO4J_ENABLED', false);
|
||||
const neo4jUri = String(env?.MEMORY_NEO4J_URI ?? '').trim();
|
||||
const neo4jHttpUrl = String(env?.MEMORY_NEO4J_HTTP_URL ?? '').trim();
|
||||
const neo4jUsername = String(env?.MEMORY_NEO4J_USER ?? '').trim();
|
||||
const neo4jPassword = String(env?.MEMORY_NEO4J_PASSWORD ?? '').trim();
|
||||
if (neo4jEnabled || neo4jUri || neo4jHttpUrl || neo4jUsername || neo4jPassword) {
|
||||
try {
|
||||
const resolvedNeo4jClient = neo4jClient ?? (
|
||||
neo4jEnabled && neo4jHttpUrl && neo4jUsername && neo4jPassword
|
||||
? createNeo4jHttpClient({
|
||||
httpUrl: neo4jHttpUrl,
|
||||
username: neo4jUsername,
|
||||
password: neo4jPassword,
|
||||
database: env?.MEMORY_NEO4J_DATABASE || 'neo4j',
|
||||
fetchImpl,
|
||||
timeoutMs: Number(env?.MEMORY_NEO4J_TIMEOUT_MS ?? 3000) || 3000,
|
||||
})
|
||||
: null
|
||||
);
|
||||
neo4jBackend = createNeo4jMemoryBackend({
|
||||
enabled: neo4jEnabled,
|
||||
uri: neo4jUri,
|
||||
httpUrl: neo4jHttpUrl,
|
||||
username: neo4jUsername,
|
||||
password: neo4jPassword,
|
||||
client: resolvedNeo4jClient,
|
||||
});
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] neo4j backend unavailable: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
neo4jBackend = createNeo4jMemoryBackend({
|
||||
enabled: false,
|
||||
unavailableReason: 'init_failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (neo4jBackend) backends.push(neo4jBackend);
|
||||
let redisStreamsBackend = null;
|
||||
const redisStreamsEnabled = readFlag(env, 'MEMORY_REDIS_STREAMS_ENABLED', false);
|
||||
const redisStreamsUrl = String(env?.MEMORY_REDIS_STREAMS_URL ?? '').trim();
|
||||
const redisStreamsStream = String(env?.MEMORY_REDIS_STREAMS_STREAM ?? '').trim();
|
||||
if (redisStreamsEnabled || redisStreamsUrl || redisStreamsStream) {
|
||||
try {
|
||||
const resolvedRedisStreamsClient = redisStreamsClient ?? (
|
||||
redisStreamsEnabled && redisStreamsUrl
|
||||
? await createRedisStreamsClient({
|
||||
url: redisStreamsUrl,
|
||||
stream: redisStreamsStream || undefined,
|
||||
importRedis,
|
||||
})
|
||||
: null
|
||||
);
|
||||
if (resolvedRedisStreamsClient?.close) closers.push(() => resolvedRedisStreamsClient.close());
|
||||
redisStreamsBackend = createRedisStreamsMemoryBackend({
|
||||
enabled: redisStreamsEnabled,
|
||||
url: redisStreamsUrl,
|
||||
stream: redisStreamsStream || undefined,
|
||||
client: resolvedRedisStreamsClient,
|
||||
});
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] redis-streams backend unavailable: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
redisStreamsBackend = createRedisStreamsMemoryBackend({
|
||||
enabled: false,
|
||||
unavailableReason: 'init_failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (redisStreamsBackend) backends.push(redisStreamsBackend);
|
||||
let langGraphBackend = null;
|
||||
const langGraphEnabled = readFlag(env, 'MEMORY_LANGGRAPH_ENABLED', false);
|
||||
const langGraphPolicyId = String(env?.MEMORY_LANGGRAPH_POLICY_ID ?? '').trim();
|
||||
const langGraphUrl = String(env?.MEMORY_LANGGRAPH_URL ?? '').trim();
|
||||
const langGraphModelProviderKeyId = String(env?.MEMORY_LANGGRAPH_MODEL_PROVIDER_KEY_ID ?? '').trim();
|
||||
const langGraphModel = String(env?.MEMORY_LANGGRAPH_MODEL ?? '').trim();
|
||||
const langGraphModelApiType = String(env?.MEMORY_LANGGRAPH_MODEL_API ?? '').trim();
|
||||
if (langGraphEnabled || langGraphPolicyId || langGraphUrl) {
|
||||
try {
|
||||
const resolvedLangGraphClient = langGraphClient ?? (
|
||||
langGraphEnabled && langGraphUrl
|
||||
? createLangGraphHttpClient({
|
||||
baseUrl: langGraphUrl,
|
||||
apiKey: env?.MEMORY_LANGGRAPH_API_KEY,
|
||||
resolvePath: env?.MEMORY_LANGGRAPH_RESOLVE_PATH || undefined,
|
||||
modelProviderKeyId: langGraphModelProviderKeyId || undefined,
|
||||
model: langGraphModel || undefined,
|
||||
modelApiType: langGraphModelApiType || undefined,
|
||||
fetchImpl,
|
||||
timeoutMs: Number(env?.MEMORY_LANGGRAPH_TIMEOUT_MS ?? 3000) || 3000,
|
||||
})
|
||||
: null
|
||||
);
|
||||
langGraphBackend = createLangGraphMemoryBackend({
|
||||
enabled: langGraphEnabled,
|
||||
policyId: langGraphPolicyId || undefined,
|
||||
client: resolvedLangGraphClient,
|
||||
});
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] langgraph backend unavailable: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
langGraphBackend = createLangGraphMemoryBackend({
|
||||
enabled: false,
|
||||
unavailableReason: 'init_failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (langGraphBackend) backends.push(langGraphBackend);
|
||||
backends.push(...createMemoryV2PluginBackends({
|
||||
exclude: [
|
||||
...(pgvectorBackend ? ['pgvector'] : []),
|
||||
...(qdrantBackend ? ['qdrant'] : []),
|
||||
...(weaviateBackend ? ['weaviate'] : []),
|
||||
...(mem0Backend ? ['mem0'] : []),
|
||||
...(lettaBackend ? ['letta'] : []),
|
||||
...(neo4jBackend ? ['neo4j'] : []),
|
||||
...(redisStreamsBackend ? ['redis-streams'] : []),
|
||||
...(langGraphBackend ? ['langgraph'] : []),
|
||||
],
|
||||
}));
|
||||
|
||||
const memory = createMemoryV2({
|
||||
legacyMemoryService,
|
||||
backends,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
memory.close = async () => {
|
||||
const results = await Promise.allSettled(closers.map((close) => close()));
|
||||
for (const result of results) {
|
||||
if (result.status === 'rejected') {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] close skipped: ${result.reason instanceof Error ? result.reason.message : result.reason}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return memory;
|
||||
}
|
||||
|
||||
export async function createManagedMemoryV2Runtime({
|
||||
legacyMemoryService = null,
|
||||
configService = null,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
importPg = (specifier) => import(specifier),
|
||||
importRedis = (specifier) => import(specifier),
|
||||
importModule = (specifier) => import(specifier),
|
||||
fetchImpl = globalThis.fetch,
|
||||
lettaClient = null,
|
||||
weaviateClient = null,
|
||||
neo4jClient = null,
|
||||
redisStreamsClient = null,
|
||||
langGraphClient = null,
|
||||
} = {}) {
|
||||
let activeRuntime = null;
|
||||
let activeFingerprint = null;
|
||||
let activeMeta = { source: 'env', updatedAt: null, updatedBy: null, configError: null };
|
||||
|
||||
async function loadRuntimeState() {
|
||||
if (!configService?.getRuntimeState) {
|
||||
return {
|
||||
source: 'env',
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
fingerprint: 'env-only',
|
||||
effectiveEnv: { ...env },
|
||||
configError: null,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const state = await configService.getRuntimeState();
|
||||
return {
|
||||
source: state?.source ?? 'admin-db',
|
||||
updatedAt: state?.updatedAt ?? null,
|
||||
updatedBy: state?.updatedBy ?? null,
|
||||
fingerprint: state?.fingerprint ?? `admin-db:${Date.now()}`,
|
||||
effectiveEnv: { ...env, ...(state?.overrides ?? {}) },
|
||||
configError: null,
|
||||
};
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] admin config unavailable, using process env: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
return {
|
||||
source: 'env-fallback',
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
fingerprint: 'env-fallback',
|
||||
effectiveEnv: { ...env },
|
||||
configError: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureRuntime() {
|
||||
const state = await loadRuntimeState();
|
||||
if (activeRuntime && activeFingerprint === state.fingerprint) {
|
||||
activeMeta = state;
|
||||
return activeRuntime;
|
||||
}
|
||||
const nextRuntime = await createMemoryV2Runtime({
|
||||
legacyMemoryService,
|
||||
env: state.effectiveEnv,
|
||||
logger,
|
||||
importPg,
|
||||
importRedis,
|
||||
importModule,
|
||||
fetchImpl,
|
||||
lettaClient,
|
||||
weaviateClient,
|
||||
neo4jClient,
|
||||
redisStreamsClient,
|
||||
langGraphClient,
|
||||
});
|
||||
const previous = activeRuntime;
|
||||
activeRuntime = nextRuntime;
|
||||
activeFingerprint = state.fingerprint;
|
||||
activeMeta = state;
|
||||
if (previous?.close) {
|
||||
await previous.close().catch((err) => {
|
||||
logger?.warn?.(
|
||||
`[memory-v2] previous runtime close skipped: ${err instanceof Error ? err.message : err}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
return activeRuntime;
|
||||
}
|
||||
|
||||
const managed = {
|
||||
get policy() {
|
||||
return activeRuntime?.policy ?? resolveMemoryV2Policy({ env });
|
||||
},
|
||||
|
||||
async getStatus() {
|
||||
const runtime = await ensureRuntime();
|
||||
const status = await Promise.resolve(runtime.getStatus());
|
||||
return {
|
||||
...status,
|
||||
configSource: activeMeta.source,
|
||||
configUpdatedAt: activeMeta.updatedAt,
|
||||
configUpdatedBy: activeMeta.updatedBy,
|
||||
configError: activeMeta.configError,
|
||||
};
|
||||
},
|
||||
|
||||
async resolve(input = {}) {
|
||||
const runtime = await ensureRuntime();
|
||||
return runtime.resolve(input);
|
||||
},
|
||||
|
||||
async write(input = {}) {
|
||||
const runtime = await ensureRuntime();
|
||||
return runtime.write(input);
|
||||
},
|
||||
|
||||
async compact(input = {}) {
|
||||
const runtime = await ensureRuntime();
|
||||
return runtime.compact(input);
|
||||
},
|
||||
|
||||
async close() {
|
||||
const runtime = activeRuntime;
|
||||
activeRuntime = null;
|
||||
activeFingerprint = null;
|
||||
if (runtime?.close) {
|
||||
await runtime.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return managed;
|
||||
}
|
||||
Reference in New Issue
Block a user