diff --git a/openclaw/cli/commands.ts b/openclaw/cli/commands.ts index 3f525636e..e7c62ac46 100644 --- a/openclaw/cli/commands.ts +++ b/openclaw/cli/commands.ts @@ -1420,9 +1420,8 @@ export function registerCliCommands( }); }, { - commands: ["mem0"], descriptors: [ - { name: "mem0", description: "Mem0 memory plugin commands" }, + { name: "mem0", description: "Mem0 memory plugin commands", hasSubcommands: true }, ], }, ); diff --git a/openclaw/index.ts b/openclaw/index.ts index ef6668ca9..0435f3490 100644 --- a/openclaw/index.ts +++ b/openclaw/index.ts @@ -16,6 +16,7 @@ * - Dual mode: platform or open-source (self-hosted) */ +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { @@ -90,12 +91,10 @@ export { createProvider } from "./providers.ts"; // Plugin Definition // ============================================================================ -const memoryPlugin = { +const memoryPlugin = definePluginEntry({ id: "openclaw-mem0", name: "Memory (Mem0)", description: "Mem0 memory backend — Mem0 platform or self-hosted open-source", - kind: "memory" as const, - configSchema: mem0ConfigSchema, register(api: OpenClawPluginApi) { // Read auth from openclaw.json plugin config (picks up post-startup login). @@ -234,6 +233,7 @@ const memoryPlugin = { api, provider, cfg, + backend, resolveUserId: _resolveUserId, effectiveUserId: _effectiveUserId, agentUserId: _agentUserId, @@ -301,7 +301,7 @@ const memoryPlugin = { }, }); }, -}; +}); // ============================================================================ // Lifecycle Hook Registration @@ -582,11 +582,11 @@ function registerHooks( // Track last seen session ID to detect actual new sessions (not every turn) let lastRecallSessionId: string | undefined; - // Auto-recall: inject relevant memories before agent starts + // Auto-recall: inject relevant memories before prompt is built if (cfg.autoRecall) { const RECALL_TIMEOUT_MS = 8_000; - api.on("before_agent_start", async (event: any, ctx: any) => { + api.on("before_prompt_build", async (event: any, ctx: any) => { if (!event.prompt || event.prompt.length < 5) return; // Skip non-interactive triggers (cron, heartbeat, automation) diff --git a/openclaw/openclaw-plugin-sdk.d.ts b/openclaw/openclaw-plugin-sdk.d.ts index c1719fac8..95d923fb3 100644 --- a/openclaw/openclaw-plugin-sdk.d.ts +++ b/openclaw/openclaw-plugin-sdk.d.ts @@ -9,19 +9,46 @@ declare module "openclaw/plugin-sdk" { }; resolvePath(p: string): string; registerTool( - definition: Record, - metadata?: Record, + definition: { + name: string; + description: string; + parameters: unknown; + execute: ( + toolCallId: string, + params: Record, + ) => Promise<{ content: Array<{ type: string; text: string }>; [key: string]: unknown }>; + [key: string]: unknown; + }, + metadata?: { optional?: boolean; [key: string]: unknown }, ): void; on(event: string, handler: (event: any, ctx: any) => any): void; registerCli( handler: (context: { program: any }) => void, options?: Record, ): void; + registerCommand?(definition: Record): void; registerService(service: { id: string; - start: () => void; + start: (...args: any[]) => void; stop: () => void; }): void; [key: string]: unknown; } } + +declare module "openclaw/plugin-sdk/plugin-entry" { + import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; + + export interface PluginEntry { + id: string; + name: string; + description?: string; + register(api: OpenClawPluginApi): void; + } + + export function definePluginEntry(entry: T): T; +} + +declare module "openclaw/plugin-sdk/core" { + export * from "openclaw/plugin-sdk"; +} diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json index ce71b1786..70f56d6f1 100644 --- a/openclaw/openclaw.plugin.json +++ b/openclaw/openclaw.plugin.json @@ -7,7 +7,7 @@ "contracts": { "tools": [ "memory_search", "memory_add", "memory_get", "memory_list", - "memory_update", "memory_delete" + "memory_update", "memory_delete", "memory_event_list", "memory_event_status" ] }, "providerAuthEnvVars": { diff --git a/openclaw/providers.ts b/openclaw/providers.ts index ace456270..b33de702d 100644 --- a/openclaw/providers.ts +++ b/openclaw/providers.ts @@ -81,6 +81,7 @@ class PlatformProvider implements Mem0Provider { constructor( private readonly apiKey: string, + private readonly baseUrl?: string, private readonly orgId?: string, private readonly projectId?: string, ) {} @@ -97,11 +98,17 @@ class PlatformProvider implements Mem0Provider { private async _init(): Promise { const { default: MemoryClient } = await import("mem0ai"); - const opts: { apiKey: string; org_id?: string; project_id?: string } = { + const opts: { + apiKey: string; + host?: string; + organizationId?: string; + projectId?: string; + } = { apiKey: this.apiKey, }; - if (this.orgId) opts.org_id = this.orgId; - if (this.projectId) opts.project_id = this.projectId; + 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); } @@ -133,20 +140,11 @@ class PlatformProvider implements Mem0Provider { async search(query: string, options: SearchOptions): Promise { await this.ensureClient(); - // Base filters: always scope by user_id, optionally by run_id - const baseFilters: Record = { user_id: options.user_id }; - if (options.run_id) baseFilters.run_id = options.run_id; - - // Merge agent-provided filters (created_at ranges, metadata, etc.) - // with base filters. Agent filters extend, never override user scoping. - const mergedFilters = options.filters - ? { AND: [baseFilters, options.filters] } - : baseFilters; - const opts: Record = { api_version: "v2", - filters: mergedFilters, + user_id: options.user_id, }; + if (options.run_id) opts.run_id = options.run_id; if (options.top_k != null) opts.top_k = options.top_k; if (options.threshold != null) opts.threshold = options.threshold; if (options.keyword_search != null) @@ -155,6 +153,14 @@ class PlatformProvider implements Mem0Provider { if (options.filter_memories != null) opts.filter_memories = options.filter_memories; if (options.categories != null) opts.categories = options.categories; + const baseFilters: Record = { user_id: options.user_id }; + if (options.run_id) baseFilters.run_id = options.run_id; + + if (options.filters) { + opts.filters = { AND: [baseFilters, options.filters] }; + } else { + opts.filters = baseFilters; + } const results = await this.client.search(query, opts); return normalizeSearchResults(results); @@ -168,10 +174,16 @@ class PlatformProvider implements Mem0Provider { async getAll(options: ListOptions): Promise { await this.ensureClient(); - const opts: Record = { user_id: options.user_id }; - if (options.run_id) opts.run_id = options.run_id; + const opts: Record = { + api_version: "v2", + user_id: options.user_id, + filters: { user_id: options.user_id }, + }; + if (options.run_id) { + opts.run_id = options.run_id; + (opts.filters as Record).run_id = options.run_id; + } if (options.page_size != null) opts.page_size = options.page_size; - if (options.source) opts.source = options.source; const results = await this.client.getAll(opts); if (Array.isArray(results)) return results.map(normalizeMemoryItem); @@ -419,7 +431,7 @@ export function createProvider( ); } - return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId); + return new PlatformProvider(cfg.apiKey!, cfg.baseUrl, cfg.orgId, cfg.projectId); } // ============================================================================ diff --git a/openclaw/test-shims/plugin-entry.ts b/openclaw/test-shims/plugin-entry.ts new file mode 100644 index 000000000..c7c2a8938 --- /dev/null +++ b/openclaw/test-shims/plugin-entry.ts @@ -0,0 +1,7 @@ +/** + * Test shim for openclaw/plugin-sdk/plugin-entry. + * At runtime this is resolved from the OpenClaw gateway. + */ +export function definePluginEntry(entry: T): T { + return entry; +} diff --git a/openclaw/test-shims/plugin-sdk.ts b/openclaw/test-shims/plugin-sdk.ts new file mode 100644 index 000000000..5ef59417c --- /dev/null +++ b/openclaw/test-shims/plugin-sdk.ts @@ -0,0 +1,19 @@ +/** + * Test shim for openclaw/plugin-sdk. + * At runtime this is resolved from the OpenClaw gateway. + */ +export interface OpenClawPluginApi { + pluginConfig: Record; + logger: { + info(msg: string): void; + warn(msg: string): void; + error(msg: string): void; + debug(msg: string): void; + }; + resolvePath(p: string): string; + registerTool(definition: Record, metadata?: Record): void; + on(event: string, handler: (event: any, ctx: any) => any): void; + registerCli(handler: (context: { program: any }) => void, options?: Record): void; + registerService(service: { id: string; start: (...args: any[]) => void; stop: () => void }): void; + [key: string]: unknown; +} diff --git a/openclaw/tests/tools.test.ts b/openclaw/tests/tools.test.ts index 9be032734..9515dbf50 100644 --- a/openclaw/tests/tools.test.ts +++ b/openclaw/tests/tools.test.ts @@ -15,6 +15,8 @@ import { createMemoryGetTool } from "../tools/memory-get.ts"; import { createMemoryDeleteTool } from "../tools/memory-delete.ts"; import { createMemoryListTool } from "../tools/memory-list.ts"; import { createMemoryUpdateTool } from "../tools/memory-update.ts"; +import { createMemoryEventListTool } from "../tools/memory-event-list.ts"; +import { createMemoryEventStatusTool } from "../tools/memory-event-status.ts"; // --------------------------------------------------------------------------- // Mock helper @@ -79,10 +81,10 @@ function createMockToolDeps(overrides = {}): ToolDeps { // --------------------------------------------------------------------------- describe("registerAllTools", () => { - it("calls api.registerTool exactly 6 times", () => { + it("calls api.registerTool exactly 8 times", () => { const ctx = createMockToolDeps(); registerAllTools(ctx); - expect(ctx.api.registerTool).toHaveBeenCalledTimes(6); + expect(ctx.api.registerTool).toHaveBeenCalledTimes(8); }); it("registers tools with the correct names", () => { @@ -102,6 +104,8 @@ describe("registerAllTools", () => { "memory_list", "memory_update", "memory_delete", + "memory_event_list", + "memory_event_status", ]); }); @@ -127,6 +131,8 @@ describe("tool factory shape", () => { { fn: createMemoryGetTool, expectedName: "memory_get" }, { fn: createMemoryDeleteTool, expectedName: "memory_delete" }, { fn: createMemoryListTool, expectedName: "memory_list" }, + { fn: createMemoryEventListTool, expectedName: "memory_event_list" }, + { fn: createMemoryEventStatusTool, expectedName: "memory_event_status" }, ]; for (const { fn, expectedName } of factories) { diff --git a/openclaw/tools/index.ts b/openclaw/tools/index.ts index d82e8db42..14219d4de 100644 --- a/openclaw/tools/index.ts +++ b/openclaw/tools/index.ts @@ -1,5 +1,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { Mem0Config, Mem0Provider, AddOptions, SearchOptions } from "../types.ts"; +import type { Backend } from "../backend/base.ts"; import { createMemorySearchTool } from "./memory-search.ts"; import { createMemoryAddTool } from "./memory-add.ts"; @@ -7,11 +8,14 @@ import { createMemoryGetTool } from "./memory-get.ts"; import { createMemoryListTool } from "./memory-list.ts"; import { createMemoryUpdateTool } from "./memory-update.ts"; import { createMemoryDeleteTool } from "./memory-delete.ts"; +import { createMemoryEventListTool } from "./memory-event-list.ts"; +import { createMemoryEventStatusTool } from "./memory-event-status.ts"; export interface ToolDeps { api: OpenClawPluginApi; provider: Mem0Provider; cfg: Mem0Config; + backend?: Backend; resolveUserId: (opts: { agentId?: string; userId?: string }) => string; effectiveUserId: (sessionKey?: string) => string; agentUserId: (id: string) => string; @@ -31,4 +35,6 @@ export function registerAllTools(deps: ToolDeps): void { api.registerTool(createMemoryListTool(deps)); api.registerTool(createMemoryUpdateTool(deps)); api.registerTool(createMemoryDeleteTool(deps)); + api.registerTool(createMemoryEventListTool(deps)); + api.registerTool(createMemoryEventStatusTool(deps)); } diff --git a/openclaw/tools/memory-event-list.ts b/openclaw/tools/memory-event-list.ts new file mode 100644 index 000000000..37f89b79e --- /dev/null +++ b/openclaw/tools/memory-event-list.ts @@ -0,0 +1,60 @@ +import { Type } from "@sinclair/typebox"; +import type { ToolDeps } from "./index.ts"; + +export function createMemoryEventListTool(deps: ToolDeps) { + return { + name: "memory_event_list", + label: "Memory Event List", + description: + "List recent background processing events from the Mem0 Platform. Use to check whether memory operations (add, update, delete) were processed successfully.", + parameters: Type.Object({}), + + async execute(_toolCallId: string, _params: Record) { + const start = Date.now(); + try { + if (!deps.backend) { + deps.captureToolEvent("memory_event_list", { success: false, latency_ms: 0, error: "not_platform" }); + return { + content: [{ type: "text", text: "Event tracking is only available in platform mode." }], + details: { error: "not_platform" }, + }; + } + + const results = await deps.backend.listEvents(); + if (!results.length) { + deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: 0 }); + return { + content: [{ type: "text", text: "No events found." }], + details: { count: 0 }, + }; + } + + const rows = results.map((ev) => { + const evId = String(ev.id ?? ""); + const evType = String(ev.event_type ?? "—"); + const status = String(ev.status ?? "—"); + const latency = + typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—"; + const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " "); + return { id: evId, type: evType, status, latency, created }; + }); + + const text = rows + .map((r) => `- ${r.id} | ${r.type} | ${r.status} | ${r.latency} | ${r.created}`) + .join("\n"); + + deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: results.length }); + return { + content: [{ type: "text", text: `${results.length} event(s):\n${text}` }], + details: { count: results.length, events: rows }, + }; + } catch (err) { + deps.captureToolEvent("memory_event_list", { success: false, latency_ms: Date.now() - start, error: String(err) }); + return { + content: [{ type: "text", text: `Failed to list events: ${String(err)}` }], + details: { error: String(err) }, + }; + } + }, + }; +} diff --git a/openclaw/tools/memory-event-status.ts b/openclaw/tools/memory-event-status.ts new file mode 100644 index 000000000..b11c5bca0 --- /dev/null +++ b/openclaw/tools/memory-event-status.ts @@ -0,0 +1,63 @@ +import { Type } from "@sinclair/typebox"; +import type { ToolDeps } from "./index.ts"; + +export function createMemoryEventStatusTool(deps: ToolDeps) { + return { + name: "memory_event_status", + label: "Memory Event Status", + description: + "Get detailed status of a specific background processing event. Use to verify whether a memory add/update/delete was processed, view latency, and inspect results.", + parameters: Type.Object({ + event_id: Type.String({ description: "The event ID to check" }), + }), + + async execute(_toolCallId: string, params: Record) { + const { event_id: eventId } = params as { event_id: string }; + const start = Date.now(); + try { + if (!deps.backend) { + deps.captureToolEvent("memory_event_status", { success: false, latency_ms: 0, error: "not_platform" }); + return { + content: [{ type: "text", text: "Event tracking is only available in platform mode." }], + details: { error: "not_platform" }, + }; + } + + const ev = await deps.backend.getEvent(eventId); + + const status = String(ev.status ?? "—"); + const evType = String(ev.event_type ?? "—"); + const latency = + typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—"; + const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " "); + const updated = String(ev.updated_at ?? "—").slice(0, 19).replace("T", " "); + + let text = `Event: ${eventId}\nType: ${evType}\nStatus: ${status}\nLatency: ${latency}\nCreated: ${created}\nUpdated: ${updated}`; + + const results = ev.results as Record[] | undefined; + if (results && Array.isArray(results) && results.length) { + const resultLines = results.map((r) => { + const memId = String(r.id ?? "").slice(0, 8); + const data = r.data as Record | undefined; + const memory = data?.memory ?? ""; + const evName = String(r.event ?? ""); + return `- [${evName}] ${memory} (${memId})`; + }); + text += `\n\nResults (${results.length}):\n${resultLines.join("\n")}`; + } + + deps.captureToolEvent("memory_event_status", { success: true, latency_ms: Date.now() - start }); + return { + content: [{ type: "text", text }], + details: { event: ev }, + }; + } catch (err) { + deps.captureToolEvent("memory_event_status", { success: false, latency_ms: Date.now() - start, error: String(err) }); + return { + content: [{ type: "text", text: `Failed to get event: ${String(err)}` }], + details: { error: String(err) }, + }; + } + }, + }; +} diff --git a/openclaw/tsconfig.json b/openclaw/tsconfig.json index 1e360fd7a..2292f3295 100644 --- a/openclaw/tsconfig.json +++ b/openclaw/tsconfig.json @@ -19,6 +19,6 @@ "allowImportingTsExtensions": true, "noEmit": true }, - "include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts"], + "include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts", "telemetry.ts", "fs-safe.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts"] } diff --git a/openclaw/tsup.config.ts b/openclaw/tsup.config.ts index f47f6d802..397df11fe 100644 --- a/openclaw/tsup.config.ts +++ b/openclaw/tsup.config.ts @@ -7,5 +7,5 @@ export default defineConfig({ dts: true, sourcemap: true, clean: true, - external: [/^node:/, "fs", "os", "path", "url", "readline", "module"], + external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module"], }); diff --git a/openclaw/vitest.config.ts b/openclaw/vitest.config.ts new file mode 100644 index 000000000..94b1bab79 --- /dev/null +++ b/openclaw/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + alias: { + // OpenClaw SDK modules are resolved from the gateway at runtime. + // During unit tests we provide lightweight stubs. + "openclaw/plugin-sdk/plugin-entry": new URL( + "./test-shims/plugin-entry.ts", + import.meta.url, + ).pathname, + "openclaw/plugin-sdk": new URL( + "./test-shims/plugin-sdk.ts", + import.meta.url, + ).pathname, + }, + }, +});