openclaw - ✅(Solved) Fix registerMemoryCorpusSupplement registrations get wiped on lazy tool-load re-register of non-activating plugins [5 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#77039Fetched 2026-05-05 05:53:12
View on GitHub
Comments
1
Participants
2
Timeline
17
Reactions
2
Author
Timeline (top)
cross-referenced ×5referenced ×5mentioned ×3subscribed ×3

Root Cause

  • The second register() ALSO reports mode="full" (not "validate" or "inspect") — so this isn't a metadata-only pass.
  • The second register() does NOT call service.start() again (only one service.start line in the trace) — presumably because the host already has the service registered.
  • Result: after the second register(), memory_search corpus=wiki returns [], and the supplement.search method (also patched with a trace) is never invoked. So memory-core's searchMemoryCorpusSupplements() is calling listMemoryCorpusSupplements() and getting an empty list back.

Fix Action

Workaround

We're working around it on InfraClaw (which doesn't seem to hit the second-register-on-tool-load path — possibly because InfraClaw's agent flow doesn't lazy-load memory_search the same way) by indexing the same content via agents.defaults.memorySearch.extraPaths instead of via the corpus supplement. Acceptable for our deployment but loses the structural advantages of the corpus-supplement pattern (independent index lifecycle, separate metrics, etc.).

PR fix notes

PR #2: fix(memory): make registerMemoryCorpusSupplement idempotent to prevent supplement loss during lazy re-register

Description (problem / solution / changelog)

Summary

When a plugin registers a MemoryCorpusSupplement from inside api.registerService({ start }), the supplement is correctly added to the global corpus supplements array.

