From 0fe2dabceef4af689cb7d70d9d313c9bd6b0fb93 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Tue, 18 Aug 2026 08:14:07 -0700 Subject: [PATCH] feat(sdk-playground): reflect SDK-owned memory block in debug view Update the playground to display the current deduplicated replacement block produced by the SDK middleware instead of a browser-side seen-facts delta. Add memory-dedupe helper and ignore local tsbuildinfo. Co-Authored-By: Claude Opus 4.8 --- apps/sdk-playground/src/lib/context-api.ts | 25 ++++--- apps/sdk-playground/src/lib/memory-dedupe.ts | 76 ++++++++++++++++++++ 2 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 apps/sdk-playground/src/lib/memory-dedupe.ts 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, + } +}