Skip to content

feat(openclaw-agent-memory): register OB1 as active-memory capability for governed recall and writeback (#279)#283

Open
MicScalise wants to merge 3 commits into
NateBJones-Projects:mainfrom
MicScalise:feat-282-openclaw-active-memory-capability
Open

feat(openclaw-agent-memory): register OB1 as active-memory capability for governed recall and writeback (#279)#283
MicScalise wants to merge 3 commits into
NateBJones-Projects:mainfrom
MicScalise:feat-282-openclaw-active-memory-capability

Conversation

@MicScalise
Copy link
Copy Markdown

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 from memory_search, and when plugins.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 real MemoryPluginRuntime that 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() wraps client.recall() and maps OB1 memories to OpenClaw's chunk shape ({source, path, startLine, endLine, score, snippet}) using a synthetic openbrain://memory/<id> path. status() reports backend="ob1" so downstream code that branches on backend === "qmd" treats us as a generic plugin-owned backend.
  • Standard tool wrappersmemory_search, memory_get, memory_store register under the names OpenClaw's host and active-memory plugin look for. The seven existing openbrain_* tools stay registered as the advanced governance surface (review queue, recall traces, explicit usage reporting) and remain unchanged.
  • memory_store semantics — typed memory_type parameter maps to OB1 governance categories (decisions, lessons, constraints, outputs, failures, next_steps, unresolved_questions).
  • client.ts schema_version — every recall and writeback now sends the required schema_version literal (openbrain.openclaw.recall.v1 / openbrain.openclaw.writeback.v1). Without it the Edge Function returned 400 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.
  • LLM tool-call coercioncoerceObjectFields() helper JSON.parses string-shaped object args (LLMs occasionally JSON.stringify nested object parameters, which the Edge Function's zod schema then rejects). Applied on openbrain_recall and openbrain_writeback.
  • Default runtime.name = "openclaw" on writeback — agents calling openbrain_writeback without a runtime field used to land rows with runtime_name="unknown". Default it from the plugin so attribution is automatic; explicit input still wins.

Activation contract (unchanged from existing OpenClaw memory plugin model)

registerMemoryCapability declares 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:

  • Cross-session recall: agent "personal-assistant" wrote a uniquely-tagged decision via openbrain_writeback in session A, then in a fresh session B called memory_search with the marker as query. Recall returned the exact memory with content match. Trace row shows runtime_name=openclaw, items=3.
  • Second-agent verification: agent "dev-gpu" recalled 9 prior memories on a generic "Hermes-OB1" query. Same runtime_name=openclaw attribution.
  • Hermes side unaffected: Hermes recall against the same backend continues to work post-cutover.
  • openclaw doctor moves from "No active memory plugin is registered" to recognizing OB1 as the active memory plugin (after plugins.slots.memory is set).

Tools surface notes

When OB1 is selected as the active memory plugin, OpenClaw's host policy auto-exposes memory_search and memory_get to agents under the default tools.profile = "coding". The seven openbrain_* governance tools are registered but require either tools.profile = "full" per-agent or explicit tools.allow listing — same convention OpenClaw uses for any plugin-specific tool surface. Documented in the README.

Diff scope

  • integrations/openclaw-agent-memory/plugin/openclaw.plugin.json — adds memory_search, memory_get, memory_store to contracts.tools.
  • integrations/openclaw-agent-memory/plugin/src/index.ts — +252 LoC. New: capability registration block, three standard tool wrappers, coerceObjectFields helper, runtime.name default. Existing 7 openbrain_* tools unchanged in shape.
  • integrations/openclaw-agent-memory/plugin/src/client.ts — +5 LoC. schema_version on recall + writeback request bodies.
  • integrations/openclaw-agent-memory/plugin/src/search-manager.ts — new (242 LoC). OB1MemorySearchManager class + createOB1Runtime factory.

No new dependencies. No dist/ regeneration committed (please run npm run build to refresh).

Test plan

  • cd integrations/openclaw-agent-memory/plugin && npm install --ignore-scripts --omit=peer && npm run build — esbuild succeeds (~24kb bundle).
  • Configure OB1 agent-memory-api endpoint + access key + workspaceId in ~/.openclaw/openclaw.json under plugins.entries.nbj-ob1-agent-memory.config.
  • Set plugins.slots.memory = "nbj-ob1-agent-memory" to select.
  • Restart gateway.
  • Run openclaw plugins inspect nbj-ob1-agent-memory --runtime --json — confirm activated: true, all 10 tools registered.
  • Drive an agent turn calling memory_search — confirm a recall trace lands with runtime_name=openclaw.

Related

MicScalise added 2 commits May 9, 2026 16:59
…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).
@github-actions github-actions Bot added the integration Contribution: MCP extension or capture source label May 10, 2026
…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.
MicScalise added a commit to MicScalise/OB1 that referenced this pull request May 10, 2026
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.
@alanshurafa alanshurafa added review: needs-author-response Contributor response or clarification needed risk: privacy Touches private data, memory trust, visibility, or redaction concerns area: integrations Review area: integrations/MCP/capture sources alan-reviewed Reviewed by Alan Shurafa in Community Reviewer role labels May 20, 2026
@alanshurafa
Copy link
Copy Markdown
Collaborator

Thanks for the contribution. This is a substantial piece, and the inline comments explaining the Edge Function quirks are genuinely helpful.

Two notes on client.ts:

schema_version is placed before the ...input spread in both recall() and writeback() — the same ordering raised on #278 and #290, where a model-supplied value would override the constant. Moving each line to just after ...input fixes it.

recall() now defaults scope.include_unconfirmed to true (was false). The rationale in your comment is sound — writes start as requires_review=true, so otherwise naive recall returns nothing. But defaulting recall to include un-reviewed memories changes the Agent Memory trust posture, which is a maintainer decision. Labeled risk: privacy and flagged for security/privacy review rather than treated as settled.

On the bigger picture: #283 is the active-memory-slot approach (#281 is the additive-supplement approach). Activation looks correctly opt-in (gated on plugins.slots.memory), so existing deployments are unaffected by default. The direction choice and four-PR merge ordering are escalated to the maintainer separately. dist/index.js is not regenerated here.

Recommend author refresh for the schema_version reorder; the include_unconfirmed default and the active-slot direction need security/privacy and product review from the maintainer.

— Alan (community reviewer; non-binding)

MicScalise added a commit to MicScalise/OB1 that referenced this pull request May 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

alan-reviewed Reviewed by Alan Shurafa in Community Reviewer role area: integrations Review area: integrations/MCP/capture sources integration Contribution: MCP extension or capture source review: needs-author-response Contributor response or clarification needed risk: privacy Touches private data, memory trust, visibility, or redaction concerns

Projects

None yet

2 participants