6f3e53a56a
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>
191 lines
5.8 KiB
JavaScript
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();
|
|
}
|