tokentracker-cli 1.5.13 → 1.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-Ct0vfdYb.js → AchievementBadge-CwjI28oT.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-dfoRxqOv.js → AchievementsPage-Dq544fjX.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DHFyssU0.js → AchievementsSection-DufV6nod.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-ChJYZMSq.js → BadgeDetailModal-vw8xydKu.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-C_qaJy1-.js → BotAnimated-De0anwsh.js} +1 -1
  6. package/dashboard/dist/assets/{Card-BhK2nmOu.js → Card-CuSeWBDM.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-Ele1kkut.js → CloudCheckoutPage-CxFcOwAj.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-C5DMqcoV.js → CloudPage-Beb7_JLW.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-C_hxoIwL.js → CommandPalette-PsUq2iRS.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-BSTqBIEU.js → CommunityStatsModal-b54WaPbK.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-DI4QK5EV.js → ConfirmModal-_NdunIh5.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-zK1rzS_8.js → Controls-5Is6MWln.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-BdDnmc60.js → DashboardPage-C0n0qC7h.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-DqwgxpNF.js → DateRangePopover-C3WfGy-0.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-C0fQKKuI.js → DevicePage-Vn6k-mGd.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-C_cbyHpb.js → DialogClose-CXttnna-.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-C3KGos_G.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-BcBpd--_.js → DialogTitle-B0cNIo0E.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-Dw9gkExE.js → ElectricLogo-DC3XSfIu.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Ci69gBGl.js → FadeIn-H7S3TMHi.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-pb03c0kb.js → HeaderGithubStar-xgbIo5OP.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-Cqd0D789.js → HoverTooltip-CpzM5Dju.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-qILbvOJn.js → IpCheckPage-BdV7S4dY.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-CCIwaAeu.js → LandingPage-D9f28ryO.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BwzABmKU.js → LeaderboardPage--WUFpkBI.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-7kdzfw8r.js → LeaderboardProBadge-CEvrQ75Y.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-uMnN-IGe.js → LeaderboardProfileModal-Bn0HFr5h.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CQNc4MES.js → LeaderboardProfilePage-DJ7fQI68.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-CJ1LImRm.js → LimitsPage-7tPDheuX.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-Dm0l_F0I.js → LocalOnlyNotice-DlGbVQS8.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard--tUiBVtr.js → LoginCard-BZZmzUGh.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-B1-5fFMY.js → LoginModal-BT5_bIxU.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-BYl6NnZ8.js → LoginPage-DMjDUPPi.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DJNGJZrR.js → PetPage-Dk3j6ITn.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-aRAdIkTs.js → PopoverPopup-pugRu3RC.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-uiA-KBe2.js → ProviderIcon-CtmL1CmY.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BZT353_T.js → ResetPasswordPage-BB6U7t8P.js} +1 -1
  38. package/dashboard/dist/assets/{Select-CPC04j0g.js → Select-7w1su8lw.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-D1vJVrFO.js → SelectItemText-CaEUmWkj.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-FL3EDqmb.js → SelfHostPage-DctWoF-A.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-BohSSNai.js → ServiceStatusPage-Dg3ljEXC.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-Cf_HASvt.js → SessionsPage-B1dvW-be.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-B4ztKHnj.js → SettingsPage-DuXSX1R2.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-BFiQkY7O.js → SkillsPage-CvXQFgc9.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BHVdTJAP.js → TokenGalaxy-DKC8Rk_Y.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-BMtQyFn6.js → TooltipPopup-Dp9EpHLG.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-DN-g3CNm.js → TrendMonitor-DWnlBn3H.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-Bv32yu8M.js → WidgetsPage-GECNmb1Q.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-HVTOehNb.js → WrappedPage-Dq_1lOnb.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-DnYJww7c.js → __vite-browser-external-CkF0pdTJ.js} +2 -2
  51. package/dashboard/dist/assets/{agent-logos-BOwAJQqL.js → agent-logos-Dqe3Vrah.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-left-CeXuUuES.js → arrow-left-Cr_2QnDu.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-right-DOdBG1FZ.js → arrow-right-BsK2sJgt.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-Dponssa4.js → arrow-up-right-DkWMGqXo.js} +1 -1
  55. package/dashboard/dist/assets/{calendar-days-CsxE6ot0.js → calendar-days-BB9VV438.js} +1 -1
  56. package/dashboard/dist/assets/{check-BroNG_vu.js → check-bpSCJZWh.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-down-C5ZpGeO1.js → chevron-down-CtECtDmx.js} +1 -1
  58. package/dashboard/dist/assets/{cloud-checkout-BtS-hZTc.js → cloud-checkout-C4PrzKhN.js} +1 -1
  59. package/dashboard/dist/assets/{composite-fgJbx88k.js → composite-BHisG82k.js} +1 -1
  60. package/dashboard/dist/assets/{copy-CHBuwNUs.js → copy-Dmxzd-Sw.js} +1 -1
  61. package/dashboard/dist/assets/{download-EKd2MHkv.js → download-CkdQzw2w.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-GkSo9C78.js → external-link-C971FjvC.js} +1 -1
  63. package/dashboard/dist/assets/{flame-Cg96qJrE.js → flame-mKbp7kxw.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-ChG2_n4h.js → format-tokens-C35FrJnH.js} +1 -1
  65. package/dashboard/dist/assets/{gift-B3QDbWPE.js → gift-BX2lSOmc.js} +1 -1
  66. package/dashboard/dist/assets/{icons-BEAyMjau.js → icons-ov92u52T.js} +1 -1
  67. package/dashboard/dist/assets/{index-B0kZThUB.js → index-9OCL-BL8.js} +1 -1
  68. package/dashboard/dist/assets/{index-DFcQyAnr.js → index-Bgm38oHe.js} +1 -1
  69. package/dashboard/dist/assets/{index-Cuc8ZXgl.js → index-Ca5xkNrK.js} +1 -1
  70. package/dashboard/dist/assets/{index-B9a1fK8I.js → index-DF03nIHb.js} +1 -1
  71. package/dashboard/dist/assets/{info-796bNttV.js → info-B-f9hWcw.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-LYRIPLwz.js → link-2-l1wuezTo.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-DpKC9Uel.js → loader-circle-Lmjrs8XX.js} +1 -1
  74. package/dashboard/dist/assets/{local-usage-transfer-Ctbuc6U4.js → local-usage-transfer-Ds6cF41d.js} +1 -1
  75. package/dashboard/dist/assets/{modifiers.esm-C5YTeNW1.js → modifiers.esm-tVlltJXO.js} +1 -1
  76. package/dashboard/dist/assets/{monitor-smartphone-DJUCfEvG.js → monitor-smartphone-XRiYiY8u.js} +1 -1
  77. package/dashboard/dist/assets/{play-LC-lnw6w.js → play-DaD9EMjk.js} +1 -1
  78. package/dashboard/dist/assets/{plus-CDF0S5xd.js → plus-D30fbsKQ.js} +1 -1
  79. package/dashboard/dist/assets/{provider-display-CkITUZgR.js → provider-display-CKdhBOwz.js} +1 -1
  80. package/dashboard/dist/assets/{react-BIpM-G7K.js → react-BY0y0K4x.js} +1 -1
  81. package/dashboard/dist/assets/{safePolygon-C3Ea423q.js → safePolygon-CSG_P7rG.js} +1 -1
  82. package/dashboard/dist/assets/{search-C-tN1mTM.js → search-Kiq8wVHA.js} +1 -1
  83. package/dashboard/dist/assets/{server-DJ4gq_fT.js → server-CjovGBGz.js} +1 -1
  84. package/dashboard/dist/assets/{shield-check-f75hry9N.js → shield-check-DF9MmMCq.js} +1 -1
  85. package/dashboard/dist/assets/{skills-api-CZ_vfnRL.js → skills-api-CEzr6p92.js} +1 -1
  86. package/dashboard/dist/assets/{sortable.esm-mLJSvq3M.js → sortable.esm-D7jd9mE_.js} +1 -1
  87. package/dashboard/dist/assets/{store-CZV1KXw2.js → store-D9_jo_do.js} +1 -1
  88. package/dashboard/dist/assets/{terminal-B9vOwCl9.js → terminal-CLOlvzR-.js} +1 -1
  89. package/dashboard/dist/assets/{trash-BN9GbynA.js → trash-DIe0fsDD.js} +1 -1
  90. package/dashboard/dist/assets/{use-community-stats-Bc2d8tqa.js → use-community-stats-gDkdiRBz.js} +1 -1
  91. package/dashboard/dist/assets/{use-latest-request-guard-_0H4Rk58.js → use-latest-request-guard-tXGuKvua.js} +1 -1
  92. package/dashboard/dist/assets/{use-limits-display-prefs-Dln-Rxps.js → use-limits-display-prefs-CCuLFy00.js} +1 -1
  93. package/dashboard/dist/assets/{use-native-settings-CpkcGzXp.js → use-native-settings-hgYqaiR7.js} +1 -1
  94. package/dashboard/dist/assets/{use-ordered-list-DpGA5U4S.js → use-ordered-list-D6-YzYJI.js} +1 -1
  95. package/dashboard/dist/assets/use-reduced-motion-DfbDxs5a.js +1 -0
  96. package/dashboard/dist/assets/{use-transform-JXJt1W74.js → use-transform-BEiXvAuE.js} +1 -1
  97. package/dashboard/dist/assets/{use-usage-limits-BK_H1-b7.js → use-usage-limits-BlwtyPtT.js} +1 -1
  98. package/dashboard/dist/assets/{useCurrency-ilbtKs16.js → useCurrency-CwCGJLHb.js} +1 -1
  99. package/dashboard/dist/assets/{usePositioner-M35LtCGR.js → usePositioner-BqHtRIrE.js} +1 -1
  100. package/dashboard/dist/assets/{useTokenFormat-BtTQjv7z.js → useTokenFormat-BUNBUX-x.js} +1 -1
  101. package/dashboard/dist/assets/{zap-5xemt9EH.js → zap-C0RFs2qQ.js} +1 -1
  102. package/dashboard/dist/index.html +1 -1
  103. package/dashboard/dist/ip-check.html +1 -1
  104. package/dashboard/dist/leaderboard.html +1 -1
  105. package/dashboard/dist/share.html +1 -1
  106. package/package.json +1 -1
  107. package/src/commands/sync.js +2 -2
  108. package/src/lib/limits-cache.js +475 -0
  109. package/src/lib/pricing/curated-overrides.json +24 -0
  110. package/src/lib/pricing/matcher.js +4 -1
  111. package/src/lib/pricing/seed-snapshot.json +1 -1
  112. package/src/lib/rollout.js +261 -102
  113. package/src/lib/usage-limits.js +370 -1063
  114. package/dashboard/dist/assets/DialogDescription--_wg9JdV.js +0 -1
  115. package/dashboard/dist/assets/use-reduced-motion-Cy21hg3t.js +0 -1
@@ -29,6 +29,12 @@ const {
29
29
  fetchCursorSandUsageStatus,
30
30
  } = require("./cursor-config");
31
31
  const { fetchGrokLimits, grokTokenSubject, loadGrokAuthEntry } = require("./grok-limits");
32
+ const {
33
+ createLimitsCache,
34
+ parseTimeMs,
35
+ readCacheJson,
36
+ withLimitsCacheFallback,
37
+ } = require("./limits-cache");
32
38
  const { fetchZcodeLimits } = require("./zcode-limits");
33
39
  const { fetchOpencodeGoLimits } = require("./opencode-go-limits");
34
40
  const { fetchCommandcodeLimits } = require("./commandcode-limits");
