Skip to content

fix(integrations): serve OpenClaw memory through registerMemoryCapability - #1968

Closed
groksrc wants to merge 2 commits into
fix/1880-openclaw-commit-turnfrom
fix/openclaw-memory-capability
Closed

groksrc wants to merge 2 commits into
fix/1880-openclaw-commit-turnfrom
fix/openclaw-memory-capability

Conversation

@groksrc

@groksrc groksrc commented Oct 29, 2026

Copy link
Copy Markdown
Member

This PR makes Basic Memory OpenClaw's actual memory provider on OpenClaw 2026.10 and later. Today the plugin's memory tools never run there, and OpenClaw's memory features read only workspace files.

Stacked on #1966. The base branch is fix/1880-openclaw-commit-turn, so this diff shows only this change. Merge #1966 first; this PR's base then moves to main.

Problem

Two separate faults made the plugin's memory search invisible on OpenClaw 2026.10.9:

  1. Duplicate tool names. The plugin registered memory_search and memory_get with api.registerTool (tools/memory-provider.ts). OpenClaw's bundled Memory Core plugin registers the same names, even when another plugin owns the memory slot (dist/extensions/memory-core/index.js:989 in the installed OpenClaw). OpenClaw rejects ours and logs plugin tool name conflict (openclaw-basic-memory): memory_search. The plugin's combined search never ran.
  2. No memory capability. OpenClaw 2026.10 serves memory through api.registerMemoryCapability. Its docs call this "the exclusive memory-plugin API" (docs/plugins/sdk-overview/memory-and-context.md). The plugin never called it. So gateway memory.search/memory.get/memory.status, doctor, Active Memory recall and the pre-compaction flush all used Memory Core's file index.

A third, smaller bug: search_notes, /recall and the CLI showed scores as percentages. Basic Memory fuses keyword and vector scores into the range 0 to 1.3 (src/basic_memory/repository/search_reader.py:349, FUSION_BONUS = 0.3), so a strong match displayed as "118%".

What changed

  • New memory/basic-memory-capability.ts, registered in index.ts:
    • providerRuntime opens a reader over the plugin's shared BM client with search, get, health and close. Capabilities: source memory, no pagination, no automatic-recall candidates, no project filter.
      • search caps fused scores at 1 for the host's 0–1 minScore scale, and enforces minScore.
      • get applies the host's from/lines window. A missing note returns not_found, and any other failure propagates.
      • health lists projects to prove the BM session answers, without reading note content.
      • Each BM call is wrapped in context.assertCurrent(), so a caller whose authority was revoked is refused.
    • recallToolNames: ["search_notes", "read_note", "build_context"] for Active Memory deep recall.
    • providerFlushPlanResolver returns a tools-arm plan. The pre-compaction flush saves with write_note/edit_note, looks up with search_notes/read_note, and replies NO_REPLY when there is nothing to save.
    • promptBuilder adds a short "Basic Memory" system-prompt section. Without it, OpenClaw fills the field from Memory Core's registration, whose section points agents at its file-only memory_search.
  • Removed tools/memory-provider.ts and its tests. They never ran on current OpenClaw, and Memory Core's memory_search already covers MEMORY.md. memory_search/memory_get are also dropped from openclaw.plugin.json.
  • Scores show as (score 1.18) in tools/search-notes.ts, commands/slash.ts and commands/cli.ts.
  • Small changes:
    • isNoteNotFoundError in bm-client.ts is now exported for reuse.
    • package.json files swaps tools/memory-provider.ts for memory/basic-memory-capability.ts.
  • Docs:
    • README "Memory search" explains which search covers what.
    • CLAUDE.md and DEVELOPMENT.md describe the capability.
    • MEMORY_TASK_FLOW.md is marked superseded.

Design decision: agents keep two searches

Memory Core's memory_search cannot be routed to Basic Memory. It always opens Memory Core's own index (getMemorySearchManager in dist/memory-h3csZjpI.mjs reports backend: "builtin"), and the docs say Memory Core tools "keep their existing ownership". Of the three options, this PR keeps Memory Core and steers agents to search_notes through the prompt section. The alternatives were:

  • tell users to disable Memory Core, which also removes its file memory and its background memory-consolidation feature ("dreaming");
  • re-add the combined search under a new tool name.

