From 39f3a45fe1e3c770d870f36850d5f2f8f56753dc Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Tue, 12 May 2026 20:32:35 +0100 Subject: [PATCH 1/2] fix(openclaw): claim plugins.slots.memory via registerMemoryCapability The OpenClaw integration declared kind: "memory" in its manifest and wired before_agent_start / agent_end hooks, but never called api.registerMemoryCapability(...). On OpenClaw 2026.4.9+ that left plugins.slots.memory = "agentmemory" reporting unavailable even though the hooks worked end-to-end and the REST API was healthy. Per OpenClaw's plugin SDK declarations (plugin-sdk/src/plugins/types.d.ts), api.registerMemoryCapability takes a MemoryPluginCapability and is the canonical way to occupy the memory slot. All capability fields are optional. The promptBuilder field has signature: (params: { availableTools: Set; citationsMode? }) => string[] This commit: - Calls api.registerMemoryCapability({ promptBuilder }) at register time when the host exposes it. promptBuilder returns three lines describing agentmemory as the active memory provider, including the configured base_url so the agent knows where recall is coming from. - Guards on `typeof api.registerMemoryCapability === "function"` so older OpenClaw builds (pre-capability API) still load via the existing hook-only path. Net behaviour on older hosts is unchanged. - Does NOT register a runtime adapter. OpenClaw's current MemoryRuntimeBackendConfig type is exactly { backend: "builtin" } or { backend: "qmd"; qmd?: { command?: string } }; both are in-process backends and don't fit an external REST service. The hook-driven recall + capture flow is the working integration path; runtime registration is documented as a follow-up gated on an upstream OpenClaw change to add an "external" backend variant. README troubleshooting block adds a specific entry for the "unavailable slot" symptom, pointing at the registerMemoryCapability fix. The README also documents the runtime-adapter scope decision so users running deep host integrations aren't surprised. 3 new tests in test/openclaw-plugin.test.ts assert: 1. registerMemoryCapability is called with a promptBuilder when the host supports it. 2. Plugin still registers hooks and does not throw on older hosts that don't expose registerMemoryCapability. 3. promptBuilder respects the configured base_url. 871 / 871 tests pass. Verified against the openclaw npm package surface: - api.registerMemoryCapability is declared at plugin-sdk/src/plugins/types.d.ts:2028 - MemoryPluginCapability is declared at plugin-sdk/src/plugins/memory-state.d.ts - promptBuilder signature matches MemoryPromptSectionBuilder - No import from openclaw/extensions/* needed (and that path is not in the package's exports map anyway) This is a fresh implementation. The closed PR #302 attempted the same end goal but imported memoryRuntime from a non-existent path (openclaw/extensions/memory-core/runtime-api) and would have failed with ERR_PACKAGE_PATH_NOT_EXPORTED at load time. --- integrations/openclaw/README.md | 11 ++++-- integrations/openclaw/plugin.mjs | 17 +++++++-- test/openclaw-plugin.test.ts | 62 ++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 test/openclaw-plugin.test.ts 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..f2255d06 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,18 @@ const plugin = { }; const client = createClient(cfg, api); + if (typeof api.registerMemoryCapability === "function") { + api.registerMemoryCapability({ + promptBuilder: () => [ + "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/); + }); +}); From 10ef4b7b079c5fe3cde43aab1a3b32e8dc981f10 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Tue, 12 May 2026 23:37:38 +0100 Subject: [PATCH 2/2] fix(openclaw): accept params arg in promptBuilder to match MemoryPromptSectionBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer flagged that promptBuilder was declared as () => [...] which doesn't match the MemoryPluginCapability contract verified from openclaw@2026.5.7 (plugin-sdk/src/plugins/memory-state.d.ts): type MemoryPromptSectionBuilder = (params: { availableTools: Set; citationsMode?: MemoryCitationsMode; }) => string[]; JavaScript would tolerate the zero-arg form at runtime (extra args are ignored), but the signature was misleading and would fail a TypeScript port. Updated to (_params) => [...] with a comment explaining we accept-but-ignore. Underscore-prefix conveys intentional non-use to lint/readers. Validation: 3/3 openclaw tests still pass (they invoke promptBuilder({ availableTools: new Set() }) — both old and new signatures accept that call). --- integrations/openclaw/plugin.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/integrations/openclaw/plugin.mjs b/integrations/openclaw/plugin.mjs index f2255d06..7579a9d8 100644 --- a/integrations/openclaw/plugin.mjs +++ b/integrations/openclaw/plugin.mjs @@ -123,7 +123,10 @@ const plugin = { if (typeof api.registerMemoryCapability === "function") { api.registerMemoryCapability({ - promptBuilder: () => [ + // 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 + ").",