tokentracker-cli 0.84.4 → 0.84.6

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 (95) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-C29PwsK_.js → AchievementBadge-CKIvD3lG.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-BlZODLp8.js → AchievementsPage-CZWCUiS5.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-Bc6NMnIa.js → AchievementsSection-BSk_fR8P.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-BUy4636q.js → BadgeDetailModal-CjHkcmuq.js} +1 -1
  5. package/dashboard/dist/assets/{Card-CqxuFMAN.js → Card-D6hdzwvI.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-BG3m8BgW.js → ClawdAnimated-Cl6gmF7u.js} +1 -1
  7. package/dashboard/dist/assets/{CommandPalette-Dcwtom2b.js → CommandPalette-B0rcodcF.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-dBjHrgjE.js → CommunityStatsModal-BtlTJbf4.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-DncQkptq.js → ConfirmModal-tJCWVZQH.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-CdF5WNHN.js → DashboardPage-Dtxq8dzI.js} +1 -1
  11. package/dashboard/dist/assets/{DevicePage-B-PO4s-r.js → DevicePage-9USuEsRu.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-DGtj17VY.js → DialogClose-DxHdqMcl.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-DgUSe6A1.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-DLkuu1Iw.js → DialogTitle-C1ZeQgf2.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-Dd5RBmXd.js → FadeIn-Cx4jLl9G.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-BBCOpHqO.js → HeaderGithubStar-BOyuGkoA.js} +1 -1
  17. package/dashboard/dist/assets/{HoverTooltip-BbigYxdC.js → HoverTooltip-BDhqMR3L.js} +1 -1
  18. package/dashboard/dist/assets/{IpCheckPage-BBpZYjSC.js → IpCheckPage-DTvA9Wmx.js} +1 -1
  19. package/dashboard/dist/assets/{LandingPage-ttXnt_s9.js → LandingPage-k1271lWV.js} +2 -2
  20. package/dashboard/dist/assets/{LeaderboardAvatar-DOl_cOO9.js → LeaderboardAvatar-3ibNg6If.js} +1 -1
  21. package/dashboard/dist/assets/{LeaderboardPage-BYIyoLkB.js → LeaderboardPage-Byww5UpT.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardProfileModal-BO2n4LSQ.js → LeaderboardProfileModal-DeGbJtvi.js} +3 -3
  23. package/dashboard/dist/assets/{LeaderboardProfilePage-CqwW5yMw.js → LeaderboardProfilePage-DEPzO6hW.js} +1 -1
  24. package/dashboard/dist/assets/{LimitsPage-B-w771Tl.js → LimitsPage-DPtqL0PS.js} +1 -1
  25. package/dashboard/dist/assets/{LocalOnlyNotice-Cqc_diwF.js → LocalOnlyNotice-C76LAT_3.js} +1 -1
  26. package/dashboard/dist/assets/{LoginCard-D6zc1JKY.js → LoginCard-DEyCJT67.js} +1 -1
  27. package/dashboard/dist/assets/{LoginModal-udN3TqoF.js → LoginModal-CueMQgAZ.js} +1 -1
  28. package/dashboard/dist/assets/{LoginPage-BucPr3_X.js → LoginPage-CwqBb5Fd.js} +1 -1
  29. package/dashboard/dist/assets/{PetPage-6_nmMXWp.js → PetPage--xQAjW4_.js} +1 -1
  30. package/dashboard/dist/assets/{PopoverPopup-DntczBmH.js → PopoverPopup-Dt9VY2VQ.js} +1 -1
  31. package/dashboard/dist/assets/{ProviderIcon-Z6qlwSj1.js → ProviderIcon-CHx4ohfx.js} +1 -1
  32. package/dashboard/dist/assets/{ResetPasswordPage-WqQwsQC1.js → ResetPasswordPage-DB6R7fjf.js} +1 -1
  33. package/dashboard/dist/assets/{SegmentedControl-D3NM3dOv.js → SegmentedControl-BINOpXIY.js} +1 -1
  34. package/dashboard/dist/assets/{Select-DCpwSFK7.js → Select-DcYXcsKv.js} +1 -1
  35. package/dashboard/dist/assets/{SelectItemText-DICaclqa.js → SelectItemText-BDv2Kb3x.js} +1 -1
  36. package/dashboard/dist/assets/{SessionsPage-COHoSjPP.js → SessionsPage-cL_w0ytB.js} +1 -1
  37. package/dashboard/dist/assets/{SettingsPage-nzbrBo_U.js → SettingsPage-CHGA0bQS.js} +1 -1
  38. package/dashboard/dist/assets/{SkillsPage-DE8mK1lS.js → SkillsPage-I29SqkXO.js} +1 -1
  39. package/dashboard/dist/assets/{TokenGalaxy-BSWGbByr.js → TokenGalaxy-BHq4YSv2.js} +1 -1
  40. package/dashboard/dist/assets/{ToolbarRootContext-CxxXTuTT.js → ToolbarRootContext-akXaaKb6.js} +1 -1
  41. package/dashboard/dist/assets/{TrendMonitor-DillSTMV.js → TrendMonitor-DPhFXzQc.js} +1 -1
  42. package/dashboard/dist/assets/{WidgetsPage-LqyxKYAt.js → WidgetsPage-Nf0UEz7s.js} +1 -1
  43. package/dashboard/dist/assets/{WrappedPage-DNDuRKjf.js → WrappedPage-D4zOSsqD.js} +1 -1
  44. package/dashboard/dist/assets/{__vite-browser-external-DEhUaWQY.js → __vite-browser-external-UqlSqI-1.js} +2 -2
  45. package/dashboard/dist/assets/{agent-logos-D34B25op.js → agent-logos-Bja3VCAa.js} +1 -1
  46. package/dashboard/dist/assets/{arrow-up-right-BYWbL4WV.js → arrow-up-right-CzcCNJJ2.js} +1 -1
  47. package/dashboard/dist/assets/{check-DkfaR6JX.js → check-DUHSZCYn.js} +1 -1
  48. package/dashboard/dist/assets/{chevron-right-D2cpt_J3.js → chevron-right-jLvhaNUy.js} +1 -1
  49. package/dashboard/dist/assets/{copy-Cv5Ix0Pn.js → copy-ChyH-ynx.js} +1 -1
  50. package/dashboard/dist/assets/{download-5nr9Tl80.js → download-CgwvXrUe.js} +1 -1
  51. package/dashboard/dist/assets/{flame-B6M-msYH.js → flame-ysf1F8uB.js} +1 -1
  52. package/dashboard/dist/assets/{format-tokens-DiRRwgY8.js → format-tokens-DAa2cubl.js} +1 -1
  53. package/dashboard/dist/assets/{icons-DQHoG48j.js → icons-DccoWQq9.js} +1 -1
  54. package/dashboard/dist/assets/{index-hSxFduER.js → index-CAycNPQf.js} +1 -1
  55. package/dashboard/dist/assets/{index-BoH_HaYH.js → index-CxExExXh.js} +1 -1
  56. package/dashboard/dist/assets/{index-vFee2-sp.js → index-kkQrDRat.js} +1 -1
  57. package/dashboard/dist/assets/{info-CM5MR8lk.js → info-DWQQIq9M.js} +1 -1
  58. package/dashboard/dist/assets/{limits-providers-BV4Y9nI4.js → limits-providers-B_18X9Rp.js} +1 -1
  59. package/dashboard/dist/assets/{link-2-Bd5jF09Z.js → link-2-CKeVVq4P.js} +1 -1
  60. package/dashboard/dist/assets/{loader-circle-B5oZJSYV.js → loader-circle-Dw0tu6IN.js} +1 -1
  61. package/dashboard/dist/assets/{maximize-2-CiYy9Kri.js → maximize-2-BMU0YzEF.js} +1 -1
  62. package/dashboard/dist/assets/{provider-display-lzLynfde.js → provider-display-Cf9I-VLS.js} +1 -1
  63. package/dashboard/dist/assets/{react-CllryaXi.js → react-BhbcISKy.js} +1 -1
  64. package/dashboard/dist/assets/{search-DDdsotHp.js → search-BoY8EW1z.js} +1 -1
  65. package/dashboard/dist/assets/{skills-api-487vZmwI.js → skills-api-KVH21GJw.js} +1 -1
  66. package/dashboard/dist/assets/{store-CYaTRd_k.js → store-BL7rSajG.js} +1 -1
  67. package/dashboard/dist/assets/{terminal-OyeOY0tb.js → terminal-CeMuITdm.js} +1 -1
  68. package/dashboard/dist/assets/{trash-2-COI9stXJ.js → trash-2-jVUCKqrR.js} +1 -1
  69. package/dashboard/dist/assets/{use-community-stats-DkzinngC.js → use-community-stats-uWK0fE6l.js} +1 -1
  70. package/dashboard/dist/assets/{use-limits-display-prefs-CiMETU5Y.js → use-limits-display-prefs-DWsF4ShM.js} +1 -1
  71. package/dashboard/dist/assets/{use-native-settings-qzpxm9Bh.js → use-native-settings-SEbqWDvc.js} +1 -1
  72. package/dashboard/dist/assets/{use-ordered-list-BI-Qh9xR.js → use-ordered-list-D4UhrPvw.js} +1 -1
  73. package/dashboard/dist/assets/use-reduced-motion-C3UrOh55.js +1 -0
  74. package/dashboard/dist/assets/{use-session-efficiency-pref-1WyhIjeG.js → use-session-efficiency-pref-DbysyUTJ.js} +1 -1
  75. package/dashboard/dist/assets/{use-transform-D5i_ArzG.js → use-transform-DaEssGaZ.js} +1 -1
  76. package/dashboard/dist/assets/{use-usage-limits-Drcnt7__.js → use-usage-limits-CmmQ34Ov.js} +1 -1
  77. package/dashboard/dist/assets/{useCurrency-KWJo1GIr.js → useCurrency-DvBEn4rx.js} +1 -1
  78. package/dashboard/dist/assets/{useScrollLock-K0FPky1Y.js → useScrollLock-QozsdFng.js} +1 -1
  79. package/dashboard/dist/assets/{useTokenFormat-CwG4ck8v.js → useTokenFormat-BTLrL7Mi.js} +1 -1
  80. package/dashboard/dist/assets/{zap-wZ6IQAyU.js → zap-Dp_aJ1q6.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/commands/status.js +17 -7
  87. package/src/commands/sync.js +333 -5
  88. package/src/lib/codex-rollout-parser.js +17 -39
  89. package/src/lib/codex-token-usage.js +195 -0
  90. package/src/lib/pricing/curated-overrides.json +536 -101
  91. package/src/lib/pricing/seed-snapshot.json +1 -1
  92. package/src/lib/rollout.js +732 -163
  93. package/src/lib/usage-limits.js +211 -41
  94. package/dashboard/dist/assets/DialogDescription-wStLk8Br.js +0 -1
  95. package/dashboard/dist/assets/use-reduced-motion-DrsPn1ux.js +0 -1
@@ -113,6 +113,11 @@ const {
113
113
  } = require("../lib/cursor-store");
114
114
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
115
115
  const { resolveRuntimeConfig } = require("../lib/runtime-config");
116
+ const { extractTokenCount } = require("../lib/codex-rollout-parser");
117
+ const {
118
+ consumeUsageDelta,
119
+ createUsageDeltaState,
120
+ } = require("../lib/codex-token-usage");
116
121
 
117
122
  const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
118
123
  const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
@@ -189,6 +194,14 @@ const CODEX_RESCAN_DEDUP_REPAIR_KEY = "codexRescanDedupRepair_2026_06";
189
194
  // offset reset for the cloud overwrite). Pre-gated: installs with no forked
190
195
  // rollout on disk carry no fork phantom and mark done without the rebuild.
191
196
  const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
197
+ // One-time repair for Codex rollouts that contain token counters from multiple
198
+ // interleaved SessionState instances. Older parsers subtracted each cumulative
199
+ // total from the most recently observed total, even when it belonged to another
200
+ // state, which could turn an ordinary turn into a multi-billion-token delta.
201
+ const CODEX_USAGE_LINEAGE_REPAIR_KEY = "codexUsageLineageRepair_2026_07";
202
+ // Keep the one escalated desktop refresh bounded; explicit full syncs can retry
203
+ // the same migration without this ceiling when the history needs a deeper scan.
204
+ const CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES = 1024 * 1024;
192
205
  const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
193
206
  const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
194
207
  const NOTIFY_LOCK_WAIT_MS = 130_000;
@@ -457,6 +470,28 @@ async function cmdSync(argv, context = {}) {
457
470
  if (autoSourceScope) return sources.includes(autoSourceScope);
458
471
  return true;
459
472
  };
473
+ // Desktop refreshes are deliberately lightweight, but that also means a
474
+ // migration wired only to full sync would never repair an existing native
475
+ // install. Escalate exactly the first eligible background run: fresh
476
+ // installs have no persisted Codex buckets to repair, while retryable
477
+ // failures remain reserved for an explicit full sync instead of turning
478
+ // every five-minute refresh into a deep historical scan.
479
+ const backgroundCodexUsageMigrationEligible = Boolean(
480
+ isBackgroundLightweightSync &&
481
+ sourceAllowed("codex") &&
482
+ !cursors.migrations?.[CODEX_USAGE_LINEAGE_REPAIR_KEY]
483
+ );
484
+ const hasPersistedCodexUsage = Object.keys(cursors.hourly?.buckets || {})
485
+ .some((key) => key.startsWith("codex|"));
486
+ const backgroundCodexUsageRepair =
487
+ backgroundCodexUsageMigrationEligible && hasPersistedCodexUsage;
488
+ if (backgroundCodexUsageMigrationEligible && !hasPersistedCodexUsage) {
489
+ // A fresh install has no old-parser buckets to repair. Mark it complete
490
+ // before the first bounded parse so the next refresh does not mistake
491
+ // newly parsed, already-correct data for legacy history.
492
+ (cursors.migrations ||= {})[CODEX_USAGE_LINEAGE_REPAIR_KEY] =
493
+ new Date().toISOString();
494
+ }
460
495
 
461
496
  const sources = [];
462
497
  if (sourceAllowed("codex")) {
@@ -467,13 +502,13 @@ async function cmdSync(argv, context = {}) {
467
502
  const codexPaths = resolveInstallPaths({ nativeValue: codexNativeValue, wslValue: wslCodexDir });
468
503
  if (codexPaths.native) {
469
504
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "sessions"), codexInventoryCache: true });
470
- if (!isBackgroundLightweightSync) {
505
+ if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
471
506
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "archived_sessions"), deep: true });
472
507
  }
473
508
  }
474
509
  if (codexPaths.wsl) {
475
510
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "sessions"), codexInventoryCache: true });
476
- if (!isBackgroundLightweightSync) {
511
+ if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
477
512
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "archived_sessions"), deep: true });
478
513
  }
479
514
  }
@@ -523,7 +558,7 @@ async function cmdSync(argv, context = {}) {
523
558
  projectQueueStatePath,
524
559
  rolloutFiles,
525
560
  });
526
- await repairCodexForkReplayInflation({
561
+ const codexForkRepairRan = await repairCodexForkReplayInflation({
527
562
  cursors,
528
563
  queuePath,
529
564
  queueStatePath,
@@ -532,6 +567,15 @@ async function cmdSync(argv, context = {}) {
532
567
  rolloutFiles,
533
568
  legacyRepairRan: codexRescanRepairRan,
534
569
  });
570
+ await repairCodexInterleavedUsageInflation({
571
+ cursors,
572
+ queuePath,
573
+ queueStatePath,
574
+ projectQueuePath,
575
+ projectQueueStatePath,
576
+ rolloutFiles,
577
+ legacyRepairRan: codexRescanRepairRan || codexForkRepairRan,
578
+ });
535
579
  await repairDroidDuplicateSessionInflation({ cursors, queuePath, queueStatePath });
536
580
  await repairMimoClaudeMislabel({
537
581
  cursors,
@@ -540,6 +584,33 @@ async function cmdSync(argv, context = {}) {
540
584
  projectQueuePath,
541
585
  projectQueueStatePath,
542
586
  });
587
+ } else if (backgroundCodexUsageRepair) {
588
+ await cursorStore.materializeAllCodexState(cursors);
589
+ try {
590
+ await repairCodexInterleavedUsageInflation({
591
+ cursors,
592
+ queuePath,
593
+ queueStatePath,
594
+ projectQueuePath,
595
+ projectQueueStatePath,
596
+ rolloutFiles,
597
+ maxLineageScanBytes: CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES,
598
+ });
599
+ } catch (err) {
600
+ warnProviderParseFailure("Codex usage lineage repair", err, opts);
601
+ } finally {
602
+ // Rebuild failures intentionally leave the migration key unset so a
603
+ // future full sync can retry. Record a retryable sentinel here to keep
604
+ // routine native background refreshes bounded after that first attempt.
605
+ const migrations = (cursors.migrations ||= {});
606
+ if (!migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY]) {
607
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
608
+ skipped: true,
609
+ reason: "background_repair_requires_full_sync",
610
+ at: new Date().toISOString(),
611
+ };
612
+ }
613
+ }
543
614
  }
544
615
 
545
616
  const codexColdSkipEnabled = opts.auto && sourceAllowed("codex");
@@ -1326,8 +1397,9 @@ async function cmdSync(argv, context = {}) {
1326
1397
  );
1327
1398
  }
