Files
memind/memory-v2-runtime.mjs

664 lines
25 KiB
JavaScript

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 { createPersonalMemoryShadowPipeline } from './memory-v2-personal-shadow.mjs';
import { createPersonalMemoryCandidateStore } from './memory-v2-personal-store.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 { backfillLegacyMemoriesToPgvector } from './memory-v2-pgvector-backfill.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';
import { createMemoryV2LifecycleService } from './memory-v2-lifecycle.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,
mysqlPool = 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;
let pgPoolRef = null;
let pgEmbedQuery = 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?.());
pgPoolRef = pool;
pgEmbedQuery = embedQuery;
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 personalCandidateStore = readFlag(env, 'MEMORY_CANDIDATE_PERSISTENCE_ENABLED', false)
? createPersonalMemoryCandidateStore(mysqlPool)
: null;
const personalShadowPipeline = createPersonalMemoryShadowPipeline({
env,
store: personalCandidateStore,
});
const memory = createMemoryV2({
legacyMemoryService,
backends,
env,
logger,
personalShadowPipeline,
});
const lifecycle = createMemoryV2LifecycleService({ pool: mysqlPool, env, logger });
let lifecycleTimer = null;
const lifecycleWorkerEnabled = readFlag(env, 'MEMORY_LIFECYCLE_WORKER_ENABLED', false);
if (lifecycleWorkerEnabled && mysqlPool?.query) {
const intervalMs = Math.max(60_000, Number(lifecycle.policy.compactIntervalHours ?? 24) * 3_600_000);
const runLifecycle = () => lifecycle.expire().then(() => lifecycle.compact()).then(() => lifecycle.promote()).then(() => lifecycle.reflect()).catch((err) => {
logger?.warn?.(`[memory-v2] lifecycle worker skipped: ${err instanceof Error ? err.message : err}`);
});
lifecycleTimer = setInterval(runLifecycle, intervalMs);
lifecycleTimer.unref?.();
}
const originalGetStatus = memory.getStatus.bind(memory);
memory.getStatus = () => ({
...originalGetStatus(),
lifecycle: lifecycle.getStatus(),
});
memory.lifecycle = lifecycle;
const pgBackfillEnabled = readFlag(env, 'MEMORY_PGVECTOR_BACKFILL_ENABLED', pgvectorEnabled);
let pgBackfillBusy = false;
let pgBackfillCursor = { updatedAt: 0, id: '' };
async function syncPgvectorFromLegacy() {
if (!pgBackfillEnabled || !pgPoolRef || !pgEmbedQuery || !mysqlPool?.query) return;
if (pgBackfillBusy) return;
pgBackfillBusy = true;
try {
const maxBatches = Math.max(1, Number(env?.MEMORY_PGVECTOR_BACKFILL_MAX_BATCHES ?? 3) || 3);
const batchLimit = Math.max(1, Number(env?.MEMORY_PGVECTOR_BACKFILL_LIMIT ?? 50) || 50);
for (let batch = 0; batch < maxBatches; batch += 1) {
const result = await backfillLegacyMemoriesToPgvector({
mysqlPool,
pgPool: pgPoolRef,
embedMemory: (item) => pgEmbedQuery(item.text, item),
tableName: env?.MEMORY_PGVECTOR_TABLE,
cursor: pgBackfillCursor,
limit: batchLimit,
dryRun: false,
});
pgBackfillCursor = result.nextCursor;
if (!result.hasMore || result.scanned === 0) break;
}
} catch (err) {
logger?.warn?.(
`[memory-v2] pgvector backfill skipped: ${err instanceof Error ? err.message : err}`,
);
} finally {
pgBackfillBusy = false;
}
}
function wrapMemorySideEffect(methodName) {
const original = memory[methodName].bind(memory);
memory[methodName] = async (input = {}) => {
const result = await original(input);
if ((result?.memories ?? 0) > 0 || (result?.analyzed ?? 0) > 0) {
void syncPgvectorFromLegacy();
}
return result;
};
}
if (pgBackfillEnabled && pgPoolRef && pgEmbedQuery && mysqlPool) {
wrapMemorySideEffect('write');
wrapMemorySideEffect('compact');
}
memory.close = async () => {
if (lifecycleTimer) clearInterval(lifecycleTimer);
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,
mysqlPool = 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,
mysqlPool,
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 observePersonalMemory(input = {}) {
const runtime = await ensureRuntime();
return runtime.observePersonalMemory(input);
},
async listMemories(input = {}) {
const runtime = await ensureRuntime();
return runtime.lifecycle?.listMemories?.(input) ?? [];
},
async forgetMemory(input = {}) {
const runtime = await ensureRuntime();
return runtime.lifecycle?.forgetMemory?.(input) ?? { ok: false, skipped: true, reason: 'unavailable' };
},
async runLifecycle(input = {}) {
const runtime = await ensureRuntime();
const lifecycle = runtime.lifecycle;
if (!lifecycle) return { ok: false, reason: 'unavailable' };
return {
expire: await lifecycle.expire(input),
compact: await lifecycle.compact(input),
promote: await lifecycle.promote(input),
reflect: await lifecycle.reflect(input),
};
},
async close() {
const runtime = activeRuntime;
activeRuntime = null;
activeFingerprint = null;
if (runtime?.close) {
await runtime.close();
}
},
};
return managed;
}