From 0f172c2890ce5b9eab1cd4c15a0a5049c77f74fe Mon Sep 17 00:00:00 2001 From: Kartik Date: Fri, 14 Aug 2026 16:56:03 +0530 Subject: [PATCH] fix(ts-oss): stop prototype keys from short-circuiting embedding lookup (#6903) --- mem0-ts/src/oss/src/memory/index.ts | 20 ++- .../memory.prototype-key-embedding.test.ts | 125 ++++++++++++++++++ 2 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 mem0-ts/src/oss/tests/memory.prototype-key-embedding.test.ts diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts index 5cef1c256..58634cb55 100644 --- a/mem0-ts/src/oss/src/memory/index.ts +++ b/mem0-ts/src/oss/src/memory/index.ts @@ -1000,7 +1000,8 @@ export class Memory { for (const mem of extractedMemories) { const text = mem.text; - if (!text || !(text in embedMap)) continue; + if (!text || !Object.prototype.hasOwnProperty.call(embedMap, text)) + continue; const memHash = createHash("md5").update(text).digest("hex"); if (existingHashes.has(memHash) || seenHashes.has(memHash)) { @@ -1943,8 +1944,12 @@ export class Memory { metadata: Record, ): Promise { const memoryId = uuidv4(); - const embedding = - existingEmbeddings[data] || (await this.embedder.embed(data, "add")); + const embedding = Object.prototype.hasOwnProperty.call( + existingEmbeddings, + data, + ) + ? existingEmbeddings[data] + : await this.embedder.embed(data, "add"); const memoryMetadata = { ...metadata, @@ -1987,9 +1992,12 @@ export class Memory { } const textChanged = newData !== prevValue; - const embedding = - existingEmbeddings[newData] || - (await this.embedder.embed(newData, "update")); + const embedding = Object.prototype.hasOwnProperty.call( + existingEmbeddings, + newData, + ) + ? existingEmbeddings[newData] + : await this.embedder.embed(newData, "update"); const sanitizedMetadata = stripIdentityKeys(metadata); diff --git a/mem0-ts/src/oss/tests/memory.prototype-key-embedding.test.ts b/mem0-ts/src/oss/tests/memory.prototype-key-embedding.test.ts new file mode 100644 index 000000000..b96588f51 --- /dev/null +++ b/mem0-ts/src/oss/tests/memory.prototype-key-embedding.test.ts @@ -0,0 +1,125 @@ +/// +/** add() with infer=false must embed Object.prototype-colliding text instead of resolving it off the prototype chain. */ +import { Memory } from "../src/memory"; +import { MemoryVectorStore } from "../src/vector_stores/memory"; +import type { SearchResult } from "../src/types"; + +const mockEmbedding = new Array(1536).fill(0.1); +const mockEmbed = jest.fn().mockResolvedValue(mockEmbedding); + +jest.mock("../src/embeddings/openai", () => ({ + OpenAIEmbedder: jest.fn().mockImplementation(() => ({ + embed: mockEmbed, + embedBatch: jest.fn(), + embeddingDims: 1536, + })), +})); + +function createMemory(): Memory { + return new Memory({ + version: "v1.1", + embedder: { + provider: "openai", + config: { apiKey: "test-key", model: "text-embedding-3-small" }, + }, + vectorStore: { + provider: "memory", + config: { + collectionName: `test-proto-${Date.now()}-${Math.random()}`, + dimension: 1536, + dbPath: ":memory:", + }, + }, + llm: { + provider: "openai", + config: { apiKey: "test-key", model: "gpt-5-mini" }, + }, + historyDbPath: ":memory:", + }); +} + +describe("add() with infer=false and Object.prototype-colliding text", () => { + let memory: Memory; + let insertSpy: jest.SpyInstance; + + beforeEach(() => { + memory = createMemory(); + mockEmbed.mockClear(); + insertSpy = jest.spyOn(MemoryVectorStore.prototype, "insert"); + }); + + afterEach(async () => { + insertSpy.mockRestore(); + await memory.reset(); + }); + + test.each([ + "constructor", + "toString", + "valueOf", + "hasOwnProperty", + "__proto__", + ])( + 'embeds "%s" instead of resolving it off Object.prototype', + async (text) => { + const result: SearchResult = await memory.add(text, { + userId: "u1", + infer: false, + }); + + expect(mockEmbed).toHaveBeenCalledWith(text, "add"); + + const [storedVectors] = + insertSpy.mock.calls[insertSpy.mock.calls.length - 1]; + expect(Array.isArray(storedVectors[0])).toBe(true); + expect(storedVectors[0]).toEqual(mockEmbedding); + expect( + storedVectors[0].every((n: unknown) => typeof n === "number"), + ).toBe(true); + + expect(result.results[0].memory).toBe(text); + }, + ); +}); + +describe("update() metadata-only on Object.prototype-colliding text (#6323)", () => { + let memory: Memory; + let updateSpy: jest.SpyInstance; + + beforeEach(() => { + memory = createMemory(); + updateSpy = jest.spyOn(MemoryVectorStore.prototype, "update"); + }); + + afterEach(async () => { + updateSpy.mockRestore(); + await memory.reset(); + }); + + test.each([ + "constructor", + "toString", + "valueOf", + "hasOwnProperty", + "__proto__", + ])( + 're-embeds "%s" on a metadata-only update instead of resolving it off Object.prototype', + async (text) => { + const added: SearchResult = await memory.add(text, { + userId: "u1", + infer: false, + }); + const memoryId = added.results[0].id!; + mockEmbed.mockClear(); + + await memory.update(memoryId, { metadata: { pinned: true } }); + + expect(mockEmbed).toHaveBeenCalledWith(text, "update"); + + const [, storedVector] = + updateSpy.mock.calls[updateSpy.mock.calls.length - 1]; + expect(Array.isArray(storedVector)).toBe(true); + expect(storedVector).toEqual(mockEmbedding); + }, + ); +});