@@ -1809,83 +1815,18 @@ function readKiroCreditsSummary({ home = os.homedir() } = {}) {
1809
1815
  // Kiro limits disk cache — last good windows parsed from the kiro-cli panel.
1810
1816
  // ─────────────────────────────────────────────────────────────────────────────
1811
1817
 
1812
- function resolveKiroLimitsCachePath({ home } = {}) {
1813
- return path.join(home || os.homedir(), ".tokentracker", "tracker", KIRO_LIMITS_CACHE_FILE);
1814
- }
1815
-
1816
- function hasKiroWindow(limits) {
1817
- return Boolean(limits?.primary_window || limits?.secondary_window);
1818
- }
1819
-
1820
- // Kiro windows carry `reset_at` as an ISO string (the plan's monthly reset date,
1821
- // the bonus credits' own expiry). A window whose reset has already passed is
1822
- // stale data, not a usable fallback, so drop it. Windows without a reset stamp
1823
- // are kept — the overall cached_at max-age gate already bounds them.
1824
- function isKiroCacheWindowUsable(window, { nowMs } = {}) {
1825
- if (!window || typeof window !== "object") return false;
1826
- const resetAtMs = parseTimeMs(window.reset_at);
1827
- if (resetAtMs === null) return true;
1828
- return resetAtMs > nowMs;
1829
- }
1830
-
1831
- function normalizeKiroCachedLimits(
1832
- raw,
1833
- { nowMs = Date.now(), maxAgeMs = KIRO_LIMITS_CACHE_MAX_AGE_MS } = {},
1834
- ) {
1835
- const cachedAtMs = parseTimeMs(raw?.cached_at);
1836
- if (!Number.isFinite(cachedAtMs)) return null;
1837
- if (cachedAtMs > nowMs + 60_000) return null;
1838
- if (nowMs - cachedAtMs > maxAgeMs) return null;
1839
-
1840
- const cached = {
1841
- configured: true,
1842
- error: null,
1843
- plan_name: typeof raw?.plan_name === "string" ? raw.plan_name : null,
1844
- primary_window: isKiroCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
1845
- secondary_window: isKiroCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
1846
- stale: true,
1847
- cached_at: raw.cached_at,
1848
- };
1849
- return hasKiroWindow(cached) ? cached : null;
1850
- }
1851
-
1852
- // The signed-in Kiro account partitions the cache (zcode / Cursor login-identity
1853
- // precedent): the id is the CodeWhisperer profile ARN kiro-cli itself persists
1854
- // next to its tokens. Reads require the stored identity to match the current
1855
- // one, and a write without an identity is refused — after an account switch the
1856
- // previous account's windows must never resurface as another login's fallback.
1857
- function readKiroLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
1858
- if (!identity) return null;
1859
- try {
1860
- const parsed = JSON.parse(fs.readFileSync(resolveKiroLimitsCachePath({ home }), "utf8"));
1861
- const entry = parsed?.kiro;
1862
- if (entry?.identity !== identity) return null;
1863
- return normalizeKiroCachedLimits(entry, { nowMs });
1864
- } catch (_error) {
1865
- return null;
1866
- }
1867
- }
1818
+ // kiro-cli's quota windows ride on the profile the CLI is signed into; the
1819
+ // profileArn partitions the cache. Windows carry `reset_at` as an ISO string.
1820
+ const kiroLimitsCache = createLimitsCache({
1821
+ file: KIRO_LIMITS_CACHE_FILE,
1822
+ key: "kiro",
1823
+ maxAgeMs: KIRO_LIMITS_CACHE_MAX_AGE_MS,
1824
+ fields: { plan_name: "string" },
1825
+ windowKeys: ["primary_window", "secondary_window"],
1826
+ identityField: "identity",
1827
+ identityRequired: true,
1828
+ });
1868
1829
 
1869
- function writeKiroLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
1870
- if (!identity) return;
1871
- if (!limits?.configured || limits.error || !hasKiroWindow(limits)) return;
1872
- const cachePath = resolveKiroLimitsCachePath({ home });
1873
- const payload = {
1874
- kiro: {
1875
- identity,
1876
- plan_name: limits.plan_name || null,
1877
- primary_window: limits.primary_window || null,
1878
- secondary_window: limits.secondary_window || null,
1879
- cached_at: new Date(nowMs).toISOString(),
1880
- },
1881
- };
1882
- try {
1883
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
1884
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
1885
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
1886
- fs.renameSync(tmpPath, cachePath);
1887
- } catch (_error) {}
1888
- }
1889
1830
 
1890
1831
  function resolveKiroAuthIdentity({ home } = {}) {
1891
1832
  try {
@@ -2642,98 +2583,70 @@ async function detectAntigravityProcess({
2642
2583
  return { configured: false };
2643
2584
  }
2644
2585
 
2645
- function resolveAntigravityLimitsCachePath({ home } = {}) {
2646
- return path.join(home || os.homedir(), ".tokentracker", "tracker", ANTIGRAVITY_LIMITS_CACHE_FILE);
2647
- }
2648
-
2649
- function parseTimeMs(value) {
2650
- if (typeof value !== "string" || !value) return null;
2651
- const ts = Date.parse(value);
2652
- return Number.isFinite(ts) && ts > 0 ? ts : null;
2653
- }
2654
-
2655
- function isCacheWindowUsable(window, { cachedAtMs, nowMs } = {}) {
2656
- if (!window || typeof window !== "object") return false;
2657
- const resetAtMs = parseTimeMs(window.reset_at);
2658
- if (resetAtMs !== null) return resetAtMs > nowMs;
2659
- return Number.isFinite(cachedAtMs)
2660
- && nowMs - cachedAtMs <= ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS;
2661
- }
2586
+ // Last-good cache for the Antigravity quota summary. Windows without a reset
2587
+ // stamp are bounded by the 12h unknown-reset TTL instead of living as long as
2588
+ // the snapshot; the read is gated off entirely by the disable flag.
2589
+ const antigravityLimitsCache = createLimitsCache({
2590
+ file: ANTIGRAVITY_LIMITS_CACHE_FILE,
2591
+ key: "antigravity",
2592
+ maxAgeMs: ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS,
2593
+ unknownResetTtlMs: ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS,
2594
+ fields: { account_email: "string", account_plan: "string" },
2595
+ windowKeys: ["primary_window", "secondary_window", "tertiary_window", "quaternary_window"],
2596
+ cachedFlag: true,
2597
+ // The cached annotation is this provider's own marker — no stale flag.
2598
+ stale: null,
2599
+ });
2662
2600
 
2663
2601
  function hasAntigravityWindow(limits) {
2664
2602
  return Boolean(limits?.primary_window || limits?.secondary_window || limits?.tertiary_window || limits?.quaternary_window);
2665
2603
  }
2666
2604
 
2667
- function normalizeAntigravityCachedLimits(raw, { nowMs = Date.now() } = {}) {
2668
- const cachedAtMs = parseTimeMs(raw?.cached_at);
2669
- if (!Number.isFinite(cachedAtMs)) return null;
2670
- if (cachedAtMs > nowMs + 60_000) return null;
2671
- if (nowMs - cachedAtMs > ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS) return null;
2672
-
2673
- const cached = {
2674
- configured: true,
2675
- error: null,
2676
- account_email: typeof raw?.account_email === "string" ? raw.account_email : null,
2677
- account_plan: typeof raw?.account_plan === "string" ? raw.account_plan : null,
2678
- primary_window: isCacheWindowUsable(raw?.primary_window, { cachedAtMs, nowMs }) ? raw.primary_window : null,
2679
- secondary_window: isCacheWindowUsable(raw?.secondary_window, { cachedAtMs, nowMs }) ? raw.secondary_window : null,
2680
- tertiary_window: isCacheWindowUsable(raw?.tertiary_window, { cachedAtMs, nowMs }) ? raw.tertiary_window : null,
2681
- quaternary_window: isCacheWindowUsable(raw?.quaternary_window, { cachedAtMs, nowMs }) ? raw.quaternary_window : null,
2682
- cached: true,
2683
- cached_at: raw.cached_at,
2684
- };
2685
- return hasAntigravityWindow(cached) ? cached : null;
2686
- }
2687
-
2688
2605
  function isAntigravityQuotaDisabled() {
2689
2606
  return process.env.TOKENTRACKER_DISABLE_ANTIGRAVITY_QUOTA === "1";
2690
2607
  }
2691
2608
 
2692
2609
  function readAntigravityLimitsCache({ home, nowMs = Date.now() } = {}) {
2693
2610
  if (isAntigravityQuotaDisabled()) return null;
2694
- const cachePath = resolveAntigravityLimitsCachePath({ home });
2695
- try {
2696
- const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
2697
- return normalizeAntigravityCachedLimits(parsed?.antigravity, { nowMs });
2698
- } catch (_error) {
2699
- return null;
2700
- }
2611
+ return antigravityLimitsCache.read({ home, nowMs });
2701
2612
  }
2702
2613
 
2703
2614
  function writeAntigravityLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
2704
- if (!limits?.configured || limits.error || !hasAntigravityWindow(limits)) return;
2705
- const cachePath = resolveAntigravityLimitsCachePath({ home });
2706
- const payload = {
2707
- antigravity: {
2708
- account_email: limits.account_email || null,
2709
- account_plan: limits.account_plan || null,
2710
- primary_window: limits.primary_window || null,
2711
- secondary_window: limits.secondary_window || null,
2712
- tertiary_window: limits.tertiary_window || null,
2713
- quaternary_window: limits.quaternary_window || null,
2714
- cached_at: new Date(nowMs).toISOString(),
2715
- },
2716
- };
2717
- try {
2718
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
2719
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
2720
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
2721
- fs.renameSync(tmpPath, cachePath);
2722
- } catch (_error) {}
2615
+ antigravityLimitsCache.write(limits, { home, nowMs });
2723
2616
  }
2724
2617
 
2725
2618
  function resolveClaudeLimitsCachePath({ home } = {}) {
2726
- return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_LIMITS_CACHE_FILE);
2727
- }
2619
+ return claudeLimitsCache.path({ home });
2620
+ }
2621
+
2622
+ // Claude windows carry their own `resets_at`; a window whose reset has already
2623
+ // passed is stale data, not a usable fallback, so drop it. Windows without a
2624
+ // reset stamp are kept (the overall cached_at max-age gate already bounds them).
2625
+ const claudeLimitsCache = createLimitsCache({
2626
+ file: CLAUDE_LIMITS_CACHE_FILE,
2627
+ key: "claude",
2628
+ maxAgeMs: CLAUDE_LIMITS_CACHE_MAX_AGE_MS,
2629
+ fields: {
2630
+ extra_usage: { write: (record) => record?.extra_usage || null, read: (raw) => raw?.extra_usage ?? null },
2631
+ // Cached scoped windows are filtered per-entry like the fixed windows: a
2632
+ // scoped window whose reset has passed is stale data, not a usable fallback.
2633
+ weekly_scoped: {
2634
+ write: (record) => (Array.isArray(record?.weekly_scoped) && record.weekly_scoped.length > 0
2635
+ ? record.weekly_scoped
2636
+ : null),
2637
+ read: (raw, ctx) => normalizeClaudeCachedScopedWeekly(raw?.weekly_scoped, { nowMs: ctx.nowMs }),
2638
+ // Scoped entries count as window survivors: a live read carrying only
2639
+ // scoped windows must reach the disk cache and survive a later fallback
2640
+ // read, while an all-expired array still refuses the snapshot.
2641
+ survivor: (value, { nowMs } = {}) => normalizeClaudeCachedScopedWeekly(value, { nowMs }) !== null,
2642
+ },
2643
+ },
2644
+ windowKeys: ["five_hour", "seven_day", "seven_day_opus"],
2645
+ windowResetField: "resets_at",
2646
+ });
2728
2647
 
2729
- // Claude windows carry their own `resets_at`; a window whose reset has already passed is
2730
- // stale data, not a usable fallback, so drop it. Windows without a reset stamp are kept
2731
- // (the overall cached_at max-age gate already bounds them).
2732
2648
  function isClaudeCacheWindowUsable(window, { nowMs } = {}) {
2733
- if (!window || typeof window !== "object") return false;
2734
- const resetAtMs = parseTimeMs(window.resets_at);
2735
- if (resetAtMs === null) return true;
2736
- return resetAtMs > nowMs;
2649
+ return claudeLimitsCache.isWindowUsable(window, { nowMs });
2737
2650
  }
2738
2651
 
2739
2652
  function hasClaudeWindow(limits) {
@@ -2754,6 +2667,8 @@ function normalizeClaudeCachedScopedWeekly(raw, { nowMs } = {}) {
2754
2667
  return usable.length > 0 ? usable : null;
2755
2668
  }
2756
2669
 
2670
+ // Parametrized so the fresh pre-fetch gate can read with the 10m TTL and a
2671
+ // non-stale annotation while the fallback reads with the 7d bound.
2757
2672
  function normalizeClaudeCachedLimits(
2758
2673
  raw,
2759
2674
  {
@@ -2762,32 +2677,12 @@ function normalizeClaudeCachedLimits(
2762
2677
  stale = true,
2763
2678
  } = {},
2764
2679
  ) {
2765
- const cachedAtMs = parseTimeMs(raw?.cached_at);
2766
- if (!Number.isFinite(cachedAtMs)) return null;
2767
- if (cachedAtMs > nowMs + 60_000) return null;
2768
- if (nowMs - cachedAtMs > maxAgeMs) return null;
2769
-
2770
- const cached = {
2771
- configured: true,
2772
- error: null,
2773
- five_hour: isClaudeCacheWindowUsable(raw?.five_hour, { nowMs }) ? raw.five_hour : null,
2774
- seven_day: isClaudeCacheWindowUsable(raw?.seven_day, { nowMs }) ? raw.seven_day : null,
2775
- seven_day_opus: isClaudeCacheWindowUsable(raw?.seven_day_opus, { nowMs }) ? raw.seven_day_opus : null,
2776
- weekly_scoped: normalizeClaudeCachedScopedWeekly(raw?.weekly_scoped, { nowMs }),
2777
- extra_usage: raw?.extra_usage ?? null,
2778
- stale,
2779
- cached_at: raw.cached_at,
2780
- };
2781
- return hasClaudeWindow(cached) ? cached : null;
2680
+ return claudeLimitsCache.normalize(raw, { nowMs, maxAgeMs, stale });
2782
2681
  }
2783
2682
 
2784
2683
  function readClaudeLimitsCacheRaw({ home } = {}) {
2785
- try {
2786
- const parsed = JSON.parse(fs.readFileSync(resolveClaudeLimitsCachePath({ home }), "utf8"));
2787
- return parsed?.claude ?? null;
2788
- } catch (_error) {
2789
- return null;
2790
- }
2684
+ const parsed = readCacheJson(claudeLimitsCache.path({ home }));
2685
+ return parsed?.claude ?? null;
2791
2686
  }
2792
2687
 
2793
2688
  function readClaudeLimitsCache({
@@ -2899,286 +2794,99 @@ function readFreshClaudeLimitsCache({ home, nowMs = Date.now() } = {}) {
2899
2794
  }
2900
2795
 
2901
2796
  function writeClaudeLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
2902
- if (!limits?.configured || limits.error || !hasClaudeWindow(limits)) return;
2903
- const cachePath = resolveClaudeLimitsCachePath({ home });
2904
- const payload = {
2905
- claude: {
2906
- five_hour: limits.five_hour || null,
2907
- seven_day: limits.seven_day || null,
2908
- seven_day_opus: limits.seven_day_opus || null,
2909
- weekly_scoped: Array.isArray(limits.weekly_scoped) && limits.weekly_scoped.length > 0
2910
- ? limits.weekly_scoped
2911
- : null,
2912
- extra_usage: limits.extra_usage || null,
2913
- cached_at: new Date(nowMs).toISOString(),
2914
- },
2915
- };
2916
- try {
2917
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
2918
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
2919
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
2920
- fs.renameSync(tmpPath, cachePath);
2921
- } catch (_error) {}
2797
+ claudeLimitsCache.write(limits, { home, nowMs });
2922
2798
  }
2923
2799
 
2924
- function resolveCodexLimitsCachePath({ home } = {}) {
2925
- return path.join(home || os.homedir(), ".tokentracker", "tracker", CODEX_LIMITS_CACHE_FILE);
2926
- }
2927
2800
 
2928
- // Codex windows carry `reset_at` as unix seconds (not an ISO string). A window whose reset has
2929
- // already passed is stale data, not a usable fallback, so drop it. Windows without a reset stamp
2930
- // are kept — the overall cached_at max-age gate already bounds them.
2931
- function isCodexCacheWindowUsable(window, { nowMs } = {}) {
2932
- if (!window || typeof window !== "object") return false;
2933
- const resetAt = Number(window.reset_at);
2934
- if (!Number.isFinite(resetAt) || resetAt <= 0) return true;
2935
- return resetAt * 1000 > nowMs;
2936
- }
2801
+ // Codex windows carry `reset_at` as unix seconds (not an ISO string). A window
2802
+ // whose reset has already passed is stale data, not a usable fallback, so drop
2803
+ // it; windows without a reset stamp are kept — the cached_at max-age gate
2804
+ // already bounds them.
2805
+ const codexLimitsCache = createLimitsCache({
2806
+ file: CODEX_LIMITS_CACHE_FILE,
2807
+ key: "codex",
2808
+ maxAgeMs: CODEX_LIMITS_CACHE_MAX_AGE_MS,
2809
+ fields: {
2810
+ plan_type: "string",
2811
+ reset_credits: { write: (record) => record?.reset_credits || null, read: (raw) => raw?.reset_credits ?? null },
2812
+ },
2813
+ windowKeys: ["primary_window", "secondary_window", "credit_window", "spark_primary_window", "spark_secondary_window"],
2814
+ resetTimeMode: "unix-seconds",
2815
+ });
2937
2816
 
2938
- function hasCodexWindow(limits) {
2939
- return Boolean(
2940
- limits?.primary_window
2941
- || limits?.secondary_window
2942
- || limits?.credit_window
2943
- || limits?.spark_primary_window
2944
- || limits?.spark_secondary_window,
2945
- );
2946
- }
2947
-
2948
- function normalizeCodexCachedLimits(
2949
- raw,
2950
- { nowMs = Date.now(), maxAgeMs = CODEX_LIMITS_CACHE_MAX_AGE_MS } = {},
2951
- ) {
2952
- const cachedAtMs = parseTimeMs(raw?.cached_at);
2953
- if (!Number.isFinite(cachedAtMs)) return null;
2954
- if (cachedAtMs > nowMs + 60_000) return null;
2955
- if (nowMs - cachedAtMs > maxAgeMs) return null;
2956
-
2957
- const cached = {
2958
- configured: true,
2959
- error: null,
2960
- plan_type: typeof raw?.plan_type === "string" ? raw.plan_type : null,
2961
- primary_window: isCodexCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
2962
- secondary_window: isCodexCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
2963
- credit_window: isCodexCacheWindowUsable(raw?.credit_window, { nowMs }) ? raw.credit_window : null,
2964
- spark_primary_window: isCodexCacheWindowUsable(raw?.spark_primary_window, { nowMs }) ? raw.spark_primary_window : null,
2965
- spark_secondary_window: isCodexCacheWindowUsable(raw?.spark_secondary_window, { nowMs }) ? raw.spark_secondary_window : null,
2966
- reset_credits: raw?.reset_credits ?? null,
2967
- stale: true,
2968
- cached_at: raw.cached_at,
2969
- };
2970
- return hasCodexWindow(cached) ? cached : null;
2971
- }
2972
-
2973
- function readCodexLimitsCache({ home, nowMs = Date.now(), maxAgeMs = CODEX_LIMITS_CACHE_MAX_AGE_MS } = {}) {
2974
- const cachePath = resolveCodexLimitsCachePath({ home });
2975
- try {
2976
- const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
2977
- return normalizeCodexCachedLimits(parsed?.codex, { nowMs, maxAgeMs });
2978
- } catch (_error) {
2979
- return null;
2980
- }
2817
+ function readCodexLimitsCache({ home, nowMs = Date.now(), maxAgeMs } = {}) {
2818
+ return codexLimitsCache.read({ home, nowMs, maxAgeMs });
2981
2819
  }
2982
2820
 
2983
2821
  function writeCodexLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
2984
- if (!limits?.configured || limits.error || !hasCodexWindow(limits)) return;
2985
- const cachePath = resolveCodexLimitsCachePath({ home });
2986
- const payload = {
2987
- codex: {
2988
- plan_type: limits.plan_type || null,
2989
- primary_window: limits.primary_window || null,
2990
- secondary_window: limits.secondary_window || null,
2991
- credit_window: limits.credit_window || null,
2992
- spark_primary_window: limits.spark_primary_window || null,
2993
- spark_secondary_window: limits.spark_secondary_window || null,
2994
- reset_credits: limits.reset_credits || null,
2995
- cached_at: new Date(nowMs).toISOString(),
2822
+ codexLimitsCache.write(limits, { home, nowMs });
2823
+ }
2824
+
2825
+ const opencodeGoLimitsCache = createLimitsCache({
2826
+ file: OPENCODE_GO_LIMITS_CACHE_FILE,
2827
+ key: "opencodeGo",
2828
+ // The dashboard's configured tracker data dir overrides the default
2829
+ // tracker directory for this provider's cache.
2830
+ dirOverride: "trackerDir",
2831
+ // The max-age is source-dependent: local estimates are heuristic readings,
2832
+ // not provider truth, so they expire within the hour.
2833
+ maxAgeMs: (raw) => (raw?.source === "local-estimate"
2834
+ ? OPENCODE_GO_LOCAL_ESTIMATE_CACHE_MAX_AGE_MS
2835
+ : OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS),
2836
+ fields: {
2837
+ source: {
2838
+ write: (record) => record?.source || "api",
2839
+ read: (raw) => (typeof raw?.source === "string" ? raw.source : "api"),
2996
2840
  },
2997
- };
2998
- try {
2999
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3000
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
3001
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3002
- fs.renameSync(tmpPath, cachePath);
3003
- } catch (_error) {}
3004
- }
2841
+ credential_source: {
2842
+ write: (record) => (record?.credential_source === "env" || record?.credential_source === "stored"
2843
+ ? record.credential_source
2844
+ : null),
2845
+ read: (raw) => (raw?.credential_source === "env" || raw?.credential_source === "stored"
2846
+ ? raw.credential_source
2847
+ : null),
2848
+ },
2849
+ subscription_status: "string",
2850
+ plan_label: "string",
2851
+ },
2852
+ windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
2853
+ });
3005
2854
 
3006
2855
  function resolveOpencodeGoLimitsCachePath({ trackerDir, home } = {}) {
3007
- const dir =
3008
- typeof trackerDir === "string" && trackerDir.trim()
3009
- ? trackerDir.trim()
3010
- : path.join(home || os.homedir(), ".tokentracker", "tracker");
3011
- return path.join(dir, OPENCODE_GO_LIMITS_CACHE_FILE);
2856
+ return opencodeGoLimitsCache.path({ trackerDir, home });
3012
2857
  }
3013
2858
 
3014
2859
  function hasOpencodeGoWindow(limits) {
3015
2860
  return Boolean(limits?.primary_window || limits?.secondary_window || limits?.tertiary_window);
3016
2861
  }
3017
2862
 
3018
- function isOpencodeGoCacheWindowUsable(window, { nowMs } = {}) {
3019
- if (!window || typeof window !== "object") return false;
3020
- const resetAtMs = parseTimeMs(window.reset_at);
3021
- if (resetAtMs === null) return true;
3022
- return resetAtMs > nowMs;
3023
- }
3024
-
3025
- function normalizeOpencodeGoCachedLimits(
3026
- raw,
3027
- { nowMs = Date.now(), maxAgeMs } = {},
3028
- ) {
3029
- const effectiveMaxAge = Number.isFinite(maxAgeMs)
3030
- ? maxAgeMs
3031
- : raw?.source === "local-estimate"
3032
- ? OPENCODE_GO_LOCAL_ESTIMATE_CACHE_MAX_AGE_MS
3033
- : OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS;
3034
- const cachedAtMs = parseTimeMs(raw?.cached_at);
3035
- if (!Number.isFinite(cachedAtMs)) return null;
3036
- if (cachedAtMs > nowMs + 60_000) return null;
3037
- if (nowMs - cachedAtMs > effectiveMaxAge) return null;
3038
-
3039
- const credentialSource =
3040
- raw?.credential_source === "env" || raw?.credential_source === "stored"
3041
- ? raw.credential_source
3042
- : null;
3043
- const cached = {
3044
- configured: true,
3045
- error: null,
3046
- source: typeof raw?.source === "string" ? raw.source : "api",
3047
- credential_source: credentialSource,
3048
- subscription_status: typeof raw?.subscription_status === "string" ? raw.subscription_status : null,
3049
- plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
3050
- primary_window: isOpencodeGoCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3051
- secondary_window: isOpencodeGoCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3052
- tertiary_window: isOpencodeGoCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
3053
- stale: true,
3054
- cached_at: raw.cached_at,
3055
- };
3056
- return hasOpencodeGoWindow(cached) ? cached : null;
3057
- }
3058
-
3059
2863
  function readOpencodeGoLimitsCache({ trackerDir, home, nowMs = Date.now() } = {}) {
3060
- const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
3061
- try {
3062
- const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
3063
- return normalizeOpencodeGoCachedLimits(parsed?.opencodeGo, { nowMs });
3064
- } catch (_error) {
3065
- return null;
3066
- }
2864
+ return opencodeGoLimitsCache.read({ trackerDir, home, nowMs });
3067
2865
  }
3068
2866
 
3069
2867
  function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now() } = {}) {
3070
- if (!limits?.configured || limits.error || !hasOpencodeGoWindow(limits)) return;
3071
- const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
3072
- const credentialSource =
3073
- limits.credential_source === "env" || limits.credential_source === "stored"
3074
- ? limits.credential_source
3075
- : null;
3076
- const payload = {
3077
- opencodeGo: {
3078
- source: limits.source || "api",
3079
- credential_source: credentialSource,
3080
- subscription_status: limits.subscription_status || null,
3081
- plan_label: limits.plan_label || null,
3082
- primary_window: limits.primary_window || null,
3083
- secondary_window: limits.secondary_window || null,
3084
- tertiary_window: limits.tertiary_window || null,
3085
- cached_at: new Date(nowMs).toISOString(),
3086
- },
3087
- };
3088
- try {
3089
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3090
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
3091
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3092
- fs.renameSync(tmpPath, cachePath);
3093
- } catch (_error) {}
3094
- }
3095
-
3096
- function resolveCursorLimitsCachePath({ home } = {}) {
3097
- return path.join(home || os.homedir(), ".tokentracker", "tracker", CURSOR_LIMITS_CACHE_FILE);
3098
- }
3099
-
3100
- function hasCursorWindow(limits) {
3101
- return Boolean(
3102
- limits?.primary_window
3103
- || limits?.secondary_window
3104
- || limits?.tertiary_window
3105
- || limits?.quaternary_window,
3106
- );
2868
+ opencodeGoLimitsCache.write(limits, { trackerDir, home, nowMs });
3107
2869
  }
3108
2870
 
3109
- // Cursor windows carry `reset_at` as an ISO string (billing-cycle end for the three
3110
- // summary windows, the RPC's own expiry for the Grok Bot window). A window whose reset
3111
- // has already passed is stale data, not a usable fallback, so drop it. Windows without
3112
- // a reset stamp are kept — the overall cached_at max-age gate already bounds them.
3113
- function isCursorCacheWindowUsable(window, { nowMs } = {}) {
3114
- if (!window || typeof window !== "object") return false;
3115
- const resetAtMs = parseTimeMs(window.reset_at);
3116
- if (resetAtMs === null) return true;
3117
- return resetAtMs > nowMs;
3118
- }
3119
-
3120
- function normalizeCursorCachedLimits(
3121
- raw,
3122
- { nowMs = Date.now(), maxAgeMs = CURSOR_LIMITS_CACHE_MAX_AGE_MS } = {},
3123
- ) {
3124
- const cachedAtMs = parseTimeMs(raw?.cached_at);
3125
- if (!Number.isFinite(cachedAtMs)) return null;
3126
- if (cachedAtMs > nowMs + 60_000) return null;
3127
- if (nowMs - cachedAtMs > maxAgeMs) return null;
3128
-
3129
- const cached = {
3130
- configured: true,
3131
- error: null,
3132
- membership_type: typeof raw?.membership_type === "string" ? raw.membership_type : null,
3133
- primary_window: isCursorCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3134
- secondary_window: isCursorCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3135
- tertiary_window: isCursorCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
3136
- quaternary_window: isCursorCacheWindowUsable(raw?.quaternary_window, { nowMs }) ? raw.quaternary_window : null,
3137
- stale: true,
3138
- cached_at: raw.cached_at,
3139
- };
3140
- return hasCursorWindow(cached) ? cached : null;
3141
- }
3142
2871
 
3143
2872
  // The signed-in Cursor account partitions the cache (zcode login-identity precedent):
3144
2873
  // the id is the non-secret normalized subject Cursor itself puts in the session cookie.
3145
2874
  // A single home holds one active login, so one stamped entry suffices; a read requires
3146
2875
  // the stored identity to match the current one, and a write without an identity is
3147
2876
  // refused — an unreadable login must never pin another account's bars in place.
3148
- function readCursorLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3149
- if (!identity) return null;
3150
- try {
3151
- const parsed = JSON.parse(fs.readFileSync(resolveCursorLimitsCachePath({ home }), "utf8"));
3152
- const entry = parsed?.cursor;
3153
- if (entry?.identity !== identity) return null;
3154
- return normalizeCursorCachedLimits(entry, { nowMs });
3155
- } catch (_error) {
3156
- return null;
3157
- }
3158
- }
3159
-
3160
- function writeCursorLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3161
- if (!identity) return;
3162
- if (!limits?.configured || limits.error || !hasCursorWindow(limits)) return;
3163
- const cachePath = resolveCursorLimitsCachePath({ home });
3164
- const payload = {
3165
- cursor: {
3166
- identity,
3167
- membership_type: limits.membership_type || null,
3168
- primary_window: limits.primary_window || null,
3169
- secondary_window: limits.secondary_window || null,
3170
- tertiary_window: limits.tertiary_window || null,
3171
- quaternary_window: limits.quaternary_window || null,
3172
- cached_at: new Date(nowMs).toISOString(),
3173
- },
3174
- };
3175
- try {
3176
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3177
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
3178
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3179
- fs.renameSync(tmpPath, cachePath);
3180
- } catch (_error) {}
3181
- }
2877
+ const cursorLimitsCache = createLimitsCache({
2878
+ file: CURSOR_LIMITS_CACHE_FILE,
2879
+ key: "cursor",
2880
+ maxAgeMs: CURSOR_LIMITS_CACHE_MAX_AGE_MS,
2881
+ fields: { membership_type: "string" },
2882
+ // Billing-cycle end for the three summary windows, the RPC's own expiry for
2883
+ // the Grok Bot window. A window whose reset has already passed is stale
2884
+ // data, not a usable fallback, so drop it; windows without a reset stamp
2885
+ // are kept — the overall cached_at max-age gate already bounds them.
2886
+ windowKeys: ["primary_window", "secondary_window", "tertiary_window", "quaternary_window"],
2887
+ identityField: "identity",
2888
+ identityRequired: true,
2889
+ });
3182
2890
 
3183
2891
  // Failure-only identity re-derivation for the cache fallback: when the provider slot's
3184
2892
  // result was produced by the outer timeout race, the fetcher's own identity field is
@@ -3191,106 +2899,6 @@ function resolveCursorCacheIdentity({ home, env, trackerDir } = {}) {
3191
2899
  }
3192
2900
  }
3193
2901
 
3194
- function resolveGeminiLimitsCachePath({ home } = {}) {
3195
- return path.join(home || os.homedir(), ".tokentracker", "tracker", GEMINI_LIMITS_CACHE_FILE);
3196
- }
3197
-
3198
- function hasGeminiWindow(limits) {
3199
- return Boolean(
3200
- limits?.primary_window
3201
- || limits?.secondary_window
3202
- || limits?.tertiary_window,
3203
- );
3204
- }
3205
-
3206
- // Gemini windows carry `reset_at` as an ISO string (the Code Assist bucket's own
3207
- // resetTime — daily for free-tier quotas). A window whose reset has already passed
3208
- // is stale data, not a usable fallback, so drop it. Windows without a reset stamp
3209
- // are kept — the overall cached_at max-age gate already bounds them.
3210
- function isGeminiCacheWindowUsable(window, { nowMs } = {}) {
3211
- if (!window || typeof window !== "object") return false;
3212
- const resetAtMs = parseTimeMs(window.reset_at);
3213
- if (resetAtMs === null) return true;
3214
- return resetAtMs > nowMs;
3215
- }
3216
-
3217
- function normalizeGeminiCachedLimits(
3218
- raw,
3219
- { nowMs = Date.now(), maxAgeMs = GEMINI_LIMITS_CACHE_MAX_AGE_MS } = {},
3220
- ) {
3221
- const cachedAtMs = parseTimeMs(raw?.cached_at);
3222
- if (!Number.isFinite(cachedAtMs)) return null;
3223
- if (cachedAtMs > nowMs + 60_000) return null;
3224
- if (nowMs - cachedAtMs > maxAgeMs) return null;
3225
-
3226
- const cached = {
3227
- configured: true,
3228
- error: null,
3229
- account_email: typeof raw?.account_email === "string" ? raw.account_email : null,
3230
- account_plan: typeof raw?.account_plan === "string" ? raw.account_plan : null,
3231
- primary_window: isGeminiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3232
- secondary_window: isGeminiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3233
- tertiary_window: isGeminiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
3234
- stale: true,
3235
- cached_at: raw.cached_at,
3236
- };
3237
- return hasGeminiWindow(cached) ? cached : null;
3238
- }
3239
-
3240
- // The signed-in Google account partitions the cache (zcode login-identity
3241
- // precedent): the email decoded from the local id_token JWT is non-secret and is
3242
- // the same id the payload already surfaces as account_email. GEMINI_HOME can aim
3243
- // the credential lookup at a different installation, but the cache stays keyed by
3244
- // account — a read requires the stored identity to match the account the current
3245
- // credentials resolve to, and a write without an identity is refused: an
3246
- // unreadable login must never pin another account's bars in place.
3247
- function readGeminiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3248
- if (!identity) return null;
3249
- try {
3250
- const parsed = JSON.parse(fs.readFileSync(resolveGeminiLimitsCachePath({ home }), "utf8"));
3251
- const entry = parsed?.gemini;
3252
- if (entry?.identity !== identity) return null;
3253
- return normalizeGeminiCachedLimits(entry, { nowMs });
3254
- } catch (_error) {
3255
- return null;
3256
- }
3257
- }
3258
-
3259
- function writeGeminiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3260
- if (!identity) return;
3261
- if (!limits?.configured || limits.error || !hasGeminiWindow(limits)) return;
3262
- const cachePath = resolveGeminiLimitsCachePath({ home });
3263
- const payload = {
3264
- gemini: {
3265
- identity,
3266
- account_email: limits.account_email || null,
3267
- account_plan: limits.account_plan || null,
3268
- primary_window: limits.primary_window || null,
3269
- secondary_window: limits.secondary_window || null,
3270
- tertiary_window: limits.tertiary_window || null,
3271
- cached_at: new Date(nowMs).toISOString(),
3272
- },
3273
- };
3274
- try {
3275
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3276
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
3277
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3278
- fs.renameSync(tmpPath, cachePath);
3279
- } catch (_error) {}
3280
- }
3281
-
3282
- // Failure-only identity re-derivation for the cache fallback: when the provider
3283
- // slot's result was produced by the outer timeout race, the fetcher's own
3284
- // identity field is lost, so decode the (still-local) id_token once to learn
3285
- // who we are serving for.
3286
- function resolveGeminiCacheIdentity({ home, env } = {}) {
3287
- try {
3288
- return decodeJwtPayload(loadGeminiCredentials({ home, env })?.id_token)?.email || null;
3289
- } catch (_error) {
3290
- return null;
3291
- }
3292
- }
3293
-
3294
2902
  // The active Kimi profile partitions the cache (ark profile_identity precedent):
