tokentracker-cli 1.3.16 → 1.4.2

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 (145) hide show
  1. package/README.de.md +5 -4
  2. package/README.ja.md +5 -4
  3. package/README.ko.md +5 -4
  4. package/README.md +5 -4
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{AchievementBadge-D5sRTMmp.js → AchievementBadge-WGbdoboF.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CaiCfchd.js → AchievementsPage-BvybbsXv.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-D6dTxdUd.js → AchievementsSection-ChNzTFSR.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-C0UrWb9m.js → BadgeDetailModal-DhI9Ejz3.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-CgpBB5FZ.js → BotAnimated-Bch0Yxej.js} +1 -1
  11. package/dashboard/dist/assets/{Card-CuwBiIYz.js → Card-DWdL7d1T.js} +1 -1
  12. package/dashboard/dist/assets/{CloudCheckoutPage-BNCnBR0h.js → CloudCheckoutPage-BjGoOz6P.js} +1 -1
  13. package/dashboard/dist/assets/{CloudPage-CuYXUQnv.js → CloudPage-Btb6a97x.js} +2 -2
  14. package/dashboard/dist/assets/{CommandPalette-CV6a_7yK.js → CommandPalette-DwP3k7LE.js} +1 -1
  15. package/dashboard/dist/assets/{CommunityStatsModal-Dw-64Tig.js → CommunityStatsModal-mzqLxpwE.js} +1 -1
  16. package/dashboard/dist/assets/{ConfirmModal-CoFO1Nas.js → ConfirmModal-Dm76q4EX.js} +1 -1
  17. package/dashboard/dist/assets/{Controls-DB9eQ_rg.js → Controls-ClZjOBoD.js} +1 -1
  18. package/dashboard/dist/assets/DashboardPage-Doc8f4NL.js +1 -0
  19. package/dashboard/dist/assets/{DateRangePopover-DgZEnn4X.js → DateRangePopover-vfTUePPg.js} +1 -1
  20. package/dashboard/dist/assets/DevicePage-Jjg5q0C2.js +1 -0
  21. package/dashboard/dist/assets/{DialogClose-BR9QrB34.js → DialogClose-Ci5mPZ6X.js} +1 -1
  22. package/dashboard/dist/assets/DialogDescription-Di5ev5K6.js +1 -0
  23. package/dashboard/dist/assets/DialogTitle-DI21oZxH.js +1 -0
  24. package/dashboard/dist/assets/{ElectricLogo-nrlBy-l9.js → ElectricLogo-DfeZBwPZ.js} +1 -1
  25. package/dashboard/dist/assets/{FadeIn-CyJJ3l_P.js → FadeIn-C7SIILTc.js} +1 -1
  26. package/dashboard/dist/assets/{HeaderGithubStar-C7BX7loS.js → HeaderGithubStar-RStLbadn.js} +1 -1
  27. package/dashboard/dist/assets/{HoverTooltip-KGluh6sO.js → HoverTooltip-DI7uAp1_.js} +1 -1
  28. package/dashboard/dist/assets/{IpCheckPage-Ci2ybTDg.js → IpCheckPage-DTZNTJVh.js} +1 -1
  29. package/dashboard/dist/assets/{LandingPage-C9N17IpV.js → LandingPage-Df5scNlw.js} +2 -2
  30. package/dashboard/dist/assets/{LeaderboardPage-Cco7iFen.js → LeaderboardPage-Ck3SqtnI.js} +2 -2
  31. package/dashboard/dist/assets/{LeaderboardProBadge-tmm5NN8p.js → LeaderboardProBadge-CGx6m7R6.js} +1 -1
  32. package/dashboard/dist/assets/{LeaderboardProfileModal-WhjsvZ9Q.js → LeaderboardProfileModal-DuP7rgfv.js} +3 -3
  33. package/dashboard/dist/assets/{LeaderboardProfilePage-Ba3oVu5z.js → LeaderboardProfilePage-B5qUJkZX.js} +1 -1
  34. package/dashboard/dist/assets/{LimitsPage-CUa46XJy.js → LimitsPage-Bv_Lcvcz.js} +1 -1
  35. package/dashboard/dist/assets/{LocalOnlyNotice-BSewrWlY.js → LocalOnlyNotice-Cg4y02JM.js} +1 -1
  36. package/dashboard/dist/assets/{LoginCard-CUWn0Tz4.js → LoginCard-Wz0uJKXx.js} +1 -1
  37. package/dashboard/dist/assets/{LoginModal-UKk3xCsV.js → LoginModal-C13v_Gs0.js} +1 -1
  38. package/dashboard/dist/assets/{LoginPage-BhxiWgc5.js → LoginPage-COH9xXtd.js} +1 -1
  39. package/dashboard/dist/assets/{PetPage-BRyxC-Iu.js → PetPage-BWCnURub.js} +2 -2
  40. package/dashboard/dist/assets/PopoverPopup-Dw811N-_.js +1 -0
  41. package/dashboard/dist/assets/{ProviderIcon-htNu1RTN.js → ProviderIcon-CHxhYzK-.js} +1 -1
  42. package/dashboard/dist/assets/{ResetPasswordPage-DawMNiM1.js → ResetPasswordPage-Ll6jshqg.js} +1 -1
  43. package/dashboard/dist/assets/Select-BbooQ3sa.js +1 -0
  44. package/dashboard/dist/assets/SelectItemText-DBiW70wi.js +1 -0
  45. package/dashboard/dist/assets/{SelfHostPage-BMCMrt5I.js → SelfHostPage-BMHJxz5q.js} +1 -1
  46. package/dashboard/dist/assets/{ServiceStatusPage-e_7xC0RY.js → ServiceStatusPage-D6LH0ac-.js} +1 -1
  47. package/dashboard/dist/assets/{SessionsPage-Xr2FJa6E.js → SessionsPage-Mrer6v17.js} +2 -2
  48. package/dashboard/dist/assets/SettingsPage-D_7P_y_l.js +5 -0
  49. package/dashboard/dist/assets/SkillsPage-Ka7xm-1v.js +1 -0
  50. package/dashboard/dist/assets/{TokenGalaxy--6ijNK6P.js → TokenGalaxy-BEURDPFq.js} +1 -1
  51. package/dashboard/dist/assets/TooltipPopup-ZiuFFARC.js +1 -0
  52. package/dashboard/dist/assets/{TrendMonitor-DjIZyzxp.js → TrendMonitor-H73lyGEf.js} +1 -1
  53. package/dashboard/dist/assets/{WidgetsPage-CzCqGbb4.js → WidgetsPage-_7iCvyTk.js} +1 -1
  54. package/dashboard/dist/assets/{WrappedPage-B0RMtpvr.js → WrappedPage-DEXNb6-k.js} +1 -1
  55. package/dashboard/dist/assets/__vite-browser-external-BkNP5Ecj.js +45 -0
  56. package/dashboard/dist/assets/{agent-logos-CSlyeabY.js → agent-logos-OVBc_0cF.js} +1 -1
  57. package/dashboard/dist/assets/{arrow-left-DaqMiXTO.js → arrow-left-DDphsclm.js} +1 -1
  58. package/dashboard/dist/assets/{arrow-right-B1p9R8vH.js → arrow-right-B1aNq0Ki.js} +1 -1
  59. package/dashboard/dist/assets/{arrow-up-right-CK_9HuEh.js → arrow-up-right-yTM9azz_.js} +1 -1
  60. package/dashboard/dist/assets/{calendar-days-BVyCx4dJ.js → calendar-days-D22ithNF.js} +1 -1
  61. package/dashboard/dist/assets/{check-DIfCVsPJ.js → check-Dk7tknvE.js} +1 -1
  62. package/dashboard/dist/assets/{chevron-down-DrWxRBXX.js → chevron-down-DdyEzQ4f.js} +1 -1
  63. package/dashboard/dist/assets/{cloud-checkout-DOxBG7oS.js → cloud-checkout-BJjbipnH.js} +1 -1
  64. package/dashboard/dist/assets/composite-DEqP00XV.js +1 -0
  65. package/dashboard/dist/assets/{download-CEXAmejX.js → download-C9ixNyzn.js} +1 -1
  66. package/dashboard/dist/assets/{external-link-tDBZy6QP.js → external-link-DL4HSsLw.js} +1 -1
  67. package/dashboard/dist/assets/{flame-86y9Xf0g.js → flame-DqNH65nB.js} +1 -1
  68. package/dashboard/dist/assets/{format-tokens-BNN9k5mV.js → format-tokens-CyVjKz4I.js} +1 -1
  69. package/dashboard/dist/assets/{gift-BEOvq2zc.js → gift-ChJQQe_G.js} +1 -1
  70. package/dashboard/dist/assets/{host-mode-UjbNKxFB.js → host-mode-DpUDpoOz.js} +1 -1
  71. package/dashboard/dist/assets/{icons-DJmZD0si.js → icons-CJiHeOaU.js} +1 -1
  72. package/dashboard/dist/assets/{index-BwUwnqE0.js → index-72LQsAaf.js} +1 -1
  73. package/dashboard/dist/assets/{index-B8kizz-s.js → index-BucEg3fu.js} +1 -1
  74. package/dashboard/dist/assets/{index-Ci3CZ-2g.js → index-CrFlTSra.js} +1 -1
  75. package/dashboard/dist/assets/{info-ynVg8RzK.js → info-DReYqxC7.js} +1 -1
  76. package/dashboard/dist/assets/{link-2-B9SLPcBg.js → link-2-kIZbhK2S.js} +1 -1
  77. package/dashboard/dist/assets/{loader-circle-FTE9k7Sk.js → loader-circle-1An-X85y.js} +1 -1
  78. package/dashboard/dist/assets/{modifiers.esm-B4babCm_.js → modifiers.esm-FoVheEKT.js} +1 -1
  79. package/dashboard/dist/assets/module-DOCYJaxl.js +3 -0
  80. package/dashboard/dist/assets/{monitor-smartphone-CNzgbo-M.js → monitor-smartphone-Ch6lr_P2.js} +1 -1
  81. package/dashboard/dist/assets/{play-9W4Ua21o.js → play-CbJ6ZF1K.js} +1 -1
  82. package/dashboard/dist/assets/{plus-BA85lApI.js → plus-C-5ID3a9.js} +1 -1
  83. package/dashboard/dist/assets/{provider-display-D1ETarsz.js → provider-display-BFzRtoL2.js} +2 -2
  84. package/dashboard/dist/assets/{react-VsHxeqq7.js → react-D4CR_fLQ.js} +1 -1
  85. package/dashboard/dist/assets/safePolygon-DP21Ibqj.js +1 -0
  86. package/dashboard/dist/assets/{search-Svi--7CN.js → search-CaznkY7U.js} +1 -1
  87. package/dashboard/dist/assets/{server-CgHdX6TE.js → server-CUOmC6xZ.js} +1 -1
  88. package/dashboard/dist/assets/{shield-check-Cjv-T4aT.js → shield-check-Dr2KBMmA.js} +1 -1
  89. package/dashboard/dist/assets/{skills-api-Cn8hEYL4.js → skills-api-BAz_fblS.js} +1 -1
  90. package/dashboard/dist/assets/{sortable.esm-g3UcmvFU.js → sortable.esm-knEfm5hE.js} +1 -1
  91. package/dashboard/dist/assets/store-CL97jcij.js +1 -0
  92. package/dashboard/dist/assets/{terminal-o0jvDfI0.js → terminal-EjFyreWj.js} +1 -1
  93. package/dashboard/dist/assets/{trash-0cqpTyN6.js → trash-ByqaZXkQ.js} +1 -1
  94. package/dashboard/dist/assets/{use-community-stats-BFgORaJS.js → use-community-stats-Bs4RDJZE.js} +1 -1
  95. package/dashboard/dist/assets/{use-latest-request-guard-9eZP_jA5.js → use-latest-request-guard-CG3Ir1cP.js} +1 -1
  96. package/dashboard/dist/assets/{use-limits-display-prefs-BjO3hsmw.js → use-limits-display-prefs-tACgCxeR.js} +1 -1
  97. package/dashboard/dist/assets/{use-native-settings-YKn-htVo.js → use-native-settings-CWjXyJXi.js} +1 -1
  98. package/dashboard/dist/assets/{use-ordered-list-D5h4UwhF.js → use-ordered-list-DsuC9JQa.js} +1 -1
  99. package/dashboard/dist/assets/use-reduced-motion-CwFguzkm.js +1 -0
  100. package/dashboard/dist/assets/{use-session-efficiency-pref-BShLuIbk.js → use-session-efficiency-pref-NeoJE8BL.js} +1 -1
  101. package/dashboard/dist/assets/{use-transform-DVZHJPVD.js → use-transform-BWGKOsya.js} +1 -1
  102. package/dashboard/dist/assets/{use-usage-limits-BhJbW76K.js → use-usage-limits-CFWFNwHS.js} +1 -1
  103. package/dashboard/dist/assets/{useCurrency-BmEbDVaa.js → useCurrency-B_C4_P5x.js} +1 -1
  104. package/dashboard/dist/assets/usePositioner-B0L5_o38.js +1 -0
  105. package/dashboard/dist/assets/{useTokenFormat-CBlBP2aA.js → useTokenFormat-DH1GWHSU.js} +1 -1
  106. package/dashboard/dist/assets/{zap-cML2-Nx4.js → zap-Chv1p7pS.js} +1 -1
  107. package/dashboard/dist/index.html +13 -11
  108. package/dashboard/dist/ip-check.html +1 -1
  109. package/dashboard/dist/leaderboard.html +1 -1
  110. package/dashboard/dist/llms.txt +3 -2
  111. package/dashboard/dist/share.html +1 -1
  112. package/package.json +3 -3
  113. package/src/commands/doctor.js +15 -1
  114. package/src/commands/init.js +9 -0
  115. package/src/commands/status.js +21 -3
  116. package/src/commands/sync.js +59 -4
  117. package/src/lib/cloud-device-token.js +77 -10
  118. package/src/lib/cloud-usage-reset.js +115 -0
  119. package/src/lib/diagnostics.js +5 -2
  120. package/src/lib/kimi-model-repair.js +173 -0
  121. package/src/lib/kimi-session.js +216 -0
  122. package/src/lib/local-api.js +46 -1
  123. package/src/lib/pricing/index.js +2 -1
  124. package/src/lib/pricing/seed-snapshot.json +1 -1
  125. package/src/lib/qoder-cn-cli-limits.js +9 -7
  126. package/src/lib/qoder-limits.js +45 -15
  127. package/src/lib/rollout.js +408 -4
  128. package/src/lib/session-analytics.js +47 -9
  129. package/dashboard/dist/assets/DashboardPage-sZKijU6h.js +0 -1
  130. package/dashboard/dist/assets/DevicePage-YCnJnsmz.js +0 -1
  131. package/dashboard/dist/assets/DialogDescription-Cx1R3WT7.js +0 -1
  132. package/dashboard/dist/assets/DialogTitle-ck8f1qeP.js +0 -1
  133. package/dashboard/dist/assets/PopoverPopup-D-Tpl294.js +0 -1
  134. package/dashboard/dist/assets/Select-6WChWtqK.js +0 -1
  135. package/dashboard/dist/assets/SelectItemText-DRmxMwo5.js +0 -1
  136. package/dashboard/dist/assets/SettingsPage-Bxq2Mhxz.js +0 -5
  137. package/dashboard/dist/assets/SkillsPage-CEIBW63c.js +0 -1
  138. package/dashboard/dist/assets/TooltipPopup--TLYUg0c.js +0 -1
  139. package/dashboard/dist/assets/__vite-browser-external-BsOjTB8Q.js +0 -45
  140. package/dashboard/dist/assets/composite-W1R1XwoE.js +0 -1
  141. package/dashboard/dist/assets/module-BNnaKM__.js +0 -3
  142. package/dashboard/dist/assets/safePolygon-CCDOsx-z.js +0 -1
  143. package/dashboard/dist/assets/store-D2gn9bSV.js +0 -1
  144. package/dashboard/dist/assets/use-reduced-motion-C83gmg0F.js +0 -1
  145. package/dashboard/dist/assets/usePositioner-nY0YNa-6.js +0 -1
