From 15f62d1d4e9072be7682bdde88a225fd5e83b7db Mon Sep 17 00:00:00 2001 From: Omer Aplak Date: Sun, 15 Mar 2026 09:57:23 -0700 Subject: [PATCH 1/3] fix: delete vectors when clearing messages --- .changeset/mean-pets-perform.md | 21 ++++++++ packages/core/src/memory/index.ts | 39 +++++++++++++++ .../core/src/memory/semantic-search.spec.ts | 48 +++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 .changeset/mean-pets-perform.md diff --git a/.changeset/mean-pets-perform.md b/.changeset/mean-pets-perform.md new file mode 100644 index 000000000..6d1e67cda --- /dev/null +++ b/.changeset/mean-pets-perform.md @@ -0,0 +1,21 @@ +--- +"@voltagent/core": patch +--- + +Fix stale semantic-search results after `Memory.clearMessages()`. + +Previously, `clearMessages()` removed conversation messages from storage but left vector +embeddings behind when a vector adapter was configured. This meant semantic search could +still return hits for cleared conversations even though the message history had been removed. + +## What Changed + +- `Memory.clearMessages(userId, conversationId)` now deletes vector entries for that + conversation before clearing storage +- `Memory.clearMessages(userId)` now also deletes vector entries across all of the user's + conversations + +## Impact + +- Cleared conversations no longer appear in semantic search results +- Message storage and vector storage stay in sync after cleanup diff --git a/packages/core/src/memory/index.ts b/packages/core/src/memory/index.ts index e7a613967..126f4070d 100644 --- a/packages/core/src/memory/index.ts +++ b/packages/core/src/memory/index.ts @@ -170,6 +170,20 @@ export class Memory { conversationId?: string, context?: OperationContext, ): Promise { + if (this.vector) { + try { + const vectorIds = await this.getMessageVectorIdsForClear(userId, conversationId); + if (vectorIds.length > 0) { + await this.vector.deleteBatch(vectorIds); + } + } catch (error) { + console.warn( + `Failed to delete vectors while clearing messages for user ${userId}${conversationId ? ` conversation ${conversationId}` : ""}:`, + error, + ); + } + } + return this.storage.clearMessages(userId, conversationId, context); } @@ -376,6 +390,31 @@ export class Memory { return ordered; } + private async getMessageVectorIdsForClear( + userId: string, + conversationId?: string, + ): Promise { + const vectorIds = new Set(); + + if (conversationId) { + const messages = await this.storage.getMessages(userId, conversationId); + for (const message of messages) { + vectorIds.add(`msg_${conversationId}_${message.id}`); + } + return Array.from(vectorIds); + } + + const conversations = await this.storage.getConversationsByUserId(userId); + for (const conversation of conversations) { + const messages = await this.storage.getMessages(userId, conversation.id); + for (const message of messages) { + vectorIds.add(`msg_${conversation.id}_${message.id}`); + } + } + + return Array.from(vectorIds); + } + /** * Merge two arrays of messages, removing duplicates */ diff --git a/packages/core/src/memory/semantic-search.spec.ts b/packages/core/src/memory/semantic-search.spec.ts index fbc0f4788..4a8d8cb57 100644 --- a/packages/core/src/memory/semantic-search.spec.ts +++ b/packages/core/src/memory/semantic-search.spec.ts @@ -455,5 +455,53 @@ describe("Memory V2 - Semantic Search", () => { expect.arrayContaining([`msg_${conversationId}_msg1`, `msg_${conversationId}_msg2`]), ); }); + + it("should delete vectors when conversation messages are cleared", async () => { + const userId = "user123"; + const conversationId = "conv789"; + + await memory.createConversation({ + id: conversationId, + userId, + resourceId: "agent1", + title: "Clear Conversation", + }); + + const messages: UIMessage[] = [ + { + id: "msg1", + role: "user", + parts: [{ type: "text", text: "My name is Sujal" }], + }, + { + id: "msg2", + role: "assistant", + parts: [{ type: "text", text: "Your name is Sujal." }], + }, + ]; + + await memory.addMessages(messages, userId, conversationId); + + const beforeClear = await memory.searchSimilar("What is my name?", { + limit: 5, + filter: { userId, conversationId }, + }); + const deleteBatchSpy = vi.spyOn(vector, "deleteBatch"); + + await memory.clearMessages(userId, conversationId); + + const storedMessages = await memory.getMessages(userId, conversationId); + const afterClear = await memory.searchSimilar("What is my name?", { + limit: 5, + filter: { userId, conversationId }, + }); + + expect(beforeClear.length).toBeGreaterThan(0); + expect(deleteBatchSpy).toHaveBeenCalledWith( + expect.arrayContaining([`msg_${conversationId}_msg1`, `msg_${conversationId}_msg2`]), + ); + expect(storedMessages).toHaveLength(0); + expect(afterClear).toHaveLength(0); + }); }); }); From 134da7829153b8532a296b38ae951d2388821d50 Mon Sep 17 00:00:00 2001 From: Omer Aplak Date: Sun, 15 Mar 2026 10:06:06 -0700 Subject: [PATCH 2/3] fix: paginate user-wide vector cleanup --- packages/core/src/memory/index.ts | 27 ++++++-- .../core/src/memory/semantic-search.spec.ts | 67 +++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/core/src/memory/index.ts b/packages/core/src/memory/index.ts index 126f4070d..c024e22ee 100644 --- a/packages/core/src/memory/index.ts +++ b/packages/core/src/memory/index.ts @@ -44,6 +44,8 @@ const isEmbeddingAdapter = (value: EmbeddingAdapterInput): value is EmbeddingAda const isEmbeddingAdapterConfig = (value: EmbeddingAdapterInput): value is EmbeddingAdapterConfig => typeof value === "object" && value !== null && "model" in value && !isEmbeddingAdapter(value); +const VECTOR_CLEAR_CONVERSATION_PAGE_SIZE = 200; + const resolveEmbeddingAdapter = ( embedding?: EmbeddingAdapterInput, ): EmbeddingAdapter | undefined => { @@ -404,12 +406,27 @@ export class Memory { return Array.from(vectorIds); } - const conversations = await this.storage.getConversationsByUserId(userId); - for (const conversation of conversations) { - const messages = await this.storage.getMessages(userId, conversation.id); - for (const message of messages) { - vectorIds.add(`msg_${conversation.id}_${message.id}`); + let offset = 0; + + while (true) { + const conversations = await this.storage.queryConversations({ + userId, + limit: VECTOR_CLEAR_CONVERSATION_PAGE_SIZE, + offset, + }); + + for (const conversation of conversations) { + const messages = await this.storage.getMessages(userId, conversation.id); + for (const message of messages) { + vectorIds.add(`msg_${conversation.id}_${message.id}`); + } } + + if (conversations.length < VECTOR_CLEAR_CONVERSATION_PAGE_SIZE) { + break; + } + + offset += VECTOR_CLEAR_CONVERSATION_PAGE_SIZE; } return Array.from(vectorIds); diff --git a/packages/core/src/memory/semantic-search.spec.ts b/packages/core/src/memory/semantic-search.spec.ts index 4a8d8cb57..d6a0cdc83 100644 --- a/packages/core/src/memory/semantic-search.spec.ts +++ b/packages/core/src/memory/semantic-search.spec.ts @@ -8,6 +8,7 @@ import type { EmbeddingAdapter } from "./adapters/embedding/types"; import { InMemoryStorageAdapter } from "./adapters/storage/in-memory"; import { InMemoryVectorAdapter } from "./adapters/vector/in-memory"; import { Memory } from "./index"; +import type { ConversationQueryOptions } from "./types"; // Mock embedding adapter class MockEmbeddingAdapter implements EmbeddingAdapter { @@ -32,6 +33,24 @@ class MockEmbeddingAdapter implements EmbeddingAdapter { } } +class PagedConversationStorageAdapter extends InMemoryStorageAdapter { + constructor(private readonly defaultPageSize: number) { + super(); + } + + override async getConversationsByUserId( + userId: string, + options?: Omit, + ) { + return this.queryConversations({ + ...options, + userId, + limit: options?.limit ?? this.defaultPageSize, + offset: options?.offset ?? 0, + }); + } +} + describe("Memory V2 - Semantic Search", () => { let memory: Memory; let storage: InMemoryStorageAdapter; @@ -503,5 +522,53 @@ describe("Memory V2 - Semantic Search", () => { expect(storedMessages).toHaveLength(0); expect(afterClear).toHaveLength(0); }); + + it("should paginate user-wide vector cleanup across all conversations", async () => { + const userId = "paged-user"; + const pagedStorage = new PagedConversationStorageAdapter(2); + const pagedVector = new InMemoryVectorAdapter(); + const pagedMemory = new Memory({ + storage: pagedStorage, + embedding: new MockEmbeddingAdapter(), + vector: pagedVector, + }); + + for (const conversationId of ["conv-1", "conv-2", "conv-3"]) { + await pagedMemory.createConversation({ + id: conversationId, + userId, + resourceId: "agent1", + title: conversationId, + }); + + await pagedMemory.addMessage( + { + id: `msg-${conversationId}`, + role: "user", + parts: [{ type: "text", text: `Memory from ${conversationId}` }], + }, + userId, + conversationId, + ); + } + + const beforeClear = await pagedMemory.searchSimilar("Memory from conv-3", { + limit: 5, + filter: { userId }, + }); + + await pagedMemory.clearMessages(userId); + + const afterClear = await pagedMemory.searchSimilar("Memory from conv-3", { + limit: 5, + filter: { userId }, + }); + + expect(beforeClear.length).toBeGreaterThan(0); + expect(await pagedMemory.getMessages(userId, "conv-1")).toHaveLength(0); + expect(await pagedMemory.getMessages(userId, "conv-2")).toHaveLength(0); + expect(await pagedMemory.getMessages(userId, "conv-3")).toHaveLength(0); + expect(afterClear).toHaveLength(0); + }); }); }); From 2acd40a707f9ece7a5be8fea60e67f28cd4f6d31 Mon Sep 17 00:00:00 2001 From: Omer Aplak Date: Sun, 15 Mar 2026 10:18:45 -0700 Subject: [PATCH 3/3] fix: handle paged conversation cleanup --- packages/core/src/memory/index.ts | 7 ++++--- packages/core/src/memory/semantic-search.spec.ts | 14 ++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/core/src/memory/index.ts b/packages/core/src/memory/index.ts index c024e22ee..a63206f2f 100644 --- a/packages/core/src/memory/index.ts +++ b/packages/core/src/memory/index.ts @@ -406,9 +406,10 @@ export class Memory { return Array.from(vectorIds); } + const totalConversations = await this.storage.countConversations({ userId }); let offset = 0; - while (true) { + while (offset < totalConversations) { const conversations = await this.storage.queryConversations({ userId, limit: VECTOR_CLEAR_CONVERSATION_PAGE_SIZE, @@ -422,11 +423,11 @@ export class Memory { } } - if (conversations.length < VECTOR_CLEAR_CONVERSATION_PAGE_SIZE) { + if (conversations.length === 0) { break; } - offset += VECTOR_CLEAR_CONVERSATION_PAGE_SIZE; + offset += conversations.length; } return Array.from(vectorIds); diff --git a/packages/core/src/memory/semantic-search.spec.ts b/packages/core/src/memory/semantic-search.spec.ts index d6a0cdc83..8ba6f03b3 100644 --- a/packages/core/src/memory/semantic-search.spec.ts +++ b/packages/core/src/memory/semantic-search.spec.ts @@ -38,15 +38,13 @@ class PagedConversationStorageAdapter extends InMemoryStorageAdapter { super(); } - override async getConversationsByUserId( - userId: string, - options?: Omit, - ) { - return this.queryConversations({ + override async queryConversations(options: ConversationQueryOptions) { + const limit = Math.min(options.limit ?? Number.POSITIVE_INFINITY, this.defaultPageSize); + + return super.queryConversations({ ...options, - userId, - limit: options?.limit ?? this.defaultPageSize, - offset: options?.offset ?? 0, + limit, + offset: options.offset ?? 0, }); } }