tokentracker-cli 0.84.5 → 0.84.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.de.md +10 -8
  2. package/README.ja.md +9 -7
  3. package/README.ko.md +9 -7
  4. package/README.md +12 -10
  5. package/README.zh-CN.md +9 -7
  6. package/dashboard/dist/assets/{AchievementBadge-DJvRfvu1.js → AchievementBadge-DAPsTMsx.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-Cr8WToxS.js → AchievementsPage-B9oXfXvm.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-BCPYB46e.js → AchievementsSection-DI_LTZb3.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-D-12Ek-H.js → BadgeDetailModal--WCrJWVa.js} +1 -1
  10. package/dashboard/dist/assets/Card-BzbSN3EC.js +1 -0
  11. package/dashboard/dist/assets/{ClawdAnimated-DoGgAppT.js → ClawdAnimated-Dgxae1At.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-DIKocEAD.js → CommandPalette-BiZBYV5A.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-BNuMbEWy.js → CommunityStatsModal-C6jZx10_.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-F2gPIIS9.js → ConfirmModal-lD-mHMvO.js} +1 -1
  15. package/dashboard/dist/assets/{DashboardPage-YvoCEGG9.js → DashboardPage-DCPNoiRG.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-CbC_J22s.js → DevicePage-DiF2IOjJ.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-VEynQCYg.js → DialogClose-Crx2EFnI.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-CHsHo924.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-BbAVHI_v.js → DialogTitle-amHXttsp.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-CZVqSnis.js → FadeIn-CIhF_9PP.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-Cs_h332Y.js → HeaderGithubStar-D6Ph2GuY.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-DvvHTphK.js → HoverTooltip-BS8eO6tj.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-DPVmqSS_.js → IpCheckPage-Cz1Je6WS.js} +1 -1
  24. package/dashboard/dist/assets/LandingPage-DqeXJfwn.js +2 -0
  25. package/dashboard/dist/assets/{LeaderboardAvatar-BAPZ2ROX.js → LeaderboardAvatar-D09yLoVA.js} +1 -1
  26. package/dashboard/dist/assets/LeaderboardPage-BetpFetS.js +2 -0
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-r03akfe2.js → LeaderboardProfileModal-UW3KnANZ.js} +4 -4
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CU63YW2M.js → LeaderboardProfilePage-Chd_2AVY.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-uhpNteXJ.js +4 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-BHlcPCZk.js → LocalOnlyNotice-CpFFX8ro.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-C-dqUumR.js → LoginCard-DkZpGnVu.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BFzRg4Iy.js → LoginModal-CALv6jkD.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-QtlnGF1S.js → LoginPage-BZuXHYAT.js} +1 -1
  34. package/dashboard/dist/assets/PetPage-C1LuDyYr.js +1 -0
  35. package/dashboard/dist/assets/{PopoverPopup-CltmqcrT.js → PopoverPopup-CluVhrMr.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-BqOI9EiD.js → ProviderIcon-D3YKjVKb.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BsfCBUZD.js → ResetPasswordPage-CzN50nX5.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-D4biXw_Q.js → SegmentedControl-DFOAcEKU.js} +1 -1
  39. package/dashboard/dist/assets/{Select-DTw-14iE.js → Select-B3W_ihs_.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-3tUrnu3f.js → SelectItemText-FXRtXnFV.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-BItZqH7q.js +1 -0
  42. package/dashboard/dist/assets/{SessionsPage-DYYlNqHg.js → SessionsPage-DVQU-hTH.js} +2 -2
  43. package/dashboard/dist/assets/SettingsPage-ByvrwOi6.js +1 -0
  44. package/dashboard/dist/assets/SkillsPage-DrGr5Qij.js +1 -0
  45. package/dashboard/dist/assets/{TokenGalaxy-DU77e8sM.js → TokenGalaxy-ClF_n9od.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-DV3KGf-a.js → ToolbarRootContext-B82hGhAf.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-D6QYKmbl.js → TrendMonitor-DkWm7_1z.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DSP4jCsH.js → WidgetsPage-DCY4pyR2.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-B0x6O_DU.js → WrappedPage-yGQ4DLqj.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-BdRrPLk6.js +1483 -0
  51. package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-B2S_cdOL.js → agent-logos-D5SHGOxB.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-up-right-DNUVuhx2.js → arrow-up-right-CbMcAcss.js} +1 -1
  54. package/dashboard/dist/assets/{check-C3ARiiE_.js → check-Dyzh7lTz.js} +1 -1
  55. package/dashboard/dist/assets/{chevron-right-3bY7H8jO.js → chevron-right-DT06ZIKC.js} +1 -1
  56. package/dashboard/dist/assets/{copy-C-UIMKoO.js → copy-B3Kt3Wrf.js} +1 -1
  57. package/dashboard/dist/assets/{download-DtRzJfcr.js → download-B3rOytqI.js} +1 -1
  58. package/dashboard/dist/assets/external-link-BamTP0Ks.js +1 -0
  59. package/dashboard/dist/assets/{flame-Bas3ry-g.js → flame-msy2h1ms.js} +1 -1
  60. package/dashboard/dist/assets/{format-tokens-DFtRUIH8.js → format-tokens-8b1OUM_8.js} +1 -1
  61. package/dashboard/dist/assets/{icons-CHBt4Tmo.js → icons-C2NR2_Tf.js} +1 -1
  62. package/dashboard/dist/assets/{index-CZRlrK7w.js → index-C8sDJzUY.js} +1 -1
  63. package/dashboard/dist/assets/{index-C1ADnvzm.js → index-CvTfpiCF.js} +1 -1
  64. package/dashboard/dist/assets/{index-4unZlm2w.js → index-x3vFJQFt.js} +1 -1
  65. package/dashboard/dist/assets/{info-BkrO-fg3.js → info-C9j23aGq.js} +1 -1
  66. package/dashboard/dist/assets/limits-providers-ySB9Ci5l.js +1 -0
  67. package/dashboard/dist/assets/{link-2-DTLFz8uG.js → link-2-rrcyTT6S.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-BqQW_Ipq.js → loader-circle-x9x9I8Hv.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-YdpeE4dq.js → maximize-2-7TWFb2k7.js} +1 -1
  70. package/dashboard/dist/assets/{provider-display-CgBOpjq0.js → provider-display-JbSwpvxU.js} +1 -1
  71. package/dashboard/dist/assets/{react-DD9jpv--.js → react-DK5clHtY.js} +1 -1
  72. package/dashboard/dist/assets/{search--TvX-fig.js → search-DukXRvEV.js} +1 -1
  73. package/dashboard/dist/assets/{skills-api-C5C8a1at.js → skills-api-BUFJuwiR.js} +1 -1
  74. package/dashboard/dist/assets/{store-CRA8gPnP.js → store-BTh7bv09.js} +1 -1
  75. package/dashboard/dist/assets/{terminal-BXZyl2ek.js → terminal-CcUqLtn1.js} +1 -1
  76. package/dashboard/dist/assets/{trash-2-IFdBQ65v.js → trash-2-Bq7hnvNq.js} +1 -1
  77. package/dashboard/dist/assets/{use-community-stats-LyJ5aMpX.js → use-community-stats-qTpx6ugG.js} +1 -1
  78. package/dashboard/dist/assets/{use-limits-display-prefs-CKvFS1Nn.js → use-limits-display-prefs-Di_J4eVv.js} +1 -1
  79. package/dashboard/dist/assets/{use-native-settings-Cg-4j_0z.js → use-native-settings-CIqpv9Z3.js} +1 -1
  80. package/dashboard/dist/assets/{use-ordered-list-2pX5PXZO.js → use-ordered-list-OwogSXT7.js} +1 -1
  81. package/dashboard/dist/assets/use-reduced-motion-5QGGPoom.js +1 -0
  82. package/dashboard/dist/assets/{use-session-efficiency-pref-DilJOODA.js → use-session-efficiency-pref-CjAvMh7u.js} +1 -1
  83. package/dashboard/dist/assets/{use-transform-DiyYTLpI.js → use-transform-KZLdt-NZ.js} +1 -1
  84. package/dashboard/dist/assets/{use-usage-limits-DkUmp_nW.js → use-usage-limits-Tbifa037.js} +1 -1
  85. package/dashboard/dist/assets/{useCurrency-lm7KjGoD.js → useCurrency-D2td-bkn.js} +1 -1
  86. package/dashboard/dist/assets/{useScrollLock-DVSyJ-GE.js → useScrollLock-Cvyfyezc.js} +1 -1
  87. package/dashboard/dist/assets/useTokenFormat-DYIUS6IH.js +1 -0
  88. package/dashboard/dist/assets/{zap-CKZt1_i5.js → zap-B19mEed5.js} +1 -1
  89. package/dashboard/dist/brand-logos/qoder.svg +5 -0
  90. package/dashboard/dist/index.html +18 -15
  91. package/dashboard/dist/ip-check.html +2 -2
  92. package/dashboard/dist/leaderboard.html +2 -2
  93. package/dashboard/dist/llms.txt +3 -2
  94. package/dashboard/dist/share.html +2 -2
  95. package/package.json +2 -2
  96. package/src/commands/init.js +1 -0
  97. package/src/commands/status.js +34 -7
  98. package/src/commands/sync.js +353 -5
  99. package/src/lib/codex-rollout-parser.js +17 -39
  100. package/src/lib/codex-token-usage.js +195 -0
  101. package/src/lib/local-api.js +4 -1
  102. package/src/lib/pet-packages.js +44 -2
  103. package/src/lib/pricing/seed-snapshot.json +1 -1
  104. package/src/lib/provider-status.js +104 -0
  105. package/src/lib/qoder-limits.js +985 -0
  106. package/src/lib/rollout.js +744 -91
  107. package/src/lib/usage-limits.js +243 -43
  108. package/dashboard/dist/assets/Card-Ha3x9zIj.js +0 -1
  109. package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +0 -1
  110. package/dashboard/dist/assets/LandingPage-BCvNh3zE.js +0 -2
  111. package/dashboard/dist/assets/LeaderboardPage-DZSxazHP.js +0 -2
  112. package/dashboard/dist/assets/LimitsPage-JWZDa5_Y.js +0 -4
  113. package/dashboard/dist/assets/PetPage-C6UMY2qD.js +0 -1
  114. package/dashboard/dist/assets/SettingsPage-CYlEboSl.js +0 -1
  115. package/dashboard/dist/assets/SkillsPage-Bzq8V5Pd.js +0 -1
  116. package/dashboard/dist/assets/__vite-browser-external-D3llGq6p.js +0 -1465
  117. package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
  118. package/dashboard/dist/assets/limits-providers-BtwYmYHA.js +0 -1
  119. package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +0 -1
  120. package/dashboard/dist/assets/useTokenFormat-Li8BANKO.js +0 -1
@@ -75,8 +75,8 @@
75
75
  "description": "Shareable Token Tracker dashboard snapshot."
76
76
  }
