Skip to content

fix: Control UI context % shows 100% when actual is ~56%#60649

Closed
progamman wants to merge 1 commit intoopenclaw:mainfrom
progamman:fix/control-ui-context-percentage
Closed

fix: Control UI context % shows 100% when actual is ~56%#60649
progamman wants to merge 1 commit intoopenclaw:mainfrom
progamman:fix/control-ui-context-percentage

Conversation

@progamman
Copy link
Copy Markdown

Root cause: UI used lifetime accumulated tokens (totalTokens) for context percentage, which stays high after compaction. Should use current window tokens (post-compaction) instead.

Fix:

  1. Add currentWindowTokens field to GatewaySessionRow type
  2. Gateway computes it: when totalTokensFresh=false (post-compaction), use transcriptUsage.totalTokens (current window), else use totalTokens
  3. UI uses currentWindowTokens for context % calculation, with fallback to totalTokens for backwards compatibility
  4. Remove early return when totalTokensFresh=false - now shows correct post-compaction percentage via currentWindowTokens

Fixes #48252

Root cause: UI used lifetime accumulated tokens (totalTokens) for
context percentage, which stays high after compaction. Should use
current window tokens (post-compaction) instead.

Fix:
1. Add currentWindowTokens field to GatewaySessionRow type
2. Gateway computes it: when totalTokensFresh=false (post-compaction),
   use transcriptUsage.totalTokens (current window), else use totalTokens
3. UI uses currentWindowTokens for context % calculation, with
   fallback to totalTokens for backwards compatibility
4. Remove early return when totalTokensFresh=false - now shows
   correct post-compaction percentage via currentWindowTokens

Fixes openclaw#48252
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime size: XS labels Apr 4, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 4, 2026

Greptile Summary

This PR aims to fix a Control UI bug where the context usage percentage shows an inflated value (e.g. 100%) after compaction instead of the true post-compaction window size (~56%). The fix has three parts: adding a currentWindowTokens field to GatewaySessionRow, computing it in buildGatewaySessionRow, and updating renderContextNotice in chat.ts to use it.

Key changes:

  • src/gateway/session-utils.types.ts – adds optional currentWindowTokens?: number to GatewaySessionRow (additive, backwards-compatible).
  • src/gateway/session-utils.ts – computes currentWindowTokens using a ternary on totalTokensFresh.
  • ui/src/ui/views/chat.ts – removes the early return nothing guard when totalTokensFresh === false and prefers currentWindowTokens with fallback to totalTokens.

Notable concern: The currentWindowTokens computation is algebraically equivalent to totalTokens in every reachable code path. When totalTokensFresh is true, the first branch returns totalTokens; when totalTokensFresh is false, resolveFreshSessionTotalTokens has already returned undefined and totalTokens was set from transcriptUsage?.totalTokens, so the fallback in the second branch also resolves to totalTokens. The field therefore carries no distinct information beyond totalTokens as currently coded. To actually expose the transcript's current-window figure independently of the computed totalTokensFresh flag, the condition should test transcriptUsage?.totalTokensFresh === false directly.

The removal of the premature return nothing guard in chat.ts is the substantive UI fix and is safe — the existing !used || !limit guard already handles the zero/undefined token case. No tests cover the new field or the post-compaction percentage behavior.

Confidence Score: 3/5

Merge with caution — the UI fix is safe, but the gateway-side currentWindowTokens computation does not deliver the semantic separation the PR intends, so the stated root-cause fix may be incomplete.

The chat.ts change (early-return removal) is correct and low-risk. The currentWindowTokens field is additive and backwards-compatible. However, the core computation in session-utils.ts produces a value identical to totalTokens in all paths, meaning the underlying scenario described in the PR description (using transcript current-window tokens instead of a lifetime total) is not actually achieved by the new field. If the production bug genuinely required differentiating transcriptUsage.totalTokens from totalTokens, the fix is incomplete. No tests cover the new field or the post-compaction percentage behavior.

Pay close attention to src/gateway/session-utils.ts line 1289 — the currentWindowTokens ternary needs to condition on transcriptUsage?.totalTokensFresh rather than the derived totalTokensFresh to achieve the stated goal.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/session-utils.ts
Line: 1289

Comment:
**`currentWindowTokens` is always equal to `totalTokens`**

The computation produces the same value as `totalTokens` in every reachable code path, so the new field provides no additional data despite the intent in the PR description.

Trace through the two possible states:

**When `totalTokensFresh = true`**
`totalTokens` came from `resolveFreshSessionTotalTokens(entry)`, so `currentWindowTokens = totalTokens` (first branch).

**When `totalTokensFresh = false`**
`resolveFreshSessionTotalTokens(entry)` returned `undefined` (it short-circuits when `entry.totalTokensFresh === false` per `src/config/sessions/types.ts:320`), so `totalTokens` was set from `transcriptUsage?.totalTokens`. Therefore `transcriptUsage?.totalTokens ?? totalTokens` equals `totalTokens` (second branch also collapses to the same value).

The scenario the PR intends to handle — where `resolveFreshSessionTotalTokens` returns a positive lifetime total while `transcriptUsage.totalTokensFresh === false` — would require the entry's `totalTokensFresh` flag to be unset even after compaction. In that case `totalTokensFresh` in `GatewaySessionRow` would be `true`, so the ternary still falls into the first branch (`currentWindowTokens = totalTokens = lifetime total`).

What the intent likely requires is to condition on `transcriptUsage?.totalTokensFresh` directly:

```ts
// Use transcript's current-window count when the transcript signals post-compaction;
// otherwise fall back to the accumulated total.
currentWindowTokens:
  transcriptUsage?.totalTokensFresh === false
    ? (transcriptUsage.totalTokens ?? totalTokens)
    : totalTokens,
```

The actual user-visible fix in `chat.ts` (removing the premature `return nothing`) is independent and correct, but the intent of having a semantically distinct `currentWindowTokens` is not achieved by the current implementation.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/gateway/session-utils.types.ts
Line: 51-52

Comment:
**Misleading JSDoc on `currentWindowTokens`**

The comment "Use this for UI context %, **not totalTokens which is lifetime**" is inaccurate. In `GatewaySessionRow`, `totalTokens` is NOT always the lifetime-accumulated value — when `totalTokensFresh === false`, `totalTokens` already holds the current-window figure sourced from the transcript (because `resolveFreshSessionTotalTokens` rejects it and the fallback is `transcriptUsage?.totalTokens`). Calling it "lifetime" here will mislead future readers into thinking the fields carry different semantics when they currently don't.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/gateway/session-utils.ts
Line: 1288-1289

Comment:
**No test coverage for the post-compaction context percentage path**

There are no tests (in `session-utils.test.ts` or elsewhere) that exercise the `currentWindowTokens` field, either for the `totalTokensFresh = true` or `totalTokensFresh = false` branches. Given that the PR is fixing a subtle UI bug around post-compaction token display, a test asserting the computed value of `currentWindowTokens` when transcript data is present (and `entry.totalTokensFresh === false`) would prevent regressions and make the intended semantics explicit.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: Control UI context % shows 100% whe..." | Re-trigger Greptile

totalTokens,
totalTokensFresh,
// currentWindowTokens: actual context in use (post-compaction = transcript tokens when fresh=false)
currentWindowTokens: totalTokensFresh ? totalTokens : (transcriptUsage?.totalTokens ?? totalTokens),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 currentWindowTokens is always equal to totalTokens

The computation produces the same value as totalTokens in every reachable code path, so the new field provides no additional data despite the intent in the PR description.

Trace through the two possible states:

When totalTokensFresh = true
totalTokens came from resolveFreshSessionTotalTokens(entry), so currentWindowTokens = totalTokens (first branch).

When totalTokensFresh = false
resolveFreshSessionTotalTokens(entry) returned undefined (it short-circuits when entry.totalTokensFresh === false per src/config/sessions/types.ts:320), so totalTokens was set from transcriptUsage?.totalTokens. Therefore transcriptUsage?.totalTokens ?? totalTokens equals totalTokens (second branch also collapses to the same value).

The scenario the PR intends to handle — where resolveFreshSessionTotalTokens returns a positive lifetime total while transcriptUsage.totalTokensFresh === false — would require the entry's totalTokensFresh flag to be unset even after compaction. In that case totalTokensFresh in GatewaySessionRow would be true, so the ternary still falls into the first branch (currentWindowTokens = totalTokens = lifetime total).

What the intent likely requires is to condition on transcriptUsage?.totalTokensFresh directly:

// Use transcript's current-window count when the transcript signals post-compaction;
// otherwise fall back to the accumulated total.
currentWindowTokens:
  transcriptUsage?.totalTokensFresh === false
    ? (transcriptUsage.totalTokens ?? totalTokens)
    : totalTokens,

The actual user-visible fix in chat.ts (removing the premature return nothing) is independent and correct, but the intent of having a semantically distinct currentWindowTokens is not achieved by the current implementation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/session-utils.ts
Line: 1289

Comment:
**`currentWindowTokens` is always equal to `totalTokens`**

The computation produces the same value as `totalTokens` in every reachable code path, so the new field provides no additional data despite the intent in the PR description.

Trace through the two possible states:

**When `totalTokensFresh = true`**
`totalTokens` came from `resolveFreshSessionTotalTokens(entry)`, so `currentWindowTokens = totalTokens` (first branch).

**When `totalTokensFresh = false`**
`resolveFreshSessionTotalTokens(entry)` returned `undefined` (it short-circuits when `entry.totalTokensFresh === false` per `src/config/sessions/types.ts:320`), so `totalTokens` was set from `transcriptUsage?.totalTokens`. Therefore `transcriptUsage?.totalTokens ?? totalTokens` equals `totalTokens` (second branch also collapses to the same value).

The scenario the PR intends to handle — where `resolveFreshSessionTotalTokens` returns a positive lifetime total while `transcriptUsage.totalTokensFresh === false` — would require the entry's `totalTokensFresh` flag to be unset even after compaction. In that case `totalTokensFresh` in `GatewaySessionRow` would be `true`, so the ternary still falls into the first branch (`currentWindowTokens = totalTokens = lifetime total`).

What the intent likely requires is to condition on `transcriptUsage?.totalTokensFresh` directly:

```ts
// Use transcript's current-window count when the transcript signals post-compaction;
// otherwise fall back to the accumulated total.
currentWindowTokens:
  transcriptUsage?.totalTokensFresh === false
    ? (transcriptUsage.totalTokens ?? totalTokens)
    : totalTokens,
```

The actual user-visible fix in `chat.ts` (removing the premature `return nothing`) is independent and correct, but the intent of having a semantically distinct `currentWindowTokens` is not achieved by the current implementation.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +51 to +52
/** Current context window tokens (post-compaction). Use this for UI context %, not totalTokens which is lifetime. */
currentWindowTokens?: number;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading JSDoc on currentWindowTokens

The comment "Use this for UI context %, not totalTokens which is lifetime" is inaccurate. In GatewaySessionRow, totalTokens is NOT always the lifetime-accumulated value — when totalTokensFresh === false, totalTokens already holds the current-window figure sourced from the transcript (because resolveFreshSessionTotalTokens rejects it and the fallback is transcriptUsage?.totalTokens). Calling it "lifetime" here will mislead future readers into thinking the fields carry different semantics when they currently don't.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/session-utils.types.ts
Line: 51-52

Comment:
**Misleading JSDoc on `currentWindowTokens`**

The comment "Use this for UI context %, **not totalTokens which is lifetime**" is inaccurate. In `GatewaySessionRow`, `totalTokens` is NOT always the lifetime-accumulated value — when `totalTokensFresh === false`, `totalTokens` already holds the current-window figure sourced from the transcript (because `resolveFreshSessionTotalTokens` rejects it and the fallback is `transcriptUsage?.totalTokens`). Calling it "lifetime" here will mislead future readers into thinking the fields carry different semantics when they currently don't.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1288 to +1289
// currentWindowTokens: actual context in use (post-compaction = transcript tokens when fresh=false)
currentWindowTokens: totalTokensFresh ? totalTokens : (transcriptUsage?.totalTokens ?? totalTokens),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No test coverage for the post-compaction context percentage path

There are no tests (in session-utils.test.ts or elsewhere) that exercise the currentWindowTokens field, either for the totalTokensFresh = true or totalTokensFresh = false branches. Given that the PR is fixing a subtle UI bug around post-compaction token display, a test asserting the computed value of currentWindowTokens when transcript data is present (and entry.totalTokensFresh === false) would prevent regressions and make the intended semantics explicit.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/session-utils.ts
Line: 1288-1289

Comment:
**No test coverage for the post-compaction context percentage path**

There are no tests (in `session-utils.test.ts` or elsewhere) that exercise the `currentWindowTokens` field, either for the `totalTokensFresh = true` or `totalTokensFresh = false` branches. Given that the PR is fixing a subtle UI bug around post-compaction token display, a test asserting the computed value of `currentWindowTokens` when transcript data is present (and `entry.totalTokensFresh === false`) would prevent regressions and make the intended semantics explicit.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 472498fb2c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/src/ui/views/chat.ts
const used = session?.totalTokens ?? 0;
// Use currentWindowTokens if available (post-compaction), fall back to totalTokens (lifetime)
// Note: We no longer hide when totalTokensFresh=false because currentWindowTokens is valid
const used = session?.currentWindowTokens ?? session?.totalTokens ?? 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep stale totals hidden without current-window tokens

This change removed the totalTokensFresh === false guard and now always falls back to totalTokens when currentWindowTokens is missing. In payloads that do not include the new field (for example older gateway responses or any partial row shape), stale totalTokens values are rendered again, which reintroduces incorrect high context percentages instead of suppressing them. The fallback should only use totalTokens when freshness is true, otherwise continue hiding the notice.

Useful? React with 👍 / 👎.

totalTokens,
totalTokensFresh,
// currentWindowTokens: actual context in use (post-compaction = transcript tokens when fresh=false)
currentWindowTokens: totalTokensFresh ? totalTokens : (transcriptUsage?.totalTokens ?? totalTokens),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive current-window tokens from stale-state source

currentWindowTokens is keyed off totalTokensFresh, but totalTokensFresh is computed earlier from whether totalTokens is present, so this expression collapses to the same value as totalTokens for any session with tokens. As written, the new field does not provide a distinct post-compaction metric, so switching the UI to currentWindowTokens cannot correct over-reported context usage in cases where totalTokens is already wrong.

Useful? React with 👍 / 👎.

progamman pushed a commit to progamman/openclaw that referenced this pull request Apr 6, 2026
1. openclaw#60667 - Properly drop incomplete-thinking (unsigned) to avoid API errors
2. openclaw#60664 - Handle deletion edits (empty newText) in edit recovery
3. openclaw#60649 - Fix currentWindowTokens to use transcriptUsage?.totalTokensFresh

All fixes address Greptile review feedback.
@progamman
Copy link
Copy Markdown
Author

Update: v2 branch with fix

Created v2 branch: progamman/fix/control-ui-context-percentage-v2

Fixed: The currentWindowTokens computation now uses transcriptUsage?.totalTokensFresh directly instead of the derived totalTokensFresh variable. This ensures we get the true post-compaction context window size.

@progamman
Copy link
Copy Markdown
Author

👋 Gentle reminder - this PR is awaiting review. Please let us know if any changes are needed. Thanks!

@BunsDev
Copy link
Copy Markdown
Member

BunsDev commented Apr 25, 2026

Closing as superseded by #71297. This PR and #71297 address the same Control UI chat warning failure tracked by #48252: stale/current-context mismatch causing false 100% context used warnings after compaction or long sessions.

#71297 is the canonical review path because it fixes the chat notice against stale snapshots, applies live session metadata as gateway events arrive, coalesces session reloads, and adds focused tests for those freshness paths plus the compact recommendation CTA.

@BunsDev BunsDev closed this Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui gateway Gateway runtime size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Control UI shows 100% context when actual is ~56%

2 participants