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