tokentracker-cli 1.4.2 → 1.4.4

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 (111) hide show
  1. package/README.md +4 -0
  2. package/dashboard/dist/assets/{AchievementBadge-WGbdoboF.js → AchievementBadge-slL-XBCu.js} +1 -1
  3. package/dashboard/dist/assets/{AchievementsPage-BvybbsXv.js → AchievementsPage-BeFkGc5M.js} +2 -2
  4. package/dashboard/dist/assets/{AchievementsSection-ChNzTFSR.js → AchievementsSection-BrGUIipD.js} +1 -1
  5. package/dashboard/dist/assets/{BadgeDetailModal-DhI9Ejz3.js → BadgeDetailModal-avmK1ODc.js} +1 -1
  6. package/dashboard/dist/assets/{BotAnimated-Bch0Yxej.js → BotAnimated-DaEPdEZi.js} +1 -1
  7. package/dashboard/dist/assets/{Card-DWdL7d1T.js → Card-CIZCccTA.js} +1 -1
  8. package/dashboard/dist/assets/{CloudCheckoutPage-BjGoOz6P.js → CloudCheckoutPage-BhRj0SYO.js} +1 -1
  9. package/dashboard/dist/assets/{CloudPage-Btb6a97x.js → CloudPage-CvaN8a54.js} +2 -2
  10. package/dashboard/dist/assets/{CommandPalette-DwP3k7LE.js → CommandPalette-BUNuXmYF.js} +1 -1
  11. package/dashboard/dist/assets/{CommunityStatsModal-mzqLxpwE.js → CommunityStatsModal-BCI0WHPr.js} +1 -1
  12. package/dashboard/dist/assets/{ConfirmModal-Dm76q4EX.js → ConfirmModal-CT7IwtLR.js} +1 -1
  13. package/dashboard/dist/assets/{Controls-ClZjOBoD.js → Controls-CtnMyxNH.js} +1 -1
  14. package/dashboard/dist/assets/{DashboardPage-Doc8f4NL.js → DashboardPage-BuzyITeL.js} +1 -1
  15. package/dashboard/dist/assets/{DateRangePopover-vfTUePPg.js → DateRangePopover-1Ps1-1dw.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-Jjg5q0C2.js → DevicePage-Dt-dVQAG.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-Ci5mPZ6X.js → DialogClose-D-DRTF1u.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-BOYDS1mM.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-DI21oZxH.js → DialogTitle-CiiRTn3_.js} +1 -1
  20. package/dashboard/dist/assets/{ElectricLogo-DfeZBwPZ.js → ElectricLogo-k6RFJ2qC.js} +1 -1
  21. package/dashboard/dist/assets/{FadeIn-C7SIILTc.js → FadeIn-CNHg83aK.js} +1 -1
  22. package/dashboard/dist/assets/{HeaderGithubStar-RStLbadn.js → HeaderGithubStar-CIYW5dc3.js} +1 -1
  23. package/dashboard/dist/assets/{HoverTooltip-DI7uAp1_.js → HoverTooltip-Dn80DyPM.js} +1 -1
  24. package/dashboard/dist/assets/{IpCheckPage-DTZNTJVh.js → IpCheckPage-B29dYfoI.js} +1 -1
  25. package/dashboard/dist/assets/{LandingPage-Df5scNlw.js → LandingPage-Dzc8_QyW.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardPage-Ck3SqtnI.js → LeaderboardPage-DXPcIhyQ.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardProBadge-CGx6m7R6.js → LeaderboardProBadge-DxEdj2Zq.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardProfileModal-DuP7rgfv.js → LeaderboardProfileModal-DnKY2223.js} +3 -3
  29. package/dashboard/dist/assets/{LeaderboardProfilePage-B5qUJkZX.js → LeaderboardProfilePage-CY94ngqy.js} +1 -1
  30. package/dashboard/dist/assets/{LimitsPage-Bv_Lcvcz.js → LimitsPage-AlvnAPCD.js} +1 -1
  31. package/dashboard/dist/assets/{LocalOnlyNotice-Cg4y02JM.js → LocalOnlyNotice-EHw7BeYS.js} +1 -1
  32. package/dashboard/dist/assets/{LoginCard-Wz0uJKXx.js → LoginCard-E5E3i2kj.js} +1 -1
  33. package/dashboard/dist/assets/{LoginModal-C13v_Gs0.js → LoginModal-DxhQI9kh.js} +1 -1
  34. package/dashboard/dist/assets/{LoginPage-COH9xXtd.js → LoginPage-DRwJn-Fw.js} +1 -1
  35. package/dashboard/dist/assets/{PetPage-BWCnURub.js → PetPage-DIpVTdiC.js} +2 -2
  36. package/dashboard/dist/assets/{PopoverPopup-Dw811N-_.js → PopoverPopup-Bjs7E91g.js} +1 -1
  37. package/dashboard/dist/assets/{ProviderIcon-CHxhYzK-.js → ProviderIcon-DqXIjym-.js} +1 -1
  38. package/dashboard/dist/assets/{ResetPasswordPage-Ll6jshqg.js → ResetPasswordPage-DxSRYQgP.js} +1 -1
  39. package/dashboard/dist/assets/{Select-BbooQ3sa.js → Select-BfjrcaiM.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-DBiW70wi.js → SelectItemText-Dpdlb0cU.js} +1 -1
  41. package/dashboard/dist/assets/{SelfHostPage-BMHJxz5q.js → SelfHostPage-Cnp_PMaD.js} +1 -1
  42. package/dashboard/dist/assets/{ServiceStatusPage-D6LH0ac-.js → ServiceStatusPage-fBwG-qXq.js} +1 -1
  43. package/dashboard/dist/assets/{SessionsPage-Mrer6v17.js → SessionsPage-BEk7AHdq.js} +1 -1
  44. package/dashboard/dist/assets/{SettingsPage-D_7P_y_l.js → SettingsPage-BgML2jQR.js} +3 -3
  45. package/dashboard/dist/assets/{SkillsPage-Ka7xm-1v.js → SkillsPage-DcvVZ3Wn.js} +1 -1
  46. package/dashboard/dist/assets/{TokenGalaxy-BEURDPFq.js → TokenGalaxy-D29yX2Hb.js} +1 -1
  47. package/dashboard/dist/assets/{TooltipPopup-ZiuFFARC.js → TooltipPopup-CjzmNK2l.js} +1 -1
  48. package/dashboard/dist/assets/{TrendMonitor-H73lyGEf.js → TrendMonitor-DfnQrsrx.js} +1 -1
  49. package/dashboard/dist/assets/{WidgetsPage-_7iCvyTk.js → WidgetsPage-CRYomYfk.js} +1 -1
  50. package/dashboard/dist/assets/{WrappedPage-DEXNb6-k.js → WrappedPage-CvQI5zUw.js} +1 -1
  51. package/dashboard/dist/assets/{__vite-browser-external-BkNP5Ecj.js → __vite-browser-external-BZ-i40Kf.js} +2 -2
  52. package/dashboard/dist/assets/{agent-logos-OVBc_0cF.js → agent-logos-CvOX7WXW.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-DDphsclm.js → arrow-left-BrQrZJtW.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-B1aNq0Ki.js → arrow-right-fC11gmFY.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-yTM9azz_.js → arrow-up-right-CCMEqQQp.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-D22ithNF.js → calendar-days-OaIQ06qD.js} +1 -1
  57. package/dashboard/dist/assets/{check-Dk7tknvE.js → check-D888uFpx.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-DdyEzQ4f.js → chevron-down-BJ0BpS_w.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-BJjbipnH.js → cloud-checkout-BeOpEJTZ.js} +1 -1
  60. package/dashboard/dist/assets/{composite-DEqP00XV.js → composite-CUuXvKos.js} +1 -1
  61. package/dashboard/dist/assets/{download-C9ixNyzn.js → download-BJ7CKwx5.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-DL4HSsLw.js → external-link-CDaZwJVN.js} +1 -1
  63. package/dashboard/dist/assets/{flame-DqNH65nB.js → flame-ChyUKs-9.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-CyVjKz4I.js → format-tokens-B6WX7zcH.js} +1 -1
  65. package/dashboard/dist/assets/{gift-ChJQQe_G.js → gift-CQlAZUJ5.js} +1 -1
  66. package/dashboard/dist/assets/{host-mode-DpUDpoOz.js → host-mode-Gw6KD5OJ.js} +1 -1
  67. package/dashboard/dist/assets/{icons-CJiHeOaU.js → icons-C7gGTMuQ.js} +1 -1
  68. package/dashboard/dist/assets/{index-72LQsAaf.js → index-Bm2dDAVL.js} +1 -1
  69. package/dashboard/dist/assets/{index-BucEg3fu.js → index-C0pbymQa.js} +1 -1
  70. package/dashboard/dist/assets/{index-CrFlTSra.js → index-CLb64Tlj.js} +1 -1
  71. package/dashboard/dist/assets/{info-DReYqxC7.js → info-CvOmd88O.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-kIZbhK2S.js → link-2-BNEPxvlF.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-1An-X85y.js → loader-circle-bRHxCgY3.js} +1 -1
  74. package/dashboard/dist/assets/{modifiers.esm-FoVheEKT.js → modifiers.esm-D-X0-uvt.js} +1 -1
  75. package/dashboard/dist/assets/{monitor-smartphone-Ch6lr_P2.js → monitor-smartphone-Bo7d-q73.js} +1 -1
  76. package/dashboard/dist/assets/{play-CbJ6ZF1K.js → play-DqpGELN5.js} +1 -1
  77. package/dashboard/dist/assets/{plus-C-5ID3a9.js → plus-2fA1tz7S.js} +1 -1
  78. package/dashboard/dist/assets/{provider-display-BFzRtoL2.js → provider-display-B1KigDYZ.js} +1 -1
  79. package/dashboard/dist/assets/{react-D4CR_fLQ.js → react-DYja9tZG.js} +1 -1
  80. package/dashboard/dist/assets/{safePolygon-DP21Ibqj.js → safePolygon-BerB8y-A.js} +1 -1
  81. package/dashboard/dist/assets/{search-CaznkY7U.js → search-CU6UVI1L.js} +1 -1
  82. package/dashboard/dist/assets/{server-CUOmC6xZ.js → server-CZR4PTdb.js} +1 -1
  83. package/dashboard/dist/assets/{shield-check-Dr2KBMmA.js → shield-check-BGbNmrMP.js} +1 -1
  84. package/dashboard/dist/assets/{skills-api-BAz_fblS.js → skills-api-CJLEzxDf.js} +1 -1
  85. package/dashboard/dist/assets/{sortable.esm-knEfm5hE.js → sortable.esm-CJ1NZWER.js} +1 -1
  86. package/dashboard/dist/assets/{store-CL97jcij.js → store-CJFdru_6.js} +1 -1
  87. package/dashboard/dist/assets/{terminal-EjFyreWj.js → terminal-ClyQ8tvQ.js} +1 -1
  88. package/dashboard/dist/assets/{trash-ByqaZXkQ.js → trash-Dg2PWl5-.js} +1 -1
  89. package/dashboard/dist/assets/{use-community-stats-Bs4RDJZE.js → use-community-stats-Ca995kap.js} +1 -1
  90. package/dashboard/dist/assets/{use-latest-request-guard-CG3Ir1cP.js → use-latest-request-guard-BKbMGVji.js} +1 -1
  91. package/dashboard/dist/assets/{use-limits-display-prefs-tACgCxeR.js → use-limits-display-prefs-CGyd8lvc.js} +1 -1
  92. package/dashboard/dist/assets/{use-native-settings-CWjXyJXi.js → use-native-settings-Dqeuqm9O.js} +1 -1
  93. package/dashboard/dist/assets/{use-ordered-list-DsuC9JQa.js → use-ordered-list-Nj7R1iUu.js} +1 -1
  94. package/dashboard/dist/assets/use-reduced-motion-CGMbDpHQ.js +1 -0
  95. package/dashboard/dist/assets/{use-session-efficiency-pref-NeoJE8BL.js → use-session-efficiency-pref-C_H5BaGV.js} +1 -1
  96. package/dashboard/dist/assets/{use-transform-BWGKOsya.js → use-transform-CC90hurD.js} +1 -1
  97. package/dashboard/dist/assets/{use-usage-limits-CFWFNwHS.js → use-usage-limits-DtaaE7D8.js} +1 -1
  98. package/dashboard/dist/assets/{useCurrency-B_C4_P5x.js → useCurrency-CMHIL39_.js} +1 -1
  99. package/dashboard/dist/assets/{usePositioner-B0L5_o38.js → usePositioner-CRSfrGQq.js} +1 -1
  100. package/dashboard/dist/assets/{useTokenFormat-DH1GWHSU.js → useTokenFormat-B5jhM_Kb.js} +1 -1
  101. package/dashboard/dist/assets/{zap-Chv1p7pS.js → zap-CKx8ySZs.js} +1 -1
  102. package/dashboard/dist/index.html +1 -1
  103. package/dashboard/dist/ip-check.html +1 -1
  104. package/dashboard/dist/leaderboard.html +1 -1
  105. package/dashboard/dist/share.html +1 -1
  106. package/package.json +1 -1
  107. package/src/commands/sync.js +25 -3
  108. package/src/lib/pricing/seed-snapshot.json +1 -1
  109. package/src/lib/rollout.js +334 -52
  110. package/dashboard/dist/assets/DialogDescription-Di5ev5K6.js +0 -1
  111. package/dashboard/dist/assets/use-reduced-motion-CwFguzkm.js +0 -1
@@ -10802,14 +10802,102 @@ function workbuddySqliteUsageSnapshot(row) {
10802
10802
  };
10803
10803
  }
10804
10804
 
10805
+ // WorkBuddy writes OpenAI/DeepSeek-shaped rawUsage on every LLM round-trip.
10806
+ // The totals pass and the project pass below need the same breakdown, so the
10807
+ // arithmetic lives here once. Callers treat total_tokens === 0 as "no billable
10808
+ // usage": every component is non-negative, so that is equivalent to testing
10809
+ // each field for zero.
10810
+ function workbuddyUsageDelta(rawUsage) {
10811
+ const promptTokens = toNonNegativeInt(rawUsage.prompt_tokens);
10812
+ const completionTokens = toNonNegativeInt(rawUsage.completion_tokens);
10813
+ const promptDetails =
10814
+ rawUsage.prompt_tokens_details && typeof rawUsage.prompt_tokens_details === "object"
10815
+ ? rawUsage.prompt_tokens_details
10816
+ : {};
10817
+ const completionDetails =
10818
+ rawUsage.completion_tokens_details && typeof rawUsage.completion_tokens_details === "object"
10819
+ ? rawUsage.completion_tokens_details
10820
+ : {};
10821
+
10822
+ // Cache reads are mirrored across up to three fields depending on which
10823
+ // upstream the auto-router used; take the largest non-zero mirror.
10824
+ const cacheRead = Math.max(
10825
+ toNonNegativeInt(rawUsage.cache_read_input_tokens),
10826
+ toNonNegativeInt(promptDetails.cached_tokens),
10827
+ toNonNegativeInt(rawUsage.prompt_cache_hit_tokens),
10828
+ );
10829
+ const cacheCreation = toNonNegativeInt(rawUsage.cache_creation_input_tokens);
10830
+ // prompt_tokens is the FULL prompt: subtract BOTH reads and writes so
10831
+ // input_tokens is pure non-cached input (no double-counting cache writes).
10832
+ const inputTokens = Math.max(0, promptTokens - cacheRead - cacheCreation);
10833
+ // completion_tokens INCLUDES reasoning (verified: total == prompt+completion).
10834
+ const reasoningTokens = Math.min(
10835
+ completionTokens,
10836
+ toNonNegativeInt(completionDetails.reasoning_tokens),
10837
+ );
10838
+ const outputTokens = Math.max(0, completionTokens - reasoningTokens);
10839
+
10840
+ return {
10841
+ input_tokens: inputTokens,
10842
+ cached_input_tokens: cacheRead,
10843
+ cache_creation_input_tokens: cacheCreation,
10844
+ output_tokens: outputTokens,
10845
+ reasoning_output_tokens: reasoningTokens,
10846
+ total_tokens: inputTokens + outputTokens + cacheRead + cacheCreation + reasoningTokens,
10847
+ conversation_count: 1,
10848
+ };
10849
+ }
10850
+
10851
+ // WorkBuddy stamps `cwd` on its records rather than in a session header, and a
10852
+ // session can open with a record that lacks it while later appends omit it
10853
+ // altogether. Resolve it from a bounded prefix of the file so every record in
10854
+ // the incremental window is attributed, not just the ones that happen to carry
10855
+ // their own cwd. A file with no recoverable cwd stays outside project
10856
+ // attribution — there is nothing to attribute it to.
10857
+ async function resolveWorkbuddyFileCwd(filePath, maxScanBytes = 8 * 1024 * 1024) {
10858
+ let stream;
10859
+ try {
10860
+ const stat = fssync.statSync(filePath);
10861
+ if (stat.size <= 0) return null;
10862
+ stream = fssync.createReadStream(filePath, {
10863
+ start: 0,
10864
+ end: Math.min(stat.size, maxScanBytes) - 1,
10865
+ });
10866
+ } catch {
10867
+ return null;
10868
+ }
10869
+ let cwd = null;
10870
+ try {
10871
+ for await (const record of physicalJsonlRecords(stream, {
10872
+ invalidUtf8: "record",
10873
+ maxRecordBytes: OMP_LIKE_MAX_RECORD_BYTES,
10874
+ })) {
10875
+ if (!record.line || !record.line.trim()) continue;
10876
+ let entry;
10877
+ try { entry = JSON.parse(record.line); } catch { continue; }
10878
+ if (entry && typeof entry === "object" && typeof entry.cwd === "string" && entry.cwd) {
10879
+ cwd = entry.cwd;
10880
+ break;
10881
+ }
10882
+ }
10883
+ } catch {
10884
+ // No recoverable cwd — the file stays un-attributed.
10885
+ } finally {
10886
+ try { stream.destroy(); } catch {}
10887
+ }
10888
+ return cwd;
10889
+ }
10890
+
10805
10891
  async function parseWorkbuddyIncremental({
10806
10892
  projectFiles,
10807
10893
  cursors,
10808
10894
  queuePath,
10895
+ projectQueuePath,
10809
10896
  onProgress,
10810
10897
  env,
10811
10898
  defaultModel,
10812
10899
  source = "workbuddy",
10900
+ publicRepoResolver,
10813
10901
  } = {}) {
10814
10902
  await ensureDir(path.dirname(queuePath));
10815
10903
  // `source` doubles as the variant selector and the bucket source key, so the
@@ -10841,6 +10929,45 @@ async function parseWorkbuddyIncremental({
10841
10929
  );
10842
10930
  const detailedSessionsWithUsage = new Set();
10843
10931
 
10932
+ // The shared normalizers copy the bucket maps but alias each bucket/totals
10933
+ // object, and the enqueue helpers stamp queuedKey before appendFile runs.
10934
+ // Give the buckets this parser can mutate private copies so a failed append
10935
+ // leaves the caller's published state untouched — the same staging contract
10936
+ // the Devin reader uses. Callers retry from unchanged offsets, so a polluted
10937
+ // published state would add the same usage twice.
10938
+ const ownsWorkbuddyBucket = (key) =>
10939
+ (normalizeSourceInput(key.split(BUCKET_SEPARATOR)[0]) || DEFAULT_SOURCE) === variant;
10940
+ const ownsWorkbuddyProjectBucket = (key) => {
10941
+ // Project keys are `projectKey|source|hour`; a projectKey may itself
10942
+ // contain the separator, so read the source segment from the right.
10943
+ const last = key.lastIndexOf(BUCKET_SEPARATOR);
10944
+ const prev = last > 0 ? key.lastIndexOf(BUCKET_SEPARATOR, last - 1) : -1;
10945
+ const source = prev >= 0 ? key.slice(prev + 1, last) : "";
10946
+ return (normalizeSourceInput(source) || DEFAULT_SOURCE) === variant;
10947
+ };
10948
+
10949
+ // Project attribution runs as a second, independent pass over the same JSONL
10950
+ // files (see "Project pass" below). It keeps its own offsets and seen-set, so
10951
+ // an install that already has totals backfills project history on upgrade
10952
+ // without re-counting a single token in the totals queue.
10953
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
10954
+ const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
10955
+ if (projectState) {
10956
+ projectState.buckets = stageProviderBuckets(projectState.buckets, ownsWorkbuddyProjectBucket);
10957
+ }
10958
+ const projectTouchedBuckets = projectEnabled ? new Set() : null;
10959
+ const projectMetaCache = projectEnabled ? new Map() : null;
10960
+ const publicRepoCache = projectEnabled ? new Map() : null;
10961
+ const projectSeenIds = new Set(
10962
+ Array.isArray(workbuddyState.projectSeenIds) ? workbuddyState.projectSeenIds : [],
10963
+ );
10964
+ const projectFileOffsets =
10965
+ workbuddyState.projectFileOffsets && typeof workbuddyState.projectFileOffsets === "object"
10966
+ ? { ...workbuddyState.projectFileOffsets }
10967
+ : {};
10968
+ let projectBucketsQueued = 0;
10969
+ let projectCursorUpdated = false;
10970
+
10844
10971
  const allFiles = Array.isArray(projectFiles)
10845
10972
  ? projectFiles
10846
10973
  : resolveWorkbuddyProjectFiles(env || process.env, variant);
@@ -10866,10 +10993,15 @@ async function parseWorkbuddyIncremental({
10866
10993
  detailedSessions,
10867
10994
  updatedAt: new Date().toISOString(),
10868
10995
  };
10869
- return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
10996
+ return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0, projectBucketsQueued: 0, projectCursorUpdated: false };
10870
10997
  }
10871
10998
 
10872
10999
  const hourlyState = normalizeHourlyState(cursors?.hourly);
11000
+ hourlyState.buckets = stageProviderBuckets(hourlyState.buckets, ownsWorkbuddyBucket);
11001
+ hourlyState.groupQueued =
11002
+ hourlyState.groupQueued && typeof hourlyState.groupQueued === "object"
11003
+ ? { ...hourlyState.groupQueued }
11004
+ : {};
10873
11005
  const touchedBuckets = new Set();
10874
11006
  const cb = typeof onProgress === "function" ? onProgress : null;
10875
11007
  let recordsProcessed = 0;
@@ -10941,39 +11073,8 @@ async function parseWorkbuddyIncremental({
10941
11073
 
10942
11074
  recordsProcessed++;
10943
11075
 
10944
- const promptTokens = toNonNegativeInt(rawUsage.prompt_tokens);
10945
- const completionTokens = toNonNegativeInt(rawUsage.completion_tokens);
10946
- const promptDetails =
10947
- rawUsage.prompt_tokens_details && typeof rawUsage.prompt_tokens_details === "object"
10948
- ? rawUsage.prompt_tokens_details
10949
- : {};
10950
- const completionDetails =
10951
- rawUsage.completion_tokens_details && typeof rawUsage.completion_tokens_details === "object"
10952
- ? rawUsage.completion_tokens_details
10953
- : {};
10954
-
10955
- // Cache reads are mirrored across up to three fields depending on which
10956
- // upstream the auto-router used; take the largest non-zero mirror.
10957
- const cacheRead = Math.max(
10958
- toNonNegativeInt(rawUsage.cache_read_input_tokens),
10959
- toNonNegativeInt(promptDetails.cached_tokens),
10960
- toNonNegativeInt(rawUsage.prompt_cache_hit_tokens),
10961
- );
10962
- const cacheCreation = toNonNegativeInt(rawUsage.cache_creation_input_tokens);
10963
- // prompt_tokens is the FULL prompt: subtract BOTH reads and writes so
10964
- // input_tokens is pure non-cached input (no double-counting cache writes).
10965
- const inputTokens = Math.max(0, promptTokens - cacheRead - cacheCreation);
10966
- // completion_tokens INCLUDES reasoning (verified: total == prompt+completion).
10967
- const reasoningTokens = Math.min(completionTokens, toNonNegativeInt(completionDetails.reasoning_tokens));
10968
- const outputTokens = Math.max(0, completionTokens - reasoningTokens);
10969
-
10970
- if (
10971
- inputTokens === 0 &&
10972
- outputTokens === 0 &&
10973
- cacheRead === 0 &&
10974
- cacheCreation === 0 &&
10975
- reasoningTokens === 0
10976
- ) {
11076
+ const delta = workbuddyUsageDelta(rawUsage);
11077
+ if (delta.total_tokens === 0) {
10977
11078
  seenIds.add(messageId);
10978
11079
  continue;
10979
11080
  }
@@ -10992,17 +11093,6 @@ async function parseWorkbuddyIncremental({
10992
11093
  normalizeModelInput(entry.model) ||
10993
11094
  fallbackModel;
10994
11095
 
10995
- const delta = {
10996
- input_tokens: inputTokens,
10997
- cached_input_tokens: cacheRead,
10998
- cache_creation_input_tokens: cacheCreation,
10999
- output_tokens: outputTokens,
11000
- reasoning_output_tokens: reasoningTokens,
11001
- total_tokens:
11002
- inputTokens + outputTokens + cacheRead + cacheCreation + reasoningTokens,
11003
- conversation_count: 1,
11004
- };
11005
-
11006
11096
  const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
11007
11097
  addTotals(bucket.totals, delta);
11008
11098
  touchedBuckets.add(bucketKey(variant, model, bucketStart));
@@ -11036,6 +11126,165 @@ async function parseWorkbuddyIncremental({
11036
11126
  };
11037
11127
  }
11038
11128
 
11129
+ // ── Project pass ───────────────────────────────────────────────────────
11130
+ // WorkBuddy stamps `cwd` on its records, so unlike the trace summaries below
11131
+ // there is nothing to infer: attribute each round-trip to the git repository
11132
+ // its cwd belongs to. This is a second loop over the same files with its own
11133
+ // cursor, so an install that already has totals backfills project history on
11134
+ // upgrade without touching (or double-counting) them.
11135
+ if (projectEnabled) {
11136
+ for (let fileIdx = 0; fileIdx < jsonlFiles.length; fileIdx++) {
11137
+ const filePath = jsonlFiles[fileIdx];
11138
+ let stat;
11139
+ try { stat = fssync.statSync(filePath); } catch { continue; }
11140
+
11141
+ const prevEntry = projectFileOffsets[filePath] || {};
11142
+ const prevSize = Number(prevEntry.size) || 0;
11143
+ const prevIno = prevEntry.ino;
11144
+ const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
11145
+ const truncated = stat.size < prevSize;
11146
+ const startOffset = truncated || inodeChanged ? 0 : prevSize;
11147
+ if (stat.size <= startOffset) continue;
11148
+
11149
+ // cwd is fixed for a session file's lifetime. Reuse the resolved value
11150
+ // while it is the same file, otherwise read it from the file head — not
11151
+ // from the incremental window, which may hold only appends that omit it.
11152
+ // Truncation (same inode, smaller size) must drop the cached cwd: the
11153
+ // replacement transcript may belong to a different checkout.
11154
+ let cwd =
11155
+ !inodeChanged && !truncated && typeof prevEntry.cwd === "string"
11156
+ ? prevEntry.cwd
11157
+ : null;
11158
+
11159
+ // No recoverable cwd yet: do not advance past history. A later append
11160
+ // may stamp cwd, and early records must still be attributable then.
11161
+ // Check the unchanged-probe cache BEFORE the head scan so idle syncs do
11162
+ // not re-read up to 8 MiB of JSONL on every pass.
11163
+ if (
11164
+ !cwd &&
11165
+ Number(prevEntry.noCwdProbedSize) === stat.size &&
11166
+ prevEntry.ino === stat.ino &&
11167
+ Number(prevEntry.size) === startOffset
11168
+ ) {
11169
+ continue;
11170
+ }
11171
+ if (!cwd) cwd = await resolveWorkbuddyFileCwd(filePath);
11172
+ if (!cwd) {
11173
+ projectCursorUpdated = true;
11174
+ projectFileOffsets[filePath] = {
11175
+ size: startOffset,
11176
+ mtimeMs: stat.mtimeMs,
11177
+ ino: stat.ino,
11178
+ noCwdProbedSize: stat.size,
11179
+ };
11180
+ continue;
11181
+ }
11182
+
11183
+ let projectRef = null;
11184
+ let projectKey = null;
11185
+ const projectContext = await resolveProjectContextForPath({
11186
+ startDir: wsl.mapWslCwdToUnc(cwd, filePath),
11187
+ projectMetaCache,
11188
+ publicRepoCache,
11189
+ publicRepoResolver,
11190
+ projectState,
11191
+ });
11192
+ projectRef = projectContext?.projectRef || null;
11193
+ projectKey = projectContext?.projectKey || null;
11194
+
11195
+ // cwd resolved but the repo is not yet public_verified (or has no git
11196
+ // checkout): keep the offset unadvanced so a later sync can retry, but
11197
+ // do not JSON-parse the transcript on every pass — there is nothing to
11198
+ // attribute until both identifiers exist.
11199
+ if (!projectKey || !projectRef) {
11200
+ projectCursorUpdated = true;
11201
+ projectFileOffsets[filePath] = {
11202
+ size: startOffset,
11203
+ mtimeMs: stat.mtimeMs,
11204
+ ino: stat.ino,
11205
+ cwd,
11206
+ };
11207
+ continue;
11208
+ }
11209
+
11210
+ const committedEndOffset = await scanOmpLikeTranscript(
11211
+ filePath,
11212
+ startOffset,
11213
+ OMP_LIKE_MAX_RECORD_BYTES,
11214
+ (entry) => {
11215
+ // Mirror the totals pass: a JSON null / non-object line must not
11216
+ // abort the whole WorkBuddy parse (and strand both cursors).
11217
+ if (!entry || typeof entry !== "object") return;
11218
+
11219
+ const provider = entry.providerData;
11220
+ const rawUsage = provider && typeof provider === "object" ? provider.rawUsage : null;
11221
+ if (!rawUsage || typeof rawUsage !== "object") return;
11222
+
11223
+ // Mirror the totals pass's suppression rules so the project queue
11224
+ // never reports a session the totals queue deliberately skipped.
11225
+ const sessionId =
11226
+ typeof entry.sessionId === "string" && entry.sessionId
11227
+ ? entry.sessionId
11228
+ : path.basename(filePath, ".jsonl");
11229
+ const sqliteSession = sqliteSessions[sessionId];
11230
+ if (sqliteSession?.detailed || tracedSessionIds.has(sessionId)) return;
11231
+
11232
+ const tsMs =
11233
+ Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
11234
+ ? Number(entry.timestamp)
11235
+ : null;
11236
+ if (tsMs == null) return;
11237
+
11238
+ // Same response-level id preference as the totals pass, so both
11239
+ // queues dedup on identical keys.
11240
+ const messageId =
11241
+ typeof provider?.messageId === "string" && provider.messageId
11242
+ ? provider.messageId
11243
+ : typeof entry.uuid === "string" && entry.uuid
11244
+ ? entry.uuid
11245
+ : typeof entry.id === "string" && entry.id
11246
+ ? entry.id
11247
+ : `${sessionId}:${tsMs}`;
11248
+ if (projectSeenIds.has(messageId)) return;
11249
+ projectSeenIds.add(messageId);
11250
+ // The totals pass also registers the raw entry id so an older or
11251
+ // rewritten copy of the same record cannot be counted twice; the
11252
+ // project pass has to keep the same alias.
11253
+ if (typeof entry.id === "string" && entry.id && entry.id !== messageId) {
11254
+ projectSeenIds.add(entry.id);
11255
+ }
11256
+
11257
+ const delta = workbuddyUsageDelta(rawUsage);
11258
+ if (delta.total_tokens === 0) return;
11259
+
11260
+ const bucketStart = toUtcHalfHourStart(new Date(tsMs).toISOString());
11261
+ if (!bucketStart) return;
11262
+
11263
+ const projectBucket = getProjectBucket(
11264
+ projectState,
11265
+ projectKey,
11266
+ source,
11267
+ bucketStart,
11268
+ projectRef,
11269
+ );
11270
+ addTotals(projectBucket.totals, delta);
11271
+ projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
11272
+ },
11273
+ );
11274
+
11275
+ // Commit only past records that were read in full: a record appended
11276
+ // while this pass was running, or an unfinished tail line, has to stay
11277
+ // visible to the next sync. Only advance after successful attribution.
11278
+ projectCursorUpdated = true;
11279
+ projectFileOffsets[filePath] = {
11280
+ size: committedEndOffset,
11281
+ mtimeMs: stat.mtimeMs,
11282
+ ino: stat.ino,
11283
+ cwd,
11284
+ };
11285
+ }
11286
+ }
11287
+
11039
11288
  // Trace summaries are a fallback for sessions whose JSONL has no detailed
11040
11289
  // providerData.rawUsage. A trace's modelInfo totals include the cache read
11041
11290
  // portion of input, so split input into uncached input + cached input. Do
@@ -11294,6 +11543,23 @@ async function parseWorkbuddyIncremental({
11294
11543
  ? tracedSessionArr.slice(tracedSessionArr.length - 10_000)
11295
11544
  : tracedSessionArr;
11296
11545
 
11546
+ const projectSeenArr = Array.from(projectSeenIds);
11547
+ const cappedProjectSeenIds =
11548
+ projectSeenArr.length > 10_000
11549
+ ? projectSeenArr.slice(projectSeenArr.length - 10_000)
11550
+ : projectSeenArr;
11551
+
11552
+ // Publish only after both appends succeed: a throw here leaves `cursors`
11553
+ // untouched, so a retry re-derives the same contribution from the same
11554
+ // offsets instead of adding it on top of a half-applied state.
11555
+ projectBucketsQueued = projectEnabled
11556
+ ? await enqueueTouchedProjectBuckets({
11557
+ projectQueuePath,
11558
+ projectState,
11559
+ projectTouchedBuckets,
11560
+ })
11561
+ : 0;
11562
+
11297
11563
  const bucketsQueued = await enqueueTouchedBuckets({
11298
11564
  queuePath,
11299
11565
  hourlyState,
@@ -11302,6 +11568,10 @@ async function parseWorkbuddyIncremental({
11302
11568
  const updatedAt = new Date().toISOString();
11303
11569
  hourlyState.updatedAt = updatedAt;
11304
11570
  cursors.hourly = hourlyState;
11571
+ if (projectState) {
11572
+ projectState.updatedAt = updatedAt;
11573
+ cursors.projectHourly = projectState;
11574
+ }
11305
11575
  cursors[variant] = {
11306
11576
  ...workbuddyState,
11307
11577
  seenIds: cappedSeen,
@@ -11310,10 +11580,22 @@ async function parseWorkbuddyIncremental({
11310
11580
  fileOffsets,
11311
11581
  sqliteSessions: cappedSqliteSessions,
11312
11582
  detailedSessions: cappedDetailedSessions,
11583
+ ...(projectEnabled
11584
+ ? {
11585
+ projectSeenIds: cappedProjectSeenIds,
11586
+ projectFileOffsets,
11587
+ }
11588
+ : {}),
11313
11589
  updatedAt,
11314
11590
  };
11315
11591
 
11316
- return { recordsProcessed, eventsAggregated, bucketsQueued };
11592
+ return {
11593
+ recordsProcessed,
11594
+ eventsAggregated,
11595
+ bucketsQueued,
11596
+ projectBucketsQueued,
11597
+ projectCursorUpdated: projectEnabled && projectCursorUpdated,
11598
+ };
11317
11599
  }
11318
11600
 
11319
11601
  // ─────────────────────────────────────────────────────────────────────────────
@@ -14151,14 +14433,14 @@ function devinStringMap(value) {
14151
14433
  // Stage only the bucket objects this parser can mutate. The shared
14152
14434
  // normalizers copy the bucket maps but alias each bucket/totals object, and
14153
14435
  // the enqueue helpers stamp queuedKey before appendFile runs — so a failed
14154
- // append used to leave the caller's published state polluted. Only
14155
- // devin-owned entries get private copies; every other provider's buckets
14436
+ // append used to leave the caller's published state polluted. Only entries
14437
+ // the calling parser owns get private copies; every other provider's buckets
14156
14438
  // stay shared read-only references.
14157
- function stageDevinBuckets(buckets, isDevinBucket) {
14439
+ function stageProviderBuckets(buckets, isOwnedBucket) {
14158
14440
  const staged = {};
14159
14441
  for (const [key, bucket] of Object.entries(buckets || {})) {
14160
14442
  staged[key] =
14161
- bucket && typeof bucket === "object" && isDevinBucket(key, bucket)
14443
+ bucket && typeof bucket === "object" && isOwnedBucket(key, bucket)
14162
14444
  ? {
14163
14445
  ...bucket,
14164
14446
  totals:
@@ -14247,7 +14529,7 @@ async function parseDevinIncremental({
14247
14529
  // rows recover either queue. Unrelated providers' buckets stay shared
14248
14530
  // read-only references (this parser never writes their keys).
14249
14531
  const hourlyState = normalizeHourlyState(cursors?.hourly);
14250
- hourlyState.buckets = stageDevinBuckets(
14532
+ hourlyState.buckets = stageProviderBuckets(
14251
14533
  hourlyState.buckets,
14252
14534
  (key) =>
14253
14535
  (normalizeSourceInput(parseBucketKey(key).source) || DEFAULT_SOURCE) ===
@@ -14266,7 +14548,7 @@ async function parseDevinIncremental({
14266
14548
  if (projectState) {
14267
14549
  // Project bucket keys are `projectKey|source|hourStart`; this parser only
14268
14550
  // ever looks up keys whose middle segment is the devin source.
14269
- projectState.buckets = stageDevinBuckets(projectState.buckets, (key) => {
14551
+ projectState.buckets = stageProviderBuckets(projectState.buckets, (key) => {
14270
14552
  const last = key.lastIndexOf(BUCKET_SEPARATOR);
14271
14553
  const prev = last > 0 ? key.lastIndexOf(BUCKET_SEPARATOR, last - 1) : -1;
14272
14554
  const source = prev >= 0 ? key.slice(prev + 1, last) : "";
@@ -1 +0,0 @@
1
- import{r as n,K as a}from"./__vite-browser-external-BkNP5Ecj.js";import{u as p}from"./DialogTitle-DI21oZxH.js";import{u as c}from"./composite-DEqP00XV.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};
@@ -1 +0,0 @@
1
- import{r as o}from"./__vite-browser-external-BkNP5Ecj.js";import{h as r,i as t,p as s}from"./react-D4CR_fLQ.js";function i(){!r.current&&t();const[e]=o.useState(s.current);return e}export{i as u};