Skip to content

feat(copilot): last-good disk-cache fallback for usage limits - #994

Open
Acfufu wants to merge 2 commits into
xiufengsun:mainfrom
Acfufu:feat-copilot-limits-cache-fallback
Open

Acfufu wants to merge 2 commits into
xiufengsun:mainfrom
Acfufu:feat-copilot-limits-cache-fallback

Conversation

@Acfufu

@Acfufu Acfufu commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Copilot item of #941 (Cursor landed in #985). The remaining providers (Gemini/Kimi/Kiro/Grok/Devin) will follow as separate per-provider PRs.

What

The GitHub Copilot usage-limits card previously flashed "fetch failed" (or blanked) on any timeout / 5xx from api.github.com/copilot_internal/user, until the next poll succeeded. Now every successful live read is persisted to disk, and a failed poll serves the last-good windows with stale: true + original cached_at — both UIs already render that provenance, no UI change.

  • Own cache file ~/.tokentracker/tracker/copilot-usage-limits-cache.json (never shared), atomic tmp+rename+0600 write, mirroring the codex / claude / opencode-go / cursor caches.

Red lines kept

  1. Auth errors punch through the cache — a 401/403 token rejection throws with an AUTH_EXPIRED code (message text unchanged: "GitHub Copilot token rejected. Re-authenticate via GitHub Copilot CLI/extension.") and the assembly never covers it with stale cache (opencode-go auth_error precedent).
  2. No rolled-over resets — Copilot windows carry reset_at built from the monthly quota_reset_date; cached windows whose reset has already passed are dropped and never served as current (windows are per-snapshot, so a partially-rolled entry serves only the surviving window).
  3. Bounded staleness — whole entries expire after 7 days (COPILOT_LIMITS_CACHE_MAX_AGE_MS, codex precedent).
  4. Identity partition — the entry is stamped with the non-secret GitHub login stored alongside the OAuth token: the plaintext hosts.json/apps.json entry's user field, or the auth.db row's user_login (zcode login-identity precedent). The identity always comes from the same auth-source resolution that produced the token, so it can never point at a different account than the one being served. Reads require stored identity == current identity; writes without an identity are refused. An account switch therefore surfaces the live error instead of another account's bars.

Implementation notes

  • Token resolution gained readCopilotAuth/readCopilotAuthAsync (token + identity in one pass); the public readCopilotOauthToken[Async] shape is unchanged (still returns the bare token string), and status.js keeps working untouched.
  • fetchCopilotLimits now returns an internal cache_identity (and auth_error: true on token rejection); both are stripped in the aggregate before the payload is served — payload shape unchanged, otel probe fields preserved.
  • When the outer provider-timeout race wins (the most common failure mode this cache exists for), the slot's error object has lost the fetcher's identity field, so the failure-only path re-derives it from the local auth store (resolveCopilotCacheIdentity, same source priority as the token) and the fallback still lands.
  • A cached overlay spreads the live slot object first and lets the cached entry win only the limit fields, so the otel probe fields survive a stale serve.
  • The disk write sits behind the same cache-generation check as opencode-go/cursor, so an in-flight fetch orphaned by a credential invalidation cannot repopulate the disk cache.
  • Shared-helper extraction across the existing hand-copied atomic-write pipelines remains deliberately out of scope (per the per-provider direct-port decision recorded on [feature]: 将 last-good 磁盘缓存兜底铺到其余网络型 provider——Cursor/Gemini/Kimi/Kiro/Grok/Copilot/Devin (roll the stale disk-cache fallback out to the remaining network-API limit providers) #941).

Tests

New test/copilot-limits-cache-fallback.test.js — 8 aggregate-level pinning tests through getUsageLimits:

  • live read stamps stale:false/cached_at, writes the identity-stamped disk entry (on-disk schema pinned), later failure serves it stale with provenance.source: "disk-cache", preserved cached_at, and intact otel fields;
  • 401 token rejection is never hidden behind the on-disk cache; internal markers never reach the payload;
  • provider-timeout race still lands the stale fallback (identity re-derivation path);
  • account switch refuses the previous account's cached entry;
  • seeded window with passed reset_at is dropped; a partially-rolled entry serves only the surviving window;
  • seeded entry beyond the 7-day max-age is ignored;
  • an auth source with no readable login never writes a cache entry.

The fixture uses the plaintext hosts.json path (token + non-secret user login), so the suite runs on every engine with no sqlite/keychain dependency; the auth.db identity plumbing is pinned by the existing test/copilot-auth-db.test.js plus a direct readCopilotAuth assertion.

Local: full root suite green (npm test, node v24, 4609 tests; the two load-sensitive flakes seen in a saturated full run — owner-proven trial recheck timeout, reset-bank race — pass in isolated reruns 4/4, known host-specific, untouched by this change); targeted: copilot-auth-db 15, new 8, usage-limits 229.

Summary by CodeRabbit

  • Bug Fixes
    • Copilot usage information can remain available during temporary provider outages by showing a recent successful result for the same account.
    • Outdated quota windows are excluded, and cached information isn’t reused after an account change or an authentication error.
    • Plan information continues to reflect the available Copilot usage result.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 8 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 60e3527 and 233d6f4.

Files selected for processing: 1
  • src/lib/usage-limits.js
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2af6ec0 and 60e3527.

Files selected for processing: 2
  • src/lib/usage-limits.js
  • test/copilot-limits-cache-fallback.test.js

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 4 remain after this review.

Walkthrough

Copilot auth readers now return an account identity with the token. Copilot quota results use identity-partitioned disk caching, with stale fallback for eligible provider failures and safeguards for authentication errors, expired windows, and invalid cache timestamps.

Changes: Copilot usage cache

Layer / File(s) Summary
Resolve Copilot identity
src/lib/usage-limits.js
Plaintext and database auth resolution now return the token with the account login. Copilot limit requests use the combined auth result, and 401/403 errors receive an authentication-expired marker.
Persist and use cached quota windows
src/lib/usage-limits.js, test/copilot-limits-cache-fallback.test.js
Successful quota results are cached by identity. Eligible failures use matching cached windows after expired windows are removed. Tests cover persistence, fallback, authentication errors, account changes, window expiry, cache age, and missing identity.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant fetchCopilotLimits
  participant readCopilotAuthAsync
  participant CopilotAPI
  participant CopilotDiskCache
  participant usageAggregation
  fetchCopilotLimits->>readCopilotAuthAsync: Resolve token and identity
  readCopilotAuthAsync-->>fetchCopilotLimits: Return token and identity
  fetchCopilotLimits->>CopilotAPI: Request quota windows
  CopilotAPI-->>fetchCopilotLimits: Return quota result
  alt Successful quota result
    fetchCopilotLimits->>CopilotDiskCache: Store windows for identity
  else Eligible provider failure
    fetchCopilotLimits->>CopilotDiskCache: Read matching cached windows
    CopilotDiskCache-->>fetchCopilotLimits: Return unexpired windows
  end
  fetchCopilotLimits->>usageAggregation: Provide result without internal markers
Loading

Possibly related issues

  • Issue 941: The issue lists Copilot as part of the last-good disk-cache fallback rollout. This change adds Copilot stale fallback and cache safeguards.

Suggested labels: cli, tests

Merge Risk: Low · previous review 60e35

Copilot cache fallback can briefly stall the embedded server when it re-reads account identity. Address the blocking path before merge if responsive polling is required; otherwise this is a bounded risk to accept.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 34.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: adding a last-good disk-cache fallback for Copilot usage limits.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 34.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 23 functions across 2 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread src/lib/usage-limits.js Resolved

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant