refactor: drop orgId, projectId, enableGraph config options, update CLI prompts, and clean up related code (#4734)

This commit is contained in:
Kartik
2026-04-09 14:57:33 +05:30
committed by GitHub
parent 081eca6d8f
commit 88f696a60a
20 changed files with 624 additions and 206 deletions
+10
View File
@@ -2,6 +2,16 @@
All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file. All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file.
## [1.0.5] - 2026-04-07
### Fixed
- **Init interactive choice bug**: Fixed number selection in `openclaw mem0 init` — entering 1/2/3 now correctly selects the corresponding option (was broken by readline prefill concatenating with user input)
- **OSS pgvector crash** ([#4727]): Fixed "Client has already been connected" cascade when using pgvector in OSS mode. The warmup call swallowed errors leaving a half-initialized pg client; concurrent recall/capture then all hit `client.connect()` on the same client. Fix: let warmup errors propagate (so `initPromise` resets and retries with a fresh Memory + fresh pg client) and build fresh config objects per attempt instead of mutating shared state.
### Removed
- **`orgId` / `projectId` config parameters**: Removed from config schema, CLI (`config show/get/set`), init display, and providers. The API key is project-scoped, so separate org/project IDs are unnecessary and could cause access errors if mismatched.
- **`enableGraph` config parameter**: Removed from all config surfaces, providers, backend, and tools. Graph memory is being deprecated — removing the flag avoids unnecessary exposure.
## [1.0.4] - 2026-04-04 ## [1.0.4] - 2026-04-04
### Added ### Added
-3
View File
@@ -14,7 +14,6 @@ export interface AddOptions {
infer?: boolean; infer?: boolean;
expires?: string; expires?: string;
categories?: string[]; categories?: string[];
enableGraph?: boolean;
} }
export interface SearchOptions { export interface SearchOptions {
@@ -28,7 +27,6 @@ export interface SearchOptions {
keyword?: boolean; keyword?: boolean;
filters?: Record<string, unknown>; filters?: Record<string, unknown>;
fields?: string[]; fields?: string[];
enableGraph?: boolean;
} }
export interface ListOptions { export interface ListOptions {
@@ -41,7 +39,6 @@ export interface ListOptions {
category?: string; category?: string;
after?: string; after?: string;
before?: string; before?: string;
enableGraph?: boolean;
} }
export interface DeleteOptions { export interface DeleteOptions {
-3
View File
@@ -111,7 +111,6 @@ export class PlatformBackend implements Backend {
if (opts.infer === false) payload.infer = false; if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires; if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories; if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
return (await this._request("POST", "/v1/memories/", { return (await this._request("POST", "/v1/memories/", {
json: payload, json: payload,
@@ -171,7 +170,6 @@ export class PlatformBackend implements Backend {
if (opts.rerank) payload.rerank = true; if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true; if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields; if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/search/", { const result = (await this._request("POST", "/v2/memories/search/", {
json: payload, json: payload,
@@ -222,7 +220,6 @@ export class PlatformBackend implements Backend {
extraFilters: Object.keys(extra).length > 0 ? extra : undefined, extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
}); });
if (apiFilters) payload.filters = apiFilters; if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/", { const result = (await this._request("POST", "/v2/memories/", {
json: payload, json: payload,
+12 -16
View File
@@ -384,10 +384,6 @@ export function registerCliCommands(
console.log(` User ID: ${existingAuth.userId}`); console.log(` User ID: ${existingAuth.userId}`);
if (existingAuth.mode) if (existingAuth.mode)
console.log(` Mode: ${existingAuth.mode}`); console.log(` Mode: ${existingAuth.mode}`);
if (existingAuth.orgId)
console.log(` Org ID: ${existingAuth.orgId}`);
if (existingAuth.projectId)
console.log(` Project: ${existingAuth.projectId}`);
console.log(""); console.log("");
// Validate existing key before asking // Validate existing key before asking
@@ -408,7 +404,7 @@ export function registerCliCommands(
} }
const reuse = await promptInput( const reuse = await promptInput(
" Keep existing configuration? (Y/n): ", " Keep existing configuration? (y/n): ",
); );
if ( if (
reuse === "" || reuse === "" ||
@@ -432,7 +428,7 @@ export function registerCliCommands(
console.log(" 2. Enter API key manually"); console.log(" 2. Enter API key manually");
console.log(" 3. Open-source mode (self-hosted)\n"); console.log(" 3. Open-source mode (self-hosted)\n");
const choice = (await promptInput(" Choice: ", "1")) || "1"; const choice = (await promptInput(" Choice (1/2/3): ")) || "1";
if (choice === "1") { if (choice === "1") {
// --- Email interactive flow --- // --- Email interactive flow ---
@@ -952,9 +948,6 @@ export function registerCliCommands(
email: "userEmail", email: "userEmail",
base_url: "baseUrl", base_url: "baseUrl",
user_id: "userId", user_id: "userId",
org_id: "orgId",
project_id: "projectId",
enable_graph: "enableGraph",
auto_recall: "autoRecall", auto_recall: "autoRecall",
auto_capture: "autoCapture", auto_capture: "autoCapture",
top_k: "topK", top_k: "topK",
@@ -969,6 +962,8 @@ export function registerCliCommands(
vector_host: "oss.vectorStore.config.host", vector_host: "oss.vectorStore.config.host",
vector_port: "oss.vectorStore.config.port", vector_port: "oss.vectorStore.config.port",
collection_name: "oss.vectorStore.config.collectionName", collection_name: "oss.vectorStore.config.collectionName",
vector_db_name: "oss.vectorStore.config.dbname",
vector_db_user: "oss.vectorStore.config.user",
vector_db_path: "oss.vectorStore.config.dbPath", vector_db_path: "oss.vectorStore.config.dbPath",
history_db_path: "oss.historyDbPath", history_db_path: "oss.historyDbPath",
disable_history: "oss.disableHistory", disable_history: "oss.disableHistory",
@@ -979,7 +974,6 @@ export function registerCliCommands(
// Boolean config fields — coerce "true"/"1"/"yes" on set // Boolean config fields — coerce "true"/"1"/"yes" on set
const BOOLEAN_KEYS = new Set([ const BOOLEAN_KEYS = new Set([
"enableGraph",
"autoRecall", "autoRecall",
"autoCapture", "autoCapture",
"oss.disableHistory", "oss.disableHistory",
@@ -1009,11 +1003,8 @@ export function registerCliCommands(
apiKey: auth.apiKey ?? cfg.apiKey, apiKey: auth.apiKey ?? cfg.apiKey,
baseUrl: auth.baseUrl ?? cfg.baseUrl ?? "https://api.mem0.ai", baseUrl: auth.baseUrl ?? cfg.baseUrl ?? "https://api.mem0.ai",
userId: auth.userId ?? cfg.userId, userId: auth.userId ?? cfg.userId,
orgId: auth.orgId ?? cfg.orgId,
projectId: auth.projectId ?? cfg.projectId,
mode: auth.mode ?? cfg.mode, mode: auth.mode ?? cfg.mode,
userEmail: auth.userEmail, userEmail: auth.userEmail,
enableGraph: cfg.enableGraph,
autoRecall: cfg.autoRecall, autoRecall: cfg.autoRecall,
autoCapture: cfg.autoCapture, autoCapture: cfg.autoCapture,
topK: cfg.topK, topK: cfg.topK,
@@ -1055,9 +1046,6 @@ export function registerCliCommands(
entries.push( entries.push(
["api_key", "apiKey"], ["api_key", "apiKey"],
["email", "userEmail"], ["email", "userEmail"],
["org_id", "orgId"],
["project_id", "projectId"],
["enable_graph", "enableGraph"],
); );
} else { } else {
entries.push( entries.push(
@@ -1238,6 +1226,10 @@ export function registerCliCommands(
.description("List recent background events") .description("List recent background events")
.action(async () => { .action(async () => {
try { try {
if (!backend || cfg.mode === "open-source") {
console.log("Event tracking is only available in platform mode.");
return;
}
const results = await backend.listEvents(); const results = await backend.listEvents();
if (!results.length) { if (!results.length) {
console.log("No events found."); console.log("No events found.");
@@ -1282,6 +1274,10 @@ export function registerCliCommands(
.argument("<event_id>", "Event ID to check") .argument("<event_id>", "Event ID to check")
.action(async (eventId: string) => { .action(async (eventId: string) => {
try { try {
if (!backend || cfg.mode === "open-source") {
console.log("Event tracking is only available in platform mode.");
return;
}
const ev = await backend.getEvent(eventId); const ev = await backend.getEvent(eventId);
const status = String(ev.status ?? "—"); const status = String(ev.status ?? "—");
-6
View File
@@ -29,11 +29,8 @@ export interface PluginAuthConfig {
apiKey?: string; apiKey?: string;
baseUrl?: string; baseUrl?: string;
userId?: string; userId?: string;
orgId?: string;
projectId?: string;
userEmail?: string; userEmail?: string;
mode?: string; mode?: string;
enableGraph?: boolean;
autoRecall?: boolean; autoRecall?: boolean;
autoCapture?: boolean; autoCapture?: boolean;
topK?: number; topK?: number;
@@ -76,11 +73,8 @@ export function readPluginAuth(): PluginAuthConfig {
apiKey: (cfg.apiKey ?? cfg.api_key) as string | undefined, apiKey: (cfg.apiKey ?? cfg.api_key) as string | undefined,
baseUrl: (cfg.baseUrl ?? cfg.base_url) as string | undefined, baseUrl: (cfg.baseUrl ?? cfg.base_url) as string | undefined,
userId: (cfg.userId ?? cfg.user_id) as string | undefined, userId: (cfg.userId ?? cfg.user_id) as string | undefined,
orgId: (cfg.orgId ?? cfg.org_id) as string | undefined,
projectId: (cfg.projectId ?? cfg.project_id) as string | undefined,
userEmail: (cfg.userEmail ?? cfg.user_email) as string | undefined, userEmail: (cfg.userEmail ?? cfg.user_email) as string | undefined,
mode: cfg.mode as string | undefined, mode: cfg.mode as string | undefined,
enableGraph: cfg.enableGraph as boolean | undefined,
autoRecall: cfg.autoRecall as boolean | undefined, autoRecall: cfg.autoRecall as boolean | undefined,
autoCapture: cfg.autoCapture as boolean | undefined, autoCapture: cfg.autoCapture as boolean | undefined,
topK: cfg.topK as number | undefined, topK: cfg.topK as number | undefined,
+9 -14
View File
@@ -19,8 +19,6 @@ import type { Mem0Config, Mem0Mode } from "./types.ts";
export interface FileConfig { export interface FileConfig {
apiKey?: string; apiKey?: string;
baseUrl?: string; baseUrl?: string;
orgId?: string;
projectId?: string;
} }
// ============================================================================ // ============================================================================
@@ -152,14 +150,11 @@ const ALLOWED_KEYS = [
"baseUrl", "baseUrl",
"userId", "userId",
"userEmail", "userEmail",
"orgId",
"projectId",
"autoCapture", "autoCapture",
"autoRecall", "autoRecall",
"customInstructions", "customInstructions",
"customCategories", "customCategories",
"customPrompt", "customPrompt",
"enableGraph",
"searchThreshold", "searchThreshold",
"topK", "topK",
"oss", "oss",
@@ -185,6 +180,15 @@ export const mem0ConfigSchema = {
assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-mem0 config"); assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-mem0 config");
// Only two modes: "platform" (default) or "open-source" // Only two modes: "platform" (default) or "open-source"
if (
typeof cfg.mode === "string" &&
cfg.mode !== "platform" &&
cfg.mode !== "open-source"
) {
console.warn(
`[mem0] Unknown mode "${cfg.mode}" — expected "platform" or "open-source". Defaulting to "platform".`,
);
}
const mode: Mem0Mode = const mode: Mem0Mode =
cfg.mode === "open-source" ? "open-source" : "platform"; cfg.mode === "open-source" ? "open-source" : "platform";
@@ -193,15 +197,9 @@ export const mem0ConfigSchema = {
typeof cfg.apiKey === "string" ? cfg.apiKey : undefined; typeof cfg.apiKey === "string" ? cfg.apiKey : undefined;
let resolvedBaseUrl = let resolvedBaseUrl =
typeof cfg.baseUrl === "string" ? cfg.baseUrl : undefined; typeof cfg.baseUrl === "string" ? cfg.baseUrl : undefined;
let resolvedOrgId = typeof cfg.orgId === "string" ? cfg.orgId : undefined;
let resolvedProjectId =
typeof cfg.projectId === "string" ? cfg.projectId : undefined;
if (mode === "platform" && !resolvedApiKey && fileConfig) { if (mode === "platform" && !resolvedApiKey && fileConfig) {
if (fileConfig.apiKey) resolvedApiKey = fileConfig.apiKey; if (fileConfig.apiKey) resolvedApiKey = fileConfig.apiKey;
if (fileConfig.baseUrl) resolvedBaseUrl = fileConfig.baseUrl; if (fileConfig.baseUrl) resolvedBaseUrl = fileConfig.baseUrl;
if (!resolvedOrgId && fileConfig.orgId) resolvedOrgId = fileConfig.orgId;
if (!resolvedProjectId && fileConfig.projectId)
resolvedProjectId = fileConfig.projectId;
} }
// Platform mode requires apiKey — but don't throw on missing config. // Platform mode requires apiKey — but don't throw on missing config.
@@ -228,8 +226,6 @@ export const mem0ConfigSchema = {
return "default"; return "default";
} }
})(), })(),
orgId: resolvedOrgId,
projectId: resolvedProjectId,
autoCapture: cfg.autoCapture !== false, autoCapture: cfg.autoCapture !== false,
autoRecall: cfg.autoRecall !== false, autoRecall: cfg.autoRecall !== false,
customInstructions: customInstructions:
@@ -246,7 +242,6 @@ export const mem0ConfigSchema = {
typeof cfg.customPrompt === "string" typeof cfg.customPrompt === "string"
? cfg.customPrompt ? cfg.customPrompt
: DEFAULT_CUSTOM_INSTRUCTIONS, : DEFAULT_CUSTOM_INSTRUCTIONS,
enableGraph: cfg.enableGraph === true,
searchThreshold: searchThreshold:
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5, typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5,
topK: typeof cfg.topK === "number" ? cfg.topK : 5, topK: typeof cfg.topK === "number" ? cfg.topK : 5,
+58 -28
View File
@@ -103,15 +103,21 @@ const memoryPlugin = definePluginEntry({
const fileConfig: FileConfig = { const fileConfig: FileConfig = {
apiKey: pluginAuth.apiKey, apiKey: pluginAuth.apiKey,
baseUrl: pluginAuth.baseUrl, baseUrl: pluginAuth.baseUrl,
orgId: pluginAuth.orgId,
projectId: pluginAuth.projectId,
}; };
const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig); const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig);
// Telemetry context bound to this plugin instance's config // Telemetry context bound to this plugin instance's config
const telemetryCtx = { apiKey: cfg.apiKey, mode: cfg.mode, skillsActive: false }; const telemetryCtx = {
apiKey: cfg.apiKey,
mode: cfg.mode,
skillsActive: false,
};
const _captureEvent = (event: string, props?: Record<string, unknown>) => { const _captureEvent = (event: string, props?: Record<string, unknown>) => {
try { captureEvent(event, props, telemetryCtx); } catch { /* silently swallow */ } try {
captureEvent(event, props, telemetryCtx);
} catch {
/* silently swallow */
}
}; };
if (cfg.needsSetup) { if (cfg.needsSetup) {
@@ -182,7 +188,7 @@ const memoryPlugin = definePluginEntry({
}); });
api.logger.info( api.logger.info(
`openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, graph: ${cfg.enableGraph}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`, `openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`,
); );
// Helper: build add options // Helper: build add options
@@ -197,7 +203,6 @@ const memoryPlugin = definePluginEntry({
}; };
if (runId) opts.run_id = runId; if (runId) opts.run_id = runId;
if (cfg.mode === "platform") { if (cfg.mode === "platform") {
opts.enable_graph = cfg.enableGraph;
opts.output_format = "v1.1"; opts.output_format = "v1.1";
} }
return opts; return opts;
@@ -242,7 +247,10 @@ const memoryPlugin = definePluginEntry({
getCurrentSessionId: () => currentSessionId, getCurrentSessionId: () => currentSessionId,
skillsActive, skillsActive,
captureToolEvent: (toolName: string, props: Record<string, unknown>) => { captureToolEvent: (toolName: string, props: Record<string, unknown>) => {
_captureEvent(`openclaw.tool.${toolName}`, { tool_name: toolName, ...props }); _captureEvent(`openclaw.tool.${toolName}`, {
tool_name: toolName,
...props,
});
}, },
}; };
registerAllTools(toolDeps); registerAllTools(toolDeps);
@@ -328,7 +336,10 @@ function registerHooks(
getStateDir: () => string | undefined; getStateDir: () => string | undefined;
}, },
skillsActive: boolean = false, skillsActive: boolean = false,
_captureEvent: (event: string, props?: Record<string, unknown>) => void = () => {}, _captureEvent: (
event: string,
props?: Record<string, unknown>,
) => void = () => {},
) { ) {
// ======================================================================== // ========================================================================
// SKILLS MODE: Agentic memory via before_prompt_build // SKILLS MODE: Agentic memory via before_prompt_build
@@ -355,16 +366,17 @@ function registerHooks(
return; return;
} }
// Skip recall for system/bootstrap prompts. These are OpenClaw internal
// commands (/new, /reset) that contain system instructions, not user queries.
// Sending them to mem0 search wastes API calls and returns noise.
const promptLower = event.prompt.toLowerCase(); const promptLower = event.prompt.toLowerCase();
const isChannelSystemEvent = /^system(?:\s*\(untrusted\))?:\s*\[/i.test(
event.prompt,
);
const isSystemPrompt = const isSystemPrompt =
promptLower.includes("a new session was started") || !isChannelSystemEvent &&
promptLower.includes("session startup sequence") || (promptLower.includes("a new session was started") ||
promptLower.includes("/new or /reset") || promptLower.includes("session startup sequence") ||
promptLower.startsWith("system:") || promptLower.includes("/new or /reset") ||
promptLower.startsWith("run your session"); promptLower.startsWith("system:") ||
promptLower.startsWith("run your session"));
if (isSystemPrompt) { if (isSystemPrompt) {
api.logger.info( api.logger.info(
"openclaw-mem0: skills-mode skipping recall for system/bootstrap prompt", "openclaw-mem0: skills-mode skipping recall for system/bootstrap prompt",
@@ -472,7 +484,10 @@ function registerHooks(
"\n</auto-dream>"; "\n</auto-dream>";
// Track which session triggered dream (session-keyed, not global) // Track which session triggered dream (session-keyed, not global)
dreamSessionId = sessionId; dreamSessionId = sessionId;
_captureEvent("openclaw.hook.dream", { phase: "triggered", memory_count: memCount }); _captureEvent("openclaw.hook.dream", {
phase: "triggered",
memory_count: memCount,
});
api.logger.info( api.logger.info(
`openclaw-mem0: auto-dream triggered (${memCount} memories, gate passed)`, `openclaw-mem0: auto-dream triggered (${memCount} memories, gate passed)`,
); );
@@ -545,7 +560,10 @@ function registerHooks(
if (writeToolUsed) { if (writeToolUsed) {
releaseDreamLock(stateDir); releaseDreamLock(stateDir);
recordDreamCompletion(stateDir); recordDreamCompletion(stateDir);
_captureEvent("openclaw.hook.dream", { phase: "completed", write_tools_used: true }); _captureEvent("openclaw.hook.dream", {
phase: "completed",
write_tools_used: true,
});
api.logger.info( api.logger.info(
"openclaw-mem0: auto-dream completed (verified write tool usage), lock released", "openclaw-mem0: auto-dream completed (verified write tool usage), lock released",
); );
@@ -599,14 +617,17 @@ function registerHooks(
return; return;
} }
// Skip recall for system/bootstrap prompts to save API calls
const promptLower = event.prompt.toLowerCase(); const promptLower = event.prompt.toLowerCase();
const isChannelSystemEvent = /^system(?:\s*\(untrusted\))?:\s*\[/i.test(
event.prompt,
);
const isSystemPrompt = const isSystemPrompt =
promptLower.includes("a new session was started") || !isChannelSystemEvent &&
promptLower.includes("session startup sequence") || (promptLower.includes("a new session was started") ||
promptLower.includes("/new or /reset") || promptLower.includes("session startup sequence") ||
promptLower.startsWith("system:") || promptLower.includes("/new or /reset") ||
promptLower.startsWith("run your session"); promptLower.startsWith("system:") ||
promptLower.startsWith("run your session"));
if (isSystemPrompt) { if (isSystemPrompt) {
api.logger.info( api.logger.info(
"openclaw-mem0: skipping recall for system/bootstrap prompt", "openclaw-mem0: skipping recall for system/bootstrap prompt",
@@ -775,11 +796,17 @@ function registerHooks(
// Update shared state for tools (best-effort — tools don't have ctx) // Update shared state for tools (best-effort — tools don't have ctx)
if (sessionId) session.setCurrentSessionId(sessionId); if (sessionId) session.setCurrentSessionId(sessionId);
const MEMORY_MUTATE_TOOLS = new Set(["memory_add", "memory_update", "memory_delete"]); const MEMORY_MUTATE_TOOLS = new Set([
"memory_add",
"memory_update",
"memory_delete",
]);
const agentUsedMemoryTool = event.messages.some((msg: any) => { const agentUsedMemoryTool = event.messages.some((msg: any) => {
if (msg?.role !== "assistant" || !Array.isArray(msg?.content)) return false; if (msg?.role !== "assistant" || !Array.isArray(msg?.content))
return false;
return msg.content.some( return msg.content.some(
(block: any) => block?.type === "tool_use" && MEMORY_MUTATE_TOOLS.has(block.name), (block: any) =>
block?.type === "tool_use" && MEMORY_MUTATE_TOOLS.has(block.name),
); );
}); });
if (agentUsedMemoryTool) { if (agentUsedMemoryTool) {
@@ -848,7 +875,10 @@ function registerHooks(
if (!textContent) continue; if (!textContent) continue;
} }
// Strip OpenClaw sender metadata prefix (prevents storing TUI identity as memory) // Strip OpenClaw sender metadata prefix (prevents storing TUI identity as memory)
if (textContent.includes("Sender") && textContent.includes("untrusted metadata")) { if (
textContent.includes("Sender") &&
textContent.includes("untrusted metadata")
) {
textContent = textContent textContent = textContent
.replace( .replace(
/Sender\s*\(untrusted metadata\):\s*```json[\s\S]*?```\s*/gi, /Sender\s*\(untrusted metadata\):\s*```json[\s\S]*?```\s*/gi,
+1
View File
@@ -2,6 +2,7 @@
"id": "openclaw-mem0", "id": "openclaw-mem0",
"name": "Memory (Mem0)", "name": "Memory (Mem0)",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source", "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"version": "1.0.5",
"kind": "memory", "kind": "memory",
"skills": ["skills"], "skills": ["skills"],
"contracts": { "contracts": {
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@mem0/openclaw-mem0", "name": "@mem0/openclaw-mem0",
"version": "1.0.4", "version": "1.0.5",
"type": "module", "type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source", "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -42,12 +42,12 @@
"./dist/index.js" "./dist/index.js"
], ],
"compat": { "compat": {
"pluginApi": ">=2026.3.24-beta.2", "pluginApi": ">=2026.3.28 <=2026.4.2",
"minGatewayVersion": "2026.3.24-beta.2" "minGatewayVersion": "2026.3.28"
}, },
"build": { "build": {
"openclawVersion": "2026.3.24-beta.2", "openclawVersion": "2026.4.1",
"pluginSdkVersion": "2026.3.24-beta.2" "pluginSdkVersion": "2026.4.1"
} }
}, },
"devDependencies": { "devDependencies": {
+65 -44
View File
@@ -82,8 +82,6 @@ class PlatformProvider implements Mem0Provider {
constructor( constructor(
private readonly apiKey: string, private readonly apiKey: string,
private readonly baseUrl?: string, private readonly baseUrl?: string,
private readonly orgId?: string,
private readonly projectId?: string,
) {} ) {}
private async ensureClient(): Promise<void> { private async ensureClient(): Promise<void> {
@@ -101,14 +99,10 @@ class PlatformProvider implements Mem0Provider {
const opts: { const opts: {
apiKey: string; apiKey: string;
host?: string; host?: string;
organizationId?: string;
projectId?: string;
} = { } = {
apiKey: this.apiKey, apiKey: this.apiKey,
}; };
if (this.baseUrl) opts.host = this.baseUrl; if (this.baseUrl) opts.host = this.baseUrl;
if (this.orgId) opts.organizationId = this.orgId;
if (this.projectId) opts.projectId = this.projectId;
this.client = new MemoryClient(opts); this.client = new MemoryClient(opts);
} }
@@ -123,7 +117,6 @@ class PlatformProvider implements Mem0Provider {
opts.custom_instructions = options.custom_instructions; opts.custom_instructions = options.custom_instructions;
if (options.custom_categories) if (options.custom_categories)
opts.custom_categories = options.custom_categories; opts.custom_categories = options.custom_categories;
if (options.enable_graph) opts.enable_graph = options.enable_graph;
if (options.output_format) opts.output_format = options.output_format; if (options.output_format) opts.output_format = options.output_format;
if (options.source) opts.source = options.source; if (options.source) opts.source = options.source;
// Agentic harness: direct storage bypass // Agentic harness: direct storage bypass
@@ -208,9 +201,7 @@ class PlatformProvider implements Mem0Provider {
await this.client.deleteAll({ user_id: userId }); await this.client.deleteAll({ user_id: userId });
} }
async history( async history(memoryId: string): Promise<
memoryId: string,
): Promise<
Array<{ Array<{
id: string; id: string;
old_memory: string; old_memory: string;
@@ -249,23 +240,29 @@ class OSSProvider implements Mem0Provider {
return this.initPromise; return this.initPromise;
} }
private async _init(): Promise<void> { private _buildConfig(disableHistory = false): Record<string, unknown> {
const { Memory } = await import("mem0ai/oss");
const config: Record<string, unknown> = { version: "v1.1" }; const config: Record<string, unknown> = { version: "v1.1" };
const defaultEmbedder = { provider: "openai", config: { model: "text-embedding-3-small" } }; const defaultEmbedder = {
provider: "openai",
config: { model: "text-embedding-3-small" },
};
const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } }; const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } };
// Helper: strip empty-string values so they don't clobber defaults
const stripEmpty = (obj: Record<string, unknown>) => { const stripEmpty = (obj: Record<string, unknown>) => {
const out = { ...obj }; const out = { ...obj };
for (const k of Object.keys(out)) { if (out[k] === "") delete out[k]; } for (const k of Object.keys(out)) {
if (out[k] === "") delete out[k];
}
return out; return out;
}; };
if (this.ossConfig?.embedder) { if (this.ossConfig?.embedder) {
const ec = stripEmpty(this.ossConfig.embedder.config ?? {}); const ec = stripEmpty(this.ossConfig.embedder.config ?? {});
if (ec.host && !ec.url) {
ec.url = ec.host;
delete ec.host;
}
config.embedder = { config.embedder = {
provider: this.ossConfig.embedder.provider || defaultEmbedder.provider, provider: this.ossConfig.embedder.provider || defaultEmbedder.provider,
config: { ...defaultEmbedder.config, ...ec }, config: { ...defaultEmbedder.config, ...ec },
@@ -276,6 +273,10 @@ class OSSProvider implements Mem0Provider {
if (this.ossConfig?.llm) { if (this.ossConfig?.llm) {
const lc = stripEmpty(this.ossConfig.llm.config ?? {}); const lc = stripEmpty(this.ossConfig.llm.config ?? {});
if (lc.host && !lc.url) {
lc.url = lc.host;
delete lc.host;
}
config.llm = { config.llm = {
provider: this.ossConfig.llm.provider || defaultLlm.provider, provider: this.ossConfig.llm.provider || defaultLlm.provider,
config: { ...defaultLlm.config, ...lc }, config: { ...defaultLlm.config, ...lc },
@@ -285,7 +286,7 @@ class OSSProvider implements Mem0Provider {
} }
if (this.ossConfig?.vectorStore) if (this.ossConfig?.vectorStore)
config.vectorStore = this.ossConfig.vectorStore; config.vectorStore = { ...this.ossConfig.vectorStore };
if (this.ossConfig?.historyDbPath) { if (this.ossConfig?.historyDbPath) {
const dbPath = this.resolvePath const dbPath = this.resolvePath
@@ -294,42 +295,61 @@ class OSSProvider implements Mem0Provider {
config.historyDbPath = dbPath; config.historyDbPath = dbPath;
} }
if (this.ossConfig?.disableHistory) { if (disableHistory || this.ossConfig?.disableHistory) {
config.disableHistory = true; config.disableHistory = true;
} }
if (this.customPrompt) config.customPrompt = this.customPrompt; if (this.customPrompt) config.customPrompt = this.customPrompt;
return config;
}
private async _init(): Promise<void> {
const mod = await import("mem0ai/oss");
const Memory = mod.Memory;
for (const cls of ["PGVector", "RedisDB", "Qdrant"]) {
const VectorCls = (mod as any)[cls];
if (!VectorCls || VectorCls.prototype.__patched) continue;
const origInit = VectorCls.prototype.initialize;
VectorCls.prototype.initialize = function (this: any) {
if (!this.config?.embeddingModelDims && this.config?.dimension) {
this.config.embeddingModelDims = this.config.dimension;
}
// Qdrant reads this.dimension directly
if (!this.dimension && this.config?.dimension) {
this.dimension = this.config.dimension;
}
// Skip premature constructor call when dimensions unknown
const dims = this.config?.embeddingModelDims ?? this.dimension;
if (!dims) return Promise.resolve();
// Run the real initialize only once
if (!this._initializePromise) {
this._initializePromise = origInit.call(this);
}
return this._initializePromise;
};
VectorCls.prototype.__patched = true;
}
let mem: any;
try { try {
this.memory = new Memory(config); mem = new Memory(this._buildConfig());
} catch (err) { } catch (err) {
// If initialization fails (e.g. native SQLite binding resolution under // If constructor fails (e.g. native SQLite binding under jiti/Docker),
// jiti), retry with history disabled — the history DB is the most common // retry with a FRESH config that has history disabled.
// source of native-binding failures and is not required for core if (!this.ossConfig?.disableHistory) {
// memory operations.
if (!config.disableHistory) {
console.warn( console.warn(
"[mem0] Memory initialization failed, retrying with history disabled:", "[mem0] Memory initialization failed, retrying with history disabled:",
err instanceof Error ? err.message : err, err instanceof Error ? err.message : err,
); );
config.disableHistory = true; mem = new Memory(this._buildConfig(true));
this.memory = new Memory(config);
} else { } else {
throw err; throw err;
} }
} }
// Force the SDK's internal auto-initialization to complete now. await mem.getAll({ userId: "__mem0_warmup__" });
// Without this, concurrent method calls (e.g. auto-recall + search)
// both trigger _autoInitialize() simultaneously, causing PGVector's this.memory = mem;
// pg client to call connect() twice → "Client has already been
// connected" crash. (#4638)
try {
await this.memory.getAll({ userId: "__mem0_warmup__" });
} catch {
// Warmup errors are non-fatal — the SDK may still work for
// subsequent calls once its internal state settles.
}
} }
async add( async add(
@@ -423,9 +443,7 @@ class OSSProvider implements Mem0Provider {
await this.memory.deleteAll({ userId }); await this.memory.deleteAll({ userId });
} }
async history( async history(memoryId: string): Promise<
memoryId: string,
): Promise<
Array<{ Array<{
id: string; id: string;
old_memory: string; old_memory: string;
@@ -438,8 +456,12 @@ class OSSProvider implements Mem0Provider {
try { try {
const result = await this.memory.history(memoryId); const result = await this.memory.history(memoryId);
return Array.isArray(result) ? result : []; return Array.isArray(result) ? result : [];
} catch { } catch (err) {
// OSS may not support history depending on config // OSS may not support history depending on config (e.g. disableHistory)
console.warn(
"[mem0] OSS history() failed:",
err instanceof Error ? err.message : err,
);
return []; return [];
} }
} }
@@ -459,7 +481,7 @@ export function createProvider(
); );
} }
return new PlatformProvider(cfg.apiKey!, cfg.baseUrl, cfg.orgId, cfg.projectId); return new PlatformProvider(cfg.apiKey!, cfg.baseUrl);
} }
// ============================================================================ // ============================================================================
@@ -489,7 +511,6 @@ export function providerToBackend(
...(opts.immutable && { immutable: true }), ...(opts.immutable && { immutable: true }),
...(opts.infer === false && { infer: false }), ...(opts.infer === false && { infer: false }),
...(opts.expires && { expiration_date: opts.expires }), ...(opts.expires && { expiration_date: opts.expires }),
...(opts.enableGraph && { enable_graph: true }),
}, },
); );
return result as unknown as Record<string, unknown>; return result as unknown as Record<string, unknown>;
+1 -1
View File
@@ -7,7 +7,7 @@ description: >
Also triggers automatically after sufficient activity (configurable). Also triggers automatically after sufficient activity (configurable).
user-invocable: true user-invocable: true
metadata: metadata:
{"openclaw": {"emoji": "💤"}} {"openclaw": {"emoji": "💤", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}}
--- ---
# Memory Consolidation # Memory Consolidation
+1 -1
View File
@@ -8,7 +8,7 @@ description: >
Read this skill at the start of every session. Read this skill at the start of every session.
user-invocable: false user-invocable: false
metadata: metadata:
{"openclaw": {"always": true, "emoji": "🧠"}} {"openclaw": {"always": true, "emoji": "🧠", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}}
--- ---
# Memory Protocol # Memory Protocol
+387 -9
View File
@@ -4,9 +4,18 @@
* 2. initPromise poisoning fix (retry after failure) * 2. initPromise poisoning fix (retry after failure)
* 3. Graceful SQLite fallback in OSSProvider * 3. Graceful SQLite fallback in OSSProvider
*/ */
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mem0ConfigSchema, createProvider } from "./index.ts"; import { mem0ConfigSchema, createProvider } from "./index.ts";
/** Stub vector-store classes required by OSSProvider._init's patching loop. */
function vectorStubs() {
return {
PGVector: class { initialize() { return Promise.resolve(); } },
RedisDB: class { initialize() { return Promise.resolve(); } },
Qdrant: class { initialize() { return Promise.resolve(); } },
};
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 1. Config: disableHistory passthrough // 1. Config: disableHistory passthrough
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -60,6 +69,7 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
beforeEach(() => { beforeEach(() => {
capturedConfig = undefined; capturedConfig = undefined;
memoryCallCount = 0; memoryCallCount = 0;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({ vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory { Memory: class MockMemory {
@@ -81,9 +91,14 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
} }
async delete() {} async delete() {}
}, },
...vectorStubs(),
})); }));
}); });
afterEach(() => {
vi.restoreAllMocks();
});
it("passes disableHistory: true to Memory when configured", async () => { it("passes disableHistory: true to Memory when configured", async () => {
const { createProvider } = await import("./index.ts"); const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({ const cfg = mem0ConfigSchema.parse({
@@ -130,6 +145,7 @@ describe("OSSProvider — initPromise retry after failure", () => {
beforeEach(() => { beforeEach(() => {
callCount = 0; callCount = 0;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({ vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory { Memory: class MockMemory {
@@ -154,9 +170,14 @@ describe("OSSProvider — initPromise retry after failure", () => {
} }
async delete() {} async delete() {}
}, },
...vectorStubs(),
})); }));
}); });
afterEach(() => {
vi.restoreAllMocks();
});
it("retries initialization after a transient failure", async () => { it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts"); const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({ const cfg = mem0ConfigSchema.parse({
@@ -184,14 +205,21 @@ describe("OSSProvider — initPromise retry after failure", () => {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
describe("OSSProvider — graceful SQLite fallback", () => { describe("OSSProvider — graceful SQLite fallback", () => {
let capturedConfigs: Record<string, unknown>[]; let capturedConfigs: Record<string, unknown>[];
/** When set, the mock Memory constructor always throws with this message. */
let forceConstructorError: string | null;
beforeEach(() => { beforeEach(() => {
capturedConfigs = []; capturedConfigs = [];
forceConstructorError = null;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({ vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory { Memory: class MockMemory {
constructor(config: Record<string, unknown>) { constructor(config: Record<string, unknown>) {
capturedConfigs.push({ ...config }); capturedConfigs.push({ ...config });
if (forceConstructorError) {
throw new Error(forceConstructorError);
}
if (!config.disableHistory) { if (!config.disableHistory) {
throw new Error("Could not locate the bindings file"); throw new Error("Could not locate the bindings file");
} }
@@ -211,9 +239,14 @@ describe("OSSProvider — graceful SQLite fallback", () => {
} }
async delete() {} async delete() {}
}, },
...vectorStubs(),
})); }));
}); });
afterEach(() => {
vi.restoreAllMocks();
});
it("retries with disableHistory: true when initial construction fails", async () => { it("retries with disableHistory: true when initial construction fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts"); const { createProvider } = await import("./index.ts");
@@ -242,14 +275,8 @@ describe("OSSProvider — graceful SQLite fallback", () => {
}); });
it("does not retry when disableHistory is already true", async () => { it("does not retry when disableHistory is already true", async () => {
vi.doMock("mem0ai/oss", () => ({ // Force the constructor to always throw, regardless of disableHistory
Memory: class MockMemory { forceConstructorError = "vector store connection refused";
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 { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({ const cfg = mem0ConfigSchema.parse({
@@ -274,6 +301,7 @@ describe("PlatformProvider — initPromise retry after failure", () => {
beforeEach(() => { beforeEach(() => {
callCount = 0; callCount = 0;
vi.resetModules();
vi.doMock("mem0ai", () => ({ vi.doMock("mem0ai", () => ({
default: class MockMemoryClient { default: class MockMemoryClient {
@@ -300,6 +328,10 @@ describe("PlatformProvider — initPromise retry after failure", () => {
})); }));
}); });
afterEach(() => {
vi.restoreAllMocks();
});
it("retries initialization after a transient failure", async () => { it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts"); const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({ const cfg = mem0ConfigSchema.parse({
@@ -320,3 +352,349 @@ describe("PlatformProvider — initPromise retry after failure", () => {
expect(callCount).toBe(2); expect(callCount).toBe(2);
}); });
}); });
// ---------------------------------------------------------------------------
// 6. OSSProvider: _buildConfig covers all branches
// ---------------------------------------------------------------------------
describe("OSSProvider — _buildConfig branch coverage", () => {
let capturedConfig: Record<string, unknown> | undefined;
beforeEach(() => {
capturedConfig = undefined;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfig = { ...config };
}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("builds config with custom embedder, llm, vectorStore, and historyDbPath", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "openai", config: { apiKey: "sk-e", model: "text-embedding-3-small" } },
llm: { provider: "openai", config: { apiKey: "sk-l", model: "gpt-4" } },
vectorStore: { provider: "qdrant", config: { host: "localhost", port: 6333 } },
historyDbPath: "/tmp/history.db",
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => `/resolved${p}` } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
expect(capturedConfig!.embedder).toEqual({
provider: "openai",
config: { model: "text-embedding-3-small", apiKey: "sk-e" },
});
expect(capturedConfig!.llm).toEqual({
provider: "openai",
config: expect.objectContaining({ model: "gpt-4", apiKey: "sk-l" }),
});
expect(capturedConfig!.vectorStore).toEqual({ provider: "qdrant", config: { host: "localhost", port: 6333 } });
expect(capturedConfig!.historyDbPath).toBe("/resolved/tmp/history.db");
expect(capturedConfig!.disableHistory).toBe(true);
});
it("strips empty-string values from embedder and llm config", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "openai", config: { apiKey: "", model: "custom-model" } },
llm: { provider: "openai", config: { apiKey: "", model: "" } },
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
// Empty apiKey should be stripped, leaving only the non-empty model
const embedderCfg = (capturedConfig!.embedder as any).config;
expect(embedderCfg.apiKey).toBeUndefined();
expect(embedderCfg.model).toBe("custom-model");
// Both empty keys in llm should be stripped, defaults applied
const llmCfg = (capturedConfig!.llm as any).config;
expect(llmCfg.apiKey).toBeUndefined();
});
it("falls back to default provider when embedder/llm provider is empty", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "", config: { apiKey: "sk-e" } },
llm: { provider: "", config: { apiKey: "sk-l" } },
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
// Empty provider should fall back to "openai" default
expect((capturedConfig!.embedder as any).provider).toBe("openai");
expect((capturedConfig!.llm as any).provider).toBe("openai");
});
});
// ---------------------------------------------------------------------------
// 7. OSSProvider: vector store dimension patching
// ---------------------------------------------------------------------------
describe("OSSProvider — vector store dimension patching", () => {
let capturedModule: any;
beforeEach(() => {
vi.resetModules();
vi.doMock("mem0ai/oss", () => {
const mod = {
Memory: class MockMemory {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
PGVector: class {
config: any;
dimension: any;
_initializePromise: any;
initialize() { return Promise.resolve("pg-initialized"); }
},
RedisDB: class {
config: any;
_initializePromise: any;
initialize() { return Promise.resolve("redis-initialized"); }
},
Qdrant: class {
config: any;
dimension: any;
_initializePromise: any;
initialize() { return Promise.resolve("qdrant-initialized"); }
},
};
capturedModule = mod;
return mod;
});
});
afterEach(() => {
vi.restoreAllMocks();
});
async function triggerInit() {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const provider = createProvider(cfg, { resolvePath: (p: string) => p } as any);
await provider.search("test", { user_id: "u1" });
}
it("copies config.dimension to embeddingModelDims and this.dimension", async () => {
await triggerInit();
const pg = new capturedModule.PGVector();
pg.config = { dimension: 1536 };
await pg.initialize();
expect(pg.config.embeddingModelDims).toBe(1536);
expect(pg.dimension).toBe(1536);
});
it("returns resolved promise when no dimensions are known", async () => {
await triggerInit();
const pg = new capturedModule.PGVector();
pg.config = {};
const result = await pg.initialize();
expect(result).toBeUndefined();
});
it("runs original initialize only once via cached promise", async () => {
await triggerInit();
const q = new capturedModule.Qdrant();
q.config = { dimension: 768 };
const first = await q.initialize();
const second = await q.initialize();
expect(first).toBe("qdrant-initialized");
expect(second).toBe("qdrant-initialized");
expect(q._initializePromise).toBeDefined();
});
it("skips missing vector store classes without crashing", async () => {
// Override with a mock that omits PGVector entirely
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
PGVector: undefined, // explicitly absent — tests the !VectorCls guard
RedisDB: class { initialize() { return Promise.resolve(); } },
Qdrant: class { initialize() { return Promise.resolve(); } },
}));
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const provider = createProvider(cfg, { resolvePath: (p: string) => p } as any);
// Should not throw even though PGVector is missing
const results = await provider.search("test", { user_id: "u1" });
expect(results).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// 8. OSSProvider: history() error handler
// ---------------------------------------------------------------------------
describe("OSSProvider — history error handling", () => {
/** When set, the mock history() throws this value instead of an Error. */
let historyThrowValue: unknown;
beforeEach(() => {
historyThrowValue = new Error("history not available");
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
async history() { throw historyThrowValue; }
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("returns empty array and warns when history() throws an Error", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
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);
await provider.search("test", { user_id: "u1" });
const result = await provider.history("mem-123");
expect(result).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
"[mem0] OSS history() failed:",
"history not available",
);
warnSpy.mockRestore();
});
it("handles non-Error thrown values in history()", async () => {
historyThrowValue = "raw string error";
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
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);
await provider.search("test", { user_id: "u1" });
const result = await provider.history("mem-456");
expect(result).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
"[mem0] OSS history() failed:",
"raw string error",
);
warnSpy.mockRestore();
});
});
// ---------------------------------------------------------------------------
// 9. OSSProvider: customPrompt passthrough
// ---------------------------------------------------------------------------
describe("OSSProvider — customPrompt passthrough", () => {
let capturedConfig: Record<string, unknown> | undefined;
beforeEach(() => {
capturedConfig = undefined;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfig = { ...config };
}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("passes customPrompt to Memory config when provided", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
customPrompt: "Extract only user preferences.",
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
expect(capturedConfig!.customPrompt).toBe("Extract only user preferences.");
});
});
+69 -26
View File
@@ -175,7 +175,6 @@ function createMockCfg() {
apiKey: "m0-test-key-1234", apiKey: "m0-test-key-1234",
baseUrl: "https://api.mem0.ai", baseUrl: "https://api.mem0.ai",
topK: 5, topK: 5,
enableGraph: false,
autoCapture: true, autoCapture: true,
autoRecall: true, autoRecall: true,
searchThreshold: 0.5, searchThreshold: 0.5,
@@ -967,7 +966,7 @@ describe("registerCliCommands", () => {
const configCmd = findCommand(mem0, "config")!; const configCmd = findCommand(mem0, "config")!;
const getCmd = findCommand(configCmd, "get")!; const getCmd = findCommand(configCmd, "get")!;
getCmd._action!("org_id"); getCmd._action!("email");
expect(consoleSpy.log).toHaveBeenCalledWith("(not set)"); expect(consoleSpy.log).toHaveBeenCalledWith("(not set)");
}); });
@@ -1033,18 +1032,6 @@ describe("registerCliCommands", () => {
); );
}); });
it("coerces 'true' to boolean for boolean keys", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const setCmd = findCommand(configCmd, "set")!;
setCmd._action!("enable_graph", "true");
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({ enableGraph: true }),
);
});
it("coerces 'false' to boolean false for boolean keys", () => { it("coerces 'false' to boolean false for boolean keys", () => {
const { mem0 } = setup(); const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!; const configCmd = findCommand(mem0, "config")!;
@@ -1069,18 +1056,6 @@ describe("registerCliCommands", () => {
); );
}); });
it("coerces 'yes' to boolean true for boolean keys", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
const setCmd = findCommand(configCmd, "set")!;
setCmd._action!("enable_graph", "yes");
expect(writePluginAuth).toHaveBeenCalledWith(
expect.objectContaining({ enableGraph: true }),
);
});
it("coerces integer string for integer keys", () => { it("coerces integer string for integer keys", () => {
const { mem0 } = setup(); const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!; const configCmd = findCommand(mem0, "config")!;
@@ -1412,5 +1387,73 @@ describe("registerCliCommands", () => {
expect.stringContaining("Failed to get event"), expect.stringContaining("Failed to get event"),
); );
}); });
it("event list returns early in open-source mode", async () => {
const provider = createMockProvider();
const cfg = { ...createMockCfg(), mode: "open-source" as const };
const mockApi = {
registerCli: vi.fn((cb: any) => {
const root = createMockCommand("root");
cb({ program: root });
const mem0 = findCommand(root, "mem0")!;
const eventCmd = findCommand(mem0, "event")!;
const listCmd = findCommand(eventCmd, "list")!;
listCmd._action!();
}),
logger: { info: vi.fn(), warn: vi.fn() },
} as any;
registerCliCommands(
mockApi,
null as any,
provider as any,
cfg as any,
vi.fn().mockReturnValue("testuser"),
vi.fn((id: string) => `testuser:agent:${id}`),
vi.fn().mockReturnValue({ user_id: "testuser", top_k: 5 }),
vi.fn().mockReturnValue(undefined),
);
// Wait for async action
await new Promise((r) => setTimeout(r, 10));
expect(consoleSpy.log).toHaveBeenCalledWith(
"Event tracking is only available in platform mode.",
);
});
it("event status returns early in open-source mode", async () => {
const provider = createMockProvider();
const cfg = { ...createMockCfg(), mode: "open-source" as const };
const mockApi = {
registerCli: vi.fn((cb: any) => {
const root = createMockCommand("root");
cb({ program: root });
const mem0 = findCommand(root, "mem0")!;
const eventCmd = findCommand(mem0, "event")!;
const statusCmd = findCommand(eventCmd, "status")!;
statusCmd._action!("evt-123");
}),
logger: { info: vi.fn(), warn: vi.fn() },
} as any;
registerCliCommands(
mockApi,
null as any,
provider as any,
cfg as any,
vi.fn().mockReturnValue("testuser"),
vi.fn((id: string) => `testuser:agent:${id}`),
vi.fn().mockReturnValue({ user_id: "testuser", top_k: 5 }),
vi.fn().mockReturnValue(undefined),
);
// Wait for async action
await new Promise((r) => setTimeout(r, 10));
expect(consoleSpy.log).toHaveBeenCalledWith(
"Event tracking is only available in platform mode.",
);
});
}); });
}); });
+1 -11
View File
@@ -69,10 +69,7 @@ describe("readPluginAuth", () => {
apiKey: "sk-test-123", apiKey: "sk-test-123",
baseUrl: "https://custom.api.com", baseUrl: "https://custom.api.com",
userId: "user-1", userId: "user-1",
orgId: "org-1",
projectId: "proj-1",
mode: "platform", mode: "platform",
enableGraph: true,
autoRecall: true, autoRecall: true,
autoCapture: false, autoCapture: false,
topK: 10, topK: 10,
@@ -87,17 +84,14 @@ describe("readPluginAuth", () => {
apiKey: "sk-test-123", apiKey: "sk-test-123",
baseUrl: "https://custom.api.com", baseUrl: "https://custom.api.com",
userId: "user-1", userId: "user-1",
orgId: "org-1",
projectId: "proj-1",
mode: "platform", mode: "platform",
enableGraph: true,
autoRecall: true, autoRecall: true,
autoCapture: false, autoCapture: false,
topK: 10, topK: 10,
}); });
}); });
it("handles snake_case aliases (api_key, base_url, user_id, org_id, project_id)", () => { it("handles snake_case aliases (api_key, base_url, user_id)", () => {
setConfigFile({ setConfigFile({
plugins: { plugins: {
entries: { entries: {
@@ -107,8 +101,6 @@ describe("readPluginAuth", () => {
api_key: "sk-snake", api_key: "sk-snake",
base_url: "https://snake.api.com", base_url: "https://snake.api.com",
user_id: "user-snake", user_id: "user-snake",
org_id: "org-snake",
project_id: "proj-snake",
}, },
}, },
}, },
@@ -119,8 +111,6 @@ describe("readPluginAuth", () => {
expect(auth.apiKey).toBe("sk-snake"); expect(auth.apiKey).toBe("sk-snake");
expect(auth.baseUrl).toBe("https://snake.api.com"); expect(auth.baseUrl).toBe("https://snake.api.com");
expect(auth.userId).toBe("user-snake"); expect(auth.userId).toBe("user-snake");
expect(auth.orgId).toBe("org-snake");
expect(auth.projectId).toBe("proj-snake");
}); });
it("returns empty object when JSON is invalid", () => { it("returns empty object when JSON is invalid", () => {
+5 -30
View File
@@ -64,11 +64,6 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
expect(cfg.searchThreshold).toBe(0.5); expect(cfg.searchThreshold).toBe(0.5);
}); });
it("enableGraph defaults to false", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.enableGraph).toBe(false);
});
it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => { it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" }); const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.customInstructions).toBe(DEFAULT_CUSTOM_INSTRUCTIONS); expect(cfg.customInstructions).toBe(DEFAULT_CUSTOM_INSTRUCTIONS);
@@ -274,14 +269,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => {
expect(cfg.autoRecall).toBe(false); expect(cfg.autoRecall).toBe(false);
}); });
it("enableGraph can be set to true", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
enableGraph: true,
});
expect(cfg.enableGraph).toBe(true);
});
it("custom topK is used when provided", () => { it("custom topK is used when provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", topK: 20 }); const cfg = mem0ConfigSchema.parse({ apiKey: "k", topK: 20 });
expect(cfg.topK).toBe(20); expect(cfg.topK).toBe(20);
@@ -330,18 +317,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => {
expect(cfg.baseUrl).toBe("https://custom.api.com"); expect(cfg.baseUrl).toBe("https://custom.api.com");
}); });
it("orgId is passed through when provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", orgId: "org-123" });
expect(cfg.orgId).toBe("org-123");
});
it("projectId is passed through when provided", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
projectId: "proj-456",
});
expect(cfg.projectId).toBe("proj-456");
});
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -359,22 +334,23 @@ describe("mem0ConfigSchema.parse() — oss config", () => {
historyDbPath: "/tmp/history.db", historyDbPath: "/tmp/history.db",
disableHistory: false, disableHistory: false,
}; };
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: ossConfig }); const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: ossConfig });
expect(cfg.mode).toBe("open-source");
expect(cfg.oss).toEqual(ossConfig); expect(cfg.oss).toEqual(ossConfig);
}); });
it("ignores oss when it is not a plain object", () => { it("ignores oss when it is not a plain object", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: "not-an-object" }); const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: "not-an-object" });
expect(cfg.oss).toBeUndefined(); expect(cfg.oss).toBeUndefined();
}); });
it("ignores oss when it is an array", () => { it("ignores oss when it is an array", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: [1, 2, 3] }); const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: [1, 2, 3] });
expect(cfg.oss).toBeUndefined(); expect(cfg.oss).toBeUndefined();
}); });
it("ignores oss when it is null", () => { it("ignores oss when it is null", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: null }); const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: null });
expect(cfg.oss).toBeUndefined(); expect(cfg.oss).toBeUndefined();
}); });
}); });
@@ -388,7 +364,6 @@ describe("mem0ConfigSchema.parse() — skills config", () => {
triage: { triage: {
enabled: true, enabled: true,
importanceThreshold: 3, importanceThreshold: 3,
enableGraph: false,
credentialPatterns: ["sk-", "ghp_"], credentialPatterns: ["sk-", "ghp_"],
}, },
recall: { recall: {
-2
View File
@@ -127,7 +127,6 @@ describe("providerToBackend — add", () => {
immutable: true, immutable: true,
infer: false, infer: false,
expires: "2027-01-01", expires: "2027-01-01",
enableGraph: true,
}); });
expect(provider.add).toHaveBeenCalledWith( expect(provider.add).toHaveBeenCalledWith(
@@ -139,7 +138,6 @@ describe("providerToBackend — add", () => {
immutable: true, immutable: true,
infer: false, infer: false,
expiration_date: "2027-01-01", expiration_date: "2027-01-01",
enable_graph: true,
}), }),
); );
}); });
-1
View File
@@ -32,7 +32,6 @@ function createMockToolDeps(overrides = {}): ToolDeps {
mode: "platform", mode: "platform",
userId: "testuser", userId: "testuser",
topK: 5, topK: 5,
enableGraph: false,
autoCapture: true, autoCapture: true,
autoRecall: true, autoRecall: true,
searchThreshold: 0.5, searchThreshold: 0.5,
-1
View File
@@ -67,7 +67,6 @@ export function createMemoryAddTool(deps: ToolDeps) {
if (runId) addOpts.run_id = runId; if (runId) addOpts.run_id = runId;
if (cfg.mode === "platform") { if (cfg.mode === "platform") {
addOpts.output_format = "v1.1"; addOpts.output_format = "v1.1";
if (cfg.enableGraph || cfg.skills?.triage?.enableGraph) addOpts.enable_graph = true;
} }
const result = await provider.add([{ role: "user", content: allFacts.join("\n") }], addOpts); const result = await provider.add([{ role: "user", content: allFacts.join("\n") }], addOpts);
-5
View File
@@ -9,11 +9,8 @@ export type Mem0Config = {
// Platform-specific // Platform-specific
apiKey?: string; apiKey?: string;
baseUrl?: string; baseUrl?: string;
orgId?: string;
projectId?: string;
customInstructions: string; customInstructions: string;
customCategories: Record<string, string>; customCategories: Record<string, string>;
enableGraph: boolean;
// OSS-specific // OSS-specific
customPrompt?: string; customPrompt?: string;
oss?: { oss?: {
@@ -40,7 +37,6 @@ export interface AddOptions {
run_id?: string; run_id?: string;
custom_instructions?: string; custom_instructions?: string;
custom_categories?: Array<Record<string, string>>; custom_categories?: Array<Record<string, string>>;
enable_graph?: boolean;
output_format?: string; output_format?: string;
source?: string; source?: string;
// Agentic harness additions // Agentic harness additions
@@ -79,7 +75,6 @@ export interface SkillsConfig {
triage?: { triage?: {
enabled?: boolean; enabled?: boolean;
importanceThreshold?: number; importanceThreshold?: number;
enableGraph?: boolean;
credentialPatterns?: string[]; credentialPatterns?: string[];
}; };
recall?: { recall?: {