feat(openclaw-agent-memory): register OB1 as active-memory capability for governed recall and writeback (#279)#283
Conversation
…NateBJones-Projects#282) Closes the gap left by NateBJones-Projects#281. The supplements landed by NateBJones-Projects#281 are additive — they layer prompt and corpus contributions on top of whatever the active memory plugin happens to be. Until now, OpenClaw agents had no way to reach OB1 for native memory_search recall because the plugin never registered a real MemoryCapability with a runtime. What this adds: * api.registerMemoryCapability({ promptBuilder, runtime }) - promptBuilder: emits an "OB1 Agent Memory (active backend)" system prompt section when the agent is selected onto OB1. - runtime: a MemoryPluginRuntime that produces per-agent OB1MemorySearchManager instances on demand. * OB1MemorySearchManager (new src/search-manager.ts): - search(query, opts) → MemorySearchHit[] backed by client.recall. Maps OB1 memories to OpenClaw chunk shape using a synthetic openbrain://memory/<id> path and rank-derived scores. - status() returns backend="ob1" so downstream code that branches on backend="qmd" treats OB1 as a generic plugin-owned backend. - probeVectorAvailability() pings /health. - close(), warmSession(), sync(), hasIndexedContent() implemented as no-ops or trivial returns since OB1 is HTTP-backed and has no local index to manage. - Per-agent manager cache cleared by closeAllMemorySearchManagers. * Standard tool wrappers — memory_search, memory_get, memory_store register under the names OpenClaw's host and active-memory plugin look for. The seven openbrain_* tools stay registered as the advanced surface for governance ops (review queue, recall traces, explicit usage reporting). memory_store is intentionally narrow — a typed memory_type maps to OB1 governance categories (decisions, lessons, constraints, outputs, failures, next_steps, unresolved_questions). * client.ts: every recall and writeback now sends the required schema_version literal (openbrain.openclaw.recall.v1 / .writeback.v1). Without it the Edge Function returned 400 Invalid recall payload — the bug was latent because no agent had ever actually invoked the plugin's tools before NateBJones-Projects#282. Activation still requires the user to set plugins.slots.memory = "nbj-ob1-agent-memory" in openclaw.json, which selects this plugin into the single active-memory slot. registerMemoryCapability declares candidacy; the slots config flips the switch. Verified live: a personal-assistant agent turn calling memory_search returns 10 prior memories from the openclaw-fleet workspace; the recall trace lands with runtime_name="openclaw". Note on tool surfacing: write tools (openbrain_writeback, memory_store) are filtered by the OpenClaw host policy when an active memory plugin is selected — agents see memory_search and memory_get automatically, but write tools require explicit tools.allow allowlist. That is a deployment-config concern, not a code concern, and is documented in the plugin README.
…t runtime=openclaw Two follow-ups discovered while running the first end-to-end agent test of openbrain_writeback through Fred (personal-assistant agent): 1. LLM tool callers occasionally JSON.stringify nested-object parameters instead of passing them as native JSON objects. The Edge Function's zod schema then rejects them with "Invalid input: expected object, received string." Add a coerceObjectFields() helper that walks the tool input and JSON.parses any string-shaped fields that the API contract expects to be objects. Apply on openbrain_recall and openbrain_writeback (the two tools whose contracts have nested object parameters). 2. Default runtime.name = "openclaw" inside the openbrain_writeback tool's run handler when not supplied by the caller. Without this, writes from the OpenClaw plugin landed with runtime_name="unknown" in agent_memories rows because the agent didn't know to pass a runtime field. Explicit input.runtime still wins. Verified live: Fred can now complete a full recall→writeback→recall cycle with correct provenance attribution (runtime_name=openclaw, review_status=pending per governance defaults).
…aceMode)
Adds opt-in multi-tenant memory isolation. Each OpenClaw agent on the
same gateway can have its own OB1 workspace, preventing memory bleed
across agents that share the same plugin install.
Config (in ~/.openclaw/openclaw.json):
plugins.entries.nbj-ob1-agent-memory.config:
workspaceMode: "shared" | "per-agent" (default: "shared")
workspaceId: "<fallback>" # used in shared mode + as
# fallback in per-agent mode
workspacePrefix: "<optional-prefix>" # per-agent mode only
In "per-agent" mode the plugin uses each agent's id (optionally prefixed)
as the OB1 workspace_id for both writes and recalls. Agent A's memory is
invisible to Agent B's recall — verified live on Nina/Laleh:
workspace=nina contains nina's writes only
workspace=laleh contains laleh's writes only
Nina's recall scoped to "nina" cannot return laleh's rows
Implementation:
* src/search-manager.ts — search() now passes explicit workspace_id
in the recall body (overrides client config default), so per-agent
SearchManager instances correctly scope their queries.
* src/index.ts — converted memory_search, memory_get, memory_store,
and openbrain_writeback to factory-form registrations
`api.registerTool((ctx) => ({ ... }), { names: ["..."] })` so the
tool execute handler can read ctx.agentId from the per-call
OpenClawPluginToolContext. Required factory + names: opts together
or the host can't discover the tool name without invoking the
factory eagerly.
* src/index.ts — new resolveAgentWorkspaceId() helper mirrors the
SearchManager's workspaceIdFor logic so write tools and search
tools agree on workspace selection.
* openclaw.plugin.json — added workspaceMode and workspacePrefix to
configSchema with descriptions.
Backwards compatible. workspaceMode defaults to "shared" so existing
deployments continue to behave exactly as before.
Mirrors the OpenClaw plugin's workspaceMode option (PR NateBJones-Projects#283 Phase 9) so both runtimes have symmetric multi-tenant memory semantics. Config (in $HERMES_HOME/ob1.json or via env vars): workspace_mode: "shared" | "per-agent" (default: "shared") workspace_prefix: "<optional-prefix>" (per-agent mode only) workspace_id: "<fallback>" (used in shared mode + as fallback in per-agent mode) Env: OPENBRAIN_WORKSPACE_MODE, OPENBRAIN_WORKSPACE_PREFIX In "per-agent" mode workspace_id = workspace_prefix + agent_identity, so each Hermes agent identity gets its own isolated OB1 workspace. Empty or "default" agent identities fall back to the configured workspace so the plugin never sends an empty workspace_id. Implementation: * _resolve_workspace_id() — pure helper applying the mode rules, matching the OpenClaw plugin's resolveAgentWorkspaceId() logic so cross-runtime behavior is identical. * initialize() reads agent_identity BEFORE resolving workspace_id so per-agent mode can use it. * _load_ob1_config() handles new env vars + validates workspace_mode to one of the allowed values (defaults invalid values to "shared"). Tests: 6 new TestResolveWorkspaceId cases covering all four resolution paths (shared-fallback, per-agent-with-identity, prefix application, default-identity fallback, empty-identity fallback, unknown-mode-as-shared). Total suite: 81 tests passing. Backwards compatible. workspace_mode defaults to "shared" so existing deployments behave exactly as before.
|
Thanks for the contribution. This is a substantial piece, and the inline comments explaining the Edge Function quirks are genuinely helpful. Two notes on
On the bigger picture: #283 is the active-memory-slot approach (#281 is the additive-supplement approach). Activation looks correctly opt-in (gated on Recommend author refresh for the — Alan (community reviewer; non-binding) |
…onfirmed default) Two changes per @alan's community review on PR NateBJones-Projects#280: 1. Remove subfolder LICENSE (MIT) so this dir inherits the repo's FSL-1.1-MIT. The MIT-in-FSL nested-license situation was a real compatibility issue; following the same convention as integrations/openclaw-agent-memory/ which inherits root LICENSE.md. 2. Change include_unconfirmed_recall config fallback from False to True (line 159 of plugin/__init__.py) so it matches the DEFAULTS dict at line 96 and the design intent documented in phase-8 lessons learned. Recall returned 0 with False — pending memories (which is the default for governed writes per require_review_by_default=True) were filtered out before reaching the caller. Edge Function's scoreMemory still ranks pending memories lower so they don't dominate; they just appear. Same governance direction as PR NateBJones-Projects#283 (OpenClaw companion). Whatever the maintainer settles on for the OpenClaw cluster (NateBJones-Projects#278/281/283/309) applies consistently here; current change aligns Hermes-OB1 with the resolved defaults documented in the Hermes-OB1 phase-8 final report. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the architecture gap left by PR #281. Companion to #280.
What this changes
PR #281 added two additive memory hooks (
registerMemoryPromptSupplement,registerMemoryCorpusSupplement) so OB1 results contribute alongside whatever active memory plugin happens to be selected. That was the safe minimum. It did not make OB1 be the active memory plugin — agents still couldn't reach OB1 frommemory_search, and whenplugins.slots.memory = "nbj-ob1-agent-memory"was set the slot stayed empty (`openclaw doctor` reported "No active memory plugin is registered for the current config").This PR fills the slot. Inside
register(api):api.registerMemoryCapability({ promptBuilder, runtime })— emits an OB1 discipline section into the active memory prompt and provides a realMemoryPluginRuntimethat produces per-agent search managers.OB1MemorySearchManager(src/search-manager.ts, new) — implements the surface OpenClaw consumers actually call:search,status,probeVectorAvailability,close,hasIndexedContent,warmSession,sync.search()wrapsclient.recall()and maps OB1 memories to OpenClaw's chunk shape ({source, path, startLine, endLine, score, snippet}) using a syntheticopenbrain://memory/<id>path.status()reportsbackend="ob1"so downstream code that branches onbackend === "qmd"treats us as a generic plugin-owned backend.memory_search,memory_get,memory_storeregister under the names OpenClaw's host and active-memory plugin look for. The seven existingopenbrain_*tools stay registered as the advanced governance surface (review queue, recall traces, explicit usage reporting) and remain unchanged.memory_storesemantics — typedmemory_typeparameter maps to OB1 governance categories (decisions,lessons,constraints,outputs,failures,next_steps,unresolved_questions).client.tsschema_version — every recall and writeback now sends the requiredschema_versionliteral (openbrain.openclaw.recall.v1/openbrain.openclaw.writeback.v1). Without it the Edge Function returned400 Invalid recall payload. This was a latent bug because no agent had actually invoked the plugin's recall path before this work — pure tool-call paths bypassed the issue.coerceObjectFields()helper JSON.parses string-shaped object args (LLMs occasionallyJSON.stringifynested object parameters, which the Edge Function's zod schema then rejects). Applied onopenbrain_recallandopenbrain_writeback.runtime.name = "openclaw"on writeback — agents callingopenbrain_writebackwithout a runtime field used to land rows withruntime_name="unknown". Default it from the plugin so attribution is automatic; explicit input still wins.Activation contract (unchanged from existing OpenClaw memory plugin model)
registerMemoryCapabilitydeclares candidacy. The user activates by setting:{ "plugins": { "slots": { "memory": "nbj-ob1-agent-memory" } } }Without that slots entry the plugin stays dormant and does not affect existing memory routing.
Why additive supplements (#281) are still useful
This PR doesn't supersede #281. The additive supplements still let OB1 contribute prompt + corpus contributions when a different memory plugin is active (e.g., users on memory-core or memory-lancedb who want OB1 governance signals layered on top). The two PRs serve different deployment styles.
Verified live
End-to-end against a self-hosted Supabase + agent-memory-api Edge Function, running an OpenClaw 2026.5.7 gateway locally:
openbrain_writebackin session A, then in a fresh session B calledmemory_searchwith the marker as query. Recall returned the exact memory with content match. Trace row showsruntime_name=openclaw, items=3.runtime_name=openclawattribution.openclaw doctormoves from "No active memory plugin is registered" to recognizing OB1 as the active memory plugin (afterplugins.slots.memoryis set).Tools surface notes
When OB1 is selected as the active memory plugin, OpenClaw's host policy auto-exposes
memory_searchandmemory_getto agents under the defaulttools.profile = "coding". The sevenopenbrain_*governance tools are registered but require eithertools.profile = "full"per-agent or explicittools.allowlisting — same convention OpenClaw uses for any plugin-specific tool surface. Documented in the README.Diff scope
integrations/openclaw-agent-memory/plugin/openclaw.plugin.json— addsmemory_search,memory_get,memory_storetocontracts.tools.integrations/openclaw-agent-memory/plugin/src/index.ts— +252 LoC. New: capability registration block, three standard tool wrappers,coerceObjectFieldshelper,runtime.namedefault. Existing 7openbrain_*tools unchanged in shape.integrations/openclaw-agent-memory/plugin/src/client.ts— +5 LoC.schema_versionon recall + writeback request bodies.integrations/openclaw-agent-memory/plugin/src/search-manager.ts— new (242 LoC).OB1MemorySearchManagerclass +createOB1Runtimefactory.No new dependencies. No
dist/regeneration committed (please runnpm run buildto refresh).Test plan
cd integrations/openclaw-agent-memory/plugin && npm install --ignore-scripts --omit=peer && npm run build— esbuild succeeds (~24kb bundle).~/.openclaw/openclaw.jsonunderplugins.entries.nbj-ob1-agent-memory.config.plugins.slots.memory = "nbj-ob1-agent-memory"to select.openclaw plugins inspect nbj-ob1-agent-memory --runtime --json— confirmactivated: true, all 10 tools registered.memory_search— confirm a recall trace lands withruntime_name=openclaw.Related
MemoryProviderfor OB1).