Files
memind/temporal-recall-service/recall.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

126 lines
3.8 KiB
JavaScript

import { searchCalendar } from './adapters/calendar.mjs';
import { searchChat } from './adapters/chat.mjs';
import { searchMeinput } from './adapters/meinput.mjs';
import { buildContextPlan } from './context-planner.mjs';
import { dedupeTimelineItems } from './dedupe.mjs';
import { rankTimelineItems } from './rank.mjs';
const SOURCE_HANDLERS = {
calendar: searchCalendar,
meinput: searchMeinput,
chat: searchChat,
};
/**
* @param {import('mysql2/promise').Pool | null} pool
* @param {{ plan: object, userId: string, sessionId?: string, limit?: number }} opts
*/
export async function executeTemporalRecall(pool, opts) {
const started = Date.now();
const plan = opts.plan;
const userId = opts.userId;
const limit = Math.min(200, Math.max(1, Number(opts.limit ?? plan.output?.max_items ?? 50)));
const skipBelow = 0.3;
const sourcesQueried = [];
const tasks = (plan.retrievals ?? [])
.filter((r) => (r.weight ?? 0) >= (r.skip_below ?? skipBelow))
.map(async (retrieval) => {
const handler = SOURCE_HANDLERS[retrieval.source];
if (!handler) return [];
sourcesQueried.push(retrieval.source);
const ctx = {
userId,
pool,
retrieval,
time: plan.time,
temporalMode: plan.temporal_mode,
sessionId: opts.sessionId,
};
if (retrieval.source === 'chat') return searchChat(pool, ctx);
return handler(ctx);
});
const batches = await Promise.all(tasks);
const raw = batches.flat();
const ranked = rankTimelineItems(raw, plan);
const deduped = plan.output?.dedupe !== false ? dedupeTimelineItems(ranked) : ranked;
const returned = deduped.slice(0, limit);
let groups = null;
if (plan.temporal_mode === 'AMBIGUOUS' && plan.output?.timeline) {
const mentionStart = new Date(plan.time.mention_range.start).getTime();
const mentionEnd = new Date(plan.time.mention_range.end).getTime();
const occurred = [];
const mentioned = [];
for (const item of returned) {
const obs = new Date(item.observed_time).getTime();
if (item.event_time) {
const ev = new Date(item.event_time).getTime();
if (ev >= mentionStart && ev < mentionEnd) occurred.push(item);
else mentioned.push(item);
} else if (obs >= mentionStart && obs < mentionEnd) {
mentioned.push(item);
} else {
occurred.push(item);
}
}
groups = [
{ label: 'occurred_in_range', items: occurred },
{ label: 'mentioned_or_planned', items: mentioned },
];
}
return {
query_type: plan.query_type,
temporal_mode: plan.temporal_mode,
time_range: plan.time.mention_range,
groups,
items: groups ? [] : returned,
stats: {
sources_queried: sourcesQueried,
raw_count: raw.length,
deduped_count: deduped.length,
returned_count: returned.length,
elapsed_ms: Date.now() - started,
},
plan,
};
}
/**
* @param {import('mysql2/promise').Pool | null} pool
* @param {{ query?: string, plan?: object, user_id: string, now?: Date, session_id?: string, limit?: number }} input
*/
export async function queryTemporalRecall(pool, input) {
const userId = input.user_id;
const plan =
input.plan ??
buildContextPlan({
query: input.query ?? '',
user_id: userId,
now: input.now,
});
if (plan.context_needs?.temporal_recall === 'SKIP') {
return {
query_type: plan.query_type,
temporal_mode: plan.temporal_mode,
time_range: plan.time?.mention_range ?? null,
groups: null,
items: [],
stats: { sources_queried: [], raw_count: 0, deduped_count: 0, returned_count: 0, elapsed_ms: 0 },
plan,
};
}
return executeTemporalRecall(pool, {
plan,
userId,
sessionId: input.session_id,
limit: input.limit,
});
}
export { buildContextPlan };