tokentracker-cli 0.84.4 → 0.84.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-C29PwsK_.js → AchievementBadge-DJvRfvu1.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-BlZODLp8.js → AchievementsPage-Cr8WToxS.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-Bc6NMnIa.js → AchievementsSection-BCPYB46e.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-BUy4636q.js → BadgeDetailModal-D-12Ek-H.js} +1 -1
  5. package/dashboard/dist/assets/{Card-CqxuFMAN.js → Card-Ha3x9zIj.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-BG3m8BgW.js → ClawdAnimated-DoGgAppT.js} +1 -1
  7. package/dashboard/dist/assets/{CommandPalette-Dcwtom2b.js → CommandPalette-DIKocEAD.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-dBjHrgjE.js → CommunityStatsModal-BNuMbEWy.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-DncQkptq.js → ConfirmModal-F2gPIIS9.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-CdF5WNHN.js → DashboardPage-YvoCEGG9.js} +1 -1
  11. package/dashboard/dist/assets/{DevicePage-B-PO4s-r.js → DevicePage-CbC_J22s.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-DGtj17VY.js → DialogClose-VEynQCYg.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-DLkuu1Iw.js → DialogTitle-BbAVHI_v.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-Dd5RBmXd.js → FadeIn-CZVqSnis.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-BBCOpHqO.js → HeaderGithubStar-Cs_h332Y.js} +1 -1
  17. package/dashboard/dist/assets/{HoverTooltip-BbigYxdC.js → HoverTooltip-DvvHTphK.js} +1 -1
  18. package/dashboard/dist/assets/{IpCheckPage-BBpZYjSC.js → IpCheckPage-DPVmqSS_.js} +1 -1
  19. package/dashboard/dist/assets/{LandingPage-ttXnt_s9.js → LandingPage-BCvNh3zE.js} +2 -2
  20. package/dashboard/dist/assets/{LeaderboardAvatar-DOl_cOO9.js → LeaderboardAvatar-BAPZ2ROX.js} +1 -1
  21. package/dashboard/dist/assets/{LeaderboardPage-BYIyoLkB.js → LeaderboardPage-DZSxazHP.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardProfileModal-BO2n4LSQ.js → LeaderboardProfileModal-r03akfe2.js} +3 -3
  23. package/dashboard/dist/assets/{LeaderboardProfilePage-CqwW5yMw.js → LeaderboardProfilePage-CU63YW2M.js} +1 -1
  24. package/dashboard/dist/assets/{LimitsPage-B-w771Tl.js → LimitsPage-JWZDa5_Y.js} +1 -1
  25. package/dashboard/dist/assets/{LocalOnlyNotice-Cqc_diwF.js → LocalOnlyNotice-BHlcPCZk.js} +1 -1
  26. package/dashboard/dist/assets/{LoginCard-D6zc1JKY.js → LoginCard-C-dqUumR.js} +1 -1
  27. package/dashboard/dist/assets/{LoginModal-udN3TqoF.js → LoginModal-BFzRg4Iy.js} +1 -1
  28. package/dashboard/dist/assets/{LoginPage-BucPr3_X.js → LoginPage-QtlnGF1S.js} +1 -1
  29. package/dashboard/dist/assets/{PetPage-6_nmMXWp.js → PetPage-C6UMY2qD.js} +1 -1
  30. package/dashboard/dist/assets/{PopoverPopup-DntczBmH.js → PopoverPopup-CltmqcrT.js} +1 -1
  31. package/dashboard/dist/assets/{ProviderIcon-Z6qlwSj1.js → ProviderIcon-BqOI9EiD.js} +1 -1
  32. package/dashboard/dist/assets/{ResetPasswordPage-WqQwsQC1.js → ResetPasswordPage-BsfCBUZD.js} +1 -1
  33. package/dashboard/dist/assets/{SegmentedControl-D3NM3dOv.js → SegmentedControl-D4biXw_Q.js} +1 -1
  34. package/dashboard/dist/assets/{Select-DCpwSFK7.js → Select-DTw-14iE.js} +1 -1
  35. package/dashboard/dist/assets/{SelectItemText-DICaclqa.js → SelectItemText-3tUrnu3f.js} +1 -1
  36. package/dashboard/dist/assets/{SessionsPage-COHoSjPP.js → SessionsPage-DYYlNqHg.js} +1 -1
  37. package/dashboard/dist/assets/{SettingsPage-nzbrBo_U.js → SettingsPage-CYlEboSl.js} +1 -1
  38. package/dashboard/dist/assets/{SkillsPage-DE8mK1lS.js → SkillsPage-Bzq8V5Pd.js} +1 -1
  39. package/dashboard/dist/assets/{TokenGalaxy-BSWGbByr.js → TokenGalaxy-DU77e8sM.js} +1 -1
  40. package/dashboard/dist/assets/{ToolbarRootContext-CxxXTuTT.js → ToolbarRootContext-DV3KGf-a.js} +1 -1
  41. package/dashboard/dist/assets/{TrendMonitor-DillSTMV.js → TrendMonitor-D6QYKmbl.js} +1 -1
  42. package/dashboard/dist/assets/{WidgetsPage-LqyxKYAt.js → WidgetsPage-DSP4jCsH.js} +1 -1
  43. package/dashboard/dist/assets/{WrappedPage-DNDuRKjf.js → WrappedPage-B0x6O_DU.js} +1 -1
  44. package/dashboard/dist/assets/{__vite-browser-external-DEhUaWQY.js → __vite-browser-external-D3llGq6p.js} +2 -2
  45. package/dashboard/dist/assets/{agent-logos-D34B25op.js → agent-logos-B2S_cdOL.js} +1 -1
  46. package/dashboard/dist/assets/{arrow-up-right-BYWbL4WV.js → arrow-up-right-DNUVuhx2.js} +1 -1
  47. package/dashboard/dist/assets/{check-DkfaR6JX.js → check-C3ARiiE_.js} +1 -1
  48. package/dashboard/dist/assets/{chevron-right-D2cpt_J3.js → chevron-right-3bY7H8jO.js} +1 -1
  49. package/dashboard/dist/assets/{copy-Cv5Ix0Pn.js → copy-C-UIMKoO.js} +1 -1
  50. package/dashboard/dist/assets/{download-5nr9Tl80.js → download-DtRzJfcr.js} +1 -1
  51. package/dashboard/dist/assets/{flame-B6M-msYH.js → flame-Bas3ry-g.js} +1 -1
  52. package/dashboard/dist/assets/{format-tokens-DiRRwgY8.js → format-tokens-DFtRUIH8.js} +1 -1
  53. package/dashboard/dist/assets/{icons-DQHoG48j.js → icons-CHBt4Tmo.js} +1 -1
  54. package/dashboard/dist/assets/{index-hSxFduER.js → index-4unZlm2w.js} +1 -1
  55. package/dashboard/dist/assets/{index-BoH_HaYH.js → index-C1ADnvzm.js} +1 -1
  56. package/dashboard/dist/assets/{index-vFee2-sp.js → index-CZRlrK7w.js} +1 -1
  57. package/dashboard/dist/assets/{info-CM5MR8lk.js → info-BkrO-fg3.js} +1 -1
  58. package/dashboard/dist/assets/{limits-providers-BV4Y9nI4.js → limits-providers-BtwYmYHA.js} +1 -1
  59. package/dashboard/dist/assets/{link-2-Bd5jF09Z.js → link-2-DTLFz8uG.js} +1 -1
  60. package/dashboard/dist/assets/{loader-circle-B5oZJSYV.js → loader-circle-BqQW_Ipq.js} +1 -1
  61. package/dashboard/dist/assets/{maximize-2-CiYy9Kri.js → maximize-2-YdpeE4dq.js} +1 -1
  62. package/dashboard/dist/assets/{provider-display-lzLynfde.js → provider-display-CgBOpjq0.js} +1 -1
  63. package/dashboard/dist/assets/{react-CllryaXi.js → react-DD9jpv--.js} +1 -1
  64. package/dashboard/dist/assets/{search-DDdsotHp.js → search--TvX-fig.js} +1 -1
  65. package/dashboard/dist/assets/{skills-api-487vZmwI.js → skills-api-C5C8a1at.js} +1 -1
  66. package/dashboard/dist/assets/{store-CYaTRd_k.js → store-CRA8gPnP.js} +1 -1
  67. package/dashboard/dist/assets/{terminal-OyeOY0tb.js → terminal-BXZyl2ek.js} +1 -1
  68. package/dashboard/dist/assets/{trash-2-COI9stXJ.js → trash-2-IFdBQ65v.js} +1 -1
  69. package/dashboard/dist/assets/{use-community-stats-DkzinngC.js → use-community-stats-LyJ5aMpX.js} +1 -1
  70. package/dashboard/dist/assets/{use-limits-display-prefs-CiMETU5Y.js → use-limits-display-prefs-CKvFS1Nn.js} +1 -1
  71. package/dashboard/dist/assets/{use-native-settings-qzpxm9Bh.js → use-native-settings-Cg-4j_0z.js} +1 -1
  72. package/dashboard/dist/assets/{use-ordered-list-BI-Qh9xR.js → use-ordered-list-2pX5PXZO.js} +1 -1
  73. package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +1 -0
  74. package/dashboard/dist/assets/{use-session-efficiency-pref-1WyhIjeG.js → use-session-efficiency-pref-DilJOODA.js} +1 -1
  75. package/dashboard/dist/assets/{use-transform-D5i_ArzG.js → use-transform-DiyYTLpI.js} +1 -1
  76. package/dashboard/dist/assets/{use-usage-limits-Drcnt7__.js → use-usage-limits-DkUmp_nW.js} +1 -1
  77. package/dashboard/dist/assets/{useCurrency-KWJo1GIr.js → useCurrency-lm7KjGoD.js} +1 -1
  78. package/dashboard/dist/assets/{useScrollLock-K0FPky1Y.js → useScrollLock-DVSyJ-GE.js} +1 -1
  79. package/dashboard/dist/assets/{useTokenFormat-CwG4ck8v.js → useTokenFormat-Li8BANKO.js} +1 -1
  80. package/dashboard/dist/assets/{zap-wZ6IQAyU.js → zap-CKZt1_i5.js} +1 -1
  81. package/dashboard/dist/index.html +1 -1
  82. package/dashboard/dist/ip-check.html +1 -1
  83. package/dashboard/dist/leaderboard.html +1 -1
  84. package/dashboard/dist/share.html +1 -1
  85. package/package.json +1 -1
  86. package/src/lib/pricing/curated-overrides.json +536 -101
  87. package/src/lib/pricing/seed-snapshot.json +1 -1
  88. package/src/lib/rollout.js +373 -72
  89. package/dashboard/dist/assets/DialogDescription-wStLk8Br.js +0 -1
  90. package/dashboard/dist/assets/use-reduced-motion-DrsPn1ux.js +0 -1
