openclaw - ✅(Solved) Fix [Bug]: `openclaw status` / `openclaw health` take 40s / 17s — worker thread pegged in `jiti`'s `normalizeAliases` [1 pull requests]

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…

On a fresh, correctly-installed [email protected] (commit 041266a), one-shot informational commands take tens of seconds:

commandrealusersys
openclaw --version0.18s0.19s0.06s
openclaw --help0.07s0.05s0.03s
openclaw status40.7s62.8s4.6s
openclaw health17.4–42.3s (variable between runs)27–65s2–5s

The user time exceeding real confirms it's a multi-threaded CPU-bound workload, not I/O. The tool is not broken — it returns correct output — it's just very slow on every invocation.

Root Cause

Root cause hypothesis

Fix Action

Fixed

PR fix notes

PR #1: fix(onboard): guard quickstart channel skip

Description (problem / solution / changelog)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->

Summary

  • Problem: QuickStart channel onboarding assumed any non-"__skip__" picker result was a real channel id, so the reported skip path could fall through with an invalid value and crash instead of continuing.
  • Why it matters: this breaks first-run onboarding in the repo's current UX focus area and makes a basic "Skip for now" flow unreliable.
  • What changed: src/flows/channel-setup.ts now validates that the returned quickstart choice matches one of the rendered channel entries before attempting setup, otherwise it treats the result like a skip.
  • What did NOT change (scope boundary): no channel-specific setup logic, plugin loading rules, or wider onboarding flow behavior changed.
  • AI-assisted: Yes. I ran the fix, tests, and changed-scope validation locally in this workspace.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #69497
  • Related #69462
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: the quickstart channel picker trusted the prompter result too broadly and only special-cased "__skip__"; it did not verify that the returned value was one of the currently rendered channel ids.
  • Missing detection / guardrail: there was no regression test covering an invalid or undefined quickstart picker result.
  • Contributing context (if known): the issue report shows the skip path in interactive onboarding regressed to a trim crash instead of cleanly advancing.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/flows/channel-setup.test.ts
  • Scenario the test should lock in: when the quickstart picker returns undefined, channel setup behaves like "Skip for now" and does not try to load or configure a channel.
  • Why this is the smallest reliable guardrail: the bug is in the selection guard inside the shared channel setup flow, so a focused flow-level unit test exercises the failing branch without bringing in a full interactive onboarding stack.
  • Existing test that already covers this (if any): none.
  • If no new test is added, why not: N/A.

User-visible / Behavior Changes

  • QuickStart onboarding no longer crashes when the channel picker produces an invalid or missing selection during the "Skip for now" path; onboarding continues instead.

Diagram (if applicable)

Before:
[QuickStart channel picker skip/invalid result] -> [assume real channel choice] -> [downstream setup path] -> [crash]

After:
[QuickStart channel picker skip/invalid result] -> [validate against rendered channels] -> [treat as skip] -> [continue onboarding]

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: Ubuntu Linux in Cursor Cloud
  • Runtime/container: Node 22 + pnpm workspace
  • Model/provider: N/A
  • Integration/channel (if any): onboarding channel setup flow
  • Relevant config (redacted): none

Steps

  1. Start interactive onboarding in QuickStart mode.
  2. Reach Select channel (QuickStart).
  3. Trigger the skip path / missing picker value case.

Expected

  • Onboarding should continue without configuring a channel.

Actual

  • Before this fix, the reported path crashed with TypeError: Cannot read properties of undefined (reading 'trim').

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: ran pnpm test src/flows/channel-setup.test.ts; ran pnpm check:changed.
  • Edge cases checked: invalid undefined quickstart selection now behaves like skip and does not load or configure a channel.
  • What you did not verify: interactive TTY reproduction of the original skip path in this cloud environment.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: an unexpected but valid future picker value could be ignored if it is not included in the rendered entries list.
    • Mitigation: the guard intentionally only accepts currently rendered channel ids, which matches the setup flow contract and is covered by the new regression test.
<!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-a225a134-d59a-46cf-a2c4-14c5aaa6297d"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-a225a134-d59a-46cf-a2c4-14c5aaa6297d"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

Changed files

  • src/flows/channel-setup.test.ts (modified, +67/-0)
  • src/flows/channel-setup.ts (modified, +6/-1)

Code Example

% time     seconds  usecs/call     calls    errors syscall
 49.58    0.344574           7     47592     15400 openat
 45.64    0.317212           7     42515      7191 access
  4.74    0.032910         866        38        28 execve
  0.04    0.000311           8        37        29 stat
------ ----------- --------- --------- ----------------
100.00    0.695007     90182     22648 total

---

worker isolate summary:
   25847 ticks   61.4%   JavaScript
    4039 ticks    9.6%   C++
    1016 ticks    2.4%   GC
   11623 ticks   27.6%   Shared libraries (V8 itself)

