Skip to content

fix(metrics): windowed speed and long transcripts - #771

Merged
sirmalloc merged 3 commits into
sirmalloc:mainfrom
eric-engberg:fix/speed-metrics-transcripts
Oct 30, 2026
Merged

sirmalloc merged 3 commits into
sirmalloc:mainfrom
eric-engberg:fix/speed-metrics-transcripts

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

  • Output, Input and Total Speed with a window set (1-120s) now read the speed the responses actually ran at, instead of up to hundreds of times more.
  • On the Node build (npm/npx), long sessions no longer lose their transcript widgets. Previously, once one transcript passed about 125k usage rows, any speed widget turned Tokens to 0, speed to "—", Compaction Counter to a false ↻ 0 and Session Name to blank. When neither Claude Code nor the usage API gave a 5-hour reset time (so the transcripts are scanned for the block), Block Timer, Block Reset Timer and the block usage-percent fallback showed no block, with or without a speed widget.

Why (evidence)

Windowed speed. buildSpeedMetrics picks each request whose response lands inside the window and adds all of its tokens, but divides by only the part of its user-to-response interval inside the window. A response longer than the window, or a tool result arriving soon after one (it moves the window's end), leaves a thin slice of time against the whole request's tokens. For one 90s response at 50 t/s:

Transcript Session avg 10s 30s 60s
tool result 9.9s after the response 50.0 45.0k 223.9 89.8
response is the last row 50.0 450.0 150.0 75.0
agentic loop: 20s responses, 3s tools 50.0 142.9 83.3 58.8

Long transcripts. mergeCollectedSpeedMetrics does requests.push(...part.requests) and the block scan does timestamps.push(...fileTimestamps). Both pass one argument per usage row, and the call throws "RangeError: Maximum call stack size exceeded" past about 125k arguments under Node 26 (works at 124k, throws at 126k), or about 640k under Bun 1.4 (works at 600k, throws at 700k).

  • The speed merge runs inside the transcript scan's single try, whose catch returns the empty result, so everything already gathered is lost.
  • The block scan's catch returns "no block". That result is cached for only 60s, so the failing full scan runs again every minute.

With a 140k-row transcript, the built dist under Node prints Total: 0 | Out: 0 | Out: — | ↻ 0 | Block: 0hr 0m. The same input under Bun, or under Node without the speed widget, shows the tokens (Total: 4.2M | Out: 2.8M …); without the speed widget, Node still shows no block. At 700k rows Bun fails the same way.

How

Demo

Four Output Speed widgets (session average, 10s, 30s, 60s) on fake transcripts where every response runs at 50 t/s.

Powerline

Before:

Windowed speed before the fix, Powerline

After:

Windowed speed after the fix, Powerline

Plain

Before:

Windowed speed before the fix, plain

After:

Windowed speed after the fix, plain

A long session's 140k-row transcript through the built dist under Node (npx) and Bun (bunx). Each render starts from an empty cache.

Powerline

Before:

Large transcript before the fix, Powerline

After:

Large transcript after the fix, Powerline

Plain

Before:

Large transcript before the fix, plain

After:

Large transcript after the fix, plain

Testing

  • On main as of [low] perf(claude-status): hold a lock while the status page fetch is in flight #635 and [medium] perf(git-review): key the PR cache through the cached runGit #639 (62a371b): merges cleanly, bun run lint is clean, and bun test gives 3706 pass, 0 fail. The built CLI under Bun 1.4.4 and Node 26.13.0 matches main in plain and Powerline mode with the baseline configs, and the TUI opens and exits cleanly. This branch's tests against main's code: 9 fail, 2 for the long transcripts and 7 for windowed counts (including the three fix: count each API response once in speed and token metrics #622 window tests above).
  • New tests: a request that straddles the window (10s and 40s windows), a 700k-row transcript through getTranscriptAnalysis, and a 700k-row transcript through getBlockMetrics. Together with the updated subagent window test, all 4 fail on main under both Bun and Node and pass here. The large-transcript tests use 700k rows so they fail under Bun too; each adds about 0.3s under Bun and 1s under Node.
  • bun test: 3171 pass, 0 fail. bun run lint: clean.
  • Vitest under Node: 2962 passed; the 186 failures are the same tests that fail on main. Both changed test files pass 38/38 under Node and Bun.
  • The built dist was checked under Bun 1.4.2 and Node 26.10, plain and Powerline, with the default payload and with a 140k-row transcript. Every runtime agrees on this branch; on main they disagree for the large transcript.

With a speed window set (for example 10s), Output, Input and Total Speed
picked each request whose response landed inside the window and added all
of its tokens, but divided by only the part of its user-to-response
interval that falls inside the window. A response that ran longer than the
window, or a tool result arriving soon after one (it moves the window's
end), left a thin slice of time against the whole request's tokens. One
90s response at 50 t/s read 450 t/s at a 10s window, and 45.0k t/s when
its tool result came 9.9s later, while the session average showed 50 t/s.

Each selected request now keeps the share of its tokens that matches the
share of its interval inside the window. A single request reads its own
speed at any window size, and several requests average by the time each
spent inside the window. The subagent window test expected the old
mismatch (15 t/s for a 12 t/s request) and now expects 12 t/s.
Speed metrics merged each transcript's requests with
requests.push(...part.requests), and the block scan gathered each file's
timestamps with timestamps.push(...fileTimestamps). Both pass one argument
per usage row. Past about 125k rows in one transcript under Node (the
npm/npx install), or about 640k under Bun, the call throws "RangeError:
Maximum call stack size exceeded".

The speed merge runs inside the transcript scan's single try, so the throw
discarded everything the scan had gathered: with any speed widget on the
line, Tokens read 0, speed read the em dash, and Compaction Counter and
Session Name went blank. The block scan's catch turned the throw into "no
block", so Block Timer, Block Reset Timer and the block usage-percent
fallback showed 0hr 0m or nothing, and because that result is cached for
only 60s, the failing full scan ran again every minute.

Both now push one item at a time.
# Conflicts:
#	src/utils/jsonl-metrics.ts
@sirmalloc
sirmalloc merged commit c6c7f7b into sirmalloc:main Oct 30, 2026
3 checks passed
pcvelz added a commit to pcvelz/ccstatusline-usage that referenced this pull request Nov 2, 2026
Upstream: weekly limit timers (sirmalloc#768), prompt cache miss widgets (sirmalloc#770), context window size env override (sirmalloc#352, sirmalloc#407), windowed speed fixes (sirmalloc#769, sirmalloc#771), separator/renderer fixes (sirmalloc#773), custom-command styling (sirmalloc#775), clickable cwd links (sirmalloc#744), account email from live credentials (sirmalloc#724), cache timer reported expiry + short toggle (sirmalloc#576, sirmalloc#592), claude-status lock (sirmalloc#635, sirmalloc#772), dedup token metrics (sirmalloc#622, sirmalloc#725, sirmalloc#735).
Not adopted: sirmalloc#765 terminal width from COLUMNS with fixed reserve - the fork keeps flexMode/compactThreshold and tmux pane priority.

Hand edits outside conflicts:
- README.md: kept fork Configure flex separator behavior bullet and older-updates details wrapper; dropped upstream Related Projects additions (claude-eta)
- AGENTS.md: kept fork terminal width wording (sirmalloc#765 not adopted)
- src/types/FlexMode.ts, src/tui/components/TerminalWidthMenu.tsx, src/tui/components/__tests__/TerminalWidthMenu.test.ts, src/tui/components/TerminalOptionsMenu.tsx, src/tui/components/__tests__/TerminalOptionsMenu.test.ts: restored, fork keeps terminal width settings
- src/tui/App.tsx, src/tui/components/index.ts, src/types/index.ts: re-wired terminal width menu and FlexMode export
- src/types/StatusJSON.ts: dropped fork duplicate prompt_cache key, upstream schema is a superset
- src/utils/terminal.ts: COLUMNS source gated on non-tmux so tmux pane width keeps priority
- src/widgets/WeeklyLimitTimer.ts, src/utils/usage-prefetch.ts, src/widgets/__tests__/WeeklyLimitTimer.test.ts: compat fix - fableUsage/fableResetAt -> weeklyFableUsage/weeklyFableResetAt
- src/widgets/ApiUsage.tsx: compat fix - Context Bar honors effective context window (override, clamp, 1.0M format); Weekly renders a valid value despite a session error
- src/widgets/__tests__/ApiUsage.localModelFallback.test.ts: output tokens no longer counted in context (upstream sirmalloc#730)
- src/utils/__tests__/renderer-flex-width.test.ts, src/utils/__tests__/renderer-ansi.test.ts, src/utils/__tests__/gradient.test.ts, src/utils/__tests__/renderer-separator-collapse.test.ts, src/utils/__tests__/config.test.ts, src/tui/__tests__/App.test.ts: fork deviation - fork width model (flexMode, 6-column full reserve, kept settings, Terminal Width menu entry)
- src/utils/__tests__/default-colors.test.ts: fork deviation - accept ansi256:/hex: defaults
- src/utils/__tests__/usage-prefetch.test.ts: fork deviation - weekly usage comes from the API, fork widget imports from ApiUsage
- src/widgets/__tests__/CurrentWorkingDir.test.ts, src/widgets/__tests__/CustomCommand.test.ts: Settings literal gains flexMode/compactThreshold
- src/utils/__tests__/context-window.test.ts, src/utils/__tests__/jsonl-metrics.test.ts: restored fork tests upstream dropped
- docs/test-retirements.md: ledger for the dev-reload snapshot-path and RemoteControlStatus glyph (sirmalloc#605) tests upstream replaced
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants