tokentracker-cli 1.3.15 → 1.4.1

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 (136) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-CSXb26DC.js → AchievementBadge-DWECUOAX.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-Bs6vIpd0.js → AchievementsPage-Bakfwwn_.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-BO4u-574.js → AchievementsSection-DiEX2_5U.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-ic60vrZX.js → BadgeDetailModal-ClVMnxVW.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-BHHAUEQL.js → BotAnimated-D7eyrAPs.js} +1 -1
  6. package/dashboard/dist/assets/{Card-8t2FYLJV.js → Card-CsAQxZZ8.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-CXaDDial.js → CloudCheckoutPage-BAN-DtXB.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-DN6s_UrT.js → CloudPage-D9sNlUAy.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-D7dHMpHK.js → CommandPalette-D9TNT6un.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-C55r20Vb.js → CommunityStatsModal-CotRRQNH.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-jSEqSSnW.js → ConfirmModal-C_il5anp.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-CqmoR37l.js → Controls-Bd1kODP4.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-De0EL8Hn.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-z89rKqjK.js → DateRangePopover-quw6hpxy.js} +1 -1
  15. package/dashboard/dist/assets/DevicePage-CP20lVYv.js +1 -0
  16. package/dashboard/dist/assets/{DialogClose-imfpTM8a.js → DialogClose-BEUL9bD6.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-DpkD2qwW.js +1 -0
  18. package/dashboard/dist/assets/DialogTitle-C--sXGjf.js +1 -0
  19. package/dashboard/dist/assets/{ElectricLogo-CrnDjhtG.js → ElectricLogo-DhP7S1Cu.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-CAr2EMcK.js → FadeIn-BPH15_jE.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-UK7qFFNh.js → HeaderGithubStar-DziE8s6w.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-fs1HBOXB.js → HoverTooltip-tQyY2CuV.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-Bk-MlPWf.js → IpCheckPage-BjoB05O7.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DJnPCdqP.js → LandingPage-Cav49COr.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-wVjwsqyc.js → LeaderboardPage-CHjFKLvs.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-BaQdHu06.js → LeaderboardProBadge-D97MfK5W.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-D5ZZtIPl.js → LeaderboardProfileModal-NNTwtWSu.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-BZsBL2jT.js → LeaderboardProfilePage-xI81GH3D.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-Cks_mKSL.js → LimitsPage-BPOo3FOW.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-DLnC_Tq4.js → LocalOnlyNotice-j8Tijts2.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-FXZbjmOY.js → LoginCard-C7_n6J7k.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-_R-0ZtpQ.js → LoginModal-DwlNscw5.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-MzDRyv1Q.js → LoginPage-BqhWatt2.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DJ58EiPT.js → PetPage-CJlC0V0i.js} +2 -2
  35. package/dashboard/dist/assets/PopoverPopup-wleZ4DTI.js +1 -0
  36. package/dashboard/dist/assets/{ProviderIcon-xSNRuia-.js → ProviderIcon-biLVsOGr.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BcVcWbsW.js → ResetPasswordPage-5Sl45OsN.js} +1 -1
  38. package/dashboard/dist/assets/Select-77l2QNqa.js +1 -0
  39. package/dashboard/dist/assets/SelectItemText-BQbmtjd3.js +1 -0
  40. package/dashboard/dist/assets/{SelfHostPage-BLocCkUo.js → SelfHostPage-C8hlgVu4.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-CzMjS7EQ.js → ServiceStatusPage-DrY9Mef8.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-Dqzuthrd.js → SessionsPage-BDTsH3Mk.js} +2 -2
  43. package/dashboard/dist/assets/SettingsPage-C_id9Go3.js +5 -0
  44. package/dashboard/dist/assets/SkillsPage-kb6_5uUk.js +1 -0
  45. package/dashboard/dist/assets/{TokenGalaxy-B2sMNsM0.js → TokenGalaxy-BoS0yUFS.js} +1 -1
  46. package/dashboard/dist/assets/TooltipPopup-LV1aWIRZ.js +1 -0
  47. package/dashboard/dist/assets/{TrendMonitor-DpMcMWVh.js → TrendMonitor-DN-nwcSA.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-aKfztwsk.js → WidgetsPage-C6lvwceS.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-NoY2hqbF.js → WrappedPage-CfC39wWP.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-kAUz3kFs.css → __vite-browser-external-B0m8a6Te.css} +1 -1
  51. package/dashboard/dist/assets/__vite-browser-external-DpZbUcLL.js +45 -0
  52. package/dashboard/dist/assets/{agent-logos-Oh8CLAYk.js → agent-logos-CvcdCGj5.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-Mwggxctm.js → arrow-left-BX4LX5TF.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-Crx-kuck.js → arrow-right-Aktjo2RL.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-uwoo8I0B.js → arrow-up-right-kkQt6Du0.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-C6MeKaqh.js → calendar-days-OvLTNZBH.js} +1 -1
  57. package/dashboard/dist/assets/{check-dsWQRYNj.js → check-v5Ie379p.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-BIf5bhyB.js → chevron-down-CPlz1cYW.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-GnFRQdnr.js → cloud-checkout-C_Rhiwno.js} +1 -1
  60. package/dashboard/dist/assets/composite-DD_boZdD.js +1 -0
  61. package/dashboard/dist/assets/{download-B7eyvOxn.js → download-NqHXZ-zn.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-RNTsFDG4.js → external-link-B0D03J0f.js} +1 -1
  63. package/dashboard/dist/assets/{flame-uwCJ6brb.js → flame-av87we58.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-Bf7JxB4E.js → format-tokens-B43Ehkbb.js} +1 -1
  65. package/dashboard/dist/assets/{gift-BY7kCGBK.js → gift-CZus7M21.js} +1 -1
  66. package/dashboard/dist/assets/{host-mode-Bs-UHw-7.js → host-mode-Dd9fzAdq.js} +1 -1
  67. package/dashboard/dist/assets/{icons-CrLu8vlW.js → icons-CnScwnf_.js} +1 -1
  68. package/dashboard/dist/assets/{index-CXQag63s.js → index-BZTIoCof.js} +1 -1
  69. package/dashboard/dist/assets/{index-DYIIk93n.js → index-Cj9NdXOj.js} +1 -1
  70. package/dashboard/dist/assets/{index-DieuzFHA.js → index-D3XPaYEL.js} +1 -1
  71. package/dashboard/dist/assets/{info-BteHQN_N.js → info-BUmCmjwd.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-C1RSbOHv.js → link-2-t-wSLB2a.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-BM3x6_YL.js → loader-circle-DM-FdXeX.js} +1 -1
  74. package/dashboard/dist/assets/{modifiers.esm-BHzOJfH3.js → modifiers.esm-D5AKZNa7.js} +1 -1
  75. package/dashboard/dist/assets/module-DOCYJaxl.js +3 -0
  76. package/dashboard/dist/assets/{monitor-smartphone-Ps3VptSt.js → monitor-smartphone-CDHFLZS-.js} +1 -1
  77. package/dashboard/dist/assets/{play-7MpFYU7a.js → play-BIQH--yZ.js} +1 -1
  78. package/dashboard/dist/assets/{plus-Dk-p9OaZ.js → plus-CztG5eum.js} +1 -1
  79. package/dashboard/dist/assets/{provider-display-Czwv4ClR.js → provider-display-B2gZ38Oh.js} +1 -1
  80. package/dashboard/dist/assets/{react-D9vco0ZF.js → react-D1u6HsZx.js} +1 -1
  81. package/dashboard/dist/assets/safePolygon-ClG4WM06.js +1 -0
  82. package/dashboard/dist/assets/{search-Z2Hfw99-.js → search-NNy_fIbJ.js} +1 -1
  83. package/dashboard/dist/assets/{server-Dh_OraRG.js → server-N94HJtJY.js} +1 -1
  84. package/dashboard/dist/assets/{shield-check-59NUSZvk.js → shield-check-BGfaKXG9.js} +1 -1
  85. package/dashboard/dist/assets/{skills-api-Bguk8iPD.js → skills-api-CWydQc4G.js} +1 -1
  86. package/dashboard/dist/assets/{sortable.esm-C9M-1k5o.js → sortable.esm-BF83ljWQ.js} +1 -1
  87. package/dashboard/dist/assets/store-l5WfCa7P.js +1 -0
  88. package/dashboard/dist/assets/{terminal-DKTXh9tj.js → terminal-CRBThKUQ.js} +1 -1
  89. package/dashboard/dist/assets/{trash-CQRRKbCs.js → trash-BXN5tg81.js} +1 -1
  90. package/dashboard/dist/assets/{use-community-stats-BoOd2Hhq.js → use-community-stats-dHketgyF.js} +1 -1
  91. package/dashboard/dist/assets/{use-latest-request-guard-vQtFEBIX.js → use-latest-request-guard-CdM1Pn_p.js} +1 -1
  92. package/dashboard/dist/assets/{use-limits-display-prefs-BRTj9_xd.js → use-limits-display-prefs-dOm4SCFU.js} +1 -1
  93. package/dashboard/dist/assets/{use-native-settings-CC6uEn3j.js → use-native-settings-GL9uAidm.js} +1 -1
  94. package/dashboard/dist/assets/{use-ordered-list-BUx6eIN7.js → use-ordered-list-Btz_NSiA.js} +1 -1
  95. package/dashboard/dist/assets/use-reduced-motion-D9n2I_eN.js +1 -0
  96. package/dashboard/dist/assets/{use-session-efficiency-pref-BsUypXPb.js → use-session-efficiency-pref-BPZel3HL.js} +1 -1
  97. package/dashboard/dist/assets/{use-transform-oomGXI86.js → use-transform-D2xePGbE.js} +1 -1
  98. package/dashboard/dist/assets/{use-usage-limits-CXeCKJZ_.js → use-usage-limits-vEmyPwMY.js} +1 -1
  99. package/dashboard/dist/assets/{useCurrency-DIZEXq5g.js → useCurrency-DnhS0llv.js} +1 -1
  100. package/dashboard/dist/assets/usePositioner-7yozJJJ0.js +1 -0
  101. package/dashboard/dist/assets/{useTokenFormat-DKURSOqb.js → useTokenFormat-DvjyXv8i.js} +1 -1
  102. package/dashboard/dist/assets/{zap-BQ8nGK7b.js → zap-CElRpX2w.js} +1 -1
  103. package/dashboard/dist/index.html +2 -2
  104. package/dashboard/dist/ip-check.html +2 -2
  105. package/dashboard/dist/leaderboard.html +2 -2
  106. package/dashboard/dist/share.html +2 -2
  107. package/package.json +2 -2
  108. package/src/commands/status.js +1 -1
  109. package/src/commands/sync.js +34 -4
  110. package/src/lib/cloud-usage-reset.js +115 -0
  111. package/src/lib/diagnostics.js +1 -1
  112. package/src/lib/kimi-model-repair.js +173 -0
  113. package/src/lib/kimi-session.js +216 -0
  114. package/src/lib/local-api.js +46 -1
  115. package/src/lib/pricing/seed-snapshot.json +1 -1
  116. package/src/lib/qoder-cn-cli-limits.js +9 -7
  117. package/src/lib/qoder-limits.js +45 -15
  118. package/src/lib/rollout.js +6 -4
  119. package/src/lib/session-analytics.js +47 -9
  120. package/dashboard/dist/assets/DashboardPage-BLr2XmyZ.js +0 -1
  121. package/dashboard/dist/assets/DevicePage-Ddm5uKOw.js +0 -1
  122. package/dashboard/dist/assets/DialogDescription-BE5CNMW0.js +0 -1
  123. package/dashboard/dist/assets/DialogTitle-Ca1-hka8.js +0 -1
  124. package/dashboard/dist/assets/PopoverPopup-uQa7G-IO.js +0 -1
  125. package/dashboard/dist/assets/Select-Dwix4ZFd.js +0 -1
  126. package/dashboard/dist/assets/SelectItemText-Bz5cn5Gk.js +0 -1
  127. package/dashboard/dist/assets/SettingsPage-DEdNhzKR.js +0 -5
  128. package/dashboard/dist/assets/SkillsPage-CqocOQID.js +0 -1
  129. package/dashboard/dist/assets/TooltipPopup-LzbtjRSA.js +0 -1
  130. package/dashboard/dist/assets/__vite-browser-external-BnGukaND.js +0 -45
  131. package/dashboard/dist/assets/composite-lINFdwLI.js +0 -1
  132. package/dashboard/dist/assets/module-BNnaKM__.js +0 -3
  133. package/dashboard/dist/assets/safePolygon-Ca2iBTNb.js +0 -1
  134. package/dashboard/dist/assets/store-nR9Ah4fr.js +0 -1
  135. package/dashboard/dist/assets/use-reduced-motion-BZBrXeyc.js +0 -1
  136. package/dashboard/dist/assets/usePositioner-Drh2sy7w.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
  }
@@ -37,6 +37,7 @@ const { resolveZcodeNativeDbPath, resolveInstallPaths } = require("./install-res
37
37
  const { readCursorStateSummary } = require("./cursor-store");
38
38
  const { USD_TICKS_PER_USD, normalizeGrokUsage } = require("./grok-usage");
39
39
  const wsl = require("./wsl-probe");
40
+ const { discoverKimiWireFiles, kimiSessionDependencies, readKimiSessions } = require("./kimi-session");
40
41
  const {
41
42
  appendUniqueDirs,
42
43
  dedupeDirsByRealpath,
@@ -85,7 +86,8 @@ const {
85
86
  // v22 adds metadata-only OpenCode database sessions.
86
87
  // v23 excludes database updates and user records from request durations.
87
88
  // v24 keeps OpenCode usage dates at event time and fork history on its ancestor.
88
- const SIDECAR_VERSION = 24;
89
+ // v25 adds Kimi wire sessions, including official/legacy migration dedup.
90
+ const SIDECAR_VERSION = 25;
89
91
  const EDIT_TOOLS = new Set([
90
92
  "apply_patch",
91
93
  "edit",
@@ -1925,6 +1927,14 @@ function groupCodexFiles(filePaths) {
1925
1927
  return ordered;
1926
1928
  }
1927
1929
 
1930
+ async function scanKimiSessions(filePaths) {
1931
+ const rows = await readKimiSessions(Array.isArray(filePaths) ? filePaths : [filePaths]);
1932
+ return rows.map(({ identity, files, ...row }) => finalizeRecord({
1933
+ ...row, version: SIDECAR_VERSION, source: "kimi", session_hash: sessionHash("kimi", identity),
1934
+ project_key: projectKey(row.project_ref, files[0]), ...sessionStorageUsage(files),
1935
+ }));
1936
+ }
1937
+
1928
1938
  async function discoverSessionFiles(home, env = process.env, deps = {}) {
1929
1939
  const grokHome = resolveGrokHome(home);
1930
1940
  // config.scanRoots (#657): read from the tracker config unless the caller
@@ -2007,7 +2017,17 @@ async function discoverSessionFiles(home, env = process.env, deps = {}) {
2007
2017
  const opencodePaths = resolveInstallPaths({ nativeValue: opencodeNativeDir, wslValue: opencodeWslDir }, env, { platform: zcodePlatform });
2008
2018
  const opencode = [...new Set(Object.values(opencodePaths).filter(Boolean)
2009
2019
  .map((dir) => path.join(dir, "opencode.db")).filter((dbPath) => fs.existsSync(dbPath)))];
2010
- return { claude, codex: groupCodexFiles([...codex, ...archived]), grok, zcode, opencode, incomplete };
2020
+ const kimiRoots = [".kimi", ".kimi-code"].flatMap((directory) => {
2021
+ const roots = providerRoots(home, directory, env, deps);
2022
+ const override = env[directory === ".kimi" ? "KIMI_HOME" : "KIMI_CODE_HOME"];
2023
+ if (override && path.resolve(home) === path.resolve((deps.homedir || os.homedir)())) {
2024
+ const native = path.join(home, directory);
2025
+ return roots.map((root) => root === native ? expandHome(override, home) : root);
2026
+ }
2027
+ return roots;
2028
+ });
2029
+ const kimi = discoverKimiWireFiles(dedupeDirsByRealpath(kimiRoots), incomplete);
2030
+ return { claude, codex: groupCodexFiles([...codex, ...archived]), grok, zcode, opencode, kimi, incomplete };
2011
2031
  }
2012
2032
 
2013
2033
  // Warn once per unreadable directory per process: the dashboard polls this
@@ -2108,6 +2128,9 @@ function analyticsEntryStatKey(source, filePath) {
2108
2128
  ));
2109
2129
  return `${sessionStat}|title-index:${titleStats.join("|")}`;
2110
2130
  }
2131
+ if (source === "kimi") {
2132
+ return `${sessionStat}|metadata:${filesSignature(kimiSessionDependencies(filePaths))}`;
2133
+ }
2111
2134
  if (source === "grok") {
2112
2135
  const primary = filePaths[0];
2113
2136
  return `${sessionStat}|summary:${sessionFileStatKey(grokSummaryPathFor(primary)) || "missing"}|signals:${sessionFileStatKey(grokSignalsPathFor(primary)) || "missing"}`;
@@ -2147,13 +2170,15 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2147
2170
  const retryZcodeScan = fs.existsSync(incompleteZcodePath);
2148
2171
  const incompleteOpencodePath = `${metaPath}.opencode-incomplete`;
2149
2172
  const retryOpencodeScan = fs.existsSync(incompleteOpencodePath);
2173
+ const incompleteKimiPath = `${metaPath}.kimi-incomplete`;
2174
+ const retryKimiScan = fs.existsSync(incompleteKimiPath);
2150
2175
  let previousMeta = null;
2151
2176
  if (!force) {
2152
2177
  try {
2153
2178
  previousMeta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
2154
2179
  const checkedAt = Date.parse(previousMeta.checked_at || previousMeta.generated_at || "");
2155
2180
  if (
2156
- !retryZcodeScan && !retryOpencodeScan && previousMeta.version === SIDECAR_VERSION &&
2181
+ !retryZcodeScan && !retryOpencodeScan && !retryKimiScan && previousMeta.version === SIDECAR_VERSION &&
2157
2182
  Number.isFinite(checkedAt) &&
2158
2183
  Date.now() - checkedAt < Math.max(0, Number(cacheTtlMs) || 0)
2159
2184
  ) {
@@ -2183,13 +2208,15 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2183
2208
  ...discovered.claude.flat(),
2184
2209
  ...discovered.codex.flat(),
2185
2210
  ...discovered.codex.flat().map(codexTitleIndexPathFor).filter(Boolean),
2211
+ ...discovered.kimi,
2212
+ ...kimiSessionDependencies(discovered.kimi),
2186
2213
  ...discovered.grok,
2187
2214
  ...discovered.grok.map(grokSummaryPathFor),
2188
2215
  ...discovered.grok.map(grokSignalsPathFor),
2189
2216
  // db + its WAL sidecars: the trio is one logical file for change detection.
2190
2217
  ...[...(discovered.zcode || []), ...(discovered.opencode || [])].flatMap((dbPath) => [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]),
2191
2218
  ]);
2192
- if (!force && !retryZcodeScan && !retryOpencodeScan && previousMeta?.version === SIDECAR_VERSION && previousMeta.signature === signature) {
2219
+ if (!force && !retryZcodeScan && !retryOpencodeScan && !retryKimiScan && previousMeta?.version === SIDECAR_VERSION && previousMeta.signature === signature) {
2193
2220
  await writeAtomic(metaPath, `${JSON.stringify({ ...previousMeta, checked_at: new Date().toISOString() })}\n`);
2194
2221
  return readSidecar(sidecarPath);
2195
2222
  }
@@ -2211,6 +2238,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2211
2238
  ...discovered.grok.map((filePath) => ({ source: "grok", filePath, scan: scanGrokSession })),
2212
2239
  ...(discovered.zcode.length ? [{ source: "zcode", filePath: discovered.zcode, scan: scanZcodeSessions }] : []),
2213
2240
  ...(discovered.opencode.length ? [{ source: "opencode", filePath: discovered.opencode, scan: scanOpencodeSessions }] : []),
2241
+ ...(discovered.kimi.length ? [{ source: "kimi", filePath: discovered.kimi, scan: scanKimiSessions }] : []),
2214
2242
  ];
2215
2243
  // Files we could not turn into a row (permission denied, half-written line,
2216
2244
  // vanished mid-scan). Swallowing these silently made sessions disappear with
@@ -2218,6 +2246,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2218
2246
  let skippedFiles = 0;
2219
2247
  let incompleteZcodeScan = false;
2220
2248
  let incompleteOpencodeScan = false;
2249
+ let incompleteKimiScan = false;
2221
2250
  for (const entry of entries) {
2222
2251
  const cacheKey = sessionFileCacheKey(entry.source, entry.filePath);
2223
2252
  const statKey = analyticsEntryStatKey(entry.source, entry.filePath);
@@ -2225,11 +2254,12 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2225
2254
  skippedFiles += 1;
2226
2255
  if (entry.source === "zcode") incompleteZcodeScan = true;
2227
2256
  if (entry.source === "opencode") incompleteOpencodeScan = true;
2257
+ if (entry.source === "kimi") incompleteKimiScan = true;
2228
2258
  continue;
2229
2259
  }
2230
2260
  let rows = null;
2231
- if (!force && !(entry.source === "zcode" && retryZcodeScan) && !(entry.source === "opencode" && retryOpencodeScan) && previousFiles[cacheKey]?.stat_key === statKey) {
2232
- if (entry.source === "zcode" || entry.source === "opencode") {
2261
+ if (!force && !(entry.source === "zcode" && retryZcodeScan) && !(entry.source === "opencode" && retryOpencodeScan) && !(entry.source === "kimi" && retryKimiScan) && previousFiles[cacheKey]?.stat_key === statKey) {
2262
+ if (entry.source === "zcode" || entry.source === "opencode" || entry.source === "kimi") {
2233
2263
  // One db scans to many cached rows; recover them all from the previous
2234
2264
  // pass. An empty recovery means the sidecar lost its rows — rescan.
2235
2265
  const cached = previousRows.filter((row) => row?.source === entry.source);
@@ -2249,6 +2279,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2249
2279
  skippedFiles += 1;
2250
2280
  if (entry.source === "zcode") incompleteZcodeScan = true;
2251
2281
  if (entry.source === "opencode") incompleteOpencodeScan = true;
2282
+ if (entry.source === "kimi") incompleteKimiScan = true;
2252
2283
  if (!process.env.NODE_TEST_CONTEXT) {
2253
2284
  console.warn(`[session-analytics] skipped ${entry.source} session file: ${error?.message || error}`);
2254
2285
  }
@@ -2262,8 +2293,8 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2262
2293
  // so key each by the db path plus its session id; a bare session id as a
2263
2294
  // separate path element would be path.resolve()d against the process cwd
2264
2295
  // and never match across runs.
2265
- row._cache_key = (entry.source === "zcode" || entry.source === "opencode") && row.session_id
2266
- ? `${cacheKey}#${sessionHash(entry.source, row.session_id)}`
2296
+ row._cache_key = (entry.source === "zcode" || entry.source === "opencode" || entry.source === "kimi") && (row.session_id || row.session_hash)
2297
+ ? `${cacheKey}#${sessionHash(entry.source, row.session_id || row.session_hash)}`
2267
2298
  : cacheKey;
2268
2299
  sessions.push(row);
2269
2300
  }
@@ -2282,10 +2313,14 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2282
2313
  sessions.push(...readSidecar(sidecarPath).filter((row) =>
2283
2314
  row.source === "opencode" && row.version === SIDECAR_VERSION));
2284
2315
  }
2316
+ if (incompleteKimiScan) {
2317
+ await writeAtomic(incompleteKimiPath, "1\n");
2318
+ sessions.push(...readSidecar(sidecarPath).filter((row) => row.source === "kimi" && row.version === SIDECAR_VERSION));
2319
+ }
2285
2320
  sessions.sort((a, b) => String(b.ended_at || "").localeCompare(String(a.ended_at || "")));
2286
2321
  // A failed database scan is an incomplete inventory. Keep the last complete
2287
2322
  // snapshot and signature so an unchanged database is retried next build.
2288
- if (incompleteDirs.length === 0 && !incompleteZcodeScan && !incompleteOpencodeScan) {
2323
+ if (incompleteDirs.length === 0 && !incompleteZcodeScan && !incompleteOpencodeScan && !incompleteKimiScan) {
2289
2324
  const content = sessions.map(serializeSessionRecord).join("\n") + (sessions.length ? "\n" : "");
2290
2325
  await writeAtomic(sidecarPath, content);
2291
2326
  const generatedAt = new Date().toISOString();
@@ -2298,6 +2333,7 @@ async function buildSessionAnalyticsInternal({ home = os.homedir(), force = fals
2298
2333
  })}\n`);
2299
2334
  await fsp.rm(incompleteZcodePath, { force: true });
2300
2335
  await fsp.rm(incompleteOpencodePath, { force: true });
2336
+ await fsp.rm(incompleteKimiPath, { force: true });
2301
2337
  }
2302
2338
  // Non-enumerable so the array still behaves exactly like a plain row list
2303
2339
  // for every existing caller (map/filter/JSON of the rows is unaffected).
@@ -2579,6 +2615,7 @@ function resumeCommandFor(source, sessionId) {
2579
2615
  if (source === "claude") return `claude --resume ${id}`;
2580
2616
  if (source === "codex") return `codex resume ${id}`;
2581
2617
  if (source === "opencode") return `opencode --session ${id}`;
2618
+ if (source === "kimi") return `kimi --session ${id}`;
2582
2619
  // Grok Build: `grok --resume <session-id>` (also accepts a title). The
2583
2620
  // command must still be run from the session's project_ref cwd.
2584
2621
  if (source === "grok") return `grok --resume ${id}`;
@@ -2922,6 +2959,7 @@ module.exports = {
2922
2959
  scanGrokSession,
2923
2960
  scanZcodeSessions,
2924
2961
  scanOpencodeSessions,
2962
+ scanKimiSessions,
2925
2963
  buildSessionAnalytics,
2926
2964
  summarizeSessions,
2927
2965
  listSessionsForBrowser,