You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The gateway enters a hot loop in the plugin loader / bundled-runtime-deps mirror that pegs a single core at ~100% indefinitely after startup. The loop starves the Node event loop, which in turn breaks Telegram long-polling (getUpdates HTTPS reads time out before the response can be parsed) and prevents the bot from processing any incoming messages. WS RPC paths (TUI connect, openclaw channels status --deep) also time out at 10 s, but openclaw status --json (which doesn't go through the WS handler) still responds.
This looks distinct from #72338 (which describes the WS self-loopback timeout pattern). Disabling memory search, removing legacy tools.web keys, individually disabling each enabled plugin, and quarantining a runaway agent session (the visible loop-detection warnings) all left the spin in place. Profiling showed the spin is in the plugin loader, not in WS handling.
OpenClaw: 2026.4.25 (aa36ee6) — also reproduced on 2026.4.26 before downgrade
Node: v22.22.0 (LTS)
OS: Linux 5.15 x86_64
Gateway mode: local loopback systemd user service, port 18789
Symptoms
Gateway process is in R (running) state with wchan=0 — pure user-space spin, never blocks on a syscall
~100–115% CPU on a single core indefinitely after restart; doesn't decay after warmup
RSS climbs ~120 MB/min from a clean restart; we observed up to 1.4 GB after ~5 min
VIRT reaches ~32 GB (V8 reservation pressure)
openclaw doctor, openclaw channels status --deep, openclaw tui all time out with gateway timeout after 10000ms against ws://127.0.0.1:18789
openclaw status --json (no WS round-trip) still responds correctly
Telegram polling logs Polling stall detected (no completed getUpdates for 148.49s); forcing restart. error=Network request for 'getUpdates' failed. Telegram updates queued on the upstream API are never drained, even after a clean gateway restart and a 90 s warmup window
The Telegram REST API is reachable and the bot token is valid (verified with curl: getMe, deleteWebhook, getUpdates all succeed within ms when called directly from the same host)
The bundled-runtime-deps mirror at ~/.openclaw/plugin-runtime-deps/openclaw-<version>-<hash>/dist/ is well-formed (2667 entries vs 2666 in the source dist/; the only diff is the mirror's own package.json; no broken symlinks). The mirror does not need rebuilding, yet prepareBundledPluginRuntimeDistMirror is invoked repeatedly enough to dominate CPU. Combined with loadPluginManifestRegistry re-parsing manifests with JSON5 on the same hot path, this looks like the plugin-registry cache is missing on every call and re-doing the full load.
loadOpenClawPlugins does have a cache (getCachedPluginRegistry(cacheKey)), so the suspicion is one of:
cacheKey varies between callers (runtimeSubagentMode, workspaceDir, etc.), so different code paths each get their own cache and each thrashes
The cache is invalidated/cleared too aggressively (e.g., on every WS connection, every health probe, every tools-invoke)
cacheEnabled === false is set somewhere on the hot path
Whatever the trigger, the inner work (filesystem lock + per-file shouldMaterialize… decisions + JSON5 re-parse) runs frequently enough to saturate one core and starve the event loop.
Reproducer
Linux x86_64, Node 22 LTS, OpenClaw 2026.4.25 from npm, default install layout
Within 30 s the gateway is at ~100% CPU, RSS climbing, openclaw doctor health check times out at 10 s, Telegram updates begin to queue without being drained
Suggested next steps
Instrument getCachedPluginRegistry / setCachedPluginRegistry to log cache key + hit/miss for a few seconds and confirm whether the cache is missing on every call
If yes, normalize the cacheKey derivation (or share the cache across runtimeSubagentMode/workspaceDir permutations) so steady-state callers hit the cache instead of re-mirroring + re-parsing
Rate-limit / debounce prepareBundledPluginRuntimeDistMirror — the mirror is a steady-state directory, it should never need to run more than once per process lifetime unless the source dist/ changes
Happy to capture additional traces (heap snapshot, longer profile, instrumented log) if helpful.
Summary
The gateway enters a hot loop in the plugin loader / bundled-runtime-deps mirror that pegs a single core at ~100% indefinitely after startup. The loop starves the Node event loop, which in turn breaks Telegram long-polling (
getUpdatesHTTPS reads time out before the response can be parsed) and prevents the bot from processing any incoming messages. WS RPC paths (TUI connect,openclaw channels status --deep) also time out at 10 s, butopenclaw status --json(which doesn't go through the WS handler) still responds.This looks distinct from #72338 (which describes the WS self-loopback timeout pattern). Disabling memory search, removing legacy
tools.webkeys, individually disabling each enabled plugin, and quarantining a runaway agent session (the visible loop-detection warnings) all left the spin in place. Profiling showed the spin is in the plugin loader, not in WS handling.Private identifiers (hostnames, tokens, chat IDs, absolute user paths, peer Tailnet) omitted.
Environment
2026.4.25 (aa36ee6)— also reproduced on2026.4.26before downgradev22.22.0(LTS)Symptoms
R(running) state withwchan=0— pure user-space spin, never blocks on a syscallopenclaw doctor,openclaw channels status --deep,openclaw tuiall time out withgateway timeout after 10000msagainstws://127.0.0.1:18789openclaw status --json(no WS round-trip) still responds correctlyPolling stall detected (no completed getUpdates for 148.49s); forcing restart. error=Network request for 'getUpdates' failed. Telegram updates queued on the upstream API are never drained, even after a clean gateway restart and a 90 s warmup windowgetMe,deleteWebhook,getUpdatesall succeed within ms when called directly from the same host)Triggers ruled out
plugins.entries; 11 enabledagents.list; runaway sessions on disk quarantined; orphan dirs flagged by doctoragents.defaults.memorySearch.enabled = falsetools.web/tools.alsoAllowremovedchannels.telegram.commands.native = false(this did eliminate a separate retry storm but did not affect the CPU spin)openclaw doctor --fix: only archived an orphan transcript; CPU spin returns within ~30 sCPU profile (V8
--profover 60 s, 74 301 ticks captured)Top JS leaves (tick share):
Bottom-up (heavy parents) — the dominant call stack:
Parallel hot stack (JSON5 manifest parsing):
Other contributors visible in the bottom-up:
compileSourceTextModule/getModuleJobForImport/package_json_reader— ~7% of ticks. Suggests dynamic ESM imports happening on the hot path.collectRuntimePackageWildcardImportTargets→registerBundledRuntimeDependencyJitiAliasesbuildInstalledPluginIndex/safeHashFile→ repeated hashing of plugin index filesensureOpenClawPluginSdkAlias→ re-enteringprepareBundledPluginRuntimeDistMirrorInterpretation
The bundled-runtime-deps mirror at
~/.openclaw/plugin-runtime-deps/openclaw-<version>-<hash>/dist/is well-formed (2667 entries vs 2666 in the sourcedist/; the only diff is the mirror's ownpackage.json; no broken symlinks). The mirror does not need rebuilding, yetprepareBundledPluginRuntimeDistMirroris invoked repeatedly enough to dominate CPU. Combined withloadPluginManifestRegistryre-parsing manifests with JSON5 on the same hot path, this looks like the plugin-registry cache is missing on every call and re-doing the full load.loadOpenClawPluginsdoes have a cache (getCachedPluginRegistry(cacheKey)), so the suspicion is one of:cacheKeyvaries between callers (runtimeSubagentMode,workspaceDir, etc.), so different code paths each get their own cache and each thrashescacheEnabled === falseis set somewhere on the hot pathWhatever the trigger, the inner work (filesystem lock + per-file
shouldMaterialize…decisions + JSON5 re-parse) runs frequently enough to saturate one core and starve the event loop.Reproducer
systemctl --user restart openclaw-gatewayopenclaw doctorhealth check times out at 10 s, Telegram updates begin to queue without being drainedSuggested next steps
getCachedPluginRegistry/setCachedPluginRegistryto log cache key + hit/miss for a few seconds and confirm whether the cache is missing on every callcacheKeyderivation (or share the cache acrossruntimeSubagentMode/workspaceDirpermutations) so steady-state callers hit the cache instead of re-mirroring + re-parsingprepareBundledPluginRuntimeDistMirror— the mirror is a steady-state directory, it should never need to run more than once per process lifetime unless the sourcedist/changesHappy to capture additional traces (heap snapshot, longer profile, instrumented log) if helpful.