From c2c3a12838733b92995eedbd6c20f604f099d429 Mon Sep 17 00:00:00 2001 From: shafdev <96260000+shafdev@users.noreply.github.com> Date: Sat, 1 Aug 2026 02:25:19 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20delete=5Fall=20now=20drains=20all=20page?= =?UTF-8?q?s,=20not=20just=20the=20first=20batch=20(close=E2=80=A6=20(#487?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: kartik-mem0 --- mem0-ts/src/oss/src/memory/index.ts | 35 +++++++++-- mem0-ts/src/oss/tests/memory.crud.test.ts | 60 +++++++++++++++++++ .../src/oss/tests/memory.validation.test.ts | 5 +- .../oss/tests/vector-stores-compat.test.ts | 28 +++++---- 4 files changed, 109 insertions(+), 19 deletions(-) diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts index d21951bb3..8a6c2cf24 100644 --- a/mem0-ts/src/oss/src/memory/index.ts +++ b/mem0-ts/src/oss/src/memory/index.ts @@ -105,6 +105,10 @@ const ENTITY_PARAMS = [ // in both casings (the default store promotes camelCase on read); actor_id has no camelCase alias. const IDENTITY_KEYS = [...ENTITY_PARAMS, "actor_id"]; +// Batch size for deleteAll pagination. Larger than most vector store default +// page limits (~100) to minimize roundtrips while bounded to avoid memory pressure. +const DELETE_ALL_BATCH_SIZE = 1000; + /** * Validates that no top-level entity parameters are passed in config. * @throws Error if entity params are found at top level @@ -1727,18 +1731,39 @@ export class Memory { ); } - const [memories] = await this.vectorStore.list(filters); - for (const memory of memories) { - await this.deleteMemory(memory.id); + let deletedCount = 0; + const seenBatches = new Set(); + + while (true) { + const [batch] = await this.vectorStore.list( + filters, + DELETE_ALL_BATCH_SIZE, + ); + if (!batch.length) { + break; + } + const batchKey = batch + .map((memory) => String(memory.id)) + .sort() + .join(","); + if (seenBatches.has(batchKey)) { + logger.warn("Stopping deleteAll after a repeated memory batch"); + break; + } + seenBatches.add(batchKey); + for (const memory of batch) { + await this.deleteMemory(memory.id); + } + deletedCount += batch.length; } const result = { message: "Memories deleted successfully!" }; - if (memories.length > 0) { + if (deletedCount > 0) { await this._displayDecayUsageNotice({ triggerFunction: "delete_all", triggerSource: "delete_all", triggerReason: "bulk_delete", - deletedCount: memories.length, + deletedCount, }); } else { await this._displayFirstRunNotice("delete_all"); diff --git a/mem0-ts/src/oss/tests/memory.crud.test.ts b/mem0-ts/src/oss/tests/memory.crud.test.ts index d33776730..f3f5f5799 100644 --- a/mem0-ts/src/oss/tests/memory.crud.test.ts +++ b/mem0-ts/src/oss/tests/memory.crud.test.ts @@ -649,6 +649,66 @@ describe("Memory - deleteAll()", () => { "At least one filter is required to delete all memories", ); }); + + test("deletes past the vector store default page limit", async () => { + const mem = createMemory(); + const internals = mem as any; + await internals._ensureInitialized(); + const pagedUserId = `deleteall_paged_${Date.now()}`; + const total = 150; + + await internals.vectorStore.insert( + Array.from({ length: total }, () => new Array(1536).fill(0.1)), + Array.from({ length: total }, (_, index) => `paged-${index}`), + Array.from({ length: total }, (_, index) => ({ + data: `Fact ${index}`, + hash: `hash-${index}`, + user_id: pagedUserId, + createdAt: new Date().toISOString(), + })), + ); + + const [before] = await internals.vectorStore.list( + { user_id: pagedUserId }, + total * 2, + ); + expect(before).toHaveLength(total); + + const result = await mem.deleteAll({ userId: pagedUserId }); + expect(result.message).toBe("Memories deleted successfully!"); + + const [after] = await internals.vectorStore.list( + { user_id: pagedUserId }, + total * 2, + ); + expect(after).toHaveLength(0); + }, 60000); + + test("stops when the vector store keeps returning the same batch", async () => { + const mem = createMemory(); + const internals = mem as any; + await internals._ensureInitialized(); + const stale = [ + { id: "stale-1", payload: { data: "x", user_id: "u1" } }, + { id: "stale-2", payload: { data: "x", user_id: "u1" } }, + ]; + const listSpy = jest + .spyOn(internals.vectorStore, "list") + .mockResolvedValueOnce([stale, stale.length]) + .mockResolvedValueOnce([stale, stale.length]) + .mockImplementation(async () => { + throw new Error("deleteAll should have stopped after a repeated batch"); + }); + const deleteSpy = jest + .spyOn(internals, "deleteMemory") + .mockResolvedValue("stale-1"); + + const result = await mem.deleteAll({ userId: "u1" }); + + expect(result.message).toBe("Memories deleted successfully!"); + expect(listSpy).toHaveBeenCalledTimes(2); + expect(deleteSpy).toHaveBeenCalledTimes(2); + }); }); // ─── getAll() ──────────────────────────────────────────── diff --git a/mem0-ts/src/oss/tests/memory.validation.test.ts b/mem0-ts/src/oss/tests/memory.validation.test.ts index 9efd96b6f..02aad751f 100644 --- a/mem0-ts/src/oss/tests/memory.validation.test.ts +++ b/mem0-ts/src/oss/tests/memory.validation.test.ts @@ -362,7 +362,10 @@ describe("Memory Input Validation", () => { await memory.deleteAll({ userId: " alice " }); - expect(listSpy).toHaveBeenCalledWith({ user_id: "alice" }); + expect(listSpy).toHaveBeenCalledWith( + { user_id: "alice" }, + expect.any(Number), + ); }); }); }); diff --git a/mem0-ts/src/oss/tests/vector-stores-compat.test.ts b/mem0-ts/src/oss/tests/vector-stores-compat.test.ts index 2aa1f0a6c..d294a5124 100644 --- a/mem0-ts/src/oss/tests/vector-stores-compat.test.ts +++ b/mem0-ts/src/oss/tests/vector-stores-compat.test.ts @@ -5177,20 +5177,22 @@ describe("Memory class – backward compat with all providers", () => { updated_at: new Date().toISOString(), }, }); - mockVStore.list.mockResolvedValue([ - [ - { - id: memoryId, - payload: { - memory: "test", - hash: "h", - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), + mockVStore.list + .mockResolvedValueOnce([ + [ + { + id: memoryId, + payload: { + memory: "test", + hash: "h", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, }, - }, - ], - 1, - ]); + ], + 1, + ]) + .mockResolvedValue([[], 0]); mockVectorStoreFactory.create.mockReturnValue(mockVStore); const mem = new MemoryClass({