mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-01 02:01:40 +00:00
## Stack Context
This stack moves memory deduplication **out of the playground UI and into the SDKs themselves**, so every integration injects a single, deduplicated, self-replacing memory block. Three PRs:
1. **`sdk-dedup/tools-ts`** (this PR) — TypeScript SDK core + integrations
2. `sdk-dedup/python` — Python SDKs
3. `sdk-dedup/playground` — playground debug view reflects the SDK-owned block
## What?
Move profile deduplication into the SDK middleware for the TypeScript tools package.
- Facts are normalized (strip leading `[YYYY-MM-DD]`, trim, collapse whitespace, casefold) and deduplicated in **`static > dynamic > search`** priority within a single request.
- The result is injected as one **owned `<supermemory>` block** that *replaces* the previous block instead of accumulating a new one each turn.
- Dedup is **mode-aware**: in query mode, search results are not dropped against a profile that isn't being injected.
- Deduplication is **request-local** — no global/browser `Set`. Safe for multiple users, concurrent requests, and Cloudflare Worker isolates.
Covers AI SDK, OpenAI (Chat + Responses), Mastra, and VoltAgent. New `shared/memory-context.ts` owns the block-replacement logic.
## Why?
The earlier "conversation-scoped deduplication" was only a playground browser `Set` — a UI debug affordance that did not change what the SDK sent to the model, and would have been unsafe as server-side global state. Real cross-source dedup belongs in the SDK, applied fresh per stateless model request.
## Testing
- `bun run test` in `packages/tools`: 145 passed (the one failing suite, `claude-memory.test.ts`, is a pre-existing broken import unrelated to this change).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes how system prompts and instructions are built across all TypeScript integrations; behavior is well-covered by unit tests but incorrect strip/replace logic could drop or duplicate context in production prompts.
>
> **Overview**
> Moves **cross-source memory deduplication** and **owned prompt injection** into `@supermemory/tools` so every integration sends one deduplicated memory block per request instead of growing context each turn.
>
> **Deduplication:** Facts are normalized via `normalizeMemoryFact` (strip `[YYYY-MM-DD]`, trim, collapse whitespace, lowercase) and deduplicated with **static → dynamic → search** priority. `deduplicateMemoriesForMode` keeps search hits in **query** mode when the profile is not injected.
>
> **Owned `<supermemory>` block:** New `shared/memory-context.ts` wraps memories in `<supermemory context="user-memories" readonly>`, strips stale blocks, and **replaces** prior SDK context while preserving caller system instructions. Applied in AI SDK (`injectMemoriesIntoParams`), OpenAI Chat/Responses middleware, Mastra input processor (`wrapMemoryContext`), and VoltAgent hooks.
>
> **Tests:** Unit coverage for block replacement (with-supermemory, OpenAI, VoltAgent), Mastra wrapper tag assertion, normalized dedup variants, and concurrent `containerTag` isolation.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2fa2e0d85c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
65 lines
1.8 KiB
TypeScript
65 lines
1.8 KiB
TypeScript
import type OpenAI from "openai"
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
import { withSupermemory } from "../src/openai"
|
|
|
|
describe("OpenAI middleware memory context", () => {
|
|
const originalApiKey = process.env.SUPERMEMORY_API_KEY
|
|
|
|
beforeEach(() => {
|
|
process.env.SUPERMEMORY_API_KEY = "sm_test_key"
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (originalApiKey === undefined) delete process.env.SUPERMEMORY_API_KEY
|
|
else process.env.SUPERMEMORY_API_KEY = originalApiKey
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it("replaces prior SDK context in chat system messages", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
profile: { static: [{ memory: "Fresh profile fact" }], dynamic: [] },
|
|
searchResults: { results: [] },
|
|
}),
|
|
}),
|
|
)
|
|
const originalCreate = vi.fn(() =>
|
|
Object.assign(Promise.resolve({ choices: [] }), {
|
|
asResponse: async () => new Response(),
|
|
}),
|
|
)
|
|
const client = {
|
|
chat: { completions: { create: originalCreate } },
|
|
} as unknown as OpenAI
|
|
const wrapped = withSupermemory(client, {
|
|
containerTag: "user-a",
|
|
customId: "conversation-a",
|
|
mode: "profile",
|
|
addMemory: "never",
|
|
})
|
|
|
|
await wrapped.chat.completions.create({
|
|
model: "gpt-4o-mini",
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content:
|
|
'Be helpful.\n\n<supermemory context="user-memories" readonly>\nStale profile fact\n</supermemory>',
|
|
},
|
|
{ role: "user", content: "What do you remember?" },
|
|
],
|
|
})
|
|
|
|
const forwarded = originalCreate.mock.calls[0]?.[0]
|
|
const content = String(forwarded.messages[0].content)
|
|
expect(content).toContain("Be helpful.")
|
|
expect(content).toContain("Fresh profile fact")
|
|
expect(content).not.toContain("Stale profile fact")
|
|
expect(
|
|
content.match(/<supermemory context="user-memories" readonly>/g),
|
|
).toHaveLength(1)
|
|
})
|
|
})
|