fix: delete_all now drains all pages, not just the first batch (close… (#4872)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
@@ -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<string>();
|
||||
|
||||
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");
|
||||
|
||||
@@ -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() ────────────────────────────────────────────
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user