tokentracker-cli 0.84.6 → 0.84.8

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 (118) 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-CKIvD3lG.js → AchievementBadge-DAAenS6A.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CZWCUiS5.js → AchievementsPage-DJ9GMKxL.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-BSk_fR8P.js → AchievementsSection-DtpLrh2Q.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-CjHkcmuq.js → BadgeDetailModal-CkE1DwdN.js} +1 -1
  10. package/dashboard/dist/assets/Card-Cs2vVMHG.js +1 -0
  11. package/dashboard/dist/assets/{ClawdAnimated-Cl6gmF7u.js → ClawdAnimated-BCSNeob2.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-B0rcodcF.js → CommandPalette-CFDTjp76.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-BtlTJbf4.js → CommunityStatsModal-D2oJpI9i.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-tJCWVZQH.js → ConfirmModal-D0a_xdwF.js} +1 -1
  15. package/dashboard/dist/assets/{DashboardPage-Dtxq8dzI.js → DashboardPage-CMCqvpkA.js} +1 -1
  16. package/dashboard/dist/assets/{DevicePage-9USuEsRu.js → DevicePage-c9Fk13gJ.js} +1 -1
  17. package/dashboard/dist/assets/{DialogClose-DxHdqMcl.js → DialogClose-jsfDvF2n.js} +1 -1
  18. package/dashboard/dist/assets/DialogDescription-Dwxj5h5w.js +1 -0
  19. package/dashboard/dist/assets/{DialogTitle-C1ZeQgf2.js → DialogTitle-Dt835L7T.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Cx4jLl9G.js → FadeIn-DJssGrj8.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-BOyuGkoA.js → HeaderGithubStar-B9MwwA-P.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-BDhqMR3L.js → HoverTooltip-DPLMJkai.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-DTvA9Wmx.js → IpCheckPage-BXKjxEHe.js} +1 -1
  24. package/dashboard/dist/assets/LandingPage-BJNMQTec.js +2 -0
  25. package/dashboard/dist/assets/{LeaderboardAvatar-3ibNg6If.js → LeaderboardAvatar-BNwbnOa3.js} +1 -1
  26. package/dashboard/dist/assets/LeaderboardPage-BOxejyMy.js +2 -0
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-DeGbJtvi.js → LeaderboardProfileModal-BEqUZCOD.js} +4 -4
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-DEPzO6hW.js → LeaderboardProfilePage-CUDV5iyh.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-ejlJN1vi.js +4 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-C76LAT_3.js → LocalOnlyNotice-B3BSeCHd.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-DEyCJT67.js → LoginCard-2mAb696j.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-CueMQgAZ.js → LoginModal-cacKhc6U.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-CwqBb5Fd.js → LoginPage-rnBlie8p.js} +1 -1
  34. package/dashboard/dist/assets/PetPage-D3DhuS6O.js +1 -0
  35. package/dashboard/dist/assets/{PopoverPopup-Dt9VY2VQ.js → PopoverPopup-CxATX7M9.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-CHx4ohfx.js → ProviderIcon-B5b9LkmM.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DB6R7fjf.js → ResetPasswordPage-BGKmQ_JW.js} +1 -1
  38. package/dashboard/dist/assets/{SegmentedControl-BINOpXIY.js → SegmentedControl-BMeYCm2b.js} +1 -1
  39. package/dashboard/dist/assets/{Select-DcYXcsKv.js → Select-CYHXq8im.js} +1 -1
  40. package/dashboard/dist/assets/{SelectItemText-BDv2Kb3x.js → SelectItemText-DvI4i06h.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-dv26-VvI.js +1 -0
  42. package/dashboard/dist/assets/{SessionsPage-cL_w0ytB.js → SessionsPage-DBQruqkO.js} +2 -2
  43. package/dashboard/dist/assets/SettingsPage-BWRbQ3Iq.js +1 -0
  44. package/dashboard/dist/assets/SkillsPage-BonhKgAt.js +1 -0
  45. package/dashboard/dist/assets/{TokenGalaxy-BHq4YSv2.js → TokenGalaxy-BBUD1kpU.js} +1 -1
  46. package/dashboard/dist/assets/{ToolbarRootContext-akXaaKb6.js → ToolbarRootContext-DPMm8uYz.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-DPhFXzQc.js → TrendMonitor-CZVE6gID.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-Nf0UEz7s.js → WidgetsPage-Bz9cieeZ.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-D4zOSsqD.js → WrappedPage-CruMolZd.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-C3VH6nS4.js +1483 -0
  51. package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
  52. package/dashboard/dist/assets/{agent-logos-Bja3VCAa.js → agent-logos-HIeYwIqN.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-up-right-CzcCNJJ2.js → arrow-up-right-n7Bi4Tvd.js} +1 -1
  54. package/dashboard/dist/assets/{check-DUHSZCYn.js → check-C5Q44G5k.js} +1 -1
  55. package/dashboard/dist/assets/{chevron-right-jLvhaNUy.js → chevron-right-BsnptUU0.js} +1 -1
  56. package/dashboard/dist/assets/{copy-ChyH-ynx.js → copy-BGuL9uFa.js} +1 -1
  57. package/dashboard/dist/assets/{download-CgwvXrUe.js → download-BljgPwR2.js} +1 -1
  58. package/dashboard/dist/assets/external-link-B4Oz8hUe.js +1 -0
  59. package/dashboard/dist/assets/{flame-ysf1F8uB.js → flame-ChLG_NZE.js} +1 -1
  60. package/dashboard/dist/assets/{format-tokens-DAa2cubl.js → format-tokens-DjuNk13Z.js} +1 -1
  61. package/dashboard/dist/assets/{icons-DccoWQq9.js → icons-CkO42M-u.js} +1 -1
  62. package/dashboard/dist/assets/{index-CAycNPQf.js → index-CEsvYBWY.js} +1 -1
  63. package/dashboard/dist/assets/{index-CxExExXh.js → index-CGxawuWx.js} +1 -1
  64. package/dashboard/dist/assets/{index-kkQrDRat.js → index-D87oE_rV.js} +1 -1
  65. package/dashboard/dist/assets/{info-DWQQIq9M.js → info-et8xtJ4v.js} +1 -1
  66. package/dashboard/dist/assets/limits-providers-vO_86yRy.js +1 -0
  67. package/dashboard/dist/assets/{link-2-CKeVVq4P.js → link-2-Dge2rBkr.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-Dw0tu6IN.js → loader-circle-Cs3wpqJe.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-BMU0YzEF.js → maximize-2-zn71uZX3.js} +1 -1
  70. package/dashboard/dist/assets/{provider-display-Cf9I-VLS.js → provider-display-CTt6mzLe.js} +1 -1
  71. package/dashboard/dist/assets/{react-BhbcISKy.js → react-BbHDQe6Q.js} +1 -1
  72. package/dashboard/dist/assets/{search-BoY8EW1z.js → search-3CMhatWb.js} +1 -1
  73. package/dashboard/dist/assets/{skills-api-KVH21GJw.js → skills-api-Cm1aiqWL.js} +1 -1
  74. package/dashboard/dist/assets/{store-BL7rSajG.js → store-DclXV77t.js} +1 -1
  75. package/dashboard/dist/assets/{terminal-CeMuITdm.js → terminal-BC7uFcA7.js} +1 -1
  76. package/dashboard/dist/assets/{trash-2-jVUCKqrR.js → trash-2-BT8V531F.js} +1 -1
  77. package/dashboard/dist/assets/{use-community-stats-uWK0fE6l.js → use-community-stats-DJr59SGu.js} +1 -1
  78. package/dashboard/dist/assets/{use-limits-display-prefs-DWsF4ShM.js → use-limits-display-prefs-DU8OOFVb.js} +1 -1
  79. package/dashboard/dist/assets/{use-native-settings-SEbqWDvc.js → use-native-settings-BvSwI4BL.js} +1 -1
  80. package/dashboard/dist/assets/{use-ordered-list-D4UhrPvw.js → use-ordered-list-Cq5AyV2x.js} +1 -1
  81. package/dashboard/dist/assets/use-reduced-motion-D1_q3aRK.js +1 -0
  82. package/dashboard/dist/assets/{use-session-efficiency-pref-DbysyUTJ.js → use-session-efficiency-pref-WS8NhXuW.js} +1 -1
  83. package/dashboard/dist/assets/{use-transform-DaEssGaZ.js → use-transform-CLYsi8xc.js} +1 -1
  84. package/dashboard/dist/assets/{use-usage-limits-CmmQ34Ov.js → use-usage-limits-CGoQQDG-.js} +1 -1
  85. package/dashboard/dist/assets/{useCurrency-DvBEn4rx.js → useCurrency-D0GtEoJD.js} +1 -1
  86. package/dashboard/dist/assets/{useScrollLock-QozsdFng.js → useScrollLock-TIUBSeIM.js} +1 -1
  87. package/dashboard/dist/assets/useTokenFormat-B0TBL9La.js +1 -0
  88. package/dashboard/dist/assets/{zap-Dp_aJ1q6.js → zap-D89769CL.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 +17 -0
  98. package/src/commands/sync.js +146 -126
  99. package/src/lib/local-api.js +4 -1
  100. package/src/lib/pet-packages.js +44 -2
  101. package/src/lib/pricing/seed-snapshot.json +1 -1
  102. package/src/lib/provider-status.js +104 -0
  103. package/src/lib/qoder-limits.js +782 -0
  104. package/src/lib/rollout.js +385 -0
  105. package/src/lib/usage-limits.js +31 -2
  106. package/dashboard/dist/assets/Card-D6hdzwvI.js +0 -1
  107. package/dashboard/dist/assets/DialogDescription-DgUSe6A1.js +0 -1
  108. package/dashboard/dist/assets/LandingPage-k1271lWV.js +0 -2
  109. package/dashboard/dist/assets/LeaderboardPage-Byww5UpT.js +0 -2
  110. package/dashboard/dist/assets/LimitsPage-DPtqL0PS.js +0 -4
  111. package/dashboard/dist/assets/PetPage--xQAjW4_.js +0 -1
  112. package/dashboard/dist/assets/SettingsPage-CHGA0bQS.js +0 -1
  113. package/dashboard/dist/assets/SkillsPage-I29SqkXO.js +0 -1
  114. package/dashboard/dist/assets/__vite-browser-external-UqlSqI-1.js +0 -1465
  115. package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
  116. package/dashboard/dist/assets/limits-providers-B_18X9Rp.js +0 -1
  117. package/dashboard/dist/assets/use-reduced-motion-C3UrOh55.js +0 -1
  118. package/dashboard/dist/assets/useTokenFormat-BTLrL7Mi.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-UqlSqI-1.js"></script>
79
- <link rel="stylesheet" crossorigin href="/assets/__vite-browser-external-WKilYZnq.css">
78
+ <script type="module" crossorigin src="/assets/__vite-browser-external-C3VH6nS4.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.6",
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.8",
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
 
@@ -62,6 +62,7 @@ const {
62
62
  resolveKilocodeTaskFiles,
63
63
  resolveRoocodeTaskFiles,
64
64
  resolveZedDbPath,
65
+ resolveQoderDbPaths,
65
66
  resolveAnythingllmDbPath,
66
67
  resolveGooseDbPath,
67
68
  listDroidSettingsFiles,
@@ -321,6 +322,16 @@ async function cmdStatus(argv = []) {
321
322
  const zcodeInstalled = zcodeActive.length > 0;
322
323
  const zcodeDbPath = zcodeActive.join(" | ");
323
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
+
324
335
  // OpenCode (JSON files + SQLite DB) — passive scan of storage/message/ and opencode.db.
325
336
  const opencodeStorageNativeValue = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
326
337
  const wslOpencodeStorageDir = process.platform === "win32" && shouldProbeWsl(process.env)
@@ -673,6 +684,9 @@ async function cmdStatus(argv = []) {
673
684
  zcode: zcodeInstalled
674
685
  ? { installed: true, detail: zcodeDbPath }
675
686
  : { installed: false },
687
+ qoder: qoderInstalled
688
+ ? { installed: true, detail: qoderDbPath }
689
+ : { installed: false },
676
690
  kilocode: kilocodeInstalled
677
691
  ? { installed: true, files: kilocodeTaskFiles.length }
678
692
  : { installed: false },
@@ -785,6 +799,9 @@ async function cmdStatus(argv = []) {
785
799
  zcodeInstalled
786
800
  ? `- ZCode: passive reader (${zcodeDbPath})`
787
801
  : null,
802
+ qoderInstalled
803
+ ? `- Qoder: passive reader (${qoderDbPath})`
804
+ : null,
788
805
  opencodeInstalled
789
806
  ? `- OpenCode: passive reader (storage: ${opencodeStorageActive.join(" | ") || "not found"}, DB: ${opencodeDbActive.join(" | ") || "not found"})`
790
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,
@@ -198,7 +201,10 @@ const CODEX_FORK_REPLAY_REPAIR_KEY = "codexForkReplayRepair_2026_07";
198
201
  // interleaved SessionState instances. Older parsers subtracted each cumulative
199
202
  // total from the most recently observed total, even when it belonged to another
200
203
  // state, which could turn an ordinary turn into a multi-billion-token delta.
201
- const CODEX_USAGE_LINEAGE_REPAIR_KEY = "codexUsageLineageRepair_2026_07";
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";
202
208
  // Keep the one escalated desktop refresh bounded; explicit full syncs can retry
203
209
  // the same migration without this ceiling when the history needs a deeper scan.
204
210
  const CODEX_BACKGROUND_LINEAGE_SCAN_MAX_BYTES = 1024 * 1024;
@@ -249,6 +255,7 @@ const AUTO_SYNC_SOURCES = new Set([
249
255
  "opencode",
250
256
  "openclaw",
251
257
  "pi",
258
+ "qoder",
252
259
  "roocode",
253
260
  "workbuddy",
254
261
  "zcode",
@@ -974,6 +981,47 @@ async function cmdSync(argv, context = {}) {
974
981
  };
975
982
  }
976
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
+
977
1025
  async function parseOpencodeDbForInstall({ dbPath, readFn, source, cursorKey, ...rest }) {
978
1026
  if (!dbPath || !fssync.existsSync(dbPath)) {
979
1027
  return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
@@ -2068,6 +2116,7 @@ async function cmdSync(argv, context = {}) {
2068
2116
  geminiResult.filesProcessed +
2069
2117
  antigravityResult.filesProcessed +
2070
2118
  opencodeResult.filesProcessed +
2119
+ qoderResult.recordsProcessed +
2071
2120
  cursorResult.recordsProcessed +
2072
2121
  kiroResult.recordsProcessed +
2073
2122
  kiroCliResult.recordsProcessed +
@@ -2097,6 +2146,7 @@ async function cmdSync(argv, context = {}) {
2097
2146
  geminiResult.bucketsQueued +
2098
2147
  antigravityResult.bucketsQueued +
2099
2148
  opencodeResult.bucketsQueued +
2149
+ qoderResult.bucketsQueued +
2100
2150
  cursorResult.bucketsQueued +
2101
2151
  kiroResult.bucketsQueued +
2102
2152
  kiroCliResult.bucketsQueued +
@@ -3479,6 +3529,7 @@ async function repairCodexRescanInflation({
3479
3529
  projectQueuePath,
3480
3530
  projectQueueStatePath,
3481
3531
  rolloutFiles,
3532
+ expectedCodexFileSnapshots = null,
3482
3533
  // The fork-replay repair (#169 follow-up) re-runs this exact rebuild under
3483
3534
  // its own key: the rebuild always uses the CURRENT parser, so any parser fix
3484
3535
  // shipped since the last run is applied to the rebuilt history.
@@ -3667,6 +3718,16 @@ async function repairCodexRescanInflation({
3667
3718
  }
3668
3719
  }
3669
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
+
3670
3731
  // COMMIT (only after a verified rebuild). A crash partway just leaves the
3671
3732
  // migration to re-run next sync — re-rebuild + re-strip + re-commit converges.
3672
3733
  //
@@ -3903,10 +3964,11 @@ async function repairCodexForkReplayInflation({
3903
3964
  }
3904
3965
 
3905
3966
  // Rebuild historical Codex usage only when a rollout proves that the old
3906
- // single-baseline parser crossed cumulative SessionState lineages. The cheap
3907
- // head gate keeps the one-time migration from fully reading installations that
3908
- // predate Codex multi-agent support; the rebuild itself retains the existing
3909
- // missing-history, project-queue, atomic-swap, and upload-reset guards.
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.
3910
3972
  async function repairCodexInterleavedUsageInflation({
3911
3973
  cursors,
3912
3974
  queuePath,
@@ -3961,8 +4023,9 @@ async function repairCodexInterleavedUsageInflation({
3961
4023
  projectQueuePath,
3962
4024
  projectQueueStatePath,
3963
4025
  rolloutFiles,
4026
+ expectedCodexFileSnapshots: scan.fileSnapshots,
3964
4027
  migrationKey: CODEX_USAGE_LINEAGE_REPAIR_KEY,
3965
- uploadNote: "reset_after_codex_usage_lineage_2026_07",
4028
+ uploadNote: "reset_after_codex_usage_lineage_2026_07_v2",
3966
4029
  });
3967
4030
  }
3968
4031
 
@@ -3970,138 +4033,41 @@ async function scanForInterleavedCodexUsage(
3970
4033
  rolloutFiles,
3971
4034
  { maxLineageScanBytes = Infinity } = {},
3972
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
+
3973
4043
  for (const entry of Array.isArray(rolloutFiles) ? rolloutFiles : []) {
3974
4044
  const fp = typeof entry === "string" ? entry : entry?.path;
3975
4045
  const src = typeof entry === "string" ? "codex" : String(entry?.source || "codex");
3976
- if (!fp || src !== "codex") continue;
3977
-
3978
- const head = await scanCodexMultiAgentHead(fp);
3979
- if (head.indeterminate) return { affected: false, indeterminate: true };
3980
- if (!head.candidate) continue;
4046
+ if (!fp || src !== "codex" || seenPaths.has(fp)) continue;
4047
+ seenPaths.add(fp);
3981
4048
 
3982
- const lineage = await scanCodexUsageLineages(fp, maxLineageScanBytes);
3983
- if (lineage.indeterminate) return { affected: false, indeterminate: true };
3984
- if (lineage.affected) return { affected: true, indeterminate: false };
3985
- }
3986
- return { affected: false, indeterminate: false };
3987
- }
3988
-
3989
- // New rollouts emit multi_agent_version in the initial turn_context. A session
3990
- // created by an older Codex build can later be resumed under a multi-agent
3991
- // build, though, so an inactive initial context also checks the final context
3992
- // from a bounded tail read. The 1 MiB ceilings protect this one-time migration
3993
- // from malformed metadata; an inconclusive bound is retryable, never a final
3994
- // "not affected" verdict.
3995
- async function scanCodexMultiAgentHead(filePath) {
3996
- const MAX_BYTES = 1024 * 1024;
3997
- let stream = null;
3998
- let rl = null;
3999
- let needsTail = false;
4000
- try {
4001
- stream = fssync.createReadStream(filePath, {
4002
- encoding: "utf8",
4003
- highWaterMark: 32 * 1024,
4004
- });
4005
- rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4006
- let bytesRead = 0;
4007
- for await (const line of rl) {
4008
- bytesRead += Buffer.byteLength(line, "utf8") + 1;
4009
- if (line.includes('"turn_context"')) {
4010
- let obj;
4011
- try {
4012
- obj = JSON.parse(line);
4013
- } catch (_e) {
4014
- return { candidate: false, indeterminate: true };
4015
- }
4016
- if (obj?.type === "turn_context") {
4017
- if (isActiveMultiAgentVersion(obj?.payload?.multi_agent_version)) {
4018
- return { candidate: true, indeterminate: false };
4019
- }
4020
- needsTail = true;
4021
- }
4022
- }
4023
- if (line.includes('"token_count"')) {
4024
- needsTail = true;
4025
- break;
4026
- }
4027
- if (bytesRead >= MAX_BYTES) return { candidate: false, indeterminate: true };
4049
+ const lineage = await scanCodexUsageLineages(fp, remainingBytes);
4050
+ if (lineage.indeterminate) {
4051
+ return { affected: false, indeterminate: true, fileSnapshots: new Map() };
4028
4052
  }
4029
- } catch (_e) {
4030
- return { candidate: false, indeterminate: true };
4031
- } finally {
4032
- if (rl) rl.close();
4033
- if (stream) stream.destroy();
4034
- }
4035
- if (needsTail) return scanCodexMultiAgentTail(filePath);
4036
- return { candidate: false, indeterminate: false };
4037
- }
4038
-
4039
- async function scanCodexMultiAgentTail(filePath) {
4040
- const MAX_BYTES = 1024 * 1024;
4041
- let handle = null;
4042
- try {
4043
- handle = await fs.open(filePath, "r");
4044
- const stat = await handle.stat();
4045
- const size = Number(stat.size || 0);
4046
- const start = Math.max(0, size - MAX_BYTES);
4047
- const length = Math.max(0, size - start);
4048
- const buffer = Buffer.alloc(length);
4049
- let bytesRead = 0;
4050
- while (bytesRead < length) {
4051
- const result = await handle.read(
4052
- buffer,
4053
- bytesRead,
4054
- length - bytesRead,
4055
- start + bytesRead,
4056
- );
4057
- if (result.bytesRead <= 0) break;
4058
- bytesRead += result.bytesRead;
4059
- }
4060
- if (bytesRead !== length) return { candidate: false, indeterminate: true };
4061
- const lines = buffer.toString("utf8").split("\n");
4062
- if (start > 0) lines.shift(); // discard a line truncated by the tail boundary
4063
- for (let index = lines.length - 1; index >= 0; index -= 1) {
4064
- const line = lines[index];
4065
- if (!line.includes('"turn_context"')) continue;
4066
- let obj;
4067
- try {
4068
- obj = JSON.parse(line);
4069
- } catch (_e) {
4070
- return { candidate: false, indeterminate: true };
4071
- }
4072
- if (obj?.type !== "turn_context") continue;
4073
- return {
4074
- candidate: isActiveMultiAgentVersion(obj?.payload?.multi_agent_version),
4075
- indeterminate: false,
4076
- };
4053
+ affected ||= lineage.affected;
4054
+ fileSnapshots.set(fp, lineage.fileSnapshot);
4055
+ if (Number.isFinite(remainingBytes)) {
4056
+ remainingBytes = Math.max(0, remainingBytes - lineage.bytesRead);
4077
4057
  }
4078
- return { candidate: false, indeterminate: start > 0 };
4079
- } catch (_e) {
4080
- return { candidate: false, indeterminate: true };
4081
- } finally {
4082
- if (handle) await handle.close().catch(() => {});
4083
4058
  }
4084
- }
4085
-
4086
- function isActiveMultiAgentVersion(value) {
4087
- if (value === true) return true;
4088
- if (typeof value !== "string") return false;
4089
- const normalized = value.trim().toLowerCase();
4090
- return Boolean(
4091
- normalized &&
4092
- normalized !== "off" &&
4093
- normalized !== "none" &&
4094
- normalized !== "disabled",
4095
- );
4059
+ return { affected, indeterminate: false, fileSnapshots };
4096
4060
  }
4097
4061
 
4098
4062
  async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
4099
4063
  let stream = null;
4100
4064
  let rl = null;
4101
4065
  try {
4066
+ const before = await readCodexFileSnapshot(filePath);
4102
4067
  const state = createUsageDeltaState();
4103
4068
  const byteLimit = Number.isFinite(maxBytes) ? Math.max(0, maxBytes) : Infinity;
4104
4069
  let bytesRead = 0;
4070
+ let affected = false;
4105
4071
  stream = fssync.createReadStream(filePath, {
4106
4072
  encoding: "utf8",
4107
4073
  highWaterMark: 32 * 1024,
@@ -4110,22 +4076,31 @@ async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
4110
4076
  for await (const line of rl) {
4111
4077
  bytesRead += Buffer.byteLength(line, "utf8") + 1;
4112
4078
  if (bytesRead > byteLimit) return { affected: false, indeterminate: true };
4113
- if (!line.includes('"token_count"')) continue;
4079
+ if (!line.trim()) continue;
4114
4080
  let obj;
4115
4081
  try {
4116
4082
  obj = JSON.parse(line);
4117
4083
  } catch (_e) {
4118
- continue;
4084
+ return { affected: false, indeterminate: true };
4119
4085
  }
4120
4086
  const token = extractTokenCount(obj);
4121
4087
  const info = token?.info;
4122
4088
  if (!info || typeof info !== "object") continue;
4123
4089
  consumeUsageDelta(state, info.last_token_usage, info.total_token_usage);
4124
4090
  if (state.sawInterleaved || state.sawDivergentCumulative) {
4125
- return { affected: true, indeterminate: false };
4091
+ affected = true;
4126
4092
  }
4127
4093
  }
4128
- return { affected: false, indeterminate: false };
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
+ };
4129
4104
  } catch (_e) {
4130
4105
  return { affected: false, indeterminate: true };
4131
4106
  } finally {
@@ -4134,6 +4109,51 @@ async function scanCodexUsageLineages(filePath, maxBytes = Infinity) {
4134
4109
  }
4135
4110
  }
4136
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
+
4137
4157
  function isCodexHistoryCovered(cursors, rolloutFiles) {
4138
4158
  const scannedPaths = new Set();
4139
4159
  const scannedSessionIds = new Set();
@@ -2684,7 +2684,10 @@ function createLocalApiHandler({ queuePath }) {
2684
2684
  json(res, { importable, codexDetected: importable.length > 0 });
2685
2685
  return true;
2686
2686
  }
2687
- json(res, { pets: pets.listInstalledPets() });
2687
+ json(res, {
2688
+ pets: pets.listInstalledPets(),
2689
+ hiddenBuiltinIds: pets.readHiddenBuiltinIds(),
2690
+ });
2688
2691
  return true;
2689
2692
  }
2690
2693
  if (method === "POST") {
@@ -10,6 +10,8 @@ const MAX_PACKAGE_BYTES = 12 * 1024 * 1024;
10
10
  const MAX_MANIFEST_BYTES = 16 * 1024;
11
11
  const MAX_SPRITESHEET_BYTES = 10 * 1024 * 1024;
12
12
  const BUILTIN_IDS = new Set(["clawd", "sprout", "byte", "ember"]);
13
+ const REMOVABLE_BUILTIN_IDS = new Set(["sprout", "byte", "ember"]);
14
+ const HIDDEN_BUILTINS_FILE = ".hidden-builtins.json";
13
15
  // codex-pets.net added `kind` to pet.json long after launch, and its enum may keep
14
16
  // growing — packages published before that (and future kinds) must stay importable,
15
17
  // so kind is optional metadata validated only as a short slug.
@@ -35,6 +37,44 @@ function resolveCodexPetsDir() {
35
37
  );
36
38
  }
37
39
 
40
+ function readHiddenBuiltinIds() {
41
+ const file = path.join(resolvePetsDir(), HIDDEN_BUILTINS_FILE);
42
+ try {
43
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
44
+ if (!Array.isArray(parsed)) return [];
45
+ return [...new Set(parsed
46
+ .map((id) => String(id || "").trim().toLowerCase())
47
+ .filter((id) => REMOVABLE_BUILTIN_IDS.has(id)))].sort();
48
+ } catch {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ function hideBuiltinPet(id) {
54
+ const normalized = String(id || "").trim().toLowerCase();
55
+ if (normalized === "clawd") throw new Error("Clawd cannot be removed");
56
+ if (!REMOVABLE_BUILTIN_IDS.has(normalized)) throw new Error("Invalid built-in pet id");
57
+
58
+ const root = resolvePetsDir();
59
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
60
+ const hidden = new Set(readHiddenBuiltinIds());
61
+ hidden.add(normalized);
62
+ const destination = path.join(root, HIDDEN_BUILTINS_FILE);
63
+ const stage = path.join(root, `${HIDDEN_BUILTINS_FILE}.${process.pid}-${crypto.randomBytes(6).toString("hex")}`);
64
+ const backup = `${stage}-backup`;
65
+ try {
66
+ fs.writeFileSync(stage, `${JSON.stringify([...hidden].sort(), null, 2)}\n`, { mode: 0o600 });
67
+ if (fs.existsSync(destination)) fs.renameSync(destination, backup);
68
+ fs.renameSync(stage, destination);
69
+ } catch (error) {
70
+ try { fs.rmSync(stage, { force: true }); } catch {}
71
+ if (!fs.existsSync(destination) && fs.existsSync(backup)) fs.renameSync(backup, destination);
72
+ throw error;
73
+ }
74
+ try { fs.rmSync(backup, { force: true }); } catch {}
75
+ return { id: normalized, hidden: true };
76
+ }
77
+
38
78
  function readUInt24LE(buffer, offset) {
39
79
  return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16);
40
80
  }
@@ -368,10 +408,11 @@ async function installFromCodexPets(input, fetchImpl = fetch) {
368
408
 
369
409
  function removeInstalledPet(id) {
370
410
  const normalized = String(id || "").trim().toLowerCase();
371
- if (!PET_ID_RE.test(normalized) || BUILTIN_IDS.has(normalized)) throw new Error("Invalid custom pet id");
411
+ if (!PET_ID_RE.test(normalized)) throw new Error("Invalid pet id");
412
+ if (BUILTIN_IDS.has(normalized)) return hideBuiltinPet(normalized);
372
413
  const root = resolvePetsDir();
373
414
  const destination = path.join(root, normalized);
374
- if (!destination.startsWith(`${root}${path.sep}`)) throw new Error("Invalid custom pet path");
415
+ if (!destination.startsWith(`${root}${path.sep}`)) throw new Error("Invalid pet path");
375
416
  fs.rmSync(destination, { recursive: true, force: true });
376
417
  petDirectoryCache.delete(destination);
377
418
  return { id: normalized };
@@ -627,6 +668,7 @@ module.exports = {
627
668
  petIdFromCodexPetsUrl,
628
669
  readCodexBuiltinPets,
629
670
  readCodexImportableAsset,
671
+ readHiddenBuiltinIds,
630
672
  readWebpDimensions,
631
673
  removeInstalledPet,
632
674
  resolveCodexPetsDir,