Repository navigation
fix(claude-status): stop showing an outdated status as current - #772
Merged
Merged
Conversation
Claude Status counted its cache as fresh while `now - fetchedAt` was under five minutes, and its failure lock as active while `now - mtime` was under 30 seconds. Neither check had a lower bound, so a timestamp ahead of the clock always passed. After a clock that ran fast is set back (an NTP correction after a VM resume, or a dual-boot machine that keeps the hardware clock in local time), the widget kept showing the cached status, "ok" through a major incident for instance, and fetched nothing until real time caught up with the stamp: for as long as the clock had been ahead. A lock written in that window held off every fetch just as long. Both checks now follow the rule the usage cache already uses: a stamp counts while it lies within the window on either side of now. One a little ahead still counts, since clocks sharing a home directory can sit slightly apart. One further ahead than the window was written under a clock since set back, so the cache is refetched and the lock ignored; a failed fetch rewrites the lock with the current time, so it heals on the next render.
While status.claude.com can't be reached (offline, a network that blocks it, or the page itself failing), Claude Status falls back to its cached status. It never checked how old that cache was, so the last status stayed on screen as current for as long as fetches kept failing: days later, a laptop that fetched during an incident still showed "Claude: major". With the history strip on, an incident still open in that cache counts as running until now, so it spread into a new six-hour block every six hours, and two days on all eight blocks showed red for hours nobody had seen. The fallback now serves the cache only while it is under an hour old, and after that shows `?`, the widget's existing state for status it doesn't have. An hour rides out a dropped connection or a short offline stretch, twelve refresh intervals, while a status that old says little about now. It also keeps the strip honest: within the hour, an open incident reaches only into the newest block, which the part already seen overlaps, so it colors the same blocks it would had it ended when last seen.
# Conflicts: # docs/USAGE.md
# Conflicts: # docs/DEVELOPMENT.md
# Conflicts: # src/utils/claude-service-status.ts
…h claim The in-flight fetch lock rechecks the cache once it holds the claim, and judges another process's claim by its age. Both compared ages directly, so a cache or claim dated hours ahead by a clock since set back counted as current: the recheck served a far-future cache without fetching, and a far-future claim left behind by a stopped process held off every fetch until real time caught up. Both now use the same window as the cache and failure lock checks.
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
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
Two fixes to how Claude Status decides whether its cached status is still current:
?, as it already does when it has no status at all. A cache dated more than an hour ahead isn't served either, since its age can't be known.Why
Clock set back. The cache counted as fresh while
now - fetchedAt < 300s, and the lock as active whilenow - mtime < 30s. Neither check had a lower bound, so any timestamp in the future passed both. This happens when NTP corrects a clock that ran fast, as after a VM resume, or on a dual-boot machine that keeps the hardware clock in local time. After such a correction, the widget kept showing the cached status and made no request until real time caught up. Reproduced through the piped CLI onmain, with the status page answered in-process and reportingmajor:okdated 2 h ahead:Claude: ok, 0 requests, for the next 2 h 5 minClaude: ok, 0 requestsThe docs say responses are "cached for about five minutes". The usage cache already treats files dated more than one lifetime ahead as stale.
Page unreachable. The fallback never looked at the cache's age. Take a cache from two days ago saying
major, with a critical incident still open, and fail every request. The built CLI onmainprintedClaude: major ▮▮▮▮▮▮▮▮on every render, under both Bun and Node. With history on, all eight six-hour blocks were red, because an open incident counts as running until now and so spread into every block nobody had observed. A 30-day-old cache printed the same. Only having no cache at all gave?.How
isWithinAge(stamp, now, maxAge)helper (|now - stamp| < maxAge) replaces the one-sided checks: memory cache, disk cache and failure lock, plus the two that [low] perf(claude-status): hold a lock while the status page fetch is in flight #635's in-flight fetch lock added (the cache recheck once the claim is held, and the age of another process's claim). A failed fetch rewrites the lock with the current time, so a lock dated in the future heals on the next render. A claim dated more than 30 seconds ahead is taken over like an expired one.serveStaleOrError, the fallback used when a fetch fails or the failure backoff holds off fetching, now also requires the cache's timestamp to lie withinSTALE_MAX_AGE(one hour) of now, through the same helper.docs/USAGE.mdnow says the widget shows the last fetched status while it is under an hour old, and?otherwise.docs/DEVELOPMENT.mdsays the stale cache is served while it is under an hour old.Left as is:
Demo
These are piped renders through the real CLI, each starting from a seeded cache or from no cache. status.claude.com is answered in-process (reporting
major, or failing every request), so nothing reaches the network. Each line shows how many requests the render made.Cache or lock dated ahead of the clock
The page reports
major, and each render starts from a cache sayingok: fetched 2 minutes ago, dated 2 h ahead, and fetched 10 minutes ago with a failure lock dated 2 h ahead. Before: all three showClaude: okwith 0 requests. After: the fresh cache still showsokwith no request; both future-dated cases make 1 request and showClaude: major.Powerline
Before:
After:
Plain
Before:
After:
Status page unreachable
History strip on. No cache, then a cache saying
majorwith a critical incident open for an hour, fetched 30 minutes ago and 2 days ago. Before: the 2-day-old cache showsClaude: majorwith all eight blocks red. After: it showsClaude: ?, like no cache, while the 30-minute-old one is still served with only the newest block red.Powerline
Before:
After:
Plain
Before:
After:
Testing
mainas 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 lintis clean, andbun testgives 3717 pass, 0 fail. The built CLI under Bun 1.4.4 and Node 26.13.0 matchesmainin plain and Powerline mode with the baseline configs, and the TUI opens and exits cleanly.bun run lintclean;bun test3717 pass, 0 fail across 196 files. Without the last commit, 2 of the earlier tests fail, because the recheck under the claim served a cache dated 2 h ahead. Three new tests cover the claim (10 s old, a few seconds ahead, 2 h ahead); without the claim's change, the 2 h case fails.bun run lint: clean.bun test: 3179 pass, 0 fail across 179 files (main: 3168 pass, 0 fail across 178). The 11 new tests are insrc/utils/__tests__/claude-service-status-cache.test.ts. Likeusage-fetch.test.ts, they run the module in a child process with a scratch HOME and a stubbedhttps.request, because the module resolves its cache paths when it loads.main's code, 6 of the 11 fail. The other 5 are controls: a fresh cache, a cache or lock a few seconds ahead, a 10-second-old lock, and a 50-minute-old cache served while the page is down.?from the unreachable page still fail.claude-service-status.test.ts,ClaudeStatus.test.tsandusage-fetch.test.ts: 52 failed, 71 passed (main, without the new file: 41 failed, 71 passed). The 48 tests inclaude-service-status.test.tsandClaudeStatus.test.tspass on both. Every failure isERR_MODULE_NOT_FOUNDfrom a child-process probe that loads the TypeScript sources, whose extensionless imports Node can't resolve: the new file's 11, andusage-fetch.test.ts's existing 41.mainin both render modes, and the TUI opens and exits cleanly. With Claude Status configs and seeded caches, both runtimes produce the same output:majorcache with the page unreachable printsClaude: ?(onmain:Claude: major ▮▮▮▮▮▮▮▮).Claude: ?after a failed fetch, and again on the next render under the failure backoff (the failed fetch is unit-tested inclaude-service-status-cache.test.ts; both renders were checked by hand under Bun and Node). Onmainit printsClaude: ok, with no request made.