77
77
  </script>
78
- <script type="module" crossorigin src="/assets/__vite-browser-external-D3llGq6p.js"></script>
79
- <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-WKilYZnq.css">
78
+ <script type="module" crossorigin src="/assets/__vite-browser-external-BdRrPLk6.js"></script>
79
+ <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-hXV_ZYTG.css">
80
80
  </head>
81
81
  <body>
82
82
  <main class="aeo-seed-content" aria-label="Token Tracker share page summary">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.84.5",
4
- "description": "Local-first AI coding token usage and cost tracker for 27 tools, with a dashboard, desktop pet, widgets, achievements, and native macOS/Windows apps.",
3
+ "version": "0.84.7",
4
+ "description": "Local-first AI coding token usage and cost tracker for 28 tools, with a dashboard, desktop pet, widgets, achievements, and native macOS/Windows apps.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
7
7
  "tokentracker-cli": "bin/tracker.js",
@@ -124,6 +124,7 @@ const SUPPORTED_PROVIDERS = [
124
124
  "Droid",
125
125
  "Mimo",
126
126
  "ZCode",
127
+ "Qoder",
127
128
  "AnythingLLM Desktop",
128
129
  ];
129
130
 
@@ -47,6 +47,7 @@ const {
47
47
  resolveKimiWireFiles,
48
48
  resolveKimiCodeWireFiles,
49
49
  resolveKiroCliDbPath,
50
+ resolveKiroCliSessionFiles,
50
51
  resolveCodebuddyHome,
51
52
  resolveCodebuddyProjectFiles,
52
53
  resolveWorkbuddyHome,
@@ -61,6 +62,7 @@ const {
61
62
  resolveKilocodeTaskFiles,
62
63
  resolveRoocodeTaskFiles,
63
64
  resolveZedDbPath,
65
+ resolveQoderDbPaths,
64
66
  resolveAnythingllmDbPath,
65
67
  resolveGooseDbPath,
66
68
  listDroidSettingsFiles,
@@ -229,12 +231,14 @@ async function cmdStatus(argv = []) {
229
231
  const kimiCodeHome = process.env.KIMI_CODE_HOME || path.join(home, ".kimi-code");
230
232
  const kimiCodeInstalled = fssync.existsSync(path.join(kimiCodeHome, "sessions"));
231
233
 
232
- // Kiro CLI — reads from SQLite at
233
- // ~/Library/Application Support/kiro-cli/data.sqlite3. End-user dashboards
234
- // show CLI and IDE merged under a single "Kiro" brand; this status line
235
- // surfaces the CLI sub-path separately for operators.
234
+ // Kiro CLI — reads the legacy SQLite/session files and Kiro CLI 2.13+
235
+ // event sessions. End-user dashboards show them merged under "Kiro"; this
236
+ // status line surfaces which passive sources are actually present.
236
237
  const kiroCliDbPath = resolveKiroCliDbPath(process.env);
237
- const kiroCliInstalled = fssync.existsSync(kiroCliDbPath);
238
+ const kiroCliDbExists = fssync.existsSync(kiroCliDbPath);
239
+ const kiroCliSessionFiles = resolveKiroCliSessionFiles(process.env);
240
+ const kiroCliInstalled =
241
+ kiroCliDbExists || kiroCliSessionFiles.length > 0;
238
242
 
239
243
  // AnythingLLM Desktop — per-message token metrics in workspace_chats.
240
244
  const anythingllmDbPath = resolveAnythingllmDbPath(process.env);
@@ -318,6 +322,16 @@ async function cmdStatus(argv = []) {
318
322
  const zcodeInstalled = zcodeActive.length > 0;
319
323
  const zcodeDbPath = zcodeActive.join(" | ");
320
324
 
325
+ // Qoder Desktop 1.18+ — token usage lives in SharedClientCache/local.db.
326
+ const qoderPaths = resolveQoderDbPaths({
327
+ home,
328
+ env: process.env,
329
+ platform: process.platform,
330
+ });
331
+ const qoderActive = formatResolvedPaths(qoderPaths);
332
+ const qoderInstalled = qoderActive.length > 0;
333
+ const qoderDbPath = qoderActive.join(" | ");
334
+
321
335
  // OpenCode (JSON files + SQLite DB) — passive scan of storage/message/ and opencode.db.
322
336
  const opencodeStorageNativeValue = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
323
337
  const wslOpencodeStorageDir = process.platform === "win32" && shouldProbeWsl(process.env)
@@ -634,7 +648,14 @@ async function cmdStatus(argv = []) {
634
648
  ? { installed: true, files: kimiWireFiles.length + kimiCodeWireFiles.length }
635
649
  : { installed: false },
636
650
  kiro_cli: kiroCliInstalled
637
- ? { installed: true, detail: kiroCliDbPath }
651
+ ? {
652
+ installed: true,
653
+ detail: kiroCliDbExists
654
+ ? kiroCliDbPath
655
+ : path.dirname(kiroCliSessionFiles[0]),
656
+ database: kiroCliDbExists ? kiroCliDbPath : null,
657
+ files: kiroCliSessionFiles.length,
658
+ }
638
659
  : { installed: false },
639
660
  codebuddy: codebuddyInstalled
640
661
  ? { installed: true, files: codebuddyFiles.length }
@@ -663,6 +684,9 @@ async function cmdStatus(argv = []) {
663
684
  zcode: zcodeInstalled
664
685
  ? { installed: true, detail: zcodeDbPath }
665
686
  : { installed: false },
687
+ qoder: qoderInstalled
688
+ ? { installed: true, detail: qoderDbPath }
689
+ : { installed: false },
666
690
  kilocode: kilocodeInstalled
667
691
  ? { installed: true, files: kilocodeTaskFiles.length }
668
692
  : { installed: false },
@@ -746,7 +770,7 @@ async function cmdStatus(argv = []) {
746
770
  ? `- Kimi Code: passive reader (${kimiWireFiles.length + kimiCodeWireFiles.length} wire.jsonl file${(kimiWireFiles.length + kimiCodeWireFiles.length) !== 1 ? "s" : ""} found, directories: ${kimiActive.join(" | ") || "none"})`
747
771
  : null,
748
772
  kiroCliInstalled
749
- ? `- Kiro CLI: SQLite data.sqlite3 found (tokens approximated from char lengths, merged under 'kiro' source)`
773
+ ? `- Kiro CLI: passive reader (${kiroCliSessionFiles.length} session file${kiroCliSessionFiles.length !== 1 ? "s" : ""} found, SQLite ${kiroCliDbExists ? "found" : "not found"}; tokens approximated from char lengths and merged under 'kiro')`
750
774
  : null,
751
775
  codebuddyInstalled
752
776
  ? `- CodeBuddy hooks: ${codebuddyHookConfigured ? "set" : "unset"} (${codebuddyFiles.length} usage file${codebuddyFiles.length !== 1 ? "s" : ""} found)`
@@ -775,6 +799,9 @@ async function cmdStatus(argv = []) {
775
799
  zcodeInstalled
776
800
  ? `- ZCode: passive reader (${zcodeDbPath})`
777
801
  : null,
802
+ qoderInstalled
803
+ ? `- Qoder: passive reader (${qoderDbPath})`
804
+ : null,
778
805
  opencodeInstalled
779
806
  ? `- OpenCode: passive reader (storage: ${opencodeStorageActive.join(" | ") || "not found"}, DB: ${opencodeDbActive.join(" | ") || "not found"})`
780
807
  : null,
@@ -20,6 +20,8 @@ const {
20
20
  readOpencodeDbMessages,
21
21
  readMimoDbMessages,
22
22
  readZcodeDbMessages,
23
+ resolveQoderDbPaths,
24
+ readQoderDbMessages,
23
25
  resolveKiroDbPath,
24
26
  resolveKiroJsonlPath,
25
27
  resolveHermesPath,
@@ -35,6 +37,7 @@ const {
35
37
  parseGeminiIncremental,
36
38
  parseOpencodeIncremental,
37
39
  parseOpencodeDbIncremental,
40
+ parseQoderDbIncremental,
38
41
  parseOpenclawIncremental,
39
42
  parseCursorApiIncremental,
40
43
  parseKiroIncremental,
@@ -113,6 +116,11 @@ const {
113
116
  } = require("../lib/cursor-store");
114
117
  const { resolveTrackerPaths } = require("../lib/tracker-paths");
115
118
  const { resolveRuntimeConfig } = require("../lib/runtime-config");
119
+ const { extractTokenCount } = require("../lib/codex-rollout-parser");
120
+ const {
121
+ consumeUsageDelta,
122
+ createUsageDeltaState,
123
+ } = require("../lib/codex-token-usage");
116
124
 
117
125
  const CURSOR_UNKNOWN_MIGRATION_KEY = "cursorUnknownPurge_2026_04";
118
126
  const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_2026_05";
@@ -189,6 +197,17 @@ const CODEX_RESCAN_DEDUP_REPAIR_KEY = "codexRescanDedupRepair_2026_06";
189
197
  // offset reset for the cloud overwrite). Pre-gated: installs with no forked
190
198
  // rollout on disk carry no fork phantom and mark done without the rebuild.
191
199
  const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
200
+ // One-time repair for Codex rollouts that contain token counters from multiple
201
+ // interleaved SessionState instances. Older parsers subtracted each cumulative
202
+ // total from the most recently observed total, even when it belonged to another
203
+ // state, which could turn an ordinary turn into a multi-billion-token delta.
204
+ // v2 removes the unreliable multi_agent_version pre-gate and validates every
205
+ // contributing rollout before committing the rebuild. The new key is required
206
+ // so installs that finalized the original migration on a false negative retry.
207
+ const CODEX_USAGE_LINEAGE_REPAIR_KEY = "codexUsageLineageRepair_2026_07_v2";
208
+ // Keep the one escalated desktop refresh bounded; explicit full syncs can retry
209
+ // the same migration without this ceiling when the history needs a deeper scan.
210
+ const CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES = 1024 * 1024;
192
211
  const DROID_DUP_SESSION_REPAIR_KEY = "droidDupSessionInflationRepair_2026_06";
193
212
  const CODEX_COLD_SCAN_AUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
194
213
  const NOTIFY_LOCK_WAIT_MS = 130_000;
@@ -236,6 +255,7 @@ const AUTO_SYNC_SOURCES = new Set([
236
255
  "opencode",
237
256
  "openclaw",
238
257
  "pi",
258
+ "qoder",
239
259
  "roocode",
240
260
  "workbuddy",
241
261
  "zcode",
@@ -457,6 +477,28 @@ async function cmdSync(argv, context = {}) {
457
477
  if (autoSourceScope) return sources.includes(autoSourceScope);
458
478
  return true;
459
479
  };
480
+ // Desktop refreshes are deliberately lightweight, but that also means a
481
+ // migration wired only to full sync would never repair an existing native
482
+ // install. Escalate exactly the first eligible background run: fresh
483
+ // installs have no persisted Codex buckets to repair, while retryable
484
+ // failures remain reserved for an explicit full sync instead of turning
485
+ // every five-minute refresh into a deep historical scan.
486
+ const backgroundCodexUsageMigrationEligible = Boolean(
487
+ isBackgroundLightweightSync &&
488
+ sourceAllowed("codex") &&
489
+ !cursors.migrations?.[CODEX_USAGE_LINEAGE_REPAIR_KEY]
490
+ );
491
+ const hasPersistedCodexUsage = Object.keys(cursors.hourly?.buckets || {})
492
+ .some((key) => key.startsWith("codex|"));
493
+ const backgroundCodexUsageRepair =
494
+ backgroundCodexUsageMigrationEligible && hasPersistedCodexUsage;
495
+ if (backgroundCodexUsageMigrationEligible && !hasPersistedCodexUsage) {
496
+ // A fresh install has no old-parser buckets to repair. Mark it complete
497
+ // before the first bounded parse so the next refresh does not mistake
498
+ // newly parsed, already-correct data for legacy history.
499
+ (cursors.migrations ||= {})[CODEX_USAGE_LINEAGE_REPAIR_KEY] =
500
+ new Date().toISOString();
501
+ }
460
502
 
461
503
  const sources = [];
462
504
  if (sourceAllowed("codex")) {
@@ -467,13 +509,13 @@ async function cmdSync(argv, context = {}) {
467
509
  const codexPaths = resolveInstallPaths({ nativeValue: codexNativeValue, wslValue: wslCodexDir });
468
510
  if (codexPaths.native) {
469
511
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "sessions"), codexInventoryCache: true });
470
- if (!isBackgroundLightweightSync) {
512
+ if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
471
513
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.native, "archived_sessions"), deep: true });
472
514
  }
473
515
  }
474
516
  if (codexPaths.wsl) {
475
517
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "sessions"), codexInventoryCache: true });
476
- if (!isBackgroundLightweightSync) {
518
+ if (!isBackgroundLightweightSync || backgroundCodexUsageRepair) {
477
519
  sources.push({ source: "codex", sessionsDir: path.join(codexPaths.wsl, "archived_sessions"), deep: true });
478
520
  }
479
521
  }
@@ -523,7 +565,7 @@ async function cmdSync(argv, context = {}) {
523
565
  projectQueueStatePath,
524
566
  rolloutFiles,
525
567
  });
526
- await repairCodexForkReplayInflation({
568
+ const codexForkRepairRan = await repairCodexForkReplayInflation({
527
569
  cursors,
528
570
  queuePath,
529
571
  queueStatePath,
@@ -532,6 +574,15 @@ async function cmdSync(argv, context = {}) {
532
574
  rolloutFiles,
533
575
  legacyRepairRan: codexRescanRepairRan,
534
576
  });
577
+ await repairCodexInterleavedUsageInflation({
578
+ cursors,
579
+ queuePath,
580
+ queueStatePath,
581
+ projectQueuePath,
582
+ projectQueueStatePath,
583
+ rolloutFiles,
584
+ legacyRepairRan: codexRescanRepairRan || codexForkRepairRan,
585
+ });
535
586
  await repairDroidDuplicateSessionInflation({ cursors, queuePath, queueStatePath });
536
587
  await repairMimoClaudeMislabel({
537
588
  cursors,
@@ -540,6 +591,33 @@ async function cmdSync(argv, context = {}) {
540
591
  projectQueuePath,
541
592
  projectQueueStatePath,
542
593
  });
594
+ } else if (backgroundCodexUsageRepair) {
595
+ await cursorStore.materializeAllCodexState(cursors);
596
+ try {
597
+ await repairCodexInterleavedUsageInflation({
598
+ cursors,
599
+ queuePath,
600
+ queueStatePath,
601
+ projectQueuePath,
602
+ projectQueueStatePath,
603
+ rolloutFiles,
604
+ maxLineageScanBytes: CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES,
605
+ });
606
+ } catch (err) {
607
+ warnProviderParseFailure("Codex usage lineage repair", err, opts);
608
+ } finally {
609
+ // Rebuild failures intentionally leave the migration key unset so a
610
+ // future full sync can retry. Record a retryable sentinel here to keep
611
+ // routine native background refreshes bounded after that first attempt.
612
+ const migrations = (cursors.migrations ||= {});
613
+ if (!migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY]) {
614
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
615
+ skipped: true,
616
+ reason: "background_repair_requires_full_sync",
617
+ at: new Date().toISOString(),
618
+ };
619
+ }
620
+ }
543
621
  }
544
622
 
545
623
  const codexColdSkipEnabled = opts.auto && sourceAllowed("codex");
@@ -903,6 +981,47 @@ async function cmdSync(argv, context = {}) {
903
981
  };
904
982
  }
905
983
 
984
+ let qoderResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
985
+ if (sourceAllowed("qoder")) {
986
+ const qoderPaths = resolveQoderDbPaths({
987
+ home,
988
+ env: process.env,
989
+ platform: process.platform,
990
+ });
991
+ if (qoderPaths.native || qoderPaths.wsl) {
992
+ if (progress?.enabled) {
993
+ progress.start(`Parsing Qoder ${renderBar(0)} | buckets 0`);
994
+ }
995
+ try {
996
+ const result = await multiInstallParse({
997
+ paths: qoderPaths,
998
+ parserFn: async ({ dbPath, ...rest }) => {
999
+ const dbMessages = await readQoderDbMessages(dbPath);
1000
+ const parsed = await parseQoderDbIncremental({ dbMessages, ...rest });
1001
+ return {
1002
+ recordsProcessed: parsed.messagesProcessed || 0,
1003
+ eventsAggregated: parsed.eventsAggregated || 0,
1004
+ bucketsQueued: parsed.bucketsQueued || 0,
1005
+ };
1006
+ },
1007
+ providerName: "qoder",
1008
+ cursors,
1009
+ queuePath,
1010
+ projectQueuePath,
1011
+ getParams: (dbPath) => ({ dbPath }),
1012
+ onProgress: makeProviderProgress("Qoder"),
1013
+ });
1014
+ qoderResult = {
1015
+ recordsProcessed: result.recordsProcessed || 0,
1016
+ eventsAggregated: result.eventsAggregated || 0,
1017
+ bucketsQueued: result.bucketsQueued || 0,
1018
+ };
1019
+ } catch (err) {
1020
+ warnProviderParseFailure("Qoder", err, opts);
1021
+ }
1022
+ }
1023
+ }
1024
+
906
1025
  async function parseOpencodeDbForInstall({ dbPath, readFn, source, cursorKey, ...rest }) {
907
1026
  if (!dbPath || !fssync.existsSync(dbPath)) {
908
1027
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
@@ -1326,8 +1445,9 @@ async function cmdSync(argv, context = {}) {
1326
1445
  );
1327
1446
  }
1328
1447
 
1329
- // ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3
1330
- // AND live sessions under ~/.kiro/sessions/cli/{uuid}.json) ──
1448
+ // ── Kiro CLI (reads ~/Library/Application Support/kiro-cli/data.sqlite3,
1449
+ // legacy ~/.kiro/sessions/cli/{uuid}.json, and Kiro CLI 2.13+
1450
+ // ~/.kiro/sessions/{workspace}/sess_{uuid}/messages.jsonl) ──
1331
1451
  // Runs IN PARALLEL with the Kiro IDE branch above — NOT instead of it.
1332
1452
  // Both emit source='kiro' so totals merge transparently; cursor state
1333
1453
  // is isolated in cursors.kiroCli. Kiro CLI does not persist explicit
@@ -1996,6 +2116,7 @@ async function cmdSync(argv, context = {}) {
1996
2116
  geminiResult.filesProcessed +
1997
2117
  antigravityResult.filesProcessed +
1998
2118
  opencodeResult.filesProcessed +
2119
+ qoderResult.recordsProcessed +
1999
2120
  cursorResult.recordsProcessed +
2000
2121
  kiroResult.recordsProcessed +
2001
2122
  kiroCliResult.recordsProcessed +
@@ -2025,6 +2146,7 @@ async function cmdSync(argv, context = {}) {
2025
2146
  geminiResult.bucketsQueued +
2026
2147
  antigravityResult.bucketsQueued +
2027
2148
  opencodeResult.bucketsQueued +
2149
+ qoderResult.bucketsQueued +
2028
2150
  cursorResult.bucketsQueued +
2029
2151
  kiroResult.bucketsQueued +
2030
2152
  kiroCliResult.bucketsQueued +
@@ -2339,6 +2461,7 @@ module.exports = {
2339
2461
  migrateRolloutCumulativeDeltaBuckets,
2340
2462
  repairCodexRescanInflation,
2341
2463
  repairCodexForkReplayInflation,
2464
+ repairCodexInterleavedUsageInflation,
2342
2465
  repairDroidDuplicateSessionInflation,
2343
2466
  repairMimoClaudeMislabel,
2344
2467
  reincludeClaudeMemObserverFiles,
@@ -2352,6 +2475,7 @@ module.exports = {
2352
2475
  ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY,
2353
2476
  CODEX_RESCAN_DEDUP_REPAIR_KEY,
2354
2477
  CODEX_FORK_REPLAY_REPAIR_KEY,
2478
+ CODEX_USAGE_LINEAGE_REPAIR_KEY,
2355
2479
  DROID_DUP_SESSION_REPAIR_KEY,
2356
2480
  CLAUDE_MEM_OBSERVER_REINCLUDE_KEY,
2357
2481
  GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY,
@@ -3405,6 +3529,7 @@ async function repairCodexRescanInflation({
3405
3529
  projectQueuePath,
3406
3530
  projectQueueStatePath,
3407
3531
  rolloutFiles,
3532
+ expectedCodexFileSnapshots = null,
3408
3533
  // The fork-replay repair (#169 follow-up) re-runs this exact rebuild under
3409
3534
  // its own key: the rebuild always uses the CURRENT parser, so any parser fix
3410
3535
  // shipped since the last run is applied to the rebuilt history.
@@ -3593,6 +3718,16 @@ async function repairCodexRescanInflation({
3593
3718
  }
3594
3719
  }
3595
3720
 
3721
+ const rebuildValidation = expectedCodexFileSnapshots instanceof Map
3722
+ ? await validateCodexRebuildFileSnapshots(rebuilt.files, expectedCodexFileSnapshots)
3723
+ : { ok: true };
3724
+ if (!rebuildValidation.ok) {
3725
+ console.error(
3726
+ `[sync] codex rescan repair: ${rebuildValidation.reason} — skipping to avoid data loss`,
3727
+ );
3728
+ return false;
3729
+ }
3730
+
3596
3731
  // COMMIT (only after a verified rebuild). A crash partway just leaves the
3597
3732
  // migration to re-run next sync — re-rebuild + re-strip + re-commit converges.
3598
3733
  //
@@ -3828,6 +3963,219 @@ async function repairCodexForkReplayInflation({
3828
3963
  });
3829
3964
  }
3830
3965
 
3966
+ // Rebuild historical Codex usage only when a rollout proves that the old
3967
+ // single-baseline parser crossed cumulative SessionState lineages. Detection is
3968
+ // based on the token counter stream itself, not multi_agent_version metadata:
3969
+ // affected legacy rollouts can predate that marker or carry it only in the
3970
+ // middle of a large file. Every contributing rollout must also be valid and
3971
+ // stable before the atomic rebuild can replace live history.
3972
+ async function repairCodexInterleavedUsageInflation({
3973
+ cursors,
3974
+ queuePath,
3975
+ queueStatePath,
3976
+ projectQueuePath,
3977
+ projectQueueStatePath,
3978
+ rolloutFiles,
3979
+ // Either older guarded repair rebuilt every Codex file with the current
3980
+ // parser earlier in this sync, so a second full rebuild is unnecessary.
3981
+ legacyRepairRan = false,
3982
+ maxLineageScanBytes = Infinity,
3983
+ }) {
3984
+ if (!cursors || typeof cursors !== "object") return false;
3985
+ const migrations = (cursors.migrations ||= {});
3986
+ const prior = migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY];
3987
+ if (prior && !(typeof prior === "object" && prior.skipped)) return false;
3988
+
3989
+ if (legacyRepairRan) {
3990
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = new Date().toISOString();
3991
+ return false;
3992
+ }
3993
+
3994
+ const scan = await scanForInterleavedCodexUsage(rolloutFiles, {
3995
+ maxLineageScanBytes,
3996
+ });
3997
+ if (scan.indeterminate) {
3998
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
3999
+ skipped: true,
4000
+ reason: "usage_lineage_scan_indeterminate",
4001
+ at: new Date().toISOString(),
4002
+ };
4003
+ return false;
4004
+ }
4005
+
4006
+ if (!scan.affected) {
4007
+ if (!isCodexHistoryCovered(cursors, rolloutFiles)) {
4008
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = {
4009
+ skipped: true,
4010
+ reason: "codex_history_not_covered",
4011
+ at: new Date().toISOString(),
4012
+ };
4013
+ return false;
4014
+ }
4015
+ migrations[CODEX_USAGE_LINEAGE_REPAIR_KEY] = new Date().toISOString();
4016
+ return false;
4017
+ }
4018
+
4019
+ return repairCodexRescanInflation({
4020
+ cursors,
4021
+ queuePath,
4022
+ queueStatePath,
4023
+ projectQueuePath,
4024
+ projectQueueStatePath,
4025
+ rolloutFiles,
4026
+ expectedCodexFileSnapshots: scan.fileSnapshots,
4027
+ migrationKey: CODEX_USAGE_LINEAGE_REPAIR_KEY,
4028
+ uploadNote: "reset_after_codex_usage_lineage_2026_07_v2",
4029
+ });
4030
+ }
4031
+
4032
+ async function scanForInterleavedCodexUsage(
4033
+ rolloutFiles,
4034
+ { maxLineageScanBytes = Infinity } = {},
4035
+ ) {
4036
+ let affected = false;
4037
+ let remainingBytes = Number.isFinite(maxLineageScanBytes)
4038
+ ? Math.max(0, maxLineageScanBytes)
4039
+ : Infinity;
4040
+ const fileSnapshots = new Map();
4041
+ const seenPaths = new Set();
4042
+
4043
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
4044
+ const fp = typeof entry === "string" ? entry : entry?.path;
4045
+ const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
4046
+ if (!fp || src !== "codex" || seenPaths.has(fp)) continue;
4047
+ seenPaths.add(fp);
4048
+
4049
+ const lineage = await scanCodexUsageLineages(fp, remainingBytes);
4050
+ if (lineage.indeterminate) {
4051
+ return { affected: false, indeterminate: true, fileSnapshots: new Map() };
4052
+ }
4053
+ affected ||= lineage.affected;
4054
+ fileSnapshots.set(fp, lineage.fileSnapshot);
4055
+ if (Number.isFinite(remainingBytes)) {
4056
+ remainingBytes = Math.max(0, remainingBytes - lineage.bytesRead);
4057
+ }
4058
+ }
4059
+ return { affected, indeterminate: false, fileSnapshots };
4060
+ }
4061
+
4062
+ async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
4063
+ let stream = null;
4064
+ let rl = null;
4065
+ try {
4066
+ const before = await readCodexFileSnapshot(filePath);
4067
+ const state = createUsageDeltaState();
4068
+ const byteLimit = Number.isFinite(maxBytes) ? Math.max(0, maxBytes) : Infinity;
4069
+ let bytesRead = 0;
4070
+ let affected = false;
4071
+ stream = fssync.createReadStream(filePath, {
4072
+ encoding: "utf8",
4073
+ highWaterMark: 32 * 1024,
4074
+ });
4075
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4076
+ for await (const line of rl) {
4077
+ bytesRead += Buffer.byteLength(line, "utf8") + 1;
4078
+ if (bytesRead > byteLimit) return { affected: false, indeterminate: true };
4079
+ if (!line.trim()) continue;
4080
+ let obj;
4081
+ try {
4082
+ obj = JSON.parse(line);
4083
+ } catch (_e) {
4084
+ return { affected: false, indeterminate: true };
4085
+ }
4086
+ const token = extractTokenCount(obj);
4087
+ const info = token?.info;
4088
+ if (!info || typeof info !== "object") continue;
4089
+ consumeUsageDelta(state, info.last_token_usage, info.total_token_usage);
4090
+ if (state.sawInterleaved || state.sawDivergentCumulative) {
4091
+ affected = true;
4092
+ }
4093
+ }
4094
+ const after = await readCodexFileSnapshot(filePath);
4095
+ if (!sameCodexFileSnapshot(before, after)) {
4096
+ return { affected: false, indeterminate: true };
4097
+ }
4098
+ return {
4099
+ affected,
4100
+ indeterminate: false,
4101
+ bytesRead,
4102
+ fileSnapshot: after,
4103
+ };
4104
+ } catch (_e) {
4105
+ return { affected: false, indeterminate: true };
4106
+ } finally {
4107
+ if (rl) rl.close();
4108
+ if (stream) stream.destroy();
4109
+ }
4110
+ }
4111
+
4112
+ async function readCodexFileSnapshot(filePath) {
4113
+ const stat = await fs.stat(filePath);
4114
+ return {
4115
+ size: Number(stat.size),
4116
+ mtimeMs: Number(stat.mtimeMs),
4117
+ ctimeMs: Number(stat.ctimeMs),
4118
+ ino: Number(stat.ino),
4119
+ dev: Number(stat.dev),
4120
+ };
4121
+ }
4122
+
4123
+ function sameCodexFileSnapshot(left, right) {
4124
+ return Boolean(
4125
+ left &&
4126
+ right &&
4127
+ left.size === right.size &&
4128
+ left.mtimeMs === right.mtimeMs &&
4129
+ left.ctimeMs === right.ctimeMs &&
4130
+ left.ino === right.ino &&
4131
+ left.dev === right.dev
4132
+ );
4133
+ }
4134
+
4135
+ async function validateCodexRebuildFileSnapshots(rebuiltFiles, expectedSnapshots) {
4136
+ for (const [filePath, expected] of expectedSnapshots) {
4137
+ let actual;
4138
+ try {
4139
+ actual = await readCodexFileSnapshot(filePath);
4140
+ } catch (_e) {
4141
+ return { ok: false, reason: "a scanned rollout became unreadable during rebuild" };
4142
+ }
4143
+ if (!sameCodexFileSnapshot(expected, actual)) {
4144
+ return { ok: false, reason: "a scanned rollout changed during rebuild" };
4145
+ }
4146
+ const rebuiltCursor = rebuiltFiles?.[filePath];
4147
+ if (!rebuiltCursor) {
4148
+ return { ok: false, reason: "the rebuild omitted a scanned rollout cursor" };
4149
+ }
4150
+ if (Number(rebuiltCursor.offset) !== expected.size) {
4151
+ return { ok: false, reason: "the rebuild did not consume a scanned rollout through EOF" };
4152
+ }
4153
+ }
4154
+ return { ok: true };
4155
+ }
4156
+
4157
+ function isCodexHistoryCovered(cursors, rolloutFiles) {
4158
+ const scannedPaths = new Set();
4159
+ const scannedSessionIds = new Set();
4160
+ for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
4161
+ const fp = typeof entry === "string" ? entry : entry?.path;
4162
+ const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
4163
+ if (!fp || src !== "codex") continue;
4164
+ scannedPaths.add(fp);
4165
+ const id = codexSessionIdFromPath(fp);
4166
+ if (id) scannedSessionIds.add(id);
4167
+ }
4168
+ if (!cursors.files || typeof cursors.files !== "object") return true;
4169
+ for (const fp of Object.keys(cursors.files)) {
4170
+ if (!isCodexSessionCursorPath(fp)) continue;
4171
+ if (scannedPaths.has(fp)) continue;
4172
+ const id = codexSessionIdFromPath(fp);
4173
+ if (id && scannedSessionIds.has(id)) continue;
4174
+ return false;
4175
+ }
4176
+ return true;
4177
+ }
4178
+
3831
4179
  // Scans codex rollout heads for the fork marker. The child session_meta (with
3832
4180
  // forked_from_id) is the FIRST line of a forked rollout — observed at byte
3833
4181
  // offset ≤169 across every local sample — so a bounded head read is