1328
1399
 
1329
- // ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3
1330
- // AND live sessions under ~/.kiro/sessions/cli/{uuid}.json) ──
1400
+ // ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3,
1401
+ // legacy ~/.kiro/sessions/cli/{uuid}.json, and Kiro CLI 2.13+
1402
+ // ~/.kiro/sessions/{workspace}/sess_{uuid}/messages.jsonl) ──
1331
1403
  // Runs IN PARALLEL with the Kiro IDE branch above — NOT instead of it.
1332
1404
  // Both emit source='kiro' so totals merge transparently; cursor state
1333
1405
  // is isolated in cursors.kiroCli. Kiro CLI does not persist explicit
@@ -2339,6 +2411,7 @@ module.exports = {
2339
2411
  migrateRolloutCumulativeDeltaBuckets,
2340
2412
  repairCodexRescanInflation,
2341
2413
  repairCodexForkReplayInflation,
2414
+ repairCodexInterleavedUsageInflation,
2342
2415
  repairDroidDuplicateSessionInflation,
2343
2416
  repairMimoClaudeMislabel,
2344
2417
  reincludeClaudeMemObserverFiles,
@@ -2352,6 +2425,7 @@ module.exports = {
2352
2425
  ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
2353
2426
  CODEX_RESCAN_DEDUP_REPAIR_KEY,
2354
2427
  CODEX_FORK_REPLAY_REPAIR_KEY,
2428
+ CODEX_USAGE_LINEAGE_REPAIR_KEY,
2355
2429
  DROID_DUP_SESSION_REPAIR_KEY,
2356
2430
  CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
2357
2431
  GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY,
@@ -3828,6 +3902,260 @@ async function repairCodexForkReplayInflation({
3828
3902
  });
3829
3903
  }
3830
3904
 
3905
+ // Rebuild historical Codex usage only when a rollout proves that the old
3906
+ // single-baseline parser crossed cumulative SessionState lineages. The cheap
3907
+ // head gate keeps the one-time migration from fully reading installations that
3908
+ // predate Codex multi-agent support; the rebuild itself retains the existing
3909
+ // missing-history, project-queue, atomic-swap, and upload-reset guards.
3910
+ async function repairCodexInterleavedUsageInflation({
3911
+ cursors,
3912
+ queuePath,
3913
+ queueStatePath,
3914
+ projectQueuePath,
3915
+ projectQueueStatePath,
3916
+ rolloutFiles,
3917
+ // Either older guarded repair rebuilt every Codex file with the current
3918
+ // parser earlier in this sync, so a second full rebuild is unnecessary.
3919
+ legacyRepairRan = false,
3920
+ maxLineageScanBytes = Infinity,
3921
+ }) {
3922
+ if (!cursors || typeof cursors !== "object") return false;
3923
+ const migrations = (cursors.migrations ||= {});
3924
+ const prior = migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY];
3925
+ if (prior && !(typeof prior === "object" && prior.skipped)) return false;
3926
+
3927
+ if (legacyRepairRan) {
3928
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = new Date().toISOString();
3929
+ return false;
3930
+ }
3931
+
3932
+ const scan = await scanForInterleavedCodexUsage(rolloutFiles, {
3933
+ maxLineageScanBytes,
3934
+ });
3935
+ if (scan.indeterminate) {
3936
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
3937
+ skipped: true,
3938
+ reason: "usage_lineage_scan_indeterminate",
3939
+ at: new Date().toISOString(),
3940
+ };
3941
+ return false;
3942
+ }
3943
+
3944
+ if (!scan.affected) {
3945
+ if (!isCodexHistoryCovered(cursors, rolloutFiles)) {
3946
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
3947
+ skipped: true,
3948
+ reason: "codex_history_not_covered",
3949
+ at: new Date().toISOString(),
3950
+ };
3951
+ return false;
3952
+ }
3953
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = new Date().toISOString();
3954
+ return false;
3955
+ }
3956
+
3957
+ return repairCodexRescanInflation({
3958
+ cursors,
3959
+ queuePath,
3960
+ queueStatePath,
3961
+ projectQueuePath,
3962
+ projectQueueStatePath,
3963
+ rolloutFiles,
3964
+ migrationKey: CODEX_USAGE_LINEAGE_REPAIR_KEY,
3965
+ uploadNote: "reset_after_codex_usage_lineage_2026_07",
3966
+ });
3967
+ }
3968
+
3969
+ async function scanForInterleavedCodexUsage(
3970
+ rolloutFiles,
3971
+ { maxLineageScanBytes = Infinity } = {},
3972
+ ) {
3973
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
3974
+ const fp = typeof entry === "string" ? entry : entry?.path;
3975
+ const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
3976
+ if (!fp || src !== "codex") continue;
3977
+
3978
+ const head = await scanCodexMultiAgentHead(fp);
3979
+ if (head.indeterminate) return { affected: false, indeterminate: true };
3980
+ if (!head.candidate) continue;
3981
+
3982
+ const lineage = await scanCodexUsageLineages(fp, maxLineageScanBytes);
3983
+ if (lineage.indeterminate) return { affected: false, indeterminate: true };
3984
+ if (lineage.affected) return { affected: true, indeterminate: false };
3985
+ }
3986
+ return { affected: false, indeterminate: false };
3987
+ }
3988
+
3989
+ // New rollouts emit multi_agent_version in the initial turn_context. A session
3990
+ // created by an older Codex build can later be resumed under a multi-agent
3991
+ // build, though, so an inactive initial context also checks the final context
3992
+ // from a bounded tail read. The 1 MiB ceilings protect this one-time migration
3993
+ // from malformed metadata; an inconclusive bound is retryable, never a final
3994
+ // "not affected" verdict.
3995
+ async function scanCodexMultiAgentHead(filePath) {
3996
+ const MAX_BYTES = 1024 * 1024;
3997
+ let stream = null;
3998
+ let rl = null;
3999
+ let needsTail = false;
4000
+ try {
4001
+ stream = fssync.createReadStream(filePath, {
4002
+ encoding: "utf8",
4003
+ highWaterMark: 32 * 1024,
4004
+ });
4005
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4006
+ let bytesRead = 0;
4007
+ for await (const line of rl) {
4008
+ bytesRead += Buffer.byteLength(line, "utf8") + 1;
4009
+ if (line.includes('"turn_context"')) {
4010
+ let obj;
4011
+ try {
4012
+ obj = JSON.parse(line);
4013
+ } catch (_e) {
4014
+ return { candidate: false, indeterminate: true };
4015
+ }
4016
+ if (obj?.type === "turn_context") {
4017
+ if (isActiveMultiAgentVersion(obj?.payload?.multi_agent_version)) {
4018
+ return { candidate: true, indeterminate: false };
4019
+ }
4020
+ needsTail = true;
4021
+ }
4022
+ }
4023
+ if (line.includes('"token_count"')) {
4024
+ needsTail = true;
4025
+ break;
4026
+ }
4027
+ if (bytesRead >= MAX_BYTES) return { candidate: false, indeterminate: true };
4028
+ }
4029
+ } catch (_e) {
4030
+ return { candidate: false, indeterminate: true };
4031
+ } finally {
4032
+ if (rl) rl.close();
4033
+ if (stream) stream.destroy();
4034
+ }
4035
+ if (needsTail) return scanCodexMultiAgentTail(filePath);
4036
+ return { candidate: false, indeterminate: false };
4037
+ }
4038
+
4039
+ async function scanCodexMultiAgentTail(filePath) {
4040
+ const MAX_BYTES = 1024 * 1024;
4041
+ let handle = null;
4042
+ try {
4043
+ handle = await fs.open(filePath, "r");
4044
+ const stat = await handle.stat();
4045
+ const size = Number(stat.size || 0);
4046
+ const start = Math.max(0, size - MAX_BYTES);
4047
+ const length = Math.max(0, size - start);
4048
+ const buffer = Buffer.alloc(length);
4049
+ let bytesRead = 0;
4050
+ while (bytesRead < length) {
4051
+ const result = await handle.read(
4052
+ buffer,
4053
+ bytesRead,
4054
+ length - bytesRead,
4055
+ start + bytesRead,
4056
+ );
4057
+ if (result.bytesRead <= 0) break;
4058
+ bytesRead += result.bytesRead;
4059
+ }
4060
+ if (bytesRead !== length) return { candidate: false, indeterminate: true };
4061
+ const lines = buffer.toString("utf8").split("\n");
4062
+ if (start > 0) lines.shift(); // discard a line truncated by the tail boundary
4063
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
4064
+ const line = lines[index];
4065
+ if (!line.includes('"turn_context"')) continue;
4066
+ let obj;
4067
+ try {
4068
+ obj = JSON.parse(line);
4069
+ } catch (_e) {
4070
+ return { candidate: false, indeterminate: true };
4071
+ }
4072
+ if (obj?.type !== "turn_context") continue;
4073
+ return {
4074
+ candidate: isActiveMultiAgentVersion(obj?.payload?.multi_agent_version),
4075
+ indeterminate: false,
4076
+ };
4077
+ }
4078
+ return { candidate: false, indeterminate: start > 0 };
4079
+ } catch (_e) {
4080
+ return { candidate: false, indeterminate: true };
4081
+ } finally {
4082
+ if (handle) await handle.close().catch(() => {});
4083
+ }
4084
+ }
4085
+
4086
+ function isActiveMultiAgentVersion(value) {
4087
+ if (value === true) return true;
4088
+ if (typeof value !== "string") return false;
4089
+ const normalized = value.trim().toLowerCase();
4090
+ return Boolean(
4091
+ normalized &&
4092
+ normalized !== "off" &&
4093
+ normalized !== "none" &&
4094
+ normalized !== "disabled",
4095
+ );
4096
+ }
4097
+
4098
+ async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
4099
+ let stream = null;
4100
+ let rl = null;
4101
+ try {
4102
+ const state = createUsageDeltaState();
4103
+ const byteLimit = Number.isFinite(maxBytes) ? Math.max(0, maxBytes) : Infinity;
4104
+ let bytesRead = 0;
4105
+ stream = fssync.createReadStream(filePath, {
4106
+ encoding: "utf8",
4107
+ highWaterMark: 32 * 1024,
4108
+ });
4109
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4110
+ for await (const line of rl) {
4111
+ bytesRead += Buffer.byteLength(line, "utf8") + 1;
4112
+ if (bytesRead > byteLimit) return { affected: false, indeterminate: true };
4113
+ if (!line.includes('"token_count"')) continue;
4114
+ let obj;
4115
+ try {
4116
+ obj = JSON.parse(line);
4117
+ } catch (_e) {
4118
+ continue;
4119
+ }
4120
+ const token = extractTokenCount(obj);
4121
+ const info = token?.info;
4122
+ if (!info || typeof info !== "object") continue;
4123
+ consumeUsageDelta(state, info.last_token_usage, info.total_token_usage);
4124
+ if (state.sawInterleaved || state.sawDivergentCumulative) {
4125
+ return { affected: true, indeterminate: false };
4126
+ }
4127
+ }
4128
+ return { affected: false, indeterminate: false };
4129
+ } catch (_e) {
4130
+ return { affected: false, indeterminate: true };
4131
+ } finally {
4132
+ if (rl) rl.close();
4133
+ if (stream) stream.destroy();
4134
+ }
4135
+ }
4136
+
4137
+ function isCodexHistoryCovered(cursors, rolloutFiles) {
4138
+ const scannedPaths = new Set();
4139
+ const scannedSessionIds = new Set();
4140
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
4141
+ const fp = typeof entry === "string" ? entry : entry?.path;
4142
+ const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
4143
+ if (!fp || src !== "codex") continue;
4144
+ scannedPaths.add(fp);
4145
+ const id = codexSessionIdFromPath(fp);
4146
+ if (id) scannedSessionIds.add(id);
4147
+ }
4148
+ if (!cursors.files || typeof cursors.files !== "object") return true;
4149
+ for (const fp of Object.keys(cursors.files)) {
4150
+ if (!isCodexSessionCursorPath(fp)) continue;
4151
+ if (scannedPaths.has(fp)) continue;
4152
+ const id = codexSessionIdFromPath(fp);
4153
+ if (id && scannedSessionIds.has(id)) continue;
4154
+ return false;
4155
+ }
4156
+ return true;
4157
+ }
4158
+
3831
4159
  // Scans codex rollout heads for the fork marker. The child session_meta (with
3832
4160
  // forked_from_id) is the FIRST line of a forked rollout — observed at byte
3833
4161
  // offset ≤169 across every local sample — so a bounded head read is
@@ -11,6 +11,12 @@ const path = require("node:path");
11
11
  const readline = require("node:readline");
12
12
 
13
13
  const { listRolloutFiles } = require("./rollout");
14
+ const {
15
+ consumeUsageDelta,
16
+ createUsageDeltaState,
17
+ snapshotUsageBaselines,
18
+ totalsReset,
19
+ } = require("./codex-token-usage");
14
20
  const {
15
21
  emptyTotals,
16
22
  addInto,
@@ -434,42 +440,10 @@ function normalizeUsage(u) {
434
440
  return out;
435
441
  }
436
442
 
437
- function totalsReset(curr, prev) {
438
- const a = Number(curr?.total_tokens);
439
- const b = Number(prev?.total_tokens);
440
- if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
441
- return a < b;
442
- }
443
-
444
443
  function pickDelta(lastUsage, totalUsage, prevTotals) {
445
- const hasLast = lastUsage && typeof lastUsage === "object";
446
- const hasTotal = totalUsage && typeof totalUsage === "object";
447
- const hasPrev = prevTotals && typeof prevTotals === "object";
448
-
449
- if (hasTotal && hasPrev) {
450
- if (totalsReset(totalUsage, prevTotals)) {
451
- const resetUsage = hasLast ? lastUsage : totalUsage;
452
- return normalizeUsage(resetUsage);
453
- }
454
- const delta = {};
455
- for (const k of [
456
- "input_tokens",
457
- "cached_input_tokens",
458
- "cache_creation_input_tokens",
459
- "output_tokens",
460
- "reasoning_output_tokens",
461
- "total_tokens",
462
- ]) {
463
- const a = Number(totalUsage[k]);
464
- const b = Number(prevTotals[k]);
465
- if (Number.isFinite(a) && Number.isFinite(b)) delta[k] = Math.max(0, a - b);
466
- }
467
- return normalizeUsage(delta);
468
- }
469
-
470
- if (hasLast) return normalizeUsage(lastUsage);
471
- if (hasTotal) return normalizeUsage(totalUsage);
472
- return null;
444
+ const state = createUsageDeltaState({ lastTotal: prevTotals });
445
+ const delta = consumeUsageDelta(state, lastUsage, totalUsage);
446
+ return delta ? normalizeUsage(delta) : null;
473
447
  }
474
448
 
475
449
  // ---------------------------------------------------------------------------
@@ -596,7 +570,10 @@ async function parseCodexRolloutFile(filePath, {
596
570
  let provider = isResuming ? resumeState.provider || null : null;
597
571
  let cliVersion = isResuming ? resumeState.cliVersion || null : null;
598
572
 
599
- let prevTotals = isResuming ? resumeState.prevTotals || null : null;
573
+ const usageDeltaState = createUsageDeltaState({
574
+ lastTotal: isResuming ? resumeState.prevTotals || null : null,
575
+ baselines: isResuming ? resumeState.tokenUsageBaselines || null : null,
576
+ });
600
577
  let pendingToolNames = isResuming
601
578
  ? Array.from(resumeState.pendingToolNames || [])
602
579
  : [];
@@ -875,8 +852,8 @@ async function parseCodexRolloutFile(filePath, {
875
852
  const info = tokenCount.info;
876
853
  const lastUsage = info?.last_token_usage;
877
854
  const totalUsage = info?.total_token_usage;
878
- const delta = pickDelta(lastUsage, totalUsage, prevTotals);
879
- if (totalUsage && typeof totalUsage === "object") prevTotals = totalUsage;
855
+ const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
856
+ const delta = rawDelta ? normalizeUsage(rawDelta) : null;
880
857
  const isNewResumeEvent = noteTokenEvent(ts, lastUsage, totalUsage);
881
858
  if (inRequestedRange) {
882
859
  const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
@@ -927,7 +904,8 @@ async function parseCodexRolloutFile(filePath, {
927
904
  model,
928
905
  provider,
929
906
  cliVersion,
930
- prevTotals,
907
+ prevTotals: usageDeltaState.lastTotal,
908
+ tokenUsageBaselines: snapshotUsageBaselines(usageDeltaState),
931
909
  pendingToolNames,
932
910
  pendingSkills,
933
911
  pendingExecDetails,
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+
3
+ // A rollout can contain TokenCount events from more than one Codex SessionState
4
+ // (for example, a parent agent and an embedded reviewer). Each SessionState has
5
+ // its own cumulative total, but the event does not carry a stable stream id.
6
+ // Recover the stream from Codex's invariant: total - last = that stream's prior
7
+ // total. Keeping a small LRU of stream heads also makes repeated rate-limit
8
+ // snapshots idempotent when the streams are interleaved.
9
+
10
+ const USAGE_FIELDS = [
11
+ "input_tokens",
12
+ "cached_input_tokens",
13
+ "cache_creation_input_tokens",
14
+ "output_tokens",
15
+ "reasoning_output_tokens",
16
+ "total_tokens",
17
+ ];
18
+ const MAX_USAGE_BASELINES = 32;
19
+
20
+ function canonicalUsage(value) {
21
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
22
+ const out = {};
23
+ let sawNumber = false;
24
+ for (const field of USAGE_FIELDS) {
25
+ const raw = field === "cache_creation_input_tokens"
26
+ ? value.cache_creation_input_tokens ?? value.cache_write_input_tokens
27
+ : value[field];
28
+ const number = Number(raw);
29
+ if (Number.isFinite(number) && number >= 0) {
30
+ out[field] = Math.floor(number);
31
+ sawNumber = true;
32
+ } else {
33
+ out[field] = 0;
34
+ }
35
+ }
36
+ return sawNumber ? out : null;
37
+ }
38
+
39
+ function usageSignature(value) {
40
+ const usage = canonicalUsage(value);
41
+ return usage ? USAGE_FIELDS.map((field) => usage[field]).join(":") : null;
42
+ }
43
+
44
+ function sameUsage(left, right) {
45
+ const leftSignature = usageSignature(left);
46
+ return leftSignature !== null && leftSignature === usageSignature(right);
47
+ }
48
+
49
+ function subtractUsage(totalUsage, lastUsage) {
50
+ const total = canonicalUsage(totalUsage);
51
+ const last = canonicalUsage(lastUsage);
52
+ if (!total || !last) return null;
53
+ const previous = {};
54
+ for (const field of USAGE_FIELDS) {
55
+ if (total[field] < last[field]) return null;
56
+ previous[field] = total[field] - last[field];
57
+ }
58
+ return previous;
59
+ }
60
+
61
+ function diffUsage(totalUsage, previousUsage) {
62
+ const total = canonicalUsage(totalUsage);
63
+ const previous = canonicalUsage(previousUsage);
64
+ if (!total || !previous) return null;
65
+ const delta = {};
66
+ for (const field of USAGE_FIELDS) {
67
+ delta[field] = Math.max(0, total[field] - previous[field]);
68
+ }
69
+ return delta;
70
+ }
71
+
72
+ function totalsReset(totalUsage, previousUsage) {
73
+ const total = canonicalUsage(totalUsage);
74
+ const previous = canonicalUsage(previousUsage);
75
+ return Boolean(total && previous && total.total_tokens < previous.total_tokens);
76
+ }
77
+
78
+ function createUsageDeltaState({ lastTotal = null, baselines = null } = {}) {
79
+ const state = {
80
+ lastTotal: null,
81
+ baselines: [],
82
+ sawDivergentCumulative: false,
83
+ sawInterleaved: false,
84
+ };
85
+ for (const baseline of Array.isArray(baselines) ? baselines : []) {
86
+ touchBaseline(state, baseline);
87
+ }
88
+ if (canonicalUsage(lastTotal)) touchBaseline(state, lastTotal);
89
+ return state;
90
+ }
91
+
92
+ function snapshotUsageBaselines(state) {
93
+ return Array.isArray(state?.baselines)
94
+ ? state.baselines.map((usage) => ({ ...usage }))
95
+ : [];
96
+ }
97
+
98
+ function consumeUsageDelta(state, lastUsage, totalUsage) {
99
+ if (!state || typeof state !== "object") {
100
+ throw new TypeError("usage delta state is required");
101
+ }
102
+ if (!Array.isArray(state.baselines)) state.baselines = [];
103
+
104
+ const last = canonicalUsage(lastUsage);
105
+ const total = canonicalUsage(totalUsage);
106
+ if (!total) return last;
107
+
108
+ const activeSignature = usageSignature(state.lastTotal);
109
+ const duplicateIndex = findBaselineIndex(state, total);
110
+ if (duplicateIndex >= 0) {
111
+ if (
112
+ activeSignature !== null &&
113
+ usageSignature(state.baselines[duplicateIndex]) !== activeSignature
114
+ ) {
115
+ state.sawInterleaved = true;
116
+ }
117
+ touchBaselineAt(state, duplicateIndex, total);
118
+ return null;
119
+ }
120
+
121
+ if (last) {
122
+ const expectedPrevious = subtractUsage(total, last);
123
+ const lineageIndex = findBaselineIndex(state, expectedPrevious);
124
+ if (lineageIndex >= 0) {
125
+ if (
126
+ activeSignature !== null &&
127
+ usageSignature(state.baselines[lineageIndex]) !== activeSignature
128
+ ) {
129
+ state.sawInterleaved = true;
130
+ }
131
+ touchBaselineAt(state, lineageIndex, total);
132
+ return last;
133
+ }
134
+ if (expectedPrevious) {
135
+ if (canonicalUsage(state.lastTotal)) state.sawDivergentCumulative = true;
136
+ touchBaseline(state, total);
137
+ return last;
138
+ }
139
+ }
140
+
141
+ const active = canonicalUsage(state.lastTotal);
142
+ if (active && !totalsReset(total, active)) {
143
+ const delta = diffUsage(total, active);
144
+ if (!last || Number(delta?.total_tokens || 0) <= Number(last.total_tokens || 0)) {
145
+ const activeIndex = findBaselineIndex(state, active);
146
+ touchBaselineAt(state, activeIndex, total);
147
+ return delta;
148
+ }
149
+
150
+ // A cumulative jump larger than last usage cannot come from the normal
151
+ // append_last_usage path. Treat it as a new stream instead of charging the
152
+ // unrelated cumulative gap; a later event will establish its lineage.
153
+ state.sawDivergentCumulative = true;
154
+ }
155
+
156
+ touchBaseline(state, total);
157
+ return last || total;
158
+ }
159
+
160
+ function findBaselineIndex(state, usage) {
161
+ const signature = usageSignature(usage);
162
+ if (signature === null) return -1;
163
+ return state.baselines.findIndex((baseline) => usageSignature(baseline) === signature);
164
+ }
165
+
166
+ function touchBaselineAt(state, index, usage) {
167
+ const canonical = canonicalUsage(usage);
168
+ if (!canonical) return;
169
+ if (Number.isInteger(index) && index >= 0 && index < state.baselines.length) {
170
+ state.baselines.splice(index, 1);
171
+ } else {
172
+ const duplicateIndex = findBaselineIndex(state, canonical);
173
+ if (duplicateIndex >= 0) state.baselines.splice(duplicateIndex, 1);
174
+ }
175
+ state.baselines.push(canonical);
176
+ while (state.baselines.length > MAX_USAGE_BASELINES) state.baselines.shift();
177
+ state.lastTotal = canonical;
178
+ }
179
+
180
+ function touchBaseline(state, usage) {
181
+ touchBaselineAt(state, findBaselineIndex(state, usage), usage);
182
+ }
183
+
184
+ module.exports = {
185
+ MAX_USAGE_BASELINES,
186
+ canonicalUsage,
187
+ consumeUsageDelta,
188
+ createUsageDeltaState,
189
+ diffUsage,
190
+ sameUsage,
191
+ snapshotUsageBaselines,
192
+ subtractUsage,
193
+ totalsReset,
194
+ usageSignature,
195
+ };