Skip to content
Closed
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/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with the OpenClaw packag

## Project Overview

`@basicmemory/openclaw-basic-memory` is a TypeScript OpenClaw plugin that integrates [Basic Memory](https://github.com/basicmachines-co/basic-memory) with the OpenClaw agent framework. It lives under `integrations/openclaw/` in the monorepo, manages a persistent MCP stdio session to a `bm mcp` process, exposes 14 agent tools (including workspace/project management and cross-project operations), composited memory search/get providers, slash commands, CLI commands, and optional auto-capture of conversations.
`@basicmemory/openclaw-basic-memory` is a TypeScript OpenClaw plugin that integrates [Basic Memory](https://github.com/basicmachines-co/basic-memory) with the OpenClaw agent framework. It lives under `integrations/openclaw/` in the monorepo, manages a persistent MCP stdio session to a `bm mcp` process, exposes 12 agent tools (including workspace/project management and cross-project operations), registers an OpenClaw memory capability and context engine, slash commands, CLI commands, and optional auto-capture of conversations.

## Development Commands

Expand Down Expand Up @@ -66,7 +66,14 @@ Central orchestration layer that:
Each tool file exports a function that calls `api.registerTool()` with a TypeBox schema and handler. Tools delegate to `BmClient` methods and return OpenClaw-standard responses (`{ content: [{type: "text", text}], details? }`).

- `search-notes.ts`, `read-note.ts`, `write-note.ts`, `edit-note.ts`, `delete-note.ts`, `move-note.ts`, `build-context.ts`, `list-memory-projects.ts`, `list-workspaces.ts`, `schema-validate.ts`, `schema-infer.ts`, `schema-diff.ts` — thin wrappers around `BmClient`; all content tools accept an optional `project` param for cross-project operations
- `memory-provider.ts` — composited `memory_search` + `memory_get` providers. `memory_search` queries 3 sources in parallel: MEMORY.md (grep), BM knowledge graph (FTS + vector), and active task notes (YAML frontmatter scan)

### Memory capability (`memory/basic-memory-capability.ts`)

Registered with `api.registerMemoryCapability`. OpenClaw 2026.10's bundled Memory Core plugin owns the `memory_search` and `memory_get` tool names, so the plugin does not register them. The capability supplies:
- `providerRuntime` — host-facing search, get, health and close over the shared `BmClient` (gateway `memory.search` v2, `memory.status`, doctor)
- `recallToolNames` — `search_notes`, `read_note`, `build_context` for Active Memory deep recall
- `providerFlushPlanResolver` — a tools-arm pre-compaction flush through `write_note` / `edit_note`
- `promptBuilder` — a system-prompt section steering agents to the Basic Memory tools

### Commands & Hooks

Expand Down
7 changes: 5 additions & 2 deletions integrations/openclaw/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ openclaw-basic-memory/
│ ├── list-workspaces.ts # list_workspaces
│ ├── schema-validate.ts # schema_validate
│ ├── schema-infer.ts # schema_infer
│ ├── schema-diff.ts # schema_diff
│ └── memory-provider.ts # Composited memory_search + memory_get
│ └── schema-diff.ts # schema_diff
├── memory/
│ └── basic-memory-capability.ts # OpenClaw memory capability (host search, recall tools, flush)
├── context-engine/
│ └── basic-memory-context-engine.ts # Auto-recall and conversation capture
├── commands/
│ ├── slash.ts # /remember, /recall
│ ├── skills.ts # /tasks, /reflect, /defrag, /schema
Expand Down
2 changes: 2 additions & 0 deletions integrations/openclaw/MEMORY_TASK_FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This document explains how `openclaw-basic-memory` manages short-term memory, long-term memory, and active tasks during normal agent usage.

> **Superseded on OpenClaw 2026.10 and later.** The combined `memory_search` described below no longer runs. OpenClaw's bundled Memory Core plugin owns the `memory_search` and `memory_get` tool names, so this plugin now registers an OpenClaw memory capability instead (`memory/basic-memory-capability.ts`). Agents search Basic Memory with `search_notes` and read notes with `read_note`; see the README's "Memory search" section. The note and task conventions below still apply.

## Mental Model

The plugin composes memory from three places:
Expand Down
20 changes: 11 additions & 9 deletions integrations/openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ This plugin connects Basic Memory to OpenClaw so your agent can:
- **Remember across sessions** — search and recall past conversations, decisions, and context
- **Track work in progress** — structured tasks that survive context compaction
- **Build knowledge over time** — notes, observations, and relations that grow into a connected graph
- **Search intelligently** — composited search across working memory, the knowledge graph, and active tasks in parallel
- **Search intelligently** — hybrid full-text and vector search across the knowledge graph

All data stays on your machine as Markdown files indexed locally with SQLite. Cloud sync is available but entirely optional.

Expand All @@ -60,7 +60,7 @@ openclaw gateway restart

The plugin fills two OpenClaw slots, and each one does a different job:

- **`memory`** provides the `memory_search` and `memory_get` tools.
- **`memory`** registers Basic Memory as OpenClaw's memory provider: gateway memory search and status, `openclaw doctor` health, Active Memory recall, and the pre-compaction memory flush.
- **`contextEngine`** runs automatic recall at session start and automatic conversation capture after each turn. **Without this slot, nothing is captured or recalled automatically**; the agent only remembers what it saves with an explicit tool call.

Tested with OpenClaw 2026.10.9. OpenClaw 2026.10 and later require context engines to commit turns through `commitTurn`, which this plugin implements; older plugin versions are degraded to OpenClaw's legacy engine on those hosts and capture nothing.
Expand Down Expand Up @@ -141,13 +141,17 @@ This uses sensible defaults: auto-generated project name, maps to your workspace

### Memory search

When your agent calls `memory_search`, three sources are queried in parallel:
Agents search Basic Memory with `search_notes` and read results with `read_note`. The plugin adds a short system-prompt section that points the agent at these tools.

1. **MEMORY.md** — text search with surrounding context
2. **Knowledge Graph** — hybrid full-text + vector search across all indexed notes
3. **Active Tasks** — scans `memory/tasks/` for in-progress work
OpenClaw's built-in `memory_search` and `memory_get` tools belong to its bundled Memory Core plugin, which OpenClaw keeps loaded even when this plugin holds the memory slot. Those tools search only the workspace `MEMORY.md` and `memory/` files, not Basic Memory.

Results come back in clear sections so the agent knows where each piece of context came from.
As the memory slot owner, the plugin also serves OpenClaw's own memory consumers:

- **Gateway memory search, get and status, and `openclaw doctor`** read Basic Memory through the plugin's provider runtime.
- **Active Memory deep recall** uses `search_notes`, `read_note` and `build_context`.
- **Pre-compaction flush** saves durable context with `write_note` and `edit_note` instead of appending to a workspace file.

Search scores are shown as raw numbers. A score above 1 means the note matched on both keywords and meaning.

### Auto-recall

Expand All @@ -167,8 +171,6 @@ All tools accept an optional `project` parameter for cross-project operations.

| Tool | Description |
|------|-------------|
| `memory_search` | Composited search across all memory sources |
| `memory_get` | Read a specific note by title or path |
| `search_notes` | Search the knowledge graph directly |
| `read_note` | Read a note by title, permalink, or `memory://` URL |
| `write_note` | Create or overwrite a note with optional tags, note type, and custom frontmatter metadata |
Expand Down
16 changes: 16 additions & 0 deletions integrations/openclaw/bm-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,22 @@ describe("BmClient MCP behavior", () => {
expect(args[4]).toBeUndefined() // overwrite
})

it("reports the service as running only between start() and stop()", async () => {
;(client as any).connectWithRetries = jest.fn().mockResolvedValue(undefined)
;(client as any).disconnectCurrent = jest.fn().mockResolvedValue(undefined)

expect(client.isServiceRunning()).toBe(false)
// A lazy connection from a tool call is not service ownership.
setConnected(client, jest.fn())
expect(client.isServiceRunning()).toBe(false)

await client.start()
expect(client.isServiceRunning()).toBe(true)

await client.stop()
expect(client.isServiceRunning()).toBe(false)
})

describe("indexConversation with a turn key", () => {
// In-memory stand-in for the conversation notes so the duplicate check,
// append, and create paths run against shared state.
Expand Down
12 changes: 11 additions & 1 deletion integrations/openclaw/bm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ function isRecoverableConnectionError(err: unknown): boolean {
)
}

function isNoteNotFoundError(err: unknown): boolean {
export function isNoteNotFoundError(err: unknown): boolean {
return (
err instanceof BmToolError &&
parseRefusalPayload(err.toolMessage)?.error === "NOT_FOUND"
Expand Down Expand Up @@ -305,6 +305,10 @@ export class BmClient {
private cwd?: string
private env?: Record<string, string>
private shouldRun = false
// True only between start() and stop(), i.e. while the plugin service owns
// the BM MCP session. A lazy connection opened by a tool or memory call in a
// one-shot CLI process leaves this false.
private serviceRunning = false

private client: Client | null = null
private transport: StdioClientTransport | null = null
Expand All @@ -324,6 +328,7 @@ export class BmClient {
env?: Record<string, string>
}): Promise<void> {
this.shouldRun = true
this.serviceRunning = true
if (options?.cwd) {
this.cwd = options.cwd
}
Expand All @@ -334,8 +339,13 @@ export class BmClient {
await this.connectWithRetries()
}

isServiceRunning(): boolean {
return this.serviceRunning
}

async stop(): Promise<void> {
this.shouldRun = false
this.serviceRunning = false
await this.disconnectCurrent(this.client, this.transport)
this.client = null
this.transport = null
Expand Down
2 changes: 1 addition & 1 deletion integrations/openclaw/commands/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function registerCli(
}

for (const r of results) {
const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : ""
const score = r.score ? ` (score ${r.score.toFixed(2)})` : ""
console.log(`- ${r.title}${score}`)
if (r.content) {
const preview =
Expand Down
2 changes: 1 addition & 1 deletion integrations/openclaw/commands/slash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function registerCommands(
}

const lines = results.map((r, i) => {
const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : ""
const score = r.score ? ` (score ${r.score.toFixed(2)})` : ""
return `${i + 1}. ${r.title}${score}`
})

Expand Down
15 changes: 15 additions & 0 deletions integrations/openclaw/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe("plugin service lifecycle", () => {
registerCommand: jest.fn(),
registerCli: jest.fn(),
registerContextEngine: jest.fn(),
registerMemoryCapability: jest.fn(),
registerService: jest.fn((service: any) => {
services.push(service)
}),
Expand All @@ -62,6 +63,20 @@ describe("plugin service lifecycle", () => {
)
expect(api.on).not.toHaveBeenCalled()

// memory-core owns memory_search and memory_get on OpenClaw 2026.10, so
// the plugin serves the host through its memory capability instead.
expect(api.registerMemoryCapability).toHaveBeenCalledWith(
expect.objectContaining({
providerRuntime: expect.any(Object),
recallToolNames: ["search_notes", "read_note", "build_context"],
}),
)
const toolNames = api.registerTool.mock.calls.map(
([tool]) => (tool as { name: string }).name,
)
expect(toolNames).not.toContain("memory_search")
expect(toolNames).not.toContain("memory_get")

await services[0].start({ workspaceDir: "/tmp/workspace" })

expect(startSpy).toHaveBeenCalledWith({ cwd: "/tmp/workspace" })
Expand Down
20 changes: 10 additions & 10 deletions integrations/openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@ import {
} from "./config.ts"
import { BasicMemoryContextEngine } from "./context-engine/basic-memory-context-engine.ts"
import { initLogger, log } from "./logger.ts"
import { buildBasicMemoryCapability } from "./memory/basic-memory-capability.ts"
import { CONVERSATION_SCHEMA_CONTENT } from "./schema/conversation-schema.ts"
import { TASK_SCHEMA_CONTENT } from "./schema/task-schema.ts"
import { registerContextTool } from "./tools/build-context.ts"
import { registerDeleteTool } from "./tools/delete-note.ts"
import { registerEditTool } from "./tools/edit-note.ts"
import { registerProjectListTool } from "./tools/list-memory-projects.ts"
import { registerWorkspaceListTool } from "./tools/list-workspaces.ts"
import {
registerMemoryProvider,
setWorkspaceDir,
} from "./tools/memory-provider.ts"
import { registerMoveTool } from "./tools/move-note.ts"
import { registerReadTool } from "./tools/read-note.ts"
import { registerSchemaDiffTool } from "./tools/schema-diff.ts"
Expand All @@ -44,7 +41,7 @@ export default definePluginEntry({
id: "openclaw-basic-memory",
name: "Basic Memory",
description:
"Local-first knowledge graph for OpenClaw — persistent memory with graph search and composited memory_search",
"Local-first knowledge graph for OpenClaw — persistent memory with graph search, recall and conversation capture",
configSchema: basicMemoryConfigSchema,

register(api) {
Expand Down Expand Up @@ -72,9 +69,14 @@ export default definePluginEntry({
registerSchemaInferTool(api, client)
registerSchemaDiffTool(api, client)

// --- Composited memory_search + memory_get (always registered) ---
registerMemoryProvider(api, client, cfg)
log.info("registered composited memory_search + memory_get")
// --- Memory capability ---
// OpenClaw 2026.10 owns the memory_search and memory_get tool names
// (memory-core registers them even when another plugin holds the memory
// slot), so the plugin serves the host through registerMemoryCapability:
// gateway memory search, status and doctor, Active Memory recall, and the
// pre-compaction flush.
api.registerMemoryCapability(buildBasicMemoryCapability(client))
log.info("registered Basic Memory memory capability")
api.registerContextEngine(
"openclaw-basic-memory",
() => new BasicMemoryContextEngine(client, cfg),
Expand Down Expand Up @@ -166,8 +168,6 @@ export default definePluginEntry({
}
}

setWorkspaceDir(workspace)

log.info("connected — BM MCP stdio session running")
},
stop: async () => {
Expand Down
Loading
Loading