---

ticks   pct   name
7057   16.8%   Builtin: StringPrototypeStartsWith
3810    9.0%   Builtin: LoadIC
3664    8.7%   JS: ^normalizeAliases
                   .../openclaw/node_modules/jiti/dist/jiti.cjs:1:141278
2118    5.0%   Builtin: EnumeratedKeyedLoadIC
1989    4.7%   Builtin: StrictEqual_Baseline
 ...
  89    0.2%   JS: *string  .../openclaw/node_modules/json5/lib/parse.js:570:12

---

total plugins: 161    sum of elapsedMs: 4307.9

---

const scopedCacheKey = `${params.jitiFilename ?? params.modulePath}::${params.cacheScopeKey ?? cacheKey}`;
const cached = params.cache.get(scopedCacheKey);
if (cached) return cached;
const loader = (params.createLoader ?? createJiti)(params.jitiFilename ?? params.modulePath, {
  ...buildPluginLoaderJitiOptions(aliasMap),
  tryNative
});
params.cache.set(scopedCacheKey, loader);

---

npm install -g openclaw@2026.4.15
time openclaw --version     # ~0.2s
time openclaw status        # ~40s  <-- bug
time openclaw health        # ~17–40s

# For the full evidence chain:
strace -f -c -e trace=execve,openat,stat,access openclaw status >/dev/null
OPENCLAW_PLUGIN_LOAD_PROFILE=1 openclaw status 2>&1 | grep plugin-load-profile | wc -l
node --prof "$(which openclaw)" status   # inspect worker isolate

---
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

Summary

On a fresh, correctly-installed [email protected] (commit 041266a), one-shot informational commands take tens of seconds:

commandrealusersys
openclaw --version0.18s0.19s0.06s
openclaw --help0.07s0.05s0.03s
openclaw status40.7s62.8s4.6s
openclaw health17.4–42.3s (variable between runs)27–65s2–5s

The user time exceeding real confirms it's a multi-threaded CPU-bound workload, not I/O. The tool is not broken — it returns correct output — it's just very slow on every invocation.

Environment

  • openclaw 2026.4.15 (041266a), installed via npm install -g openclaw
  • Node v24.15.0 (nvm)
  • Ubuntu 24.04, bash 5.2
  • NVIDIA RTX A5000, CUDA driver loaded; not a VM, local NVMe
  • Gateway daemon running (systemd user service), port 8082 open
  • No custom plugins, no user skills; only bundled extensions
  • Install path: ~/.nvm/versions/node/v24.15.0/lib/node_modules/openclaw/

The interesting part: it's not Node startup, it's jiti

My first hypothesis (and the one an LLM-based triage arrived at) was "1.1 GB / 92k-file install → Node module resolution is slow." That turns out to be wrong:

  • openclaw --version is 0.18s — module loading for the main entry is cheap.
  • Only status / health / similar cross-plugin commands are slow.

Syscall profile (full run of status)

strace -f -c -e trace=execve,openat,stat,access over a full openclaw status:

% time     seconds  usecs/call     calls    errors syscall
 49.58    0.344574           7     47592     15400 openat
 45.64    0.317212           7     42515      7191 access
  4.74    0.032910         866        38        28 execve
  0.04    0.000311           8        37        29 stat
------ ----------- --------- --------- ----------------
100.00    0.695007     90182     22648 total

