tokentracker-cli 0.87.9 → 0.87.11

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 (96) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-D3_hERkQ.js → AchievementBadge-C-w9f93M.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DZGjT92g.js → AchievementsPage-BAtc6581.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-CEzq3U3A.js → AchievementsSection-Bok48qmR.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-D3_B8E--.js → BadgeDetailModal-CnR1S5Fk.js} +1 -1
  5. package/dashboard/dist/assets/{Card-CMXM3-lp.js → Card-A4JQKxIZ.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-lQxUKRzw.js → ClawdAnimated-CNRbH_ri.js} +1 -1
  7. package/dashboard/dist/assets/{CommandPalette-BCGdAeFB.js → CommandPalette-CAkZSh_I.js} +1 -1
  8. package/dashboard/dist/assets/{CommunityStatsModal-BwE5h2y5.js → CommunityStatsModal-Dh1PvEU5.js} +1 -1
  9. package/dashboard/dist/assets/{ConfirmModal-8uhS2ccj.js → ConfirmModal-DtUn4PvI.js} +1 -1
  10. package/dashboard/dist/assets/{Controls-CBEQTl2X.js → Controls-Bju6_Qgc.js} +1 -1
  11. package/dashboard/dist/assets/{DashboardPage-UE_3tGVy.js → DashboardPage-BFXwPC6L.js} +1 -1
  12. package/dashboard/dist/assets/DevicePage-BbiVkFkJ.js +1 -0
  13. package/dashboard/dist/assets/{DialogClose-L_t-nrYm.js → DialogClose-OelNPG0Y.js} +1 -1
  14. package/dashboard/dist/assets/DialogDescription-DtcxB2Zm.js +1 -0
  15. package/dashboard/dist/assets/{DialogTitle-Bsft4VvL.js → DialogTitle-C_CTTQtO.js} +1 -1
  16. package/dashboard/dist/assets/{FadeIn-BNDdLqTu.js → FadeIn-CkRSoCb2.js} +1 -1
  17. package/dashboard/dist/assets/{HeaderGithubStar-CvNaKDbl.js → HeaderGithubStar-CTX7kP5L.js} +1 -1
  18. package/dashboard/dist/assets/{HoverTooltip-D5j5IpNx.js → HoverTooltip-CPlF8ZwO.js} +1 -1
  19. package/dashboard/dist/assets/{IpCheckPage-Csf8gMJW.js → IpCheckPage-BJsKVMC-.js} +1 -1
  20. package/dashboard/dist/assets/{LandingPage-BVSSwM0D.js → LandingPage-9jAUDqx0.js} +2 -2
  21. package/dashboard/dist/assets/{LeaderboardAvatar-DXxSHqmP.js → LeaderboardAvatar-8dN340iC.js} +1 -1
  22. package/dashboard/dist/assets/{LeaderboardPage-DXj21BBR.js → LeaderboardPage-B1MBedOd.js} +2 -2
  23. package/dashboard/dist/assets/{LeaderboardProfileModal-DbEzZGg4.js → LeaderboardProfileModal-CxrQFLfo.js} +3 -3
  24. package/dashboard/dist/assets/{LeaderboardProfilePage-DoIm3jB_.js → LeaderboardProfilePage-C7LkCn7j.js} +1 -1
  25. package/dashboard/dist/assets/{LimitsPage-D_tpQrUM.js → LimitsPage-CX2ohexs.js} +1 -1
  26. package/dashboard/dist/assets/{LocalOnlyNotice-DBi5iuPr.js → LocalOnlyNotice-Bx3Y6V6z.js} +1 -1
  27. package/dashboard/dist/assets/{LoginCard-qDIJgUX1.js → LoginCard-C6nKjmVd.js} +1 -1
  28. package/dashboard/dist/assets/{LoginModal-D4b5p4u1.js → LoginModal-Cy3pcURe.js} +1 -1
  29. package/dashboard/dist/assets/{LoginPage-CsSgItFA.js → LoginPage-DwA2WE_J.js} +1 -1
  30. package/dashboard/dist/assets/{PetPage-B-wElg5q.js → PetPage-CeMz69em.js} +1 -1
  31. package/dashboard/dist/assets/{PopoverPopup-9C_VarUQ.js → PopoverPopup-Dr_89ZsN.js} +1 -1
  32. package/dashboard/dist/assets/{ProviderIcon-CciEdEeR.js → ProviderIcon-yBPK67if.js} +1 -1
  33. package/dashboard/dist/assets/{ResetPasswordPage-lsI8qlzE.js → ResetPasswordPage-5foVX-ji.js} +1 -1
  34. package/dashboard/dist/assets/{SegmentedControl-BGonMV45.js → SegmentedControl-ByMBjrQu.js} +1 -1
  35. package/dashboard/dist/assets/{Select-DhRrGrDT.js → Select-BmiLelJH.js} +1 -1
  36. package/dashboard/dist/assets/{SelectItemText-ClMYgZsa.js → SelectItemText-ra9fOxTH.js} +1 -1
  37. package/dashboard/dist/assets/{ServiceStatusPage-DuzttgL7.js → ServiceStatusPage-DN15AhqV.js} +1 -1
  38. package/dashboard/dist/assets/{SessionsPage-gZbxx0f_.js → SessionsPage-DcNtVUlG.js} +1 -1
  39. package/dashboard/dist/assets/{SettingsPage-NhxTrVNW.js → SettingsPage-B62dIBq2.js} +1 -1
  40. package/dashboard/dist/assets/{SkillsPage-BeXojJqQ.js → SkillsPage-BH9Y-Jr3.js} +1 -1
  41. package/dashboard/dist/assets/{TokenGalaxy-DM49zKzo.js → TokenGalaxy-Dop8r6gR.js} +1 -1
  42. package/dashboard/dist/assets/{ToolbarRootContext-BfvoVNMW.js → ToolbarRootContext--p8cQmQC.js} +1 -1
  43. package/dashboard/dist/assets/{TrendMonitor-Bu-dXcrV.js → TrendMonitor-O_3Y-SZE.js} +1 -1
  44. package/dashboard/dist/assets/{WidgetsPage-D2Wo7seE.js → WidgetsPage-fmeuoBRn.js} +1 -1
  45. package/dashboard/dist/assets/{WrappedPage-BXAwic4D.js → WrappedPage-D31IOcLs.js} +1 -1
  46. package/dashboard/dist/assets/{__vite-browser-external-BDEQk--l.js → __vite-browser-external-B8xHeUWG.js} +2 -2
  47. package/dashboard/dist/assets/{agent-logos-DMEEHjG7.js → agent-logos-qv-6cVem.js} +1 -1
  48. package/dashboard/dist/assets/{arrow-up-right-gNviKtY8.js → arrow-up-right-D2jDALUn.js} +1 -1
  49. package/dashboard/dist/assets/{check-Cvg5pmYP.js → check-Ct6KAwPF.js} +1 -1
  50. package/dashboard/dist/assets/{chevron-right-DxdaNTo8.js → chevron-right-C6_7i5IV.js} +1 -1
  51. package/dashboard/dist/assets/{copy-BNIJL7bf.js → copy-CuN61rn4.js} +1 -1
  52. package/dashboard/dist/assets/{download-C9McJlPa.js → download-CGQHQ_O1.js} +1 -1
  53. package/dashboard/dist/assets/{external-link-C_IU6zDK.js → external-link-DMK8oGV1.js} +1 -1
  54. package/dashboard/dist/assets/{flame-CII7X8Je.js → flame-DYDfZm4m.js} +1 -1
  55. package/dashboard/dist/assets/{format-tokens-DuQbGpPt.js → format-tokens-Cr3tGM0M.js} +1 -1
  56. package/dashboard/dist/assets/{icons-B1YTqnxG.js → icons-C5uWoSyt.js} +1 -1
  57. package/dashboard/dist/assets/{index-Dc9xsIu2.js → index-BnRjukrI.js} +1 -1
  58. package/dashboard/dist/assets/{index-BRMxraNK.js → index-BxQbRCxm.js} +1 -1
  59. package/dashboard/dist/assets/{index-QtcYir6O.js → index-CvXbHj4v.js} +1 -1
  60. package/dashboard/dist/assets/{info-CIgVqJoj.js → info-DrAzGhOW.js} +1 -1
  61. package/dashboard/dist/assets/{limits-providers-B31NsGar.js → limits-providers-DxW9ioQB.js} +1 -1
  62. package/dashboard/dist/assets/{link-2-DTPXzHuB.js → link-2-BI38rNr_.js} +1 -1
  63. package/dashboard/dist/assets/{loader-circle-CgQSyCz8.js → loader-circle-Bo5D6bhR.js} +1 -1
  64. package/dashboard/dist/assets/{maximize-2-BL5J4aHZ.js → maximize-2-CYdScbRg.js} +1 -1
  65. package/dashboard/dist/assets/{modifiers.esm-s_rzcEf2.js → modifiers.esm-quleLIKM.js} +1 -1
  66. package/dashboard/dist/assets/{monitor-smartphone-C9ei_xde.js → monitor-smartphone-BIX1vqmO.js} +1 -1
  67. package/dashboard/dist/assets/{provider-display-ZgvoJi05.js → provider-display-BIgyIwfN.js} +1 -1
  68. package/dashboard/dist/assets/{react-DhfPVGOm.js → react-D3GtDc7w.js} +1 -1
  69. package/dashboard/dist/assets/{search-Ba_MWO7q.js → search-BkIdCRLX.js} +1 -1
  70. package/dashboard/dist/assets/{skills-api-BkmJmyg1.js → skills-api-CderQz5B.js} +1 -1
  71. package/dashboard/dist/assets/{sortable.esm-Bmd5t12E.js → sortable.esm-2gcAB4zs.js} +1 -1
  72. package/dashboard/dist/assets/{store-BxRQs9yc.js → store-C1IKZc33.js} +1 -1
  73. package/dashboard/dist/assets/{terminal-BzUwAs4g.js → terminal-p1ztJj4p.js} +1 -1
  74. package/dashboard/dist/assets/{trash-2-CCprY7cu.js → trash-2-CJWF-VNC.js} +1 -1
  75. package/dashboard/dist/assets/{use-community-stats-TCdPJ_ir.js → use-community-stats-Dn06TrFv.js} +1 -1
  76. package/dashboard/dist/assets/{use-limits-display-prefs-CNF9luO6.js → use-limits-display-prefs-BgaBwlW4.js} +1 -1
  77. package/dashboard/dist/assets/{use-ordered-list-BlTuSYJy.js → use-ordered-list-CLAfnQJp.js} +1 -1
  78. package/dashboard/dist/assets/use-reduced-motion-DAOu1VYi.js +1 -0
  79. package/dashboard/dist/assets/{use-session-efficiency-pref-iHS0O_Fo.js → use-session-efficiency-pref-Bjt6Wz-F.js} +1 -1
  80. package/dashboard/dist/assets/{use-transform-CcVfL3mi.js → use-transform-Bacfpv38.js} +1 -1
  81. package/dashboard/dist/assets/{use-usage-limits-Xdy-Y_1Z.js → use-usage-limits-BF6HF4CU.js} +1 -1
  82. package/dashboard/dist/assets/{useCurrency-IXccXbmx.js → useCurrency-BResSm4n.js} +1 -1
  83. package/dashboard/dist/assets/{useScrollLock-BEVewfZm.js → useScrollLock-C7qX5L-D.js} +1 -1
  84. package/dashboard/dist/assets/{useTokenFormat-Cihv3Ghl.js → useTokenFormat-COB2oujy.js} +1 -1
  85. package/dashboard/dist/assets/{zap-zG2Fbb5_.js → zap-8w1maR-b.js} +1 -1
  86. package/dashboard/dist/index.html +1 -1
  87. package/dashboard/dist/ip-check.html +1 -1
  88. package/dashboard/dist/leaderboard.html +1 -1
  89. package/dashboard/dist/share.html +1 -1
  90. package/package.json +1 -1
  91. package/src/commands/sync.js +186 -1
  92. package/src/lib/pricing/seed-snapshot.json +1 -1
  93. package/src/lib/rollout.js +232 -44
  94. package/dashboard/dist/assets/DevicePage-B06ybHJI.js +0 -1
  95. package/dashboard/dist/assets/DialogDescription-B7FFtR7A.js +0 -1
  96. package/dashboard/dist/assets/use-reduced-motion-CfiR91Eg.js +0 -1
@@ -144,6 +144,7 @@ const ROLLOUT_CUMULATIVE_DELTA_MIGRATION_KEY = "rolloutCumulativeDeltaReparse_20
144
144
  const CLAUDE_MEM_OBSERVER_REINCLUDE_KEY = "claudeMemObserverReinclude_2026_05_v3";
145
145
  const GROK_APPEND_ONLY_REPAIR_MIGRATION_KEY = "grokAppendOnlyRepair_2026_05_v4";
146
146
  const CODEBUDDY_LOG_JSONL_REPAIR_KEY = "codebuddyLogJsonlOverlapRepair_2026_08";
147
+ const WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY = "workbuddyContextUsageRepair_2026_08";
147
148
  const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
148
149
  // v1 had a cursor-format bug (wrote plain integer instead of {inode, offset,
149
150
  // updatedAt}), which made parseClaudeIncremental reread every jsonl from
@@ -1837,6 +1838,19 @@ async function cmdSync(argv, context = {}) {
1837
1838
  ? mergeBothFileSources({ resolveFiles: resolveWorkbuddyProjectFiles, env: process.env })
1838
1839
  : [];
1839
1840
  if (sourceAllowed("workbuddy")) {
1841
+ if (isFullSourceScan) {
1842
+ try {
1843
+ await repairWorkbuddyContextUsage({
1844
+ cursors,
1845
+ queuePath,
1846
+ queueStatePath,
1847
+ workbuddyFiles,
1848
+ env: process.env,
1849
+ });
1850
+ } catch (err) {
1851
+ warnProviderParseFailure("WorkBuddy context-usage repair", err, opts);
1852
+ }
1853
+ }
1840
1854
  if (progress?.enabled) {
1841
1855
  progress.start(`Parsing WorkBuddy ${renderBar(0)} | buckets 0`);
1842
1856
  }
@@ -2762,6 +2776,8 @@ module.exports = {
2762
2776
  migrateCursorUnknownBuckets,
2763
2777
  migrateRolloutCumulativeDeltaBuckets,
2764
2778
  repairCodebuddyLogJsonlOverlap,
2779
+ repairWorkbuddyContextUsage,
2780
+ WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY,
2765
2781
  repairCodexRescanInflation,
2766
2782
  repairCodexForkReplayInflation,
2767
2783
  repairCodexInterleavedUsageInflation,
@@ -3965,7 +3981,10 @@ async function repairCodebuddyLogJsonlOverlap({
3965
3981
  let rebuiltRows = [];
3966
3982
  try {
3967
3983
  await parseCodebuddyIncremental({
3968
- projectFiles: jsonlFiles,
3984
+ // Rebuild from both sources. The parser now preserves legacy-only log
3985
+ // sessions while consuming mirrored log fingerprints against JSONL, so
3986
+ // rebuilding JSONL alone would silently discard valid pre-rawUsage data.
3987
+ projectFiles: files,
3969
3988
  cursors: tmpCursors,
3970
3989
  queuePath: tmpQueue,
3971
3990
  env,
@@ -4053,6 +4072,172 @@ async function repairCodebuddyLogJsonlOverlap({
4053
4072
  return true;
4054
4073
  }
4055
4074
 
4075
+ // WorkBuddy's 0.87.9 SQLite fallback treated the bounded session_usage.used
4076
+ // context snapshot as cumulative input tokens. A current database proves that
4077
+ // assumption false (`size` is a 192k context limit while trace input totals can
4078
+ // exceed it), so rebuild historical WorkBuddy buckets from JSONL/trace sources
4079
+ // once the new parser is available. The same missing-source guard used by the
4080
+ // other data migrations keeps this recoverable: if a previously contributing
4081
+ // file is gone, leave history untouched and stop adding new context snapshots.
4082
+ async function repairWorkbuddyContextUsage({
4083
+ cursors,
4084
+ queuePath,
4085
+ queueStatePath,
4086
+ workbuddyFiles,
4087
+ env = process.env,
4088
+ } = {}) {
4089
+ if (!cursors || typeof cursors !== "object") return false;
4090
+ const migrations = (cursors.migrations ||= {});
4091
+ const prior = migrations[WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY];
4092
+ if (prior && !(typeof prior === "object" && prior.skipped)) return false;
4093
+
4094
+ const hourly = cursors.hourly && typeof cursors.hourly === "object" ? cursors.hourly : {};
4095
+ const liveHourlyKeys = new Set(
4096
+ Object.keys(hourly.buckets || {}).filter((key) => key.startsWith("workbuddy|")),
4097
+ );
4098
+ let queueRaw = "";
4099
+ try {
4100
+ queueRaw = await fs.readFile(queuePath, "utf8");
4101
+ } catch (e) {
4102
+ if (e?.code !== "ENOENT") throw e;
4103
+ }
4104
+ const queueWorkbuddyKeys = new Set();
4105
+ const keptQueueLines = [];
4106
+ for (const line of queueRaw.split("\n")) {
4107
+ if (!line.trim()) continue;
4108
+ let row;
4109
+ try { row = JSON.parse(line); } catch (_e) {
4110
+ keptQueueLines.push(line);
4111
+ continue;
4112
+ }
4113
+ if (row && row.source === "workbuddy" && typeof row.model === "string" && typeof row.hour_start === "string") {
4114
+ queueWorkbuddyKeys.add(bucketKey("workbuddy", row.model || "unknown", row.hour_start));
4115
+ } else {
4116
+ keptQueueLines.push(line);
4117
+ }
4118
+ }
4119
+ const liveKeys = new Set([...liveHourlyKeys, ...queueWorkbuddyKeys]);
4120
+ if (liveKeys.size === 0) {
4121
+ migrations[WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY] = {
4122
+ status: "noop",
4123
+ appliedAt: new Date().toISOString(),
4124
+ reason: "no-workbuddy-history",
4125
+ };
4126
+ return false;
4127
+ }
4128
+
4129
+ const files = Array.isArray(workbuddyFiles) ? workbuddyFiles : [];
4130
+ const discoveredPaths = new Set(
4131
+ files.map((entry) => typeof entry === "string" ? entry : entry?.path).filter(Boolean),
4132
+ );
4133
+ const priorFiles = cursors.workbuddy?.fileOffsets && typeof cursors.workbuddy.fileOffsets === "object"
4134
+ ? Object.keys(cursors.workbuddy.fileOffsets)
4135
+ : [];
4136
+ if (files.length === 0 || priorFiles.some((filePath) => !discoveredPaths.has(filePath))) {
4137
+ migrations[WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY] = {
4138
+ skipped: true,
4139
+ reason: files.length === 0 ? "no-rebuild-sources" : "workbuddy_file_unreproducible",
4140
+ at: new Date().toISOString(),
4141
+ };
4142
+ return false;
4143
+ }
4144
+
4145
+ const tmpQueue = `${queuePath}.workbuddyrebuild.${process.pid}.${Date.now()}`;
4146
+ const tmpCursors = {
4147
+ version: 1,
4148
+ hourly: { buckets: {}, groupQueued: {} },
4149
+ workbuddy: {},
4150
+ };
4151
+ let rebuiltRows = [];
4152
+ try {
4153
+ await parseWorkbuddyIncremental({
4154
+ projectFiles: files,
4155
+ cursors: tmpCursors,
4156
+ queuePath: tmpQueue,
4157
+ env,
4158
+ });
4159
+ let rebuiltRaw = "";
4160
+ try { rebuiltRaw = await fs.readFile(tmpQueue, "utf8"); } catch (e) {
4161
+ if (e?.code !== "ENOENT") throw e;
4162
+ }
4163
+ rebuiltRows = rebuiltRaw.split("\n").filter((line) => line.trim());
4164
+ } catch (e) {
4165
+ console.error("[sync] WorkBuddy context-usage repair: rebuild failed, leaving history untouched:", e?.message || e);
4166
+ return false;
4167
+ } finally {
4168
+ await fs.rm(tmpQueue, { force: true }).catch(() => {});
4169
+ }
4170
+
4171
+ const rebuiltKeys = new Set();
4172
+ const validRebuiltRows = [];
4173
+ for (const line of rebuiltRows) {
4174
+ let row;
4175
+ try { row = JSON.parse(line); } catch (_e) { continue; }
4176
+ if (!row || row.source !== "workbuddy" || typeof row.model !== "string" || typeof row.hour_start !== "string") continue;
4177
+ rebuiltKeys.add(bucketKey("workbuddy", row.model || "unknown", row.hour_start));
4178
+ validRebuiltRows.push(line);
4179
+ }
4180
+
4181
+ const staleRetractions = [];
4182
+ for (const key of liveKeys) {
4183
+ if (rebuiltKeys.has(key)) continue;
4184
+ const [, model, ...hourParts] = key.split("|");
4185
+ staleRetractions.push(JSON.stringify({
4186
+ source: "workbuddy",
4187
+ model: model || "unknown",
4188
+ hour_start: hourParts.join("|"),
4189
+ input_tokens: 0,
4190
+ cached_input_tokens: 0,
4191
+ cache_creation_input_tokens: 0,
4192
+ output_tokens: 0,
4193
+ reasoning_output_tokens: 0,
4194
+ total_tokens: 0,
4195
+ billable_total_tokens: 0,
4196
+ conversation_count: 0,
4197
+ }));
4198
+ }
4199
+
4200
+ const nextQueueLines = keptQueueLines.concat(staleRetractions, validRebuiltRows);
4201
+ await ensureDir(path.dirname(queuePath));
4202
+ const queueTmp = `${queuePath}.tmp.${process.pid}.${Date.now()}`;
4203
+ await fs.writeFile(queueTmp, nextQueueLines.length ? `${nextQueueLines.join("\n")}\n` : "", "utf8");
4204
+ await fs.rename(queueTmp, queuePath);
4205
+
4206
+ const nextHourly = (cursors.hourly ||= { buckets: {}, groupQueued: {} });
4207
+ nextHourly.buckets ||= {};
4208
+ nextHourly.groupQueued ||= {};
4209
+ for (const key of Object.keys(nextHourly.buckets)) {
4210
+ if (key.startsWith("workbuddy|")) delete nextHourly.buckets[key];
4211
+ }
4212
+ for (const key of Object.keys(nextHourly.groupQueued)) {
4213
+ if (key.startsWith("workbuddy|")) delete nextHourly.groupQueued[key];
4214
+ }
4215
+ for (const [key, bucket] of Object.entries(tmpCursors.hourly.buckets || {})) {
4216
+ if (key.startsWith("workbuddy|")) nextHourly.buckets[key] = bucket;
4217
+ }
4218
+ for (const [key, value] of Object.entries(tmpCursors.hourly.groupQueued || {})) {
4219
+ if (key.startsWith("workbuddy|")) nextHourly.groupQueued[key] = value;
4220
+ }
4221
+ cursors.workbuddy = tmpCursors.workbuddy || {};
4222
+
4223
+ let uploadState = {};
4224
+ try { uploadState = JSON.parse(await fs.readFile(queueStatePath, "utf8")); } catch (_e) {}
4225
+ uploadState.offset = 0;
4226
+ uploadState.updatedAt = new Date().toISOString();
4227
+ uploadState.note = "reset_after_workbuddy_context_usage_repair_2026_08";
4228
+ await ensureDir(path.dirname(queueStatePath));
4229
+ await fs.writeFile(queueStatePath, JSON.stringify(uploadState, null, 2) + "\n", "utf8");
4230
+
4231
+ migrations[WORKBUDDY_CONTEXT_USAGE_REPAIR_KEY] = {
4232
+ status: "applied",
4233
+ appliedAt: new Date().toISOString(),
4234
+ previousBuckets: liveKeys.size,
4235
+ rebuiltBuckets: rebuiltKeys.size,
4236
+ retractedBuckets: staleRetractions.length,
4237
+ };
4238
+ return true;
4239
+ }
4240
+
4056
4241
  // One-time repair (#187): rebuild codex hourly buckets that the inode-keyed
4057
4242
  // re-scan double-counted before the codexHashes event-dedup landed, and push
4058
4243
  // the corrected values to the cloud. Runs BEFORE the codex parse in the same