How it was verified

Automated. just package-check-openclaw exits 0 with 264 Bun tests passing.

  • memory/basic-memory-capability.test.ts covers capabilities, hit mapping and score cap, minScore, line windows, not_found versus propagated errors, revoked callers, health, the flush plan and the prompt section.
  • index.test.ts now asserts the capability is registered and memory_search/memory_get are not.
  • tools/search-notes.test.ts checks a fused score of 1.18 renders as (score 1.18) with no "%".

Live, on OpenClaw 2026.10.9. I installed the packed tarball and did a full gateway restart. Basic Memory ran from #1966's branch with an isolated config.

Check Command Result
Health openclaw gateway call memory.status {"providerId": "openclaw-basic-memory", "status": "ready"}
Search memory.search with version: 2 Hit from conversations/conversations-2026-10-29.md, score 1
Line window memory.get with from: 10, lines: 3 status: ok, 3 lines, truncated: true, nextFrom: 13
Missing note memory.get with an unknown id status: not_found
Tool conflict gateway log after restart 0 tool name conflict lines
Agent fresh session asks about a saved fact The agent called search_notes and Memory Core's memory_search

Not verified

  • Pre-compaction flush: not triggered live. That needs a session near its compaction threshold. The plan shape is unit-tested.
  • Active Memory recall: not exercised live.
  • Access policy: every OpenClaw caller with memory access can read every note in the configured project. That matches what search_notes and read_note already expose. A group-chat caller is not restricted to a "conversation" audience; adding that would be a separate policy decision.

Follow-up found during testing

search_notes previews only the first 200 characters of each hit. Conversation capture appends a whole day's turns to one note, so a fact from a later turn is matched but cut off before the agent sees it. A fresh session found a captured fact through Memory Core's files instead. This should be its own issue.

Next step

A maintainer reviews this after #1966 merges.

🤖 Generated with Claude Code

…lity

On OpenClaw 2026.10 the plugin's memory_search and memory_get never ran.
The bundled Memory Core plugin registers both names even when another
plugin owns the memory slot, so OpenClaw rejected ours ("plugin tool name
conflict"). The plugin also never called registerMemoryCapability, so the
host's memory consumers fell back to Memory Core's file index.

Register an OpenClaw memory capability (memory/basic-memory-capability.ts):

- providerRuntime: search, get, health and close over the shared BmClient.
  Gateway memory.search v2, memory.get, memory.status and doctor now read
  Basic Memory. Fused scores (0-1.3) are capped at 1 for the host's 0-1
  minScore scale, and a missing note returns not_found while other
  failures propagate.
- recallToolNames: search_notes, read_note, build_context for Active Memory.
- providerFlushPlanResolver: a tools-arm pre-compaction flush that saves
  through write_note and edit_note instead of a workspace file.
- promptBuilder: a system-prompt section pointing agents at search_notes.
  Without it OpenClaw fills the field from Memory Core, whose section points
  at its file-only memory_search.

Remove the dead memory_search/memory_get registrations (tools/memory-
provider.ts) and drop them from the plugin manifest.

Show search scores as raw numbers ("score 1.18") in search_notes, /recall
and the CLI. They were multiplied by 100 and shown as a percentage, so a
strong match on both keyword and vector signals displayed as "118%".

Docs: README, CLAUDE.md and DEVELOPMENT.md describe the capability;
MEMORY_TASK_FLOW.md is marked superseded.

Verified live on OpenClaw 2026.10.9: memory.status returns
{providerId: openclaw-basic-memory, status: ready}; memory.search v2 and
memory.get return Basic Memory notes, line windows and not_found; no tool
name conflicts after a gateway restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T19:54:02.243597Z 1a754dc PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

`openclaw memory status --agent main` printed "Health: ready" and then
never exited. The CLI loads the plugin without starting its service, so
the provider's health() call spawned a `bm mcp` child lazily, and the
reader's close() was a no-op. The child's open pipes kept the process
alive.

BmClient now records whether the plugin service owns the session
(isServiceRunning(), true only between start() and stop()). A reader
opened while the service is not running owns its connection and stops it
on close(). In the gateway, close() still leaves the shared session open.

Also rewrite the search_notes description. It told agents to use
memory_search for cross-source recall covering MEMORY.md and tasks, which
no longer exists on OpenClaw 2026.10.

Verified live on OpenClaw 2026.10.9: `openclaw memory status --agent main`
exits 0 after about 13 s, reporting the openclaw-basic-memory provider as
ready, and leaves no extra `bm mcp` process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
@groksrc

groksrc commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Added commit 3e5954e with two fixes found by the OpenClaw agent during testing.

1. openclaw memory status --agent main never exited. It printed Health: ready and then hung. The CLI loads the plugin without starting its service. Our provider's health() therefore spawned a bm mcp child on first use, and the reader's close() did nothing. The fix:

  • BmClient now records whether the plugin service owns the session (isServiceRunning(), true only between start() and stop()).
  • A reader opened while the service is not running stops its connection on close(). In the gateway, close() still leaves the shared session open.

Verified live on 2026.10.9: the command exits 0 after about 13 s and reports the openclaw-basic-memory provider as ready. No extra bm mcp process remains.

2. The search_notes description was stale. It told agents to use memory_search for cross-source recall covering MEMORY.md and tasks. It now says search_notes is the search for long-term memory, and that memory_search covers only workspace files.

Checked, no change needed: openclaw plugins inspect shows Shape: non-capability. That is a gap in OpenClaw's inspect, not a registration failure. It builds the shape from a fixed list of capability kinds (status-CrE1PAGs.mjs, buildPluginCapabilityEntries), and memory capabilities are not on it. Gateway memory.status does return our provider.

This branch is no longer rebased on the latest #1966 commits, because updating it that way would need a force-push. The PR diff is unaffected.

@groksrc

groksrc commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Closing: this work now lives in #1966, combined into one branch at the maintainer's request. Commits 1a754dc and 3e5954e were cherry-picked there as 2f554e1 and 60c153d, and #1966's description covers both.

@groksrc groksrc closed this Oct 29, 2026
groksrc added a commit that referenced this pull request Oct 29, 2026
…1966)

This PR makes the OpenClaw plugin work on OpenClaw 2026.10 and later. On
those hosts, conversation capture never ran (#1880), the plugin's memory
search never ran, and OpenClaw's memory features read only workspace
files. All fixes were tested together against a live OpenClaw 2026.10.9
gateway.

This PR now also contains the work from #1968, which is closed so that
one branch carries everything.

## Commits

| Commit | What it fixes |
|---|---|
| `e49f7255` | **Capture (#1880).** OpenClaw 2026.10 rejected the
context engine on every turn, so nothing was captured. |
| `16a30828` | `context-engine/types.ts` was missing from the npm
`files` list. |
| `1e29bb0d` | Heartbeat polls and `NO_REPLY` turns were captured as
conversation. |
| `2f554e17` | **Memory provider.** The plugin's
`memory_search`/`memory_get` never ran, and OpenClaw's memory features
fell back to its file index. Search scores displayed as "118%". |
| `60c153d8` | `openclaw memory status` printed "Health: ready" and then
never exited. |
| `a32aa118` | `write_note`/`edit_note` told agents to cite `file_path`,
but the result text did not include it. |
| `61a1d305` | `edit_note` hid every server error behind "It may not
exist", and offered no way to add to a section. |
| `ccccfbbe` | A first start overran OpenClaw's 5 s service limit and
could wedge the gateway; a missing workspace directory broke a fresh
install; search previews cut off long notes; `build_context` repeated
observations and printed `null` for unresolved links; every other tool
hid server errors. |

## 1. Capture (#1880)

OpenClaw 2026.10 runs a context engine for durable turns only if the
engine meets two requirements. It must declare
`info.transcriptSemantics` (`currentTurnFence:
"before-current-turn-entry-v1"`, `turnAdvancementIdempotency:
"atomic-idempotent-v1"`). It must also implement `commitTurn()`, an
atomic write keyed by `advancementKey` that is safe to repeat. Our
engine did neither. OpenClaw logged `Context engine
"openclaw-basic-memory" degraded to "legacy" ... current-turn transcript
fencing is not declared` and captured nothing.

- **Declarations.** The engine declares both values. The fence holds
because `assemble()` passes the host's messages through unchanged and
the engine stores no transcript.
- **`commitTurn()`.** It captures the turn keyed by `advancementKey`.
- Each entry carries a hidden `<!-- openclaw-turn:<sha256 prefix> -->`
marker.
- A key already present in today's or yesterday's conversation note
returns `duplicate`.
- Captures run one at a time, so a duplicate check and its append cannot
interleave.
  - Errors propagate, so OpenClaw keeps the turn queued and retries it.
- **Fallback.** `afterTurn()` stays for older hosts. OpenClaw skips
`afterTurn()` for turns it sends to `commitTurn()`.
- **Housekeeping turns.** Both paths skip heartbeat turns
(`isHeartbeat`) and replies that are exactly `NO_REPLY`.
- **Build.** The plugin builds against `openclaw ^2026.10.6`. 2026.10
removed the bare `openclaw/plugin-sdk` export, so `ContextEngine` types
now come from `OpenClawPluginApi.registerContextEngine`.

## 2. Memory provider

The plugin registered `memory_search` and `memory_get` as plain tools.
OpenClaw's bundled Memory Core plugin registers the same names even when
another plugin owns the memory slot, so OpenClaw rejected ours (`plugin
tool name conflict`). The plugin also never called
`registerMemoryCapability`, OpenClaw's "exclusive memory-plugin API".

`memory/basic-memory-capability.ts` now registers a capability:

- **`providerRuntime`** with `search`, `get`, `health` and `close` over
the shared BM client. Gateway `memory.search` v2, `memory.get`,
`memory.status` and doctor now read Basic Memory.
  - Fused scores (0–1.3) are capped at 1 for the host's 0–1 `minScore`.
  - A missing note returns `not_found`; any other failure propagates.
- Every BM call is wrapped in `context.assertCurrent()`, so a revoked
caller is refused.
- A reader opened outside the plugin service, as in a one-shot CLI
command, stops its own `bm mcp` connection on `close()`.
- **`recallToolNames`**: `search_notes`, `read_note` and
`build_context`, for Active Memory.
- **`providerFlushPlanResolver`**: the pre-compaction flush saves
through `write_note`/`edit_note` instead of a workspace file.
- **`promptBuilder`**: a short system-prompt section that points agents
at `search_notes`. Otherwise OpenClaw fills this field from Memory Core,
which points agents at its file-only `memory_search`.

The dead `tools/memory-provider.ts` is removed, and its tools are
dropped from `openclaw.plugin.json`.

**Design decision.** Memory Core's `memory_search` cannot be routed to
Basic Memory: it always opens Memory Core's own index (`backend:
"builtin"`). This PR keeps Memory Core loaded and steers agents to
`search_notes` through the prompt section and the `search_notes`
description. The alternatives were to disable Memory Core, which also
loses its file memory and its background memory-consolidation feature
("dreaming"), or to re-add the combined search under a new tool name.

## 3. Smaller fixes

- **Scores.** Search results show `(score 1.18)` instead of "118%" in
`search_notes`, `/recall` and the CLI. Basic Memory's fused scores reach
1.3 by design (`search_reader.py:349`).
- **File paths.** `write_note` and `edit_note` results now include a
`File:` line, matching their descriptions.
- **`edit_note` errors.** Failures show the server's reason, such as
`INVALID_ARGUMENTS: \`section\` is only for section commands, not
append.`, instead of "It may not exist".
- **`edit_note` section inserts.** `insert_after_section` and
`insert_before_section` are exposed. Agents can add an observation under
`## Observations`; `append` always lands after `## Relations`.
- **README.** The install steps set both slots with `openclaw config
set`, because 2026.10.9 has no `plugins enable --slot`. They also state
that capture and recall require the `contextEngine` slot.

## 4. Startup and tool output

- **Background startup.** Service start no longer waits for `bm`.
- `BmClient.start()` runs connect plus project creation as a startup
gate. Tool calls wait for the gate, and a failed startup is retried by
the next tool call.
  - Schema seeding runs after the gate opens.
  - The `bm --version` probe and the `uv` auto-install are async.
- Measured: the first note write takes 4–9 s and a first start about 12
s, against OpenClaw's 5 s limit. A timed-out start failed the plugin,
and the failed hot reload left the gateway unable to list models.
- **Workspace directory.** The service creates the workspace before
spawning `bm`. On a fresh install, OpenClaw creates it only on the first
agent run, and spawning into it failed with `ENOENT ... posix_spawn
'bm'`.
- **Search previews** use the server's `matched_chunk`, up to 500
characters, in `search_notes`, the CLI and the memory provider. Before,
a fact deep in a day's conversation note was matched but cut off.
- **`build_context`** lists only observations missing from the shown
note body. It shows an unresolved `[[link]]` by its name, with "(no note
yet)", instead of `null`.
- **Errors.** Every tool's catch-all now shows the server's reason
(`describeToolError`/`describeNoteError`). It says "does not exist" only
when the server reported `NOT_FOUND`.

## How it was verified

**Automated.** `just package-check-openclaw` exits 0 with 278 Bun tests
passing. It covers type check, Biome lint, build and the `npm pack` dry
run.

**Live, on OpenClaw 2026.10.9 (macOS VM).** I installed packed tarballs
of this code with both slots set. Basic Memory ran from this branch with
an isolated config.

| Check | Result |
|---|---|
| Tool-free turn, then check for "degraded to legacy" | No warning after
the fix. The only one is from the pre-fix reproduction. |
| Turn captured automatically | 16 turns recorded, each with its own
marker |
| Restart, then check for duplicates | 4 turns across 4 sessions and 1
restart gave 4 entries with 4 distinct markers |
| `openclaw gateway call memory.status` | `{"providerId":
"openclaw-basic-memory", "status": "ready"}` |
| `memory.search` v2 / `memory.get` | Basic Memory hits. A line window
returned `nextFrom`; an unknown id returned `not_found`. |
| `openclaw memory status --agent main` | Exits 0 in about 13 s with
`Health: ready` and no leftover `bm mcp` process |
| Tool-name conflicts after restart | None |
| Agent read/write loop | An OpenClaw agent ran `write_note`,
`edit_note`, `search_notes`, `build_context`, `read_note` and
`delete_note`; each worked on the first try |
| Heartbeat skip | The 16:00 CDT heartbeat turn ran (`trigger=heartbeat`
in the gateway log) and was not recorded. The 15:00 heartbeat, before
the fix, had been. |
| Hot install, brand-new project | "Plugin replacement applied" 0.09 s
after start, instead of a timeout. Connect and schema seeding finished
in the background 2.5 s later. |
| Search preview | `search_notes` "test plant fern" previewed the
matching turn instead of the note's opening |
| `build_context` | The observation is listed once. A link to a missing
note shows `relates_to → **No Such Note Zebra** (no note yet)`. |
| Missing note | `read_note` returned `it does not exist. NOT_FOUND: No
note matches ...` |
| `edit_note` errors and inserts | Append with `section` returned the
`INVALID_ARGUMENTS` reason; `insert_after_section` "Observations" placed
the line directly under that heading |

## Not verified

- **Missing workspace directory:** covered by a unit test that starts
the service with a nonexistent workspace. The VM's workspace already
exists, so this was not reproduced live after the fix.
- **Pre-compaction flush and Active Memory recall:** not exercised live.
The flush plan shape is unit-tested.
- **OpenClaw before 2026.10:** not run. The `afterTurn()` fallback is
unchanged, so the peer range stays `>=2026.5.2`.
- **Automatic recall on CLI runtimes such as `claude-cli`:** these never
call `assemble()`, so recall text is not injected. That is host
behaviour.
- **Access policy:** every OpenClaw caller with memory access can read
every note in the project, the same access `search_notes` already gives.

## Not fixed here: `openclaw memory status` takes 11–13 s

Timestamped output shows where the time goes:

| Stage | Time | Owner |
|---|---|---|
| CLI start until plugins register | 0 → 5.3 s | OpenClaw |
| Second plugin load pass | 5.3 → 8.7 s | OpenClaw |
| `bm mcp` connect | 8.7 → 10.6 s | Basic Memory: #1731, eager FastAPI
import at MCP startup |
| Health call and close | 10.6 → 11.5 s | plugin |

Loading the plugin module takes about 0.2–0.3 s per pass. No plugin
change would make a real difference. The `bm` share is tracked in #1731,
which is waiting on Python 3.15 lazy imports (#1740 was declined).

## Next step

A maintainer reviews and merges.

Closes #1880

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant