feat(memory): add gated agent recall and lifecycle controls

This commit is contained in:
john
2026-07-16 21:05:27 +08:00
parent c1012da120
commit 6e460336f7
13 changed files with 655 additions and 3 deletions
+41
View File
@@ -13,6 +13,7 @@ import { backfillLegacyMemoriesToPgvector } from './memory-v2-pgvector-backfill.
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';
@@ -416,6 +417,23 @@ export async function createMemoryV2Runtime({
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;
@@ -467,6 +485,7 @@ export async function createMemoryV2Runtime({
}
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') {
@@ -608,6 +627,28 @@ export async function createManagedMemoryV2Runtime({
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;