212ff3ff80
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>
88 lines
2.9 KiB
JavaScript
88 lines
2.9 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import { createScheduleService } from '../../schedule-service.mjs';
|
|
import { extractEventTime, itemMatchesTimeWindow } from '../event-time-extract.mjs';
|
|
|
|
function envEnabled(name, fallback = true) {
|
|
const raw = String(process.env[name] ?? '').trim().toLowerCase();
|
|
if (!raw) return fallback;
|
|
return ['1', 'true', 'yes', 'on'].includes(raw);
|
|
}
|
|
|
|
function newTimelineId(sourceRef) {
|
|
const hash = crypto.createHash('sha256').update(`timeline-v1|${sourceRef}`).digest('hex');
|
|
return [
|
|
hash.slice(0, 8),
|
|
hash.slice(8, 12),
|
|
`4${hash.slice(13, 16)}`,
|
|
hash.slice(16, 20),
|
|
hash.slice(20, 32),
|
|
].join('-');
|
|
}
|
|
|
|
/**
|
|
* @param {ReturnType<import('../../schedule-service.mjs').createScheduleService> extends { listItems: infer _ } ? object : never>} item
|
|
*/
|
|
export function scheduleItemToTimelineItem(item, ctx) {
|
|
const eventMs = item.startAt ?? item.dueAt ?? null;
|
|
const eventIso = eventMs != null ? new Date(eventMs).toISOString() : null;
|
|
const observedIso = new Date(item.createdAt).toISOString();
|
|
const sourceRef = `calendar:schedule:${item.id}`;
|
|
const text = [item.title, item.description].filter(Boolean).join(' — ');
|
|
const extracted = eventIso ? null : extractEventTime(text, observedIso);
|
|
|
|
const timelineItem = {
|
|
timeline_item_id: newTimelineId(sourceRef),
|
|
user_id: item.userId,
|
|
source: 'calendar',
|
|
type: item.kind === 'event' ? 'event' : 'todo',
|
|
event_time: eventIso ?? extracted?.event_time ?? null,
|
|
observed_time: observedIso,
|
|
title: String(item.title ?? '').slice(0, 80),
|
|
content: String(text).slice(0, 8192),
|
|
importance: item.kind === 'event' ? 0.9 : 0.82,
|
|
confidence: 0.98,
|
|
source_ref: sourceRef,
|
|
participants: [],
|
|
status: 'planned',
|
|
metadata: {
|
|
location: item.location ?? null,
|
|
timezone: item.timezone ?? null,
|
|
schedule_kind: item.kind,
|
|
},
|
|
};
|
|
|
|
if (ctx?.time && !itemMatchesTimeWindow(timelineItem, ctx.time, ctx.temporalMode)) {
|
|
return null;
|
|
}
|
|
return timelineItem;
|
|
}
|
|
|
|
/**
|
|
* @param {{ pool?: import('mysql2/promise').Pool, userId: string, retrieval: object, time: object, temporalMode: string }} ctx
|
|
*/
|
|
export async function searchCalendar(ctx) {
|
|
if (!envEnabled('MEMIND_TEMPORAL_RECALL_CALENDAR_ENABLED', true)) return [];
|
|
if (!ctx.pool?.query || !ctx.userId) return [];
|
|
|
|
const scheduleService = createScheduleService(ctx.pool);
|
|
const rangeStart = ctx.time?.event_range?.start ?? ctx.time?.mention_range?.start;
|
|
const rangeEnd = ctx.time?.event_range?.end ?? ctx.time?.mention_range?.end;
|
|
if (!rangeStart || !rangeEnd) return [];
|
|
|
|
const from = new Date(rangeStart).getTime();
|
|
const to = new Date(rangeEnd).getTime();
|
|
if (Number.isNaN(from) || Number.isNaN(to)) return [];
|
|
|
|
const items = await scheduleService.listItems({
|
|
userId: ctx.userId,
|
|
from,
|
|
to,
|
|
status: 'active',
|
|
limit: 100,
|
|
});
|
|
|
|
return items
|
|
.map((item) => scheduleItemToTimelineItem(item, ctx))
|
|
.filter(Boolean);
|
|
}
|