From 86fe275f539833f2c1852ebf756291401196a65f Mon Sep 17 00:00:00 2001 From: Saket Aryan Date: Wed, 15 Apr 2026 21:01:01 +0530 Subject: [PATCH] fix(ts): entity store isolation, backward compat, pgvector + redis init fixes (#4841) --- mem0-ts/src/client/mem0.ts | 2 +- mem0-ts/src/oss/src/memory/index.ts | 11 ++++---- .../tests/better-sqlite3-migration.test.ts | 6 ++-- .../src/tests/sqlite-backward-compat.test.ts | 2 +- mem0-ts/src/oss/src/vector_stores/memory.ts | 26 ++++++++++++++--- mem0-ts/src/oss/src/vector_stores/pgvector.ts | 8 ++++++ mem0-ts/src/oss/src/vector_stores/redis.ts | 28 ++++++++++++------- .../src/oss/tests/vector-store.unit.test.ts | 16 ++++++----- .../oss/tests/vector-stores-compat.test.ts | 8 +++--- 9 files changed, 71 insertions(+), 36 deletions(-) diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts index cd5a765ea..bda29c058 100644 --- a/mem0-ts/src/client/mem0.ts +++ b/mem0-ts/src/client/mem0.ts @@ -308,7 +308,7 @@ export default class MemoryClient { headers: this.headers, body: JSON.stringify(body), }); - return Array.isArray(response) ? response : (response?.results ?? response); + return response; } async search( diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts index 7414b5bb0..1fd7bf88a 100644 --- a/mem0-ts/src/oss/src/memory/index.ts +++ b/mem0-ts/src/oss/src/memory/index.ts @@ -52,6 +52,7 @@ import { ENTITY_BOOST_WEIGHT, ScoredResult, } from "../utils/scoring"; +import { getDefaultVectorStoreDbPath } from "../utils/sqlite"; // Entity params that must be passed via filters - check both snake_case and camelCase const ENTITY_PARAMS = [ @@ -247,12 +248,10 @@ export class Memory { ...this.config.vectorStore.config, collectionName: entityCollectionName, }; - // For file-based stores (memory/SQLite), use a separate DB path for entities - if (entityConfig.dbPath) { - entityConfig.dbPath = entityConfig.dbPath.replace( - /\.db$/, - "_entities.db", - ); + // For file-based stores (memory/SQLite), always use a separate DB for entities + if (this.config.vectorStore.provider === "memory") { + const basePath = entityConfig.dbPath || getDefaultVectorStoreDbPath(); + entityConfig.dbPath = basePath.replace(/\.db$/, "_entities.db"); } this._entityStore = VectorStoreFactory.create( this.config.vectorStore.provider, diff --git a/mem0-ts/src/oss/src/tests/better-sqlite3-migration.test.ts b/mem0-ts/src/oss/src/tests/better-sqlite3-migration.test.ts index 0edf309b7..290c0d3f1 100644 --- a/mem0-ts/src/oss/src/tests/better-sqlite3-migration.test.ts +++ b/mem0-ts/src/oss/src/tests/better-sqlite3-migration.test.ts @@ -197,7 +197,7 @@ describe("MemoryVectorStore (better-sqlite3)", () => { expect(result).not.toBeNull(); expect(result!.id).toBe("id-1"); expect(result!.payload.data).toBe("hello"); - expect(result!.payload.userId).toBe("u1"); + expect(result!.payload.user_id).toBe("u1"); }); it("get returns null for non-existent id", async () => { @@ -253,7 +253,7 @@ describe("MemoryVectorStore (better-sqlite3)", () => { ], ); - const results = await store.search(v, 10, { userId: "alice" }); + const results = await store.search(v, 10, { user_id: "alice" }); expect(results).toHaveLength(1); expect(results[0].id).toBe("id-1"); }); @@ -320,7 +320,7 @@ describe("MemoryVectorStore (better-sqlite3)", () => { expect(all).toHaveLength(3); expect(totalAll).toBe(3); - const [filtered, totalFiltered] = await store.list({ userId: "alice" }); + const [filtered, totalFiltered] = await store.list({ user_id: "alice" }); expect(filtered).toHaveLength(2); expect(totalFiltered).toBe(2); }); diff --git a/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts b/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts index 72fd642ec..820aae913 100644 --- a/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts +++ b/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts @@ -308,7 +308,7 @@ describe("backward compat: MemoryVectorStore", () => { ); const results = await store.search(normalize([1, 0, 0]), 10, { - userId: "user2", + user_id: "user2", }); expect(results).toHaveLength(1); expect(results[0].id).toBe("b"); diff --git a/mem0-ts/src/oss/src/vector_stores/memory.ts b/mem0-ts/src/oss/src/vector_stores/memory.ts index d8876f130..7969194f0 100644 --- a/mem0-ts/src/oss/src/vector_stores/memory.ts +++ b/mem0-ts/src/oss/src/vector_stores/memory.ts @@ -19,6 +19,24 @@ export class MemoryVectorStore implements VectorStore { private dimension: number; private dbPath: string; + private static readonly CAMEL_TO_SNAKE: Record = { + userId: "user_id", + agentId: "agent_id", + runId: "run_id", + }; + + private normalizePayload(payload: Record): Record { + for (const [camel, snake] of Object.entries( + MemoryVectorStore.CAMEL_TO_SNAKE, + )) { + if (camel in payload && !(snake in payload)) { + payload[snake] = payload[camel]; + delete payload[camel]; + } + } + return payload; + } + constructor(config: VectorStoreConfig) { this.dimension = config.dimension || 1536; // Default OpenAI dimension this.dbPath = config.dbPath || getDefaultVectorStoreDbPath(); @@ -246,7 +264,7 @@ export class MemoryVectorStore implements VectorStore { }[] = []; for (const row of rows) { - const payload = JSON.parse(row.payload); + const payload = this.normalizePayload(JSON.parse(row.payload)); const memoryVector: MemoryVector = { id: row.id, vector: Array.from( @@ -351,7 +369,7 @@ export class MemoryVectorStore implements VectorStore { row.vector.byteOffset, row.vector.byteLength / 4, ); - const payload = JSON.parse(row.payload); + const payload = this.normalizePayload(JSON.parse(row.payload)); const memoryVector: MemoryVector = { id: row.id, vector: Array.from(vector), @@ -378,7 +396,7 @@ export class MemoryVectorStore implements VectorStore { .get(vectorId) as any; if (!row) return null; - const payload = JSON.parse(row.payload); + const payload = this.normalizePayload(JSON.parse(row.payload)); return { id: row.id, payload, @@ -418,7 +436,7 @@ export class MemoryVectorStore implements VectorStore { const results: VectorStoreResult[] = []; for (const row of rows) { - const payload = JSON.parse(row.payload); + const payload = this.normalizePayload(JSON.parse(row.payload)); const memoryVector: MemoryVector = { id: row.id, vector: Array.from( diff --git a/mem0-ts/src/oss/src/vector_stores/pgvector.ts b/mem0-ts/src/oss/src/vector_stores/pgvector.ts index 9fdd56e0b..542527757 100644 --- a/mem0-ts/src/oss/src/vector_stores/pgvector.ts +++ b/mem0-ts/src/oss/src/vector_stores/pgvector.ts @@ -22,6 +22,7 @@ export class PGVector implements VectorStore { private useHnsw: boolean; private readonly dbName: string; private config: PGVectorConfig; + private _initPromise?: Promise; constructor(config: PGVectorConfig) { this.collectionName = config.collectionName || "memories"; @@ -41,6 +42,13 @@ export class PGVector implements VectorStore { } async initialize(): Promise { + if (!this._initPromise) { + this._initPromise = this._doInitialize(); + } + return this._initPromise; + } + + private async _doInitialize(): Promise { try { await this.client.connect(); diff --git a/mem0-ts/src/oss/src/vector_stores/redis.ts b/mem0-ts/src/oss/src/vector_stores/redis.ts index ee59a2714..0fa52488f 100644 --- a/mem0-ts/src/oss/src/vector_stores/redis.ts +++ b/mem0-ts/src/oss/src/vector_stores/redis.ts @@ -256,17 +256,25 @@ export class RedisDB implements VectorStore { const modulesResponse = (await this.client.moduleList()) as unknown as any[]; - // Parse module list to find search module - const hasSearch = modulesResponse.some((module: any[]) => { - const moduleMap = new Map(); - for (let i = 0; i < module.length; i += 2) { - moduleMap.set(module[i], module[i + 1]); + const hasSearch = modulesResponse.some((mod: any) => { + // node-redis v4+ returns objects: { name: "search", ver: ..., ... } + if (typeof mod === "object" && !Array.isArray(mod) && mod.name) { + const name = String(mod.name).toLowerCase(); + return name === "search" || name === "searchlight"; } - const moduleName = moduleMap.get("name"); - return ( - moduleName?.toLowerCase() === "search" || - moduleName?.toLowerCase() === "searchlight" - ); + // Fallback: legacy flat array format [key, value, key, value, ...] + if (Array.isArray(mod)) { + const moduleMap = new Map(); + for (let i = 0; i < mod.length; i += 2) { + moduleMap.set(mod[i], mod[i + 1]); + } + const name = moduleMap.get("name"); + return ( + name?.toLowerCase() === "search" || + name?.toLowerCase() === "searchlight" + ); + } + return false; }); if (!hasSearch) { diff --git a/mem0-ts/src/oss/tests/vector-store.unit.test.ts b/mem0-ts/src/oss/tests/vector-store.unit.test.ts index 413c59cc7..6055f6c71 100644 --- a/mem0-ts/src/oss/tests/vector-store.unit.test.ts +++ b/mem0-ts/src/oss/tests/vector-store.unit.test.ts @@ -85,14 +85,16 @@ describe("MemoryVectorStore - search", () => { expect(results).toHaveLength(1); }); - test("filters by userId", async () => { - const results = await store.search(vec([1, 0, 0, 0]), 10, { userId: "u2" }); - expect(results.every((r) => r.payload.userId === "u2")).toBe(true); + test("filters by user_id", async () => { + const results = await store.search(vec([1, 0, 0, 0]), 10, { + user_id: "u2", + }); + expect(results.every((r) => r.payload.user_id === "u2")).toBe(true); }); test("returns empty when filter matches nothing", async () => { const results = await store.search(vec([1, 0, 0, 0]), 10, { - userId: "nobody", + user_id: "nobody", }); expect(results).toHaveLength(0); }); @@ -168,10 +170,10 @@ describe("MemoryVectorStore - list", () => { expect(results).toHaveLength(3); }); - test("filters by userId", async () => { - const [results, count] = await store.list({ userId: "u1" }); + test("filters by user_id", async () => { + const [results, count] = await store.list({ user_id: "u1" }); expect(count).toBe(2); - expect(results.every((r) => r.payload.userId === "u1")).toBe(true); + expect(results.every((r) => r.payload.user_id === "u1")).toBe(true); }); test("respects limit", async () => { 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 605aac867..8a71049e0 100644 --- a/mem0-ts/src/oss/tests/vector-stores-compat.test.ts +++ b/mem0-ts/src/oss/tests/vector-stores-compat.test.ts @@ -95,19 +95,19 @@ describe("MemoryVectorStore – full backward compat", () => { expect(results.length).toBe(2); expect(results[0].id).toBe("id-1"); - // Search with filters - const filtered = await store.search(vec1, 2, { userId: "u1" }); + // Search with filters (camelCase in payload is normalized to snake_case) + const filtered = await store.search(vec1, 2, { user_id: "u1" }); expect(filtered.length).toBe(2); // Update const vec3 = new Array(1536).fill(0); vec3[2] = 1.0; - await store.update("id-1", vec3, { data: "updated", userId: "u1" }); + await store.update("id-1", vec3, { data: "updated", user_id: "u1" }); const updated = await store.get("id-1"); expect(updated!.payload.data).toBe("updated"); // List - const [listed, count] = await store.list({ userId: "u1" }); + const [listed, count] = await store.list({ user_id: "u1" }); expect(count).toBe(2); // List with limit