Files
memind/scripts/repair-memory-v2-candidates.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

191 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
import mysql from 'mysql2/promise';
import { pathToFileURL } from 'node:url';
import { detectFalseStoreCandidate } from '../memory-v2-shadow-audit.mjs';
import { inspectMemoryContentDurability } from '../memory-v2-personal-shadow.mjs';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
function usage() {
return [
'Usage: node scripts/repair-memory-v2-candidates.mjs [--apply] [--user-id <id>] [--limit <n>] [--json]',
'',
'Finds low-quality accepted/candidate rows and rejects them.',
'Default mode is dry-run. --apply performs UPDATE status=rejected.',
].join('\n');
}
function parseArgs(argv) {
const args = {
apply: false,
userId: '',
limit: 500,
json: false,
};
for (let i = 2; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--apply') args.apply = true;
else if (arg === '--user-id' && argv[i + 1]) args.userId = argv[++i];
else if (arg === '--limit' && argv[i + 1]) {
args.limit = Math.max(1, Math.min(5000, Number(argv[++i]) || 500));
} else if (arg === '--json') args.json = true;
else if (arg === '--help' || arg === '-h') {
console.log(usage());
process.exit(0);
} else {
console.error(`Unknown argument: ${arg}`);
console.error(usage());
process.exit(2);
}
}
return args;
}
function createPoolFromEnv() {
loadMemindEnvFiles(process.cwd());
const poolOptions = { connectionLimit: 3 };
if (process.env.DATABASE_URL) {
return mysql.createPool({ uri: process.env.DATABASE_URL, ...poolOptions });
}
return mysql.createPool({
host: process.env.MYSQL_HOST ?? 'localhost',
port: Number(process.env.MYSQL_PORT ?? 3306),
user: process.env.MYSQL_USER ?? 'boot',
password: process.env.MYSQL_PASSWORD ?? '',
database: process.env.MYSQL_DATABASE ?? 'tkmind',
...poolOptions,
});
}
export function evaluateRepairCandidate(row) {
const content = String(row.content ?? '');
const durabilityReason = inspectMemoryContentDurability(content);
if (durabilityReason) {
return { repair: true, code: durabilityReason, message: `Non-durable content (${durabilityReason})` };
}
const falseStore = detectFalseStoreCandidate(content);
if (falseStore.suspicious) {
return { repair: true, code: falseStore.code, message: falseStore.message };
}
if (String(row.policy_reason ?? '') === 'decision_signal' && /(?:public\/|page data|durability-)/iu.test(content)) {
return { repair: true, code: 'decision_signal_task_leak', message: 'Decision signal on agent task content' };
}
return { repair: false };
}
async function loadCandidates(pool, { userId, limit }) {
const clauses = ["status IN ('candidate', 'accepted')"];
const params = [];
if (userId) {
clauses.push('user_id = ?');
params.push(userId);
}
params.push(limit);
const [rows] = await pool.query(
`SELECT id, user_id, session_id, memory_type, content, status, policy_reason,
confidence, importance, created_at, updated_at
FROM h5_memory_v2_candidates
WHERE ${clauses.join(' AND ')}
ORDER BY updated_at DESC
LIMIT ?`,
params,
);
return rows;
}
async function applyRepairs(pool, repairs, nowMs) {
let updated = 0;
for (const item of repairs) {
const [result] = await pool.query(
`UPDATE h5_memory_v2_candidates
SET status = 'rejected',
reviewed_by = 'system:repair',
reviewed_at = ?,
updated_at = ?
WHERE id = ?
AND status IN ('candidate', 'accepted')`,
[nowMs, nowMs, item.id],
);
updated += Number(result?.affectedRows ?? 0);
}
return updated;
}
function printTextReport(report) {
console.log(`memory v2 candidate repair: ${report.apply ? 'apply' : 'dry-run'}`);
console.log(`scanned: ${report.scanned}`);
console.log(`repairable: ${report.repairable}`);
if (report.repairs.length > 0) {
for (const item of report.repairs.slice(0, 20)) {
console.log(`- [${item.code}] ${item.id} (${item.status}/${item.policyReason}): ${item.contentPreview}`);
}
if (report.repairs.length > 20) {
console.log(`... ${report.repairs.length - 20} more`);
}
}
if (report.apply) {
console.log(`updated: ${report.updated}`);
}
}
export async function runMemoryV2CandidateRepair(args, { pool: injectedPool, nowMs = Date.now() } = {}) {
const ownsPool = !injectedPool;
const activePool = injectedPool ?? createPoolFromEnv();
try {
const rows = await loadCandidates(activePool, args);
const repairs = rows
.map((row) => {
const verdict = evaluateRepairCandidate(row);
if (!verdict.repair) return null;
return {
id: String(row.id),
userId: String(row.user_id),
status: String(row.status),
policyReason: String(row.policy_reason ?? ''),
code: verdict.code,
message: verdict.message,
contentPreview: String(row.content ?? '').slice(0, 120),
};
})
.filter(Boolean);
const report = {
ok: true,
apply: args.apply,
scanned: rows.length,
repairable: repairs.length,
repairs,
updated: 0,
};
if (args.apply && repairs.length > 0) {
report.updated = await applyRepairs(activePool, repairs, nowMs);
}
return report;
} finally {
if (ownsPool) {
await activePool.end();
}
}
}
async function main() {
const args = parseArgs(process.argv);
try {
const report = await runMemoryV2CandidateRepair(args);
if (args.json) {
console.log(JSON.stringify(report, null, 2));
} else {
printTextReport(report);
}
process.exit(0);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`repair failed: ${message}`);
process.exit(1);
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
void main();
}