tokentracker-cli 1.5.15 → 1.5.17

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 (118) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-CwjI28oT.js → AchievementBadge-BiEoQE7Z.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-Dq544fjX.js → AchievementsPage-4g0Q8O0U.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DufV6nod.js → AchievementsSection-D5dyOWFj.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-vw8xydKu.js → BadgeDetailModal-Bz067XUw.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-De0anwsh.js → BotAnimated-CFiGrlrg.js} +1 -1
  6. package/dashboard/dist/assets/{Card-CuSeWBDM.js → Card-CkYKahsn.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-CxFcOwAj.js → CloudCheckoutPage-BCzIRPkW.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-Beb7_JLW.js → CloudPage-C0MZTK-9.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-PsUq2iRS.js → CommandPalette-DwSGE7c-.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-b54WaPbK.js → CommunityStatsModal-B80ps9B5.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-_NdunIh5.js → ConfirmModal-xthCJoY5.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-5Is6MWln.js → Controls-DZ9vKkCI.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-C0n0qC7h.js → DashboardPage-C4SwA5lD.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-C3WfGy-0.js → DateRangePopover-D8ihZ5SK.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-Vn6k-mGd.js → DevicePage-OVnpUpAc.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-CXttnna-.js → DialogClose-DpueWMdH.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-CNb40Z5w.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-B0cNIo0E.js → DialogTitle-Be_-fYZA.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-DC3XSfIu.js → ElectricLogo-BJek75ac.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-H7S3TMHi.js → FadeIn-HIpnpyJ-.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-xgbIo5OP.js → HeaderGithubStar-Dj0guZ3R.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-CpzM5Dju.js → HoverTooltip-B3p_QB2m.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-BdV7S4dY.js → IpCheckPage-DrxLV2uL.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-D9f28ryO.js → LandingPage-C3RPJvXW.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage--WUFpkBI.js → LeaderboardPage-CkVKRCA7.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-CEvrQ75Y.js → LeaderboardProBadge-C39qylf0.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-Bn0HFr5h.js → LeaderboardProfileModal-D1DEHNRN.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-DJ7fQI68.js → LeaderboardProfilePage-CoBBl1-f.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-7tPDheuX.js → LimitsPage-BnjPGpcE.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-DlGbVQS8.js → LocalOnlyNotice-CqCRY_ls.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-BZZmzUGh.js → LoginCard-BeA3kcgy.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BT5_bIxU.js → LoginModal-D9S7DnuH.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-DMjDUPPi.js → LoginPage-CXRATGB_.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-Dk3j6ITn.js → PetPage-0_l-kTr4.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-pugRu3RC.js → PopoverPopup-CfPgHc44.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-CtmL1CmY.js → ProviderIcon-DtjD5jVb.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BB6U7t8P.js → ResetPasswordPage-Bt8QpFOe.js} +1 -1
  38. package/dashboard/dist/assets/{Select-7w1su8lw.js → Select-BgQXA6kK.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-CaEUmWkj.js → SelectItemText-C7hWQpTf.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-DctWoF-A.js → SelfHostPage-CUK_eL6K.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-Dg3ljEXC.js → ServiceStatusPage-CF0nAsAf.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-B1dvW-be.js → SessionsPage-CE2cz-4_.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-DuXSX1R2.js → SettingsPage-su2wAKfz.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CvXQFgc9.js → SkillsPage-Bp0PnSf_.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-DKC8Rk_Y.js → TokenGalaxy-CjY25cur.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-Dp9EpHLG.js → TooltipPopup-DDztBp5x.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-DWnlBn3H.js → TrendMonitor-CDaOhv8o.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-GECNmb1Q.js → WidgetsPage-CjaQe3QN.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-Dq_1lOnb.js → WrappedPage-P4LVouC8.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-CkF0pdTJ.js → __vite-browser-external-CzHn8J8J.js} +2 -2
  51. package/dashboard/dist/assets/{agent-logos-Dqe3Vrah.js → agent-logos-CS9Iaf0t.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-left-Cr_2QnDu.js → arrow-left-nH7mwAsq.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-right-BsK2sJgt.js → arrow-right-BoRjpMCP.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-DkWMGqXo.js → arrow-up-right-Cg0soM7i.js} +1 -1
  55. package/dashboard/dist/assets/{calendar-days-BB9VV438.js → calendar-days-6BSY0SCe.js} +1 -1
  56. package/dashboard/dist/assets/{check-bpSCJZWh.js → check-Ldxu9X3c.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-down-CtECtDmx.js → chevron-down-Cl1UIsFC.js} +1 -1
  58. package/dashboard/dist/assets/{cloud-checkout-C4PrzKhN.js → cloud-checkout-CgoKsrLa.js} +1 -1
  59. package/dashboard/dist/assets/{composite-BHisG82k.js → composite-CqtsbFSD.js} +1 -1
  60. package/dashboard/dist/assets/{copy-Dmxzd-Sw.js → copy-CHDv4TDb.js} +1 -1
  61. package/dashboard/dist/assets/{download-CkdQzw2w.js → download-bExf6Ccu.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-C971FjvC.js → external-link-DPe3LHcW.js} +1 -1
  63. package/dashboard/dist/assets/{flame-mKbp7kxw.js → flame-CYUEYQbn.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-C35FrJnH.js → format-tokens-CJ73U0H5.js} +1 -1
  65. package/dashboard/dist/assets/{gift-BX2lSOmc.js → gift-BCmo7fKV.js} +1 -1
  66. package/dashboard/dist/assets/{icons-ov92u52T.js → icons-BtAa7a8Q.js} +1 -1
  67. package/dashboard/dist/assets/{index-Bgm38oHe.js → index--VJpPvce.js} +1 -1
  68. package/dashboard/dist/assets/{index-DF03nIHb.js → index-CKwNx_s1.js} +1 -1
  69. package/dashboard/dist/assets/{index-Ca5xkNrK.js → index-CSFt_XvV.js} +1 -1
  70. package/dashboard/dist/assets/{index-9OCL-BL8.js → index-CyhRNpQp.js} +1 -1
  71. package/dashboard/dist/assets/{info-B-f9hWcw.js → info-Cc5_kre6.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-l1wuezTo.js → link-2-_RZOZ7kJ.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-Lmjrs8XX.js → loader-circle-7GinPf0v.js} +1 -1
  74. package/dashboard/dist/assets/{local-usage-transfer-Ds6cF41d.js → local-usage-transfer-Cx5WBzma.js} +1 -1
  75. package/dashboard/dist/assets/{modifiers.esm-tVlltJXO.js → modifiers.esm-CO1bm3IT.js} +1 -1
  76. package/dashboard/dist/assets/{monitor-smartphone-XRiYiY8u.js → monitor-smartphone-CjBB2EQT.js} +1 -1
  77. package/dashboard/dist/assets/{play-DaD9EMjk.js → play-B9AHJ0ir.js} +1 -1
  78. package/dashboard/dist/assets/{plus-D30fbsKQ.js → plus-BYHC32xe.js} +1 -1
  79. package/dashboard/dist/assets/{provider-display-CKdhBOwz.js → provider-display-N2l_YZhb.js} +1 -1
  80. package/dashboard/dist/assets/{react-BY0y0K4x.js → react-DSS6zSQ7.js} +1 -1
  81. package/dashboard/dist/assets/{safePolygon-CSG_P7rG.js → safePolygon-BdbLh1jE.js} +1 -1
  82. package/dashboard/dist/assets/{search-Kiq8wVHA.js → search-D89mjm04.js} +1 -1
  83. package/dashboard/dist/assets/{server-CjovGBGz.js → server-PR9yTCiu.js} +1 -1
  84. package/dashboard/dist/assets/{shield-check-DF9MmMCq.js → shield-check-BYLC3hFn.js} +1 -1
  85. package/dashboard/dist/assets/{skills-api-CEzr6p92.js → skills-api-CM8yP8ZQ.js} +1 -1
  86. package/dashboard/dist/assets/{sortable.esm-D7jd9mE_.js → sortable.esm-4l8igdkd.js} +1 -1
  87. package/dashboard/dist/assets/{store-D9_jo_do.js → store-uku0hfBl.js} +1 -1
  88. package/dashboard/dist/assets/{terminal-CLOlvzR-.js → terminal-CQsyMIKh.js} +1 -1
  89. package/dashboard/dist/assets/{trash-DIe0fsDD.js → trash-Du6DwmPp.js} +1 -1
  90. package/dashboard/dist/assets/{use-community-stats-gDkdiRBz.js → use-community-stats-BI2Vkwhm.js} +1 -1
  91. package/dashboard/dist/assets/{use-latest-request-guard-tXGuKvua.js → use-latest-request-guard-AGXK-7W_.js} +1 -1
  92. package/dashboard/dist/assets/{use-limits-display-prefs-CCuLFy00.js → use-limits-display-prefs-XaEErSEN.js} +1 -1
  93. package/dashboard/dist/assets/{use-native-settings-hgYqaiR7.js → use-native-settings-BzNwI3lk.js} +1 -1
  94. package/dashboard/dist/assets/{use-ordered-list-D6-YzYJI.js → use-ordered-list-BUkFot2m.js} +1 -1
  95. package/dashboard/dist/assets/use-reduced-motion-Dl-5CnUW.js +1 -0
  96. package/dashboard/dist/assets/{use-transform-BEiXvAuE.js → use-transform-CRTGmr0C.js} +1 -1
  97. package/dashboard/dist/assets/{use-usage-limits-BlwtyPtT.js → use-usage-limits-DMHAKEU7.js} +1 -1
  98. package/dashboard/dist/assets/{useCurrency-CwCGJLHb.js → useCurrency-D7bYEIAY.js} +1 -1
  99. package/dashboard/dist/assets/{usePositioner-BqHtRIrE.js → usePositioner-PRBIy_6t.js} +1 -1
  100. package/dashboard/dist/assets/{useTokenFormat-BUNBUX-x.js → useTokenFormat-CGacmoDz.js} +1 -1
  101. package/dashboard/dist/assets/{zap-C0RFs2qQ.js → zap-B8svUQzd.js} +1 -1
  102. package/dashboard/dist/index.html +1 -1
  103. package/dashboard/dist/ip-check.html +1 -1
  104. package/dashboard/dist/leaderboard.html +1 -1
  105. package/dashboard/dist/share.html +1 -1
  106. package/package.json +1 -1
  107. package/src/lib/ark-agent-plan-limits.js +26 -55
  108. package/src/lib/ark-coding-plan-limits.js +31 -62
  109. package/src/lib/cloud-account.js +29 -3
  110. package/src/lib/dsh-session.js +81 -0
  111. package/src/lib/local-api.js +92 -5
  112. package/src/lib/pricing/curated-overrides.json +8 -0
  113. package/src/lib/pricing/seed-snapshot.json +1 -1
  114. package/src/lib/qoder-limits.js +35 -69
  115. package/src/lib/session-analytics.js +4 -1
  116. package/src/lib/session-performance.js +15 -0
  117. package/dashboard/dist/assets/DialogDescription-C3KGos_G.js +0 -1
  118. package/dashboard/dist/assets/use-reduced-motion-DfbDxs5a.js +0 -1
@@ -26,6 +26,7 @@ const os = require("node:os");
26
26
  const path = require("node:path");
27
27
 
28
28
  const { runCommand, resolveBinaryPath, statBinaryInDirs, commonGlobalBinDirectories, resolvedCliEnvironment } = require("./command-runner");
29
+ const { createLimitsCache } = require("./limits-cache");
29
30
 
30
31
  const ARK_LIMITS_CACHE_FILE = "ark-agent-plan-limits-cache.json";
31
32
  const ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
@@ -188,68 +189,38 @@ function normalizeArkAgentPlanResponse(body) {
188
189
  // Alias for shared shape
189
190
  const normalizeArkCodingPlanResponse = normalizeArkAgentPlanResponse;
190
191
 
192
+ // Deliberately fail-open on identity: the guard only applies when `profile
193
+ // show` could establish the current identity — when it also failed, serving
194
+ // the stale cache beats erroring out (availability over strictness). There is
195
+ // no global max-age: dated windows are bounded by their own reset stamp and
196
+ // undated ones by the 12h unknown-reset TTL.
197
+ const arkLimitsCache = createLimitsCache({
198
+ file: ARK_LIMITS_CACHE_FILE,
199
+ key: null,
200
+ maxAgeMs: null,
201
+ unknownResetTtlMs: ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS,
202
+ fields: { plan_label: "string", product: "string", profile_identity: "string" },
203
+ windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
204
+ identityField: "profile_identity",
205
+ identityRequired: false,
206
+ // The normalizer throws when no periods survive, so the write guard skips
207
+ // the window check the other providers need.
208
+ writeGuard: (record) => Boolean(record?.configured) && !record.error,
209
+ cachedFlag: true,
210
+ stale: true,
211
+ source: { const: "disk-cache" },
212
+ });
213
+
191
214
  function arkAgentPlanCachePath({ home = os.homedir() } = {}) {
192
- return path.join(home, ".tokentracker", "tracker", ARK_LIMITS_CACHE_FILE);
215
+ return arkLimitsCache.path({ home });
193
216
  }
194
217
 
195
218
  function readArkAgentPlanLimitsCache({ home = os.homedir(), nowMs = Date.now(), profileIdentity } = {}) {
196
- try {
197
- const parsed = JSON.parse(fs.readFileSync(arkAgentPlanCachePath({ home }), "utf8"));
198
- if (profileIdentity && parsed?.profile_identity !== profileIdentity) return null;
199
- const cachedAtMs = Date.parse(parsed?.cached_at || "");
200
- if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
201
- const windows = [parsed?.primary_window, parsed?.secondary_window, parsed?.tertiary_window];
202
- const surviving = windows.map((window) => {
203
- if (!window) return null;
204
- const resetAtMs = Date.parse(window.reset_at || "");
205
- if (Number.isFinite(resetAtMs) && resetAtMs <= nowMs) return null;
206
- return window;
207
- });
208
- if (surviving.every((window) => !window)) return null;
209
- const bounded = surviving.map((window) => {
210
- if (!window) return null;
211
- return Number.isFinite(Date.parse(window.reset_at || ""))
212
- || nowMs - cachedAtMs <= ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS
213
- ? window
214
- : null;
215
- });
216
- if (bounded.every((window) => !window)) return null;
217
- return {
218
- configured: true,
219
- error: null,
220
- plan_label: typeof parsed?.plan_label === "string" ? parsed.plan_label : null,
221
- product: typeof parsed?.product === "string" ? parsed.product : null,
222
- primary_window: bounded[0],
223
- secondary_window: bounded[1],
224
- tertiary_window: bounded[2],
225
- cached: true,
226
- stale: true,
227
- cached_at: parsed.cached_at,
228
- source: "disk-cache",
229
- };
230
- } catch (_error) {
231
- return null;
232
- }
219
+ return arkLimitsCache.read({ home, nowMs, identity: profileIdentity });
233
220
  }
234
221
 
235
222
  function writeArkAgentPlanLimitsCache(limits, { home = os.homedir(), nowMs = Date.now() } = {}) {
236
- if (!limits?.configured || limits.error) return;
237
- const cachePath = arkAgentPlanCachePath({ home });
238
- const payload = {
239
- plan_label: limits.plan_label || null,
240
- product: limits.product || null,
241
- profile_identity: limits.profile_identity || null,
242
- primary_window: limits.primary_window || null,
243
- secondary_window: limits.secondary_window || null,
244
- tertiary_window: limits.tertiary_window || null,
245
- cached_at: new Date(nowMs).toISOString(),
246
- };
247
- try {
248
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
249
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
250
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
251
- fs.renameSync(tmpPath, cachePath);
252
- } catch (_error) {}
223
+ arkLimitsCache.write(limits, { home, nowMs });
253
224
  }
254
225
 
255
226
  function hasArkCliInstallEvidence({ home = os.homedir(), platform = process.platform } = {}) {
@@ -26,6 +26,7 @@ const os = require("node:os");
26
26
  const path = require("node:path");
27
27
 
28
28
  const { runCommand, resolveBinaryPath, statBinaryInDirs, commonGlobalBinDirectories, resolvedCliEnvironment } = require("./command-runner");
29
+ const { createLimitsCache } = require("./limits-cache");
29
30
 
30
31
  const ARK_LIMITS_CACHE_FILE = "ark-coding-plan-limits-cache.json";
31
32
  const ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
@@ -212,75 +213,43 @@ function normalizeArkCodingPlanResponse(body) {
212
213
  };
213
214
  }
214
215
 
216
+ // Deliberately fail-open on identity: the guard only applies when `profile
217
+ // show` could establish the current identity — when it also failed, serving
218
+ // the stale cache beats erroring out (availability over strictness). There is
219
+ // no global max-age: dated windows are bounded by their own reset stamp and
220
+ // undated ones by the 12h unknown-reset TTL.
221
+
222
+ // A window whose reset_at has passed is stale — the quota has already rolled
223
+ // over, so serving its old used_percent would mislead. Undated windows can't be
224
+ // checked against a reset, so each one is dropped when the snapshot is too old;
225
+ // a future-dated sibling must not keep it alive.
226
+ const arkLimitsCache = createLimitsCache({
227
+ file: ARK_LIMITS_CACHE_FILE,
228
+ key: null,
229
+ maxAgeMs: null,
230
+ unknownResetTtlMs: ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS,
231
+ fields: { plan_label: "string", product: "string", profile_identity: "string" },
232
+ windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
233
+ identityField: "profile_identity",
234
+ identityRequired: false,
235
+ // The normalizer throws when no periods survive, so the write guard skips
236
+ // the window check the other providers need.
237
+ writeGuard: (record) => Boolean(record?.configured) && !record.error,
238
+ cachedFlag: true,
239
+ stale: true,
240
+ source: { const: "disk-cache" },
241
+ });
242
+
215
243
  function arkCodingPlanCachePath({ home = os.homedir() } = {}) {
216
- return path.join(home, ".tokentracker", "tracker", ARK_LIMITS_CACHE_FILE);
244
+ return arkLimitsCache.path({ home });
217
245
  }
218
246
 
219
247
  function readArkCodingPlanLimitsCache({ home = os.homedir(), nowMs = Date.now(), profileIdentity } = {}) {
220
- try {
221
- const parsed = JSON.parse(fs.readFileSync(arkCodingPlanCachePath({ home }), "utf8"));
222
- // Deliberately fail-open: the guard only applies when `profile show`
223
- // could establish the current identity. When it also failed, serving
224
- // the stale cache beats erroring out — availability over strictness.
225
- if (profileIdentity && parsed?.profile_identity !== profileIdentity) return null;
226
- const cachedAtMs = Date.parse(parsed?.cached_at || "");
227
- if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
228
- // A window whose reset_at has passed is stale — the quota has already
229
- // rolled over, so serving its old used_percent would mislead. Drop it.
230
- const windows = [parsed?.primary_window, parsed?.secondary_window, parsed?.tertiary_window];
231
- const surviving = windows.map((window) => {
232
- if (!window) return null;
233
- const resetAtMs = Date.parse(window.reset_at || "");
234
- if (Number.isFinite(resetAtMs) && resetAtMs <= nowMs) return null;
235
- return window;
236
- });
237
- if (surviving.every((window) => !window)) return null;
238
- // Undated windows can't be checked against a reset, so drop each one when
239
- // the snapshot is too old. A future-dated sibling must not keep it alive.
240
- const bounded = surviving.map((window) => {
241
- if (!window) return null;
242
- return Number.isFinite(Date.parse(window.reset_at || ""))
243
- || nowMs - cachedAtMs <= ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS
244
- ? window
245
- : null;
246
- });
247
- if (bounded.every((window) => !window)) return null;
248
- return {
249
- configured: true,
250
- error: null,
251
- plan_label: typeof parsed?.plan_label === "string" ? parsed.plan_label : null,
252
- product: typeof parsed?.product === "string" ? parsed.product : null,
253
- primary_window: bounded[0],
254
- secondary_window: bounded[1],
255
- tertiary_window: bounded[2],
256
- cached: true,
257
- stale: true,
258
- cached_at: parsed.cached_at,
259
- source: "disk-cache",
260
- };
261
- } catch (_error) {
262
- return null;
263
- }
248
+ return arkLimitsCache.read({ home, nowMs, identity: profileIdentity });
264
249
  }
265
250
 
266
251
  function writeArkCodingPlanLimitsCache(limits, { home = os.homedir(), nowMs = Date.now() } = {}) {
267
- if (!limits?.configured || limits.error) return;
268
- const cachePath = arkCodingPlanCachePath({ home });
269
- const payload = {
270
- plan_label: limits.plan_label || null,
271
- product: limits.product || null,
272
- profile_identity: limits.profile_identity || null,
273
- primary_window: limits.primary_window || null,
274
- secondary_window: limits.secondary_window || null,
275
- tertiary_window: limits.tertiary_window || null,
276
- cached_at: new Date(nowMs).toISOString(),
277
- };
278
- try {
279
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
280
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
281
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
282
- fs.renameSync(tmpPath, cachePath);
283
- } catch (_error) {}
252
+ arkLimitsCache.write(limits, { home, nowMs });
284
253
  }
285
254
 
286
255
  // runCommand / whichBinary / resolveBinaryPath now live in ./command-runner,
@@ -216,6 +216,28 @@ function csrfTokenFromRefreshPayload(data) {
216
216
  return raw && raw.length > 0 ? raw : null;
217
217
  }
218
218
 
219
+ function userFromRefreshPayload(data) {
220
+ if (!data || typeof data !== "object") return null;
221
+ const raw = data.user && typeof data.user === "object" && !Array.isArray(data.user)
222
+ ? data.user
223
+ : data.session && typeof data.session === "object" && data.session.user && typeof data.session.user === "object" && !Array.isArray(data.session.user)
224
+ ? data.session.user
225
+ : null;
226
+ return raw;
227
+ }
228
+
229
+ function userFromAccessToken(accessToken) {
230
+ try {
231
+ const payload = JSON.parse(Buffer.from(String(accessToken).split(".")[1], "base64url").toString("utf8"));
232
+ if (typeof payload?.sub !== "string" || !payload.sub) return null;
233
+ const user = { id: payload.sub };
234
+ if (typeof payload.email === "string" && payload.email) user.email = payload.email;
235
+ return user;
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+
219
241
  /**
220
242
  * Perform the actual refresh POST. Throws a classified `AccountAuthError` on
221
243
  * every failure so callers can distinguish a transient outage (timeout,
@@ -283,7 +305,8 @@ async function performMint({ root, anonKey, refreshToken, fetchImpl, now, timeou
283
305
  const rotated = refreshTokenFromRefreshPayload(data);
284
306
  // The caller persists a rotation before its next account read. Key the
285
307
  // still-valid access token by that current refresh token, not the consumed one.
286
- tokenCache = { cacheKey: `${root}\0${rotated || refreshToken}`, accessToken, expMs };
308
+ const user = userFromRefreshPayload(data) || userFromAccessToken(accessToken);
309
+ tokenCache = { cacheKey: `${root}\0${rotated || refreshToken}`, accessToken, expMs, user };
287
310
  return {
288
311
  accessToken,
289
312
  refreshToken: rotated && rotated !== refreshToken ? rotated : null,
@@ -292,6 +315,9 @@ async function performMint({ root, anonKey, refreshToken, fetchImpl, now, timeou
292
315
  // relayed (refresh, csrf) pair in sync — a stale relayed csrf breaks the
293
316
  // dashboard's cookie-path refresh with 403 Invalid CSRF.
294
317
  csrfToken: csrfTokenFromRefreshPayload(data),
318
+ // The InsForge SDK's refreshSession persists the session only when the
319
+ // response includes a user object alongside the access token.
320
+ user,
295
321
  };
296
322
  }
297
323
 
@@ -303,7 +329,7 @@ async function performMint({ root, anonKey, refreshToken, fetchImpl, now, timeou
303
329
  *
304
330
  * @param {boolean} [throwOnFailure] when true, reject with a classified
305
331
  * `AccountAuthError` instead of resolving to `null`.
306
- * @returns {Promise<{accessToken: string, refreshToken: string|null, csrfToken: string|null}|null>}
332
+ * @returns {Promise<{accessToken: string, refreshToken: string|null, csrfToken: string|null, user: object|null}|null>}
307
333
  * null when no refresh token is available or the refresh failed.
308
334
  */
309
335
  async function mintAccessToken({
@@ -327,7 +353,7 @@ async function mintAccessToken({
327
353
  tokenCache.accessToken &&
328
354
  tokenCache.expMs - skewMs > now()
329
355
  ) {
330
- return { accessToken: tokenCache.accessToken, refreshToken: null, csrfToken: null };
356
+ return { accessToken: tokenCache.accessToken, refreshToken: null, csrfToken: null, user: tokenCache.user || null };
331
357
  }
332
358
 
333
359
  let inflight = mintInflight.get(cacheKey);
@@ -8,6 +8,7 @@
8
8
  const fs = require("node:fs");
9
9
  const path = require("node:path");
10
10
  const crypto = require("node:crypto");
11
+ const { createDshPerformanceCollector, normalizePerformance } = require("./session-performance");
11
12
  const {
12
13
  isDshSessionLogName,
13
14
  parseDshVersion,
@@ -101,6 +102,48 @@ function eventTimestampMs(entry) {
101
102
  return Number.isFinite(time) && time >= MIN_DSH_TIMESTAMP_MS ? time : null;
102
103
  }
103
104
 
105
+ function firstStreamTokenTime(stream) {
106
+ // DSH v4 stores raw chunks and packed delta runs together. Only non-empty
107
+ // deltas (or a name-bearing tool delta) mark a token; block-start does not.
108
+ for (const record of stream) {
109
+ if (record?.type === "chunk") {
110
+ const chunk = record.chunk;
111
+ const token = ["text-delta", "reasoning-delta"].includes(chunk?.type)
112
+ ? typeof chunk.text === "string" && chunk.text.length > 0
113
+ : chunk?.type === "tool-call-delta" && (
114
+ (typeof chunk.argumentsDelta === "string" && chunk.argumentsDelta.length > 0)
115
+ || (typeof chunk.name === "string" && chunk.name.length > 0));
116
+ const time = eventTimestampMs(record);
117
+ if (token && time !== null) return time;
118
+ } else if (["text-chunks", "reasoning-chunks", "tool-call-chunks"].includes(record?.type)) {
119
+ const tool = record.type === "tool-call-chunks";
120
+ const fragments = tool ? record.args : record.texts;
121
+ let time = eventTimestampMs({ time: record.time0 });
122
+ if (time === null || !Array.isArray(fragments)) continue;
123
+ for (let index = 0; index < fragments.length; index += 1) {
124
+ if (index > 0) {
125
+ const gap = record.dt?.[index - 1];
126
+ if (typeof gap !== "number" || !Number.isFinite(gap)) break;
127
+ time += gap;
128
+ }
129
+ if ((typeof fragments[index] === "string" && fragments[index].length > 0)
130
+ || (tool && typeof record.name === "string" && record.name.length > 0)) return time;
131
+ }
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function messageTiming(data, step, end) {
138
+ if (!step || step.failed || data?.interrupted || step.turn !== data?.turn || step.step !== data?.step) return null;
139
+ const stream = Array.isArray(data?.stream) ? data.stream : null;
140
+ if (stream) {
141
+ const finish = stream.findLast((record) => record?.type === "chunk" && record.chunk?.type === "finish");
142
+ if (!finish || !finish.chunk.reason?.kind || ["error", "aborted"].includes(finish.chunk.reason.kind)) return null;
143
+ }
144
+ return { start: step.ts, end, firstToken: stream ? firstStreamTokenTime(stream) : null };
145
+ }
146
+
104
147
  function directoryEntryIs(entry, fullPath, kind) {
105
148
  const check = kind === "dir" ? entry.isDirectory() : entry.isFile();
106
149
  if (check) return true;
@@ -279,6 +322,7 @@ async function parseDshTranscript(filePath) {
279
322
  events: [],
280
323
  };
281
324
  const lines = text.split("\n");
325
+ let openStep = null;
282
326
  for (let index = 0; index < lines.length; index += 1) {
283
327
  const line = lines[index];
284
328
  if (!line) continue;
@@ -292,6 +336,33 @@ async function parseDshTranscript(filePath) {
292
336
  const data = entry.data && typeof entry.data === "object" ? entry.data : null;
293
337
  const seq = Number.isFinite(entry.seq) ? entry.seq : index;
294
338
  switch (entry.type) {
339
+ case "step/start": {
340
+ openStep = Number.isInteger(data?.turn) && Number.isInteger(data?.step)
341
+ ? { turn: data.turn, step: data.step, ts: eventTimestampMs(entry), failed: false } : null;
342
+ break;
343
+ }
344
+ case "assistant/attempt":
345
+ case "assistant/chunk": {
346
+ // A retry's step time includes failed work/backoff, so it cannot be a
347
+ // clean request sample even if the eventual message reports usage.
348
+ // v4 records the failed try as assistant/attempt. v1 has no attempt
349
+ // event: the failed try ends as a top-level assistant/chunk whose
350
+ // chunk is a finish with an error or aborted reason.
351
+ const chunk = data?.chunk;
352
+ const failedFinish = entry.type === "assistant/chunk"
353
+ && chunk?.type === "finish"
354
+ && ["error", "aborted"].includes(chunk.reason?.kind);
355
+ if ((entry.type === "assistant/attempt" || failedFinish)
356
+ && openStep && openStep.turn === data?.turn && openStep.step === data?.step) {
357
+ openStep.failed = true;
358
+ }
359
+ break;
360
+ }
361
+ case "step/end":
362
+ case "turn/start":
363
+ case "turn/end":
364
+ openStep = null;
365
+ break;
295
366
  case "session": {
296
367
  transcript.header = entry;
297
368
  break;
@@ -329,6 +400,8 @@ async function parseDshTranscript(filePath) {
329
400
  const ts = eventTimestampMs(entry);
330
401
  const model = modelName(message?.source?.model) || transcript.fallbackModel || "unknown";
331
402
  const delta = dshUsageDelta(data?.usage);
403
+ const timing = messageTiming(data, openStep, ts);
404
+ openStep = null;
332
405
  if (delta) {
333
406
  transcript.events.push({
334
407
  kind: "usage",
@@ -336,6 +409,7 @@ async function parseDshTranscript(filePath) {
336
409
  ts,
337
410
  model,
338
411
  delta,
412
+ timing,
339
413
  });
340
414
  }
341
415
  break;
@@ -405,6 +479,7 @@ function buildRow(transcript, owners) {
405
479
  const times = new Set();
406
480
  const agentTypes = new Map();
407
481
  const modelsForEdits = new Map();
482
+ const performanceCollector = createDshPerformanceCollector();
408
483
  let turns = 0;
409
484
  let edits = 0;
410
485
  let retries = 0;
@@ -451,6 +526,9 @@ function buildRow(transcript, owners) {
451
526
  addUsage(modelRow, delta);
452
527
  modelRow.usage_events += 1;
453
528
  modelUsage.set(model, modelRow);
529
+ if (event.timing) performanceCollector.consumeUsage({
530
+ model, outputTokens: delta.output_tokens + delta.reasoning_output_tokens, ...event.timing,
531
+ });
454
532
  if (event.ts != null) {
455
533
  const bucketTime = new Date(Math.floor(event.ts / 60000) * 60000).toISOString();
456
534
  const key = `${model}\u0000${bucketTime}`;
@@ -478,9 +556,11 @@ function buildRow(transcript, owners) {
478
556
  const gap = orderedTimes[index] - orderedTimes[index - 1];
479
557
  if (gap > 0 && gap <= DSH_ACTIVE_GAP_MS) activeMs += gap;
480
558
  }
559
+ const performance = performanceCollector.finish();
481
560
  const models = [...modelUsage.values()].map((row) => ({
482
561
  ...row,
483
562
  edit_turns: modelsForEdits.get(row.model) || 0,
563
+ performance: performance.byModel.get(row.model) || normalizePerformance(),
484
564
  }));
485
565
  // A session that only handshook (no user turn, no usage) has nothing to say.
486
566
  if (!tokens.total_tokens && !turns) return null;
@@ -507,6 +587,7 @@ function buildRow(transcript, owners) {
507
587
  started_at: orderedTimes.length ? new Date(orderedTimes[0]).toISOString() : null,
508
588
  ended_at: orderedTimes.length ? new Date(orderedTimes.at(-1)).toISOString() : null,
509
589
  active_ms: activeMs,
590
+ performance: performance.performance,
510
591
  provenance: {
511
592
  source: "local-session-log",
512
593
  confidence: "observed",
@@ -1400,6 +1400,13 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
1400
1400
  }
1401
1401
  }
1402
1402
 
1403
+ function rememberIssuedAccessToken(accessToken, sourceFingerprint) {
1404
+ if (!sourceFingerprint || typeof accessToken !== "string" || accessToken.length > 8192) return;
1405
+ const hash = crypto.createHash("sha256").update(accessToken).digest("hex");
1406
+ if (!issuedAccessTokenInstances.has(hash)) issuedAccessTokenInstances.set(hash, sourceFingerprint);
1407
+ while (issuedAccessTokenInstances.size > 64) issuedAccessTokenInstances.delete(issuedAccessTokenInstances.keys().next().value);
1408
+ }
1409
+
1403
1410
  function captureAuthTokensFromBody(bodyBuffer, contentType, { newLogin = false, sourceFingerprint } = {}) {
1404
1411
  if (!bodyBuffer || !String(contentType || "").toLowerCase().includes("application/json")) return;
1405
1412
  let parsed = null;
@@ -1408,11 +1415,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
1408
1415
  } catch {
1409
1416
  return;
1410
1417
  }
1411
- if (sourceFingerprint && typeof parsed?.accessToken === "string" && parsed.accessToken.length <= 8192) {
1412
- const hash = crypto.createHash("sha256").update(parsed.accessToken).digest("hex");
1413
- if (!issuedAccessTokenInstances.has(hash)) issuedAccessTokenInstances.set(hash, sourceFingerprint);
1414
- while (issuedAccessTokenInstances.size > 64) issuedAccessTokenInstances.delete(issuedAccessTokenInstances.keys().next().value);
1415
- }
1418
+ rememberIssuedAccessToken(parsed?.accessToken, sourceFingerprint);
1416
1419
  let changed = false;
1417
1420
  const token = typeof parsed?.csrfToken === "string" ? parsed.csrfToken.trim() : "";
1418
1421
  if (token) {
@@ -2073,6 +2076,90 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2073
2076
  const hasCsrfHeader = typeof proxyHeaders["x-csrf-token"] === "string" && proxyHeaders["x-csrf-token"].trim().length > 0;
2074
2077
  const relayCsrfToken = getRelayCookieValue(csrfRelayCookieName);
2075
2078
  const relayRefreshToken = getRelayCookieValue("insforge_refresh_token", { decode: true });
2079
+ // Issue #1074: the backend rotates refresh tokens, but this proxy and
2080
+ // `mintAccessToken` used to POST the same token independently. A page
2081
+ // refresh concurrent with a background account read then consumed the
2082
+ // token twice — the loser got 401 and the SPA signed the user out.
2083
+ // Route every refresh that still has a persisted token through the
2084
+ // shared mint (cache + single-flight + one retry against a just-
2085
+ // rotated file) so both callers share one result.
2086
+ if (p === "/api/auth/refresh" && relayRefreshToken && String(req.method || "GET").toUpperCase() === "POST") {
2087
+ for await (const _chunk of req) { /* drain unused body */ }
2088
+ const latestRuntime = getRuntime();
2089
+ if (latestRuntime.configurationError || latestRuntime.baseUrl !== runtime.baseUrl || latestRuntime.anonKey !== runtime.anonKey) {
2090
+ json(res, { code: "instance_changed", error: "The configured backend changed during authentication. Reload and sign in again." }, 409);
2091
+ return true;
2092
+ }
2093
+ const mintOpts = {
2094
+ baseUrl: insforgeBase,
2095
+ anonKey: runtime.anonKey,
2096
+ timeoutMs: runtime.httpTimeoutMs,
2097
+ throwOnFailure: true,
2098
+ };
2099
+ const rejectIfRefreshContextChanged = () => {
2100
+ const currentRuntime = getRuntime();
2101
+ if (currentRuntime.configurationError || currentRuntime.baseUrl !== runtime.baseUrl || currentRuntime.anonKey !== runtime.anonKey) {
2102
+ json(res, { code: "instance_changed", error: "The configured backend changed during authentication. Reload and sign in again." }, 409);
2103
+ return true;
2104
+ }
2105
+ if (authSessionAtStart !== getCloudAccountSessionGeneration()) {
2106
+ json(res, { error: "Account session changed", code: "auth_session_changed" }, 409);
2107
+ return true;
2108
+ }
2109
+ return false;
2110
+ };
2111
+ let minted = null;
2112
+ try {
2113
+ try {
2114
+ minted = await mintAccessToken({ ...mintOpts, refreshToken: relayRefreshToken });
2115
+ } catch (error) {
2116
+ // Only retry a consumed-token 401 against a just-rotated
2117
+ // persisted token for the SAME backend/session. Otherwise a
2118
+ // rejected mint from instance A could submit instance B's
2119
+ // newly saved refresh token to A.
2120
+ if (rejectIfRefreshContextChanged()) return true;
2121
+ const latest = getRelayCookieValue("insforge_refresh_token", { decode: true });
2122
+ if (latest && latest !== relayRefreshToken && error?.status === 401) {
2123
+ minted = await mintAccessToken({ ...mintOpts, refreshToken: latest });
2124
+ } else {
2125
+ throw error;
2126
+ }
2127
+ }
2128
+ } catch (error) {
2129
+ if (rejectIfRefreshContextChanged()) return true;
2130
+ // performMint tags every non-OK HTTP status as auth_rejected.
2131
+ // Only a real 401 means the credential was consumed/invalid;
2132
+ // 429/503 must not sign the SPA out.
2133
+ if (error?.status === 401) {
2134
+ json(res, { error: "AUTH_UNAUTHORIZED", message: "Invalid or expired refresh token" }, 401);
2135
+ return true;
2136
+ }
2137
+ throw error;
2138
+ }
2139
+ if (rejectIfRefreshContextChanged()) return true;
2140
+ if (!minted?.accessToken) {
2141
+ json(res, { error: "AUTH_UNAUTHORIZED", message: "Invalid or expired refresh token" }, 401);
2142
+ return true;
2143
+ }
2144
+ const payload = { accessToken: minted.accessToken };
2145
+ if (minted.refreshToken) payload.refreshToken = minted.refreshToken;
2146
+ if (minted.user) payload.user = minted.user;
2147
+ // Persist the rotation without bumping sessionGeneration — that
2148
+ // would 409 a concurrent waiter that shared this mint.
2149
+ // setRelayRefreshToken matches the background account-read path.
2150
+ if (minted.refreshToken) setRelayRefreshToken(minted.refreshToken);
2151
+ if (minted.csrfToken) setRelayCsrfToken(minted.csrfToken);
2152
+ // Cache hits return csrfToken:null. The SDK still needs the current
2153
+ // CSRF to sign out; without it, logout 403s and a reload signs the
2154
+ // user back in. Fall back to the relayed token we just persisted.
2155
+ const csrfToken = minted.csrfToken || getRelayCookieValue(csrfRelayCookieName);
2156
+ if (csrfToken) payload.csrfToken = csrfToken;
2157
+ rememberIssuedAccessToken(minted.accessToken, fingerprintFor(runtime));
2158
+ const resBody = Buffer.from(JSON.stringify(payload));
2159
+ res.writeHead(200, { "content-type": "application/json" });
2160
+ res.end(resBody);
2161
+ return true;
2162
+ }
2076
2163
  // A cookie-less client (fresh WebView after an app update/restart) has no
2077
2164
  // browser session to pair a CSRF token with — the persisted refresh token
2078
2165
  // replayed through the mobile flow is the only viable recovery. The relay
@@ -903,6 +903,14 @@
903
903
  "match": "gpt-5.6",
904
904
  "ref": "gpt-5.6-sol"
905
905
  },
906
+ {
907
+ "match": "gemini-4-argon",
908
+ "ref": "gemini-4-argon"
909
+ },
910
+ {
911
+ "match": "gemini-4.argon",
912
+ "ref": "gemini-4-argon"
913
+ },
906
914
  {
907
915
  "match": "kiro",
908
916
  "ref": "kiro-cli-agent"