3295
2903
  // mainland and global logins — and any KIMI_HOME override — resolve to different
3296
2904
  // endpoint-scoped credential slots, and one slot must never serve another's
@@ -3312,160 +2920,20 @@ function resolveKimiCacheIdentity({ home, env } = {}) {
3312
2920
  }
3313
2921
  }
3314
2922
 
3315
- function resolveKimiLimitsCachePath({ home } = {}) {
3316
- return path.join(home || os.homedir(), ".tokentracker", "tracker", KIMI_LIMITS_CACHE_FILE);
3317
- }
3318
-
3319
- function hasKimiWindow(limits) {
3320
- return Boolean(
3321
- limits?.primary_window
3322
- || limits?.secondary_window
3323
- || limits?.tertiary_window,
3324
- );
3325
- }
3326
-
3327
- // Kimi windows carry `reset_at` (ISO) when the API reports a cycle end; a window
3328
- // whose reset has already passed is stale data, not a usable fallback, so drop
3329
- // it. Windows without a reset stamp are kept — the overall cached_at max-age
3330
- // gate already bounds them.
3331
- function isKimiCacheWindowUsable(window, { nowMs } = {}) {
3332
- if (!window || typeof window !== "object") return false;
3333
- const resetAtMs = parseTimeMs(window.reset_at);
3334
- if (resetAtMs === null) return true;
3335
- return resetAtMs > nowMs;
3336
- }
3337
-
3338
- function normalizeKimiCachedLimits(
3339
- raw,
3340
- { nowMs = Date.now(), maxAgeMs = KIMI_LIMITS_CACHE_MAX_AGE_MS } = {},
3341
- ) {
3342
- const cachedAtMs = parseTimeMs(raw?.cached_at);
3343
- if (!Number.isFinite(cachedAtMs)) return null;
3344
- if (cachedAtMs > nowMs + 60_000) return null;
3345
- if (nowMs - cachedAtMs > maxAgeMs) return null;
3346
-
3347
- const cached = {
3348
- configured: true,
3349
- error: null,
3350
- membership_level: typeof raw?.membership_level === "string" ? raw.membership_level : null,
3351
- subscription_type: typeof raw?.subscription_type === "string" ? raw.subscription_type : null,
3352
- parallel_limit: Number.isFinite(raw?.parallel_limit) ? raw.parallel_limit : null,
3353
- primary_window: isKimiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3354
- secondary_window: isKimiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3355
- tertiary_window: isKimiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
3356
- stale: true,
3357
- cached_at: raw.cached_at,
3358
- };
3359
- return hasKimiWindow(cached) ? cached : null;
3360
- }
3361
-
3362
- function readKimiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3363
- if (!identity) return null;
3364
- try {
3365
- const parsed = JSON.parse(fs.readFileSync(resolveKimiLimitsCachePath({ home }), "utf8"));
3366
- const entry = parsed?.kimi;
3367
- if (entry?.identity !== identity) return null;
3368
- return normalizeKimiCachedLimits(entry, { nowMs });
3369
- } catch (_error) {
3370
- return null;
3371
- }
3372
- }
3373
-
3374
- function writeKimiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3375
- if (!identity) return;
3376
- if (!limits?.configured || limits.error || !hasKimiWindow(limits)) return;
3377
- const cachePath = resolveKimiLimitsCachePath({ home });
3378
- const payload = {
3379
- kimi: {
3380
- identity,
3381
- membership_level: limits.membership_level || null,
3382
- subscription_type: limits.subscription_type || null,
3383
- parallel_limit: Number.isFinite(limits.parallel_limit) ? limits.parallel_limit : null,
3384
- primary_window: limits.primary_window || null,
3385
- secondary_window: limits.secondary_window || null,
3386
- tertiary_window: limits.tertiary_window || null,
3387
- cached_at: new Date(nowMs).toISOString(),
3388
- },
3389
- };
3390
- try {
3391
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3392
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
3393
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3394
- fs.renameSync(tmpPath, cachePath);
3395
- } catch (_error) {}
3396
- }
3397
-
3398
- function resolveGrokLimitsCachePath({ home } = {}) {
3399
- return path.join(home || os.homedir(), ".tokentracker", "tracker", GROK_LIMITS_CACHE_FILE);
3400
- }
3401
-
3402
- function hasGrokWindow(limits) {
3403
- return Boolean(limits?.primary_window || limits?.secondary_window);
3404
- }
3405
-
3406
- // Grok windows carry `reset_at` as an ISO string — the billing-period end shared
3407
- // by the pool, on-demand, and per-product windows. A window whose reset has
3408
- // already passed is stale data, not a usable fallback, so drop it. Windows
3409
- // without a reset stamp are kept — the overall cached_at max-age gate already
3410
- // bounds them (mirrors Cursor).
3411
- function isGrokCacheWindowUsable(window, { nowMs } = {}) {
3412
- if (!window || typeof window !== "object") return false;
3413
- const resetAtMs = parseTimeMs(window.reset_at);
3414
- if (resetAtMs === null) return true;
3415
- return resetAtMs > nowMs;
3416
- }
3417
-
3418
- function resolveCopilotLimitsCachePath({ home } = {}) {
3419
- return path.join(home || os.homedir(), ".tokentracker", "tracker", COPILOT_LIMITS_CACHE_FILE);
3420
- }
3421
-
3422
- function hasCopilotWindow(limits) {
3423
- return Boolean(limits?.primary_window || limits?.secondary_window);
3424
- }
3425
-
3426
- // Copilot windows carry `reset_at` as an ISO string built from the monthly
3427
- // quota_reset_date. A window whose reset has already passed is stale data, not a
3428
- // usable fallback, so drop it. Windows without a reset stamp are kept — the overall
3429
- // cached_at max-age gate already bounds them.
3430
- function isCopilotCacheWindowUsable(window, { nowMs } = {}) {
3431
- if (!window || typeof window !== "object") return false;
3432
- const resetAtMs = parseTimeMs(window.reset_at);
3433
- if (resetAtMs === null) return true;
3434
- return resetAtMs > nowMs;
3435
- }
3436
-
3437
- function normalizeGrokCachedLimits(
3438
- raw,
3439
- { nowMs = Date.now(), maxAgeMs = GROK_LIMITS_CACHE_MAX_AGE_MS } = {},
3440
- ) {
3441
- const cachedAtMs = parseTimeMs(raw?.cached_at);
3442
- if (!Number.isFinite(cachedAtMs)) return null;
3443
- if (cachedAtMs > nowMs + 60_000) return null;
3444
- if (nowMs - cachedAtMs > maxAgeMs) return null;
3445
-
3446
- const cached = {
3447
- configured: true,
3448
- error: null,
3449
- plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
3450
- period_type: typeof raw?.period_type === "string" ? raw.period_type : null,
3451
- monthly_credits_limit: raw?.monthly_credits_limit ?? null,
3452
- monthly_credits_used: raw?.monthly_credits_used ?? null,
3453
- credit_usage_percent: raw?.credit_usage_percent ?? null,
3454
- on_demand_cap: raw?.on_demand_cap ?? null,
3455
- on_demand_used: raw?.on_demand_used ?? null,
3456
- billing_period_start: typeof raw?.billing_period_start === "string" ? raw.billing_period_start : null,
3457
- primary_window: isGrokCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3458
- secondary_window: isGrokCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3459
- // Per-product shares of the pool, not independent windows — each is gated
3460
- // by the same reset stamp as the pool window it attributes.
3461
- products: Array.isArray(raw?.products)
3462
- ? raw.products.filter((entry) => isGrokCacheWindowUsable(entry?.window, { nowMs }))
3463
- : [],
3464
- stale: true,
3465
- cached_at: raw.cached_at,
3466
- };
3467
- return hasGrokWindow(cached) ? cached : null;
3468
- }
2923
+ // The endpoint-scoped profile (home + OAuth host) partitions the cache: one
2924
+ // slot must never serve another region's windows (ark profile_identity
2925
+ // precedent). Windows carry `reset_at` (ISO) when the API reports a cycle
2926
+ // end; a window whose reset has already passed is stale data, not a usable
2927
+ // fallback, so drop it.
2928
+ const kimiLimitsCache = createLimitsCache({
2929
+ file: KIMI_LIMITS_CACHE_FILE,
2930
+ key: "kimi",
2931
+ maxAgeMs: KIMI_LIMITS_CACHE_MAX_AGE_MS,
2932
+ fields: { membership_level: "string", subscription_type: "string", parallel_limit: "number" },
2933
+ windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
2934
+ identityField: "identity",
2935
+ identityRequired: true,
2936
+ });
3469
2937
 
3470
2938
  // The signed-in Grok account partitions the cache (zcode login-identity precedent):
3471
2939
  // the id is the non-secret OIDC `sub` claim inside the access token — the
@@ -3476,106 +2944,34 @@ function normalizeGrokCachedLimits(
3476
2944
  // (fail-closed) — an unattributable login must never pin another account's bars,
3477
2945
  // which a scope-key fallback cannot guarantee because `issuer::client_id` stays
3478
2946
  // constant across account switches.
3479
- function readGrokLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3480
- if (!identity) return null;
3481
- try {
3482
- const parsed = JSON.parse(fs.readFileSync(resolveGrokLimitsCachePath({ home }), "utf8"));
3483
- const entry = parsed?.grok;
3484
- if (entry?.identity !== identity) return null;
3485
- return normalizeGrokCachedLimits(entry, { nowMs });
3486
- } catch (_error) {
3487
- return null;
3488
- }
3489
- }
3490
-
3491
- function normalizeCopilotCachedLimits(
3492
- raw,
3493
- { nowMs = Date.now(), maxAgeMs = COPILOT_LIMITS_CACHE_MAX_AGE_MS } = {},
3494
- ) {
3495
- const cachedAtMs = parseTimeMs(raw?.cached_at);
3496
- if (!Number.isFinite(cachedAtMs)) return null;
3497
- if (cachedAtMs > nowMs + 60_000) return null;
3498
- if (nowMs - cachedAtMs > maxAgeMs) return null;
3499
-
3500
- const cached = {
3501
- configured: true,
3502
- error: null,
3503
- plan_name: typeof raw?.plan_name === "string" ? raw.plan_name : null,
3504
- primary_window: isCopilotCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3505
- secondary_window: isCopilotCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3506
- stale: true,
3507
- cached_at: raw.cached_at,
3508
- };
3509
- return hasCopilotWindow(cached) ? cached : null;
3510
- }
3511
-
3512
- // The signed-in GitHub account partitions the cache (zcode login-identity precedent):
3513
- // the id is the non-secret GitHub login stored alongside the OAuth token (the
3514
- // plaintext files' `user` field, or the auth.db row's `user_login`). A single home
3515
- // resolves one active token, so one stamped entry suffices; a read requires the stored
3516
- // identity to match the current one, and a write without an identity is refused — an
3517
- // unreadable login must never pin another account's bars in place.
3518
- function readCopilotLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3519
- if (!identity) return null;
3520
- try {
3521
- const parsed = JSON.parse(fs.readFileSync(resolveCopilotLimitsCachePath({ home }), "utf8"));
3522
- const entry = parsed?.copilot;
3523
- if (entry?.identity !== identity) return null;
3524
- return normalizeCopilotCachedLimits(entry, { nowMs });
3525
- } catch (_error) {
3526
- return null;
3527
- }
3528
- }
3529
-
3530
- function writeGrokLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3531
- if (!identity) return;
3532
- if (!limits?.configured || limits.error || !hasGrokWindow(limits)) return;
3533
- const cachePath = resolveGrokLimitsCachePath({ home });
3534
- const payload = {
3535
- grok: {
3536
- identity,
3537
- plan_label: limits.plan_label || null,
3538
- period_type: limits.period_type || null,
3539
- monthly_credits_limit: limits.monthly_credits_limit ?? null,
3540
- monthly_credits_used: limits.monthly_credits_used ?? null,
3541
- credit_usage_percent: limits.credit_usage_percent ?? null,
3542
- on_demand_cap: limits.on_demand_cap ?? null,
3543
- on_demand_used: limits.on_demand_used ?? null,
3544
- billing_period_start: limits.billing_period_start || null,
3545
- primary_window: limits.primary_window || null,
3546
- secondary_window: limits.secondary_window || null,
3547
- products: Array.isArray(limits.products) ? limits.products : [],
3548
- cached_at: new Date(nowMs).toISOString(),
2947
+ const grokLimitsCache = createLimitsCache({
2948
+ file: GROK_LIMITS_CACHE_FILE,
2949
+ key: "grok",
2950
+ maxAgeMs: GROK_LIMITS_CACHE_MAX_AGE_MS,
2951
+ fields: {
2952
+ plan_label: "string",
2953
+ period_type: "string",
2954
+ monthly_credits_limit: "raw",
2955
+ monthly_credits_used: "raw",
2956
+ credit_usage_percent: "raw",
2957
+ on_demand_cap: "raw",
2958
+ on_demand_used: "raw",
2959
+ billing_period_start: "string",
2960
+ // Per-product shares of the pool, not independent windows — each is
2961
+ // gated by the same reset stamp as the pool window it attributes.
2962
+ products: {
2963
+ write: (record) => (Array.isArray(record?.products) ? record.products : []),
2964
+ read: (raw, ctx) => (Array.isArray(raw?.products)
2965
+ ? raw.products.filter((entry) => ctx.isWindowUsable(entry?.window, { nowMs: ctx.nowMs }))
2966
+ : []),
3549
2967
  },
3550
- };
3551
- try {
3552
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3553
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
3554
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3555
- fs.renameSync(tmpPath, cachePath);
3556
- } catch (_error) {}
3557
- }
3558
-
3559
- function writeCopilotLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3560
- if (!identity) return;
3561
- if (!limits?.configured || limits.error || !hasCopilotWindow(limits)) return;
3562
- const cachePath = resolveCopilotLimitsCachePath({ home });
3563
- const payload = {
3564
- copilot: {
3565
- identity,
3566
- plan_name: limits.plan_name || null,
3567
- primary_window: limits.primary_window || null,
3568
- secondary_window: limits.secondary_window || null,
3569
- cached_at: new Date(nowMs).toISOString(),
3570
- },
3571
- };
3572
- try {
3573
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3574
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
3575
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3576
- fs.renameSync(tmpPath, cachePath);
3577
- } catch (_error) {}
3578
- }
2968
+ },
2969
+ // Windows carry `reset_at` as an ISO string — the billing-period end shared
2970
+ // by the pool, on-demand, and per-product windows.
2971
+ windowKeys: ["primary_window", "secondary_window"],
2972
+ identityField: "identity",
2973
+ identityRequired: true,
2974
+ });
3579
2975
 
3580
2976
  // Failure-only identity re-derivation for the cache fallback: when the provider
3581
2977
  // slot's result was produced by the outer timeout race, the fetcher's own
