Repository navigation
fix(blocks): four ways the transcript block fallback found the wrong block - #752
Merged
Merged
Conversation
…cters
The transcript fallback for Block Timer, Block Reset Timer and Block %
globbed `<config dir>/projects/**/*.jsonl` with the absolute config
directory written into the pattern. Characters a directory name can
contain, such as ( ) [ ] { } * ?, are glob syntax inside a pattern, so
with CLAUDE_CONFIG_DIR at a path like "~/claude (work)/.claude" the
pattern never matched the literal path. Every scan found no
transcripts, so Block Timer stayed at "0hr 0m" and Block Reset Timer at
[Loading] for as long as that directory was in use.
The glob already runs with the config directory as its cwd, so the
pattern is now the relative `projects/**/*.jsonl`. The directory is
then only ever a path, never part of the pattern.
When a request is rejected (a usage limit, an expired login, an API outage), Claude Code writes an assistant row with isApiErrorMessage set, model "<synthetic>" and every usage field 0. The block scan only checked that input_tokens and output_tokens were numbers, so these rows passed as activity even though nothing was served: - With only rejected requests in the last 5 hours, Block Timer and Block Reset Timer showed a running block that doesn't exist, e.g. "Block: 3hr 1m" / "Reset: 1hr 58m" instead of "0hr 0m" / [Loading]. - When rejected requests came before real work, the block started at the hour of the first rejection, so the reset was reported hours early. Either wrong start was then cached for 5 hours. The scan now skips rows flagged isApiErrorMessage, as the token and context metrics and Cache Timer already do.
…may fill To find the current 5-hour block, the transcript fallback walks back from the newest activity to the first gap of 5 hours or more, then chains blocks forward from there. It reads in widening passes (10h, 20h, 48h), and each pass reads only the transcripts last written after its cutoff. But it keeps every row in the transcripts it reads, so a session left open since yesterday brings in rows from long before the cutoff. A gap between those rows and today's was taken as the session boundary even when another transcript, skipped only because it was last written before the cutoff, has rows inside it. The wider passes never ran, the chain started at the wrong row, and the block start came out hours early: Block Timer read 4hr 57m instead of 2hr 57m, the reset was predicted 2 hours early, and the wrong start was cached until then. A skipped transcript can only hold rows from before the cutoff. So a gap is now a boundary only if the part of it after the cutoff spans a session by itself; otherwise the next, wider pass reads the skipped transcripts first. The last pass still takes any gap, as before.
The block cache keeps either the current block's start or, after a scan that found no block, an instant until which scans are skipped (one minute ahead). Neither had an upper bound. When the system clock runs ahead for a render (a dual-boot clock mismatch, a manual change) and is then set back: - a "no block" entry stood for the whole skew plus a minute, so Block Timer showed "0hr 0m" and Block Reset Timer [Loading] for that long (a day, after a day's skew), even with fresh activity in the transcripts; - a block start cached under the fast clock lies in the future, and still counted as valid, so elapsed time stayed at 0 and Block Reset Timer at 5hr for the skew plus 5 hours. A "no block" entry is now trusted only while it ends within one interval, and a cached start only once it has begun; otherwise the transcripts are scanned again.
The block scan counts only served requests as activity. Besides rejected requests, it skips lines that don't parse, rows without both input and output token counts (such as user messages), sidechain rows, and rows whose timestamp is missing, not a string, or not a valid date. Only the rejected-request case had a test against the real scan; this one puts a row of each other kind after a real request and checks that the block and its last activity come from that request alone. The row with the invalid date comes first so that, if it were kept, it would sort ahead of the real request and show up as the last activity.
getAllTimestampsFromFile both read the transcript and decided which rows count as block activity, so the six reasons to skip a row sat as a run of continue statements inside the line loop. getActivityTimestamp now answers that question for one row, returning its timestamp or null with early returns, and the loop only reads lines and keeps what it returns. The row shape both functions use gets a name, TranscriptRow. No change in which rows count.
The block scan counts a row as activity only when it has both an input and an output token count. The skipped-rows test had a row with input tokens and no output tokens, but none with output tokens and no input tokens, so a scan that checked only the output count still passed it. This adds that row, so the test now fails if either count stops being required.
axisrow
added a commit
to axisrow/ccstatusline
that referenced
this pull request
Oct 29, 2026
sirmalloc#755 live picker preview, sirmalloc#754 fast mode widget, sirmalloc#753 thinking-effort test stub, sirmalloc#752 transcript block fallback fixes, sirmalloc#751 editor keys no longer pause on global install lookup, sirmalloc#750 git cache TTL starts after the run, sirmalloc#749 npx cache skip, sirmalloc#747 Block Usage hides at 100% and swaps with Block Reset Timer) Conflict policy: upstream infra wins, fork features ported on top. Joins: sirmalloc#750 stamps the git cache after the run while keeping the fork's execGit hardening/env snapshot and daemon prefetch bookkeeping; sirmalloc#752's isApiErrorMessage guard lands in the fork's shared collectTimestampFromLine (sync + async twins), upstream's duplicate getActivityTimestamp dropped; sirmalloc#747 adds SESSION_AT_LIMIT alongside the fork's zero hide state (BlockResetTimer keeps zero + under-limit via isAtSessionLimit, SessionUsage is no-data/zero/at-limit); sirmalloc#755 replaces the fork's picker window + "Showing X-Y of N" note with upstream's renderWindowedList/getPickerMaxVisible and live preview (markers ↑/↓ N more cover the counts); sirmalloc#751's installationState useMemo replaces the fork's inline lookups, the fork's mainMenuIndex helper is kept. Co-Authored-By: Claude Code <noreply@anthropic.com>
pcvelz
added a commit
to pcvelz/ccstatusline-usage
that referenced
this pull request
Oct 29, 2026
Upstream: Extra Usage Limit widget (sirmalloc#756), live widget preview while picking (sirmalloc#755), Fast Mode widget (sirmalloc#754), transcript block fallback fixes (sirmalloc#752), editor keys on a global install (sirmalloc#751), git cache TTL (sirmalloc#750), npx cached copy skip (sirmalloc#749), Block Usage hide at 100% (sirmalloc#747), Block Reset Timer hide until 100% (sirmalloc#697), fish-style branch abbreviation (sirmalloc#614), jj detached HEAD (sirmalloc#705), hooks save warning (sirmalloc#703), dev reload (sirmalloc#700), merge ends at a hidden widget (sirmalloc#698), Block Forecast and Block Limit Timer widgets (sirmalloc#699), contributor guide (sirmalloc#706), POSIX path case fix (sirmalloc#707), whitespace-distinct conflict filenames (sirmalloc#709), Quick Start reorder (sirmalloc#715), bunx launch guidance (sirmalloc#714), jj diff summary counts (sirmalloc#711), powerline manual separators (sirmalloc#713), keychain credential by account (sirmalloc#532), Keychain probe timeout (sirmalloc#760), usage cache scoped per CLAUDE_CONFIG_DIR (sirmalloc#266), test isolation (sirmalloc#717, sirmalloc#753), dev-dependency bumps (sirmalloc#745) Conflicts resolved per the standard-resolution table; fork deletions re-applied. Hand edits outside conflicts: - src/utils/global-command-resolution.ts: fork-rename (ccstatusline@ -> ccstatusline-usage@ in upstream's new bunx pattern) - src/utils/__tests__/global-package-manager.test.ts: fork-rename (package dir ccstatusline-usage in upstream's new tests) - src/utils/usage-fetch.ts: compat fix - cacheUsageData gained a fetchedAtMs arg (upstream sirmalloc#266); adapted the fork-only fetchFreshUsageData call site - src/utils/__tests__/usage-fetch.test.ts: compat fixes - cache/lock now hashed per CLAUDE_CONFIG_DIR (usageFilePath helper) in four fork tests; upstream fableUsage renamed to fork's weeklyFableUsage; seedUsageCache takes TokenHome - src/utils/__tests__/usage-prefetch.test.ts: compat fix - mockFetchUsageData renamed to upstream's mockFetchUsage in cleanly-merged tests - README.md: took upstream's Quick Start move (sirmalloc#715) and dropped the fork's old copy; fork-rename of its ccstatusline@ specifiers; links to fork-deleted docs (docs/USAGE.md, docs/WINDOWS.md) replaced with the fork's inline text - CONTRIBUTING.md: links to the fork-deleted docs/DEVELOPMENT.md pointed at the README Development section - docs/test-retirements.md: ledger entries for the 12 upstream test renames (sirmalloc#697, sirmalloc#698, sirmalloc#700, sirmalloc#705, sirmalloc#713, sirmalloc#760)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
When Claude Code's payload has no
rate_limitsreset time (API-key, Bedrock and Vertex logins, accounts without a five-hour window, or a failed usage fetch), Block Timer, Block Reset Timer and Block % find the current 5-hour block from the transcripts. This fixes four ways that fallback got the block wrong:~/claude (work)/.claude.Why (evidence)
{"type":"assistant","isApiErrorMessage":true,"message":{"model":"<synthetic>","stop_reason":"stop_sequence","usage":{"input_tokens":0,"output_tokens":0,...}}}. The scan only checked thatinput_tokens/output_tokensare numbers.mainreads only A, takes A's 21h gap as the boundary, never runs the wider passes, and anchors the block 2 hours early: "Block: 4hr 54m | Reset: 5m".<config dir>/projects/**/*.jsonlwith the absolute directory written in. Underclaude (work),cfg[1],brace{a},star*xorq?markit matched nothing, so Block Timer stayed at "0hr 0m" and Block Reset Timer at [Loading] for as long as that directory was in use.now < emptyUntil. After a 24h-fast clock was corrected, it hid the block for a day, even with fresh activity. A cached start in the future counted as valid (now <= start + 5h), freezing elapsed time at 0 and Reset at 5hr. The usage lock inusage-fetch.tsalready bounds its deadline against a clock set back.How
getAllTimestampsFromFileskips rows withisApiErrorMessage === true, the same check the token/context metrics and Cache Timer use. Every main-chain<synthetic>row in a sample of real transcripts carried the flag and all-zero usage (session limit, spend limit, model limit, expired login, not logged in, sleep, unreachable API). This makes Block Timer differ fromccusage blocks, which counts these zero-token rows as entries; the scan's own comment says to treat only entries with real token usage as activity.projects/**/*.jsonl, with the existingcwd: rootDir. tinyglobby escapes and normalizes the cwd itself, so Windows paths keep working./, so a directory namedback\slashstill finds no transcripts, as onmain.getCachedBlockMetricstrustsemptyUntilonly while it ends within one interval, and a cached start only once it has begun. Otherwise it rescans. (If a transcript itself holds rows stamped by the fast clock, the fresh scan still waits for them to age out; that's outside the cache.)Demo
Piped renders of Block Timer and Block Reset Timer over fake transcripts in a scratch HOME. The payload has no
rate_limits, there's no login and nothing is fetched. Times are relative to when each take was recorded.Rejected requests. Line 1: only rejected requests, so there is no block. Line 2: rejections, then real requests from just after midnight, so the block is 00:00–05:00.
Powerline
Before:
After:
Plain
Before:
After:
A transcript the first pass skips. A resumed session plus another project's session last written 10.5h ago, whose rows chain into the current block.
Powerline
Before:
After:
Plain
Before:
After:
Config directory with parentheses. The same transcript under
~/claude-work/.claudeand~/claude (work)/.claude.Powerline
Before:
After:
Plain
Before:
After:
Testing
main. The other two check that a gap no transcript fills is still a boundary, and that unparseable lines, rows without both token counts, sidechain rows and rows without a valid timestamp still don't count as activity; both pass on both.bun test: 3180 pass, 0 fail (main: 3168), Bun 1.4.2.bun run lintis clean.main; a Block Timer + Block Reset Timer line over each of the three scenarios agrees across runtimes and differs frommainas intended; the TUI opens and exits cleanly.