@@ -6204,7 +6204,7 @@ async function parseKimiCodeIncremental({ wireFiles, cursors, queuePath, onProgr
6204
6204
  // each sync (passive scan only, same shape as Kimi's wire.jsonl reader).
6205
6205
  //
6206
6206
  // Per-line record types: message, reasoning, topic, file-history-snapshot.
6207
- // Only `type=="message" && role=="assistant"` carry token usage. The shape:
6207
+ // Only `type=="message" and role=="assistant"` carry token usage. The shape:
6208
6208
  //
6209
6209
  // providerData.rawUsage = {
6210
6210
  // prompt_tokens: 22223, // OpenAI-style — INCLUDES cached
@@ -6734,7 +6734,7 @@ async function parseCodebuddyIncremental({
6734
6734
  // (each verified against real ~/.workbuddy logs, NOT assumed from CodeBuddy):
6735
6735
  //
6736
6736
  // 1. Usage lives on `function_call` records too — not only on
6737
- // `type=="message" && role=="assistant"`. Each LLM round-trip (whether it
6737
+ // `type=="message" and role=="assistant"`. Each LLM round-trip (whether it
6738
6738
  // ends in a tool call or a text reply) carries its own providerData.rawUsage.
6739
6739
  // We therefore aggregate EVERY record that has providerData.rawUsage and
6740
6740
  // dedup per response id, instead of filtering by record type.
@@ -12387,7 +12387,8 @@ async function parseCopilotAppDbIncremental({
12387
12387
  // ─────────────────────────────────────────────────────────────────────────────
12388
12388
 
12389
12389
  const GROK_ESTIMATED_INPUT_RATIO = 0.8;
12390
- const GROK_CURSOR_VERSION = 3;
12390
+ // v4: bill from turn_completed.usage (true cumulative API usage), not context-window totalTokens.
12391
+ const GROK_CURSOR_VERSION = 4;
12391
12392
 
12392
12393
  function resolveGrokBuildHome(env = process.env) {
12393
12394
  if (env.TOKENTRACKER_GROK_HOME) return env.TOKENTRACKER_GROK_HOME;
@@ -12540,6 +12541,9 @@ function grokMessageCountFromSignals(signals) {
12540
12541
  );
12541
12542
  }
12542
12543
 
12544
+ // Context-window telemetry only. Prefer turn_completed.usage when available —
12545
+ // signals.totalTokens / contextTokensUsed track the live window, not billable
12546
+ // cumulative spend across a session (especially after compaction).
12543
12547
  function grokEffectiveTotalFromSignals(signals) {
12544
12548
  if (!signals || typeof signals !== "object") return 0;
12545
12549
  const beforeCompaction = normalizeNonNegativeNumber(signals.totalTokensBeforeCompaction);
@@ -12612,28 +12616,135 @@ function grokFileEndsWithNewline(filePath, size) {
12612
12616
  }
12613
12617
  }
12614
12618
 
12615
- async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOffsetEntry) {
12616
- if (!updatesPath) return { events: [], offsetEntry: null };
12619
+
12620
+ function canonicalizeGrokUsageModel(model) {
12621
+ const raw = normalizeModelInput(model) || "grok-build";
12622
+ const lower = raw.toLowerCase();
12623
+ // Free Build SKU must not fuzzy-match paid grok-4.5 rates in native clients.
12624
+ if (lower.includes("build-free") || lower.endsWith("-free") || lower.includes("free-tier")) {
12625
+ return "grok-build-free";
12626
+ }
12627
+ if (lower === "grok-4.5-build" || lower === "grok-4-5-build") {
12628
+ return "grok-4.5-build";
12629
+ }
12630
+ return raw;
12631
+ }
12632
+
12633
+ function normalizeGrokTurnUsage(usage, model, timestamp, eventId) {
12634
+ if (!usage || typeof usage !== "object") return null;
12635
+ const inputRaw = normalizeNonNegativeNumber(
12636
+ usage.inputTokens ?? usage.input_tokens,
12637
+ );
12638
+ const output = normalizeNonNegativeNumber(
12639
+ usage.outputTokens ?? usage.output_tokens,
12640
+ );
12641
+ const cached = normalizeNonNegativeNumber(
12642
+ usage.cachedReadTokens ??
12643
+ usage.cache_read_input_tokens ??
12644
+ usage.cached_input_tokens,
12645
+ );
12646
+ const reasoning = normalizeNonNegativeNumber(
12647
+ usage.reasoningTokens ?? usage.reasoning_output_tokens,
12648
+ );
12649
+ // Grok reports inputTokens as the full prompt (including cache hits). Split
12650
+ // so pricing can apply cache_read rates correctly.
12651
+ const nonCachedInput = Math.max(0, inputRaw - cached);
12652
+ let total = normalizeNonNegativeNumber(usage.totalTokens ?? usage.total_tokens);
12653
+ if (total <= 0) {
12654
+ total = inputRaw + output + reasoning;
12655
+ }
12656
+ if (total <= 0 && nonCachedInput <= 0 && cached <= 0 && output <= 0) return null;
12657
+ return {
12658
+ input_tokens: nonCachedInput,
12659
+ cached_input_tokens: cached,
12660
+ cache_creation_input_tokens: 0,
12661
+ output_tokens: output,
12662
+ reasoning_output_tokens: reasoning,
12663
+ total_tokens: total > 0 ? total : nonCachedInput + cached + output + reasoning,
12664
+ billable_total_tokens: total > 0 ? total : nonCachedInput + cached + output + reasoning,
12665
+ conversation_count: 1,
12666
+ model: canonicalizeGrokUsageModel(model),
12667
+ timestamp,
12668
+ eventId,
12669
+ };
12670
+ }
12671
+
12672
+ function extractGrokTurnUsageEvents(record, fallbackTimestamp, fallbackModel, lineIndex) {
12673
+ const update = record?.params?.update;
12674
+ if (!update || typeof update !== "object") return [];
12675
+ if (update.sessionUpdate !== "turn_completed") return [];
12676
+ const usage = update.usage;
12677
+ if (!usage || typeof usage !== "object") return [];
12678
+
12679
+ const meta = record?.params?._meta || record?._meta || {};
12680
+ const timestamp = grokTimestampFromUpdate(meta, record, fallbackTimestamp);
12681
+ const baseEventId = grokEventId(
12682
+ meta.eventId ?? record?.eventId ?? record?.id ?? update.prompt_id,
12683
+ String(lineIndex),
12684
+ );
12685
+
12686
+ const modelUsage =
12687
+ usage.modelUsage && typeof usage.modelUsage === "object" ? usage.modelUsage : null;
12688
+ const events = [];
12689
+ if (modelUsage && Object.keys(modelUsage).length > 0) {
12690
+ for (const [modelName, modelUsageEntry] of Object.entries(modelUsage)) {
12691
+ if (!modelUsageEntry || typeof modelUsageEntry !== "object") continue;
12692
+ const event = normalizeGrokTurnUsage(
12693
+ modelUsageEntry,
12694
+ modelName,
12695
+ timestamp,
12696
+ `${baseEventId}|${modelName}`,
12697
+ );
12698
+ if (event) events.push(event);
12699
+ }
12700
+ }
12701
+ if (events.length === 0) {
12702
+ const event = normalizeGrokTurnUsage(usage, fallbackModel, timestamp, baseEventId);
12703
+ if (event) events.push(event);
12704
+ }
12705
+ return events;
12706
+ }
12707
+
12708
+ // Context-window watermark events (legacy / fallback only).
12709
+ function extractGrokContextTokenEvent(record, fallbackTimestamp, lineIndex) {
12710
+ const meta = record?.params?._meta || record?._meta;
12711
+ if (!meta || typeof meta !== "object") return null;
12712
+ const totalTokens = normalizeNonNegativeNumber(meta.totalTokens);
12713
+ if (totalTokens <= 0) return null;
12714
+ return {
12715
+ totalTokens,
12716
+ timestamp: grokTimestampFromUpdate(meta, record, fallbackTimestamp),
12717
+ eventId: grokEventId(meta.eventId ?? record?.eventId ?? record?.id, String(lineIndex)),
12718
+ };
12719
+ }
12720
+
12721
+ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOffsetEntry, options = {}) {
12722
+ const fallbackModel = options.fallbackModel || "grok-build";
12723
+ if (!updatesPath) {
12724
+ return { turnEvents: [], contextEvents: [], offsetEntry: null };
12725
+ }
12617
12726
  let stat;
12618
12727
  try {
12619
12728
  stat = fssync.statSync(updatesPath);
12620
- if (!stat.isFile()) return { events: [], offsetEntry: null };
12729
+ if (!stat.isFile()) return { turnEvents: [], contextEvents: [], offsetEntry: null };
12621
12730
  } catch {
12622
- return { events: [], offsetEntry: null };
12731
+ return { turnEvents: [], contextEvents: [], offsetEntry: null };
12623
12732
  }
12624
12733
 
12625
- // updates.jsonl is append-only and carries cumulative totalTokens, deduped
12626
- // by the session high-watermark, so resuming from the last consumed byte is
12627
- // safe: re-read events stay below the watermark (no double count), and any
12628
- // bytes a write race leaves unparsed are covered by the next event's
12629
- // cumulative total. Re-read from 0 on truncation or inode change.
12734
+ // updates.jsonl is append-only. Turn usage is additive per turn_completed, so
12735
+ // resuming from the last consumed byte is safe. Re-read from 0 on truncation
12736
+ // or inode change.
12630
12737
  const prevSize = Number(prevOffsetEntry?.size) || 0;
12631
12738
  const prevIno = prevOffsetEntry?.ino;
12632
12739
  const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
12633
12740
  const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
12634
12741
  const baseOffset = { mtimeMs: stat.mtimeMs, ino: stat.ino };
12635
12742
  if (stat.size <= startOffset) {
12636
- return { events: [], offsetEntry: { size: startOffset, ...baseOffset } };
12743
+ return {
12744
+ turnEvents: [],
12745
+ contextEvents: [],
12746
+ offsetEntry: { size: startOffset, ...baseOffset },
12747
+ };
12637
12748
  }
12638
12749
 
12639
12750
  // Only advance the stored offset to the end of the last newline-terminated
@@ -12642,7 +12753,8 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
12642
12753
  // complete instead of skipping it forever (which would undercount tokens).
12643
12754
  const endsWithNewline = grokFileEndsWithNewline(updatesPath, stat.size);
12644
12755
 
12645
- const events = [];
12756
+ const turnEvents = [];
12757
+ const contextEvents = [];
12646
12758
  let lineIndex = 0;
12647
12759
  let lastLine = "";
12648
12760
  const input = fssync.createReadStream(updatesPath, {
@@ -12662,21 +12774,24 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
12662
12774
  } catch {
12663
12775
  continue;
12664
12776
  }
12665
- const meta = record?.params?._meta || record?._meta;
12666
- if (!meta || typeof meta !== "object") continue;
12667
- const totalTokens = normalizeNonNegativeNumber(meta.totalTokens);
12668
- if (totalTokens <= 0) continue;
12669
- const timestamp = grokTimestampFromUpdate(meta, record, fallbackTimestamp);
12670
- events.push({
12671
- totalTokens,
12672
- timestamp,
12673
- eventId: grokEventId(meta.eventId ?? record?.eventId ?? record?.id, String(lineIndex)),
12674
- });
12777
+ const turns = extractGrokTurnUsageEvents(
12778
+ record,
12779
+ fallbackTimestamp,
12780
+ fallbackModel,
12781
+ lineIndex,
12782
+ );
12783
+ if (turns.length > 0) {
12784
+ turnEvents.push(...turns);
12785
+ continue;
12786
+ }
12787
+ const contextEvent = extractGrokContextTokenEvent(record, fallbackTimestamp, lineIndex);
12788
+ if (contextEvent) contextEvents.push(contextEvent);
12675
12789
  }
12676
12790
  } catch {
12677
- // Stream error mid-read: keep the events we got, but do not advance the
12678
- // offset so the next sync retries the same range (watermark-safe).
12679
- return { events, offsetEntry: prevOffsetEntry || null };
12791
+ // Stream error mid-read: discard partial events and do not advance the
12792
+ // offset, so the next sync re-extracts from the same range exactly once
12793
+ // instead of double-counting already-parsed turn events.
12794
+ return { turnEvents: [], contextEvents: [], offsetEntry: prevOffsetEntry || null };
12680
12795
  }
12681
12796
 
12682
12797
  // When the file does not end on a newline, the final emitted line is a
@@ -12684,7 +12799,11 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
12684
12799
  // stays on a complete-line boundary and the line is re-read once finished.
12685
12800
  const trailingPartialBytes = endsWithNewline ? 0 : Buffer.byteLength(lastLine, "utf8");
12686
12801
  const committedSize = Math.max(startOffset, stat.size - trailingPartialBytes);
12687
- return { events, offsetEntry: { size: committedSize, ...baseOffset } };
12802
+ return {
12803
+ turnEvents,
12804
+ contextEvents,
12805
+ offsetEntry: { size: committedSize, ...baseOffset },
12806
+ };
12688
12807
  }
12689
12808
 
12690
12809
  function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
@@ -12706,6 +12825,70 @@ function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
12706
12825
  };
12707
12826
  }
12708
12827
 
12828
+ function clearGrokHourlyBuckets(hourlyState) {
12829
+ if (!hourlyState || typeof hourlyState !== "object") return;
12830
+ const buckets = hourlyState.buckets && typeof hourlyState.buckets === "object" ? hourlyState.buckets : null;
12831
+ if (buckets) {
12832
+ for (const key of Object.keys(buckets)) {
12833
+ if (key.startsWith("grok|")) delete buckets[key];
12834
+ }
12835
+ }
12836
+ const groupQueued =
12837
+ hourlyState.groupQueued && typeof hourlyState.groupQueued === "object"
12838
+ ? hourlyState.groupQueued
12839
+ : null;
12840
+ if (groupQueued) {
12841
+ for (const key of Object.keys(groupQueued)) {
12842
+ if (key.startsWith("grok|")) delete groupQueued[key];
12843
+ }
12844
+ }
12845
+ }
12846
+
12847
+ async function retractStaleGrokQueueRows(queuePath, keepKeys) {
12848
+ if (!queuePath) return 0;
12849
+ let raw = "";
12850
+ try {
12851
+ raw = fssync.readFileSync(queuePath, "utf8");
12852
+ } catch (error) {
12853
+ if (error?.code === "ENOENT") return 0;
12854
+ throw error;
12855
+ }
12856
+
12857
+ const latestGrok = new Map();
12858
+ for (const line of raw.split("\n")) {
12859
+ if (!line.trim()) continue;
12860
+ let row;
12861
+ try {
12862
+ row = JSON.parse(line);
12863
+ } catch {
12864
+ continue;
12865
+ }
12866
+ if ((row?.source || "") !== "grok") continue;
12867
+ const model = normalizeModelInput(row.model) || DEFAULT_MODEL;
12868
+ const hourStart = typeof row.hour_start === "string" ? row.hour_start : null;
12869
+ if (!hourStart) continue;
12870
+ latestGrok.set(bucketKey("grok", model, hourStart), { model, hour_start: hourStart, row });
12871
+ }
12872
+
12873
+ const zero = initTotals();
12874
+ const lines = [];
12875
+ for (const [key, entry] of latestGrok.entries()) {
12876
+ if (keepKeys.has(key)) continue;
12877
+ if (totalsKey(entry.row) === totalsKey(zero)) continue;
12878
+ lines.push(
12879
+ JSON.stringify({
12880
+ source: "grok",
12881
+ model: entry.model,
12882
+ hour_start: entry.hour_start,
12883
+ ...zero,
12884
+ }),
12885
+ );
12886
+ }
12887
+ if (lines.length === 0) return 0;
12888
+ await fs.appendFile(queuePath, `${lines.join("\n")}\n`, "utf8");
12889
+ return lines.length;
12890
+ }
12891
+
12709
12892
  async function parseGrokBuildIncremental({
12710
12893
  sessions,
12711
12894
  cursors = {},
@@ -12716,11 +12899,44 @@ async function parseGrokBuildIncremental({
12716
12899
  if (queuePath) await ensureDir(path.dirname(queuePath));
12717
12900
  const hourlyState = normalizeHourlyState(cursors?.hourly);
12718
12901
  const grokState = cursors.grok && typeof cursors.grok === "object" ? { ...cursors.grok } : {};
12719
- let sessionSnapshots = normalizeGrokSessionSnapshots(grokState);
12902
+ const prevVersion = Number(grokState.version) || 0;
12903
+ const needsTurnUsageMigration = prevVersion < GROK_CURSOR_VERSION;
12904
+
12905
+ // v3 and earlier treated context-window totalTokens as cumulative spend, which
12906
+ // undercounts heavily (often 10-50x) and mis-splits input/output. Rebuild from
12907
+ // turn_completed.usage when migrating to v4.
12908
+ //
12909
+ // Drop prior watermark totals / updateOffsets so files are re-read from byte 0,
12910
+ // but keep legacySeen markers from seenSessions so the one-shot baseline
12911
+ // (sessions already counted under the old scanner) still applies.
12912
+ let sessionSnapshots;
12913
+ if (needsTurnUsageMigration) {
12914
+ const normalized = normalizeGrokSessionSnapshots(grokState);
12915
+ sessionSnapshots = {};
12916
+ for (const [sessionId, snapshot] of Object.entries(normalized)) {
12917
+ if (!snapshot?.legacySeen) continue;
12918
+ sessionSnapshots[sessionId] = {
12919
+ totalTokens: 0,
12920
+ messageCount: 0,
12921
+ model: null,
12922
+ source: null,
12923
+ lastEventId: null,
12924
+ lastEventTimestamp: null,
12925
+ updatedAt: snapshot.updatedAt || null,
12926
+ legacySeen: true,
12927
+ };
12928
+ }
12929
+ clearGrokHourlyBuckets(hourlyState);
12930
+ } else {
12931
+ sessionSnapshots = normalizeGrokSessionSnapshots(grokState);
12932
+ }
12720
12933
  const prevUpdateOffsets =
12721
- grokState.updateOffsets && typeof grokState.updateOffsets === "object"
12934
+ !needsTurnUsageMigration &&
12935
+ grokState.updateOffsets &&
12936
+ typeof grokState.updateOffsets === "object"
12722
12937
  ? grokState.updateOffsets
12723
12938
  : {};
12939
+
12724
12940
  // Rebuilt from the sessions seen this scan, so entries for deleted session
12725
12941
  // dirs are pruned and the cursor stays bounded by the on-disk session count.
12726
12942
  const updateOffsets = {};
@@ -12755,83 +12971,142 @@ async function parseGrokBuildIncremental({
12755
12971
  const model = grokModelFromSignals(safeSignals);
12756
12972
  const lastActive = grokLastActiveFromSignals(safeSignals, summary);
12757
12973
 
12758
- let highWatermark = previousTotal;
12759
- let observedTotal = previousTotal;
12974
+ let cumulativeTotal = previousTotal;
12760
12975
  let tokenDeltaForSession = 0;
12761
12976
  let finalTouchedHourStart = null;
12762
12977
  let source = previous.source || null;
12763
12978
  let lastEventId = previous.lastEventId || null;
12764
12979
  let lastEventTimestamp = previous.lastEventTimestamp || null;
12765
- const pendingTokenDeltas = [];
12766
-
12767
- const recordTokenDelta = (deltaTokens, timestamp, deltaSource) => {
12768
- const hourStartStr = toUtcHalfHourStart(timestamp) || toUtcHalfHourStart(Date.now());
12769
- if (!hourStartStr) return false;
12770
- pendingTokenDeltas.push({ deltaTokens, hourStartStr });
12771
- tokenDeltaForSession += deltaTokens;
12772
- finalTouchedHourStart = hourStartStr;
12773
- source = deltaSource;
12774
- lastEventTimestamp = timestamp || lastEventTimestamp;
12775
- return true;
12776
- };
12980
+ let lastModel = previous.model || model;
12981
+ let sawTurnUsage = source === "turn_usage" || previous.source === "turn_usage";
12982
+ // Defer bucket writes until after we know whether this is a legacy baseline
12983
+ // pass (first sighting of a session already counted under an older scanner).
12984
+ const pendingBucketDeltas = [];
12777
12985
 
12778
12986
  const updatesPath = grokUpdatesPathForSession(sess);
12779
12987
  const updates = await readGrokUpdateTokenEvents(
12780
12988
  updatesPath,
12781
12989
  lastActive,
12782
12990
  updatesPath ? prevUpdateOffsets[updatesPath] : null,
12991
+ { fallbackModel: model },
12783
12992
  );
12784
12993
  if (updatesPath && updates.offsetEntry) {
12785
12994
  updateOffsets[updatesPath] = updates.offsetEntry;
12786
12995
  }
12787
- for (const event of updates.events) {
12788
- observedTotal = Math.max(observedTotal, event.totalTokens);
12996
+
12997
+ // Preferred path: each turn_completed carries true cumulative API usage for
12998
+ // that turn (input/output/cache/reasoning). Sum them.
12999
+ for (const event of updates.turnEvents) {
13000
+ sawTurnUsage = true;
13001
+ const hourStartStr = toUtcHalfHourStart(event.timestamp) || toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
13002
+ if (!hourStartStr) continue;
13003
+ const eventModel = event.model || model;
13004
+ const delta = {
13005
+ input_tokens: event.input_tokens,
13006
+ cached_input_tokens: event.cached_input_tokens,
13007
+ cache_creation_input_tokens: event.cache_creation_input_tokens,
13008
+ output_tokens: event.output_tokens,
13009
+ reasoning_output_tokens: event.reasoning_output_tokens,
13010
+ total_tokens: event.total_tokens,
13011
+ billable_total_tokens: event.billable_total_tokens,
13012
+ conversation_count: event.conversation_count || 1,
13013
+ };
13014
+ pendingBucketDeltas.push({ model: eventModel, hourStartStr, delta });
13015
+ cumulativeTotal += event.total_tokens;
13016
+ tokenDeltaForSession += event.total_tokens;
13017
+ finalTouchedHourStart = hourStartStr;
13018
+ source = "turn_usage";
12789
13019
  lastEventId = event.eventId || lastEventId;
12790
13020
  lastEventTimestamp = event.timestamp || lastEventTimestamp;
12791
- if (event.totalTokens <= highWatermark) continue;
12792
- const deltaTokens = event.totalTokens - highWatermark;
12793
- highWatermark = event.totalTokens;
12794
- recordTokenDelta(deltaTokens, event.timestamp || lastActive, "updates");
12795
- }
13021
+ lastModel = eventModel;
13022
+ }
13023
+
13024
+ // Fallback only when this session never emitted turn_completed usage
13025
+ // (older logs / partial sessions). Context watermark is a lower-bound
13026
+ // estimate and must not run on top of turn usage.
13027
+ if (!sawTurnUsage) {
13028
+ let highWatermark = previousTotal;
13029
+ for (const event of updates.contextEvents) {
13030
+ lastEventId = event.eventId || lastEventId;
13031
+ lastEventTimestamp = event.timestamp || lastEventTimestamp;
13032
+ if (event.totalTokens <= highWatermark) continue;
13033
+ const deltaTokens = event.totalTokens - highWatermark;
13034
+ highWatermark = event.totalTokens;
13035
+ const hourStartStr =
13036
+ toUtcHalfHourStart(event.timestamp) ||
13037
+ toUtcHalfHourStart(lastActive) ||
13038
+ toUtcHalfHourStart(Date.now());
13039
+ if (!hourStartStr) continue;
13040
+ const delta = estimateGrokTokenDelta(deltaTokens, 0, { allowZeroConversationCount: true });
13041
+ pendingBucketDeltas.push({ model, hourStartStr, delta });
13042
+ tokenDeltaForSession += deltaTokens;
13043
+ finalTouchedHourStart = hourStartStr;
13044
+ source = "updates";
13045
+ }
12796
13046
 
12797
- const effectiveSignalTotal = grokEffectiveTotalFromSignals(safeSignals);
12798
- observedTotal = Math.max(observedTotal, effectiveSignalTotal);
12799
- if (effectiveSignalTotal > highWatermark) {
12800
- const deltaTokens = effectiveSignalTotal - highWatermark;
12801
- highWatermark = effectiveSignalTotal;
12802
- recordTokenDelta(deltaTokens, lastActive, "signals");
13047
+ const effectiveSignalTotal = grokEffectiveTotalFromSignals(safeSignals);
13048
+ if (effectiveSignalTotal > highWatermark) {
13049
+ const deltaTokens = effectiveSignalTotal - highWatermark;
13050
+ highWatermark = effectiveSignalTotal;
13051
+ const hourStartStr = toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
13052
+ if (hourStartStr) {
13053
+ const delta = estimateGrokTokenDelta(deltaTokens, 0, { allowZeroConversationCount: true });
13054
+ pendingBucketDeltas.push({ model, hourStartStr, delta });
13055
+ tokenDeltaForSession += deltaTokens;
13056
+ finalTouchedHourStart = hourStartStr;
13057
+ source = "signals";
13058
+ }
13059
+ }
13060
+ cumulativeTotal = Math.max(previousTotal, highWatermark);
12803
13061
  }
12804
13062
 
12805
- const finalTotal = Math.max(previousTotal, highWatermark, observedTotal);
13063
+ const finalTotal = Math.max(previousTotal, cumulativeTotal);
13064
+ // Sessions already observed under an older scanner must establish a watermark
13065
+ // without backfilling historical tokens as brand-new usage.
12806
13066
  const legacyBaselineOnly = previous.legacySeen && previousTotal === 0 && finalTotal > 0;
13067
+
12807
13068
  if (!legacyBaselineOnly) {
12808
- for (const pending of pendingTokenDeltas) {
12809
- const delta = estimateGrokTokenDelta(pending.deltaTokens, 0, { allowZeroConversationCount: true });
12810
- const bucket = getHourlyBucket(hourlyState, "grok", model, pending.hourStartStr);
12811
- addTotals(bucket.totals, delta);
12812
- touchedBuckets.add(bucketKey("grok", model, pending.hourStartStr));
13069
+ for (const pending of pendingBucketDeltas) {
13070
+ const bucket = getHourlyBucket(hourlyState, "grok", pending.model, pending.hourStartStr);
13071
+ addTotals(bucket.totals, pending.delta);
13072
+ touchedBuckets.add(bucketKey("grok", pending.model, pending.hourStartStr));
12813
13073
  eventsAggregated++;
12814
13074
  }
12815
- }
12816
13075
 
12817
- if (!legacyBaselineOnly && tokenDeltaForSession > 0 && finalTouchedHourStart) {
12818
- const deltaMessageCount =
12819
- messageCount > previousMessageCount ? messageCount - previousMessageCount : 1;
12820
- const bucket = getHourlyBucket(hourlyState, "grok", model, finalTouchedHourStart);
12821
- addTotals(bucket.totals, { conversation_count: deltaMessageCount });
12822
- touchedBuckets.add(bucketKey("grok", model, finalTouchedHourStart));
13076
+ // Message/conversation count for fallback-only sessions (turn path already
13077
+ // counts each turn_completed as one conversation).
13078
+ if (!sawTurnUsage && tokenDeltaForSession > 0 && finalTouchedHourStart) {
13079
+ const deltaMessageCount =
13080
+ messageCount > previousMessageCount ? messageCount - previousMessageCount : 1;
13081
+ const bucket = getHourlyBucket(hourlyState, "grok", lastModel || model, finalTouchedHourStart);
13082
+ addTotals(bucket.totals, { conversation_count: deltaMessageCount });
13083
+ touchedBuckets.add(bucketKey("grok", lastModel || model, finalTouchedHourStart));
13084
+ }
12823
13085
  }
12824
13086
 
12825
13087
  if (finalTotal > 0 && (tokenDeltaForSession > 0 || previousTotal > 0 || legacyBaselineOnly)) {
12826
13088
  sessionSnapshots[sessionId] = {
12827
13089
  totalTokens: finalTotal,
12828
13090
  messageCount: Math.max(previousMessageCount, messageCount),
12829
- model,
13091
+ model: lastModel || model,
12830
13092
  source: source || previous.source || null,
12831
13093
  lastEventId,
12832
13094
  lastEventTimestamp,
12833
13095
  updatedAt: new Date().toISOString(),
12834
13096
  };
13097
+ } else if (previous.legacySeen && finalTotal === 0) {
13098
+ // Keep the baseline marker across empty syncs so later growth is still
13099
+ // baselined once instead of backfilled as brand-new usage.
13100
+ sessionSnapshots[sessionId] = {
13101
+ totalTokens: 0,
13102
+ messageCount: Math.max(previousMessageCount, messageCount),
13103
+ model: lastModel || model,
13104
+ source: previous.source || null,
13105
+ lastEventId: lastEventId || previous.lastEventId || null,
13106
+ lastEventTimestamp: lastEventTimestamp || previous.lastEventTimestamp || null,
13107
+ updatedAt: new Date().toISOString(),
13108
+ legacySeen: true,
13109
+ };
12835
13110
  }
12836
13111
 
12837
13112
  if (onProgress) {
@@ -12839,19 +13114,45 @@ async function parseGrokBuildIncremental({
12839
13114
  }
12840
13115
  }
12841
13116
 
12842
- const bucketsQueued = queuePath
13117
+ let bucketsQueued = queuePath
12843
13118
  ? await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
12844
13119
  : 0;
13120
+
13121
+ // After a semantics migration, retract stale grok queue keys that the full
13122
+ // rescan no longer produces so dashboard "latest per key" no longer keeps
13123
+ // the old undercounted rows.
13124
+ if (needsTurnUsageMigration && queuePath) {
13125
+ const keepKeys = new Set();
13126
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
13127
+ if (!key.startsWith("grok|") || !bucket?.totals) continue;
13128
+ keepKeys.add(key);
13129
+ }
13130
+ const retracted = await retractStaleGrokQueueRows(queuePath, keepKeys);
13131
+ bucketsQueued += retracted;
13132
+ }
13133
+
12845
13134
  hourlyState.updatedAt = new Date().toISOString();
12846
13135
  cursors.hourly = hourlyState;
12847
13136
  sessionSnapshots = capGrokSessionSnapshots(sessionSnapshots);
12848
13137
 
13138
+ const migrations = grokState.migrations && typeof grokState.migrations === "object"
13139
+ ? { ...grokState.migrations }
13140
+ : {};
13141
+ if (needsTurnUsageMigration) {
13142
+ migrations.turnUsageV4 = {
13143
+ appliedAt: new Date().toISOString(),
13144
+ fromVersion: prevVersion,
13145
+ toVersion: GROK_CURSOR_VERSION,
13146
+ };
13147
+ }
13148
+
12849
13149
  cursors.grok = {
12850
13150
  ...grokState,
12851
13151
  version: GROK_CURSOR_VERSION,
12852
13152
  sessionSnapshots,
12853
13153
  seenSessions: Object.keys(sessionSnapshots),
12854
13154
  updateOffsets,
13155
+ migrations,
12855
13156
  updatedAt: new Date().toISOString()
12856
13157
  };
12857
13158
 
@@ -1 +0,0 @@
1
- import{r as n,z as a}from"./__vite-browser-external-DEhUaWQY.js";import{u as p}from"./DialogTitle-DLkuu1Iw.js";import{u as c}from"./useScrollLock-K0FPky1Y.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
@@ -1 +0,0 @@
1
- import{r as o}from"./__vite-browser-external-DEhUaWQY.js";import{h as r,i as t,p as s}from"./react-CllryaXi.js";function i(){!r.current&&t();const[e]=o.useState(s.current);return e}export{i as u};