Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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,46 @@
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([]);
},
);
});
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