import { aggregateMemoryV2ProductMetrics, ensureMemoryV2ProductEventsSchema, } from './memory-v2-product-events.mjs'; import { runCandidateAutoReviewBatch } from './memory-v2-candidate-auto-review.mjs'; import { createPersonalMemoryCandidateStore, ensurePersonalMemoryCandidateSchema, } from './memory-v2-personal-store.mjs'; import { auditMemoryV2Shadow, parseSinceArg } from './memory-v2-shadow-audit.mjs'; async function tableExists(pool, tableName) { const [rows] = await pool.query( `SELECT 1 AS ok FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ? LIMIT 1`, [String(tableName)], ); return rows.length > 0; } export function createMemoryV2AdminOpsService(pool, { now = () => Date.now() } = {}) { if (!pool?.query) return null; let schemaReady = false; async function ensureSchema() { if (schemaReady) return; await ensureMemoryV2ProductEventsSchema(pool); await ensurePersonalMemoryCandidateSchema(pool); schemaReady = true; } return { async getProductMetrics({ since = '7d', userId = null } = {}) { await ensureSchema(); return aggregateMemoryV2ProductMetrics(pool, { since, userId, now: now() }); }, async getShadowAudit({ since = '7d', userId = null } = {}) { await ensureSchema(); const window = parseSinceArg(since, now()); const params = [window.sinceMs]; let candidateSql = `SELECT * FROM h5_memory_v2_candidates WHERE created_at >= ?`; if (userId) { candidateSql += ' AND user_id = ?'; params.push(String(userId)); } candidateSql += ' ORDER BY created_at DESC LIMIT 5000'; const [candidates] = await pool.query(candidateSql, params); const itemParams = [window.sinceMs]; let itemSql = `SELECT * FROM h5_user_memory_items WHERE updated_at >= ?`; if (userId) { itemSql += ' AND user_id = ?'; itemParams.push(String(userId)); } itemSql += ' ORDER BY updated_at DESC LIMIT 5000'; const [memoryItems] = (await tableExists(pool, 'h5_user_memory_items')) ? await pool.query(itemSql, itemParams) : [[]]; const eventParams = [window.sinceMs]; let eventSql = `SELECT e.event_type, e.data_json, e.created_at FROM h5_agent_run_events e WHERE e.event_type = 'agent_memory_resolved' AND e.created_at >= ?`; if (userId) { eventSql = `SELECT e.event_type, e.data_json, e.created_at FROM h5_agent_run_events e INNER JOIN h5_agent_runs r ON r.id = e.run_id WHERE e.event_type = 'agent_memory_resolved' AND e.created_at >= ? AND r.user_id = ?`; eventParams.push(String(userId)); } eventSql += ' ORDER BY e.created_at DESC LIMIT 5000'; const [agentMemoryEvents] = (await tableExists(pool, 'h5_agent_run_events')) ? await pool.query(eventSql, eventParams) : [[]]; return auditMemoryV2Shadow({ candidates, memoryItems, pgvectorMemoryIds: new Set(memoryItems.map((row) => String(row.id))), pgvectorConfigured: false, agentMemoryEvents, sinceMs: window.sinceMs, nowMs: now(), }); }, async listCandidates({ status = 'candidate', userId = null, limit = 50, offset = 0 } = {}) { await ensureSchema(); const store = createPersonalMemoryCandidateStore(pool, { now }); return store.listCandidates({ status, userId, limit, offset }); }, async countCandidatesByStatus() { await ensureSchema(); const store = createPersonalMemoryCandidateStore(pool, { now }); return store.countByStatus(); }, async reviewCandidate(id, status, { reviewedBy = null } = {}) { await ensureSchema(); const store = createPersonalMemoryCandidateStore(pool, { now }); return store.reviewCandidate(id, status, { reviewedBy }); }, async runAutoReview({ userId = null, limit = 200 } = {}) { await ensureSchema(); return runCandidateAutoReviewBatch(pool, { userId, limit, reviewedBy: 'system:admin-auto-review', now: now(), }); }, }; }