Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions integrations/openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 18 additions & 2 deletions integrations/openclaw/plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -120,6 +121,21 @@ const plugin = {
};
const client = createClient(cfg, api);

if (typeof api.registerMemoryCapability === "function") {
api.registerMemoryCapability({
// OpenClaw passes { availableTools: Set<string>, 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() : "";
Expand Down
62 changes: 62 additions & 0 deletions test/openclaw-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it, vi } from "vitest";

type Capability = {
promptBuilder?: (params: {
availableTools: Set<string>;
}) => string[] | undefined;
};

type RegisterFn = (capability: Capability) => void;

interface FakeApi {
registerMemoryCapability: RegisterFn;
on: ReturnType<typeof vi.fn>;
pluginConfig: Record<string, unknown>;
logger: { warn: ReturnType<typeof vi.fn> };
}

function makeApi(overrides: Partial<FakeApi> = {}): 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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).mock.calls[0][0] as Capability;
const lines = capability.promptBuilder?.({ availableTools: new Set() }) ?? [];
expect(lines.join("\n")).toMatch(/memory\.internal:9999/);
});
});
Loading