tokentracker-cli 1.5.2 → 1.5.4

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 (134) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-B5kmt0VD.js → AchievementBadge-CKNb_cwg.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-C0FlkOo0.js → AchievementsPage-u9P8DXJI.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-CRcp80Pp.js → AchievementsSection-BSCUJ4DR.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-DAFjGbKW.js → BadgeDetailModal-Bg7cE3Zq.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-CO0bbimB.js → BotAnimated-C7D2WbQA.js} +1 -1
  6. package/dashboard/dist/assets/{Card-Xhr5exz8.js → Card-B4IqAhai.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-DsQ7FmsI.js → CloudCheckoutPage-P75pH9Ym.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-DkSexu5V.js → CloudPage-DDEZfmZg.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-Bm0huQdj.js → CommandPalette-CER_AklA.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-BsL8GN8Q.js → CommunityStatsModal-Cbb74eib.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-_WPp292g.js → ConfirmModal-Q1ZQdZHt.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-ktzn68KM.js → Controls-DaGOfcoz.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-CXI7RmxO.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-DWr4zIzF.js → DateRangePopover-DSS1Ira_.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-CBa8Mk47.js → DevicePage-6zPLYydh.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-CZ5jzgtb.js → DialogClose-DdCQ36LK.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-DztHQvj-.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-Dr0N6q15.js → DialogTitle-BjP217Y1.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-sYqgrO6-.js → ElectricLogo-Dhq8PM4h.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Qq-hcrZW.js → FadeIn-CGNbED4E.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-B77zSv7i.js → HeaderGithubStar-DwqEFzb6.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-lRDaUmoW.js → HoverTooltip-CQZTgyQy.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-B6AvHZjA.js → IpCheckPage-Bz09HXLn.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DgzOjf3x.js → LandingPage-DC-lZsYR.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BtjjtDMC.js → LeaderboardPage-D8MpLM_t.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-B2wNTUTU.js → LeaderboardProBadge-pdXgokhE.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-DJJckwHh.js → LeaderboardProfileModal-nHZLiE0m.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-DJdxByl2.js → LeaderboardProfilePage-BKv3oIAW.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-TmRTgSCb.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-B2lVqf_H.js → LocalOnlyNotice-aF01udyR.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-i0gg1yby.js → LoginCard-BaroD0Ct.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-B-loLNZM.js → LoginModal-0jgyMGVM.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-D_swI75j.js → LoginPage-BUx8U58i.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DDpKvpjG.js → PetPage-CpOjtJqg.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-Gja4MyEK.js → PopoverPopup-BYjfer-M.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-DOPj7tAZ.js → ProviderIcon-NtYa2h0u.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BVADLk89.js → ResetPasswordPage-CYUg7TR1.js} +1 -1
  38. package/dashboard/dist/assets/{Select-D-38tN-E.js → Select-CYP95o5y.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-jN61ukxk.js → SelectItemText-BCDfusOM.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-k7AkGpl1.js → SelfHostPage-BmVc2rIT.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-Dyp8NwIn.js +1 -0
  42. package/dashboard/dist/assets/SessionsPage-tGEj6Ztz.js +3 -0
  43. package/dashboard/dist/assets/{SettingsPage-DfnMCAuA.js → SettingsPage-CCMbq5wc.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-YyIHIaHN.js → SkillsPage-C4O4fbaQ.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BLcvZuQ9.js → TokenGalaxy-D2d738xr.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-3E8kyVzz.js → TooltipPopup-BJfFYesG.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-ByKAdXSF.js → TrendMonitor-CElakFSU.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-BgEVIIzL.js → WidgetsPage-Cvd1LbJm.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-HjbX8QaA.js → WrappedPage-wRC1tE_a.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-Br-KAXh6.css → __vite-browser-external-CMBWGkOq.css} +1 -1
  51. package/dashboard/dist/assets/__vite-browser-external-V4TPLCJG.js +45 -0
  52. package/dashboard/dist/assets/{agent-logos-BVF2siPQ.js → agent-logos-Ba57HF3w.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-CYO24PGP.js → arrow-left-B1bKnxXL.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-Xg9PZYhw.js → arrow-right-DrOlKzX5.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-B7ah6Qv-.js → arrow-up-right-hlYpo3yw.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-BV_iG938.js → calendar-days-CSY6SEsI.js} +1 -1
  57. package/dashboard/dist/assets/{check--EZH8RTp.js → check-xrP1gLw5.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-BzGTlk0-.js → chevron-down-6-zT5n9d.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-BgyxPHEh.js → cloud-checkout-BZymZD4K.js} +1 -1
  60. package/dashboard/dist/assets/{composite-C0KrmLEK.js → composite-C9izWafa.js} +1 -1
  61. package/dashboard/dist/assets/{copy-BtucSK2U.js → copy-Dkas5wZX.js} +1 -1
  62. package/dashboard/dist/assets/{download-CWHq03Y2.js → download-CagGRs0_.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-DCTXF5VZ.js → external-link-DRl10hxd.js} +1 -1
  64. package/dashboard/dist/assets/{flame-BE5TsqVF.js → flame-CkvmsTFB.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-ThNIMWnZ.js → format-tokens-BtwhO3mV.js} +1 -1
  66. package/dashboard/dist/assets/{gift-DgaN9s4-.js → gift-YzjXkMQx.js} +1 -1
  67. package/dashboard/dist/assets/{icons-B4r5EiDC.js → icons-CHhKYs0u.js} +1 -1
  68. package/dashboard/dist/assets/{index-DqCtgU0G.js → index-B0MQZTrM.js} +1 -1
  69. package/dashboard/dist/assets/{index-fNKH4U7j.js → index-C2DK8av-.js} +1 -1
  70. package/dashboard/dist/assets/{index-CKhcCLNC.js → index-CJknN9Jd.js} +1 -1
  71. package/dashboard/dist/assets/{index-DFF9xo6f.js → index-DojzoMqd.js} +1 -1
  72. package/dashboard/dist/assets/{info-B4YzRR4y.js → info-Torep-Ti.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-DzbUbaBw.js → link-2-DE5Yu-z6.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-B9c7D-7v.js → loader-circle-BL8LxD9n.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-btuk_b0Y.js → local-usage-transfer-8X1InWix.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-BjrU-Xz7.js → modifiers.esm-D60-6UQa.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-Bwn4tb4q.js → monitor-smartphone-C-IFPjqk.js} +1 -1
  78. package/dashboard/dist/assets/{play-DIFOwGIQ.js → play-n6uWrdH9.js} +1 -1
  79. package/dashboard/dist/assets/{plus-BpBqnwVg.js → plus-B2f6TaNj.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-DSrs4u7T.js → provider-display-DWqCEcsP.js} +1 -1
  81. package/dashboard/dist/assets/{react-BgG7_MY0.js → react-B-zfi_j6.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-BUGWIDOM.js → safePolygon-C2PsvI-v.js} +1 -1
  83. package/dashboard/dist/assets/{search-aHrmBmtZ.js → search-DZOaTLF9.js} +1 -1
  84. package/dashboard/dist/assets/{server-DOEvwlEU.js → server-DIp4Qfsv.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-BPTnLhsT.js → shield-check-Cs2M8-iT.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-C052Mwez.js → skills-api-CoYy6Tov.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-DaZKhjhu.js → sortable.esm-B5_hjIfs.js} +1 -1
  88. package/dashboard/dist/assets/{store-Cq30Pofw.js → store-BLXqtJzb.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-CoSDSkE7.js → terminal-CEMKX3ba.js} +1 -1
  90. package/dashboard/dist/assets/{trash-BAmt-kPj.js → trash-DOWYaXjK.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-Bxq0kBhm.js → use-community-stats-BjRI1Bzz.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-DmIQDlvk.js → use-latest-request-guard-T7L5qBU6.js} +1 -1
  93. package/dashboard/dist/assets/use-limits-display-prefs-vNIrCPZA.js +1 -0
  94. package/dashboard/dist/assets/{use-native-settings-s_CIC8Bb.js → use-native-settings-Cg5Zb4q_.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list-BHhykAbt.js → use-ordered-list-BF-wIvmG.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-B9K5mIx4.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-CVeMFlS6.js → use-transform-CbJ1IcNc.js} +1 -1
  98. package/dashboard/dist/assets/use-usage-limits-Dgg3jpC4.js +1 -0
  99. package/dashboard/dist/assets/{useCurrency-DilqL2Iv.js → useCurrency-3oBHWfSe.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-BcnySw8a.js → usePositioner-D0YrIW0A.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-CvKW46WP.js → useTokenFormat-DZAHWE_R.js} +1 -1
  102. package/dashboard/dist/assets/{zap-D7VRNTwk.js → zap-kLub6Jiv.js} +1 -1
  103. package/dashboard/dist/index.html +2 -2
  104. package/dashboard/dist/ip-check.html +2 -2
  105. package/dashboard/dist/leaderboard.html +2 -2
  106. package/dashboard/dist/share.html +2 -2
  107. package/package.json +1 -1
  108. package/src/commands/serve.js +25 -15
  109. package/src/lib/accept-encoding.js +59 -0
  110. package/src/lib/cursor-config.js +3 -1
  111. package/src/lib/deepseek-limits.js +280 -0
  112. package/src/lib/deepseek-status.js +157 -0
  113. package/src/lib/grok-limits.js +40 -1
  114. package/src/lib/local-api.js +248 -10
  115. package/src/lib/opencode-status.js +88 -0
  116. package/src/lib/pricing/index.js +182 -79
  117. package/src/lib/pricing/seed-snapshot.json +1 -1
  118. package/src/lib/provider-api-key-store.js +2 -0
  119. package/src/lib/provider-status.js +27 -2
  120. package/src/lib/qoder-limits.js +17 -2
  121. package/src/lib/session-analytics.js +194 -29
  122. package/src/lib/static-server.js +74 -2
  123. package/src/lib/usage-limits.js +714 -53
  124. package/src/lib/workbuddy-limits.js +291 -26
  125. package/src/lib/zcode-limits.js +143 -35
  126. package/dashboard/dist/assets/DashboardPage-CdNXi3Im.js +0 -1
  127. package/dashboard/dist/assets/DialogDescription-kCZ4gN09.js +0 -1
  128. package/dashboard/dist/assets/LimitsPage-gSBHSBi_.js +0 -6
  129. package/dashboard/dist/assets/ServiceStatusPage-D_Fa_pYz.js +0 -1
  130. package/dashboard/dist/assets/SessionsPage-BymccEEz.js +0 -3
  131. package/dashboard/dist/assets/__vite-browser-external-D_cMeuqI.js +0 -45
  132. package/dashboard/dist/assets/use-limits-display-prefs-CyAn1fme.js +0 -1
  133. package/dashboard/dist/assets/use-reduced-motion-BmFyFE3S.js +0 -1
  134. package/dashboard/dist/assets/use-usage-limits-DRZMI20u.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");
@@ -50,21 +51,54 @@ const execFileAsync = promisify(cp.execFile);
50
51
  // 2-minute in-memory cache. It also expires early at the earliest upcoming window
51
52
  // reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
52
53
  // a reset "right now" can't turn every poll into a full upstream round.
53
- // Partitioned by the Devin opt-in selection AND the Antigravity quota opt-out, so
54
- // a request made while Devin is off is never served (or joined onto) a response
55
- // fetched while it was on, and an aggregate fetched while Antigravity was enabled
56
- // is never served after the opt-out is set. Key = `${devin}-${antigravity}`.
57
- const USAGE_LIMITS_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
54
+ // Partitioned by the Devin opt-in selection AND the Antigravity quota opt-out AND
55
+ // the per-request hidden-provider set (#956), so a request made while Devin is off
56
+ // is never served (or joined onto) a response fetched while it was on, an aggregate
57
+ // fetched while Antigravity was enabled is never served after the opt-out is set,
58
+ // and a response fetched for a visibility selection is never served to one made
59
+ // under a different one. Key = `${devin}-${antigravity}[-<hidden ids>]`.
60
+ const USAGE_LIMITS_BASE_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
58
61
  const cacheBySelection = {};
59
- for (const key of USAGE_LIMITS_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
62
+ for (const key of USAGE_LIMITS_BASE_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
60
63
  // Credential replacements (provider-key save) bump this generation so a fetch
61
64
  // that started under the old credential cannot repopulate the memory or disk
62
65
  // caches after invalidation.
63
66
  let usageLimitsCacheGeneration = 0;
67
+ // Provider row keys of the usage-limits aggregate — the id space `hidden`
68
+ // parameter values are validated against (dashboard rows, menu-bar
69
+ // LimitsSettingsStore.allProviders, and this payload all share it).
70
+ const USAGE_LIMITS_PROVIDER_IDS = Object.freeze([
71
+ "claude", "codex", "cursor", "gemini", "kimi", "kiro", "grok",
72
+ "copilot", "antigravity", "zcode", "opencodeGo", "commandCode",
73
+ "qoder", "qoderCn", "codingPlan", "agentPlan", "devin",
74
+ "workbuddy", "workbuddyAi", "deepseek",
75
+ ]);
76
+
77
+ // Normalize a caller-supplied hidden-provider list (array, Set, or
78
+ // comma-separated query value) into a sorted, de-duplicated id array, or null
79
+ // when nothing is hidden. Unknown ids are dropped: the parameter only ever
80
+ // narrows the fetch, so a silently ignored typo must not widen the cache-key
81
+ // space (and leniency matches the devin query param, which accepts any value
82
+ // that is not "1"/"true" as off).
83
+ function normalizeHiddenProviders(input) {
84
+ const raw = input instanceof Set
85
+ ? [...input]
86
+ : Array.isArray(input)
87
+ ? input
88
+ : typeof input === "string" && input.trim().length > 0
89
+ ? input.split(",")
90
+ : [];
91
+ const ids = [...new Set(
92
+ raw.map((id) => String(id).trim()).filter((id) => USAGE_LIMITS_PROVIDER_IDS.includes(id)),
93
+ )].sort();
94
+ return ids.length > 0 ? ids : null;
95
+ }
96
+
64
97
  function usageLimitsSelectionKey(options) {
65
98
  const devin = options?.devinEnabled === true ? "on" : "off";
66
99
  const antigravity = isAntigravityQuotaDisabled() ? "off" : "on";
67
- return `${devin}-${antigravity}`;
100
+ const hidden = normalizeHiddenProviders(options?.hiddenProviders);
101
+ return `${devin}-${antigravity}${hidden ? `-${hidden.join("+")}` : ""}`;
68
102
  }
69
103
  const CACHE_TTL_MS = 2 * 60 * 1000;
70
104
  // Must stay below the macOS app's post-reset re-fetch grace (10s in
@@ -139,6 +173,24 @@ const CODEX_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
139
173
  const OPENCODE_GO_LIMITS_CACHE_FILE = "opencode-go-usage-limits-cache.json";
140
174
  const OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
141
175
  const OPENCODE_GO_LOCAL_ESTIMATE_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
176
+ // Cursor's web summary (cursor.com) and Grok Bot RPCs (api2.cursor.sh) both traverse
177
+ // proxied routes where a timeout or 5xx previously blanked the card until the next
178
+ // poll. Persist the last successful windows so a blip serves stale bars instead
179
+ // (mirrors Claude / Codex / OpenCode Go).
180
+ const CURSOR_LIMITS_CACHE_FILE = "cursor-usage-limits-cache.json";
181
+ const CURSOR_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
182
+ // The Kimi coding API (api.kimi.com / api.kimi.ai) is another proxied route where
183
+ // a timeout or 5xx previously blanked the card until the next poll. Persist the
184
+ // last successful windows so a blip serves stale bars instead
185
+ // (mirrors Cursor / Codex / OpenCode Go).
186
+ const KIMI_LIMITS_CACHE_FILE = "kimi-usage-limits-cache.json";
187
+ const KIMI_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
188
+ // Grok Build's billing proxy (cli-chat-proxy.grok.com) sits behind the same kind of
189
+ // proxied routes where a timeout or 5xx previously blanked the card until the next
190
+ // poll. Persist the last successful windows so a blip serves stale bars instead
191
+ // (mirrors Claude / Codex / Cursor).
192
+ const GROK_LIMITS_CACHE_FILE = "grok-usage-limits-cache.json";
193
+ const GROK_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
142
194
  // A 429 from the usage endpoint carries a long `retry-after` (often 20+ minutes). Persist
143
195
  // the cooldown so every surface — this process, the menu bar app's embedded server, a later
144
196
  // restart — stops calling until it expires. Hammering during the cooldown just renews the
@@ -237,6 +289,43 @@ function withAbortableProviderTimeout(start, label, timeoutMs) {
237
289
  .finally(() => controller.abort());
238
290
  }
239
291
 
292
+ // WorkBuddy bounds its own live chain (the IPC proxy timeout is internal), and
293
+ // its disk-cache fallback runs after the abort lands: the abort makes the
294
+ // socket fail within milliseconds and the cache read is synchronous. A plain
295
+ // provider-timeout race would settle on the timeout first and drop that
296
+ // fallback, so after the budget expires, abort and give the attempt a short
297
+ // grace to land its fallback — its outcome (cache hit or specific error)
298
+ // replaces the generic timeout. The hard bound is timeoutMs + graceMs.
299
+ function withWorkbuddyProviderTimeout(start, label, timeoutMs, graceMs = 2_000) {
300
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return start(undefined);
301
+ const controller = new AbortController();
302
+ const work = start(controller.signal);
303
+ let timeoutTimer;
304
+ let graceTimer;
305
+ let deadlineFired = false;
306
+ // Once the budget aborts the attempt, any rejection it produces is the
307
+ // deadline's doing — the IPC socket fails with its cancellation error, a
308
+ // fetch with an AbortError — so report the labeled timeout instead of the
309
+ // symptom. A post-abort resolution still wins the grace race (the
310
+ // disk-cache fallback).
311
+ const guardedWork = work.catch((error) => {
312
+ if (deadlineFired) throw new Error(`${label} usage request timed out.`);
313
+ throw error;
314
+ });
315
+ const timeoutPromise = new Promise((_, reject) => {
316
+ timeoutTimer = setTimeout(() => {
317
+ deadlineFired = true;
318
+ controller.abort();
319
+ graceTimer = setTimeout(() => reject(new Error(`${label} usage request timed out.`)), graceMs);
320
+ }, timeoutMs);
321
+ });
322
+ return Promise.race([guardedWork, timeoutPromise]).finally(() => {
323
+ clearTimeout(timeoutTimer);
324
+ clearTimeout(graceTimer);
325
+ controller.abort();
326
+ });
327
+ }
328
+
240
329
  function parseRetryAfterSeconds(headers) {
241
330
  const ra = headers?.get ? headers.get("retry-after") : null;
242
331
  const sec = ra ? Number.parseInt(ra, 10) : NaN;
@@ -790,11 +879,11 @@ function normalizeCursorSandUsageStatus(body, { eligible = true } = {}) {
790
879
  return buildWindow({ usedPercent, resetAt, windowSeconds });
791
880
  }
792
881
 
793
- async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
794
- if (!isCursorInstalled({ home })) {
882
+ async function fetchCursorLimits({ home, env, fetchImpl = fetch } = {}) {
883
+ if (!isCursorInstalled({ home, env })) {
795
884
  return { configured: false };
796
885
  }
797
- const auth = extractCursorAuth({ home });
886
+ const auth = extractCursorAuth({ home, env });
798
887
  if (!auth?.cookie || !auth?.accessToken) {
799
888
  return { configured: false };
800
889
  }
@@ -817,11 +906,18 @@ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
817
906
  error: null,
818
907
  ...normalizeCursorUsageSummary(body),
819
908
  quaternary_window: grokBotWindow,
909
+ // Internal cache-partition key (stripped before the payload is served).
910
+ cache_identity: auth.userId,
820
911
  };
821
912
  } catch (error) {
822
913
  return {
823
914
  configured: true,
824
915
  error: error?.message || "Unknown error",
916
+ // A session-expiry error is actionable (re-login) and must not be hidden
917
+ // behind the stale disk-cache fallback (opencode-go auth_error precedent).
918
+ // Internal marker — stripped before the payload is served.
919
+ ...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
920
+ cache_identity: auth.userId,
825
921
  };
826
922
  }
827
923
  }
@@ -894,7 +990,9 @@ function kimiCredentialsExpired(creds, nowMs = Date.now()) {
894
990
 
895
991
  async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch } = {}) {
896
992
  if (typeof refreshToken !== "string" || !refreshToken.trim()) {
897
- throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
993
+ const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
994
+ error.code = "AUTH_EXPIRED";
995
+ throw error;
898
996
  }
899
997
 
900
998
  const body = new URLSearchParams({
@@ -913,7 +1011,9 @@ async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch
913
1011
  body,
914
1012
  });
915
1013
  if (res.status === 401 || res.status === 403) {
916
- throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
1014
+ const error = new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
1015
+ error.code = "AUTH_EXPIRED";
1016
+ throw error;
917
1017
  }
918
1018
  if (!res.ok) {
919
1019
  throw new Error(`Kimi token refresh failed (HTTP ${res.status})`);
@@ -947,7 +1047,11 @@ async function fetchKimiUsage(accessToken, { profile, fetchImpl = fetch } = {})
947
1047
  },
948
1048
  });
949
1049
  if (res.status === 401) {
950
- throw new Error("token_expired");
1050
+ // The retry-with-refresh path keys off this message; the code marks the
1051
+ // error actionable (re-login) so the cache fallback never hides it.
1052
+ const error = new Error("token_expired");
1053
+ error.code = "AUTH_EXPIRED";
1054
+ throw error;
951
1055
  }
952
1056
  if (!res.ok) {
953
1057
  throw new Error(`Kimi API returned ${res.status}`);
@@ -960,12 +1064,17 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
960
1064
  if (!profile.configured) {
961
1065
  return { configured: false };
962
1066
  }
963
- if (profile.error) return { configured: true, error: profile.error };
1067
+ // A profile misconfiguration (unsupported endpoints, non-file credential
1068
+ // storage) is actionable — the user must fix their config — so it must not
1069
+ // be hidden behind the stale disk-cache fallback. Internal marker —
1070
+ // stripped before the payload is served.
1071
+ if (profile.error) return { configured: true, error: profile.error, config_error: true };
964
1072
  const creds = loadKimiCredentials({ profile });
965
1073
  let accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
966
1074
  if (!accessToken) {
967
1075
  return { configured: false };
968
1076
  }
1077
+ const cacheIdentity = kimiProfileIdentity(profile);
969
1078
  try {
970
1079
  if (kimiCredentialsExpired(creds) && creds?.refresh_token) {
971
1080
  accessToken = await refreshKimiAccessToken({
@@ -993,11 +1102,18 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
993
1102
  configured: true,
994
1103
  error: null,
995
1104
  ...normalizeKimiUsageResponse(body),
1105
+ // Internal cache-partition key (stripped before the payload is served).
1106
+ cache_identity: cacheIdentity,
996
1107
  };
997
1108
  } catch (error) {
998
1109
  return {
999
1110
  configured: true,
1000
1111
  error: error?.message || "Unknown error",
1112
+ // A session-expiry error is actionable (re-login) and must not be hidden
1113
+ // behind the stale disk-cache fallback (opencode-go auth_error precedent).
1114
+ // Internal marker — stripped before the payload is served.
1115
+ ...(error?.code === "AUTH_EXPIRED" ? { auth_error: true } : {}),
1116
+ cache_identity: cacheIdentity,
1001
1117
  };
1002
1118
  }
1003
1119
  }
@@ -2697,6 +2813,323 @@ function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now
2697
2813
  } catch (_error) {}
2698
2814
  }
2699
2815
 
2816
+ function resolveCursorLimitsCachePath({ home } = {}) {
2817
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", CURSOR_LIMITS_CACHE_FILE);
2818
+ }
2819
+
2820
+ function hasCursorWindow(limits) {
2821
+ return Boolean(
2822
+ limits?.primary_window
2823
+ || limits?.secondary_window
2824
+ || limits?.tertiary_window
2825
+ || limits?.quaternary_window,
2826
+ );
2827
+ }
2828
+
2829
+ // Cursor windows carry `reset_at` as an ISO string (billing-cycle end for the three
2830
+ // summary windows, the RPC's own expiry for the Grok Bot window). A window whose reset
2831
+ // has already passed is stale data, not a usable fallback, so drop it. Windows without
2832
+ // a reset stamp are kept — the overall cached_at max-age gate already bounds them.
2833
+ function isCursorCacheWindowUsable(window, { nowMs } = {}) {
2834
+ if (!window || typeof window !== "object") return false;
2835
+ const resetAtMs = parseTimeMs(window.reset_at);
2836
+ if (resetAtMs === null) return true;
2837
+ return resetAtMs > nowMs;
2838
+ }
2839
+
2840
+ function normalizeCursorCachedLimits(
2841
+ raw,
2842
+ { nowMs = Date.now(), maxAgeMs = CURSOR_LIMITS_CACHE_MAX_AGE_MS } = {},
2843
+ ) {
2844
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
2845
+ if (!Number.isFinite(cachedAtMs)) return null;
2846
+ if (cachedAtMs > nowMs + 60_000) return null;
2847
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
2848
+
2849
+ const cached = {
2850
+ configured: true,
2851
+ error: null,
2852
+ membership_type: typeof raw?.membership_type === "string" ? raw.membership_type : null,
2853
+ primary_window: isCursorCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
2854
+ secondary_window: isCursorCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
2855
+ tertiary_window: isCursorCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
2856
+ quaternary_window: isCursorCacheWindowUsable(raw?.quaternary_window, { nowMs }) ? raw.quaternary_window : null,
2857
+ stale: true,
2858
+ cached_at: raw.cached_at,
2859
+ };
2860
+ return hasCursorWindow(cached) ? cached : null;
2861
+ }
2862
+
2863
+ // The signed-in Cursor account partitions the cache (zcode login-identity precedent):
2864
+ // the id is the non-secret normalized subject Cursor itself puts in the session cookie.
2865
+ // A single home holds one active login, so one stamped entry suffices; a read requires
2866
+ // the stored identity to match the current one, and a write without an identity is
2867
+ // refused — an unreadable login must never pin another account's bars in place.
2868
+ function readCursorLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
2869
+ if (!identity) return null;
2870
+ try {
2871
+ const parsed = JSON.parse(fs.readFileSync(resolveCursorLimitsCachePath({ home }), "utf8"));
2872
+ const entry = parsed?.cursor;
2873
+ if (entry?.identity !== identity) return null;
2874
+ return normalizeCursorCachedLimits(entry, { nowMs });
2875
+ } catch (_error) {
2876
+ return null;
2877
+ }
2878
+ }
2879
+
2880
+ function writeCursorLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
2881
+ if (!identity) return;
2882
+ if (!limits?.configured || limits.error || !hasCursorWindow(limits)) return;
2883
+ const cachePath = resolveCursorLimitsCachePath({ home });
2884
+ const payload = {
2885
+ cursor: {
2886
+ identity,
2887
+ membership_type: limits.membership_type || null,
2888
+ primary_window: limits.primary_window || null,
2889
+ secondary_window: limits.secondary_window || null,
2890
+ tertiary_window: limits.tertiary_window || null,
2891
+ quaternary_window: limits.quaternary_window || null,
2892
+ cached_at: new Date(nowMs).toISOString(),
2893
+ },
2894
+ };
2895
+ try {
2896
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
2897
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
2898
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
2899
+ fs.renameSync(tmpPath, cachePath);
2900
+ } catch (_error) {}
2901
+ }
2902
+
2903
+ // Failure-only identity re-derivation for the cache fallback: when the provider slot's
2904
+ // result was produced by the outer timeout race, the fetcher's own identity field is
2905
+ // lost, so read the (still-local) state DB once to learn who we are serving for.
2906
+ function resolveCursorCacheIdentity({ home, env } = {}) {
2907
+ try {
2908
+ return extractCursorAuth({ home, env })?.userId || null;
2909
+ } catch (_error) {
2910
+ return null;
2911
+ }
2912
+ }
2913
+
2914
+ // The active Kimi profile partitions the cache (ark profile_identity precedent):
2915
+ // mainland and global logins — and any KIMI_HOME override — resolve to different
2916
+ // endpoint-scoped credential slots, and one slot must never serve another's
2917
+ // windows. The profile home plus OAuth host is non-secret and derivable from
2918
+ // local files alone (the credential file holds no account id).
2919
+ function kimiProfileIdentity(profile) {
2920
+ if (!profile?.home || !profile?.oauthHost) return null;
2921
+ return `${profile.home}::${profile.oauthHost}`;
2922
+ }
2923
+
2924
+ // Failure-only identity re-derivation for the cache fallback: when the provider
2925
+ // slot's result was produced by the outer timeout race, the fetcher's own
2926
+ // identity field is lost, so re-resolve the (still-local) Kimi profile.
2927
+ function resolveKimiCacheIdentity({ home, env } = {}) {
2928
+ try {
2929
+ return kimiProfileIdentity(resolveKimiProfile({ home, env }));
2930
+ } catch (_error) {
2931
+ return null;
2932
+ }
2933
+ }
2934
+
2935
+ function resolveKimiLimitsCachePath({ home } = {}) {
2936
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", KIMI_LIMITS_CACHE_FILE);
2937
+ }
2938
+
2939
+ function hasKimiWindow(limits) {
2940
+ return Boolean(
2941
+ limits?.primary_window
2942
+ || limits?.secondary_window
2943
+ || limits?.tertiary_window,
2944
+ );
2945
+ }
2946
+
2947
+ // Kimi windows carry `reset_at` (ISO) when the API reports a cycle end; a window
2948
+ // whose reset has already passed is stale data, not a usable fallback, so drop
2949
+ // it. Windows without a reset stamp are kept — the overall cached_at max-age
2950
+ // gate already bounds them.
2951
+ function isKimiCacheWindowUsable(window, { nowMs } = {}) {
2952
+ if (!window || typeof window !== "object") return false;
2953
+ const resetAtMs = parseTimeMs(window.reset_at);
2954
+ if (resetAtMs === null) return true;
2955
+ return resetAtMs > nowMs;
2956
+ }
2957
+
2958
+ function normalizeKimiCachedLimits(
2959
+ raw,
2960
+ { nowMs = Date.now(), maxAgeMs = KIMI_LIMITS_CACHE_MAX_AGE_MS } = {},
2961
+ ) {
2962
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
2963
+ if (!Number.isFinite(cachedAtMs)) return null;
2964
+ if (cachedAtMs > nowMs + 60_000) return null;
2965
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
2966
+
2967
+ const cached = {
2968
+ configured: true,
2969
+ error: null,
2970
+ membership_level: typeof raw?.membership_level === "string" ? raw.membership_level : null,
2971
+ subscription_type: typeof raw?.subscription_type === "string" ? raw.subscription_type : null,
2972
+ parallel_limit: Number.isFinite(raw?.parallel_limit) ? raw.parallel_limit : null,
2973
+ primary_window: isKimiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
2974
+ secondary_window: isKimiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
2975
+ tertiary_window: isKimiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
2976
+ stale: true,
2977
+ cached_at: raw.cached_at,
2978
+ };
2979
+ return hasKimiWindow(cached) ? cached : null;
2980
+ }
2981
+
2982
+ function readKimiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
2983
+ if (!identity) return null;
2984
+ try {
2985
+ const parsed = JSON.parse(fs.readFileSync(resolveKimiLimitsCachePath({ home }), "utf8"));
2986
+ const entry = parsed?.kimi;
2987
+ if (entry?.identity !== identity) return null;
2988
+ return normalizeKimiCachedLimits(entry, { nowMs });
2989
+ } catch (_error) {
2990
+ return null;
2991
+ }
2992
+ }
2993
+
2994
+ function writeKimiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
2995
+ if (!identity) return;
2996
+ if (!limits?.configured || limits.error || !hasKimiWindow(limits)) return;
2997
+ const cachePath = resolveKimiLimitsCachePath({ home });
2998
+ const payload = {
2999
+ kimi: {
3000
+ identity,
3001
+ membership_level: limits.membership_level || null,
3002
+ subscription_type: limits.subscription_type || null,
3003
+ parallel_limit: Number.isFinite(limits.parallel_limit) ? limits.parallel_limit : null,
3004
+ primary_window: limits.primary_window || null,
3005
+ secondary_window: limits.secondary_window || null,
3006
+ tertiary_window: limits.tertiary_window || null,
3007
+ cached_at: new Date(nowMs).toISOString(),
3008
+ },
3009
+ };
3010
+ try {
3011
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3012
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
3013
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3014
+ fs.renameSync(tmpPath, cachePath);
3015
+ } catch (_error) {}
3016
+ }
3017
+
3018
+ function resolveGrokLimitsCachePath({ home } = {}) {
3019
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", GROK_LIMITS_CACHE_FILE);
3020
+ }
3021
+
3022
+ function hasGrokWindow(limits) {
3023
+ return Boolean(limits?.primary_window || limits?.secondary_window);
3024
+ }
3025
+
3026
+ // Grok windows carry `reset_at` as an ISO string — the billing-period end shared
3027
+ // by the pool, on-demand, and per-product windows. A window whose reset has
3028
+ // already passed is stale data, not a usable fallback, so drop it. Windows
3029
+ // without a reset stamp are kept — the overall cached_at max-age gate already
3030
+ // bounds them (mirrors Cursor).
3031
+ function isGrokCacheWindowUsable(window, { nowMs } = {}) {
3032
+ if (!window || typeof window !== "object") return false;
3033
+ const resetAtMs = parseTimeMs(window.reset_at);
3034
+ if (resetAtMs === null) return true;
3035
+ return resetAtMs > nowMs;
3036
+ }
3037
+
3038
+ function normalizeGrokCachedLimits(
3039
+ raw,
3040
+ { nowMs = Date.now(), maxAgeMs = GROK_LIMITS_CACHE_MAX_AGE_MS } = {},
3041
+ ) {
3042
+ const cachedAtMs = parseTimeMs(raw?.cached_at);
3043
+ if (!Number.isFinite(cachedAtMs)) return null;
3044
+ if (cachedAtMs > nowMs + 60_000) return null;
3045
+ if (nowMs - cachedAtMs > maxAgeMs) return null;
3046
+
3047
+ const cached = {
3048
+ configured: true,
3049
+ error: null,
3050
+ plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
3051
+ period_type: typeof raw?.period_type === "string" ? raw.period_type : null,
3052
+ monthly_credits_limit: raw?.monthly_credits_limit ?? null,
3053
+ monthly_credits_used: raw?.monthly_credits_used ?? null,
3054
+ credit_usage_percent: raw?.credit_usage_percent ?? null,
3055
+ on_demand_cap: raw?.on_demand_cap ?? null,
3056
+ on_demand_used: raw?.on_demand_used ?? null,
3057
+ billing_period_start: typeof raw?.billing_period_start === "string" ? raw.billing_period_start : null,
3058
+ primary_window: isGrokCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
3059
+ secondary_window: isGrokCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
3060
+ // Per-product shares of the pool, not independent windows — each is gated
3061
+ // by the same reset stamp as the pool window it attributes.
3062
+ products: Array.isArray(raw?.products)
3063
+ ? raw.products.filter((entry) => isGrokCacheWindowUsable(entry?.window, { nowMs }))
3064
+ : [],
3065
+ stale: true,
3066
+ cached_at: raw.cached_at,
3067
+ };
3068
+ return hasGrokWindow(cached) ? cached : null;
3069
+ }
3070
+
3071
+ // The signed-in Grok account partitions the cache (zcode login-identity precedent):
3072
+ // the id is the non-secret OIDC `sub` claim inside the access token — the
3073
+ // credential itself never enters the id (no token fingerprint fallback: CodeQL
3074
+ // js/insufficient-password-hash precedent). auth.x.ai issues JWT access tokens
3075
+ // (RFC 9068), so the subject is present in practice; an opaque token yields no
3076
+ // identity and the cache is simply refused on both the write and the read path
3077
+ // (fail-closed) — an unattributable login must never pin another account's bars,
3078
+ // which a scope-key fallback cannot guarantee because `issuer::client_id` stays
3079
+ // constant across account switches.
3080
+ function readGrokLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
3081
+ if (!identity) return null;
3082
+ try {
3083
+ const parsed = JSON.parse(fs.readFileSync(resolveGrokLimitsCachePath({ home }), "utf8"));
3084
+ const entry = parsed?.grok;
3085
+ if (entry?.identity !== identity) return null;
3086
+ return normalizeGrokCachedLimits(entry, { nowMs });
3087
+ } catch (_error) {
3088
+ return null;
3089
+ }
3090
+ }
3091
+
3092
+ function writeGrokLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
3093
+ if (!identity) return;
3094
+ if (!limits?.configured || limits.error || !hasGrokWindow(limits)) return;
3095
+ const cachePath = resolveGrokLimitsCachePath({ home });
3096
+ const payload = {
3097
+ grok: {
3098
+ identity,
3099
+ plan_label: limits.plan_label || null,
3100
+ period_type: limits.period_type || null,
3101
+ monthly_credits_limit: limits.monthly_credits_limit ?? null,
3102
+ monthly_credits_used: limits.monthly_credits_used ?? null,
3103
+ credit_usage_percent: limits.credit_usage_percent ?? null,
3104
+ on_demand_cap: limits.on_demand_cap ?? null,
3105
+ on_demand_used: limits.on_demand_used ?? null,
3106
+ billing_period_start: limits.billing_period_start || null,
3107
+ primary_window: limits.primary_window || null,
3108
+ secondary_window: limits.secondary_window || null,
3109
+ products: Array.isArray(limits.products) ? limits.products : [],
3110
+ cached_at: new Date(nowMs).toISOString(),
3111
+ },
3112
+ };
3113
+ try {
3114
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
3115
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
3116
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3117
+ fs.renameSync(tmpPath, cachePath);
3118
+ } catch (_error) {}
3119
+ }
3120
+
3121
+ // Failure-only identity re-derivation for the cache fallback: when the provider
3122
+ // slot's result was produced by the outer timeout race, the fetcher's own
3123
+ // identity field is lost, so read auth.json once to learn who we are serving for.
3124
+ // Opaque tokens yield null → the read is refused (fail-closed, see above).
3125
+ function resolveGrokCacheIdentity({ home, env } = {}) {
3126
+ try {
3127
+ return grokTokenSubject(loadGrokAuthEntry({ home, env })?.entry?.key);
3128
+ } catch (_error) {
3129
+ return null;
3130
+ }
3131
+ }
3132
+
2700
3133
  function resolveClaudeRateLimitPath({ home } = {}) {
2701
3134
  return path.join(home || os.homedir(), ".tokentracker", "tracker", CLAUDE_RATE_LIMIT_FILE);
2702
3135
  }
@@ -3959,7 +4392,6 @@ function withPlanLabel(obj, raw, brand) {
3959
4392
  // local-api.js): a refresh arriving while a fetch is already running reuses that
3960
4393
  // in-flight fetch and returns its result.
3961
4394
  const inFlightBySelection = {};
3962
- for (const key of USAGE_LIMITS_SELECTION_KEYS) inFlightBySelection[key] = null;
3963
4395
 
3964
4396
  // Codex stamps reset_at as unix seconds; every other provider (and Claude's
3965
4397
  // resets_at) uses ISO strings. Numbers that look like epoch milliseconds are
@@ -4001,7 +4433,9 @@ async function getUsageLimits(options = {}) {
4001
4433
  const selection = usageLimitsSelectionKey(options);
4002
4434
  const cache = cacheBySelection[selection];
4003
4435
  const nowMs = Date.now();
4004
- if (cache.data && nowMs < cache.expiresAtMs) {
4436
+ // Unseen selections (a hidden set never requested before this process's
4437
+ // lifetime) simply miss — partitions are created lazily on first fetch.
4438
+ if (cache?.data && nowMs < cache.expiresAtMs) {
4005
4439
  return cache.data;
4006
4440
  }
4007
4441
  if (inFlightBySelection[selection]) {
@@ -4037,6 +4471,11 @@ async function fetchUsageLimitsUncached({
4037
4471
  providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
4038
4472
  forceRefresh = false,
4039
4473
  devinEnabled = false,
4474
+ // Provider visibility gate (#956): ids to skip entirely — no upstream call,
4475
+ // no credential read, no subprocess, no cache read/write. Null/empty means
4476
+ // "fetch every configured provider" (the legacy behavior clients that don't
4477
+ // send the parameter keep getting).
4478
+ hiddenProviders = null,
4040
4479
  // Generation captured by getUsageLimits before the fetch began. A provider
4041
4480
  // key save bumps it, and this fetch must then stop at returning its own
4042
4481
  // result: publishing it would restore pre-save data for every later caller.
@@ -4045,13 +4484,30 @@ async function fetchUsageLimitsUncached({
4045
4484
  // here would let an opt-out flipped mid-fetch store a pre-opt-out result in the
4046
4485
  // post-opt-out slot.
4047
4486
  selectionKey,
4487
+ // Injectable clock for tests. Production leaves it unset so per-provider
4488
+ // cache gates sample the time when they actually run (the live attempt can
4489
+ // spend the whole budget before its fallback reads the cache) instead of
4490
+ // inheriting this fetch's start instant.
4491
+ nowMs,
4048
4492
  } = {}) {
4049
- const nowMs = Date.now();
4050
-
4493
+ const injectedNowMs = Number.isFinite(nowMs) ? nowMs : undefined;
4494
+ nowMs = injectedNowMs ?? Date.now();
4495
+ const hiddenSet = new Set(normalizeHiddenProviders(hiddenProviders) || []);
4496
+
4497
+ // A hidden provider must not have its local credentials read at all (#956):
4498
+ // these upfront token/subscription probes are the first file and keychain
4499
+ // touches of the Claude/Codex chains, so they are gated here and every
4500
+ // downstream step keys off the resulting null token.
4051
4501
  const [claudeOauth, claudeSubscription, codexAuth] = await Promise.all([
4052
- Promise.resolve().then(() => readClaudeCodeOauthToken({ platform, securityRunner, home, nowMs })),
4053
- Promise.resolve().then(() => detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home })),
4054
- readCodexAuthBundle({ home, env }),
4502
+ hiddenSet.has("claude")
4503
+ ? Promise.resolve(null)
4504
+ : Promise.resolve().then(() => readClaudeCodeOauthToken({ platform, securityRunner, home, nowMs })),
4505
+ hiddenSet.has("claude")
4506
+ ? Promise.resolve(null)
4507
+ : Promise.resolve().then(() => detectClaudeCodeSubscriptionDetails({ platform, securityRunner, home })),
4508
+ hiddenSet.has("codex")
4509
+ ? Promise.resolve(null)
4510
+ : readCodexAuthBundle({ home, env }),
4055
4511
  ]);
4056
4512
  const claudeToken = claudeOauth?.accessToken || null;
4057
4513
  const claudeTokenExpiresAtMs = claudeOauth?.expiresAtMs ?? null;
@@ -4107,7 +4563,16 @@ async function fetchUsageLimitsUncached({
4107
4563
  : null;
4108
4564
 
4109
4565
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
4110
- const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGoRaw, qoder, qoderCn, codingPlan, agentPlan, commandCodeRaw, devinRaw, workbuddy, workbuddyAi, claudeServiceStatus] = await Promise.all([
4566
+ // What a hidden provider's Promise.all slot resolves to. Both shapes flow
4567
+ // through the same assembly branches as a genuinely unconfigured provider —
4568
+ // the CommandCode/Devin fulfilled branches keep the value verbatim, the
4569
+ // OpenCode Go branch passes `configured === false` through, Claude/Codex
4570
+ // naturally produce `{ configured: false }` from their gated null tokens —
4571
+ // so no hidden error, cache read, or credential touch can reach the payload
4572
+ // (#956).
4573
+ const hiddenSentinel = Promise.resolve({ configured: false });
4574
+ const hiddenSettled = Promise.resolve({ status: "fulfilled", value: { configured: false } });
4575
+ 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([
4111
4576
  claudeToken && !freshClaudeCache && !claudeRetryAtMs
4112
4577
  ? withProviderTimeout(fetchClaudeUsageLimits(claudeToken, { fetchImpl: providerFetch, maxAttempts: 1 }), "Claude", providerTimeoutMs).then(
4113
4578
  (value) => ({ status: "fulfilled", value }),
@@ -4124,17 +4589,17 @@ async function fetchUsageLimitsUncached({
4124
4589
  (reason) => ({ status: "rejected", reason }),
4125
4590
  )
4126
4591
  : Promise.resolve(null),
4127
- withProviderTimeout(fetchCursorLimits({ home, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
4592
+ hiddenSet.has("cursor") ? hiddenSentinel : withProviderTimeout(fetchCursorLimits({ home, env, fetchImpl: providerFetch }), "Cursor", providerTimeoutMs)
4128
4593
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4129
- withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
4594
+ hiddenSet.has("kimi") ? hiddenSentinel : withProviderTimeout(fetchKimiLimits({ home, env, fetchImpl: providerFetch }), "Kimi", providerTimeoutMs)
4130
4595
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4131
- withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
4596
+ hiddenSet.has("gemini") ? hiddenSentinel : withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
4132
4597
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4133
- fetchKiroLimits({ commandRunner, now, platform, home }),
4598
+ hiddenSet.has("kiro") ? hiddenSentinel : fetchKiroLimits({ commandRunner, now, platform, home }),
4134
4599
  // Antigravity's own budget keeps the serial chain inside providerTimeoutMs; this
4135
4600
  // outer race is the enforcing backstop every other provider already has, and the
4136
4601
  // signal makes a fired race actually kill the spawned scans and open sockets.
4137
- withAbortableProviderTimeout(
4602
+ hiddenSet.has("antigravity") ? hiddenSentinel : withAbortableProviderTimeout(
4138
4603
  (signal) => fetchAntigravityLimits({
4139
4604
  home,
4140
4605
  commandRunner,
@@ -4150,18 +4615,18 @@ async function fetchUsageLimitsUncached({
4150
4615
  "Antigravity",
4151
4616
  providerTimeoutMs,
4152
4617
  ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4153
- withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
4618
+ hiddenSet.has("copilot") ? hiddenSentinel : withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
4154
4619
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4155
- withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch, timeoutMs: providerTimeoutMs }), "Grok Build", providerTimeoutMs)
4620
+ hiddenSet.has("grok") ? hiddenSentinel : withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch, timeoutMs: providerTimeoutMs }), "Grok Build", providerTimeoutMs)
4156
4621
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4157
- withProviderTimeout(fetchZcodeLimits({ home, env, fetchImpl: providerFetch, providerTimeoutMs }), "ZCode", providerTimeoutMs)
4622
+ hiddenSet.has("zcode") ? hiddenSentinel : withProviderTimeout(fetchZcodeLimits({ home, env, fetchImpl: providerFetch, providerTimeoutMs }), "ZCode", providerTimeoutMs)
4158
4623
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4159
4624
  // OpenCode Go: authoritative subscription windows come from the dashboard
4160
4625
  // scrape; local opencode.db cost is available only as an explicit estimate.
4161
4626
  // See src/lib/opencode-go-limits.js.
4162
- withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
4627
+ hiddenSet.has("opencodeGo") ? hiddenSentinel : withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
4163
4628
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4164
- withProviderTimeout(
4629
+ hiddenSet.has("qoder") ? hiddenSentinel : withProviderTimeout(
4165
4630
  fetchQoderLimits({
4166
4631
  home,
4167
4632
  env,
@@ -4171,7 +4636,7 @@ async function fetchUsageLimitsUncached({
4171
4636
  "Qoder",
4172
4637
  providerTimeoutMs,
4173
4638
  ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4174
- withProviderTimeout(
4639
+ hiddenSet.has("qoderCn") ? hiddenSentinel : withProviderTimeout(
4175
4640
  fetchQoderCnLimits({
4176
4641
  home,
4177
4642
  env,
@@ -4185,7 +4650,7 @@ async function fetchUsageLimitsUncached({
4185
4650
  // products sharing the same arkcli binary. No token-consumption source —
4186
4651
  // consumption for the compatible CLIs is already counted from their
4187
4652
  // local files; these only surface the 5h/week/month quota percentages.
4188
- withAbortableProviderTimeout(
4653
+ hiddenSet.has("codingPlan") ? hiddenSentinel : withAbortableProviderTimeout(
4189
4654
  (signal) => fetchArkCodingPlanLimits({
4190
4655
  commandRunner,
4191
4656
  home,
@@ -4197,7 +4662,7 @@ async function fetchUsageLimitsUncached({
4197
4662
  "Ark Coding Plan",
4198
4663
  providerTimeoutMs,
4199
4664
  ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4200
- withAbortableProviderTimeout(
4665
+ hiddenSet.has("agentPlan") ? hiddenSentinel : withAbortableProviderTimeout(
4201
4666
  (signal) => fetchArkAgentPlanLimits({
4202
4667
  commandRunner,
4203
4668
  home,
@@ -4217,7 +4682,7 @@ async function fetchUsageLimitsUncached({
4217
4682
  // outer race bounds the whole slot: without it three sequential per-request
4218
4683
  // timeouts (whoami, then credits+subscriptions) could hold the aggregate
4219
4684
  // ~2x longer than any sibling provider.
4220
- withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
4685
+ hiddenSet.has("commandCode") ? hiddenSettled : withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
4221
4686
  .then(
4222
4687
  (value) => ({ status: "fulfilled", value }),
4223
4688
  (reason) => ({ status: "rejected", reason }),
@@ -4227,18 +4692,39 @@ async function fetchUsageLimitsUncached({
4227
4692
  // ~/.local/share/devin/credentials.toml. No local fallback — window state
4228
4693
  // lives server-side. fetchDevinLimits throws on auth expiry so the
4229
4694
  // assemble step below can flag auth_action_required.
4230
- withProviderTimeout(fetchDevinLimits({ home, env, enabled: devinEnabled === true, fetchImpl: providerFetch }), "Devin", providerTimeoutMs)
4695
+ hiddenSet.has("devin") ? hiddenSettled : withProviderTimeout(fetchDevinLimits({ home, env, enabled: devinEnabled === true, fetchImpl: providerFetch }), "Devin", providerTimeoutMs)
4231
4696
  .then(
4232
4697
  (value) => ({ status: "fulfilled", value }),
4233
4698
  (reason) => ({ status: "rejected", reason }),
4234
4699
  ),
4235
4700
  // WorkBuddy CN and WorkBuddy AI are separate editions. Each one calls only
4236
4701
  // its own local authenticated billing proxy. Abort closes the IPC socket
4237
- // when the provider budget expires. Reward credits are not a plan window.
4238
- withAbortableProviderTimeout((signal) => fetchWorkbuddyLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy", providerTimeoutMs)
4702
+ // when the provider budget expires; the post-abort grace lets the edition's
4703
+ // disk-cache fallback land instead of the bare timeout. Reward credits are
4704
+ // not a plan window.
4705
+ hiddenSet.has("workbuddy") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy", providerTimeoutMs)
4239
4706
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4240
- withAbortableProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal }), "WorkBuddy AI", providerTimeoutMs)
4707
+ hiddenSet.has("workbuddyAi") ? hiddenSentinel : withWorkbuddyProviderTimeout((signal) => fetchWorkbuddyAiLimits({ home, env, fetchImpl: providerFetch, signal, nowMs: injectedNowMs }), "WorkBuddy AI", providerTimeoutMs)
4241
4708
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4709
+ // DeepSeek (platform.deepseek.com, pay-as-you-go): the balance endpoint is
4710
+ // the only documented programmatic source, so this slot yields a balance
4711
+ // card, not quota windows (#960). The last-good disk-cache fallback with a
4712
+ // pure TTL and a key fingerprint binding lives inside deepseek-limits.js;
4713
+ // auth errors bypass it so a rejected key stays actionable. persistCache
4714
+ // is sampled at slot creation — a mid-fetch key save can only leave a
4715
+ // cache entry bound to the OLD key's fingerprint, which a fetch under the
4716
+ // new key will never read.
4717
+ hiddenSet.has("deepseek") ? hiddenSentinel : withProviderTimeout(
4718
+ fetchDeepseekLimits({
4719
+ home,
4720
+ trackerDir,
4721
+ env,
4722
+ fetchImpl: providerFetch,
4723
+ persistCache: cacheGeneration === usageLimitsCacheGeneration,
4724
+ }),
4725
+ "DeepSeek",
4726
+ providerTimeoutMs,
4727
+ ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4242
4728
  // Public status-page probe (fail-soft, own 5-min cache in provider-status.js).
4243
4729
  // Only probed for configured accounts — without a token the Claude section
4244
4730
  // never renders, so the reading would have nowhere to go.
@@ -4255,8 +4741,10 @@ async function fetchUsageLimitsUncached({
4255
4741
  // that still exists means the latter — reporting `configured: false` there hides the
4256
4742
  // whole Claude section while the usage bars (parsed from local logs, no auth needed)
4257
4743
  // keep updating, which reads as "TokenTracker doesn't support my plan" rather than
4258
- // "your CLI login expired". Existence-only probe: no secret is read here.
4259
- const credentialsPresent = detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
4744
+ // "your CLI login expired". Existence-only probe: no secret is read here — and a
4745
+ // hidden Claude skips even that (#956): its row is the plain sentinel either way.
4746
+ const credentialsPresent = !hiddenSet.has("claude")
4747
+ && detectClaudeCodeCredentialsPresence({ platform, securityRunner, home });
4260
4748
  claude = credentialsPresent
4261
4749
  ? { configured: true, error: CLAUDE_AUTH_EXPIRED_MESSAGE, auth_action_required: "reauth" }
4262
4750
  : { configured: false };
@@ -4467,9 +4955,142 @@ async function fetchUsageLimitsUncached({
4467
4955
  : { configured: true, error: reason?.message || "Unknown error" };
4468
4956
  }
4469
4957
 
4470
- const cursorLimits = cursorSandDuplicatesGrokPool(cursor, grok)
4471
- ? { ...cursor, quaternary_window: null }
4472
- : cursor;
4958
+ // Cursor: persist last successful windows so a transient timeout/5xx on the
4959
+ // cursor.com / api2.cursor.sh routes serves stale bars instead of a flashing
4960
+ // "fetch failed" (mirrors Codex / Claude / OpenCode Go). A session-expiry error
4961
+ // (401/403 — "re-login in Cursor") is actionable and must not be hidden behind
4962
+ // stale cache, so it passes through untouched.
4963
+ let cursorWithCache;
4964
+ if (!cursor || cursor.configured === false) {
4965
+ cursorWithCache = cursor;
4966
+ } else if (cursor.auth_error === true) {
4967
+ cursorWithCache = cursor;
4968
+ } else if (!cursor.error && hasCursorWindow(cursor)) {
4969
+ cursorWithCache = {
4970
+ ...cursor,
4971
+ stale: false,
4972
+ cached_at: new Date(nowMs).toISOString(),
4973
+ };
4974
+ if (cacheGeneration === usageLimitsCacheGeneration) {
4975
+ writeCursorLimitsCache(cursorWithCache, {
4976
+ home,
4977
+ nowMs,
4978
+ identity: typeof cursor.cache_identity === "string"
4979
+ ? cursor.cache_identity
4980
+ : resolveCursorCacheIdentity({ home, env }),
4981
+ });
4982
+ }
4983
+ } else {
4984
+ // The slot's error object loses the fetcher's identity field when the outer
4985
+ // timeout race wins (the common failure mode this cache exists for), so
4986
+ // re-derive it from the local state DB on this failure-only path.
4987
+ const identity = typeof cursor.cache_identity === "string"
4988
+ ? cursor.cache_identity
4989
+ : resolveCursorCacheIdentity({ home, env });
4990
+ const cached = readCursorLimitsCache({ home, nowMs, identity });
4991
+ cursorWithCache = cached || cursor;
4992
+ }
4993
+ // cache_identity / auth_error are internal cache-plumbing markers, not payload.
4994
+ if (cursorWithCache && ("cache_identity" in cursorWithCache || "auth_error" in cursorWithCache)) {
4995
+ const { cache_identity: _identity, auth_error: _authError, ...cursorPayload } = cursorWithCache;
4996
+ cursorWithCache = cursorPayload;
4997
+ }
4998
+
4999
+ // Grok: persist last successful windows so a transient timeout/5xx on the
5000
+ // cli-chat-proxy.grok.com route serves stale bars instead of a flashing
5001
+ // "fetch failed" (mirrors Codex / Claude / Cursor). A re-login error
5002
+ // (GROK_AUTH_REQUIRED / GROK_REAUTH_REQUIRED — "run `grok login`") is
5003
+ // actionable and must not be hidden behind stale cache, so it passes
5004
+ // through untouched.
5005
+ let grokWithCache;
5006
+ if (!grok || grok.configured === false) {
5007
+ grokWithCache = grok;
5008
+ } else if (grok.auth_error === true) {
5009
+ grokWithCache = grok;
5010
+ } else if (!grok.error && hasGrokWindow(grok)) {
5011
+ grokWithCache = {
5012
+ ...grok,
5013
+ stale: false,
5014
+ cached_at: new Date(nowMs).toISOString(),
5015
+ };
5016
+ // Write only under the identity the fetch itself carried: re-reading
5017
+ // auth.json here could attribute a finished fetch's windows to an account
5018
+ // that logged in while the request was in flight (opaque → JWT switch).
5019
+ // No fetch-side identity (opaque token) → refuse the write (fail-closed).
5020
+ if (cacheGeneration === usageLimitsCacheGeneration && typeof grok.cache_identity === "string") {
5021
+ writeGrokLimitsCache(grokWithCache, {
5022
+ home,
5023
+ nowMs,
5024
+ identity: grok.cache_identity,
5025
+ });
5026
+ }
5027
+ } else {
5028
+ // The slot's error object loses the fetcher's identity field when the outer
5029
+ // timeout race wins (the common failure mode this cache exists for), so
5030
+ // re-derive it from auth.json on this failure-only path.
5031
+ const identity = typeof grok.cache_identity === "string"
5032
+ ? grok.cache_identity
5033
+ : resolveGrokCacheIdentity({ home, env });
5034
+ const cached = readGrokLimitsCache({ home, nowMs, identity });
5035
+ grokWithCache = cached || grok;
5036
+ }
5037
+ // cache_identity / auth_error are internal cache-plumbing markers, not payload.
5038
+ if (grokWithCache && ("cache_identity" in grokWithCache || "auth_error" in grokWithCache)) {
5039
+ const { cache_identity: _identity, auth_error: _authError, ...grokPayload } = grokWithCache;
5040
+ grokWithCache = grokPayload;
5041
+ }
5042
+
5043
+ const cursorLimits = cursorSandDuplicatesGrokPool(cursorWithCache, grokWithCache)
5044
+ ? { ...cursorWithCache, quaternary_window: null }
5045
+ : cursorWithCache;
5046
+
5047
+ // Kimi: persist last successful windows so a transient timeout/5xx on the
5048
+ // api.kimi.com / api.kimi.ai coding routes serves stale bars instead of a
5049
+ // flashing "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Re-login
5050
+ // errors ("Not logged in to Kimi" / "token_expired") and actionable profile
5051
+ // misconfigurations must not be hidden behind stale cache, so they pass
5052
+ // through untouched.
5053
+ let kimiWithCache;
5054
+ if (!kimi || kimi.configured === false) {
5055
+ kimiWithCache = kimi;
5056
+ } else if (kimi.auth_error === true || kimi.config_error === true) {
5057
+ kimiWithCache = kimi;
5058
+ } else if (!kimi.error && hasKimiWindow(kimi)) {
5059
+ kimiWithCache = {
5060
+ ...kimi,
5061
+ stale: false,
5062
+ cached_at: new Date(nowMs).toISOString(),
5063
+ };
5064
+ if (cacheGeneration === usageLimitsCacheGeneration) {
5065
+ writeKimiLimitsCache(kimiWithCache, {
5066
+ home,
5067
+ nowMs,
5068
+ identity: typeof kimi.cache_identity === "string"
5069
+ ? kimi.cache_identity
5070
+ : resolveKimiCacheIdentity({ home, env }),
5071
+ });
5072
+ }
5073
+ } else {
5074
+ // The slot's error object loses the fetcher's identity field when the outer
5075
+ // timeout race wins (the common failure mode this cache exists for), so
5076
+ // re-derive it from the local Kimi profile on this failure-only path.
5077
+ const identity = typeof kimi.cache_identity === "string"
5078
+ ? kimi.cache_identity
5079
+ : resolveKimiCacheIdentity({ home, env });
5080
+ const cached = readKimiLimitsCache({ home, nowMs, identity });
5081
+ kimiWithCache = cached || kimi;
5082
+ }
5083
+ // cache_identity / auth_error / config_error are internal cache-plumbing
5084
+ // markers, not payload.
5085
+ if (kimiWithCache && ("cache_identity" in kimiWithCache || "auth_error" in kimiWithCache || "config_error" in kimiWithCache)) {
5086
+ const {
5087
+ cache_identity: _kimiIdentity,
5088
+ auth_error: _kimiAuthError,
5089
+ config_error: _kimiConfigError,
5090
+ ...kimiPayload
5091
+ } = kimiWithCache;
5092
+ kimiWithCache = kimiPayload;
5093
+ }
4473
5094
 
4474
5095
  const data = {
4475
5096
  fetched_at: new Date(nowMs).toISOString(),
@@ -4480,7 +5101,7 @@ async function fetchUsageLimitsUncached({
4480
5101
  // tier, and membership.level is an opaque enum (LEVEL_INTERMEDIATE) with no
4481
5102
  // authoritative human-readable plan mapping. Both rendered as garbage like
4482
5103
  // "Kimi Type_event" (issue #130), so show the bare brand instead.
4483
- kimi: withPlanLabel(kimi, null, "Kimi"),
5104
+ kimi: withPlanLabel(kimiWithCache, null, "Kimi"),
4484
5105
  gemini: withPlanLabel(gemini, gemini.account_plan, "Gemini"),
4485
5106
  kiro: withPlanLabel(kiro, kiro.plan_name, "Kiro"),
4486
5107
  antigravity: withPlanLabel(antigravity, antigravity.account_plan, "Antigravity"),
@@ -4491,7 +5112,7 @@ async function fetchUsageLimitsUncached({
4491
5112
  // outright, so surface the label as-is like CommandCode below. Error and
4492
5113
  // unconfigured branches never carry plan_label, so the panel still falls
4493
5114
  // back to the bare brand name.
4494
- grok,
5115
+ grok: grokWithCache,
4495
5116
  zcode: withPlanLabel(zcode, zcode.plan_label, "ZCode"),
4496
5117
  opencodeGo: withPlanLabel(opencodeGo, opencodeGo?.plan_label, "OpenCode Go"),
4497
5118
  // CommandCode tiers are acronym brands (GOAT/Pro/Max) — the fetcher already
@@ -4510,10 +5131,18 @@ async function fetchUsageLimitsUncached({
4510
5131
  // as separate providers — one row per signed-in edition, never merged.
4511
5132
  workbuddy,
4512
5133
  workbuddyAi,
5134
+ // DeepSeek carries a balance card (no windows, no plan label): the fetcher
5135
+ // stamps cached_at/stale on both its live and last-good paths, so pass it
5136
+ // through verbatim like grok.
5137
+ deepseek,
4513
5138
  };
4514
5139
 
4515
5140
  for (const [providerName, provider] of Object.entries(data)) {
4516
5141
  if (providerName === "fetched_at" || !provider || typeof provider !== "object") continue;
5142
+ // An exact not-configured sentinel (hidden provider #956, or an opted-out
5143
+ // Devin) is metadata-free by contract — it was never fetched, so it must
5144
+ // not be aged into a "disk-cache"/stale provenance.
5145
+ if (provider.configured === false && Object.keys(provider).length === 1) continue;
4517
5146
  const capturedAt = provider.cached_at || data.fetched_at;
4518
5147
  const ageMs = Math.max(0, nowMs - Date.parse(capturedAt || ""));
4519
5148
  const stale = provider.stale === true || (Number.isFinite(ageMs) && ageMs > 10 * 60 * 1000);
@@ -4534,7 +5163,8 @@ async function fetchUsageLimitsUncached({
4534
5163
  }
4535
5164
 
4536
5165
  if (cacheGeneration === usageLimitsCacheGeneration) {
4537
- cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
5166
+ pruneSelectionPartitions(nowMs);
5167
+ cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled, hiddenProviders })] = {
4538
5168
  data,
4539
5169
  expiresAtMs: cacheExpiresAtMs(data, nowMs),
4540
5170
  };
@@ -4542,20 +5172,50 @@ async function fetchUsageLimitsUncached({
4542
5172
  return data;
4543
5173
  }
4544
5174
 
5175
+ // Hidden-set keys make the partition space user-shaped and unbounded in
5176
+ // principle, while realistic sessions only ever hold a handful of selections.
5177
+ // Keep it bounded: once past the soft cap, drop expired partitions first and
5178
+ // then the oldest still-unexpired ones — they are simply refetched on demand.
5179
+ const SELECTION_PARTITION_CAP = 48;
5180
+ function pruneSelectionPartitions(nowMs) {
5181
+ const keys = Object.keys(cacheBySelection);
5182
+ if (keys.length <= SELECTION_PARTITION_CAP) return;
5183
+ const expired = [];
5184
+ const live = [];
5185
+ for (const key of keys) {
5186
+ (cacheBySelection[key].expiresAtMs <= nowMs ? expired : live).push(key);
5187
+ }
5188
+ const dropCount = keys.length - SELECTION_PARTITION_CAP;
5189
+ const drop = expired.length >= dropCount
5190
+ ? expired.slice(0, dropCount)
5191
+ : [...expired, ...live.slice(0, dropCount - expired.length)];
5192
+ for (const key of drop) delete cacheBySelection[key];
5193
+ }
5194
+
4545
5195
  function resetUsageLimitsCache({ invalidateInFlight = false } = {}) {
4546
5196
  if (invalidateInFlight) usageLimitsCacheGeneration += 1;
4547
- for (const key of USAGE_LIMITS_SELECTION_KEYS) {
5197
+ // Selection partitions are created lazily (hidden sets extend the key
5198
+ // space), so reset whatever exists instead of a fixed key list.
5199
+ for (const key of Object.keys(cacheBySelection)) {
4548
5200
  cacheBySelection[key] = { data: null, expiresAtMs: 0 };
4549
- // Ordinary resets still let a running fetch serve refresh=1 callers; a
4550
- // credential replacement must not, so drop the old-key promises too.
4551
- if (invalidateInFlight) inFlightBySelection[key] = null;
5201
+ }
5202
+ // Ordinary resets still let a running fetch serve refresh=1 callers; a
5203
+ // credential replacement must not, so drop the old-key promises too. The
5204
+ // in-flight map is walked on its own: a selection can hold a promise before
5205
+ // its partition exists (stored only when the fetch completes).
5206
+ if (invalidateInFlight) {
5207
+ for (const key of Object.keys(inFlightBySelection)) {
5208
+ delete inFlightBySelection[key];
5209
+ }
4552
5210
  }
4553
5211
  }
4554
5212
 
4555
5213
  module.exports = {
4556
5214
  getUsageLimits,
4557
5215
  normalizePlanLabel,
5216
+ normalizeHiddenProviders,
4558
5217
  resetUsageLimitsCache,
5218
+ USAGE_LIMITS_PROVIDER_IDS,
4559
5219
  OPENCODE_GO_LIMITS_CACHE_FILE,
4560
5220
  resolveOpencodeGoLimitsCachePath,
4561
5221
  cacheExpiresAtMs,
@@ -4601,4 +5261,5 @@ module.exports = {
4601
5261
  fetchQoderCnLimits,
4602
5262
  fetchWorkbuddyLimits,
4603
5263
  fetchWorkbuddyAiLimits,
5264
+ withWorkbuddyProviderTimeout,
4604
5265
  };