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>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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(),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user