Bug type
Behavior bug (incorrect output/state without crash) — performance.
Beta release blocker
No
Summary
ensureRuntimePluginsLoaded (src/agents/runtime-plugins.ts:6) cache-misses on every inbound message dispatch, triggering a full loadOpenClawPlugins rebuild and re-running every plugin's register(). On hosted gateways with plugins.entries populated this costs ~5–6s per inbound message even though the active plugin registry is already a valid answer.
Steps to reproduce
- Run any gateway whose
loadGatewayPlugins boot-path populates the active plugin registry with non-trivial options (any production setup — onlyPluginIds, autoEnabledReasons, etc. are set). Confirmed against 2026.4.26-f53b52ad6d21.
- Connect any third-party plugin that exports a
default register(api) and logs on entry.
- Send any inbound message to the gateway (channel or DM).
- Observe
register() invoked once during the dispatch with stack runPluginRegisterSync ← loadOpenClawPlugins ← resolveRuntimePluginRegistry ← ensureRuntimePluginsLoaded ← dispatchReplyFromConfig. The wall-clock cost is ~5–6s per message.
Expected behavior
After the boot-path loadGatewayPlugins has populated the active plugin registry, ensureRuntimePluginsLoaded invocations during inbound dispatch should be no-ops — the active registry is already a valid answer and the function exists to ensure plugins are loaded.
Actual behavior
ensureRuntimePluginsLoaded builds a 3-field options object:
const loadOptions = {
config: params.config,
workspaceDir,
runtimeOptions: allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : undefined,
};
resolveRuntimePluginRegistry(loadOptions);
resolveRuntimePluginRegistry calls getCompatibleActivePluginRegistry(options), which derives a cacheKey from loadOptions via resolvePluginLoadCacheContext and strictly equality-compares it to the boot's activeCacheKey.
The boot path (loadGatewayPlugins in src/gateway/server-plugins.ts:587) calls loadOpenClawPlugins with 9+ fields: config, activationSourceConfig, autoEnabledReasons, workspaceDir, onlyPluginIds: pluginIds, coreGatewayHandlers, coreGatewayMethodNames, runtimeOptions, etc.
The two cacheKey hashes always differ. Strict equality always fails. The fall-through path runs loadOpenClawPlugins(options), which re-imports every plugin module via Jiti, re-validates manifests, and re-runs every plugin's register().
On a hosted gateway with memory-core + a third-party channel plugin, this is ~5–6s of work per inbound message that produces no useful change to runtime state.
Environment
- OpenClaw
2026.4.26-f53b52ad6d21
- Node
v24.14.0
- Linux x64 (Elestio-hosted Docker container)
Logs / evidence
Per-turn timing from instrumented third-party plugin (paths trimmed):
register #2 timing: total=2ms
register #2 caller: runPluginRegisterSync ← loadOpenClawPlugins ← resolveRuntimePluginRegistry ← ensureRuntimePluginsLoaded ← dispatchReplyFromConfig ← async withReplyDispatcher ← async dispatchInboundMessage
The plugin's register() body itself runs in 2ms; the ~5–6s wall-clock is consumed inside loadOpenClawPlugins's rebuild path (Jiti loader + manifest validation + per-plugin register re-runs).
Proposed fix
Fast path: if getActivePluginRegistry() returns a populated registry, ensureRuntimePluginsLoaded returns immediately. The function's intent — ensure plugins are loaded — is already met. Plugin reconfiguration already invalidates the active registry through other code paths (setActivePluginRegistry, gateway restart on config write), so a stale active registry is not a concern at this call site.
export function ensureRuntimePluginsLoaded(params): void {
+ if (getActivePluginRegistry()) {
+ return;
+ }
const workspaceDir = ...
resolveRuntimePluginRegistry(loadOptions);
}
PR with fix + regression test filed alongside.
This is independent of #73793 (capability-provider cache bypass) and #74096 (eager media-tool provider listing) — different code path, same class of bug (strict cache-key equality failing on a path that doesn't need a fresh load).
Bug type
Behavior bug (incorrect output/state without crash) — performance.
Beta release blocker
No
Summary
ensureRuntimePluginsLoaded(src/agents/runtime-plugins.ts:6) cache-misses on every inbound message dispatch, triggering a fullloadOpenClawPluginsrebuild and re-running every plugin'sregister(). On hosted gateways withplugins.entriespopulated this costs ~5–6s per inbound message even though the active plugin registry is already a valid answer.Steps to reproduce
loadGatewayPluginsboot-path populates the active plugin registry with non-trivial options (any production setup —onlyPluginIds,autoEnabledReasons, etc. are set). Confirmed against2026.4.26-f53b52ad6d21.default register(api)and logs on entry.register()invoked once during the dispatch with stackrunPluginRegisterSync ← loadOpenClawPlugins ← resolveRuntimePluginRegistry ← ensureRuntimePluginsLoaded ← dispatchReplyFromConfig. The wall-clock cost is ~5–6s per message.Expected behavior
After the boot-path
loadGatewayPluginshas populated the active plugin registry,ensureRuntimePluginsLoadedinvocations during inbound dispatch should be no-ops — the active registry is already a valid answer and the function exists to ensure plugins are loaded.Actual behavior
ensureRuntimePluginsLoadedbuilds a 3-field options object:resolveRuntimePluginRegistrycallsgetCompatibleActivePluginRegistry(options), which derives acacheKeyfromloadOptionsviaresolvePluginLoadCacheContextand strictly equality-compares it to the boot'sactiveCacheKey.The boot path (
loadGatewayPluginsinsrc/gateway/server-plugins.ts:587) callsloadOpenClawPluginswith 9+ fields:config,activationSourceConfig,autoEnabledReasons,workspaceDir,onlyPluginIds: pluginIds,coreGatewayHandlers,coreGatewayMethodNames,runtimeOptions, etc.The two
cacheKeyhashes always differ. Strict equality always fails. The fall-through path runsloadOpenClawPlugins(options), which re-imports every plugin module via Jiti, re-validates manifests, and re-runs every plugin'sregister().On a hosted gateway with
memory-core+ a third-party channel plugin, this is ~5–6s of work per inbound message that produces no useful change to runtime state.Environment
2026.4.26-f53b52ad6d21v24.14.0Logs / evidence
Per-turn timing from instrumented third-party plugin (paths trimmed):
The plugin's
register()body itself runs in 2ms; the ~5–6s wall-clock is consumed insideloadOpenClawPlugins's rebuild path (Jiti loader + manifest validation + per-plugin register re-runs).Proposed fix
Fast path: if
getActivePluginRegistry()returns a populated registry,ensureRuntimePluginsLoadedreturns immediately. The function's intent — ensure plugins are loaded — is already met. Plugin reconfiguration already invalidates the active registry through other code paths (setActivePluginRegistry, gateway restart on config write), so a stale active registry is not a concern at this call site.PR with fix + regression test filed alongside.
This is independent of #73793 (capability-provider cache bypass) and #74096 (eager media-tool provider listing) — different code path, same class of bug (strict cache-key equality failing on a path that doesn't need a fresh load).