0.7 seconds in syscalls out of a 40-second wall-clock run. This rules out filesystem / metadata bottleneck. The ~22k ENOENT errors are PATH-search misses on systemctl / docker / git lookups (nvm's bin/ is searched first) — noisy but cheap.

V8 CPU profile (node --prof)

Two isolates — the main process plus one worker thread. The worker thread is where ~95% of the CPU is spent:

worker isolate summary:
   25847 ticks   61.4%   JavaScript
    4039 ticks    9.6%   C++
    1016 ticks    2.4%   GC
   11623 ticks   27.6%   Shared libraries (V8 itself)

Top hot frames in the worker:

ticks   pct   name
7057   16.8%   Builtin: StringPrototypeStartsWith
3810    9.0%   Builtin: LoadIC
3664    8.7%   JS: ^normalizeAliases
                   .../openclaw/node_modules/jiti/dist/jiti.cjs:1:141278
2118    5.0%   Builtin: EnumeratedKeyedLoadIC
1989    4.7%   Builtin: StrictEqual_Baseline
 ...
  89    0.2%   JS: *string  .../openclaw/node_modules/json5/lib/parse.js:570:12

The hot function is jiti's normalizeAliases, called via dist/jiti-loader-cache-*.jscreateJiti() per plugin. StringPrototypeStartsWith and LoadIC are very likely the implementation of alias prefix matching inside normalizeAliases.

Plugin-load profile (OPENCLAW_PLUGIN_LOAD_PROFILE=1)

total plugins: 161    sum of elapsedMs: 4307.9

161 plugins × ~27ms avg, summing to ~4.3s on the main thread. That 4.3s is not the main cost — it's what else happens in the worker (presumably another full plugin pass, plus session/health scans) that grinds for ~30 more seconds.

Individual plugins above 100ms on the main thread:

  • codex — 1410 ms
  • browser — 358 ms
  • google — 314 ms
  • acpx — 227 ms
  • memory-core — 110 ms
  • amazon-bedrock — 107 ms

Root cause hypothesis

dist/jiti-loader-cache-*.js caches the jiti loader object in an in-process Map:

const scopedCacheKey = `${params.jitiFilename ?? params.modulePath}::${params.cacheScopeKey ?? cacheKey}`;
const cached = params.cache.get(scopedCacheKey);
if (cached) return cached;
const loader = (params.createLoader ?? createJiti)(params.jitiFilename ?? params.modulePath, {
  ...buildPluginLoaderJitiOptions(aliasMap),
  tryNative
});
params.cache.set(scopedCacheKey, loader);

This deduplicates loaders within a single process, but every fresh CLI invocation starts with an empty Map. The jiti filesystem cache at ~/.cache/jiti is empty on this install, suggesting jiti is either running with cache: false or a cache location it can't populate.

If jiti is re-walking its alias table for ~161 plugin loads per invocation, that matches the observed profile (big normalizeAliases + StringPrototypeStartsWith hotspot).

Steps to reproduce

Things I tried that did NOT help (status stays at ~40s)

  • OPENCLAW_LIVE_PROVIDERS=0
  • OPENCLAW_CODEX_DISCOVERY_LIVE=0
  • OPENCLAW_LIVE_GATEWAY_PROVIDERS=0
  • OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS=3600000
  • OPENCLAW_PLUGIN_MANIFEST_CACHE_MS=3600000
  • OPENCLAW_SESSION_CACHE_TTL_MS=3600000
  • OPENCLAW_SESSION_MANAGER_CACHE_TTL_MS=3600000
  • Warmer passes (no disk-cache effect between invocations)

Reproduction

npm install -g [email protected]
time openclaw --version     # ~0.2s
time openclaw status        # ~40s  <-- bug
time openclaw health        # ~17–40s

# For the full evidence chain:
strace -f -c -e trace=execve,openat,stat,access openclaw status >/dev/null
OPENCLAW_PLUGIN_LOAD_PROFILE=1 openclaw status 2>&1 | grep plugin-load-profile | wc -l
node --prof "$(which openclaw)" status   # inspect worker isolate

Expected behavior

Suggestions

  1. Enable a persistent jiti filesystem cache so alias normalization pays its cost once per install, not once per invocation. Pass cache: true (or an explicit cache dir under ~/.cache/openclaw-jiti) in buildPluginLoaderJitiOptions.
  2. Avoid jiti for dist/extensions/*/index.js entries — those are already-built JS. Unless the aliasing is required for something else, a plain dynamic import() skips the whole jiti code path. Restrict jiti to user-supplied plugin sources.
  3. Lazy-load plugins for read-only commands. status / health arguably only need the plugins that are currently enabled/running, not all 161.
  4. Amortize the worker startup: the gateway daemon is already running — route status/health through it via IPC/HTTP rather than reconstructing the plugin graph in the CLI.

Actual behavior

Openclaw cli commands take 30-60 seconds each.

OpenClaw version

2026.4.15

Operating system

ubuntu 24.04

Install method

npm

Model

gemini pro

Provider / routing chain

openclaw -> gemini pro

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

No response

Additional information

No response

extent analysis

TL;DR

Enable a persistent jiti filesystem cache to reduce the time spent on alias normalization in the OpenClaw CLI.

Guidance

  1. Verify jiti cache configuration: Check if the jiti cache is enabled and configured correctly in the OpenClaw setup.
  2. Implement lazy plugin loading: Only load plugins that are necessary for the specific command being executed, such as status or health.
  3. Route commands through the gateway daemon: Use the already running gateway daemon to handle status and health commands, reducing the need to reconstruct the plugin graph.
  4. Restrict jiti usage: Limit jiti to user-supplied plugin sources and avoid using it for built-in extensions.

Example

To enable the jiti cache, you can pass cache: true in the buildPluginLoaderJitiOptions function:

const buildPluginLoaderJitiOptions = (aliasMap) => {
  // ...
  return {
    // ...
    cache: true,
  };
};

Notes

The provided steps to reproduce the issue and the expected behavior suggest that the problem is related to the jiti cache and plugin loading. However, without further information about the OpenClaw configuration and setup, it's difficult to provide a more specific solution.

Recommendation

Apply the suggested workaround by enabling the jiti cache and implementing lazy plugin loading to reduce the time spent on alias normalization and plugin loading. This should improve the performance of the OpenClaw CLI commands.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING