Files
memind/memory-v2-admin-ops.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

120 lines
4.2 KiB
JavaScript

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(),
});
},
};
}