From 7a3bc22161497acc4481cc8a84efd49db715d4e2 Mon Sep 17 00:00:00 2001 From: utkarsh240799 Date: Thu, 12 Mar 2026 13:45:49 +0530 Subject: [PATCH] fix(ts-sdk): resolve SQLite db paths correctly in OSS mode Propagate top-level historyDbPath into historyStore.config so it survives config merging, default the memory vector store to ~/.mem0/vector_store.db instead of process.cwd(), auto-create parent directories for file-backed SQLite databases, and remove dead code in the Memory constructor. Co-Authored-By: Claude Opus 4.6 --- mem0-ts/src/oss/src/config/manager.ts | 28 +++- mem0-ts/src/oss/src/memory/index.ts | 18 +-- mem0-ts/src/oss/src/storage/SQLiteManager.ts | 2 + .../src/tests/sqlite-path-resolution.test.ts | 124 ++++++++++++++++++ mem0-ts/src/oss/src/types/index.ts | 2 + mem0-ts/src/oss/src/utils/sqlite.ts | 15 +++ mem0-ts/src/oss/src/vector_stores/memory.ts | 20 ++- 7 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 mem0-ts/src/oss/src/tests/sqlite-path-resolution.test.ts create mode 100644 mem0-ts/src/oss/src/utils/sqlite.ts diff --git a/mem0-ts/src/oss/src/config/manager.ts b/mem0-ts/src/oss/src/config/manager.ts index c58bd01ec..e12fc2bd8 100644 --- a/mem0-ts/src/oss/src/config/manager.ts +++ b/mem0-ts/src/oss/src/config/manager.ts @@ -95,16 +95,34 @@ export class ConfigManager { })(), }, historyDbPath: - userConfig.historyDbPath || DEFAULT_MEMORY_CONFIG.historyDbPath, + userConfig.historyDbPath || + userConfig.historyStore?.config?.historyDbPath || + DEFAULT_MEMORY_CONFIG.historyStore?.config?.historyDbPath, customPrompt: userConfig.customPrompt, graphStore: { ...DEFAULT_MEMORY_CONFIG.graphStore, ...userConfig.graphStore, }, - historyStore: { - ...DEFAULT_MEMORY_CONFIG.historyStore, - ...userConfig.historyStore, - }, + historyStore: (() => { + const defaultHistoryStore = DEFAULT_MEMORY_CONFIG.historyStore!; + const historyProvider = + userConfig.historyStore?.provider || defaultHistoryStore.provider; + + // Precedence: explicit historyStore.config > top-level historyDbPath > default + return { + ...defaultHistoryStore, + ...userConfig.historyStore, + provider: historyProvider, + config: { + ...defaultHistoryStore.config, + ...(historyProvider.toLowerCase() === "sqlite" && + userConfig.historyDbPath + ? { historyDbPath: userConfig.historyDbPath } + : {}), + ...userConfig.historyStore?.config, + }, + }; + })(), disableHistory: userConfig.disableHistory || DEFAULT_MEMORY_CONFIG.disableHistory, enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph, diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts index 46ca8bf2a..9eabc8357 100644 --- a/mem0-ts/src/oss/src/memory/index.ts +++ b/mem0-ts/src/oss/src/memory/index.ts @@ -70,20 +70,10 @@ export class Memory { if (this.config.disableHistory) { this.db = new DummyHistoryManager(); } else { - const defaultConfig = { - provider: "sqlite", - config: { - historyDbPath: this.config.historyDbPath || ":memory:", - }, - }; - - this.db = - this.config.historyStore && !this.config.disableHistory - ? HistoryManagerFactory.create( - this.config.historyStore.provider, - this.config.historyStore, - ) - : HistoryManagerFactory.create("sqlite", defaultConfig); + this.db = HistoryManagerFactory.create( + this.config.historyStore!.provider, + this.config.historyStore!, + ); } this.collectionName = this.config.vectorStore.config.collectionName; diff --git a/mem0-ts/src/oss/src/storage/SQLiteManager.ts b/mem0-ts/src/oss/src/storage/SQLiteManager.ts index 89c2a6d17..6950093a4 100644 --- a/mem0-ts/src/oss/src/storage/SQLiteManager.ts +++ b/mem0-ts/src/oss/src/storage/SQLiteManager.ts @@ -1,5 +1,6 @@ import Database from "better-sqlite3"; import { HistoryManager } from "./base"; +import { ensureSQLiteDirectory } from "../utils/sqlite"; export class SQLiteManager implements HistoryManager { private db: Database.Database; @@ -7,6 +8,7 @@ export class SQLiteManager implements HistoryManager { private stmtSelect!: Database.Statement; constructor(dbPath: string) { + ensureSQLiteDirectory(dbPath); this.db = new Database(dbPath); this.init(); } diff --git a/mem0-ts/src/oss/src/tests/sqlite-path-resolution.test.ts b/mem0-ts/src/oss/src/tests/sqlite-path-resolution.test.ts new file mode 100644 index 000000000..b805624ed --- /dev/null +++ b/mem0-ts/src/oss/src/tests/sqlite-path-resolution.test.ts @@ -0,0 +1,124 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { ConfigManager } from "../config/manager"; +import { SQLiteManager } from "../storage/SQLiteManager"; +import { MemoryVectorStore } from "../vector_stores/memory"; + +function normalize(vector: number[]): number[] { + const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0)); + return vector.map((value) => value / norm); +} + +describe("SQLite path resolution", () => { + const originalCwd = process.cwd(); + + afterEach(() => { + process.chdir(originalCwd); + jest.restoreAllMocks(); + }); + + it("propagates top-level historyDbPath into the sqlite history store config", async () => { + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), "mem0-history-path-"), + ); + const historyDbPath = path.join(tempDir, "nested", "history.db"); + let manager: SQLiteManager | undefined; + + try { + const mergedConfig = ConfigManager.mergeConfig({ historyDbPath }); + + expect(mergedConfig.historyDbPath).toBe(historyDbPath); + expect(mergedConfig.historyStore?.provider).toBe("sqlite"); + expect(mergedConfig.historyStore?.config.historyDbPath).toBe( + historyDbPath, + ); + + // Verify the DB is actually created at the configured path + manager = new SQLiteManager( + mergedConfig.historyStore!.config.historyDbPath!, + ); + await manager.addHistory("memory-1", null, "remember me", "ADD"); + + expect(fs.existsSync(historyDbPath)).toBe(true); + } finally { + manager?.close(); + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("explicit historyStore.config.historyDbPath takes precedence over top-level historyDbPath", () => { + const mergedConfig = ConfigManager.mergeConfig({ + historyDbPath: "/path/from/shorthand.db", + historyStore: { + provider: "sqlite", + config: { historyDbPath: "/path/from/explicit.db" }, + }, + }); + + expect(mergedConfig.historyStore?.config.historyDbPath).toBe( + "/path/from/explicit.db", + ); + }); + + it("uses default memory.db when neither historyDbPath nor historyStore is provided", () => { + const mergedConfig = ConfigManager.mergeConfig({}); + + expect(mergedConfig.historyStore?.provider).toBe("sqlite"); + expect(mergedConfig.historyStore?.config.historyDbPath).toBe("memory.db"); + }); + + it("defaults the memory vector store dbPath to ~/.mem0/vector_store.db", async () => { + const fakeHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-")); + const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-cwd-")); + const expectedDbPath = path.join(fakeHomeDir, ".mem0", "vector_store.db"); + + try { + jest.spyOn(os, "homedir").mockReturnValue(fakeHomeDir); + process.chdir(tempCwd); + + const store = new MemoryVectorStore({ dimension: 4 }); + await store.insert( + [normalize([1, 0, 0, 0])], + ["vector-1"], + [{ data: "hello" }], + ); + + expect(fs.existsSync(expectedDbPath)).toBe(true); + expect(fs.existsSync(path.join(tempCwd, "vector_store.db"))).toBe(false); + } finally { + fs.rmSync(fakeHomeDir, { recursive: true, force: true }); + fs.rmSync(tempCwd, { recursive: true, force: true }); + } + }); + + it("does not depend on a writable current working directory for the default vector store db", async () => { + const fakeHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-")); + const readOnlyCwd = fs.mkdtempSync( + path.join(os.tmpdir(), "mem0-readonly-cwd-"), + ); + const expectedDbPath = path.join(fakeHomeDir, ".mem0", "vector_store.db"); + + try { + fs.chmodSync(readOnlyCwd, 0o555); + jest.spyOn(os, "homedir").mockReturnValue(fakeHomeDir); + process.chdir(readOnlyCwd); + + const store = new MemoryVectorStore({ dimension: 4 }); + await store.insert( + [normalize([0, 1, 0, 0])], + ["vector-2"], + [{ data: "hello again" }], + ); + + expect(fs.existsSync(expectedDbPath)).toBe(true); + expect(fs.existsSync(path.join(readOnlyCwd, "vector_store.db"))).toBe( + false, + ); + } finally { + fs.chmodSync(readOnlyCwd, 0o755); + fs.rmSync(fakeHomeDir, { recursive: true, force: true }); + fs.rmSync(readOnlyCwd, { recursive: true, force: true }); + } + }); +}); diff --git a/mem0-ts/src/oss/src/types/index.ts b/mem0-ts/src/oss/src/types/index.ts index 6119806d2..a5beaa1a2 100644 --- a/mem0-ts/src/oss/src/types/index.ts +++ b/mem0-ts/src/oss/src/types/index.ts @@ -23,6 +23,7 @@ export interface EmbeddingConfig { export interface VectorStoreConfig { collectionName?: string; dimension?: number; + dbPath?: string; client?: any; instance?: any; [key: string]: any; @@ -129,6 +130,7 @@ export const MemoryConfigSchema = z.object({ .object({ collectionName: z.string().optional(), dimension: z.number().optional(), + dbPath: z.string().optional(), client: z.any().optional(), }) .passthrough(), diff --git a/mem0-ts/src/oss/src/utils/sqlite.ts b/mem0-ts/src/oss/src/utils/sqlite.ts new file mode 100644 index 000000000..7da81c6c8 --- /dev/null +++ b/mem0-ts/src/oss/src/utils/sqlite.ts @@ -0,0 +1,15 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; + +export function getDefaultVectorStoreDbPath(): string { + return path.join(os.homedir(), ".mem0", "vector_store.db"); +} + +export function ensureSQLiteDirectory(dbPath: string): void { + if (!dbPath || dbPath === ":memory:" || dbPath.startsWith("file:")) { + return; + } + + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); +} diff --git a/mem0-ts/src/oss/src/vector_stores/memory.ts b/mem0-ts/src/oss/src/vector_stores/memory.ts index 4aed96838..62a825507 100644 --- a/mem0-ts/src/oss/src/vector_stores/memory.ts +++ b/mem0-ts/src/oss/src/vector_stores/memory.ts @@ -1,7 +1,12 @@ import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import Database from "better-sqlite3"; +import fs from "fs"; import path from "path"; +import { + ensureSQLiteDirectory, + getDefaultVectorStoreDbPath, +} from "../utils/sqlite"; interface MemoryVector { id: string; @@ -16,10 +21,19 @@ export class MemoryVectorStore implements VectorStore { constructor(config: VectorStoreConfig) { this.dimension = config.dimension || 1536; // Default OpenAI dimension - this.dbPath = path.join(process.cwd(), "vector_store.db"); - if (config.dbPath) { - this.dbPath = config.dbPath; + this.dbPath = config.dbPath || getDefaultVectorStoreDbPath(); + + if (!config.dbPath) { + const oldDefault = path.join(process.cwd(), "vector_store.db"); + if (fs.existsSync(oldDefault) && oldDefault !== this.dbPath) { + console.warn( + `[mem0] Default vector_store.db location changed from ${oldDefault} to ${this.dbPath}. ` + + `Move your existing file or set vectorStore.config.dbPath explicitly.`, + ); + } } + + ensureSQLiteDirectory(this.dbPath); this.db = new Database(this.dbPath); this.init(); }