tokentracker-cli 0.81.2 → 0.82.0

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 (137) hide show
  1. package/dashboard/dist/assets/AchievementBadge-D6iPEEbz.js +1 -0
  2. package/dashboard/dist/assets/AchievementsPage-Jry355YE.js +2 -0
  3. package/dashboard/dist/assets/AchievementsSection-ah-kG00E.js +1 -0
  4. package/dashboard/dist/assets/BadgeDetailModal-FyvUrVvF.js +1 -0
  5. package/dashboard/dist/assets/Card-C3nLScju.js +1 -0
  6. package/dashboard/dist/assets/ClawdAnimated-CLHhE26z.js +1 -0
  7. package/dashboard/dist/assets/CommunityStatsModal-DA3PHLV5.js +1 -0
  8. package/dashboard/dist/assets/ConfirmModal-CTDarLrD.js +1 -0
  9. package/dashboard/dist/assets/DashboardPage-DZIiWc3Y.js +1 -0
  10. package/dashboard/dist/assets/DevicePage-CyXjUO8W.js +1 -0
  11. package/dashboard/dist/assets/DialogDescription-ahxSNUcB.js +1 -0
  12. package/dashboard/dist/assets/DialogTitle-z86PHHyk.js +1 -0
  13. package/dashboard/dist/assets/FadeIn-BDWtfIsO.js +1 -0
  14. package/dashboard/dist/assets/HeaderGithubStar-DE1USQbb.js +1 -0
  15. package/dashboard/dist/assets/{IpCheckPage-CMf9CHFr.js → IpCheckPage-B2SkgT0P.js} +3 -3
  16. package/dashboard/dist/assets/{LandingPage-BT61_TqT.js → LandingPage-PU5gxsfY.js} +165 -165
  17. package/dashboard/dist/assets/LeaderboardAvatar-CaOV_hFE.js +1 -0
  18. package/dashboard/dist/assets/LeaderboardPage-B0JtEyRP.js +2 -0
  19. package/dashboard/dist/assets/{LeaderboardProfileModal-C_kxqOMn.js → LeaderboardProfileModal-DtNEbVH8.js} +4 -4
  20. package/dashboard/dist/assets/{LeaderboardProfilePage-zgNsE0EQ.js → LeaderboardProfilePage-DRklax3-.js} +3 -3
  21. package/dashboard/dist/assets/LimitsPage-Dg0cJa40.js +4 -0
  22. package/dashboard/dist/assets/LocalOnlyNotice-si9PMKya.js +1 -0
  23. package/dashboard/dist/assets/LoginPage-DgLwhXKo.js +1 -0
  24. package/dashboard/dist/assets/PetPage-DEZbjk99.js +1 -0
  25. package/dashboard/dist/assets/PopoverPopup-DX7v1XfE.js +1 -0
  26. package/dashboard/dist/assets/ResetPasswordPage-C6NvRO-Q.js +1 -0
  27. package/dashboard/dist/assets/Select-CH2xVU8X.js +1 -0
  28. package/dashboard/dist/assets/SelectItemText-CCW6JeeN.js +1 -0
  29. package/dashboard/dist/assets/SettingsPage-Cm3uT11D.js +1 -0
  30. package/dashboard/dist/assets/SkillsPage-CljRL6yF.js +1 -0
  31. package/dashboard/dist/assets/TrendMonitor-DM1_WNfw.js +19 -0
  32. package/dashboard/dist/assets/WidgetsPage-mJ1j6hly.js +1 -0
  33. package/dashboard/dist/assets/WrappedPage-Dz71ln4t.js +1 -0
  34. package/dashboard/dist/assets/__vite-browser-external-C7JwykID.css +1 -0
  35. package/dashboard/dist/assets/__vite-browser-external-Eoyj4eG-.js +1341 -0
  36. package/dashboard/dist/assets/agent-logos-D3K_ZRUj.js +1 -0
  37. package/dashboard/dist/assets/arrow-up-right-CT4-gmOS.js +1 -0
  38. package/dashboard/dist/assets/check-B87_qvAg.js +1 -0
  39. package/dashboard/dist/assets/copy-Dtr68epm.js +1 -0
  40. package/dashboard/dist/assets/download-DaWsA_Gj.js +1 -0
  41. package/dashboard/dist/assets/flame-vLBOq0X0.js +1 -0
  42. package/dashboard/dist/assets/format-tokens-C6Pidz3s.js +1 -0
  43. package/dashboard/dist/assets/icons-BaaYJxUN.js +1 -0
  44. package/dashboard/dist/assets/index-lKnLA6Pd.js +1 -0
  45. package/dashboard/dist/assets/info-B46Qt16h.js +1 -0
  46. package/dashboard/dist/assets/limits-providers-13af4PEg.js +1 -0
  47. package/dashboard/dist/assets/link-2-DeN6MJpT.js +1 -0
  48. package/dashboard/dist/assets/loader-circle-ByEuaaSw.js +1 -0
  49. package/dashboard/dist/assets/provider-display-CKbOKeV4.js +42 -0
  50. package/dashboard/dist/assets/terminal-DyicX7lG.js +1 -0
  51. package/dashboard/dist/assets/trash-2-CVCC3DtQ.js +1 -0
  52. package/dashboard/dist/assets/use-community-stats-CExk3D6G.js +1 -0
  53. package/dashboard/dist/assets/use-limits-display-prefs-CRl4uU95.js +1 -0
  54. package/dashboard/dist/assets/use-native-settings-Bi6E9dsE.js +1 -0
  55. package/dashboard/dist/assets/{use-ordered-list-DPbiKnPc.js → use-ordered-list-Dk0p2otw.js} +2 -2
  56. package/dashboard/dist/assets/use-session-efficiency-pref-YAG-OYIh.js +1 -0
  57. package/dashboard/dist/assets/use-transform-lflyghWN.js +1 -0
  58. package/dashboard/dist/assets/use-usage-limits-DJXvJ25Q.js +1 -0
  59. package/dashboard/dist/assets/useCurrency-BtHtcsO5.js +1 -0
  60. package/dashboard/dist/assets/useScrollLock-C2C-Yazq.js +1 -0
  61. package/dashboard/dist/assets/useTokenFormat-BdD057ih.js +1 -0
  62. package/dashboard/dist/assets/zap-DWWma29E.js +1 -0
  63. package/dashboard/dist/index.html +2 -2
  64. package/dashboard/dist/share.html +2 -2
  65. package/package.json +1 -1
  66. package/src/cli.js +6 -0
  67. package/src/commands/serve.js +8 -6
  68. package/src/commands/sessions.js +29 -0
  69. package/src/commands/status.js +2 -1
  70. package/src/commands/sync.js +164 -36
  71. package/src/lib/codex-context-breakdown.js +330 -13
  72. package/src/lib/codex-rollout-parser.js +323 -42
  73. package/src/lib/context-health.js +124 -0
  74. package/src/lib/cursor-store.js +1289 -0
  75. package/src/lib/diagnostics.js +8 -5
  76. package/src/lib/fs.js +254 -16
  77. package/src/lib/git-outcomes.js +147 -0
  78. package/src/lib/local-api.js +151 -20
  79. package/src/lib/outcomes-engine.js +23 -0
  80. package/src/lib/pricing/index.js +8 -0
  81. package/src/lib/pricing/seed-snapshot.json +1 -1
  82. package/src/lib/rollout.js +101 -9
  83. package/src/lib/session-analytics.js +612 -0
  84. package/src/lib/usage-limits.js +21 -0
  85. package/dashboard/dist/assets/AchievementBadge-DXWHZrAL.js +0 -1
  86. package/dashboard/dist/assets/AchievementsPage-BhrQXniL.js +0 -2
  87. package/dashboard/dist/assets/AchievementsSection-gzP3m1Ml.js +0 -1
  88. package/dashboard/dist/assets/BadgeDetailModal-DGKPuvDU.js +0 -1
  89. package/dashboard/dist/assets/Card-BRtYQ-Ot.js +0 -1
  90. package/dashboard/dist/assets/ClawdAnimated-UsNG-qDv.js +0 -1
  91. package/dashboard/dist/assets/CommunityStatsModal-CJQw-P4t.js +0 -1
  92. package/dashboard/dist/assets/ConfirmModal-ByY4GDsy.js +0 -1
  93. package/dashboard/dist/assets/DashboardPage-CuLVnW98.js +0 -19
  94. package/dashboard/dist/assets/DevicePage-D67CxpmR.js +0 -1
  95. package/dashboard/dist/assets/DialogClose-Ci8hkTw5.js +0 -1
  96. package/dashboard/dist/assets/DialogDescription-C9vXUZU0.js +0 -1
  97. package/dashboard/dist/assets/DialogTitle-sLJxKWjv.js +0 -1
  98. package/dashboard/dist/assets/FadeIn-xVds37w6.js +0 -1
  99. package/dashboard/dist/assets/HeaderGithubStar-kK3omgmB.js +0 -1
  100. package/dashboard/dist/assets/LeaderboardAvatar-DyKz6NdT.js +0 -1
  101. package/dashboard/dist/assets/LeaderboardPage-DJ4zihJJ.js +0 -2
  102. package/dashboard/dist/assets/LimitsPage-BiQpqxj8.js +0 -4
  103. package/dashboard/dist/assets/LocalOnlyNotice-DTrrUVbZ.js +0 -1
  104. package/dashboard/dist/assets/LoginPage-BOM9Q2sI.js +0 -1
  105. package/dashboard/dist/assets/PetPage-BURk7RG-.js +0 -1
  106. package/dashboard/dist/assets/PopoverPopup-BV_aoNBd.js +0 -1
  107. package/dashboard/dist/assets/ResetPasswordPage-SBL2LvXC.js +0 -1
  108. package/dashboard/dist/assets/Select-BsRdHde3.js +0 -1
  109. package/dashboard/dist/assets/SelectItemText-DgkrZxIq.js +0 -1
  110. package/dashboard/dist/assets/SettingsPage-DqxVsfnB.js +0 -1
  111. package/dashboard/dist/assets/SkillsPage-B0VfWkOh.js +0 -1
  112. package/dashboard/dist/assets/WidgetsPage-4QQ8wI_g.js +0 -1
  113. package/dashboard/dist/assets/WrappedPage-BK53khVY.js +0 -1
  114. package/dashboard/dist/assets/agent-logos-DXpW_Wmz.js +0 -1
  115. package/dashboard/dist/assets/arrow-up-right-DCc--V5G.js +0 -1
  116. package/dashboard/dist/assets/check-DqxuosV2.js +0 -1
  117. package/dashboard/dist/assets/copy-qChAT7MY.js +0 -1
  118. package/dashboard/dist/assets/download-W9R3sBph.js +0 -1
  119. package/dashboard/dist/assets/flame-0eyhMnfU.js +0 -1
  120. package/dashboard/dist/assets/format-tokens-CkfBUcoa.js +0 -1
  121. package/dashboard/dist/assets/info-DY1O85-U.js +0 -1
  122. package/dashboard/dist/assets/link-2-B_Z66hFT.js +0 -1
  123. package/dashboard/dist/assets/loader-circle-xH40lon2.js +0 -1
  124. package/dashboard/dist/assets/main-CeaB5N36.js +0 -1259
  125. package/dashboard/dist/assets/main-DAc77vwF.css +0 -1
  126. package/dashboard/dist/assets/provider-display-C_XKZQcF.js +0 -42
  127. package/dashboard/dist/assets/trash-2-DVhNJ74r.js +0 -1
  128. package/dashboard/dist/assets/use-community-stats-CO-nr2Vf.js +0 -1
  129. package/dashboard/dist/assets/use-limits-display-prefs-RRqmyoxt.js +0 -1
  130. package/dashboard/dist/assets/use-native-settings-79piPeET.js +0 -1
  131. package/dashboard/dist/assets/use-quality-per-dollar-pref-DyF49QRF.js +0 -1
  132. package/dashboard/dist/assets/use-transform-BvXyGAtb.js +0 -1
  133. package/dashboard/dist/assets/use-usage-limits-BiZ_I-KE.js +0 -1
  134. package/dashboard/dist/assets/useCurrency-D-C1e7eE.js +0 -1
  135. package/dashboard/dist/assets/useScrollLock-BfU2EMvG.js +0 -1
  136. package/dashboard/dist/assets/useTokenFormat-Cm4EvrQp.js +0 -1
  137. package/dashboard/dist/assets/zap-DlAOTwFs.js +0 -1
