From 0516f19f721d1b0bbf9171410031cd28fd1c19ac Mon Sep 17 00:00:00 2001
From: Shashidhar Kundur <106020668+shashiKundur1@users.noreply.github.com>
Date: Wed, 7 Oct 2026 15:53:25 +0530
Subject: [PATCH] fix(ts-oss): stop add() from mutating the caller's filters
object (#6797)
---
mem0-ts/src/oss/src/memory/index.ts | 4 +-
.../memory.add-filters-isolation.test.ts | 115 ++++++++++++++++++
2 files changed, 118 insertions(+), 1 deletion(-)
create mode 100644 mem0-ts/src/oss/tests/memory.add-filters-isolation.test.ts
diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts
index 1efbc3e45..42f3ca22e 100644
--- a/mem0-ts/src/oss/src/memory/index.ts
+++ b/mem0-ts/src/oss/src/memory/index.ts
@@ -768,7 +768,9 @@ export class Memory {
has_filters: !!config.filters,
infer: config.infer,
});
- const { filters = {}, infer = true } = config;
+ // Copy: scope keys are written into this object below, and the caller's next add() must not inherit them (#6796).
+ const filters: SearchFilters = { ...(config.filters ?? {}) };
+ const { infer = true } = config;
const metadata = stripIdentityKeys(config.metadata);
// Validate and trim entity IDs
diff --git a/mem0-ts/src/oss/tests/memory.add-filters-isolation.test.ts b/mem0-ts/src/oss/tests/memory.add-filters-isolation.test.ts
new file mode 100644
index 000000000..a23926e41
--- /dev/null
+++ b/mem0-ts/src/oss/tests/memory.add-filters-isolation.test.ts
@@ -0,0 +1,115 @@
+///
+/**
+ * add() writes the resolved scope (user_id / agent_id / run_id) into its
+ * filters object. If that object is the caller's, the scope survives the call
+ * and leaks into the next add() that reuses it, which also defeats the
+ * "one of userId, agentId or runId is required" guard.
+ *
+ * search() and getAll() already build a fresh object from config.filters.
+ */
+jest.mock("../src/utils/telemetry", () => ({
+ captureClientEvent: jest.fn().mockResolvedValue(undefined),
+ isTelemetryEnabled: jest.fn(() => false),
+}));
+
+import { Memory } from "../src/memory";
+
+const DIM = 1536;
+let seq = 0;
+
+function createMemory(): Memory {
+ const m = new Memory({
+ disableHistory: true,
+ vectorStore: {
+ provider: "memory",
+ config: {
+ collectionName: `test-add-filters-${seq++}`,
+ dimension: DIM,
+ dbPath: ":memory:",
+ },
+ },
+ historyDbPath: ":memory:",
+ embedder: { provider: "openai", config: { apiKey: "test-key" } },
+ llm: { provider: "openai", config: { apiKey: "test-key" } },
+ } as any);
+
+ // infer:false is enough to exercise the scope handling, so the LLM is never
+ // called; the embedder just has to return a well-formed vector.
+ (m as any).embedder = {
+ embed: async () => new Array(DIM).fill(0.1),
+ embedBatch: async (texts: string[]) =>
+ texts.map(() => new Array(DIM).fill(0.1)),
+ };
+ return m;
+}
+
+describe("add() does not mutate the caller's filters object", () => {
+ it("leaves the caller's object untouched", async () => {
+ const memory = createMemory();
+ const filters: Record = {};
+
+ await memory.add([{ role: "user", content: "alice note" }], {
+ userId: "alice",
+ infer: false,
+ filters,
+ });
+
+ expect(filters).toEqual({});
+ });
+
+ it("still throws when a reused object is the only source of scope", async () => {
+ const memory = createMemory();
+ const filters: Record = {};
+
+ await memory.add([{ role: "user", content: "alice note" }], {
+ userId: "alice",
+ infer: false,
+ filters,
+ });
+
+ // No userId/agentId/runId on this call, so the required-scope guard must
+ // fire rather than reading a value left behind by the previous call.
+ await expect(
+ memory.add([{ role: "user", content: "bob note" }], {
+ infer: false,
+ filters,
+ }),
+ ).rejects.toThrow(
+ "One of the filters: userId, agentId or runId is required!",
+ );
+ });
+
+ it("does not store one user's memory under another user's scope", async () => {
+ const memory = createMemory();
+ const filters: Record = {};
+
+ await memory.add([{ role: "user", content: "alice note" }], {
+ userId: "alice",
+ infer: false,
+ filters,
+ });
+ // A second caller reuses the object and supplies no scope of its own. This
+ // must not be silently attributed to alice.
+ await memory
+ .add([{ role: "user", content: "bob note" }], {
+ infer: false,
+ filters,
+ })
+ .catch(() => undefined);
+
+ const alice = await memory.getAll({ filters: { user_id: "alice" } });
+ expect(alice.results.map((r) => r.memory)).toEqual(["alice note"]);
+ });
+
+ it("keeps scope keys the caller passed in", async () => {
+ const memory = createMemory();
+
+ await memory.add([{ role: "user", content: "scoped note" }], {
+ infer: false,
+ filters: { user_id: "carol" },
+ });
+
+ const carol = await memory.getAll({ filters: { user_id: "carol" } });
+ expect(carol.results.map((r) => r.memory)).toEqual(["scoped note"]);
+ });
+});