Files
memind/user-model-service/service.mjs
T
john 212ff3ff80 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>
2026-09-03 23:25:18 +08:00

67 lines
2.5 KiB
JavaScript

import { mergeCandidatesFromSignals } from './candidates.mjs';
import { ingestEvidenceBatch, updateIngestCursor } from './ingest.mjs';
import { extractSignalsFromEnvelope, upsertSignals } from './signals.mjs';
import { materializeProfileAndSnapshot } from './snapshot.mjs';
/**
* @param {import('mysql2/promise').Pool} pool
* @param {{ items: object[], source_type?: string, dry_run?: boolean }} input
*/
export async function processIngestBatch(pool, input) {
const ingestResult = await ingestEvidenceBatch(pool, input);
if (input.dry_run) {
return { ...ingestResult, signals_computed: 0, candidates_touched: 0, snapshot: null };
}
const userIds = new Set();
for (const id of ingestResult.accepted) {
const [rows] = await pool.query(`SELECT user_id, payload_json, evidence_type, evidence_id, occurred_at, source_type, source_ref, content_hash, schema_version, privacy_level FROM um_evidence WHERE evidence_id = ?`, [id]);
const row = rows[0];
if (!row) continue;
userIds.add(row.user_id);
const envelope = {
evidence_id: row.evidence_id,
user_id: row.user_id,
source_type: row.source_type,
source_ref: row.source_ref,
occurred_at: row.occurred_at instanceof Date ? row.occurred_at.toISOString() : row.occurred_at,
evidence_type: row.evidence_type,
payload: typeof row.payload_json === 'string' ? JSON.parse(row.payload_json) : row.payload_json,
content_hash: row.content_hash,
schema_version: row.schema_version,
privacy_level: row.privacy_level,
};
const drafts = extractSignalsFromEnvelope(envelope);
await upsertSignals(pool, row.user_id, drafts);
}
let signalsComputed = 0;
let candidatesTouched = 0;
let snapshot = null;
for (const userId of userIds) {
signalsComputed += 1;
candidatesTouched += await mergeCandidatesFromSignals(pool, userId);
snapshot = await materializeProfileAndSnapshot(pool, userId, { reason: 'ingest_batch' });
}
const sourceType = input.source_type ?? input.items?.[0]?.source_type ?? 'meinput';
const lastItem = input.items?.[input.items.length - 1];
if (lastItem?.occurred_at && userIds.size === 1) {
await updateIngestCursor(pool, [...userIds][0], sourceType, lastItem.occurred_at);
}
return {
...ingestResult,
signals_computed: signalsComputed,
candidates_touched: candidatesTouched,
snapshot: snapshot
? {
profile_version_bumped: true,
fast_revision_bumped: true,
...snapshot,
}
: { profile_version_bumped: false, fast_revision_bumped: false },
};
}