#!/usr/bin/env node import fs from 'node:fs/promises'; import path from 'node:path'; import mysql from 'mysql2/promise'; import { auditMemoryV2Shadow, formatMemoryV2ShadowAuditReport, parseSinceArg, } from '../memory-v2-shadow-audit.mjs'; import { loadMemindEnvFiles } from './memind-runtime-profile.mjs'; function usage() { return [ 'Usage: node scripts/audit-memory-v2-shadow.mjs [--since 7d] [--user-id ] [--output ] [--json]', '', 'Audits Memory V2 shadow/canary data quality:', '- candidate status and policy reason distribution', '- suspicious false-store samples', '- active memories missing from pgvector', '- agent_memory_resolved hit rate', ].join('\n'); } function parseArgs(argv) { const args = { since: '7d', userId: '', output: '', json: false, }; for (let i = 2; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--since' && argv[i + 1]) { args.since = argv[++i]; } else if (arg === '--user-id' && argv[i + 1]) { args.userId = argv[++i]; } else if (arg === '--output' && argv[i + 1]) { args.output = argv[++i]; } 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 createMysqlPoolFromEnv() { loadMemindEnvFiles(process.cwd()); const poolOptions = { connectionLimit: 3 }; if (process.env.DATABASE_URL) { return mysql.createPool({ uri: process.env.DATABASE_URL, ...poolOptions, }); } if (!process.env.MYSQL_HOST && !process.env.MYSQL_DATABASE) { throw new Error('Memory V2 shadow audit requires DATABASE_URL or MYSQL_* configuration'); } 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, }); } async function createPgPoolFromEnv() { const connectionString = String(process.env.MEMORY_PGVECTOR_DATABASE_URL ?? '').trim(); if (!connectionString) return null; const imported = await import('pg'); const PgPool = imported?.Pool ?? imported?.default?.Pool; if (typeof PgPool !== 'function') { throw new Error('pg module does not export Pool'); } return new PgPool({ connectionString, max: Math.max(1, Number(process.env.MEMORY_PGVECTOR_POOL_MAX ?? 3) || 3), }); } function isSafeIdentifier(value) { return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(String(value ?? '')); } async function tableExists(pool, tableName) { const [rows] = await pool.query( `SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1`, [tableName], ); return rows.length > 0; } async function loadCandidates(pool, { sinceMs, userId }) { if (!(await tableExists(pool, 'h5_memory_v2_candidates'))) { return { rows: [], tableMissing: true }; } const clauses = ['created_at >= ?']; const params = [sinceMs]; if (userId) { clauses.push('user_id = ?'); params.push(userId); } 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 created_at DESC LIMIT 5000`, params, ); return { rows, tableMissing: false }; } async function loadMemoryItems(pool, { sinceMs, userId }) { const clauses = ['updated_at >= ?']; const params = [sinceMs]; if (userId) { clauses.push('user_id = ?'); params.push(userId); } const [rows] = await pool.query( `SELECT id, user_id, label, memory_text, status, created_at, updated_at FROM h5_user_memory_items WHERE ${clauses.join(' AND ')} ORDER BY updated_at DESC LIMIT 5000`, params, ); const activeClauses = ["status = 'active'"]; const activeParams = []; if (userId) { activeClauses.push('user_id = ?'); activeParams.push(userId); } const [activeRows] = await pool.query( `SELECT id, user_id, label, memory_text, status, created_at, updated_at FROM h5_user_memory_items WHERE ${activeClauses.join(' AND ')} ORDER BY updated_at DESC LIMIT 10000`, activeParams, ); return { updatedRows: rows, activeRows, }; } async function loadAgentMemoryEvents(pool, { sinceMs, userId }) { const clauses = ["e.event_type = 'agent_memory_resolved'", 'e.created_at >= ?']; const params = [sinceMs]; if (userId) { clauses.push('r.user_id = ?'); params.push(userId); } const [rows] = await pool.query( `SELECT e.event_type, e.data_json, e.created_at, r.user_id FROM h5_agent_run_events e INNER JOIN h5_agent_runs r ON r.id = e.run_id WHERE ${clauses.join(' AND ')} ORDER BY e.created_at DESC LIMIT 5000`, params, ); return rows; } async function loadPgvectorMemoryIds(pgPool, { userId, tableName }) { if (!pgPool) return new Set(); if (!isSafeIdentifier(tableName)) { throw new Error(`Invalid pgvector table name: ${tableName}`); } const clauses = ['source_memory_id IS NOT NULL']; const params = []; if (userId) { clauses.push('user_id = $1'); params.push(userId); } try { const result = await pgPool.query( `SELECT source_memory_id FROM "${tableName}" WHERE ${clauses.join(' AND ')}`, params, ); return new Set( result.rows .map((row) => String(row.source_memory_id ?? '').trim()) .filter(Boolean), ); } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new Error(`Failed to query pgvector table "${tableName}": ${message}`); } } async function writeOutput(outputPath, payload) { if (!outputPath) return; const resolved = path.resolve(outputPath); await fs.mkdir(path.dirname(resolved), { recursive: true }); await fs.writeFile(resolved, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); } const args = parseArgs(process.argv); const nowMs = Date.now(); const { sinceMs, label: sinceLabel } = parseSinceArg(args.since, nowMs); const mysqlPool = createMysqlPoolFromEnv(); let pgPool = null; try { pgPool = await createPgPoolFromEnv(); const [candidateResult, memoryResult, agentMemoryEvents] = await Promise.all([ loadCandidates(mysqlPool, { sinceMs, userId: args.userId }), loadMemoryItems(mysqlPool, { sinceMs, userId: args.userId }), loadAgentMemoryEvents(mysqlPool, { sinceMs, userId: args.userId }), ]); const pgvectorTable = String(process.env.MEMORY_PGVECTOR_TABLE ?? 'memory_embeddings').trim() || 'memory_embeddings'; const pgvectorMemoryIds = await loadPgvectorMemoryIds(pgPool, { userId: args.userId, tableName: pgvectorTable, }); const report = auditMemoryV2Shadow({ candidates: candidateResult.rows, memoryItems: memoryResult.activeRows, pgvectorMemoryIds, pgvectorConfigured: Boolean(pgPool), agentMemoryEvents, sinceMs, nowMs, }); const payload = { ...report, meta: { since: sinceLabel, userId: args.userId || null, candidateTableMissing: candidateResult.tableMissing, pgvectorConfigured: Boolean(pgPool), pgvectorTable, }, }; await writeOutput(args.output, payload); if (args.json) { console.log(JSON.stringify(payload, null, 2)); } else { console.log(formatMemoryV2ShadowAuditReport(payload)); if (candidateResult.tableMissing) { console.log('\nwarning: h5_memory_v2_candidates table is missing'); } if (!pgPool) { console.log('\nwarning: MEMORY_PGVECTOR_DATABASE_URL not configured; pgvector lag check skipped'); } if (args.output) { console.log(`\nreport written: ${path.resolve(args.output)}`); } } process.exit(report.ok || !pgPool || candidateResult.tableMissing ? 0 : 1); } finally { await mysqlPool.end(); await pgPool?.end?.(); }