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
+43 -5
View File
@@ -13,6 +13,13 @@ const TRIVIAL_PATTERNS = [
/^(?:你好|您好|谢谢|好的|可以|收到|再见|hi|hello|thanks)[!。.\s]*$/i,
/^(?:查一下|搜一下|看看|继续|下一步)[。.!?\s]*$/i,
];
const NON_DURABLE_PATTERNS = [
/^(?:继续|接着|然后|现在请|请继续|帮我继续|重试上一轮)/u,
/(?:public\/[\w./-]+\.html|read_image|edit_file|page-data|Page Data|DURABILITY-|【Memind 任务编排】|\[Memory Context\])/iu,
/[?]\s*$/,
/^(?:什么是|解释一下|介绍一下|请问|能不能|是否可以|怎么|如何|为什么|啥是)/u,
];
const AUTO_ACCEPT_POLICY_REASONS = new Set(['explicit_memory_request']);
function readFlag(env, key, fallback = false) {
const raw = env?.[key];
@@ -62,7 +69,13 @@ function stableHash(value) {
function classify(text) {
const rules = [
{ type: 'episodic', importance: 0.95, confidence: 0.98, pattern: /(?:请记住|记住|以后要|从现在起)/i, reason: 'explicit_memory_request' },
{ type: 'episodic', importance: 0.9, confidence: 0.9, pattern: /(?:决定|确定|统一采用|必须|不允许|禁止|最终选择)/i, reason: 'decision_signal' },
{
type: 'episodic',
importance: 0.9,
confidence: 0.9,
pattern: /(?:我(?:们)?(?:已经)?决定|最终决定|统一(?:采用|使用|改为)|从现在起必须|不再允许|永久禁止)/iu,
reason: 'decision_signal',
},
{ type: 'preference', importance: 0.8, confidence: 0.88, pattern: /(?:我喜欢|我偏好|我不喜欢|我的习惯|倾向于|更希望)/i, reason: 'preference_signal' },
{ type: 'goal', importance: 0.85, confidence: 0.86, pattern: /(?:长期目标|当前目标|目标是|计划要|正在建设|准备实现|希望长期)/i, reason: 'goal_signal' },
{ type: 'semantic', importance: 0.75, confidence: 0.82, pattern: /(?:技术栈|架构原则|项目使用|系统采用|仓库位于|运行在)/i, reason: 'stable_fact_signal' },
@@ -77,13 +90,22 @@ function normalizeCandidateMode(value, fallback = 'shadow') {
return CANDIDATE_MODES.has(normalized) ? normalized : fallback;
}
export function inspectMemoryContentDurability(text) {
const normalized = normalizeText(text);
if (!normalized || normalized.length < 8) return 'too_short';
if (TRIVIAL_PATTERNS.some((pattern) => pattern.test(normalized))) return 'trivial';
if (NON_DURABLE_PATTERNS.some((pattern) => pattern.test(normalized))) {
return 'non_durable_content';
}
return null;
}
export function shouldAutoAcceptCandidate(candidate, config) {
if (!candidate || !config?.enabled) return false;
const mode = normalizeCandidateMode(config.requestedMode, 'shadow');
if (mode === 'shadow' || mode === 'off') return false;
if (mode === 'active') return true;
if (candidate.policyReason === 'explicit_memory_request') return true;
return Number(candidate.confidence) >= Math.max(Number(config.minConfidence) || 0, 0.9);
if (config.autoAcceptAll) return true;
return AUTO_ACCEPT_POLICY_REASONS.has(String(candidate.policyReason ?? ''));
}
export function resolvePersonalShadowConfig(env = process.env) {
@@ -98,6 +120,7 @@ export function resolvePersonalShadowConfig(env = process.env) {
requestedMode,
effectiveMode: enabled ? requestedMode : 'off',
autoReviewEnabled: enabled && requestedMode !== 'shadow',
autoAcceptAll: readFlag(env, 'MEMORY_CANDIDATE_AUTO_ACCEPT_ALL', false),
minImportance: readNumber(env, 'MEMORY_CANDIDATE_MIN_IMPORTANCE', 0.7, { min: 0, max: 1 }),
minConfidence: readNumber(env, 'MEMORY_CANDIDATE_MIN_CONFIDENCE', 0.8, { min: 0, max: 1 }),
maxPending: Math.round(readNumber(env, 'MEMORY_CANDIDATE_MAX_PENDING', 500, { min: 1, max: 5000 })),
@@ -135,6 +158,9 @@ export function createPersonalMemoryShadowPipeline({ env = process.env, now = ()
const text = messageText(message);
if (!text || text.length < 8) return reject('too_short');
if (TRIVIAL_PATTERNS.some((pattern) => pattern.test(text))) return reject('trivial');
if (NON_DURABLE_PATTERNS.some((pattern) => pattern.test(text))) {
return reject('non_durable_content');
}
if (config.rejectSensitive && SECRET_PATTERNS.some((pattern) => pattern.test(text))) {
return reject('sensitive_content');
}
@@ -205,7 +231,7 @@ export function createPersonalMemoryShadowPipeline({ env = process.env, now = ()
const persisted = await store.saveCandidate(result.candidate, { autoAccept });
if (persisted?.inserted === false) {
metrics.deduped += 1;
} else if (autoAccept) {
} else if (persisted?.status === 'accepted' || autoAccept) {
result.candidate.status = 'accepted';
metrics.autoReviewed += 1;
} else {
@@ -229,6 +255,18 @@ export function createPersonalMemoryShadowPipeline({ env = process.env, now = ()
autoReviewed: results.filter((result) => result.accepted && result.candidate?.status === 'accepted').length,
pendingReview: results.filter((result) => result.accepted && result.candidate?.status === 'candidate').length,
rejected: results.filter((result) => !result.accepted).length,
results: results.map((result) => ({
accepted: Boolean(result.accepted),
reason: result.reason ?? null,
candidate: result.candidate
? {
status: result.candidate.status,
content: result.candidate.content,
policyReason: result.candidate.policyReason,
memoryType: result.candidate.memoryType,
}
: null,
})),
};
} catch (err) {
metrics.errors += 1;