tokentracker-cli 1.5.3 → 1.5.5

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 (137) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-uuDGO9Mq.js → AchievementBadge-BYxdWhS1.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-BCTVT7Jo.js → AchievementsPage-Bv9o7Aj8.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-B6fJSJSr.js → AchievementsSection-DE5bZGxg.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-FjUN__9G.js → BadgeDetailModal-CgZaSVTp.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-B-KqY_Wr.js → BotAnimated-CwhPfUBI.js} +1 -1
  6. package/dashboard/dist/assets/{Card-DS8T8piP.js → Card-Y7qaxMuE.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-_0UjsFPu.js → CloudCheckoutPage-DVl7jk6b.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-BstgVBaV.js → CloudPage-BnIvpHGH.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-DtVsYZgc.js → CommandPalette-CzMv2Zxn.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-DTpEJqV3.js → CommunityStatsModal-D8sOZqUc.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-Lfdh2vr7.js → ConfirmModal-Bun5xKMm.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-DpwQ7pkq.js → Controls-ltfnvkFW.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-C3Xu6_3t.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-CrWgrzT2.js → DateRangePopover-e3gJYqFJ.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-DbK5KWJS.js → DevicePage-JgT16dEp.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-Dd2B_o03.js → DialogClose-DeWert7h.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-Ca6W3nCT.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-3JQBVha3.js → DialogTitle-BfK_yOeR.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-LYAqG8pG.js → ElectricLogo-Bbev0Y8Q.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-DhDvUor7.js → FadeIn-fypYWLDK.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-9f1XmgYC.js → HeaderGithubStar-Cnil2Lbo.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-BXhHAkP7.js → HoverTooltip-Bw_8tru-.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-ChPdDeMl.js → IpCheckPage-CHnhUgA2.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DnJ7Y9zw.js → LandingPage-Bq4iQS6S.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BF9isLzu.js → LeaderboardPage-D42lUdw-.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-BM4JKgkC.js → LeaderboardProBadge-eTKceT90.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-DfZABOpN.js → LeaderboardProfileModal-D-3tRrOR.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-BaZ38iT5.js → LeaderboardProfilePage-DkRCJoiA.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-dUXjP3_1.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-DvhlYoqQ.js → LocalOnlyNotice-CtpjB6Z2.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-g_Gbnzhs.js → LoginCard-zrRd8o54.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-DkJrI6b7.js → LoginModal-DNl5Rt1e.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-CqCKSo0n.js → LoginPage-BuB707vV.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DbF7ROsj.js → PetPage-BpDriXkp.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-4WuH41ZT.js → PopoverPopup-rQNSs8R6.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-6j0A2KD9.js → ProviderIcon-CumyO371.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DBd-iwuM.js → ResetPasswordPage-ecUHfJAk.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BBEpLeiG.js → Select-YVEqEhk4.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-Cn_U_rrO.js → SelectItemText-D1NBc4uc.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-2WZc6Rml.js → SelfHostPage-8nAin68c.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-D1zxuzOg.js +1 -0
  42. package/dashboard/dist/assets/SessionsPage-B6ksEER_.js +3 -0
  43. package/dashboard/dist/assets/{SettingsPage-rXNr96i-.js → SettingsPage-COqP8ZbJ.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CudTvbZL.js → SkillsPage-DCX7xTj0.js} +1 -1
  45. package/dashboard/dist/assets/TokenGalaxy-D4n7AR-R.js +4431 -0
  46. package/dashboard/dist/assets/{TooltipPopup-CsLKXhWC.js → TooltipPopup-DvSoB_eS.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-8onUTbkk.js → TrendMonitor-5GVzh0Ye.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-CcwE0lz8.js → WidgetsPage-BIc4MBiu.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-Cj9LLYqP.js → WrappedPage-BpQk4g0o.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-B_rKFTTN.js +45 -0
  51. package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-ChBA1nAH.js → agent-logos-DVg-sTm4.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-Bb5mfoPc.js → arrow-left-Ch6Q7eU5.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-D1Ti2XUI.js → arrow-right-B2B9GvSI.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-B_CjA_y8.js → arrow-up-right-DwNO_uSb.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-DdfrAqie.js → calendar-days---NjJlBi.js} +1 -1
  57. package/dashboard/dist/assets/{check-29zGw1MK.js → check-D3mhgRrW.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-DAzlFX7g.js → chevron-down-x-b39Zto.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-C2SeCjFM.js → cloud-checkout-CycWyTsg.js} +1 -1
  60. package/dashboard/dist/assets/{composite-DIf735Hq.js → composite-CnA1qtYn.js} +1 -1
  61. package/dashboard/dist/assets/{copy-BOuOM0HQ.js → copy-JRnRMgdX.js} +1 -1
  62. package/dashboard/dist/assets/{download-50tQRpGe.js → download-CRLqGD62.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-BhlQClVe.js → external-link-BrmaWV_t.js} +1 -1
  64. package/dashboard/dist/assets/{flame-CeGfA-0L.js → flame-meQ5-vRD.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-Cq7YiAkJ.js → format-tokens-D8mIJGz1.js} +1 -1
  66. package/dashboard/dist/assets/{gift-Do-UFhiT.js → gift-Cd0FPXgO.js} +1 -1
  67. package/dashboard/dist/assets/{icons-C6EZ_HmP.js → icons-D-PTKw7b.js} +1 -1
  68. package/dashboard/dist/assets/{index-BWhbBAd9.js → index-CMV9UKNf.js} +1 -1
  69. package/dashboard/dist/assets/{index-c0crpe9Z.js → index-C_6gEM83.js} +1 -1
  70. package/dashboard/dist/assets/{index-BFrbVqsm.js → index-DCIZaLNC.js} +1 -1
  71. package/dashboard/dist/assets/index-HLYcwmPD.js +502 -0
  72. package/dashboard/dist/assets/{info-B4MXmsNE.js → info-e0tP9P3D.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-DuWCslzl.js → link-2-DZ2TQhER.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-C2I3NZAO.js → loader-circle-DjflRntu.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-BQasvsUF.js → local-usage-transfer-XTZLA3XH.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-DbeNS6FT.js → modifiers.esm-BpEQ8-Gt.js} +1 -1
  77. package/dashboard/dist/assets/module-BbVsRaE2.js +3 -0
  78. package/dashboard/dist/assets/{monitor-smartphone-C6Y4ktsc.js → monitor-smartphone-VVBcI30G.js} +1 -1
  79. package/dashboard/dist/assets/{play-CwgcDJC2.js → play-DN4Sb3W_.js} +1 -1
  80. package/dashboard/dist/assets/{plus-B796wsUO.js → plus-Cz_k7arH.js} +1 -1
  81. package/dashboard/dist/assets/{provider-display-Cw26RFDm.js → provider-display-S1mSaydF.js} +1 -1
  82. package/dashboard/dist/assets/{react-d7Bk1-_K.js → react-MQBcjrn_.js} +1 -1
  83. package/dashboard/dist/assets/{safePolygon-CDE0dNv_.js → safePolygon-InKxYcXd.js} +1 -1
  84. package/dashboard/dist/assets/{search-DtKvsdRs.js → search-C-N0RDi0.js} +1 -1
  85. package/dashboard/dist/assets/{server-gAURHUyz.js → server-CZkn8vIZ.js} +1 -1
  86. package/dashboard/dist/assets/{shield-check-DFVdDTwh.js → shield-check-DPrGFouZ.js} +1 -1
  87. package/dashboard/dist/assets/{skills-api-2rdPUrJI.js → skills-api-wMwM-GpF.js} +1 -1
  88. package/dashboard/dist/assets/{sortable.esm-DFlfwnJr.js → sortable.esm-CUPP1ihT.js} +1 -1
  89. package/dashboard/dist/assets/{store-BKPCk_de.js → store-BpVdRYnQ.js} +1 -1
  90. package/dashboard/dist/assets/{terminal-BtWFakvS.js → terminal-C-7TFeKg.js} +1 -1
  91. package/dashboard/dist/assets/{trash-bpr9zTAM.js → trash-Be8cd4HD.js} +1 -1
  92. package/dashboard/dist/assets/{use-community-stats-DMnGgLne.js → use-community-stats-cG0WrB4Y.js} +1 -1
  93. package/dashboard/dist/assets/{use-latest-request-guard-BSBlJJHe.js → use-latest-request-guard-C91enyPD.js} +1 -1
  94. package/dashboard/dist/assets/use-limits-display-prefs-DdwoLqPD.js +1 -0
  95. package/dashboard/dist/assets/{use-native-settings-OthqiF6g.js → use-native-settings-BR-NiMKh.js} +1 -1
  96. package/dashboard/dist/assets/{use-ordered-list-DpAtr0R-.js → use-ordered-list-oqV724BP.js} +1 -1
  97. package/dashboard/dist/assets/use-reduced-motion-DW5YHBBI.js +1 -0
  98. package/dashboard/dist/assets/{use-transform-BRz9dz_2.js → use-transform-BsNSgVD6.js} +1 -1
  99. package/dashboard/dist/assets/{use-usage-limits-CwmhudAX.js → use-usage-limits-Dk8LcMU-.js} +1 -1
  100. package/dashboard/dist/assets/{useCurrency-CAA-tQ27.js → useCurrency-FxerRfd2.js} +1 -1
  101. package/dashboard/dist/assets/{usePositioner-Cj2DPmwM.js → usePositioner-D2J-rSfX.js} +1 -1
  102. package/dashboard/dist/assets/{useTokenFormat-Bf5-f7Hx.js → useTokenFormat-CaI2nl2C.js} +1 -1
  103. package/dashboard/dist/assets/{zap-B8Fg6PaC.js → zap-D3C37XAj.js} +1 -1
  104. package/dashboard/dist/index.html +2 -2
  105. package/dashboard/dist/ip-check.html +2 -2
  106. package/dashboard/dist/leaderboard.html +2 -2
  107. package/dashboard/dist/share.html +2 -2
  108. package/package.json +4 -4
  109. package/src/commands/serve.js +3 -2
  110. package/src/lib/accept-encoding.js +59 -0
  111. package/src/lib/cursor-config.js +18 -2
  112. package/src/lib/deepseek-limits.js +280 -0
  113. package/src/lib/grok-limits.js +40 -1
  114. package/src/lib/local-api.js +229 -31
  115. package/src/lib/opencode-status.js +88 -0
  116. package/src/lib/pricing/curated-overrides.json +14 -0
  117. package/src/lib/pricing/index.js +182 -79
  118. package/src/lib/pricing/seed-snapshot.json +1 -1
  119. package/src/lib/provider-api-key-store.js +2 -0
  120. package/src/lib/provider-status.js +17 -5
  121. package/src/lib/rollout.js +13 -4
  122. package/src/lib/session-analytics.js +194 -29
  123. package/src/lib/static-server.js +74 -2
  124. package/src/lib/usage-limits.js +801 -26
  125. package/src/lib/workbuddy-limits.js +106 -14
  126. package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +0 -1
  127. package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +0 -1
  128. package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +0 -6
  129. package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +0 -1
  130. package/dashboard/dist/assets/SessionsPage-CWS0iQ1L.js +0 -3
  131. package/dashboard/dist/assets/TokenGalaxy-BRKrbTsJ.js +0 -4426
  132. package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +0 -45
  133. package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +0 -1
  134. package/dashboard/dist/assets/index-D9kwYWLX.js +0 -502
  135. package/dashboard/dist/assets/module-ZyVsxBG_.js +0 -3
  136. package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +0 -1
  137. package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +0 -1
@@ -27,7 +27,7 @@ const {
27
27
  fetchCursorSandAccessStatus,
28
28
  fetchCursorSandUsageStatus,
29
29
  } = require("./cursor-config");
30
- const { fetchGrokLimits } = require("./grok-limits");
30
+ const { fetchGrokLimits, grokTokenSubject, loadGrokAuthEntry } = require("./grok-limits");
31
31
  const { fetchZcodeLimits } = require("./zcode-limits");
32
32
  const { fetchOpencodeGoLimits } = require("./opencode-go-limits");
33
33
  const { fetchCommandcodeLimits } = require("./commandcode-limits");
@@ -36,6 +36,7 @@ const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
36
36
  const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
37
37
  const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
38
38
  const { fetchWorkbuddyLimits, fetchWorkbuddyAiLimits } = require("./workbuddy-limits");
39
+ const { fetchDeepseekLimits } = require("./deepseek-limits");
39
40
  const { fetchProviderServiceStatus } = require("./provider-status");
40
41
  const { resolveKimiProfile } = require("./kimi-profile");
41
42
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
@@ -70,7 +71,7 @@ const USAGE_LIMITS_PROVIDER_IDS = Object.freeze([
70
71
  "claude", "codex", "cursor", "gemini", "kimi", "kiro", "grok",
71
72
  "copilot", "antigravity", "zcode", "opencodeGo", "commandCode",
72
73
  "qoder", "qoderCn", "codingPlan", "agentPlan", "devin",
73
- "workbuddy", "workbuddyAi",
74
+ "workbuddy", "workbuddyAi", "deepseek",
74
75
  ]);
75
76
 
76
77
  // Normalize a caller-supplied hidden-provider list (array, Set, or
@@ -130,6 +131,7 @@ const ANTIGRAVITY_QUOTA_SUMMARY_URLS = Object.freeze([
130
131
  "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
131
132
  "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
132
133
  ]);
134
+ const ANTIGRAVITY_AVAILABLE_MODELS_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
133
135
  const ANTIGRAVITY_USER_AGENT = "antigravity/2.9.1";
134
136
  const ANTIGRAVITY_TOKEN_REFRESH_SKEW_MS = 60 * 1000;
135
137
  // Per-step ceilings for the Antigravity serial chain. `providerTimeoutMs` bounds the
@@ -172,6 +174,30 @@ const CODEX_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
172
174
  const OPENCODE_GO_LIMITS_CACHE_FILE = "opencode-go-usage-limits-cache.json";
173
175
  const OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
174
176
  const OPENCODE_GO_LOCAL_ESTIMATE_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
177
+ // Cursor's web summary (cursor.com) and Grok Bot RPCs (api2.cursor.sh) both traverse
178
+ // proxied routes where a timeout or 5xx previously blanked the card until the next
179
+ // poll. Persist the last successful windows so a blip serves stale bars instead
180
+ // (mirrors Claude / Codex / OpenCode Go).
181
+ const CURSOR_LIMITS_CACHE_FILE = "cursor-usage-limits-cache.json";
182
+ const CURSOR_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
183
+ // Gemini's Code Assist quota RPCs (cloudcode-pa.googleapis.com) are routinely
184
+ // degraded on GFW-affected routes; a timeout or 5xx previously blanked the card
185
+ // until the next poll. Persist the last successful windows so a blip serves
186
+ // stale bars instead (mirrors Cursor / Codex / OpenCode Go).
187
+ const GEMINI_LIMITS_CACHE_FILE = "gemini-usage-limits-cache.json";
188
+ const GEMINI_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
189
+ // The Kimi coding API (api.kimi.com / api.kimi.ai) is another proxied route where
190
+ // a timeout or 5xx previously blanked the card until the next poll. Persist the
191
+ // last successful windows so a blip serves stale bars instead
192
+ // (mirrors Cursor / Codex / OpenCode Go).
193
+ const KIMI_LIMITS_CACHE_FILE = "kimi-usage-limits-cache.json";
194
+ const KIMI_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
195
+ // Grok Build's billing proxy (cli-chat-proxy.grok.com) sits behind the same kind of
196
+ // proxied routes where a timeout or 5xx previously blanked the card until the next
197
+ // poll. Persist the last successful windows so a blip serves stale bars instead
198
+ // (mirrors Claude / Codex / Cursor).
199
+ const GROK_LIMITS_CACHE_FILE = "grok-usage-limits-cache.json";
200
+ const GROK_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
175
201
  // A 429 from the usage endpoint carries a long `retry-after` (often 20+ minutes). Persist
176
202
  // the cooldown so every surface — this process, the menu bar app's embedded server, a later
177
203
  // restart — stops calling until it expires. Hammering during the cooldown just renews the
@@ -860,11 +886,11 @@ function normalizeCursorSandUsageStatus(body, { eligible = true } = {}) {
860
886
  return buildWindow({ usedPercent, resetAt, windowSeconds });
861
887
  }
862
888
 
863
- async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
864
- if (!isCursorInstalled({ home })) {
889
+ async function fetchCursorLimits({ home, env, fetchImpl = fetch } = {}) {
890
+ if (!isCursorInstalled({ home, env })) {
865
891
  return { configured: false };
866
892
  }
867
- const auth = extractCursorAuth({ home });
893
+ const auth = extractCursorAuth({ home, env });
868
894
  if (!auth?.cookie || !auth?.accessToken) {
869
895
  return { configured: false };
870
896
  }
@@ -887,11 +913,18 @@ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
887
913
  error: null,
888
914
  ...normalizeCursorUsageSummary(body),
889
915
  quaternary_window: grokBotWindow,
916
+ // Internal cache-partition key (stripped before the payload is served).
917
+ cache_identity: auth.userId,
890
918
  };
891
919
  } catch (error) {
892
920
  return {
893
921
  configured: true,
894
922
  error: error?.message || "Unknown error",
923
+ // A session-expiry error is actionable (re-login) and must not be hidden
924
+ // behind the stale disk-cache fallback (opencode-go auth_error precedent).
925
+ // Internal marker — stripped before the payload is served.
926
+ ...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
927
+ cache_identity: auth.userId,
895
928
  };
896
929
  }
897
930
  }
@@ -964,7 +997,9 @@ function kimiCredentialsExpired(creds, nowMs = Date.now()) {
964
997
 
965
998
  async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch } = {}) {
966
999
  if (typeof refreshToken !== "string" || !refreshToken.trim()) {
967
- throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
1000
+ const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
1001
+ error.code = "AUTH_EXPIRED";
1002
+ throw error;
968
1003
  }
969
1004
 
970
1005
  const body = new URLSearchParams({
@@ -983,7 +1018,9 @@ async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch
983
1018
  body,
984
1019
  });
985
1020
  if (res.status === 401 || res.status === 403) {
986
- throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
1021
+ const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
1022
+ error.code = "AUTH_EXPIRED";
1023
+ throw error;
987
1024
  }
988
1025
  if (!res.ok) {
989
1026
  throw new Error(`Kimi token refresh failed (HTTP ${res.status})`);
@@ -1017,7 +1054,11 @@ async function fetchKimiUsage(accessToken, { profile, fetchImpl = fetch } = {})
1017
1054
  },
1018
1055
  });
1019
1056
  if (res.status === 401) {
1020
- throw new Error("token_expired");
1057
+ // The retry-with-refresh path keys off this message; the code marks the
1058
+ // error actionable (re-login) so the cache fallback never hides it.
1059
+ const error = new Error("token_expired");
1060
+ error.code = "AUTH_EXPIRED";
1061
+ throw error;
1021
1062
  }
1022
1063
  if (!res.ok) {
1023
1064
  throw new Error(`Kimi API returned ${res.status}`);
@@ -1030,12 +1071,17 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
1030
1071
  if (!profile.configured) {
1031
1072
  return { configured: false };
1032
1073
  }
1033
- if (profile.error) return { configured: true, error: profile.error };
1074
+ // A profile misconfiguration (unsupported endpoints, non-file credential
1075
+ // storage) is actionable — the user must fix their config — so it must not
1076
+ // be hidden behind the stale disk-cache fallback. Internal marker —
1077
+ // stripped before the payload is served.
1078
+ if (profile.error) return { configured: true, error: profile.error, config_error: true };
1034
1079
  const creds = loadKimiCredentials({ profile });
1035
1080
  let accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
1036
1081
  if (!accessToken) {
1037
1082
  return { configured: false };
1038
1083
  }
1084
+ const cacheIdentity = kimiProfileIdentity(profile);
1039
1085
  try {
1040
1086
  if (kimiCredentialsExpired(creds) && creds?.refresh_token) {
1041
1087
  accessToken = await refreshKimiAccessToken({
@@ -1063,11 +1109,18 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
1063
1109
  configured: true,
1064
1110
  error: null,
1065
1111
  ...normalizeKimiUsageResponse(body),
1112
+ // Internal cache-partition key (stripped before the payload is served).
1113
+ cache_identity: cacheIdentity,
1066
1114
  };
1067
1115
  } catch (error) {
1068
1116
  return {
1069
1117
  configured: true,
1070
1118
  error: error?.message || "Unknown error",
1119
+ // A session-expiry error is actionable (re-login) and must not be hidden
1120
+ // behind the stale disk-cache fallback (opencode-go auth_error precedent).
1121
+ // Internal marker — stripped before the payload is served.
1122
+ ...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
1123
+ cache_identity: cacheIdentity,
1071
1124
  };
1072
1125
  }
1073
1126
  }
@@ -1199,6 +1252,12 @@ async function extractGeminiOauthClientCredentials({ commandRunner, home } = {})
1199
1252
  return { ...GEMINI_CLI_FALLBACK_OAUTH_CLIENT };
1200
1253
  }
1201
1254
 
1255
+ // Shared by the refresh-401 path, the missing-credentials path, and the quota
1256
+ // endpoint's 401: all three mean the Google OAuth session is gone. Re-login is
1257
+ // actionable, so none of them may be hidden behind the stale disk-cache
1258
+ // fallback (opencode-go auth_error precedent).
1259
+ const GEMINI_NOT_LOGGED_IN_MESSAGE = "Not logged in to Gemini. Run 'gemini' in Terminal to authenticate.";
1260
+
1202
1261
  async function refreshGeminiAccessToken({
1203
1262
  refreshToken,
1204
1263
  home,
@@ -1224,7 +1283,14 @@ async function refreshGeminiAccessToken({
1224
1283
  body,
1225
1284
  });
1226
1285
  if (!res.ok) {
1227
- throw new Error("Not logged in to Gemini. Run 'gemini' in Terminal to authenticate.");
1286
+ const error = new Error(GEMINI_NOT_LOGGED_IN_MESSAGE);
1287
+ // Confirmed credential failures only (invalid_grant = 400, invalid_client
1288
+ // = 401/403): a transient 429/5xx from the token endpoint must stay
1289
+ // cache-eligible so the stale fallback can still serve bars.
1290
+ if (res.status === 400 || res.status === 401 || res.status === 403) {
1291
+ error.code = "AUTH_EXPIRED";
1292
+ }
1293
+ throw error;
1228
1294
  }
1229
1295
 
1230
1296
  const json = await res.json();
@@ -1365,18 +1431,38 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
1365
1431
  if (!settings && !credentials) {
1366
1432
  return { configured: false };
1367
1433
  }
1434
+ // An unsupported auth mode is a stable, actionable configuration state: the
1435
+ // quota RPC can never succeed here and stale OAuth-era bars would be
1436
+ // misleading, so it punches through the cache like an auth error.
1368
1437
  if (selectedType === "api-key") {
1369
- return { configured: true, error: "Gemini API key auth not supported. Use Google account (OAuth) instead." };
1438
+ return {
1439
+ configured: true,
1440
+ error: "Gemini API key auth not supported. Use Google account (OAuth) instead.",
1441
+ auth_error: true,
1442
+ };
1370
1443
  }
1371
1444
  if (selectedType === "vertex-ai") {
1372
- return { configured: true, error: "Gemini Vertex AI auth not supported. Use Google account (OAuth) instead." };
1445
+ return {
1446
+ configured: true,
1447
+ error: "Gemini Vertex AI auth not supported. Use Google account (OAuth) instead.",
1448
+ auth_error: true,
1449
+ };
1373
1450
  }
1374
1451
 
1375
1452
  const creds = credentials;
1376
1453
  if (!creds?.access_token) {
1377
- return { configured: true, error: "Not logged in to Gemini. Run 'gemini' in Terminal to authenticate." };
1454
+ return {
1455
+ configured: true,
1456
+ error: GEMINI_NOT_LOGGED_IN_MESSAGE,
1457
+ auth_error: true,
1458
+ };
1378
1459
  }
1379
1460
 
1461
+ // The Google account email from the local id_token JWT partitions the disk
1462
+ // cache (zcode login-identity precedent). Decoded before the network work —
1463
+ // it cannot throw — so the failure path below still knows who we serve for.
1464
+ const claims = decodeJwtPayload(creds.id_token);
1465
+
1380
1466
  try {
1381
1467
  let accessToken = creds.access_token;
1382
1468
  const expiry = Number(creds.expiry_date);
@@ -1390,7 +1476,6 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
1390
1476
  });
1391
1477
  }
1392
1478
 
1393
- const claims = decodeJwtPayload(creds.id_token);
1394
1479
  const codeAssist = await loadGeminiCodeAssistStatus(accessToken, { fetchImpl });
1395
1480
  const res = await fetchImpl("https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota", {
1396
1481
  method: "POST",
@@ -1401,7 +1486,18 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
1401
1486
  body: JSON.stringify(codeAssist.projectId ? { project: codeAssist.projectId } : {}),
1402
1487
  });
1403
1488
  if (res.status === 401) {
1404
- throw new Error("Not logged in to Gemini. Run 'gemini' in Terminal to authenticate.");
1489
+ const error = new Error(GEMINI_NOT_LOGGED_IN_MESSAGE);
1490
+ error.code = "AUTH_EXPIRED";
1491
+ throw error;
1492
+ }
1493
+ if (res.status === 403) {
1494
+ // PERMISSION_DENIED (seen for Enterprise accounts, gemini-cli#25512):
1495
+ // re-login cannot fix it, but stale bars for a project the account can
1496
+ // no longer see would mislead — punch through like an auth error while
1497
+ // keeping the displayed message unchanged.
1498
+ const error = new Error(`Gemini API error: HTTP ${res.status}`);
1499
+ error.code = "AUTH_EXPIRED";
1500
+ throw error;
1405
1501
  }
1406
1502
  if (!res.ok) {
1407
1503
  throw new Error(`Gemini API error: HTTP ${res.status}`);
@@ -1415,11 +1511,19 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
1415
1511
  email: claims?.email || null,
1416
1512
  tier: codeAssist.tier,
1417
1513
  }),
1514
+ // Internal cache-partition key (stripped before the payload is served):
1515
+ // the same non-secret id the payload already surfaces as account_email.
1516
+ cache_identity: claims?.email || null,
1418
1517
  };
1419
1518
  } catch (error) {
1420
1519
  return {
1421
1520
  configured: true,
1422
1521
  error: error?.message || "Unknown error",
1522
+ // A session-expiry error is actionable (re-login) and must not be hidden
1523
+ // behind the stale disk-cache fallback. Internal marker — stripped
1524
+ // before the payload is served.
1525
+ ...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
1526
+ cache_identity: claims?.email || null,
1423
1527
  };
1424
1528
  }
1425
1529
  }
@@ -2767,6 +2871,423 @@ function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now
2767
2871
  } catch (_error) {}
2768
2872
  }
2769
2873
 
2874
+ function resolveCursorLimitsCachePath({ home } = {}) {
2875
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", CURSOR_LIMITS_CACHE_FILE);
2876
+ }
2877
+
2878
+ function hasCursorWindow(limits) {
2879
+ return Boolean(
2880
+ limits?.primary_window
2881
+ || limits?.secondary_window
2882
+ || limits?.tertiary_window
2883
+ || limits?.quaternary_window,
2884
+ );
2885
+ }
2886
+
2887
+ // Cursor windows carry `reset_at` as an ISO string (billing-cycle end for the three
2888
+ // summary windows, the RPC's own expiry for the Grok Bot window). A window whose reset
2889
+ // has already passed is stale data, not a usable fallback, so drop it. Windows without
2890
+ // a reset stamp are kept — the overall cached_at max-age gate already bounds them.
2891
+ function isCursorCacheWindowUsable(window, { nowMs } = {}) {
2892
+ if (!window || typeof window !== "object") return false;
2893
+ const resetAtMs = parseTimeMs(window.reset_at);
2894
+ if (resetAtMs === null) return true;
2895
+ return resetAtMs > nowMs;
2896
+ }
2897
+
2898
+ function normalizeCursorCachedLimits(
2899
+ raw,
2900
+ { nowMs = Date.now(), maxAgeMs = CURSOR_LIMITS_CACHE_MAX_AGE_MS } = {},
2901
+ ) {
2902
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
2903
+ if (!Number.isFinite(cachedAtMs)) return null;
2904
+ if (cachedAtMs > nowMs + 60_000) return null;
2905
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
2906
+
2907
+ const cached = {
2908
+ configured: true,
2909
+ error: null,
2910
+ membership_type: typeof raw?.membership_type === "string" ? raw.membership_type : null,
2911
+ primary_window: isCursorCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
2912
+ secondary_window: isCursorCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
2913
+ tertiary_window: isCursorCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
2914
+ quaternary_window: isCursorCacheWindowUsable(raw?.quaternary_window, { nowMs }) ? raw.quaternary_window : null,
2915
+ stale: true,
2916
+ cached_at: raw.cached_at,
2917
+ };
2918
+ return hasCursorWindow(cached) ? cached : null;
2919
+ }
2920
+
2921
+ // The signed-in Cursor account partitions the cache (zcode login-identity precedent):
2922
+ // the id is the non-secret normalized subject Cursor itself puts in the session cookie.
2923
+ // A single home holds one active login, so one stamped entry suffices; a read requires
2924
+ // the stored identity to match the current one, and a write without an identity is
2925
+ // refused — an unreadable login must never pin another account's bars in place.
2926
+ function readCursorLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
2927
+ if (!identity) return null;
2928
+ try {
2929
+ const parsed = JSON.parse(fs.readFileSync(resolveCursorLimitsCachePath({ home }), "utf8"));
2930
+ const entry = parsed?.cursor;
2931
+ if (entry?.identity !== identity) return null;
2932
+ return normalizeCursorCachedLimits(entry, { nowMs });
2933
+ } catch (_error) {
2934
+ return null;
2935
+ }
2936
+ }
2937
+
2938
+ function writeCursorLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
2939
+ if (!identity) return;
2940
+ if (!limits?.configured || limits.error || !hasCursorWindow(limits)) return;
2941
+ const cachePath = resolveCursorLimitsCachePath({ home });
2942
+ const payload = {
2943
+ cursor: {
2944
+ identity,
2945
+ membership_type: limits.membership_type || null,
2946
+ primary_window: limits.primary_window || null,
2947
+ secondary_window: limits.secondary_window || null,
2948
+ tertiary_window: limits.tertiary_window || null,
2949
+ quaternary_window: limits.quaternary_window || null,
2950
+ cached_at: new Date(nowMs).toISOString(),
2951
+ },
2952
+ };
2953
+ try {
2954
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
2955
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
2956
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
2957
+ fs.renameSync(tmpPath, cachePath);
2958
+ } catch (_error) {}
2959
+ }
2960
+
2961
+ // Failure-only identity re-derivation for the cache fallback: when the provider slot's
2962
+ // result was produced by the outer timeout race, the fetcher's own identity field is
2963
+ // lost, so read the (still-local) state DB once to learn who we are serving for.
2964
+ function resolveCursorCacheIdentity({ home, env } = {}) {
2965
+ try {
2966
+ return extractCursorAuth({ home, env })?.userId || null;
2967
+ } catch (_error) {
2968
+ return null;
2969
+ }
2970
+ }
2971
+
2972
+ function resolveGeminiLimitsCachePath({ home } = {}) {
2973
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", GEMINI_LIMITS_CACHE_FILE);
2974
+ }
2975
+
2976
+ function hasGeminiWindow(limits) {
2977
+ return Boolean(
2978
+ limits?.primary_window
2979
+ || limits?.secondary_window
2980
+ || limits?.tertiary_window,
2981
+ );
2982
+ }
2983
+
2984
+ // Gemini windows carry `reset_at` as an ISO string (the Code Assist bucket's own
2985
+ // resetTime — daily for free-tier quotas). A window whose reset has already passed
2986
+ // is stale data, not a usable fallback, so drop it. Windows without a reset stamp
2987
+ // are kept — the overall cached_at max-age gate already bounds them.
2988
+ function isGeminiCacheWindowUsable(window, { nowMs } = {}) {
2989
+ if (!window || typeof window !== "object") return false;
2990
+ const resetAtMs = parseTimeMs(window.reset_at);
2991
+ if (resetAtMs === null) return true;
2992
+ return resetAtMs > nowMs;
2993
+ }
2994
+
2995
+ function normalizeGeminiCachedLimits(
2996
+ raw,
2997
+ { nowMs = Date.now(), maxAgeMs = GEMINI_LIMITS_CACHE_MAX_AGE_MS } = {},
2998
+ ) {
2999
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
3000
+ if (!Number.isFinite(cachedAtMs)) return null;
3001
+ if (cachedAtMs > nowMs + 60_000) return null;
3002
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
3003
+
3004
+ const cached = {
3005
+ configured: true,
3006
+ error: null,
3007
+ account_email: typeof raw?.account_email === "string" ? raw.account_email : null,
3008
+ account_plan: typeof raw?.account_plan === "string" ? raw.account_plan : null,
3009
+ primary_window: isGeminiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3010
+ secondary_window: isGeminiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3011
+ tertiary_window: isGeminiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
3012
+ stale: true,
3013
+ cached_at: raw.cached_at,
3014
+ };
3015
+ return hasGeminiWindow(cached) ? cached : null;
3016
+ }
3017
+
3018
+ // The signed-in Google account partitions the cache (zcode login-identity
3019
+ // precedent): the email decoded from the local id_token JWT is non-secret and is
3020
+ // the same id the payload already surfaces as account_email. GEMINI_HOME can aim
3021
+ // the credential lookup at a different installation, but the cache stays keyed by
3022
+ // account — a read requires the stored identity to match the account the current
3023
+ // credentials resolve to, and a write without an identity is refused: an
3024
+ // unreadable login must never pin another account's bars in place.
3025
+ function readGeminiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3026
+ if (!identity) return null;
3027
+ try {
3028
+ const parsed = JSON.parse(fs.readFileSync(resolveGeminiLimitsCachePath({ home }), "utf8"));
3029
+ const entry = parsed?.gemini;
3030
+ if (entry?.identity !== identity) return null;
3031
+ return normalizeGeminiCachedLimits(entry, { nowMs });
3032
+ } catch (_error) {
3033
+ return null;
3034
+ }
3035
+ }
3036
+
3037
+ function writeGeminiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3038
+ if (!identity) return;
3039
+ if (!limits?.configured || limits.error || !hasGeminiWindow(limits)) return;
3040
+ const cachePath = resolveGeminiLimitsCachePath({ home });
3041
+ const payload = {
3042
+ gemini: {
3043
+ identity,
3044
+ account_email: limits.account_email || null,
3045
+ account_plan: limits.account_plan || null,
3046
+ primary_window: limits.primary_window || null,
3047
+ secondary_window: limits.secondary_window || null,
3048
+ tertiary_window: limits.tertiary_window || null,
3049
+ cached_at: new Date(nowMs).toISOString(),
3050
+ },
3051
+ };
3052
+ try {
3053
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3054
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
3055
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3056
+ fs.renameSync(tmpPath, cachePath);
3057
+ } catch (_error) {}
3058
+ }
3059
+
3060
+ // Failure-only identity re-derivation for the cache fallback: when the provider
3061
+ // slot's result was produced by the outer timeout race, the fetcher's own
3062
+ // identity field is lost, so decode the (still-local) id_token once to learn
3063
+ // who we are serving for.
3064
+ function resolveGeminiCacheIdentity({ home, env } = {}) {
3065
+ try {
3066
+ return decodeJwtPayload(loadGeminiCredentials({ home, env })?.id_token)?.email || null;
3067
+ } catch (_error) {
3068
+ return null;
3069
+ }
3070
+ }
3071
+
3072
+ // The active Kimi profile partitions the cache (ark profile_identity precedent):
3073
+ // mainland and global logins — and any KIMI_HOME override — resolve to different
3074
+ // endpoint-scoped credential slots, and one slot must never serve another's
3075
+ // windows. The profile home plus OAuth host is non-secret and derivable from
3076
+ // local files alone (the credential file holds no account id).
3077
+ function kimiProfileIdentity(profile) {
3078
+ if (!profile?.home || !profile?.oauthHost) return null;
3079
+ return `${profile.home}::${profile.oauthHost}`;
3080
+ }
3081
+
3082
+ // Failure-only identity re-derivation for the cache fallback: when the provider
3083
+ // slot's result was produced by the outer timeout race, the fetcher's own
3084
+ // identity field is lost, so re-resolve the (still-local) Kimi profile.
3085
+ function resolveKimiCacheIdentity({ home, env } = {}) {
3086
+ try {
3087
+ return kimiProfileIdentity(resolveKimiProfile({ home, env }));
3088
+ } catch (_error) {
3089
+ return null;
3090
+ }
3091
+ }
3092
+
3093
+ function resolveKimiLimitsCachePath({ home } = {}) {
3094
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", KIMI_LIMITS_CACHE_FILE);
3095
+ }
3096
+
3097
+ function hasKimiWindow(limits) {
3098
+ return Boolean(
3099
+ limits?.primary_window
3100
+ || limits?.secondary_window
3101
+ || limits?.tertiary_window,
3102
+ );
3103
+ }
3104
+
3105
+ // Kimi windows carry `reset_at` (ISO) when the API reports a cycle end; a window
3106
+ // whose reset has already passed is stale data, not a usable fallback, so drop
3107
+ // it. Windows without a reset stamp are kept — the overall cached_at max-age
3108
+ // gate already bounds them.
3109
+ function isKimiCacheWindowUsable(window, { nowMs } = {}) {
3110
+ if (!window || typeof window !== "object") return false;
3111
+ const resetAtMs = parseTimeMs(window.reset_at);
3112
+ if (resetAtMs === null) return true;
3113
+ return resetAtMs > nowMs;
3114
+ }
3115
+
3116
+ function normalizeKimiCachedLimits(
3117
+ raw,
3118
+ { nowMs = Date.now(), maxAgeMs = KIMI_LIMITS_CACHE_MAX_AGE_MS } = {},
3119
+ ) {
3120
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
3121
+ if (!Number.isFinite(cachedAtMs)) return null;
3122
+ if (cachedAtMs > nowMs + 60_000) return null;
3123
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
3124
+
3125
+ const cached = {
3126
+ configured: true,
3127
+ error: null,
3128
+ membership_level: typeof raw?.membership_level === "string" ? raw.membership_level : null,
3129
+ subscription_type: typeof raw?.subscription_type === "string" ? raw.subscription_type : null,
3130
+ parallel_limit: Number.isFinite(raw?.parallel_limit) ? raw.parallel_limit : null,
3131
+ primary_window: isKimiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3132
+ secondary_window: isKimiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3133
+ tertiary_window: isKimiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
3134
+ stale: true,
3135
+ cached_at: raw.cached_at,
3136
+ };
3137
+ return hasKimiWindow(cached) ? cached : null;
3138
+ }
3139
+
3140
+ function readKimiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3141
+ if (!identity) return null;
3142
+ try {
3143
+ const parsed = JSON.parse(fs.readFileSync(resolveKimiLimitsCachePath({ home }), "utf8"));
3144
+ const entry = parsed?.kimi;
3145
+ if (entry?.identity !== identity) return null;
3146
+ return normalizeKimiCachedLimits(entry, { nowMs });
3147
+ } catch (_error) {
3148
+ return null;
3149
+ }
3150
+ }
3151
+
3152
+ function writeKimiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3153
+ if (!identity) return;
3154
+ if (!limits?.configured || limits.error || !hasKimiWindow(limits)) return;
3155
+ const cachePath = resolveKimiLimitsCachePath({ home });
3156
+ const payload = {
3157
+ kimi: {
3158
+ identity,
3159
+ membership_level: limits.membership_level || null,
3160
+ subscription_type: limits.subscription_type || null,
3161
+ parallel_limit: Number.isFinite(limits.parallel_limit) ? limits.parallel_limit : null,
3162
+ primary_window: limits.primary_window || null,
3163
+ secondary_window: limits.secondary_window || null,
3164
+ tertiary_window: limits.tertiary_window || null,
3165
+ cached_at: new Date(nowMs).toISOString(),
3166
+ },
3167
+ };
3168
+ try {
3169
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3170
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
3171
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3172
+ fs.renameSync(tmpPath, cachePath);
3173
+ } catch (_error) {}
3174
+ }
3175
+
3176
+ function resolveGrokLimitsCachePath({ home } = {}) {
3177
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", GROK_LIMITS_CACHE_FILE);
3178
+ }
3179
+
3180
+ function hasGrokWindow(limits) {
3181
+ return Boolean(limits?.primary_window || limits?.secondary_window);
3182
+ }
3183
+
3184
+ // Grok windows carry `reset_at` as an ISO string — the billing-period end shared
3185
+ // by the pool, on-demand, and per-product windows. A window whose reset has
3186
+ // already passed is stale data, not a usable fallback, so drop it. Windows
3187
+ // without a reset stamp are kept — the overall cached_at max-age gate already
3188
+ // bounds them (mirrors Cursor).
3189
+ function isGrokCacheWindowUsable(window, { nowMs } = {}) {
3190
+ if (!window || typeof window !== "object") return false;
3191
+ const resetAtMs = parseTimeMs(window.reset_at);
3192
+ if (resetAtMs === null) return true;
3193
+ return resetAtMs > nowMs;
3194
+ }
3195
+
3196
+ function normalizeGrokCachedLimits(
3197
+ raw,
3198
+ { nowMs = Date.now(), maxAgeMs = GROK_LIMITS_CACHE_MAX_AGE_MS } = {},
3199
+ ) {
3200
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
3201
+ if (!Number.isFinite(cachedAtMs)) return null;
3202
+ if (cachedAtMs > nowMs + 60_000) return null;
3203
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
3204
+
3205
+ const cached = {
3206
+ configured: true,
3207
+ error: null,
3208
+ plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
3209
+ period_type: typeof raw?.period_type === "string" ? raw.period_type : null,
3210
+ monthly_credits_limit: raw?.monthly_credits_limit ?? null,
3211
+ monthly_credits_used: raw?.monthly_credits_used ?? null,
3212
+ credit_usage_percent: raw?.credit_usage_percent ?? null,
3213
+ on_demand_cap: raw?.on_demand_cap ?? null,
3214
+ on_demand_used: raw?.on_demand_used ?? null,
3215
+ billing_period_start: typeof raw?.billing_period_start === "string" ? raw.billing_period_start : null,
3216
+ primary_window: isGrokCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3217
+ secondary_window: isGrokCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3218
+ // Per-product shares of the pool, not independent windows — each is gated
3219
+ // by the same reset stamp as the pool window it attributes.
3220
+ products: Array.isArray(raw?.products)
3221
+ ? raw.products.filter((entry) => isGrokCacheWindowUsable(entry?.window, { nowMs }))
3222
+ : [],
3223
+ stale: true,
3224
+ cached_at: raw.cached_at,
3225
+ };
3226
+ return hasGrokWindow(cached) ? cached : null;
3227
+ }
3228
+
3229
+ // The signed-in Grok account partitions the cache (zcode login-identity precedent):
3230
+ // the id is the non-secret OIDC `sub` claim inside the access token — the
3231
+ // credential itself never enters the id (no token fingerprint fallback: CodeQL
3232
+ // js/insufficient-password-hash precedent). auth.x.ai issues JWT access tokens
3233
+ // (RFC 9068), so the subject is present in practice; an opaque token yields no
3234
+ // identity and the cache is simply refused on both the write and the read path
3235
+ // (fail-closed) — an unattributable login must never pin another account's bars,
3236
+ // which a scope-key fallback cannot guarantee because `issuer::client_id` stays
3237
+ // constant across account switches.
3238
+ function readGrokLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3239
+ if (!identity) return null;
3240
+ try {
3241
+ const parsed = JSON.parse(fs.readFileSync(resolveGrokLimitsCachePath({ home }), "utf8"));
3242
+ const entry = parsed?.grok;
3243
+ if (entry?.identity !== identity) return null;
3244
+ return normalizeGrokCachedLimits(entry, { nowMs });
3245
+ } catch (_error) {
3246
+ return null;
3247
+ }
3248
+ }
3249
+
3250
+ function writeGrokLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3251
+ if (!identity) return;
3252
+ if (!limits?.configured || limits.error || !hasGrokWindow(limits)) return;
3253
+ const cachePath = resolveGrokLimitsCachePath({ home });
3254
+ const payload = {
3255
+ grok: {
3256
+ identity,
3257
+ plan_label: limits.plan_label || null,
3258
+ period_type: limits.period_type || null,
3259
+ monthly_credits_limit: limits.monthly_credits_limit ?? null,
3260
+ monthly_credits_used: limits.monthly_credits_used ?? null,
3261
+ credit_usage_percent: limits.credit_usage_percent ?? null,
3262
+ on_demand_cap: limits.on_demand_cap ?? null,
3263
+ on_demand_used: limits.on_demand_used ?? null,
3264
+ billing_period_start: limits.billing_period_start || null,
3265
+ primary_window: limits.primary_window || null,
3266
+ secondary_window: limits.secondary_window || null,
3267
+ products: Array.isArray(limits.products) ? limits.products : [],
3268
+ cached_at: new Date(nowMs).toISOString(),
3269
+ },
3270
+ };
3271
+ try {
3272
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3273
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
3274
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3275
+ fs.renameSync(tmpPath, cachePath);
3276
+ } catch (_error) {}
3277
+ }
3278
+
3279
+ // Failure-only identity re-derivation for the cache fallback: when the provider
3280
+ // slot's result was produced by the outer timeout race, the fetcher's own
3281
+ // identity field is lost, so read auth.json once to learn who we are serving for.
3282
+ // Opaque tokens yield null → the read is refused (fail-closed, see above).
3283
+ function resolveGrokCacheIdentity({ home, env } = {}) {
3284
+ try {
3285
+ return grokTokenSubject(loadGrokAuthEntry({ home, env })?.entry?.key);
3286
+ } catch (_error) {
3287
+ return null;
3288
+ }
3289
+ }
3290
+
2770
3291
  function resolveClaudeRateLimitPath({ home } = {}) {
2771
3292
  return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
2772
3293
  }
@@ -3636,6 +4157,11 @@ async function fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal)
3636
4157
  for (const url of ANTIGRAVITY_QUOTA_SUMMARY_URLS) {
3637
4158
  try {
3638
4159
  const res = await postAntigravityCloudCode(fetchImpl, url, accessToken, {}, signal);
4160
+ if (res.status === 403 && await isAntigravitySubscriptionRequired(res)) {
4161
+ const err = new Error("Antigravity quota summary requires a subscription.");
4162
+ err.code = "SUBSCRIPTION_REQUIRED";
4163
+ throw err;
4164
+ }
3639
4165
  if (res.status === 401 || res.status === 403) {
3640
4166
  const err = new Error(ANTIGRAVITY_AUTH_EXPIRED_MESSAGE);
3641
4167
  err.code = "AUTH_EXPIRED";
@@ -3648,13 +4174,44 @@ async function fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal)
3648
4174
  }
3649
4175
  return await res.json();
3650
4176
  } catch (error) {
3651
- if (error?.code === "AUTH_EXPIRED") throw error;
4177
+ if (error?.code === "AUTH_EXPIRED" || error?.code === "SUBSCRIPTION_REQUIRED") throw error;
3652
4178
  lastError = error;
3653
4179
  }
3654
4180
  }
3655
4181
  throw lastError || new Error("Antigravity quota request failed.");
3656
4182
  }
3657
4183
 
4184
+ // Free-tier ("Starter Quota") accounts get 403 SUBSCRIPTION_REQUIRED from
4185
+ // retrieveUserQuotaSummary even with a valid token — that is not an auth failure.
4186
+ async function isAntigravitySubscriptionRequired(res) {
4187
+ try {
4188
+ const body = await res.json();
4189
+ const details = Array.isArray(body?.error?.details) ? body.error.details : [];
4190
+ return details.some((d) => d?.reason === "SUBSCRIPTION_REQUIRED");
4191
+ } catch {
4192
+ return false;
4193
+ }
4194
+ }
4195
+
4196
+ // Per-model quotaInfo, available to every tier. Reshaped into the language
4197
+ // server's clientModelConfigs so normalizeAntigravityResponse can pick windows.
4198
+ async function fetchAntigravityAvailableModelsLimits(fetchImpl, accessToken, signal) {
4199
+ const res = await postAntigravityCloudCode(fetchImpl, ANTIGRAVITY_AVAILABLE_MODELS_URL, accessToken, {}, signal);
4200
+ if (res.status === 401 || res.status === 403) {
4201
+ const err = new Error(ANTIGRAVITY_AUTH_EXPIRED_MESSAGE);
4202
+ err.code = "AUTH_EXPIRED";
4203
+ err.status = res.status;
4204
+ throw err;
4205
+ }
4206
+ if (!res.ok) throw new Error(`Antigravity API error: HTTP ${res.status}`);
4207
+ const json = await res.json();
4208
+ const models = json?.models && typeof json.models === "object" ? json.models : {};
4209
+ const clientModelConfigs = Object.entries(models)
4210
+ .filter(([, m]) => typeof m?.displayName === "string" && m.displayName)
4211
+ .map(([id, m]) => ({ label: m.displayName, modelOrAlias: { model: id }, quotaInfo: m.quotaInfo }));
4212
+ return normalizeAntigravityResponse({ clientModelConfigs }, { fallbackToConfigs: true });
4213
+ }
4214
+
3658
4215
  /** Read the paid plan label without failing a successful quota response if this lookup fails. */
3659
4216
  async function fetchAntigravityPlanLabel(fetchImpl, accessToken, signal) {
3660
4217
  try {
@@ -3694,8 +4251,14 @@ async function fetchAntigravityRemoteLimits({
3694
4251
  if (!resolvedCreds) return null;
3695
4252
 
3696
4253
  const loadWithToken = async (accessToken) => {
3697
- const payload = await fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal);
3698
- const windows = normalizeAntigravityQuotaSummary(payload);
4254
+ let windows;
4255
+ try {
4256
+ const payload = await fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal);
4257
+ windows = normalizeAntigravityQuotaSummary(payload);
4258
+ } catch (error) {
4259
+ if (error?.code !== "SUBSCRIPTION_REQUIRED") throw error;
4260
+ windows = await fetchAntigravityAvailableModelsLimits(fetchImpl, accessToken, signal);
4261
+ }
3699
4262
  const accountPlan = windows.account_plan || await fetchAntigravityPlanLabel(fetchImpl, accessToken, signal);
3700
4263
  return {
3701
4264
  configured: true,
@@ -4209,7 +4772,7 @@ async function fetchUsageLimitsUncached({
4209
4772
  // (#956).
4210
4773
  const hiddenSentinel = Promise.resolve({ configured: false });
4211
4774
  const hiddenSettled = Promise.resolve({ status: "fulfilled", value: { configured: false } });
4212
- const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, workbuddy, workbuddyAi, claudeServiceStatus] = await Promise.all([
4775
+ const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, workbuddy, workbuddyAi, deepseek, claudeServiceStatus] = await Promise.all([
4213
4776
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
4214
4777
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
4215
4778
  (value) => ({ status: "fulfilled", value }),
@@ -4226,7 +4789,7 @@ async function fetchUsageLimitsUncached({
4226
4789
  (reason) => ({ status: "rejected", reason }),
4227
4790
  )
4228
4791
  : Promise.resolve(null),
4229
- hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
4792
+ hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, env, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
4230
4793
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4231
4794
  hiddenSet.has("kimi") ? hiddenSentinel : withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
4232
4795
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
@@ -4343,6 +4906,25 @@ async function fetchUsageLimitsUncached({
4343
4906
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4344
4907
  hiddenSet.has("workbuddyAi") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy AI", providerTimeoutMs)
4345
4908
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4909
+ // DeepSeek (platform.deepseek.com, pay-as-you-go): the balance endpoint is
4910
+ // the only documented programmatic source, so this slot yields a balance
4911
+ // card, not quota windows (#960). The last-good disk-cache fallback with a
4912
+ // pure TTL and a key fingerprint binding lives inside deepseek-limits.js;
4913
+ // auth errors bypass it so a rejected key stays actionable. persistCache
4914
+ // is sampled at slot creation — a mid-fetch key save can only leave a
4915
+ // cache entry bound to the OLD key's fingerprint, which a fetch under the
4916
+ // new key will never read.
4917
+ hiddenSet.has("deepseek") ? hiddenSentinel : withProviderTimeout(
4918
+ fetchDeepseekLimits({
4919
+ home,
4920
+ trackerDir,
4921
+ env,
4922
+ fetchImpl: providerFetch,
4923
+ persistCache: cacheGeneration === usageLimitsCacheGeneration,
4924
+ }),
4925
+ "DeepSeek",
4926
+ providerTimeoutMs,
4927
+ ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4346
4928
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
4347
4929
  // Only probed for configured accounts — without a token the Claude section
4348
4930
  // never renders, so the reading would have nowhere to go.
@@ -4573,9 +5155,198 @@ async function fetchUsageLimitsUncached({
4573
5155
  : { configured: true, error: reason?.message || "Unknown error" };
4574
5156
  }
4575
5157
 
4576
- const cursorLimits = cursorSandDuplicatesGrokPool(cursor, grok)
4577
- ? { ...cursor, quaternary_window: null }
4578
- : cursor;
5158
+ // Cursor: persist last successful windows so a transient timeout/5xx on the
5159
+ // cursor.com / api2.cursor.sh routes serves stale bars instead of a flashing
5160
+ // "fetch failed" (mirrors Codex / Claude / OpenCode Go). A session-expiry error
5161
+ // (401/403 — "re-login in Cursor") is actionable and must not be hidden behind
5162
+ // stale cache, so it passes through untouched.
5163
+ let cursorWithCache;
5164
+ if (!cursor || cursor.configured === false) {
5165
+ cursorWithCache = cursor;
5166
+ } else if (cursor.auth_error === true) {
5167
+ cursorWithCache = cursor;
5168
+ } else if (!cursor.error && hasCursorWindow(cursor)) {
5169
+ cursorWithCache = {
5170
+ ...cursor,
5171
+ stale: false,
5172
+ cached_at: new Date(nowMs).toISOString(),
5173
+ };
5174
+ if (cacheGeneration === usageLimitsCacheGeneration) {
5175
+ writeCursorLimitsCache(cursorWithCache, {
5176
+ home,
5177
+ nowMs,
5178
+ identity: typeof cursor.cache_identity === "string"
5179
+ ? cursor.cache_identity
5180
+ : resolveCursorCacheIdentity({ home, env }),
5181
+ });
5182
+ }
5183
+ } else {
5184
+ // The slot's error object loses the fetcher's identity field when the outer
5185
+ // timeout race wins (the common failure mode this cache exists for), so
5186
+ // re-derive it from the local state DB on this failure-only path.
5187
+ const identity = typeof cursor.cache_identity === "string"
5188
+ ? cursor.cache_identity
5189
+ : resolveCursorCacheIdentity({ home, env });
5190
+ const cached = readCursorLimitsCache({ home, nowMs, identity });
5191
+ cursorWithCache = cached || cursor;
5192
+ }
5193
+ // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5194
+ if (cursorWithCache && ("cache_identity" in cursorWithCache || "auth_error" in cursorWithCache)) {
5195
+ const { cache_identity: _identity, auth_error: _authError, ...cursorPayload } = cursorWithCache;
5196
+ cursorWithCache = cursorPayload;
5197
+ }
5198
+
5199
+ // Grok: persist last successful windows so a transient timeout/5xx on the
5200
+ // cli-chat-proxy.grok.com route serves stale bars instead of a flashing
5201
+ // "fetch failed" (mirrors Codex / Claude / Cursor). A re-login error
5202
+ // (GROK_AUTH_REQUIRED / GROK_REAUTH_REQUIRED — "run `grok login`") is
5203
+ // actionable and must not be hidden behind stale cache, so it passes
5204
+ // through untouched.
5205
+ let grokWithCache;
5206
+ if (!grok || grok.configured === false) {
5207
+ grokWithCache = grok;
5208
+ } else if (grok.auth_error === true) {
5209
+ grokWithCache = grok;
5210
+ } else if (!grok.error && hasGrokWindow(grok)) {
5211
+ grokWithCache = {
5212
+ ...grok,
5213
+ stale: false,
5214
+ cached_at: new Date(nowMs).toISOString(),
5215
+ };
5216
+ // Write only under the identity the fetch itself carried: re-reading
5217
+ // auth.json here could attribute a finished fetch's windows to an account
5218
+ // that logged in while the request was in flight (opaque → JWT switch).
5219
+ // No fetch-side identity (opaque token) → refuse the write (fail-closed).
5220
+ if (cacheGeneration === usageLimitsCacheGeneration && typeof grok.cache_identity === "string") {
5221
+ writeGrokLimitsCache(grokWithCache, {
5222
+ home,
5223
+ nowMs,
5224
+ identity: grok.cache_identity,
5225
+ });
5226
+ }
5227
+ } else {
5228
+ // The slot's error object loses the fetcher's identity field when the outer
5229
+ // timeout race wins (the common failure mode this cache exists for), so
5230
+ // re-derive it from auth.json on this failure-only path.
5231
+ const identity = typeof grok.cache_identity === "string"
5232
+ ? grok.cache_identity
5233
+ : resolveGrokCacheIdentity({ home, env });
5234
+ const cached = readGrokLimitsCache({ home, nowMs, identity });
5235
+ grokWithCache = cached || grok;
5236
+ }
5237
+ // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5238
+ if (grokWithCache && ("cache_identity" in grokWithCache || "auth_error" in grokWithCache)) {
5239
+ const { cache_identity: _identity, auth_error: _authError, ...grokPayload } = grokWithCache;
5240
+ grokWithCache = grokPayload;
5241
+ }
5242
+
5243
+ const cursorLimits = cursorSandDuplicatesGrokPool(cursorWithCache, grokWithCache)
5244
+ ? { ...cursorWithCache, quaternary_window: null }
5245
+ : cursorWithCache;
5246
+
5247
+ // Kimi: persist last successful windows so a transient timeout/5xx on the
5248
+ // api.kimi.com / api.kimi.ai coding routes serves stale bars instead of a
5249
+ // flashing "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Re-login
5250
+ // errors ("Not logged in to Kimi" / "token_expired") and actionable profile
5251
+ // misconfigurations must not be hidden behind stale cache, so they pass
5252
+ // through untouched.
5253
+ let kimiWithCache;
5254
+ if (!kimi || kimi.configured === false) {
5255
+ kimiWithCache = kimi;
5256
+ } else if (kimi.auth_error === true || kimi.config_error === true) {
5257
+ kimiWithCache = kimi;
5258
+ } else if (!kimi.error && hasKimiWindow(kimi)) {
5259
+ kimiWithCache = {
5260
+ ...kimi,
5261
+ stale: false,
5262
+ cached_at: new Date(nowMs).toISOString(),
5263
+ };
5264
+ if (cacheGeneration === usageLimitsCacheGeneration) {
5265
+ writeKimiLimitsCache(kimiWithCache, {
5266
+ home,
5267
+ nowMs,
5268
+ identity: typeof kimi.cache_identity === "string"
5269
+ ? kimi.cache_identity
5270
+ : resolveKimiCacheIdentity({ home, env }),
5271
+ });
5272
+ }
5273
+ } else {
5274
+ // The slot's error object loses the fetcher's identity field when the outer
5275
+ // timeout race wins (the common failure mode this cache exists for), so
5276
+ // re-derive it from the local Kimi profile on this failure-only path.
5277
+ const identity = typeof kimi.cache_identity === "string"
5278
+ ? kimi.cache_identity
5279
+ : resolveKimiCacheIdentity({ home, env });
5280
+ const cached = readKimiLimitsCache({ home, nowMs, identity });
5281
+ kimiWithCache = cached || kimi;
5282
+ }
5283
+ // cache_identity / auth_error / config_error are internal cache-plumbing
5284
+ // markers, not payload.
5285
+ if (kimiWithCache && ("cache_identity" in kimiWithCache || "auth_error" in kimiWithCache || "config_error" in kimiWithCache)) {
5286
+ const {
5287
+ cache_identity: _kimiIdentity,
5288
+ auth_error: _kimiAuthError,
5289
+ config_error: _kimiConfigError,
5290
+ ...kimiPayload
5291
+ } = kimiWithCache;
5292
+ kimiWithCache = kimiPayload;
5293
+ }
5294
+
5295
+ // Gemini: persist last successful windows so a transient timeout/5xx on the
5296
+ // cloudcode-pa.googleapis.com routes serves stale bars instead of a flashing
5297
+ // "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Auth errors (a dead
5298
+ // OAuth session, or the unsupported api-key / vertex-ai auth modes) are
5299
+ // actionable and must not be hidden behind stale cache, so they pass through
5300
+ // untouched.
5301
+ let geminiWithCache;
5302
+ if (!gemini || gemini.configured === false) {
5303
+ geminiWithCache = gemini;
5304
+ } else if (gemini.auth_error === true) {
5305
+ geminiWithCache = gemini;
5306
+ } else if (!gemini.error && hasGeminiWindow(gemini)) {
5307
+ geminiWithCache = {
5308
+ ...gemini,
5309
+ stale: false,
5310
+ cached_at: new Date(nowMs).toISOString(),
5311
+ };
5312
+ if (cacheGeneration === usageLimitsCacheGeneration) {
5313
+ // A slow in-flight fetch must not clobber the on-disk entry of an
5314
+ // account that has since been switched away: only write when the
5315
+ // snapshot's identity still matches the current credentials.
5316
+ const currentIdentity = resolveGeminiCacheIdentity({ home, env });
5317
+ const snapshotIdentity = typeof gemini.cache_identity === "string"
5318
+ ? gemini.cache_identity
5319
+ : currentIdentity;
5320
+ if (snapshotIdentity && snapshotIdentity === currentIdentity) {
5321
+ writeGeminiLimitsCache(geminiWithCache, {
5322
+ home,
5323
+ nowMs,
5324
+ identity: snapshotIdentity,
5325
+ });
5326
+ }
5327
+ }
5328
+ } else {
5329
+ // The slot's error object loses the fetcher's identity field when the outer
5330
+ // timeout race wins (the common failure mode this cache exists for). Always
5331
+ // re-derive from the CURRENT local credentials — the request may have
5332
+ // started under an account that has since been switched away, in which
5333
+ // case the read's identity gate refuses the old entry. Revalidate the auth
5334
+ // mode too: a mid-flight switch to api-key/vertex-ai never reached the
5335
+ // punch-through branch above, but stale OAuth bars would still mislead.
5336
+ const selectedType = loadGeminiSettings({ home, env })?.security?.auth?.selectedType ?? null;
5337
+ if (selectedType === "api-key" || selectedType === "vertex-ai") {
5338
+ geminiWithCache = gemini;
5339
+ } else {
5340
+ const identity = resolveGeminiCacheIdentity({ home, env });
5341
+ const cached = readGeminiLimitsCache({ home, nowMs, identity });
5342
+ geminiWithCache = cached || gemini;
5343
+ }
5344
+ }
5345
+ // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5346
+ if (geminiWithCache && ("cache_identity" in geminiWithCache || "auth_error" in geminiWithCache)) {
5347
+ const { cache_identity: _identity, auth_error: _authError, ...geminiPayload } = geminiWithCache;
5348
+ geminiWithCache = geminiPayload;
5349
+ }
4579
5350
 
4580
5351
  const data = {
4581
5352
  fetched_at: new Date(nowMs).toISOString(),
@@ -4586,8 +5357,8 @@ async function fetchUsageLimitsUncached({
4586
5357
  // tier, and membership.level is an opaque enum (LEVEL_INTERMEDIATE) with no
4587
5358
  // authoritative human-readable plan mapping. Both rendered as garbage like
4588
5359
  // "Kimi Type_event" (issue #130), so show the bare brand instead.
4589
- kimi: withPlanLabel(kimi, null, "Kimi"),
4590
- gemini: withPlanLabel(gemini, gemini.account_plan, "Gemini"),
5360
+ kimi: withPlanLabel(kimiWithCache, null, "Kimi"),
5361
+ gemini: withPlanLabel(geminiWithCache, geminiWithCache.account_plan, "Gemini"),
4591
5362
  kiro: withPlanLabel(kiro, kiro.plan_name, "Kiro"),
4592
5363
  antigravity: withPlanLabel(antigravity, antigravity.account_plan, "Antigravity"),
4593
5364
  copilot: withPlanLabel(copilot, copilot.plan_name, "Copilot"),
@@ -4597,7 +5368,7 @@ async function fetchUsageLimitsUncached({
4597
5368
  // outright, so surface the label as-is like CommandCode below. Error and
4598
5369
  // unconfigured branches never carry plan_label, so the panel still falls
4599
5370
  // back to the bare brand name.
4600
- grok,
5371
+ grok: grokWithCache,
4601
5372
  zcode: withPlanLabel(zcode, zcode.plan_label, "ZCode"),
4602
5373
  opencodeGo: withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go"),
4603
5374
  // CommandCode tiers are acronym brands (GOAT/Pro/Max) — the fetcher already
@@ -4616,6 +5387,10 @@ async function fetchUsageLimitsUncached({
4616
5387
  // as separate providers — one row per signed-in edition, never merged.
4617
5388
  workbuddy,
4618
5389
  workbuddyAi,
5390
+ // DeepSeek carries a balance card (no windows, no plan label): the fetcher
5391
+ // stamps cached_at/stale on both its live and last-good paths, so pass it
5392
+ // through verbatim like grok.
5393
+ deepseek,
4619
5394
  };
4620
5395
 
4621
5396
  for (const [providerName, provider] of Object.entries(data)) {