openclaw - ✅(Solved) Fix Send your assistant a photo. Watch its memory disappear. [1 pull requests, 2 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#56835Fetched 2026-04-08 01:47:13
View on GitHub
Comments
2
Participants
2
Timeline
7
Reactions
1
Author
Timeline (top)
commented ×2closed ×1cross-referenced ×1locked ×1

Error Message

Except it's not fine. Your memory plugin just died. Your context engine just died. Your guardrails just died. Every plugin loaded via plugins.load.paths has been silently wiped from the hook runner, and they're never coming back. Not until you restart the gateway. And you won't know to restart, because there's no error. No crash. No warning you'd notice unless you're actively watching logs for an info-level message about an empty allowlist.

Root Cause

When a media message arrives, the pipeline calls applyMediaUnderstanding() to process attachments. This function calls buildProviderRegistry(params.providers), passing provider overrides but forgetting to pass cfg (the gateway config). One function deeper, buildMediaUnderstandingRegistry falls back to:

loadOpenClawPlugins({ config: cfg }) // cfg is undefined here

Because activate defaults to true, this:

  1. Builds a fresh plugin registry from config: undefined (no plugins.allow, no plugins.load.paths)
  2. Discovers only extension-dir plugins
  3. Replaces the global active registry via setActivePluginRegistry()
  4. Replaces the global hook runner via initializeGlobalHookRunner()

The new hook runner has zero hooks from your path-loaded plugins. agent_end, before_agent_start, before_tool_call: all gone. The gateway keeps running, messages keep flowing, but everything that made your assistant yours is dead.

The irony: params.cfg is right there on the params object. It just never gets passed down.

Fix Action

Fix

Five changes across five source files. Change 1 fixes the root cause. Changes 2-5 are defense-in-depth so this class of bug can't recur through other code paths.

PR fix notes

PR #56836: fix: prevent media understanding and speech provider resolution from replacing global hook runner

Description (problem / solution / changelog)

Fixes #56835

Fix: prevent media understanding provider resolution from replacing global hook runner

Summary

Inbound media messages (photos, stickers, audio, video) silently and permanently break all plugin hooks (agent_end, before_agent_start, etc.) for the lifetime of the gateway process. Once triggered, hook-driven plugins (memory, analytics, guardrails) stop functioning. Only a full process restart recovers.

The root cause: applyMediaUnderstanding calls buildProviderRegistry without passing cfg, causing buildMediaUnderstandingRegistry to call loadOpenClawPlugins({ config: undefined }). This creates a near-empty registry (only extension-dir plugins) and activates it by default, replacing the global hook runner with one that has none of the user's plugin hooks.

Problem

When a media message arrives, the pipeline calls applyMediaUnderstanding to process attachments. This calls buildProviderRegistry(params.providers), passing provider overrides but not cfg (the gateway config). Inside buildMediaUnderstandingRegistry, when no media understanding providers exist in the active registry, it falls through to:

loadOpenClawPlugins({ config: cfg }) // cfg is undefined!

Because activate defaults to true, this:

  1. Creates a fresh plugin registry from config: undefined (no plugins.allow, no plugins.load.paths)
  2. Discovers only extension-dir plugins
  3. Replaces the global active registry via setActivePluginRegistry()
  4. Replaces the global hook runner via initializeGlobalHookRunner()

All hook-driven functionality (memory capture, analytics, guardrails) is silently dead from this point forward.

Root cause call chain

dispatchInboundMessage
  -> applyMediaUnderstanding (src/media-understanding/apply.ts)
    -> buildProviderRegistry(params.providers)        <- cfg NOT passed
      -> buildMediaUnderstandingRegistry(overrides, undefined)
        -> loadOpenClawPlugins({ config: undefined })
          -> activatePluginRegistry()                 <- replaces hook runner

Fix

One line, one file: src/plugins/loader.ts, function resolveRuntimePluginRegistry.

-  return getCompatibleActivePluginRegistry(options) ?? loadOpenClawPlugins(options);
+  return getCompatibleActivePluginRegistry(options)
+    ?? loadOpenClawPlugins({
+      ...options,
+      ...(getActivePluginRegistry()
+        ? { activate: false, cache: false }
+        : {}),
+    });

All five call sites identified during investigation (apply.ts, provider-registry.ts, runner.ts, audio-transcription-runner.ts, tts/provider-registry.ts) funnel through this shared helper. Three of the original per-site fixes were already merged into main. The remaining two were refactored into resolveRuntimePluginRegistry. So the five-site bug collapses to a single-line fix at the shared chokepoint.

Why the guard is conditional

resolveRuntimePluginRegistry is called by two categories of callers:

Provider-resolution callers (media understanding, speech, capabilities, tools) that just need to read plugin entries. These pass transformed config whose cache key won't match the active registry, triggering the fallback. They should never activate.

Bootstrap callers (ensureRuntimePluginsLoaded, ensureMemoryRuntime, maybeBootstrapChannelPlugin) that depend on the fallback to initialize global state during cold start.

The conditional guard handles both: when getActivePluginRegistry() returns a registry (normal operation after startup), provider callers get a read-only snapshot without wiping hooks. When it returns null (cold start), bootstrap callers initialize normally because activate defaults to true.

One caveat: maybeBootstrapChannelPlugin safety depends on the main gateway bootstrap having already set a compatible active registry. That's true today but is a runtime observation, not a structural guarantee. Worth covering with a test that maybeBootstrapChannelPlugin still activates when called before the main bootstrap.

Reproduction

  1. Configure a gateway with any plugin using agent_end or before_agent_start hooks via plugins.load.paths
  2. Start the gateway, verify hooks fire on text messages
  3. Send a photo to any channel that triggers applyMediaUnderstanding
  4. Hooks stop firing for all subsequent messages
  5. Check logs for plugins.allow is empty; discovered non-bundled plugins may auto-load

Who is affected

Any deployment that loads plugins via plugins.load.paths and receives media messages. This is a silent data-loss bug: memory plugins stop capturing, guardrail plugins stop enforcing. No error or crash to alert the operator.

Testing

Tested on a live OpenClaw 2026.3.24 gateway with 7 agents, openclaw-mem0 and lossless-claw plugins.

Before fix: Send photo -> plugins.allow is empty -> hooks permanently dead After fix: Send photo -> no warning -> hooks survive. Multiple photos across multiple restarts, zero occurrences.

Note: resolveRuntimePluginRegistry does not exist in v2026.3.24 (introduced after that version). We patched five individual call sites in our production dist, which has been stable. This PR consolidates those into the single source-level fix.

Related issues

  • #47625: Same root cause class. loadOpenClawPlugins secondary call overwrites active plugin registry.
  • #30674: Identical trigger (Telegram stops working after receiving image).
  • #5513: Plugin hooks never invoked. Consistent with hook runner replacement.
  • #23452: Vision/image recognition broken. Same buildMediaUnderstandingRegistry code path.

Design note

The broader issue is that loadOpenClawPlugins defaults to activate: true, making it a footgun for any caller that just needs to read plugin data. Consider separating "load plugins" from "activate registry" into distinct functions, or adding a runtime assertion that activate: true is only called during startup.

Changed files

  • src/plugins/loader.ts (modified, +1/-1)

Code Example

loadOpenClawPlugins({ config: cfg }) // cfg is undefined here

---

dispatchInboundMessage
  → withReplyDispatcher
    → dispatchReplyFromConfig
      → getReplyFromConfig
        → applyMediaUnderstandingIfNeeded
applyMediaUnderstanding (src/media-understanding/apply.ts)
buildProviderRegistry(params.providers)     ← cfg not passed
buildMediaUnderstandingRegistry(overrides, undefined)
loadOpenClawPlugins({ config: undefined })
activatePluginRegistry()              ← replaces hook runner
initializeGlobalHookRunner(emptyRegistry)

---

- const providerRegistry = buildProviderRegistry(params.providers);
+ const providerRegistry = buildProviderRegistry(params.providers, params.cfg);

---

- const pluginRegistry = (...) > 0 ? active : loadOpenClawPlugins({ config: cfg });
+ const pluginRegistry = (...) > 0 ? active : loadOpenClawPlugins({ config: cfg, activate: false, cache: false });

---

- loadOpenClawPlugins({ config: cfg })
+ loadOpenClawPlugins({ config: cfg, activate: false, cache: false })

---

- const providerRegistry = buildProviderRegistry();
+ const providerRegistry = buildProviderRegistry(undefined, params.cfg);

---

- const providerRegistry = buildProviderRegistry(params.providers);
+ const providerRegistry = buildProviderRegistry(params.providers, params.cfg);
RAW_BUFFERClick to expand / collapse

Send your assistant a photo. Watch its memory disappear.

What happens

Send a photo to your OpenClaw bot on Telegram. Any photo. A sunset, your cat, a screenshot of the bug you're about to report. The gateway processes the image, your assistant responds, and everything looks fine.

Except it's not fine. Your memory plugin just died. Your context engine just died. Your guardrails just died. Every plugin loaded via plugins.load.paths has been silently wiped from the hook runner, and they're never coming back. Not until you restart the gateway. And you won't know to restart, because there's no error. No crash. No warning you'd notice unless you're actively watching logs for an info-level message about an empty allowlist.

Your assistant is now running without memory. Every conversation after that photo is a first conversation. And it'll keep happening after every restart, the next time someone sends media.

How we found it

We run a home gateway with openclaw-mem0 and lossless-claw (LCM context engine) loaded via plugins.load.paths. We noticed memories stopped being captured after a few hours of normal use. No errors. Same PID. Restarting fixed it, but it kept coming back.

We spent a while chasing memory leaks, hook dispatch races, and plugin re-registration bugs before landing on the real pattern: it broke every time someone sent a photo in a group chat. Not sometimes. Every time.

We added a stack trace diagnostic to the warnWhenAllowlistIsOpen path, sent a photo, and got our answer in one shot.

Root cause

When a media message arrives, the pipeline calls applyMediaUnderstanding() to process attachments. This function calls buildProviderRegistry(params.providers), passing provider overrides but forgetting to pass cfg (the gateway config). One function deeper, buildMediaUnderstandingRegistry falls back to:

loadOpenClawPlugins({ config: cfg }) // cfg is undefined here

Because activate defaults to true, this:

  1. Builds a fresh plugin registry from config: undefined (no plugins.allow, no plugins.load.paths)
  2. Discovers only extension-dir plugins
  3. Replaces the global active registry via setActivePluginRegistry()
  4. Replaces the global hook runner via initializeGlobalHookRunner()

The new hook runner has zero hooks from your path-loaded plugins. agent_end, before_agent_start, before_tool_call: all gone. The gateway keeps running, messages keep flowing, but everything that made your assistant yours is dead.

The irony: params.cfg is right there on the params object. It just never gets passed down.

The call chain

dispatchInboundMessage
  → withReplyDispatcher
    → dispatchReplyFromConfig
      → getReplyFromConfig
        → applyMediaUnderstandingIfNeeded
          → applyMediaUnderstanding (src/media-understanding/apply.ts)
            → buildProviderRegistry(params.providers)     ← cfg not passed
              → buildMediaUnderstandingRegistry(overrides, undefined)
                → loadOpenClawPlugins({ config: undefined })
                  → activatePluginRegistry()              ← replaces hook runner
                    → initializeGlobalHookRunner(emptyRegistry)

What you'll see in logs

If you know to look:

  • [plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load fires right after the first photo
  • [tools] agents.*.tools.allow allowlist contains unknown entries (memory_forget, lcm_describe, ...) because the plugin tools vanished with their plugins
  • Zero agent_end or before_agent_start hook activity from path-loaded plugins after the trigger
  • No crash. Same PID. No restart. Just silence where your plugins used to be.

Reproduction

  1. Set up a gateway with any plugin using agent_end or before_agent_start hooks, loaded via plugins.load.paths
  2. Start the gateway, confirm hooks fire on text messages
  3. Send a photo to the bot (any channel with media support)
  4. Hooks stop firing. Check logs for the plugins.allow is empty warning.
  5. Restart gateway. Hooks work again. Send another photo. They're gone again.

Who this affects

Anyone who:

  • Loads plugins via plugins.load.paths (not bundled or in the extensions directory)
  • Receives media messages (photos, stickers, audio, video) on any channel
  • Relies on plugin hooks for anything (memory, analytics, guardrails, logging)

That's probably most people running custom plugins. This is a silent data-loss bug for memory plugins, a silent security regression for guardrail plugins, and a silent feature regression for everything else.

Fix

Five changes across five source files. Change 1 fixes the root cause. Changes 2-5 are defense-in-depth so this class of bug can't recur through other code paths.

Change 1 (root cause): src/media-understanding/apply.ts

Pass cfg to buildProviderRegistry:

- const providerRegistry = buildProviderRegistry(params.providers);
+ const providerRegistry = buildProviderRegistry(params.providers, params.cfg);

Change 2: src/media-understanding/provider-registry.ts

buildMediaUnderstandingRegistry only needs to read provider entries. It should never activate the registry:

- const pluginRegistry = (...) > 0 ? active : loadOpenClawPlugins({ config: cfg });
+ const pluginRegistry = (...) > 0 ? active : loadOpenClawPlugins({ config: cfg, activate: false, cache: false });

Change 3: src/tts/provider-registry.ts

Same pattern in resolveSpeechProviderPluginEntries:

- loadOpenClawPlugins({ config: cfg })
+ loadOpenClawPlugins({ config: cfg, activate: false, cache: false })

Change 4: src/media-understanding/runner.ts

resolveAutoImageModel has the same missing-cfg pattern:

- const providerRegistry = buildProviderRegistry();
+ const providerRegistry = buildProviderRegistry(undefined, params.cfg);

Change 5: src/media-understanding/audio-transcription-runner.ts

runAudioTranscription, same thing:

- const providerRegistry = buildProviderRegistry(params.providers);
+ const providerRegistry = buildProviderRegistry(params.providers, params.cfg);

Testing

All five changes verified on a live OpenClaw 2026.3.24 gateway running 7 agents, openclaw-mem0, and lossless-claw. Multiple JPEG photos sent across multiple gateway restarts. Before fix: every single photo triggered the registry wipe. After fix: zero occurrences across all tests.

Related issues

  • #47625: Same root cause class. loadOpenClawPlugins secondary call overwrites the active plugin registry. Fix in PR #47902 addressed HTTP route registry via pinning but didn't cover hook runner replacement.
  • #30674: Identical trigger (Telegram stops working after receiving image). Closed with a message-thread-ID fix that likely didn't address the underlying registry wipe.
  • #40929: Gateway unresponsive after plugin install. Same cascade.
  • #5513: Plugin hooks never invoked. Hooks register but hasHooks() returns false, consistent with hook runner replacement.
  • #23452: Vision/image recognition broken across channels. Same buildMediaUnderstandingRegistry code path.

A note on the broader pattern

The real issue is that loadOpenClawPlugins defaults to activate: true. That makes sense for bootstrap, but it's a footgun for every secondary caller that just needs to peek at provider lists. Five of the nine call sites in the codebase exist only to read plugin data, not to set up the runtime. They shouldn't be able to nuke the hook runner as a side effect.

Some options worth considering:

  1. Flip the default to activate: false (breaking change, needs an audit of all callers)
  2. Separate "load plugins" from "activate registry" into distinct functions
  3. Add a runtime assertion that activate: true is only called during startup

Disclosure

This investigation was AI-assisted (Claude Opus 4.6 for diagnosis and stack tracing, Claude Code for source mapping). Fully tested on our live gateway. We understand what the code does and have been living with the consequences of this bug for weeks.

Version: OpenClaw 2026.3.24 (also present in 2026.3.28) Platform: Ubuntu 24.04, Node v24.14.0 Channel: Telegram (but the bug is channel-agnostic; any media-capable channel triggers it)

extent analysis

Fix Plan

To fix the issue, apply the following changes:

  • Change 1: In src/media-understanding/apply.ts, pass cfg to buildProviderRegistry:
  • const providerRegistry = buildProviderRegistry(params.providers);
  • const providerRegistry = buildProviderRegistry(params.providers, params.cfg);
  • Change 2: In src/media-understanding/provider-registry.ts, modify buildMediaUnderstandingRegistry to not activate the registry:
  • const pluginRegistry = (...) > 0 ? active : loadOpenClawPlugins({ config: cfg });
  • const pluginRegistry = (...) > 0 ? active : loadOpenClawPlugins({ config: cfg, activate: false, cache: false });
  • Change 3: In src/tts/provider-registry.ts, update resolveSpeechProviderPluginEntries to not activate the registry:
  • loadOpenClawPlugins({ config: cfg })
  • loadOpenClawPlugins({ config: cfg, activate: false, cache: false })
  • Change 4: In src/media-understanding/runner.ts, modify resolveAutoImageModel to pass cfg:
  • const providerRegistry = buildProviderRegistry();
  • const providerRegistry = buildProviderRegistry(undefined, params.cfg);
  • Change 5: In src/media-understanding/audio-transcription-runner.ts, update runAudioTranscription to pass cfg:
  • const providerRegistry = buildProviderRegistry(params.providers);
  • const providerRegistry = buildProviderRegistry(params.providers, params.cfg);

Verification

To verify the fix, follow these steps:

  1. Apply the changes to the codebase.
  2. Restart the gateway.
  3. Send a photo to the bot on any channel.
  4. Check the logs for the plugins.allow is empty warning.
  5. Verify that plugin hooks are still firing after sending the photo.

Extra Tips

Consider the following to prevent similar issues:

  • Review all call sites of loadOpenClawPlugins to ensure activate: true is only used during startup.
  • Separate "load plugins" from "activate registry" into distinct functions.
  • Add a runtime assertion to prevent activate: true from being called outside of startup.

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