Repository navigation
feat(chat): show tokens per second and context usage below the composer - #1748
Conversation
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: needs maintainer review before merge. What this changesThis PR adds session-specific response-speed statistics and a context-usage bar below the native chat composer. Example: A model call produces 100 output tokens over two observed seconds.
Review scores
ProductKind: Feature · Worth it: Yes · Fix scope: Complete Merge readiness✅ Ready for maintainer review Keep open: this collaborator-authored feature remains absent from main, has explicit collaborator approval, and has no actionable source-level findings. Priority: P2 Before mergeNone. FindingsNone. Tests
Agent review detailsHow this fits togetherGateway lifecycle, text, thinking, and usage events enter the chat provider; an in-memory tracker calculates session statistics that a Reactor strip renders alongside existing context metadata. flowchart LR
G[Gateway events] --> P[Chat provider]
P --> T[Throughput tracker]
P --> M[Existing chat metadata]
T --> S[Reactor statistics strip]
M --> F[Usage formatter]
F --> S
Technical reviewBest possible solution: Retain the focused in-memory tracker and Reactor strip while preserving existing context formatting and connection lifecycle ownership. Do we have a high-confidence way to reproduce the issue? This is a feature request rather than a current-main bug; source inspection confirms main lacks the response-speed strip. Is this the best way to solve the issue? The implementation is appropriately scoped: the tracker owns bounded per-session measurements, the strip owns rendering and subscription cleanup, and context resolution reuses the existing formatter without changing its output. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 0a68951e794d. Provenance checked
TestingProof path: in-process harness. SecurityNone. EvidenceWhat I checked:
Likely related people:
Review metrics
LabelsLabel changes:
Label justifications:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment HistoryReview history (4 earlier review cycles)
Reviewed October 28, 2026, 7:48 PM ET / 23:48 UTC (Revision 5). |
fdc37c9 to
acaacff
Compare
|
Addressed the P2 from the review of The accounting hole was real on that revision: That open now initializes the tracker in The missing coverage is in Closeout on this head:
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
|
@joelagnel @karkarl please review. |
4b0cfa2 to
49752b7
Compare
|
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
|
@clawsweeper what's missing for waiting for maintainer look tag? |
|
|
|
On the current head ( |
49752b7 to
83a4b99
Compare
|
Addressed the reconnect P2 from ClawSweeper and @joelagnel in The finding was real. Fix: Tests:
Also rebased on Validation on this head (ARM64 Windows):
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
|
LGTM, please merge after clawsweeper passes. |
Add a slim strip under the native Reactor chat composer (tray ChatWindow and Hub ChatPage) showing current tokens/sec with mean (μ) and p95 over the last 50 measured replies per session, plus a context-window bar with used/total. - ChatThroughputTracker: per-thread, in-memory. Live value is a chars/4 estimate over streamed thinking/assistant text. Exact samples are one model call's output tokens over its streaming window, closed by the gateway `usage` agent event (run-cumulative outputTokens, diffed per call); the final assistant message's OutputTokens is the fallback when no usage stream exists. - ChatEventMapper reads `thinking` deltas and `usage` totals, which are not timeline events. - The strip subscribes to provider ThroughputChanged so thinking/usage updates re-render only the strip, not the timeline. - ChatUsageFormatter resolves context usage once for both the existing pill and the bar; existing Format outputs are unchanged.
…un starts Address rubber-duck review of the throughput strip: - Start a run for lifecycle starts opened from the reset buffer (HandleOpenedLifecycle), not only for directly processed start events. - Track whether the run's cumulative token baseline is known. A usage total for a run whose start was missed (provider created mid-run, ended run) only becomes the baseline, instead of being recorded as one call's tokens. - End runs on lifecycle end/error: close usage-run segments so aborted runs cannot leave a stale window open; keep the fallback segment for the final message that arrives after lifecycle end. - Strip reads stats from the provider in Render with a render-token subscription, so a session switch never shows the previous session's tps. - Tooltip: samples are per model call, not per reply. - Tests for thinking receipts, usage parsing, missed starts, and EndRun.
ClawSweeper's review of d9d8692 asked for a provider test of the start-before-send-result ordering with a prior measurement. After sessions.reset, a lifecycle start buffered until chat.send is accepted must record the new run's tokens, not the delta from the previous run.
main openclaw#1688 removed compact chat (IsCompact, ChatVisuals.CompactSessionBreakpoint, ChannelGroup.cs). Drop the strip's IsCompact prop, keep its narrow context-bar breakpoint local, and remove the stale ChannelGroup.cs test include.
A connection transition can drop lifecycle start/end events, so a pre-disconnect run baseline was subtracted from a post-reconnect run total ((5000 - 100) / 2 = 2450 tps). OnStatusChanged now calls ChatThroughputTracker.InvalidateRuns on disconnect/reconnect: open windows close and baselines become unknown; completed samples are kept.
6b6f755 to
c68e850
Compare
|
@clawsweeper review |
|
🦞👀 Re-review progress:
|
…er (openclaw#1748) * feat(chat): show tokens/sec and context usage below the composer Add a slim strip under the native Reactor chat composer (tray ChatWindow and Hub ChatPage) showing current tokens/sec with mean (μ) and p95 over the last 50 measured replies per session, plus a context-window bar with used/total. - ChatThroughputTracker: per-thread, in-memory. Live value is a chars/4 estimate over streamed thinking/assistant text. Exact samples are one model call's output tokens over its streaming window, closed by the gateway `usage` agent event (run-cumulative outputTokens, diffed per call); the final assistant message's OutputTokens is the fallback when no usage stream exists. - ChatEventMapper reads `thinking` deltas and `usage` totals, which are not timeline events. - The strip subscribes to provider ThroughputChanged so thinking/usage updates re-render only the strip, not the timeline. - ChatUsageFormatter resolves context usage once for both the existing pill and the bar; existing Format outputs are unchanged. * fix(chat): keep throughput samples honest across missed or buffered run starts Address rubber-duck review of the throughput strip: - Start a run for lifecycle starts opened from the reset buffer (HandleOpenedLifecycle), not only for directly processed start events. - Track whether the run's cumulative token baseline is known. A usage total for a run whose start was missed (provider created mid-run, ended run) only becomes the baseline, instead of being recorded as one call's tokens. - End runs on lifecycle end/error: close usage-run segments so aborted runs cannot leave a stale window open; keep the fallback segment for the final message that arrives after lifecycle end. - Strip reads stats from the provider in Render with a render-token subscription, so a session switch never shows the previous session's tps. - Tooltip: samples are per model call, not per reply. - Tests for thinking receipts, usage parsing, missed starts, and EndRun. * test(chat): cover buffered post-reset throughput baseline reset ClawSweeper's review of d9d8692 asked for a provider test of the start-before-send-result ordering with a prior measurement. After sessions.reset, a lifecycle start buffered until chat.send is accepted must record the new run's tokens, not the delta from the previous run. * fix(chat): align throughput strip with retired compact chat after rebase main openclaw#1688 removed compact chat (IsCompact, ChatVisuals.CompactSessionBreakpoint, ChannelGroup.cs). Drop the strip's IsCompact prop, keep its narrow context-bar breakpoint local, and remove the stale ChannelGroup.cs test include. * fix(chat): invalidate throughput baselines across reconnects A connection transition can drop lifecycle start/end events, so a pre-disconnect run baseline was subtracted from a post-reconnect run total ((5000 - 100) / 2 = 2450 tps). OnStatusChanged now calls ChatThroughputTracker.InvalidateRuns on disconnect/reconnect: open windows close and baselines become unknown; completed samples are kept.

What Problem This Solves
The native chat gives no sense of how fast the model is responding or how full the context window is while you work.
User Impact
User impact: a slim strip below the composer, in both the tray chat window and the Hub chat page, shows tokens per second for the latest model response, with the mean (μ) and p95 over the last 50 measured model calls in that session. Next to it is a context-window bar with used/total tokens and a percentage (for example
45.4K/200K (23%)).~.—.Why This Change Was Made
The Gateway does not report tokens per second, so the app works it out from event timing.
usageagent event gives the run's total output tokens so far. The app takes the increase since the previous event as that model call's output, and divides it by the time from the call's first streamed text to that event. On a Gateway that sends nousageevents, it falls back to the token count on the final assistant message. Each model call is counted once.~value is a rough estimate of about 4 characters per token.stream=thinkingdeltas while the reply text arrives in a single burst. So the measurement uses the thinking and usage streams, which the chat timeline otherwise ignores.Evidence
ChatThroughputTrackerTests(13) cover:ChatEventMapperThroughputTestscheck thatthinkingdeltas count only the delta, that persistence receipts are ignored, and thatusagetotals are parsed.A provider test replays the event order seen on the live Gateway (lifecycle start, thinking deltas,
usage, reply in one burst, final) and asserts exactly 50 tok/s.—.A provider test with no observed run start records no outlier, and the next call in the same run still measures exactly 60 tok/s.
A provider test covers the reset-gate ordering: a prior run records 100 tokens over 2 seconds (50 tok/s),
sessions.resetbuffers the next lifecycle start untilchat.sendis accepted, and the new run's 150 tokens over 2 seconds record 75 tok/s rather than(150 - 100) / 2 = 25. The buffered start does not reset the baseline until it is accepted.Rubber-duck review (reviewer agent) of the first commit found:
All four are fixed in the second commit. The review confirmed that every existing
ChatUsageFormatter.Formatoverload gives byte-identical output.ClawSweeper review of
d9d8692(needs changes, P2) asked for the same buffered-start reset, plus a provider test of the start-before-send-result ordering with a prior measurement. That open is initialized inHandleOpenedLifecycle, and the provider test above covers the requested ordering. The review was against a revision that did not include commitacaacff0.ChatUsageFormatterTestsgained tests for context resolution and the tps text format. All existing output tests pass unchanged.Change Type
Scope
winnodeRequired proof pools
windows-winui-interactive: a new visible strip below the chat composer, with its own accessibility names and tooltips.Validation
Run on the current head, rebased on
origin/main(ARM64 Windows host):./run-app-local.ps1: WinUI built with 0 errors, and the app launched.-AllowNonMainwas removed upstream in improve(dev): build only the tray graph in run-app-local.ps1 #1586 (improve(dev): build only the tray graph in run-app-local.ps1).dotnet test ./tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj --no-restore: 4313 passed, 36 skipped, 0 failed.dotnet test ./tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj --no-restore: 4107 passed, 0 failed.Follow-up closeout on this x64 host, after
4b0cfa27:./build.ps1: all projects succeeded.dotnet test ./tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj: 4350 passed, 36 skipped, 0 failed.--no-restorecould not run because the shared test assets file was missing in this worktree, so restore ran first.dotnet test ./tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj --no-restore: 4115 passed, 0 failed.BufferedLifecycleStartAfterReset_ResetsThroughputBaselinepassed.Real Behavior Proof
openclaw-tray.logevent sequence of real chat turns with the Gateway source (emitAgentRunOutputTokens,stream: "thinking"). Then built and launched with./run-app-local.ps1, enabled Local MCP, and confirmed withwinnode --list-toolsthatapp.chat.sendandapp.chat.snapshotare listed.—on a live Gateway. The cause was the burst reply and the ignored thinking and usage streams, which this head fixes.windows-winui-interactive). No Gateway was reachable for the retest: nothing was listening on127.0.0.1:18789, the default profile's Gateway record has no credentials, and the Dev profile that ran the earlier turns no longer exists. Local MCP itself worked (app.connection.statusreportedLocalMcpOnly,Idle).Security Impact
Yes, explain the risk and mitigation:Compatibility and Migration
Review Conversations