Compare commits

..
Author SHA1 Message Date
kartik-mem0 3e33f9cca2 chore: adding test 2026-03-16 14:34:07 +05:30
kartik-mem0andClaude Opus 4.6 1584e24d6e fix: skip telemetry vector store init when MEM0_TELEMETRY is disabled
The mem0migrations vector store was always initialized regardless of
the MEM0_TELEMETRY setting, creating unnecessary connections and
collections on remote vector stores for users who opted out.

Closes #4200

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 14:18:45 +05:30
Saket AryanandGitHub 06ee1b588c fix(openclaw): point plugin extension entry to built output for npm compatibility (#4340) 2026-03-15 04:41:11 +05:30
Saket AryanandGitHub dc6122ec3d chore(openclaw): add tsup build pipeline with ESM output and type declarations (#4335) 2026-03-15 00:41:07 +05:30
11 changed files with 4272 additions and 357 deletions
+34 -33
View File
@@ -24,7 +24,7 @@ from mem0.exceptions import ValidationError as Mem0ValidationError
from mem0.memory.base import MemoryBase
from mem0.memory.setup import mem0_dir, setup_config
from mem0.memory.storage import SQLiteManager
from mem0.memory.telemetry import capture_event
from mem0.memory.telemetry import MEM0_TELEMETRY, capture_event
from mem0.memory.utils import (
extract_json,
get_fact_retrieval_messages,
@@ -37,8 +37,8 @@ from mem0.utils.factory import (
EmbedderFactory,
GraphStoreFactory,
LlmFactory,
VectorStoreFactory,
RerankerFactory,
VectorStoreFactory,
)
# Suppress SWIG deprecation warnings globally
@@ -204,32 +204,33 @@ class Memory(MemoryBase):
self.enable_graph = True
else:
self.graph = None
# Create telemetry config manually to avoid deepcopy issues with thread locks
telemetry_config_dict = {}
if hasattr(self.config.vector_store.config, 'model_dump'):
# For pydantic models
telemetry_config_dict = self.config.vector_store.config.model_dump()
else:
# For other objects, manually copy common attributes
for attr in ['host', 'port', 'path', 'api_key', 'index_name', 'dimension', 'metric']:
if hasattr(self.config.vector_store.config, attr):
telemetry_config_dict[attr] = getattr(self.config.vector_store.config, attr)
if MEM0_TELEMETRY:
# Create telemetry config manually to avoid deepcopy issues with thread locks
telemetry_config_dict = {}
if hasattr(self.config.vector_store.config, 'model_dump'):
# For pydantic models
telemetry_config_dict = self.config.vector_store.config.model_dump()
else:
# For other objects, manually copy common attributes
for attr in ['host', 'port', 'path', 'api_key', 'index_name', 'dimension', 'metric']:
if hasattr(self.config.vector_store.config, attr):
telemetry_config_dict[attr] = getattr(self.config.vector_store.config, attr)
# Override collection name for telemetry
telemetry_config_dict['collection_name'] = "mem0migrations"
# Override collection name for telemetry
telemetry_config_dict['collection_name'] = "mem0migrations"
# Set path for file-based vector stores
telemetry_config = _safe_deepcopy_config(self.config.vector_store.config)
if self.config.vector_store.provider in ["faiss", "qdrant"]:
provider_path = f"migrations_{self.config.vector_store.provider}"
telemetry_config_dict['path'] = os.path.join(mem0_dir, provider_path)
os.makedirs(telemetry_config_dict['path'], exist_ok=True)
# Set path for file-based vector stores
telemetry_config = _safe_deepcopy_config(self.config.vector_store.config)
if self.config.vector_store.provider in ["faiss", "qdrant"]:
provider_path = f"migrations_{self.config.vector_store.provider}"
telemetry_config_dict['path'] = os.path.join(mem0_dir, provider_path)
os.makedirs(telemetry_config_dict['path'], exist_ok=True)
# Create the config object using the same class as the original
telemetry_config = self.config.vector_store.config.__class__(**telemetry_config_dict)
self._telemetry_vector_store = VectorStoreFactory.create(
self.config.vector_store.provider, telemetry_config
)
# Create the config object using the same class as the original
telemetry_config = self.config.vector_store.config.__class__(**telemetry_config_dict)
self._telemetry_vector_store = VectorStoreFactory.create(
self.config.vector_store.provider, telemetry_config
)
capture_event("mem0.init", self, {"sync_type": "sync"})
@classmethod
@@ -1272,14 +1273,14 @@ class AsyncMemory(MemoryBase):
else:
self.graph = None
telemetry_config = _safe_deepcopy_config(self.config.vector_store.config)
telemetry_config.collection_name = "mem0migrations"
if self.config.vector_store.provider in ["faiss", "qdrant"]:
provider_path = f"migrations_{self.config.vector_store.provider}"
telemetry_config.path = os.path.join(mem0_dir, provider_path)
os.makedirs(telemetry_config.path, exist_ok=True)
self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config)
if MEM0_TELEMETRY:
telemetry_config = _safe_deepcopy_config(self.config.vector_store.config)
telemetry_config.collection_name = "mem0migrations"
if self.config.vector_store.provider in ["faiss", "qdrant"]:
provider_path = f"migrations_{self.config.vector_store.provider}"
telemetry_config.path = os.path.join(mem0_dir, provider_path)
os.makedirs(telemetry_config.path, exist_ok=True)
self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config)
capture_event("mem0.init", self, {"sync_type": "async"})
@classmethod
+2
View File
@@ -0,0 +1,2 @@
package-manager-strict-version=false
approve-builds=esbuild
+6 -34
View File
@@ -41,7 +41,6 @@ type Mem0Config = {
vectorStore?: { provider: string; config: Record<string, unknown> };
llm?: { provider: string; config: Record<string, unknown> };
historyDbPath?: string;
disableHistory?: boolean;
};
// Shared
userId: string;
@@ -133,16 +132,13 @@ class PlatformProvider implements Mem0Provider {
private async ensureClient(): Promise<void> {
if (this.client) return;
if (this.initPromise) return this.initPromise;
this.initPromise = this._init().catch((err) => {
this.initPromise = null;
throw err;
});
this.initPromise = this._init();
return this.initPromise;
}
private async _init(): Promise<void> {
const { default: MemoryClient } = await import("mem0ai");
const opts: Record<string, string> = { apiKey: this.apiKey };
const opts: { apiKey: string; org_id?: string; project_id?: string } = { apiKey: this.apiKey };
if (this.orgId) opts.org_id = this.orgId;
if (this.projectId) opts.project_id = this.projectId;
this.client = new MemoryClient(opts);
@@ -229,10 +225,7 @@ class OSSProvider implements Mem0Provider {
private async ensureMemory(): Promise<void> {
if (this.memory) return;
if (this.initPromise) return this.initPromise;
this.initPromise = this._init().catch((err) => {
this.initPromise = null;
throw err;
});
this.initPromise = this._init();
return this.initPromise;
}
@@ -253,30 +246,9 @@ class OSSProvider implements Mem0Provider {
config.historyDbPath = dbPath;
}
if (this.ossConfig?.disableHistory) {
config.disableHistory = true;
}
if (this.customPrompt) config.customPrompt = this.customPrompt;
try {
this.memory = new Memory(config);
} catch (err) {
// If initialization fails (e.g. native SQLite binding resolution under
// jiti), retry with history disabled — the history DB is the most common
// source of native-binding failures and is not required for core
// memory operations.
if (!config.disableHistory) {
console.warn(
"[mem0] Memory initialization failed, retrying with history disabled:",
err instanceof Error ? err.message : err,
);
config.disableHistory = true;
this.memory = new Memory(config);
} else {
throw err;
}
}
this.memory = new Memory(config);
}
async add(
@@ -549,7 +521,7 @@ function assertAllowedKeys(
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
}
export const mem0ConfigSchema = {
const mem0ConfigSchema = {
parse(value: unknown): Mem0Config {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("openclaw-mem0 config required");
@@ -615,7 +587,7 @@ export const mem0ConfigSchema = {
// Provider Factory
// ============================================================================
export function createProvider(
function createProvider(
cfg: Mem0Config,
api: OpenClawPluginApi,
): Mem0Provider {
+30
View File
@@ -0,0 +1,30 @@
declare module "openclaw/plugin-sdk" {
export interface OpenClawPluginApi {
pluginConfig: Record<string, unknown>;
logger: {
info(msg: string): void;
warn(msg: string): void;
error(msg: string): void;
debug(msg: string): void;
};
resolvePath(p: string): string;
registerTool(
definition: Record<string, unknown>,
metadata?: Record<string, unknown>,
): void;
on(
event: string,
handler: (event: any, ctx: any) => any,
): void;
registerCli(
handler: (context: { program: any }) => void,
options?: Record<string, unknown>,
): void;
registerService(service: {
id: string;
start: () => void;
stop: () => void;
}): void;
[key: string]: unknown;
}
}
+18 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "0.3.1",
"version": "0.3.3",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
@@ -11,7 +11,20 @@
"mem0",
"long-term-memory"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"openclaw.plugin.json"
],
"scripts": {
"build": "tsup",
"test": "vitest run"
},
"dependencies": {
@@ -20,10 +33,13 @@
},
"openclaw": {
"extensions": [
"./index.ts"
"./dist/index.js"
]
},
"devDependencies": {
"@types/node": "^22.15.0",
"tsup": "^8.5.0",
"typescript": "^5.8.3",
"vitest": "^4.0.18"
}
}
+4105
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
approveBuilds: esbuild
onlyBuiltDependencies:
- better-sqlite3
- esbuild
- protobufjs
-288
View File
@@ -1,288 +0,0 @@
/**
* Tests for SQLite resilience fixes:
* 1. disableHistory config passthrough
* 2. initPromise poisoning fix (retry after failure)
* 3. Graceful SQLite fallback in OSSProvider
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mem0ConfigSchema, createProvider } from "./index.ts";
// ---------------------------------------------------------------------------
// 1. Config: disableHistory passthrough
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema — disableHistory", () => {
const baseConfig = {
mode: "open-source",
oss: {
embedder: { provider: "openai", config: { apiKey: "sk-test" } },
},
};
it("preserves oss.disableHistory: true through config parsing", () => {
const cfg = mem0ConfigSchema.parse({
...baseConfig,
oss: { ...baseConfig.oss, disableHistory: true },
});
expect(cfg.oss?.disableHistory).toBe(true);
});
it("preserves oss.disableHistory: false through config parsing", () => {
const cfg = mem0ConfigSchema.parse({
...baseConfig,
oss: { ...baseConfig.oss, disableHistory: false },
});
expect(cfg.oss?.disableHistory).toBe(false);
});
it("omits disableHistory when not provided", () => {
const cfg = mem0ConfigSchema.parse(baseConfig);
expect(cfg.oss?.disableHistory).toBeUndefined();
});
it("does not reject unknown keys inside oss object", () => {
// oss sub-object is passed through resolveEnvVarsDeep, not key-checked
expect(() =>
mem0ConfigSchema.parse({
...baseConfig,
oss: { ...baseConfig.oss, disableHistory: true },
}),
).not.toThrow();
});
});
// ---------------------------------------------------------------------------
// 2. OSSProvider: disableHistory flows to Memory constructor
// ---------------------------------------------------------------------------
describe("OSSProvider — disableHistory passthrough to Memory", () => {
let capturedConfig: Record<string, unknown> | undefined;
let memoryCallCount: number;
beforeEach(() => {
capturedConfig = undefined;
memoryCallCount = 0;
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
memoryCallCount++;
capturedConfig = { ...config };
}
async add() { return { results: [] }; }
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async delete() { }
},
}));
});
it("passes disableHistory: true to Memory when configured", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
// Trigger lazy init by calling search
try {
await provider.search("test", { user_id: "u1" });
} catch { /* provider may fail on mock, that's ok */ }
expect(capturedConfig).toBeDefined();
expect(capturedConfig!.disableHistory).toBe(true);
});
it("does not set disableHistory when not configured", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {},
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
try {
await provider.search("test", { user_id: "u1" });
} catch { }
expect(capturedConfig).toBeDefined();
expect(capturedConfig!.disableHistory).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// 3. OSSProvider: initPromise is cleared on failure (allows retry)
// ---------------------------------------------------------------------------
describe("OSSProvider — initPromise retry after failure", () => {
let callCount: number;
beforeEach(() => {
callCount = 0;
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor() {
callCount++;
if (callCount === 1) {
throw new Error("SQLITE_CANTOPEN: simulated binding failure");
}
// Second+ call succeeds
}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() { }
},
}));
});
it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
// First call: _init throws, but initPromise is cleared so retry is possible
await expect(
provider.search("test", { user_id: "u1" }),
).rejects.toThrow("SQLITE_CANTOPEN");
// Second call: should retry _init (not return cached rejection)
// callCount === 1 threw, so callCount === 2 should succeed
const results = await provider.search("test", { user_id: "u1" });
expect(results).toBeDefined();
expect(callCount).toBe(2);
});
});
// ---------------------------------------------------------------------------
// 4. OSSProvider: graceful fallback disables history on init failure
// ---------------------------------------------------------------------------
describe("OSSProvider — graceful SQLite fallback", () => {
let capturedConfigs: Record<string, unknown>[];
beforeEach(() => {
capturedConfigs = [];
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfigs.push({ ...config });
if (!config.disableHistory) {
throw new Error("Could not locate the bindings file");
}
// Succeeds when disableHistory is true
}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() { }
},
}));
});
it("retries with disableHistory: true when initial construction fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {},
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
// Should succeed — first attempt fails, fallback with disableHistory succeeds
const results = await provider.search("test", { user_id: "u1" });
expect(results).toBeDefined();
// Memory constructor was called twice
expect(capturedConfigs).toHaveLength(2);
expect(capturedConfigs[0].disableHistory).toBeFalsy();
expect(capturedConfigs[1].disableHistory).toBe(true);
// Warning was logged
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("[mem0] Memory initialization failed"),
expect.stringContaining("bindings file"),
);
warnSpy.mockRestore();
});
it("does not retry when disableHistory is already true", async () => {
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
// Fail even with disableHistory (e.g. vector store issue)
throw new Error("vector store connection refused");
}
},
}));
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
// Should throw — no fallback possible when disableHistory was already set
await expect(
provider.search("test", { user_id: "u1" }),
).rejects.toThrow("vector store connection refused");
});
});
// ---------------------------------------------------------------------------
// 5. PlatformProvider — initPromise retry after failure
// ---------------------------------------------------------------------------
describe("PlatformProvider — initPromise retry after failure", () => {
let callCount: number;
beforeEach(() => {
callCount = 0;
vi.doMock("mem0ai", () => ({
default: class MockMemoryClient {
constructor() {
callCount++;
if (callCount === 1) {
throw new Error("Network timeout");
}
}
async search() { return []; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() { }
},
}));
});
it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "platform",
apiKey: "test-api-key",
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
// First call fails
await expect(
provider.search("test", { user_id: "u1" }),
).rejects.toThrow("Network timeout");
// Second call should retry (not return cached rejection)
const results = await provider.search("test", { user_id: "u1" });
expect(results).toBeDefined();
expect(callCount).toBe(2);
});
});
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": ".",
"strict": false,
"noImplicitAny": false,
"types": ["node"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"include": ["index.ts", "openclaw-plugin-sdk.d.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["index.ts"],
format: ["esm"],
dts: true,
sourcemap: true,
clean: true,
});
+40
View File
@@ -296,3 +296,43 @@ def test_custom_prompts(memory_custom_instance):
messages=[{"role": "user", "content": mock_get_update_memory_messages.return_value}],
response_format={"type": "json_object"},
)
def test_no_telemetry_vector_store_when_disabled():
"""VectorStoreFactory should only be called once (for user data) when telemetry is disabled."""
with (
patch("mem0.memory.main.MEM0_TELEMETRY", False),
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store,
patch("mem0.utils.factory.LlmFactory") as mock_llm,
patch("mem0.memory.telemetry.capture_event"),
):
mock_embedder.create.return_value = Mock()
mock_vector_store.create.return_value = Mock()
mock_llm.create.return_value = Mock()
config = MemoryConfig(version="v1.1")
Memory(config)
# VectorStoreFactory.create should be called exactly once — for user data only, not telemetry
assert mock_vector_store.create.call_count == 1
def test_telemetry_vector_store_created_when_enabled():
"""VectorStoreFactory should be called twice (user data + telemetry) when telemetry is enabled."""
with (
patch("mem0.memory.main.MEM0_TELEMETRY", True),
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store,
patch("mem0.utils.factory.LlmFactory") as mock_llm,
patch("mem0.memory.telemetry.capture_event"),
):
mock_embedder.create.return_value = Mock()
mock_vector_store.create.return_value = Mock()
mock_llm.create.return_value = Mock()
config = MemoryConfig(version="v1.1")
Memory(config)
# VectorStoreFactory.create should be called twice — user data + telemetry
assert mock_vector_store.create.call_count == 2