tokentracker-cli 0.99.0 → 1.0.1

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 (108) hide show
  1. package/README.de.md +6 -5
  2. package/README.ja.md +6 -5
  3. package/README.ko.md +6 -5
  4. package/README.md +7 -5
  5. package/README.zh-CN.md +6 -5
  6. package/dashboard/dist/assets/{AchievementBadge-kRsjlB3p.js → AchievementBadge-pib0F8p8.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-C3xybY0k.js → AchievementsPage-BMnub6CL.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-y5mok2w0.js → AchievementsSection-DG_-yxAt.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-IFLXYuKp.js → BadgeDetailModal-Sk2dHIkJ.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-D6x0PDt2.js → BotAnimated-VwwvOniA.js} +1 -1
  11. package/dashboard/dist/assets/{Card-CiKwEyVu.js → Card-COfjRX96.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-BQFcAIOy.js → ClawdAnimated-C3sF5c29.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-BvpX8PM_.js → CommandPalette-DvJFPsR2.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-D5rNlwfO.js → CommunityStatsModal-DfYHIeKr.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-CR0Evu8-.js → ConfirmModal-DtoHm0RD.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-lyLUkbJW.js → Controls-DHHn10dy.js} +1 -1
  17. package/dashboard/dist/assets/{DashboardPage-X8CLr_au.js → DashboardPage-DJ_nDPjA.js} +1 -1
  18. package/dashboard/dist/assets/DevicePage-DlXWUpau.js +1 -0
  19. package/dashboard/dist/assets/{DialogClose-Bplj_718.js → DialogClose-BLegTe7F.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-Dl_wqXjc.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-Ckzf1tdi.js → DialogTitle-BZb-SEg0.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-C2Jqe95f.js → FadeIn-Dsk0V4DK.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-DgE7Cd5u.js → HeaderGithubStar-B20nLMWQ.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-CEmo0N-P.js → HoverTooltip-tGFmzZiJ.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-BwkpsLaF.js → IpCheckPage-BK7XI_aT.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-CA2klwbE.js → LandingPage-BYX6NXbl.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-B_PUenGZ.js → LeaderboardAvatar-CIDqKSNn.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-D6HGW3jX.js → LeaderboardPage-BxIpRUwn.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-Bfo1dSsS.js → LeaderboardProfileModal-DJlNN9YG.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-2OqwrqOs.js → LeaderboardProfilePage-D4HSgrjG.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-BHrkLXHZ.js → LimitsPage-BKTMBbKS.js} +1 -1
  32. package/dashboard/dist/assets/{LocalOnlyNotice-C45MmWYt.js → LocalOnlyNotice-DpZiPX4A.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-BikBibax.js → LoginCard-Cd03cg54.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-D0Aq85Zg.js → LoginModal-yPZi5JsE.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-B-kQaDQ_.js → LoginPage-BTL9oeVV.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-ZgPREiwD.js → PetPage-BWMSKd_3.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-eQgg3h1i.js → PopoverPopup-Dba9PYte.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-nDBPxn0N.js → ProviderIcon-DXI7Ztov.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-CjsOtiGr.js → ResetPasswordPage-wKDU07tl.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-6NcttkEH.js → SegmentedControl-DZh5rJHn.js} +1 -1
  41. package/dashboard/dist/assets/{Select-CzEhPICe.js → Select-CiJIsDf1.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-BwvzfXwj.js → SelectItemText-B8YkLJtE.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-C35wpxrT.js → ServiceStatusPage-CF_bMZMz.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-a30adkhf.js → SessionsPage-B0PnK1TY.js} +1 -1
  45. package/dashboard/dist/assets/{SettingsPage-C_7ZPBUF.js → SettingsPage-onYZIk-h.js} +2 -2
  46. package/dashboard/dist/assets/{SkillsPage-DsxkFnrV.js → SkillsPage-CSyoblke.js} +1 -1
  47. package/dashboard/dist/assets/{TokenGalaxy-D_bXRotA.js → TokenGalaxy-kK6wMGuy.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-CL7ma7ba.js → ToolbarRootContext-CnffyHPW.js} +1 -1
  49. package/dashboard/dist/assets/{TrendMonitor-QZBF2IEY.js → TrendMonitor-CpxDm5q_.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-CWlxR0H-.js → WidgetsPage-vBedlR_d.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-D8XQ72nF.js → WrappedPage-D25YzXGY.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-CTGqjrCe.js → __vite-browser-external-OxXsgcds.js} +3 -3
  53. package/dashboard/dist/assets/{agent-logos-BscA25fS.js → agent-logos-frktji23.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-DTxCEGbR.js → arrow-up-right-BtBcf39e.js} +1 -1
  55. package/dashboard/dist/assets/{check-CCEoh2wj.js → check-PVwhLtMf.js} +1 -1
  56. package/dashboard/dist/assets/{composite-B2s880f5.js → composite-BxlyIOoF.js} +1 -1
  57. package/dashboard/dist/assets/{copy-CJVQglwG.js → copy-s5UqKoXC.js} +1 -1
  58. package/dashboard/dist/assets/{download-Cn-OpYm6.js → download-Ns8TNfey.js} +1 -1
  59. package/dashboard/dist/assets/{external-link-BnYlUn56.js → external-link-zpvfw1sA.js} +1 -1
  60. package/dashboard/dist/assets/{flame-D4yFLV1L.js → flame-DhPzeD5Z.js} +1 -1
  61. package/dashboard/dist/assets/{format-tokens-CBUNbBvp.js → format-tokens-C4eAHkPw.js} +1 -1
  62. package/dashboard/dist/assets/{icons-CcgNgAvb.js → icons-CNeKwVFm.js} +1 -1
  63. package/dashboard/dist/assets/{index-CTjeDkbu.js → index-9qLVgAvh.js} +1 -1
  64. package/dashboard/dist/assets/{index-CkELwUyg.js → index-B5UHZH4-.js} +1 -1
  65. package/dashboard/dist/assets/{index-ZWeEKbo-.js → index-D8ciCYQM.js} +1 -1
  66. package/dashboard/dist/assets/{info-DoehRSQU.js → info-p2pKHzPt.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-D8xSsPqy.js → link-2-BQhZyxUQ.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-DMu0-X5r.js → loader-circle-BnFE5J64.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-DLyYsZvE.js → maximize-2-Zt1L2xuV.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-Dm5Xy2jA.js → modifiers.esm-Sbhv1CrS.js} +1 -1
  71. package/dashboard/dist/assets/{monitor-smartphone-5uZGQNGJ.js → monitor-smartphone-Pmy9ktPf.js} +1 -1
  72. package/dashboard/dist/assets/{plus-8fnU0dkt.js → plus-FPP4P4tW.js} +1 -1
  73. package/dashboard/dist/assets/{provider-display-B3ed-pSc.js → provider-display-zpzUqFZ8.js} +2 -2
  74. package/dashboard/dist/assets/{react-Gft01KYk.js → react-C_5Bvle-.js} +1 -1
  75. package/dashboard/dist/assets/{search-jqzJg2kG.js → search-7f0zYUoC.js} +1 -1
  76. package/dashboard/dist/assets/{skills-api-Bwn_xcHV.js → skills-api-CdEcHyY0.js} +1 -1
  77. package/dashboard/dist/assets/{sortable.esm-CYjzLCp7.js → sortable.esm-DwOWVPK8.js} +1 -1
  78. package/dashboard/dist/assets/{store-CgJHe9jU.js → store-D3n2aJ7w.js} +1 -1
  79. package/dashboard/dist/assets/{terminal-CZ5j8yEQ.js → terminal-C6zDcArO.js} +1 -1
  80. package/dashboard/dist/assets/{trash-DtMLjI_S.js → trash-C_bcv-F0.js} +1 -1
  81. package/dashboard/dist/assets/{use-community-stats-Px3dHGlR.js → use-community-stats-fCkrn5Kd.js} +1 -1
  82. package/dashboard/dist/assets/{use-latest-request-guard-B7Vw4Wsd.js → use-latest-request-guard-DD2Nt1qS.js} +1 -1
  83. package/dashboard/dist/assets/{use-limits-display-prefs-DWWrcMPi.js → use-limits-display-prefs-a9HtQcOE.js} +1 -1
  84. package/dashboard/dist/assets/{use-ordered-list-CevbB79U.js → use-ordered-list-B6-Nb6rL.js} +1 -1
  85. package/dashboard/dist/assets/use-reduced-motion-DyXrwS-h.js +1 -0
  86. package/dashboard/dist/assets/{use-session-efficiency-pref-DoIaI0vm.js → use-session-efficiency-pref-gD9i2h4k.js} +1 -1
  87. package/dashboard/dist/assets/{use-transform-qRwUetOk.js → use-transform-DP6mCHUn.js} +1 -1
  88. package/dashboard/dist/assets/{use-usage-limits-DlP7yg78.js → use-usage-limits-TdwK-gf2.js} +1 -1
  89. package/dashboard/dist/assets/{useCurrency-DRUe8YoY.js → useCurrency-Bbxwhe-A.js} +1 -1
  90. package/dashboard/dist/assets/{useTokenFormat-CmLbznUC.js → useTokenFormat-CzeTrne_.js} +1 -1
  91. package/dashboard/dist/assets/{zap-CS80c_uN.js → zap-CucEQWyU.js} +1 -1
  92. package/dashboard/dist/index.html +13 -11
  93. package/dashboard/dist/ip-check.html +1 -1
  94. package/dashboard/dist/leaderboard.html +1 -1
  95. package/dashboard/dist/llms.txt +3 -2
  96. package/dashboard/dist/share.html +1 -1
  97. package/package.json +2 -2
  98. package/src/commands/init.js +10 -0
  99. package/src/commands/status.js +16 -0
  100. package/src/commands/sync.js +43 -1
  101. package/src/lib/codex-rollout-parser.js +86 -8
  102. package/src/lib/codex-usage-record.js +195 -0
  103. package/src/lib/pricing/seed-snapshot.json +1 -1
  104. package/src/lib/rollout.js +261 -11
  105. package/src/lib/skills-manager.js +2 -2
  106. package/dashboard/dist/assets/DevicePage-RCIANm45.js +0 -1
  107. package/dashboard/dist/assets/DialogDescription-C2EzE7DU.js +0 -1
  108. package/dashboard/dist/assets/use-reduced-motion-CCuEY-79.js +0 -1