@@ -61,15 +61,16 @@ async function cnRequest(url, options, fetchImpl, { tokenExchange = false } = {}
61
61
  function normalizeQoderCnCliUsage(response, normalizeRpcUsage) {
62
62
  const usage = response?.displayMode === "qoder" ? response.qoderUsage : response;
63
63
  const rawQuota = usage?.userQuota ?? usage?.user_quota;
64
- if (!rawQuota || typeof rawQuota !== "object") {
64
+ const addOnQuota = usage?.addOnQuota ?? usage?.add_on_quota;
65
+ if ((!rawQuota || typeof rawQuota !== "object") && !addOnQuota) {
65
66
  throw new Error("Qoder CN usage response is missing the plan quota.");
66
67
  }
67
- const total = Number(rawQuota.total);
68
- const used = Number(rawQuota.used);
69
- const remaining = rawQuota.remaining == null ? Math.max(0, total - used) : Number(rawQuota.remaining);
70
- if (rawQuota.total == null || rawQuota.used == null
68
+ const total = Number(rawQuota?.total);
69
+ const used = Number(rawQuota?.used);
70
+ const remaining = rawQuota?.remaining == null ? Math.max(0, total - used) : Number(rawQuota.remaining);
71
+ if (rawQuota && (rawQuota.total == null || rawQuota.used == null
71
72
  || ![total, used, remaining].every(Number.isFinite) || Math.min(total, used, remaining) < 0
72
- || (total === 0 && (used !== 0 || remaining !== 0))) {
73
+ || (total === 0 && (used !== 0 || remaining !== 0)))) {
73
74
  throw new Error("Qoder CN usage response has invalid quota values.");
74
75
  }
75
76
  // The OpenAPI returns fractions; the old desktop IPC already returns percents.
@@ -77,7 +78,8 @@ function normalizeQoderCnCliUsage(response, normalizeRpcUsage) {
77
78
  userType: usage.userType ?? usage.user_type,
78
79
  expiresAt: usage.expiresAt ?? usage.expires_at,
79
80
  isQuotaExceeded: usage.isQuotaExceeded ?? usage.is_quota_exceeded,
80
- userQuota: { used, total, remaining, unit: rawQuota.unit },
81
+ userQuota: rawQuota ? { used, total, remaining, unit: rawQuota.unit } : null,
82
+ addOnQuota,
81
83
  });
82
84
  result.source = "cli-api";
83
85
  return result;
@@ -240,21 +240,47 @@ function qoderRpcRequest(method, params = {}, {
240
240
  });
241
241
  }
242
242
 
243
+ function normalizeQoderAddOnQuota(quota) {
244
+ if (quota == null) return null;
245
+ const used = Number(quota.used);
246
+ const total = Number(quota.total);
247
+ const remaining = quota.remaining == null ? Math.max(0, total - used) : Number(quota.remaining);
248
+ if (quota.used == null || quota.total == null
249
+ || ![used, total, remaining].every(Number.isFinite) || Math.min(used, total, remaining) < 0
250
+ || (total === 0 && (used !== 0 || remaining !== 0))) {
251
+ throw new Error("Qoder add-on quota values are invalid.");
252
+ }
253
+ return {
254
+ used_percent: total > 0 ? Math.min(100, (used / total) * 100) : 0,
255
+ // The official addOnQuota has no renewal date. Plan expiry does not apply.
256
+ reset_at: null,
257
+ used_credits: used,
258
+ limit_credits: total,
259
+ remaining_credits: remaining,
260
+ unit: typeof quota.unit === "string" && quota.unit.trim() ? quota.unit.trim() : "credits",
261
+ };
262
+ }
263
+
243
264
  function normalizeQoderRpcUsage(response) {
244
265
  if (!response || typeof response !== "object") return null;
245
266
  const quota = response.userQuota;
246
- if (!quota || typeof quota !== "object") return null;
247
- const used = finiteNumber(quota.used, "userQuota.used");
248
- const total = finiteNumber(quota.total, "userQuota.total");
249
- const remaining = finiteNumber(quota.remaining, "userQuota.remaining");
250
- const reportedPercent = Number(response.totalUsagePercentage ?? quota.percentage);
267
+ // Qoder CN CLI 1.1.71 renders userQuota and addOnQuota as separate buckets.
268
+ // Only its account-level total_remaining adds them together.
269
+ const addOnWindow = normalizeQoderAddOnQuota(response.addOnQuota);
270
+ if ((!quota || typeof quota !== "object") && !addOnWindow) return null;
271
+ const used = quota ? finiteNumber(quota.used, "userQuota.used") : 0;
272
+ const total = quota ? finiteNumber(quota.total, "userQuota.total") : 0;
273
+ const remaining = quota ? finiteNumber(quota.remaining, "userQuota.remaining") : 0;
274
+ const reportedPercent = Number(response.totalUsagePercentage ?? quota?.percentage);
251
275
  const usedPercent = total === 0
252
276
  ? 0
253
- : response.isQuotaExceeded === true
254
- ? 100
255
- : Number.isFinite(reportedPercent)
256
- ? reportedPercent
257
- : (used / total) * 100;
277
+ : addOnWindow
278
+ ? (used / total) * 100
279
+ : response.isQuotaExceeded === true
280
+ ? 100
281
+ : Number.isFinite(reportedPercent)
282
+ ? reportedPercent
283
+ : (used / total) * 100;
258
284
  const normalizedExpiry = normalizeResetAt(response.expiresAt);
259
285
  const expiryMs = normalizedExpiry ? Date.parse(normalizedExpiry) : NaN;
260
286
  return {
@@ -262,7 +288,7 @@ function normalizeQoderRpcUsage(response) {
262
288
  error: null,
263
289
  plan_label: typeof response.userType === "string" ? response.userType : null,
264
290
  quota_exceeded: response.isQuotaExceeded === true,
265
- primary_window: {
291
+ primary_window: quota ? {
266
292
  used_percent: Math.max(0, Math.min(100, usedPercent)),
267
293
  // Qoder uses 9999-12-31 as a no-expiry sentinel for Free accounts.
268
294
  reset_at: Number.isFinite(expiryMs) && expiryMs < Date.UTC(2100, 0, 1)
@@ -272,7 +298,8 @@ function normalizeQoderRpcUsage(response) {
272
298
  limit_credits: total,
273
299
  remaining_credits: remaining,
274
300
  unit: typeof quota.unit === "string" && quota.unit.trim() ? quota.unit.trim() : "credits",
275
- },
301
+ } : null,
302
+ ...(addOnWindow ? { tertiary_window: addOnWindow } : {}),
276
303
  source: "local-ipc",
277
304
  };
278
305
  }
@@ -389,8 +416,9 @@ function readQoderLimitsCache({
389
416
  if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
390
417
  const primaryWindow = qoderCachedWindow(raw.primary_window, { cachedAtMs, nowMs });
391
418
  const secondaryWindow = qoderCachedWindow(raw.secondary_window, { cachedAtMs, nowMs });
392
- if (!primaryWindow && !secondaryWindow) return null;
393
- const hasUnexpiredDatedWindow = [primaryWindow, secondaryWindow]
419
+ const tertiaryWindow = qoderCachedWindow(raw.tertiary_window, { cachedAtMs, nowMs });
420
+ if (!primaryWindow && !secondaryWindow && !tertiaryWindow) return null;
421
+ const hasUnexpiredDatedWindow = [primaryWindow, secondaryWindow, tertiaryWindow]
394
422
  .some((window) => Number.isFinite(Date.parse(window?.reset_at || "")));
395
423
  if (nowMs - cachedAtMs > QODER_LIMITS_CACHE_MAX_AGE_MS && !hasUnexpiredDatedWindow) {
396
424
  return null;
@@ -402,6 +430,7 @@ function readQoderLimitsCache({
402
430
  quota_exceeded: raw.quota_exceeded === true,
403
431
  primary_window: primaryWindow,
404
432
  secondary_window: secondaryWindow,
433
+ ...(tertiaryWindow ? { tertiary_window: tertiaryWindow } : {}),
405
434
  cached: true,
406
435
  stale: true,
407
436
  cached_at: raw.cached_at,
@@ -417,7 +446,7 @@ function writeQoderLimitsCache(limits, {
417
446
  nowMs = Date.now(),
418
447
  namespace,
419
448
  } = {}) {
420
- if (!limits?.configured || limits.error || (!limits.primary_window && !limits.secondary_window)) return;
449
+ if (!limits?.configured || limits.error || (!limits.primary_window && !limits.secondary_window && !limits.tertiary_window)) return;
421
450
  const cachePath = qoderLimitsCachePath({ home, namespace });
422
451
  const payload = {
423
452
  qoder: {
@@ -425,6 +454,7 @@ function writeQoderLimitsCache(limits, {
425
454
  quota_exceeded: limits.quota_exceeded === true,
426
455
  primary_window: limits.primary_window || null,
427
456
  secondary_window: limits.secondary_window || null,
457
+ ...(limits.tertiary_window ? { tertiary_window: limits.tertiary_window } : {}),
428
458
  cached_at: new Date(nowMs).toISOString(),
429
459
  },
430
460
  };
@@ -9750,8 +9750,8 @@ async function parseKimiCodeIncremental({ wireFiles, cursors, queuePath, onProgr
9750
9750
  const prevIno = prevEntry.ino;
9751
9751
  const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
9752
9752
  const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
9753
- // Model is declared in a `config.update` near the file head; persist it on
9754
- // the cursor so incremental resumes (which start past that line) keep it.
9753
+ // Persist the latest config/profile/request model so an incremental read
9754
+ // starting after that declaration keeps the same attribution.
9755
9755
  let fileModel = (typeof prevEntry.model === "string" && prevEntry.model) || fallbackModel;
9756
9756
  if (stat.size <= startOffset) continue;
9757
9757
 
@@ -9766,8 +9766,10 @@ async function parseKimiCodeIncremental({ wireFiles, cursors, queuePath, onProgr
9766
9766
  let entry;
9767
9767
  try { entry = JSON.parse(line); } catch { continue; }
9768
9768
 
9769
- if (entry.type === "config.update") {
9770
- const alias = kimiCodeModelAlias(entry.modelAlias);
9769
+ // Kimi Code 2.x can bind or reroute the model without config.update.
9770
+ // These records precede step.end and carry the model actually requested.
9771
+ if (["config.update", "profile.bind", "llm.request"].includes(entry.type)) {
9772
+ const alias = kimiCodeModelAlias(entry.modelAlias || entry.model);
9771
9773
  if (alias) fileModel = alias;
9772
9774
  continue;
9773
9775
  }
@@ -17077,6 +17079,405 @@ async function parseReasonixIncremental({ telemetryFiles, cursors, queuePath, on
17077
17079
  return { recordsProcessed, eventsAggregated, bucketsQueued };
17078
17080
  }
17079
17081
 
17082
+ // ─────────────────────────────────────────────────────────────────────────────
17083
+ // Muse Code (Meta) — passive session.jsonl reader.
17084
+ //
17085
+ // ~/.local/share/muse/sessions/YYYY/MM/DD/<session>/session.jsonl
17086
+ // ~/.local/share/muse/sessions/YYYY/MM/DD/<session>/subagent/<id>/session.jsonl
17087
+ //
17088
+ // Every record is `{id, stream, sequence, recorded_at (epoch µs), payload_type,
17089
+ // payload}`. Per-model-step usage rides on a `runtime.session` record whose
17090
+ // `payload.event.kind` is `goal_usage_attribution`:
17091
+ //
17092
+ // quantity: {unit:"tokens", reported:true, input_tokens, output_tokens,
17093
+ // cached_tokens, reasoning_tokens, main_llm_steps}
17094
+ //
17095
+ // Only `usage_family:"provider"` + `reported:true` rows are model usage (tool and
17096
+ // reminder rows are zero placeholders). The usage row names no model: the model
17097
+ // comes from the `run.model.configured` record of the same `run_id`.
17098
+ //
17099
+ // Token semantics (verified against the session snapshot `cost_ledger`, where
17100
+ // `input_tokens` equals the sum of these rows and `cache_read_tokens` equals
17101
+ // `cached_tokens`): `input_tokens` INCLUDES cache reads, and `reasoning_tokens`
17102
+ // is a subset of `output_tokens`. Subagent logs are separate streams whose usage
17103
+ // is not repeated in the parent log, so each file is counted once. No prompt,
17104
+ // response or path text is read or stored; only counts, timestamps and the
17105
+ // model id.
17106
+ function resolveMuseHome(env = process.env) {
17107
+ if (env.TOKENTRACKER_MUSE_HOME) return expandHomePath(env.TOKENTRACKER_MUSE_HOME, env);
17108
+ return path.join(env.HOME || require("node:os").homedir(), ".local", "share", "muse");
17109
+ }
17110
+
17111
+ const MUSE_SCAN_MAX_DEPTH = 8;
17112
+ const MUSE_READ_CHUNK_BYTES = 8 * 1024 * 1024;
17113
+ const MUSE_WATERMARK_IDS_MAX = 100;
17114
+
17115
+ function collectMuseSessionFiles(dir, files, depth = 0) {
17116
+ if (depth > MUSE_SCAN_MAX_DEPTH) return;
17117
+ let entries;
17118
+ try { entries = fssync.readdirSync(dir, { withFileTypes: true }); } catch { return; }
17119
+ for (const entry of entries) {
17120
+ // Dot directories (.msp-view-v1) hold derived views, not the session log.
17121
+ if (entry.name.startsWith(".")) continue;
17122
+ const full = path.join(dir, entry.name);
17123
+ if (entry.isDirectory()) collectMuseSessionFiles(full, files, depth + 1);
17124
+ else if (entry.isFile() && entry.name === "session.jsonl") files.push(full);
17125
+ }
17126
+ }
17127
+
17128
+ function resolveMuseSessionFiles(env = process.env) {
17129
+ const sessionsDir = path.join(resolveMuseHome(env), "sessions");
17130
+ const files = [];
17131
+ collectMuseSessionFiles(sessionsDir, files);
17132
+ return files;
17133
+ }
17134
+
17135
+ // The model Muse is configured to use, for rows whose run has no model record
17136
+ // in the portion of the log we read. Reads only the `model` string.
17137
+ function resolveMuseDefaultModel(env = process.env) {
17138
+ const fallback = "muse";
17139
+ try {
17140
+ const configHome = env.XDG_CONFIG_HOME || path.join(env.HOME || require("node:os").homedir(), ".config");
17141
+ const settings = JSON.parse(fssync.readFileSync(path.join(configHome, "muse", "settings.json"), "utf8"));
17142
+ return typeof settings?.model === "string" && settings.model.trim() ? settings.model.trim() : fallback;
17143
+ } catch {
17144
+ return fallback;
17145
+ }
17146
+ }
17147
+
17148
+ // A permission transaction wraps its records as JSON strings under `children`;
17149
+ // unwrap them so a model or usage record is never missed inside a frame.
17150
+ function* museRecords(entry) {
17151
+ if (entry && Array.isArray(entry.children) && entry.retained_frame) {
17152
+ for (const child of entry.children) {
17153
+ if (!child || typeof child.record_json !== "string") continue;
17154
+ try { yield JSON.parse(child.record_json); } catch { /* skip */ }
17155
+ }
17156
+ return;
17157
+ }
17158
+ yield entry;
17159
+ }
17160
+
17161
+
17162
+ const MUSE_HEAD_FINGERPRINT_BYTES = 256;
17163
+ const MUSE_PREFIX_FINGERPRINT_MAX_BYTES = 8 * 1024 * 1024;
17164
+
17165
+ // Fingerprints for Muse session logs so an in-place rewrite is not mistaken
17166
+ // for an append. Prefer a hash of the entire consumed prefix (capped); fall
17167
+ // back to head+tail samples only when the prefix exceeds the cap.
17168
+ function museRangeFingerprint(fd, offset, length) {
17169
+ const n = Math.max(0, Number(length) || 0);
17170
+ if (n <= 0) return null;
17171
+ const buf = Buffer.alloc(n);
17172
+ const read = fssync.readSync(fd, buf, 0, n, Math.max(0, Number(offset) || 0));
17173
+ if (read <= 0) return null;
17174
+ return crypto.createHash("sha256").update(buf.subarray(0, read)).digest("hex");
17175
+ }
17176
+
17177
+ function museHeadFingerprint(fd, size) {
17178
+ return museRangeFingerprint(fd, 0, Math.min(MUSE_HEAD_FINGERPRINT_BYTES, Math.max(0, Number(size) || 0)));
17179
+ }
17180
+
17181
+ function musePrefixFingerprint(fd, size) {
17182
+ const total = Math.max(0, Number(size) || 0);
17183
+ if (total <= 0) return null;
17184
+ if (total <= MUSE_PREFIX_FINGERPRINT_MAX_BYTES) {
17185
+ const hash = crypto.createHash("sha256");
17186
+ let offset = 0;
17187
+ const chunk = Buffer.alloc(Math.min(64 * 1024, total));
17188
+ while (offset < total) {
17189
+ const want = Math.min(chunk.length, total - offset);
17190
+ const read = fssync.readSync(fd, chunk, 0, want, offset);
17191
+ if (read <= 0) break;
17192
+ hash.update(chunk.subarray(0, read));
17193
+ offset += read;
17194
+ }
17195
+ return offset === total ? hash.digest("hex") : null;
17196
+ }
17197
+ // Large logs: sample head and tail of the consumed prefix.
17198
+ const head = museHeadFingerprint(fd, total);
17199
+ const tailLen = Math.min(MUSE_HEAD_FINGERPRINT_BYTES, total);
17200
+ const tail = museRangeFingerprint(fd, total - tailLen, tailLen);
17201
+ return head && tail ? `${head}:${tail}` : null;
17202
+ }
17203
+
17204
+ function museResumeLooksAppendOnly(fd, prevSize, prev, currentPrefixFp, currentMtimeMs, currentSize) {
17205
+ if (prev?.forceRewind === true) return false;
17206
+ if (!(prevSize > 0)) return true;
17207
+ if (typeof prev?.prefixFp === "string" && prev.prefixFp && prev.prefixFp !== currentPrefixFp) return false;
17208
+ // Legacy cursors without prefixFp: reject when the head sample diverges.
17209
+ if (!prev?.prefixFp && typeof prev?.headFp === "string" && prev.headFp) {
17210
+ const head = museHeadFingerprint(fd, prevSize);
17211
+ if (head && head !== prev.headFp) return false;
17212
+ }
17213
+ // Equal-size rewrite above the full-prefix cap (or any equal-size rewrite that
17214
+ // preserves sampled bytes) still bumps mtime on real filesystems. Only apply
17215
+ // when the size is unchanged — growth from an append also changes mtime.
17216
+ if (
17217
+ Number(currentSize) === Number(prevSize) &&
17218
+ Number.isFinite(Number(prev?.mtimeMs)) &&
17219
+ Number.isFinite(Number(currentMtimeMs)) &&
17220
+ Number(prev.mtimeMs) !== Number(currentMtimeMs)
17221
+ ) {
17222
+ return false;
17223
+ }
17224
+ // Resume offset must be a JSONL record boundary.
17225
+ const buf = Buffer.alloc(1);
17226
+ const read = fssync.readSync(fd, buf, 0, 1, prevSize - 1);
17227
+ return read === 1 && buf[0] === 0x0a;
17228
+ }
17229
+
17230
+
17231
+ // Subagent logs live under .../sessions/<session>/subagent/... . Matching any
17232
+ // ancestor named "subagent" would misclassify roots when the Muse home path
17233
+ // itself contains that segment (e.g. TOKENTRACKER_MUSE_HOME=/data/subagent/muse).
17234
+ function isMuseSubagentSessionFile(filePath, env = process.env) {
17235
+ const sessionsRoot = path.join(resolveMuseHome(env), "sessions");
17236
+ const relative = path.relative(sessionsRoot, filePath);
17237
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
17238
+ // Outside the configured sessions root — fall back to a local segment check
17239
+ // that still requires the subagent directory to be a parent of session.jsonl.
17240
+ const parts = filePath.split(path.sep);
17241
+ const sessionIdx = parts.lastIndexOf("session.jsonl");
17242
+ return sessionIdx > 0 && parts.slice(0, sessionIdx).includes("subagent");
17243
+ }
17244
+ return relative.split(path.sep).includes("subagent");
17245
+ }
17246
+
17247
+ async function parseMuseIncremental({ sessionFiles, cursors, queuePath, onProgress, env } = {}) {
17248
+ await ensureDir(path.dirname(queuePath));
17249
+ const state = cursors.muse && typeof cursors.muse === "object" ? cursors.muse : {};
17250
+ const seenIds = new Set(Array.isArray(state.seenIds) ? state.seenIds : []);
17251
+ const fileOffsets =
17252
+ state.fileOffsets && typeof state.fileOffsets === "object" ? { ...state.fileOffsets } : {};
17253
+ const files = Array.isArray(sessionFiles) ? sessionFiles : resolveMuseSessionFiles(env || process.env);
17254
+ const defaultModel = resolveMuseDefaultModel(env || process.env);
17255
+
17256
+ const hourlyState = normalizeHourlyState(cursors?.hourly);
17257
+ const touchedBuckets = new Set();
17258
+ const cb = typeof onProgress === "function" ? onProgress : null;
17259
+ let recordsProcessed = 0;
17260
+ let eventsAggregated = 0;
17261
+
17262
+ const handleRecord = (rec, fileState) => {
17263
+ if (!rec || typeof rec !== "object") return;
17264
+ const payload = rec.payload;
17265
+ if (!payload || typeof payload !== "object") return;
17266
+ if (rec.payload_type === "run.model.configured") {
17267
+ const modelRec = payload.record;
17268
+ const runId = modelRec?.run_stream?.id;
17269
+ if (typeof modelRec?.model_id === "string" && modelRec.model_id) {
17270
+ if (runId) fileState.runModels[runId] = modelRec.model_id;
17271
+ fileState.model = modelRec.model_id;
17272
+ }
17273
+ return;
17274
+ }
17275
+ if (rec.payload_type !== "runtime.session") return;
17276
+ const event = payload.event;
17277
+ if (!event || event.kind !== "goal_usage_attribution") return;
17278
+ const usage = event.record;
17279
+ const quantity = usage?.quantity;
17280
+ if (!quantity || usage.usage_family !== "provider" || quantity.reported !== true) return;
17281
+ const usageId = typeof usage.usage_id === "string" ? usage.usage_id : null;
17282
+ if (!usageId || seenIds.has(usageId)) return;
17283
+ // A rewritten log is re-read from the start, and `seenIds` keeps only the
17284
+ // newest 10k ids. Rows older than the last one counted from this file are
17285
+ // already in the queue, so skip them even if their id was evicted. Rows at
17286
+ // exactly that timestamp are skipped only when their id was counted, so a
17287
+ // new row that shares the timestamp is kept.
17288
+ const micros = Number(rec.recorded_at);
17289
+ if (fileState.skipThrough && Number.isFinite(micros)) {
17290
+ if (micros < fileState.skipThrough) return;
17291
+ if (micros === fileState.skipThrough && fileState.skipIds.has(usageId)) return;
17292
+ }
17293
+
17294
+ recordsProcessed++;
17295
+ const inputTotal = toNonNegativeInt(quantity.input_tokens);
17296
+ const output = toNonNegativeInt(quantity.output_tokens);
17297
+ const cached = Math.min(toNonNegativeInt(quantity.cached_tokens), inputTotal);
17298
+ const reasoning = Math.min(toNonNegativeInt(quantity.reasoning_tokens), output);
17299
+ seenIds.add(usageId);
17300
+ if (inputTotal === 0 && output === 0) return;
17301
+
17302
+ if (!Number.isFinite(micros) || micros <= 0) return;
17303
+ const bucketStart = toUtcHalfHourStart(new Date(Math.floor(micros / 1000)).toISOString());
17304
+ if (!bucketStart) return;
17305
+
17306
+ const input = inputTotal - cached;
17307
+ const model = fileState.runModels[payload.run_id] || fileState.model || defaultModel;
17308
+ // One conversation per root session, charged to its first usage record.
17309
+ // Subagent logs are part of their parent's conversation.
17310
+ const conversations = fileState.counted || fileState.isSubagent ? 0 : 1;
17311
+ fileState.counted = true;
17312
+ const bucket = getHourlyBucket(hourlyState, "muse", model, bucketStart);
17313
+ addTotals(bucket.totals, {
17314
+ input_tokens: input,
17315
+ cached_input_tokens: cached,
17316
+ cache_creation_input_tokens: 0,
17317
+ output_tokens: output,
17318
+ reasoning_output_tokens: reasoning,
17319
+ total_tokens: input + cached + output,
17320
+ conversation_count: conversations,
17321
+ });
17322
+ touchedBuckets.add(bucketKey("muse", model, bucketStart));
17323
+ eventsAggregated++;
17324
+ // Advance only for rows that reached a bucket, remembering the ids that
17325
+ // share the newest timestamp.
17326
+ if (micros > fileState.watermark) {
17327
+ fileState.watermark = micros;
17328
+ fileState.watermarkIds = [usageId];
17329
+ } else if (micros === fileState.watermark && fileState.watermarkIds.length < MUSE_WATERMARK_IDS_MAX) {
17330
+ fileState.watermarkIds.push(usageId);
17331
+ }
17332
+ };
17333
+
17334
+ for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
17335
+ const filePath = files[fileIdx];
17336
+ // Open first and stat the descriptor, so the size and inode describe the
17337
+ // file that is actually read (no check-then-open race).
17338
+ let fd;
17339
+ try { fd = fssync.openSync(filePath, "r"); } catch { continue; }
17340
+ let stat;
17341
+ try { stat = fssync.fstatSync(fd); } catch {
17342
+ try { fssync.closeSync(fd); } catch { /* ignore */ }
17343
+ continue;
17344
+ }
17345
+
17346
+ const prev = fileOffsets[filePath] || {};
17347
+ const prevSize = Number(prev.size) || 0;
17348
+ const inodeChanged = typeof prev.ino === "number" && prev.ino !== stat.ino;
17349
+ const prefixFp = musePrefixFingerprint(fd, Math.min(prevSize, stat.size));
17350
+ const headFp = museHeadFingerprint(fd, stat.size);
17351
+ // Same-inode compaction can shrink then grow past prevSize, rewrite the
17352
+ // middle at equal size, or be observed while empty mid-rewrite. Validate
17353
+ // the consumed-prefix fingerprint (and forceRewind) before append-only.
17354
+ const appendOnly = !inodeChanged && stat.size >= prevSize &&
17355
+ museResumeLooksAppendOnly(fd, prevSize, prev, prefixFp, stat.mtimeMs, stat.size);
17356
+ const rewound = !appendOnly;
17357
+ const startOffset = rewound ? 0 : prevSize;
17358
+ if (stat.size <= startOffset) {
17359
+ const emptyObserved = stat.size <= 0 && prevSize > 0;
17360
+ fileOffsets[filePath] = {
17361
+ ...prev,
17362
+ size: Math.min(prevSize, stat.size),
17363
+ mtimeMs: stat.mtimeMs,
17364
+ ino: stat.ino,
17365
+ headFp,
17366
+ prefixFp: stat.size > 0 ? musePrefixFingerprint(fd, stat.size) : null,
17367
+ // An empty observation between truncate and rewrite must not clear
17368
+ // watermark-based replay protection on the next population.
17369
+ forceRewind: emptyObserved || prev.forceRewind === true || rewound,
17370
+ };
17371
+ try { fssync.closeSync(fd); } catch { /* ignore */ }
17372
+ continue;
17373
+ }
17374
+
17375
+ const fileState = {
17376
+ // Keep known model attribution across rewinds: compaction may drop the
17377
+ // original run.model.configured rows while later usage for those runs
17378
+ // remains. Replayed configuration records still overwrite these maps.
17379
+ model: prev.model || null,
17380
+ runModels: { ...(prev.runModels || {}) },
17381
+ // A rewritten log keeps its flag: its usage ids were already counted.
17382
+ counted: prev.counted === true,
17383
+ // Latest usage timestamp (epoch µs) counted from this file; on a rewrite
17384
+ // it becomes the point below which rows are not counted again.
17385
+ watermark: Number(prev.watermark) || 0,
17386
+ watermarkIds: Array.isArray(prev.watermarkIds) ? prev.watermarkIds.slice(0, MUSE_WATERMARK_IDS_MAX) : [],
17387
+ skipThrough: rewound ? Number(prev.watermark) || 0 : 0,
17388
+ skipIds: new Set(rewound && Array.isArray(prev.watermarkIds) ? prev.watermarkIds : []),
17389
+ isSubagent: isMuseSubagentSessionFile(filePath, env || process.env),
17390
+ };
17391
+
17392
+ let consumed = 0;
17393
+ // Hash the bytes we actually parse so a rewrite between read and cursor
17394
+ // save cannot invent a matching fingerprint for unread content.
17395
+ const consumedHash = crypto.createHash("sha256");
17396
+ let hashedPrefix = startOffset === 0;
17397
+ if (startOffset > 0 && startOffset <= MUSE_PREFIX_FINGERPRINT_MAX_BYTES) {
17398
+ // Include the already-trusted prefix so the saved fingerprint covers
17399
+ // the full consumed region, not only this sync's delta.
17400
+ let off = 0;
17401
+ const pre = Buffer.alloc(Math.min(64 * 1024, startOffset));
17402
+ while (off < startOffset) {
17403
+ const want = Math.min(pre.length, startOffset - off);
17404
+ const read = fssync.readSync(fd, pre, 0, want, off);
17405
+ if (read <= 0) break;
17406
+ consumedHash.update(pre.subarray(0, read));
17407
+ off += read;
17408
+ }
17409
+ hashedPrefix = off === startOffset;
17410
+ } else if (startOffset > MUSE_PREFIX_FINGERPRINT_MAX_BYTES) {
17411
+ hashedPrefix = false;
17412
+ }
17413
+ {
17414
+ let position = startOffset;
17415
+ let carry = Buffer.alloc(0);
17416
+ while (position < stat.size) {
17417
+ const length = Math.min(MUSE_READ_CHUNK_BYTES, stat.size - position);
17418
+ const chunk = Buffer.alloc(length);
17419
+ const read = fssync.readSync(fd, chunk, 0, length, position);
17420
+ if (read <= 0) break;
17421
+ position += read;
17422
+ const data = carry.length ? Buffer.concat([carry, chunk.subarray(0, read)]) : chunk.subarray(0, read);
17423
+ // Only complete lines are consumed, so a record still being written is
17424
+ // read whole on the next sync instead of being skipped.
17425
+ const lastNewline = data.lastIndexOf(0x0a);
17426
+ if (lastNewline < 0) { carry = data; continue; }
17427
+ const complete = data.subarray(0, lastNewline + 1);
17428
+ carry = Buffer.from(data.subarray(lastNewline + 1));
17429
+ if (hashedPrefix) consumedHash.update(complete);
17430
+ consumed += complete.length;
17431
+ for (const line of complete.toString("utf8").split("\n")) {
17432
+ if (!line.trim()) continue;
17433
+ let entry;
17434
+ try { entry = JSON.parse(line); } catch { continue; }
17435
+ for (const rec of museRecords(entry)) handleRecord(rec, fileState);
17436
+ }
17437
+ }
17438
+ }
17439
+ const runModelKeys = Object.keys(fileState.runModels);
17440
+ const keptRunModels = {};
17441
+ // Bound the cursor: later records reference recent runs.
17442
+ for (const key of runModelKeys.slice(-50)) keptRunModels[key] = fileState.runModels[key];
17443
+ const consumedSize = startOffset + consumed;
17444
+ // Fingerprint from the same descriptor we read — never reopen (CodeQL
17445
+ // file-system-race) and never trust a post-read rewrite.
17446
+ let savedPrefixFp = null;
17447
+ if (hashedPrefix && consumedSize <= MUSE_PREFIX_FINGERPRINT_MAX_BYTES) {
17448
+ savedPrefixFp = consumedHash.digest("hex");
17449
+ } else {
17450
+ try { savedPrefixFp = musePrefixFingerprint(fd, consumedSize); } catch { /* best-effort */ }
17451
+ }
17452
+ fileOffsets[filePath] = {
17453
+ size: consumedSize,
17454
+ mtimeMs: stat.mtimeMs,
17455
+ ino: stat.ino,
17456
+ headFp,
17457
+ prefixFp: savedPrefixFp,
17458
+ forceRewind: false,
17459
+ model: fileState.model,
17460
+ runModels: keptRunModels,
17461
+ counted: fileState.counted,
17462
+ watermark: fileState.watermark,
17463
+ watermarkIds: fileState.watermarkIds,
17464
+ };
17465
+ try { fssync.closeSync(fd); } catch { /* ignore */ }
17466
+ if (cb) {
17467
+ cb({ index: fileIdx + 1, total: files.length, recordsProcessed, eventsAggregated, bucketsQueued: touchedBuckets.size });
17468
+ }
17469
+ }
17470
+
17471
+ const seenArr = Array.from(seenIds);
17472
+ const cappedSeen = seenArr.length > 10_000 ? seenArr.slice(seenArr.length - 10_000) : seenArr;
17473
+ const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
17474
+ const updatedAt = new Date().toISOString();
17475
+ hourlyState.updatedAt = updatedAt;
17476
+ cursors.hourly = hourlyState;
17477
+ cursors.muse = { ...state, seenIds: cappedSeen, fileOffsets, updatedAt };
17478
+ return { recordsProcessed, eventsAggregated, bucketsQueued };
17479
+ }
17480
+
17080
17481
  async function parseCraftIncremental({
17081
17482
  sessionFiles,
17082
17483
  cursors,
@@ -24863,6 +25264,9 @@ module.exports = {
24863
25264
  resolveReasonixTelemetryFiles,
24864
25265
  normalizeReasonixModel,
24865
25266
  parseReasonixIncremental,
25267
+ resolveMuseHome,
25268
+ resolveMuseSessionFiles,
25269
+ parseMuseIncremental,
24866
25270
  // Exposed for regression tests covering cache-token accounting.
24867
25271
  normalizeGeminiTokens,
24868
25272
  normalizeOpencodeTokens,