openclaw - 💡(How to fix) Fix control-ui: loadOverview fires usage.cost unconditionally, ignoring active tab [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…

loadOverview in ui/src/ui/app-settings.ts calls loadUsage(app) inside a fire-and-forget void Promise.allSettled([...]) with no tab guard. The isCurrentOverviewRefresh() check only gates the .then() callback (preventing stale results from being applied), but the API request — including usage.cost — fires regardless of which tab the user is on.

Root Cause

ui/src/ui/app-settings.ts, loadOverview (secondary load block):

void Promise.allSettled([
  loadDebug(app),
  loadSkills(app),
  loadUsage(app),          // ← no tab guard; fires unconditionally
  loadOverviewLogs(app),
  loadModelAuthStatusState(app, { refresh: opts?.refresh }),
]).then((results) => {
  if (!isCurrentOverviewRefresh()) { return; }  // ← guard only here, after requests complete
  // ...
});

isCurrentOverviewRefresh() checks host.tab === "overview", but this only runs in .then() — after usage.cost has already been sent to the gateway. All other secondary-load functions (loadDebug, loadSkills, etc.) are only meaningful to the overview tab as well, but the cost is negligible compared to usage.cost.

Fix Action

Fixed

Code Example

15:05:51  sessions.messages.subscribe ×13  conn=21a0be19…539e
15:05:51  sessions.subscribe               conn=21a0be19…539e
15:05:52  usage.cost   656ms  id=6100  ← initial connect
15:06:55  usage.cost 11176ms  id=6115  ← repeated, non-usage tab
15:08:44  usage.cost 12263ms  id=6116
15:09:52  usage.cost  9107ms  id=6119
15:10:32  usage.cost 11945ms  id=6120
15:11:06  usage.cost 13077ms  id=6121
... continues for the full 30-minute session ...

---

09:21:13  usage.cost  656ms  conn=a682412d…009a
09:22:13  usage.cost  642ms  conn=a682412d…009a  (+60 s)
09:23:13  usage.cost  656ms  conn=a682412d…009a  (+60 s)
09:24:13  usage.cost  642ms  conn=a682412d…009a  (+60 s)

---

void Promise.allSettled([
  loadDebug(app),
  loadSkills(app),
  loadUsage(app),          // ← no tab guard; fires unconditionally
  loadOverviewLogs(app),
  loadModelAuthStatusState(app, { refresh: opts?.refresh }),
]).then((results) => {
  if (!isCurrentOverviewRefresh()) { return; }  // ← guard only here, after requests complete
  // ...
});

---

void Promise.allSettled([
  loadDebug(app),
  loadSkills(app),
  isCurrentOverviewRefresh() ? loadUsage(app) : Promise.resolve(),
  loadOverviewLogs(app),
  loadModelAuthStatusState(app, { refresh: opts?.refresh }),
]).then((results) => {
  if (!isCurrentOverviewRefresh()) { return; }
  // ...
});
RAW_BUFFERClick to expand / collapse

Summary

loadOverview in ui/src/ui/app-settings.ts calls loadUsage(app) inside a fire-and-forget void Promise.allSettled([...]) with no tab guard. The isCurrentOverviewRefresh() check only gates the .then() callback (preventing stale results from being applied), but the API request — including usage.cost — fires regardless of which tab the user is on.

Real behavior proof

Gateway log from a session where the control-ui dashboard was open but the user was not on the usage tab. The subscribe burst (13× sessions.messages.subscribe + 1× sessions.subscribe) confirms this is the overview panel, not the usage tab. Despite that, usage.cost fires repeatedly throughout the session:

15:05:51  sessions.messages.subscribe ×13  conn=21a0be19…539e
15:05:51  sessions.subscribe               conn=21a0be19…539e
15:05:52  usage.cost   656ms  id=6100  ← initial connect
15:06:55  usage.cost 11176ms  id=6115  ← repeated, non-usage tab
15:08:44  usage.cost 12263ms  id=6116
15:09:52  usage.cost  9107ms  id=6119
15:10:32  usage.cost 11945ms  id=6120
15:11:06  usage.cost 13077ms  id=6121
... continues for the full 30-minute session ...

A second connection shows the same pattern with a regular 60 s cadence and warm-cache responses, confirming this is not user-triggered:

09:21:13  usage.cost  656ms  conn=a682412d…009a
09:22:13  usage.cost  642ms  conn=a682412d…009a  (+60 s)
09:23:13  usage.cost  656ms  conn=a682412d…009a  (+60 s)
09:24:13  usage.cost  642ms  conn=a682412d…009a  (+60 s)

Root cause

ui/src/ui/app-settings.ts, loadOverview (secondary load block):

void Promise.allSettled([
  loadDebug(app),
  loadSkills(app),
  loadUsage(app),          // ← no tab guard; fires unconditionally
  loadOverviewLogs(app),
  loadModelAuthStatusState(app, { refresh: opts?.refresh }),
]).then((results) => {
  if (!isCurrentOverviewRefresh()) { return; }  // ← guard only here, after requests complete
  // ...
});

isCurrentOverviewRefresh() checks host.tab === "overview", but this only runs in .then() — after usage.cost has already been sent to the gateway. All other secondary-load functions (loadDebug, loadSkills, etc.) are only meaningful to the overview tab as well, but the cost is negligible compared to usage.cost.

Impact

  • On 5.22 (pre-beta.2), each usage.cost call takes 9–18 s because warmCurrentProviderAuthState blocks the Node event loop on every call (the regression fixed in beta.2 by #84816). Repeated calls every ~60 s cause sustained CPU spikes and 3+ minute perceived response latency across all sessions while the dashboard is open.
  • On beta.2+ (warm auth-state, ~600 ms responses), these calls are cheap individually but still unnecessary noise when the user is not viewing usage data — they hold the usageLoading lock and suppress any user-initiated usage refresh that arrives while one is in-flight.

Proposed fix

Guard loadUsage with the same tab check that already gates the .then():

void Promise.allSettled([
  loadDebug(app),
  loadSkills(app),
  isCurrentOverviewRefresh() ? loadUsage(app) : Promise.resolve(),
  loadOverviewLogs(app),
  loadModelAuthStatusState(app, { refresh: opts?.refresh }),
]).then((results) => {
  if (!isCurrentOverviewRefresh()) { return; }
  // ...
});

This is safe because loadOverview is only called when host.tab === "overview" (via refreshActiveTab), so isCurrentOverviewRefresh() will be true on entry; it only becomes false if the user navigates away during the async primary load — exactly the case where firing usage.cost is wasteful.

Version

Reproduced on 5.22 stable. Not fixed in 5.24-beta.2 (beta.2 fixes usage.cost slowness via auth-state pre-warming, but does not add this tab guard).


Investigated with AI assistance (Claude Code).

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