@@ -18,6 +18,7 @@ const {
18
18
  updateJsonLocked,
19
19
  } = require("../lib/fs");
20
20
  const { physicalJsonlRecords } = require("../lib/jsonl-lines");
21
+ const { countRecordOnlyFiles, formatRecordOnlyWarning } = require("../lib/codex-usage-record");
21
22
  const {
22
23
  listRolloutFiles,
23
24
  listRolloutFilesDeep,
@@ -91,6 +92,8 @@ const {
91
92
  piAgentDirCollidesWithOmp,
92
93
  resolvePrimeAgentSessionFiles,
93
94
  parsePrimeAgentIncremental,
95
+ resolveMinimaxCodeSessionFiles,
96
+ parseMinimaxCodeIncremental,
94
97
  resolveCraftSessionFiles,
95
98
  parseCraftIncremental,
96
99
  resolveReasonixTelemetryFiles,
@@ -312,6 +315,7 @@ const AUTO_SYNC_SOURCES = new Set([
312
315
  "kimi-code",
313
316
  "lmstudio",
314
317
  "mimo",
318
+ "minimax-code",
315
319
  "omo",
316
320
  "omp",
317
321
  "opencode",
@@ -2514,6 +2518,34 @@ async function cmdSync(argv, context = {}) {
2514
2518
  }
2515
2519
  }
2516
2520
 
2521
+ // ── MiniMax Code — passive ~/.minimax/v2/sessions/**/messages.jsonl usage reader ──
2522
+ let minimaxCodeResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2523
+ const minimaxCodeFiles = sourceAllowed("minimax-code")
2524
+ ? mergeBothFileSources({ resolveFiles: resolveMinimaxCodeSessionFiles, env: process.env })
2525
+ : [];
2526
+ if (minimaxCodeFiles.length > 0) {
2527
+ if (progress?.enabled) {
2528
+ progress.start(`Parsing MiniMax Code ${renderBar(0)} | buckets 0`);
2529
+ }
2530
+ try {
2531
+ minimaxCodeResult = await parseMinimaxCodeIncremental({
2532
+ sessionFiles: minimaxCodeFiles,
2533
+ cursors,
2534
+ queuePath,
2535
+ env: process.env,
2536
+ onProgress: (p) => {
2537
+ if (!progress?.enabled) return;
2538
+ const pct = p.total > 0 ? p.index / p.total : 1;
2539
+ progress.update(
2540
+ `Parsing MiniMax Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
2541
+ );
2542
+ },
2543
+ });
2544
+ } catch (err) {
2545
+ warnProviderParseFailure("MiniMax Code", err, opts);
2546
+ }
2547
+ }
2548
+
2517
2549
  // ── Craft Agents (passive ~/.craft-agent + workspaces session.jsonl reader) ──
2518
2550
  let craftResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2519
2551
  const craftFiles = sourceAllowed("craft")
@@ -3014,6 +3046,7 @@ async function cmdSync(argv, context = {}) {
3014
3046
  omoResult.recordsProcessed +
3015
3047
  piResult.recordsProcessed +
3016
3048
  primeAgentResult.recordsProcessed +
3049
+ minimaxCodeResult.recordsProcessed +
3017
3050
  craftResult.recordsProcessed +
3018
3051
  reasonixResult.recordsProcessed +
3019
3052
  grokResult.recordsProcessed +
@@ -3054,6 +3087,7 @@ async function cmdSync(argv, context = {}) {
3054
3087
  omoResult.bucketsQueued +
3055
3088
  piResult.bucketsQueued +
3056
3089
  primeAgentResult.bucketsQueued +
3090
+ minimaxCodeResult.bucketsQueued +
3057
3091
  craftResult.bucketsQueued +
3058
3092
  reasonixResult.bucketsQueued +
3059
3093
  grokResult.bucketsQueued +
@@ -3272,6 +3306,7 @@ async function cmdSync(argv, context = {}) {
3272
3306
  }
3273
3307
 
3274
3308
  if (!opts.auto) {
3309
+ const codexRecordOnlyWarning = formatRecordOnlyWarning(countRecordOnlyFiles(cursors));
3275
3310
  process.stdout.write(
3276
3311
  [
3277
3312
  "Sync finished:",
@@ -3283,6 +3318,7 @@ async function cmdSync(argv, context = {}) {
3283
3318
  runtime.deviceToken && pendingBytes > 0 && !opts.drain
3284
3319
  ? `- Remaining: ${formatBytes(pendingBytes)} pending (run sync again, or use --drain)`
3285
3320
  : null,
3321
+ codexRecordOnlyWarning ? `- Warning: ${codexRecordOnlyWarning}` : null,
3286
3322
  "",
3287
3323
  ]
3288
3324
  .filter(Boolean)
@@ -4779,8 +4815,10 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
4779
4815
  // The migration clears Codex buckets and reparses the discovered corpus from
4780
4816
  // byte zero. Persisted event keys belong to the cleared buckets, so retaining
4781
4817
  // them can suppress the rebuild when a moved session still has an old path
4782
- // cursor. Rebuild the hash inventory together with the buckets.
4818
+ // cursor. Rebuild the hash inventory together with the buckets. Counted
4819
+ // compaction ids (#652) belong to the cleared buckets the same way.
4783
4820
  cursors.codexHashes = [];
4821
+ cursors.codexCompactionResponseIds = [];
4784
4822
 
4785
4823
  const buckets = cursors.hourly?.buckets;
4786
4824
  const retractions = [];
@@ -5332,6 +5370,9 @@ async function repairCodexRescanInflation({
5332
5370
  buckets: tmpCursors.hourly.buckets || {},
5333
5371
  groupQueued: tmpCursors.hourly.groupQueued || {},
5334
5372
  codexHashes: Array.isArray(tmpCursors.codexHashes) ? tmpCursors.codexHashes : [],
5373
+ codexCompactionResponseIds: Array.isArray(tmpCursors.codexCompactionResponseIds)
5374
+ ? tmpCursors.codexCompactionResponseIds
5375
+ : [],
5335
5376
  files: tmpCursors.files || {},
5336
5377
  queueRows: tmpRaw.split("\n").filter((l) => l.trim()),
5337
5378
  projectHourly: tmpCursors.projectHourly || null,
@@ -5486,6 +5527,7 @@ async function repairCodexRescanInflation({
5486
5527
  cursors.files[fp] = v;
5487
5528
  }
5488
5529
  cursors.codexHashes = rebuilt.codexHashes;
5530
+ cursors.codexCompactionResponseIds = rebuilt.codexCompactionResponseIds;
5489
5531
 
5490
5532
  // 3. Project usage mirrors the main Codex repair: drop inflated Codex project
5491
5533
  // rows, append the rebuilt rows, and swap only Codex project buckets. Project
@@ -31,6 +31,15 @@ const {
31
31
  snapshotCodexModelAttributionState,
32
32
  } = require("./codex-model-attribution");
33
33
  const { readCodexServiceTier, isPriorityServiceTier } = require("./codex-service-tier");
34
+ const {
35
+ createUsageRecordState,
36
+ extractTokenUsageRecord,
37
+ noteLineAfterUsageRecord,
38
+ noteTokenCount,
39
+ noteUsageRecord,
40
+ snapshotUsageRecordState,
41
+ takeCompactionOnTokenCount,
42
+ } = require("./codex-usage-record");
34
43
 
35
44
  const DISCOVERY_FULL_AUDIT_INTERVAL_MS = 60 * 1000;
36
45
  const MAX_DISCOVERY_INVENTORIES = 32;
@@ -643,9 +652,37 @@ async function parseCodexRolloutFile(filePath, {
643
652
 
644
653
  let turnCount = 0;
645
654
 
646
- function recordModelUsage(delta, rawRequestUsage) {
655
+ // Compaction records (issue #652, rules in codex-usage-record.js). A resume
656
+ // state from before #652 with prevTotals has seen a token_count.
657
+ const usageRecordState = createUsageRecordState(
658
+ isResuming
659
+ ? resumeState.usageRecordState || (resumeState.prevTotals ? { sawTokenCount: true } : null)
660
+ : null,
661
+ );
662
+ // Compactions counted in this session, by response_id: the files merged for
663
+ // a session can repeat a record (an archived copy, a split fragment), and a
664
+ // resumed parse must not count one from an earlier chunk again.
665
+ const compactionResponseIds = new Set(
666
+ isResuming && Array.isArray(resumeState.compactionResponseIds)
667
+ ? resumeState.compactionResponseIds
668
+ : [],
669
+ );
670
+
671
+ // Attribution as of now. A compaction record is counted later than it is
672
+ // read, so it carries the attribution captured when it was read.
673
+ function currentUsageAttribution() {
674
+ return {
675
+ model: currentCodexModel(modelAttributionState) || "unknown",
676
+ selectedModel: modelAttributionState.selectedModel || null,
677
+ rerouted: Boolean(modelAttributionState.rerouted),
678
+ rerouteReason: modelAttributionState.rerouteReason || null,
679
+ serviceTier,
680
+ };
681
+ }
682
+
683
+ function recordModelUsage(delta, rawRequestUsage, attribution = currentUsageAttribution()) {
647
684
  if (!collectModelUsage || !delta || delta.total_tokens <= 0) return;
648
- const effectiveModel = currentCodexModel(modelAttributionState) || "unknown";
685
+ const effectiveModel = attribution.model;
649
686
  let row = byModel.get(effectiveModel);
650
687
  if (!row) {
651
688
  row = {
@@ -677,13 +714,13 @@ async function parseCodexRolloutFile(filePath, {
677
714
  }
678
715
  addInto(row, delta);
679
716
  row.usage_events += 1;
680
- if (modelAttributionState.selectedModel) {
681
- row.selected_models.add(modelAttributionState.selectedModel);
717
+ if (attribution.selectedModel) {
718
+ row.selected_models.add(attribution.selectedModel);
682
719
  }
683
- if (modelAttributionState.rerouted) {
720
+ if (attribution.rerouted) {
684
721
  row.rerouted_usage_events += 1;
685
- if (modelAttributionState.rerouteReason) {
686
- row.reroute_reasons.add(modelAttributionState.rerouteReason);
722
+ if (attribution.rerouteReason) {
723
+ row.reroute_reasons.add(attribution.rerouteReason);
687
724
  }
688
725
  }
689
726
  // OpenAI applies the long-context tier to the whole request when its raw
@@ -704,7 +741,7 @@ async function parseCodexRolloutFile(filePath, {
704
741
  // instead of replacing it. The two subsets alone cannot say how much usage
705
742
  // was on BOTH, so record the intersection explicitly — otherwise a long
706
743
  // Fast request prices at 3x Standard input where it should be 4x.
707
- if (isPriorityServiceTier(serviceTier)) {
744
+ if (isPriorityServiceTier(attribution.serviceTier)) {
708
745
  row.priority_usage_events += 1;
709
746
  row.priority_input_tokens += delta.input_tokens;
710
747
  row.priority_cached_input_tokens += delta.cached_input_tokens;
@@ -885,6 +922,14 @@ async function parseCodexRolloutFile(filePath, {
885
922
  pendingExecDetails = [];
886
923
  }
887
924
 
925
+ function countCompactionRecord(record) {
926
+ if (compactionResponseIds.has(record.responseId)) return;
927
+ compactionResponseIds.add(record.responseId);
928
+ if (!record.inRequestedRange) return;
929
+ recordModelUsage(record.delta, record.rawUsage, record.attribution);
930
+ attributeTurn(record.delta);
931
+ }
932
+
888
933
  function usageEventSignature(lastUsage, totalUsage) {
889
934
  return JSON.stringify({ lastUsage, totalUsage });
890
935
  }
@@ -952,6 +997,8 @@ async function parseCodexRolloutFile(filePath, {
952
997
  // turn_context.payload.model itself would make codex-model-attribution.js
953
998
  // one of two authorities on the same question.
954
999
  if (typeof onObject === "function") onObject(obj, attributedModel);
1000
+ const usageRecord = extractTokenUsageRecord(obj);
1001
+ if (!usageRecord) noteLineAfterUsageRecord(usageRecordState, obj?.type === "compacted");
955
1002
  const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
956
1003
  if (!ts) continue;
957
1004
  const inRequestedRange = isTimestampInRequestedDayRange(ts, { from, to, timeZoneContext });
@@ -1003,13 +1050,42 @@ async function parseCodexRolloutFile(filePath, {
1003
1050
  continue;
1004
1051
  }
1005
1052
 
1053
+ if (usageRecord) {
1054
+ const delta = normalizeUsage(usageRecord.usage);
1055
+ noteUsageRecord(
1056
+ usageRecordState,
1057
+ // Without a response_id a compaction cannot be deduplicated.
1058
+ delta.total_tokens > 0 && usageRecord.responseId
1059
+ ? {
1060
+ responseId: usageRecord.responseId,
1061
+ timestamp: usageRecord.timestamp,
1062
+ inRequestedRange,
1063
+ rawUsage: usageRecord.usage,
1064
+ delta,
1065
+ attribution: currentUsageAttribution(),
1066
+ }
1067
+ : null,
1068
+ );
1069
+ continue;
1070
+ }
1071
+
1006
1072
  const tokenCount = extractTokenCount(obj);
1007
1073
  if (tokenCount) {
1008
1074
  const info = tokenCount.info;
1009
1075
  const lastUsage = info?.last_token_usage;
1010
1076
  const totalUsage = info?.total_token_usage;
1077
+ let compaction = null;
1078
+ if (info && typeof info === "object") {
1079
+ noteTokenCount(usageRecordState);
1080
+ compaction = takeCompactionOnTokenCount(
1081
+ usageRecordState,
1082
+ usageDeltaState.lastTotal,
1083
+ totalUsage,
1084
+ );
1085
+ }
1011
1086
  const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
1012
1087
  const delta = rawDelta ? normalizeUsage(rawDelta) : null;
1088
+ if (compaction) countCompactionRecord(compaction);
1013
1089
  const isNewResumeEvent = noteTokenEvent(ts, lastUsage, totalUsage);
1014
1090
  if (inRequestedRange) {
1015
1091
  const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
@@ -1082,6 +1158,8 @@ async function parseCodexRolloutFile(filePath, {
1082
1158
  pendingExecDetails,
1083
1159
  lastTokenTimestamp,
1084
1160
  lastTimestampEventSignatures: Array.from(materializeLastTimestampSignatures()),
1161
+ usageRecordState: snapshotUsageRecordState(usageRecordState),
1162
+ compactionResponseIds: Array.from(compactionResponseIds),
1085
1163
  };
1086
1164
  result.endOffset = readProgress.endOffset;
1087
1165
  result.appendable = Boolean(contentHasher) && (
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+
3
+ // Codex rollouts written since CLI 0.151 carry a top-level
4
+ // {"type":"token_usage_record","payload":{usage,turn_token_usage,thread_token_usage,...}}
5
+ // next to the event_msg token_count that has always carried usage (issue
6
+ // #652). While both are written they describe the same model response, so
7
+ // token_count stays the only source of normal usage and a record is counted
8
+ // in exactly one case:
9
+ //
10
+ // Compaction. Codex bills the compaction model call in a record that is
11
+ // immediately followed by a top-level `compacted` line, and the next
12
+ // token_count repeats the previous cumulative total (its last_token_usage is
13
+ // the post-compaction context size, not a response). The cumulative delta is
14
+ // zero, so without the record the call is lost. Inside a file that has
15
+ // already shown a token_count, such a record is held as `compaction` and
16
+ // counted when the next token_count arrives with a total identical to the
17
+ // previous one. If that total advanced, the token_count already covers the
18
+ // call and the record is dropped. Each compaction is counted once per
19
+ // payload.response_id: a fork replays its parent's records and a copied
20
+ // rollout repeats them, both with the same id. A record without one is never
21
+ // counted.
22
+ //
23
+ // A file whose records are never matched by a token_count is not counted from
24
+ // its records. It is flagged `recordOnly` instead, and sync surfaces a
25
+ // warning. The proof is the same ordering Codex keeps for every response: its
26
+ // token_count is written after its record and before the next record or the
27
+ // turn end. So a record followed by another record or a turn end with no
28
+ // token_count in between flags the file; a sync that stops between a record
29
+ // and its token_count does not.
30
+ //
31
+ // payload.usage is the per-response delta; turn_token_usage and
32
+ // thread_token_usage are cumulative and are never read. All state below is
33
+ // persisted in the sync cursor and the session parser resume state, so the
34
+ // record, the `compacted` line and the token_count may land in different
35
+ // reads.
36
+
37
+ const { canonicalUsage, sameUsage } = require("./codex-token-usage");
38
+
39
+ function extractTokenUsageRecord(obj) {
40
+ if (obj?.type !== "token_usage_record") return null;
41
+ const usage = obj.payload?.usage;
42
+ if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
43
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : null;
44
+ if (!timestamp) return null;
45
+ // canonicalUsage folds cache_write_input_tokens into
46
+ // cache_creation_input_tokens exactly as the token_count path does.
47
+ const canonical = canonicalUsage(usage);
48
+ if (!canonical) return null;
49
+ const responseId = typeof obj.payload.response_id === "string" && obj.payload.response_id
50
+ ? obj.payload.response_id
51
+ : null;
52
+ return { usage: canonical, timestamp, responseId };
53
+ }
54
+
55
+ // Compaction calls already counted by sync, by response_id, kept in core
56
+ // cursor state as cursors.codexCompactionResponseIds (oldest first). The id
57
+ // is unique per API response and survives fork replays and file copies, so
58
+ // it is the only dedup key a compaction needs. Compactions are rare (19 on
59
+ // the maintainer's corpus), so the cap is far above any real history.
60
+ const MAX_COMPACTION_RESPONSE_IDS = 20000;
61
+
62
+ function createCompactionResponseIds(cursors) {
63
+ const stored = Array.isArray(cursors.codexCompactionResponseIds)
64
+ ? cursors.codexCompactionResponseIds
65
+ : [];
66
+ const ids = new Set(stored);
67
+ const added = [];
68
+ return {
69
+ has: (id) => ids.has(id),
70
+ add(id) {
71
+ if (ids.has(id)) return;
72
+ ids.add(id);
73
+ added.push(id);
74
+ },
75
+ persist() {
76
+ if (added.length === 0) return;
77
+ cursors.codexCompactionResponseIds = [...stored, ...added].slice(-MAX_COMPACTION_RESPONSE_IDS);
78
+ },
79
+ };
80
+ }
81
+
82
+ function isCodexTurnEndEvent(obj) {
83
+ if (obj?.type !== "event_msg") return false;
84
+ const type = obj.payload?.type;
85
+ return type === "task_complete" || type === "turn_aborted";
86
+ }
87
+
88
+ function isPlainObject(value) {
89
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
90
+ }
91
+
92
+ function createUsageRecordState(snapshot) {
93
+ const compaction = snapshot?.compaction;
94
+ return {
95
+ sawTokenCount: Boolean(snapshot?.sawTokenCount),
96
+ unmatchedRecord: Boolean(snapshot?.unmatchedRecord),
97
+ recordOnly: Boolean(snapshot?.recordOnly),
98
+ compaction: isPlainObject(compaction) && isPlainObject(compaction.candidate)
99
+ ? { candidate: compaction.candidate, compacted: Boolean(compaction.compacted) }
100
+ : null,
101
+ };
102
+ }
103
+
104
+ // Called for every token_count carrying an info object.
105
+ function noteTokenCount(state) {
106
+ state.sawTokenCount = true;
107
+ state.unmatchedRecord = false;
108
+ state.recordOnly = false;
109
+ }
110
+
111
+ // Called for every record. `candidate` is what the caller would count if the
112
+ // record turns out to be a compaction call (null when it cannot be counted).
113
+ function noteUsageRecord(state, candidate) {
114
+ if (state.sawTokenCount) {
115
+ state.compaction = candidate ? { candidate, compacted: false } : null;
116
+ return;
117
+ }
118
+ if (state.unmatchedRecord) state.recordOnly = true;
119
+ state.unmatchedRecord = true;
120
+ }
121
+
122
+ // Turn end: a record still waiting for its token_count will not get one.
123
+ function noteTurnEnd(state) {
124
+ if (state.sawTokenCount) return;
125
+ if (state.unmatchedRecord) state.recordOnly = true;
126
+ state.unmatchedRecord = false;
127
+ }
128
+
129
+ // True while the line right after a record decides whether it was a
130
+ // compaction call.
131
+ function awaitsCompactedLine(state) {
132
+ return Boolean(state?.compaction && !state.compaction.compacted);
133
+ }
134
+
135
+ // Called for each complete line after a record, other than a record. Only a
136
+ // `compacted` line directly after the record keeps it as a candidate.
137
+ function noteLineAfterUsageRecord(state, isCompactedLine) {
138
+ if (!awaitsCompactedLine(state)) return;
139
+ if (isCompactedLine) state.compaction.compacted = true;
140
+ else state.compaction = null;
141
+ }
142
+
143
+ // Called for every token_count carrying an info object, with the cumulative
144
+ // total that was active before it. Returns the compaction candidate to count,
145
+ // or null.
146
+ function takeCompactionOnTokenCount(state, previousTotal, totalUsage) {
147
+ const compaction = state.compaction;
148
+ state.compaction = null;
149
+ if (!compaction?.compacted) return null;
150
+ if (!canonicalUsage(previousTotal) || !sameUsage(previousTotal, totalUsage)) return null;
151
+ return compaction.candidate;
152
+ }
153
+
154
+ function isRecordOnly(state) {
155
+ return Boolean(state?.recordOnly && !state.sawTokenCount);
156
+ }
157
+
158
+ function snapshotUsageRecordState(state) {
159
+ if (!state || (!state.sawTokenCount && !state.unmatchedRecord && !state.recordOnly)) return null;
160
+ const out = { sawTokenCount: state.sawTokenCount };
161
+ if (state.unmatchedRecord) out.unmatchedRecord = true;
162
+ if (state.recordOnly) out.recordOnly = true;
163
+ if (state.compaction) out.compaction = state.compaction;
164
+ return out;
165
+ }
166
+
167
+ // Sync keeps the flagged rollouts in core cursor state:
168
+ // cursors.codexUsageRecordOnlyFiles = { [rolloutPath]: true }.
169
+ function countRecordOnlyFiles(cursors) {
170
+ const flagged = cursors?.codexUsageRecordOnlyFiles;
171
+ return isPlainObject(flagged) ? Object.keys(flagged).length : 0;
172
+ }
173
+
174
+ function formatRecordOnlyWarning(count) {
175
+ if (!count) return null;
176
+ return `${count} Codex session(s) report usage only via token_usage_record; not counted yet (see #652)`;
177
+ }
178
+
179
+ module.exports = {
180
+ MAX_COMPACTION_RESPONSE_IDS,
181
+ createCompactionResponseIds,
182
+ countRecordOnlyFiles,
183
+ formatRecordOnlyWarning,
184
+ awaitsCompactedLine,
185
+ createUsageRecordState,
186
+ extractTokenUsageRecord,
187
+ isCodexTurnEndEvent,
188
+ isRecordOnly,
189
+ noteLineAfterUsageRecord,
190
+ noteTokenCount,
191
+ noteTurnEnd,
192
+ noteUsageRecord,
193
+ snapshotUsageRecordState,
194
+ takeCompactionOnTokenCount,
195
+ };