tokentracker-cli 1.4.7 → 1.4.9

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 (146) hide show
  1. package/README.md +9 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/tracker.js +6 -0
  4. package/dashboard/dist/assets/{AchievementBadge-DF8q1x6R.js → AchievementBadge-_dRLgxnp.js} +1 -1
  5. package/dashboard/dist/assets/{AchievementsPage-B8Dg5myo.js → AchievementsPage-DtvlZLlv.js} +2 -2
  6. package/dashboard/dist/assets/{AchievementsSection-oPPiyP3y.js → AchievementsSection-OgzKkbes.js} +1 -1
  7. package/dashboard/dist/assets/{BadgeDetailModal-PneMW4B4.js → BadgeDetailModal-D4Rlm_um.js} +1 -1
  8. package/dashboard/dist/assets/{BotAnimated-GOlSvEiz.js → BotAnimated-errJ5IvB.js} +1 -1
  9. package/dashboard/dist/assets/{Card-BT5G0GE0.js → Card-esELVQcD.js} +1 -1
  10. package/dashboard/dist/assets/{CloudCheckoutPage-BNeL5Muo.js → CloudCheckoutPage-3sH4Dbx9.js} +1 -1
  11. package/dashboard/dist/assets/CloudPage-Bi_vgiqE.js +2 -0
  12. package/dashboard/dist/assets/{CommandPalette-CiMSYHNK.js → CommandPalette-I0aze2EJ.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-fK3BE9CW.js → CommunityStatsModal-CEn2g4J9.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-CrUlEHyZ.js → ConfirmModal-D7SfFCaf.js} +1 -1
  15. package/dashboard/dist/assets/{Controls-CbjSwq6k.js → Controls-Dyb5Vmk_.js} +1 -1
  16. package/dashboard/dist/assets/DashboardPage-CMHX8zDq.js +1 -0
  17. package/dashboard/dist/assets/{DateRangePopover-E-2Z6imh.js → DateRangePopover-CgmxsHVR.js} +1 -1
  18. package/dashboard/dist/assets/DevicePage-9R2MfK2Z.js +1 -0
  19. package/dashboard/dist/assets/{DialogClose-xJKE22Y6.js → DialogClose-DzR109OA.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-CdNjUDLZ.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-f8vnnH7z.js → DialogTitle-BLH1MgOX.js} +1 -1
  22. package/dashboard/dist/assets/{ElectricLogo-Ci9WZfHK.js → ElectricLogo-DcpBcJIy.js} +1 -1
  23. package/dashboard/dist/assets/{FadeIn-BXqtyAmn.js → FadeIn-CBAXe_zb.js} +1 -1
  24. package/dashboard/dist/assets/{HeaderGithubStar-BV5SYJMi.js → HeaderGithubStar-BZjQ3uJo.js} +1 -1
  25. package/dashboard/dist/assets/{HoverTooltip-D3gpW3Jh.js → HoverTooltip-Bq4HuWOZ.js} +1 -1
  26. package/dashboard/dist/assets/{IpCheckPage-G7fIWXpL.js → IpCheckPage-Oiah1-YB.js} +1 -1
  27. package/dashboard/dist/assets/LandingPage-Cc1FfkWg.js +2 -0
  28. package/dashboard/dist/assets/{LeaderboardPage-D2_wXazL.js → LeaderboardPage-Bc2j_DQY.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProBadge-3xJ-mH2v.js → LeaderboardProBadge-w1olVHVp.js} +1 -1
  30. package/dashboard/dist/assets/{LeaderboardProfileModal-23pH6Ud7.js → LeaderboardProfileModal-mgG7mdl8.js} +3 -3
  31. package/dashboard/dist/assets/{LeaderboardProfilePage-DSezSR58.js → LeaderboardProfilePage-DqxyooT5.js} +1 -1
  32. package/dashboard/dist/assets/LimitsPage-BIvK0GFj.js +6 -0
  33. package/dashboard/dist/assets/{LocalOnlyNotice-Dsk-d0Hr.js → LocalOnlyNotice-DZLgI75v.js} +1 -1
  34. package/dashboard/dist/assets/{LoginCard-C7dugIIn.js → LoginCard-DVNU_QtS.js} +1 -1
  35. package/dashboard/dist/assets/{LoginModal-B7RuLsTF.js → LoginModal-yxi5pCwC.js} +1 -1
  36. package/dashboard/dist/assets/{LoginPage-u32vJQN9.js → LoginPage-CW9pu4mf.js} +1 -1
  37. package/dashboard/dist/assets/{PetPage-Dbc975oU.js → PetPage-DwZsq4nC.js} +2 -2
  38. package/dashboard/dist/assets/{PopoverPopup-D-yECvH9.js → PopoverPopup-DYGmmoD8.js} +1 -1
  39. package/dashboard/dist/assets/{ProviderIcon-CHtZiJUN.js → ProviderIcon-BKfj89GN.js} +1 -1
  40. package/dashboard/dist/assets/{ResetPasswordPage-ctuIsNk0.js → ResetPasswordPage-gQdgCp9s.js} +1 -1
  41. package/dashboard/dist/assets/{Select-DN4Kq_R1.js → Select-DvSR56Lj.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-C-HmeD8L.js → SelectItemText-DXo30s3O.js} +1 -1
  43. package/dashboard/dist/assets/{SelfHostPage-BZOL3JMt.js → SelfHostPage-jA51Nckw.js} +1 -1
  44. package/dashboard/dist/assets/{ServiceStatusPage-C0sDYa8c.js → ServiceStatusPage-DEkRRPx8.js} +1 -1
  45. package/dashboard/dist/assets/{SessionsPage-l0-FkcJB.js → SessionsPage-J7hvGPCj.js} +3 -3
  46. package/dashboard/dist/assets/SettingsPage-ewH1Mzsm.js +5 -0
  47. package/dashboard/dist/assets/{SkillsPage-Cz6G9d3o.js → SkillsPage-BaeBHhU7.js} +1 -1
  48. package/dashboard/dist/assets/TokenGalaxy-D52pHpzj.js +4426 -0
  49. package/dashboard/dist/assets/TooltipPopup-mLpESQcv.js +1 -0
  50. package/dashboard/dist/assets/TrendMonitor-ErowR5GE.js +19 -0
  51. package/dashboard/dist/assets/{WidgetsPage-BKo3DBxT.js → WidgetsPage-Bf1uuKE7.js} +1 -1
  52. package/dashboard/dist/assets/{WrappedPage-Blm3aPO5.js → WrappedPage-C8qacvpR.js} +1 -1
  53. package/dashboard/dist/assets/__vite-browser-external-CIpYjfAT.js +45 -0
  54. package/dashboard/dist/assets/__vite-browser-external-CnVnsbqV.css +1 -0
  55. package/dashboard/dist/assets/{agent-logos-DMtDEx7_.js → agent-logos-tmdKvCcK.js} +1 -1
  56. package/dashboard/dist/assets/{arrow-left-Bo9y7VzF.js → arrow-left-CRKi420c.js} +1 -1
  57. package/dashboard/dist/assets/{arrow-right-BLMU8Y5U.js → arrow-right-C_o_iQbK.js} +1 -1
  58. package/dashboard/dist/assets/{arrow-up-right-B4hXO4sc.js → arrow-up-right-N5YqKKnI.js} +1 -1
  59. package/dashboard/dist/assets/{calendar-days-D9HaEtgo.js → calendar-days-BJh4FDj9.js} +1 -1
  60. package/dashboard/dist/assets/{check-BJNGKpX8.js → check-BfHGb92g.js} +1 -1
  61. package/dashboard/dist/assets/{chevron-down-CoGC_RLK.js → chevron-down-D1pO8Xgc.js} +1 -1
  62. package/dashboard/dist/assets/{cloud-checkout-Du5FoHqV.js → cloud-checkout-Ds13TmYz.js} +1 -1
  63. package/dashboard/dist/assets/{composite-BfIUPpl8.js → composite-DDEsQKPD.js} +1 -1
  64. package/dashboard/dist/assets/copy-CJTosYLe.js +1 -0
  65. package/dashboard/dist/assets/{download-C1K_v0C0.js → download-Bkqx3c3y.js} +1 -1
  66. package/dashboard/dist/assets/{external-link-D-xHhfj5.js → external-link-IYUo5LcK.js} +1 -1
  67. package/dashboard/dist/assets/{flame-DXD6uvhM.js → flame-BD_hiMfM.js} +1 -1
  68. package/dashboard/dist/assets/{format-tokens-B9riGvNj.js → format-tokens-DxwbhPag.js} +1 -1
  69. package/dashboard/dist/assets/{gift-LPR-nD0J.js → gift-DLzFdL-1.js} +1 -1
  70. package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
  71. package/dashboard/dist/assets/{icons-DLdtLqEt.js → icons-CbrWeFPS.js} +1 -1
  72. package/dashboard/dist/assets/{index-BL11MboX.js → index-3xGpnZnP.js} +1 -1
  73. package/dashboard/dist/assets/{index-CXdJ7zMN.js → index-AVEWo-aY.js} +1 -1
  74. package/dashboard/dist/assets/{index-CC3cXeBa.js → index-DfzCE-FL.js} +1 -1
  75. package/dashboard/dist/assets/index-Dj1QnoPF.js +502 -0
  76. package/dashboard/dist/assets/{info-tnXKCyuK.js → info-odlIlCnm.js} +1 -1
  77. package/dashboard/dist/assets/{link-2-DOk9Tcg1.js → link-2-Bt28Ey9r.js} +1 -1
  78. package/dashboard/dist/assets/{loader-circle-DIl96Mw-.js → loader-circle-lGnIhIFS.js} +1 -1
  79. package/dashboard/dist/assets/local-usage-transfer-DSS0WaL-.js +1 -0
  80. package/dashboard/dist/assets/{modifiers.esm-2H_1taJ6.js → modifiers.esm-CitRa_Z1.js} +1 -1
  81. package/dashboard/dist/assets/module-C2SOSUuR.js +3 -0
  82. package/dashboard/dist/assets/{monitor-smartphone-tnp-03hs.js → monitor-smartphone-1T0GFcOz.js} +1 -1
  83. package/dashboard/dist/assets/{play-BGMuGTdN.js → play-d6YYOmED.js} +1 -1
  84. package/dashboard/dist/assets/{plus-XETAaErq.js → plus-GrGV9Fkm.js} +1 -1
  85. package/dashboard/dist/assets/{provider-display-BYNT4zPc.js → provider-display-D1v3cgDm.js} +1 -1
  86. package/dashboard/dist/assets/{react-OW7HALVO.js → react-DYCAyQbo.js} +1 -1
  87. package/dashboard/dist/assets/{safePolygon-CAfS8-ct.js → safePolygon-iPu060Qe.js} +1 -1
  88. package/dashboard/dist/assets/{search-B7ZJhoau.js → search-AQhOTwzm.js} +1 -1
  89. package/dashboard/dist/assets/{server-CWrP7mtL.js → server-uYTbWPMd.js} +1 -1
  90. package/dashboard/dist/assets/{shield-check-ip5IQEK3.js → shield-check-DB1NVCDf.js} +1 -1
  91. package/dashboard/dist/assets/{skills-api-CpyoNTeB.js → skills-api-D7X10n0t.js} +1 -1
  92. package/dashboard/dist/assets/{sortable.esm-YvzWzPDu.js → sortable.esm-B13KhrCd.js} +1 -1
  93. package/dashboard/dist/assets/{store-DRK6-JBg.js → store-D6XHiq8o.js} +1 -1
  94. package/dashboard/dist/assets/{terminal-DQRoJSvh.js → terminal-t37F8qyF.js} +1 -1
  95. package/dashboard/dist/assets/{trash-8h_26atF.js → trash-CDHRwGsy.js} +1 -1
  96. package/dashboard/dist/assets/{use-community-stats-DYlhIXsp.js → use-community-stats--kaGg0EW.js} +1 -1
  97. package/dashboard/dist/assets/{use-latest-request-guard-AxJF9SEJ.js → use-latest-request-guard-ChyODWU3.js} +1 -1
  98. package/dashboard/dist/assets/{use-limits-display-prefs-DOZzuTwo.js → use-limits-display-prefs-katFAxOA.js} +1 -1
  99. package/dashboard/dist/assets/{use-native-settings-BQgB6_PB.js → use-native-settings-i3O4GTY1.js} +1 -1
  100. package/dashboard/dist/assets/{use-ordered-list-ogi7Okzf.js → use-ordered-list-BtewEEX_.js} +1 -1
  101. package/dashboard/dist/assets/use-reduced-motion-DPacKl9n.js +1 -0
  102. package/dashboard/dist/assets/{use-transform-BQqoo1et.js → use-transform-NBAyuphu.js} +1 -1
  103. package/dashboard/dist/assets/{use-usage-limits-DAf6O4aZ.js → use-usage-limits-hsv-93TF.js} +1 -1
  104. package/dashboard/dist/assets/{useCurrency-Bv9g8mpq.js → useCurrency-BdsxkAID.js} +1 -1
  105. package/dashboard/dist/assets/{usePositioner-CQiZGknc.js → usePositioner-BAN79miB.js} +1 -1
  106. package/dashboard/dist/assets/{useTokenFormat-BUVirY0k.js → useTokenFormat-CGEO_mX1.js} +1 -1
  107. package/dashboard/dist/assets/{zap-DR3VD0yY.js → zap-BDHWp-gL.js} +1 -1
  108. package/dashboard/dist/index.html +2 -2
  109. package/dashboard/dist/ip-check.html +2 -2
  110. package/dashboard/dist/leaderboard.html +2 -2
  111. package/dashboard/dist/privacy.html +8 -2
  112. package/dashboard/dist/share.html +2 -2
  113. package/package.json +3 -2
  114. package/src/cli.js +10 -0
  115. package/src/commands/sync.js +18 -3
  116. package/src/lib/codex-rollout-parser.js +29 -2
  117. package/src/lib/codex-service-tier.js +13 -0
  118. package/src/lib/commandcode-limits.js +27 -11
  119. package/src/lib/local-api.js +122 -11
  120. package/src/lib/local-usage-transfer.js +271 -0
  121. package/src/lib/opencode-go-limits.js +17 -6
  122. package/src/lib/pricing/curated-overrides.json +24 -0
  123. package/src/lib/pricing/index.js +50 -39
  124. package/src/lib/pricing/seed-snapshot.json +1 -1
  125. package/src/lib/provider-api-key-store.js +110 -0
  126. package/src/lib/rollout.js +27 -3
  127. package/src/lib/sentry.js +231 -0
  128. package/src/lib/session-analytics.js +15 -2
  129. package/src/lib/static-server.js +15 -2
  130. package/src/lib/usage-limits.js +54 -16
  131. package/dashboard/dist/assets/CloudPage-26bhEYqI.js +0 -2
  132. package/dashboard/dist/assets/DashboardPage-CP_PMMga.js +0 -1
  133. package/dashboard/dist/assets/DevicePage-B0fLd80M.js +0 -1
  134. package/dashboard/dist/assets/DialogDescription-CXDxWzGi.js +0 -1
  135. package/dashboard/dist/assets/LandingPage-BQNOdAkR.js +0 -2
  136. package/dashboard/dist/assets/LimitsPage-BLJsmVVy.js +0 -9
  137. package/dashboard/dist/assets/SettingsPage-CrczfZiZ.js +0 -5
  138. package/dashboard/dist/assets/TokenGalaxy-B5IGaLQK.js +0 -4426
  139. package/dashboard/dist/assets/TooltipPopup-B6FqLWO7.js +0 -1
  140. package/dashboard/dist/assets/TrendMonitor-B4kJUwmF.js +0 -19
  141. package/dashboard/dist/assets/__vite-browser-external--HDUIUZr.js +0 -45
  142. package/dashboard/dist/assets/__vite-browser-external-B0m8a6Te.css +0 -1
  143. package/dashboard/dist/assets/host-mode-C2K0MMOs.js +0 -1
  144. package/dashboard/dist/assets/module-DOCYJaxl.js +0 -3
  145. package/dashboard/dist/assets/use-reduced-motion-D5PHT_kx.js +0 -1
  146. package/dashboard/dist/assets/use-session-efficiency-pref-DP3hu3Rp.js +0 -1
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ // Provider API keys saved from the dashboard's Limits panel.
4
+ //
5
+ // The launch environment is a shared, machine-wide namespace: a
6
+ // `launchctl setenv OPENCODE_GO_API_KEY` reaches every launchd app on the
7
+ // machine (that is how a global key silently unlocked another app's field in
8
+ // #837). Keys entered in the panel are therefore persisted here instead, in a
9
+ // 0600 JSON file under the tracker data dir next to cloud-device-token.json.
10
+ //
11
+ // The environment variable still wins when set, so headless installs and
12
+ // wrapper-launched apps keep working; the store is the durable default for
13
+ // users who never touch a terminal.
14
+
15
+ const fssync = require("node:fs");
16
+ const path = require("node:path");
17
+ const { ensureDir, writeFileAtomic, chmod600IfPossible } = require("./fs");
18
+
19
+ const PROVIDER_KEY_FILES = {
20
+ "opencode-go": "opencode-go-api-key.json",
21
+ "command-code": "command-code-api-key.json",
22
+ };
23
+
24
+ const PROVIDER_ENV_NAMES = {
25
+ "opencode-go": "OPENCODE_GO_API_KEY",
26
+ "command-code": "COMMAND_CODE_API_KEY",
27
+ };
28
+
29
+ const MIN_API_KEY_LENGTH = 8;
30
+ const MAX_API_KEY_LENGTH = 4096;
31
+
32
+ function isProviderId(provider) {
33
+ return typeof provider === "string" && Object.prototype.hasOwnProperty.call(PROVIDER_KEY_FILES, provider);
34
+ }
35
+
36
+ function envNameForProvider(provider) {
37
+ return isProviderId(provider) ? PROVIDER_ENV_NAMES[provider] : null;
38
+ }
39
+
40
+ function resolveProviderKeyDir({ trackerDir, home } = {}) {
41
+ if (typeof trackerDir === "string" && trackerDir.trim()) return trackerDir.trim();
42
+ if (typeof home === "string" && home.trim()) {
43
+ return path.join(home.trim(), ".tokentracker", "tracker");
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function resolveProviderKeyPath({ provider, trackerDir, home } = {}) {
49
+ if (!isProviderId(provider)) return null;
50
+ const dir = resolveProviderKeyDir({ trackerDir, home });
51
+ if (!dir) return null;
52
+ return path.join(dir, PROVIDER_KEY_FILES[provider]);
53
+ }
54
+
55
+ // Keys are opaque bearer tokens. Reject anything that cannot be pasted into an
56
+ // Authorization header verbatim (internal whitespace/control characters) and
57
+ // keep an upper bound so a mis-pasted blob cannot bloat the auth file.
58
+ function normalizeApiKey(value) {
59
+ if (typeof value !== "string") return "";
60
+ const trimmed = value.trim();
61
+ if (trimmed.length < MIN_API_KEY_LENGTH || trimmed.length > MAX_API_KEY_LENGTH) return "";
62
+ if (/[\s\u0000-\u001f\u007f]/.test(trimmed)) return "";
63
+ return trimmed;
64
+ }
65
+
66
+ // Never throws: a missing, unreadable, or malformed store must degrade to
67
+ // "no stored key" so a limits fetch can still fall back to env/cookie/CLI.
68
+ function readStoredApiKey({ provider, trackerDir, home } = {}) {
69
+ const filePath = resolveProviderKeyPath({ provider, trackerDir, home });
70
+ if (!filePath) return "";
71
+ let raw;
72
+ try {
73
+ raw = fssync.readFileSync(filePath, "utf8");
74
+ } catch {
75
+ return "";
76
+ }
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(raw);
80
+ } catch {
81
+ return "";
82
+ }
83
+ return normalizeApiKey(parsed?.apiKey);
84
+ }
85
+
86
+ async function writeStoredApiKey({ provider, trackerDir, home, apiKey } = {}) {
87
+ const filePath = resolveProviderKeyPath({ provider, trackerDir, home });
88
+ if (!filePath) throw new Error("No tracker data directory available for the API key.");
89
+ const normalized = normalizeApiKey(apiKey);
90
+ if (!normalized) throw new Error("Invalid API key.");
91
+ await ensureDir(path.dirname(filePath));
92
+ const contents = `${JSON.stringify({ apiKey: normalized, updatedAt: new Date().toISOString() }, null, 2)}\n`;
93
+ await writeFileAtomic(filePath, contents, { mode: 0o600 });
94
+ // writeFileAtomic only applies `mode` to newly created files; an existing
95
+ // store with looser permissions must still end up private.
96
+ await chmod600IfPossible(filePath);
97
+ return filePath;
98
+ }
99
+
100
+ module.exports = {
101
+ PROVIDER_KEY_FILES,
102
+ PROVIDER_ENV_NAMES,
103
+ isProviderId,
104
+ envNameForProvider,
105
+ resolveProviderKeyDir,
106
+ resolveProviderKeyPath,
107
+ normalizeApiKey,
108
+ readStoredApiKey,
109
+ writeStoredApiKey,
110
+ };
@@ -28,6 +28,7 @@ const {
28
28
  CODEX_SERVICE_TIER_MARKER,
29
29
  readCodexServiceTier,
30
30
  isPriorityServiceTier,
31
+ isUltrafastServiceTier,
31
32
  } = require("./codex-service-tier");
32
33
  const {
33
34
  awaitsCompactedLine,
@@ -2649,6 +2650,7 @@ async function parseRolloutRecords({
2649
2650
  const bucket = getHourlyBucket(hourlyState, source, event.model, event.bucketStart);
2650
2651
  addTotals(bucket.totals, event.delta);
2651
2652
  if (isPriorityServiceTier(event.serviceTier)) addPriorityUsage(bucket.totals, event.delta);
2653
+ if (isUltrafastServiceTier(event.serviceTier)) addUltrafastUsage(bucket.totals, event.delta);
2652
2654
  touchedBuckets.add(bucketKey(source, event.model, event.bucketStart));
2653
2655
  if (event.projectKey && projectState && projectTouchedBuckets) {
2654
2656
  const projectBucket = getProjectBucket(
@@ -2903,6 +2905,7 @@ async function parseRolloutRecords({
2903
2905
  // Only the model bucket gets the subset: the project bucket below carries
2904
2906
  // no model, so no per-model rate can be applied to it anyway.
2905
2907
  if (isPriorityServiceTier(serviceTier)) addPriorityUsage(bucket.totals, delta);
2908
+ if (isUltrafastServiceTier(serviceTier)) addUltrafastUsage(bucket.totals, delta);
2906
2909
  touchedBuckets.add(bucketKey(source, model, bucketStart));
2907
2910
  if (currentProjectKey && projectState && projectTouchedBuckets) {
2908
2911
  const projectBucket = getProjectBucket(
@@ -4422,6 +4425,16 @@ const PRIORITY_SUBSET_FIELDS = [
4422
4425
  ["priority_reasoning_output_tokens", "reasoning_output_tokens"],
4423
4426
  ];
4424
4427
 
4428
+ const ULTRAFAST_SUBSET_FIELDS = [
4429
+ ["ultrafast_input_tokens", "input_tokens"],
4430
+ ["ultrafast_cached_input_tokens", "cached_input_tokens"],
4431
+ ["ultrafast_cache_creation_input_tokens", "cache_creation_input_tokens"],
4432
+ ["ultrafast_output_tokens", "output_tokens"],
4433
+ ["ultrafast_reasoning_output_tokens", "reasoning_output_tokens"],
4434
+ ];
4435
+
4436
+ const SPEED_TIER_SUBSET_FIELDS = [...PRIORITY_SUBSET_FIELDS, ...ULTRAFAST_SUBSET_FIELDS];
4437
+
4425
4438
  // The whole delta belongs to a priority request, so every base column it
4426
4439
  // carries is priority usage.
4427
4440
  function addPriorityUsage(target, delta) {
@@ -4431,9 +4444,16 @@ function addPriorityUsage(target, delta) {
4431
4444
  }
4432
4445
  }
4433
4446
 
4447
+ function addUltrafastUsage(target, delta) {
4448
+ for (const [field, base] of ULTRAFAST_SUBSET_FIELDS) {
4449
+ const value = Number(delta?.[base] || 0);
4450
+ if (value > 0) target[field] = (Number(target[field]) || 0) + value;
4451
+ }
4452
+ }
4453
+
4434
4454
  function prioritySubsetOf(totals) {
4435
4455
  const subset = {};
4436
- for (const [field] of PRIORITY_SUBSET_FIELDS) {
4456
+ for (const [field] of SPEED_TIER_SUBSET_FIELDS) {
4437
4457
  const value = Number(totals?.[field]) || 0;
4438
4458
  if (value > 0) subset[field] = value;
4439
4459
  }
@@ -4441,7 +4461,7 @@ function prioritySubsetOf(totals) {
4441
4461
  }
4442
4462
 
4443
4463
  function addTotals(target, delta) {
4444
- for (const [field] of PRIORITY_SUBSET_FIELDS) {
4464
+ for (const [field] of SPEED_TIER_SUBSET_FIELDS) {
4445
4465
  const value = Number(delta?.[field]) || 0;
4446
4466
  if (value > 0) target[field] = (Number(target[field]) || 0) + value;
4447
4467
  }
@@ -4511,7 +4531,11 @@ function totalsKey(totals) {
4511
4531
  // Appended only when a priority subset exists, so buckets that never see the
4512
4532
  // tier keep their previously persisted queuedKey and are not re-enqueued.
4513
4533
  const priority = PRIORITY_SUBSET_FIELDS.map(([field]) => Number(totals[field]) || 0);
4514
- return priority.some((value) => value > 0) ? `${base}|p:${priority.join(",")}` : base;
4534
+ const ultrafast = ULTRAFAST_SUBSET_FIELDS.map(([field]) => Number(totals[field]) || 0);
4535
+ let key = base;
4536
+ if (priority.some((value) => value > 0)) key += `|p:${priority.join(",")}`;
4537
+ if (ultrafast.some((value) => value > 0)) key += `|u:${ultrafast.join(",")}`;
4538
+ return key;
4515
4539
  }
4516
4540
 
4517
4541
  function toUtcHalfHourStart(ts) {
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Optional Sentry error reporting for the Node CLI / EmbeddedServer runtime.
5
+ *
6
+ * Privacy contract (keep in sync with docs/PRIVACY.md and dashboard/src/lib/sentry.js):
7
+ * - Off unless TOKENTRACKER_SENTRY_DSN (or SENTRY_DSN) is set.
8
+ * - Honors the same telemetry opt-out as the daily heartbeat
9
+ * (TOKENTRACKER_NO_TELEMETRY / DO_NOT_TRACK / config.json `"telemetry": false`).
10
+ * - Fail-closed in the node test runner.
11
+ * - Never intended to carry prompts, code, or provider secrets; beforeSend
12
+ * scrubs common secret patterns from event payloads.
13
+ */
14
+
15
+ const path = require("node:path");
16
+ const pkg = require("../../package.json");
17
+ const { readJson } = require("./fs");
18
+ const { isTelemetryDisabled } = require("./telemetry");
19
+
20
+ const SECRET_PATTERN =
21
+ /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|cookie|secret|password|dsn)[=:\s]+["']?[^\s"'\\]{8,}/gi;
22
+ const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi;
23
+ const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._-]{10,}\.[A-Za-z0-9._-]{10,}\b/g;
24
+
25
+ let initialized = false;
26
+
27
+ function resolveDsn(env = process.env) {
28
+ const raw = env?.TOKENTRACKER_SENTRY_DSN || env?.SENTRY_DSN || "";
29
+ return typeof raw === "string" ? raw.trim() : "";
30
+ }
31
+
32
+ function resolveEnvironment(env = process.env) {
33
+ const raw = typeof env?.TOKENTRACKER_SENTRY_ENVIRONMENT === "string"
34
+ ? env.TOKENTRACKER_SENTRY_ENVIRONMENT.trim()
35
+ : "";
36
+ if (raw) return raw;
37
+ if (env?.NODE_ENV === "production") return "production";
38
+ return "development";
39
+ }
40
+
41
+ function resolveShell(env = process.env) {
42
+ const raw = typeof env?.TOKENTRACKER_APP_SHELL === "string"
43
+ ? env.TOKENTRACKER_APP_SHELL.trim().toLowerCase()
44
+ : "";
45
+ if (raw === "macos" || raw === "windows" || raw === "linux" || raw === "cli") return raw;
46
+ return "cli";
47
+ }
48
+
49
+ function scrubString(value) {
50
+ if (typeof value !== "string" || !value) return value;
51
+ return value
52
+ .replace(BEARER_PATTERN, "Bearer [Filtered]")
53
+ .replace(JWT_PATTERN, "[Filtered]")
54
+ .replace(SECRET_PATTERN, "[Filtered]");
55
+ }
56
+
57
+ function scrubValue(value, depth = 0) {
58
+ if (depth > 6 || value == null) return value;
59
+ if (typeof value === "string") return scrubString(value);
60
+ if (Array.isArray(value)) return value.map((item) => scrubValue(item, depth + 1));
61
+ if (typeof value === "object") {
62
+ const out = {};
63
+ for (const [key, nested] of Object.entries(value)) {
64
+ const lower = key.toLowerCase();
65
+ if (
66
+ lower.includes("authorization") ||
67
+ lower.includes("cookie") ||
68
+ lower.includes("password") ||
69
+ lower.includes("secret") ||
70
+ lower.includes("token") ||
71
+ lower.includes("api_key") ||
72
+ lower.includes("apikey")
73
+ ) {
74
+ out[key] = "[Filtered]";
75
+ } else {
76
+ out[key] = scrubValue(nested, depth + 1);
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+ return value;
82
+ }
83
+
84
+ function truncateMaybe(value, max = 240) {
85
+ if (typeof value !== "string") return value;
86
+ if (value.length <= max) return value;
87
+ return `${value.slice(0, max)}…[truncated]`;
88
+ }
89
+
90
+ function scrubEvent(event) {
91
+ if (!event || typeof event !== "object") return event;
92
+ const next = { ...event };
93
+ if (next.message) next.message = truncateMaybe(scrubString(next.message));
94
+ if (next.extra) next.extra = scrubValue(next.extra);
95
+ if (next.contexts) next.contexts = scrubValue(next.contexts);
96
+ if (next.request) next.request = scrubValue(next.request);
97
+ // Console breadcrumbs can carry parser / JSON.parse excerpts of local logs
98
+ // (prompt text). Drop them entirely; keep only non-console crumbs and scrub.
99
+ if (Array.isArray(next.breadcrumbs)) {
100
+ next.breadcrumbs = next.breadcrumbs
101
+ .filter((crumb) => crumb && crumb.category !== "console")
102
+ .map((crumb) => scrubValue(crumb));
103
+ }
104
+ if (next.exception?.values) {
105
+ next.exception = {
106
+ ...next.exception,
107
+ values: next.exception.values.map((item) => ({
108
+ ...item,
109
+ value: truncateMaybe(scrubString(item?.value)),
110
+ })),
111
+ };
112
+ }
113
+ // Never attach IP / user PII by default.
114
+ if (next.user) {
115
+ next.user = { id: typeof next.user.id === "string" ? next.user.id : undefined };
116
+ }
117
+ return next;
118
+ }
119
+
120
+ /**
121
+ * Initialise Sentry once for this process. Never throws.
122
+ * @returns {Promise<{ enabled: boolean, reason?: string }>}
123
+ */
124
+ async function initSentry({
125
+ env = process.env,
126
+ trackerDir,
127
+ version = pkg.version,
128
+ } = {}) {
129
+ try {
130
+ if (initialized) return { enabled: true, reason: "already-initialized" };
131
+ if (env?.NODE_TEST_CONTEXT !== undefined) {
132
+ return { enabled: false, reason: "test-env" };
133
+ }
134
+
135
+ const dsn = resolveDsn(env);
136
+ if (!dsn) return { enabled: false, reason: "no-dsn" };
137
+
138
+ let config = {};
139
+ if (trackerDir) {
140
+ config = (await readJson(path.join(trackerDir, "config.json"))) || {};
141
+ }
142
+ if (isTelemetryDisabled({ env, config })) {
143
+ return { enabled: false, reason: "opt-out" };
144
+ }
145
+
146
+ // Honor the same outbound proxy / fail-closed policy as the rest of the CLI.
147
+ // Sentry's Node transport uses node:https (not undici), so pass proxy explicitly.
148
+ const {
149
+ readPersistedProxyConfig,
150
+ resolveEffectiveProxySource,
151
+ } = require("./proxy-env");
152
+ const proxyConfig = readPersistedProxyConfig();
153
+ const proxyResolved = resolveEffectiveProxySource({ env, proxyConfig });
154
+ if (proxyResolved?.source === "blocked") {
155
+ return { enabled: false, reason: "proxy-blocked" };
156
+ }
157
+
158
+ // Lazy require keeps unit tests that never call init from paying the
159
+ // module graph cost at import time of unrelated modules.
160
+ const Sentry = require("@sentry/node");
161
+ Sentry.init({
162
+ dsn,
163
+ environment: resolveEnvironment(env),
164
+ release: `tokentracker@${String(version || "").trim() || "0.0.0"}`,
165
+ sendDefaultPii: false,
166
+ tracesSampleRate: 0,
167
+ // Errors only: drop process-session + console; replace Http so request
168
+ // sessions stay off (Http defaults sessions:true and bypasses beforeSend).
169
+ integrations(defaults) {
170
+ const filtered = defaults.filter((integration) => {
171
+ const name = integration?.name || "";
172
+ return name !== "ProcessSession" && name !== "Console" && name !== "Http";
173
+ });
174
+ filtered.push(Sentry.httpIntegration({ sessions: false }));
175
+ return filtered;
176
+ },
177
+ transportOptions: proxyResolved?.proxyUrl
178
+ ? { proxy: proxyResolved.proxyUrl }
179
+ : undefined,
180
+ beforeSend(event) {
181
+ return scrubEvent(event);
182
+ },
183
+ initialScope: {
184
+ tags: {
185
+ shell: resolveShell(env),
186
+ runtime: "node",
187
+ },
188
+ },
189
+ });
190
+ initialized = true;
191
+ return { enabled: true };
192
+ } catch {
193
+ return { enabled: false, reason: "init-failed" };
194
+ }
195
+ }
196
+
197
+ function captureException(error, context) {
198
+ if (!initialized) return;
199
+ try {
200
+ const Sentry = require("@sentry/node");
201
+ if (context) {
202
+ Sentry.withScope((scope) => {
203
+ if (context.tags) {
204
+ for (const [key, value] of Object.entries(context.tags)) {
205
+ scope.setTag(key, String(value));
206
+ }
207
+ }
208
+ if (context.extra) scope.setExtras(scrubValue(context.extra));
209
+ Sentry.captureException(error);
210
+ });
211
+ return;
212
+ }
213
+ Sentry.captureException(error);
214
+ } catch {
215
+ /* never break the CLI */
216
+ }
217
+ }
218
+
219
+ module.exports = {
220
+ initSentry,
221
+ captureException,
222
+ resolveDsn,
223
+ resolveEnvironment,
224
+ resolveShell,
225
+ scrubEvent,
226
+ scrubString,
227
+ // test helpers
228
+ _resetForTests() {
229
+ initialized = false;
230
+ },
231
+ };
@@ -87,7 +87,9 @@ const {
87
87
  // v23 excludes database updates and user records from request durations.
88
88
  // v24 keeps OpenCode usage dates at event time and fork history on its ancestor.
89
89
  // v25 adds Kimi wire sessions, including official/legacy migration dedup.
90
- const SIDECAR_VERSION = 25;
90
+ // v26 retains Codex Ultrafast (service_tier ultrafast) subset columns so
91
+ // date-filtered session views and cached sidecars keep the 6x premium.
92
+ const SIDECAR_VERSION = 26;
91
93
  const EDIT_TOOLS = new Set([
92
94
  "apply_patch",
93
95
  "edit",
@@ -141,7 +143,7 @@ function addTotals(target, delta) {
141
143
  target[key] = finite(target[key]) + finite(delta?.[key]);
142
144
  }
143
145
  for (const key of MODEL_USAGE_SUM_FIELDS) {
144
- if (!/^(?:long_context_|priority_)|^(?:usage_events|rerouted_usage_events)$/.test(key)) continue;
146
+ if (!/^(?:long_context_|priority_|ultrafast_)|^(?:usage_events|rerouted_usage_events|long_context_usage_events|priority_usage_events|ultrafast_usage_events)$/.test(key)) continue;
145
147
  if (delta?.[key]) target[key] = finite(target[key]) + finite(delta[key]);
146
148
  }
147
149
  }
@@ -179,10 +181,21 @@ const MODEL_USAGE_SUM_FIELDS = [
179
181
  "priority_long_context_cache_creation_input_tokens",
180
182
  "priority_long_context_output_tokens",
181
183
  "priority_long_context_reasoning_output_tokens",
184
+ "ultrafast_input_tokens",
185
+ "ultrafast_cached_input_tokens",
186
+ "ultrafast_cache_creation_input_tokens",
187
+ "ultrafast_output_tokens",
188
+ "ultrafast_reasoning_output_tokens",
189
+ "ultrafast_long_context_input_tokens",
190
+ "ultrafast_long_context_cached_input_tokens",
191
+ "ultrafast_long_context_cache_creation_input_tokens",
192
+ "ultrafast_long_context_output_tokens",
193
+ "ultrafast_long_context_reasoning_output_tokens",
182
194
  "usage_events",
183
195
  "rerouted_usage_events",
184
196
  "long_context_usage_events",
185
197
  "priority_usage_events",
198
+ "ultrafast_usage_events",
186
199
  // Not a token counter: an edit turn is a turn, and turn_context both marks
187
200
  // where a turn begins and names its model, so an edit turn has exactly one
188
201
  // owner. Splitting it here keeps by_model.tokens_per_edit a ratio over one
@@ -33,11 +33,22 @@ async function serveStaticFile(baseDir, pathname, res, { localRuntimeConfig = fa
33
33
  // prevent directory traversal
34
34
  if (!filePath.startsWith(baseDir)) return false;
35
35
 
36
+ // Watch for the client going away from the start: if the browser cancels the
37
+ // request during the awaits below, "close" fires before the stream exists and
38
+ // a listener added later never runs, leaking the FileHandle (fatal on Node 25+).
39
+ let clientGone = res.destroyed;
40
+ let stream = null;
41
+ res.once("close", () => {
42
+ clientGone = true;
43
+ if (stream) stream.destroy();
44
+ });
45
+
36
46
  let handle;
37
47
  try {
38
48
  handle = await fsPromises.open(filePath, "r");
39
49
  const stat = await handle.stat();
40
50
  if (!stat.isFile()) return false;
51
+ if (clientGone) return true; // finally closes the handle
41
52
 
42
53
  const ext = path.extname(filePath).toLowerCase();
43
54
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -47,21 +58,23 @@ async function serveStaticFile(baseDir, pathname, res, { localRuntimeConfig = fa
47
58
  const source = await handle.readFile("utf8");
48
59
  const content = source.replace(/<head(?:\s[^>]*)?>/i,
49
60
  "$&\n<script>window.__TOKENTRACKER_RUNTIME_CONFIG__={configurationError:\"runtime_config_unavailable\"};</script>\n<script src=\"/api/runtime-config.js\"></script>");
61
+ if (clientGone) return true;
50
62
  res.writeHead(200, { "Content-Type": contentType, "Content-Length": Buffer.byteLength(content), "Cache-Control": "no-store" });
51
63
  res.end(content);
52
64
  return true;
53
65
  }
54
66
 
67
+ if (clientGone) return true;
68
+
55
69
  res.writeHead(200, {
56
70
  "Content-Type": contentType,
57
71
  "Content-Length": stat.size,
58
72
  "Cache-Control": isHtml ? "no-cache" : "public, max-age=31536000, immutable",
59
73
  });
60
74
 
61
- const stream = handle.createReadStream();
75
+ stream = handle.createReadStream();
62
76
  handle = null; // The stream owns and closes this descriptor.
63
77
  stream.on("error", (error) => res.destroy(error));
64
- res.on("close", () => stream.destroy());
65
78
  stream.pipe(res);
66
79
  return true;
67
80
  } catch (_e) {
@@ -56,6 +56,10 @@ const execFileAsync = promisify(cp.execFile);
56
56
  const USAGE_LIMITS_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
57
57
  const cacheBySelection = {};
58
58
  for (const key of USAGE_LIMITS_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
59
+ // Credential replacements (provider-key save) bump this generation so a fetch
60
+ // that started under the old credential cannot repopulate the memory or disk
61
+ // caches after invalidation.
62
+ let usageLimitsCacheGeneration = 0;
59
63
  function usageLimitsSelectionKey(options) {
60
64
  const devin = options?.devinEnabled === true ? "on" : "off";
61
65
  const antigravity = isAntigravityQuotaDisabled() ? "off" : "on";
@@ -2603,8 +2607,12 @@ function writeCodexLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
2603
2607
  } catch (_error) {}
2604
2608
  }
2605
2609
 
2606
- function resolveOpencodeGoLimitsCachePath({ home } = {}) {
2607
- return path.join(home || os.homedir(), ".tokentracker", "tracker", OPENCODE_GO_LIMITS_CACHE_FILE);
2610
+ function resolveOpencodeGoLimitsCachePath({ trackerDir, home } = {}) {
2611
+ const dir =
2612
+ typeof trackerDir === "string" && trackerDir.trim()
2613
+ ? trackerDir.trim()
2614
+ : path.join(home || os.homedir(), ".tokentracker", "tracker");
2615
+ return path.join(dir, OPENCODE_GO_LIMITS_CACHE_FILE);
2608
2616
  }
2609
2617
 
2610
2618
  function hasOpencodeGoWindow(limits) {
@@ -2632,10 +2640,15 @@ function normalizeOpencodeGoCachedLimits(
2632
2640
  if (cachedAtMs > nowMs + 60_000) return null;
2633
2641
  if (nowMs - cachedAtMs > effectiveMaxAge) return null;
2634
2642
 
2643
+ const credentialSource =
2644
+ raw?.credential_source === "env" || raw?.credential_source === "stored"
2645
+ ? raw.credential_source
2646
+ : null;
2635
2647
  const cached = {
2636
2648
  configured: true,
2637
2649
  error: null,
2638
2650
  source: typeof raw?.source === "string" ? raw.source : "api",
2651
+ credential_source: credentialSource,
2639
2652
  subscription_status: typeof raw?.subscription_status === "string" ? raw.subscription_status : null,
2640
2653
  plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
2641
2654
  primary_window: isOpencodeGoCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
@@ -2647,8 +2660,8 @@ function normalizeOpencodeGoCachedLimits(
2647
2660
  return hasOpencodeGoWindow(cached) ? cached : null;
2648
2661
  }
2649
2662
 
2650
- function readOpencodeGoLimitsCache({ home, nowMs = Date.now() } = {}) {
2651
- const cachePath = resolveOpencodeGoLimitsCachePath({ home });
2663
+ function readOpencodeGoLimitsCache({ trackerDir, home, nowMs = Date.now() } = {}) {
2664
+ const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
2652
2665
  try {
2653
2666
  const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
2654
2667
  return normalizeOpencodeGoCachedLimits(parsed?.opencodeGo, { nowMs });
@@ -2657,12 +2670,17 @@ function readOpencodeGoLimitsCache({ home, nowMs = Date.now() } = {}) {
2657
2670
  }
2658
2671
  }
2659
2672
 
2660
- function writeOpencodeGoLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
2673
+ function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now() } = {}) {
2661
2674
  if (!limits?.configured || limits.error || !hasOpencodeGoWindow(limits)) return;
2662
- const cachePath = resolveOpencodeGoLimitsCachePath({ home });
2675
+ const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
2676
+ const credentialSource =
2677
+ limits.credential_source === "env" || limits.credential_source === "stored"
2678
+ ? limits.credential_source
2679
+ : null;
2663
2680
  const payload = {
2664
2681
  opencodeGo: {
2665
2682
  source: limits.source || "api",
2683
+ credential_source: credentialSource,
2666
2684
  subscription_status: limits.subscription_status || null,
2667
2685
  plan_label: limits.plan_label || null,
2668
2686
  primary_window: limits.primary_window || null,
@@ -3989,7 +4007,12 @@ async function getUsageLimits(options = {}) {
3989
4007
  if (inFlightBySelection[selection]) {
3990
4008
  return inFlightBySelection[selection];
3991
4009
  }
3992
- const promise = fetchUsageLimitsUncached({ ...options, selectionKey: selection }).finally(() => {
4010
+ const cacheGeneration = usageLimitsCacheGeneration;
4011
+ const promise = fetchUsageLimitsUncached({
4012
+ ...options,
4013
+ selectionKey: selection,
4014
+ cacheGeneration,
4015
+ }).finally(() => {
3993
4016
  if (inFlightBySelection[selection] === promise) inFlightBySelection[selection] = null;
3994
4017
  });
3995
4018
  inFlightBySelection[selection] = promise;
@@ -4002,6 +4025,7 @@ async function getUsageLimits(options = {}) {
4002
4025
  */
4003
4026
  async function fetchUsageLimitsUncached({
4004
4027
  home,
4028
+ trackerDir,
4005
4029
  env,
4006
4030
  platform,
4007
4031
  securityRunner,
@@ -4013,6 +4037,10 @@ async function fetchUsageLimitsUncached({
4013
4037
  providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
4014
4038
  forceRefresh = false,
4015
4039
  devinEnabled = false,
4040
+ // Generation captured by getUsageLimits before the fetch began. A provider
4041
+ // key save bumps it, and this fetch must then stop at returning its own
4042
+ // result: publishing it would restore pre-save data for every later caller.
4043
+ cacheGeneration = usageLimitsCacheGeneration,
4016
4044
  // Selection key fixed by getUsageLimits before the fetch began. Re-deriving it
4017
4045
  // here would let an opt-out flipped mid-fetch store a pre-opt-out result in the
4018
4046
  // post-opt-out slot.
@@ -4131,7 +4159,7 @@ async function fetchUsageLimitsUncached({
4131
4159
  // OpenCode Go: authoritative subscription windows come from the dashboard
4132
4160
  // scrape; local opencode.db cost is available only as an explicit estimate.
4133
4161
  // See src/lib/opencode-go-limits.js.
4134
- withProviderTimeout(fetchOpencodeGoLimits({ home, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
4162
+ withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
4135
4163
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
4136
4164
  withProviderTimeout(
4137
4165
  fetchQoderLimits({
@@ -4189,7 +4217,7 @@ async function fetchUsageLimitsUncached({
4189
4217
  // outer race bounds the whole slot: without it three sequential per-request
4190
4218
  // timeouts (whoami, then credits+subscriptions) could hold the aggregate
4191
4219
  // ~2x longer than any sibling provider.
4192
- withProviderTimeout(fetchCommandcodeLimits({ home, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
4220
+ withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
4193
4221
  .then(
4194
4222
  (value) => ({ status: "fulfilled", value }),
4195
4223
  (reason) => ({ status: "rejected", reason }),
@@ -4382,9 +4410,11 @@ async function fetchUsageLimitsUncached({
4382
4410
  stale: false,
4383
4411
  cached_at: new Date(nowMs).toISOString(),
4384
4412
  };
4385
- writeOpencodeGoLimitsCache(opencodeGo, { home, nowMs });
4413
+ if (cacheGeneration === usageLimitsCacheGeneration) {
4414
+ writeOpencodeGoLimitsCache(opencodeGo, { trackerDir, home, nowMs });
4415
+ }
4386
4416
  } else {
4387
- const cached = readOpencodeGoLimitsCache({ home, nowMs });
4417
+ const cached = readOpencodeGoLimitsCache({ trackerDir, home, nowMs });
4388
4418
  if (cached) {
4389
4419
  opencodeGo = cached;
4390
4420
  } else {
@@ -4491,16 +4521,22 @@ async function fetchUsageLimitsUncached({
4491
4521
  };
4492
4522
  }
4493
4523
 
4494
- cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
4495
- data,
4496
- expiresAtMs: cacheExpiresAtMs(data, nowMs),
4497
- };
4524
+ if (cacheGeneration === usageLimitsCacheGeneration) {
4525
+ cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
4526
+ data,
4527
+ expiresAtMs: cacheExpiresAtMs(data, nowMs),
4528
+ };
4529
+ }
4498
4530
  return data;
4499
4531
  }
4500
4532
 
4501
- function resetUsageLimitsCache() {
4533
+ function resetUsageLimitsCache({ invalidateInFlight = false } = {}) {
4534
+ if (invalidateInFlight) usageLimitsCacheGeneration += 1;
4502
4535
  for (const key of USAGE_LIMITS_SELECTION_KEYS) {
4503
4536
  cacheBySelection[key] = { data: null, expiresAtMs: 0 };
4537
+ // Ordinary resets still let a running fetch serve refresh=1 callers; a
4538
+ // credential replacement must not, so drop the old-key promises too.
4539
+ if (invalidateInFlight) inFlightBySelection[key] = null;
4504
4540
  }
4505
4541
  }
4506
4542
 
@@ -4508,6 +4544,8 @@ module.exports = {
4508
4544
  getUsageLimits,
4509
4545
  normalizePlanLabel,
4510
4546
  resetUsageLimitsCache,
4547
+ OPENCODE_GO_LIMITS_CACHE_FILE,
4548
+ resolveOpencodeGoLimitsCachePath,
4511
4549
  cacheExpiresAtMs,
4512
4550
  runCommand,
4513
4551
  extractGeminiOauthClientCredentials,