Skip to content

Fix: quota indicator (5h/wk %) gets stuck showing a stale value - #24

Merged
Carl723000 merged 1 commit into
ClaudeCodeUsage:mainfrom
nickearnshaw:nickearnshaw/fix-stale-quota-indicator
Jun 15, 2026
Merged

Carl723000 merged 1 commit into
ClaudeCodeUsage:mainfrom
nickearnshaw:nickearnshaw/fix-stale-quota-indicator

Conversation

@nickearnshaw

Copy link
Copy Markdown
Contributor

Problem

The right-hand status-bar quota indicator (5h:… wk:…) can get stuck showing an old percentage indefinitely. A user reported a 5h:100% that was carried over from a session hours earlier and stayed at 100% even after they upgraded their plan.

Root cause

Two behaviors compound:

  1. Indefinite stale fallback — when the OAuth usage fetch fails (expired/rotated credentials, 401/403, 429 cooldown, or offline), maybeFetchUsageLimits() returns the last successful response with no upper bound on age.
  2. No reset awareness — updateQuota() rendered utilization directly, ignoring each window's resets_at.

So once a fetch returned e.g. 5h:100% and later fetches kept failing silently, the extension kept displaying 100% long after that 5-hour window had rolled over.

Fix

A window's utilization is a point-in-time snapshot only valid until its resets_at. This PR adds liveWindows() in statusBar.ts, which strips out any window whose reset time has already passed before display:

  • A rolled-over window is no longer rendered with its stale percentage.
  • If nothing current remains, the indicator hides rather than asserting a number we can no longer trust.
  • Windows with an unparseable resets_at are kept (we don't hide data we can't reason about).
  • Genuine real-time values (a real 5h:100% with a future reset) are unaffected.

This bounds staleness to each window's reset period (≤5h for 5h, ≤1 week for wk) instead of forever.

Testing

  • npm run compile passes clean.

🤖 Generated with Claude Code

The right-hand quota indicator (5h/wk %) could get stuck showing an old
value indefinitely. When the OAuth usage fetch fails (expired/rotated
credentials, 401/403, 429 cooldown, or offline), maybeFetchUsageLimits()
falls back to the last successful response with no upper bound, and
updateQuota() rendered utilization directly without checking each window's
resets_at. So a "5h:100%" snapshot kept displaying long after that window
rolled over — surviving for hours and even across a plan upgrade.

A window's utilization is only valid until its resets_at. Add
liveWindows() to strip any window whose reset time has passed before
display; if nothing current remains, hide the indicator rather than
assert a number we can no longer trust. Genuine real-time values (a real
5h:100% with a future reset) are unaffected, and the staleness is now
bounded to each window's reset period.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Carl723000

Copy link
Copy Markdown
Collaborator

Great catch and easy fix, @nickearnshaw. The "snapshot is only valid until resets_at" framing is spot on – I've also had problems with a stale 5h:100% lingering after the window rolled over.

I'll fold liveWindows() into the next patch (v2.0.2), adapting it to the v2.0.1 progress-bar tooltip. I'll credit you in the CHANGELOG.

Many thanks!

