Add User Model Service and Temporal Recall for MeMind V0.1.
Introduce UMS ingest/snapshot pipeline, Context Planner with multi-source recall, runtime context injection, canonical user mapping, and session snapshot loading on auth/me. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
function newId() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
function nowMs() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function validateEnvelope(item) {
|
||||
const required = [
|
||||
'evidence_id',
|
||||
'user_id',
|
||||
'source_type',
|
||||
'source_ref',
|
||||
'occurred_at',
|
||||
'evidence_type',
|
||||
'payload',
|
||||
'content_hash',
|
||||
'schema_version',
|
||||
];
|
||||
for (const key of required) {
|
||||
if (item[key] === undefined || item[key] === null || item[key] === '') {
|
||||
return { ok: false, reason: `missing ${key}` };
|
||||
}
|
||||
}
|
||||
if (item.schema_version !== 1) return { ok: false, reason: 'unsupported schema_version' };
|
||||
if (item.privacy_level === 'secure_skip') return { ok: false, reason: 'secure_skip' };
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('mysql2/promise').Pool} pool
|
||||
* @param {{ items: object[], dry_run?: boolean }} input
|
||||
*/
|
||||
export async function ingestEvidenceBatch(pool, { items, dry_run = false }) {
|
||||
const accepted = [];
|
||||
const duplicates = [];
|
||||
const rejected = [];
|
||||
|
||||
for (const item of items ?? []) {
|
||||
const check = validateEnvelope(item);
|
||||
if (!check.ok) {
|
||||
rejected.push({ evidence_id: item.evidence_id ?? null, reason: check.reason });
|
||||
continue;
|
||||
}
|
||||
|
||||
const [existing] = await pool.query(
|
||||
`SELECT evidence_id, content_hash FROM um_evidence WHERE user_id = ? AND content_hash = ? LIMIT 1`,
|
||||
[item.user_id, item.content_hash],
|
||||
);
|
||||
if (existing[0]) {
|
||||
duplicates.push(existing[0].evidence_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [byId] = await pool.query(`SELECT content_hash FROM um_evidence WHERE evidence_id = ? LIMIT 1`, [
|
||||
item.evidence_id,
|
||||
]);
|
||||
if (byId[0] && byId[0].content_hash !== item.content_hash) {
|
||||
rejected.push({ evidence_id: item.evidence_id, reason: 'evidence_conflict' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dry_run) {
|
||||
accepted.push(item.evidence_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO um_evidence
|
||||
(evidence_id, user_id, source_type, source_ref, evidence_type, occurred_at, received_at,
|
||||
content_hash, schema_version, privacy_level, payload_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW(3), ?, ?, ?, ?)`,
|
||||
[
|
||||
item.evidence_id,
|
||||
item.user_id,
|
||||
item.source_type,
|
||||
item.source_ref,
|
||||
item.evidence_type,
|
||||
item.occurred_at.replace('T', ' ').replace('Z', '').slice(0, 23),
|
||||
item.content_hash,
|
||||
item.schema_version,
|
||||
item.privacy_level ?? 'normal',
|
||||
JSON.stringify(item.payload),
|
||||
],
|
||||
);
|
||||
accepted.push(item.evidence_id);
|
||||
}
|
||||
|
||||
return { accepted, duplicates, rejected };
|
||||
}
|
||||
|
||||
export async function updateIngestCursor(pool, userId, sourceType, cursorValue) {
|
||||
await pool.query(
|
||||
`INSERT INTO um_ingest_cursors (user_id, source_type, cursor_value, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE cursor_value = VALUES(cursor_value), updated_at = VALUES(updated_at)`,
|
||||
[userId, sourceType, cursorValue, nowMs()],
|
||||
);
|
||||
}
|
||||
|
||||
export { validateEnvelope };
|
||||
Reference in New Issue
Block a user