tokentracker-cli 1.4.4 → 1.4.7

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 (123) hide show
  1. package/README.de.md +1 -0
  2. package/README.ja.md +1 -0
  3. package/README.ko.md +1 -0
  4. package/README.md +1 -0
  5. package/README.zh-CN.md +1 -0
  6. package/dashboard/dist/assets/{AchievementBadge-slL-XBCu.js → AchievementBadge-DF8q1x6R.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BeFkGc5M.js → AchievementsPage-B8Dg5myo.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-BrGUIipD.js → AchievementsSection-oPPiyP3y.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-avmK1ODc.js → BadgeDetailModal-PneMW4B4.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-DaEPdEZi.js → BotAnimated-GOlSvEiz.js} +1 -1
  11. package/dashboard/dist/assets/{Card-CIZCccTA.js → Card-BT5G0GE0.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-BhRj0SYO.js → CloudCheckoutPage-BNeL5Muo.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-CvaN8a54.js → CloudPage-26bhEYqI.js} +2 -2
  14. package/dashboard/dist/assets/{CommandPalette-BUNuXmYF.js → CommandPalette-CiMSYHNK.js} +1 -1
  15. package/dashboard/dist/assets/{CommunityStatsModal-BCI0WHPr.js → CommunityStatsModal-fK3BE9CW.js} +1 -1
  16. package/dashboard/dist/assets/{ConfirmModal-CT7IwtLR.js → ConfirmModal-CrUlEHyZ.js} +1 -1
  17. package/dashboard/dist/assets/{Controls-CtnMyxNH.js → Controls-CbjSwq6k.js} +1 -1
  18. package/dashboard/dist/assets/{DashboardPage-BuzyITeL.js → DashboardPage-CP_PMMga.js} +1 -1
  19. package/dashboard/dist/assets/{DateRangePopover-1Ps1-1dw.js → DateRangePopover-E-2Z6imh.js} +1 -1
  20. package/dashboard/dist/assets/{DevicePage-Dt-dVQAG.js → DevicePage-B0fLd80M.js} +1 -1
  21. package/dashboard/dist/assets/{DialogClose-D-DRTF1u.js → DialogClose-xJKE22Y6.js} +1 -1
  22. package/dashboard/dist/assets/DialogDescription-CXDxWzGi.js +1 -0
  23. package/dashboard/dist/assets/{DialogTitle-CiiRTn3_.js → DialogTitle-f8vnnH7z.js} +1 -1
  24. package/dashboard/dist/assets/{ElectricLogo-k6RFJ2qC.js → ElectricLogo-Ci9WZfHK.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-CNHg83aK.js → FadeIn-BXqtyAmn.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-CIYW5dc3.js → HeaderGithubStar-BV5SYJMi.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-Dn80DyPM.js → HoverTooltip-D3gpW3Jh.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-B29dYfoI.js → IpCheckPage-G7fIWXpL.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-Dzc8_QyW.js → LandingPage-BQNOdAkR.js} +2 -2
  30. package/dashboard/dist/assets/{LeaderboardPage-DXPcIhyQ.js → LeaderboardPage-D2_wXazL.js} +2 -2
  31. package/dashboard/dist/assets/{LeaderboardProBadge-DxEdj2Zq.js → LeaderboardProBadge-3xJ-mH2v.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-DnKY2223.js → LeaderboardProfileModal-23pH6Ud7.js} +3 -3
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-CY94ngqy.js → LeaderboardProfilePage-DSezSR58.js} +1 -1
  34. package/dashboard/dist/assets/{LimitsPage-AlvnAPCD.js → LimitsPage-BLJsmVVy.js} +8 -8
  35. package/dashboard/dist/assets/{LocalOnlyNotice-EHw7BeYS.js → LocalOnlyNotice-Dsk-d0Hr.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-E5E3i2kj.js → LoginCard-C7dugIIn.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-DxhQI9kh.js → LoginModal-B7RuLsTF.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-DRwJn-Fw.js → LoginPage-u32vJQN9.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-DIpVTdiC.js → PetPage-Dbc975oU.js} +2 -2
  40. package/dashboard/dist/assets/{PopoverPopup-Bjs7E91g.js → PopoverPopup-D-yECvH9.js} +1 -1
  41. package/dashboard/dist/assets/{ProviderIcon-DqXIjym-.js → ProviderIcon-CHtZiJUN.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-DxSRYQgP.js → ResetPasswordPage-ctuIsNk0.js} +1 -1
  43. package/dashboard/dist/assets/{Select-BfjrcaiM.js → Select-DN4Kq_R1.js} +1 -1
  44. package/dashboard/dist/assets/{SelectItemText-Dpdlb0cU.js → SelectItemText-C-HmeD8L.js} +1 -1
  45. package/dashboard/dist/assets/{SelfHostPage-Cnp_PMaD.js → SelfHostPage-BZOL3JMt.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-fBwG-qXq.js → ServiceStatusPage-C0sDYa8c.js} +1 -1
  47. package/dashboard/dist/assets/{SessionsPage-BEk7AHdq.js → SessionsPage-l0-FkcJB.js} +1 -1
  48. package/dashboard/dist/assets/{SettingsPage-BgML2jQR.js → SettingsPage-CrczfZiZ.js} +3 -3
  49. package/dashboard/dist/assets/{SkillsPage-DcvVZ3Wn.js → SkillsPage-Cz6G9d3o.js} +1 -1
  50. package/dashboard/dist/assets/{TokenGalaxy-D29yX2Hb.js → TokenGalaxy-B5IGaLQK.js} +1 -1
  51. package/dashboard/dist/assets/{TooltipPopup-CjzmNK2l.js → TooltipPopup-B6FqLWO7.js} +1 -1
  52. package/dashboard/dist/assets/{TrendMonitor-DfnQrsrx.js → TrendMonitor-B4kJUwmF.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-CRYomYfk.js → WidgetsPage-BKo3DBxT.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-CvQI5zUw.js → WrappedPage-Blm3aPO5.js} +1 -1
  55. package/dashboard/dist/assets/{__vite-browser-external-BZ-i40Kf.js → __vite-browser-external--HDUIUZr.js} +3 -3
  56. package/dashboard/dist/assets/{agent-logos-CvOX7WXW.js → agent-logos-DMtDEx7_.js} +1 -1
  57. package/dashboard/dist/assets/{arrow-left-BrQrZJtW.js → arrow-left-Bo9y7VzF.js} +1 -1
  58. package/dashboard/dist/assets/{arrow-right-fC11gmFY.js → arrow-right-BLMU8Y5U.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-up-right-CCMEqQQp.js → arrow-up-right-B4hXO4sc.js} +1 -1
  60. package/dashboard/dist/assets/{calendar-days-OaIQ06qD.js → calendar-days-D9HaEtgo.js} +1 -1
  61. package/dashboard/dist/assets/{check-D888uFpx.js → check-BJNGKpX8.js} +1 -1
  62. package/dashboard/dist/assets/{chevron-down-BJ0BpS_w.js → chevron-down-CoGC_RLK.js} +1 -1
  63. package/dashboard/dist/assets/{cloud-checkout-BeOpEJTZ.js → cloud-checkout-Du5FoHqV.js} +1 -1
  64. package/dashboard/dist/assets/{composite-CUuXvKos.js → composite-BfIUPpl8.js} +1 -1
  65. package/dashboard/dist/assets/{download-BJ7CKwx5.js → download-C1K_v0C0.js} +1 -1
  66. package/dashboard/dist/assets/{external-link-CDaZwJVN.js → external-link-D-xHhfj5.js} +1 -1
  67. package/dashboard/dist/assets/{flame-ChyUKs-9.js → flame-DXD6uvhM.js} +1 -1
  68. package/dashboard/dist/assets/{format-tokens-B6WX7zcH.js → format-tokens-B9riGvNj.js} +1 -1
  69. package/dashboard/dist/assets/{gift-CQlAZUJ5.js → gift-LPR-nD0J.js} +1 -1
  70. package/dashboard/dist/assets/{host-mode-Gw6KD5OJ.js → host-mode-C2K0MMOs.js} +1 -1
  71. package/dashboard/dist/assets/{icons-C7gGTMuQ.js → icons-DLdtLqEt.js} +1 -1
  72. package/dashboard/dist/assets/{index-CLb64Tlj.js → index-BL11MboX.js} +1 -1
  73. package/dashboard/dist/assets/{index-Bm2dDAVL.js → index-CC3cXeBa.js} +1 -1
  74. package/dashboard/dist/assets/{index-C0pbymQa.js → index-CXdJ7zMN.js} +1 -1
  75. package/dashboard/dist/assets/{info-CvOmd88O.js → info-tnXKCyuK.js} +1 -1
  76. package/dashboard/dist/assets/{link-2-BNEPxvlF.js → link-2-DOk9Tcg1.js} +1 -1
  77. package/dashboard/dist/assets/{loader-circle-bRHxCgY3.js → loader-circle-DIl96Mw-.js} +1 -1
  78. package/dashboard/dist/assets/{modifiers.esm-D-X0-uvt.js → modifiers.esm-2H_1taJ6.js} +1 -1
  79. package/dashboard/dist/assets/{monitor-smartphone-Bo7d-q73.js → monitor-smartphone-tnp-03hs.js} +1 -1
  80. package/dashboard/dist/assets/{play-DqpGELN5.js → play-BGMuGTdN.js} +1 -1
  81. package/dashboard/dist/assets/{plus-2fA1tz7S.js → plus-XETAaErq.js} +1 -1
  82. package/dashboard/dist/assets/{provider-display-B1KigDYZ.js → provider-display-BYNT4zPc.js} +1 -1
  83. package/dashboard/dist/assets/{react-DYja9tZG.js → react-OW7HALVO.js} +1 -1
  84. package/dashboard/dist/assets/{safePolygon-BerB8y-A.js → safePolygon-CAfS8-ct.js} +1 -1
  85. package/dashboard/dist/assets/{search-CU6UVI1L.js → search-B7ZJhoau.js} +1 -1
  86. package/dashboard/dist/assets/{server-CZR4PTdb.js → server-CWrP7mtL.js} +1 -1
  87. package/dashboard/dist/assets/{shield-check-BGbNmrMP.js → shield-check-ip5IQEK3.js} +1 -1
  88. package/dashboard/dist/assets/{skills-api-CJLEzxDf.js → skills-api-CpyoNTeB.js} +1 -1
  89. package/dashboard/dist/assets/{sortable.esm-CJ1NZWER.js → sortable.esm-YvzWzPDu.js} +1 -1
  90. package/dashboard/dist/assets/{store-CJFdru_6.js → store-DRK6-JBg.js} +1 -1
  91. package/dashboard/dist/assets/{terminal-ClyQ8tvQ.js → terminal-DQRoJSvh.js} +1 -1
  92. package/dashboard/dist/assets/{trash-Dg2PWl5-.js → trash-8h_26atF.js} +1 -1
  93. package/dashboard/dist/assets/{use-community-stats-Ca995kap.js → use-community-stats-DYlhIXsp.js} +1 -1
  94. package/dashboard/dist/assets/{use-latest-request-guard-BKbMGVji.js → use-latest-request-guard-AxJF9SEJ.js} +1 -1
  95. package/dashboard/dist/assets/{use-limits-display-prefs-CGyd8lvc.js → use-limits-display-prefs-DOZzuTwo.js} +1 -1
  96. package/dashboard/dist/assets/{use-native-settings-Dqeuqm9O.js → use-native-settings-BQgB6_PB.js} +1 -1
  97. package/dashboard/dist/assets/{use-ordered-list-Nj7R1iUu.js → use-ordered-list-ogi7Okzf.js} +1 -1
  98. package/dashboard/dist/assets/use-reduced-motion-D5PHT_kx.js +1 -0
  99. package/dashboard/dist/assets/{use-session-efficiency-pref-C_H5BaGV.js → use-session-efficiency-pref-DP3hu3Rp.js} +1 -1
  100. package/dashboard/dist/assets/{use-transform-CC90hurD.js → use-transform-BQqoo1et.js} +1 -1
  101. package/dashboard/dist/assets/use-usage-limits-DAf6O4aZ.js +1 -0
  102. package/dashboard/dist/assets/{useCurrency-CMHIL39_.js → useCurrency-Bv9g8mpq.js} +1 -1
  103. package/dashboard/dist/assets/{usePositioner-CRSfrGQq.js → usePositioner-CQiZGknc.js} +1 -1
  104. package/dashboard/dist/assets/{useTokenFormat-B5jhM_Kb.js → useTokenFormat-BUVirY0k.js} +1 -1
  105. package/dashboard/dist/assets/{zap-CKx8ySZs.js → zap-DR3VD0yY.js} +1 -1
  106. package/dashboard/dist/index.html +1 -1
  107. package/dashboard/dist/ip-check.html +1 -1
  108. package/dashboard/dist/leaderboard.html +1 -1
  109. package/dashboard/dist/share.html +1 -1
  110. package/package.json +1 -1
  111. package/src/commands/sync.js +546 -31
  112. package/src/lib/async-queue-reader.js +37 -16
  113. package/src/lib/codex-context-breakdown.js +8 -1
  114. package/src/lib/codex-rollout-parser.js +65 -5
  115. package/src/lib/grok-limits.js +56 -0
  116. package/src/lib/pricing/matcher.js +9 -0
  117. package/src/lib/pricing/seed-snapshot.json +1 -1
  118. package/src/lib/rollout.js +356 -8
  119. package/src/lib/session-analytics.js +1 -1
  120. package/src/lib/usage-limits.js +33 -1
  121. package/dashboard/dist/assets/DialogDescription-BOYDS1mM.js +0 -1
  122. package/dashboard/dist/assets/use-reduced-motion-CGMbDpHQ.js +0 -1
  123. package/dashboard/dist/assets/use-usage-limits-DtaaE7D8.js +0 -1
@@ -1,9 +1,9 @@
1
1
  const fs = require("node:fs/promises");
2
+ const { createHash } = require("node:crypto");
2
3
  const { StringDecoder } = require("node:string_decoder");
3
4
  const { setImmediate: yieldToRequests } = require("node:timers/promises");
4
5
 
5
6
  const CHUNK_BYTES = 64 * 1024;
6
- const PROBE_BYTES = 256;
7
7
 
8
8
  function signature(stat) {
9
9
  return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
@@ -17,10 +17,18 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
17
17
  const cache = new Map();
18
18
  const pending = new Map();
19
19
 
20
- async function probe(file, position, length) {
21
- const buffer = Buffer.alloc(length);
22
- const { bytesRead } = await file.read(buffer, 0, length, position);
23
- return buffer.subarray(0, bytesRead);
20
+ async function hashPrefix(file, length) {
21
+ const hash = createHash("sha256");
22
+ const buffer = Buffer.allocUnsafe(CHUNK_BYTES);
23
+ let position = 0;
24
+ while (position < length) {
25
+ const { bytesRead } = await file.read(buffer, 0, Math.min(CHUNK_BYTES, length - position), position);
26
+ if (!bytesRead) throw new Error("Queue truncated during validation");
27
+ hash.update(buffer.subarray(0, bytesRead));
28
+ position += bytesRead;
29
+ await yieldToRequests();
30
+ }
31
+ return hash;
24
32
  }
25
33
 
26
34
  async function load(queuePath) {
@@ -32,19 +40,28 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
32
40
  // between stat and open. Its signature always describes the bytes we read.
33
41
  const stat = await file.stat({ bigint: true });
34
42
  const nextSignature = signature(stat);
35
- if (cached?.signature === nextSignature) return cached.rows;
36
43
  const identity = `${stat.dev}:${stat.ino}`;
37
44
  const size = Number(stat.size);
45
+ if (cached?.signature === nextSignature) {
46
+ // Windows can reuse mtime/ctime for same-size writes anywhere in the
47
+ // file. Validate every byte before reusing the parsed snapshot.
48
+ const hash = await hashPrefix(file, size);
49
+ if (hash.digest("hex") === cached.digest) return cached.rows;
50
+ }
38
51
  let offset = 0;
39
52
  let seen = new Map();
53
+ let hash = createHash("sha256");
40
54
  if (cached && cached.identity === identity && size > cached.size && cached.consumedBytes > 0) {
41
- const head = await probe(file, 0, cached.head.length);
42
- const anchor = await probe(file, cached.consumedBytes - cached.anchor.length, cached.anchor.length);
43
- if (head.equals(cached.head) && anchor.equals(cached.anchor)) {
55
+ // Growth can include an interior rewrite, so verify the entire old
56
+ // committed prefix before parsing only the new tail.
57
+ const prefixHash = await hashPrefix(file, cached.consumedBytes);
58
+ if (prefixHash.copy().digest("hex") === cached.committedDigest) {
44
59
  offset = cached.consumedBytes;
45
60
  seen = cached.seen;
61
+ hash = prefixHash;
46
62
  }
47
63
  }
64
+ let committedHash = hash.copy();
48
65
 
49
66
  const changes = new Map();
50
67
  let malformed = 0;
@@ -66,9 +83,14 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
66
83
  const { bytesRead } = await file.read(buffer, 0, length, position);
67
84
  if (!bytesRead) throw new Error("Queue truncated during read");
68
85
  const newline = buffer.lastIndexOf(0x0a, bytesRead - 1);
69
- if (newline >= 0) consumedBytes = position + newline + 1;
86
+ const chunk = buffer.subarray(0, bytesRead);
87
+ if (newline >= 0) {
88
+ consumedBytes = position + newline + 1;
89
+ committedHash = hash.copy().update(chunk.subarray(0, newline + 1));
90
+ }
91
+ hash.update(chunk);
70
92
  position += bytesRead;
71
- remainder += decoder.write(buffer.subarray(0, bytesRead));
93
+ remainder += decoder.write(chunk);
72
94
  let start = 0;
73
95
  let end;
74
96
  while ((end = remainder.indexOf("\n", start)) !== -1) {
@@ -79,10 +101,6 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
79
101
  await yieldToRequests();
80
102
  }
81
103
  remainder += decoder.end();
82
- // Capture probes before committing any state: a failed read must not mutate
83
- // a snapshot still being used by another endpoint.
84
- const head = await probe(file, 0, Math.min(PROBE_BYTES, consumedBytes));
85
- const anchor = await probe(file, Math.max(0, consumedBytes - PROBE_BYTES), Math.min(PROBE_BYTES, consumedBytes));
86
104
  for (const [key, row] of changes) seen.set(key, row);
87
105
  const tail = new Map();
88
106
  parse(remainder, tail);
@@ -90,7 +108,10 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
90
108
  // key never enters the committed map (completion may change that key).
91
109
  const rows = tail.size ? Array.from(new Map([...seen, ...tail]).values()) : Array.from(seen.values());
92
110
  cache.delete(queuePath);
93
- cache.set(queuePath, { signature: nextSignature, identity, size, consumedBytes, head, anchor, seen, rows });
111
+ cache.set(queuePath, {
112
+ signature: nextSignature, identity, size, consumedBytes,
113
+ digest: hash.digest("hex"), committedDigest: committedHash.digest("hex"), seen, rows,
114
+ });
94
115
  while (cache.size > maxEntries) cache.delete(cache.keys().next().value);
95
116
  if (malformed) console.error(`[LocalAPI] ${label}: skipped ${malformed} malformed line(s)`);
96
117
  return rows;
@@ -35,6 +35,7 @@ const {
35
35
  finalizeExecRows,
36
36
  buildSkillStatsEntry,
37
37
  isCodexResumeInvalidated,
38
+ isRecoverableGroupedReadError,
38
39
  } = require("./codex-rollout-parser");
39
40
 
40
41
  // ---------------------------------------------------------------------------
@@ -473,6 +474,7 @@ async function computeCodexContextBreakdown({
473
474
  PARSED_GROUP_CACHE.delete(parsedCacheKey);
474
475
  PARSED_GROUP_CACHE.set(parsedCacheKey, cachedGroup);
475
476
  } else {
477
+ try {
476
478
  let captured = null;
477
479
  const singleEntry = entries.length === 1 ? entries[0] : null;
478
480
  const appendHashEligible = Boolean(
@@ -538,7 +540,7 @@ async function computeCodexContextBreakdown({
538
540
  diagnostics.incremental_parse_fallbacks += 1;
539
541
  }
540
542
  } catch (error) {
541
- if (!isCodexResumeInvalidated(error)) throw error;
543
+ if (!isCodexResumeInvalidated(error) && !isRecoverableGroupedReadError(error)) throw error;
542
544
  diagnostics.incremental_parse_fallbacks += 1;
543
545
  } finally {
544
546
  await resumeHandle.close();
@@ -587,6 +589,11 @@ async function computeCodexContextBreakdown({
587
589
  stat: singleEntry?.stat || null,
588
590
  });
589
591
  }
592
+ } catch (error) {
593
+ // One unreadable archive/session must not fail the whole breakdown.
594
+ if (!isRecoverableGroupedReadError(error)) throw error;
595
+ continue;
596
+ }
590
597
  }
591
598
  if (!parsed || !parsed.totals || !parsed.totals.total_tokens) continue;
592
599
  sessions.push(parsed);
@@ -42,6 +42,28 @@ const {
42
42
  } = require("./codex-usage-record");
43
43
 
44
44
  const DISCOVERY_FULL_AUDIT_INTERVAL_MS = 60 * 1000;
45
+ const CODEX_ROLLOUT_TEXT_MAX_BYTES = 256 * 1024 * 1024;
46
+
47
+ async function readCodexRolloutText(filePath, { maxOutputBytes = CODEX_ROLLOUT_TEXT_MAX_BYTES } = {}) {
48
+ if (typeof filePath !== "string" || !/\.jsonl\.zstd?$/i.test(filePath)) {
49
+ return fs.readFile(filePath, "utf8");
50
+ }
51
+ // The snapshot decoder lives in rollout.js (reused by the sync parser);
52
+ // require lazily so this module stays loadable without pulling the whole
53
+ // rollout graph at import time.
54
+ const rollout = require("./rollout");
55
+ const snapshot = await rollout.readCodexRolloutSnapshot(filePath, { maxOutputBytes });
56
+ if (!snapshot) {
57
+ // Treat undecodable / vanished archives as recoverable I/O so grouped
58
+ // session reads (live + archive) can still use a readable sibling copy.
59
+ const error = new Error(`unreadable Codex archive ${filePath}`);
60
+ error.code = "EIO";
61
+ throw error;
62
+ }
63
+ // Snapshot lines keep their "\n" terminators; the tail element may be an
64
+ // unterminated final line — join preserves exactly that.
65
+ return snapshot.lines.join("\n");
66
+ }
45
67
  const MAX_DISCOVERY_INVENTORIES = 32;
46
68
  const DISCOVERY_INVENTORIES = new Map();
47
69
  const ZONED_DAY_FORMATTERS = new Map();
@@ -143,6 +165,11 @@ function safeJsonParse(str, diagnostics = null) {
143
165
  }
144
166
  }
145
167
 
168
+ function isRolloutCandidateFileName(name) {
169
+ if (typeof name !== "string") return false;
170
+ return name.endsWith(".jsonl") || /^rollout-.*\.jsonl\.zstd?$/.test(name);
171
+ }
172
+
146
173
  function listJsonlFiles(rootDir) {
147
174
  const out = [];
148
175
  const stack = [rootDir];
@@ -160,7 +187,7 @@ function listJsonlFiles(rootDir) {
160
187
  const filePath = path.join(dir, entry.name);
161
188
  if (entry.isDirectory()) {
162
189
  stack.push(filePath);
163
- } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
190
+ } else if (entry.isFile() && isRolloutCandidateFileName(entry.name)) {
164
191
  out.push(filePath);
165
192
  }
166
193
  }
@@ -331,7 +358,7 @@ function discoverCodexSessionFiles(rootDirs, options = {}) {
331
358
  stack.push(filePath);
332
359
  continue;
333
360
  }
334
- if (!child.isFile() || !child.name.endsWith(".jsonl") || visited.has(filePath)) continue;
361
+ if (!child.isFile() || !isRolloutCandidateFileName(child.name) || visited.has(filePath)) continue;
335
362
  visited.add(filePath);
336
363
  if (diagnostics) diagnostics.discovered_files += 1;
337
364
  const cached = inventory.files.get(filePath) || null;
@@ -1175,7 +1202,13 @@ async function parseCodexRolloutFile(filePath, {
1175
1202
  compactionResponseIds: Array.from(compactionResponseIds),
1176
1203
  };
1177
1204
  result.endOffset = readProgress.endOffset;
1178
- result.appendable = Boolean(contentHasher) && (
1205
+ // Compressed snapshots are not append-only logs: endOffset stays at the
1206
+ // pre-decode cursor (often 0) and must never be marked appendable, or the
1207
+ // next growth would "resume" by parsing compressed bytes as JSONL.
1208
+ const hasCompressedArchive = filePaths.some(
1209
+ (candidate) => typeof candidate === "string" && /\.jsonl\.zstd?$/i.test(candidate),
1210
+ );
1211
+ result.appendable = Boolean(contentHasher) && !hasCompressedArchive && (
1179
1212
  readProgress.endOffset === 0 || readProgress.lastByte === 0x0a
1180
1213
  );
1181
1214
  result.contentHashState = contentHasher;
@@ -1309,6 +1342,27 @@ async function* readCodexObjectsFull(filePath, diagnostics, fileIndex, {
1309
1342
  readProgress = null,
1310
1343
  contentHasher = null,
1311
1344
  } = {}) {
1345
+ // Compressed archives are immutable snapshots, not append-only logs: byte
1346
+ // offsets are meaningless inside them, so decode the whole text and split
1347
+ // into lines. `readProgress` stays null so callers never persist a fake
1348
+ // byte cursor for them.
1349
+ if (typeof filePath === "string" && /\.jsonl\.zstd?$/i.test(filePath)) {
1350
+ const text = await readCodexRolloutText(filePath);
1351
+ let lineIndex = 0;
1352
+ const lines = text.split("\n");
1353
+ for (let index = 0; index < lines.length; index += 1) {
1354
+ const isLast = index + 1 >= lines.length;
1355
+ if (isLast && lines[index] === "") continue;
1356
+ const lineBuffer = Buffer.from(lines[index], "utf8");
1357
+ const obj = parseCodexLine(lineBuffer, diagnostics);
1358
+ if (obj) {
1359
+ yield { obj, fileIndex, lineIndex };
1360
+ lineIndex += 1;
1361
+ }
1362
+ }
1363
+ if (diagnostics) diagnostics.parsed_files += 1;
1364
+ return;
1365
+ }
1312
1366
  yield* readCodexObjectsIncremental(filePath, diagnostics, fileIndex, {
1313
1367
  startOffset,
1314
1368
  endOffset,
@@ -1331,6 +1385,7 @@ function isRecoverableGroupedReadError(error) {
1331
1385
 
1332
1386
  async function* iterateCodexObjects(filePaths, diagnostics, options = {}) {
1333
1387
  if (filePaths.length <= 1) {
1388
+ if (!filePaths[0]) return;
1334
1389
  for await (const record of readCodexObjects(filePaths[0], diagnostics, 0, options)) {
1335
1390
  yield record.obj;
1336
1391
  }
@@ -1355,7 +1410,11 @@ async function* iterateCodexObjects(filePaths, diagnostics, options = {}) {
1355
1410
  if (!isRecoverableGroupedReadError(error)) throw error;
1356
1411
  }
1357
1412
  }
1358
- if (!scannedFiles) throw new Error("all grouped Codex rollout files failed during read");
1413
+ if (!scannedFiles) {
1414
+ const error = new Error("all grouped Codex rollout files failed during read");
1415
+ error.code = "EIO";
1416
+ throw error;
1417
+ }
1359
1418
  records.sort((a, b) => {
1360
1419
  const aTimestamp = typeof a.obj?.timestamp === "string" ? a.obj.timestamp : "";
1361
1420
  const bTimestamp = typeof b.obj?.timestamp === "string" ? b.obj.timestamp : "";
@@ -1373,7 +1432,7 @@ async function* iterateCodexObjects(filePaths, diagnostics, options = {}) {
1373
1432
 
1374
1433
  function rolloutSessionIdFromPath(filePath) {
1375
1434
  const match = path.basename(String(filePath || "")).match(
1376
- /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i,
1435
+ /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zstd?)?$/i,
1377
1436
  );
1378
1437
  return match ? match[1] : null;
1379
1438
  }
@@ -1404,6 +1463,7 @@ module.exports = {
1404
1463
  isTimestampInRequestedDayRange,
1405
1464
  rolloutSessionIdFromPath,
1406
1465
  isCodexResumeInvalidated,
1466
+ isRecoverableGroupedReadError,
1407
1467
  finalizeToolRows,
1408
1468
  finalizeSkillRows,
1409
1469
  finalizeExecRows,
@@ -529,6 +529,59 @@ function deriveGrokPlanLabel(rawTier) {
529
529
  return GROK_PLAN_TIERS.get(key) ?? null;
530
530
  }
531
531
 
532
+ // Billing product ids → the name grok.com shows in the usage legend.
533
+ // Unknown ids are humanized below so a new product still gets a row.
534
+ const GROK_PRODUCT_LABELS = new Map([
535
+ ["GrokBuild", "Grok Build"],
536
+ ["GrokBot", "Grok Bot"],
537
+ ["GrokChat", "Chat"],
538
+ ["GrokImagine", "Imagine"],
539
+ ["Cursor", "Cursor"],
540
+ ]);
541
+
542
+ function normalizeGrokProductId(value) {
543
+ if (typeof value !== "string") return null;
544
+ const id = value.trim();
545
+ if (!/^[A-Za-z][A-Za-z0-9]{0,31}$/.test(id)) return null;
546
+ return id;
547
+ }
548
+
549
+ function humanizeGrokProductId(productId) {
550
+ const spaced = productId
551
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
552
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
553
+ .replace(/[_-]+/g, " ")
554
+ .replace(/\s+/g, " ")
555
+ .trim();
556
+ return spaced.slice(0, 40) || null;
557
+ }
558
+
559
+ /**
560
+ * Per-product shares of the same pool as creditUsagePercent.
561
+ * A 12% Grok Bot row is 12 points of the weekly cap, not its own 100% quota.
562
+ * Zero and non-finite rows are omitted (grok.com hides unused products).
563
+ * API order is preserved; a repeated product id keeps the first finite row.
564
+ */
565
+ function normalizeGrokProductUsage(productUsage, resetAt) {
566
+ if (!Array.isArray(productUsage)) return [];
567
+ const seen = new Set();
568
+ const products = [];
569
+ for (const entry of productUsage) {
570
+ if (!entry || typeof entry !== "object") continue;
571
+ const product = normalizeGrokProductId(entry.product);
572
+ if (!product || seen.has(product)) continue;
573
+ const raw = Number(entry.usagePercent);
574
+ if (!Number.isFinite(raw) || raw <= 0) continue;
575
+ const window = buildWindow({ usedPercent: raw, resetAt });
576
+ if (!window || !(window.used_percent > 0)) continue;
577
+ const label = GROK_PRODUCT_LABELS.get(product) || humanizeGrokProductId(product);
578
+ if (!label) continue;
579
+ seen.add(product);
580
+ products.push({ product, label, window });
581
+ }
582
+ return products;
583
+ }
584
+
532
585
  /**
533
586
  * Parse either:
534
587
  * - Unified billing (`?format=credits`): weekly/monthly period + creditUsagePercent
@@ -614,6 +667,8 @@ function normalizeGrokBillingResponse(body) {
614
667
  billing_period_start: periodStart,
615
668
  primary_window: primaryWindow,
616
669
  secondary_window: secondaryWindow,
670
+ // Shares of primary_window. Do not add these on top of credit_usage_percent.
671
+ products: normalizeGrokProductUsage(config.productUsage, resetAt),
617
672
  };
618
673
  }
619
674
 
@@ -803,6 +858,7 @@ module.exports = {
803
858
  inferGrokPeriodTypeFromDates,
804
859
  sumProductUsagePercent,
805
860
  deriveGrokPlanLabel,
861
+ normalizeGrokProductUsage,
806
862
  normalizeGrokBillingResponse,
807
863
  fetchGrokBilling,
808
864
  fetchGrokLimits,
@@ -280,6 +280,15 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
280
280
  value: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
281
281
  };
282
282
  }
283
+ // OpenCode Zen free models carry a `-free` suffix on the paid model id
284
+ // (`deepseek-v4.1-flash-free`); the base-name fuzzy match must not bill them.
285
+ if (sourceKey === "opencode" && lower.endsWith("-free")) {
286
+ return {
287
+ hit: true,
288
+ source: "curated:opencode-free-suffix",
289
+ value: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
290
+ };
291
+ }
283
292
  // AStudio does not disclose its routed model. Stop before generic aliases
284
293
  // and fuzzy matching can turn an unresolved router into a priced model.
285
294
  if (sourceKey === "acode" && (lower === "auto" || lower.endsWith("-auto"))) {