tokentracker-cli 0.93.7 → 0.94.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 (117) hide show
  1. package/README.de.md +1 -1
  2. package/README.ja.md +1 -1
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/README.zh-CN.md +1 -1
  6. package/dashboard/dist/assets/{AchievementBadge-DqvblIQY.js → AchievementBadge-DnmgEa4q.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-C-xm27pB.js → AchievementsPage-B7YTbAuK.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-D8Oe2FGJ.js → AchievementsSection-CwgT5sqH.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-BJOcOnHo.js → BadgeDetailModal-Dets6jzW.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-6fB046ld.js → BotAnimated-5KwUvC9F.js} +1 -1
  11. package/dashboard/dist/assets/{Card-BKbWtBaL.js → Card-jf9w-4-N.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-DbpeAldp.js → ClawdAnimated-CGxDVQ0c.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-Jte7TBon.js → CommandPalette-B1bUUf32.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-DDm7tCXO.js → CommunityStatsModal-DRDpZycV.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-CrIjcrpZ.js → ConfirmModal-BR-aIRH9.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-BWJtMkIy.js → Controls-CbHsJAqV.js} +1 -1
  17. package/dashboard/dist/assets/{DashboardPage-Bcq4EwG5.js → DashboardPage-B5XMQoxy.js} +1 -1
  18. package/dashboard/dist/assets/{DevicePage-C9NcPZGX.js → DevicePage-DDlicLLn.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-CY-LguUG.js → DialogClose-B89uLXqp.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-DiVbwvTR.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-BkAmJrQ_.js → DialogTitle-jOyycYKF.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-U-bxcM-y.js → FadeIn-BEND42ze.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-4HUCxVhg.js → HeaderGithubStar-Dacl1WGS.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-BXcXOCs8.js → HoverTooltip-B2Mh2LTi.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-Cppkc-gE.js → IpCheckPage-CPEuRhiU.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-D3KXfYKE.js → LandingPage-BYWNR_dx.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-CdsHg6L0.js → LeaderboardAvatar-B9_5LDVj.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-niat0TZW.js → LeaderboardPage-_Hl1NaIa.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-DgjhhEbB.js → LeaderboardProfileModal-Cydp7CyA.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-CZej8gpc.js → LeaderboardProfilePage-Dz0ecujs.js} +1 -1
  31. package/dashboard/dist/assets/LimitsPage-CmdT2qOZ.js +5 -0
  32. package/dashboard/dist/assets/{LocalOnlyNotice-DQswDMSD.js → LocalOnlyNotice-DhuPGUkw.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-glPyVqYh.js → LoginCard-DQtHaX_5.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-slsdEXC7.js → LoginModal-DvpPRnYV.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-DX5QT917.js → LoginPage-DOjfmdwb.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-B8U3CJwt.js → PetPage-Dd6U9fh2.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-BVPcQAio.js → PopoverPopup-B1V01u10.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-CljIacVj.js → ProviderIcon-BLRe55c7.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-DTBmQk2y.js → ResetPasswordPage-BJ95vy1S.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-C6npelCT.js → SegmentedControl-DacS_m9M.js} +1 -1
  41. package/dashboard/dist/assets/{Select-D1elkRBG.js → Select-bQDO5BEA.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-3N-hvYuD.js → SelectItemText-CUGrs9CY.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-C5ZK9X2o.js → ServiceStatusPage-CsERvmVC.js} +1 -1
  44. package/dashboard/dist/assets/SessionsPage-0VvqEfPx.js +2 -0
  45. package/dashboard/dist/assets/SettingsPage-9EHh2HfD.js +2 -0
  46. package/dashboard/dist/assets/{SkillsPage-Cwkevv29.js → SkillsPage-B1AcLX41.js} +1 -1
  47. package/dashboard/dist/assets/{TokenGalaxy-a1-E6_jp.js → TokenGalaxy-DSKTHQSs.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-Cm5cQb9Z.js → ToolbarRootContext-DP4mQLbq.js} +1 -1
  49. package/dashboard/dist/assets/{TrendMonitor-DoNDyarF.js → TrendMonitor-rabtcE5w.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-QMTmKnRl.js → WidgetsPage-Cr6Wg9QX.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-C9rJcY2o.js → WrappedPage-DqQvRcnA.js} +1 -1
  52. package/dashboard/dist/assets/__vite-browser-external-BHz2N7Ih.css +1 -0
  53. package/dashboard/dist/assets/{__vite-browser-external-CsbVaBbw.js → __vite-browser-external-C8KwSyTz.js} +12 -12
  54. package/dashboard/dist/assets/{agent-logos-B39DKy3X.js → agent-logos-BjIh_mqf.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-Cw5mMBik.js → arrow-up-right-BIhbN0NO.js} +1 -1
  56. package/dashboard/dist/assets/{check-DrQztnq1.js → check-U-wNzPvd.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-right-CEelysET.js → chevron-right-Du-QVNj4.js} +1 -1
  58. package/dashboard/dist/assets/{composite-MhgOkYfv.js → composite-BR91CxNl.js} +1 -1
  59. package/dashboard/dist/assets/{copy-Bouz-qQR.js → copy-Bw3hXBCu.js} +1 -1
  60. package/dashboard/dist/assets/{download-BugS2cl5.js → download-WOqcQzvK.js} +1 -1
  61. package/dashboard/dist/assets/{external-link-Ceqto-f4.js → external-link-BZtf6WfS.js} +1 -1
  62. package/dashboard/dist/assets/{flame-B67y0kLz.js → flame-B5kp2VI9.js} +1 -1
  63. package/dashboard/dist/assets/{format-tokens-CR8njNJs.js → format-tokens-BOCnuMuJ.js} +1 -1
  64. package/dashboard/dist/assets/{icons-DFj5ctTx.js → icons-CA3FiIa_.js} +1 -1
  65. package/dashboard/dist/assets/{index-ZGfRrxuq.js → index-DQaanK52.js} +1 -1
  66. package/dashboard/dist/assets/{index-CrymIhtC.js → index-DjWC63Bs.js} +1 -1
  67. package/dashboard/dist/assets/{index-Bx6EP36K.js → index-mEG0HZ-a.js} +1 -1
  68. package/dashboard/dist/assets/{info-C6XoktLa.js → info-Djer9i3-.js} +1 -1
  69. package/dashboard/dist/assets/{limits-providers-B9L6hIZT.js → limits-providers-F-RnNFAb.js} +1 -1
  70. package/dashboard/dist/assets/{link-2-DKG2j43e.js → link-2-DV4ocD0l.js} +1 -1
  71. package/dashboard/dist/assets/{loader-circle-DB2txq05.js → loader-circle-DJK72c-o.js} +1 -1
  72. package/dashboard/dist/assets/{maximize-2-3-dz0brp.js → maximize-2-BwBA_lMm.js} +1 -1
  73. package/dashboard/dist/assets/{modifiers.esm-D0KjKi1o.js → modifiers.esm-B3sl4qyZ.js} +1 -1
  74. package/dashboard/dist/assets/{monitor-smartphone-Ds8H_QW9.js → monitor-smartphone-BUTpM4ng.js} +1 -1
  75. package/dashboard/dist/assets/{plus-BDsmAe4Z.js → plus-BA9RP5L6.js} +1 -1
  76. package/dashboard/dist/assets/{provider-display-DRz643iD.js → provider-display-DqsJzGSr.js} +1 -1
  77. package/dashboard/dist/assets/{react-D3Afmy9b.js → react-BYw5AVSz.js} +1 -1
  78. package/dashboard/dist/assets/{search-CO2Xf-Kg.js → search-BzcnFWVD.js} +1 -1
  79. package/dashboard/dist/assets/{skills-api-C1BZuUIq.js → skills-api-vAME1P7G.js} +1 -1
  80. package/dashboard/dist/assets/{sortable.esm-DutNEc4F.js → sortable.esm-BpEiRqqI.js} +1 -1
  81. package/dashboard/dist/assets/{store-a-oUpY-g.js → store-GeT5fhRX.js} +1 -1
  82. package/dashboard/dist/assets/{terminal-DAePk67T.js → terminal-CZCMYQ8K.js} +1 -1
  83. package/dashboard/dist/assets/{trash-2-bnFmtRSG.js → trash-2-DzlWEwKO.js} +1 -1
  84. package/dashboard/dist/assets/{use-community-stats-PJBeXyek.js → use-community-stats-DOvJfj-i.js} +1 -1
  85. package/dashboard/dist/assets/use-limits-display-prefs-BmLs4HyN.js +1 -0
  86. package/dashboard/dist/assets/{use-ordered-list-BaVvCzZ9.js → use-ordered-list-BgozDSSV.js} +1 -1
  87. package/dashboard/dist/assets/use-reduced-motion-Bc3RFJdg.js +1 -0
  88. package/dashboard/dist/assets/{use-session-efficiency-pref-COTG-zJs.js → use-session-efficiency-pref-HP1ckVkS.js} +1 -1
  89. package/dashboard/dist/assets/{use-transform-BQEPyChL.js → use-transform-B4VSogOx.js} +1 -1
  90. package/dashboard/dist/assets/{use-usage-limits-LR2gHJJn.js → use-usage-limits-DNNhD2H6.js} +1 -1
  91. package/dashboard/dist/assets/{useCurrency-CeqKrzFz.js → useCurrency-CSuzCBAL.js} +1 -1
  92. package/dashboard/dist/assets/{useTokenFormat-D0IMlZlO.js → useTokenFormat-ClnL9OFe.js} +1 -1
  93. package/dashboard/dist/assets/{zap-BBn6G5_E.js → zap-C_T9CCiS.js} +1 -1
  94. package/dashboard/dist/index.html +2 -2
  95. package/dashboard/dist/ip-check.html +2 -2
  96. package/dashboard/dist/leaderboard.html +2 -2
  97. package/dashboard/dist/share.html +2 -2
  98. package/package.json +1 -1
  99. package/src/commands/status.js +3 -1
  100. package/src/commands/sync.js +8 -3
  101. package/src/lib/codex-rollout-parser.js +35 -4
  102. package/src/lib/grok-context-breakdown.js +24 -33
  103. package/src/lib/grok-usage.js +95 -0
  104. package/src/lib/local-api.js +2 -0
  105. package/src/lib/pricing/index.js +10 -0
  106. package/src/lib/pricing/seed-snapshot.json +1 -1
  107. package/src/lib/rollout.js +817 -239
  108. package/src/lib/session-analytics.js +305 -50
  109. package/src/lib/telemetry.js +2 -2
  110. package/src/lib/usage-limits.js +20 -10
  111. package/dashboard/dist/assets/DialogDescription-DPyhpdeR.js +0 -1
  112. package/dashboard/dist/assets/LimitsPage-BXzzX87I.js +0 -5
  113. package/dashboard/dist/assets/SessionsPage-CJ0JvE8-.js +0 -2
  114. package/dashboard/dist/assets/SettingsPage-C32uYzqG.js +0 -2
  115. package/dashboard/dist/assets/__vite-browser-external-D95WhsZe.css +0 -1
  116. package/dashboard/dist/assets/use-limits-display-prefs-Blvn-7CB.js +0 -1
  117. package/dashboard/dist/assets/use-reduced-motion-f51jgK_S.js +0 -1
