diff --git a/integrations/openclaw/README.md b/integrations/openclaw/README.md index 8dc0258d..1fe774ae 100644 --- a/integrations/openclaw/README.md +++ b/integrations/openclaw/README.md @@ -125,14 +125,21 @@ Then enable it in `~/.openclaw/openclaw.json`: What the plugin does: -- recalls relevant long-term memory before the agent starts -- captures completed conversation turns after the agent finishes +- claims the `plugins.slots.memory = "agentmemory"` slot via `api.registerMemoryCapability({ promptBuilder })` so OpenClaw recognises it as the active memory plugin +- recalls relevant long-term memory before the agent starts (via the `before_agent_start` hook) +- captures completed conversation turns after the agent finishes (via the `agent_end` hook) - shares the same backend with Claude Code, Codex CLI, Gemini CLI, Hermes, pi, and other agents +### Memory runtime (current scope) + +The plugin currently registers a `promptBuilder` only — not a full `MemoryPluginRuntime` adapter. OpenClaw's `MemoryRuntimeBackendConfig` type today is `{ backend: "builtin" }` or `{ backend: "qmd" }`; both are openclaw-internal backends that don't fit agentmemory's external REST shape. The hook-driven recall + capture flow above is the working integration path. If you need OpenClaw's in-process memory-runtime APIs (e.g. `getMemorySearchManager`) backed by agentmemory, file an upstream request against `openclaw` for an `"external"` backend type and we'll wire `runtime` here once the contract supports it. + ## Troubleshooting **Plugin validates but does not load** — make sure the folder contains `package.json`, `openclaw.plugin.json`, and `plugin.mjs`, and that `plugins.slots.memory` is set to `agentmemory`. +**`plugins.slots.memory = "agentmemory"` reports `unavailable`** — upgrade to v0.9.11+. Older versions of this plugin registered hooks but never called `api.registerMemoryCapability(...)`, so the memory-slot machinery did not consider the slot claimed. The current plugin registers a memory capability (prompt builder) at startup, which is the documented OpenClaw API for occupying the slot. + **Connection refused on port 3111** — the agentmemory server is not running. Start it with `npx @agentmemory/agentmemory`. **No memories returned** — open `http://localhost:3113` and verify observations are being captured. diff --git a/integrations/openclaw/plugin.mjs b/integrations/openclaw/plugin.mjs index 1ed9a3b5..7579a9d8 100644 --- a/integrations/openclaw/plugin.mjs +++ b/integrations/openclaw/plugin.mjs @@ -2,8 +2,9 @@ * agentmemory plugin for OpenClaw * * Deeper integration than raw MCP: - * - recalls relevant memories before the agent starts - * - captures completed conversation turns after the agent finishes + * - claims the plugins.slots.memory slot via api.registerMemoryCapability({ promptBuilder }) + * - recalls relevant memories before the agent starts (before_agent_start hook) + * - captures completed conversation turns after the agent finishes (agent_end hook) * * Requires the agentmemory server on localhost:3111. * Start it with: npx @agentmemory/agentmemory @@ -120,6 +121,21 @@ const plugin = { }; const client = createClient(cfg, api); + if (typeof api.registerMemoryCapability === "function") { + api.registerMemoryCapability({ + // OpenClaw passes { availableTools: Set, citationsMode? }. We + // don't currently branch on tool availability, but accept the params + // object so the signature matches MemoryPromptSectionBuilder exactly. + promptBuilder: (_params) => [ + "Long-term memory provider: agentmemory (external REST service on " + + client.baseUrl + + ").", + "agentmemory recalls relevant prior observations before each turn via the before_agent_start hook and captures completed turns via agent_end.", + "Treat recalled context as background, not authoritative — prefer current workspace state and explicit user instructions when they conflict.", + ], + }); + } + api.on("before_agent_start", async (event) => { if (!cfg.enabled) return; const prompt = typeof event?.prompt === "string" ? event.prompt.trim() : ""; diff --git a/test/openclaw-plugin.test.ts b/test/openclaw-plugin.test.ts new file mode 100644 index 00000000..ed95d493 --- /dev/null +++ b/test/openclaw-plugin.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it, vi } from "vitest"; + +type Capability = { + promptBuilder?: (params: { + availableTools: Set; + }) => string[] | undefined; +}; + +type RegisterFn = (capability: Capability) => void; + +interface FakeApi { + registerMemoryCapability: RegisterFn; + on: ReturnType; + pluginConfig: Record; + logger: { warn: ReturnType }; +} + +function makeApi(overrides: Partial = {}): FakeApi { + return { + registerMemoryCapability: vi.fn(), + on: vi.fn(), + pluginConfig: { base_url: "http://localhost:3111" }, + logger: { warn: vi.fn() }, + ...overrides, + }; +} + +describe("openclaw plugin — memory capability registration (closes #286 follow-up)", () => { + it("calls api.registerMemoryCapability with a promptBuilder when the host supports it", async () => { + const mod = await import("../integrations/openclaw/plugin.mjs"); + const plugin = (mod as unknown as { default: { register(api: FakeApi): void } }).default; + const api = makeApi(); + plugin.register(api); + expect(api.registerMemoryCapability).toHaveBeenCalledTimes(1); + const capability = (api.registerMemoryCapability as ReturnType).mock.calls[0][0] as Capability; + expect(typeof capability.promptBuilder).toBe("function"); + const lines = capability.promptBuilder?.({ availableTools: new Set() }); + expect(Array.isArray(lines)).toBe(true); + expect((lines as string[]).join(" ")).toMatch(/agentmemory/i); + }); + + it("still registers hooks and tolerates older OpenClaw builds without registerMemoryCapability", async () => { + const mod = await import("../integrations/openclaw/plugin.mjs"); + const plugin = (mod as unknown as { default: { register(api: FakeApi): void } }).default; + const api = makeApi({ registerMemoryCapability: undefined as unknown as RegisterFn }); + expect(() => plugin.register(api)).not.toThrow(); + expect(api.on).toHaveBeenCalled(); + const events = (api.on as ReturnType).mock.calls.map((c) => c[0]); + expect(events).toContain("before_agent_start"); + expect(events).toContain("agent_end"); + }); + + it("promptBuilder returns lines that mention the configured base_url", async () => { + const mod = await import("../integrations/openclaw/plugin.mjs"); + const plugin = (mod as unknown as { default: { register(api: FakeApi): void } }).default; + const api = makeApi({ pluginConfig: { base_url: "http://memory.internal:9999" } }); + plugin.register(api); + const capability = (api.registerMemoryCapability as ReturnType).mock.calls[0][0] as Capability; + const lines = capability.promptBuilder?.({ availableTools: new Set() }) ?? []; + expect(lines.join("\n")).toMatch(/memory\.internal:9999/); + }); +});