Files
memind/memory-v2-shadow-audit.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

279 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
}