Skip to content

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

@poolside-ventures

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions