tokentracker-cli 1.3.4 → 1.3.5

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 (145) hide show
  1. package/README.de.md +5 -5
  2. package/README.ja.md +3 -3
  3. package/README.ko.md +3 -3
  4. package/README.md +6 -5
  5. package/README.zh-CN.md +3 -3
  6. package/dashboard/dist/assets/{AchievementBadge-CKjtQp-U.js → AchievementBadge-Dazs3pEa.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-DpOXrabE.js → AchievementsPage-LWnxZvmw.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DiXyIoa0.js → AchievementsSection-BLqczmU9.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-CILTTkPF.js → BadgeDetailModal-B_n4aj2S.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-CrBHWiRi.js → BotAnimated-KDWI_YcZ.js} +1 -1
  11. package/dashboard/dist/assets/{Card-DUItW3xI.js → Card-BYoxd4OC.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-C_t_PDlG.js → CloudCheckoutPage-BR2_B03P.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-pvJUrhya.js → CloudPage-DL5RkJKK.js} +1 -1
  14. package/dashboard/dist/assets/{CloudUsageExport-DQbDwiyk.js → CloudUsageExport-C_DqZKo1.js} +1 -1
  15. package/dashboard/dist/assets/{CommandPalette-BvHilrbV.js → CommandPalette-DkCaIQI4.js} +1 -1
  16. package/dashboard/dist/assets/{CommunityStatsModal-CizxEYv-.js → CommunityStatsModal-pX9OePi4.js} +1 -1
  17. package/dashboard/dist/assets/{ConfirmModal-h9nISVy7.js → ConfirmModal-tkgZwz4u.js} +1 -1
  18. package/dashboard/dist/assets/{Controls-BjdIonTX.js → Controls-MTUhGWy8.js} +1 -1
  19. package/dashboard/dist/assets/{DashboardPage-Bxryr4qp.js → DashboardPage-SOx-A7lH.js} +1 -1
  20. package/dashboard/dist/assets/{DateRangePopover-DpKh-0l8.js → DateRangePopover-B-BMOm3O.js} +1 -1
  21. package/dashboard/dist/assets/{DevicePage-DUJR9hJC.js → DevicePage-DSK9lm7v.js} +1 -1
  22. package/dashboard/dist/assets/{DialogClose-BmVAmN-o.js → DialogClose-uVUNu4rg.js} +1 -1
  23. package/dashboard/dist/assets/DialogDescription-DBqGGWNj.js +1 -0
  24. package/dashboard/dist/assets/{DialogTitle-BsaIbN8k.js → DialogTitle-B-VifiA3.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-BpSt2P_W.js → FadeIn-B6lrj4uo.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-Cc_VHB0R.js → HeaderGithubStar-Cx5zOhXa.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-WFd21iP9.js → HoverTooltip-DY1ENf69.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-C_mUPSRB.js → IpCheckPage-D6TEq64d.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-DVR8ClZO.js → LandingPage-OdZUfq--.js} +2 -2
  30. package/dashboard/dist/assets/LeaderboardPage-DCK5xcAt.js +2 -0
  31. package/dashboard/dist/assets/{LeaderboardProBadge-BjX4EFl2.js → LeaderboardProBadge-CBXJ1u_m.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-vbPMNn7W.js → LeaderboardProfileModal-Ccdzb7QK.js} +4 -4
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-CqClmuA-.js → LeaderboardProfilePage-CFXlZ0fL.js} +1 -1
  34. package/dashboard/dist/assets/LimitsPage-5krjm-g9.js +9 -0
  35. package/dashboard/dist/assets/{LocalOnlyNotice-B6nDke_u.js → LocalOnlyNotice-Rk1B8PaZ.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-C28QVjd7.js → LoginCard-DTTIm8vk.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-Bx1btNhH.js → LoginModal-BNcDdjfr.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-DVOGNGYI.js → LoginPage-CQgXP7Ee.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-nOKryTde.js → PetPage-Cw_kAPRs.js} +1 -1
  40. package/dashboard/dist/assets/{PopoverPopup-Ch6NLnu6.js → PopoverPopup-NYiGE8A2.js} +1 -1
  41. package/dashboard/dist/assets/{ProviderIcon-BRY2br-g.js → ProviderIcon-Bp7iFJ_m.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-ko1OQecQ.js → ResetPasswordPage-Cf9kAWDW.js} +1 -1
  43. package/dashboard/dist/assets/{Select-Q8mS3_od.js → Select-kIcvQFAo.js} +1 -1
  44. package/dashboard/dist/assets/{SelectItemText-C1id8GDj.js → SelectItemText-BpIsAg3b.js} +1 -1
  45. package/dashboard/dist/assets/{SelfHostPage-Dzb2bDiM.js → SelfHostPage-Ch2vvSwI.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-ewFwz29J.js → ServiceStatusPage-hoYBvGbu.js} +1 -1
  47. package/dashboard/dist/assets/SessionsPage-BzRB6q-Z.js +3 -0
  48. package/dashboard/dist/assets/SettingsPage-DRl2uX-i.js +2 -0
  49. package/dashboard/dist/assets/{SkillsPage-CcG77tqU.js → SkillsPage-CkqThTZL.js} +1 -1
  50. package/dashboard/dist/assets/{TokenGalaxy-D2HDrbQR.js → TokenGalaxy-eLG-UIZY.js} +1 -1
  51. package/dashboard/dist/assets/{TooltipPopup-C_bF2ZwO.js → TooltipPopup-bnMJyilQ.js} +1 -1
  52. package/dashboard/dist/assets/{TrendMonitor-BcyoN5ns.js → TrendMonitor-X7-aGzIv.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-BirK0U_N.js → WidgetsPage-CGznIzrW.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-thsXBuw6.js → WrappedPage-p31-uUoF.js} +1 -1
  55. package/dashboard/dist/assets/{__vite-browser-external-Bq8nj-JV.js → __vite-browser-external-2g_4s3ot.js} +12 -12
  56. package/dashboard/dist/assets/__vite-browser-external-LpFw4zXh.css +1 -0
  57. package/dashboard/dist/assets/agent-logos-2TVT3fIE.js +1 -0
  58. package/dashboard/dist/assets/{arrow-left-c_NOSfoq.js → arrow-left-DL0hAp_7.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-right-CP7drKCT.js → arrow-right-DFCpnV_9.js} +1 -1
  60. package/dashboard/dist/assets/{arrow-up-right-DQ6QfCpo.js → arrow-up-right-DLvfE0Wu.js} +1 -1
  61. package/dashboard/dist/assets/{calendar-days-D_pxFQvT.js → calendar-days-B0aDoKSG.js} +1 -1
  62. package/dashboard/dist/assets/{check-HAS31kTT.js → check-C_cfrxiz.js} +1 -1
  63. package/dashboard/dist/assets/{chevron-down-BIMuk6oN.js → chevron-down-Ck8ml43e.js} +1 -1
  64. package/dashboard/dist/assets/{cloud-checkout-COmDwrUT.js → cloud-checkout-PYnwNfdH.js} +1 -1
  65. package/dashboard/dist/assets/{composite-kLwqpqEk.js → composite-INW-ELWX.js} +1 -1
  66. package/dashboard/dist/assets/{download-9RgsZIiH.js → download-B7uokeIo.js} +1 -1
  67. package/dashboard/dist/assets/{external-link-C_gYjHGF.js → external-link-CI3crg4K.js} +1 -1
  68. package/dashboard/dist/assets/{flame-BSsCvgT8.js → flame-DCzvFKV1.js} +1 -1
  69. package/dashboard/dist/assets/{format-tokens-N22Wqv1X.js → format-tokens-CSTEYK2m.js} +1 -1
  70. package/dashboard/dist/assets/gift-dEHDFMc6.js +1 -0
  71. package/dashboard/dist/assets/{host-mode-JzrhHX7F.js → host-mode-r2SW9Mdv.js} +1 -1
  72. package/dashboard/dist/assets/{icons-pK99mIMv.js → icons-q-3zvEkh.js} +1 -1
  73. package/dashboard/dist/assets/{index-CmD84EJi.js → index-BV7qwUw4.js} +1 -1
  74. package/dashboard/dist/assets/{index-Dk43KNa6.js → index-BbrsgHf4.js} +1 -1
  75. package/dashboard/dist/assets/{index-BhviBroj.js → index-CyHYj9XI.js} +1 -1
  76. package/dashboard/dist/assets/{info-D-RLFnSK.js → info-BBdY9hV4.js} +1 -1
  77. package/dashboard/dist/assets/{link-2-BiKXW9mV.js → link-2-CLPePfXs.js} +1 -1
  78. package/dashboard/dist/assets/{loader-circle-KVUaEKV2.js → loader-circle-ETrjzfmn.js} +1 -1
  79. package/dashboard/dist/assets/{modifiers.esm-5NK8j3mT.js → modifiers.esm-DHqnp2ol.js} +1 -1
  80. package/dashboard/dist/assets/module-BNnaKM__.js +3 -0
  81. package/dashboard/dist/assets/{monitor-smartphone-u9yr-_Es.js → monitor-smartphone-D1kdzrdb.js} +1 -1
  82. package/dashboard/dist/assets/{play-D0MaDCOx.js → play-Cxp-ronD.js} +1 -1
  83. package/dashboard/dist/assets/{plus-DTJVtoAJ.js → plus-4n94VzDK.js} +1 -1
  84. package/dashboard/dist/assets/{provider-display-YhtG0I9Y.js → provider-display-CRlbeO7t.js} +1 -1
  85. package/dashboard/dist/assets/{react-gIVxucOB.js → react-DlHjGQFN.js} +1 -1
  86. package/dashboard/dist/assets/{safePolygon-C_sPnJ1L.js → safePolygon-CVZ27a2_.js} +1 -1
  87. package/dashboard/dist/assets/{search-DdQRwFYV.js → search-BDPr56_h.js} +1 -1
  88. package/dashboard/dist/assets/{server-M0rmbGWy.js → server-Bm7fHvVa.js} +1 -1
  89. package/dashboard/dist/assets/{shield-check-9-sqQKdD.js → shield-check-DtT8ACzI.js} +1 -1
  90. package/dashboard/dist/assets/{skills-api-B0qBWnuK.js → skills-api-Dc1-2qu6.js} +1 -1
  91. package/dashboard/dist/assets/{sortable.esm-iqbm7x5D.js → sortable.esm-B-qN-Xk2.js} +1 -1
  92. package/dashboard/dist/assets/{store-DstuWd4a.js → store-gHeBU8dQ.js} +1 -1
  93. package/dashboard/dist/assets/{terminal-Dz0MxHNG.js → terminal-D4VZbvhg.js} +1 -1
  94. package/dashboard/dist/assets/{trash-DBMFOFVz.js → trash-BZtLAFQy.js} +1 -1
  95. package/dashboard/dist/assets/{use-community-stats-CacbAcUR.js → use-community-stats-BdqndrbB.js} +1 -1
  96. package/dashboard/dist/assets/{use-latest-request-guard-nBuQVZOR.js → use-latest-request-guard-DhGg1cDS.js} +1 -1
  97. package/dashboard/dist/assets/use-limits-display-prefs-D3HOcRFv.js +1 -0
  98. package/dashboard/dist/assets/{use-native-settings-EAqAFx2x.js → use-native-settings-BniKwA_n.js} +1 -1
  99. package/dashboard/dist/assets/{use-ordered-list-DGLjjU6Q.js → use-ordered-list-b09H7EHq.js} +1 -1
  100. package/dashboard/dist/assets/{use-pet-settings-C5MzWHE7.js → use-pet-settings-CPZdlq_e.js} +2 -2
  101. package/dashboard/dist/assets/use-reduced-motion-D1gcKJUo.js +1 -0
  102. package/dashboard/dist/assets/{use-transform-C25eCuz_.js → use-transform-CIdxjmI8.js} +1 -1
  103. package/dashboard/dist/assets/use-usage-limits-eJlU65yY.js +1 -0
  104. package/dashboard/dist/assets/{useCurrency-t3PYQi1l.js → useCurrency-CM22CmAq.js} +1 -1
  105. package/dashboard/dist/assets/{usePositioner-C59mj6kG.js → usePositioner-B3l2ulTw.js} +1 -1
  106. package/dashboard/dist/assets/{useTokenFormat-C0xQsBGf.js → useTokenFormat-D7nzgxKw.js} +1 -1
  107. package/dashboard/dist/assets/{zap-CyH0tvKM.js → zap-Daxct1WV.js} +1 -1
  108. package/dashboard/dist/index.html +14 -12
  109. package/dashboard/dist/ip-check.html +2 -2
  110. package/dashboard/dist/leaderboard.html +2 -2
  111. package/dashboard/dist/llms.txt +2 -2
  112. package/dashboard/dist/share.html +2 -2
  113. package/package.json +2 -2
  114. package/src/commands/init.js +43 -1
  115. package/src/commands/status.js +27 -0
  116. package/src/commands/sync.js +54 -4
  117. package/src/commands/uninstall.js +19 -0
  118. package/src/lib/claude-config.js +47 -15
  119. package/src/lib/codex-rollout-parser.js +11 -3
  120. package/src/lib/command-runner.js +15 -1
  121. package/src/lib/cursor-config.js +2 -1
  122. package/src/lib/gemini-config.js +3 -2
  123. package/src/lib/local-api.js +13 -3
  124. package/src/lib/notify-command.js +38 -0
  125. package/src/lib/openclaw-session-plugin.js +47 -14
  126. package/src/lib/passive-mode.js +12 -0
  127. package/src/lib/pricing/curated-overrides.json +42 -5
  128. package/src/lib/pricing/matcher.js +14 -5
  129. package/src/lib/pricing/seed-snapshot.json +1 -1
  130. package/src/lib/rollout.js +150 -28
  131. package/src/lib/session-analytics.js +108 -9
  132. package/src/lib/usage-limits.js +183 -20
  133. package/src/lib/zcode-limits.js +218 -18
  134. package/src/lib/zcode-reset-credits.js +66 -0
  135. package/dashboard/dist/assets/DialogDescription-C0Cgq67g.js +0 -1
  136. package/dashboard/dist/assets/LeaderboardPage-D8ydzVpn.js +0 -2
  137. package/dashboard/dist/assets/LimitsPage-DaRggHge.js +0 -8
  138. package/dashboard/dist/assets/SessionsPage-C6Y8kbnM.js +0 -3
  139. package/dashboard/dist/assets/SettingsPage-nztCXEPs.js +0 -2
  140. package/dashboard/dist/assets/__vite-browser-external-BwmeEfv7.css +0 -1
  141. package/dashboard/dist/assets/agent-logos-CK6SzhI7.js +0 -1
  142. package/dashboard/dist/assets/module-WSx8viXK.js +0 -3
  143. package/dashboard/dist/assets/use-limits-display-prefs-fqg8hs6b.js +0 -1
  144. package/dashboard/dist/assets/use-reduced-motion-DK2oZ2uC.js +0 -1
  145. package/dashboard/dist/assets/use-usage-limits-BhqnEai3.js +0 -1
@@ -12,6 +12,7 @@ const { physicalJsonlRecords } = require("./jsonl-lines");
12
12
  const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
13
13
  const wsl = require("./wsl-probe");
14
14
  const { resolveInstallPaths } = require("./install-resolver");
15
+ const { expandHome } = require("./scan-roots");
15
16
  const {
16
17
  consumeUsageDelta,
17
18
  createUsageDeltaState,
@@ -1634,9 +1635,19 @@ async function resolveOpenclawSessionFiles(env = process.env, deps = {}) {
1634
1635
  out.push(full);
1635
1636
  }
1636
1637
  }
1638
+ for (const dbPath of [
1639
+ path.join(agentsDir, agent.name, "agent", "openclaw-agent.sqlite"),
1640
+ path.join(agentsDir, agent.name, "openclaw-agent.sqlite"),
1641
+ ]) {
1642
+ if ((await fs.stat(dbPath).catch(() => null))?.isFile()) {
1643
+ out.push(dbPath);
1644
+ break; // alternate layouts of one agent database, not two sources
1645
+ }
1646
+ }
1637
1647
  }
1638
1648
  }
1639
- out.sort((a, b) => a.localeCompare(b));
1649
+ // Import archives must seed dedup before the corresponding SQLite history.
1650
+ out.sort((a, b) => Number(a.endsWith(".sqlite")) - Number(b.endsWith(".sqlite")) || a.localeCompare(b));
1640
1651
  return out;
