tokentracker-cli 1.5.2 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{AchievementBadge-B5kmt0VD.js → AchievementBadge-uuDGO9Mq.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-C0FlkOo0.js → AchievementsPage-BCTVT7Jo.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CRcp80Pp.js → AchievementsSection-B6fJSJSr.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DAFjGbKW.js → BadgeDetailModal-FjUN__9G.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CO0bbimB.js → BotAnimated-B-KqY_Wr.js} +1 -1
- package/dashboard/dist/assets/{Card-Xhr5exz8.js → Card-DS8T8piP.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-DsQ7FmsI.js → CloudCheckoutPage-_0UjsFPu.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-DkSexu5V.js → CloudPage-BstgVBaV.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-Bm0huQdj.js → CommandPalette-DtVsYZgc.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BsL8GN8Q.js → CommunityStatsModal-DTpEJqV3.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-_WPp292g.js → ConfirmModal-Lfdh2vr7.js} +1 -1
- package/dashboard/dist/assets/{Controls-ktzn68KM.js → Controls-DpwQ7pkq.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-DWr4zIzF.js → DateRangePopover-CrWgrzT2.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-CBa8Mk47.js → DevicePage-DbK5KWJS.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CZ5jzgtb.js → DialogClose-Dd2B_o03.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Dr0N6q15.js → DialogTitle-3JQBVha3.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-sYqgrO6-.js → ElectricLogo-LYAqG8pG.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Qq-hcrZW.js → FadeIn-DhDvUor7.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B77zSv7i.js → HeaderGithubStar-9f1XmgYC.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-lRDaUmoW.js → HoverTooltip-BXhHAkP7.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-B6AvHZjA.js → IpCheckPage-ChPdDeMl.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DgzOjf3x.js → LandingPage-DnJ7Y9zw.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BtjjtDMC.js → LeaderboardPage-BF9isLzu.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-B2wNTUTU.js → LeaderboardProBadge-BM4JKgkC.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DJJckwHh.js → LeaderboardProfileModal-DfZABOpN.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DJdxByl2.js → LeaderboardProfilePage-BaZ38iT5.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B2lVqf_H.js → LocalOnlyNotice-DvhlYoqQ.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-i0gg1yby.js → LoginCard-g_Gbnzhs.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B-loLNZM.js → LoginModal-DkJrI6b7.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-D_swI75j.js → LoginPage-CqCKSo0n.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DDpKvpjG.js → PetPage-DbF7ROsj.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-Gja4MyEK.js → PopoverPopup-4WuH41ZT.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DOPj7tAZ.js → ProviderIcon-6j0A2KD9.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BVADLk89.js → ResetPasswordPage-DBd-iwuM.js} +1 -1
- package/dashboard/dist/assets/{Select-D-38tN-E.js → Select-BBEpLeiG.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-jN61ukxk.js → SelectItemText-Cn_U_rrO.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-k7AkGpl1.js → SelfHostPage-2WZc6Rml.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +1 -0
- package/dashboard/dist/assets/{SessionsPage-BymccEEz.js → SessionsPage-CWS0iQ1L.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DfnMCAuA.js → SettingsPage-rXNr96i-.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-YyIHIaHN.js → SkillsPage-CudTvbZL.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BLcvZuQ9.js → TokenGalaxy-BRKrbTsJ.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-3E8kyVzz.js → TooltipPopup-CsLKXhWC.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-ByKAdXSF.js → TrendMonitor-8onUTbkk.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BgEVIIzL.js → WidgetsPage-CcwE0lz8.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-HjbX8QaA.js → WrappedPage-Cj9LLYqP.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +1 -0
- package/dashboard/dist/assets/{agent-logos-BVF2siPQ.js → agent-logos-ChBA1nAH.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CYO24PGP.js → arrow-left-Bb5mfoPc.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-Xg9PZYhw.js → arrow-right-D1Ti2XUI.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B7ah6Qv-.js → arrow-up-right-B_CjA_y8.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BV_iG938.js → calendar-days-DdfrAqie.js} +1 -1
- package/dashboard/dist/assets/{check--EZH8RTp.js → check-29zGw1MK.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BzGTlk0-.js → chevron-down-DAzlFX7g.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-BgyxPHEh.js → cloud-checkout-C2SeCjFM.js} +1 -1
- package/dashboard/dist/assets/{composite-C0KrmLEK.js → composite-DIf735Hq.js} +1 -1
- package/dashboard/dist/assets/{copy-BtucSK2U.js → copy-BOuOM0HQ.js} +1 -1
- package/dashboard/dist/assets/{download-CWHq03Y2.js → download-50tQRpGe.js} +1 -1
- package/dashboard/dist/assets/{external-link-DCTXF5VZ.js → external-link-BhlQClVe.js} +1 -1
- package/dashboard/dist/assets/{flame-BE5TsqVF.js → flame-CeGfA-0L.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-ThNIMWnZ.js → format-tokens-Cq7YiAkJ.js} +1 -1
- package/dashboard/dist/assets/{gift-DgaN9s4-.js → gift-Do-UFhiT.js} +1 -1
- package/dashboard/dist/assets/{icons-B4r5EiDC.js → icons-C6EZ_HmP.js} +1 -1
- package/dashboard/dist/assets/{index-CKhcCLNC.js → index-BFrbVqsm.js} +1 -1
- package/dashboard/dist/assets/{index-DqCtgU0G.js → index-BWhbBAd9.js} +1 -1
- package/dashboard/dist/assets/{index-fNKH4U7j.js → index-D9kwYWLX.js} +1 -1
- package/dashboard/dist/assets/{index-DFF9xo6f.js → index-c0crpe9Z.js} +1 -1
- package/dashboard/dist/assets/{info-B4YzRR4y.js → info-B4MXmsNE.js} +1 -1
- package/dashboard/dist/assets/{link-2-DzbUbaBw.js → link-2-DuWCslzl.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B9c7D-7v.js → loader-circle-C2I3NZAO.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-btuk_b0Y.js → local-usage-transfer-BQasvsUF.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-BjrU-Xz7.js → modifiers.esm-DbeNS6FT.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-Bwn4tb4q.js → monitor-smartphone-C6Y4ktsc.js} +1 -1
- package/dashboard/dist/assets/{play-DIFOwGIQ.js → play-CwgcDJC2.js} +1 -1
- package/dashboard/dist/assets/{plus-BpBqnwVg.js → plus-B796wsUO.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DSrs4u7T.js → provider-display-Cw26RFDm.js} +1 -1
- package/dashboard/dist/assets/{react-BgG7_MY0.js → react-d7Bk1-_K.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-BUGWIDOM.js → safePolygon-CDE0dNv_.js} +1 -1
- package/dashboard/dist/assets/{search-aHrmBmtZ.js → search-DtKvsdRs.js} +1 -1
- package/dashboard/dist/assets/{server-DOEvwlEU.js → server-gAURHUyz.js} +1 -1
- package/dashboard/dist/assets/{shield-check-BPTnLhsT.js → shield-check-DFVdDTwh.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C052Mwez.js → skills-api-2rdPUrJI.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DaZKhjhu.js → sortable.esm-DFlfwnJr.js} +1 -1
- package/dashboard/dist/assets/{store-Cq30Pofw.js → store-BKPCk_de.js} +1 -1
- package/dashboard/dist/assets/{terminal-CoSDSkE7.js → terminal-BtWFakvS.js} +1 -1
- package/dashboard/dist/assets/{trash-BAmt-kPj.js → trash-bpr9zTAM.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Bxq0kBhm.js → use-community-stats-DMnGgLne.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DmIQDlvk.js → use-latest-request-guard-BSBlJJHe.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-s_CIC8Bb.js → use-native-settings-OthqiF6g.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BHhykAbt.js → use-ordered-list-DpAtr0R-.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +1 -0
- package/dashboard/dist/assets/{use-transform-CVeMFlS6.js → use-transform-BRz9dz_2.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CwmhudAX.js +1 -0
- package/dashboard/dist/assets/{useCurrency-DilqL2Iv.js → useCurrency-CAA-tQ27.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-BcnySw8a.js → usePositioner-Cj2DPmwM.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CvKW46WP.js → useTokenFormat-Bf5-f7Hx.js} +1 -1
- package/dashboard/dist/assets/{zap-D7VRNTwk.js → zap-B8Fg6PaC.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/serve.js +22 -13
- package/src/lib/deepseek-status.js +157 -0
- package/src/lib/local-api.js +44 -4
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-status.js +15 -2
- package/src/lib/qoder-limits.js +17 -2
- package/src/lib/usage-limits.js +181 -39
- package/src/lib/workbuddy-limits.js +191 -18
- package/src/lib/zcode-limits.js +143 -35
- package/dashboard/dist/assets/DashboardPage-CdNXi3Im.js +0 -1
- package/dashboard/dist/assets/DialogDescription-kCZ4gN09.js +0 -1
- package/dashboard/dist/assets/LimitsPage-gSBHSBi_.js +0 -6
- package/dashboard/dist/assets/ServiceStatusPage-D_Fa_pYz.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-Br-KAXh6.css +0 -1
- package/dashboard/dist/assets/__vite-browser-external-D_cMeuqI.js +0 -45
- package/dashboard/dist/assets/use-limits-display-prefs-CyAn1fme.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BmFyFE3S.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-DRZMI20u.js +0 -1
|
@@ -19,10 +19,18 @@
|
|
|
19
19
|
// Display names are English regardless of the backend's localized names.
|
|
20
20
|
|
|
21
21
|
const fs = require("node:fs");
|
|
22
|
+
const os = require("node:os");
|
|
22
23
|
const path = require("node:path");
|
|
23
24
|
const { resolveWorkbuddyHome } = require("./rollout");
|
|
24
25
|
const { UNAVAILABLE, readWorkbuddyIpcDiscovery, openWorkbuddyIpc } = require("./workbuddy-ipc");
|
|
25
26
|
|
|
27
|
+
const WORKBUDDY_LIMITS_CACHE_FILE = "workbuddy-usage-limits-cache.json";
|
|
28
|
+
// Windows without a usable reset_at — the pooled resource window by design,
|
|
29
|
+
// plus a plan row whose cycle end never parsed — cannot be checked against a
|
|
30
|
+
// quota rollover, so they are only served while the snapshot is young enough
|
|
31
|
+
// to trust.
|
|
32
|
+
const WORKBUDDY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
33
|
+
|
|
26
34
|
const PLAN_PRIORITY = Object.freeze({
|
|
27
35
|
TCACA_code_027_0FCGVA6vSa: 4, // flagship
|
|
28
36
|
TCACA_code_026_BaESVICNoi: 3, // advanced
|
|
@@ -451,12 +459,16 @@ function readAuthFile(filePath) {
|
|
|
451
459
|
const auth = parsed?.auth;
|
|
452
460
|
if (!auth || typeof auth !== "object") return null;
|
|
453
461
|
const domain = isNonEmptyString(auth.domain) ? auth.domain.trim() : "";
|
|
462
|
+
// The app records the signed-in account next to its (often encrypted)
|
|
463
|
+
// session. The uid stays readable while the app is closed, so it is the
|
|
464
|
+
// one account signal that survives into the UNAVAILABLE state.
|
|
465
|
+
const uid = isNonEmptyString(parsed.account?.uid) ? parsed.account.uid.trim() : "";
|
|
454
466
|
const token = auth.accessToken;
|
|
455
|
-
if (isNonEmptyString(token)) return { token: token.trim(), domain, encrypted: false };
|
|
467
|
+
if (isNonEmptyString(token)) return { token: token.trim(), domain, encrypted: false, uid };
|
|
456
468
|
if (token && typeof token === "object" && token.$wbEncrypted === 1) {
|
|
457
|
-
return { token: null, domain, encrypted: true };
|
|
469
|
+
return { token: null, domain, encrypted: true, uid };
|
|
458
470
|
}
|
|
459
|
-
return { token: null, domain, encrypted: false };
|
|
471
|
+
return { token: null, domain, encrypted: false, uid };
|
|
460
472
|
}
|
|
461
473
|
|
|
462
474
|
function resolveSession(variant, { env, authFiles, workbuddyHome }) {
|
|
@@ -464,15 +476,120 @@ function resolveSession(variant, { env, authFiles, workbuddyHome }) {
|
|
|
464
476
|
? authFiles
|
|
465
477
|
: authSearchDirs(env, workbuddyHome).flatMap((dir) => variant.authFileNames.map((name) => path.join(dir, name)));
|
|
466
478
|
let encryptedForVariant = false;
|
|
479
|
+
let encryptedUid = "";
|
|
467
480
|
for (const filePath of files) {
|
|
468
481
|
if (!filePath) continue;
|
|
469
482
|
const session = readAuthFile(filePath);
|
|
470
483
|
if (!session) continue;
|
|
471
484
|
if (!hostAllowed(hostOf(session.domain), variant.allowedHostSuffixes)) continue;
|
|
472
|
-
if (session.token) return { token: session.token, encrypted: false, matched: true };
|
|
473
|
-
if (session.encrypted)
|
|
485
|
+
if (session.token) return { token: session.token, encrypted: false, matched: true, uid: session.uid };
|
|
486
|
+
if (session.encrypted) {
|
|
487
|
+
encryptedForVariant = true;
|
|
488
|
+
// First encrypted candidate wins: when the app is closed this record is
|
|
489
|
+
// what a live read would authenticate with, so its account is the one
|
|
490
|
+
// the cached windows must match.
|
|
491
|
+
encryptedUid = encryptedUid || session.uid;
|
|
492
|
+
}
|
|
474
493
|
}
|
|
475
|
-
return { token: null, encrypted: encryptedForVariant, matched: encryptedForVariant };
|
|
494
|
+
return { token: null, encrypted: encryptedForVariant, matched: encryptedForVariant, uid: encryptedUid };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Last-good disk cache for WorkBuddy quota windows (house pattern: codex /
|
|
499
|
+
// ark / qoder / zcode, issue #952). A live read — IPC or API — persists the
|
|
500
|
+
// windows per edition; when the live chain later fails, the stale windows are
|
|
501
|
+
// served with data-age markers instead of a bare "Start WorkBuddy and sign
|
|
502
|
+
// in". The steady state this covers is the app being closed: wbipc discovery
|
|
503
|
+
// is gone and the encrypted session cannot be read.
|
|
504
|
+
//
|
|
505
|
+
// Editions never borrow each other's data: workbuddy CN and workbuddyAi are
|
|
506
|
+
// separate partitions of one file, matching the edition-local socket
|
|
507
|
+
// discipline. Partitions are also scoped to the login that produced the
|
|
508
|
+
// windows, so an account switch followed by a live failure cannot serve the
|
|
509
|
+
// previous account's bars.
|
|
510
|
+
//
|
|
511
|
+
// The cache identity comes from the session selection itself (resolveSession):
|
|
512
|
+
// the record whose token supplies — or, while the app is closed, would supply
|
|
513
|
+
// — the live read. It is the app's own account uid, readable after the app
|
|
514
|
+
// closes when the encrypted session token is not, and it is an account id,
|
|
515
|
+
// not a credential: nothing secret is persisted or hashed. An account the
|
|
516
|
+
// records do not name ("unknown") matches only its own entries.
|
|
517
|
+
|
|
518
|
+
function workbuddyLimitsCachePath({ home } = {}) {
|
|
519
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", WORKBUDDY_LIMITS_CACHE_FILE);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// A window whose reset_at has already passed is stale data, not a usable
|
|
523
|
+
// fallback — the quota has rolled over, so drop it. Undated windows (the
|
|
524
|
+
// resource pool, or a plan row without a readable cycle end) cannot be
|
|
525
|
+
// checked against a reset and are bounded by the snapshot-age TTL instead.
|
|
526
|
+
function workbuddyCachedWindow(window, { cachedAtMs, nowMs } = {}) {
|
|
527
|
+
if (!window || typeof window !== "object") return null;
|
|
528
|
+
const resetAtMs = Date.parse(window.reset_at || "");
|
|
529
|
+
if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
|
|
530
|
+
return nowMs - cachedAtMs <= WORKBUDDY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Read the last-good windows for one edition, scoped to the login that wrote them. */
|
|
534
|
+
function readWorkbuddyLimitsCache({ home, nowMs = Date.now(), variantId, identity } = {}) {
|
|
535
|
+
// An explicit home keeps the cache hermetic: callers that never said where
|
|
536
|
+
// the user's tracker state lives (tests, odd embedders) must not touch the
|
|
537
|
+
// developer's real cache file.
|
|
538
|
+
if (!home || !variantId || !identity) return null;
|
|
539
|
+
let parsed;
|
|
540
|
+
try {
|
|
541
|
+
parsed = JSON.parse(fs.readFileSync(workbuddyLimitsCachePath({ home }), "utf8"));
|
|
542
|
+
} catch (_error) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
546
|
+
const raw = parsed[variantId];
|
|
547
|
+
if (!raw || typeof raw !== "object") return null;
|
|
548
|
+
if (raw.identity !== identity) return null;
|
|
549
|
+
const cachedAtMs = Date.parse(raw.cached_at || "");
|
|
550
|
+
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
551
|
+
const context = { cachedAtMs, nowMs };
|
|
552
|
+
const primaryWindow = workbuddyCachedWindow(raw.primary_window, context);
|
|
553
|
+
const secondaryWindow = workbuddyCachedWindow(raw.secondary_window, context);
|
|
554
|
+
// A partition whose windows all rolled or expired no longer describes the
|
|
555
|
+
// account's quota — withholding it lets the live error stay visible.
|
|
556
|
+
if (!primaryWindow && !secondaryWindow) return null;
|
|
557
|
+
return {
|
|
558
|
+
configured: true,
|
|
559
|
+
error: null,
|
|
560
|
+
plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
|
|
561
|
+
primary_window: primaryWindow,
|
|
562
|
+
secondary_window: secondaryWindow,
|
|
563
|
+
cached: true,
|
|
564
|
+
stale: true,
|
|
565
|
+
cached_at: raw.cached_at,
|
|
566
|
+
source: "disk-cache",
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Merge one edition's partition into the disk cache; best-effort, never throws. */
|
|
571
|
+
function writeWorkbuddyLimitsCache(limits, { home, nowMs = Date.now(), variantId, identity } = {}) {
|
|
572
|
+
if (!home || !variantId || !identity || !limits?.configured || limits.error) return;
|
|
573
|
+
if (!limits.primary_window && !limits.secondary_window) return;
|
|
574
|
+
const cachePath = workbuddyLimitsCachePath({ home });
|
|
575
|
+
let payload = {};
|
|
576
|
+
try {
|
|
577
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
578
|
+
if (parsed && typeof parsed === "object") payload = parsed;
|
|
579
|
+
} catch (_error) {}
|
|
580
|
+
payload[variantId] = {
|
|
581
|
+
identity,
|
|
582
|
+
plan_label: typeof limits.plan_label === "string" ? limits.plan_label : null,
|
|
583
|
+
primary_window: limits.primary_window || null,
|
|
584
|
+
secondary_window: limits.secondary_window || null,
|
|
585
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
586
|
+
};
|
|
587
|
+
try {
|
|
588
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
589
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
590
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
591
|
+
fs.renameSync(tmpPath, cachePath);
|
|
592
|
+
} catch (_error) {}
|
|
476
593
|
}
|
|
477
594
|
|
|
478
595
|
function readProductEndpoint(workbuddyHome) {
|
|
@@ -584,19 +701,24 @@ function scopedEnv(env, home) {
|
|
|
584
701
|
return next;
|
|
585
702
|
}
|
|
586
703
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
704
|
+
// Live chain for one edition, unchanged by the cache layer: the IPC proxy
|
|
705
|
+
// while the app publishes it, then the API source with a resolvable session.
|
|
706
|
+
// The session is selected by the caller (fetchVariant) so the cache identity
|
|
707
|
+
// and the live read cannot disagree about the account. Throws propagate to
|
|
708
|
+
// the cache wrapper below; the { configured: true, error: UNAVAILABLE }
|
|
709
|
+
// result comes back when the app is closed and no session can be read.
|
|
710
|
+
async function fetchVariantLive(variant, variantId, { env, workbuddyHome, installed, opts, session }) {
|
|
593
711
|
const discovery = readWorkbuddyIpcDiscovery(workbuddyHome);
|
|
594
712
|
if (discovery) {
|
|
595
713
|
// Discovery is edition-local. Never fall back to the other edition's
|
|
596
714
|
// socket or allow an explicitly redirected product to be mislabelled.
|
|
597
715
|
const productEndpoint = readProductEndpoint(workbuddyHome);
|
|
598
716
|
if (productEndpoint && !originAllowed(productEndpoint, variant)) {
|
|
599
|
-
|
|
717
|
+
const error = new Error("WorkBuddy quota host does not match this edition.");
|
|
718
|
+
// Tagged so the cache wrapper surfaces the mislabelled host instead of
|
|
719
|
+
// stale bars from whichever login wrote them.
|
|
720
|
+
error.code = "EDITION_MISMATCH";
|
|
721
|
+
throw error;
|
|
600
722
|
}
|
|
601
723
|
const client = await openWorkbuddyIpc({
|
|
602
724
|
discovery, signal: opts.signal, timeoutMs: opts.timeoutMs, connectImpl: opts.connectImpl,
|
|
@@ -607,10 +729,6 @@ async function fetchVariant(variantId, opts = {}) {
|
|
|
607
729
|
client.close();
|
|
608
730
|
}
|
|
609
731
|
}
|
|
610
|
-
const explicitToken = resolveToken(variant, env);
|
|
611
|
-
const session = explicitToken
|
|
612
|
-
? { token: explicitToken, encrypted: false, matched: true }
|
|
613
|
-
: resolveSession(variant, { env, authFiles: opts.authFiles, workbuddyHome });
|
|
614
732
|
if (!session.token) {
|
|
615
733
|
if (session.encrypted || installed) return { configured: true, error: UNAVAILABLE };
|
|
616
734
|
if (!installed) return { configured: false };
|
|
@@ -625,12 +743,64 @@ async function fetchVariant(variantId, opts = {}) {
|
|
|
625
743
|
workbuddyHome: opts.origin ? null : workbuddyHome,
|
|
626
744
|
});
|
|
627
745
|
if (!originAllowed(origin, variant)) {
|
|
628
|
-
|
|
746
|
+
const error = new Error("WorkBuddy quota host does not match this edition.");
|
|
747
|
+
error.code = "EDITION_MISMATCH";
|
|
748
|
+
throw error;
|
|
629
749
|
}
|
|
630
750
|
return { ...await fetchPlanQuota((apiPath, body) => postBilling(fetchImpl,
|
|
631
751
|
`${origin}${apiPath}`, session.token, body, opts.signal), variantId), source: "api" };
|
|
632
752
|
}
|
|
633
753
|
|
|
754
|
+
async function fetchVariant(variantId, opts = {}) {
|
|
755
|
+
const variant = variantById(variantId);
|
|
756
|
+
if (!variant) return { configured: false };
|
|
757
|
+
const env = scopedEnv(opts.env || process.env, opts.home);
|
|
758
|
+
const workbuddyHome = opts.workbuddyHome || resolveWorkbuddyHome(env, variant.source);
|
|
759
|
+
const installed = directoryExists(workbuddyHome);
|
|
760
|
+
// Sampled when used, not when the fetch starts: the live attempt can spend
|
|
761
|
+
// the whole provider budget before a cache read or write happens, and a
|
|
762
|
+
// window whose reset rolled in that span must not be served. An injected
|
|
763
|
+
// clock (tests) keeps every use on the same instant.
|
|
764
|
+
const nowMs = () => (Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now());
|
|
765
|
+
// One session selection feeds both the live attempt and the cache identity:
|
|
766
|
+
// cached windows are stamped with (and served only to) the account whose
|
|
767
|
+
// record supplies — or, while the app is closed, would supply — the session
|
|
768
|
+
// token. An env-token session names no record; "unknown" matches only its
|
|
769
|
+
// own entries.
|
|
770
|
+
const explicitToken = resolveToken(variant, env);
|
|
771
|
+
const session = explicitToken
|
|
772
|
+
? { token: explicitToken, encrypted: false, matched: true, uid: "" }
|
|
773
|
+
: resolveSession(variant, { env, authFiles: opts.authFiles, workbuddyHome });
|
|
774
|
+
const identity = (explicitToken ? "" : session.uid) || "unknown";
|
|
775
|
+
let live;
|
|
776
|
+
try {
|
|
777
|
+
live = await fetchVariantLive(variant, variantId, { env, workbuddyHome, installed, opts, session });
|
|
778
|
+
} catch (error) {
|
|
779
|
+
// Re-login stays actionable (opencode-go auth_error / codex reauth
|
|
780
|
+
// precedent) and a mislabelled host is a configuration fault, not
|
|
781
|
+
// transient data loss: neither may be masked by stale bars.
|
|
782
|
+
if (error?.code === "AUTH_EXPIRED" || error?.code === "EDITION_MISMATCH") throw error;
|
|
783
|
+
const cached = readWorkbuddyLimitsCache({ home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
784
|
+
if (cached) return cached;
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
if (live.configured === true && !live.error) {
|
|
788
|
+
// Live read is current as of now: persist it so a later failure serves
|
|
789
|
+
// stale bars instead of a vanished card.
|
|
790
|
+
writeWorkbuddyLimitsCache(live, { home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
791
|
+
return live;
|
|
792
|
+
}
|
|
793
|
+
if (live.configured === true && live.error) {
|
|
794
|
+
// The steady state the cache exists for (UNAVAILABLE: app closed, session
|
|
795
|
+
// unreadable) and transient API/IPC failures. A never-signed-in install
|
|
796
|
+
// ({ configured: false }) stays untouched — an edition that produced no
|
|
797
|
+
// windows here has nothing to fall back to.
|
|
798
|
+
const cached = readWorkbuddyLimitsCache({ home: opts.home, nowMs: nowMs(), variantId, identity });
|
|
799
|
+
if (cached) return cached;
|
|
800
|
+
}
|
|
801
|
+
return live;
|
|
802
|
+
}
|
|
803
|
+
|
|
634
804
|
function fetchWorkbuddyLimits(opts) {
|
|
635
805
|
return fetchVariant("workbuddy", opts);
|
|
636
806
|
}
|
|
@@ -651,6 +821,9 @@ module.exports = {
|
|
|
651
821
|
readSubscriptionIdentity,
|
|
652
822
|
originAllowed,
|
|
653
823
|
hostAllowed,
|
|
824
|
+
readWorkbuddyLimitsCache,
|
|
825
|
+
writeWorkbuddyLimitsCache,
|
|
826
|
+
workbuddyLimitsCachePath,
|
|
654
827
|
fetchWorkbuddyLimits,
|
|
655
828
|
fetchWorkbuddyAiLimits,
|
|
656
829
|
};
|
package/src/lib/zcode-limits.js
CHANGED
|
@@ -1156,6 +1156,10 @@ function readZcodeLimitsCache({ home, nowMs = Date.now(), providerKeys = [], ide
|
|
|
1156
1156
|
configured: true,
|
|
1157
1157
|
error: null,
|
|
1158
1158
|
provider_key: providerKey,
|
|
1159
|
+
// Live results carry their provider's region for the other_region
|
|
1160
|
+
// attach; the cached shape mirrors them so a cache-served card keeps
|
|
1161
|
+
// that section too (issue #959).
|
|
1162
|
+
region: zcodeRegionFromProviderId(providerKey),
|
|
1159
1163
|
plan_kind: typeof raw.plan_kind === "string" ? raw.plan_kind : null,
|
|
1160
1164
|
plan_id: typeof raw.plan_id === "string" ? raw.plan_id : null,
|
|
1161
1165
|
plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
|
|
@@ -1172,27 +1176,23 @@ function readZcodeLimitsCache({ home, nowMs = Date.now(), providerKeys = [], ide
|
|
|
1172
1176
|
return null;
|
|
1173
1177
|
}
|
|
1174
1178
|
|
|
1175
|
-
/**
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
+
/**
|
|
1180
|
+
* Merge partitions into the disk cache atomically; best-effort, never throws.
|
|
1181
|
+
* Cross-process semantics are last-writer-wins on purpose, matching the house
|
|
1182
|
+
* pattern (ark / qoder / codex caches): a concurrent writer can discard a
|
|
1183
|
+
* partition added between its read and rename, which costs one refresh cycle
|
|
1184
|
+
* — the next fetch rewrites it. Snapshots in particular keep getting
|
|
1185
|
+
* re-persisted on every fetch while their region's rows are inside the scan
|
|
1186
|
+
* window, so a lost write self-heals long before the window expires.
|
|
1187
|
+
*/
|
|
1188
|
+
function mergeZcodeLimitsCacheEntries(entries, { home } = {}) {
|
|
1179
1189
|
const cachePath = zcodeLimitsCachePath({ home });
|
|
1180
1190
|
let payload = {};
|
|
1181
1191
|
try {
|
|
1182
1192
|
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
1183
1193
|
if (parsed && typeof parsed === "object") payload = parsed;
|
|
1184
1194
|
} catch (_error) {}
|
|
1185
|
-
payload
|
|
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
|
-
};
|
|
1195
|
+
Object.assign(payload, entries);
|
|
1196
1196
|
try {
|
|
1197
1197
|
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
1198
1198
|
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
@@ -1201,28 +1201,129 @@ function writeZcodeLimitsCache(limits, { home, nowMs = Date.now(), providerKey,
|
|
|
1201
1201
|
} catch (_error) {}
|
|
1202
1202
|
}
|
|
1203
1203
|
|
|
1204
|
+
/** Merge one provider partition into the disk cache; best-effort, never throws. */
|
|
1205
|
+
function writeZcodeLimitsCache(limits, { home, nowMs = Date.now(), providerKey, identity } = {}) {
|
|
1206
|
+
if (!providerKey || !identity || !limits?.configured || limits.error) return;
|
|
1207
|
+
if (!limits.primary_window && !limits.secondary_window && !limits.tertiary_window) return;
|
|
1208
|
+
mergeZcodeLimitsCacheEntries({
|
|
1209
|
+
[providerKey]: {
|
|
1210
|
+
identity,
|
|
1211
|
+
plan_kind: typeof limits.plan_kind === "string" ? limits.plan_kind : null,
|
|
1212
|
+
plan_id: typeof limits.plan_id === "string" ? limits.plan_id : null,
|
|
1213
|
+
plan_label: typeof limits.plan_label === "string" ? limits.plan_label : null,
|
|
1214
|
+
buckets: Array.isArray(limits.buckets) ? limits.buckets : [],
|
|
1215
|
+
primary_window: limits.primary_window || null,
|
|
1216
|
+
secondary_window: limits.secondary_window || null,
|
|
1217
|
+
tertiary_window: limits.tertiary_window || null,
|
|
1218
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
1219
|
+
},
|
|
1220
|
+
}, { home });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Issue #959: ZCode only queries the balance of the region currently in use,
|
|
1224
|
+
// so the other region's log rows stop appearing the moment it goes idle and
|
|
1225
|
+
// then age out of the 6h scan window — the other_region section would vanish.
|
|
1226
|
+
// While a region's rows are still fresh, snapshot its latest record into a
|
|
1227
|
+
// per-region cache partition; withZcodeOtherRegion falls back to it after the
|
|
1228
|
+
// window expires. The snapshot shape comes straight from
|
|
1229
|
+
// normalizeZcodeBalanceResponse, so the no-reset_credits / no-start_plan red
|
|
1230
|
+
// line holds structurally. Log records carry no login id, so unlike the
|
|
1231
|
+
// windows partitions there is no identity guard here: staleness is
|
|
1232
|
+
// self-attested by the snapshot's log_timestamp (rendered as the section's
|
|
1233
|
+
// time row), and the section stays collapsed and informational.
|
|
1234
|
+
function zcodeLogSnapshotCacheKey(region) {
|
|
1235
|
+
return `log-snapshot:${region}`;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function writeZcodeLogSnapshotCache(byRegion, { home, nowMs = Date.now() } = {}) {
|
|
1239
|
+
if (!(byRegion instanceof Map) || byRegion.size === 0) return;
|
|
1240
|
+
const entries = {};
|
|
1241
|
+
for (const [region, record] of byRegion) {
|
|
1242
|
+
if (region !== "bigmodel" && region !== "zai") continue;
|
|
1243
|
+
let normalized;
|
|
1244
|
+
try {
|
|
1245
|
+
normalized = normalizeZcodeBalanceResponse(record.body);
|
|
1246
|
+
} catch (_error) {
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
entries[zcodeLogSnapshotCacheKey(region)] = {
|
|
1250
|
+
source: "zcode-log",
|
|
1251
|
+
region,
|
|
1252
|
+
provider_key: record.providerKey,
|
|
1253
|
+
log_timestamp: record.log_timestamp,
|
|
1254
|
+
...normalized,
|
|
1255
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
if (Object.keys(entries).length > 0) mergeZcodeLimitsCacheEntries(entries, { home });
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Read one region's persisted log snapshot, shaped exactly like the fresh
|
|
1263
|
+
* withZcodeOtherRegion attach (issue #938). Deliberately no per-window
|
|
1264
|
+
* expiry: clipping aged windows would make the cached section render
|
|
1265
|
+
* differently from the same record served fresh inside the scan window.
|
|
1266
|
+
*/
|
|
1267
|
+
function readZcodeLogSnapshotCache(region, { home, nowMs = Date.now() } = {}) {
|
|
1268
|
+
if (region !== "bigmodel" && region !== "zai") return null;
|
|
1269
|
+
let parsed;
|
|
1270
|
+
try {
|
|
1271
|
+
parsed = JSON.parse(fs.readFileSync(zcodeLimitsCachePath({ home }), "utf8"));
|
|
1272
|
+
} catch (_error) {
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
const raw = parsed?.[zcodeLogSnapshotCacheKey(region)];
|
|
1276
|
+
if (!raw || typeof raw !== "object" || raw.source !== "zcode-log" || raw.region !== region) return null;
|
|
1277
|
+
if (!Number.isFinite(Date.parse(typeof raw.log_timestamp === "string" ? raw.log_timestamp : ""))) return null;
|
|
1278
|
+
const cachedAtMs = Date.parse(typeof raw.cached_at === "string" ? raw.cached_at : "");
|
|
1279
|
+
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
1280
|
+
return {
|
|
1281
|
+
source: "zcode-log",
|
|
1282
|
+
region,
|
|
1283
|
+
provider_key: typeof raw.provider_key === "string" ? raw.provider_key : null,
|
|
1284
|
+
log_timestamp: raw.log_timestamp,
|
|
1285
|
+
plan_kind: typeof raw.plan_kind === "string" ? raw.plan_kind : null,
|
|
1286
|
+
plan_id: typeof raw.plan_id === "string" ? raw.plan_id : null,
|
|
1287
|
+
plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
|
|
1288
|
+
buckets: Array.isArray(raw.buckets) ? raw.buckets : [],
|
|
1289
|
+
primary_window: raw.primary_window || null,
|
|
1290
|
+
secondary_window: raw.secondary_window || null,
|
|
1291
|
+
tertiary_window: raw.tertiary_window || null,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1204
1295
|
/**
|
|
1205
1296
|
* Attach the freshest logged balance snapshot from the user's other region
|
|
1206
1297
|
* (issue #938): ZCode's own usage logs carry both regions' billing data even
|
|
1207
|
-
* when only the active region's credentials are on disk.
|
|
1298
|
+
* when only the active region's credentials are on disk. Once those rows age
|
|
1299
|
+
* out of the 6h scan window (ZCode only queries the active region's balance,
|
|
1300
|
+
* issue #959), fall back to the snapshot persisted by
|
|
1301
|
+
* writeZcodeLogSnapshotCache while they were fresh. Either way the section is
|
|
1208
1302
|
* read-only and deliberately carries no reset_credits / start_plan extras —
|
|
1209
1303
|
* those are login-scoped and must never ride along across regions.
|
|
1210
1304
|
*/
|
|
1211
|
-
function withZcodeOtherRegion(result, byRegion) {
|
|
1305
|
+
function withZcodeOtherRegion(result, byRegion, { home, env = process.env, nowMs = Date.now() } = {}) {
|
|
1212
1306
|
if (!result || result.error || !result.region) return result;
|
|
1213
1307
|
const otherRegion = result.region === "bigmodel" ? "zai" : "bigmodel";
|
|
1214
1308
|
const record = byRegion instanceof Map ? byRegion.get(otherRegion) : null;
|
|
1215
|
-
if (
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1309
|
+
if (record) {
|
|
1310
|
+
return {
|
|
1311
|
+
...result,
|
|
1312
|
+
other_region: {
|
|
1313
|
+
source: "zcode-log",
|
|
1314
|
+
region: otherRegion,
|
|
1315
|
+
provider_key: record.providerKey,
|
|
1316
|
+
log_timestamp: record.log_timestamp,
|
|
1317
|
+
...normalizeZcodeBalanceResponse(record.body),
|
|
1318
|
+
},
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
// Saved snapshots are log-derived data, so the log-fallback kill switch
|
|
1322
|
+
// silences them along with the scan that feeds them.
|
|
1323
|
+
if (env?.TOKENTRACKER_ZCODE_DISABLE_LOG_FALLBACK === "1") return result;
|
|
1324
|
+
const snapshot = readZcodeLogSnapshotCache(otherRegion, { home, nowMs });
|
|
1325
|
+
if (!snapshot) return result;
|
|
1326
|
+
return { ...result, other_region: snapshot };
|
|
1226
1327
|
}
|
|
1227
1328
|
|
|
1228
1329
|
/**
|
|
@@ -1264,8 +1365,9 @@ function loadZcodeLoginScopedTokens(family, auth, { home, env } = {}) {
|
|
|
1264
1365
|
* logs, and finally the last-good disk cache. The result carries the winning
|
|
1265
1366
|
* provider's `region` ("bigmodel" | "zai") and, when ZCode's own logs hold one,
|
|
1266
1367
|
* an `other_region` snapshot of the second region's plan (issue #938) — read-only
|
|
1267
|
-
* log data, never extended with login-scoped extras
|
|
1268
|
-
*
|
|
1368
|
+
* log data, never extended with login-scoped extras, served from the logs while
|
|
1369
|
+
* fresh and from the disk cache once the scan window expires (issue #959).
|
|
1370
|
+
* Credentials and internal team routing identifiers are omitted from the result.
|
|
1269
1371
|
*/
|
|
1270
1372
|
async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now(), providerTimeoutMs } = {}) {
|
|
1271
1373
|
const startedAt = Date.now();
|
|
@@ -1283,11 +1385,15 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
|
|
|
1283
1385
|
providerKeys: authCandidates.map((auth) => auth.providerKey),
|
|
1284
1386
|
nowMs,
|
|
1285
1387
|
});
|
|
1388
|
+
// Issue #959: capture each region's freshest log record while it is still
|
|
1389
|
+
// inside the scan window, so the other_region section survives the window
|
|
1390
|
+
// expiring (ZCode stops writing rows for a region the moment it goes idle).
|
|
1391
|
+
writeZcodeLogSnapshotCache(logScan.byRegion, { home, nowMs });
|
|
1286
1392
|
const requireProviderMatch = authCandidates.some((auth) => auth.planKind === "coding-plan");
|
|
1287
1393
|
const selectLogFallback = (errors) => {
|
|
1288
1394
|
const record = logScan.preferred
|
|
1289
1395
|
|| (requireProviderMatch && authCandidates.length > 0 ? null : logScan.fallback);
|
|
1290
|
-
return withZcodeOtherRegion(zcodeLogFallbackResult(record, errors), logScan.byRegion);
|
|
1396
|
+
return withZcodeOtherRegion(zcodeLogFallbackResult(record, errors), logScan.byRegion, { home, env, nowMs });
|
|
1291
1397
|
};
|
|
1292
1398
|
if (!authCandidates.length) {
|
|
1293
1399
|
return selectLogFallback([]) || { configured: false };
|
|
@@ -1403,13 +1509,13 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
|
|
|
1403
1509
|
}
|
|
1404
1510
|
}
|
|
1405
1511
|
}
|
|
1406
|
-
return withZcodeOtherRegion(result, logScan.byRegion);
|
|
1512
|
+
return withZcodeOtherRegion(result, logScan.byRegion, { home, env, nowMs });
|
|
1407
1513
|
} catch (error) {
|
|
1408
1514
|
errors.push(`${auth.providerKey}: ${error?.message || "Unknown error"}`);
|
|
1409
1515
|
partitionFailures.push({ providerKey: auth.providerKey, authError: error?.authError === true });
|
|
1410
1516
|
}
|
|
1411
1517
|
}
|
|
1412
|
-
if (emptySuccess) return withZcodeOtherRegion(emptySuccess, logScan.byRegion);
|
|
1518
|
+
if (emptySuccess) return withZcodeOtherRegion(emptySuccess, logScan.byRegion, { home, env, nowMs });
|
|
1413
1519
|
// Rank fallback partitions in live candidate priority order, and withhold
|
|
1414
1520
|
// any partition whose attempts all ended in auth errors: re-login is
|
|
1415
1521
|
// actionable, so neither a stale log record nor stale cache bars may mask
|
|
@@ -1428,7 +1534,7 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
|
|
|
1428
1534
|
// A log record for a provider with no live candidate is untouched by this
|
|
1429
1535
|
// gate: nothing was attempted for it, so no error is being masked.
|
|
1430
1536
|
if (logRecord && !authErrorOnlyProviders.has(logRecord.providerKey)) {
|
|
1431
|
-
return withZcodeOtherRegion(zcodeLogFallbackResult(logRecord, errors), logScan.byRegion);
|
|
1537
|
+
return withZcodeOtherRegion(zcodeLogFallbackResult(logRecord, errors), logScan.byRegion, { home, env, nowMs });
|
|
1432
1538
|
}
|
|
1433
1539
|
// Last stop before the error result: serve the disk cache in live candidate
|
|
1434
1540
|
// priority order, scoped to the login that produced the windows so an
|
|
@@ -1440,7 +1546,7 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
|
|
|
1440
1546
|
identityByKey.get(auth.providerKey).add(zcodeCacheIdentity(loginIdFor(zcodeProviderFamily(auth.providerKey)), auth.teamContext));
|
|
1441
1547
|
}
|
|
1442
1548
|
const cached = readZcodeLimitsCache({ home, nowMs, providerKeys: cacheEligible, identityByKey });
|
|
1443
|
-
if (cached) return withZcodeOtherRegion(cached, logScan.byRegion);
|
|
1549
|
+
if (cached) return withZcodeOtherRegion(cached, logScan.byRegion, { home, env, nowMs });
|
|
1444
1550
|
return {
|
|
1445
1551
|
configured: true,
|
|
1446
1552
|
error: errors[0] || "Unknown error",
|
|
@@ -1474,6 +1580,8 @@ module.exports = {
|
|
|
1474
1580
|
fetchZcodeStartPlanGrants,
|
|
1475
1581
|
readZcodeLimitsCache,
|
|
1476
1582
|
writeZcodeLimitsCache,
|
|
1583
|
+
writeZcodeLogSnapshotCache,
|
|
1584
|
+
readZcodeLogSnapshotCache,
|
|
1477
1585
|
zcodeCacheIdentity,
|
|
1478
1586
|
fetchZcodeLimits,
|
|
1479
1587
|
};
|