Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plain-message-identifiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@voltagent/core": patch
---

Use dictionaries without inherited keys for in-memory message storage so user and conversation identifiers such as constructor, toString and __proto__ do not break message operations or user isolation, including after clearing storage.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { UIMessage } from "ai";
import { describe, expect, it } from "vitest";
import { InMemoryStorageAdapter } from "./in-memory";

const message: UIMessage = {
id: "message",
role: "user",
parts: [{ type: "text", text: "Hello" }],
};

describe("InMemoryStorageAdapter string identifiers", () => {
it.each(["constructor", "toString", "__proto__"])(
"keeps messages isolated when a user identifier is %s",
async (userId) => {
const adapter = new InMemoryStorageAdapter();
expect(await adapter.getMessages(userId, "constructor")).toEqual([]);
await adapter.addMessage(message, userId, "conversation");
expect(await adapter.getMessages(userId, "conversation")).toHaveLength(1);
expect(await adapter.getMessages("another-user", "conversation")).toEqual([]);
expect(adapter.getStats()).toMatchObject({ totalUsers: 1, totalMessages: 1 });

await adapter.clearMessages(userId);
await adapter.addMessage(message, userId, "conversation");
expect(await adapter.getMessages(userId, "conversation")).toHaveLength(1);
adapter.clear();
await adapter.addMessage(message, userId, "conversation");
expect(adapter.getStats()).toMatchObject({ totalUsers: 1, totalMessages: 1 });
},
);

it.each(["constructor", "toString", "__proto__"])(
"supports a conversation identifier named %s through message CRUD",
async (conversationId) => {
const adapter = new InMemoryStorageAdapter();
expect(await adapter.getMessages("user", conversationId)).toEqual([]);
await adapter.deleteMessages([message.id], "user", conversationId);
await adapter.addMessage(message, "user", conversationId);
expect(await adapter.getMessages("user", conversationId)).toHaveLength(1);
await adapter.deleteMessages([message.id], "user", conversationId);
expect(await adapter.getMessages("user", conversationId)).toEqual([]);
await adapter.addMessage(message, "user", conversationId);
await adapter.clearMessages("user", conversationId);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
expect(await adapter.getMessages("user", conversationId)).toEqual([]);
await adapter.clearMessages("user");
await adapter.addMessage(message, "user", conversationId);
expect(await adapter.getMessages("user", conversationId)).toHaveLength(1);
adapter.clear();
await adapter.addMessage(message, "user", conversationId);
expect(await adapter.getMessages("user", conversationId)).toHaveLength(1);
},
);
});
11 changes: 6 additions & 5 deletions packages/core/src/memory/adapters/storage/in-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function areMetadataValuesEqual(left: unknown, right: unknown): boolean {
* Simple implementation for testing and development
*/
export class InMemoryStorageAdapter implements StorageAdapter {
private storage: Record<string, Record<string, StoredUIMessage[]>> = {};
private storage: Record<string, Record<string, StoredUIMessage[]>> = Object.create(null);
private conversations: Map<string, Conversation> = new Map();
private users: Map<string, UserInfo> = new Map();
private workflowStates: Map<string, WorkflowStateEntry> = new Map();
Expand All @@ -106,7 +106,7 @@ export class InMemoryStorageAdapter implements StorageAdapter {
): Promise<void> {
// Create user's messages container if it doesn't exist
if (!this.storage[userId]) {
this.storage[userId] = {};
this.storage[userId] = Object.create(null);
}

// Create conversation's messages array if it doesn't exist
Expand Down Expand Up @@ -165,7 +165,8 @@ export class InMemoryStorageAdapter implements StorageAdapter {
const { limit, before, after, roles } = options || {};

// Get user's messages or return empty array
const userMessages = this.storage[userId] || {};
const userMessages: Record<string, StoredUIMessage[]> =
this.storage[userId] || Object.create(null);
let messages = userMessages[conversationId] || [];

// Apply role filter if provided
Expand Down Expand Up @@ -299,7 +300,7 @@ export class InMemoryStorageAdapter implements StorageAdapter {
}
} else {
// Clear all messages for the user
this.storage[userId] = {};
this.storage[userId] = Object.create(null);
this.conversationSteps.delete(userId);
}
}
Expand Down Expand Up @@ -724,7 +725,7 @@ export class InMemoryStorageAdapter implements StorageAdapter {
* Clear all data
*/
clear(): void {
this.storage = {};
this.storage = Object.create(null);
this.conversations.clear();
this.users.clear();
this.workflowStates.clear();
Expand Down