Files
memind/memory-v2-candidate-auto-review.mjs
T
john 6f3e53a56a 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>
2026-08-01 17:14:06 +08:00

187 lines
5.8 KiB
JavaScript

import { detectFalseStoreCandidate } from './memory-v2-shadow-audit.mjs';
import {
inspectMemoryContentDurability,
resolvePersonalShadowConfig,
} from './memory-v2-personal-shadow.mjs';
import { createPersonalMemoryCandidateStore } from './memory-v2-personal-store.mjs';
const AUTO_REVIEW_ACCEPT_REASONS = new Set([
'explicit_memory_request',
'preference_signal',
'goal_signal',
'stable_fact_signal',
]);
const AUTO_REVIEW_REJECT_REASONS = new Set([
'decision_signal',
]);
export function resolveCandidateAutoReviewAction(candidate, { mode = 'canary' } = {}) {
const normalizedMode = String(mode ?? 'canary').trim().toLowerCase();
if (normalizedMode === 'off' || normalizedMode === 'shadow') {
return { action: 'pending', reason: 'shadow_mode' };
}
if (!candidate || String(candidate.status ?? 'candidate') !== 'candidate') {
return { action: 'pending', reason: 'not_pending' };
}
const content = String(candidate.content ?? '');
const durabilityIssue = inspectMemoryContentDurability(content);
if (durabilityIssue) {
return { action: 'reject', reason: durabilityIssue };
}
const falseStore = detectFalseStoreCandidate(content);
if (falseStore.suspicious) {
return { action: 'reject', reason: falseStore.code ?? 'false_store' };
}
const policyReason = String(candidate.policyReason ?? '');
if (AUTO_REVIEW_REJECT_REASONS.has(policyReason)) {
return { action: 'reject', reason: 'decision_signal_not_durable' };
}
if (AUTO_REVIEW_ACCEPT_REASONS.has(policyReason)) {
return { action: 'accept', reason: 'auto_review_policy' };
}
return { action: 'pending', reason: 'no_auto_review_rule' };
}
export function resolveCandidateAutoReviewScopes(env = process.env) {
const config = resolvePersonalShadowConfig(env);
if (!config.enabled || config.effectiveMode === 'shadow') return [];
if (config.effectiveMode === 'active') return [null];
const rolloutMode = String(env.MEMORY_LIFECYCLE_ROLLOUT_MODE ?? 'off').trim().toLowerCase();
const rolloutUserIds = String(env.MEMORY_LIFECYCLE_ROLLOUT_USER_IDS ?? '')
.split(/[\s,]+/u)
.map((item) => item.trim())
.filter(Boolean)
.slice(0, 1000);
if (rolloutMode === 'canary' && rolloutUserIds.length > 0) {
return [...new Set(rolloutUserIds)];
}
if (config.effectiveMode === 'canary') {
const agentCanaryUserIds = String(env.MEMORY_AGENT_CANARY_USER_IDS ?? '')
.split(/[\s,]+/u)
.map((item) => item.trim())
.filter(Boolean)
.slice(0, 1000);
if (agentCanaryUserIds.length > 0) return [...new Set(agentCanaryUserIds)];
}
return [];
}
export async function runCandidateAutoReviewBatch(
pool,
{
env = process.env,
userId = null,
limit = 100,
reviewedBy = 'system:auto-review',
dryRun = false,
now = () => Date.now(),
} = {},
) {
if (!pool?.query) {
return { ok: false, skipped: true, reason: 'no_pool', accepted: 0, rejected: 0, pending: 0 };
}
const config = resolvePersonalShadowConfig(env);
if (!config.autoReviewEnabled) {
return { ok: true, skipped: true, reason: 'auto_review_disabled', accepted: 0, rejected: 0, pending: 0 };
}
const store = createPersonalMemoryCandidateStore(pool, { now });
if (!store?.listCandidates || !store?.reviewCandidate) {
return { ok: false, skipped: true, reason: 'no_store', accepted: 0, rejected: 0, pending: 0 };
}
const items = await store.listCandidates({
status: 'candidate',
userId,
limit: Math.max(1, Math.min(500, Number(limit) || 100)),
offset: 0,
});
let accepted = 0;
let rejected = 0;
let pending = 0;
const samples = [];
for (const item of items) {
const decision = resolveCandidateAutoReviewAction(item, { mode: config.effectiveMode });
if (decision.action === 'accept') {
if (dryRun) {
accepted += 1;
if (samples.length < 5) samples.push({ id: item.id, action: 'accepted', reason: decision.reason });
continue;
}
const result = await store.reviewCandidate(item.id, 'accepted', { reviewedBy });
if (result.updated) {
accepted += 1;
if (samples.length < 5) samples.push({ id: item.id, action: 'accepted', reason: decision.reason });
} else {
pending += 1;
}
continue;
}
if (decision.action === 'reject') {
if (dryRun) {
rejected += 1;
if (samples.length < 5) samples.push({ id: item.id, action: 'rejected', reason: decision.reason });
continue;
}
const result = await store.reviewCandidate(item.id, 'rejected', { reviewedBy });
if (result.updated) {
rejected += 1;
if (samples.length < 5) samples.push({ id: item.id, action: 'rejected', reason: decision.reason });
} else {
pending += 1;
}
continue;
}
pending += 1;
}
return {
ok: true,
skipped: false,
dryRun,
mode: config.effectiveMode,
scanned: items.length,
accepted,
rejected,
pending,
samples,
};
}
export async function autoReviewCandidateIfPending(
pool,
candidate,
{
env = process.env,
reviewedBy = 'system:auto-review',
now = () => Date.now(),
} = {},
) {
if (!pool?.query || !candidate?.id || String(candidate.status ?? '') !== 'candidate') {
return { reviewed: false, reason: 'not_pending' };
}
const config = resolvePersonalShadowConfig(env);
const decision = resolveCandidateAutoReviewAction(candidate, { mode: config.effectiveMode });
if (decision.action !== 'accept' && decision.action !== 'reject') {
return { reviewed: false, reason: decision.reason };
}
const store = createPersonalMemoryCandidateStore(pool, { now });
const status = decision.action === 'accept' ? 'accepted' : 'rejected';
const result = await store.reviewCandidate(candidate.id, status, { reviewedBy });
return {
reviewed: Boolean(result.updated),
status,
reason: decision.reason,
};
}