However, when lazy tool-load triggers a subsequent non-activating re-register of the same plugin, the loader:

  1. Snapshots the supplements BEFORE register
  2. Runs register() which does NOT re-register the supplement (because service.start() only runs once)
  3. Calls restoreMemoryPluginState() with the pre-register snapshot (which is empty of this plugin's supplement)

This ZAPS the previously registered supplement.

Fix

registerMemoryCorpusSupplement now checks if the plugin already has a registered supplement and skips adding a duplicate instead of replacing. This makes the registration idempotent: re-registering during lazy tool-load no longer wipes existing registrations.

Before (replaces all entries for this pluginId):

  • filter out by pluginId, push new entry, reassign array

After (only adds if not already registered by this pluginId):

  • check if exists, only push if not found

Testing

  • Plugin with MemoryCorpusSupplement registers inside service.start()
  • Lazy tool-load triggers a second register() call
  • Verify supplement is NOT lost after the re-register
  • Verify memory_search corpus=wiki still returns results from the supplement

Fixes openclaw/openclaw#77039

Changed files

  • src/plugins/memory-state.ts (modified, +5/-4)

PR #77065: fix(plugins): preserve corpus supplements from service.start() during activating re-loads

Description (problem / solution / changelog)

Summary

  • Corpus supplements registered from a plugin's service.start() callback were silently wiped when loadOpenClawPlugins ran with shouldActivate=true on a subsequent call (e.g. lazy memory_search tool load re-trigger)
  • clearActivatedPluginRuntimeState() wipes all supplements at the start of every activating load; since registerService is idempotent, service.start() is never re-called, so supplements from it are permanently lost
  • Fix: capture preLoadCorpusSupplements before clearing; after the registration loop, restore any supplement whose plugin did not re-register via register() in the new pass

Root cause (verified by source trace)

loader.ts:1505clearActivatedPluginRuntimeState()clearMemoryPluginState() wipes supplements. registry.ts:1403registerService returns early (idempotent guard) if same service ID was already registered; service.start() not called again. Result: supplement lost with no error.

Test plan

  • New regression test: preserves corpus supplements registered from service.start() during activating re-loads (#77039) — verifies a pre-load supplement survives a full activating reload where the plugin's register() does not call registerMemoryCorpusSupplement
  • pnpm exec vitest run src/plugins/loader.test.ts — 124/124 pass
  • pnpm exec vitest run src/plugins/loader*.test.ts — 177/177 pass
  • pnpm exec tsc --noEmit --project tsconfig.core.json — no new errors
  • pnpm exec oxlint src/plugins/loader.ts src/plugins/loader.test.ts — clean
  • pnpm exec oxfmt --threads=1 src/plugins/loader.ts src/plugins/loader.test.ts — formatted

Fixes #77039.

🤖 Generated with Claude Code

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/plugins/loader.test.ts (modified, +144/-0)
  • src/plugins/loader.ts (modified, +22/-0)

PR #3: fix(memory): make registerMemoryCorpusSupplement idempotent to prevent supplement loss during lazy re-register

Description (problem / solution / changelog)

Summary

When a plugin registers a MemoryCorpusSupplement from inside api.registerService({ start }), the supplement is correctly added to the global corpus supplements array.

However, when lazy tool-load triggers a subsequent non-activating re-register of the same plugin, the loader:

  1. Snapshots the supplements BEFORE register
  2. Runs register() which does NOT re-register the supplement (because service.start() only runs once)
  3. Calls restoreMemoryPluginState() with the pre-register snapshot (which is empty of this plugin's supplement)

This ZAPS the previously registered supplement.

Fix

registerMemoryCorpusSupplement now checks if the plugin already has a registered supplement and skips adding a duplicate instead of replacing. This makes the registration idempotent: re-registering during lazy tool-load no longer wipes existing registrations.

Testing

  • Plugin with MemoryCorpusSupplement registers inside service.start()
  • Lazy tool-load triggers a second register() call
  • Verify supplement is NOT lost after the re-register
  • Verify memory_search corpus=wiki still returns results from the supplement

Fixes openclaw/openclaw#77039

Changed files

  • .agents/skills/openclaw-parallels-smoke/SKILL.md (modified, +7/-2)
  • CHANGELOG.md (modified, +14/-0)
  • docs/plugins/google-meet.md (modified, +17/-9)
  • docs/web/control-ui.md (modified, +1/-1)
  • extensions/google-meet/doctor-contract-api.ts (added, +1/-0)
  • extensions/google-meet/index.test.ts (modified, +161/-7)
  • extensions/google-meet/index.ts (modified, +9/-1)
  • extensions/google-meet/openclaw.plugin.json (modified, +28/-1)
  • extensions/google-meet/src/config-compat.test.ts (added, +98/-0)
  • extensions/google-meet/src/config-compat.ts (added, +84/-0)
  • extensions/google-meet/src/config.ts (modified, +12/-2)
  • extensions/google-meet/src/realtime.ts (modified, +9/-8)
  • extensions/google-meet/src/runtime.ts (modified, +9/-2)
  • extensions/google-meet/src/setup.ts (modified, +2/-1)
  • extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.test.ts (modified, +58/-0)
  • extensions/qa-lab/src/mantis/desktop-browser-smoke.runtime.ts (modified, +2/-2)
  • extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.test.ts (modified, +65/-1)
  • extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts (modified, +3/-2)
  • scripts/e2e/parallels/filesystem.ts (modified, +27/-4)
  • scripts/e2e/parallels/guest-transports.ts (modified, +4/-2)
  • scripts/e2e/parallels/linux-smoke.ts (modified, +25/-0)
  • scripts/e2e/parallels/macos-smoke.ts (modified, +20/-1)
  • scripts/e2e/parallels/npm-update-smoke.ts (modified, +180/-28)
  • scripts/e2e/parallels/package-artifact.ts (modified, +31/-0)
  • scripts/e2e/parallels/windows-smoke.ts (modified, +30/-1)
  • scripts/lib/docker-e2e-scenarios.mjs (modified, +6/-2)
  • src/mcp/channel-server.test.ts (modified, +49/-0)
  • src/mcp/channel-shared.ts (modified, +10/-0)
  • src/mcp/channel-tools.ts (modified, +3/-2)
  • src/media/store.test.ts (modified, +29/-0)
  • src/media/store.ts (modified, +12/-1)
  • src/plugin-sdk/channel-streaming.test.ts (modified, +22/-0)
  • src/plugin-sdk/channel-streaming.ts (modified, +11/-2)
  • src/plugins/memory-state.ts (modified, +5/-4)
  • src/plugins/tools.optional.test.ts (modified, +56/-0)
  • src/plugins/tools.ts (modified, +32/-5)
  • src/plugins/update.test.ts (modified, +57/-0)
  • src/plugins/update.ts (modified, +48/-22)
  • test/scripts/docker-e2e-plan.test.ts (modified, +16/-5)
  • test/scripts/parallels-npm-update-smoke.test.ts (modified, +21/-0)
  • test/scripts/parallels-smoke-model.test.ts (modified, +6/-0)
  • ui/src/styles/chat/tool-cards.css (modified, +9/-1)
  • ui/src/ui/chat/tool-cards.node.test.ts (modified, +29/-0)
  • ui/src/ui/chat/tool-cards.ts (modified, +12/-0)
  • ui/src/ui/views/config.browser.test.ts (modified, +3/-0)
  • ui/src/ui/views/config.ts (modified, +1/-1)
  • ui/src/ui/views/dreaming.test.ts (modified, +23/-0)
  • ui/src/ui/views/dreaming.ts (modified, +3/-1)

PR #77098: fix(plugins/loader): preserve memory supplements during non-activating lazy re-register

Description (problem / solution / changelog)

Summary

When a plugin registers a MemoryCorpusSupplement and a lazy tool-load triggers a subsequent non-activating re-register, the loader's snapshot/restore path was overwriting the active supplements with the pre-register snapshot (which didn't include them, since they were registered in an earlier activation).

Root Cause

In loadOpenClawPlugins, the non-activating restore path snapshots memory state before register() and restores it after — but:

  1. Active supplements were registered in a previous activation cycle
  2. The snapshot captures whatever is currently registered (which may be empty for a lazy re-register)
  3. restoreMemoryPluginState() replaces the current state with the snapshot, losing any registrations made during the activation phase

Fix

In loader.ts non-activating restore path, instead of blindly replacing with the pre-register snapshot, merge: keep supplements from the snapshot + add any NEW supplements registered during this register() call that weren't in the snapshot.

Same fix applied to promptSupplements for consistency.

This preserves the loader's snapshot/restore semantics while preventing supplement loss during lazy re-registers.

Testing

  • Existing loader.test.ts "does not replace active memory plugin registries during non-activating loads" test passes
  • New memory-state.test.ts unit tests for registerMemoryCorpusSupplement behavior
  • Manual test: plugin with MemoryCorpusSupplement → lazy tool-load re-register → supplement preserved

Acceptance

  • pnpm test src/plugins/loader.test.ts src/plugins/memory-state.test.ts passes
  • pnpm exec oxfmt --check --threads=1 src/plugins/loader.ts src/plugins/memory-state.ts CHANGELOG.md
  • pnpm check:changed in Testbox before handoff

Changed files

  • src/plugins/memory-state.ts (modified, +5/-4)

PR #77200: fix(plugins/loader): preserve memory supplements during non-activating lazy re-register

Description (problem / solution / changelog)

Fix for #77039

When a plugin registers a MemoryCorpusSupplement from inside api.registerService({ start }), the supplement is correctly added to the global corpus supplements array.

However, when lazy tool-load triggers a subsequent non-activating re-register of the same plugin, the loader snapshots the supplements before register and restores after. Since the plugin's register() call does not re-add the supplement (service.start() does not re-run), the restoreMemoryPluginState() call effectively zaps the previously registered supplement by overwriting the array with the pre-register snapshot.

Root cause

In loadOpenClawPlugins (loader.ts), both the normal error-handling rollback path and the non-activating reload path call:

restoreMemoryPluginState({
  corpusSupplements: previousMemoryCorpusSupplements,  // snapshot taken before register()
  promptSupplements: previousMemoryPromptSupplements,
});

This blindly replaces corpusSupplements and promptSupplements with the pre-register snapshot, wiping any supplements registered by previously-active plugin instances.

Fix

Merge snapshot + newly-registered supplements in both restore paths:

restoreMemoryPluginState({
  corpusSupplements: [
    ...previousMemoryCorpusSupplements,
    ...memoryPluginState.corpusSupplements.filter(
      (s) => !previousMemoryCorpusSupplements.some((p) => p.pluginId === s.pluginId),
    ),
  ],
  // same for promptSupplements
});

This preserves supplements from earlier activation cycles while still restoring the snapshot for supplements that were only registered during the failed register attempt.

Fixes #77039


⚠️ This PR replaces the earlier #77098, which used a helper-level idempotent fix that addressed the symptom but not the root cause.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/plugins/loader.ts (modified, +24/-4)

Code Example

[02:03:50.385Z] register() ENTERED mode="full" activating=undefined hasRegisterMemoryCorpusSupplement=true
[02:03:51.188Z] service.start() ENTERED hasRegisterMemoryCorpusSupplement=true
[02:03:55.452Z] About to call api.registerMemoryCorpusSupplement(supplement)
[02:03:55.452Z] AFTER registerMemoryCorpusSupplement call
[02:12:21.941Z] register() ENTERED mode="full" activating=undefined hasRegisterMemoryCorpusSupplement=true

---

const previousMemoryCorpusSupplements = listMemoryCorpusSupplements();
try {
    withProfile(..., () => runPluginRegisterSync(register, api));
    if (!shouldActivate) {
        ...
        restoreMemoryPluginState({
            corpusSupplements: previousMemoryCorpusSupplements,
            ...
        });
    }
}
RAW_BUFFERClick to expand / collapse

Symptom

When a non-bundled OpenClaw plugin registers a MemoryCorpusSupplement from inside its api.registerService({ start }) callback, the supplement initially registers successfully but is then silently wiped on a subsequent re-register triggered by lazy memory_search tool loading. Result: memory_search corpus=wiki and corpus=all return zero results from the supplement, even though the plugin's register() has run and the supplement object was instantiated.

Affected plugin

bob-wiki-memory v0.1.8 — registers a MemoryCorpusSupplement via api.registerMemoryCorpusSupplement(supplement) inside api.registerService({ id: "bob-wiki-memory-storage", start: async () => { ... } }). Plugin source: https://github.com/inxaos-repo/bob-wiki-memory/blob/main/src/index.ts

(For reference: memory-wiki stock plugin is the architectural template — declares no kind, activation.onStartup: true, registers a corpus supplement non-exclusively. bwm follows the same pattern.)

Repro / live observation

OpenClaw 2026.5.2 host (Bob-main native install, ginaz). Patched the plugin's register() and service.start() and the line just before api.registerMemoryCorpusSupplement(supplement) to write timestamped trace lines to a file.

Trace from a single boot-to-query cycle:

[02:03:50.385Z] register() ENTERED mode="full" activating=undefined hasRegisterMemoryCorpusSupplement=true
[02:03:51.188Z] service.start() ENTERED hasRegisterMemoryCorpusSupplement=true
[02:03:55.452Z] About to call api.registerMemoryCorpusSupplement(supplement)
[02:03:55.452Z] AFTER registerMemoryCorpusSupplement call
[02:12:21.941Z] register() ENTERED mode="full" activating=undefined hasRegisterMemoryCorpusSupplement=true

The 02:12:21 call coincides exactly with a memory_search corpus=wiki query from the agent — i.e. the lazy tool-load path triggered a second register() run. Note:

  • The second register() ALSO reports mode="full" (not "validate" or "inspect") — so this isn't a metadata-only pass.
  • The second register() does NOT call service.start() again (only one service.start line in the trace) — presumably because the host already has the service registered.
  • Result: after the second register(), memory_search corpus=wiki returns [], and the supplement.search method (also patched with a trace) is never invoked. So memory-core's searchMemoryCorpusSupplements() is calling listMemoryCorpusSupplements() and getting an empty list back.

The plugin DID register the supplement at 02:03:55 (we can see the "AFTER ... call" line from inside the patched code). But by query time it's gone.

Hypothesis

In dist/loader-CiaemmFD.js we see the pattern:

const previousMemoryCorpusSupplements = listMemoryCorpusSupplements();
try {
    withProfile(..., () => runPluginRegisterSync(register, api));
    if (!shouldActivate) {
        ...
        restoreMemoryPluginState({
            corpusSupplements: previousMemoryCorpusSupplements,
            ...
        });
    }
}

When the host re-runs register() with shouldActivate: false (which can happen on non-activating loader contexts — install/inspect/validate/lazy-tool-context), the host snapshots the supplement list BEFORE register, then restores AFTER. The plugin's register() itself doesn't add a new supplement (because the supplement was registered inside service.start, which doesn't re-run). Net effect: the prior supplement registration is still ZAPPED by the restore.

If this hypothesis is correct, the fix is one of:

  1. Don't snapshot+restore corpus-supplement state for plugins that registered the supplement in a previous activating call.
  2. Skip non-activating re-registers entirely for plugins that have already had a successful activating register().
  3. Make registerMemoryCorpusSupplement idempotent by pluginId and ignore the snapshot-restore for entries that have a service still running.

(I haven't confirmed which is correct without more investigation; happy to dig further if the OpenClaw team would like additional traces.)

Workaround

We're working around it on InfraClaw (which doesn't seem to hit the second-register-on-tool-load path — possibly because InfraClaw's agent flow doesn't lazy-load memory_search the same way) by indexing the same content via agents.defaults.memorySearch.extraPaths instead of via the corpus supplement. Acceptable for our deployment but loses the structural advantages of the corpus-supplement pattern (independent index lifecycle, separate metrics, etc.).

Severity

Medium — the public registerMemoryCorpusSupplement SDK is documented as the canonical way to add corpus content to memory_search, and it appears to silently break for non-bundled plugins when used from inside registerService.start. Stock memory-wiki may be unaffected because its supplement registers in a different path (need to verify).

Discovery

Bob the Skull, 2026-05-03, while bringing up bob-wiki-memory on Bob-main native gateway (separate from InfraClaw containerized gateway where it works). Full debug session captured at https://github.com/inxaos-repo/homelab-infra/blob/main/docs/openclaw/plugin-runtime.md (homelab-internal — happy to share specific sections if helpful).

Environment

  • OpenClaw 2026.5.2 (npm package, global install) on Debian 13 trixie
  • Node v24.14.1
  • Plugin: bob-wiki-memory v0.1.8 (loaded from ~/.openclaw/extensions/bob-wiki-memory/)
  • Other memory plugins: memory-core (slot owner), no other supplements

extent analysis

TL;DR

The issue can be fixed by making registerMemoryCorpusSupplement idempotent by pluginId and ignoring the snapshot-restore for entries that have a service still running.

Guidance

  • Investigate the dist/loader-CiaemmFD.js file to understand the pattern of snapshotting and restoring memory corpus supplements.
  • Consider implementing one of the proposed fixes:
    • Don't snapshot+restore corpus-supplement state for plugins that registered the supplement in a previous activating call.
    • Skip non-activating re-registers entirely for plugins that have already had a successful activating register().
    • Make registerMemoryCorpusSupplement idempotent by pluginId and ignore the snapshot-restore for entries that have a service still running.
  • Verify the fix by checking if the supplement is still registered after the second register() call and if memory_search corpus=wiki returns the expected results.

Example

No code example is provided as the issue requires changes to the OpenClaw core code.

Notes

The issue seems to be specific to non-bundled plugins that register a MemoryCorpusSupplement inside api.registerService({ start }). The proposed fixes require changes to the OpenClaw core code, and it's recommended to investigate and test the changes thoroughly to ensure they don't introduce any regressions.

Recommendation

Apply a workaround by making registerMemoryCorpusSupplement idempotent by pluginId and ignoring the snapshot-restore for entries that have a service still running, as this seems to be the most promising fix based on the provided hypothesis.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING