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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user