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

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);
}