Carl723000 added a commit that referenced this pull request Jun 11, 2026
…charts (#29)

* fix(quota): drop expired windows + activity-aware refresh + coalesce

Three related status-bar refresh fixes (problem reported: quota stuck at
a stale 100% after the 5h window reset; status bar not updating promptly
during ultracode runs).

1. Stale quota after reset (adapted from PR #24 by @nickearnshaw):
   add liveWindows() in statusBar.ts. A window's utilization is only valid
   until its resets_at; once that passes the cached value is stale. We now
   drop expired windows before display and hide the indicator if nothing
   current remains, rather than rendering a value we can no longer trust.
   Bounds staleness to each window's period (<=5h / <=1 week) instead of
   forever.

2. Activity-aware cadence: track the last .jsonl write (lastActivityAt,
   updated by the recursive fs.watch). While active (a write in the last
   60s) the polling timer ticks every ~15s and the quota cache shrinks to
   20s, so high-consumption ultracode/workflow runs update promptly. When
   idle, both fall back to the user's interval (min 30s) / 120s. The auto
   refresh timer became self-rescheduling to support this.

3. Coalesce overlapping refreshes: instead of dropping a trigger that
   arrives mid-load (which starved updates while many sub-agent files were
   being written rapidly), set a pendingRefresh flag and run exactly one
   more refresh after the current finishes.

* feat(dashboard): gridded stacked cost-composition chart for daily & monthly

The daily (This Month tab) and monthly (All Time tab) charts previously
showed a flat single-colour cost bar with no scale. They now:

- Render inside the gridded hc-wrap layout: a Y-axis (max / max-2 / 0) and
  two dashed reference lines, matching the hourly chart. This gives the
  "three reference levels" the user asked for.
- Render the cost metric as a stacked composition — input / output /
  cache-write / cache-read, same colours as the summary's Cost Composition
  (blue / orange / purple / green). Each segment's height is its share of
  that day's/month's cost, so a glance shows both total spend and where it
  went.

The metric switcher still works: switching to a token/message metric
re-renders single-colour bars (updateMainChart now rebuilds bar content
per metric from data attributes rather than only resizing). Cost segments
are pointer-events:none so clicking a bar still drills down to the
hourly / daily detail.

Implementation notes:
- Shared renderMainCostChart() backs both renderDailyChart and
  renderAllTimeChart.
- Bars carry the cost breakdown as data-cost-* attributes so the client
  can rebuild the stack on metric switch without a round-trip.
- The fixed-height .chart-container wrapper was removed for these two
  charts (hc-wrap is self-contained with its own scroll), avoiding a
  double scrollbar.

* docs: fix CHANGELOG link casing + refresh all language READMEs to v2

- README.md: the changelog links pointed to `changelog.md` (lowercase)
  but the file is `CHANGELOG.md` — broken on case-sensitive GitHub.
  Fixed both occurrences. Dropped the stale "v2 translations in 2.0.1"
  note.

- The five language READMEs were still v1-era content: old feature lists,
  old upstream jack21 screenshot links, inline v1 changelogs. Rewritten
  to v2:
  - en / zh-TW / ja / ko: concise editions — slogan, v2 screenshots
    (images/v2-*-en.png), feature list, install, key settings,
    troubleshooting (incl. cleanupPeriodDays + sub-agent note), and a
    pointer to the main README for full detail.
  - zh-CN: full translation of README.md (Chinese is the highest-traffic
    locale for this repo's README).

All language editions now carry the "coach in your status bar"
positioning and the "issues/PRs welcome" note.

* docs+chore: refresh CLAUDE.md, add CLAUDE.local.md/CONTRIBUTING + issue templates

- CLAUDE.md rewritten to the v2 architecture (advisor / claudeApiClient /
  adviceDemoSample modules, adaptive refresh, curl-based OAuth, automated
  release workflow) and the project's positioning + conventions.
- CLAUDE.local.md added for per-maintainer personal notes (gitignored and
  .vscodeignored — not committed, not packaged). Documents the local
  environment, working preferences, collaborators and roadmap.
- CONTRIBUTING.md added: scope, dev setup, PR checklist, the planned
  node:test direction (issue #25), and the automated release flow.
- GitHub issue templates (bug report / feature request) added, both noting
  the project scope so requests land in-scope.
- CHANGELOG [2.0.2] entry covering the quota / refresh / chart fixes and
  the docs refresh.

* feat: stacked hourly cost chart, gridded drill-downs, scroll-to-detail, per-workspace session

Follow-ups from v2.0.2 RC testing:

- Today's hourly cost chart is now a stacked composition (input / output /
  cache-write / cache-read), matching the daily and monthly charts.

- The drill-down charts (daily under a month, hourly under a day) were
  still flat single-colour bars with no scale and day-number-only labels.
  They now use the same gridded layout (Y-axis + two reference lines) and
  stacked cost composition via a shared `griddedChart` client helper. The
  daily drill-down x-axis shows month/day instead of a bare day number.

- Expanding a detail now scrolls it into view. Clicking a bar at the top
  of a tab previously expanded a detail row far down the table that users
  never noticed.

- "Current session" in the status bar is now the single most-recent
  conversation scoped to this window's workspace, instead of a global
  5-hour aggregate across all projects. Different workspaces now show
  their own current-session cost; returns nothing if idle >5h so a stale
  session doesn't show as current.

* fix: per-metric chart Y-axis, quota shows 0% after reset, status bar 'this project today'

Final v2.0.2 RC fixes from testing:

- Chart Y-axis now updates per metric. The metric switcher left the
  reference-line labels stuck on the cost metric's units because the
  container holds two hc-yaxis elements (main chart + token-composition
  chart) — the 6-value query failed its `=== 3` guard. Now scoped to the
  main chart's own wrap.

- Quota no longer vanishes when a window resets. An expired window is
  rolled forward to 0% with its reset time advanced by whole periods
  (5h / 1 week), instead of being hidden. A forced refetch (bypassing the
  cache when an expired window is detected) replaces the 0% estimate with
  the real new-window value shortly after.

- Status bar secondary number changed from "current session" to
  "this project today" — today's cost for the current workspace. Both
  numbers are now midnight-stable (the old session figure vanished after
  5h idle and was identical across all windows because it aggregated the
  last 5h globally). New i18n key `workspaceToday` (6 languages); icon is
  $(folder). The per-workspace current-session figure is still computed
  for the webview. Adds ClaudeDataLoader.filterByWorkspace().

* fix: guard refresh-timer generations and bound quota roll-forward (code review)

Two correctness fixes from a self code-review of the v2.0.2 branch:

- startAutoRefresh's self-rescheduling timer could spawn duplicate chains:
  if it ran again (e.g. on a config change) while a tick's
  refreshData().finally(tick) was mid-flight, the orphaned chain would
  keep its own setTimeout alive alongside the new one, multiplying the
  refresh cadence over time. A generation token now stops any superseded
  chain.

- liveWindows roll-to-0% (so a reset window doesn't vanish) is now bounded:
  a window that expired more than ~2 periods ago is dropped instead of
  shown at a fabricated 0%. This avoids implying "full quota available"
  when the OAuth fetch has actually been failing for a long time (expired
  creds, offline). A genuine recent reset still shows 0% and is corrected
  by the forced refetch within a TTL.

* feat: Fable 5 pricing, stable workspace-today, session titles, sub-agent session attribution

Batch of fixes from v2.0.2 RC round 3 + new-model support:

Pricing
- Claude Fable 5 / Mythos 5 added ($10/$50, cache write $12.50, cache
  read $1 per MTok — verified 2026-06-09 against the official pricing
  page). Family fallback maps unknown fable/mythos snapshots to this tier.
- getModelPricing now strips a trailing "[1m]" long-context marker before
  lookup (1M-context variants bill at standard rates). This also fixes
  pricing for users whose proxy config pins e.g. "deepseek-v4-pro[1m]".

Status bar
- "This project" no longer disappears at $0: the figure shows whenever a
  workspace is open, including $0.00. Hiding at zero read as a bug.
- filterByWorkspace now also matches records that only carry the
  dash-encoded project directory name (records without a cwd field):
  the workspace path is encoded the same way Claude Code does
  (D:\Jiaming\My_Proj → d--Jiaming-My-Proj) and compared directly.
  Previously such records counted in the global total but never in the
  per-project figure — part of the "project sums don't add up" report.

Session attribution (pre-v2.1 hardening)
- parseSessionInfo now resolves files nested under a session directory
  (subagents/workflows/*/agent-*.jsonl) to their parent session id and
  real project directory. They previously got a 'wf_xxx' pseudo-project
  and 'agent-xxx' session ids, fragmenting Sessions/Projects aggregation
  — the other part of the mismatch report, and groundwork for the v2.1
  workflow attribution.

Sessions tab
- New sortable "Session" column showing the conversation title (the name
  `claude --resume` shows, harvested from each session's summary log
  line), falling back to the session id. Same-project sessions are now
  distinguishable. i18n key sessionTitle added for all six languages.

Drill-down charts
- Removed the fixed-height chart-container wrapper around the gridded
  drill-down charts (double scrollbar).
- Date labels/tooltips parse 'YYYY-MM-DD' textually instead of
  new Date(...) UTC parsing, which shifted labels a day back in
  negative-UTC timezones.

Cleanup
- getCurrentSessionData reuses filterByWorkspace (drops duplicate norm).

* docs: V2.1 workflow-attribution implementation spec

Full implementation spec for v2.1.0 at docs/V2.1-WORKFLOW-SPEC.md,
written to be handed to a fresh Claude Code session for end-to-end
implementation. Covers: verified on-disk data formats for workflow /
sub-agent logs, the five phases (advice reliability fix, loader data
model, Workflows tab, thinking share + quota guard, per-skill stats,
advice upgrade), acceptance criteria per phase, conventions, and real
verification data locations. docs/ excluded from the .vsix.

* fix: session-home attribution, real session titles, user-typed message count, startup visibility

Root-caused with on-disk diagnostics this round:

Workspace attribution ("This project" undercount)
- A single conversation's per-record cwd wanders as work moves between
  folders (observed: one session split 10/71 across two cwds), so
  cwd-based matching fragmented the per-project figure ($9.51 of $36.78).
  Records now carry _projectDirEncoded — the encoded project directory
  the session file lives in (= where the session was started) — and
  filterByWorkspace matches that first, attributing whole conversations
  to their workspace. cwd matching remains as a secondary net.
  Verified: workspace-today now equals global-today when all of today's
  work happened in one workspace.

Session titles
- Titles were showing raw uuids: current Claude Code stores titles as
  `ai-title` / `custom-title` jsonl lines, not the legacy `summary` type.
  Harvest both (custom beats AI; summary kept for old logs). Verified
  against real data — custom titles like 【CC usage插件】… now display.

Messages = what the user typed
- messageCount previously counted API calls (one conversation showed 33
  "messages" for 5 actual prompts). The loader now synthesizes zero-usage
  marker records for genuine user prompts (excluding meta/command lines,
  sidechain dispatches, and anything in sub-agent logs) and messageCount
  counts only those. Token figures are unchanged. Sessions with spend but
  no typed prompt in scope remain listed.

Startup visibility (also reported on GitHub: "usage not showing the
first time I open VS Code")
- The cost item rendered empty text until the first full refresh, which
  was blocked behind an awaited OAuth quota fetch (curl; seconds or a
  failure on a cold network). The status bar now shows a loading state
  from t=0, the quota fetch is fire-and-forget so local figures land
  immediately, and a failed first quota fetch retries up to 3× with
  backoff instead of waiting for the next tick.

Cadence
- Active refresh tick 15s → 8s, active quota TTL 20s → 15s (Fable 5
  consumption moves fast enough that 15s read as laggy).

Community standards
- PULL_REQUEST_TEMPLATE.md (structured for fast review and future
  automated triage), question.yml issue template.

Docs
- docs/V2.2-AUTOMATION-SPEC.md: human-gated AI issue/PR handling spec
  (mention-driven claude-code-action + label/draft-queue triage where
  merging a triage PR is the explicit authorization to post).

* fix: quota recovers without restart, gentler /usage cadence, exclude command echoes from message count

Two issues from RC testing:

Quota "only comes back after I restart VS Code"
- Root cause: when the in-memory OAuth token expired and our own refresh
  failed, getValidCredentials never re-read ~/.claude/.credentials.json —
  which Claude Code itself keeps refreshing. So a stale cached token stuck
  until the extension host restarted. Now an expired in-memory token
  triggers a disk re-read first (Claude Code has very likely already
  rotated it), falling back to our refresh only if the disk copy is also
  expired.
- The 429 cool-down was a flat 5 minutes, so a single rate-limit looked
  like a multi-minute outage. Cut to 60 s.
- The previous round's aggressive cadence likely caused those 429s:
  quota TTL is now 60 s active / 120 s idle (was 15 s) and the cold-start
  retry is a single attempt, not a storm. Local cost still refreshes
  every ~8 s via the fs watcher — only the slow-moving quota % is
  throttled, which is what keeps /usage under its rate limit.

Message count inflated by system markers
- "Messages" counted Claude Code's own user-line markers: interruption
  notices ([Request interrupted by user]) and slash-command echoes
  (<command-name>/model…, <local-command-stdout>…). Excluded via
  isSyntheticUserText. Example session: 106 → 86 (matches the genuine
  typed-prompt count).

* docs: V2.2 features spec (settings panel, quota warning, model-fit costing, exports)

* docs: complete the [2.0.2] CHANGELOG entry for release
@Carl723000 Carl723000 closed this Jun 11, 2026
@Carl723000

Copy link
Copy Markdown
Collaborator

Thank you for your contribution! Your changes have been included in version 2.0.2.

@Carl723000 Carl723000 reopened this Jun 15, 2026
@Carl723000
Carl723000 merged commit 134dbd5 into ClaudeCodeUsage:main Jun 15, 2026
1 check passed
@Carl723000 Carl723000 added the skip-changelog Exclude from auto-generated release notes label Jun 15, 2026
Carl723000 added a commit that referenced this pull request Jun 26, 2026
- Upstream moved to an organization: all upstream PR/issue URLs now point to
  ClaudeCodeUsage/ClaudeCodeUsage (numbers preserved on transfer) across the
  README, the translated READMEs, CONTRIBUTING and the CHANGELOG. The original
  author (@jack21) stays credited by name.
- Credits refreshed beyond the 2.0 baseline: added @ScherbakovAl (PR #31, the
  original context indicator + showCost) and @nickearnshaw's PR #24 (quota
  rollover), plus the closed issues (#25 test seed, #31 context detection).
- Reconciled the docs with the final 2.1 behaviour: AI advice/Optimizer are
  API-only (the keyless subscription backend is documented as prototyped-but-
  not-shipped), the context indicator and Optimizer are experimental/off, the
  Optimizer returns plain text, settings live in the dashboard, and the header
  was trimmed. Added CHANGELOG Changed/Fixed bullets for the header rework,
  plain-text optimizer, redesigned cards and the workspace-switch quota fix.

Docs only. Compile clean; 11/11 node:test green.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Exclude from auto-generated release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants