fix(ts-oss): stop prototype keys from short-circuiting embedding lookup (#6903)

This commit is contained in:
Kartik
2026-08-14 16:56:03 +05:30
committed by GitHub
parent 696455fd62
commit 0f172c2890
2 changed files with 139 additions and 6 deletions
+14 -6
View File
@@ -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<string, any>,
): Promise<string> {
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);
@@ -0,0 +1,125 @@
/// <reference types="jest" />
/** 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);
},
);
});