tokentracker-cli 0.84.5 → 0.84.7

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 (120) hide show
  1. package/README.de.md +10 -8
  2. package/README.ja.md +9 -7
  3. package/README.ko.md +9 -7
  4. package/README.md +12 -10
  5. package/README.zh-CN.md +9 -7
  6. package/dashboard/dist/assets/{AchievementBadge-DJvRfvu1.js → AchievementBadge-DAPsTMsx.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-Cr8WToxS.js → AchievementsPage-B9oXfXvm.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-BCPYB46e.js → AchievementsSection-DI_LTZb3.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D-12Ek-H.js → BadgeDetailModal--WCrJWVa.js} +1 -1
  10. package/dashboard/dist/assets/Card-BzbSN3EC.js +1 -0
  11. package/dashboard/dist/assets/{ClawdAnimated-DoGgAppT.js → ClawdAnimated-Dgxae1At.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-DIKocEAD.js → CommandPalette-BiZBYV5A.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-BNuMbEWy.js → CommunityStatsModal-C6jZx10_.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-F2gPIIS9.js → ConfirmModal-lD-mHMvO.js} +1 -1
  15. package/dashboard/dist/assets/{DashboardPage-YvoCEGG9.js → DashboardPage-DCPNoiRG.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-CbC_J22s.js → DevicePage-DiF2IOjJ.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-VEynQCYg.js → DialogClose-Crx2EFnI.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-CHsHo924.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-BbAVHI_v.js → DialogTitle-amHXttsp.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-CZVqSnis.js → FadeIn-CIhF_9PP.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-Cs_h332Y.js → HeaderGithubStar-D6Ph2GuY.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-DvvHTphK.js → HoverTooltip-BS8eO6tj.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-DPVmqSS_.js → IpCheckPage-Cz1Je6WS.js} +1 -1
  24. package/dashboard/dist/assets/LandingPage-DqeXJfwn.js +2 -0
  25. package/dashboard/dist/assets/{LeaderboardAvatar-BAPZ2ROX.js → LeaderboardAvatar-D09yLoVA.js} +1 -1
  26. package/dashboard/dist/assets/LeaderboardPage-BetpFetS.js +2 -0
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-r03akfe2.js → LeaderboardProfileModal-UW3KnANZ.js} +4 -4
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CU63YW2M.js → LeaderboardProfilePage-Chd_2AVY.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-uhpNteXJ.js +4 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-BHlcPCZk.js → LocalOnlyNotice-CpFFX8ro.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-C-dqUumR.js → LoginCard-DkZpGnVu.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BFzRg4Iy.js → LoginModal-CALv6jkD.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-QtlnGF1S.js → LoginPage-BZuXHYAT.js} +1 -1
  34. package/dashboard/dist/assets/PetPage-C1LuDyYr.js +1 -0
  35. package/dashboard/dist/assets/{PopoverPopup-CltmqcrT.js → PopoverPopup-CluVhrMr.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-BqOI9EiD.js → ProviderIcon-D3YKjVKb.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BsfCBUZD.js → ResetPasswordPage-CzN50nX5.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-D4biXw_Q.js → SegmentedControl-DFOAcEKU.js} +1 -1
  39. package/dashboard/dist/assets/{Select-DTw-14iE.js → Select-B3W_ihs_.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-3tUrnu3f.js → SelectItemText-FXRtXnFV.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-BItZqH7q.js +1 -0
  42. package/dashboard/dist/assets/{SessionsPage-DYYlNqHg.js → SessionsPage-DVQU-hTH.js} +2 -2
  43. package/dashboard/dist/assets/SettingsPage-ByvrwOi6.js +1 -0
  44. package/dashboard/dist/assets/SkillsPage-DrGr5Qij.js +1 -0
  45. package/dashboard/dist/assets/{TokenGalaxy-DU77e8sM.js → TokenGalaxy-ClF_n9od.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-DV3KGf-a.js → ToolbarRootContext-B82hGhAf.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-D6QYKmbl.js → TrendMonitor-DkWm7_1z.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DSP4jCsH.js → WidgetsPage-DCY4pyR2.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-B0x6O_DU.js → WrappedPage-yGQ4DLqj.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-BdRrPLk6.js +1483 -0
  51. package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-B2S_cdOL.js → agent-logos-D5SHGOxB.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-up-right-DNUVuhx2.js → arrow-up-right-CbMcAcss.js} +1 -1
  54. package/dashboard/dist/assets/{check-C3ARiiE_.js → check-Dyzh7lTz.js} +1 -1
  55. package/dashboard/dist/assets/{chevron-right-3bY7H8jO.js → chevron-right-DT06ZIKC.js} +1 -1
  56. package/dashboard/dist/assets/{copy-C-UIMKoO.js → copy-B3Kt3Wrf.js} +1 -1
  57. package/dashboard/dist/assets/{download-DtRzJfcr.js → download-B3rOytqI.js} +1 -1
  58. package/dashboard/dist/assets/external-link-BamTP0Ks.js +1 -0
  59. package/dashboard/dist/assets/{flame-Bas3ry-g.js → flame-msy2h1ms.js} +1 -1
  60. package/dashboard/dist/assets/{format-tokens-DFtRUIH8.js → format-tokens-8b1OUM_8.js} +1 -1
  61. package/dashboard/dist/assets/{icons-CHBt4Tmo.js → icons-C2NR2_Tf.js} +1 -1
  62. package/dashboard/dist/assets/{index-CZRlrK7w.js → index-C8sDJzUY.js} +1 -1
  63. package/dashboard/dist/assets/{index-C1ADnvzm.js → index-CvTfpiCF.js} +1 -1
  64. package/dashboard/dist/assets/{index-4unZlm2w.js → index-x3vFJQFt.js} +1 -1
  65. package/dashboard/dist/assets/{info-BkrO-fg3.js → info-C9j23aGq.js} +1 -1
  66. package/dashboard/dist/assets/limits-providers-ySB9Ci5l.js +1 -0
  67. package/dashboard/dist/assets/{link-2-DTLFz8uG.js → link-2-rrcyTT6S.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-BqQW_Ipq.js → loader-circle-x9x9I8Hv.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-YdpeE4dq.js → maximize-2-7TWFb2k7.js} +1 -1
  70. package/dashboard/dist/assets/{provider-display-CgBOpjq0.js → provider-display-JbSwpvxU.js} +1 -1
  71. package/dashboard/dist/assets/{react-DD9jpv--.js → react-DK5clHtY.js} +1 -1
  72. package/dashboard/dist/assets/{search--TvX-fig.js → search-DukXRvEV.js} +1 -1
  73. package/dashboard/dist/assets/{skills-api-C5C8a1at.js → skills-api-BUFJuwiR.js} +1 -1
  74. package/dashboard/dist/assets/{store-CRA8gPnP.js → store-BTh7bv09.js} +1 -1
  75. package/dashboard/dist/assets/{terminal-BXZyl2ek.js → terminal-CcUqLtn1.js} +1 -1
  76. package/dashboard/dist/assets/{trash-2-IFdBQ65v.js → trash-2-Bq7hnvNq.js} +1 -1
  77. package/dashboard/dist/assets/{use-community-stats-LyJ5aMpX.js → use-community-stats-qTpx6ugG.js} +1 -1
  78. package/dashboard/dist/assets/{use-limits-display-prefs-CKvFS1Nn.js → use-limits-display-prefs-Di_J4eVv.js} +1 -1
  79. package/dashboard/dist/assets/{use-native-settings-Cg-4j_0z.js → use-native-settings-CIqpv9Z3.js} +1 -1
  80. package/dashboard/dist/assets/{use-ordered-list-2pX5PXZO.js → use-ordered-list-OwogSXT7.js} +1 -1
  81. package/dashboard/dist/assets/use-reduced-motion-5QGGPoom.js +1 -0
  82. package/dashboard/dist/assets/{use-session-efficiency-pref-DilJOODA.js → use-session-efficiency-pref-CjAvMh7u.js} +1 -1
  83. package/dashboard/dist/assets/{use-transform-DiyYTLpI.js → use-transform-KZLdt-NZ.js} +1 -1
  84. package/dashboard/dist/assets/{use-usage-limits-DkUmp_nW.js → use-usage-limits-Tbifa037.js} +1 -1
  85. package/dashboard/dist/assets/{useCurrency-lm7KjGoD.js → useCurrency-D2td-bkn.js} +1 -1
  86. package/dashboard/dist/assets/{useScrollLock-DVSyJ-GE.js → useScrollLock-Cvyfyezc.js} +1 -1
  87. package/dashboard/dist/assets/useTokenFormat-DYIUS6IH.js +1 -0
  88. package/dashboard/dist/assets/{zap-CKZt1_i5.js → zap-B19mEed5.js} +1 -1
  89. package/dashboard/dist/brand-logos/qoder.svg +5 -0
  90. package/dashboard/dist/index.html +18 -15
  91. package/dashboard/dist/ip-check.html +2 -2
  92. package/dashboard/dist/leaderboard.html +2 -2
  93. package/dashboard/dist/llms.txt +3 -2
  94. package/dashboard/dist/share.html +2 -2
  95. package/package.json +2 -2
  96. package/src/commands/init.js +1 -0
  97. package/src/commands/status.js +34 -7
  98. package/src/commands/sync.js +353 -5
  99. package/src/lib/codex-rollout-parser.js +17 -39
  100. package/src/lib/codex-token-usage.js +195 -0
  101. package/src/lib/local-api.js +4 -1
  102. package/src/lib/pet-packages.js +44 -2
  103. package/src/lib/pricing/seed-snapshot.json +1 -1
  104. package/src/lib/provider-status.js +104 -0
  105. package/src/lib/qoder-limits.js +985 -0
  106. package/src/lib/rollout.js +744 -91
  107. package/src/lib/usage-limits.js +243 -43
  108. package/dashboard/dist/assets/Card-Ha3x9zIj.js +0 -1
  109. package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +0 -1
  110. package/dashboard/dist/assets/LandingPage-BCvNh3zE.js +0 -2
  111. package/dashboard/dist/assets/LeaderboardPage-DZSxazHP.js +0 -2
  112. package/dashboard/dist/assets/LimitsPage-JWZDa5_Y.js +0 -4
  113. package/dashboard/dist/assets/PetPage-C6UMY2qD.js +0 -1
  114. package/dashboard/dist/assets/SettingsPage-CYlEboSl.js +0 -1
  115. package/dashboard/dist/assets/SkillsPage-Bzq8V5Pd.js +0 -1
  116. package/dashboard/dist/assets/__vite-browser-external-D3llGq6p.js +0 -1465
  117. package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
  118. package/dashboard/dist/assets/limits-providers-BtwYmYHA.js +0 -1
  119. package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +0 -1
  120. package/dashboard/dist/assets/useTokenFormat-Li8BANKO.js +0 -1
@@ -1,5 +1,6 @@
1
1
  const fs = require("node:fs/promises");
2
2
  const fssync = require("node:fs");
3
+ const os = require("node:os");
3
4
  const path = require("node:path");
4
5
  const readline = require("node:readline");
5
6
 
@@ -8,6 +9,11 @@ const { ensureDir } = require("./fs");
8
9
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
9
10
  const wsl = require("./wsl-probe");
10
11
  const { resolveInstallPaths } = require("./install-resolver");
12
+ const {
13
+ consumeUsageDelta,
14
+ createUsageDeltaState,
15
+ snapshotUsageBaselines,
16
+ } = require("./codex-token-usage");
11
17
 
12
18
  const DEFAULT_SOURCE = "codex";
13
19
  const DEFAULT_MODEL = "unknown";
@@ -360,6 +366,9 @@ async function parseRolloutIncremental({
360
366
  const lastTotal = sameInode && !truncated && !rebuildingCodexBaseline
361
367
  ? prev.lastTotal || null
362
368
  : null;
369
+ const tokenUsageBaselines = sameInode && !truncated && !rebuildingCodexBaseline
370
+ ? prev.tokenUsageBaselines || null
371
+ : null;
363
372
  const lastModel = sameInode && !truncated ? prev.lastModel || null : null;
364
373
 
365
374
  const codexProjectFastPath = projectEnabled && fileSource === DEFAULT_SOURCE;
@@ -430,6 +439,7 @@ async function parseRolloutIncremental({
430
439
  filePath,
431
440
  fileStat: st,
432
441
  lastTotal,
442
+ tokenUsageBaselines,
433
443
  lastModel,
434
444
  projectState,
435
445
  projectMetaCache,
@@ -442,6 +452,7 @@ async function parseRolloutIncremental({
442
452
  fileStat: st,
443
453
  startOffset,
444
454
  lastTotal,
455
+ tokenUsageBaselines,
445
456
  lastModel,
446
457
  hourlyState,
447
458
  touchedBuckets,
@@ -462,6 +473,7 @@ async function parseRolloutIncremental({
462
473
  inode,
463
474
  offset: result.endOffset,
464
475
  lastTotal: result.lastTotal,
476
+ tokenUsageBaselines: result.tokenUsageBaselines,
465
477
  lastModel: result.lastModel,
466
478
  updatedAt: new Date().toISOString(),
467
479
  };
@@ -1731,6 +1743,7 @@ async function parseRolloutFile({
1731
1743
  fileStat,
1732
1744
  startOffset,
1733
1745
  lastTotal,
1746
+ tokenUsageBaselines,
1734
1747
  lastModel,
1735
1748
  hourlyState,
1736
1749
  touchedBuckets,
@@ -1751,14 +1764,25 @@ async function parseRolloutFile({
1751
1764
  const projectFileContexts = [];
1752
1765
  addProjectFileContext(projectFileContexts, projectContext);
1753
1766
  if (startOffset >= endOffset) {
1754
- return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
1767
+ return {
1768
+ endOffset,
1769
+ lastTotal,
1770
+ tokenUsageBaselines,
1771
+ lastModel,
1772
+ eventsAggregated: 0,
1773
+ projectFileContexts,
1774
+ };
1755
1775
  }
1756
1776
 
1757
1777
  const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
1758
1778
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1759
1779
 
1760
1780
  let model = typeof lastModel === "string" ? lastModel : null;
1761
- let totals = lastTotal && typeof lastTotal === "object" ? lastTotal : null;
1781
+ const usageDeltaState = createUsageDeltaState({
1782
+ lastTotal,
1783
+ baselines: tokenUsageBaselines,
1784
+ });
1785
+ let latestTotal = lastTotal && typeof lastTotal === "object" ? lastTotal : null;
1762
1786
  let currentCwd = null;
1763
1787
  let currentDate = null;
1764
1788
  let isForkedRollout = false;
@@ -1840,15 +1864,13 @@ async function parseRolloutFile({
1840
1864
 
1841
1865
  const lastUsage = info.last_token_usage;
1842
1866
  const totalUsage = info.total_token_usage;
1867
+ if (totalUsage && typeof totalUsage === "object") latestTotal = totalUsage;
1843
1868
 
1844
- const delta = pickDelta(lastUsage, totalUsage, totals);
1845
- if (!delta) continue;
1869
+ const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
1870
+ const delta = rawDelta ? normalizeUsage(rawDelta) : null;
1871
+ if (!delta || isAllZeroUsage(delta)) continue;
1846
1872
  delta.conversation_count = 1;
1847
1873
 
1848
- if (totalUsage && typeof totalUsage === "object") {
1849
- totals = totalUsage;
1850
- }
1851
-
1852
1874
  // Forked Codex rollouts replay the parent session's entire token history
1853
1875
  // into the child file the moment the fork is created. The date guard below
1854
1876
  // (current_date < rolloutDate) catches cross-day forks, but same-day forks
@@ -1863,8 +1885,8 @@ async function parseRolloutFile({
1863
1885
  // flush spacing and well below genuine turn cadence (≥~4.6s observed). The
1864
1886
  // first replayed row cannot be identified without lookahead, so it is still
1865
1887
  // counted (a bounded, <1% residual over-count); dropping real usage is the
1866
- // worse failure, so we bias against it. `totals` is already advanced above,
1867
- // keeping the cumulative-delta baseline correct for the live turns we keep.
1888
+ // worse failure, so we bias against it. `usageDeltaState` is already advanced above,
1889
+ // keeping the cumulative lineage correct for the live turns we keep.
1868
1890
  // Scoped to forked codex rollouts. (issue #169 follow-up.)
1869
1891
  let forkedReplaySkip = false;
1870
1892
  if (isForkedRollout && source === DEFAULT_SOURCE && replayPrefixActive) {
@@ -1903,8 +1925,8 @@ async function parseRolloutFile({
1903
1925
  // buckets. External tools rewrite session files without changing the token
1904
1926
  // data — Codex-Manager atomically rewrites them (new inode) to patch the
1905
1927
  // provider on every account/channel switch — so without dedup each switch
1906
- // double-counts the rewritten sessions. `totals` is already advanced above,
1907
- // so skipping an already-seen event keeps the cumulative-delta chain intact
1928
+ // double-counts the rewritten sessions. `usageDeltaState` is already advanced above,
1929
+ // so skipping an already-seen event keeps the cumulative lineage intact
1908
1930
  // while preventing the re-add; genuinely new turns carry new timestamps and
1909
1931
  // are still counted. Key = sessionUUID:eventTimestamp (both stable across the
1910
1932
  // rewrite and across a sessions/ -> archived_sessions/ move).
@@ -1939,13 +1961,21 @@ async function parseRolloutFile({
1939
1961
  eventsAggregated += 1;
1940
1962
  }
1941
1963
 
1942
- return { endOffset, lastTotal: totals, lastModel: model, eventsAggregated, projectFileContexts };
1964
+ return {
1965
+ endOffset,
1966
+ lastTotal: latestTotal,
1967
+ tokenUsageBaselines: snapshotUsageBaselines(usageDeltaState),
1968
+ lastModel: model,
1969
+ eventsAggregated,
1970
+ projectFileContexts,
1971
+ };
1943
1972
  }
1944
1973
 
1945
1974
  async function scanRolloutProjectFileContexts({
1946
1975
  filePath,
1947
1976
  fileStat,
1948
1977
  lastTotal,
1978
+ tokenUsageBaselines,
1949
1979
  lastModel,
1950
1980
  projectState,
1951
1981
  projectMetaCache,
@@ -1958,7 +1988,14 @@ async function scanRolloutProjectFileContexts({
1958
1988
  const projectFileContexts = [];
1959
1989
  addProjectFileContext(projectFileContexts, projectContext);
1960
1990
  if (!projectState || endOffset <= 0) {
1961
- return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
1991
+ return {
1992
+ endOffset,
1993
+ lastTotal,
1994
+ tokenUsageBaselines,
1995
+ lastModel,
1996
+ eventsAggregated: 0,
1997
+ projectFileContexts,
1998
+ };
1962
1999
  }
1963
2000
 
1964
2001
  const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: 0 });
@@ -2005,7 +2042,14 @@ async function scanRolloutProjectFileContexts({
2005
2042
  addProjectFileContext(projectFileContexts, context);
2006
2043
  }
2007
2044
 
2008
- return { endOffset, lastTotal, lastModel, eventsAggregated: 0, projectFileContexts };
2045
+ return {
2046
+ endOffset,
2047
+ lastTotal,
2048
+ tokenUsageBaselines,
2049
+ lastModel,
2050
+ eventsAggregated: 0,
2051
+ projectFileContexts,
2052
+ };
2009
2053
  }
2010
2054
 
2011
2055
  async function parseClaudeFile({
@@ -2861,6 +2905,15 @@ function normalizeOpencodeState(raw) {
2861
2905
  };
2862
2906
  }
2863
2907
 
2908
+ function normalizeQoderState(raw) {
2909
+ const state = raw && typeof raw === "object" ? raw : {};
2910
+ const messages = state.messages && typeof state.messages === "object" ? state.messages : {};
2911
+ return {
2912
+ messages,
2913
+ updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : null,
2914
+ };
2915
+ }
2916
+
2864
2917
  function normalizeMessageKeyPart(value) {
2865
2918
  if (typeof value !== "string") return "";
2866
2919
  return value.trim();
@@ -2954,6 +3007,33 @@ function addTotals(target, delta) {
2954
3007
  target.conversation_count += delta.conversation_count || 0;
2955
3008
  }
2956
3009
 
3010
+ function subtractTotals(target, totals) {
3011
+ target.input_tokens = Math.max(0, target.input_tokens - (totals.input_tokens || 0));
3012
+ target.cached_input_tokens = Math.max(
3013
+ 0,
3014
+ target.cached_input_tokens - (totals.cached_input_tokens || 0),
3015
+ );
3016
+ target.cache_creation_input_tokens = Math.max(
3017
+ 0,
3018
+ target.cache_creation_input_tokens - (totals.cache_creation_input_tokens || 0),
3019
+ );
3020
+ target.output_tokens = Math.max(0, target.output_tokens - (totals.output_tokens || 0));
3021
+ target.reasoning_output_tokens = Math.max(
3022
+ 0,
3023
+ target.reasoning_output_tokens - (totals.reasoning_output_tokens || 0),
3024
+ );
3025
+ target.total_tokens = Math.max(0, target.total_tokens - (totals.total_tokens || 0));
3026
+ target.billable_total_tokens = Math.max(
3027
+ 0,
3028
+ target.billable_total_tokens -
3029
+ (totals.billable_total_tokens ?? totals.total_tokens ?? 0),
3030
+ );
3031
+ target.conversation_count = Math.max(
3032
+ 0,
3033
+ target.conversation_count - (totals.conversation_count || 0),
3034
+ );
3035
+ }
3036
+
2957
3037
  function totalsKey(totals) {
2958
3038
  return [
2959
3039
  totals.input_tokens || 0,
@@ -3570,48 +3650,6 @@ function extractTokenCount(obj) {
3570
3650
  return null;
3571
3651
  }
3572
3652
 
3573
- function pickDelta(lastUsage, totalUsage, prevTotals) {
3574
- const hasLast = isNonEmptyObject(lastUsage);
3575
- const hasTotal = isNonEmptyObject(totalUsage);
3576
- const hasPrevTotals = isNonEmptyObject(prevTotals);
3577
-
3578
- if (hasTotal && hasPrevTotals) {
3579
- if (totalsReset(totalUsage, prevTotals)) {
3580
- const resetUsage = hasLast ? lastUsage : totalUsage;
3581
- const normalized = normalizeUsage(resetUsage);
3582
- return isAllZeroUsage(normalized) ? null : normalized;
3583
- }
3584
-
3585
- const delta = {};
3586
- for (const k of [
3587
- "input_tokens",
3588
- "cached_input_tokens",
3589
- "cache_creation_input_tokens",
3590
- "output_tokens",
3591
- "reasoning_output_tokens",
3592
- "total_tokens",
3593
- ]) {
3594
- const a = Number(totalUsage[k]);
3595
- const b = Number(prevTotals[k]);
3596
- if (Number.isFinite(a) && Number.isFinite(b)) delta[k] = Math.max(0, a - b);
3597
- }
3598
- const normalized = normalizeUsage(delta);
3599
- return isAllZeroUsage(normalized) ? null : normalized;
3600
- }
3601
-
3602
- if (hasLast) {
3603
- const normalized = normalizeUsage(lastUsage);
3604
- return isAllZeroUsage(normalized) ? null : normalized;
3605
- }
3606
-
3607
- if (hasTotal) {
3608
- const normalized = normalizeUsage(totalUsage);
3609
- return isAllZeroUsage(normalized) ? null : normalized;
3610
- }
3611
-
3612
- return null;
3613
- }
3614
-
3615
3653
  function normalizeUsage(u) {
3616
3654
  const out = {};
3617
3655
  for (const k of [
@@ -3694,13 +3732,6 @@ function isAllZeroUsage(u) {
3694
3732
  return true;
3695
3733
  }
3696
3734
 
3697
- function totalsReset(curr, prev) {
3698
- const currTotal = curr?.total_tokens;
3699
- const prevTotal = prev?.total_tokens;
3700
- if (!isFiniteNumber(currTotal) || !isFiniteNumber(prevTotal)) return false;
3701
- return currTotal < prevTotal;
3702
- }
3703
-
3704
3735
  function isFiniteNumber(v) {
3705
3736
  return typeof v === "number" && Number.isFinite(v);
3706
3737
  }
@@ -4046,6 +4077,349 @@ async function parseOpencodeDbIncremental({
4046
4077
  return { messagesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
4047
4078
  }
4048
4079
 
4080
+ const QODER_USAGE_SQL = `
4081
+ SELECT
4082
+ cm.rowid AS row_id,
4083
+ cm.id,
4084
+ cm.session_id,
4085
+ cm.request_id,
4086
+ cm.token_info,
4087
+ cm.model_info,
4088
+ cm.gmt_create,
4089
+ cr.extra AS record_extra,
4090
+ cs.preferred_model_info,
4091
+ cs.project_uri,
4092
+ cs.project_name
4093
+ FROM chat_message AS cm
4094
+ LEFT JOIN chat_record AS cr ON cr.request_id = cm.request_id
4095
+ LEFT JOIN chat_session AS cs ON cs.session_id = cm.session_id
4096
+ WHERE cm.role = 'assistant'
4097
+ AND cm.token_info IS NOT NULL
4098
+ AND trim(cm.token_info) NOT IN ('', '{}')
4099
+ ORDER BY cm.gmt_create, cm.rowid
4100
+ `;
4101
+
4102
+ function resolveQoderDbPath({
4103
+ home = os.homedir(),
4104
+ env = process.env,
4105
+ platform = process.platform,
4106
+ } = {}) {
4107
+ if (typeof env.QODER_DB_PATH === "string" && env.QODER_DB_PATH.trim()) {
4108
+ return path.resolve(env.QODER_DB_PATH.trim());
4109
+ }
4110
+ let root;
4111
+ if (typeof env.QODER_HOME === "string" && env.QODER_HOME.trim()) {
4112
+ root = path.resolve(env.QODER_HOME.trim());
4113
+ } else if (platform === "darwin") {
4114
+ root = path.join(home, "Library", "Application Support", "Qoder");
4115
+ } else if (platform === "win32") {
4116
+ root = path.join(
4117
+ env.APPDATA || path.join(home, "AppData", "Roaming"),
4118
+ "Qoder",
4119
+ );
4120
+ } else {
4121
+ root = path.join(home, ".config", "Qoder");
4122
+ }
4123
+ return path.join(root, "SharedClientCache", "cache", "db", "local.db");
4124
+ }
4125
+
4126
+ function resolveQoderDbPaths({
4127
+ home = os.homedir(),
4128
+ env = process.env,
4129
+ platform = process.platform,
4130
+ deps = {},
4131
+ } = {}) {
4132
+ const nativeValue = resolveQoderDbPath({ home, env, platform });
4133
+ if (platform !== "win32" || env.QODER_DB_PATH || env.QODER_HOME) {
4134
+ return { native: nativeValue, wsl: null };
4135
+ }
4136
+ const existsSync = deps.existsSync || fssync.existsSync;
4137
+ const native = wsl.shouldProbeNative(env) && existsSync(nativeValue) ? nativeValue : null;
4138
+ const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
4139
+ const wslRoot = wsl.shouldProbeWsl(env)
4140
+ ? discoverWslHome(".config/Qoder", { ...deps, env })
4141
+ : null;
4142
+ const wslValue = wslRoot
4143
+ ? path.join(wslRoot, "SharedClientCache", "cache", "db", "local.db")
4144
+ : null;
4145
+ const wslDb = wslValue && existsSync(wslValue) ? wslValue : null;
4146
+ return wsl.resolveAllWin32Paths({
4147
+ nativeValue: native,
4148
+ wslValue: wslDb,
4149
+ env,
4150
+ platform: "win32",
4151
+ });
4152
+ }
4153
+
4154
+ async function readQoderDbMessages(dbPath, sqliteOptions = {}) {
4155
+ if (!dbPath || !fssync.existsSync(dbPath)) return [];
4156
+ return readSqliteJsonRowsAsync(dbPath, QODER_USAGE_SQL, {
4157
+ label: "Qoder",
4158
+ ...sqliteOptions,
4159
+ });
4160
+ }
4161
+
4162
+ function parseJsonObject(value) {
4163
+ if (!value) return null;
4164
+ if (value && typeof value === "object" && !Buffer.isBuffer(value)) return value;
4165
+ try {
4166
+ const parsed = JSON.parse(String(value));
4167
+ return parsed && typeof parsed === "object" ? parsed : null;
4168
+ } catch (_error) {
4169
+ return null;
4170
+ }
4171
+ }
4172
+
4173
+ function normalizeQoderTokens(tokenInfo) {
4174
+ const tokens = parseJsonObject(tokenInfo);
4175
+ if (!tokens) return null;
4176
+ const prompt = Number(tokens.prompt_tokens);
4177
+ const cached = Number(tokens.cached_tokens || 0);
4178
+ const completion = Number(tokens.completion_tokens);
4179
+ if (
4180
+ !Number.isFinite(prompt) ||
4181
+ !Number.isFinite(cached) ||
4182
+ !Number.isFinite(completion) ||
4183
+ prompt < 0 ||
4184
+ cached < 0 ||
4185
+ completion < 0
4186
+ ) {
4187
+ return null;
4188
+ }
4189
+ // Qoder's prompt_tokens already includes cached_tokens. Keep cached input in
4190
+ // its own column and report only the remainder as ordinary input, otherwise
4191
+ // cached context is counted twice in dashboards and cost calculations.
4192
+ const input = Math.max(0, Math.trunc(prompt) - Math.trunc(cached));
4193
+ const cachedInput = Math.min(Math.trunc(prompt), Math.trunc(cached));
4194
+ const output = Math.trunc(completion);
4195
+ return {
4196
+ input_tokens: input,
4197
+ cached_input_tokens: cachedInput,
4198
+ cache_creation_input_tokens: 0,
4199
+ output_tokens: output,
4200
+ reasoning_output_tokens: 0,
4201
+ total_tokens: Math.trunc(prompt) + output,
4202
+ billable_total_tokens: Math.trunc(prompt) + output,
4203
+ };
4204
+ }
4205
+
4206
+ function qoderModelFromRow(row) {
4207
+ const direct = parseJsonObject(row?.model_info);
4208
+ const recordExtra = parseJsonObject(row?.record_extra);
4209
+ const preferred = parseJsonObject(row?.preferred_model_info);
4210
+ return (
4211
+ normalizeModelInput(direct?.model_key || direct?.modelKey) ||
4212
+ normalizeModelInput(recordExtra?.modelConfig?.key || recordExtra?.model_config?.key) ||
4213
+ normalizeModelInput(
4214
+ preferred?.model_key ||
4215
+ preferred?.modelKey ||
4216
+ preferred?.preferred_model ||
4217
+ preferred?.preferredModel,
4218
+ ) ||
4219
+ "qoder-agent"
4220
+ );
4221
+ }
4222
+
4223
+ function qoderMessageKey(row) {
4224
+ const id = normalizeMessageKeyPart(row?.id);
4225
+ const sessionId = normalizeMessageKeyPart(row?.session_id);
4226
+ if (sessionId && id) return `${sessionId}|${id}`;
4227
+ if (id) return id;
4228
+ const rowId = row?.row_id;
4229
+ return rowId === null || rowId === undefined ? null : `row:${rowId}`;
4230
+ }
4231
+
4232
+ function qoderProjectPath(row) {
4233
+ const raw = typeof row?.project_uri === "string" ? row.project_uri.trim() : "";
4234
+ if (!raw) return null;
4235
+ if (!raw.startsWith("file://")) return raw;
4236
+ try {
4237
+ return decodeURIComponent(new URL(raw).pathname);
4238
+ } catch (_error) {
4239
+ return null;
4240
+ }
4241
+ }
4242
+
4243
+ async function parseQoderDbIncremental({
4244
+ dbMessages,
4245
+ cursors,
4246
+ queuePath,
4247
+ projectQueuePath,
4248
+ onProgress,
4249
+ publicRepoResolver,
4250
+ } = {}) {
4251
+ await ensureDir(path.dirname(queuePath));
4252
+ const rows = Array.isArray(dbMessages) ? dbMessages : [];
4253
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
4254
+ const qoderState = normalizeQoderState(cursors?.qoder);
4255
+ const touchedBuckets = new Set();
4256
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
4257
+ const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
4258
+ const projectTouchedBuckets = projectEnabled ? new Set() : null;
4259
+ const projectMetaCache = projectEnabled ? new Map() : null;
4260
+ const publicRepoCache = projectEnabled ? new Map() : null;
4261
+ const requestOwners = new Map();
4262
+ for (const row of rows) {
4263
+ const messageKey = qoderMessageKey(row);
4264
+ if (
4265
+ !messageKey ||
4266
+ !normalizeQoderTokens(row?.token_info) ||
4267
+ !coerceEpochMs(row?.gmt_create)
4268
+ ) {
4269
+ continue;
4270
+ }
4271
+ const requestKey =
4272
+ normalizeMessageKeyPart(row?.request_id) ||
4273
+ normalizeMessageKeyPart(row?.session_id) ||
4274
+ messageKey;
4275
+ if (!requestOwners.has(requestKey)) requestOwners.set(requestKey, messageKey);
4276
+ }
4277
+ const cb = typeof onProgress === "function" ? onProgress : null;
4278
+ let messagesProcessed = 0;
4279
+ let eventsAggregated = 0;
4280
+
4281
+ for (let index = 0; index < rows.length; index++) {
4282
+ const row = rows[index];
4283
+ const messageKey = qoderMessageKey(row);
4284
+ const currentBase = normalizeQoderTokens(row?.token_info);
4285
+ const timestampMs = coerceEpochMs(row?.gmt_create);
4286
+ const bucketStart = timestampMs
4287
+ ? toUtcHalfHourStart(new Date(timestampMs).toISOString())
4288
+ : null;
4289
+ if (!messageKey || !currentBase || !bucketStart) {
4290
+ messagesProcessed += 1;
4291
+ continue;
4292
+ }
4293
+
4294
+ const requestKey =
4295
+ normalizeMessageKeyPart(row?.request_id) ||
4296
+ normalizeMessageKeyPart(row?.session_id) ||
4297
+ messageKey;
4298
+ const previous = qoderState.messages[messageKey];
4299
+ // Recompute request ownership from the complete ordered DB snapshot on
4300
+ // every pass. Qoder can attach token_info to an earlier assistant row only
4301
+ // after a later row was already counted; retaining the old per-message
4302
+ // owner in that case inflates one request to two conversations.
4303
+ const conversationCount = requestOwners.get(requestKey) === messageKey ? 1 : 0;
4304
+
4305
+ const currentTotals = {
4306
+ ...currentBase,
4307
+ conversation_count: conversationCount,
4308
+ };
4309
+ const model = qoderModelFromRow(row);
4310
+ let projectKey = null;
4311
+ let projectRef = null;
4312
+ if (projectEnabled) {
4313
+ const projectPath = qoderProjectPath(row);
4314
+ if (projectPath) {
4315
+ const context = await resolveProjectContextForPath({
4316
+ startDir: projectPath,
4317
+ projectMetaCache,
4318
+ publicRepoCache,
4319
+ publicRepoResolver,
4320
+ projectState,
4321
+ });
4322
+ projectKey = context?.projectKey || null;
4323
+ projectRef = context?.projectRef || null;
4324
+ }
4325
+ }
4326
+
4327
+ const previousTotals =
4328
+ previous?.totals && typeof previous.totals === "object" ? previous.totals : null;
4329
+ const unchanged =
4330
+ previousTotals &&
4331
+ totalsKey(previousTotals) === totalsKey(currentTotals) &&
4332
+ previous.bucketStart === bucketStart &&
4333
+ previous.model === model &&
4334
+ (previous.projectKey || null) === projectKey;
4335
+ if (!unchanged) {
4336
+ if (previousTotals && previous.bucketStart && previous.model) {
4337
+ const oldBucket = getHourlyBucket(
4338
+ hourlyState,
4339
+ "qoder",
4340
+ previous.model,
4341
+ previous.bucketStart,
4342
+ );
4343
+ subtractTotals(oldBucket.totals, previousTotals);
4344
+ touchedBuckets.add(bucketKey("qoder", previous.model, previous.bucketStart));
4345
+ if (projectEnabled && previous.projectKey) {
4346
+ const oldProjectBucket = getProjectBucket(
4347
+ projectState,
4348
+ previous.projectKey,
4349
+ "qoder",
4350
+ previous.bucketStart,
4351
+ previous.projectRef || null,
4352
+ );
4353
+ subtractTotals(oldProjectBucket.totals, previousTotals);
4354
+ projectTouchedBuckets.add(
4355
+ projectBucketKey(previous.projectKey, "qoder", previous.bucketStart),
4356
+ );
4357
+ }
4358
+ }
4359
+
4360
+ const bucket = getHourlyBucket(hourlyState, "qoder", model, bucketStart);
4361
+ addTotals(bucket.totals, currentTotals);
4362
+ touchedBuckets.add(bucketKey("qoder", model, bucketStart));
4363
+ if (projectEnabled && projectKey) {
4364
+ const projectBucket = getProjectBucket(
4365
+ projectState,
4366
+ projectKey,
4367
+ "qoder",
4368
+ bucketStart,
4369
+ projectRef,
4370
+ );
4371
+ addTotals(projectBucket.totals, currentTotals);
4372
+ projectTouchedBuckets.add(projectBucketKey(projectKey, "qoder", bucketStart));
4373
+ }
4374
+ qoderState.messages[messageKey] = {
4375
+ totals: currentTotals,
4376
+ conversationCount,
4377
+ requestKey,
4378
+ bucketStart,
4379
+ model,
4380
+ projectKey,
4381
+ projectRef,
4382
+ updatedAt: new Date().toISOString(),
4383
+ };
4384
+ eventsAggregated += 1;
4385
+ }
4386
+
4387
+ messagesProcessed += 1;
4388
+ if (cb) {
4389
+ cb({
4390
+ index: index + 1,
4391
+ total: rows.length,
4392
+ messagesProcessed,
4393
+ eventsAggregated,
4394
+ bucketsQueued: touchedBuckets.size,
4395
+ });
4396
+ }
4397
+ }
4398
+
4399
+ const bucketsQueued = await enqueueTouchedBuckets({
4400
+ queuePath,
4401
+ hourlyState,
4402
+ touchedBuckets,
4403
+ });
4404
+ const projectBucketsQueued = projectEnabled
4405
+ ? await enqueueTouchedProjectBuckets({
4406
+ projectQueuePath,
4407
+ projectState,
4408
+ projectTouchedBuckets,
4409
+ })
4410
+ : 0;
4411
+ const updatedAt = new Date().toISOString();
4412
+ hourlyState.updatedAt = updatedAt;
4413
+ qoderState.updatedAt = updatedAt;
4414
+ cursors.hourly = hourlyState;
4415
+ cursors.qoder = qoderState;
4416
+ if (projectState) {
4417
+ projectState.updatedAt = updatedAt;
4418
+ cursors.projectHourly = projectState;
4419
+ }
4420
+ return { messagesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
4421
+ }
4422
+
4049
4423
  // Resolve project context from DB message (no file path available)
4050
4424
  async function resolveProjectContextForDb({
4051
4425
  msg,
@@ -4940,32 +5314,77 @@ function resolveKiroCliDbPath(env = process.env) {
4940
5314
  // no hyphens. kiro-cli writes proper UUIDs; lock to the canonical shape.
4941
5315
  const KIRO_CLI_SESSION_FILE_RE =
4942
5316
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/i;
4943
-
4944
- // Lists ~/.kiro/sessions/cli/{uuid}.json files. Includes files whose sibling
4945
- // .lock is present we read those as tail-only snapshots so a running
4946
- // session's completed turns still land in the queue on the next sync. The
4947
- // .json files are rewritten atomically by kiro-cli on each turn flush, so
4948
- // a stale read just means we'll pick up the rest next time.
5317
+ // The directory prefix and direct messages.jsonl child are the stable
5318
+ // contract. Do not assume the suffix will always remain a UUID: Kiro has
5319
+ // already changed this layout once, and an opaque non-empty session id is
5320
+ // sufficient to keep discovery both bounded and forward-compatible.
5321
+ const KIRO_CLI_V2_SESSION_DIR_RE = /^sess_.+$/;
5322
+
5323
+ // Lists both Kiro CLI session layouts:
5324
+ // legacy: ~/.kiro/sessions/cli/{uuid}.json
5325
+ // 2.13+: ~/.kiro/sessions/{workspaceHash}/sess_{uuid}/messages.jsonl
5326
+ //
5327
+ // Legacy .json files are rewritten atomically per turn. The 2.13+ JSONL is
5328
+ // append-only and may be read while a session is active. Only the direct
5329
+ // messages.jsonl child is accepted; sub-executions and arbitrary nested JSONL
5330
+ // files are intentionally excluded.
4949
5331
  //
4950
5332
  // TASK-014: env.HOME is honored (symmetric with resolveKiroCliDbPath) so
4951
5333
  // callers can redirect to a tmp home for hermetic tests/CI.
4952
5334
  function resolveKiroCliSessionFiles(env = process.env) {
4953
5335
  const home = env.HOME || require("node:os").homedir();
4954
5336
  const kiroHome = env.KIRO_HOME || path.join(home, ".kiro");
4955
- const sessionsDir = path.join(kiroHome, "sessions", "cli");
4956
- if (!fssync.existsSync(sessionsDir)) return [];
5337
+ const sessionsRoot = path.join(kiroHome, "sessions");
5338
+ if (!fssync.existsSync(sessionsRoot)) return [];
4957
5339
  const files = [];
5340
+
5341
+ const legacyDir = path.join(sessionsRoot, "cli");
4958
5342
  try {
4959
- for (const entry of fssync.readdirSync(sessionsDir)) {
5343
+ for (const entry of fssync.readdirSync(legacyDir)) {
4960
5344
  // TASK-003: only canonical {uuid}.json files; backups, scratch,
4961
5345
  // typos are skipped so they don't feed JSON.parse garbage.
4962
5346
  if (!KIRO_CLI_SESSION_FILE_RE.test(entry)) continue;
4963
- files.push(path.join(sessionsDir, entry));
5347
+ files.push(path.join(legacyDir, entry));
4964
5348
  }
4965
5349
  } catch {
4966
5350
  // ignore read errors
4967
5351
  }
4968
- return files;
5352
+
5353
+ try {
5354
+ const workspaceDirs = fssync.readdirSync(sessionsRoot, {
5355
+ withFileTypes: true,
5356
+ });
5357
+ for (const workspace of workspaceDirs) {
5358
+ if (!workspace.isDirectory() || workspace.name === "cli") continue;
5359
+ const workspacePath = path.join(sessionsRoot, workspace.name);
5360
+ let sessionDirs;
5361
+ try {
5362
+ sessionDirs = fssync.readdirSync(workspacePath, {
5363
+ withFileTypes: true,
5364
+ });
5365
+ } catch {
5366
+ continue;
5367
+ }
5368
+ for (const session of sessionDirs) {
5369
+ if (
5370
+ !session.isDirectory() ||
5371
+ !KIRO_CLI_V2_SESSION_DIR_RE.test(session.name)
5372
+ ) {
5373
+ continue;
5374
+ }
5375
+ const messagesPath = path.join(
5376
+ workspacePath,
5377
+ session.name,
5378
+ "messages.jsonl",
5379
+ );
5380
+ if (fssync.existsSync(messagesPath)) files.push(messagesPath);
5381
+ }
5382
+ }
5383
+ } catch {
5384
+ // ignore read errors
5385
+ }
5386
+
5387
+ return files.sort();
4969
5388
  }
4970
5389
 
4971
5390
  // Build char-count maps from a .jsonl sibling file. Lets us approximate
@@ -5183,6 +5602,199 @@ async function readKiroCliSessionTurns(jsonPath) {
5183
5602
  return flat;
5184
5603
  }
5185
5604
 
5605
+ function kiroCliV2ContentChars(value) {
5606
+ if (typeof value === "string") return value.length;
5607
+ if (Array.isArray(value)) {
5608
+ return value.reduce(
5609
+ (sum, item) => sum + kiroCliV2ContentChars(item),
5610
+ 0,
5611
+ );
5612
+ }
5613
+ if (!value || typeof value !== "object") return 0;
5614
+ for (const key of ["content", "text", "value", "parts", "entries"]) {
5615
+ if (value[key] !== undefined) {
5616
+ return kiroCliV2ContentChars(value[key]);
5617
+ }
5618
+ }
5619
+ return 0;
5620
+ }
5621
+
5622
+ // Kiro CLI 2.13+ writes event-sourced sessions under
5623
+ // ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. The records do
5624
+ // not carry token counts, but user/assistant text, turn boundaries, timestamps,
5625
+ // and assistant reasoningModelId are sufficient for the same 4 chars/token
5626
+ // approximation used by the legacy CLI reader.
5627
+ async function readKiroCliV2SessionTurns(messagesPath) {
5628
+ if (
5629
+ !messagesPath ||
5630
+ path.basename(messagesPath) !== "messages.jsonl" ||
5631
+ !fssync.existsSync(messagesPath)
5632
+ ) {
5633
+ return [];
5634
+ }
5635
+
5636
+ const sessionDir = path.dirname(messagesPath);
5637
+ let sessionMeta = {};
5638
+ try {
5639
+ sessionMeta = JSON.parse(
5640
+ fssync.readFileSync(path.join(sessionDir, "session.json"), "utf8"),
5641
+ );
5642
+ } catch {
5643
+ // messages.jsonl is self-contained enough to parse without session.json.
5644
+ }
5645
+
5646
+ const sessionId =
5647
+ typeof sessionMeta?.id === "string" && sessionMeta.id
5648
+ ? sessionMeta.id
5649
+ : path.basename(sessionDir);
5650
+ const fallbackModel =
5651
+ typeof sessionMeta?.modelId === "string" ? sessionMeta.modelId : null;
5652
+ const fallbackTimestamp = Date.parse(
5653
+ sessionMeta?.createdAt || sessionMeta?.lastModifiedAt || "",
5654
+ );
5655
+
5656
+ const flat = [];
5657
+ let stream;
5658
+ try {
5659
+ stream = fssync.createReadStream(messagesPath, { encoding: "utf8" });
5660
+ } catch {
5661
+ return flat;
5662
+ }
5663
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
5664
+
5665
+ let pendingUserChars = 0;
5666
+ let pendingUserTimestampMs = NaN;
5667
+ let turn = null;
5668
+ let fallbackTurnIndex = 0;
5669
+
5670
+ const flushTurn = () => {
5671
+ if (!turn) return;
5672
+ const hasUsage =
5673
+ turn.outputChars > 0 ||
5674
+ turn.reasoningChars > 0;
5675
+ const tsMs = Number.isFinite(turn.timestampMs)
5676
+ ? turn.timestampMs
5677
+ : Number.isFinite(pendingUserTimestampMs)
5678
+ ? pendingUserTimestampMs
5679
+ : fallbackTimestamp;
5680
+ if (hasUsage && Number.isFinite(tsMs) && tsMs > 0) {
5681
+ const requestId =
5682
+ turn.executionId ||
5683
+ `${sessionId}:v2:${fallbackTurnIndex}`;
5684
+ const messageIds = Array.from(turn.messageIds);
5685
+ flat.push({
5686
+ request_id: requestId,
5687
+ message_id: messageIds[0] || turn.executionId || null,
5688
+ all_message_ids: messageIds,
5689
+ session_id: sessionId,
5690
+ session_model_id: fallbackModel,
5691
+ model_id: turn.modelId || fallbackModel,
5692
+ request_start_timestamp_ms: tsMs,
5693
+ user_prompt_length: turn.inputChars,
5694
+ response_size: turn.outputChars,
5695
+ reasoning_size: turn.reasoningChars,
5696
+ });
5697
+ }
5698
+ fallbackTurnIndex++;
5699
+ turn = null;
5700
+ };
5701
+
5702
+ const startTurn = (payload, timestampMs) => {
5703
+ flushTurn();
5704
+ turn = {
5705
+ executionId:
5706
+ typeof payload?.executionId === "string" ? payload.executionId : null,
5707
+ timestampMs:
5708
+ Number.isFinite(timestampMs)
5709
+ ? timestampMs
5710
+ : pendingUserTimestampMs,
5711
+ inputChars: pendingUserChars,
5712
+ outputChars: 0,
5713
+ reasoningChars: 0,
5714
+ modelId: null,
5715
+ messageIds: new Set(),
5716
+ };
5717
+ pendingUserChars = 0;
5718
+ pendingUserTimestampMs = NaN;
5719
+ };
5720
+
5721
+ try {
5722
+ for await (const line of rl) {
5723
+ if (!line || !line.trim()) continue;
5724
+ let event;
5725
+ try {
5726
+ event = JSON.parse(line);
5727
+ } catch {
5728
+ continue;
5729
+ }
5730
+ const payload = event?.payload;
5731
+ if (!payload || typeof payload !== "object") continue;
5732
+ const type =
5733
+ typeof payload.type === "string"
5734
+ ? payload.type.toLowerCase()
5735
+ : "";
5736
+ const timestampMs = Date.parse(event.timestamp || "");
5737
+
5738
+ if (type === "user") {
5739
+ if (turn) flushTurn();
5740
+ pendingUserChars = kiroCliV2ContentChars(payload.content);
5741
+ pendingUserTimestampMs = timestampMs;
5742
+ continue;
5743
+ }
5744
+
5745
+ if (type === "turn_start") {
5746
+ startTurn(payload, timestampMs);
5747
+ continue;
5748
+ }
5749
+
5750
+ if (
5751
+ ["assistant", "tool_call", "tool_result", "usage_summary"].includes(
5752
+ type,
5753
+ ) &&
5754
+ !turn
5755
+ ) {
5756
+ startTurn(payload, timestampMs);
5757
+ }
5758
+ if (!turn) continue;
5759
+
5760
+ if (
5761
+ typeof payload.executionId === "string" &&
5762
+ payload.executionId &&
5763
+ !turn.executionId
5764
+ ) {
5765
+ turn.executionId = payload.executionId;
5766
+ }
5767
+ if (typeof event.id === "string" && event.id) {
5768
+ turn.messageIds.add(event.id);
5769
+ }
5770
+
5771
+ if (type === "assistant") {
5772
+ const chars = kiroCliV2ContentChars(payload.content);
5773
+ if (
5774
+ typeof payload.operationType === "string" &&
5775
+ payload.operationType.toLowerCase() === "reasoning"
5776
+ ) {
5777
+ turn.reasoningChars += chars;
5778
+ } else {
5779
+ turn.outputChars += chars;
5780
+ }
5781
+ if (
5782
+ typeof payload.reasoningModelId === "string" &&
5783
+ payload.reasoningModelId
5784
+ ) {
5785
+ turn.modelId = payload.reasoningModelId;
5786
+ }
5787
+ } else if (type === "turn_end") {
5788
+ flushTurn();
5789
+ }
5790
+ }
5791
+ } catch {
5792
+ // Return complete turns parsed before a concurrent truncate/delete.
5793
+ }
5794
+ flushTurn();
5795
+ return flat;
5796
+ }
5797
+
5186
5798
  // Canonicalize a Kiro-CLI-emitted model id so IDE and CLI rows collapse when
5187
5799
  // they refer to the same underlying Bedrock model. Examples:
5188
5800
  // anthropic.claude-sonnet-4-20250514-v1:0 -> claude-sonnet-4
@@ -5202,6 +5814,7 @@ function canonicalizeKiroCliModelId(raw) {
5202
5814
  if (!name) return null;
5203
5815
  name = name.toLowerCase();
5204
5816
  if (name === "auto") return null;
5817
+ name = name.replace(/^(?:qdev|kiro)::/, "");
5205
5818
  // Strip provider prefix (anthropic., aws., openai., or a full Bedrock ARN).
5206
5819
  name = name.replace(
5207
5820
  /^(?:arn:aws:bedrock:[^:]*:[^:]*:(?:foundation-model\/)?|anthropic\.|openai\.|aws\.)/,
@@ -5294,10 +5907,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5294
5907
  const resolvedEnv = env || process.env;
5295
5908
  const dbPath = resolveKiroCliDbPath(resolvedEnv);
5296
5909
 
5297
- // Combine two sources under the same (source='kiro', cursors.kiroCli)
5298
- // namespace: historical rows from the SQLite DB plus live session state
5299
- // from ~/.kiro/sessions/cli/{uuid}.json (covers turns from a running
5300
- // session that hasn't flushed to SQLite yet). Request ID shapes differ:
5910
+ // Combine three sources under the same (source='kiro', cursors.kiroCli)
5911
+ // namespace: historical rows from the SQLite DB, legacy live session state
5912
+ // from ~/.kiro/sessions/cli/{uuid}.json, and Kiro CLI 2.13+ event logs at
5913
+ // ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. Request ID
5914
+ // shapes differ:
5301
5915
  // SQLite carries a persisted request_id UUID; session files synthesize
5302
5916
  // `${sessionId}:${loop_id.rand}`. When kiro-cli migrates a live session
5303
5917
  // into SQLite the same turn lands under a new request_id — the cross-
@@ -5309,8 +5923,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5309
5923
  : [];
5310
5924
  const sessionFilesList = resolveKiroCliSessionFiles(resolvedEnv);
5311
5925
  let flatSessions = [];
5312
- for (const jsonPath of sessionFilesList) {
5313
- const turns = await readKiroCliSessionTurns(jsonPath);
5926
+ for (const sessionPath of sessionFilesList) {
5927
+ const turns =
5928
+ path.basename(sessionPath) === "messages.jsonl"
5929
+ ? await readKiroCliV2SessionTurns(sessionPath)
5930
+ : await readKiroCliSessionTurns(sessionPath);
5314
5931
  for (const turn of turns) flatSessions.push(turn);
5315
5932
  }
5316
5933
  // Per-request state replaces the old seenIds set. Each entry captures
@@ -5382,7 +5999,14 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5382
5999
  toRetract.push([reqId, prev, sid]);
5383
6000
  }
5384
6001
  for (const [reqId, prev, sid] of toRetract) {
5385
- if (prev.input_tokens || prev.output_tokens) {
6002
+ if (
6003
+ prev.input_tokens ||
6004
+ prev.output_tokens ||
6005
+ prev.reasoning_output_tokens
6006
+ ) {
6007
+ const prevReasoning = toNonNegativeInt(
6008
+ prev.reasoning_output_tokens,
6009
+ );
5386
6010
  const prevBucket = getHourlyBucket(
5387
6011
  hourlyState,
5388
6012
  "kiro",
@@ -5394,8 +6018,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5394
6018
  cached_input_tokens: 0,
5395
6019
  cache_creation_input_tokens: 0,
5396
6020
  output_tokens: -prev.output_tokens,
5397
- reasoning_output_tokens: 0,
5398
- total_tokens: -(prev.input_tokens + prev.output_tokens),
6021
+ reasoning_output_tokens: -prevReasoning,
6022
+ total_tokens: -(
6023
+ prev.input_tokens +
6024
+ prev.output_tokens +
6025
+ prevReasoning
6026
+ ),
5399
6027
  conversation_count: -1,
5400
6028
  });
5401
6029
  touchedBuckets.add(bucketKey("kiro", prev.model, prev.bucketStart));
@@ -5488,8 +6116,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5488
6116
 
5489
6117
  const promptChars = toNonNegativeInt(r.user_prompt_length);
5490
6118
  const responseChars = toNonNegativeInt(r.response_size);
6119
+ const reasoningChars = toNonNegativeInt(r.reasoning_size);
5491
6120
  const approxInput = Math.floor(promptChars / KIRO_CLI_CHARS_PER_TOKEN);
5492
6121
  const approxOutput = Math.floor(responseChars / KIRO_CLI_CHARS_PER_TOKEN);
6122
+ const approxReasoning = Math.floor(
6123
+ reasoningChars / KIRO_CLI_CHARS_PER_TOKEN,
6124
+ );
5493
6125
 
5494
6126
  const tsMs = Number(r.request_start_timestamp_ms);
5495
6127
  if (!Number.isFinite(tsMs) || tsMs <= 0) continue;
@@ -5502,37 +6134,52 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5502
6134
  const model = canonical || "kiro-cli-agent";
5503
6135
 
5504
6136
  // Fingerprint captures every field whose change should cause a re-bucket.
5505
- const fingerprint = `${promptChars}:${responseChars}:${model}:${tsMs}`;
6137
+ const fingerprint =
6138
+ `${promptChars}:${responseChars}:${reasoningChars}:${model}:${tsMs}`;
5506
6139
  const prev = requestState[requestId];
5507
6140
  if (prev && prev.fingerprint === fingerprint) continue; // unchanged
5508
6141
 
5509
6142
  // Subtract the prior contribution (if any) from its prior bucket so the
5510
6143
  // bucket's absolute totals reflect the CURRENT truth, not the historical
5511
6144
  // truth. enqueueTouchedBuckets will emit the net delta at flush time.
5512
- if (prev && (prev.input_tokens || prev.output_tokens)) {
6145
+ if (
6146
+ prev &&
6147
+ (
6148
+ prev.input_tokens ||
6149
+ prev.output_tokens ||
6150
+ prev.reasoning_output_tokens
6151
+ )
6152
+ ) {
6153
+ const prevReasoning = toNonNegativeInt(
6154
+ prev.reasoning_output_tokens,
6155
+ );
5513
6156
  const prevBucket = getHourlyBucket(hourlyState, "kiro", prev.model, prev.bucketStart);
5514
6157
  addTotals(prevBucket.totals, {
5515
6158
  input_tokens: -prev.input_tokens,
5516
6159
  cached_input_tokens: 0,
5517
6160
  cache_creation_input_tokens: 0,
5518
6161
  output_tokens: -prev.output_tokens,
5519
- reasoning_output_tokens: 0,
5520
- total_tokens: -(prev.input_tokens + prev.output_tokens),
6162
+ reasoning_output_tokens: -prevReasoning,
6163
+ total_tokens: -(
6164
+ prev.input_tokens +
6165
+ prev.output_tokens +
6166
+ prevReasoning
6167
+ ),
5521
6168
  conversation_count: -1,
5522
6169
  });
5523
6170
  touchedBuckets.add(bucketKey("kiro", prev.model, prev.bucketStart));
5524
6171
  }
5525
6172
 
5526
6173
  // Add the new contribution.
5527
- if (approxInput > 0 || approxOutput > 0) {
6174
+ if (approxInput > 0 || approxOutput > 0 || approxReasoning > 0) {
5528
6175
  const bucket = getHourlyBucket(hourlyState, "kiro", model, bucketStart);
5529
6176
  addTotals(bucket.totals, {
5530
6177
  input_tokens: approxInput,
5531
6178
  cached_input_tokens: 0,
5532
6179
  cache_creation_input_tokens: 0,
5533
6180
  output_tokens: approxOutput,
5534
- reasoning_output_tokens: 0,
5535
- total_tokens: approxInput + approxOutput,
6181
+ reasoning_output_tokens: approxReasoning,
6182
+ total_tokens: approxInput + approxOutput + approxReasoning,
5536
6183
  conversation_count: 1,
5537
6184
  });
5538
6185
  touchedBuckets.add(bucketKey("kiro", model, bucketStart));
@@ -5551,6 +6198,7 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
5551
6198
  model,
5552
6199
  input_tokens: approxInput,
5553
6200
  output_tokens: approxOutput,
6201
+ reasoning_output_tokens: approxReasoning,
5554
6202
  ...(r.session_id ? { session_id: r.session_id } : {}),
5555
6203
  };
5556
6204
 
@@ -13586,6 +14234,9 @@ module.exports = {
13586
14234
  readOpencodeDbMessages,
13587
14235
  readMimoDbMessages,
13588
14236
  readZcodeDbMessages,
14237
+ resolveQoderDbPath,
14238
+ resolveQoderDbPaths,
14239
+ readQoderDbMessages,
13589
14240
  resolveKiroDbPath,
13590
14241
  resolveKiroJsonlPath,
13591
14242
  resolveHermesPath,
@@ -13610,6 +14261,7 @@ module.exports = {
13610
14261
  parseGeminiIncremental,
13611
14262
  parseOpencodeIncremental,
13612
14263
  parseOpencodeDbIncremental,
14264
+ parseQoderDbIncremental,
13613
14265
  openclawCursorKey,
13614
14266
  parseOpenclawIncremental,
13615
14267
  parseCursorApiIncremental,
@@ -13697,6 +14349,7 @@ module.exports = {
13697
14349
  // Exposed for regression tests covering cache-token accounting.
13698
14350
  normalizeGeminiTokens,
13699
14351
  normalizeOpencodeTokens,
14352
+ normalizeQoderTokens,
13700
14353
  sameGeminiTotals,
13701
14354
  diffGeminiTotals,
13702
14355
  // Exposed so the queue-repair migration can mutate cursors state in the