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.
- package/README.de.md +5 -4
- package/README.ja.md +5 -4
- package/README.ko.md +5 -4
- package/README.md +5 -4
- package/README.zh-CN.md +5 -4
- package/dashboard/dist/assets/{AchievementBadge-D5sRTMmp.js → AchievementBadge-WGbdoboF.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CaiCfchd.js → AchievementsPage-BvybbsXv.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-D6dTxdUd.js → AchievementsSection-ChNzTFSR.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-C0UrWb9m.js → BadgeDetailModal-DhI9Ejz3.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CgpBB5FZ.js → BotAnimated-Bch0Yxej.js} +1 -1
- package/dashboard/dist/assets/{Card-CuwBiIYz.js → Card-DWdL7d1T.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-BNCnBR0h.js → CloudCheckoutPage-BjGoOz6P.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-CuYXUQnv.js → CloudPage-Btb6a97x.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-CV6a_7yK.js → CommandPalette-DwP3k7LE.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-Dw-64Tig.js → CommunityStatsModal-mzqLxpwE.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CoFO1Nas.js → ConfirmModal-Dm76q4EX.js} +1 -1
- package/dashboard/dist/assets/{Controls-DB9eQ_rg.js → Controls-ClZjOBoD.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-Doc8f4NL.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-DgZEnn4X.js → DateRangePopover-vfTUePPg.js} +1 -1
- package/dashboard/dist/assets/DevicePage-Jjg5q0C2.js +1 -0
- package/dashboard/dist/assets/{DialogClose-BR9QrB34.js → DialogClose-Ci5mPZ6X.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-Di5ev5K6.js +1 -0
- package/dashboard/dist/assets/DialogTitle-DI21oZxH.js +1 -0
- package/dashboard/dist/assets/{ElectricLogo-nrlBy-l9.js → ElectricLogo-DfeZBwPZ.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CyJJ3l_P.js → FadeIn-C7SIILTc.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-C7BX7loS.js → HeaderGithubStar-RStLbadn.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-KGluh6sO.js → HoverTooltip-DI7uAp1_.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Ci2ybTDg.js → IpCheckPage-DTZNTJVh.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-C9N17IpV.js → LandingPage-Df5scNlw.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-Cco7iFen.js → LeaderboardPage-Ck3SqtnI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-tmm5NN8p.js → LeaderboardProBadge-CGx6m7R6.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-WhjsvZ9Q.js → LeaderboardProfileModal-DuP7rgfv.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Ba3oVu5z.js → LeaderboardProfilePage-B5qUJkZX.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CUa46XJy.js → LimitsPage-Bv_Lcvcz.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-BSewrWlY.js → LocalOnlyNotice-Cg4y02JM.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CUWn0Tz4.js → LoginCard-Wz0uJKXx.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-UKk3xCsV.js → LoginModal-C13v_Gs0.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BhxiWgc5.js → LoginPage-COH9xXtd.js} +1 -1
- package/dashboard/dist/assets/{PetPage-BRyxC-Iu.js → PetPage-BWCnURub.js} +2 -2
- package/dashboard/dist/assets/PopoverPopup-Dw811N-_.js +1 -0
- package/dashboard/dist/assets/{ProviderIcon-htNu1RTN.js → ProviderIcon-CHxhYzK-.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DawMNiM1.js → ResetPasswordPage-Ll6jshqg.js} +1 -1
- package/dashboard/dist/assets/Select-BbooQ3sa.js +1 -0
- package/dashboard/dist/assets/SelectItemText-DBiW70wi.js +1 -0
- package/dashboard/dist/assets/{SelfHostPage-BMCMrt5I.js → SelfHostPage-BMHJxz5q.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-e_7xC0RY.js → ServiceStatusPage-D6LH0ac-.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-Xr2FJa6E.js → SessionsPage-Mrer6v17.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-D_7P_y_l.js +5 -0
- package/dashboard/dist/assets/SkillsPage-Ka7xm-1v.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy--6ijNK6P.js → TokenGalaxy-BEURDPFq.js} +1 -1
- package/dashboard/dist/assets/TooltipPopup-ZiuFFARC.js +1 -0
- package/dashboard/dist/assets/{TrendMonitor-DjIZyzxp.js → TrendMonitor-H73lyGEf.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CzCqGbb4.js → WidgetsPage-_7iCvyTk.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B0RMtpvr.js → WrappedPage-DEXNb6-k.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-BkNP5Ecj.js +45 -0
- package/dashboard/dist/assets/{agent-logos-CSlyeabY.js → agent-logos-OVBc_0cF.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-DaqMiXTO.js → arrow-left-DDphsclm.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-B1p9R8vH.js → arrow-right-B1aNq0Ki.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CK_9HuEh.js → arrow-up-right-yTM9azz_.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BVyCx4dJ.js → calendar-days-D22ithNF.js} +1 -1
- package/dashboard/dist/assets/{check-DIfCVsPJ.js → check-Dk7tknvE.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-DrWxRBXX.js → chevron-down-DdyEzQ4f.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-DOxBG7oS.js → cloud-checkout-BJjbipnH.js} +1 -1
- package/dashboard/dist/assets/composite-DEqP00XV.js +1 -0
- package/dashboard/dist/assets/{download-CEXAmejX.js → download-C9ixNyzn.js} +1 -1
- package/dashboard/dist/assets/{external-link-tDBZy6QP.js → external-link-DL4HSsLw.js} +1 -1
- package/dashboard/dist/assets/{flame-86y9Xf0g.js → flame-DqNH65nB.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BNN9k5mV.js → format-tokens-CyVjKz4I.js} +1 -1
- package/dashboard/dist/assets/{gift-BEOvq2zc.js → gift-ChJQQe_G.js} +1 -1
- package/dashboard/dist/assets/{host-mode-UjbNKxFB.js → host-mode-DpUDpoOz.js} +1 -1
- package/dashboard/dist/assets/{icons-DJmZD0si.js → icons-CJiHeOaU.js} +1 -1
- package/dashboard/dist/assets/{index-BwUwnqE0.js → index-72LQsAaf.js} +1 -1
- package/dashboard/dist/assets/{index-B8kizz-s.js → index-BucEg3fu.js} +1 -1
- package/dashboard/dist/assets/{index-Ci3CZ-2g.js → index-CrFlTSra.js} +1 -1
- package/dashboard/dist/assets/{info-ynVg8RzK.js → info-DReYqxC7.js} +1 -1
- package/dashboard/dist/assets/{link-2-B9SLPcBg.js → link-2-kIZbhK2S.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-FTE9k7Sk.js → loader-circle-1An-X85y.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-B4babCm_.js → modifiers.esm-FoVheEKT.js} +1 -1
- package/dashboard/dist/assets/module-DOCYJaxl.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-CNzgbo-M.js → monitor-smartphone-Ch6lr_P2.js} +1 -1
- package/dashboard/dist/assets/{play-9W4Ua21o.js → play-CbJ6ZF1K.js} +1 -1
- package/dashboard/dist/assets/{plus-BA85lApI.js → plus-C-5ID3a9.js} +1 -1
- package/dashboard/dist/assets/{provider-display-D1ETarsz.js → provider-display-BFzRtoL2.js} +2 -2
- package/dashboard/dist/assets/{react-VsHxeqq7.js → react-D4CR_fLQ.js} +1 -1
- package/dashboard/dist/assets/safePolygon-DP21Ibqj.js +1 -0
- package/dashboard/dist/assets/{search-Svi--7CN.js → search-CaznkY7U.js} +1 -1
- package/dashboard/dist/assets/{server-CgHdX6TE.js → server-CUOmC6xZ.js} +1 -1
- package/dashboard/dist/assets/{shield-check-Cjv-T4aT.js → shield-check-Dr2KBMmA.js} +1 -1
- package/dashboard/dist/assets/{skills-api-Cn8hEYL4.js → skills-api-BAz_fblS.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-g3UcmvFU.js → sortable.esm-knEfm5hE.js} +1 -1
- package/dashboard/dist/assets/store-CL97jcij.js +1 -0
- package/dashboard/dist/assets/{terminal-o0jvDfI0.js → terminal-EjFyreWj.js} +1 -1
- package/dashboard/dist/assets/{trash-0cqpTyN6.js → trash-ByqaZXkQ.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BFgORaJS.js → use-community-stats-Bs4RDJZE.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-9eZP_jA5.js → use-latest-request-guard-CG3Ir1cP.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BjO3hsmw.js → use-limits-display-prefs-tACgCxeR.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-YKn-htVo.js → use-native-settings-CWjXyJXi.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-D5h4UwhF.js → use-ordered-list-DsuC9JQa.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CwFguzkm.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BShLuIbk.js → use-session-efficiency-pref-NeoJE8BL.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DVZHJPVD.js → use-transform-BWGKOsya.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BhJbW76K.js → use-usage-limits-CFWFNwHS.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BmEbDVaa.js → useCurrency-B_C4_P5x.js} +1 -1
- package/dashboard/dist/assets/usePositioner-B0L5_o38.js +1 -0
- package/dashboard/dist/assets/{useTokenFormat-CBlBP2aA.js → useTokenFormat-DH1GWHSU.js} +1 -1
- package/dashboard/dist/assets/{zap-cML2-Nx4.js → zap-Chv1p7pS.js} +1 -1
- package/dashboard/dist/index.html +13 -11
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +1 -1
- package/package.json +3 -3
- package/src/commands/doctor.js +15 -1
- package/src/commands/init.js +9 -0
- package/src/commands/status.js +21 -3
- package/src/commands/sync.js +59 -4
- package/src/lib/cloud-device-token.js +77 -10
- package/src/lib/cloud-usage-reset.js +115 -0
- package/src/lib/diagnostics.js +5 -2
- package/src/lib/kimi-model-repair.js +173 -0
- package/src/lib/kimi-session.js +216 -0
- package/src/lib/local-api.js +46 -1
- package/src/lib/pricing/index.js +2 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/qoder-cn-cli-limits.js +9 -7
- package/src/lib/qoder-limits.js +45 -15
- package/src/lib/rollout.js +408 -4
- package/src/lib/session-analytics.js +47 -9
- package/dashboard/dist/assets/DashboardPage-sZKijU6h.js +0 -1
- package/dashboard/dist/assets/DevicePage-YCnJnsmz.js +0 -1
- package/dashboard/dist/assets/DialogDescription-Cx1R3WT7.js +0 -1
- package/dashboard/dist/assets/DialogTitle-ck8f1qeP.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-D-Tpl294.js +0 -1
- package/dashboard/dist/assets/Select-6WChWtqK.js +0 -1
- package/dashboard/dist/assets/SelectItemText-DRmxMwo5.js +0 -1
- package/dashboard/dist/assets/SettingsPage-Bxq2Mhxz.js +0 -5
- package/dashboard/dist/assets/SkillsPage-CEIBW63c.js +0 -1
- package/dashboard/dist/assets/TooltipPopup--TLYUg0c.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-BsOjTB8Q.js +0 -45
- package/dashboard/dist/assets/composite-W1R1XwoE.js +0 -1
- package/dashboard/dist/assets/module-BNnaKM__.js +0 -3
- package/dashboard/dist/assets/safePolygon-CCDOsx-z.js +0 -1
- package/dashboard/dist/assets/store-D2gn9bSV.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-C83gmg0F.js +0 -1
- 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
|
-
|
|
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
|
|
68
|
-
const used = Number(rawQuota
|
|
69
|
-
const remaining = rawQuota
|
|
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;
|
package/src/lib/qoder-limits.js
CHANGED
|
@@ -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
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
const
|
|
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
|
-
:
|
|
254
|
-
? 100
|
|
255
|
-
:
|
|
256
|
-
?
|
|
257
|
-
: (
|
|
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
|
-
|
|
393
|
-
|
|
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
|
};
|
package/src/lib/rollout.js
CHANGED
|
@@ -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
|
-
//
|
|
9754
|
-
//
|
|
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
|
-
|
|
9770
|
-
|
|
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,
|