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.
## [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
### Added
-3
View File
@@ -14,7 +14,6 @@ export interface AddOptions {
infer?: boolean;
expires?: string;
categories?: string[];
enableGraph?: boolean;
}
export interface SearchOptions {
@@ -28,7 +27,6 @@ export interface SearchOptions {
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
enableGraph?: boolean;
}
export interface ListOptions {
@@ -41,7 +39,6 @@ export interface ListOptions {
category?: string;
after?: string;
before?: string;
enableGraph?: boolean;
}
export interface DeleteOptions {
-3
View File
@@ -111,7 +111,6 @@ export class PlatformBackend implements Backend {
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
return (await this._request("POST", "/v1/memories/", {
json: payload,
@@ -171,7 +170,6 @@ export class PlatformBackend implements Backend {
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
@@ -222,7 +220,6 @@ export class PlatformBackend implements Backend {
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/", {
json: payload,
+12 -16
View File
@@ -384,10 +384,6 @@ export function registerCliCommands(
console.log(` User ID: ${existingAuth.userId}`);
if (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("");
// Validate existing key before asking
@@ -408,7 +404,7 @@ export function registerCliCommands(
}
const reuse = await promptInput(
" Keep existing configuration? (Y/n): ",
" Keep existing configuration? (y/n): ",
);
if (
reuse === "" ||
@@ -432,7 +428,7 @@ export function registerCliCommands(
console.log(" 2. Enter API key manually");
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") {
// --- Email interactive flow ---
@@ -952,9 +948,6 @@ export function registerCliCommands(
email: "userEmail",
base_url: "baseUrl",
user_id: "userId",
org_id: "orgId",
project_id: "projectId",
enable_graph: "enableGraph",
auto_recall: "autoRecall",
auto_capture: "autoCapture",
top_k: "topK",
@@ -969,6 +962,8 @@ export function registerCliCommands(
vector_host: "oss.vectorStore.config.host",
vector_port: "oss.vectorStore.config.port",
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",
history_db_path: "oss.historyDbPath",
disable_history: "oss.disableHistory",
@@ -979,7 +974,6 @@ export function registerCliCommands(
// Boolean config fields — coerce "true"/"1"/"yes" on set
const BOOLEAN_KEYS = new Set([
"enableGraph",
"autoRecall",
"autoCapture",
"oss.disableHistory",
@@ -1009,11 +1003,8 @@ export function registerCliCommands(
apiKey: auth.apiKey ?? cfg.apiKey,
baseUrl: auth.baseUrl ?? cfg.baseUrl ?? "https://api.mem0.ai",
userId: auth.userId ?? cfg.userId,
orgId: auth.orgId ?? cfg.orgId,
projectId: auth.projectId ?? cfg.projectId,
mode: auth.mode ?? cfg.mode,
userEmail: auth.userEmail,
enableGraph: cfg.enableGraph,
autoRecall: cfg.autoRecall,
autoCapture: cfg.autoCapture,
topK: cfg.topK,
@@ -1055,9 +1046,6 @@ export function registerCliCommands(
entries.push(
["api_key", "apiKey"],
["email", "userEmail"],
["org_id", "orgId"],
["project_id", "projectId"],
["enable_graph", "enableGraph"],
);
} else {
entries.push(
@@ -1238,6 +1226,10 @@ export function registerCliCommands(
.description("List recent background events")
.action(async () => {
try {
if (!backend || cfg.mode === "open-source") {
console.log("Event tracking is only available in platform mode.");
return;
}
const results = await backend.listEvents();
if (!results.length) {
console.log("No events found.");
@@ -1282,6 +1274,10 @@ export function registerCliCommands(
.argument("<event_id>", "Event ID to check")
.action(async (eventId: string) => {
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 status = String(ev.status ?? "—");
-6
View File
@@ -29,11 +29,8 @@ export interface PluginAuthConfig {
apiKey?: string;
baseUrl?: string;
userId?: string;
orgId?: string;
projectId?: string;
userEmail?: string;
mode?: string;
enableGraph?: boolean;
autoRecall?: boolean;
autoCapture?: boolean;
topK?: number;
@@ -76,11 +73,8 @@ export function readPluginAuth(): PluginAuthConfig {
apiKey: (cfg.apiKey ?? cfg.api_key) as string | undefined,
baseUrl: (cfg.baseUrl ?? cfg.base_url) 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,
mode: cfg.mode as string | undefined,
enableGraph: cfg.enableGraph as boolean | undefined,
autoRecall: cfg.autoRecall as boolean | undefined,
autoCapture: cfg.autoCapture as boolean | 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 {
apiKey?: string;
baseUrl?: string;
orgId?: string;
projectId?: string;
}
// ============================================================================
@@ -152,14 +150,11 @@ const ALLOWED_KEYS = [
"baseUrl",
"userId",
"userEmail",
"orgId",
"projectId",
"autoCapture",
"autoRecall",
"customInstructions",
"customCategories",
"customPrompt",
"enableGraph",
"searchThreshold",
"topK",
"oss",
@@ -185,6 +180,15 @@ export const mem0ConfigSchema = {
assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-mem0 config");
// 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 =
cfg.mode === "open-source" ? "open-source" : "platform";
@@ -193,15 +197,9 @@ export const mem0ConfigSchema = {
typeof cfg.apiKey === "string" ? cfg.apiKey : undefined;
let resolvedBaseUrl =
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 (fileConfig.apiKey) resolvedApiKey = fileConfig.apiKey;
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.
@@ -228,8 +226,6 @@ export const mem0ConfigSchema = {
return "default";
}
})(),
orgId: resolvedOrgId,
projectId: resolvedProjectId,
autoCapture: cfg.autoCapture !== false,
autoRecall: cfg.autoRecall !== false,
customInstructions:
@@ -246,7 +242,6 @@ export const mem0ConfigSchema = {
typeof cfg.customPrompt === "string"
? cfg.customPrompt
: DEFAULT_CUSTOM_INSTRUCTIONS,
enableGraph: cfg.enableGraph === true,
searchThreshold:
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5,
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
+58 -28
View File
@@ -103,15 +103,21 @@ const memoryPlugin = definePluginEntry({
const fileConfig: FileConfig = {
apiKey: pluginAuth.apiKey,
baseUrl: pluginAuth.baseUrl,
orgId: pluginAuth.orgId,
projectId: pluginAuth.projectId,
};
const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig);
// 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>) => {
try { captureEvent(event, props, telemetryCtx); } catch { /* silently swallow */ }
try {
captureEvent(event, props, telemetryCtx);
} catch {
/* silently swallow */
}
};
if (cfg.needsSetup) {
@@ -182,7 +188,7 @@ const memoryPlugin = definePluginEntry({
});
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
@@ -197,7 +203,6 @@ const memoryPlugin = definePluginEntry({
};
if (runId) opts.run_id = runId;
if (cfg.mode === "platform") {
opts.enable_graph = cfg.enableGraph;
opts.output_format = "v1.1";
}
return opts;
@@ -242,7 +247,10 @@ const memoryPlugin = definePluginEntry({
getCurrentSessionId: () => currentSessionId,
skillsActive,
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);
@@ -328,7 +336,10 @@ function registerHooks(
getStateDir: () => string | undefined;
},
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
@@ -355,16 +366,17 @@ function registerHooks(
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 isChannelSystemEvent = /^system(?:\s*\(untrusted\))?:\s*\[/i.test(
event.prompt,
);
const isSystemPrompt =
promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session");
!isChannelSystemEvent &&
(promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session"));
if (isSystemPrompt) {
api.logger.info(
"openclaw-mem0: skills-mode skipping recall for system/bootstrap prompt",
@@ -472,7 +484,10 @@ function registerHooks(
"\n</auto-dream>";
// Track which session triggered dream (session-keyed, not global)
dreamSessionId = sessionId;
_captureEvent("openclaw.hook.dream", { phase: "triggered", memory_count: memCount });
_captureEvent("openclaw.hook.dream", {
phase: "triggered",
memory_count: memCount,
});
api.logger.info(
`openclaw-mem0: auto-dream triggered (${memCount} memories, gate passed)`,
);
@@ -545,7 +560,10 @@ function registerHooks(
if (writeToolUsed) {
releaseDreamLock(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(
"openclaw-mem0: auto-dream completed (verified write tool usage), lock released",
);
@@ -599,14 +617,17 @@ function registerHooks(
return;
}
// Skip recall for system/bootstrap prompts to save API calls
const promptLower = event.prompt.toLowerCase();
const isChannelSystemEvent = /^system(?:\s*\(untrusted\))?:\s*\[/i.test(
event.prompt,
);
const isSystemPrompt =
promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session");
!isChannelSystemEvent &&
(promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session"));
if (isSystemPrompt) {
api.logger.info(
"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)
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) => {
if (msg?.role !== "assistant" || !Array.isArray(msg?.content)) return false;
if (msg?.role !== "assistant" || !Array.isArray(msg?.content))
return false;
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) {
@@ -848,7 +875,10 @@ function registerHooks(
if (!textContent) continue;
}
// 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
.replace(
/Sender\s*\(untrusted metadata\):\s*```json[\s\S]*?```\s*/gi,
+1
View File
@@ -2,6 +2,7 @@
"id": "openclaw-mem0",
"name": "Memory (Mem0)",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"version": "1.0.5",
"kind": "memory",
"skills": ["skills"],
"contracts": {
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "1.0.4",
"version": "1.0.5",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
@@ -42,12 +42,12 @@
"./dist/index.js"
],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
"pluginApi": ">=2026.3.28 <=2026.4.2",
"minGatewayVersion": "2026.3.28"
},
"build": {
"openclawVersion": "2026.3.24-beta.2",
"pluginSdkVersion": "2026.3.24-beta.2"
"openclawVersion": "2026.4.1",
"pluginSdkVersion": "2026.4.1"
}
},
"devDependencies": {
+65 -44
View File
@@ -82,8 +82,6 @@ class PlatformProvider implements Mem0Provider {
constructor(
private readonly apiKey: string,
private readonly baseUrl?: string,
private readonly orgId?: string,
private readonly projectId?: string,
) {}
private async ensureClient(): Promise<void> {
@@ -101,14 +99,10 @@ class PlatformProvider implements Mem0Provider {
const opts: {
apiKey: string;
host?: string;
organizationId?: string;
projectId?: string;
} = {
apiKey: this.apiKey,
};
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);
}
@@ -123,7 +117,6 @@ class PlatformProvider implements Mem0Provider {
opts.custom_instructions = options.custom_instructions;
if (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.source) opts.source = options.source;
// Agentic harness: direct storage bypass
@@ -208,9 +201,7 @@ class PlatformProvider implements Mem0Provider {
await this.client.deleteAll({ user_id: userId });
}
async history(
memoryId: string,
): Promise<
async history(memoryId: string): Promise<
Array<{
id: string;
old_memory: string;
@@ -249,23 +240,29 @@ class OSSProvider implements Mem0Provider {
return this.initPromise;
}
private async _init(): Promise<void> {
const { Memory } = await import("mem0ai/oss");
private _buildConfig(disableHistory = false): Record<string, unknown> {
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" } };
// Helper: strip empty-string values so they don't clobber defaults
const stripEmpty = (obj: Record<string, unknown>) => {
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;
};
if (this.ossConfig?.embedder) {
const ec = stripEmpty(this.ossConfig.embedder.config ?? {});
if (ec.host && !ec.url) {
ec.url = ec.host;
delete ec.host;
}
config.embedder = {
provider: this.ossConfig.embedder.provider || defaultEmbedder.provider,
config: { ...defaultEmbedder.config, ...ec },
@@ -276,6 +273,10 @@ class OSSProvider implements Mem0Provider {
if (this.ossConfig?.llm) {
const lc = stripEmpty(this.ossConfig.llm.config ?? {});
if (lc.host && !lc.url) {
lc.url = lc.host;
delete lc.host;
}
config.llm = {
provider: this.ossConfig.llm.provider || defaultLlm.provider,
config: { ...defaultLlm.config, ...lc },
@@ -285,7 +286,7 @@ class OSSProvider implements Mem0Provider {
}
if (this.ossConfig?.vectorStore)
config.vectorStore = this.ossConfig.vectorStore;
config.vectorStore = { ...this.ossConfig.vectorStore };
if (this.ossConfig?.historyDbPath) {
const dbPath = this.resolvePath
@@ -294,42 +295,61 @@ class OSSProvider implements Mem0Provider {
config.historyDbPath = dbPath;
}
if (this.ossConfig?.disableHistory) {
if (disableHistory || this.ossConfig?.disableHistory) {
config.disableHistory = true;
}
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 {
this.memory = new Memory(config);
mem = new Memory(this._buildConfig());
} 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) {
// If constructor fails (e.g. native SQLite binding under jiti/Docker),
// retry with a FRESH config that has history disabled.
if (!this.ossConfig?.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);
mem = new Memory(this._buildConfig(true));
} else {
throw err;
}
}
// Force the SDK's internal auto-initialization to complete now.
// Without this, concurrent method calls (e.g. auto-recall + search)
// both trigger _autoInitialize() simultaneously, causing PGVector's
// 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.
}
await mem.getAll({ userId: "__mem0_warmup__" });
this.memory = mem;
}
async add(
@@ -423,9 +443,7 @@ class OSSProvider implements Mem0Provider {
await this.memory.deleteAll({ userId });
}
async history(
memoryId: string,
): Promise<
async history(memoryId: string): Promise<
Array<{
id: string;
old_memory: string;
@@ -438,8 +456,12 @@ class OSSProvider implements Mem0Provider {
try {
const result = await this.memory.history(memoryId);
return Array.isArray(result) ? result : [];
} catch {
// OSS may not support history depending on config
} catch (err) {
// OSS may not support history depending on config (e.g. disableHistory)
console.warn(
"[mem0] OSS history() failed:",
err instanceof Error ? err.message : err,
);
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.infer === false && { infer: false }),
...(opts.expires && { expiration_date: opts.expires }),
...(opts.enableGraph && { enable_graph: true }),
},
);
return result as unknown as Record<string, unknown>;
+1 -1
View File
@@ -7,7 +7,7 @@ description: >
Also triggers automatically after sufficient activity (configurable).
user-invocable: true
metadata:
{"openclaw": {"emoji": "💤"}}
{"openclaw": {"emoji": "💤", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}}
---
# Memory Consolidation
+1 -1
View File
@@ -8,7 +8,7 @@ description: >
Read this skill at the start of every session.
user-invocable: false
metadata:
{"openclaw": {"always": true, "emoji": "🧠"}}
{"openclaw": {"always": true, "emoji": "🧠", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}}
---
# Memory Protocol
+387 -9
View File
@@ -4,9 +4,18 @@
* 2. initPromise poisoning fix (retry after failure)
* 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";
/** 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
// ---------------------------------------------------------------------------
@@ -60,6 +69,7 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
beforeEach(() => {
capturedConfig = undefined;
memoryCallCount = 0;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
@@ -81,9 +91,14 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("passes disableHistory: true to Memory when configured", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -130,6 +145,7 @@ describe("OSSProvider — initPromise retry after failure", () => {
beforeEach(() => {
callCount = 0;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
@@ -154,9 +170,14 @@ describe("OSSProvider — initPromise retry after failure", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -184,14 +205,21 @@ describe("OSSProvider — initPromise retry after failure", () => {
// ---------------------------------------------------------------------------
describe("OSSProvider — graceful SQLite fallback", () => {
let capturedConfigs: Record<string, unknown>[];
/** When set, the mock Memory constructor always throws with this message. */
let forceConstructorError: string | null;
beforeEach(() => {
capturedConfigs = [];
forceConstructorError = null;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfigs.push({ ...config });
if (forceConstructorError) {
throw new Error(forceConstructorError);
}
if (!config.disableHistory) {
throw new Error("Could not locate the bindings file");
}
@@ -211,9 +239,14 @@ describe("OSSProvider — graceful SQLite fallback", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries with disableHistory: true when initial construction fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
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 () => {
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");
}
},
}));
// Force the constructor to always throw, regardless of disableHistory
forceConstructorError = "vector store connection refused";
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -274,6 +301,7 @@ describe("PlatformProvider — initPromise retry after failure", () => {
beforeEach(() => {
callCount = 0;
vi.resetModules();
vi.doMock("mem0ai", () => ({
default: class MockMemoryClient {
@@ -300,6 +328,10 @@ describe("PlatformProvider — initPromise retry after failure", () => {
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -320,3 +352,349 @@ describe("PlatformProvider — initPromise retry after failure", () => {
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",
baseUrl: "https://api.mem0.ai",
topK: 5,
enableGraph: false,
autoCapture: true,
autoRecall: true,
searchThreshold: 0.5,
@@ -967,7 +966,7 @@ describe("registerCliCommands", () => {
const configCmd = findCommand(mem0, "config")!;
const getCmd = findCommand(configCmd, "get")!;
getCmd._action!("org_id");
getCmd._action!("email");
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", () => {
const { mem0 } = setup();
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", () => {
const { mem0 } = setup();
const configCmd = findCommand(mem0, "config")!;
@@ -1412,5 +1387,73 @@ describe("registerCliCommands", () => {
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",
baseUrl: "https://custom.api.com",
userId: "user-1",
orgId: "org-1",
projectId: "proj-1",
mode: "platform",
enableGraph: true,
autoRecall: true,
autoCapture: false,
topK: 10,
@@ -87,17 +84,14 @@ describe("readPluginAuth", () => {
apiKey: "sk-test-123",
baseUrl: "https://custom.api.com",
userId: "user-1",
orgId: "org-1",
projectId: "proj-1",
mode: "platform",
enableGraph: true,
autoRecall: true,
autoCapture: false,
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({
plugins: {
entries: {
@@ -107,8 +101,6 @@ describe("readPluginAuth", () => {
api_key: "sk-snake",
base_url: "https://snake.api.com",
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.baseUrl).toBe("https://snake.api.com");
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", () => {
+5 -30
View File
@@ -64,11 +64,6 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
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", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.customInstructions).toBe(DEFAULT_CUSTOM_INSTRUCTIONS);
@@ -274,14 +269,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => {
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", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", topK: 20 });
expect(cfg.topK).toBe(20);
@@ -330,18 +317,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => {
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",
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);
});
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();
});
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();
});
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();
});
});
@@ -388,7 +364,6 @@ describe("mem0ConfigSchema.parse() — skills config", () => {
triage: {
enabled: true,
importanceThreshold: 3,
enableGraph: false,
credentialPatterns: ["sk-", "ghp_"],
},
recall: {
-2
View File
@@ -127,7 +127,6 @@ describe("providerToBackend — add", () => {
immutable: true,
infer: false,
expires: "2027-01-01",
enableGraph: true,
});
expect(provider.add).toHaveBeenCalledWith(
@@ -139,7 +138,6 @@ describe("providerToBackend — add", () => {
immutable: true,
infer: false,
expiration_date: "2027-01-01",
enable_graph: true,
}),
);
});
-1
View File
@@ -32,7 +32,6 @@ function createMockToolDeps(overrides = {}): ToolDeps {
mode: "platform",
userId: "testuser",
topK: 5,
enableGraph: false,
autoCapture: true,
autoRecall: true,
searchThreshold: 0.5,
-1
View File
@@ -67,7 +67,6 @@ export function createMemoryAddTool(deps: ToolDeps) {
if (runId) addOpts.run_id = runId;
if (cfg.mode === "platform") {
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);
-5
View File
@@ -9,11 +9,8 @@ export type Mem0Config = {
// Platform-specific
apiKey?: string;
baseUrl?: string;
orgId?: string;
projectId?: string;
customInstructions: string;
customCategories: Record<string, string>;
enableGraph: boolean;
// OSS-specific
customPrompt?: string;
oss?: {
@@ -40,7 +37,6 @@ export interface AddOptions {
run_id?: string;
custom_instructions?: string;
custom_categories?: Array<Record<string, string>>;
enable_graph?: boolean;
output_format?: string;
source?: string;
// Agentic harness additions
@@ -79,7 +75,6 @@ export interface SkillsConfig {
triage?: {
enabled?: boolean;
importanceThreshold?: number;
enableGraph?: boolean;
credentialPatterns?: string[];
};
recall?: {