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
@@ -0,0 +1,115 @@
1
+ const fs = require("node:fs");
2
+ const fsp = require("node:fs/promises");
3
+ const path = require("node:path");
4
+ const crypto = require("node:crypto");
5
+ const { openLock, writeFileAtomic } = require("./fs");
6
+ const { functionUrlFor, fetchFunctionResponse } = require("./function-url");
7
+
8
+ const MARKER = "cloud-usage-reset.json";
9
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ const failure = (code, status = 409) => Object.assign(new Error(code), { code, status });
11
+ const sameBase = (a, b) => String(a).replace(/\/$/, "") === String(b).replace(/\/$/, "");
12
+
13
+ function readCloudUsageReset(trackerDir) {
14
+ let raw;
15
+ try { raw = fs.readFileSync(path.join(trackerDir, MARKER), "utf8"); }
16
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
17
+ let value;
18
+ try { value = JSON.parse(raw); } catch { throw failure("cloud_usage_reset_invalid_state"); }
19
+ if (value?.version !== 1 || !UUID.test(value.operationId) || !UUID.test(value.deviceId) ||
20
+ !UUID.test(value.ownerId) || !/^https?:\/\//.test(value.baseUrl || "") ||
21
+ !["prepared", "uploading"].includes(value.phase)) throw failure("cloud_usage_reset_invalid_state");
22
+ return value;
23
+ }
24
+
25
+ function cloudUsageResetBlocksUpload(trackerDir, { ownerId, baseUrl, deviceId } = {}) {
26
+ try {
27
+ const marker = readCloudUsageReset(trackerDir);
28
+ return Boolean(marker && (marker.phase === "prepared" || marker.ownerId !== ownerId ||
29
+ !sameBase(marker.baseUrl, baseUrl) || (deviceId && marker.deviceId !== deviceId)));
30
+ }
31
+ catch { return true; }
32
+ }
33
+
34
+ async function writeMarker(trackerDir, marker) {
35
+ await writeFileAtomic(path.join(trackerDir, MARKER), JSON.stringify(marker) + "\n", { mode: 0o600 });
36
+ }
37
+
38
+ async function requestReset({ baseUrl, anonKey, authorization, body, fetchImpl = fetchFunctionResponse }) {
39
+ const response = await fetchImpl(functionUrlFor(baseUrl, "tokentracker-device-usage-reset"), {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${authorization}`,
42
+ ...(anonKey ? { apikey: anonKey } : {}) },
43
+ signal: AbortSignal.timeout(60_000),
44
+ body: JSON.stringify(body),
45
+ });
46
+ const data = await response.json().catch(() => ({}));
47
+ if (!response.ok || data.ok !== true) throw failure(data.code || "cloud_usage_reset_failed", response.status || 502);
48
+ if (data.operation_id !== body.operation_id) throw failure("cloud_usage_reset_invalid_response", 502);
49
+ return data;
50
+ }
51
+
52
+ // Called only after the local bridge has bound the JWT to its current device.
53
+ // The marker precedes the remote mutation: an interrupted request must retry the
54
+ // same operation, never clear already-replayed history a second time.
55
+ async function resetCloudDeviceUsage({ trackerDir, ownerId, deviceId, baseUrl, anonKey, accessToken,
56
+ isCurrent = () => true, fetchImpl }) {
57
+ if (!UUID.test(ownerId) || !UUID.test(deviceId) || !accessToken) throw failure("auth_required", 401);
58
+ const lock = await openLock(path.join(trackerDir, "sync.lock"), { quietIfLocked: true });
59
+ if (!lock) throw failure("cloud_usage_reset_sync_busy");
60
+ try {
61
+ if (!isCurrent()) throw failure("auth_session_changed");
62
+ let marker = readCloudUsageReset(trackerDir);
63
+ const newOperation = !marker;
64
+ if (marker && (marker.ownerId !== ownerId || marker.deviceId !== deviceId || !sameBase(marker.baseUrl, baseUrl))) {
65
+ throw failure("cloud_usage_reset_owner_changed");
66
+ }
67
+ if (marker?.phase === "uploading") return { ok: true, operation_id: marker.operationId, status: "uploading" };
68
+ if (!marker) {
69
+ marker = { version: 1, operationId: crypto.randomUUID(), ownerId, deviceId, baseUrl, phase: "prepared" };
70
+ await writeMarker(trackerDir, marker);
71
+ }
72
+ if (!isCurrent()) throw failure("auth_session_changed");
73
+ const result = await requestReset({ baseUrl, anonKey, authorization: accessToken, fetchImpl,
74
+ body: { action: "reset", device_id: deviceId, operation_id: marker.operationId } }).catch(async error => {
75
+ // A definitive first-request rejection did not erase anything. Unknown
76
+ // outcomes and retries retain their operation so they cannot erase twice.
77
+ if (newOperation && [400, 401, 403, 404, 409, 422].includes(error.status)) {
78
+ await fsp.unlink(path.join(trackerDir, MARKER));
79
+ }
80
+ throw error;
81
+ });
82
+ if (result.device_id !== deviceId || !["reset", "complete"].includes(result.status)) {
83
+ throw failure("cloud_usage_reset_invalid_response", 502);
84
+ }
85
+ if (!isCurrent()) throw failure("auth_session_changed");
86
+ await writeFileAtomic(path.join(trackerDir, "queue.state.json"),
87
+ JSON.stringify({ offset: 0, updatedAt: new Date().toISOString() }) + "\n", { mode: 0o600 });
88
+ await fsp.unlink(path.join(trackerDir, "upload.throttle.json")).catch(error => { if (error.code !== "ENOENT") throw error; });
89
+ marker.phase = "uploading";
90
+ await writeMarker(trackerDir, marker);
91
+ return { ...result, status: "uploading" };
92
+ } finally { await lock.release(); }
93
+ }
94
+
95
+ // sync owns sync.lock here. Only its fully acknowledged queue can finish the
96
+ // operation; a partial batch/checkpoint or another account leaves it pending.
97
+ async function finishCloudDeviceUsageReset({ trackerDir, ownerId, deviceId, baseUrl, anonKey, deviceToken, fetchImpl }) {
98
+ const marker = readCloudUsageReset(trackerDir);
99
+ if (!marker || marker.phase !== "uploading" || marker.ownerId !== ownerId || (deviceId && marker.deviceId !== deviceId) || !sameBase(marker.baseUrl, baseUrl)) return false;
100
+ if (!deviceToken) return false;
101
+ const queuePath = path.join(trackerDir, "queue.jsonl");
102
+ const size = await fsp.stat(queuePath).then(stat => stat.size).catch(error => { if (error.code === "ENOENT") return 0; throw error; });
103
+ let offset;
104
+ try { offset = JSON.parse(await fsp.readFile(path.join(trackerDir, "queue.state.json"), "utf8")).offset; }
105
+ catch { throw failure("cloud_usage_reset_invalid_offset"); }
106
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > size) throw failure("cloud_usage_reset_invalid_offset");
107
+ if (offset !== size) return false;
108
+ const result = await requestReset({ baseUrl, anonKey, authorization: deviceToken, fetchImpl,
109
+ body: { action: "refresh", operation_id: marker.operationId } });
110
+ if (result.status !== "complete" || result.device_id !== marker.deviceId) throw failure("cloud_usage_reset_invalid_response", 502);
111
+ await fsp.unlink(path.join(trackerDir, MARKER));
112
+ return true;
113
+ }
114
+
115
+ module.exports = { readCloudUsageReset, cloudUsageResetBlocksUpload, resetCloudDeviceUsage, finishCloudDeviceUsageReset };
@@ -228,7 +228,7 @@ async function collectTrackerDiagnostics({
228
228
  ? { wsl_mode: wsl.getWslMode(process.env), wsl_installs: kiroWslInstalls }
229
229
  : {}),
230
230
  cli_approximation:
231
- "Kiro CLI does not persist explicit token counts (billing is credit-based on Bedrock). Tokens are approximated at 4 chars/token from user prompt chars and assistant response chars. Source rows that came through this path have model='kiro-cli-agent' when the underlying model is unknown (auto-routing); known Bedrock ARNs canonicalize to their short name (e.g. claude-sonnet-4).",
231
+ "When Kiro CLI does not report token counts, tokens are estimated at ~4 chars/token from new user messages, tool outputs and assistant text. The estimate does not measure re-sent history, system context or cache usage, and can substantially undercount tool-heavy requests. contextUsage percentages are not converted to tokens because their denominator and request correspondence are not established. Dollar amounts use model rates, not Kiro credit charges; recorded usage_summary credits remain separate. Source rows use model='kiro-cli-agent' when the underlying model is unknown; known Bedrock ARNs canonicalize to their short name (e.g. claude-sonnet-4).",
232
232
  merge_policy:
233
233
  "Kiro IDE and Kiro CLI both emit source='kiro' in queue.jsonl so token, cost, heatmap, and leaderboard aggregations merge transparently. Use this block to distinguish sub-path contributions.",
234
234
  },
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs/promises");
4
+ const path = require("node:path");
5
+ const crypto = require("node:crypto");
6
+
7
+ const KIMI_MODEL_REPAIR_KEY = "kimiModelAttribution_2026_10";
8
+ const TOKEN_FIELDS = ["input_tokens", "cached_input_tokens", "cache_creation_input_tokens", "output_tokens", "reasoning_output_tokens", "total_tokens"];
9
+ const keyOf = (row) => row?.source === "kimi" && typeof row.model === "string" && typeof row.hour_start === "string"
10
+ ? `kimi|${row.model}|${row.hour_start}` : null;
11
+ const digest = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
12
+
13
+ async function readBytes(file) {
14
+ try { return await fs.readFile(file); } catch (error) { if (error.code === "ENOENT") return Buffer.alloc(0); throw error; }
15
+ }
16
+ async function readJson(file) {
17
+ const bytes = await readBytes(file);
18
+ return bytes.length ? JSON.parse(bytes.toString("utf8")) : null;
19
+ }
20
+ function latestRows(bytes) {
21
+ const rows = new Map();
22
+ for (const line of bytes.toString("utf8").split("\n")) {
23
+ let row;
24
+ try { row = JSON.parse(line); } catch { continue; }
25
+ const key = keyOf(row);
26
+ if (key) rows.set(key, row);
27
+ }
28
+ return rows;
29
+ }
30
+ function sumsByHour(rows) {
31
+ const totals = new Map();
32
+ for (const row of rows.values()) {
33
+ const total = totals.get(row.hour_start) || Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
34
+ for (const field of TOKEN_FIELDS) {
35
+ const count = Number(row[field] || 0);
36
+ if (!Number.isSafeInteger(count) || count < 0) throw new Error("Kimi repair encountered invalid token totals");
37
+ total[field] += count;
38
+ }
39
+ if (TOKEN_FIELDS.some((field) => total[field] !== 0)) totals.set(row.hour_start, total);
40
+ }
41
+ return totals;
42
+ }
43
+ function sameTotals(before, after) {
44
+ const left = sumsByHour(before), right = sumsByHour(after);
45
+ return left.size === right.size && [...left].every(([hour, total]) => right.has(hour)
46
+ && TOKEN_FIELDS.every((field) => total[field] === right.get(hour)[field]));
47
+ }
48
+ function sameSnapshot(left, right) {
49
+ return left && right && [...TOKEN_FIELDS, "conversation_count", "billable_total_tokens"]
50
+ .every((field) => Number(left[field] || 0) === Number(right[field] || 0));
51
+ }
52
+ async function fileSignature(file) {
53
+ const handle = await fs.open(file, "r");
54
+ try {
55
+ const stat = await handle.stat();
56
+ if (!stat.isFile()) throw new Error("Kimi repair source is not a file");
57
+ return { size: stat.size, ino: stat.ino, mtimeMs: stat.mtimeMs };
58
+ } finally { await handle.close(); }
59
+ }
60
+ async function writeJournal(file, journal) {
61
+ const temp = `${file}.${crypto.randomUUID()}.tmp`;
62
+ try {
63
+ await fs.writeFile(temp, JSON.stringify(journal), { mode: 0o600, flag: "wx" });
64
+ await fs.rename(temp, file);
65
+ } finally { await fs.rm(temp, { force: true }); }
66
+ }
67
+
68
+ // Caller holds sync.lock and commits cursors after this returns, before upload.
69
+ // Only append to queue.jsonl; the caller compacts its pending suffix with
70
+ // minPendingBytes:0 after cursor commit, preserving uploaded bytes and offset.
71
+ // Parsers are injected to keep rollout.js independent of its migration helper.
72
+ async function repairKimiModelAttribution({ cursors, queuePath, queueStatePath, legacyFiles = [], officialFiles = [], parseLegacy, parseOfficial, env = process.env }) {
73
+ const journalPath = `${queuePath}.kimi-model-repair.json`;
74
+ const migrations = cursors.migrations || {};
75
+ const applied = migrations[KIMI_MODEL_REPAIR_KEY];
76
+ if (applied?.status === "applied" || applied?.status === "noop") {
77
+ await fs.rm(journalPath, { force: true });
78
+ return { repaired: false, pendingCompaction: applied.pendingCompaction === true };
79
+ }
80
+ let journal = await readJson(journalPath);
81
+ if (!journal) {
82
+ const rawQueue = await readBytes(queuePath);
83
+ const live = latestRows(rawQueue);
84
+ for (const [key, bucket] of Object.entries(cursors.hourly?.buckets || {})) {
85
+ if (!key.startsWith("kimi|") || live.has(key)) continue;
86
+ const [, model, hour_start] = key.split("|");
87
+ live.set(key, { source: "kimi", model, hour_start, ...bucket.totals });
88
+ }
89
+ if (![...live.values()].some((row) => Number(row.total_tokens) > 0)) {
90
+ (cursors.migrations ||= {})[KIMI_MODEL_REPAIR_KEY] = { status: "noop", appliedAt: new Date().toISOString() };
91
+ return { repaired: false, pendingCompaction: false };
92
+ }
93
+ const files = [...new Set([...legacyFiles, ...officialFiles])];
94
+ const discovered = new Set(files);
95
+ const previousFiles = Object.entries(cursors.kimi?.fileOffsets || {}).concat(Object.entries(cursors.kimiCode?.fileOffsets || {}));
96
+ if (!officialFiles.length || previousFiles.some(([file]) => !discovered.has(file))) {
97
+ return { repaired: false, pendingCompaction: false, deferred: "missing-source" };
98
+ }
99
+ const signatures = new Map();
100
+ try {
101
+ for (const file of files) signatures.set(file, await fileSignature(file));
102
+ if (previousFiles.some(([file, prior]) => signatures.get(file).size < Number(prior.size || 0))) {
103
+ return { repaired: false, pendingCompaction: false, deferred: "truncated-source" };
104
+ }
105
+ } catch {
106
+ return { repaired: false, pendingCompaction: false, deferred: "unreadable-source" };
107
+ }
108
+ const scratch = await fs.mkdtemp(path.join(path.dirname(queuePath), ".kimi-model-repair-"));
109
+ const rebuilt = { version: 1, hourly: { version: 3, buckets: {}, groupQueued: {} } };
110
+ let corrected;
111
+ try {
112
+ const scratchQueue = path.join(scratch, "queue.jsonl");
113
+ await parseLegacy({ wireFiles: legacyFiles, cursors: rebuilt, queuePath: scratchQueue, env });
114
+ await parseOfficial({ wireFiles: officialFiles, cursors: rebuilt, queuePath: scratchQueue, env });
115
+ corrected = latestRows(await readBytes(scratchQueue));
116
+ for (const file of files) {
117
+ if (JSON.stringify(await fileSignature(file)) !== JSON.stringify(signatures.get(file))) {
118
+ return { repaired: false, pendingCompaction: false, deferred: "source-changed" };
119
+ }
120
+ }
121
+ } finally { await fs.rm(scratch, { recursive: true, force: true }); }
122
+ // Missing/corrupt history or newly appended usage must first settle through
123
+ // ordinary sync. A model-only repair may never change per-hour token totals.
124
+ if (!sameTotals(live, corrected)) return { repaired: false, pendingCompaction: false, deferred: "totals-not-conserved" };
125
+ const currentQueue = await readBytes(queuePath);
126
+ if (!currentQueue.equals(rawQueue)) return { repaired: false, pendingCompaction: false, deferred: "queue-changed" };
127
+ const state = await readJson(queueStatePath) || {};
128
+ const offset = Number(state.offset || 0);
129
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > rawQueue.length || (offset > 0 && rawQueue[offset - 1] !== 10)) {
130
+ return { repaired: false, pendingCompaction: false, deferred: "invalid-upload-offset" };
131
+ }
132
+ const corrections = [];
133
+ for (const [key, old] of live) {
134
+ if (!corrected.has(key)) corrections.push({ source: "kimi", model: old.model, hour_start: old.hour_start,
135
+ ...Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0])), billable_total_tokens: 0, conversation_count: 0 });
136
+ }
137
+ for (const [key, row] of corrected) if (!sameSnapshot(live.get(key), row)) corrections.push(row);
138
+ journal = { version: 1, uploadedOffset: offset, uploadedPrefixSha256: digest(rawQueue.subarray(0, offset)),
139
+ rows: corrections, kimi: rebuilt.kimi || {}, kimiCode: rebuilt.kimiCode || {}, hourly: rebuilt.hourly,
140
+ preparedAt: new Date().toISOString() };
141
+ await writeJournal(journalPath, journal);
142
+ }
143
+ const rawQueue = await readBytes(queuePath);
144
+ const state = await readJson(queueStatePath) || {};
145
+ if (journal.version !== 1 || !Number.isSafeInteger(journal.uploadedOffset) || journal.uploadedOffset < 0
146
+ || rawQueue.length < journal.uploadedOffset || Number(state.offset || 0) < journal.uploadedOffset
147
+ || digest(rawQueue.subarray(0, journal.uploadedOffset)) !== journal.uploadedPrefixSha256) {
148
+ return { repaired: false, pendingCompaction: false, deferred: "uploaded-prefix-changed" };
149
+ }
150
+ if (journal.rows.length) {
151
+ // A prior append can have stopped halfway through a JSON line. Preserve
152
+ // those bytes, terminate that incomplete record, then append complete LWW
153
+ // snapshots again. The journal bypasses conservation checks on partial work.
154
+ const separator = rawQueue.length && rawQueue.at(-1) !== 10 ? "\n" : "";
155
+ await fs.appendFile(queuePath, separator + journal.rows.map(JSON.stringify).join("\n") + "\n");
156
+ }
157
+ const hourly = (cursors.hourly ||= { version: 3, buckets: {}, groupQueued: {} });
158
+ for (const field of ["buckets", "groupQueued"]) {
159
+ hourly[field] ||= {};
160
+ for (const key of Object.keys(hourly[field])) if (key.startsWith("kimi|")) delete hourly[field][key];
161
+ for (const [key, value] of Object.entries(journal.hourly?.[field] || {})) if (key.startsWith("kimi|")) hourly[field][key] = value;
162
+ }
163
+ cursors.kimi = journal.kimi;
164
+ cursors.kimiCode = journal.kimiCode;
165
+ (cursors.migrations ||= {})[KIMI_MODEL_REPAIR_KEY] = {
166
+ status: "applied", appliedAt: new Date().toISOString(), correctedBuckets: journal.rows.length,
167
+ pendingCompaction: journal.rows.length > 0,
168
+ };
169
+ // Leave the journal until the next call observes the persisted marker.
170
+ return { repaired: true, pendingCompaction: journal.rows.length > 0 };
171
+ }
172
+
173
+ module.exports = { KIMI_MODEL_REPAIR_KEY, repairKimiModelAttribution };
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const { physicalJsonlRecords } = require("./jsonl-lines");
7
+ const { resolveKimiDefaultModel } = require("./rollout");
8
+ const { mergePerformance } = require("./session-performance");
9
+
10
+ function readObject(file) {
11
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return {}; }
12
+ }
13
+
14
+ function kimiSessionLocation(file) {
15
+ const official = path.basename(path.dirname(path.dirname(file))) === "agents";
16
+ const directory = official ? path.dirname(path.dirname(path.dirname(file))) : path.dirname(file);
17
+ const id = path.basename(directory);
18
+ const imported = official ? readObject(path.join(directory, "state.json"))?.custom : null;
19
+ // The official migrator keeps the legacy UUID under ses_<UUID>. Group its
20
+ // message-only mirror with the original wire, retaining any new usage tail.
21
+ const identity = imported?.imported_from_kimi_cli && typeof imported.kimi_cli_session_id === "string"
22
+ ? imported.kimi_cli_session_id : id.replace(/^(?:ses_|session_)(?=[0-9a-f-]{36}$)/i, "");
23
+ return { directory, id, identity, official, main: !official || path.basename(path.dirname(file)) === "main" };
24
+ }
25
+
26
+ function kimiSessionDependencies(files) {
27
+ return [...new Set(files.flatMap((file) => {
28
+ const location = kimiSessionLocation(file);
29
+ const root = path.dirname(path.dirname(path.dirname(location.directory)));
30
+ return [path.join(location.directory, "state.json"), path.join(root, "config.toml"), path.join(root, "kimi.json")];
31
+ }))];
32
+ }
33
+
34
+ function discoverKimiWireFiles(roots, incomplete) {
35
+ const files = new Set();
36
+ function walk(directory, depth = 0) {
37
+ if (depth > 5) return;
38
+ let entries;
39
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch (error) {
40
+ if (error.code !== "ENOENT") incomplete.push({ path: directory, error: error.code || "unreadable" });
41
+ return;
42
+ }
43
+ for (const entry of entries) {
44
+ if (entry.name.startsWith(".")) continue;
45
+ const file = path.join(directory, entry.name);
46
+ if (entry.isDirectory()) walk(file, depth + 1);
47
+ else if (entry.isFile() && entry.name === "wire.jsonl") files.add(file);
48
+ }
49
+ }
50
+ for (const root of roots) walk(path.join(root, "sessions"));
51
+ return [...files].sort();
52
+ }
53
+
54
+ const count = (value) => Number.isFinite(Number(value)) ? Math.max(0, Math.floor(Number(value))) : 0;
55
+ function kimiUsage(usage, legacy) {
56
+ if (!usage || typeof usage !== "object") return null;
57
+ let input, cached, created, output;
58
+ // Same column semantics as parseKimiIncremental / parseKimiCodeIncremental.
59
+ if (legacy || usage.inputOther != null) {
60
+ input = count(legacy ? usage.input_other : usage.inputOther);
61
+ cached = count(legacy ? usage.input_cache_read : usage.inputCacheRead);
62
+ created = count(legacy ? usage.input_cache_creation : usage.inputCacheCreation);
63
+ output = count(usage.output);
64
+ } else {
65
+ created = count(usage.cache_creation_input_tokens);
66
+ cached = count(usage.cache_read_input_tokens ?? usage.input_tokens_details?.cached_tokens);
67
+ input = Math.max(0, count(usage.input_tokens) - (usage.cache_read_input_tokens != null ? 0 : cached));
68
+ output = count(usage.output_tokens);
69
+ }
70
+ return { input_tokens: input, cached_input_tokens: cached, cache_creation_input_tokens: created,
71
+ output_tokens: output, reasoning_output_tokens: 0, total_tokens: input + cached + created + output };
72
+ }
73
+
74
+ const modelAlias = (value) => typeof value === "string" && value.trim() ? value.trim().split("/").pop() : null;
75
+ const EDIT_TOOLS = new Set(["edit", "write", "multiedit", "notebookedit", "strreplacefile", "writefile", "apply_patch"]);
76
+ const AGENT_TOOLS = new Set(["agent", "agentswarm", "task"]);
77
+ const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
78
+
79
+ // Reads both wire protocols into metadata only. A full inventory shares its
80
+ // usage-ID set, as sync does, so copied/forked step records count only once.
81
+ async function readKimiSessions(files) {
82
+ const groups = new Map();
83
+ for (const file of [...new Set(files)]) {
84
+ const location = kimiSessionLocation(file);
85
+ const group = groups.get(location.identity) || { identity: location.identity, files: [], createdAt: Infinity };
86
+ const state = location.official ? readObject(path.join(location.directory, "state.json")) : {};
87
+ group.createdAt = Math.min(group.createdAt, Number(state.createdAt) || Infinity);
88
+ group.files.push({ file, ...location, state });
89
+ groups.set(location.identity, group);
90
+ }
91
+ const seenUsage = new Set();
92
+ const seenSignals = new Set();
93
+ const rows = [];
94
+ for (const group of [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a.identity.localeCompare(b.identity))) {
95
+ const tokens = kimiUsage({}, true);
96
+ const modelUsage = new Map();
97
+ const buckets = new Map();
98
+ const times = new Set();
99
+ let turns = 0, edits = 0, retries = 0, agents = 0, errors = 0;
100
+ let lastPrompt = null;
101
+ let cwd = null;
102
+ let resumeId = null;
103
+ const modelsForEdits = new Map();
104
+ // Legacy first preserves migrated user turns; context.append_message in
105
+ // the imported wire is history, never another prompt or usage event.
106
+ group.files.sort((a, b) => Number(a.official) - Number(b.official) || Number(b.main) - Number(a.main) || a.file.localeCompare(b.file));
107
+ for (const location of group.files) {
108
+ const root = path.dirname(path.dirname(path.dirname(location.directory)));
109
+ let model = resolveKimiDefaultModel({ KIMI_HOME: root });
110
+ if (location.official && location.main) {
111
+ resumeId = location.id;
112
+ if (typeof location.state.workDir === "string") cwd = location.state.workDir;
113
+ }
114
+ if (!location.official && !cwd) {
115
+ const workspaceHash = path.basename(path.dirname(location.directory));
116
+ const workDirs = readObject(path.join(root, "kimi.json"))?.work_dirs;
117
+ const workspace = Array.isArray(workDirs) && workDirs.find((row) => typeof row?.path === "string"
118
+ && crypto.createHash("md5").update(row.path).digest("hex") === workspaceHash);
119
+ if (workspace) cwd = workspace.path;
120
+ }
121
+ let turnOpen = false, hadEdit = false, turnModel = model;
122
+ const closeTurn = () => {
123
+ if (hadEdit) { edits++; modelsForEdits.set(turnModel, (modelsForEdits.get(turnModel) || 0) + 1); }
124
+ hadEdit = false; turnOpen = false;
125
+ };
126
+ const input = fs.createReadStream(location.file);
127
+ for await (const { line, terminated } of physicalJsonlRecords(input)) {
128
+ if (!terminated || !line) continue;
129
+ let entry;
130
+ try { entry = JSON.parse(line); } catch { continue; }
131
+ if (!entry || typeof entry !== "object") continue;
132
+ const message = entry.message && typeof entry.message === "object" ? entry.message : null;
133
+ const event = entry.type === "context.append_loop_event" ? entry.event : entry;
134
+ const timestampMs = Number(location.official ? entry.time ?? event?.time : entry.timestamp ?? message?.payload?.timestamp) * (location.official ? 1 : 1000);
135
+ const timestamp = Number.isFinite(timestampMs) && timestampMs > 0 && timestampMs <= 8640000000000000
136
+ ? new Date(timestampMs).toISOString() : null;
137
+ if (timestamp) times.add(timestampMs);
138
+ if (["config.update", "profile.bind", "llm.request"].includes(entry.type)) {
139
+ model = modelAlias(entry.modelAlias || entry.model) || model;
140
+ if (location.main && typeof (entry.cwd || entry.environmentDisclosure?.cwd) === "string") cwd = entry.cwd || entry.environmentDisclosure.cwd;
141
+ }
142
+ const isLegacyUsage = message?.type === "StatusUpdate";
143
+ const usage = isLegacyUsage ? message.payload?.token_usage : event?.type === "step.end" ? event.usage : null;
144
+ const usageId = isLegacyUsage ? message.payload?.message_id : event?.uuid;
145
+ if (usage && typeof usageId === "string" && timestamp) {
146
+ const ids = [isLegacyUsage ? `message:${usageId}` : `step:${usageId}`];
147
+ if (!isLegacyUsage && typeof event.messageId === "string") ids.push(`message:${event.messageId}`);
148
+ const delta = kimiUsage(usage, isLegacyUsage);
149
+ if (delta.total_tokens > 0 && !ids.some((id) => seenUsage.has(id))) {
150
+ for (const id of ids) seenUsage.add(id);
151
+ const modelRow = modelUsage.get(model) || { model, ...kimiUsage({}, true), usage_events: 0 };
152
+ const bucketTime = new Date(Math.floor(timestampMs / 60000) * 60000).toISOString();
153
+ const key = `${model}\0${bucketTime}`;
154
+ const bucket = buckets.get(key) || { model, timestamp: bucketTime, ...kimiUsage({}, true), usage_events: 0 };
155
+ for (const field of Object.keys(delta)) { tokens[field] += delta[field]; modelRow[field] += delta[field]; bucket[field] += delta[field]; }
156
+ modelRow.usage_events++; bucket.usage_events++;
157
+ const streamMs = count(event.llmStreamDurationMs);
158
+ const firstMs = Number(event.llmFirstTokenLatencyMs);
159
+ const performance = {
160
+ ...(delta.output_tokens >= 200 && streamMs >= 1000 && streamMs <= 3600000
161
+ ? { estimated_output_tokens: delta.output_tokens, estimated_duration_ms: streamMs, estimated_request_count: 1 } : {}),
162
+ ...(event.llmFirstTokenLatencyMs != null && Number.isFinite(firstMs) && firstMs >= 0
163
+ ? { first_response_total_ms: firstMs, first_response_sample_count: 1 } : {}),
164
+ };
165
+ modelRow.performance = mergePerformance(modelRow.performance, performance);
166
+ modelUsage.set(model, modelRow); buckets.set(key, bucket);
167
+ turnModel = model;
168
+ }
169
+ }
170
+ if (!location.main) continue;
171
+ const signalId = hash(line);
172
+ if (seenSignals.has(signalId)) continue;
173
+ seenSignals.add(signalId);
174
+ const prompt = message?.type === "TurnBegin" ? message.payload?.user_input
175
+ : entry.type === "turn.prompt" && (!entry.origin?.kind || entry.origin.kind === "user") ? entry.input : null;
176
+ if (prompt != null) {
177
+ closeTurn(); turns++; turnOpen = true; turnModel = model;
178
+ // Fingerprints live only for this scan; no text or hash is persisted.
179
+ const fingerprint = hash(JSON.stringify(prompt));
180
+ if (lastPrompt && fingerprint === lastPrompt) retries++;
181
+ lastPrompt = fingerprint;
182
+ }
183
+ const tool = message?.type === "ToolCall" ? message.payload?.function?.name : event?.type === "tool.call" ? event.name : null;
184
+ if (typeof tool === "string") {
185
+ const name = tool.toLowerCase();
186
+ if (turnOpen && EDIT_TOOLS.has(name)) hadEdit = true;
187
+ if (AGENT_TOOLS.has(name)) agents++;
188
+ }
189
+ if (entry.type === "turn.step.retrying" || message?.type === "StepInterrupted") errors++;
190
+ if (message?.type === "TurnEnd" || entry.type === "turn.ended") closeTurn();
191
+ }
192
+ closeTurn();
193
+ }
194
+ const orderedTimes = [...times].sort((a, b) => a - b);
195
+ let activeMs = 0;
196
+ for (let i = 1; i < orderedTimes.length; i++) {
197
+ const gap = orderedTimes[i] - orderedTimes[i - 1];
198
+ if (gap <= 30 * 60 * 1000) activeMs += gap;
199
+ }
200
+ const models = [...modelUsage.values()].map((row) => ({ ...row, edit_turns: modelsForEdits.get(row.model) || 0 }));
201
+ if (!tokens.total_tokens && !turns) continue;
202
+ rows.push({ identity: group.identity, files: group.files.map((row) => row.file), session_id: resumeId,
203
+ title: null, project_ref: cwd, model: models.length === 1 ? models[0].model : "unknown", model_usage: models,
204
+ usage_buckets: [...buckets.values()], tokens, turns, edit_turns: edits, retry_turns: retries,
205
+ subagent_calls: agents, subagent_types: {}, error_count: errors,
206
+ usage_events: models.reduce((sum, row) => sum + row.usage_events, 0),
207
+ started_at: orderedTimes.length ? new Date(orderedTimes[0]).toISOString() : null,
208
+ ended_at: orderedTimes.length ? new Date(orderedTimes.at(-1)).toISOString() : null, active_ms: activeMs,
209
+ performance: mergePerformance(...models.map((row) => row.performance)),
210
+ provenance: { source: "local-session-log", confidence: "observed", retry_confidence: "inferred", content_retained: false, usage: "kimi-wire-metadata" },
211
+ });
212
+ }
213
+ return rows;
214
+ }
215
+
216
+ module.exports = { discoverKimiWireFiles, kimiSessionDependencies, readKimiSessions };
@@ -13,6 +13,7 @@ const {
13
13
  normalizeUsageScope,
14
14
  } = require("./source-metadata");
15
15
  const { accountSlugFor, fetchAccountUsage, mintAccessToken, invalidateCloudAccountPayloadCache, getCloudAccountSessionGeneration } = require("./cloud-account");
16
+ const { resetCloudDeviceUsage, readCloudUsageReset } = require("./cloud-usage-reset");
16
17
  const { getOrCreateMachineId, computeStableMachineId } = require("./machine-id");
17
18
  const { functionUrlFor, fetchFunctionResponse } = require("./function-url");
18
19
  const { createCloudDeviceTokenStore } = require("./cloud-device-token");
@@ -2162,6 +2163,39 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2162
2163
  return true;
2163
2164
  }
2164
2165
 
2166
+ if (p === "/functions/tokentracker-cloud-usage-reset") {
2167
+ if (!isAuthorizedLocalMutation(req) || req.headers["sec-fetch-site"] === "cross-site") { json(res, { error: "Unauthorized" }, 401); return true; }
2168
+ try {
2169
+ if (String(req.method).toUpperCase() === "GET") {
2170
+ json(res, { pending: Boolean(readCloudUsageReset(trackerDataDir)) }); return true;
2171
+ }
2172
+ if (String(req.method).toUpperCase() !== "POST") { json(res, { error: "Method Not Allowed" }, 405); return true; }
2173
+ const body = await readJsonBody(req);
2174
+ const session = localCloudSession;
2175
+ if (!getCloudSyncPref() || !session || body.cloudSessionId !== session.id || body.expectedOwnerId !== session.ownerId) {
2176
+ json(res, { code: "auth_session_changed" }, 409); return true;
2177
+ }
2178
+ const metadata = localSyncDeviceMetadata.get(session.token);
2179
+ const accessToken = String(req.headers.authorization || "").replace(/^Bearer /i, "").trim();
2180
+ const runtime = assertInsforgeRuntime(getRuntime());
2181
+ let claims;
2182
+ try { claims = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString("utf8")); } catch { /* rejected below */ }
2183
+ if (claims?.sub !== session.ownerId || !Number.isFinite(claims?.exp) || claims.exp * 1000 <= Date.now()) {
2184
+ json(res, { code: "auth_required" }, 401); return true;
2185
+ }
2186
+ const isCurrent = () => getCloudSyncPref() && localCloudSession === session && session.generation === localCloudGeneration &&
2187
+ claims.exp * 1000 > Date.now() && getRuntime().baseUrl === session.baseUrl;
2188
+ const result = await resetCloudDeviceUsage({ trackerDir: trackerDataDir, ownerId: session.ownerId,
2189
+ deviceId: metadata?.deviceId, baseUrl: session.baseUrl, anonKey: runtime.anonKey, accessToken, isCurrent });
2190
+ if (!isCurrent()) { json(res, { code: "auth_session_changed" }, 409); return true; }
2191
+ clearLocalSyncDeviceTokenCache();
2192
+ invalidateAfterAccountUpload();
2193
+ invalidateCloudAccountPayloadCache();
2194
+ json(res, { ...result, pending: true });
2195
+ } catch (error) { json(res, { error: error.code || "cloud_usage_reset_failed", code: error.code || null }, error.status || 502); }
2196
+ return true;
2197
+ }
2198
+
2165
2199
  // --- local-sync (POST) ---
2166
2200
  if (p === "/functions/tokentracker-local-sync") {
2167
2201
  if (String(req.method || "GET").toUpperCase() !== "POST") {
@@ -2263,10 +2297,20 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2263
2297
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) {
2264
2298
  json(res, { ok: false, code: "auth_session_changed" }, 409); return true;
2265
2299
  }
2300
+ if (extraEnv.TOKENTRACKER_DEVICE_TOKEN) {
2301
+ const metadata = localSyncDeviceMetadata.get(extraEnv.TOKENTRACKER_DEVICE_TOKEN);
2302
+ if (metadata?.deviceId) extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID = metadata.deviceId;
2303
+ }
2266
2304
  await Promise.all([...activeCloudSyncs].filter(controller => controller.signal.aborted).map(controller => controller.done).filter(Boolean));
2267
2305
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 });
2268
2306
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && localSyncDeviceTokenUserId) {
2269
- const binding = JSON.stringify({ ownerId: localSyncDeviceTokenUserId, baseUrl: localSyncBaseUrl || getRuntime().baseUrl });
2307
+ const uploadBaseUrl = localSyncBaseUrl || getRuntime().baseUrl;
2308
+ const reset = readCloudUsageReset(trackerDataDir);
2309
+ if (reset && (reset.ownerId !== localSyncDeviceTokenUserId || reset.baseUrl.replace(/\/$/, "") !== uploadBaseUrl.replace(/\/$/, "") ||
2310
+ (extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID && reset.deviceId !== extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID))) {
2311
+ json(res, { ok: false, code: "cloud_usage_reset_owner_changed" }, 409); return true;
2312
+ }
2313
+ const binding = JSON.stringify({ ownerId: localSyncDeviceTokenUserId, baseUrl: uploadBaseUrl });
2270
2314
  const ownerPath = path.join(path.dirname(qp), "cloud-upload-owner.json");
2271
2315
  let previous = "";
2272
2316
  try { previous = fs.readFileSync(ownerPath, "utf8"); } catch { /* an unbound legacy cursor cannot prove its previous owner */ }
@@ -2279,6 +2323,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2279
2323
  const controller = new AbortController();
2280
2324
  if (extraEnv.TOKENTRACKER_DEVICE_TOKEN) activeCloudSyncs.add(controller);
2281
2325
  const syncStartedAt = Date.now();
2326
+ if (localSyncDeviceTokenUserId) extraEnv.TOKENTRACKER_CLOUD_OWNER_ID = localSyncDeviceTokenUserId;
2282
2327
  extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID = crypto.randomUUID();
2283
2328
  const syncPromise = runSyncCommand(extraEnv, {
2284
2329
  signal: controller.signal,