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
+278
View File
@@ -0,0 +1,278 @@
const MS_PER_HOUR = 3600000;
const MS_PER_DAY = 86400000;
const FALSE_STORE_PATTERNS = [
{
code: 'question_mark',
pattern: /[?]\s*$/,
message: 'Content ends with a question mark',
},
{
code: 'what_is_question',
pattern: /^(?:什么是|解释一下|介绍一下|请问|能不能|是否可以|怎么|如何|为什么|啥是)/u,
message: 'Content looks like a question rather than a fact',
},
{
code: 'imperative_only',
pattern: /^(?:查一下|搜一下|看看|继续|下一步|帮我|请帮我)/u,
message: 'Content looks like an instruction without durable facts',
},
{
code: 'trivial_greeting',
pattern: /^(?:你好|您好|谢谢|好的|可以|收到|再见|hi|hello|thanks)[!。.\s]*$/iu,
message: 'Content is a trivial greeting or acknowledgement',
},
{
code: 'routing_leak',
pattern: /(?:\[Memory Context\]|【Memind 任务编排】|agent_orchestration)/u,
message: 'Content contains internal routing or memory envelope text',
},
];
export function parseSinceArg(value, now = Date.now()) {
const raw = String(value ?? '7d').trim().toLowerCase();
const match = raw.match(/^(\d+)(h|d|w)$/);
if (!match) {
throw new Error(`Invalid --since value "${value}". Expected formats like 24h, 7d, 2w.`);
}
const amount = Number(match[1]);
if (!Number.isFinite(amount) || amount <= 0) {
throw new Error(`Invalid --since value "${value}". Amount must be positive.`);
}
const unit = match[2];
const multiplier = unit === 'h' ? MS_PER_HOUR : unit === 'w' ? MS_PER_DAY * 7 : MS_PER_DAY;
return {
sinceMs: now - amount * multiplier,
label: raw,
};
}
export function detectFalseStoreCandidate(content) {
const text = String(content ?? '').replace(/\s+/g, ' ').trim();
if (!text) {
return { suspicious: true, code: 'empty_content', message: 'Content is empty' };
}
for (const rule of FALSE_STORE_PATTERNS) {
if (rule.pattern.test(text)) {
return { suspicious: true, code: rule.code, message: rule.message };
}
}
return { suspicious: false };
}
function normalizeCandidate(row) {
return {
id: String(row.id),
userId: String(row.user_id ?? row.userId),
sessionId: row.session_id == null ? null : String(row.session_id ?? row.sessionId),
memoryType: String(row.memory_type ?? row.memoryType ?? ''),
content: String(row.content ?? ''),
status: String(row.status ?? ''),
policyReason: String(row.policy_reason ?? row.policyReason ?? ''),
confidence: Number(row.confidence ?? 0),
importance: Number(row.importance ?? 0),
createdAt: Number(row.created_at ?? row.createdAt ?? 0),
updatedAt: Number(row.updated_at ?? row.updatedAt ?? 0),
};
}
function normalizeMemoryItem(row) {
return {
id: String(row.id),
userId: String(row.user_id ?? row.userId),
label: String(row.label ?? ''),
content: String(row.memory_text ?? row.content ?? ''),
status: String(row.status ?? ''),
createdAt: Number(row.created_at ?? row.createdAt ?? 0),
updatedAt: Number(row.updated_at ?? row.updatedAt ?? 0),
};
}
function countByField(items, field) {
const counts = new Map();
for (const item of items) {
const key = String(item[field] ?? 'unknown');
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return Object.fromEntries([...counts.entries()].sort((a, b) => b[1] - a[1]));
}
function topEntries(counts, limit = 10) {
return Object.entries(counts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([key, count]) => ({ key, count }));
}
export function auditMemoryV2Shadow({
candidates = [],
memoryItems = [],
pgvectorMemoryIds = new Set(),
pgvectorConfigured = false,
agentMemoryEvents = [],
sinceMs = 0,
nowMs = Date.now(),
falseStoreSampleLimit = 20,
pgvectorLagSampleLimit = 20,
} = {}) {
const normalizedCandidates = candidates.map(normalizeCandidate);
const normalizedItems = memoryItems.map(normalizeMemoryItem);
const inWindowCandidates = normalizedCandidates.filter((item) => item.createdAt >= sinceMs);
const inWindowItems = normalizedItems.filter((item) => item.updatedAt >= sinceMs);
const candidateStatusCounts = countByField(inWindowCandidates, 'status');
const policyReasonCounts = countByField(inWindowCandidates, 'policyReason');
const memoryTypeCounts = countByField(inWindowCandidates, 'memoryType');
const suspiciousCandidates = inWindowCandidates
.filter((item) => ['candidate', 'accepted'].includes(item.status))
.map((item) => {
const verdict = detectFalseStoreCandidate(item.content);
if (!verdict.suspicious) return null;
return {
id: item.id,
userId: item.userId,
status: item.status,
policyReason: item.policyReason,
code: verdict.code,
message: verdict.message,
contentPreview: item.content.slice(0, 120),
createdAt: item.createdAt,
};
})
.filter(Boolean);
const activeItems = normalizedItems.filter((item) => item.status === 'active');
const pgvectorLagUsers = new Map();
const pgvectorMissingSamples = [];
if (pgvectorConfigured) {
for (const item of activeItems) {
if (pgvectorMemoryIds.has(item.id)) continue;
pgvectorLagUsers.set(item.userId, (pgvectorLagUsers.get(item.userId) ?? 0) + 1);
if (pgvectorMissingSamples.length < pgvectorLagSampleLimit) {
pgvectorMissingSamples.push({
memoryId: item.id,
userId: item.userId,
label: item.label,
updatedAt: item.updatedAt,
contentPreview: item.content.slice(0, 120),
});
}
}
}
const resolvedEvents = agentMemoryEvents.filter((event) => {
const createdAt = Number(event.created_at ?? event.createdAt ?? 0);
return createdAt >= sinceMs;
});
const resolvedWithHits = resolvedEvents.filter((event) => {
const data = event.data_json ?? event.data ?? {};
const count = Number(data.count ?? data.memoryCount ?? data.memories?.length ?? 0);
return count > 0;
});
const acceptedCount = candidateStatusCounts.accepted ?? 0;
const candidateCount = candidateStatusCounts.candidate ?? 0;
const reviewedCount = acceptedCount + (candidateStatusCounts.rejected ?? 0);
const autoAcceptRate = reviewedCount > 0 ? acceptedCount / reviewedCount : null;
const falseStoreRate = inWindowCandidates.length > 0
? suspiciousCandidates.length / inWindowCandidates.length
: null;
const resolveHitRate = resolvedEvents.length > 0
? resolvedWithHits.length / resolvedEvents.length
: null;
const thresholds = {
falseStoreRateMax: 0.05,
pgvectorLagUsersMax: 0,
};
const issues = [];
if (falseStoreRate != null && falseStoreRate > thresholds.falseStoreRateMax) {
issues.push({
code: 'false_store_rate_high',
message: `False-store sample rate ${(falseStoreRate * 100).toFixed(1)}% exceeds ${thresholds.falseStoreRateMax * 100}%`,
});
}
if (pgvectorConfigured && pgvectorLagUsers.size > thresholds.pgvectorLagUsersMax) {
issues.push({
code: 'pgvector_sync_lag',
message: `${pgvectorLagUsers.size} user(s) have active memories missing from pgvector`,
});
}
return {
ok: issues.length === 0,
generatedAt: nowMs,
window: {
sinceMs,
untilMs: nowMs,
candidateCount: inWindowCandidates.length,
memoryItemCount: inWindowItems.length,
agentMemoryResolvedEvents: resolvedEvents.length,
},
summary: {
candidateStatusCounts,
policyReasonCounts,
memoryTypeCounts,
autoAcceptRate,
falseStoreRate,
resolveHitRate,
suspiciousCandidateCount: suspiciousCandidates.length,
pgvectorLagUserCount: pgvectorConfigured ? pgvectorLagUsers.size : null,
activeMemoryCount: activeItems.length,
pgvectorMemoryCount: pgvectorConfigured ? pgvectorMemoryIds.size : null,
},
topPolicyReasons: topEntries(policyReasonCounts),
pgvectorLagUsers: [...pgvectorLagUsers.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, pgvectorLagSampleLimit)
.map(([userId, missingCount]) => ({ userId, missingCount })),
sampleFalseStores: suspiciousCandidates.slice(0, falseStoreSampleLimit),
pgvectorMissingSamples,
issues,
};
}
export function formatMemoryV2ShadowAuditReport(report) {
const lines = [
`memory v2 shadow audit: ${report.ok ? 'ok' : 'issues found'}`,
`window: ${new Date(report.window.sinceMs).toISOString()} -> ${new Date(report.window.untilMs).toISOString()}`,
`candidates: ${report.window.candidateCount}`,
`memory items updated: ${report.window.memoryItemCount}`,
`agent_memory_resolved events: ${report.window.agentMemoryResolvedEvents}`,
'',
'summary:',
JSON.stringify(report.summary, null, 2),
];
if (report.topPolicyReasons.length > 0) {
lines.push('', 'top policy reasons:');
for (const item of report.topPolicyReasons) {
lines.push(`- ${item.key}: ${item.count}`);
}
}
if (report.pgvectorLagUsers.length > 0) {
lines.push('', 'pgvector lag users:');
for (const item of report.pgvectorLagUsers) {
lines.push(`- ${item.userId}: ${item.missingCount} missing`);
}
}
if (report.sampleFalseStores.length > 0) {
lines.push('', 'sample false stores:');
for (const item of report.sampleFalseStores) {
lines.push(`- [${item.code}] ${item.id} (${item.policyReason}): ${item.contentPreview}`);
}
}
if (report.issues.length > 0) {
lines.push('', 'issues:');
for (const item of report.issues) {
lines.push(`- ${item.code}: ${item.message}`);
}
}
return lines.join('\n');
}