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:
@@ -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] : [];
|
||||
|
||||
Reference in New Issue
Block a user