feat(memory-v2): close Phase A with auto-review, product events, and H5 recall UI.

Add candidate auto-review pipeline, shadow audit tooling, admin metrics page,
and user-visible memory recall hints in chat with phase-a readiness checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-01 17:14:06 +08:00
parent 666db0b939
commit 6f3e53a56a
50 changed files with 3730 additions and 38 deletions
+57
View File
@@ -9,6 +9,15 @@ import {
createPersonalMemoryCandidateStore,
ensurePersonalMemoryCandidateSchema,
} from './memory-v2-personal-store.mjs';
import {
autoReviewCandidateIfPending,
runCandidateAutoReviewBatch,
} from './memory-v2-candidate-auto-review.mjs';
import {
MEMORY_V2_PRODUCT_EVENT_TYPES,
ensureMemoryV2ProductEventsSchema,
recordMemoryV2ProductEvent,
} from './memory-v2-product-events.mjs';
import { createMem0HttpClient, createMem0MemoryBackend } from './memory-v2-mem0.mjs';
import { createNeo4jHttpClient, createNeo4jMemoryBackend } from './memory-v2-neo4j.mjs';
import { createPgvectorMemoryBackend } from './memory-v2-pgvector.mjs';
@@ -416,7 +425,42 @@ export async function createMemoryV2Runtime({
if (readFlag(env, 'MEMORY_CANDIDATE_PERSISTENCE_ENABLED', false) && mysqlPool?.query) {
try {
await ensurePersonalMemoryCandidateSchema(mysqlPool);
await ensureMemoryV2ProductEventsSchema(mysqlPool);
personalCandidateStore = createPersonalMemoryCandidateStore(mysqlPool);
const originalSaveCandidate = personalCandidateStore.saveCandidate.bind(personalCandidateStore);
personalCandidateStore.saveCandidate = async (candidate, options = {}) => {
const persisted = await originalSaveCandidate(candidate, options);
if (persisted?.inserted) {
void recordMemoryV2ProductEvent(mysqlPool, {
eventType: MEMORY_V2_PRODUCT_EVENT_TYPES.CANDIDATE_SAVED,
userId: candidate.userId,
sessionId: candidate.sessionId,
candidateId: candidate.id,
data: {
policyReason: candidate.policyReason,
memoryType: candidate.memoryType,
status: persisted.status,
autoAccept: Boolean(options.autoAccept),
},
}).catch(() => {});
if (persisted.status === 'candidate') {
try {
const reviewed = await autoReviewCandidateIfPending(mysqlPool, {
...candidate,
status: 'candidate',
}, { env });
if (reviewed.reviewed && reviewed.status) {
persisted.status = reviewed.status;
}
} catch (err) {
logger?.warn?.(
`[memory-v2] candidate auto-review skipped: ${err instanceof Error ? err.message : err}`,
);
}
}
}
return persisted;
};
} catch (err) {
logger?.warn?.(
`[memory-v2] candidate persistence unavailable: ${err instanceof Error ? err.message : err}`,
@@ -444,6 +488,11 @@ export async function createMemoryV2Runtime({
try {
for (const userId of lifecycleWorkerScopes) {
const input = userId == null ? {} : { userId };
await runCandidateAutoReviewBatch(mysqlPool, {
env,
userId,
limit: 200,
});
await lifecycle.expire(input);
await lifecycle.compact(input);
await lifecycle.promote(input);
@@ -518,6 +567,14 @@ export async function createMemoryV2Runtime({
lifecycle.promote = async (input = {}) => {
const result = await originalLifecyclePromote(input);
if (Number(result?.promoted ?? 0) > 0) {
void recordMemoryV2ProductEvent(mysqlPool, {
eventType: MEMORY_V2_PRODUCT_EVENT_TYPES.PROMOTED,
userId: input?.userId ?? null,
data: {
promoted: result.promoted,
promotedUserIds: result.promotedUserIds ?? [],
},
}).catch(() => {});
const promotedUserIds = Array.isArray(result?.promotedUserIds)
? result.promotedUserIds
: input?.userId ? [input.userId] : [];