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>
120 lines
4.2 KiB
JavaScript
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(),
|
|
});
|
|
},
|
|
};
|
|
}
|