tokentracker-cli 0.94.0 → 0.95.0

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 (130) hide show
  1. package/README.de.md +9 -7
  2. package/README.ja.md +8 -6
  3. package/README.ko.md +8 -6
  4. package/README.md +23 -10
  5. package/README.zh-CN.md +8 -6
  6. package/dashboard/dist/assets/{AchievementBadge-DPqwk2zI.js → AchievementBadge-Pi3Gvbm8.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CELHoMQt.js → AchievementsPage-C4WLT8Rl.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DdAC0VCb.js → AchievementsSection-ilvR2ssr.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-Dd4I1SpM.js → BadgeDetailModal-CjEeToxa.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-DL2-4mnm.js → BotAnimated-DDf9pBuK.js} +1 -1
  11. package/dashboard/dist/assets/{Card-B_MpSRse.js → Card-DNous84-.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-BLZZ4rG6.js → ClawdAnimated-BLMIRntb.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-B-vfdIbH.js → CommandPalette-vVekhoFO.js} +1 -1
  14. package/dashboard/dist/assets/CommunityStatsModal-C0Kp_J8B.js +1 -0
  15. package/dashboard/dist/assets/{ConfirmModal-DLnRwUXC.js → ConfirmModal-DFdpcx12.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-FZ8wEN5w.js → Controls-RNhqybmE.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-BQjQh1IQ.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-D2IzPc5o.js → DevicePage-DIhh-YAk.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-BaqQaUUk.js → DialogClose-ZqxYVIzl.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-DDM4aanX.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-DrbKjtuS.js → DialogTitle-BiRcejlu.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-GddKDE8i.js → FadeIn-BDBQpeGe.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-DUu-Z5mE.js → HeaderGithubStar-CztIwtKT.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-BmDkY35A.js → HoverTooltip-Cfso7LTC.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-CGJyqWk2.js → IpCheckPage-CNE7dLs3.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-Bd91ALUj.js → LandingPage-B2a3cweM.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-BGGUwXFd.js → LeaderboardAvatar-CwmZnIji.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-BMEqHwif.js → LeaderboardPage-DkTHj3fB.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-BSweJqm2.js → LeaderboardProfileModal-CefTO3Zh.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-CRrI3RFz.js → LeaderboardProfilePage-CnOXlQ3L.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-XIRDv46K.js → LimitsPage-BIZ1-6BB.js} +2 -2
  32. package/dashboard/dist/assets/{LocalOnlyNotice-CCD-lGWG.js → LocalOnlyNotice-CdHQiPhy.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-lDuqSv4f.js → LoginCard-yXLA9geu.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-DStt5VXN.js → LoginModal-BF2Pfkqa.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-dmVPSL72.js → LoginPage-DuOypZap.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-ybcZRctF.js → PetPage-BH3xDVV9.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-Dar_UjxF.js → PopoverPopup-CewF_cGz.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-Ct9tILpz.js → ProviderIcon-Dg0K0RPn.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-B4yLh5zx.js → ResetPasswordPage-CYwt85O3.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-BOE1CEzq.js → SegmentedControl-BJkXXomQ.js} +1 -1
  41. package/dashboard/dist/assets/{Select-D-bSh7Zz.js → Select-B3_WH0Qe.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-DMYjXXq4.js → SelectItemText-BUby_DUu.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-CaleS-6_.js → ServiceStatusPage-BDlVZk4i.js} +1 -1
  44. package/dashboard/dist/assets/SessionsPage-TaC9nahK.js +2 -0
  45. package/dashboard/dist/assets/{SettingsPage-iJH0Yyah.js → SettingsPage-CudcQh6m.js} +2 -2
  46. package/dashboard/dist/assets/SkillsPage-iDLQ8lpZ.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-CuXKoMPg.js → TokenGalaxy-DtYzUsXU.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-Dx5GB7F5.js → ToolbarRootContext-PC8Tg1Jz.js} +1 -1
  49. package/dashboard/dist/assets/TrendMonitor-r8n8YO_9.js +19 -0
  50. package/dashboard/dist/assets/{WidgetsPage-DUaIyGtu.js → WidgetsPage-vzfGccWb.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-BzqlcAUC.js → WrappedPage-DaKuIkYE.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-BHz2N7Ih.css → __vite-browser-external-CCMdjCX2.css} +1 -1
  53. package/dashboard/dist/assets/__vite-browser-external-Dnxl50k1.js +45 -0
  54. package/dashboard/dist/assets/{agent-logos-BdWWxza6.js → agent-logos-B7nzn3n2.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-CANSzmm-.js → arrow-up-right-CsxyvCER.js} +1 -1
  56. package/dashboard/dist/assets/{check-BnJpzXRZ.js → check-hYcC7MdH.js} +1 -1
  57. package/dashboard/dist/assets/{composite-Ccj6KQmh.js → composite-C79LNbex.js} +1 -1
  58. package/dashboard/dist/assets/{copy-C2Yc8Evu.js → copy-Cz4qzE62.js} +1 -1
  59. package/dashboard/dist/assets/{download-B52JTk6Y.js → download-CUaSNE2E.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-DayTwZx-.js → external-link-vq7kIyKK.js} +1 -1
  61. package/dashboard/dist/assets/{flame-B8GEX2yT.js → flame-CB6K4eR6.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-mhkbohkb.js → format-tokens-KI-e_JQ-.js} +1 -1
  63. package/dashboard/dist/assets/{icons-o8bPXqRP.js → icons-3maItkrp.js} +1 -1
  64. package/dashboard/dist/assets/{index-MBVWUKpA.js → index-BRXoZ7Jj.js} +1 -1
  65. package/dashboard/dist/assets/{index-CK_CduLm.js → index-C5WrfmLB.js} +1 -1
  66. package/dashboard/dist/assets/{index-Bp0ZHDtz.js → index-CUbC50Zp.js} +1 -1
  67. package/dashboard/dist/assets/{info-D94QM_vX.js → info-r0yGihUe.js} +1 -1
  68. package/dashboard/dist/assets/{limits-providers-BLzMyCiy.js → limits-providers-BN_94oe9.js} +1 -1
  69. package/dashboard/dist/assets/{link-2-CqBmFaDI.js → link-2-J9t0VXWM.js} +1 -1
  70. package/dashboard/dist/assets/{loader-circle-SaxUpDR6.js → loader-circle-DCvOsPze.js} +1 -1
  71. package/dashboard/dist/assets/{maximize-2-BoXqARks.js → maximize-2-CZ9JhGjf.js} +1 -1
  72. package/dashboard/dist/assets/{modifiers.esm-Conh_-BD.js → modifiers.esm-DJ0_WKg0.js} +1 -1
  73. package/dashboard/dist/assets/module-xpSFyNBK.js +3 -0
  74. package/dashboard/dist/assets/{monitor-smartphone-D6qLR9qd.js → monitor-smartphone-nqbWjJx_.js} +1 -1
  75. package/dashboard/dist/assets/{plus-D51mjIc2.js → plus-Cg7wd2jK.js} +1 -1
  76. package/dashboard/dist/assets/{provider-display-DpHPjd_s.js → provider-display-B9L1nRY9.js} +2 -2
  77. package/dashboard/dist/assets/{react-D_AcI466.js → react-D2AD6Pk2.js} +1 -1
  78. package/dashboard/dist/assets/{search-CwLcNdvZ.js → search-uNiWba1h.js} +1 -1
  79. package/dashboard/dist/assets/{skills-api-Dqi7Mjgo.js → skills-api-BBYuAhiL.js} +1 -1
  80. package/dashboard/dist/assets/{sortable.esm-BPDlUxPb.js → sortable.esm-CzPxc949.js} +1 -1
  81. package/dashboard/dist/assets/{store-ikeRIRGd.js → store-D4A7EUwE.js} +1 -1
  82. package/dashboard/dist/assets/{terminal-BfeX3L5T.js → terminal-Crmm_yKF.js} +1 -1
  83. package/dashboard/dist/assets/{trash-2-5_Qlp1k7.js → trash-2-ia9VjRnU.js} +1 -1
  84. package/dashboard/dist/assets/{use-community-stats-D90Fy2_j.js → use-community-stats-KaGCAxaF.js} +1 -1
  85. package/dashboard/dist/assets/use-latest-request-guard-Dy8gSDkk.js +1 -0
  86. package/dashboard/dist/assets/{use-limits-display-prefs-D4BYFaa-.js → use-limits-display-prefs-JSTYlbTD.js} +1 -1
  87. package/dashboard/dist/assets/{use-ordered-list-DJxLNdXY.js → use-ordered-list-C4JnHdE0.js} +1 -1
  88. package/dashboard/dist/assets/use-reduced-motion-AQE0ummK.js +1 -0
  89. package/dashboard/dist/assets/{use-session-efficiency-pref-CAX_GqXc.js → use-session-efficiency-pref-CgyUz5Gc.js} +1 -1
  90. package/dashboard/dist/assets/{use-transform-CeW6Z5R0.js → use-transform-C7-jSJ9k.js} +1 -1
  91. package/dashboard/dist/assets/use-usage-limits-B3YYqn-v.js +1 -0
  92. package/dashboard/dist/assets/{useCurrency-BarGeFyK.js → useCurrency-CK_gcb3J.js} +1 -1
  93. package/dashboard/dist/assets/{useTokenFormat-DUJ9iT9H.js → useTokenFormat-C_a-UkCA.js} +1 -1
  94. package/dashboard/dist/assets/{zap--m0IWoI3.js → zap-BIJNn-Ic.js} +1 -1
  95. package/dashboard/dist/brand-logos/lmstudio.svg +12 -0
  96. package/dashboard/dist/brand-logos/unsloth.svg +1 -0
  97. package/dashboard/dist/index.html +16 -12
  98. package/dashboard/dist/ip-check.html +2 -2
  99. package/dashboard/dist/leaderboard.html +2 -2
  100. package/dashboard/dist/llms.txt +4 -2
  101. package/dashboard/dist/share.html +2 -2
  102. package/package.json +2 -2
  103. package/src/commands/device-login.js +5 -10
  104. package/src/commands/init.js +28 -1
  105. package/src/commands/serve.js +6 -0
  106. package/src/commands/status.js +24 -0
  107. package/src/commands/sync.js +114 -18
  108. package/src/lib/cloud-account.js +102 -29
  109. package/src/lib/fs.js +30 -0
  110. package/src/lib/grok-context-breakdown.js +24 -33
  111. package/src/lib/grok-usage.js +95 -0
  112. package/src/lib/local-api.js +101 -13
  113. package/src/lib/pricing/index.js +15 -0
  114. package/src/lib/pricing/matcher.js +15 -0
  115. package/src/lib/pricing/seed-snapshot.json +1 -1
  116. package/src/lib/rollout.js +1094 -72
  117. package/src/lib/session-analytics.js +100 -40
  118. package/src/lib/usage-limits.js +507 -55
  119. package/src/lib/zcode-limits.js +65 -8
  120. package/dashboard/dist/assets/CommunityStatsModal-aAico565.js +0 -1
  121. package/dashboard/dist/assets/DashboardPage-BByRBsbt.js +0 -1
  122. package/dashboard/dist/assets/DialogDescription-BUrh0Jod.js +0 -1
  123. package/dashboard/dist/assets/SessionsPage-DOaY6_E9.js +0 -2
  124. package/dashboard/dist/assets/SkillsPage-BkzJOBLk.js +0 -1
  125. package/dashboard/dist/assets/TrendMonitor-BuKmsznP.js +0 -19
  126. package/dashboard/dist/assets/__vite-browser-external-CVIGknXl.js +0 -45
  127. package/dashboard/dist/assets/chevron-right-DJm0mbG0.js +0 -1
  128. package/dashboard/dist/assets/module-D2D5r9gC.js +0 -3
  129. package/dashboard/dist/assets/use-reduced-motion-yE3T3cat.js +0 -1
  130. package/dashboard/dist/assets/use-usage-limits-5HP8k9a3.js +0 -1
@@ -15,6 +15,7 @@ const {
15
15
  chmod600IfPossible,
16
16
  openLock,
17
17
  inspectLock,
18
+ updateJsonLocked,
18
19
  } = require("../lib/fs");
19
20
  const { physicalJsonlRecords } = require("../lib/jsonl-lines");
20
21
  const {
@@ -104,6 +105,10 @@ const {
104
105
  parseRoocodeIncremental,
105
106
  resolveZedDbPath,
106
107
  parseZedIncremental,
108
+ resolveLmstudioLogFiles,
109
+ parseLmstudioIncremental,
110
+ resolveUnslothDbPath,
111
+ parseUnslothIncremental,
107
112
  resolveAnythingllmDbPath,
108
113
  parseAnythingllmIncremental,
109
114
  resolveGooseDbPath,
@@ -260,6 +265,7 @@ const CODEX_COLD_SCAN_AUDIT_MAX_SYNCS = 288;
260
265
  const MIMO_PROVIDER_REPAIR_KEY = "mimoClaudeMislabelRepair_2026_06";
261
266
  const DSH_LEGACY_SOURCE_MIGRATION_KEY = "deepseekHarnessSourceMigration_2026_08";
262
267
  const ZCODE_NATIVE_USAGE_REPAIR_KEY = "zcodeNativeUsageRepair_2026_08";
268
+ const ZCODE_INCLUSIVE_TOKEN_REPAIR_KEY = "zcodeInclusiveTokenRepair_2026_09";
263
269
  const AUTO_SYNC_SOURCE_ALIASES = new Map([
264
270
  ["code", "every-code"],
265
271
  ["deepseek", "dsh"],
@@ -291,6 +297,7 @@ const AUTO_SYNC_SOURCES = new Set([
291
297
  "kiro",
292
298
  "kimi",
293
299
  "kimi-code",
300
+ "lmstudio",
294
301
  "mimo",
295
302
  "omp",
296
303
  "opencode",
@@ -301,6 +308,7 @@ const AUTO_SYNC_SOURCES = new Set([
301
308
  "reasonix",
302
309
  "roocode",
303
310
  "trae-cn",
311
+ "unsloth",
304
312
  "workbuddy",
305
313
  "zcode",
306
314
  "zed",
@@ -561,6 +569,8 @@ async function cmdSync(argv, context = {}) {
561
569
  legacyBaseUrlMigration = {
562
570
  previousDeviceToken,
563
571
  replacementDeviceToken,
572
+ hadPersistedAnonKey: Object.prototype.hasOwnProperty.call(config, "anonKey"),
573
+ persistedAnonKey: config.anonKey,
564
574
  };
565
575
  }
566
576
  const codexCursorRoots = [process.env.CODEX_HOME || path.join(home, ".codex")];
@@ -1432,6 +1442,13 @@ async function cmdSync(argv, context = {}) {
1432
1442
  projectQueueStatePath,
1433
1443
  });
1434
1444
  }
1445
+ await repairZcodeInclusiveTokenMigration({
1446
+ cursors,
1447
+ queuePath,
1448
+ queueStatePath,
1449
+ projectQueuePath,
1450
+ projectQueueStatePath,
1451
+ });
1435
1452
  zcodeResult = await multiInstallParse({
1436
1453
  paths: zcodePaths, parserFn: parseOpencodeDbForInstall, providerName: "zcode",
1437
1454
  cursors, getParams: (p) => ({ dbPath: p, readFn: readZcodeDbMessages, source: "zcode", cursorKey: "zcode" }),
@@ -1477,6 +1494,47 @@ async function cmdSync(argv, context = {}) {
1477
1494
  }
1478
1495
  }
1479
1496
 
1497
+ // ── LM Studio and Unsloth Studio — passive inference usage ──
1498
+ let lmstudioResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1499
+ if (sourceAllowed("lmstudio")) {
1500
+ const lmstudioLogFiles = await resolveLmstudioLogFiles(process.env);
1501
+ if (lmstudioLogFiles.length > 0) {
1502
+ if (progress?.enabled) {
1503
+ progress.start(
1504
+ `Parsing LM Studio ${renderBar(0)} 0/${formatNumber(lmstudioLogFiles.length)} logs | buckets 0`,
1505
+ );
1506
+ }
1507
+ try {
1508
+ lmstudioResult = await parseLmstudioIncremental({
1509
+ logFiles: lmstudioLogFiles,
1510
+ cursors,
1511
+ queuePath,
1512
+ onProgress: makeProviderProgress("LM Studio"),
1513
+ });
1514
+ } catch (err) {
1515
+ warnProviderParseFailure("LM Studio", err, opts);
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ let unslothResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1521
+ if (sourceAllowed("unsloth")) {
1522
+ const unslothDbPath = resolveUnslothDbPath(process.env);
1523
+ if (unslothDbPath && fssync.existsSync(unslothDbPath)) {
1524
+ if (progress?.enabled) progress.start(`Parsing Unsloth ${renderBar(0)} | buckets 0`);
1525
+ try {
1526
+ unslothResult = await parseUnslothIncremental({
1527
+ dbPath: unslothDbPath,
1528
+ cursors,
1529
+ queuePath,
1530
+ onProgress: makeProviderProgress("Unsloth"),
1531
+ });
1532
+ } catch (err) {
1533
+ warnProviderParseFailure("Unsloth", err, opts);
1534
+ }
1535
+ }
1536
+ }
1537
+
1480
1538
  // ── AnythingLLM Desktop (workspace_chats.response.metrics) ──
1481
1539
  let anythingllmResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
1482
1540
  if (sourceAllowed("anythingllm")) {
@@ -2747,6 +2805,8 @@ async function cmdSync(argv, context = {}) {
2747
2805
  reasonixResult.recordsProcessed +
2748
2806
  grokResult.recordsProcessed +
2749
2807
  copilotResult.recordsProcessed +
2808
+ lmstudioResult.recordsProcessed +
2809
+ unslothResult.recordsProcessed +
2750
2810
  anythingllmResult.recordsProcessed +
2751
2811
  kiloResult.recordsProcessed +
2752
2812
  mimoResult.recordsProcessed +
@@ -2783,6 +2843,8 @@ async function cmdSync(argv, context = {}) {
2783
2843
  reasonixResult.bucketsQueued +
2784
2844
  grokResult.bucketsQueued +
2785
2845
  copilotResult.bucketsQueued +
2846
+ lmstudioResult.bucketsQueued +
2847
+ unslothResult.bucketsQueued +
2786
2848
  anythingllmResult.bucketsQueued +
2787
2849
  kiloResult.bucketsQueued +
2788
2850
  mimoResult.bucketsQueued +
@@ -2835,6 +2897,9 @@ async function cmdSync(argv, context = {}) {
2835
2897
  progress?.stop();
2836
2898
 
2837
2899
  const runtimeConfig = config ? { ...config } : {};
2900
+ if (legacyBaseUrlMigration) {
2901
+ delete runtimeConfig.anonKey;
2902
+ }
2838
2903
  if (legacyBaseUrlMigration?.replacementDeviceToken) {
2839
2904
  runtimeConfig.deviceToken = legacyBaseUrlMigration.replacementDeviceToken;
2840
2905
  }
@@ -2888,6 +2953,7 @@ async function cmdSync(argv, context = {}) {
2888
2953
  const drainWithToken = (deviceToken) =>
2889
2954
  drainQueueToCloud({
2890
2955
  baseUrl: runtime.baseUrl,
2956
+ anonKey: runtime.anonKey,
2891
2957
  deviceToken,
2892
2958
  queuePath,
2893
2959
  queueStatePath,
@@ -2917,15 +2983,25 @@ async function cmdSync(argv, context = {}) {
2917
2983
  if (legacyBaseUrlMigration && uploadResult.batches > 0) {
2918
2984
  // device-login does not share the sync lock and may have written a
2919
2985
  // fresh current-backend config while the scan/upload was running.
2920
- // Re-read before committing, merge only while the legacy marker still
2921
- // exists, and never clobber a concurrently completed login.
2922
- const latestConfig = (await readJson(configPath)) || config;
2923
- if (isLegacyInsforgeBaseUrl(latestConfig.baseUrl)) {
2924
- latestConfig.deviceToken = successfulDeviceToken;
2925
- delete latestConfig.baseUrl;
2926
- await writeJson(configPath, latestConfig);
2927
- await chmod600IfPossible(configPath);
2928
- }
2986
+ // Re-read before committing and only remove the anonymous key this
2987
+ // migration observed. A concurrent login may replace the backend URL
2988
+ // while preserving the old key, or another writer may replace the key
2989
+ // while the legacy URL is still present.
2990
+ await updateJsonLocked(configPath, async (latestConfig) => {
2991
+ const hasLegacyBaseUrl = isLegacyInsforgeBaseUrl(latestConfig.baseUrl);
2992
+ const hasUnchangedLegacyAnonKey =
2993
+ legacyBaseUrlMigration.hadPersistedAnonKey &&
2994
+ latestConfig.anonKey === legacyBaseUrlMigration.persistedAnonKey;
2995
+ if (!hasLegacyBaseUrl && !hasUnchangedLegacyAnonKey) return null;
2996
+ if (hasLegacyBaseUrl) {
2997
+ latestConfig.deviceToken = successfulDeviceToken;
2998
+ delete latestConfig.baseUrl;
2999
+ }
3000
+ if (hasUnchangedLegacyAnonKey) {
3001
+ delete latestConfig.anonKey;
3002
+ }
3003
+ return latestConfig;
3004
+ });
2929
3005
  }
2930
3006
  // Record success so the exponential backoff step resets — otherwise
2931
3007
  // a single past failure keeps us pessimistically throttled forever.
@@ -3138,6 +3214,7 @@ module.exports = {
3138
3214
  repairDroidDuplicateSessionInflation,
3139
3215
  repairMimoClaudeMislabel,
3140
3216
  repairZcodeNativeUsageMigration,
3217
+ repairZcodeInclusiveTokenMigration,
3141
3218
  reincludeClaudeMemObserverFiles,
3142
3219
  repairGrokQueueFromSessionSnapshots,
3143
3220
  applyCloudConversationsBackfill,
@@ -3613,7 +3690,7 @@ const AUTO_RETRY_MAX_DELAY_MS = 2 * 60 * 60 * 1000;
3613
3690
  const INGEST_SLUG = "tokentracker-ingest";
3614
3691
  const MAX_INGEST_BUCKETS = 500;
3615
3692
 
3616
- async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePath, maxBatches = 5, batchSize = 200 }) {
3693
+ async function drainQueueToCloud({ baseUrl, anonKey, deviceToken, queuePath, queueStatePath, maxBatches = 5, batchSize = 200 }) {
3617
3694
  const state = (await readJson(queueStatePath)) || { offset: 0 };
3618
3695
  let offset = Number(state.offset || 0);
3619
3696
  let inserted = 0;
@@ -3632,7 +3709,6 @@ async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePa
3632
3709
  if (result.buckets.length === 0 && result.sessionStates.length === 0) break;
3633
3710
 
3634
3711
  const root = baseUrl.replace(/\/$/, "");
3635
- const anonKey = process.env.TOKENTRACKER_INSFORGE_ANON_KEY || "";
3636
3712
  const headers = {
3637
3713
  "Content-Type": "application/json",
3638
3714
  Accept: "application/json",
@@ -4116,7 +4192,7 @@ async function repairMimoClaudeMislabel({
4116
4192
  return true;
4117
4193
  }
4118
4194
 
4119
- async function resetUploadOffsetForZcodeNativeRepair(queueStatePath) {
4195
+ async function resetUploadOffsetForZcodeRepair(queueStatePath, note) {
4120
4196
  if (typeof queueStatePath !== "string" || !queueStatePath) return false;
4121
4197
  let state = {};
4122
4198
  try {
@@ -4126,7 +4202,7 @@ async function resetUploadOffsetForZcodeNativeRepair(queueStatePath) {
4126
4202
  }
4127
4203
  state.offset = 0;
4128
4204
  state.updatedAt = new Date().toISOString();
4129
- state.note = "reset_after_zcode_native_usage_repair_2026_08";
4205
+ state.note = note;
4130
4206
  await ensureDir(path.dirname(queueStatePath));
4131
4207
  await fs.writeFile(queueStatePath, JSON.stringify(state, null, 2) + "\n", "utf8");
4132
4208
  return true;
@@ -4188,7 +4264,9 @@ async function dropZcodeQueueRows(filePath, { retainRetractions = false } = {})
4188
4264
  return { removed, retractions: retractions.size };
4189
4265
  }
4190
4266
 
4191
- async function repairZcodeNativeUsageMigration({
4267
+ async function repairZcodeUsageMigration({
4268
+ migrationKey,
4269
+ queueStateNote,
4192
4270
  cursors,
4193
4271
  queuePath,
4194
4272
  queueStatePath,
@@ -4197,7 +4275,7 @@ async function repairZcodeNativeUsageMigration({
4197
4275
  } = {}) {
4198
4276
  if (!cursors || typeof cursors !== "object") return false;
4199
4277
  const migrations = (cursors.migrations ||= {});
4200
- if (migrations[ZCODE_NATIVE_USAGE_REPAIR_KEY]) return false;
4278
+ if (migrations[migrationKey]) return false;
4201
4279
 
4202
4280
  const mainRepair = await dropZcodeQueueRows(queuePath, { retainRetractions: true });
4203
4281
  const projectRepair = projectQueuePath
@@ -4225,11 +4303,13 @@ async function repairZcodeNativeUsageMigration({
4225
4303
  }
4226
4304
  delete cursors.zcode;
4227
4305
 
4228
- if (mainRepair.removed > 0) await resetUploadOffsetForZcodeNativeRepair(queueStatePath);
4306
+ if (mainRepair.removed > 0) {
4307
+ await resetUploadOffsetForZcodeRepair(queueStatePath, queueStateNote);
4308
+ }
4229
4309
  if (projectRepair.removed > 0) {
4230
- await resetUploadOffsetForZcodeNativeRepair(projectQueueStatePath);
4310
+ await resetUploadOffsetForZcodeRepair(projectQueueStatePath, queueStateNote);
4231
4311
  }
4232
- migrations[ZCODE_NATIVE_USAGE_REPAIR_KEY] = {
4312
+ migrations[migrationKey] = {
4233
4313
  appliedAt: new Date().toISOString(),
4234
4314
  removedMain: mainRepair.removed,
4235
4315
  removedProject: projectRepair.removed,
@@ -4238,6 +4318,22 @@ async function repairZcodeNativeUsageMigration({
4238
4318
  return true;
4239
4319
  }
4240
4320
 
4321
+ async function repairZcodeNativeUsageMigration(options = {}) {
4322
+ return repairZcodeUsageMigration({
4323
+ ...options,
4324
+ migrationKey: ZCODE_NATIVE_USAGE_REPAIR_KEY,
4325
+ queueStateNote: "reset_after_zcode_native_usage_repair_2026_08",
4326
+ });
4327
+ }
4328
+
4329
+ async function repairZcodeInclusiveTokenMigration(options = {}) {
4330
+ return repairZcodeUsageMigration({
4331
+ ...options,
4332
+ migrationKey: ZCODE_INCLUSIVE_TOKEN_REPAIR_KEY,
4333
+ queueStateNote: "reset_after_zcode_inclusive_token_repair_2026_09",
4334
+ });
4335
+ }
4336
+
4241
4337
  async function repairGrokQueueFromSessionSnapshots({ cursors, queuePath, queueStatePath } = {}) {
4242
4338
  if (!cursors || typeof cursors !== "object") return false;
4243
4339
  const grokState = (cursors.grok ||= {});
@@ -73,8 +73,27 @@ function decodeJwtExpMs(token) {
73
73
  // The popover polls frequently, so caching avoids hammering /api/auth/refresh.
74
74
  let tokenCache = { cacheKey: null, accessToken: null, expMs: 0 };
75
75
 
76
+ // In-flight refresh de-duplication ("single flight"), keyed the same way as
77
+ // `tokenCache`. A full popover refresh fires six account reads concurrently; on
78
+ // a cold cache every one of them used to POST /api/auth/refresh with the SAME
79
+ // refresh token. When the backend rotates refresh tokens, the losers of that
80
+ // race present an already-consumed token and fail — which is exactly the
81
+ // intermittent "Activity silently dropped to this-machine data" symptom.
82
+ const mintInflight = new Map();
83
+
76
84
  function __resetCloudAccountCacheForTests() {
77
85
  tokenCache = { cacheKey: null, accessToken: null, expMs: 0 };
86
+ mintInflight.clear();
87
+ }
88
+
89
+ // Failure classes surfaced to local-api so it can tell an intentional local
90
+ // view ("signed out", "cloud sync off") apart from a temporary cloud outage.
91
+ class AccountAuthError extends Error {
92
+ constructor(code, message) {
93
+ super(message || code);
94
+ this.name = "AccountAuthError";
95
+ this.code = code;
96
+ }
78
97
  }
79
98
 
80
99
  function csrfTokenFromRefreshPayload(data) {
@@ -87,30 +106,11 @@ function csrfTokenFromRefreshPayload(data) {
87
106
  }
88
107
 
89
108
  /**
90
- * Mint (or reuse a cached) InsForge access token from a refresh token.
91
- * @returns {Promise<{accessToken: string, refreshToken: string|null, csrfToken: string|null}|null>}
92
- * null when no refresh token is available or the refresh failed.
109
+ * Perform the actual refresh POST. Throws a classified `AccountAuthError` on
110
+ * every failure so callers can distinguish a transient outage (timeout,
111
+ * offline, rotated-token rejection) from an intentionally local view.
93
112
  */
94
- async function mintAccessToken({
95
- baseUrl,
96
- anonKey,
97
- refreshToken,
98
- fetchImpl = fetch,
99
- now = Date.now,
100
- skewMs = 60_000,
101
- timeoutMs,
102
- } = {}) {
103
- if (!refreshToken) return null;
104
- const root = String(baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
105
- const cacheKey = `${root}\0${refreshToken}`;
106
- if (
107
- tokenCache.cacheKey === cacheKey &&
108
- tokenCache.accessToken &&
109
- tokenCache.expMs - skewMs > now()
110
- ) {
111
- return { accessToken: tokenCache.accessToken, refreshToken: null, csrfToken: null };
112
- }
113
-
113
+ async function performMint({ root, cacheKey, anonKey, refreshToken, fetchImpl, now, timeoutMs }) {
114
114
  const headers = { "Content-Type": "application/json", Accept: "application/json" };
115
115
  if (anonKey) headers.apikey = anonKey;
116
116
 
@@ -132,24 +132,37 @@ async function mintAccessToken({
132
132
  body: JSON.stringify({ refresh_token: refreshToken }),
133
133
  signal,
134
134
  });
135
- } catch {
136
- return null;
135
+ } catch (e) {
136
+ const aborted = e && (e.name === "AbortError" || e.name === "TimeoutError");
137
+ throw new AccountAuthError(
138
+ aborted ? "auth_timeout" : "auth_network",
139
+ e?.message || "token refresh failed",
140
+ );
137
141
  } finally {
138
142
  if (timeoutId) {
139
143
  clearTimeout(timeoutId);
140
144
  }
141
145
  }
142
- if (!res || !res.ok) return null;
146
+ if (!res || !res.ok) {
147
+ const err = new AccountAuthError(
148
+ "auth_rejected",
149
+ `token refresh failed with HTTP ${res ? res.status : "?"}`,
150
+ );
151
+ err.status = res ? res.status : 0;
152
+ throw err;
153
+ }
143
154
 
144
155
  let data = null;
145
156
  try {
146
157
  data = await res.json();
147
- } catch {
148
- return null;
158
+ } catch (e) {
159
+ throw new AccountAuthError("auth_invalid", e?.message || "token refresh returned invalid JSON");
149
160
  }
150
161
 
151
162
  const accessToken = accessTokenFromRefreshPayload(data);
152
- if (!accessToken) return null;
163
+ if (!accessToken) {
164
+ throw new AccountAuthError("auth_invalid", "token refresh returned no access token");
165
+ }
153
166
 
154
167
  const expMs = decodeJwtExpMs(accessToken) || now() + 10 * 60_000;
155
168
  tokenCache = { cacheKey, accessToken, expMs };
@@ -166,6 +179,60 @@ async function mintAccessToken({
166
179
  };
167
180
  }
168
181
 
182
+ /**
183
+ * Mint (or reuse a cached) InsForge access token from a refresh token.
184
+ *
185
+ * Concurrent callers with the same (baseUrl, refreshToken) share ONE in-flight
186
+ * refresh; see `mintInflight`.
187
+ *
188
+ * @param {boolean} [throwOnFailure] when true, reject with a classified
189
+ * `AccountAuthError` instead of resolving to `null`.
190
+ * @returns {Promise<{accessToken: string, refreshToken: string|null, csrfToken: string|null}|null>}
191
+ * null when no refresh token is available or the refresh failed.
192
+ */
193
+ async function mintAccessToken({
194
+ baseUrl,
195
+ anonKey,
196
+ refreshToken,
197
+ fetchImpl = fetch,
198
+ now = Date.now,
199
+ skewMs = 60_000,
200
+ timeoutMs,
201
+ throwOnFailure = false,
202
+ } = {}) {
203
+ if (!refreshToken) {
204
+ if (throwOnFailure) throw new AccountAuthError("auth_missing_refresh_token", "not signed in");
205
+ return null;
206
+ }
207
+ const root = String(baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
208
+ const cacheKey = `${root}\0${refreshToken}`;
209
+ if (
210
+ tokenCache.cacheKey === cacheKey &&
211
+ tokenCache.accessToken &&
212
+ tokenCache.expMs - skewMs > now()
213
+ ) {
214
+ return { accessToken: tokenCache.accessToken, refreshToken: null, csrfToken: null };
215
+ }
216
+
217
+ let inflight = mintInflight.get(cacheKey);
218
+ if (!inflight) {
219
+ inflight = performMint({ root, cacheKey, anonKey, refreshToken, fetchImpl, now, timeoutMs })
220
+ .finally(() => {
221
+ // Only clear our own entry: a later caller may already have started a
222
+ // fresh mint after this one settled.
223
+ if (mintInflight.get(cacheKey) === inflight) mintInflight.delete(cacheKey);
224
+ });
225
+ mintInflight.set(cacheKey, inflight);
226
+ }
227
+
228
+ try {
229
+ return await inflight;
230
+ } catch (e) {
231
+ if (throwOnFailure) throw e;
232
+ return null;
233
+ }
234
+ }
235
+
169
236
  /**
170
237
  * GET a `tokentracker-account-*` edge function, forwarding the popover's query
171
238
  * params (minus `account`/`scope`, which are local-only routing knobs).
@@ -237,6 +304,7 @@ async function fetchAccountUsage({
237
304
  } = {}) {
238
305
  const slug = accountSlugFor(usageSlug);
239
306
  if (!slug) return null;
307
+ if (!refreshToken) return null;
240
308
 
241
309
  const startTime = now();
242
310
  const getRemainingTimeout = () => {
@@ -246,6 +314,9 @@ async function fetchAccountUsage({
246
314
  return remaining > 0 ? remaining : 1;
247
315
  };
248
316
 
317
+ // `throwOnFailure` keeps "not signed in" (null, handled above) distinct from
318
+ // "signed in but the refresh call failed" (throws) so the caller does not
319
+ // silently downgrade an account view into a this-machine view.
249
320
  const minted = await mintAccessToken({
250
321
  baseUrl,
251
322
  anonKey,
@@ -253,6 +324,7 @@ async function fetchAccountUsage({
253
324
  fetchImpl,
254
325
  now,
255
326
  timeoutMs: getRemainingTimeout(),
327
+ throwOnFailure: true,
256
328
  });
257
329
  if (!minted) return null;
258
330
 
@@ -269,6 +341,7 @@ async function fetchAccountUsage({
269
341
  }
270
342
 
271
343
  module.exports = {
344
+ AccountAuthError,
272
345
  USAGE_TO_ACCOUNT_SLUG,
273
346
  accountSlugFor,
274
347
  accessTokenFromRefreshPayload,
package/src/lib/fs.js CHANGED
@@ -413,6 +413,35 @@ async function openLock(
413
413
  }
414
414
  }
415
415
 
416
+ async function updateJsonLocked(
417
+ filePath,
418
+ update,
419
+ { timeoutMs = 30_000, retryMs = 10 } = {},
420
+ ) {
421
+ const lockPath = `${filePath}.lock`;
422
+ const deadline = Date.now() + timeoutMs;
423
+ let lock = null;
424
+ while (!lock) {
425
+ lock = await openLock(lockPath, { quietIfLocked: true });
426
+ if (lock) break;
427
+ if (Date.now() >= deadline) {
428
+ throw new Error(`Timed out waiting to update JSON file: ${filePath}`);
429
+ }
430
+ await new Promise((resolve) => setTimeout(resolve, retryMs));
431
+ }
432
+
433
+ try {
434
+ const current = (await readJson(filePath)) || {};
435
+ const next = await update(current);
436
+ if (next == null) return current;
437
+ await writeJson(filePath, next);
438
+ await chmod600IfPossible(filePath);
439
+ return next;
440
+ } finally {
441
+ await lock.release();
442
+ }
443
+ }
444
+
416
445
  module.exports = {
417
446
  ensureDir,
418
447
  writeFileAtomic,
@@ -422,4 +451,5 @@ module.exports = {
422
451
  chmod600IfPossible,
423
452
  openLock,
424
453
  inspectLock,
454
+ updateJsonLocked,
425
455
  };
@@ -26,8 +26,9 @@ const {
26
26
  sanitizeCommandSignature,
27
27
  getExecutableName,
28
28
  } = require("./categorizer-utils");
29
+ const { normalizeGrokUsage } = require("./grok-usage");
29
30
 
30
- const CACHE_SCHEMA_VERSION = "grok-context-v2";
31
+ const CACHE_SCHEMA_VERSION = "grok-context-v3";
31
32
  const CACHE = new Map();
32
33
  // Result-level TTL. Per-file parse cache is the main win when flipping tabs.
33
34
  const CACHE_TTL_MS = 5 * 60_000;
@@ -246,31 +247,7 @@ function outputSizeBucket(lines, chars) {
246
247
  }
247
248
 
248
249
  function readUsageTotals(usage) {
249
- if (!usage || typeof usage !== "object") return null;
250
- const inputRaw = Math.max(0, Number(usage.inputTokens ?? usage.input_tokens ?? 0) || 0);
251
- const cached = Math.max(
252
- 0,
253
- Number(usage.cachedReadTokens ?? usage.cache_read_input_tokens ?? usage.cached_input_tokens ?? 0) || 0,
254
- );
255
- const output = Math.max(0, Number(usage.outputTokens ?? usage.output_tokens ?? 0) || 0);
256
- const reasoning = Math.max(
257
- 0,
258
- Number(usage.reasoningTokens ?? usage.reasoning_output_tokens ?? 0) || 0,
259
- );
260
- const nonCachedInput = Math.max(0, inputRaw - cached);
261
- // Prefer the reported totalTokens (authoritative billable total). Fallback
262
- // includes reasoning so a missing total still matches usage-parser shape.
263
- let total = Math.max(0, Number(usage.totalTokens ?? usage.total_tokens ?? 0) || 0);
264
- if (total <= 0) total = nonCachedInput + cached + output + reasoning;
265
- if (total <= 0) return null;
266
- return {
267
- input_tokens: nonCachedInput,
268
- cached_input_tokens: cached,
269
- cache_creation_input_tokens: 0,
270
- output_tokens: output,
271
- reasoning_output_tokens: reasoning,
272
- total_tokens: total,
273
- };
250
+ return normalizeGrokUsage(usage);
274
251
  }
275
252
 
276
253
  // ---------------------------------------------------------------------------
@@ -778,15 +755,17 @@ async function computeGrokContextBreakdownUncached({
778
755
  }))
779
756
  .sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
780
757
 
781
- // Message breakdown: residual "text_response" tools + reasoning/output split
782
- const textResponse = toolRows.find((r) => r.name === "text_response");
783
- const textTotals = textResponse?.totals || emptyTotals();
758
+ // Message breakdown uses the same mutually exclusive columns as the parser.
759
+ // Grok's raw outputTokens includes reasoning, but readUsageTotals has already
760
+ // split it, so subtracting reasoning here would undercount assistant output.
784
761
  const reasoning = Number(grand.reasoning_output_tokens || 0);
785
- const assistantOut = Math.max(0, Number(grand.output_tokens || 0) - Math.min(reasoning, Number(grand.output_tokens || 0)));
762
+ const assistantOut = Number(grand.output_tokens || 0);
786
763
  // For Grok, cached reads dominate conversation history; non-cached input is
787
764
  // closer to "current user/context injection". Heuristic only — matches Codex
788
765
  // message_breakdown intent without reading message bodies.
789
- const historyTokens = Number(grand.cached_input_tokens || 0);
766
+ const cachedHistoryTokens = Number(grand.cached_input_tokens || 0);
767
+ const cacheCreationTokens = Number(grand.cache_creation_input_tokens || 0);
768
+ const historyTokens = cachedHistoryTokens + cacheCreationTokens;
790
769
  const userish = Number(grand.input_tokens || 0);
791
770
  const messageBreakdown = [
792
771
  {
@@ -806,8 +785,8 @@ async function computeGrokContextBreakdownUncached({
806
785
  name: "Conversation history",
807
786
  totals: roundTotals({
808
787
  input_tokens: 0,
809
- cached_input_tokens: historyTokens,
810
- cache_creation_input_tokens: 0,
788
+ cached_input_tokens: cachedHistoryTokens,
789
+ cache_creation_input_tokens: cacheCreationTokens,
811
790
  output_tokens: 0,
812
791
  reasoning_output_tokens: 0,
813
792
  total_tokens: historyTokens,
@@ -825,6 +804,18 @@ async function computeGrokContextBreakdownUncached({
825
804
  total_tokens: assistantOut,
826
805
  }),
827
806
  },
807
+ {
808
+ key: "reasoning",
809
+ name: "Reasoning",
810
+ totals: roundTotals({
811
+ input_tokens: 0,
812
+ cached_input_tokens: 0,
813
+ cache_creation_input_tokens: 0,
814
+ output_tokens: 0,
815
+ reasoning_output_tokens: reasoning,
816
+ total_tokens: reasoning,
817
+ }),
818
+ },
828
819
  ].sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
829
820
 
830
821
  const serializeExecRows = (rows) =>