fix(ts): entity store isolation, backward compat, pgvector + redis init fixes (#4841)

This commit is contained in:
Saket Aryan
2026-04-15 21:01:01 +05:30
committed by GitHub
parent e6d6276bb9
commit 86fe275f53
9 changed files with 71 additions and 36 deletions
+1 -1
View File
@@ -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(
+5 -6
View File
@@ -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,
@@ -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);
});
@@ -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");
+22 -4
View File
@@ -19,6 +19,24 @@ export class MemoryVectorStore implements VectorStore {
private dimension: number;
private dbPath: string;
private static readonly CAMEL_TO_SNAKE: Record<string, string> = {
userId: "user_id",
agentId: "agent_id",
runId: "run_id",
};
private normalizePayload(payload: Record<string, any>): Record<string, any> {
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(
@@ -22,6 +22,7 @@ export class PGVector implements VectorStore {
private useHnsw: boolean;
private readonly dbName: string;
private config: PGVectorConfig;
private _initPromise?: Promise<void>;
constructor(config: PGVectorConfig) {
this.collectionName = config.collectionName || "memories";
@@ -41,6 +42,13 @@ export class PGVector implements VectorStore {
}
async initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
}
return this._initPromise;
}
private async _doInitialize(): Promise<void> {
try {
await this.client.connect();
+18 -10
View File
@@ -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) {
@@ -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 () => {
@@ -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