1641
1652
  }
1642
1653
 
@@ -1699,6 +1710,67 @@ async function parseOpenclawIncremental({
1699
1710
  const previousOffset = Number(prev?.offset || 0);
1700
1711
  let accepted = null;
1701
1712
  let openedRegularFile = false;
1713
+ if (filePath.endsWith(".sqlite")) {
1714
+ const sessionFilter = typeof entry?.sessionId === "string"
1715
+ ? ` AND e.session_id = '${entry.sessionId.replace(/'/g, "''")}'` : "";
1716
+ const rows = await readSqliteJsonRowsAsync(filePath, `
1717
+ SELECT e.session_id, json_object(
1718
+ 'type', json_extract(e.event_json, '$.type'),
1719
+ 'id', json_extract(e.event_json, '$.id'),
1720
+ 'timestamp', json_extract(e.event_json, '$.timestamp'),
1721
+ 'message', json_object(
1722
+ 'role', json_extract(e.event_json, '$.message.role'),
1723
+ 'model', json_extract(e.event_json, '$.message.model'),
1724
+ 'provider', json_extract(e.event_json, '$.message.provider'),
1725
+ 'api', json_extract(e.event_json, '$.message.api'),
1726
+ 'timestamp', json_extract(e.event_json, '$.message.timestamp'),
1727
+ 'responseId', json_extract(e.event_json, '$.message.responseId'),
1728
+ 'usage', json_extract(e.event_json, '$.message.usage')
1729
+ )) AS event
1730
+ FROM transcript_events e JOIN session_windows w ON w.session_id = e.session_id
1731
+ WHERE json_valid(e.event_json) AND COALESCE(w.reason, '') != 'compaction'
1732
+ AND json_extract(e.event_json, '$.message.role') = 'assistant'
1733
+ AND json_extract(e.event_json, '$.message.usage') IS NOT NULL
1734
+ ${sessionFilter}
1735
+ ORDER BY e.session_id, e.seq
1736
+ `, { readOnly: true, throwOnReadFailure: true, label: "OpenClaw" });
1737
+ const sessions = new Map();
1738
+ for (const row of rows) {
1739
+ if (!sessions.has(row.session_id)) sessions.set(row.session_id, []);
1740
+ sessions.get(row.session_id).push(row.event);
1741
+ }
1742
+ for (const [sessionId, lines] of sessions) {
1743
+ const sessionCursorKey = `${key}#${sessionId}`;
1744
+ const prior = { ...(stagedFiles[sessionCursorKey]?.usageEvents || {}) };
1745
+ const dbDir = path.dirname(filePath);
1746
+ const agentDir = path.basename(dbDir) === "agent" && path.basename(path.dirname(path.dirname(dbDir))) === "agents"
1747
+ ? path.dirname(dbDir) : dbDir;
1748
+ const agentPrefix = openclawCursorKey(`${agentDir}${path.sep}`);
1749
+ // SQLite imports retain transcript identities. Carry forward counts
1750
+ // from this session's legacy transcript, including reset archives.
1751
+ for (const [legacyPath, cursor] of Object.entries(stagedFiles)) {
1752
+ const basename = path.basename(legacyPath);
1753
+ if (cursor.provider !== "openclaw" || !openclawCursorKey(legacyPath).startsWith(agentPrefix)) continue;
1754
+ if (!basename.startsWith(`${sessionId}.jsonl`) &&
1755
+ !(basename.startsWith(`${sessionId}-topic-`) && basename.includes(".jsonl"))) continue;
1756
+ for (const [identity, count] of Object.entries(cursor.usageEvents || {})) {
1757
+ prior[identity] = Math.max(prior[identity] || 0, count);
1758
+ }
1759
+ }
1760
+ const result = await parseOpenclawSessionFile({
1761
+ lines, previousUsageEvents: prior, appendOnly: false,
1762
+ hourlyState, touchedBuckets, source: fileSource,
1763
+ });
1764
+ stagedFiles[sessionCursorKey] = {
1765
+ provider: "openclaw", usageEvents: result.usageEvents,
1766
+ hasRealUsage: Object.keys(result.usageEvents).length > 0,
1767
+ updatedAt: new Date().toISOString(),
1768
+ };
1769
+ eventsAggregated += result.eventsAggregated;
1770
+ }
1771
+ filesProcessed += 1;
1772
+ continue;
1773
+ }
1702
1774
  for (let attempt = 0; attempt < 3; attempt += 1) {
1703
1775
  const fileHandle = await fs.open(filePath, "r").catch(() => null);
1704
1776
  if (!fileHandle) break;
@@ -1913,6 +1985,7 @@ function mergeOpenclawAttemptBuckets(
1913
1985
  }
1914
1986
 
1915
1987
  async function parseOpenclawSessionFile({
1988
+ lines,
1916
1989
  filePath,
1917
1990
  fileHandle,
1918
1991
  startOffset,
@@ -1928,7 +2001,8 @@ async function parseOpenclawSessionFile({
1928
2001
  projectState,
1929
2002
  projectTouchedBuckets,
1930
2003
  }) {
1931
- const st = fileHandle ? await fileHandle.stat() : await fs.stat(filePath);
2004
+ const st = lines ? { size: lines.length } : fileHandle ? await fileHandle.stat() : await fs.stat(filePath);
2005
+ startOffset = startOffset || 0;
1932
2006
  const endOffset =
1933
2007
  Number.isFinite(endOffsetLimit) && endOffsetLimit >= 0
1934
2008
  ? Math.min(st.size, endOffsetLimit)
@@ -1963,14 +2037,14 @@ async function parseOpenclawSessionFile({
1963
2037
  };
1964
2038
  }
1965
2039
 
1966
- const stream = fssync.createReadStream(filePath, {
2040
+ const stream = lines ? null : fssync.createReadStream(filePath, {
1967
2041
  fd: fileHandle?.fd,
1968
2042
  autoClose: !fileHandle,
1969
2043
  encoding: "utf8",
1970
2044
  start: startOffset,
1971
2045
  end: endOffset - 1,
1972
2046
  });
1973
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
2047
+ const rl = lines || readline.createInterface({ input: stream, crlfDelay: Infinity });
1974
2048
 
1975
2049
  let eventsAggregated = 0;
1976
2050
  const scanCounts = appendOnly ? { ...priorCounts } : {};
@@ -2069,8 +2143,8 @@ async function parseOpenclawSessionFile({
2069
2143
  if (!collectOnly) eventsAggregated += 1;
2070
2144
  }
2071
2145
 
2072
- rl.close();
2073
- if (!fileHandle) stream.close?.();
2146
+ if (!lines) rl.close();
2147
+ if (!fileHandle) stream?.close?.();
2074
2148
  return {
2075
2149
  endOffset,
2076
2150
  eventsAggregated,
@@ -10451,23 +10525,43 @@ async function parseCodebuddyIncremental({
10451
10525
  // is accepted by workbuddySqliteUsageSnapshot.
10452
10526
  // ─────────────────────────────────────────────────────────────────────────────
10453
10527
 
10454
- function resolveWorkbuddyHome(env = process.env) {
10455
- const home = env.HOME || require("node:os").homedir();
10456
- if (env.WORKBUDDY_HOME) return env.WORKBUDDY_HOME;
10528
+ // WorkBuddy ships two regional builds that keep their data in sibling homes:
10529
+ // ~/.workbuddy — Tencent CN build (models: hy*/glm*/deepseek-*/auto)
10530
+ // ~/.workbuddy-ai — international build (models: gpt-5.6-*, gpt-6-*)
10531
+ // Both write byte-identical JSONL, so every reader below is shared and only
10532
+ // the home directory (and the bucket source key) differs. `variant` is also
10533
+ // the source key written into hourly buckets, so the two builds stay
10534
+ // separable in the dashboard and never share dedup state.
10535
+ const WORKBUDDY_VARIANTS = {
10536
+ workbuddy: { dir: ".workbuddy", envVar: "WORKBUDDY_HOME" },
10537
+ "workbuddy-ai": { dir: ".workbuddy-ai", envVar: "WORKBUDDY_AI_HOME" },
10538
+ };
10539
+
10540
+ function resolveWorkbuddyVariant(variant) {
10541
+ return Object.prototype.hasOwnProperty.call(WORKBUDDY_VARIANTS, variant)
10542
+ ? variant
10543
+ : "workbuddy";
10544
+ }
10545
+
10546
+ function resolveWorkbuddyHome(env = process.env, variant = "workbuddy") {
10547
+ const spec = WORKBUDDY_VARIANTS[resolveWorkbuddyVariant(variant)];
10548
+ const home = env.HOME || env.USERPROFILE || require("node:os").homedir();
10549
+ const configured = expandHome(env[spec.envVar], home);
10550
+ if (configured) return configured;
10457
10551
  if (process.platform === "win32") {
10458
10552
  return pickWin32ProviderPath({
10459
10553
  env,
10460
- nativeValue: path.join(home, ".workbuddy"),
10461
- wslProviderDir: ".workbuddy",
10554
+ nativeValue: path.join(home, spec.dir),
10555
+ wslProviderDir: spec.dir,
10462
10556
  });
10463
10557
  }
10464
- return path.join(home, ".workbuddy");
10558
+ return path.join(home, spec.dir);
10465
10559
  }
10466
10560
 
10467
- function resolveWorkbuddyDefaultModel(env = process.env) {
10561
+ function resolveWorkbuddyDefaultModel(env = process.env, variant = "workbuddy") {
10468
10562
  const fallback = "auto";
10469
10563
  try {
10470
- const workbuddyHome = resolveWorkbuddyHome(env);
10564
+ const workbuddyHome = resolveWorkbuddyHome(env, variant);
10471
10565
  if (!workbuddyHome) return fallback;
10472
10566
  const settingsPath = path.join(workbuddyHome, "settings.json");
10473
10567
  const raw = fssync.readFileSync(settingsPath, "utf8");
@@ -10487,8 +10581,8 @@ function resolveWorkbuddyDefaultModel(env = process.env) {
10487
10581
  // ~/.workbuddy/traces/<pid>/trace_*.json. Those summaries are useful only when
10488
10582
  // the detailed JSONL for a session is absent, so they are returned as typed
10489
10583
  // entries and parsed after JSONL (JSONL remains authoritative when both exist).
10490
- function resolveWorkbuddyProjectFiles(env = process.env) {
10491
- const workbuddyHome = resolveWorkbuddyHome(env);
10584
+ function resolveWorkbuddyProjectFiles(env = process.env, variant = "workbuddy") {
10585
+ const workbuddyHome = resolveWorkbuddyHome(env, variant);
10492
10586
  if (!workbuddyHome) return [];
10493
10587
  const projectsDir = path.join(workbuddyHome, "projects");
10494
10588
  const files = [];
@@ -10597,10 +10691,15 @@ async function parseWorkbuddyIncremental({
10597
10691
  onProgress,
10598
10692
  env,
10599
10693
  defaultModel,
10694
+ source = "workbuddy",
10600
10695
  } = {}) {
10601
10696
  await ensureDir(path.dirname(queuePath));
10697
+ // `source` doubles as the variant selector and the bucket source key, so the
10698
+ // CN install (~/.workbuddy) and the international install (~/.workbuddy-ai)
10699
+ // get independent cursor namespaces and never share dedup state.
10700
+ const variant = resolveWorkbuddyVariant(source);
10602
10701
  const workbuddyState =
10603
- cursors.workbuddy && typeof cursors.workbuddy === "object" ? cursors.workbuddy : {};
10702
+ cursors[variant] && typeof cursors[variant] === "object" ? cursors[variant] : {};
10604
10703
  const seenIds = new Set(
10605
10704
  Array.isArray(workbuddyState.seenIds) ? workbuddyState.seenIds : [],
10606
10705
  );
@@ -10626,20 +10725,20 @@ async function parseWorkbuddyIncremental({
10626
10725
 
10627
10726
  const allFiles = Array.isArray(projectFiles)
10628
10727
  ? projectFiles
10629
- : resolveWorkbuddyProjectFiles(env || process.env);
10728
+ : resolveWorkbuddyProjectFiles(env || process.env, variant);
10630
10729
  const jsonlFiles = allFiles.filter((entry) => typeof entry === "string");
10631
10730
  const traceFiles = allFiles.filter(
10632
10731
  (entry) => entry && typeof entry === "object" && typeof entry.path === "string" && entry.kind === "trace",
10633
10732
  );
10634
10733
  const files = [...jsonlFiles, ...traceFiles];
10635
- const fallbackModel = defaultModel || resolveWorkbuddyDefaultModel(env || process.env);
10734
+ const fallbackModel = defaultModel || resolveWorkbuddyDefaultModel(env || process.env, variant);
10636
10735
 
10637
- const workbuddyHome = resolveWorkbuddyHome(env || process.env);
10736
+ const workbuddyHome = resolveWorkbuddyHome(env || process.env, variant);
10638
10737
  const dbPath = workbuddyHome ? path.join(workbuddyHome, "workbuddy.db") : null;
10639
10738
  const dbExists = Boolean(dbPath && fssync.existsSync(dbPath));
10640
10739
 
10641
10740
  if (files.length === 0 && !dbExists) {
10642
- cursors.workbuddy = {
10741
+ cursors[variant] = {
10643
10742
  ...workbuddyState,
10644
10743
  seenIds: Array.from(seenIds),
10645
10744
  seenTraceIds: Array.from(seenTraceIds),
@@ -10786,9 +10885,9 @@ async function parseWorkbuddyIncremental({
10786
10885
  conversation_count: 1,
10787
10886
  };
10788
10887
 
10789
- const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
10888
+ const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
10790
10889
  addTotals(bucket.totals, delta);
10791
- touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
10890
+ touchedBuckets.add(bucketKey(variant, model, bucketStart));
10792
10891
  seenIds.add(messageId);
10793
10892
  // Keep legacy entry-id keys alongside the response id so upgrading a
10794
10893
  // cursor cannot replay a row that an older build already counted.
@@ -10897,9 +10996,9 @@ async function parseWorkbuddyIncremental({
10897
10996
  total_tokens: inputTokens + totalCached + totalOutput,
10898
10997
  conversation_count: 1,
10899
10998
  };
10900
- const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
10999
+ const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
10901
11000
  addTotals(bucket.totals, delta);
10902
- touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
11001
+ touchedBuckets.add(bucketKey(variant, model, bucketStart));
10903
11002
  seenTraceIds.add(traceId);
10904
11003
  tracedSessionIds.add(sessionId);
10905
11004
  recordsProcessed++;
@@ -11032,9 +11131,9 @@ async function parseWorkbuddyIncremental({
11032
11131
  conversation_count: Object.keys(previousTokens).every((key) => toNonNegativeInt(previousTokens[key]) === 0) || isReset ? 1 : 0,
11033
11132
  };
11034
11133
 
11035
- const bucket = getHourlyBucket(hourlyState, "workbuddy", model, bucketStart);
11134
+ const bucket = getHourlyBucket(hourlyState, variant, model, bucketStart);
11036
11135
  addTotals(bucket.totals, delta);
11037
- touchedBuckets.add(bucketKey("workbuddy", model, bucketStart));
11136
+ touchedBuckets.add(bucketKey(variant, model, bucketStart));
11038
11137
  sqliteSessions[sessionId] = {
11039
11138
  used: usedNow,
11040
11139
  updatedAt: updatedAtRaw,
@@ -11085,7 +11184,7 @@ async function parseWorkbuddyIncremental({
11085
11184
  const updatedAt = new Date().toISOString();
11086
11185
  hourlyState.updatedAt = updatedAt;
11087
11186
  cursors.hourly = hourlyState;
11088
- cursors.workbuddy = {
11187
+ cursors[variant] = {
11089
11188
  ...workbuddyState,
11090
11189
  seenIds: cappedSeen,
11091
11190
  seenTraceIds: cappedSeenTraceIds,
@@ -15972,6 +16071,27 @@ function resolvePiSessionFiles(env = process.env) {
15972
16071
  } catch {
15973
16072
  // ignore — return what we have
15974
16073
  }
16074
+ // Multica launches pi headless with a dedicated flat session dir:
16075
+ // ~/.multica/pi-sessions/<session-id>.jsonl
16076
+ // Scan it too so orchestrated runs are counted alongside TUI sessions.
16077
+ try {
16078
+ const seen = new Set(files);
16079
+ const multicaSessionsDir = path.join(
16080
+ env.HOME || require("node:os").homedir(),
16081
+ ".multica",
16082
+ "pi-sessions",
16083
+ );
16084
+ for (const entry of fssync.readdirSync(multicaSessionsDir, { withFileTypes: true })) {
16085
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
16086
+ const full = path.join(multicaSessionsDir, entry.name);
16087
+ if (!seen.has(full)) {
16088
+ seen.add(full);
16089
+ files.push(full);
16090
+ }
16091
+ }
16092
+ } catch {
16093
+ // ignore — no Multica install
16094
+ }
15975
16095
  files.sort((a, b) => a.localeCompare(b));
15976
16096
  return files;
15977
16097
  }
@@ -24514,6 +24634,8 @@ module.exports = {
24514
24634
  resolveWorkbuddyProjectFiles,
24515
24635
  resolveWorkbuddyDefaultModel,
24516
24636
  parseWorkbuddyIncremental,
24637
+ WORKBUDDY_VARIANTS,
24638
+ resolveWorkbuddyVariant,
24517
24639
  resolveKiroCliSessionFiles,
24518
24640
  resolveKiroCliDbPath,
24519
24641
  parseKiroCliIncremental,
@@ -70,7 +70,7 @@ const {
70
70
  // long-context subset) and folds delivery signals into the token parser's
71
71
  // single pass instead of parsing every Codex file twice.
72
72
  // v14 adds request-level estimated performance and Claude model usage.
73
- const SIDECAR_VERSION = 14;
73
+ const SIDECAR_VERSION = 16;
74
74
  const EDIT_TOOLS = new Set([
75
75
  "apply_patch",
76
76
  "edit",
@@ -123,12 +123,23 @@ function addTotals(target, delta) {
123
123
  for (const key of ["input_tokens", "cached_input_tokens", "cache_creation_input_tokens", "output_tokens", "reasoning_output_tokens", "total_tokens"]) {
124
124
  target[key] = finite(target[key]) + finite(delta?.[key]);
125
125
  }
126
+ for (const key of MODEL_USAGE_SUM_FIELDS) {
127
+ if (!/^(?:long_context_|priority_)|^(?:usage_events|rerouted_usage_events)$/.test(key)) continue;
128
+ if (delta?.[key]) target[key] = finite(target[key]) + finite(delta[key]);
129
+ }
126
130
  }
127
131
 
128
132
  function emptyTotals() {
129
133
  return tokenTotals({});
130
134
  }
131
135
 
136
+ function mergeUsageMetadata(target, usage) {
137
+ for (const field of ["selected_models", "reroute_reasons"]) {
138
+ if (Array.isArray(usage[field])) target[field] = [...new Set([...(target[field] || []), ...usage[field]])];
139
+ }
140
+ if (usage.model_attribution === "effective") target.model_attribution = "effective";
141
+ }
142
+
132
143
  const MODEL_USAGE_SUM_FIELDS = [
133
144
  "input_tokens",
134
145
  "cached_input_tokens",
@@ -241,6 +252,11 @@ function serializeSessionRecord(row) {
241
252
  }
242
253
 
243
254
  function repriceSessionRecord(record) {
255
+ if (Array.isArray(record.usage_buckets)) {
256
+ record.usage_buckets = record.usage_buckets.map((bucket) => ({
257
+ ...bucket, cost_usd: computeRowCost({ source: record.source, ...bucket }),
258
+ }));
259
+ }
244
260
  const modelUsage = normalizeModelUsageRows(record?.model_usage);
245
261
  if (modelUsage.length > 0) {
246
262
  let hasUnpricedUsage = false;
@@ -469,6 +485,19 @@ function finalizeRecord(record) {
469
485
  return record;
470
486
  }
471
487
 
488
+ function sessionStorageUsage(filePaths) {
489
+ let bytes = 0;
490
+ const allocated = process.platform !== "win32";
491
+ for (const file of new Set(filePaths)) {
492
+ try {
493
+ const stat = fs.statSync(file);
494
+ if (!stat.isFile()) continue;
495
+ bytes += allocated && Number.isFinite(stat.blocks) ? stat.blocks * 512 : stat.size;
496
+ } catch { /* deleted or unreadable while scanning */ }
497
+ }
498
+ return { session_store_bytes: bytes, session_store_metric: allocated ? "allocated" : "logical" };
499
+ }
500
+
472
501
  function readableSessionPaths(filePath) {
473
502
  const candidates = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
474
503
  const readable = candidates.filter((value) => sessionFileStatKey(value));
@@ -623,6 +652,7 @@ async function scanClaudeSession(filePath) {
623
652
  // wrote one — the UI then falls back to the project name.
624
653
  title: aiTitle,
625
654
  source: "claude",
655
+ ...sessionStorageUsage(filePaths),
626
656
  project_key: projectKey(cwd, primaryFilePath),
627
657
  project_ref: cwd || null,
628
658
  model,
@@ -735,6 +765,7 @@ async function scanCodexSession(filePath) {
735
765
  const primaryFilePath = filePaths[0] || String(filePath || "");
736
766
  const signalCollector = createCodexDeliverySignalCollector();
737
767
  const performanceCollector = createCodexPerformanceCollector();
768
+ const usageBuckets = new Map();
738
769
  const parsed = await parseCodexRolloutFile(filePaths, {
739
770
  seenTokenEvents: new Set(),
740
771
  collectBreakdowns: false,
@@ -743,7 +774,20 @@ async function scanCodexSession(filePath) {
743
774
  signalCollector.consume(obj, model);
744
775
  performanceCollector.consume(obj, model);
745
776
  },
746
- onUsage: performanceCollector.consumeUsage,
777
+ onUsage(event) {
778
+ if (event.origin !== "compaction") performanceCollector.consumeUsage(event);
779
+ if (!event.delta || event.delta.total_tokens <= 0) return;
780
+ const ms = Date.parse(event.timestamp);
781
+ if (!Number.isFinite(ms)) return;
782
+ // Minute buckets preserve every current IANA local-midnight boundary
783
+ // and every integer tz_offset_minutes accepted by the local API.
784
+ const timestamp = new Date(Math.floor(ms / 60_000) * 60_000).toISOString();
785
+ const key = `${timestamp}\u0000${event.model}`;
786
+ const bucket = usageBuckets.get(key) || { timestamp, model: event.model || "unknown", ...emptyTotals() };
787
+ addTotals(bucket, event.delta);
788
+ mergeUsageMetadata(bucket, event.delta);
789
+ usageBuckets.set(key, bucket);
790
+ },
747
791
  });
748
792
  const signals = signalCollector.finish();
749
793
  const performance = performanceCollector.finish();
@@ -798,10 +842,12 @@ async function scanCodexSession(filePath) {
798
842
  // Local-only: stripped in summarizeSessions before any cloud/CSV export.
799
843
  title,
800
844
  source: "codex",
845
+ ...sessionStorageUsage(filePaths),
801
846
  project_key: projectKey(parsed.cwd, primaryFilePath),
802
847
  project_ref: parsed.cwd || null,
803
848
  model,
804
849
  model_usage: modelUsage,
850
+ usage_buckets: [...usageBuckets.values()],
805
851
  performance: performance.performance,
806
852
  ...signals.bounds,
807
853
  turns: signals.turns || finite(parsed.turnCount),
@@ -1079,6 +1125,7 @@ async function scanGrokSession(filePath) {
1079
1125
  // Local-only: stripped in summarizeSessions before any cloud/CSV export.
1080
1126
  title,
1081
1127
  source: "grok",
1128
+ ...sessionStorageUsage([filePath, grokSummaryPathFor(filePath), grokSignalsPathFor(filePath)]),
1082
1129
  project_key: projectKey(cwd, filePath),
1083
1130
  project_ref: cwd || null,
1084
1131
  model: model || "unknown",
@@ -1617,17 +1664,55 @@ function buildSessionAnalytics(options = {}) {
1617
1664
  // exactly the long/resumed ones, which also sort to the top because the sort
1618
1665
  // key is ended_at. On real data a 7-day window lost 6 sessions, the largest
1619
1666
  // 224M tokens. Treat the window as an interval intersection instead.
1620
- function withinDayRange(row, from, to) {
1621
- const startDay = String(row?.started_at || row?.ended_at || "").slice(0, 10);
1622
- const endDay = String(row?.ended_at || row?.started_at || "").slice(0, 10);
1667
+ function sessionUsageDay(timestamp, { timeZone, offsetMinutes } = {}) {
1668
+ const date = new Date(timestamp);
1669
+ if (!Number.isFinite(date.getTime())) return "";
1670
+ if (timeZone) {
1671
+ try {
1672
+ const parts = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date);
1673
+ const part = (name) => parts.find((p) => p.type === name)?.value;
1674
+ return `${part("year")}-${part("month")}-${part("day")}`;
1675
+ } catch { /* fall back to the caller's offset or UTC */ }
1676
+ }
1677
+ return new Date(date.getTime() + (Number.isFinite(offsetMinutes) ? offsetMinutes * 60_000 : 0)).toISOString().slice(0, 10);
1678
+ }
1679
+
1680
+ function withinDayRange(row, from, to, context) {
1681
+ const startDay = sessionUsageDay(row?.started_at || row?.ended_at || "", context);
1682
+ const endDay = sessionUsageDay(row?.ended_at || row?.started_at || "", context);
1623
1683
  if (from && (!endDay || endDay < from)) return false;
1624
1684
  if (to && (!startDay || startDay > to)) return false;
1625
1685
  return true;
1626
1686
  }
1627
1687
 
1628
- function summarizeSessions(sessions, { from = "", to = "", includeSessions = true } = {}) {
1688
+ function scopeSessionUsage(row, from, to, context) {
1689
+ if ((!from && !to) || !Array.isArray(row.usage_buckets)) return { ...row };
1690
+ const buckets = row.usage_buckets.filter((bucket) => {
1691
+ const day = sessionUsageDay(bucket.timestamp, context);
1692
+ return (!from || day >= from) && (!to || day <= to);
1693
+ });
1694
+ const tokens = emptyTotals();
1695
+ const byModel = new Map();
1696
+ for (const bucket of buckets) {
1697
+ addTotals(tokens, bucket);
1698
+ const model = byModel.get(bucket.model) || { model: bucket.model, ...emptyTotals(), usage_events: 0 };
1699
+ addTotals(model, bucket);
1700
+ mergeUsageMetadata(model, bucket);
1701
+ if (!bucket.usage_events) model.usage_events += 1;
1702
+ byModel.set(bucket.model, model);
1703
+ }
1704
+ return repriceSessionRecord({ ...row, tokens, total_tokens: tokens.total_tokens,
1705
+ model_usage: [...byModel.values()], usage_buckets: buckets, cost_usd: 0,
1706
+ lifetime_total_tokens: row.total_tokens, lifetime_cost_usd: row.cost_usd });
1707
+ }
1708
+
1709
+ function summarizeSessions(sessions, { from = "", to = "", includeSessions = true, timeZone, offsetMinutes } = {}) {
1710
+ const context = { timeZone, offsetMinutes };
1629
1711
  const filtered = annotateCodexThreadUsage((sessions || [])
1630
- .filter((row) => withinDayRange(row, from, to))
1712
+ .filter((row) => withinDayRange(row, from, to, context))
1713
+ // Efficiency is a session-lifetime metric: both tokens and edit turns
1714
+ // must describe that same population. The browser scopes consumption
1715
+ // separately; do not divide in-range tokens by lifetime edit counts here.
1631
1716
  .map((row) => ({ ...row })));
1632
1717
  const byModel = new Map();
1633
1718
  const subagents = new Map();
@@ -1838,12 +1923,19 @@ function toSessionBrowserRow(row) {
1838
1923
  parent_link_conflict: Boolean(row.parent_link_conflict),
1839
1924
  title: row.title || null,
1840
1925
  source: row.source,
1926
+ ...(Number.isFinite(row.session_store_bytes) ? {
1927
+ session_store_bytes: row.session_store_bytes,
1928
+ session_store_metric: row.session_store_metric,
1929
+ bytes_per_1k_tokens: (row.lifetime_total_tokens ?? row.total_tokens) > 0
1930
+ ? row.session_store_bytes * 1000 / (row.lifetime_total_tokens ?? row.total_tokens) : null,
1931
+ } : {}),
1841
1932
  project_key: row.project_key,
1842
1933
  // project_ref (the local cwd) only ever travels over the local API so the
1843
1934
  // browser can show where a session ran and compose a resume command.
1844
1935
  project_ref: row.project_ref || null,
1845
1936
  model: row.model,
1846
1937
  model_usage: modelUsageForAggregation(row),
1938
+ ...(Array.isArray(row.usage_buckets) ? { usage_buckets: row.usage_buckets } : {}),
1847
1939
  performance: normalizePerformance(row.performance),
1848
1940
  started_at: row.started_at || null,
1849
1941
  ended_at: row.ended_at || null,
@@ -1914,11 +2006,15 @@ function mergeSessionFragments(rows) {
1914
2006
  cur.performance = mergePerformance(cur.performance, row.performance);
1915
2007
  cur.subagent_calls = finite(cur.subagent_calls) + finite(row.subagent_calls);
1916
2008
  addTotals(cur.tokens, row.tokens);
2009
+ if (Array.isArray(row.usage_buckets)) {
2010
+ cur.usage_buckets = [...(cur.usage_buckets || []), ...row.usage_buckets];
2011
+ }
1917
2012
  cur.model_usage = normalizeModelUsageRows([
1918
2013
  ...(cur.model_usage || []),
1919
2014
  ...(row.model_usage || []),
1920
2015
  ]);
1921
2016
  cur.total_tokens = finite(cur.total_tokens) + finite(row.total_tokens);
2017
+ if (Number.isFinite(row.session_store_bytes)) cur.session_store_bytes = finite(cur.session_store_bytes) + row.session_store_bytes;
1922
2018
  cur.cost_usd = finite(cur.cost_usd) + finite(row.cost_usd);
1923
2019
  cur.provider_cost_usd = finite(cur.provider_cost_usd) + finite(row.provider_cost_usd);
1924
2020
  cur.usage_events = finite(cur.usage_events) + finite(row.usage_events);
@@ -2095,7 +2191,8 @@ function annotateCodexThreadUsage(rows) {
2095
2191
  // summarizeSessions (cloud/CSV safe), this retains session_id + project_ref so
2096
2192
  // the UI can offer one-click resume. Callers must only expose it over the
2097
2193
  // local API.
2098
- function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}) {
2194
+ function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0, timeZone, offsetMinutes } = {}) {
2195
+ const context = { timeZone, offsetMinutes };
2099
2196
  const filtered = annotateCodexThreadUsage(mergeSessionFragments(sessions)
2100
2197
  // Only sessions that actually spent tokens are worth listing. This drops
2101
2198
  // two kinds of noise: non-session logs under ~/.claude/projects
@@ -2103,7 +2200,9 @@ function listSessionsForBrowser(sessions, { from = "", to = "", limit = 0 } = {}
2103
2200
  // the model replied. Both render as "unknown · 0 tokens · $0.00" rows with
2104
2201
  // no model, no cost and nothing to analyze.
2105
2202
  .filter((row) => finite(row.total_tokens) > 0)
2106
- .filter((row) => withinDayRange(row, from, to)));
2203
+ .filter((row) => withinDayRange(row, from, to, context))
2204
+ .map((row) => scopeSessionUsage(row, from, to, context))
2205
+ .filter((row) => !Array.isArray(row.usage_buckets) || finite(row.total_tokens) > 0));
2107
2206
  filtered.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
2108
2207
  const cap = Number(limit) > 0 ? Number(limit) : 0;
2109
2208
  const limited = cap > 0 ? filtered.slice(0, cap) : filtered;