tokentracker-cli 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-mB7HZSGE.js → AchievementBadge-B5kmt0VD.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DLzeq_eo.js → AchievementsPage-C0FlkOo0.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-DrXQyDns.js → AchievementsSection-CRcp80Pp.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-WrgKRybi.js → BadgeDetailModal-DAFjGbKW.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-DFRaixVK.js → BotAnimated-CO0bbimB.js} +1 -1
  6. package/dashboard/dist/assets/{Card-bFq2tlO8.js → Card-Xhr5exz8.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-gwhGxavy.js → CloudCheckoutPage-DsQ7FmsI.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-BD4iBYeZ.js → CloudPage-DkSexu5V.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-C_KlPl0U.js → CommandPalette-Bm0huQdj.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-Cl1Bgv8M.js → CommunityStatsModal-BsL8GN8Q.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-NWoYevYC.js → ConfirmModal-_WPp292g.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-D2-D2cfA.js → Controls-ktzn68KM.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-vITMnPXx.js → DashboardPage-CdNXi3Im.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-8EMVXuH6.js → DateRangePopover-DWr4zIzF.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-DbP8d6bM.js → DevicePage-CBa8Mk47.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-CEstgLa1.js → DialogClose-CZ5jzgtb.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-kCZ4gN09.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-DP5TLLbz.js → DialogTitle-Dr0N6q15.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-Uvi3yMum.js → ElectricLogo-sYqgrO6-.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-Dpzihg6f.js → FadeIn-Qq-hcrZW.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-DllqemKr.js → HeaderGithubStar-B77zSv7i.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-Dc-TAH6l.js → HoverTooltip-lRDaUmoW.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-jd1D5ZKX.js → IpCheckPage-B6AvHZjA.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-CB1ZVw-a.js → LandingPage-DgzOjf3x.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-Bt2cRRmE.js → LeaderboardPage-BtjjtDMC.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-BVlyVOFf.js → LeaderboardProBadge-B2wNTUTU.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-CwQFRJg_.js → LeaderboardProfileModal-DJJckwHh.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-CeqcTbUG.js → LeaderboardProfilePage-DJdxByl2.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-gSBHSBi_.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-B2_HoCVD.js → LocalOnlyNotice-B2lVqf_H.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-nw9JOc2d.js → LoginCard-i0gg1yby.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BHNLTK1D.js → LoginModal-B-loLNZM.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-LI2GZKhM.js → LoginPage-D_swI75j.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-CK6sPeI0.js → PetPage-DDpKvpjG.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-BsXchEVa.js → PopoverPopup-Gja4MyEK.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-vgCEy0dh.js → ProviderIcon-DOPj7tAZ.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-B4CPj2vX.js → ResetPasswordPage-BVADLk89.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BkAy6P2r.js → Select-D-38tN-E.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-C0RNqDrD.js → SelectItemText-jN61ukxk.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-B-_w-Z-a.js → SelfHostPage-k7AkGpl1.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-D_Fa_pYz.js +1 -0
  42. package/dashboard/dist/assets/{SessionsPage-CmWXBqD2.js → SessionsPage-BymccEEz.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-CA70zP-t.js → SettingsPage-DfnMCAuA.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CNamhfzU.js → SkillsPage-YyIHIaHN.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-CXrVBSk6.js → TokenGalaxy-BLcvZuQ9.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-CR4MqElX.js → TooltipPopup-3E8kyVzz.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-DQfcrEve.js → TrendMonitor-ByKAdXSF.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-BBXxtZbP.js → WidgetsPage-BgEVIIzL.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-C8i2KlMF.js → WrappedPage-HjbX8QaA.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-D_cMeuqI.js +45 -0
  51. package/dashboard/dist/assets/{agent-logos-CxQb8vir.js → agent-logos-BVF2siPQ.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-left-D6zkcLVt.js → arrow-left-CYO24PGP.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-right-BGb-XVn_.js → arrow-right-Xg9PZYhw.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-XTStagDJ.js → arrow-up-right-B7ah6Qv-.js} +1 -1
  55. package/dashboard/dist/assets/{calendar-days-Cz3NGZiS.js → calendar-days-BV_iG938.js} +1 -1
  56. package/dashboard/dist/assets/{check-1z_vmHPD.js → check--EZH8RTp.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-down-CkSBY3bE.js → chevron-down-BzGTlk0-.js} +1 -1
  58. package/dashboard/dist/assets/{cloud-checkout-pD5C4r_S.js → cloud-checkout-BgyxPHEh.js} +1 -1
  59. package/dashboard/dist/assets/{composite-BtDzx3a2.js → composite-C0KrmLEK.js} +1 -1
  60. package/dashboard/dist/assets/{copy-Ctv_wxuD.js → copy-BtucSK2U.js} +1 -1
  61. package/dashboard/dist/assets/{download-DKbKMGSR.js → download-CWHq03Y2.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-g9cU017t.js → external-link-DCTXF5VZ.js} +1 -1
  63. package/dashboard/dist/assets/{flame-DZq3kXUt.js → flame-BE5TsqVF.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-CXQfx0PC.js → format-tokens-ThNIMWnZ.js} +1 -1
  65. package/dashboard/dist/assets/{gift-CKlKjGP6.js → gift-DgaN9s4-.js} +1 -1
  66. package/dashboard/dist/assets/{icons-xPGMnBjl.js → icons-B4r5EiDC.js} +1 -1
  67. package/dashboard/dist/assets/{index-fVbP5wOv.js → index-CKhcCLNC.js} +1 -1
  68. package/dashboard/dist/assets/{index-BwOjMtB2.js → index-DFF9xo6f.js} +1 -1
  69. package/dashboard/dist/assets/{index-CDBOKcDh.js → index-DqCtgU0G.js} +1 -1
  70. package/dashboard/dist/assets/{index-Dl0oFLNr.js → index-fNKH4U7j.js} +1 -1
  71. package/dashboard/dist/assets/{info-D9xNIuGh.js → info-B4YzRR4y.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-Bk5wYt2n.js → link-2-DzbUbaBw.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-CcN3pHWb.js → loader-circle-B9c7D-7v.js} +1 -1
  74. package/dashboard/dist/assets/{local-usage-transfer-CYcy1ici.js → local-usage-transfer-btuk_b0Y.js} +1 -1
  75. package/dashboard/dist/assets/{modifiers.esm-DbpEzkJe.js → modifiers.esm-BjrU-Xz7.js} +1 -1
  76. package/dashboard/dist/assets/{monitor-smartphone-WkWv1qIS.js → monitor-smartphone-Bwn4tb4q.js} +1 -1
  77. package/dashboard/dist/assets/{play-2yZGWMes.js → play-DIFOwGIQ.js} +1 -1
  78. package/dashboard/dist/assets/{plus-mOF8tG1F.js → plus-BpBqnwVg.js} +1 -1
  79. package/dashboard/dist/assets/{provider-display-7gaOvgMp.js → provider-display-DSrs4u7T.js} +1 -1
  80. package/dashboard/dist/assets/{react-CdPiPDhk.js → react-BgG7_MY0.js} +1 -1
  81. package/dashboard/dist/assets/{safePolygon-C7FJLAlh.js → safePolygon-BUGWIDOM.js} +1 -1
  82. package/dashboard/dist/assets/{search-C63nCrjo.js → search-aHrmBmtZ.js} +1 -1
  83. package/dashboard/dist/assets/{server-BaG1rnuv.js → server-DOEvwlEU.js} +1 -1
  84. package/dashboard/dist/assets/{shield-check-D_4-Cm6L.js → shield-check-BPTnLhsT.js} +1 -1
  85. package/dashboard/dist/assets/{skills-api-DfbzwpDv.js → skills-api-C052Mwez.js} +1 -1
  86. package/dashboard/dist/assets/{sortable.esm-D-v8FRX5.js → sortable.esm-DaZKhjhu.js} +1 -1
  87. package/dashboard/dist/assets/{store-Dv_FYrXK.js → store-Cq30Pofw.js} +1 -1
  88. package/dashboard/dist/assets/{terminal-FQi6MaYt.js → terminal-CoSDSkE7.js} +1 -1
  89. package/dashboard/dist/assets/{trash-TbK96dEz.js → trash-BAmt-kPj.js} +1 -1
  90. package/dashboard/dist/assets/{use-community-stats-DGoWVZuk.js → use-community-stats-Bxq0kBhm.js} +1 -1
  91. package/dashboard/dist/assets/{use-latest-request-guard-Dg6uSaYt.js → use-latest-request-guard-DmIQDlvk.js} +1 -1
  92. package/dashboard/dist/assets/use-limits-display-prefs-CyAn1fme.js +1 -0
  93. package/dashboard/dist/assets/{use-native-settings-DIk0AF3b.js → use-native-settings-s_CIC8Bb.js} +1 -1
  94. package/dashboard/dist/assets/{use-ordered-list-CUqV3Zgl.js → use-ordered-list-BHhykAbt.js} +1 -1
  95. package/dashboard/dist/assets/use-reduced-motion-BmFyFE3S.js +1 -0
  96. package/dashboard/dist/assets/{use-transform-D_-w22c8.js → use-transform-CVeMFlS6.js} +1 -1
  97. package/dashboard/dist/assets/use-usage-limits-DRZMI20u.js +1 -0
  98. package/dashboard/dist/assets/{useCurrency-B_AuLCsL.js → useCurrency-DilqL2Iv.js} +1 -1
  99. package/dashboard/dist/assets/{usePositioner-DfxvIif8.js → usePositioner-BcnySw8a.js} +1 -1
  100. package/dashboard/dist/assets/{useTokenFormat-bbO13V4s.js → useTokenFormat-CvKW46WP.js} +1 -1
  101. package/dashboard/dist/assets/{zap-k07-EXTK.js → zap-D7VRNTwk.js} +1 -1
  102. package/dashboard/dist/index.html +5 -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/codex-rollout-parser.js +43 -6
  108. package/src/lib/codex-usage-record.js +21 -0
  109. package/src/lib/pricing/curated-overrides.json +94 -0
  110. package/src/lib/pricing/seed-snapshot.json +1 -1
  111. package/src/lib/qoder-limits.js +78 -21
  112. package/src/lib/rollout.js +68 -14
  113. package/src/lib/session-analytics.js +10 -3
  114. package/src/lib/usage-limits.js +6 -4
  115. package/src/lib/workbuddy-limits.js +101 -9
  116. package/src/lib/zcode-limits.js +350 -43
  117. package/dashboard/dist/assets/DialogDescription-0wcygxtH.js +0 -1
  118. package/dashboard/dist/assets/LimitsPage-Bp4ZORmR.js +0 -6
  119. package/dashboard/dist/assets/ServiceStatusPage-5YevsGGD.js +0 -1
  120. package/dashboard/dist/assets/__vite-browser-external-BEEzY7mq.js +0 -45
  121. package/dashboard/dist/assets/use-limits-display-prefs-Djbdp7fi.js +0 -1
  122. package/dashboard/dist/assets/use-reduced-motion-DMCok-vZ.js +0 -1
  123. package/dashboard/dist/assets/use-usage-limits-h7aGsIQr.js +0 -1
@@ -10,6 +10,10 @@ const DEFAULT_BIGMODEL_MONITOR_BASE_URL = "https://bigmodel.cn";
10
10
  const ZCODE_MONITOR_QUOTA_PATH = "/api/monitor/usage/quota/limit";
11
11
  const DEFAULT_ZCODE_APP_VERSION = "3.2.5";
12
12
  const DEFAULT_ZCODE_LOG_FALLBACK_MAX_AGE_MS = 6 * 60 * 60 * 1000;
13
+ const ZCODE_LIMITS_CACHE_FILE = "zcode-usage-limits-cache.json";
14
+ // Windows without a usable reset_at cannot be checked against a quota rollover,
15
+ // so they are only served while the snapshot is young enough to trust.
16
+ const ZCODE_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
13
17
 
14
18
  function resolveZcodeHome({ home, env = process.env } = {}) {
15
19
  if (typeof env.TOKENTRACKER_ZCODE_HOME === "string" && env.TOKENTRACKER_ZCODE_HOME.trim()) {
@@ -150,6 +154,16 @@ function loadZcodeSelectedPlans({ home, env } = {}) {
150
154
  }
151
155
  }
152
156
 
157
+ /**
158
+ * Plan class ("start" | "coding") a plan id like "start-plan" or
159
+ * "individual-coding-plan" serves; null for anything else. Per-account
160
+ * spellings (`individual-`/`team-coding-plan`) and the builtin
161
+ * `coding-plan` both serve the same coding plan.
162
+ */
163
+ function zcodePlanKindFromPlanId(planId) {
164
+ return /coding-plan$/.test(planId) ? "coding" : planId === "start-plan" ? "start" : null;
165
+ }
166
+
153
167
  /**
154
168
  * ZCode 3.14+ records the chosen plan per family as `providerFamilyConnectionSelections`
155
169
  * (`{ zai: { kind: "individual-coding-plan" } }`) instead of `modelProviderFamilySelectedKeys`.
@@ -161,7 +175,7 @@ function loadZcodeConnectionSelectedPlans(selections, domain) {
161
175
  const out = [];
162
176
  for (const family of families) {
163
177
  const kind = typeof selections[family]?.kind === "string" ? selections[family].kind : "";
164
- const planKind = /coding-plan$/.test(kind) ? "coding" : kind === "start-plan" ? "start" : null;
178
+ const planKind = zcodePlanKindFromPlanId(kind);
165
179
  if (!planKind) continue;
166
180
  const providerKey = `builtin:${family}-${planKind}-plan`;
167
181
  if (out.some((plan) => plan.providerKey === providerKey)) continue;
@@ -276,6 +290,31 @@ function zcodeProviderFamily(providerKey) {
276
290
  return providerKey === "builtin:zai-coding-plan" || providerKey === "builtin:zai-start-plan" ? "zai" : "bigmodel";
277
291
  }
278
292
 
293
+ /**
294
+ * Region ("bigmodel" | "zai") for any ZCode provider id, or null when the id
295
+ * names neither region. Covers the built-in plan keys and the 3.14+ per-account
296
+ * ids ZCode writes into its own logs (`account:<family>-<plan>…`).
297
+ * @returns {"bigmodel" | "zai" | null}
298
+ */
299
+ function zcodeRegionFromProviderId(providerKey) {
300
+ const match = typeof providerKey === "string" ? providerKey.match(/^(?:builtin|account):(bigmodel|zai)-/) : null;
301
+ return match ? match[1] : null;
302
+ }
303
+
304
+ /**
305
+ * Plan class ("start" | "coding") for any ZCode provider id, or null when the
306
+ * id names no known plan. Mirrors {@link zcodeRegionFromProviderId}: covers the
307
+ * built-in plan keys and the 3.14+ per-account ids ZCode writes into its own
308
+ * logs (`account:<family>-<plan>…`).
309
+ * @returns {"start" | "coding" | null}
310
+ */
311
+ function zcodePlanKindFromProviderId(providerKey) {
312
+ const match = typeof providerKey === "string"
313
+ ? providerKey.match(/^(?:builtin|account):(bigmodel|zai)-(.+)$/)
314
+ : null;
315
+ return match ? zcodePlanKindFromPlanId(match[2]) : null;
316
+ }
317
+
279
318
  /**
280
319
  * Region whose plan should be served first. The model settings' domain is the
281
320
  * region the user actually picked, so it wins; the OAuth login region only
@@ -692,7 +731,10 @@ async function fetchZcodeBilling(apiKey, { fetchImpl = fetch, baseUrl, env, home
692
731
  signal,
693
732
  });
694
733
  if (res.status === 401 || res.status === 403) {
695
- throw new Error("Not authenticated with ZCode. Run `zcode` in Terminal to log in.");
734
+ const error = new Error("Not authenticated with ZCode. Run `zcode` in Terminal to log in.");
735
+ // Tagged so the failure chain surfaces re-login instead of stale cache bars.
736
+ error.authError = true;
737
+ throw error;
696
738
  }
697
739
  if (!res.ok) {
698
740
  let detail = "";
@@ -770,7 +812,10 @@ async function fetchZcodeCodingPlanQuota(apiKey, { fetchImpl = fetch, quotaUrl,
770
812
  headers,
771
813
  });
772
814
  if (res.status === 401 || res.status === 403) {
773
- throw new Error("Not authenticated with ZCode coding plan. Run `zcode` in Terminal to log in.");
815
+ const error = new Error("Not authenticated with ZCode coding plan. Run `zcode` in Terminal to log in.");
816
+ // Tagged so the failure chain surfaces re-login instead of stale cache bars.
817
+ error.authError = true;
818
+ throw error;
774
819
  }
775
820
  let body = null;
776
821
  try {
@@ -798,10 +843,11 @@ function quotaTimestampToIso(value) {
798
843
 
799
844
  function findZcodeQuotaLimit(limits, type, unit, number = null) {
800
845
  if (!Array.isArray(limits)) return null;
846
+ const types = Array.isArray(type) ? type : [type];
801
847
  return (
802
848
  limits.find((limit) => {
803
849
  if (!limit || typeof limit !== "object") return false;
804
- if (limit.type !== type) return false;
850
+ if (!types.includes(limit.type)) return false;
805
851
  if (zcodeValNumber(limit.unit) !== unit) return false;
806
852
  if (number == null) return true;
807
853
  return zcodeValNumber(limit.number) === number;
@@ -861,8 +907,10 @@ function normalizeZcodeCodingPlanQuotaResponse(body) {
861
907
  const limits = Array.isArray(data.limits) ? data.limits : [];
862
908
  // Match ZCode's own sidebar: TOKENS_LIMIT(unit=3,number=5)=5h,
863
909
  // TOKENS_LIMIT(unit=6)=weekly, TIME_LIMIT(unit=5,number=1)=tool calls.
864
- const fiveHourLimit = findZcodeQuotaLimit(limits, "TOKENS_LIMIT", 3, 5);
865
- const weeklyLimit = findZcodeQuotaLimit(limits, "TOKENS_LIMIT", 6);
910
+ // ZCode 3.17.1 renamed the token-window rows to CREDIT_LIMIT while keeping
911
+ // the unit/number convention, so both names anchor the named windows (#937).
912
+ const fiveHourLimit = findZcodeQuotaLimit(limits, ["TOKENS_LIMIT", "CREDIT_LIMIT"], 3, 5);
913
+ const weeklyLimit = findZcodeQuotaLimit(limits, ["TOKENS_LIMIT", "CREDIT_LIMIT"], 6);
866
914
  const toolsLimit = findZcodeQuotaLimit(limits, "TIME_LIMIT", 5, 1);
867
915
  const hasNamedWindows = Boolean(fiveHourLimit || weeklyLimit || toolsLimit);
868
916
 
@@ -944,18 +992,31 @@ function extractZcodeBalanceLogRecord(line) {
944
992
  }
945
993
  }
946
994
 
947
- function loadLatestZcodeBalanceFromLogs({
948
- home,
949
- env = process.env,
950
- providerKeys = [],
951
- nowMs = Date.now(),
952
- requireProviderMatch = false,
953
- } = {}) {
954
- if (env.TOKENTRACKER_ZCODE_DISABLE_LOG_FALLBACK === "1") return null;
995
+ /**
996
+ * One backward pass over the recent ZCode logs (today + yesterday), collecting:
997
+ * preferred — freshest record matching the candidate providers. Built-in ids
998
+ * must byte-equal a candidate key; the 3.14+ per-account ids
999
+ * (`account:<family>-<plan>…`) never do, so they match the exact
1000
+ * (region, plan kind) pair of one candidate — never the cross product of
1001
+ * the two traits, so a bigmodel-coding + zai-start candidate list cannot
1002
+ * adopt a zai coding-plan record.
1003
+ * fallback — freshest record of any provider.
1004
+ * byRegion — freshest record per region ("bigmodel" | "zai"), for the
1005
+ * other-region snapshot.
1006
+ * Records outside the age window count nowhere. Every eligible line of both
1007
+ * files is visited and buckets keep the max-timestamp record, since log lines
1008
+ * can land out of timestamp order.
1009
+ */
1010
+ function scanZcodeBalanceLogs({ home, env = process.env, providerKeys = [], nowMs = Date.now() } = {}) {
1011
+ const scan = { preferred: null, fallback: null, byRegion: new Map() };
1012
+ if (env.TOKENTRACKER_ZCODE_DISABLE_LOG_FALLBACK === "1") return scan;
955
1013
  const maxAgeMs = parsePositiveInteger(env.TOKENTRACKER_ZCODE_LOG_MAX_AGE_MS, DEFAULT_ZCODE_LOG_FALLBACK_MAX_AGE_MS);
956
1014
  const preferredProviders = new Set(providerKeys.filter(Boolean));
957
- let preferred = null;
958
- let fallback = null;
1015
+ const preferredPairs = new Set(providerKeys.map((key) => {
1016
+ const region = zcodeRegionFromProviderId(key);
1017
+ const planKind = zcodePlanKindFromProviderId(key);
1018
+ return region && planKind ? `${region}:${planKind}` : null;
1019
+ }).filter(Boolean));
959
1020
  for (const logPath of recentZcodeLogPaths({ home, env, nowMs })) {
960
1021
  if (!fs.existsSync(logPath)) continue;
961
1022
  let text = "";
@@ -970,14 +1031,36 @@ function loadLatestZcodeBalanceFromLogs({
970
1031
  if (!record) continue;
971
1032
  const ageMs = nowMs - record.timestampMs;
972
1033
  if (ageMs < 0 || ageMs > maxAgeMs) continue;
973
- if (!fallback || record.timestampMs > fallback.timestampMs) fallback = record;
974
- if (preferredProviders.has(record.providerKey) && (!preferred || record.timestampMs > preferred.timestampMs)) {
975
- preferred = record;
1034
+ if (!scan.fallback || record.timestampMs > scan.fallback.timestampMs) scan.fallback = record;
1035
+ const region = zcodeRegionFromProviderId(record.providerKey);
1036
+ const regionRecord = region ? scan.byRegion.get(region) : null;
1037
+ if (region && (!regionRecord || record.timestampMs > regionRecord.timestampMs)) {
1038
+ scan.byRegion.set(region, record);
1039
+ }
1040
+ const regionMatch = Boolean(region)
1041
+ && record.providerKey.startsWith("account:")
1042
+ && preferredPairs.has(`${region}:${zcodePlanKindFromProviderId(record.providerKey)}`);
1043
+ if (
1044
+ (preferredProviders.has(record.providerKey) || regionMatch)
1045
+ && (!scan.preferred || record.timestampMs > scan.preferred.timestampMs)
1046
+ ) {
1047
+ scan.preferred = record;
976
1048
  }
977
1049
  }
978
1050
  }
979
- if (preferred) return preferred;
980
- return requireProviderMatch && preferredProviders.size > 0 ? null : fallback;
1051
+ return scan;
1052
+ }
1053
+
1054
+ function loadLatestZcodeBalanceFromLogs({
1055
+ home,
1056
+ env = process.env,
1057
+ providerKeys = [],
1058
+ nowMs = Date.now(),
1059
+ requireProviderMatch = false,
1060
+ } = {}) {
1061
+ const scan = scanZcodeBalanceLogs({ home, env, providerKeys, nowMs });
1062
+ if (scan.preferred) return scan.preferred;
1063
+ return requireProviderMatch && providerKeys.filter(Boolean).length > 0 ? null : scan.fallback;
981
1064
  }
982
1065
 
983
1066
  function zcodeLogFallbackResult(logRecord, errors = []) {
@@ -987,23 +1070,175 @@ function zcodeLogFallbackResult(logRecord, errors = []) {
987
1070
  error: null,
988
1071
  source: "zcode-log",
989
1072
  provider_key: logRecord.providerKey,
1073
+ region: zcodeRegionFromProviderId(logRecord.providerKey),
990
1074
  log_timestamp: logRecord.log_timestamp,
1075
+ // The log line's age IS the data age: ZCode wrote it when it last fetched.
1076
+ cached_at: logRecord.log_timestamp,
1077
+ stale: true,
991
1078
  provider_errors: errors,
992
1079
  ...normalizeZcodeBalanceResponse(logRecord.body),
993
1080
  };
994
1081
  }
995
1082
 
1083
+ // Last-good disk cache for ZCode quota windows, following the house pattern
1084
+ // (codex / ark / qoder): write on every live success, serve stale bars when the
1085
+ // live chain fails. One file, partitioned per built-in provider key
1086
+ // (region × plan kind) so one region's billing read never falls back to
1087
+ // another region's data (#938's ownership principle).
1088
+
1089
+ // Cache partitions are also scoped to the login that produced the windows:
1090
+ // two logins on the same region × plan kind share one providerKey, so an
1091
+ // account switch followed by a live failure would otherwise serve the
1092
+ // previous login's bars. The login id comes from the app's own user_info —
1093
+ // the same ownership source as login-scoped tokens — plus the applicable
1094
+ // team scope for team-scoped coding-plan quota. No secret material is
1095
+ // persisted, and an unreadable login ("unknown") matches only itself.
1096
+ function zcodeCacheIdentity(loginAccountId, teamContext) {
1097
+ const login = typeof loginAccountId === "string" && loginAccountId.trim() ? loginAccountId.trim() : "unknown";
1098
+ return `${login}|${typeof teamContext === "string" ? teamContext : ""}`;
1099
+ }
1100
+
1101
+ function zcodeLimitsCachePath({ home } = {}) {
1102
+ return path.join(home || os.homedir(), ".tokentracker", "tracker", ZCODE_LIMITS_CACHE_FILE);
1103
+ }
1104
+
1105
+ // A window whose reset_at has already passed is stale data, not a usable
1106
+ // fallback — the quota has rolled over, so drop it. Undated windows cannot be
1107
+ // checked against a reset and are bounded by the snapshot-age TTL instead.
1108
+ function zcodeCachedWindow(window, { cachedAtMs, nowMs } = {}) {
1109
+ if (!window || typeof window !== "object") return null;
1110
+ const resetAtMs = Date.parse(window.reset_at || "");
1111
+ if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
1112
+ return nowMs - cachedAtMs <= ZCODE_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
1113
+ }
1114
+
1115
+ function zcodeCachedBucket(bucket, { cachedAtMs, nowMs } = {}) {
1116
+ if (!bucket || typeof bucket !== "object") return null;
1117
+ if (!bucket.window || typeof bucket.window !== "object") {
1118
+ return nowMs - cachedAtMs <= ZCODE_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? bucket : null;
1119
+ }
1120
+ const window = zcodeCachedWindow(bucket.window, { cachedAtMs, nowMs });
1121
+ return window ? { ...bucket, window } : null;
1122
+ }
1123
+
1124
+ /**
1125
+ * Read the last-good windows for the first provider key with a usable cache
1126
+ * entry. `providerKeys` arrives in live candidate priority order; keys whose
1127
+ * live attempts all ended in auth errors are withheld by the caller, and
1128
+ * `identityByKey` scopes each key to the credentials that could have
1129
+ * produced its windows.
1130
+ */
1131
+ function readZcodeLimitsCache({ home, nowMs = Date.now(), providerKeys = [], identityByKey } = {}) {
1132
+ let parsed;
1133
+ try {
1134
+ parsed = JSON.parse(fs.readFileSync(zcodeLimitsCachePath({ home }), "utf8"));
1135
+ } catch (_error) {
1136
+ return null;
1137
+ }
1138
+ if (!parsed || typeof parsed !== "object") return null;
1139
+ for (const providerKey of providerKeys) {
1140
+ const raw = parsed[providerKey];
1141
+ if (!raw || typeof raw !== "object") continue;
1142
+ const acceptableIdentities = identityByKey?.get(providerKey);
1143
+ if (!acceptableIdentities || !acceptableIdentities.size) continue;
1144
+ if (typeof raw.identity !== "string" || !acceptableIdentities.has(raw.identity)) continue;
1145
+ const cachedAtMs = Date.parse(raw.cached_at || "");
1146
+ if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) continue;
1147
+ const context = { cachedAtMs, nowMs };
1148
+ const buckets = (Array.isArray(raw.buckets) ? raw.buckets : [])
1149
+ .map((bucket) => zcodeCachedBucket(bucket, context))
1150
+ .filter(Boolean);
1151
+ const primaryWindow = zcodeCachedWindow(raw.primary_window, context);
1152
+ const secondaryWindow = zcodeCachedWindow(raw.secondary_window, context);
1153
+ const tertiaryWindow = zcodeCachedWindow(raw.tertiary_window, context);
1154
+ if (!primaryWindow && !secondaryWindow && !tertiaryWindow && !buckets.some((bucket) => bucket.window)) continue;
1155
+ return {
1156
+ configured: true,
1157
+ error: null,
1158
+ provider_key: providerKey,
1159
+ plan_kind: typeof raw.plan_kind === "string" ? raw.plan_kind : null,
1160
+ plan_id: typeof raw.plan_id === "string" ? raw.plan_id : null,
1161
+ plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
1162
+ buckets,
1163
+ primary_window: primaryWindow,
1164
+ secondary_window: secondaryWindow,
1165
+ tertiary_window: tertiaryWindow,
1166
+ cached: true,
1167
+ stale: true,
1168
+ cached_at: raw.cached_at,
1169
+ source: "disk-cache",
1170
+ };
1171
+ }
1172
+ return null;
1173
+ }
1174
+
1175
+ /** Merge one provider partition into the disk cache; best-effort, never throws. */
1176
+ function writeZcodeLimitsCache(limits, { home, nowMs = Date.now(), providerKey, identity } = {}) {
1177
+ if (!providerKey || !identity || !limits?.configured || limits.error) return;
1178
+ if (!limits.primary_window && !limits.secondary_window && !limits.tertiary_window) return;
1179
+ const cachePath = zcodeLimitsCachePath({ home });
1180
+ let payload = {};
1181
+ try {
1182
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
1183
+ if (parsed && typeof parsed === "object") payload = parsed;
1184
+ } catch (_error) {}
1185
+ payload[providerKey] = {
1186
+ identity,
1187
+ plan_kind: typeof limits.plan_kind === "string" ? limits.plan_kind : null,
1188
+ plan_id: typeof limits.plan_id === "string" ? limits.plan_id : null,
1189
+ plan_label: typeof limits.plan_label === "string" ? limits.plan_label : null,
1190
+ buckets: Array.isArray(limits.buckets) ? limits.buckets : [],
1191
+ primary_window: limits.primary_window || null,
1192
+ secondary_window: limits.secondary_window || null,
1193
+ tertiary_window: limits.tertiary_window || null,
1194
+ cached_at: new Date(nowMs).toISOString(),
1195
+ };
1196
+ try {
1197
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
1198
+ const tmpPath = `${cachePath}.${process.pid}.tmp`;
1199
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
1200
+ fs.renameSync(tmpPath, cachePath);
1201
+ } catch (_error) {}
1202
+ }
1203
+
1204
+ /**
1205
+ * Attach the freshest logged balance snapshot from the user's other region
1206
+ * (issue #938): ZCode's own usage logs carry both regions' billing data even
1207
+ * when only the active region's credentials are on disk. The snapshot is
1208
+ * read-only and deliberately carries no reset_credits / start_plan extras —
1209
+ * those are login-scoped and must never ride along across regions.
1210
+ */
1211
+ function withZcodeOtherRegion(result, byRegion) {
1212
+ if (!result || result.error || !result.region) return result;
1213
+ const otherRegion = result.region === "bigmodel" ? "zai" : "bigmodel";
1214
+ const record = byRegion instanceof Map ? byRegion.get(otherRegion) : null;
1215
+ if (!record) return result;
1216
+ return {
1217
+ ...result,
1218
+ other_region: {
1219
+ source: "zcode-log",
1220
+ region: otherRegion,
1221
+ provider_key: record.providerKey,
1222
+ log_timestamp: record.log_timestamp,
1223
+ ...normalizeZcodeBalanceResponse(record.body),
1224
+ },
1225
+ };
1226
+ }
1227
+
996
1228
  /**
997
1229
  * Login tokens for reset cards / Start Plan grants, or null when the quota key
998
1230
  * cannot be proven to belong to the signed-in login. Both extras are personal
999
1231
  * to the current login. Any key carrying an account id (per-account
1000
1232
  * credentials, or config.json keys that byte-equal one) must match the login's
1001
- * id. Once a login identity exists, an unprovable key (a config.json key no
1002
- * account credential vouches for — e.g. left over from a switched account)
1003
- * never merges its quota with this login's cards. When the login identity
1004
- * itself is unreadable (user_info missing or unparseable) nothing can prove
1005
- * the key belongs to the signed-in account either, so the extras are skipped
1006
- * and only the main plan quota stays.
1233
+ * id. In a per-account layout (credentials.json lists a coding-plan key for
1234
+ * this family) an unproven config.json key may be a leftover from a switched
1235
+ * account, so the extras stay fail-closed there. In a legacy layout without
1236
+ * any per-account credential for the family, no other account is known to
1237
+ * this ZCode home, so the config.json key cannot be attributed to a different
1238
+ * login and rides with the login's extras (#937). When the login
1239
+ * identity itself is unreadable (user_info missing or unparseable) nothing can
1240
+ * prove the key belongs to the signed-in account either, so the extras are
1241
+ * skipped and only the main plan quota stays.
1007
1242
  */
1008
1243
  function loadZcodeLoginScopedTokens(family, auth, { home, env } = {}) {
1009
1244
  if (loadZcodeActiveProvider({ home, env }) !== family) return null;
@@ -1011,7 +1246,11 @@ function loadZcodeLoginScopedTokens(family, auth, { home, env } = {}) {
1011
1246
  if (!loginAccountId) return null;
1012
1247
  if (auth.accountId) {
1013
1248
  if (auth.accountId !== loginAccountId) return null;
1014
- } else if (auth.auth_source === "credential:account-provider" || auth.auth_source === "provider:config") {
1249
+ } else if (auth.auth_source === "credential:account-provider") {
1250
+ return null;
1251
+ } else if (auth.auth_source === "provider:config"
1252
+ && Object.keys(loadZcodeAccountProviderAuths({ home, env }))
1253
+ .some((providerKey) => zcodeProviderFamily(providerKey) === family)) {
1015
1254
  return null;
1016
1255
  }
1017
1256
  const zcodeToken = loadZcodeCredential("zcodejwttoken", { home, env });
@@ -1021,8 +1260,12 @@ function loadZcodeLoginScopedTokens(family, auth, { home, env } = {}) {
1021
1260
  }
1022
1261
 
1023
1262
  /**
1024
- * Return normalized quota windows, trying eligible providers and recent billing logs.
1025
- * Credentials and internal team routing identifiers are omitted from the result.
1263
+ * Return normalized quota windows, trying eligible providers, recent billing
1264
+ * logs, and finally the last-good disk cache. The result carries the winning
1265
+ * provider's `region` ("bigmodel" | "zai") and, when ZCode's own logs hold one,
1266
+ * an `other_region` snapshot of the second region's plan (issue #938) — read-only
1267
+ * log data, never extended with login-scoped extras. Credentials and internal
1268
+ * team routing identifiers are omitted from the result.
1026
1269
  */
1027
1270
  async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now(), providerTimeoutMs } = {}) {
1028
1271
  const startedAt = Date.now();
@@ -1030,13 +1273,37 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
1030
1273
  return { configured: false };
1031
1274
  }
1032
1275
  const authCandidates = loadZcodeAuthCandidates({ home, env });
1276
+ // One log pass serves both the failed-candidate fallback and the other
1277
+ // region's snapshot. selectLogFallback keeps the former gate semantics:
1278
+ // builtin: candidates only adopt per-account records carrying the same
1279
+ // (region, plan kind) pair as one of them.
1280
+ const logScan = scanZcodeBalanceLogs({
1281
+ home,
1282
+ env,
1283
+ providerKeys: authCandidates.map((auth) => auth.providerKey),
1284
+ nowMs,
1285
+ });
1286
+ const requireProviderMatch = authCandidates.some((auth) => auth.planKind === "coding-plan");
1287
+ const selectLogFallback = (errors) => {
1288
+ const record = logScan.preferred
1289
+ || (requireProviderMatch && authCandidates.length > 0 ? null : logScan.fallback);
1290
+ return withZcodeOtherRegion(zcodeLogFallbackResult(record, errors), logScan.byRegion);
1291
+ };
1033
1292
  if (!authCandidates.length) {
1034
- const logFallback = loadLatestZcodeBalanceFromLogs({ home, env, nowMs });
1035
- if (logFallback) return zcodeLogFallbackResult(logFallback, []);
1036
- return { configured: false };
1293
+ return selectLogFallback([]) || { configured: false };
1037
1294
  }
1038
1295
  const errors = [];
1296
+ const partitionFailures = [];
1039
1297
  let emptySuccess = null;
1298
+ // The current login per region, resolved once: cache partitions are keyed
1299
+ // to it so an account switch cannot inherit the previous login's windows.
1300
+ const loginIdByFamily = new Map();
1301
+ const loginIdFor = (family) => {
1302
+ if (!loginIdByFamily.has(family)) {
1303
+ loginIdByFamily.set(family, loadZcodeLoginAccountId(family, { home, env }));
1304
+ }
1305
+ return loginIdByFamily.get(family);
1306
+ };
1040
1307
  for (const auth of authCandidates) {
1041
1308
  try {
1042
1309
  const body = auth.planKind === "coding-plan"
@@ -1064,8 +1331,18 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
1064
1331
  configured: true,
1065
1332
  error: null,
1066
1333
  provider_key: auth.providerKey,
1334
+ region: zcodeProviderFamily(auth.providerKey),
1067
1335
  ...normalized,
1068
1336
  };
1337
+ // Last-good disk cache (house pattern: codex / ark / qoder): persist the
1338
+ // windows so a later network failure serves stale bars instead of a
1339
+ // vanished card. Login-scoped extras stay live-only.
1340
+ writeZcodeLimitsCache(result, {
1341
+ home,
1342
+ nowMs,
1343
+ providerKey: auth.providerKey,
1344
+ identity: zcodeCacheIdentity(loginIdFor(zcodeProviderFamily(auth.providerKey)), auth.teamContext),
1345
+ });
1069
1346
  if (Array.isArray(normalized.buckets) && normalized.buckets.length === 0 && authCandidates.length > 1) {
1070
1347
  emptySuccess = emptySuccess || result;
1071
1348
  continue;
@@ -1126,20 +1403,44 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
1126
1403
  }
1127
1404
  }
1128
1405
  }
1129
- return result;
1406
+ return withZcodeOtherRegion(result, logScan.byRegion);
1130
1407
  } catch (error) {
1131
1408
  errors.push(`${auth.providerKey}: ${error?.message || "Unknown error"}`);
1409
+ partitionFailures.push({ providerKey: auth.providerKey, authError: error?.authError === true });
1132
1410
  }
1133
1411
  }
1134
- if (emptySuccess) return emptySuccess;
1135
- const logFallback = loadLatestZcodeBalanceFromLogs({
1136
- home,
1137
- env,
1138
- providerKeys: authCandidates.map((auth) => auth.providerKey),
1139
- nowMs,
1140
- requireProviderMatch: authCandidates.some((auth) => auth.planKind === "coding-plan"),
1141
- });
1142
- if (logFallback) return zcodeLogFallbackResult(logFallback, errors);
1412
+ if (emptySuccess) return withZcodeOtherRegion(emptySuccess, logScan.byRegion);
1413
+ // Rank fallback partitions in live candidate priority order, and withhold
1414
+ // any partition whose attempts all ended in auth errors: re-login is
1415
+ // actionable, so neither a stale log record nor stale cache bars may mask
1416
+ // it (opencode-go auth_error / codex reauth precedent).
1417
+ const cacheOrder = [];
1418
+ for (const auth of authCandidates) {
1419
+ if (!cacheOrder.includes(auth.providerKey)) cacheOrder.push(auth.providerKey);
1420
+ }
1421
+ const cacheEligible = cacheOrder.filter((providerKey) =>
1422
+ partitionFailures.some((failure) => failure.providerKey === providerKey && !failure.authError));
1423
+ const authErrorOnlyProviders = new Set(cacheOrder.filter((providerKey) => !cacheEligible.includes(providerKey)));
1424
+ // Prefer the #942 scan (region+plan preferred match) over a second log pass,
1425
+ // then apply the #947 auth-error gate so re-login stays actionable.
1426
+ const logRecord = logScan.preferred
1427
+ || (requireProviderMatch && authCandidates.length > 0 ? null : logScan.fallback);
1428
+ // A log record for a provider with no live candidate is untouched by this
1429
+ // gate: nothing was attempted for it, so no error is being masked.
1430
+ if (logRecord && !authErrorOnlyProviders.has(logRecord.providerKey)) {
1431
+ return withZcodeOtherRegion(zcodeLogFallbackResult(logRecord, errors), logScan.byRegion);
1432
+ }
1433
+ // Last stop before the error result: serve the disk cache in live candidate
1434
+ // priority order, scoped to the login that produced the windows so an
1435
+ // account switch cannot inherit the previous login's bars.
1436
+ const identityByKey = new Map();
1437
+ for (const auth of authCandidates) {
1438
+ if (!cacheEligible.includes(auth.providerKey)) continue;
1439
+ if (!identityByKey.has(auth.providerKey)) identityByKey.set(auth.providerKey, new Set());
1440
+ identityByKey.get(auth.providerKey).add(zcodeCacheIdentity(loginIdFor(zcodeProviderFamily(auth.providerKey)), auth.teamContext));
1441
+ }
1442
+ const cached = readZcodeLimitsCache({ home, nowMs, providerKeys: cacheEligible, identityByKey });
1443
+ if (cached) return withZcodeOtherRegion(cached, logScan.byRegion);
1143
1444
  return {
1144
1445
  configured: true,
1145
1446
  error: errors[0] || "Unknown error",
@@ -1157,7 +1458,10 @@ module.exports = {
1157
1458
  loadZcodeCredential,
1158
1459
  loadZcodeLoginAccountId,
1159
1460
  zcodeProviderFamily,
1461
+ zcodeRegionFromProviderId,
1462
+ zcodePlanKindFromProviderId,
1160
1463
  loadLatestZcodeBalanceFromLogs,
1464
+ scanZcodeBalanceLogs,
1161
1465
  resolveZcodeProviderBillingBaseUrl,
1162
1466
  resolveZcodeProviderQuotaUrl,
1163
1467
  loadZcodeAuthCandidates,
@@ -1168,5 +1472,8 @@ module.exports = {
1168
1472
  fetchZcodeBilling,
1169
1473
  fetchZcodeCodingPlanQuota,
1170
1474
  fetchZcodeStartPlanGrants,
1475
+ readZcodeLimitsCache,
1476
+ writeZcodeLimitsCache,
1477
+ zcodeCacheIdentity,
1171
1478
  fetchZcodeLimits,
1172
1479
  };
@@ -1 +0,0 @@
1
- import{r as n,J as a}from"./__vite-browser-external-BEEzY7mq.js";import{u as p}from"./DialogTitle-DP5TLLbz.js";import{u as c}from"./composite-BtDzx3a2.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,i=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};