#!/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 ] [--limit ] [--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(); }