@@ -5,6 +5,7 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ const crypto = require("node:crypto");
8
9
  const fs = require("node:fs");
9
10
  const path = require("node:path");
10
11
  const readline = require("node:readline");
@@ -23,6 +24,7 @@ const DISCOVERY_FULL_AUDIT_INTERVAL_MS = 60 * 1000;
23
24
  const MAX_DISCOVERY_INVENTORIES = 32;
24
25
  const DISCOVERY_INVENTORIES = new Map();
25
26
  const ZONED_DAY_FORMATTERS = new Map();
27
+ const CODEX_RESUME_INVALIDATED = "TOKENTRACKER_CODEX_RESUME_INVALIDATED";
26
28
 
27
29
  // ---------------------------------------------------------------------------
28
30
  // Timezone helpers
@@ -555,20 +557,62 @@ async function parseCodexRolloutFile(filePath, {
555
557
  timeZoneContext = null,
556
558
  diagnostics = null,
557
559
  seenTokenEvents = null,
560
+ startOffset = 0,
561
+ endOffset = null,
562
+ resumeState = null,
563
+ captureResumeState = false,
564
+ captureContentHash = false,
565
+ contentHashState = null,
566
+ sourceHandle = null,
558
567
  } = {}) {
559
568
  const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
560
569
  const primaryFilePath = filePaths[0] || String(filePath || "");
570
+ const isResuming = Boolean(resumeState && filePaths.length === 1);
571
+ const initialOffset = Math.max(0, Number(startOffset) || 0);
572
+ const readProgress = {
573
+ endOffset: initialOffset,
574
+ lastByte: initialOffset > 0 ? 0x0a : null,
575
+ };
576
+ let contentHasher = null;
577
+ if (captureResumeState && captureContentHash && filePaths.length === 1) {
578
+ if (contentHashState && typeof contentHashState.copy === "function") {
579
+ try {
580
+ contentHasher = contentHashState.copy();
581
+ } catch {
582
+ contentHasher = null;
583
+ }
584
+ }
585
+ if (initialOffset > 0 && !contentHasher) {
586
+ const error = new Error("Codex rollout append is missing its prefix hash state");
587
+ error.code = CODEX_RESUME_INVALIDATED;
588
+ throw error;
589
+ }
590
+ if (!contentHasher) contentHasher = crypto.createHash("sha256");
591
+ }
561
592
 
562
- let sessionId = null;
563
- let cwd = null;
564
- let model = null;
565
- let provider = null;
566
- let cliVersion = null;
567
-
568
- let prevTotals = null;
569
- let pendingCalls = []; // response_item function_call payloads since last token_count
570
- let pendingSkills = [];
571
- let pendingExecEnds = []; // exec_command_end payloads since last token_count
593
+ let sessionId = isResuming ? resumeState.sessionId || null : null;
594
+ let cwd = isResuming ? resumeState.cwd || null : null;
595
+ let model = isResuming ? resumeState.model || null : null;
596
+ let provider = isResuming ? resumeState.provider || null : null;
597
+ let cliVersion = isResuming ? resumeState.cliVersion || null : null;
598
+
599
+ let prevTotals = isResuming ? resumeState.prevTotals || null : null;
600
+ let pendingToolNames = isResuming
601
+ ? Array.from(resumeState.pendingToolNames || [])
602
+ : [];
603
+ let pendingSkills = isResuming ? Array.from(resumeState.pendingSkills || []) : [];
604
+ let pendingExecDetails = isResuming
605
+ ? Array.from(resumeState.pendingExecDetails || [])
606
+ : [];
607
+ let lastTokenTimestamp = isResuming
608
+ ? String(resumeState.lastTokenTimestamp || "") || null
609
+ : null;
610
+ let lastTimestampEventSignatures = new Set(
611
+ isResuming ? resumeState.lastTimestampEventSignatures || [] : [],
612
+ );
613
+ let lastTimestampLastUsage = null;
614
+ let lastTimestampTotalUsage = null;
615
+ let hasUnmaterializedLastTimestampUsage = false;
572
616
 
573
617
  const totals = emptyTotals();
574
618
  const byTool = new Map(); // tool_name -> {name,calls,totals}
@@ -639,9 +683,7 @@ async function parseCodexRolloutFile(filePath, {
639
683
  if (details.failed) row.failures += 1;
640
684
  }
641
685
 
642
- function absorbExecEnd(p) {
643
- const details = getExecKeys(p);
644
- if (!details) return;
686
+ function absorbExecEnd(details) {
645
687
  absorbExecStats(byExecKind, details.kind, details);
646
688
  absorbExecStats(byExecExit, details.exitKey, details);
647
689
  absorbExecStats(byExecExecutable, details.executable, details);
@@ -652,18 +694,15 @@ async function parseCodexRolloutFile(filePath, {
652
694
 
653
695
  function attributeTurn(delta) {
654
696
  if (!delta || delta.total_tokens <= 0) {
655
- for (const p of pendingExecEnds) absorbExecEnd(p);
656
- pendingCalls = [];
697
+ for (const details of pendingExecDetails) absorbExecEnd(details);
698
+ pendingToolNames = [];
657
699
  pendingSkills = [];
658
- pendingExecEnds = [];
700
+ pendingExecDetails = [];
659
701
  return;
660
702
  }
661
703
  turnCount += 1;
662
704
 
663
- const toolNames = pendingCalls
664
- .map((c) => normalizeToolName(c))
665
- .filter(Boolean);
666
- const unique = [...new Set(toolNames)];
705
+ const unique = [...new Set(pendingToolNames.filter(Boolean))];
667
706
  const tools = unique.length > 0 ? unique : ["text_response"];
668
707
  const share = 1 / tools.length;
669
708
 
@@ -710,11 +749,9 @@ async function parseCodexRolloutFile(filePath, {
710
749
  total_tokens: delta.total_tokens * execToolShare,
711
750
  } : null;
712
751
 
713
- if (execDelta && pendingExecEnds.length > 0) {
714
- const perExecShare = 1 / pendingExecEnds.length;
715
- for (const p of pendingExecEnds) {
716
- const details = getExecKeys(p);
717
- if (!details) continue;
752
+ if (execDelta && pendingExecDetails.length > 0) {
753
+ const perExecShare = 1 / pendingExecDetails.length;
754
+ for (const details of pendingExecDetails) {
718
755
  const attributed = {
719
756
  input_tokens: execDelta.input_tokens * perExecShare,
720
757
  cached_input_tokens: execDelta.cached_input_tokens * perExecShare,
@@ -731,20 +768,76 @@ async function parseCodexRolloutFile(filePath, {
731
768
  addInto(ensureExec(byExecDuration, details.duration).totals, attributed);
732
769
  addInto(ensureExec(byExecOutput, details.output).totals, attributed);
733
770
 
734
- absorbExecEnd(p);
771
+ absorbExecEnd(details);
735
772
  }
736
773
  } else {
737
774
  // Still ingest exec end stats without token attribution so the drill-down works.
738
- for (const p of pendingExecEnds) absorbExecEnd(p);
775
+ for (const details of pendingExecDetails) absorbExecEnd(details);
739
776
  }
740
777
 
741
778
  addInto(totals, delta);
742
- pendingCalls = [];
779
+ pendingToolNames = [];
743
780
  pendingSkills = [];
744
- pendingExecEnds = [];
781
+ pendingExecDetails = [];
782
+ }
783
+
784
+ function usageEventSignature(lastUsage, totalUsage) {
785
+ return JSON.stringify({ lastUsage, totalUsage });
786
+ }
787
+
788
+ function materializeLastTimestampSignatures() {
789
+ if (hasUnmaterializedLastTimestampUsage) {
790
+ lastTimestampEventSignatures.add(
791
+ usageEventSignature(lastTimestampLastUsage, lastTimestampTotalUsage),
792
+ );
793
+ hasUnmaterializedLastTimestampUsage = false;
794
+ }
795
+ return lastTimestampEventSignatures;
796
+ }
797
+
798
+ function noteTokenEvent(timestamp, lastUsage, totalUsage) {
799
+ if (!isResuming) {
800
+ if (timestamp !== lastTokenTimestamp) {
801
+ lastTokenTimestamp = timestamp;
802
+ lastTimestampEventSignatures = new Set();
803
+ hasUnmaterializedLastTimestampUsage = false;
804
+ } else {
805
+ materializeLastTimestampSignatures();
806
+ }
807
+ lastTimestampLastUsage = lastUsage;
808
+ lastTimestampTotalUsage = totalUsage;
809
+ hasUnmaterializedLastTimestampUsage = true;
810
+ return true;
811
+ }
812
+ if (isResuming && lastTokenTimestamp && timestamp < lastTokenTimestamp) {
813
+ const error = new Error("Codex rollout append is not timestamp-monotonic");
814
+ error.code = CODEX_RESUME_INVALIDATED;
815
+ throw error;
816
+ }
817
+ if (!lastTokenTimestamp || timestamp > lastTokenTimestamp) {
818
+ lastTokenTimestamp = timestamp;
819
+ lastTimestampEventSignatures = new Set();
820
+ lastTimestampLastUsage = lastUsage;
821
+ lastTimestampTotalUsage = totalUsage;
822
+ hasUnmaterializedLastTimestampUsage = true;
823
+ return true;
824
+ }
825
+ if (timestamp === lastTokenTimestamp) {
826
+ const signature = usageEventSignature(lastUsage, totalUsage);
827
+ const signatures = materializeLastTimestampSignatures();
828
+ if (signatures.has(signature)) return false;
829
+ signatures.add(signature);
830
+ }
831
+ return true;
745
832
  }
746
833
 
747
- for await (const obj of iterateCodexObjects(filePaths, diagnostics)) {
834
+ for await (const obj of iterateCodexObjects(filePaths, diagnostics, {
835
+ startOffset,
836
+ endOffset,
837
+ readProgress,
838
+ contentHasher,
839
+ sourceHandle,
840
+ })) {
748
841
  const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
749
842
  if (!ts) continue;
750
843
  const inRequestedRange = isTimestampInRequestedDayRange(ts, { from, to, timeZoneContext });
@@ -765,14 +858,15 @@ async function parseCodexRolloutFile(filePath, {
765
858
  }
766
859
 
767
860
  if (obj.type === "response_item" && obj.payload?.type === "function_call") {
768
- pendingCalls.push(obj.payload);
861
+ pendingToolNames.push(normalizeToolName(obj.payload));
769
862
  const skill = extractSkillNameFromFunctionCall(obj.payload, diagnostics);
770
863
  if (skill) pendingSkills.push(skill);
771
864
  continue;
772
865
  }
773
866
 
774
867
  if (obj.type === "event_msg" && obj.payload?.type === "exec_command_end") {
775
- pendingExecEnds.push(obj.payload);
868
+ const details = getExecKeys(obj.payload);
869
+ if (details) pendingExecDetails.push(details);
776
870
  continue;
777
871
  }
778
872
 
@@ -783,25 +877,26 @@ async function parseCodexRolloutFile(filePath, {
783
877
  const totalUsage = info?.total_token_usage;
784
878
  const delta = pickDelta(lastUsage, totalUsage, prevTotals);
785
879
  if (totalUsage && typeof totalUsage === "object") prevTotals = totalUsage;
880
+ const isNewResumeEvent = noteTokenEvent(ts, lastUsage, totalUsage);
786
881
  if (inRequestedRange) {
787
882
  const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
788
- const eventKey = `${eventSessionId}:${ts}:${JSON.stringify({ lastUsage, totalUsage })}`;
789
- if (seenTokenEvents && seenTokenEvents.has(eventKey)) {
883
+ const eventKey = `${eventSessionId}:${ts}:${usageEventSignature(lastUsage, totalUsage)}`;
884
+ if (!isNewResumeEvent || (seenTokenEvents && seenTokenEvents.has(eventKey))) {
790
885
  attributeTurn(null);
791
886
  } else {
792
887
  if (seenTokenEvents && delta?.total_tokens > 0) seenTokenEvents.add(eventKey);
793
888
  attributeTurn(delta);
794
889
  }
795
890
  } else {
796
- pendingCalls = [];
891
+ pendingToolNames = [];
797
892
  pendingSkills = [];
798
- pendingExecEnds = [];
893
+ pendingExecDetails = [];
799
894
  }
800
895
  continue;
801
896
  }
802
897
  }
803
898
 
804
- return {
899
+ const result = {
805
900
  sessionId: sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath,
806
901
  cwd,
807
902
  model: model || provider,
@@ -825,11 +920,178 @@ async function parseCodexRolloutFile(filePath, {
825
920
  byOutput: finalizeExecRows(byExecOutput),
826
921
  },
827
922
  };
923
+ if (captureResumeState && filePaths.length === 1) {
924
+ result.resumeState = {
925
+ sessionId,
926
+ cwd,
927
+ model,
928
+ provider,
929
+ cliVersion,
930
+ prevTotals,
931
+ pendingToolNames,
932
+ pendingSkills,
933
+ pendingExecDetails,
934
+ lastTokenTimestamp,
935
+ lastTimestampEventSignatures: Array.from(materializeLastTimestampSignatures()),
936
+ };
937
+ result.endOffset = readProgress.endOffset;
938
+ result.appendable = Boolean(contentHasher) && (
939
+ readProgress.endOffset === 0 || readProgress.lastByte === 0x0a
940
+ );
941
+ result.contentHashState = contentHasher;
942
+ }
943
+ return result;
828
944
  }
829
945
 
830
- async function* readCodexObjects(filePath, diagnostics, fileIndex) {
946
+ function parseCodexLine(lineBuffer, diagnostics) {
947
+ let content = lineBuffer;
948
+ if (content.length > 0 && content[content.length - 1] === 0x0d) {
949
+ content = content.subarray(0, content.length - 1);
950
+ }
951
+ if (content.length === 0) return null;
952
+ try {
953
+ if (diagnostics) diagnostics.json_parse_calls += 1;
954
+ return JSON.parse(content.toString("utf8"));
955
+ } catch {
956
+ return null;
957
+ }
958
+ }
959
+
960
+ async function* readCodexObjectsIncremental(filePath, diagnostics, fileIndex, {
961
+ startOffset = 0,
962
+ endOffset = null,
963
+ readProgress = null,
964
+ contentHasher = null,
965
+ sourceHandle = null,
966
+ } = {}) {
967
+ const start = Math.max(0, Number(startOffset) || 0);
968
+ const requestedEnd = Number(endOffset);
969
+ const hasBoundedEnd = endOffset !== null && endOffset !== undefined &&
970
+ Number.isFinite(requestedEnd) && requestedEnd >= 0;
971
+ if (hasBoundedEnd && requestedEnd <= start) {
972
+ if (readProgress) readProgress.endOffset = start;
973
+ return;
974
+ }
975
+
831
976
  if (diagnostics) diagnostics.opened_files += 1;
832
- const stream = fs.createReadStream(filePath, { encoding: "utf8" });
977
+ const stream = sourceHandle && typeof sourceHandle.read === "function"
978
+ ? null
979
+ : fs.createReadStream(filePath, {
980
+ start,
981
+ ...(hasBoundedEnd ? { end: requestedEnd - 1 } : {}),
982
+ });
983
+ const chunks = stream || (async function* readStableHandleChunks() {
984
+ const buffer = Buffer.allocUnsafe(64 * 1024);
985
+ const limit = hasBoundedEnd ? requestedEnd : Number.MAX_SAFE_INTEGER;
986
+ let offset = start;
987
+ while (offset < limit) {
988
+ const length = Math.min(buffer.length, limit - offset);
989
+ const result = await sourceHandle.read(buffer, 0, length, offset);
990
+ if (!result.bytesRead) break;
991
+ offset += result.bytesRead;
992
+ yield buffer.subarray(0, result.bytesRead);
993
+ }
994
+ })();
995
+ const fragments = [];
996
+ let fragmentsBytes = 0;
997
+ let nextChunkOffset = start;
998
+ let lineStartOffset = start;
999
+ let lineIndex = 0;
1000
+
1001
+ try {
1002
+ for await (const value of chunks) {
1003
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
1004
+ const chunkStart = nextChunkOffset;
1005
+ nextChunkOffset += chunk.length;
1006
+ if (contentHasher) contentHasher.update(chunk);
1007
+ if (readProgress && chunk.length > 0) {
1008
+ readProgress.lastByte = chunk[chunk.length - 1];
1009
+ }
1010
+ if (diagnostics) {
1011
+ diagnostics.bytes_read = Number(diagnostics.bytes_read || 0) + chunk.length;
1012
+ }
1013
+
1014
+ let cursor = 0;
1015
+ while (cursor < chunk.length) {
1016
+ const newline = chunk.indexOf(0x0a, cursor);
1017
+ if (newline < 0) {
1018
+ const tail = chunk.subarray(cursor);
1019
+ if (tail.length > 0) {
1020
+ // Stable-handle reads reuse their buffer, so retained fragments
1021
+ // must own their bytes before the next read overwrites it.
1022
+ fragments.push(Buffer.from(tail));
1023
+ fragmentsBytes += tail.length;
1024
+ }
1025
+ break;
1026
+ }
1027
+
1028
+ const tail = chunk.subarray(cursor, newline);
1029
+ const lineBuffer = fragments.length > 0
1030
+ ? Buffer.concat([...fragments, tail], fragmentsBytes + tail.length)
1031
+ : tail;
1032
+ fragments.length = 0;
1033
+ fragmentsBytes = 0;
1034
+
1035
+ const absoluteEnd = chunkStart + newline + 1;
1036
+ if (readProgress) readProgress.endOffset = absoluteEnd;
1037
+ lineStartOffset = absoluteEnd;
1038
+ const obj = parseCodexLine(lineBuffer, diagnostics);
1039
+ if (obj) {
1040
+ yield { obj, fileIndex, lineIndex };
1041
+ lineIndex += 1;
1042
+ }
1043
+ cursor = newline + 1;
1044
+ }
1045
+ }
1046
+
1047
+ if (fragmentsBytes > 0) {
1048
+ const lineBuffer = fragments.length === 1
1049
+ ? fragments[0]
1050
+ : Buffer.concat(fragments, fragmentsBytes);
1051
+ const obj = parseCodexLine(lineBuffer, diagnostics);
1052
+ if (obj) {
1053
+ if (readProgress) readProgress.endOffset = lineStartOffset + fragmentsBytes;
1054
+ yield { obj, fileIndex, lineIndex };
1055
+ }
1056
+ }
1057
+ } finally {
1058
+ if (readProgress) readProgress.endOffset = nextChunkOffset;
1059
+ stream?.destroy();
1060
+ if (diagnostics) diagnostics.parsed_files += 1;
1061
+ }
1062
+ }
1063
+
1064
+ async function* readCodexObjectsFull(filePath, diagnostics, fileIndex, {
1065
+ startOffset = 0,
1066
+ endOffset = null,
1067
+ readProgress = null,
1068
+ contentHasher = null,
1069
+ } = {}) {
1070
+ const start = Math.max(0, Number(startOffset) || 0);
1071
+ const requestedEnd = Number(endOffset);
1072
+ const hasBoundedEnd = endOffset !== null && endOffset !== undefined &&
1073
+ Number.isFinite(requestedEnd) && requestedEnd >= 0;
1074
+ if (hasBoundedEnd && requestedEnd <= start) {
1075
+ if (readProgress) readProgress.endOffset = start;
1076
+ return;
1077
+ }
1078
+
1079
+ if (diagnostics) diagnostics.opened_files += 1;
1080
+ const stream = fs.createReadStream(filePath, {
1081
+ ...(contentHasher ? {} : { encoding: "utf8" }),
1082
+ start,
1083
+ ...(hasBoundedEnd ? { end: requestedEnd - 1 } : {}),
1084
+ });
1085
+ const trackChunk = contentHasher
1086
+ ? (value) => {
1087
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
1088
+ contentHasher.update(chunk);
1089
+ if (readProgress && chunk.length > 0) {
1090
+ readProgress.lastByte = chunk[chunk.length - 1];
1091
+ }
1092
+ }
1093
+ : null;
1094
+ if (trackChunk) stream.on("data", trackChunk);
833
1095
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
834
1096
  let lineIndex = 0;
835
1097
  try {
@@ -846,15 +1108,29 @@ async function* readCodexObjects(filePath, diagnostics, fileIndex) {
846
1108
  lineIndex += 1;
847
1109
  }
848
1110
  } finally {
1111
+ const bytesRead = Number(stream.bytesRead || 0);
1112
+ if (readProgress) readProgress.endOffset = start + bytesRead;
1113
+ if (diagnostics) {
1114
+ diagnostics.bytes_read = Number(diagnostics.bytes_read || 0) + bytesRead;
1115
+ diagnostics.parsed_files += 1;
1116
+ }
1117
+ if (trackChunk) stream.off("data", trackChunk);
849
1118
  rl.close();
850
1119
  stream.close?.();
851
- if (diagnostics) diagnostics.parsed_files += 1;
852
1120
  }
853
1121
  }
854
1122
 
855
- async function* iterateCodexObjects(filePaths, diagnostics) {
1123
+ async function* readCodexObjects(filePath, diagnostics, fileIndex, options = {}) {
1124
+ const start = Math.max(0, Number(options.startOffset) || 0);
1125
+ const reader = start > 0 || options.sourceHandle
1126
+ ? readCodexObjectsIncremental
1127
+ : readCodexObjectsFull;
1128
+ yield* reader(filePath, diagnostics, fileIndex, options);
1129
+ }
1130
+
1131
+ async function* iterateCodexObjects(filePaths, diagnostics, options = {}) {
856
1132
  if (filePaths.length <= 1) {
857
- for await (const record of readCodexObjects(filePaths[0], diagnostics, 0)) {
1133
+ for await (const record of readCodexObjects(filePaths[0], diagnostics, 0, options)) {
858
1134
  yield record.obj;
859
1135
  }
860
1136
  return;
@@ -892,6 +1168,10 @@ function rolloutSessionIdFromPath(filePath) {
892
1168
  return match ? match[1] : null;
893
1169
  }
894
1170
 
1171
+ function isCodexResumeInvalidated(error) {
1172
+ return error?.code === CODEX_RESUME_INVALIDATED;
1173
+ }
1174
+
895
1175
  module.exports = {
896
1176
  parseCodexRolloutFile,
897
1177
  extractTokenCount,
@@ -913,6 +1193,7 @@ module.exports = {
913
1193
  timestampDayKey,
914
1194
  isTimestampInRequestedDayRange,
915
1195
  rolloutSessionIdFromPath,
1196
+ isCodexResumeInvalidated,
916
1197
  finalizeToolRows,
917
1198
  finalizeSkillRows,
918
1199
  finalizeExecRows,
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+
3
+ // Estimates fixed context overhead without returning file contents, prompts,
4
+ // tool schemas, secrets, or command arguments.
5
+
6
+ const fs = require("node:fs");
7
+ const os = require("node:os");
8
+ const path = require("node:path");
9
+ const contextCache = new Map();
10
+
11
+ function estimateTokens(text) {
12
+ const value = String(text || "");
13
+ let cjk = 0;
14
+ for (const char of value) if (/\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/u.test(char)) cjk += 1;
15
+ return Math.ceil(cjk + (value.length - cjk) / 4);
16
+ }
17
+
18
+ function readMetadata(filePath, kind, root) {
19
+ try {
20
+ const text = fs.readFileSync(filePath, "utf8");
21
+ return { kind, name: path.relative(root, filePath) || path.basename(filePath), bytes: Buffer.byteLength(text), estimated_tokens: estimateTokens(text) };
22
+ } catch { return null; }
23
+ }
24
+
25
+ function walkNamedFiles(root, targetName, limit = 500) {
26
+ const out = [];
27
+ const stack = [root];
28
+ while (stack.length && out.length < limit) {
29
+ const dir = stack.pop();
30
+ let entries;
31
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
32
+ for (const entry of entries) {
33
+ if ([".git", "node_modules", "dist", "build"].includes(entry.name)) continue;
34
+ const filePath = path.join(dir, entry.name);
35
+ if (entry.isDirectory()) stack.push(filePath);
36
+ else if (entry.isFile() && entry.name === targetName) out.push(filePath);
37
+ if (out.length >= limit) break;
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function countMcpServers(home, cwd) {
44
+ const files = [
45
+ path.join(home, ".claude.json"),
46
+ path.join(home, ".claude", "settings.json"),
47
+ path.join(cwd, ".mcp.json"),
48
+ ];
49
+ const names = new Set();
50
+ for (const filePath of files) {
51
+ try {
52
+ const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
53
+ const servers = data.mcpServers || data.mcp_servers || {};
54
+ for (const name of Object.keys(servers)) names.add(name);
55
+ } catch { /* absent or not JSON */ }
56
+ }
57
+ try {
58
+ const toml = fs.readFileSync(path.join(home, ".codex", "config.toml"), "utf8");
59
+ for (const match of toml.matchAll(/^\s*\[mcp_servers\.([^\]]+)\]/gm)) names.add(match[1]);
60
+ } catch { /* absent */ }
61
+ return names.size;
62
+ }
63
+
64
+ function computeContextHealth({ home = os.homedir(), cwd = process.cwd(), env = process.env, cacheTtlMs = 5 * 60_000 } = {}) {
65
+ const cacheKey = `${home}\0${cwd}\0${env.TOKENTRACKER_MCP_TOOL_SCHEMA_TOKENS || ""}\0${env.TOKENTRACKER_MCP_TOOLS_PER_SERVER || ""}`;
66
+ const cached = contextCache.get(cacheKey);
67
+ if (cached && Date.now() - cached.at < cacheTtlMs) return cached.value;
68
+ const files = [];
69
+ const known = [
70
+ [path.join(home, ".claude", "CLAUDE.md"), "instruction", home],
71
+ [path.join(home, ".codex", "AGENTS.md"), "instruction", home],
72
+ [path.join(cwd, "CLAUDE.md"), "instruction", cwd],
73
+ [path.join(cwd, "AGENTS.md"), "instruction", cwd],
74
+ ];
75
+ for (const [filePath, kind, root] of known) {
76
+ const item = readMetadata(filePath, kind, root);
77
+ if (item) files.push(item);
78
+ }
79
+ const skillRoots = [
80
+ path.join(home, ".claude", "skills"),
81
+ path.join(home, ".codex", "skills"),
82
+ path.join(cwd, ".claude", "skills"),
83
+ path.join(cwd, ".agents", "skills"),
84
+ ];
85
+ for (const root of skillRoots) {
86
+ for (const filePath of walkNamedFiles(root, "SKILL.md")) {
87
+ const item = readMetadata(filePath, "skill", root);
88
+ if (item) files.push(item);
89
+ }
90
+ }
91
+ const instructionTokens = files.filter((row) => row.kind === "instruction").reduce((sum, row) => sum + row.estimated_tokens, 0);
92
+ const skillTokens = files.filter((row) => row.kind === "skill").reduce((sum, row) => sum + row.estimated_tokens, 0);
93
+ const mcpServers = countMcpServers(home, cwd);
94
+ const perToolSchemaTokens = Math.max(0, Number(env.TOKENTRACKER_MCP_TOOL_SCHEMA_TOKENS || 400));
95
+ const assumedToolsPerServer = Math.max(1, Number(env.TOKENTRACKER_MCP_TOOLS_PER_SERVER || 5));
96
+ const mcpSchemaTokens = mcpServers * perToolSchemaTokens * assumedToolsPerServer;
97
+ const total = instructionTokens + skillTokens + mcpSchemaTokens;
98
+ const severity = total >= 50_000 ? "high" : total >= 20_000 ? "medium" : "low";
99
+ const result = {
100
+ generated_at: new Date().toISOString(),
101
+ estimated_fixed_tokens: total,
102
+ severity,
103
+ breakdown: {
104
+ instruction_files: files.filter((row) => row.kind === "instruction").length,
105
+ instruction_tokens: instructionTokens,
106
+ skills: files.filter((row) => row.kind === "skill").length,
107
+ skill_tokens: skillTokens,
108
+ mcp_servers: mcpServers,
109
+ mcp_schema_tokens: mcpSchemaTokens,
110
+ },
111
+ largest_items: files.sort((a, b) => b.estimated_tokens - a.estimated_tokens).slice(0, 10),
112
+ provenance: {
113
+ confidence: "inferred",
114
+ source: "local-config-metadata",
115
+ mcp_assumption: { tools_per_server: assumedToolsPerServer, tokens_per_tool_schema: perToolSchemaTokens },
116
+ content_retained: false,
117
+ },
118
+ };
119
+ contextCache.set(cacheKey, { at: Date.now(), value: result });
120
+ if (contextCache.size > 8) contextCache.delete(contextCache.keys().next().value);
121
+ return result;
122
+ }
123
+
124
+ module.exports = { estimateTokens, computeContextHealth, countMcpServers };