refactor: drop orgId, projectId, enableGraph config options, update CLI prompts, and clean up related code (#4734)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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 ?? "—");
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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?: {
|
||||||
|
|||||||
Reference in New Issue
Block a user