Skip to content

fix(usage): refetch a cached usage answer from before a 5-hour or weekly reset - #814

Open
eric-engberg wants to merge 7 commits into
sirmalloc:mainfrom
eric-engberg:fix/usage-refetch-after-reset
Open

eric-engberg wants to merge 7 commits into
sirmalloc:mainfrom
eric-engberg:fix/usage-refetch-after-reset

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

Right after a 5-hour or weekly reset, the usage widgets no longer show the previous window's numbers for up to three minutes. When Claude Code's status line data already shows a new window but the cached usage API answer is from before the reset, the next render fetches a new answer, once. Weekly Fable Usage and the other values that come only from the usage API then match the new window right away.

Why

The status line payload carries the 5-hour and weekly percentages and reset times (rate_limits.five_hour / seven_day). Weekly Fable Usage, the extra usage figures, and anything else the payload doesn't carry come only from the usage API. That answer is cached in usage.json for 180 seconds and shared by every session. The freshness check looked only at the answer's age, its login and which fields it held. So an answer cached a minute before a reset still counted as fresh after it, while the payload on the same line had already moved to the new window.

Reproduced with a fake login and a fake usage API (bun --preload / node --require, nothing fetched). The week reset 30 seconds ago, so the payload ends the week 7 days later, at 1% used. usage.json, written a minute earlier, before the reset, ends the week at the reset with Fable at 88%. The fake API now answers 3%. Built dist/ on main, piped twice, under Bun 1.4.2 and Node 26.10.0 (same output on both; the request count comes from the fake API's log):

Weekly: 1.0%  |  Weekly Reset: 6d 23hr 59m  |  Weekly Fable: 88.0%   (0 usage API requests)
Weekly: 1.0%  |  Weekly Reset: 6d 23hr 59m  |  Weekly Fable: 88.0%   (0 usage API requests)

The line contradicts itself: the week has restarted, but Fable shows last week's number, and it keeps doing so until the cache file is 180 seconds old. In use, two sessions showed the old Fable number for about 26 and 90 seconds after a weekly reset.

How

  • prefetchUsageDataIfNeeded passes the payload's 5-hour and weekly reset ends to fetchUsageData as payloadResetAt, whenever the payload has at least one.
  • fetchUsageData counts a memory or file cache as fresh only while neither window's payload end is more than 60 seconds later than the cached answer's. Both sources report the same instant: the payload in whole seconds, the API with fractions of a second. A reset moves the end by the window's whole length, 5 hours or 7 days.
  • Only a later payload end counts. A session idle since before a reset keeps showing the window of its last request, while another session may already have fetched the new one. An idle session therefore never refetches an up-to-date answer, and two sessions never trade refetches. Percentages aren't compared, for the same reason, and nothing new is stored: the decision comes from the cached answer and the payload alone.
  • The refetch goes through the existing guards:
    • During a 429 backoff, while another render's fetch is in flight, after a parse error, or without a login, nothing is fetched and the pre-reset answer is served as the stale fallback, as on main. Since fix(usage): drop stale cached windows that have already reset #736, that fallback leaves out a window whose own reset time has passed.
    • One request per reset: once a response matches the payload, later renders serve it from the cache.
    • A response that still ends the window before the payload does is cached but keeps the 30-second in-flight lock, so an API that lags behind the payload is asked at most every 30 seconds, not on every render.
  • docs/USAGE.md (usage cache) and docs/DEVELOPMENT.md (cache mechanics) describe the refetch and its limits, including that the stale fallback a blocked refetch serves leaves out windows past their reset.

Left as is:

  • A reset of a window the payload doesn't carry can't be detected this way. A Fable window ending at a different time from the weekly one would still wait out the 180-second cache. In the usage API responses on hand, Fable's weekly_scoped entry resets together with weekly_all, so a weekly reset covers it.
  • A window that the cached answer gives no end for, as the API reports for a window nothing has been used in yet, isn't compared, so it can't cause repeated refetches.
  • A reset is noticed only once Claude Code's payload shows the new window. Until then the normal 180-second lifetime applies.
  • While fetching is blocked (backoff, another fetch in flight, no login), the previous answer stays on screen as the stale fallback, as before. fix(usage): drop stale cached windows that have already reset #736 already leaves a window out of it once that window's own reset time has passed. What stays is a window whose cached end hasn't passed although the payload shows a newer one, such as after an early reset. The stale fallback itself is unchanged.

Demo

Piped renders with a fake login and a fake usage API, nothing fetched: the scenario from Why, rendered twice, two seconds apart. Before: Weekly Fable shows 88.0% on both renders with no request. After: the first render fetches once and shows 3.0%, and the second serves that from the cache without another request.

Powerline

Before:

Weekly Fable Usage just after a weekly reset in Powerline mode, before the fix

After:

Weekly Fable Usage just after a weekly reset in Powerline mode, after the fix

Plain

Before:

Weekly Fable Usage just after a weekly reset in plain mode, before the fix

After:

Weekly Fable Usage just after a weekly reset in plain mode, after the fix

Testing

  • On main as of 2.2.34 (0c94df7): it merges after conflicts next to fix(usage): show a newer usage API percent in a session left idle #767's and feat: add an Extra Usage Today widget #811's imports, test probe and docs, resolved by keeping both. bun run lint is clean, and bun test on Linux CI gives 4414 pass, 0 fail (4403 on main). The built CLI under Bun 1.4.4 and Node 26.13.0 agrees across runtimes and matches main in plain and Powerline mode, and the TUI opens and exits cleanly under both. It adds no JavaScript or Node API newer than Node 14, the build's target.
  • 11 new tests, and 5 existing usage-prefetch.test.ts assertions that now expect the reset ends in the fetch call. Run against main's code, 9 fail: the 2 refetch tests, the 30-second lock test, the new prefetch test and the 5 updated assertions. The other 7 new cases pass on main too, because main never refetches a fresh cache. They pin what this branch must keep: no refetch when the reset ends match, when the payload ends the week at most a minute later or a week earlier, or when the cached answer has no end for that window; and none during a rate-limit backoff, while another render's fetch is in flight, or without a login.
    • usage-fetch.test.ts (10 tests, through the existing subprocess probe with a stubbed API):
      • A cache from before a weekly reset is refetched once, and the next render serves the new answer with no request.
      • A cache from before a 5-hour reset is refetched.
      • The 4 no-refetch cases and the 3 blocked cases above.
      • An API still answering with the old week keeps the 30-second lock: no request inside it, and a successful retry after it.
    • usage-prefetch.test.ts (1 test): the payload's reset ends reach the fetch when Weekly Fable Usage needs the API.
  • Node: usage-fetch.test.ts and usage-prefetch.test.ts under node node_modules/vitest/vitest.mjs run on Node 26.10.0. 72 of 123 pass, against 71 of 112 on main. Every failure is a probe test whose spawned Node process can't load the .ts sources (ERR_MODULE_NOT_FOUND): the same 41 as on main, plus the 10 new probe tests, which use the same probe. The new prefetch test passes under Node.
  • runtime-check.sh on Bun 1.4.2 and Node 26.10.0:
    • The baseline line renders the same as main in plain and Powerline mode, with both runtimes agreeing. The TUI opens and exits cleanly on both.
    • The demo line with a post-reset payload, a fresh pre-reset cache and no reachable API also renders the same as main on both runtimes: the refetch fails and the cached answer stays.
  • Built dist/ under Bun 1.4.2 and Node 26.10.0, plain and Powerline, with the fake API, two renders each:
    • main: Weekly Fable: 88.0% with no request.
    • This branch: Weekly Fable: 3.0% after one request, with no request on the second render.

…kly reset

Values the statusline payload doesn't carry, such as Weekly Fable Usage
and the extra usage figures, come only from the usage API, whose answer
is cached for 180 seconds and shared by every session. When the 5-hour
or weekly window reset, Claude Code's payload already showed the new
window while the cached answer still described the old one. Fable
Weekly Usage then kept the previous week's percentage until the cache
expired, up to three minutes after the reset.

prefetchUsageDataIfNeeded now passes the payload's five_hour and
seven_day reset ends to fetchUsageData. A memory or file cache that ends
either window more than 60 seconds before the payload does was written
before that reset, so it no longer counts as fresh and is fetched again.
The two sources give the same instant to within a second, while a reset
moves the end by 5 hours or 7 days.

Only a later payload end counts. A session idle since before the reset
keeps showing the window of its last request, and must not refetch an
answer another session has already brought up to date. Percentages
aren't compared for the same reason: an idle session's stay where they
were.

The refetch keeps the existing guards. Under an active usage lock (a 429
backoff, another render's fetch in flight, a parse error) or the
no-credentials backoff, the pre-reset answer is served as the stale
fallback, as before. A response that still ends the window before the
payload does is cached but keeps the 30-second in-flight lock, so an
API that lags the payload is asked at most every 30 seconds rather than
on every render. Once a response matches the payload, later renders
serve it from the cache.

A reset of a window the payload doesn't carry, such as a Fable window
ending at a different time from the weekly one, can't be seen this way
and still waits for the cache to expire.
Upstream sirmalloc#697 and sirmalloc#747 (the reset timer's and Block Usage's at-limit hide
states) fetch the block's percent; their prefetch tests now expect the
payload's reset end this branch passes along, as fork #117 did. The
branch's own cache tests use upstream sirmalloc#266's per-profile file names.
…mer's prefetch calls

This branch passes the statusline payload's 5-hour and weekly reset ends
to fetchUsageData, so sirmalloc#768's two prefetch tests for Weekly Fable Limit
Timer, which check the exact fetchUsageData calls, expect that option too.
# Conflicts:
#	docs/USAGE.md
#	src/utils/__tests__/usage-fetch.test.ts
#	src/utils/usage-prefetch.ts
#	src/utils/usage.ts
…ir reset

Since the stale fallback leaves out windows whose own reset time has
passed, the pre-reset answer a blocked refetch falls back to no longer
shows such a window.
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.

1 participant