Skip to content

ensureRuntimePluginsLoaded cache-misses every inbound dispatch (~5-6s wasted per message) #74117

Description

@poolside-ventures

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

  1. 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.
  2. Connect any third-party plugin that exports a default register(api) and logs on entry.
  3. Send any inbound message to the gateway (channel or DM).
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions