openclaw - ✅(Solved) Fix ensureRuntimePluginsLoaded cache-misses every inbound dispatch (~5-6s wasted per message) [1 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#74117Fetched 2026-04-30 06:28:20
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
0
Timeline (top)
cross-referenced ×2commented ×1

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.

Root Cause

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.

Fix Action

Fix / Workaround

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.

PR fix notes

PR #74118: perf(agents/runtime): short-circuit ensureRuntimePluginsLoaded when active registry exists

Description (problem / solution / changelog)

Summary

  • Problem: ensureRuntimePluginsLoaded (src/agents/runtime-plugins.ts:6) is called from dispatchReplyFromConfig on every inbound message and rebuilds the entire plugin registry on each call. It builds a 3-field options object (config, workspaceDir, runtimeOptions) and hands it to resolveRuntimePluginRegistry. getCompatibleActivePluginRegistry's strict cacheKey equality comparison fails — boot's options set has 9+ fields (onlyPluginIds, activationSourceConfig, autoEnabledReasons, etc.), so the two hashes always differ. The fall-through runs a full loadOpenClawPlugins: Jiti-loads every plugin module, re-validates manifests, re-runs each plugin's register().
  • Why it matters: ~5–6s of wasted wall-clock per inbound message on hosted gateways. The active registry is already a valid answer; rebuilding it produces no useful state change. Repro and instrumented stack traces in #74117.
  • What changed: Fast path in ensureRuntimePluginsLoaded — if getActivePluginRegistry() is populated, return immediately. The function's intent is to ensure plugins are loaded; if they already are, the goal is met. Updates the existing test (which was asserting one call — that assertion was wrong; the prior behavior reactivated every time) into a proper regression test that asserts zero calls when the active registry exists.
  • What did NOT change (scope boundary): Behavior when no active registry exists (still builds load options and calls resolveRuntimePluginRegistry). loadGatewayPlugins's boot path. getCompatibleActivePluginRegistry's strict-equality logic — that's a more intrusive change to file separately if desired. Plugin reconfiguration paths that explicitly invalidate the active registry (setActivePluginRegistry, gateway restart on config write) — those continue to work as before.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution

Linked Issue/PR

  • Closes #74117
  • Related #73793 / #74096 (independent fixes for the same class of strict-cache-key issue in different code paths)
  • This PR fixes a bug or regression

Root Cause

ensureRuntimePluginsLoaded is the simplest member of a family of "ensure registry is loaded" helpers (cf. ensurePluginRegistryLoaded in plugins/runtime/runtime-registry-loader.ts, which has its own scope-tracking short-circuit via pluginRegistryLoaded rank). The agent-side ensureRuntimePluginsLoaded is a thin wrapper that defers all caching responsibility to resolveRuntimePluginRegistry → getCompatibleActivePluginRegistry. That worked when the cache-key derivation matched between boot and dispatch. It stopped working when the boot path grew additional options (onlyPluginIds, activationSourceConfig, etc.) that the dispatch path doesn't supply, but the strict-equality check was never relaxed to a "is-compatible-subset" check.

A more thorough fix would be to make getCompatibleActivePluginRegistry accept subset-compatibility, but that's a larger surface area touching multiple call sites. This PR takes the minimal approach for this specific call site: short-circuit at the helper level. The function's contract is "ensure plugins are loaded", not "always rebuild the registry".

Tests

  • Updated src/agents/runtime-plugins.test.ts:
    • Added getActivePluginRegistry mock to the existing vi.mock("../plugins/runtime.js", ...) block.
    • Renamed and rewrote the existing test (does not reactivate plugins when a process already has an active registry — which was incorrectly asserting one call) into short-circuits without rebuilding load options when an active registry exists, asserting zero calls to resolveRuntimePluginRegistry.
    • Other tests (resolves runtime plugins through the shared runtime helper when no active registry is present, etc.) now mock getActivePluginRegistry to return undefined so they exercise the fall-through path.

Verification

$ pnpm vitest run src/agents/runtime-plugins.test.ts
 Test Files  2 passed (2)
      Tests  8 passed (8)

Changed files

  • src/agents/runtime-plugins.test.ts (modified, +15/-4)
  • src/agents/runtime-plugins.ts (modified, +24/-1)

Code Example

const loadOptions = {
  config: params.config,
  workspaceDir,
  runtimeOptions: allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : undefined,
};
resolveRuntimePluginRegistry(loadOptions);

---

register #2 timing: total=2ms
register #2 caller: runPluginRegisterSync ← loadOpenClawPlugins ← resolveRuntimePluginRegistry ← ensureRuntimePluginsLoaded ← dispatchReplyFromConfig ← async withReplyDispatcher ← async dispatchInboundMessage

---

export function ensureRuntimePluginsLoaded(params): void {
+  if (getActivePluginRegistry()) {
+    return;
+  }
   const workspaceDir = ...
   resolveRuntimePluginRegistry(loadOptions);
}
RAW_BUFFERClick to expand / collapse

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).

extent analysis

TL;DR

The most likely fix is to add a fast path to ensureRuntimePluginsLoaded to return immediately if the active plugin registry is already populated.

Guidance

  • Verify that the getActivePluginRegistry() function returns a populated registry after the boot-path loadGatewayPlugins has completed.
  • Check the cacheKey generation in resolvePluginLoadCacheContext to ensure it's not unnecessarily including fields that don't affect the plugin registry.
  • Consider adding a debug log to ensureRuntimePluginsLoaded to confirm that the fast path is being taken when the active registry is already populated.
  • Review the loadOpenClawPlugins function to see if there are any opportunities to optimize its performance, as it's currently taking ~5-6s to complete.

Example

The proposed fix adds a simple check to ensureRuntimePluginsLoaded:

export function ensureRuntimePluginsLoaded(params): void {
  if (getActivePluginRegistry()) {
    return;
  }
  const workspaceDir = ...
  resolveRuntimePluginRegistry(loadOptions);
}

This change allows the function to return immediately if the active plugin registry is already populated, avoiding the unnecessary rebuild and re-run of plugins.

Notes

This fix assumes that the getActivePluginRegistry() function accurately reflects the state of the plugin registry. If this function is not reliable, additional debugging may be necessary.

Recommendation

Apply the proposed fix to add a fast path to ensureRuntimePluginsLoaded, as it directly addresses the performance issue and aligns with the function's intended behavior.

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…

FAQ

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.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix ensureRuntimePluginsLoaded cache-misses every inbound dispatch (~5-6s wasted per message) [1 pull requests, 1 comments, 2 participants]