diff --git a/apps/sdk-playground/src/lib/context-api.ts b/apps/sdk-playground/src/lib/context-api.ts index 88cf1a50..39facb89 100644 --- a/apps/sdk-playground/src/lib/context-api.ts +++ b/apps/sdk-playground/src/lib/context-api.ts @@ -3,6 +3,7 @@ import { type MiddlewareRuntimeConfig, normalizeMiddlewareConfig, } from "./middleware-config" +import { dedupeProfileForMode } from "./memory-dedupe" export interface MemoryDebugEntry { type: @@ -91,17 +92,6 @@ function summarizeProfile(profile: ContainerContext["profile"]) { } } -function selectProfileForMode( - profile: ContainerContext["profile"], - mode: "profile" | "query" | "full", -): ContainerContext["profile"] { - return { - static: mode === "query" ? [] : profile.static, - dynamic: mode === "query" ? [] : profile.dynamic, - searchResults: mode === "profile" ? [] : profile.searchResults, - } -} - function buildContextPreview( profile: ContainerContext["profile"], mode: "profile" | "query" | "full", @@ -182,7 +172,10 @@ export async function fetchContainerContext( if (!apiKey) throw new Error("Supermemory API key is required") const client = getSupermemoryClient(apiKey) - const profile = await fetchProfileContext(client, containerTag, query) + const profile = dedupeProfileForMode( + query ? "full" : "profile", + await fetchProfileContext(client, containerTag, query), + ) const docsResponse = await client.post<{ documents?: unknown[] @@ -246,7 +239,7 @@ export async function buildMiddlewareMemoryDebug( query, signal, ) - const selectedProfile = selectProfileForMode(profile, memoryMode) + const selectedProfile = dedupeProfileForMode(memoryMode, profile) const summary = summarizeProfile(selectedProfile) return [ @@ -276,6 +269,12 @@ export async function buildMiddlewareMemoryDebug( type: "context_preview", label: "Reconstructed context preview (not middleware capture)", preview: buildContextPreview(selectedProfile, memoryMode, query), + detail: { + totalFacts: + summary.staticCount + + summary.dynamicCount + + summary.searchResultCount, + }, }, config.addMemory === "always" ? { diff --git a/apps/sdk-playground/src/lib/memory-dedupe.ts b/apps/sdk-playground/src/lib/memory-dedupe.ts new file mode 100644 index 00000000..a4ac7d8f --- /dev/null +++ b/apps/sdk-playground/src/lib/memory-dedupe.ts @@ -0,0 +1,76 @@ +import type { ContainerContext } from "./context-api" + +type ProfileSlice = ContainerContext["profile"] + +/** Normalize a fact for exact comparison within retrieved context. */ +export function normalizeFactKey(text: string): string { + return text + .trim() + .replace(/^\[recent\]\s*/i, "") + .replace(/^\[\d{4}-\d{2}-\d{2}\]\s*/, "") + .trim() + .replace(/\s+/g, " ") + .toLowerCase() +} + +function memoryText(item: unknown): string { + if (typeof item === "string") return item + if (item && typeof item === "object") { + const record = item as Record + if (typeof record.memory === "string") return record.memory + if (typeof record.content === "string") return record.content + if (typeof record.chunk === "string") return record.chunk + } + return "" +} + +/** + * Deduplicate static → dynamic → search (same priority as @supermemory/tools middleware). + */ +export function dedupeProfileForMode( + mode: "profile" | "query" | "full", + profile: ProfileSlice, +): ProfileSlice { + const injectsProfile = mode !== "query" + const staticItems = injectsProfile ? profile.static : [] + const dynamicItems = injectsProfile ? profile.dynamic : [] + const searchItems = profile.searchResults + + const seen = new Set() + const staticOut: unknown[] = [] + const dynamicOut: unknown[] = [] + const searchOut: unknown[] = [] + + for (const item of staticItems) { + const text = memoryText(item).trim() + if (!text) continue + const key = normalizeFactKey(text) + if (!key || seen.has(key)) continue + seen.add(key) + staticOut.push(item) + } + + for (const item of dynamicItems) { + const text = memoryText(item).trim() + if (!text) continue + const key = normalizeFactKey(text) + if (!key || seen.has(key)) continue + seen.add(key) + dynamicOut.push(item) + } + + for (const item of searchItems) { + const text = memoryText(item).trim() + if (!text) continue + const key = normalizeFactKey(text) + if (!key || seen.has(key)) continue + seen.add(key) + searchOut.push(item) + } + + return { + static: staticOut, + dynamic: dynamicOut, + searchResults: mode === "profile" ? [] : searchOut, + } +}