tokentracker-cli 0.85.0 → 0.86.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.de.md +7 -4
  2. package/README.ja.md +7 -4
  3. package/README.ko.md +7 -4
  4. package/README.md +10 -7
  5. package/README.zh-CN.md +7 -4
  6. package/dashboard/dist/assets/{AchievementBadge-DEqbHlmn.js → AchievementBadge-BHWjTRZh.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BOaEu1AQ.js → AchievementsPage-X6Z6fiAx.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-C370ngO3.js → AchievementsSection-DL2CccQ8.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D7Wkef0K.js → BadgeDetailModal-DKn88JDX.js} +1 -1
  10. package/dashboard/dist/assets/{Card-mDEVjNdM.js → Card-B_gKsFZR.js} +1 -1
  11. package/dashboard/dist/assets/{ClawdAnimated-DdsX-z8n.js → ClawdAnimated-vEB5WCNu.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-DhxRIuHC.js → CommandPalette-BoxeCsUF.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-CG-9JPS6.js → CommunityStatsModal-BwELjQld.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-COhxErLm.js → ConfirmModal-j8yHWm2k.js} +1 -1
  15. package/dashboard/dist/assets/{DashboardPage-tX0DKgFE.js → DashboardPage-CkvAOuBF.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-BsSeHx84.js → DevicePage-k4Lr11LG.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-eyOW2cbx.js → DialogClose-C-ZhX9eY.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-BRiqm-uZ.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-H8sGiH7c.js → DialogTitle-XCGSHZDz.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-pq2ja50M.js → FadeIn-ljP_7vPC.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-DWgcpDHr.js → HeaderGithubStar-D4QupeSb.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-CcJ_VR65.js → HoverTooltip-Dkpz8RmH.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-CNUN346Q.js → IpCheckPage-WkZK9Tr3.js} +1 -1
  24. package/dashboard/dist/assets/LandingPage-Dp3m0uJg.js +2 -0
  25. package/dashboard/dist/assets/{LeaderboardAvatar-Ddai4I12.js → LeaderboardAvatar-DleP6bK7.js} +1 -1
  26. package/dashboard/dist/assets/{LeaderboardPage-Bajkxf63.js → LeaderboardPage-DTwMMaF2.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-BGYKDgIi.js → LeaderboardProfileModal-xdDlcRDd.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-Ck5vPaeN.js → LeaderboardProfilePage-Ckytgq9h.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-Bqobtsw2.js → LimitsPage-D_2cV3S4.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-e9KSE3wj.js → LocalOnlyNotice-DpCb8VIO.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-BhTSm2tb.js → LoginCard-BeAb35_3.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-CRwuThde.js → LoginModal-DNdAgKeE.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-j1i74lcc.js → LoginPage-nuWkBuqy.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-r62ViU1X.js → PetPage-CCLFROsF.js} +1 -1
  35. package/dashboard/dist/assets/{PopoverPopup-DopNTyD4.js → PopoverPopup-BCaeRKf4.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-c1LkPhYM.js → ProviderIcon-C_Fpc2jL.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DJdDNvtq.js → ResetPasswordPage-lLDxtpC9.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-DhWVOTPN.js → SegmentedControl-Bnfv5lDS.js} +1 -1
  39. package/dashboard/dist/assets/{Select-DeUmTmsJ.js → Select-CvY_Y0WS.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-pX0B5f7h.js → SelectItemText-DX4QYfBW.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-ejIPz6oF.js → ServiceStatusPage-DcUrtacl.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-WEbn90GW.js → SessionsPage-CJXyaHQA.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-B8t5gCeI.js → SettingsPage-BkgPxhGn.js} +1 -1
  44. package/dashboard/dist/assets/{SkillsPage-BWMlzeAp.js → SkillsPage-BGvK3HZ0.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-DIBRBvMj.js → TokenGalaxy-D9_H79bK.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-C6KxLvnI.js → ToolbarRootContext-BsM_3QBs.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-BaMNh67w.js → TrendMonitor-D2QSVlSi.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DDsy4M3x.js → WidgetsPage-BpWOr7-c.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-CiNnUFRI.js → WrappedPage-DliB3OZk.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-CM-H8-LW.js → __vite-browser-external-CzDhMv0q.js} +15 -14
  51. package/dashboard/dist/assets/{agent-logos-BjVa6oe7.js → agent-logos-Q7rTqkGv.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-up-right-CCum1mqX.js → arrow-up-right-DwqQgMsR.js} +1 -1
  53. package/dashboard/dist/assets/{check-ySRQc1YP.js → check-Cypjnyke.js} +1 -1
  54. package/dashboard/dist/assets/{chevron-right-5vFloHew.js → chevron-right-LZRe0crm.js} +1 -1
  55. package/dashboard/dist/assets/{copy-mhfKVMKo.js → copy-BUe15mXV.js} +1 -1
  56. package/dashboard/dist/assets/{download-GdrXglSx.js → download-BhjaQuUD.js} +1 -1
  57. package/dashboard/dist/assets/{external-link-KGFaKc8s.js → external-link-BHU7m_Df.js} +1 -1
  58. package/dashboard/dist/assets/{flame-BmNtNoS1.js → flame-BPQ7FpRu.js} +1 -1
  59. package/dashboard/dist/assets/{format-tokens-CCpCNw3d.js → format-tokens-C0XEJ96L.js} +1 -1
  60. package/dashboard/dist/assets/{icons-XLKZavTf.js → icons-De5Qo1lp.js} +1 -1
  61. package/dashboard/dist/assets/{index-IKpmeZte.js → index-Cl8hqOBU.js} +1 -1
  62. package/dashboard/dist/assets/{index-_xU5U5Ct.js → index-UfiJKvuV.js} +1 -1
  63. package/dashboard/dist/assets/{index-CsywTeCQ.js → index-lmWJH5W9.js} +1 -1
  64. package/dashboard/dist/assets/{info-DUV5UpmT.js → info-CCPDu6hQ.js} +1 -1
  65. package/dashboard/dist/assets/{limits-providers-DFkCZCZj.js → limits-providers-B2Qjydn9.js} +1 -1
  66. package/dashboard/dist/assets/{link-2-D7c64yLY.js → link-2-Dl1DAfTL.js} +1 -1
  67. package/dashboard/dist/assets/{loader-circle-sMONRK2M.js → loader-circle-C-mC8s6j.js} +1 -1
  68. package/dashboard/dist/assets/{maximize-2-CVWRgrk9.js → maximize-2-C9YpCcsr.js} +1 -1
  69. package/dashboard/dist/assets/{provider-display-DSKqVjgm.js → provider-display-DNXQkneb.js} +2 -2
  70. package/dashboard/dist/assets/{react-c1q4mGkS.js → react-LsYzFOQW.js} +1 -1
  71. package/dashboard/dist/assets/{search-CwD3s-_C.js → search-D5SGjOTl.js} +1 -1
  72. package/dashboard/dist/assets/{skills-api-BRXj8kjT.js → skills-api-CGRg1lZw.js} +1 -1
  73. package/dashboard/dist/assets/{store-lqTW2gfK.js → store-BMdm145i.js} +1 -1
  74. package/dashboard/dist/assets/{terminal-DU1_KbKo.js → terminal-HhtP7nqJ.js} +1 -1
  75. package/dashboard/dist/assets/{trash-2-B8Y5Xc-K.js → trash-2-CeEoITVO.js} +1 -1
  76. package/dashboard/dist/assets/{use-community-stats-DYwwimvp.js → use-community-stats-BTZHzzWd.js} +1 -1
  77. package/dashboard/dist/assets/{use-limits-display-prefs-Bst6ybK3.js → use-limits-display-prefs-DPNlMKcg.js} +1 -1
  78. package/dashboard/dist/assets/{use-native-settings-Cqle4lgw.js → use-native-settings-DFF0ouY5.js} +1 -1
  79. package/dashboard/dist/assets/{use-ordered-list-Dtze0ahb.js → use-ordered-list-CCLtmxXg.js} +1 -1
  80. package/dashboard/dist/assets/use-reduced-motion-DUtjZjIj.js +1 -0
  81. package/dashboard/dist/assets/{use-session-efficiency-pref-gwbDMHXa.js → use-session-efficiency-pref-CrvLKyhz.js} +1 -1
  82. package/dashboard/dist/assets/{use-transform-CU0yxXpX.js → use-transform-BnJ0PftW.js} +1 -1
  83. package/dashboard/dist/assets/{use-usage-limits-5y1I1T5_.js → use-usage-limits-BY3ZXlDo.js} +1 -1
  84. package/dashboard/dist/assets/{useCurrency-BfVNrywe.js → useCurrency-BiFDQKMu.js} +1 -1
  85. package/dashboard/dist/assets/{useScrollLock-CF1m7AMJ.js → useScrollLock-BXUoxe5p.js} +1 -1
  86. package/dashboard/dist/assets/{useTokenFormat-DiRwVvRr.js → useTokenFormat-C6DBFS-R.js} +1 -1
  87. package/dashboard/dist/assets/{zap-BZtlWtuN.js → zap-9H5t1Kye.js} +1 -1
  88. package/dashboard/dist/index.html +12 -10
  89. package/dashboard/dist/ip-check.html +1 -1
  90. package/dashboard/dist/leaderboard.html +1 -1
  91. package/dashboard/dist/llms.txt +3 -2
  92. package/dashboard/dist/share.html +1 -1
  93. package/package.json +2 -2
  94. package/src/commands/init.js +1 -0
  95. package/src/commands/status.js +15 -0
  96. package/src/commands/sync.js +117 -10
  97. package/src/lib/pricing/seed-snapshot.json +1 -1
  98. package/src/lib/rollout.js +471 -7
  99. package/src/lib/wsl-probe.js +36 -0
  100. package/dashboard/dist/assets/DialogDescription-BwtE4lKl.js +0 -1
  101. package/dashboard/dist/assets/LandingPage-yaoRGsNI.js +0 -2
  102. package/dashboard/dist/assets/use-reduced-motion-CbZDR54N.js +0 -1
@@ -66,6 +66,7 @@ const {
66
66
  resolveRoocodeTaskFiles,
67
67
  resolveZedDbPath,
68
68
  resolveQoderDbPaths,
69
+ resolveClaudeScienceDbPaths,
69
70
  resolveAnythingllmDbPath,
70
71
  resolveGooseDbPath,
71
72
  listDroidSettingsFiles,
@@ -425,6 +426,14 @@ async function cmdStatus(argv = []) {
425
426
  const qoderInstalled = qoderActive.length > 0;
426
427
  const qoderDbPath = qoderActive.join(" | ");
427
428
 
429
+ // Claude Science — token usage lives on the `frames` table of operon-cli.db.
430
+ // Unlike the native/WSL pair other providers resolve to, this is an open-ended
431
+ // list: multi-org installs keep one DB per org (and on Windows they all sit
432
+ // inside WSL), so the resolver already returns only paths that exist.
433
+ const claudeScienceActive = resolveClaudeScienceDbPaths({ home, env: process.env });
434
+ const claudeScienceInstalled = claudeScienceActive.length > 0;
435
+ const claudeScienceDbPath = claudeScienceActive.join(" | ");
436
+
428
437
  // OpenCode (JSON files + SQLite DB) — passive scan of storage/message/ and opencode.db.
429
438
  const opencodeStorageNativeValue = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
430
439
  const wslOpencodeStorageDir = process.platform === "win32" && shouldProbeWsl(process.env)
@@ -786,6 +795,9 @@ async function cmdStatus(argv = []) {
786
795
  qoder: qoderInstalled
787
796
  ? { installed: true, detail: qoderDbPath }
788
797
  : { installed: false },
798
+ "claude-science": claudeScienceInstalled
799
+ ? { installed: true, detail: claudeScienceDbPath }
800
+ : { installed: false },
789
801
  kilocode: kilocodeInstalled
790
802
  ? { installed: true, files: kilocodeTaskFiles.length }
791
803
  : { installed: false },
@@ -907,6 +919,9 @@ async function cmdStatus(argv = []) {
907
919
  qoderInstalled
908
920
  ? `- Qoder: passive reader (${qoderDbPath})`
909
921
  : null,
922
+ claudeScienceInstalled
923
+ ? `- Claude Science: passive reader (${claudeScienceDbPath})`
924
+ : null,
910
925
  opencodeInstalled
911
926
  ? `- OpenCode: passive reader (storage: ${opencodeStorageActive.join(" | ") || "not found"}, DB: ${opencodeDbActive.join(" | ") || "not found"})`
912
927
  : null,
@@ -40,6 +40,12 @@ const {
40
40
  parseOpencodeDbIncremental,
41
41
  parseQoderDbIncremental,
42
42
  parseOpenclawIncremental,
43
+ resolveOpenclawSessionFiles,
44
+ resolveOpenclawHome,
45
+ openclawCursorKey,
46
+ resolveClaudeScienceDbPaths,
47
+ readClaudeScienceFrames,
48
+ parseClaudeScienceIncremental,
43
49
  parseCursorApiIncremental,
44
50
  parseKiroIncremental,
45
51
  parseHermesIncremental,
@@ -237,6 +243,7 @@ const AUTO_SYNC_SOURCES = new Set([
237
243
  "antigravity",
238
244
  "anythingllm",
239
245
  "claude",
246
+ "claude-science",
240
247
  "codebuddy",
241
248
  "codex",
242
249
  "copilot",
@@ -473,7 +480,7 @@ async function cmdSync(argv, context = {}) {
473
480
  // OpenClaw session plugin integration: lifecycle hooks request an
474
481
  // OpenClaw-only auto sync so unrelated providers do not get walked.
475
482
  const openclawSignal = opts.fromOpenclaw
476
- ? resolveOpenclawSignal({ home, env: process.env })
483
+ ? resolveOpenclawSignal({ env: process.env })
477
484
  : null;
478
485
 
479
486
  const autoSourceScope = resolveAutoSourceScope(opts);
@@ -668,9 +675,38 @@ async function cmdSync(argv, context = {}) {
668
675
  codexCursorLoadRestarted = Boolean(loadResult?.restarted);
669
676
  }
670
677
 
671
- const openclawFiles = openclawSignal?.sessionFile
672
- ? [{ path: openclawSignal.sessionFile, source: "openclaw" }]
673
- : [];
678
+ // Plugin-triggered sync points at one specific session file; a normal full
679
+ // sync also passively scans every on-disk OpenClaw transcript so usage is
680
+ // captured even when the session plugin never fires (issue #264). The scan
681
+ // is gated to full syncs so a scoped `--from-openclaw` hook still only
682
+ // touches its own file and the 5-minute background tick stays cheap. The
683
+ // event-identity dedup makes the plugin and passive paths idempotent, so
684
+ // overlap on the same file is safe.
685
+ //
686
+ // Keyed by openclawCursorKey, not the raw path: that is the same identity
687
+ // the parser assigns cursors by, so a plugin-supplied path and a scanned
688
+ // path that differ only in Windows casing collapse to one entry instead of
689
+ // being parsed twice.
690
+ const openclawSessionFiles = new Map();
691
+ if (openclawSignal?.sessionFile) {
692
+ openclawSessionFiles.set(openclawCursorKey(openclawSignal.sessionFile), {
693
+ path: openclawSignal.sessionFile,
694
+ source: "openclaw",
695
+ });
696
+ }
697
+ if (isFullSourceScan && sourceAllowed("openclaw")) {
698
+ try {
699
+ for (const f of await resolveOpenclawSessionFiles(process.env)) {
700
+ const key = openclawCursorKey(f);
701
+ if (!openclawSessionFiles.has(key)) {
702
+ openclawSessionFiles.set(key, { path: f, source: "openclaw" });
703
+ }
704
+ }
705
+ } catch (err) {
706
+ warnProviderParseFailure("OpenClaw", err, opts);
707
+ }
708
+ }
709
+ const openclawFiles = Array.from(openclawSessionFiles.values());
674
710
 
675
711
  if (progress?.enabled) {
676
712
  progress.start(
@@ -740,7 +776,7 @@ async function cmdSync(argv, context = {}) {
740
776
 
741
777
  let openclawResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
742
778
  if (sourceAllowed("openclaw") && openclawFiles.length > 0) {
743
- // Only runs when explicitly triggered by the OpenClaw session plugin.
779
+ // Parses plugin-triggered and/or passively discovered session files.
744
780
  try {
745
781
  openclawResult = await parseOpenclawIncremental({
746
782
  sessionFiles: openclawFiles,
@@ -971,6 +1007,7 @@ async function cmdSync(argv, context = {}) {
971
1007
  dbResult = await parseOpencodeDbIncremental({
972
1008
  ...options,
973
1009
  dbMessages,
1010
+ dbPath,
974
1011
  });
975
1012
  }
976
1013
  }
@@ -1030,7 +1067,7 @@ async function cmdSync(argv, context = {}) {
1030
1067
  paths: qoderPaths,
1031
1068
  parserFn: async ({ dbPath, ...rest }) => {
1032
1069
  const dbMessages = await readQoderDbMessages(dbPath);
1033
- const parsed = await parseQoderDbIncremental({ dbMessages, ...rest });
1070
+ const parsed = await parseQoderDbIncremental({ dbMessages, dbPath, ...rest });
1034
1071
  return {
1035
1072
  recordsProcessed: parsed.messagesProcessed || 0,
1036
1073
  eventsAggregated: parsed.eventsAggregated || 0,
@@ -1055,13 +1092,52 @@ async function cmdSync(argv, context = {}) {
1055
1092
  }
1056
1093
  }
1057
1094
 
1095
+ // ── Claude Science (Anthropic's local research workbench, issue #246) ──
1096
+ // Per-frame token usage lives on the `frames` table of operon-cli.db.
1097
+ // Passive, incremental, subtract-on-change. There can be more than one DB:
1098
+ // multi-org installs keep one per org, and on Windows the app runs inside
1099
+ // WSL so the DB is on the distro home.
1100
+ let claudeScienceResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1101
+ if (sourceAllowed("claude-science")) {
1102
+ // Resolution is inside the error isolation too: it touches the filesystem
1103
+ // and (on Windows) probes WSL, and a throw here would abort cmdSync before
1104
+ // any provider's buckets are written, not just this one's.
1105
+ let claudeScienceDbPaths = [];
1106
+ try {
1107
+ claudeScienceDbPaths = resolveClaudeScienceDbPaths({ home, env: process.env });
1108
+ } catch (err) {
1109
+ warnProviderParseFailure("Claude Science", err, opts);
1110
+ }
1111
+ if (claudeScienceDbPaths.length > 0 && progress?.enabled) {
1112
+ progress.start(`Parsing Claude Science ${renderBar(0)} | buckets 0`);
1113
+ }
1114
+ for (const claudeScienceDbPath of claudeScienceDbPaths) {
1115
+ try {
1116
+ const dbRows = await readClaudeScienceFrames(claudeScienceDbPath);
1117
+ const parsed = await parseClaudeScienceIncremental({
1118
+ dbRows,
1119
+ cursors,
1120
+ queuePath,
1121
+ onProgress: makeProviderProgress("Claude Science"),
1122
+ });
1123
+ claudeScienceResult = mergeParseResult(claudeScienceResult, {
1124
+ recordsProcessed: parsed.recordsProcessed || 0,
1125
+ eventsAggregated: parsed.eventsAggregated || 0,
1126
+ bucketsQueued: parsed.bucketsQueued || 0,
1127
+ });
1128
+ } catch (err) {
1129
+ warnProviderParseFailure("Claude Science", err, opts);
1130
+ }
1131
+ }
1132
+ }
1133
+
1058
1134
  async function parseOpencodeDbForInstall({ dbPath, readFn, source, cursorKey, ...rest }) {
1059
1135
  if (!dbPath || !fssync.existsSync(dbPath)) {
1060
1136
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1061
1137
  }
1062
1138
  const dbMessages = readFn(dbPath);
1063
1139
  if (dbMessages.length === 0) return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1064
- const result = await parseOpencodeDbIncremental({ dbMessages, source, cursorKey, ...rest });
1140
+ const result = await parseOpencodeDbIncremental({ dbMessages, dbPath, source, cursorKey, ...rest });
1065
1141
  return {
1066
1142
  recordsProcessed: result.messagesProcessed || 0,
1067
1143
  eventsAggregated: result.eventsAggregated || 0,
@@ -2215,6 +2291,7 @@ async function cmdSync(argv, context = {}) {
2215
2291
  antigravityResult.filesProcessed +
2216
2292
  opencodeResult.filesProcessed +
2217
2293
  qoderResult.recordsProcessed +
2294
+ claudeScienceResult.recordsProcessed +
2218
2295
  cursorResult.recordsProcessed +
2219
2296
  kiroResult.recordsProcessed +
2220
2297
  kiroCliResult.recordsProcessed +
@@ -2245,6 +2322,7 @@ async function cmdSync(argv, context = {}) {
2245
2322
  antigravityResult.bucketsQueued +
2246
2323
  opencodeResult.bucketsQueued +
2247
2324
  qoderResult.bucketsQueued +
2325
+ claudeScienceResult.bucketsQueued +
2248
2326
  cursorResult.bucketsQueued +
2249
2327
  kiroResult.bucketsQueued +
2250
2328
  kiroCliResult.bucketsQueued +
@@ -2586,15 +2664,18 @@ function normalizeString(value) {
2586
2664
  return trimmed.length > 0 ? trimmed : null;
2587
2665
  }
2588
2666
 
2589
- function resolveOpenclawSignal({ home, env } = {}) {
2667
+ function resolveOpenclawSignal({ env } = {}) {
2590
2668
  if (!env) return null;
2591
2669
 
2592
2670
  const agentId = normalizeString(env.TOKENTRACKER_OPENCLAW_AGENT_ID);
2593
2671
  const sessionId = normalizeString(env.TOKENTRACKER_OPENCLAW_PREV_SESSION_ID);
2594
2672
  if (!agentId || !sessionId) return null;
2595
2673
 
2596
- const openclawHome =
2597
- normalizeString(env.TOKENTRACKER_OPENCLAW_HOME) || path.join(home || os.homedir(), ".openclaw");
2674
+ // Resolve the OpenClaw home identically to the passive scanner so the
2675
+ // plugin-triggered file and passively discovered files share one path
2676
+ // spelling — otherwise the same transcript could get two cursors and be
2677
+ // counted twice (issue #264 review).
2678
+ const openclawHome = resolveOpenclawHome(env);
2598
2679
  const sessionFile = path.join(openclawHome, "agents", agentId, "sessions", `${sessionId}.jsonl`);
2599
2680
 
2600
2681
  const prevTotals = {
@@ -2627,6 +2708,16 @@ async function applyOpenclawTotalsFallback({
2627
2708
  return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2628
2709
  }
2629
2710
 
2711
+ // The passive/plugin transcript parse is authoritative: if this session's
2712
+ // transcript has ever yielded real per-event usage, synthesizing sessions.json
2713
+ // totals on top would count the same tokens twice (issue #264 review). Defer
2714
+ // to the real events and advance the fallback baseline so no stale delta lingers.
2715
+ const transcriptCursor =
2716
+ signal.sessionFile && cursors?.files
2717
+ ? cursors.files[openclawCursorKey(signal.sessionFile)]
2718
+ : null;
2719
+ const transcriptHasRealUsage = Boolean(transcriptCursor?.hasRealUsage);
2720
+
2630
2721
  const sessionKey = `${signal.agentId}:${signal.sessionId}`;
2631
2722
  const statePath = path.join(trackerDir, "openclaw.fallback.state.json");
2632
2723
  const fallbackFilePath = path.join(trackerDir, "openclaw.fallback.jsonl");
@@ -2635,6 +2726,22 @@ async function applyOpenclawTotalsFallback({
2635
2726
  const prev =
2636
2727
  sessions[sessionKey] && typeof sessions[sessionKey] === "object" ? sessions[sessionKey] : null;
2637
2728
 
2729
+ if (transcriptHasRealUsage) {
2730
+ sessions[sessionKey] = {
2731
+ totalTokens: normalizeNonNegativeInt(signal?.prevTotals?.totalTokens) || 0,
2732
+ inputTokens: normalizeNonNegativeInt(signal?.prevTotals?.inputTokens) || 0,
2733
+ outputTokens: normalizeNonNegativeInt(signal?.prevTotals?.outputTokens) || 0,
2734
+ model: normalizeString(signal?.prevTotals?.model) || "unknown",
2735
+ updatedAt: normalizeIsoOrEpoch(signal?.prevTotals?.updatedAt) || new Date().toISOString(),
2736
+ seenAt: new Date().toISOString(),
2737
+ coveredByEvents: true,
2738
+ };
2739
+ state.version = 1;
2740
+ state.sessions = sessions;
2741
+ await writeJson(statePath, state);
2742
+ return { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
2743
+ }
2744
+
2638
2745
  const current = {
2639
2746
  totalTokens: normalizeNonNegativeInt(signal?.prevTotals?.totalTokens) || 0,
2640
2747
  inputTokens: normalizeNonNegativeInt(signal?.prevTotals?.inputTokens) || 0,