@@ -3595,6 +2991,24 @@ function resolveGrokCacheIdentity({ home, env } = {}) {
3595
2991
  // the identity always belongs to the account the token resolution would pick. Async
3596
2992
  // like the fetcher's own resolution so a failed poll never blocks the embedded
3597
2993
  // server's event loop on SQLite / Keychain.
2994
+ // The signed-in GitHub account partitions the cache (zcode login-identity precedent):
2995
+ // the id is the non-secret GitHub login stored alongside the OAuth token (the
2996
+ // plaintext files' `user` field, or the auth.db row's `user_login`). A single home
2997
+ // resolves one active token, so one stamped entry suffices; a read requires the stored
2998
+ // identity to match the current one, and a write without an identity is refused — an
2999
+ // unreadable login must never pin another account's bars in place.
3000
+ const copilotLimitsCache = createLimitsCache({
3001
+ file: COPILOT_LIMITS_CACHE_FILE,
3002
+ key: "copilot",
3003
+ maxAgeMs: COPILOT_LIMITS_CACHE_MAX_AGE_MS,
3004
+ fields: { plan_name: "string" },
3005
+ // Windows carry `reset_at` as an ISO string built from the monthly
3006
+ // quota_reset_date.
3007
+ windowKeys: ["primary_window", "secondary_window"],
3008
+ identityField: "identity",
3009
+ identityRequired: true,
3010
+ });
3011
+
3598
3012
  async function resolveCopilotCacheIdentity({
3599
3013
  home = os.homedir(),
3600
3014
  platform = process.platform,
@@ -3609,84 +3023,67 @@ async function resolveCopilotCacheIdentity({
3609
3023
  }
3610
3024
  }
3611
3025
 
3612
- function resolveDevinLimitsCachePath({ home } = {}) {
3613
- return path.join(home || os.homedir(), ".tokentracker", "tracker", DEVIN_LIMITS_CACHE_FILE);
3614
- }
3026
+ // The signed-in Google account partitions the cache (zcode login-identity
3027
+ // precedent): the email decoded from the local id_token JWT is non-secret and is
3028
+ // the same id the payload already surfaces as account_email. GEMINI_HOME can aim
3029
+ // the credential lookup at a different installation, but the cache stays keyed by
3030
+ // account — a read requires the stored identity to match the account the current
3031
+ // credentials resolve to, and a write without an identity is refused: an
3032
+ // unreadable login must never pin another account's bars in place.
3033
+ const geminiLimitsCache = createLimitsCache({
3034
+ file: GEMINI_LIMITS_CACHE_FILE,
3035
+ key: "gemini",
3036
+ maxAgeMs: GEMINI_LIMITS_CACHE_MAX_AGE_MS,
3037
+ fields: { account_email: "string", account_plan: "string" },
3038
+ // Windows carry `reset_at` as an ISO string (the Code Assist bucket's own
3039
+ // resetTime — daily for free-tier quotas).
3040
+ windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
3041
+ identityField: "identity",
3042
+ identityRequired: true,
3043
+ });
3615
3044
 
3616
- function hasDevinWindow(limits) {
3617
- return Boolean(limits?.primary_window || limits?.secondary_window);
3045
+ // Failure-only identity re-derivation for the cache fallback: when the provider
3046
+ // slot's result was produced by the outer timeout race, the fetcher's own
3047
+ // identity field is lost, so decode the (still-local) id_token once to learn
3048
+ // who we are serving for.
3049
+ function resolveGeminiCacheIdentity({ home, env } = {}) {
3050
+ try {
3051
+ return decodeJwtPayload(loadGeminiCredentials({ home, env })?.id_token)?.email || null;
3052
+ } catch (_error) {
3053
+ return null;
3054
+ }
3618
3055
  }
3619
3056
 
3620
- // Devin windows carry `reset_at` as an ISO string. A window whose reset has
3621
- // already passed is stale data, not a usable fallback, so drop it. Windows
3622
- // without a reset stamp are kept — the overall cached_at max-age gate already
3623
- // bounds them.
3624
- function isDevinCacheWindowUsable(window, { nowMs } = {}) {
3625
- if (!window || typeof window !== "object") return false;
3626
- const resetAtMs = parseTimeMs(window.reset_at);
3627
- if (resetAtMs === null) return true;
3628
- return resetAtMs > nowMs;
3057
+ const devinLimitsCache = createLimitsCache({
3058
+ file: DEVIN_LIMITS_CACHE_FILE,
3059
+ key: "devin",
3060
+ maxAgeMs: DEVIN_LIMITS_CACHE_MAX_AGE_MS,
3061
+ fields: { plan_label: "string" },
3062
+ windowKeys: ["primary_window", "secondary_window"],
3063
+ // Partition by the current credential-file revision, including on timeout
3064
+ // fallback. Legacy unpartitioned entries are deliberately ignored.
3065
+ identityField: "identity",
3066
+ identityRequired: true,
3067
+ });
3068
+
3069
+ function resolveDevinLimitsCachePath({ home } = {}) {
3070
+ return devinLimitsCache.path({ home });
3629
3071
  }
3630
3072
 
3631
- function normalizeDevinCachedLimits(
3632
- raw,
3633
- { nowMs = Date.now(), maxAgeMs = DEVIN_LIMITS_CACHE_MAX_AGE_MS } = {},
3634
- ) {
3635
- const cachedAtMs = parseTimeMs(raw?.cached_at);
3636
- if (!Number.isFinite(cachedAtMs)) return null;
3637
- if (cachedAtMs > nowMs + 60_000) return null;
3638
- if (nowMs - cachedAtMs > maxAgeMs) return null;
3639
-
3640
- const cached = {
3641
- configured: true,
3642
- error: null,
3643
- plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
3644
- primary_window: isDevinCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3645
- secondary_window: isDevinCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3646
- stale: true,
3647
- cached_at: raw.cached_at,
3648
- };
3649
- return hasDevinWindow(cached) ? cached : null;
3073
+ function hasDevinWindow(limits) {
3074
+ return Boolean(limits?.primary_window || limits?.secondary_window);
3650
3075
  }
3651
3076
 
3652
- // Match the current credential-file revision, including on timeout fallback.
3653
- // Legacy unpartitioned entries are deliberately ignored.
3654
3077
  function readDevinLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3655
- if (!identity) return null;
3656
- try {
3657
- const parsed = JSON.parse(fs.readFileSync(resolveDevinLimitsCachePath({ home }), "utf8"));
3658
- if (parsed?.devin?.identity !== identity) return null;
3659
- return normalizeDevinCachedLimits(parsed.devin, { nowMs });
3660
- } catch (_error) {
3661
- return null;
3662
- }
3078
+ return devinLimitsCache.read({ home, nowMs, identity });
3663
3079
  }
3664
3080
 
3665
3081
  function writeDevinLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3666
- if (!identity) return;
3667
- if (!limits?.configured || limits.error || !hasDevinWindow(limits)) return;
3668
- const cachePath = resolveDevinLimitsCachePath({ home });
3669
- const payload = {
3670
- devin: {
3671
- identity,
3672
- plan_label: limits.plan_label || null,
3673
- primary_window: limits.primary_window || null,
3674
- secondary_window: limits.secondary_window || null,
3675
- cached_at: new Date(nowMs).toISOString(),
3676
- },
3677
- };
3678
- try {
3679
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3680
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
3681
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3682
- fs.renameSync(tmpPath, cachePath);
3683
- } catch (_error) {}
3082
+ devinLimitsCache.write(limits, { home, nowMs, identity });
3684
3083
  }
3685
3084
 
3686
3085
  function clearDevinLimitsCache({ home } = {}) {
3687
- try {
3688
- fs.rmSync(resolveDevinLimitsCachePath({ home }), { force: true });
3689
- } catch (_error) {}
3086
+ devinLimitsCache.clear({ home });
3690
3087
  }
3691
3088
 
3692
3089
  // Only transient failures fall back to the disk cache. Requests without a
@@ -5635,41 +5032,23 @@ async function fetchUsageLimitsUncached({
5635
5032
  // "fetch failed" (mirrors Codex / Claude / OpenCode Go). A session-expiry error
5636
5033
  // (401/403 — "re-login in Cursor") is actionable and must not be hidden behind
5637
5034
  // stale cache, so it passes through untouched.
5638
- let cursorWithCache;
5639
- if (!cursor || cursor.configured === false) {
5640
- cursorWithCache = cursor;
5641
- } else if (cursor.auth_error === true) {
5642
- cursorWithCache = cursor;
5643
- } else if (!cursor.error && hasCursorWindow(cursor)) {
5644
- cursorWithCache = {
5645
- ...cursor,
5646
- stale: false,
5647
- cached_at: new Date(nowMs).toISOString(),
5648
- };
5649
- if (cacheGeneration === usageLimitsCacheGeneration) {
5650
- writeCursorLimitsCache(cursorWithCache, {
5651
- home,
5652
- nowMs,
5653
- identity: typeof cursor.cache_identity === "string"
5654
- ? cursor.cache_identity
5655
- : resolveCursorCacheIdentity({ home, env, trackerDir }),
5656
- });
5657
- }
5658
- } else {
5035
+ const cursorWithCache = await withLimitsCacheFallback(cursor, {
5036
+ cache: cursorLimitsCache,
5037
+ nowMs,
5038
+ home,
5039
+ env,
5040
+ isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
5659
5041
  // The slot's error object loses the fetcher's identity field when the outer
5660
- // timeout race wins (the common failure mode this cache exists for), so
5661
- // re-derive it from the local state DB on this failure-only path.
5662
- const identity = typeof cursor.cache_identity === "string"
5042
+ // timeout race wins (the common failure mode this cache exists for), so the
5043
+ // read path re-derives it from the local state DB.
5044
+ resolveWriteIdentity: (slot) => typeof slot.cache_identity === "string"
5045
+ ? slot.cache_identity
5046
+ : resolveCursorCacheIdentity({ home, env }),
5047
+ resolveReadIdentity: () => typeof cursor.cache_identity === "string"
5663
5048
  ? cursor.cache_identity
5664
- : resolveCursorCacheIdentity({ home, env, trackerDir });
5665
- const cached = readCursorLimitsCache({ home, nowMs, identity });
5666
- cursorWithCache = cached || cursor;
5667
- }
5668
- // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5669
- if (cursorWithCache && ("cache_identity" in cursorWithCache || "auth_error" in cursorWithCache)) {
5670
- const { cache_identity: _identity, auth_error: _authError, ...cursorPayload } = cursorWithCache;
5671
- cursorWithCache = cursorPayload;
5672
- }
5049
+ : resolveCursorCacheIdentity({ home, env, trackerDir }),
5050
+ passthrough: (slot) => slot.auth_error === true,
5051
+ });
5673
5052
 
5674
5053
  // Grok: persist last successful windows so a transient timeout/5xx on the
5675
5054
  // cli-chat-proxy.grok.com route serves stale bars instead of a flashing
@@ -5677,43 +5056,26 @@ async function fetchUsageLimitsUncached({
5677
5056
  // (GROK_AUTH_REQUIRED / GROK_REAUTH_REQUIRED — "run `grok login`") is
5678
5057
  // actionable and must not be hidden behind stale cache, so it passes
5679
5058
  // through untouched.
5680
- let grokWithCache;
5681
- if (!grok || grok.configured === false) {
5682
- grokWithCache = grok;
5683
- } else if (grok.auth_error === true) {
5684
- grokWithCache = grok;
5685
- } else if (!grok.error && hasGrokWindow(grok)) {
5686
- grokWithCache = {
5687
- ...grok,
5688
- stale: false,
5689
- cached_at: new Date(nowMs).toISOString(),
5690
- };
5059
+ const grokWithCache = await withLimitsCacheFallback(grok, {
5060
+ cache: grokLimitsCache,
5061
+ nowMs,
5062
+ home,
5063
+ env,
5064
+ isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
5691
5065
  // Write only under the identity the fetch itself carried: re-reading
5692
5066
  // auth.json here could attribute a finished fetch's windows to an account
5693
5067
  // that logged in while the request was in flight (opaque → JWT switch).
5694
5068
  // No fetch-side identity (opaque token) → refuse the write (fail-closed).
5695
- if (cacheGeneration === usageLimitsCacheGeneration && typeof grok.cache_identity === "string") {
5696
- writeGrokLimitsCache(grokWithCache, {
5697
- home,
5698
- nowMs,
5699
- identity: grok.cache_identity,
5700
- });
5701
- }
5702
- } else {
5703
- // The slot's error object loses the fetcher's identity field when the outer
5704
- // timeout race wins (the common failure mode this cache exists for), so
5705
- // re-derive it from auth.json on this failure-only path.
5706
- const identity = typeof grok.cache_identity === "string"
5069
+ resolveWriteIdentity: (slot) => typeof slot.cache_identity === "string"
5070
+ ? slot.cache_identity
5071
+ : null,
5072
+ // Re-derive from auth.json: the timeout race strips the fetcher's own
5073
+ // identity field.
5074
+ resolveReadIdentity: () => typeof grok.cache_identity === "string"
5707
5075
  ? grok.cache_identity
5708
- : resolveGrokCacheIdentity({ home, env });
5709
- const cached = readGrokLimitsCache({ home, nowMs, identity });
5710
- grokWithCache = cached || grok;
5711
- }
5712
- // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5713
- if (grokWithCache && ("cache_identity" in grokWithCache || "auth_error" in grokWithCache)) {
5714
- const { cache_identity: _identity, auth_error: _authError, ...grokPayload } = grokWithCache;
5715
- grokWithCache = grokPayload;
5716
- }
5076
+ : resolveGrokCacheIdentity({ home, env }),
5077
+ passthrough: (slot) => slot.auth_error === true,
5078
+ });
5717
5079
 
5718
5080
  const cursorLimits = cursorSandDuplicatesGrokPool(cursorWithCache, grokWithCache)
5719
5081
  ? { ...cursorWithCache, quaternary_window: null }
@@ -5725,38 +5087,32 @@ async function fetchUsageLimitsUncached({
5725
5087
  // passes through untouched. The otel probe fields ride along on every slot
5726
5088
  // result, so a cached overlay spreads the live object first and lets the cached
5727
5089
  // entry win only the limit fields.
5728
- let copilotWithCache;
5729
- if (!copilot || copilot.configured === false) {
5730
- copilotWithCache = copilot;
5731
- } else if (copilot.auth_error === true) {
5732
- copilotWithCache = copilot;
5733
- } else if (!copilot.error && hasCopilotWindow(copilot)) {
5734
- copilotWithCache = {
5735
- ...copilot,
5736
- stale: false,
5737
- cached_at: new Date(nowMs).toISOString(),
5738
- };
5739
- // Only the login captured with the request can own its windows. An
5740
- // unknown login must never be promoted to a newly signed-in account.
5741
- const identity = copilot.cache_identity;
5742
- if (typeof identity === "string" && identity
5743
- && identity === await resolveCopilotCacheIdentity({ home, platform, securityRunner })
5744
- && cacheGeneration === usageLimitsCacheGeneration) {
5745
- writeCopilotLimitsCache(copilotWithCache, { home, nowMs, identity });
5746
- }
5747
- } else {
5748
- // Re-read the current login even when the failed request carries an identity:
5749
- // the account may have changed while the request was in flight.
5750
- const identity = await resolveCopilotCacheIdentity({ home, platform, securityRunner });
5751
- // Sample after the async auth lookup too: a slow poll can straddle a reset.
5752
- const cached = readCopilotLimitsCache({ home, nowMs: Date.now(), identity });
5753
- copilotWithCache = cached ? { ...copilot, ...cached } : copilot;
5754
- }
5755
- // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5756
- if (copilotWithCache && ("cache_identity" in copilotWithCache || "auth_error" in copilotWithCache)) {
5757
- const { cache_identity: _identity, auth_error: _authError, ...copilotPayload } = copilotWithCache;
5758
- copilotWithCache = copilotPayload;
5759
- }
5090
+ const copilotWithCache = await withLimitsCacheFallback(copilot, {
5091
+ cache: copilotLimitsCache,
5092
+ nowMs,
5093
+ home,
5094
+ env,
5095
+ isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
5096
+ // Only the login captured with the request can own its windows: an
5097
+ // unknown login must never be promoted to a newly signed-in account, and
5098
+ // a fresh read of the auth store must still agree — a login that switched
5099
+ // while the request was in flight refuses the write.
5100
+ resolveWriteIdentity: async (slot) => {
5101
+ const identity = typeof slot.cache_identity === "string" ? slot.cache_identity : null;
5102
+ if (!identity) return null;
5103
+ const current = await resolveCopilotCacheIdentity({ home, platform, securityRunner });
5104
+ return identity === current ? identity : null;
5105
+ },
5106
+ beforeRead: async () => {
5107
+ // Re-read the current login even when the failed request carries an
5108
+ // identity: the account may have changed while the request was in flight.
5109
+ const identity = await resolveCopilotCacheIdentity({ home, platform, securityRunner });
5110
+ // Sample after the async auth lookup too: a slow poll can straddle a reset.
5111
+ return { identity, nowMs: Date.now() };
5112
+ },
5113
+ passthrough: (slot) => slot.auth_error === true,
5114
+ merge: "overlay",
5115
+ });
5760
5116
 
5761
5117
  // Kimi: persist last successful windows so a transient timeout/5xx on the
5762
5118
  // api.kimi.com / api.kimi.ai coding routes serves stale bars instead of a
@@ -5764,103 +5120,23 @@ async function fetchUsageLimitsUncached({
5764
5120
  // errors ("Not logged in to Kimi" / "token_expired") and actionable profile
5765
5121
  // misconfigurations must not be hidden behind stale cache, so they pass
5766
5122
  // through untouched.
5767
- let kimiWithCache;
5768
- if (!kimi || kimi.configured === false) {
5769
- kimiWithCache = kimi;
5770
- } else if (kimi.auth_error === true || kimi.config_error === true) {
5771
- kimiWithCache = kimi;
5772
- } else if (!kimi.error && hasKimiWindow(kimi)) {
5773
- kimiWithCache = {
5774
- ...kimi,
5775
- stale: false,
5776
- cached_at: new Date(nowMs).toISOString(),
5777
- };
5778
- if (cacheGeneration === usageLimitsCacheGeneration) {
5779
- writeKimiLimitsCache(kimiWithCache, {
5780
- home,
5781
- nowMs,
5782
- identity: typeof kimi.cache_identity === "string"
5783
- ? kimi.cache_identity
5784
- : resolveKimiCacheIdentity({ home, env }),
5785
- });
5786
- }
5787
- } else {
5788
- // The slot's error object loses the fetcher's identity field when the outer
5789
- // timeout race wins (the common failure mode this cache exists for), so
5790
- // re-derive it from the local Kimi profile on this failure-only path.
5791
- const identity = typeof kimi.cache_identity === "string"
5123
+ const kimiWithCache = await withLimitsCacheFallback(kimi, {
5124
+ cache: kimiLimitsCache,
5125
+ nowMs,
5126
+ home,
5127
+ env,
5128
+ isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
5129
+ resolveWriteIdentity: (slot) => typeof slot.cache_identity === "string"
5130
+ ? slot.cache_identity
5131
+ : resolveKimiCacheIdentity({ home, env }),
5132
+ // Re-derive from the local Kimi profile: the timeout race strips the
5133
+ // fetcher's own identity field.
5134
+ resolveReadIdentity: () => typeof kimi.cache_identity === "string"
5792
5135
  ? kimi.cache_identity
5793
- : resolveKimiCacheIdentity({ home, env });
5794
- const cached = readKimiLimitsCache({ home, nowMs, identity });
5795
- kimiWithCache = cached || kimi;
5796
- }
5797
- // cache_identity / auth_error / config_error are internal cache-plumbing
5798
- // markers, not payload.
5799
- if (kimiWithCache && ("cache_identity" in kimiWithCache || "auth_error" in kimiWithCache || "config_error" in kimiWithCache)) {
5800
- const {
5801
- cache_identity: _kimiIdentity,
5802
- auth_error: _kimiAuthError,
5803
- config_error: _kimiConfigError,
5804
- ...kimiPayload
5805
- } = kimiWithCache;
5806
- kimiWithCache = kimiPayload;
5807
- }
5808
-
5809
- // Gemini: persist last successful windows so a transient timeout/5xx on the
5810
- // cloudcode-pa.googleapis.com routes serves stale bars instead of a flashing
5811
- // "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Auth errors (a dead
5812
- // OAuth session, or the unsupported api-key / vertex-ai auth modes) are
5813
- // actionable and must not be hidden behind stale cache, so they pass through
5814
- // untouched.
5815
- let geminiWithCache;
5816
- if (!gemini || gemini.configured === false) {
5817
- geminiWithCache = gemini;
5818
- } else if (gemini.auth_error === true) {
5819
- geminiWithCache = gemini;
5820
- } else if (!gemini.error && hasGeminiWindow(gemini)) {
5821
- geminiWithCache = {
5822
- ...gemini,
5823
- stale: false,
5824
- cached_at: new Date(nowMs).toISOString(),
5825
- };
5826
- if (cacheGeneration === usageLimitsCacheGeneration) {
5827
- // A slow in-flight fetch must not clobber the on-disk entry of an
5828
- // account that has since been switched away: only write when the
5829
- // snapshot's identity still matches the current credentials.
5830
- const currentIdentity = resolveGeminiCacheIdentity({ home, env });
5831
- const snapshotIdentity = typeof gemini.cache_identity === "string"
5832
- ? gemini.cache_identity
5833
- : currentIdentity;
5834
- if (snapshotIdentity && snapshotIdentity === currentIdentity) {
5835
- writeGeminiLimitsCache(geminiWithCache, {
5836
- home,
5837
- nowMs,
5838
- identity: snapshotIdentity,
5839
- });
5840
- }
5841
- }
5842
- } else {
5843
- // The slot's error object loses the fetcher's identity field when the outer
5844
- // timeout race wins (the common failure mode this cache exists for). Always
5845
- // re-derive from the CURRENT local credentials — the request may have
5846
- // started under an account that has since been switched away, in which
5847
- // case the read's identity gate refuses the old entry. Revalidate the auth
5848
- // mode too: a mid-flight switch to api-key/vertex-ai never reached the
5849
- // punch-through branch above, but stale OAuth bars would still mislead.
5850
- const selectedType = loadGeminiSettings({ home, env })?.security?.auth?.selectedType ?? null;
5851
- if (selectedType === "api-key" || selectedType === "vertex-ai") {
5852
- geminiWithCache = gemini;
5853
- } else {
5854
- const identity = resolveGeminiCacheIdentity({ home, env });
5855
- const cached = readGeminiLimitsCache({ home, nowMs, identity });
5856
- geminiWithCache = cached || gemini;
5857
- }
5858
- }
5859
- // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5860
- if (geminiWithCache && ("cache_identity" in geminiWithCache || "auth_error" in geminiWithCache)) {
5861
- const { cache_identity: _identity, auth_error: _authError, ...geminiPayload } = geminiWithCache;
5862
- geminiWithCache = geminiPayload;
5863
- }
5136
+ : resolveKimiCacheIdentity({ home, env }),
5137
+ passthrough: (slot) => slot.auth_error === true || slot.config_error === true,
5138
+ markers: ["cache_identity", "auth_error", "config_error"],
5139
+ });
5864
5140
 
5865
5141
  // Kiro: the slot runs the local kiro-cli (its own 2s/20s command budgets,
5866
5142
  // never rejects), so the failure modes are CLI timeouts, unparseable panels
@@ -5874,28 +5150,22 @@ async function fetchUsageLimitsUncached({
5874
5150
  // account switch mid-flight refuses the write instead of stamping the
5875
5151
  // previous account's panel under the new login.
5876
5152
  const kiroAuthBlocked = isKiroAuthError(kiro?.error);
5877
- let kiroWithCache;
5878
- if (!kiro || kiro.configured === false || kiroAuthBlocked) {
5879
- kiroWithCache = kiro;
5880
- } else if (!kiro.error && hasKiroWindow(kiro)) {
5881
- kiroWithCache = {
5882
- ...kiro,
5883
- stale: false,
5884
- cached_at: new Date(nowMs).toISOString(),
5885
- };
5886
- if (cacheGeneration === usageLimitsCacheGeneration) {
5887
- const identity = typeof kiro.cache_identity === "string"
5888
- ? kiro.cache_identity
5153
+ const kiroWithCache = await withLimitsCacheFallback(kiro, {
5154
+ cache: kiroLimitsCache,
5155
+ nowMs,
5156
+ home,
5157
+ env,
5158
+ isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
5159
+ resolveWriteIdentity: (slot) => {
5160
+ const identity = typeof slot.cache_identity === "string"
5161
+ ? slot.cache_identity
5889
5162
  : resolveKiroAuthIdentity({ home });
5890
- if (identity && identity === resolveKiroAuthIdentity({ home })) {
5891
- writeKiroLimitsCache(kiroWithCache, { home, nowMs, identity });
5892
- }
5893
- }
5894
- } else if (!kiro.error) {
5895
- kiroWithCache = kiro;
5896
- } else {
5897
- const cached = readKiroLimitsCache({
5898
- home,
5163
+ return identity && identity === resolveKiroAuthIdentity({ home })
5164
+ ? identity
5165
+ : null;
5166
+ },
5167
+ passthrough: (slot) => kiroAuthBlocked || !slot.error,
5168
+ beforeRead: () => ({
5899
5169
  // Gate against the wall clock, not the aggregate's entry stamp: a slow
5900
5170
  // failed CLI read can straddle a reset, and an expired window must not
5901
5171
  // pass the reset gate on a timestamp from before the read started.
@@ -5903,16 +5173,53 @@ async function fetchUsageLimitsUncached({
5903
5173
  // The current login, never the fetch-time capture — a switch that
5904
5174
  // happened mid-flight must not match the previous account's entry.
5905
5175
  identity: resolveKiroAuthIdentity({ home }),
5906
- });
5176
+ }),
5907
5177
  // The failed slot already carries the credits-sidecar fields the fetcher
5908
5178
  // merged in; the cached windows overlay on top of them.
5909
- kiroWithCache = cached ? { ...kiro, ...cached } : kiro;
5910
- }
5911
- // cache_identity is internal cache plumbing, not payload.
5912
- if (kiroWithCache && "cache_identity" in kiroWithCache) {
5913
- const { cache_identity: _identity, ...kiroPayload } = kiroWithCache;
5914
- kiroWithCache = kiroPayload;
5915
- }
5179
+ merge: "overlay",
5180
+ markers: ["cache_identity"],
5181
+ });
5182
+
5183
+ // Gemini: persist last successful windows so a transient timeout/5xx on the
5184
+ // cloudcode-pa.googleapis.com routes serves stale bars instead of a flashing
5185
+ // "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Auth errors (a dead
5186
+ // OAuth session, or the unsupported api-key / vertex-ai auth modes) are
5187
+ // actionable and must not be hidden behind stale cache, so they pass through
5188
+ // untouched.
5189
+ const geminiWithCache = await withLimitsCacheFallback(gemini, {
5190
+ cache: geminiLimitsCache,
5191
+ nowMs,
5192
+ home,
5193
+ env,
5194
+ isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
5195
+ // A slow in-flight fetch must not clobber the on-disk entry of an
5196
+ // account that has since been switched away: only write when the
5197
+ // snapshot's own identity still matches the current credentials. An
5198
+ // identity-less snapshot (the id_token read at fetch start carried no
5199
+ // email) cannot be attributed to anyone — never promote it to the
5200
+ // account now holding the credentials.
5201
+ resolveWriteIdentity: (slot) => {
5202
+ const currentIdentity = resolveGeminiCacheIdentity({ home, env });
5203
+ const snapshotIdentity = typeof slot.cache_identity === "string"
5204
+ ? slot.cache_identity
5205
+ : null;
5206
+ return snapshotIdentity && snapshotIdentity === currentIdentity
5207
+ ? snapshotIdentity
5208
+ : null;
5209
+ },
5210
+ passthrough: (slot) => slot.auth_error === true,
5211
+ beforeRead: () => {
5212
+ // Always re-derive from the CURRENT local credentials — the request may
5213
+ // have started under an account that has since been switched away, in
5214
+ // which case the read's identity gate refuses the old entry. Revalidate
5215
+ // the auth mode too: a mid-flight switch to api-key/vertex-ai never
5216
+ // reached the punch-through branch above, but stale OAuth bars would
5217
+ // still mislead.
5218
+ const selectedType = loadGeminiSettings({ home, env })?.security?.auth?.selectedType ?? null;
5219
+ if (selectedType === "api-key" || selectedType === "vertex-ai") return null;
5220
+ return { identity: resolveGeminiCacheIdentity({ home, env }) };
5221
+ },
5222
+ });
5916
5223
 
5917
5224
  const data = {
5918
5225
  fetched_at: new Date(nowMs).toISOString(),