@@ -31,6 +31,7 @@ const readline = require("node:readline");
31
31
  const { listClaudeProjectFiles, listRolloutFilesDeep, claudeMessageDedupKey } = require("./rollout");
32
32
  const { parseCodexRolloutFile } = require("./codex-rollout-parser");
33
33
  const { computeRowCost } = require("./pricing");
34
+ const { USD_TICKS_PER_USD, normalizeGrokUsage } = require("./grok-usage");
34
35
  const wsl = require("./wsl-probe");
35
36
 
36
37
  // Bump the sidecar when derived metrics change so cached rows are rebuilt
@@ -54,7 +55,11 @@ const wsl = require("./wsl-probe");
54
55
  // stay valid while Grok entries appear on the next full rebuild.
55
56
  // v11 groups native/WSL copies of one logical Claude/Codex session and scans
56
57
  // their union, deduping shared records while retaining divergent tails.
57
- const SIDECAR_VERSION = 11;
58
+ // v12 fixes Grok's mutually exclusive token columns, retains exact reported
59
+ // cost, and adds metadata-only usage diagnostics to the local session browser.
60
+ // v12 retains Codex parent/subagent metadata locally and derives exact child
61
+ // token totals from observed child rollouts instead of spawn-call estimates.
62
+ const SIDECAR_VERSION = 12;
58
63
  const EDIT_TOOLS = new Set([
59
64
  "apply_patch",
60
65
  "edit",
@@ -238,7 +243,12 @@ function finalizeRecord(record) {
238
243
  record.active_ms = Math.max(0, finite(record.active_ms));
239
244
  record.duration_ms = record.active_ms;
240
245
  record.total_tokens = finite(record.total_tokens || record.tokens?.total_tokens);
241
- record.cost_usd = computeRowCost({ source: record.source, model: record.model, ...record.tokens });
246
+ const providerCost = Number(record.provider_cost_usd);
247
+ record.cost_usd = record.cost_source === "provider_reported"
248
+ && Number.isFinite(providerCost)
249
+ && providerCost >= 0
250
+ ? providerCost
251
+ : computeRowCost({ source: record.source, model: record.model, ...record.tokens });
242
252
  record.productive = record.edit_turns > 0;
243
253
  // A first-pass delivery has exactly one user turn containing an observed
244
254
  // edit and no repeated user request. The legacy one_shot field stays as an
@@ -511,6 +521,18 @@ async function scanCodexSession(filePath) {
511
521
  version: SIDECAR_VERSION,
512
522
  session_hash: sessionHash("codex", parsed.sessionId || primaryFilePath),
513
523
  session_id: parsed.sessionId || null,
524
+ // Local-only lineage. Codex v2 normally writes forked_from_id; keep
525
+ // parent_thread_id as a compatibility fallback, but reject disagreement.
526
+ parent_session_id: parsed.forkedFromId || parsed.parentThreadId || null,
527
+ parent_link_conflict: Boolean(
528
+ parsed.forkedFromId
529
+ && parsed.parentThreadId
530
+ && parsed.forkedFromId !== parsed.parentThreadId
531
+ ),
532
+ forked_from_id: parsed.forkedFromId || null,
533
+ thread_source: typeof parsed.threadSource === "string" ? parsed.threadSource : null,
534
+ agent_nickname: typeof parsed.agentNickname === "string" ? parsed.agentNickname : null,
535
+ agent_role: typeof parsed.agentRole === "string" ? parsed.agentRole : null,
514
536
  // Local-only: stripped in summarizeSessions before any cloud/CSV export.
515
537
  title,
516
538
  source: "codex",
@@ -569,42 +591,6 @@ function grokTimestampIso(obj) {
569
591
  return new Date(ms).toISOString();
570
592
  }
571
593
 
572
- // Grok reports inputTokens as the full prompt (including cache hits). Split so
573
- // pricing can apply cache_read rates correctly — same authority as the usage
574
- // parser and grok-context-breakdown.readUsageTotals. Prefer the reported
575
- // totalTokens; do not re-sum input+cached (that double-counts cache hits).
576
- function grokUsageTotals(usage) {
577
- if (!usage || typeof usage !== "object") return emptyTotals();
578
- const inputRaw = finite(usage.inputTokens ?? usage.input_tokens);
579
- const cached_input_tokens = finite(
580
- usage.cachedReadTokens ?? usage.cache_read_input_tokens ?? usage.cached_input_tokens,
581
- );
582
- const cache_creation_input_tokens = finite(
583
- usage.cachedWriteTokens ?? usage.cache_creation_input_tokens,
584
- );
585
- const output_tokens = finite(usage.outputTokens ?? usage.output_tokens);
586
- const reasoning_output_tokens = finite(
587
- usage.reasoningTokens ?? usage.reasoning_output_tokens,
588
- );
589
- const input_tokens = Math.max(0, inputRaw - cached_input_tokens);
590
- let total_tokens = finite(usage.totalTokens ?? usage.total_tokens);
591
- if (total_tokens <= 0) {
592
- total_tokens = input_tokens
593
- + cached_input_tokens
594
- + cache_creation_input_tokens
595
- + output_tokens
596
- + reasoning_output_tokens;
597
- }
598
- return {
599
- input_tokens,
600
- cached_input_tokens,
601
- cache_creation_input_tokens,
602
- output_tokens,
603
- reasoning_output_tokens,
604
- total_tokens,
605
- };
606
- }
607
-
608
594
  function pickGrokModel(usage, fallback) {
609
595
  const modelUsage = usage?.modelUsage;
610
596
  if (modelUsage && typeof modelUsage === "object") {
@@ -704,6 +690,13 @@ async function scanGrokSession(filePath) {
704
690
  let currentHadEdit = false;
705
691
  let subagentCalls = 0;
706
692
  const subagentTypes = new Map();
693
+ let usageEvents = 0;
694
+ let providerCostEvents = 0;
695
+ let providerCostUsd = 0;
696
+ let modelCalls = 0;
697
+ let apiDurationMs = 0;
698
+ let usageIsIncomplete = false;
699
+ let costIsPartial = false;
707
700
  let lastPromptFingerprint = null;
708
701
  // Grok streams user_message_chunk pieces for one prompt. Accumulate until
709
702
  // turn_completed, then fingerprint the full prompt for retry detection.
@@ -772,7 +765,21 @@ async function scanGrokSession(filePath) {
772
765
  if (sessionUpdate === "turn_completed") {
773
766
  closeTurn();
774
767
  const usage = update.usage;
775
- addTotals(tokens, grokUsageTotals(usage));
768
+ const normalizedUsage = normalizeGrokUsage(usage);
769
+ if (normalizedUsage) {
770
+ usageEvents += 1;
771
+ addTotals(tokens, normalizedUsage);
772
+ modelCalls += finite(normalizedUsage.model_calls);
773
+ apiDurationMs += finite(normalizedUsage.api_duration_ms);
774
+ usageIsIncomplete ||= normalizedUsage.usage_is_incomplete;
775
+ costIsPartial ||= normalizedUsage.cost_is_partial;
776
+ if (normalizedUsage.total_cost_usd != null) {
777
+ providerCostEvents += 1;
778
+ providerCostUsd = Math.round(
779
+ (providerCostUsd + normalizedUsage.total_cost_usd) * USD_TICKS_PER_USD,
780
+ ) / USD_TICKS_PER_USD;
781
+ }
782
+ }
776
783
  const candidate = pickGrokModel(usage, model);
777
784
  if (candidate && candidate !== "unknown") model = candidate;
778
785
  continue;
@@ -796,6 +803,11 @@ async function scanGrokSession(filePath) {
796
803
  model = normalizeSessionModel(signals.primaryModelId) || model;
797
804
  }
798
805
 
806
+ const hasCompleteProviderCost = usageEvents > 0
807
+ && providerCostEvents === usageEvents
808
+ && !usageIsIncomplete
809
+ && !costIsPartial;
810
+
799
811
  return finalizeRecord({
800
812
  version: SIDECAR_VERSION,
801
813
  session_hash: sessionHash("grok", observedSessionId || filePath),
@@ -813,7 +825,29 @@ async function scanGrokSession(filePath) {
813
825
  subagent_calls: subagentCalls,
814
826
  subagent_types: Object.fromEntries([...subagentTypes.entries()].sort()),
815
827
  tokens,
816
- provenance: { source: "local-session-log", confidence: "observed", retry_confidence: "inferred", content_retained: false },
828
+ usage_events: usageEvents,
829
+ usage_precision: usageIsIncomplete ? "reported_incomplete" : (usageEvents > 0 ? "reported" : "unavailable"),
830
+ usage_is_incomplete: usageIsIncomplete,
831
+ cost_is_partial: costIsPartial,
832
+ cost_source: hasCompleteProviderCost ? "provider_reported" : "model_pricing",
833
+ provider_cost_usd: hasCompleteProviderCost ? providerCostUsd : null,
834
+ model_calls: modelCalls,
835
+ api_duration_ms: apiDurationMs,
836
+ context_tokens_used: finite(signals.contextTokensUsed),
837
+ context_window_tokens: finite(signals.contextWindowTokens),
838
+ context_usage_percent: finite(signals.contextWindowUsage),
839
+ tool_calls: finite(signals.toolCallCount),
840
+ tool_failures: finite(signals.toolFailureCount),
841
+ error_count: finite(signals.errorCount),
842
+ compaction_count: finite(signals.compactionCount),
843
+ provenance: {
844
+ source: "local-session-log",
845
+ confidence: usageIsIncomplete ? "partial" : "observed",
846
+ retry_confidence: "inferred",
847
+ content_retained: false,
848
+ usage: usageEvents > 0 ? "turn_completed.usage" : "unavailable",
849
+ cost: hasCompleteProviderCost ? "provider_reported" : "model_pricing",
850
+ },
817
851
  });
818
852
  }
819
853
 
@@ -829,7 +863,14 @@ function providerRoots(home, providerDir, env, deps = {}) {
829
863
  const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
830
864
  const roots = [];
831
865
  if (platform !== "win32" || wsl.shouldProbeNative(env)) {
832
- roots.push(path.join(home, providerDir));
866
+ const useProcessCodexHome = providerDir === ".codex"
867
+ && path.resolve(home) === path.resolve(homedir())
868
+ && typeof env?.CODEX_HOME === "string"
869
+ && env.CODEX_HOME.trim();
870
+ const nativeRoot = useProcessCodexHome
871
+ ? path.resolve(env.CODEX_HOME.trim())
872
+ : path.join(home, providerDir);
873
+ roots.push(nativeRoot);
833
874
  }
834
875
  // Only the machine's own home has a WSL sibling worth probing.
835
876
  // discoverWslHome resolves \\wsl$ independently of `home`, so probing for an
@@ -1029,10 +1070,15 @@ function sessionFileStatKey(filePath) {
1029
1070
  const codexTitleIndexCache = new Map();
1030
1071
 
1031
1072
  function codexTitleIndexPathFor(filePath) {
1032
- const parts = path.resolve(filePath).split(path.sep);
1033
- const idx = parts.lastIndexOf(".codex");
1034
- if (idx === -1) return null;
1035
- return [...parts.slice(0, idx + 1), "session_index.jsonl"].join(path.sep);
1073
+ let current = path.dirname(path.resolve(filePath));
1074
+ while (true) {
1075
+ if (["sessions", "archived_sessions"].includes(path.basename(current))) {
1076
+ return path.join(path.dirname(current), "session_index.jsonl");
1077
+ }
1078
+ const parent = path.dirname(current);
1079
+ if (parent === current) return null;
1080
+ current = parent;
1081
+ }
1036
1082
  }
1037
1083
 
1038
1084
  function loadCodexTitleIndex(filePath) {
@@ -1239,7 +1285,9 @@ function withinDayRange(row, from, to) {
1239
1285
  }
1240
1286
 
1241
1287
  function summarizeSessions(sessions, { from = "", to = "", includeSessions = true } = {}) {
1242
- const filtered = (sessions || []).filter((row) => withinDayRange(row, from, to));
1288
+ const filtered = annotateCodexThreadUsage((sessions || [])
1289
+ .filter((row) => withinDayRange(row, from, to))
1290
+ .map((row) => ({ ...row })));
1243
1291
  const byModel = new Map();
1244
1292
  const subagents = new Map();
1245
1293
  for (const row of filtered) {
@@ -1268,6 +1316,26 @@ function summarizeSessions(sessions, { from = "", to = "", includeSessions = tru
1268
1316
  agg.edit_cost_usd += finite(row.cost_usd);
1269
1317
  }
1270
1318
  byModel.set(key, agg);
1319
+ if (row.source === "codex" && row.parent_session_hash) {
1320
+ const name = row.agent_role || row.agent_nickname || "subagent";
1321
+ const sub = subagents.get(name) || {
1322
+ name,
1323
+ calls: 0,
1324
+ sessions: 0,
1325
+ total_tokens: 0,
1326
+ cost_usd: 0,
1327
+ attribution: "observed-child-session",
1328
+ };
1329
+ sub.calls += 1;
1330
+ sub.sessions += 1;
1331
+ sub.total_tokens += finite(row.total_tokens);
1332
+ sub.cost_usd += finite(row.cost_usd);
1333
+ subagents.set(name, sub);
1334
+ continue;
1335
+ }
1336
+ // Codex child rollouts are first-class rows. A spawn call in the parent is
1337
+ // a control-plane event, not token usage, so do not mix in an estimate.
1338
+ if (row.source === "codex") continue;
1271
1339
  for (const [name, calls] of Object.entries(row.subagent_types || {})) {
1272
1340
  const sub = subagents.get(name) || { name, calls: 0, sessions: 0, total_tokens: 0, cost_usd: 0 };
1273
1341
  sub.calls += finite(calls);
@@ -1301,7 +1369,22 @@ function summarizeSessions(sessions, { from = "", to = "", includeSessions = tru
1301
1369
  // Git attribution and the local-only session browser, but never leave the
1302
1370
  // Node process through API/CSV payloads.
1303
1371
  sessions: includeSessions
1304
- ? filtered.map(({ project_ref: _projectRef, session_id: _sessionId, title: _title, _cache_key: _cacheKey, ...row }) => row)
1372
+ ? filtered.map(({
1373
+ project_ref: _projectRef,
1374
+ session_id: _sessionId,
1375
+ parent_session_id: _parentSessionId,
1376
+ forked_from_id: _forkedFromId,
1377
+ parent_session_hash: _parentSessionHash,
1378
+ root_session_hash: _rootSessionHash,
1379
+ agent_nickname: _agentNickname,
1380
+ agent_role: _agentRole,
1381
+ thread_source: _threadSource,
1382
+ parent_link_conflict: _parentLinkConflict,
1383
+ orphaned_subagent: _orphanedSubagent,
1384
+ title: _title,
1385
+ _cache_key: _cacheKey,
1386
+ ...row
1387
+ }) => row)
1305
1388
  : [],
1306
1389
  session_count: filtered.length,
1307
1390
  summary: {
@@ -1347,9 +1430,18 @@ function resumeCommandFor(source, sessionId) {
1347
1430
  }
1348
1431
 
1349
1432
  function toSessionBrowserRow(row) {
1433
+ const tokens = row.tokens && typeof row.tokens === "object" ? row.tokens : {};
1350
1434
  return {
1351
1435
  session_hash: row.session_hash,
1352
1436
  session_id: row.session_id || null,
1437
+ parent_session_id: row.parent_session_id || null,
1438
+ parent_session_hash: row.parent_session_hash || null,
1439
+ root_session_hash: row.root_session_hash || row.session_hash,
1440
+ thread_kind: row.parent_session_id ? "subagent" : "root",
1441
+ agent_nickname: row.agent_nickname || null,
1442
+ agent_role: row.agent_role || null,
1443
+ orphaned_subagent: Boolean(row.orphaned_subagent),
1444
+ parent_link_conflict: Boolean(row.parent_link_conflict),
1353
1445
  title: row.title || null,
1354
1446
  source: row.source,
1355
1447
  project_key: row.project_key,
@@ -1364,8 +1456,35 @@ function toSessionBrowserRow(row) {
1364
1456
  edit_turns: finite(row.edit_turns),
1365
1457
  retry_turns: finite(row.retry_turns),
1366
1458
  subagent_calls: finite(row.subagent_calls),
1459
+ input_tokens: finite(tokens.input_tokens),
1460
+ cached_input_tokens: finite(tokens.cached_input_tokens),
1461
+ cache_creation_input_tokens: finite(tokens.cache_creation_input_tokens),
1462
+ output_tokens: finite(tokens.output_tokens),
1463
+ reasoning_output_tokens: finite(tokens.reasoning_output_tokens),
1464
+ direct_subagent_count: finite(row.direct_subagent_count),
1465
+ descendant_subagent_count: finite(row.descendant_subagent_count),
1466
+ own_total_tokens: finite(row.total_tokens),
1467
+ subagent_total_tokens: finite(row.subagent_total_tokens),
1468
+ combined_total_tokens: finite(row.combined_total_tokens || row.total_tokens),
1367
1469
  total_tokens: finite(row.total_tokens),
1470
+ own_cost_usd: finite(row.cost_usd),
1471
+ subagent_cost_usd: finite(row.subagent_cost_usd),
1472
+ combined_cost_usd: finite(row.combined_cost_usd || row.cost_usd),
1368
1473
  cost_usd: finite(row.cost_usd),
1474
+ cost_source: row.cost_source || null,
1475
+ usage_precision: row.usage_precision || null,
1476
+ usage_is_incomplete: Boolean(row.usage_is_incomplete),
1477
+ cost_is_partial: Boolean(row.cost_is_partial),
1478
+ usage_events: finite(row.usage_events),
1479
+ model_calls: finite(row.model_calls),
1480
+ api_duration_ms: finite(row.api_duration_ms),
1481
+ context_tokens_used: finite(row.context_tokens_used),
1482
+ context_window_tokens: finite(row.context_window_tokens),
1483
+ context_usage_percent: finite(row.context_usage_percent),
1484
+ tool_calls: finite(row.tool_calls),
1485
+ tool_failures: finite(row.tool_failures),
1486
+ error_count: finite(row.error_count),
1487
+ compaction_count: finite(row.compaction_count),
1369
1488
  productive: Boolean(row.productive),
1370
1489
  first_pass: Boolean(row.first_pass ?? row.one_shot),
1371
1490
  resume_command: resumeCommandFor(row.source, row.session_id),
@@ -1384,18 +1503,46 @@ function mergeSessionFragments(rows) {
1384
1503
  const key = row.session_hash;
1385
1504
  const cur = byHash.get(key);
1386
1505
  if (!cur) {
1387
- byHash.set(key, { ...row, _repr_tokens: finite(row.total_tokens) });
1506
+ byHash.set(key, {
1507
+ ...row,
1508
+ tokens: row.tokens && typeof row.tokens === "object" ? { ...row.tokens } : emptyTotals(),
1509
+ _repr_tokens: finite(row.total_tokens),
1510
+ });
1388
1511
  continue;
1389
1512
  }
1390
1513
  cur.turns = finite(cur.turns) + finite(row.turns);
1391
1514
  cur.edit_turns = finite(cur.edit_turns) + finite(row.edit_turns);
1392
1515
  cur.retry_turns = finite(cur.retry_turns) + finite(row.retry_turns);
1393
1516
  cur.subagent_calls = finite(cur.subagent_calls) + finite(row.subagent_calls);
1517
+ addTotals(cur.tokens, row.tokens);
1394
1518
  cur.total_tokens = finite(cur.total_tokens) + finite(row.total_tokens);
1395
1519
  cur.cost_usd = finite(cur.cost_usd) + finite(row.cost_usd);
1520
+ cur.provider_cost_usd = finite(cur.provider_cost_usd) + finite(row.provider_cost_usd);
1521
+ cur.usage_events = finite(cur.usage_events) + finite(row.usage_events);
1522
+ cur.model_calls = finite(cur.model_calls) + finite(row.model_calls);
1523
+ cur.api_duration_ms = finite(cur.api_duration_ms) + finite(row.api_duration_ms);
1524
+ cur.tool_calls = finite(cur.tool_calls) + finite(row.tool_calls);
1525
+ cur.tool_failures = finite(cur.tool_failures) + finite(row.tool_failures);
1526
+ cur.error_count = finite(cur.error_count) + finite(row.error_count);
1527
+ cur.compaction_count = finite(cur.compaction_count) + finite(row.compaction_count);
1528
+ cur.context_tokens_used = Math.max(finite(cur.context_tokens_used), finite(row.context_tokens_used));
1529
+ cur.context_window_tokens = Math.max(finite(cur.context_window_tokens), finite(row.context_window_tokens));
1530
+ cur.context_usage_percent = Math.max(finite(cur.context_usage_percent), finite(row.context_usage_percent));
1531
+ cur.usage_is_incomplete = Boolean(cur.usage_is_incomplete) || Boolean(row.usage_is_incomplete);
1532
+ cur.cost_is_partial = Boolean(cur.cost_is_partial) || Boolean(row.cost_is_partial);
1533
+ if (cur.usage_precision !== row.usage_precision) cur.usage_precision = "mixed";
1534
+ if (cur.cost_source !== row.cost_source) cur.cost_source = "mixed";
1396
1535
  cur.productive = Boolean(cur.productive) || Boolean(row.productive);
1397
1536
  cur.active_ms = finite(cur.active_ms) + finite(row.active_ms);
1398
1537
  if (!cur.title && row.title) cur.title = row.title;
1538
+ if (!cur.parent_session_id && row.parent_session_id) cur.parent_session_id = row.parent_session_id;
1539
+ if (cur.parent_session_id && row.parent_session_id && cur.parent_session_id !== row.parent_session_id) {
1540
+ cur.parent_session_id = null;
1541
+ cur.parent_link_conflict = true;
1542
+ }
1543
+ if (!cur.forked_from_id && row.forked_from_id) cur.forked_from_id = row.forked_from_id;
1544
+ if (!cur.agent_nickname && row.agent_nickname) cur.agent_nickname = row.agent_nickname;
1545
+ if (!cur.agent_role && row.agent_role) cur.agent_role = row.agent_role;
1399
1546
  // Representative model/project comes from the busiest fragment (most
1400
1547
  // tokens) so a tiny sidechain cannot overwrite the real coding model.
1401
1548
  if (finite(row.total_tokens) > finite(cur._repr_tokens)) {
@@ -1427,19 +1574,127 @@ function mergeSessionFragments(rows) {
1427
1574
  return merged;
1428
1575
  }
1429
1576
 
1577
+ // Attach exact Codex parent/child usage without changing the authoritative
1578
+ // per-session total_tokens field. Combined fields are derived views only, so
1579
+ // summing total_tokens across all rows still equals the global usage ledger.
1580
+ function annotateCodexThreadUsage(rows) {
1581
+ const byId = new Map();
1582
+ const childrenById = new Map();
1583
+ const linkedParentById = new Map();
1584
+
1585
+ for (const row of rows || []) {
1586
+ if (row?.source !== "codex" || typeof row.session_id !== "string" || !row.session_id) continue;
1587
+ const existing = byId.get(row.session_id);
1588
+ if (existing && existing !== row) {
1589
+ existing.parent_link_conflict = true;
1590
+ row.parent_link_conflict = true;
1591
+ continue;
1592
+ }
1593
+ byId.set(row.session_id, row);
1594
+ }
1595
+
1596
+ function hasParentCycle(start) {
1597
+ const seen = new Set();
1598
+ let current = start;
1599
+ while (current?.parent_session_id && byId.has(current.parent_session_id)) {
1600
+ if (seen.has(current.session_id) || current.parent_session_id === current.session_id) return true;
1601
+ seen.add(current.session_id);
1602
+ current = byId.get(current.parent_session_id);
1603
+ }
1604
+ return false;
1605
+ }
1606
+
1607
+ for (const row of byId.values()) {
1608
+ const parentId = typeof row.parent_session_id === "string" ? row.parent_session_id.trim() : "";
1609
+ if (!parentId) continue;
1610
+ if (row.parent_link_conflict || hasParentCycle(row)) {
1611
+ row.parent_link_conflict = true;
1612
+ continue;
1613
+ }
1614
+ if (!byId.has(parentId)) {
1615
+ row.orphaned_subagent = true;
1616
+ continue;
1617
+ }
1618
+ const children = childrenById.get(parentId) || [];
1619
+ children.push(row);
1620
+ childrenById.set(parentId, children);
1621
+ linkedParentById.set(row.session_id, parentId);
1622
+ }
1623
+
1624
+ const memo = new Map();
1625
+ function aggregate(row, visiting = new Set()) {
1626
+ if (memo.has(row.session_id)) return memo.get(row.session_id);
1627
+ if (visiting.has(row.session_id)) {
1628
+ row.parent_link_conflict = true;
1629
+ return { count: 0, tokens: 0, cost: 0 };
1630
+ }
1631
+ const nextVisiting = new Set(visiting);
1632
+ nextVisiting.add(row.session_id);
1633
+ let count = 0;
1634
+ let tokens = 0;
1635
+ let cost = 0;
1636
+ for (const child of childrenById.get(row.session_id) || []) {
1637
+ const nested = aggregate(child, nextVisiting);
1638
+ count += 1 + nested.count;
1639
+ tokens += finite(child.total_tokens) + nested.tokens;
1640
+ cost += finite(child.cost_usd) + nested.cost;
1641
+ }
1642
+ const result = { count, tokens, cost };
1643
+ memo.set(row.session_id, result);
1644
+ return result;
1645
+ }
1646
+
1647
+ function rootFor(row) {
1648
+ const seen = new Set([row.session_id]);
1649
+ let current = row;
1650
+ while (current && linkedParentById.has(current.session_id)) {
1651
+ const linkedParentId = linkedParentById.get(current.session_id);
1652
+ if (seen.has(linkedParentId)) {
1653
+ row.parent_link_conflict = true;
1654
+ break;
1655
+ }
1656
+ seen.add(linkedParentId);
1657
+ current = byId.get(linkedParentId);
1658
+ }
1659
+ return current || row;
1660
+ }
1661
+
1662
+ for (const row of rows || []) {
1663
+ row.own_total_tokens = finite(row.total_tokens);
1664
+ row.own_cost_usd = finite(row.cost_usd);
1665
+ row.direct_subagent_count = row.source === "codex" && row.session_id
1666
+ ? (childrenById.get(row.session_id) || []).length
1667
+ : 0;
1668
+ const nested = row.source === "codex" && row.session_id
1669
+ ? aggregate(row)
1670
+ : { count: 0, tokens: 0, cost: 0 };
1671
+ row.descendant_subagent_count = nested.count;
1672
+ row.subagent_total_tokens = nested.tokens;
1673
+ row.subagent_cost_usd = nested.cost;
1674
+ row.combined_total_tokens = finite(row.total_tokens) + nested.tokens;
1675
+ row.combined_cost_usd = finite(row.cost_usd) + nested.cost;
1676
+ if (row.source === "codex" && row.session_id) {
1677
+ const parent = byId.get(linkedParentById.get(row.session_id));
1678
+ row.parent_session_hash = parent?.session_hash || null;
1679
+ row.root_session_hash = rootFor(row).session_hash || row.session_hash;
1680
+ }
1681
+ }
1682
+ return rows;
1683
+ }
1684
+
1430
1685
  // Local-only session list for the dashboard session browser. Unlike
1431
1686
  // summarizeSessions (cloud/CSV safe), this retains session_id + project_ref so
1432
1687
  // the UI can offer one-click resume. Callers must only expose it over the
1433
1688
  // local API.
1434
1689
  function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}) {
1435
- const filtered = mergeSessionFragments(sessions)
1690
+ const filtered = annotateCodexThreadUsage(mergeSessionFragments(sessions)
1436
1691
  // Only sessions that actually spent tokens are worth listing. This drops
1437
1692
  // two kinds of noise: non-session logs under ~/.claude/projects
1438
1693
  // (skill-injections.jsonl, journal.jsonl, …), and sessions abandoned before
1439
1694
  // the model replied. Both render as "unknown · 0 tokens · $0.00" rows with
1440
1695
  // no model, no cost and nothing to analyze.
1441
1696
  .filter((row) => finite(row.total_tokens) > 0)
1442
- .filter((row) => withinDayRange(row, from, to));
1697
+ .filter((row) => withinDayRange(row, from, to)));
1443
1698
  filtered.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
1444
1699
  const cap = Number(limit) > 0 ? Number(limit) : 0;
1445
1700
  const limited = cap > 0 ? filtered.slice(0, cap) : filtered;
@@ -6,7 +6,7 @@
6
6
  * Privacy contract (documented in README "Privacy" section — keep in sync):
7
7
  * the payload carries exactly four fields — a one-way sha256 hash of the
8
8
  * machine id, the app version, `process.platform`, and the app shell
9
- * (cli / macos / windows). No token counts, no model names, no usernames,
9
+ * (cli / macos / windows / linux). No token counts, no model names, no usernames,
10
10
  * no paths. Opt out with TOKENTRACKER_NO_TELEMETRY=1, the DO_NOT_TRACK
11
11
  * standard, or `"telemetry": false` in config.json.
12
12
  */
@@ -35,7 +35,7 @@ const MACHINE_HASH_NAMESPACE = "tokentracker-telemetry-v1:";
35
35
  // cloud host; sync awaits this at most once per day so the worst-case stall
36
36
  // is bounded and rare.
37
37
  const DEFAULT_TIMEOUT_MS = 10_000;
38
- const VALID_SHELLS = new Set(["cli", "macos", "windows"]);
38
+ const VALID_SHELLS = new Set(["cli", "macos", "windows", "linux"]);
39
39
 
40
40
  const THROTTLE_CONFIG = {
41
41
  intervalMs: HEARTBEAT_INTERVAL_MS,
@@ -1882,20 +1882,30 @@ function describeCopilotOtelStatus({ home, env = process.env } = {}) {
1882
1882
  const explicitPath = typeof env.COPILOT_OTEL_FILE_EXPORTER_PATH === "string"
1883
1883
  ? env.COPILOT_OTEL_FILE_EXPORTER_PATH
1884
1884
  : "";
1885
- const defaultDir = path.join(resolvedHome, ".copilot", "otel");
1886
- let hasFiles = false;
1887
- try {
1888
- if (fs.existsSync(defaultDir)) {
1889
- hasFiles = fs.readdirSync(defaultDir).some((entry) => entry.endsWith(".jsonl"));
1890
- }
1891
- } catch (_e) {}
1892
- if (!hasFiles && explicitPath && fs.existsSync(explicitPath)) hasFiles = true;
1885
+ const defaultDirs = [
1886
+ path.join(resolvedHome, ".copilot", "otel"),
1887
+ path.join(resolvedHome, ".copilot-otel"),
1888
+ ];
1889
+ const detectedPaths = [];
1890
+ for (const defaultDir of defaultDirs) {
1891
+ try {
1892
+ if (!fs.existsSync(defaultDir)) continue;
1893
+ for (const entry of fs.readdirSync(defaultDir)) {
1894
+ if (entry.endsWith(".jsonl")) {
1895
+ detectedPaths.push(path.join(defaultDir, entry));
1896
+ }
1897
+ }
1898
+ } catch (_e) {}
1899
+ }
1900
+ if (explicitPath && fs.existsSync(explicitPath)) detectedPaths.push(explicitPath);
1893
1901
  return {
1894
1902
  otel_enabled: enabled && (exporterType === "" || exporterType === "file"),
1895
1903
  otel_exporter_type: exporterType || null,
1896
1904
  otel_path: explicitPath || null,
1897
- otel_default_dir: defaultDir,
1898
- otel_has_files: hasFiles,
1905
+ otel_default_dir: defaultDirs[0],
1906
+ otel_default_dirs: defaultDirs,
1907
+ otel_detected_paths: detectedPaths,
1908
+ otel_has_files: detectedPaths.length > 0,
1899
1909
  };
1900
1910
  }
1901
1911
 
@@ -1 +0,0 @@
1
- import{r as n,z as a}from"./__vite-browser-external-CsbVaBbw.js";import{u as p}from"./DialogTitle-BkAmJrQ_.js";import{u as c}from"./composite-MhgOkYfv.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,i=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};