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

69 lines
2.1 KiB
JavaScript

function normalizeTitle(text) {
return String(text ?? '')
.toLowerCase()
.replace(/\s+/g, '')
.slice(0, 64);
}
function overlapScore(a, b) {
const ta = normalizeTitle(a);
const tb = normalizeTitle(b);
if (!ta || !tb) return 0;
if (ta === tb) return 1;
if (ta.includes(tb) || tb.includes(ta)) return 0.85;
const shorter = ta.length < tb.length ? ta : tb;
const longer = ta.length < tb.length ? tb : ta;
let common = 0;
for (let i = 0; i < shorter.length; i++) {
if (longer.includes(shorter[i])) common++;
}
return common / Math.max(longer.length, 1);
}
function timeClose(a, b, windowMs = 15 * 60_000) {
const ma = a ? new Date(a).getTime() : null;
const mb = b ? new Date(b).getTime() : null;
if (ma === null || mb === null) return false;
return Math.abs(ma - mb) <= windowMs;
}
/**
* @param {object[]} items
*/
export function dedupeTimelineItems(items) {
const kept = [];
for (const item of items) {
let merged = false;
for (let i = 0; i < kept.length; i++) {
const existing = kept[i];
const titleSim = overlapScore(existing.title, item.title);
const sameDayObserved =
existing.observed_time?.slice(0, 10) === item.observed_time?.slice(0, 10);
const close =
timeClose(existing.event_time, item.event_time) ||
(sameDayObserved && titleSim > 0.65);
if (titleSim >= 0.7 && close) {
const mergedFrom = [
...(existing.merged_from ?? [existing.source_ref]),
item.source_ref,
];
kept[i] = {
...existing,
recall_score: Math.max(existing.recall_score ?? 0, item.recall_score ?? 0),
importance: Math.max(existing.importance ?? 0, item.importance ?? 0),
confidence: Math.max(existing.confidence ?? 0, item.confidence ?? 0),
merged_from: mergedFrom,
content:
(existing.content?.length ?? 0) >= (item.content?.length ?? 0)
? existing.content
: item.content,
};
merged = true;
break;
}
}
if (!merged) kept.push({ ...item });
}
return kept;
}