tokentracker-cli 1.5.13 → 1.5.15
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-Ct0vfdYb.js → AchievementBadge-CwjI28oT.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-dfoRxqOv.js → AchievementsPage-Dq544fjX.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DHFyssU0.js → AchievementsSection-DufV6nod.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-ChJYZMSq.js → BadgeDetailModal-vw8xydKu.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-C_qaJy1-.js → BotAnimated-De0anwsh.js} +1 -1
- package/dashboard/dist/assets/{Card-BhK2nmOu.js → Card-CuSeWBDM.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-Ele1kkut.js → CloudCheckoutPage-CxFcOwAj.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-C5DMqcoV.js → CloudPage-Beb7_JLW.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-C_hxoIwL.js → CommandPalette-PsUq2iRS.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BSTqBIEU.js → CommunityStatsModal-b54WaPbK.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DI4QK5EV.js → ConfirmModal-_NdunIh5.js} +1 -1
- package/dashboard/dist/assets/{Controls-zK1rzS_8.js → Controls-5Is6MWln.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BdDnmc60.js → DashboardPage-C0n0qC7h.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-DqwgxpNF.js → DateRangePopover-C3WfGy-0.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-C0fQKKuI.js → DevicePage-Vn6k-mGd.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-C_cbyHpb.js → DialogClose-CXttnna-.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-C3KGos_G.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BcBpd--_.js → DialogTitle-B0cNIo0E.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Dw9gkExE.js → ElectricLogo-DC3XSfIu.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Ci69gBGl.js → FadeIn-H7S3TMHi.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-pb03c0kb.js → HeaderGithubStar-xgbIo5OP.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-Cqd0D789.js → HoverTooltip-CpzM5Dju.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-qILbvOJn.js → IpCheckPage-BdV7S4dY.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CCIwaAeu.js → LandingPage-D9f28ryO.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BwzABmKU.js → LeaderboardPage--WUFpkBI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-7kdzfw8r.js → LeaderboardProBadge-CEvrQ75Y.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-uMnN-IGe.js → LeaderboardProfileModal-Bn0HFr5h.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CQNc4MES.js → LeaderboardProfilePage-DJ7fQI68.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CJ1LImRm.js → LimitsPage-7tPDheuX.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Dm0l_F0I.js → LocalOnlyNotice-DlGbVQS8.js} +1 -1
- package/dashboard/dist/assets/{LoginCard--tUiBVtr.js → LoginCard-BZZmzUGh.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B1-5fFMY.js → LoginModal-BT5_bIxU.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BYl6NnZ8.js → LoginPage-DMjDUPPi.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DJNGJZrR.js → PetPage-Dk3j6ITn.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-aRAdIkTs.js → PopoverPopup-pugRu3RC.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-uiA-KBe2.js → ProviderIcon-CtmL1CmY.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BZT353_T.js → ResetPasswordPage-BB6U7t8P.js} +1 -1
- package/dashboard/dist/assets/{Select-CPC04j0g.js → Select-7w1su8lw.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-D1vJVrFO.js → SelectItemText-CaEUmWkj.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-FL3EDqmb.js → SelfHostPage-DctWoF-A.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-BohSSNai.js → ServiceStatusPage-Dg3ljEXC.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-Cf_HASvt.js → SessionsPage-B1dvW-be.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-B4ztKHnj.js → SettingsPage-DuXSX1R2.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-BFiQkY7O.js → SkillsPage-CvXQFgc9.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BHVdTJAP.js → TokenGalaxy-DKC8Rk_Y.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-BMtQyFn6.js → TooltipPopup-Dp9EpHLG.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DN-g3CNm.js → TrendMonitor-DWnlBn3H.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Bv32yu8M.js → WidgetsPage-GECNmb1Q.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-HVTOehNb.js → WrappedPage-Dq_1lOnb.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DnYJww7c.js → __vite-browser-external-CkF0pdTJ.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-BOwAJQqL.js → agent-logos-Dqe3Vrah.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CeXuUuES.js → arrow-left-Cr_2QnDu.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-DOdBG1FZ.js → arrow-right-BsK2sJgt.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-Dponssa4.js → arrow-up-right-DkWMGqXo.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-CsxE6ot0.js → calendar-days-BB9VV438.js} +1 -1
- package/dashboard/dist/assets/{check-BroNG_vu.js → check-bpSCJZWh.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-C5ZpGeO1.js → chevron-down-CtECtDmx.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-BtS-hZTc.js → cloud-checkout-C4PrzKhN.js} +1 -1
- package/dashboard/dist/assets/{composite-fgJbx88k.js → composite-BHisG82k.js} +1 -1
- package/dashboard/dist/assets/{copy-CHBuwNUs.js → copy-Dmxzd-Sw.js} +1 -1
- package/dashboard/dist/assets/{download-EKd2MHkv.js → download-CkdQzw2w.js} +1 -1
- package/dashboard/dist/assets/{external-link-GkSo9C78.js → external-link-C971FjvC.js} +1 -1
- package/dashboard/dist/assets/{flame-Cg96qJrE.js → flame-mKbp7kxw.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-ChG2_n4h.js → format-tokens-C35FrJnH.js} +1 -1
- package/dashboard/dist/assets/{gift-B3QDbWPE.js → gift-BX2lSOmc.js} +1 -1
- package/dashboard/dist/assets/{icons-BEAyMjau.js → icons-ov92u52T.js} +1 -1
- package/dashboard/dist/assets/{index-B0kZThUB.js → index-9OCL-BL8.js} +1 -1
- package/dashboard/dist/assets/{index-DFcQyAnr.js → index-Bgm38oHe.js} +1 -1
- package/dashboard/dist/assets/{index-Cuc8ZXgl.js → index-Ca5xkNrK.js} +1 -1
- package/dashboard/dist/assets/{index-B9a1fK8I.js → index-DF03nIHb.js} +1 -1
- package/dashboard/dist/assets/{info-796bNttV.js → info-B-f9hWcw.js} +1 -1
- package/dashboard/dist/assets/{link-2-LYRIPLwz.js → link-2-l1wuezTo.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DpKC9Uel.js → loader-circle-Lmjrs8XX.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-Ctbuc6U4.js → local-usage-transfer-Ds6cF41d.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-C5YTeNW1.js → modifiers.esm-tVlltJXO.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-DJUCfEvG.js → monitor-smartphone-XRiYiY8u.js} +1 -1
- package/dashboard/dist/assets/{play-LC-lnw6w.js → play-DaD9EMjk.js} +1 -1
- package/dashboard/dist/assets/{plus-CDF0S5xd.js → plus-D30fbsKQ.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CkITUZgR.js → provider-display-CKdhBOwz.js} +1 -1
- package/dashboard/dist/assets/{react-BIpM-G7K.js → react-BY0y0K4x.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C3Ea423q.js → safePolygon-CSG_P7rG.js} +1 -1
- package/dashboard/dist/assets/{search-C-tN1mTM.js → search-Kiq8wVHA.js} +1 -1
- package/dashboard/dist/assets/{server-DJ4gq_fT.js → server-CjovGBGz.js} +1 -1
- package/dashboard/dist/assets/{shield-check-f75hry9N.js → shield-check-DF9MmMCq.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CZ_vfnRL.js → skills-api-CEzr6p92.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-mLJSvq3M.js → sortable.esm-D7jd9mE_.js} +1 -1
- package/dashboard/dist/assets/{store-CZV1KXw2.js → store-D9_jo_do.js} +1 -1
- package/dashboard/dist/assets/{terminal-B9vOwCl9.js → terminal-CLOlvzR-.js} +1 -1
- package/dashboard/dist/assets/{trash-BN9GbynA.js → trash-DIe0fsDD.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Bc2d8tqa.js → use-community-stats-gDkdiRBz.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-_0H4Rk58.js → use-latest-request-guard-tXGuKvua.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Dln-Rxps.js → use-limits-display-prefs-CCuLFy00.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CpkcGzXp.js → use-native-settings-hgYqaiR7.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DpGA5U4S.js → use-ordered-list-D6-YzYJI.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DfbDxs5a.js +1 -0
- package/dashboard/dist/assets/{use-transform-JXJt1W74.js → use-transform-BEiXvAuE.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BK_H1-b7.js → use-usage-limits-BlwtyPtT.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-ilbtKs16.js → useCurrency-CwCGJLHb.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-M35LtCGR.js → usePositioner-BqHtRIrE.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BtTQjv7z.js → useTokenFormat-BUNBUX-x.js} +1 -1
- package/dashboard/dist/assets/{zap-5xemt9EH.js → zap-C0RFs2qQ.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/sync.js +2 -2
- package/src/lib/limits-cache.js +475 -0
- package/src/lib/pricing/curated-overrides.json +24 -0
- package/src/lib/pricing/matcher.js +4 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +261 -102
- package/src/lib/usage-limits.js +370 -1063
- package/dashboard/dist/assets/DialogDescription--_wg9JdV.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-Cy21hg3t.js +0 -1
package/src/lib/usage-limits.js
CHANGED
|
@@ -29,6 +29,12 @@ const {
|
|
|
29
29
|
fetchCursorSandUsageStatus,
|
|
30
30
|
} = require("./cursor-config");
|
|
31
31
|
const { fetchGrokLimits, grokTokenSubject, loadGrokAuthEntry } = require("./grok-limits");
|
|
32
|
+
const {
|
|
33
|
+
createLimitsCache,
|
|
34
|
+
parseTimeMs,
|
|
35
|
+
readCacheJson,
|
|
36
|
+
withLimitsCacheFallback,
|
|
37
|
+
} = require("./limits-cache");
|
|
32
38
|
const { fetchZcodeLimits } = require("./zcode-limits");
|
|
33
39
|
const { fetchOpencodeGoLimits } = require("./opencode-go-limits");
|
|
34
40
|
const { fetchCommandcodeLimits } = require("./commandcode-limits");
|
|
@@ -1809,83 +1815,18 @@ function readKiroCreditsSummary({ home = os.homedir() } = {}) {
|
|
|
1809
1815
|
// Kiro limits disk cache — last good windows parsed from the kiro-cli panel.
|
|
1810
1816
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1811
1817
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
// are kept — the overall cached_at max-age gate already bounds them.
|
|
1824
|
-
function isKiroCacheWindowUsable(window, { nowMs } = {}) {
|
|
1825
|
-
if (!window || typeof window !== "object") return false;
|
|
1826
|
-
const resetAtMs = parseTimeMs(window.reset_at);
|
|
1827
|
-
if (resetAtMs === null) return true;
|
|
1828
|
-
return resetAtMs > nowMs;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
function normalizeKiroCachedLimits(
|
|
1832
|
-
raw,
|
|
1833
|
-
{ nowMs = Date.now(), maxAgeMs = KIRO_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
1834
|
-
) {
|
|
1835
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
1836
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
1837
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
1838
|
-
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
1839
|
-
|
|
1840
|
-
const cached = {
|
|
1841
|
-
configured: true,
|
|
1842
|
-
error: null,
|
|
1843
|
-
plan_name: typeof raw?.plan_name === "string" ? raw.plan_name : null,
|
|
1844
|
-
primary_window: isKiroCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
1845
|
-
secondary_window: isKiroCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
1846
|
-
stale: true,
|
|
1847
|
-
cached_at: raw.cached_at,
|
|
1848
|
-
};
|
|
1849
|
-
return hasKiroWindow(cached) ? cached : null;
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
// The signed-in Kiro account partitions the cache (zcode / Cursor login-identity
|
|
1853
|
-
// precedent): the id is the CodeWhisperer profile ARN kiro-cli itself persists
|
|
1854
|
-
// next to its tokens. Reads require the stored identity to match the current
|
|
1855
|
-
// one, and a write without an identity is refused — after an account switch the
|
|
1856
|
-
// previous account's windows must never resurface as another login's fallback.
|
|
1857
|
-
function readKiroLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
1858
|
-
if (!identity) return null;
|
|
1859
|
-
try {
|
|
1860
|
-
const parsed = JSON.parse(fs.readFileSync(resolveKiroLimitsCachePath({ home }), "utf8"));
|
|
1861
|
-
const entry = parsed?.kiro;
|
|
1862
|
-
if (entry?.identity !== identity) return null;
|
|
1863
|
-
return normalizeKiroCachedLimits(entry, { nowMs });
|
|
1864
|
-
} catch (_error) {
|
|
1865
|
-
return null;
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1818
|
+
// kiro-cli's quota windows ride on the profile the CLI is signed into; the
|
|
1819
|
+
// profileArn partitions the cache. Windows carry `reset_at` as an ISO string.
|
|
1820
|
+
const kiroLimitsCache = createLimitsCache({
|
|
1821
|
+
file: KIRO_LIMITS_CACHE_FILE,
|
|
1822
|
+
key: "kiro",
|
|
1823
|
+
maxAgeMs: KIRO_LIMITS_CACHE_MAX_AGE_MS,
|
|
1824
|
+
fields: { plan_name: "string" },
|
|
1825
|
+
windowKeys: ["primary_window", "secondary_window"],
|
|
1826
|
+
identityField: "identity",
|
|
1827
|
+
identityRequired: true,
|
|
1828
|
+
});
|
|
1868
1829
|
|
|
1869
|
-
function writeKiroLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
1870
|
-
if (!identity) return;
|
|
1871
|
-
if (!limits?.configured || limits.error || !hasKiroWindow(limits)) return;
|
|
1872
|
-
const cachePath = resolveKiroLimitsCachePath({ home });
|
|
1873
|
-
const payload = {
|
|
1874
|
-
kiro: {
|
|
1875
|
-
identity,
|
|
1876
|
-
plan_name: limits.plan_name || null,
|
|
1877
|
-
primary_window: limits.primary_window || null,
|
|
1878
|
-
secondary_window: limits.secondary_window || null,
|
|
1879
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
1880
|
-
},
|
|
1881
|
-
};
|
|
1882
|
-
try {
|
|
1883
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
1884
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
1885
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
1886
|
-
fs.renameSync(tmpPath, cachePath);
|
|
1887
|
-
} catch (_error) {}
|
|
1888
|
-
}
|
|
1889
1830
|
|
|
1890
1831
|
function resolveKiroAuthIdentity({ home } = {}) {
|
|
1891
1832
|
try {
|
|
@@ -2642,98 +2583,70 @@ async function detectAntigravityProcess({
|
|
|
2642
2583
|
return { configured: false };
|
|
2643
2584
|
}
|
|
2644
2585
|
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
return Number.isFinite(cachedAtMs)
|
|
2660
|
-
&& nowMs - cachedAtMs <= ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS;
|
|
2661
|
-
}
|
|
2586
|
+
// Last-good cache for the Antigravity quota summary. Windows without a reset
|
|
2587
|
+
// stamp are bounded by the 12h unknown-reset TTL instead of living as long as
|
|
2588
|
+
// the snapshot; the read is gated off entirely by the disable flag.
|
|
2589
|
+
const antigravityLimitsCache = createLimitsCache({
|
|
2590
|
+
file: ANTIGRAVITY_LIMITS_CACHE_FILE,
|
|
2591
|
+
key: "antigravity",
|
|
2592
|
+
maxAgeMs: ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS,
|
|
2593
|
+
unknownResetTtlMs: ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS,
|
|
2594
|
+
fields: { account_email: "string", account_plan: "string" },
|
|
2595
|
+
windowKeys: ["primary_window", "secondary_window", "tertiary_window", "quaternary_window"],
|
|
2596
|
+
cachedFlag: true,
|
|
2597
|
+
// The cached annotation is this provider's own marker — no stale flag.
|
|
2598
|
+
stale: null,
|
|
2599
|
+
});
|
|
2662
2600
|
|
|
2663
2601
|
function hasAntigravityWindow(limits) {
|
|
2664
2602
|
return Boolean(limits?.primary_window || limits?.secondary_window || limits?.tertiary_window || limits?.quaternary_window);
|
|
2665
2603
|
}
|
|
2666
2604
|
|
|
2667
|
-
function normalizeAntigravityCachedLimits(raw, { nowMs = Date.now() } = {}) {
|
|
2668
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
2669
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
2670
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
2671
|
-
if (nowMs - cachedAtMs > ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS) return null;
|
|
2672
|
-
|
|
2673
|
-
const cached = {
|
|
2674
|
-
configured: true,
|
|
2675
|
-
error: null,
|
|
2676
|
-
account_email: typeof raw?.account_email === "string" ? raw.account_email : null,
|
|
2677
|
-
account_plan: typeof raw?.account_plan === "string" ? raw.account_plan : null,
|
|
2678
|
-
primary_window: isCacheWindowUsable(raw?.primary_window, { cachedAtMs, nowMs }) ? raw.primary_window : null,
|
|
2679
|
-
secondary_window: isCacheWindowUsable(raw?.secondary_window, { cachedAtMs, nowMs }) ? raw.secondary_window : null,
|
|
2680
|
-
tertiary_window: isCacheWindowUsable(raw?.tertiary_window, { cachedAtMs, nowMs }) ? raw.tertiary_window : null,
|
|
2681
|
-
quaternary_window: isCacheWindowUsable(raw?.quaternary_window, { cachedAtMs, nowMs }) ? raw.quaternary_window : null,
|
|
2682
|
-
cached: true,
|
|
2683
|
-
cached_at: raw.cached_at,
|
|
2684
|
-
};
|
|
2685
|
-
return hasAntigravityWindow(cached) ? cached : null;
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
2605
|
function isAntigravityQuotaDisabled() {
|
|
2689
2606
|
return process.env.TOKENTRACKER_DISABLE_ANTIGRAVITY_QUOTA === "1";
|
|
2690
2607
|
}
|
|
2691
2608
|
|
|
2692
2609
|
function readAntigravityLimitsCache({ home, nowMs = Date.now() } = {}) {
|
|
2693
2610
|
if (isAntigravityQuotaDisabled()) return null;
|
|
2694
|
-
|
|
2695
|
-
try {
|
|
2696
|
-
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
2697
|
-
return normalizeAntigravityCachedLimits(parsed?.antigravity, { nowMs });
|
|
2698
|
-
} catch (_error) {
|
|
2699
|
-
return null;
|
|
2700
|
-
}
|
|
2611
|
+
return antigravityLimitsCache.read({ home, nowMs });
|
|
2701
2612
|
}
|
|
2702
2613
|
|
|
2703
2614
|
function writeAntigravityLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
|
|
2704
|
-
|
|
2705
|
-
const cachePath = resolveAntigravityLimitsCachePath({ home });
|
|
2706
|
-
const payload = {
|
|
2707
|
-
antigravity: {
|
|
2708
|
-
account_email: limits.account_email || null,
|
|
2709
|
-
account_plan: limits.account_plan || null,
|
|
2710
|
-
primary_window: limits.primary_window || null,
|
|
2711
|
-
secondary_window: limits.secondary_window || null,
|
|
2712
|
-
tertiary_window: limits.tertiary_window || null,
|
|
2713
|
-
quaternary_window: limits.quaternary_window || null,
|
|
2714
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
2715
|
-
},
|
|
2716
|
-
};
|
|
2717
|
-
try {
|
|
2718
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
2719
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
2720
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
2721
|
-
fs.renameSync(tmpPath, cachePath);
|
|
2722
|
-
} catch (_error) {}
|
|
2615
|
+
antigravityLimitsCache.write(limits, { home, nowMs });
|
|
2723
2616
|
}
|
|
2724
2617
|
|
|
2725
2618
|
function resolveClaudeLimitsCachePath({ home } = {}) {
|
|
2726
|
-
return path
|
|
2727
|
-
}
|
|
2619
|
+
return claudeLimitsCache.path({ home });
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// Claude windows carry their own `resets_at`; a window whose reset has already
|
|
2623
|
+
// passed is stale data, not a usable fallback, so drop it. Windows without a
|
|
2624
|
+
// reset stamp are kept (the overall cached_at max-age gate already bounds them).
|
|
2625
|
+
const claudeLimitsCache = createLimitsCache({
|
|
2626
|
+
file: CLAUDE_LIMITS_CACHE_FILE,
|
|
2627
|
+
key: "claude",
|
|
2628
|
+
maxAgeMs: CLAUDE_LIMITS_CACHE_MAX_AGE_MS,
|
|
2629
|
+
fields: {
|
|
2630
|
+
extra_usage: { write: (record) => record?.extra_usage || null, read: (raw) => raw?.extra_usage ?? null },
|
|
2631
|
+
// Cached scoped windows are filtered per-entry like the fixed windows: a
|
|
2632
|
+
// scoped window whose reset has passed is stale data, not a usable fallback.
|
|
2633
|
+
weekly_scoped: {
|
|
2634
|
+
write: (record) => (Array.isArray(record?.weekly_scoped) && record.weekly_scoped.length > 0
|
|
2635
|
+
? record.weekly_scoped
|
|
2636
|
+
: null),
|
|
2637
|
+
read: (raw, ctx) => normalizeClaudeCachedScopedWeekly(raw?.weekly_scoped, { nowMs: ctx.nowMs }),
|
|
2638
|
+
// Scoped entries count as window survivors: a live read carrying only
|
|
2639
|
+
// scoped windows must reach the disk cache and survive a later fallback
|
|
2640
|
+
// read, while an all-expired array still refuses the snapshot.
|
|
2641
|
+
survivor: (value, { nowMs } = {}) => normalizeClaudeCachedScopedWeekly(value, { nowMs }) !== null,
|
|
2642
|
+
},
|
|
2643
|
+
},
|
|
2644
|
+
windowKeys: ["five_hour", "seven_day", "seven_day_opus"],
|
|
2645
|
+
windowResetField: "resets_at",
|
|
2646
|
+
});
|
|
2728
2647
|
|
|
2729
|
-
// Claude windows carry their own `resets_at`; a window whose reset has already passed is
|
|
2730
|
-
// stale data, not a usable fallback, so drop it. Windows without a reset stamp are kept
|
|
2731
|
-
// (the overall cached_at max-age gate already bounds them).
|
|
2732
2648
|
function isClaudeCacheWindowUsable(window, { nowMs } = {}) {
|
|
2733
|
-
|
|
2734
|
-
const resetAtMs = parseTimeMs(window.resets_at);
|
|
2735
|
-
if (resetAtMs === null) return true;
|
|
2736
|
-
return resetAtMs > nowMs;
|
|
2649
|
+
return claudeLimitsCache.isWindowUsable(window, { nowMs });
|
|
2737
2650
|
}
|
|
2738
2651
|
|
|
2739
2652
|
function hasClaudeWindow(limits) {
|
|
@@ -2754,6 +2667,8 @@ function normalizeClaudeCachedScopedWeekly(raw, { nowMs } = {}) {
|
|
|
2754
2667
|
return usable.length > 0 ? usable : null;
|
|
2755
2668
|
}
|
|
2756
2669
|
|
|
2670
|
+
// Parametrized so the fresh pre-fetch gate can read with the 10m TTL and a
|
|
2671
|
+
// non-stale annotation while the fallback reads with the 7d bound.
|
|
2757
2672
|
function normalizeClaudeCachedLimits(
|
|
2758
2673
|
raw,
|
|
2759
2674
|
{
|
|
@@ -2762,32 +2677,12 @@ function normalizeClaudeCachedLimits(
|
|
|
2762
2677
|
stale = true,
|
|
2763
2678
|
} = {},
|
|
2764
2679
|
) {
|
|
2765
|
-
|
|
2766
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
2767
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
2768
|
-
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
2769
|
-
|
|
2770
|
-
const cached = {
|
|
2771
|
-
configured: true,
|
|
2772
|
-
error: null,
|
|
2773
|
-
five_hour: isClaudeCacheWindowUsable(raw?.five_hour, { nowMs }) ? raw.five_hour : null,
|
|
2774
|
-
seven_day: isClaudeCacheWindowUsable(raw?.seven_day, { nowMs }) ? raw.seven_day : null,
|
|
2775
|
-
seven_day_opus: isClaudeCacheWindowUsable(raw?.seven_day_opus, { nowMs }) ? raw.seven_day_opus : null,
|
|
2776
|
-
weekly_scoped: normalizeClaudeCachedScopedWeekly(raw?.weekly_scoped, { nowMs }),
|
|
2777
|
-
extra_usage: raw?.extra_usage ?? null,
|
|
2778
|
-
stale,
|
|
2779
|
-
cached_at: raw.cached_at,
|
|
2780
|
-
};
|
|
2781
|
-
return hasClaudeWindow(cached) ? cached : null;
|
|
2680
|
+
return claudeLimitsCache.normalize(raw, { nowMs, maxAgeMs, stale });
|
|
2782
2681
|
}
|
|
2783
2682
|
|
|
2784
2683
|
function readClaudeLimitsCacheRaw({ home } = {}) {
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
return parsed?.claude ?? null;
|
|
2788
|
-
} catch (_error) {
|
|
2789
|
-
return null;
|
|
2790
|
-
}
|
|
2684
|
+
const parsed = readCacheJson(claudeLimitsCache.path({ home }));
|
|
2685
|
+
return parsed?.claude ?? null;
|
|
2791
2686
|
}
|
|
2792
2687
|
|
|
2793
2688
|
function readClaudeLimitsCache({
|
|
@@ -2899,286 +2794,99 @@ function readFreshClaudeLimitsCache({ home, nowMs = Date.now() } = {}) {
|
|
|
2899
2794
|
}
|
|
2900
2795
|
|
|
2901
2796
|
function writeClaudeLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
|
|
2902
|
-
|
|
2903
|
-
const cachePath = resolveClaudeLimitsCachePath({ home });
|
|
2904
|
-
const payload = {
|
|
2905
|
-
claude: {
|
|
2906
|
-
five_hour: limits.five_hour || null,
|
|
2907
|
-
seven_day: limits.seven_day || null,
|
|
2908
|
-
seven_day_opus: limits.seven_day_opus || null,
|
|
2909
|
-
weekly_scoped: Array.isArray(limits.weekly_scoped) && limits.weekly_scoped.length > 0
|
|
2910
|
-
? limits.weekly_scoped
|
|
2911
|
-
: null,
|
|
2912
|
-
extra_usage: limits.extra_usage || null,
|
|
2913
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
2914
|
-
},
|
|
2915
|
-
};
|
|
2916
|
-
try {
|
|
2917
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
2918
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
2919
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
2920
|
-
fs.renameSync(tmpPath, cachePath);
|
|
2921
|
-
} catch (_error) {}
|
|
2797
|
+
claudeLimitsCache.write(limits, { home, nowMs });
|
|
2922
2798
|
}
|
|
2923
2799
|
|
|
2924
|
-
function resolveCodexLimitsCachePath({ home } = {}) {
|
|
2925
|
-
return path.join(home || os.homedir(), ".tokentracker", "tracker", CODEX_LIMITS_CACHE_FILE);
|
|
2926
|
-
}
|
|
2927
2800
|
|
|
2928
|
-
// Codex windows carry `reset_at` as unix seconds (not an ISO string). A window
|
|
2929
|
-
// already passed is stale data, not a usable fallback, so drop
|
|
2930
|
-
// are kept — the
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2801
|
+
// Codex windows carry `reset_at` as unix seconds (not an ISO string). A window
|
|
2802
|
+
// whose reset has already passed is stale data, not a usable fallback, so drop
|
|
2803
|
+
// it; windows without a reset stamp are kept — the cached_at max-age gate
|
|
2804
|
+
// already bounds them.
|
|
2805
|
+
const codexLimitsCache = createLimitsCache({
|
|
2806
|
+
file: CODEX_LIMITS_CACHE_FILE,
|
|
2807
|
+
key: "codex",
|
|
2808
|
+
maxAgeMs: CODEX_LIMITS_CACHE_MAX_AGE_MS,
|
|
2809
|
+
fields: {
|
|
2810
|
+
plan_type: "string",
|
|
2811
|
+
reset_credits: { write: (record) => record?.reset_credits || null, read: (raw) => raw?.reset_credits ?? null },
|
|
2812
|
+
},
|
|
2813
|
+
windowKeys: ["primary_window", "secondary_window", "credit_window", "spark_primary_window", "spark_secondary_window"],
|
|
2814
|
+
resetTimeMode: "unix-seconds",
|
|
2815
|
+
});
|
|
2937
2816
|
|
|
2938
|
-
function
|
|
2939
|
-
return
|
|
2940
|
-
limits?.primary_window
|
|
2941
|
-
|| limits?.secondary_window
|
|
2942
|
-
|| limits?.credit_window
|
|
2943
|
-
|| limits?.spark_primary_window
|
|
2944
|
-
|| limits?.spark_secondary_window,
|
|
2945
|
-
);
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
function normalizeCodexCachedLimits(
|
|
2949
|
-
raw,
|
|
2950
|
-
{ nowMs = Date.now(), maxAgeMs = CODEX_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
2951
|
-
) {
|
|
2952
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
2953
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
2954
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
2955
|
-
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
2956
|
-
|
|
2957
|
-
const cached = {
|
|
2958
|
-
configured: true,
|
|
2959
|
-
error: null,
|
|
2960
|
-
plan_type: typeof raw?.plan_type === "string" ? raw.plan_type : null,
|
|
2961
|
-
primary_window: isCodexCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
2962
|
-
secondary_window: isCodexCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
2963
|
-
credit_window: isCodexCacheWindowUsable(raw?.credit_window, { nowMs }) ? raw.credit_window : null,
|
|
2964
|
-
spark_primary_window: isCodexCacheWindowUsable(raw?.spark_primary_window, { nowMs }) ? raw.spark_primary_window : null,
|
|
2965
|
-
spark_secondary_window: isCodexCacheWindowUsable(raw?.spark_secondary_window, { nowMs }) ? raw.spark_secondary_window : null,
|
|
2966
|
-
reset_credits: raw?.reset_credits ?? null,
|
|
2967
|
-
stale: true,
|
|
2968
|
-
cached_at: raw.cached_at,
|
|
2969
|
-
};
|
|
2970
|
-
return hasCodexWindow(cached) ? cached : null;
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
|
-
function readCodexLimitsCache({ home, nowMs = Date.now(), maxAgeMs = CODEX_LIMITS_CACHE_MAX_AGE_MS } = {}) {
|
|
2974
|
-
const cachePath = resolveCodexLimitsCachePath({ home });
|
|
2975
|
-
try {
|
|
2976
|
-
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
2977
|
-
return normalizeCodexCachedLimits(parsed?.codex, { nowMs, maxAgeMs });
|
|
2978
|
-
} catch (_error) {
|
|
2979
|
-
return null;
|
|
2980
|
-
}
|
|
2817
|
+
function readCodexLimitsCache({ home, nowMs = Date.now(), maxAgeMs } = {}) {
|
|
2818
|
+
return codexLimitsCache.read({ home, nowMs, maxAgeMs });
|
|
2981
2819
|
}
|
|
2982
2820
|
|
|
2983
2821
|
function writeCodexLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2822
|
+
codexLimitsCache.write(limits, { home, nowMs });
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
const opencodeGoLimitsCache = createLimitsCache({
|
|
2826
|
+
file: OPENCODE_GO_LIMITS_CACHE_FILE,
|
|
2827
|
+
key: "opencodeGo",
|
|
2828
|
+
// The dashboard's configured tracker data dir overrides the default
|
|
2829
|
+
// tracker directory for this provider's cache.
|
|
2830
|
+
dirOverride: "trackerDir",
|
|
2831
|
+
// The max-age is source-dependent: local estimates are heuristic readings,
|
|
2832
|
+
// not provider truth, so they expire within the hour.
|
|
2833
|
+
maxAgeMs: (raw) => (raw?.source === "local-estimate"
|
|
2834
|
+
? OPENCODE_GO_LOCAL_ESTIMATE_CACHE_MAX_AGE_MS
|
|
2835
|
+
: OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS),
|
|
2836
|
+
fields: {
|
|
2837
|
+
source: {
|
|
2838
|
+
write: (record) => record?.source || "api",
|
|
2839
|
+
read: (raw) => (typeof raw?.source === "string" ? raw.source : "api"),
|
|
2996
2840
|
},
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
}
|
|
2841
|
+
credential_source: {
|
|
2842
|
+
write: (record) => (record?.credential_source === "env" || record?.credential_source === "stored"
|
|
2843
|
+
? record.credential_source
|
|
2844
|
+
: null),
|
|
2845
|
+
read: (raw) => (raw?.credential_source === "env" || raw?.credential_source === "stored"
|
|
2846
|
+
? raw.credential_source
|
|
2847
|
+
: null),
|
|
2848
|
+
},
|
|
2849
|
+
subscription_status: "string",
|
|
2850
|
+
plan_label: "string",
|
|
2851
|
+
},
|
|
2852
|
+
windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
|
|
2853
|
+
});
|
|
3005
2854
|
|
|
3006
2855
|
function resolveOpencodeGoLimitsCachePath({ trackerDir, home } = {}) {
|
|
3007
|
-
|
|
3008
|
-
typeof trackerDir === "string" && trackerDir.trim()
|
|
3009
|
-
? trackerDir.trim()
|
|
3010
|
-
: path.join(home || os.homedir(), ".tokentracker", "tracker");
|
|
3011
|
-
return path.join(dir, OPENCODE_GO_LIMITS_CACHE_FILE);
|
|
2856
|
+
return opencodeGoLimitsCache.path({ trackerDir, home });
|
|
3012
2857
|
}
|
|
3013
2858
|
|
|
3014
2859
|
function hasOpencodeGoWindow(limits) {
|
|
3015
2860
|
return Boolean(limits?.primary_window || limits?.secondary_window || limits?.tertiary_window);
|
|
3016
2861
|
}
|
|
3017
2862
|
|
|
3018
|
-
function isOpencodeGoCacheWindowUsable(window, { nowMs } = {}) {
|
|
3019
|
-
if (!window || typeof window !== "object") return false;
|
|
3020
|
-
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3021
|
-
if (resetAtMs === null) return true;
|
|
3022
|
-
return resetAtMs > nowMs;
|
|
3023
|
-
}
|
|
3024
|
-
|
|
3025
|
-
function normalizeOpencodeGoCachedLimits(
|
|
3026
|
-
raw,
|
|
3027
|
-
{ nowMs = Date.now(), maxAgeMs } = {},
|
|
3028
|
-
) {
|
|
3029
|
-
const effectiveMaxAge = Number.isFinite(maxAgeMs)
|
|
3030
|
-
? maxAgeMs
|
|
3031
|
-
: raw?.source === "local-estimate"
|
|
3032
|
-
? OPENCODE_GO_LOCAL_ESTIMATE_CACHE_MAX_AGE_MS
|
|
3033
|
-
: OPENCODE_GO_LIMITS_CACHE_MAX_AGE_MS;
|
|
3034
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3035
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3036
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3037
|
-
if (nowMs - cachedAtMs > effectiveMaxAge) return null;
|
|
3038
|
-
|
|
3039
|
-
const credentialSource =
|
|
3040
|
-
raw?.credential_source === "env" || raw?.credential_source === "stored"
|
|
3041
|
-
? raw.credential_source
|
|
3042
|
-
: null;
|
|
3043
|
-
const cached = {
|
|
3044
|
-
configured: true,
|
|
3045
|
-
error: null,
|
|
3046
|
-
source: typeof raw?.source === "string" ? raw.source : "api",
|
|
3047
|
-
credential_source: credentialSource,
|
|
3048
|
-
subscription_status: typeof raw?.subscription_status === "string" ? raw.subscription_status : null,
|
|
3049
|
-
plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
|
|
3050
|
-
primary_window: isOpencodeGoCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3051
|
-
secondary_window: isOpencodeGoCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3052
|
-
tertiary_window: isOpencodeGoCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
|
|
3053
|
-
stale: true,
|
|
3054
|
-
cached_at: raw.cached_at,
|
|
3055
|
-
};
|
|
3056
|
-
return hasOpencodeGoWindow(cached) ? cached : null;
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
2863
|
function readOpencodeGoLimitsCache({ trackerDir, home, nowMs = Date.now() } = {}) {
|
|
3060
|
-
|
|
3061
|
-
try {
|
|
3062
|
-
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
3063
|
-
return normalizeOpencodeGoCachedLimits(parsed?.opencodeGo, { nowMs });
|
|
3064
|
-
} catch (_error) {
|
|
3065
|
-
return null;
|
|
3066
|
-
}
|
|
2864
|
+
return opencodeGoLimitsCache.read({ trackerDir, home, nowMs });
|
|
3067
2865
|
}
|
|
3068
2866
|
|
|
3069
2867
|
function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now() } = {}) {
|
|
3070
|
-
|
|
3071
|
-
const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
|
|
3072
|
-
const credentialSource =
|
|
3073
|
-
limits.credential_source === "env" || limits.credential_source === "stored"
|
|
3074
|
-
? limits.credential_source
|
|
3075
|
-
: null;
|
|
3076
|
-
const payload = {
|
|
3077
|
-
opencodeGo: {
|
|
3078
|
-
source: limits.source || "api",
|
|
3079
|
-
credential_source: credentialSource,
|
|
3080
|
-
subscription_status: limits.subscription_status || null,
|
|
3081
|
-
plan_label: limits.plan_label || null,
|
|
3082
|
-
primary_window: limits.primary_window || null,
|
|
3083
|
-
secondary_window: limits.secondary_window || null,
|
|
3084
|
-
tertiary_window: limits.tertiary_window || null,
|
|
3085
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
3086
|
-
},
|
|
3087
|
-
};
|
|
3088
|
-
try {
|
|
3089
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3090
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3091
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3092
|
-
fs.renameSync(tmpPath, cachePath);
|
|
3093
|
-
} catch (_error) {}
|
|
3094
|
-
}
|
|
3095
|
-
|
|
3096
|
-
function resolveCursorLimitsCachePath({ home } = {}) {
|
|
3097
|
-
return path.join(home || os.homedir(), ".tokentracker", "tracker", CURSOR_LIMITS_CACHE_FILE);
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
|
-
function hasCursorWindow(limits) {
|
|
3101
|
-
return Boolean(
|
|
3102
|
-
limits?.primary_window
|
|
3103
|
-
|| limits?.secondary_window
|
|
3104
|
-
|| limits?.tertiary_window
|
|
3105
|
-
|| limits?.quaternary_window,
|
|
3106
|
-
);
|
|
2868
|
+
opencodeGoLimitsCache.write(limits, { trackerDir, home, nowMs });
|
|
3107
2869
|
}
|
|
3108
2870
|
|
|
3109
|
-
// Cursor windows carry `reset_at` as an ISO string (billing-cycle end for the three
|
|
3110
|
-
// summary windows, the RPC's own expiry for the Grok Bot window). A window whose reset
|
|
3111
|
-
// has already passed is stale data, not a usable fallback, so drop it. Windows without
|
|
3112
|
-
// a reset stamp are kept — the overall cached_at max-age gate already bounds them.
|
|
3113
|
-
function isCursorCacheWindowUsable(window, { nowMs } = {}) {
|
|
3114
|
-
if (!window || typeof window !== "object") return false;
|
|
3115
|
-
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3116
|
-
if (resetAtMs === null) return true;
|
|
3117
|
-
return resetAtMs > nowMs;
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
function normalizeCursorCachedLimits(
|
|
3121
|
-
raw,
|
|
3122
|
-
{ nowMs = Date.now(), maxAgeMs = CURSOR_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3123
|
-
) {
|
|
3124
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3125
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3126
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3127
|
-
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3128
|
-
|
|
3129
|
-
const cached = {
|
|
3130
|
-
configured: true,
|
|
3131
|
-
error: null,
|
|
3132
|
-
membership_type: typeof raw?.membership_type === "string" ? raw.membership_type : null,
|
|
3133
|
-
primary_window: isCursorCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3134
|
-
secondary_window: isCursorCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3135
|
-
tertiary_window: isCursorCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
|
|
3136
|
-
quaternary_window: isCursorCacheWindowUsable(raw?.quaternary_window, { nowMs }) ? raw.quaternary_window : null,
|
|
3137
|
-
stale: true,
|
|
3138
|
-
cached_at: raw.cached_at,
|
|
3139
|
-
};
|
|
3140
|
-
return hasCursorWindow(cached) ? cached : null;
|
|
3141
|
-
}
|
|
3142
2871
|
|
|
3143
2872
|
// The signed-in Cursor account partitions the cache (zcode login-identity precedent):
|
|
3144
2873
|
// the id is the non-secret normalized subject Cursor itself puts in the session cookie.
|
|
3145
2874
|
// A single home holds one active login, so one stamped entry suffices; a read requires
|
|
3146
2875
|
// the stored identity to match the current one, and a write without an identity is
|
|
3147
2876
|
// refused — an unreadable login must never pin another account's bars in place.
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
if (!identity) return;
|
|
3162
|
-
if (!limits?.configured || limits.error || !hasCursorWindow(limits)) return;
|
|
3163
|
-
const cachePath = resolveCursorLimitsCachePath({ home });
|
|
3164
|
-
const payload = {
|
|
3165
|
-
cursor: {
|
|
3166
|
-
identity,
|
|
3167
|
-
membership_type: limits.membership_type || null,
|
|
3168
|
-
primary_window: limits.primary_window || null,
|
|
3169
|
-
secondary_window: limits.secondary_window || null,
|
|
3170
|
-
tertiary_window: limits.tertiary_window || null,
|
|
3171
|
-
quaternary_window: limits.quaternary_window || null,
|
|
3172
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
3173
|
-
},
|
|
3174
|
-
};
|
|
3175
|
-
try {
|
|
3176
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3177
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3178
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3179
|
-
fs.renameSync(tmpPath, cachePath);
|
|
3180
|
-
} catch (_error) {}
|
|
3181
|
-
}
|
|
2877
|
+
const cursorLimitsCache = createLimitsCache({
|
|
2878
|
+
file: CURSOR_LIMITS_CACHE_FILE,
|
|
2879
|
+
key: "cursor",
|
|
2880
|
+
maxAgeMs: CURSOR_LIMITS_CACHE_MAX_AGE_MS,
|
|
2881
|
+
fields: { membership_type: "string" },
|
|
2882
|
+
// Billing-cycle end for the three summary windows, the RPC's own expiry for
|
|
2883
|
+
// the Grok Bot window. A window whose reset has already passed is stale
|
|
2884
|
+
// data, not a usable fallback, so drop it; windows without a reset stamp
|
|
2885
|
+
// are kept — the overall cached_at max-age gate already bounds them.
|
|
2886
|
+
windowKeys: ["primary_window", "secondary_window", "tertiary_window", "quaternary_window"],
|
|
2887
|
+
identityField: "identity",
|
|
2888
|
+
identityRequired: true,
|
|
2889
|
+
});
|
|
3182
2890
|
|
|
3183
2891
|
// Failure-only identity re-derivation for the cache fallback: when the provider slot's
|
|
3184
2892
|
// result was produced by the outer timeout race, the fetcher's own identity field is
|
|
@@ -3191,106 +2899,6 @@ function resolveCursorCacheIdentity({ home, env, trackerDir } = {}) {
|
|
|
3191
2899
|
}
|
|
3192
2900
|
}
|
|
3193
2901
|
|
|
3194
|
-
function resolveGeminiLimitsCachePath({ home } = {}) {
|
|
3195
|
-
return path.join(home || os.homedir(), ".tokentracker", "tracker", GEMINI_LIMITS_CACHE_FILE);
|
|
3196
|
-
}
|
|
3197
|
-
|
|
3198
|
-
function hasGeminiWindow(limits) {
|
|
3199
|
-
return Boolean(
|
|
3200
|
-
limits?.primary_window
|
|
3201
|
-
|| limits?.secondary_window
|
|
3202
|
-
|| limits?.tertiary_window,
|
|
3203
|
-
);
|
|
3204
|
-
}
|
|
3205
|
-
|
|
3206
|
-
// Gemini windows carry `reset_at` as an ISO string (the Code Assist bucket's own
|
|
3207
|
-
// resetTime — daily for free-tier quotas). A window whose reset has already passed
|
|
3208
|
-
// is stale data, not a usable fallback, so drop it. Windows without a reset stamp
|
|
3209
|
-
// are kept — the overall cached_at max-age gate already bounds them.
|
|
3210
|
-
function isGeminiCacheWindowUsable(window, { nowMs } = {}) {
|
|
3211
|
-
if (!window || typeof window !== "object") return false;
|
|
3212
|
-
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3213
|
-
if (resetAtMs === null) return true;
|
|
3214
|
-
return resetAtMs > nowMs;
|
|
3215
|
-
}
|
|
3216
|
-
|
|
3217
|
-
function normalizeGeminiCachedLimits(
|
|
3218
|
-
raw,
|
|
3219
|
-
{ nowMs = Date.now(), maxAgeMs = GEMINI_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3220
|
-
) {
|
|
3221
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3222
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3223
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3224
|
-
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3225
|
-
|
|
3226
|
-
const cached = {
|
|
3227
|
-
configured: true,
|
|
3228
|
-
error: null,
|
|
3229
|
-
account_email: typeof raw?.account_email === "string" ? raw.account_email : null,
|
|
3230
|
-
account_plan: typeof raw?.account_plan === "string" ? raw.account_plan : null,
|
|
3231
|
-
primary_window: isGeminiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3232
|
-
secondary_window: isGeminiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3233
|
-
tertiary_window: isGeminiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
|
|
3234
|
-
stale: true,
|
|
3235
|
-
cached_at: raw.cached_at,
|
|
3236
|
-
};
|
|
3237
|
-
return hasGeminiWindow(cached) ? cached : null;
|
|
3238
|
-
}
|
|
3239
|
-
|
|
3240
|
-
// The signed-in Google account partitions the cache (zcode login-identity
|
|
3241
|
-
// precedent): the email decoded from the local id_token JWT is non-secret and is
|
|
3242
|
-
// the same id the payload already surfaces as account_email. GEMINI_HOME can aim
|
|
3243
|
-
// the credential lookup at a different installation, but the cache stays keyed by
|
|
3244
|
-
// account — a read requires the stored identity to match the account the current
|
|
3245
|
-
// credentials resolve to, and a write without an identity is refused: an
|
|
3246
|
-
// unreadable login must never pin another account's bars in place.
|
|
3247
|
-
function readGeminiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3248
|
-
if (!identity) return null;
|
|
3249
|
-
try {
|
|
3250
|
-
const parsed = JSON.parse(fs.readFileSync(resolveGeminiLimitsCachePath({ home }), "utf8"));
|
|
3251
|
-
const entry = parsed?.gemini;
|
|
3252
|
-
if (entry?.identity !== identity) return null;
|
|
3253
|
-
return normalizeGeminiCachedLimits(entry, { nowMs });
|
|
3254
|
-
} catch (_error) {
|
|
3255
|
-
return null;
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
function writeGeminiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3260
|
-
if (!identity) return;
|
|
3261
|
-
if (!limits?.configured || limits.error || !hasGeminiWindow(limits)) return;
|
|
3262
|
-
const cachePath = resolveGeminiLimitsCachePath({ home });
|
|
3263
|
-
const payload = {
|
|
3264
|
-
gemini: {
|
|
3265
|
-
identity,
|
|
3266
|
-
account_email: limits.account_email || null,
|
|
3267
|
-
account_plan: limits.account_plan || null,
|
|
3268
|
-
primary_window: limits.primary_window || null,
|
|
3269
|
-
secondary_window: limits.secondary_window || null,
|
|
3270
|
-
tertiary_window: limits.tertiary_window || null,
|
|
3271
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
3272
|
-
},
|
|
3273
|
-
};
|
|
3274
|
-
try {
|
|
3275
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3276
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3277
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3278
|
-
fs.renameSync(tmpPath, cachePath);
|
|
3279
|
-
} catch (_error) {}
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
// Failure-only identity re-derivation for the cache fallback: when the provider
|
|
3283
|
-
// slot's result was produced by the outer timeout race, the fetcher's own
|
|
3284
|
-
// identity field is lost, so decode the (still-local) id_token once to learn
|
|
3285
|
-
// who we are serving for.
|
|
3286
|
-
function resolveGeminiCacheIdentity({ home, env } = {}) {
|
|
3287
|
-
try {
|
|
3288
|
-
return decodeJwtPayload(loadGeminiCredentials({ home, env })?.id_token)?.email || null;
|
|
3289
|
-
} catch (_error) {
|
|
3290
|
-
return null;
|
|
3291
|
-
}
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
2902
|
// The active Kimi profile partitions the cache (ark profile_identity precedent):
|
|
3295
2903
|
// mainland and global logins — and any KIMI_HOME override — resolve to different
|
|
3296
2904
|
// endpoint-scoped credential slots, and one slot must never serve another's
|
|
@@ -3312,160 +2920,20 @@ function resolveKimiCacheIdentity({ home, env } = {}) {
|
|
|
3312
2920
|
}
|
|
3313
2921
|
}
|
|
3314
2922
|
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
// it. Windows without a reset stamp are kept — the overall cached_at max-age
|
|
3330
|
-
// gate already bounds them.
|
|
3331
|
-
function isKimiCacheWindowUsable(window, { nowMs } = {}) {
|
|
3332
|
-
if (!window || typeof window !== "object") return false;
|
|
3333
|
-
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3334
|
-
if (resetAtMs === null) return true;
|
|
3335
|
-
return resetAtMs > nowMs;
|
|
3336
|
-
}
|
|
3337
|
-
|
|
3338
|
-
function normalizeKimiCachedLimits(
|
|
3339
|
-
raw,
|
|
3340
|
-
{ nowMs = Date.now(), maxAgeMs = KIMI_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3341
|
-
) {
|
|
3342
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3343
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3344
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3345
|
-
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3346
|
-
|
|
3347
|
-
const cached = {
|
|
3348
|
-
configured: true,
|
|
3349
|
-
error: null,
|
|
3350
|
-
membership_level: typeof raw?.membership_level === "string" ? raw.membership_level : null,
|
|
3351
|
-
subscription_type: typeof raw?.subscription_type === "string" ? raw.subscription_type : null,
|
|
3352
|
-
parallel_limit: Number.isFinite(raw?.parallel_limit) ? raw.parallel_limit : null,
|
|
3353
|
-
primary_window: isKimiCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3354
|
-
secondary_window: isKimiCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3355
|
-
tertiary_window: isKimiCacheWindowUsable(raw?.tertiary_window, { nowMs }) ? raw.tertiary_window : null,
|
|
3356
|
-
stale: true,
|
|
3357
|
-
cached_at: raw.cached_at,
|
|
3358
|
-
};
|
|
3359
|
-
return hasKimiWindow(cached) ? cached : null;
|
|
3360
|
-
}
|
|
3361
|
-
|
|
3362
|
-
function readKimiLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3363
|
-
if (!identity) return null;
|
|
3364
|
-
try {
|
|
3365
|
-
const parsed = JSON.parse(fs.readFileSync(resolveKimiLimitsCachePath({ home }), "utf8"));
|
|
3366
|
-
const entry = parsed?.kimi;
|
|
3367
|
-
if (entry?.identity !== identity) return null;
|
|
3368
|
-
return normalizeKimiCachedLimits(entry, { nowMs });
|
|
3369
|
-
} catch (_error) {
|
|
3370
|
-
return null;
|
|
3371
|
-
}
|
|
3372
|
-
}
|
|
3373
|
-
|
|
3374
|
-
function writeKimiLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3375
|
-
if (!identity) return;
|
|
3376
|
-
if (!limits?.configured || limits.error || !hasKimiWindow(limits)) return;
|
|
3377
|
-
const cachePath = resolveKimiLimitsCachePath({ home });
|
|
3378
|
-
const payload = {
|
|
3379
|
-
kimi: {
|
|
3380
|
-
identity,
|
|
3381
|
-
membership_level: limits.membership_level || null,
|
|
3382
|
-
subscription_type: limits.subscription_type || null,
|
|
3383
|
-
parallel_limit: Number.isFinite(limits.parallel_limit) ? limits.parallel_limit : null,
|
|
3384
|
-
primary_window: limits.primary_window || null,
|
|
3385
|
-
secondary_window: limits.secondary_window || null,
|
|
3386
|
-
tertiary_window: limits.tertiary_window || null,
|
|
3387
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
3388
|
-
},
|
|
3389
|
-
};
|
|
3390
|
-
try {
|
|
3391
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3392
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3393
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3394
|
-
fs.renameSync(tmpPath, cachePath);
|
|
3395
|
-
} catch (_error) {}
|
|
3396
|
-
}
|
|
3397
|
-
|
|
3398
|
-
function resolveGrokLimitsCachePath({ home } = {}) {
|
|
3399
|
-
return path.join(home || os.homedir(), ".tokentracker", "tracker", GROK_LIMITS_CACHE_FILE);
|
|
3400
|
-
}
|
|
3401
|
-
|
|
3402
|
-
function hasGrokWindow(limits) {
|
|
3403
|
-
return Boolean(limits?.primary_window || limits?.secondary_window);
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
// Grok windows carry `reset_at` as an ISO string — the billing-period end shared
|
|
3407
|
-
// by the pool, on-demand, and per-product windows. A window whose reset has
|
|
3408
|
-
// already passed is stale data, not a usable fallback, so drop it. Windows
|
|
3409
|
-
// without a reset stamp are kept — the overall cached_at max-age gate already
|
|
3410
|
-
// bounds them (mirrors Cursor).
|
|
3411
|
-
function isGrokCacheWindowUsable(window, { nowMs } = {}) {
|
|
3412
|
-
if (!window || typeof window !== "object") return false;
|
|
3413
|
-
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3414
|
-
if (resetAtMs === null) return true;
|
|
3415
|
-
return resetAtMs > nowMs;
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
|
-
function resolveCopilotLimitsCachePath({ home } = {}) {
|
|
3419
|
-
return path.join(home || os.homedir(), ".tokentracker", "tracker", COPILOT_LIMITS_CACHE_FILE);
|
|
3420
|
-
}
|
|
3421
|
-
|
|
3422
|
-
function hasCopilotWindow(limits) {
|
|
3423
|
-
return Boolean(limits?.primary_window || limits?.secondary_window);
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
// Copilot windows carry `reset_at` as an ISO string built from the monthly
|
|
3427
|
-
// quota_reset_date. A window whose reset has already passed is stale data, not a
|
|
3428
|
-
// usable fallback, so drop it. Windows without a reset stamp are kept — the overall
|
|
3429
|
-
// cached_at max-age gate already bounds them.
|
|
3430
|
-
function isCopilotCacheWindowUsable(window, { nowMs } = {}) {
|
|
3431
|
-
if (!window || typeof window !== "object") return false;
|
|
3432
|
-
const resetAtMs = parseTimeMs(window.reset_at);
|
|
3433
|
-
if (resetAtMs === null) return true;
|
|
3434
|
-
return resetAtMs > nowMs;
|
|
3435
|
-
}
|
|
3436
|
-
|
|
3437
|
-
function normalizeGrokCachedLimits(
|
|
3438
|
-
raw,
|
|
3439
|
-
{ nowMs = Date.now(), maxAgeMs = GROK_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3440
|
-
) {
|
|
3441
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3442
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3443
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3444
|
-
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3445
|
-
|
|
3446
|
-
const cached = {
|
|
3447
|
-
configured: true,
|
|
3448
|
-
error: null,
|
|
3449
|
-
plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
|
|
3450
|
-
period_type: typeof raw?.period_type === "string" ? raw.period_type : null,
|
|
3451
|
-
monthly_credits_limit: raw?.monthly_credits_limit ?? null,
|
|
3452
|
-
monthly_credits_used: raw?.monthly_credits_used ?? null,
|
|
3453
|
-
credit_usage_percent: raw?.credit_usage_percent ?? null,
|
|
3454
|
-
on_demand_cap: raw?.on_demand_cap ?? null,
|
|
3455
|
-
on_demand_used: raw?.on_demand_used ?? null,
|
|
3456
|
-
billing_period_start: typeof raw?.billing_period_start === "string" ? raw.billing_period_start : null,
|
|
3457
|
-
primary_window: isGrokCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3458
|
-
secondary_window: isGrokCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3459
|
-
// Per-product shares of the pool, not independent windows — each is gated
|
|
3460
|
-
// by the same reset stamp as the pool window it attributes.
|
|
3461
|
-
products: Array.isArray(raw?.products)
|
|
3462
|
-
? raw.products.filter((entry) => isGrokCacheWindowUsable(entry?.window, { nowMs }))
|
|
3463
|
-
: [],
|
|
3464
|
-
stale: true,
|
|
3465
|
-
cached_at: raw.cached_at,
|
|
3466
|
-
};
|
|
3467
|
-
return hasGrokWindow(cached) ? cached : null;
|
|
3468
|
-
}
|
|
2923
|
+
// The endpoint-scoped profile (home + OAuth host) partitions the cache: one
|
|
2924
|
+
// slot must never serve another region's windows (ark profile_identity
|
|
2925
|
+
// precedent). Windows carry `reset_at` (ISO) when the API reports a cycle
|
|
2926
|
+
// end; a window whose reset has already passed is stale data, not a usable
|
|
2927
|
+
// fallback, so drop it.
|
|
2928
|
+
const kimiLimitsCache = createLimitsCache({
|
|
2929
|
+
file: KIMI_LIMITS_CACHE_FILE,
|
|
2930
|
+
key: "kimi",
|
|
2931
|
+
maxAgeMs: KIMI_LIMITS_CACHE_MAX_AGE_MS,
|
|
2932
|
+
fields: { membership_level: "string", subscription_type: "string", parallel_limit: "number" },
|
|
2933
|
+
windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
|
|
2934
|
+
identityField: "identity",
|
|
2935
|
+
identityRequired: true,
|
|
2936
|
+
});
|
|
3469
2937
|
|
|
3470
2938
|
// The signed-in Grok account partitions the cache (zcode login-identity precedent):
|
|
3471
2939
|
// the id is the non-secret OIDC `sub` claim inside the access token — the
|
|
@@ -3476,106 +2944,34 @@ function normalizeGrokCachedLimits(
|
|
|
3476
2944
|
// (fail-closed) — an unattributable login must never pin another account's bars,
|
|
3477
2945
|
// which a scope-key fallback cannot guarantee because `issuer::client_id` stays
|
|
3478
2946
|
// constant across account switches.
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
const cached = {
|
|
3501
|
-
configured: true,
|
|
3502
|
-
error: null,
|
|
3503
|
-
plan_name: typeof raw?.plan_name === "string" ? raw.plan_name : null,
|
|
3504
|
-
primary_window: isCopilotCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3505
|
-
secondary_window: isCopilotCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3506
|
-
stale: true,
|
|
3507
|
-
cached_at: raw.cached_at,
|
|
3508
|
-
};
|
|
3509
|
-
return hasCopilotWindow(cached) ? cached : null;
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
// The signed-in GitHub account partitions the cache (zcode login-identity precedent):
|
|
3513
|
-
// the id is the non-secret GitHub login stored alongside the OAuth token (the
|
|
3514
|
-
// plaintext files' `user` field, or the auth.db row's `user_login`). A single home
|
|
3515
|
-
// resolves one active token, so one stamped entry suffices; a read requires the stored
|
|
3516
|
-
// identity to match the current one, and a write without an identity is refused — an
|
|
3517
|
-
// unreadable login must never pin another account's bars in place.
|
|
3518
|
-
function readCopilotLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3519
|
-
if (!identity) return null;
|
|
3520
|
-
try {
|
|
3521
|
-
const parsed = JSON.parse(fs.readFileSync(resolveCopilotLimitsCachePath({ home }), "utf8"));
|
|
3522
|
-
const entry = parsed?.copilot;
|
|
3523
|
-
if (entry?.identity !== identity) return null;
|
|
3524
|
-
return normalizeCopilotCachedLimits(entry, { nowMs });
|
|
3525
|
-
} catch (_error) {
|
|
3526
|
-
return null;
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3529
|
-
|
|
3530
|
-
function writeGrokLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3531
|
-
if (!identity) return;
|
|
3532
|
-
if (!limits?.configured || limits.error || !hasGrokWindow(limits)) return;
|
|
3533
|
-
const cachePath = resolveGrokLimitsCachePath({ home });
|
|
3534
|
-
const payload = {
|
|
3535
|
-
grok: {
|
|
3536
|
-
identity,
|
|
3537
|
-
plan_label: limits.plan_label || null,
|
|
3538
|
-
period_type: limits.period_type || null,
|
|
3539
|
-
monthly_credits_limit: limits.monthly_credits_limit ?? null,
|
|
3540
|
-
monthly_credits_used: limits.monthly_credits_used ?? null,
|
|
3541
|
-
credit_usage_percent: limits.credit_usage_percent ?? null,
|
|
3542
|
-
on_demand_cap: limits.on_demand_cap ?? null,
|
|
3543
|
-
on_demand_used: limits.on_demand_used ?? null,
|
|
3544
|
-
billing_period_start: limits.billing_period_start || null,
|
|
3545
|
-
primary_window: limits.primary_window || null,
|
|
3546
|
-
secondary_window: limits.secondary_window || null,
|
|
3547
|
-
products: Array.isArray(limits.products) ? limits.products : [],
|
|
3548
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
2947
|
+
const grokLimitsCache = createLimitsCache({
|
|
2948
|
+
file: GROK_LIMITS_CACHE_FILE,
|
|
2949
|
+
key: "grok",
|
|
2950
|
+
maxAgeMs: GROK_LIMITS_CACHE_MAX_AGE_MS,
|
|
2951
|
+
fields: {
|
|
2952
|
+
plan_label: "string",
|
|
2953
|
+
period_type: "string",
|
|
2954
|
+
monthly_credits_limit: "raw",
|
|
2955
|
+
monthly_credits_used: "raw",
|
|
2956
|
+
credit_usage_percent: "raw",
|
|
2957
|
+
on_demand_cap: "raw",
|
|
2958
|
+
on_demand_used: "raw",
|
|
2959
|
+
billing_period_start: "string",
|
|
2960
|
+
// Per-product shares of the pool, not independent windows — each is
|
|
2961
|
+
// gated by the same reset stamp as the pool window it attributes.
|
|
2962
|
+
products: {
|
|
2963
|
+
write: (record) => (Array.isArray(record?.products) ? record.products : []),
|
|
2964
|
+
read: (raw, ctx) => (Array.isArray(raw?.products)
|
|
2965
|
+
? raw.products.filter((entry) => ctx.isWindowUsable(entry?.window, { nowMs: ctx.nowMs }))
|
|
2966
|
+
: []),
|
|
3549
2967
|
},
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
|
-
function writeCopilotLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3560
|
-
if (!identity) return;
|
|
3561
|
-
if (!limits?.configured || limits.error || !hasCopilotWindow(limits)) return;
|
|
3562
|
-
const cachePath = resolveCopilotLimitsCachePath({ home });
|
|
3563
|
-
const payload = {
|
|
3564
|
-
copilot: {
|
|
3565
|
-
identity,
|
|
3566
|
-
plan_name: limits.plan_name || null,
|
|
3567
|
-
primary_window: limits.primary_window || null,
|
|
3568
|
-
secondary_window: limits.secondary_window || null,
|
|
3569
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
3570
|
-
},
|
|
3571
|
-
};
|
|
3572
|
-
try {
|
|
3573
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3574
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3575
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3576
|
-
fs.renameSync(tmpPath, cachePath);
|
|
3577
|
-
} catch (_error) {}
|
|
3578
|
-
}
|
|
2968
|
+
},
|
|
2969
|
+
// Windows carry `reset_at` as an ISO string — the billing-period end shared
|
|
2970
|
+
// by the pool, on-demand, and per-product windows.
|
|
2971
|
+
windowKeys: ["primary_window", "secondary_window"],
|
|
2972
|
+
identityField: "identity",
|
|
2973
|
+
identityRequired: true,
|
|
2974
|
+
});
|
|
3579
2975
|
|
|
3580
2976
|
// Failure-only identity re-derivation for the cache fallback: when the provider
|
|
3581
2977
|
// slot's result was produced by the outer timeout race, the fetcher's own
|
|
@@ -3595,6 +2991,24 @@ function resolveGrokCacheIdentity({ home, env } = {}) {
|
|
|
3595
2991
|
// the identity always belongs to the account the token resolution would pick. Async
|
|
3596
2992
|
// like the fetcher's own resolution so a failed poll never blocks the embedded
|
|
3597
2993
|
// server's event loop on SQLite / Keychain.
|
|
2994
|
+
// The signed-in GitHub account partitions the cache (zcode login-identity precedent):
|
|
2995
|
+
// the id is the non-secret GitHub login stored alongside the OAuth token (the
|
|
2996
|
+
// plaintext files' `user` field, or the auth.db row's `user_login`). A single home
|
|
2997
|
+
// resolves one active token, so one stamped entry suffices; a read requires the stored
|
|
2998
|
+
// identity to match the current one, and a write without an identity is refused — an
|
|
2999
|
+
// unreadable login must never pin another account's bars in place.
|
|
3000
|
+
const copilotLimitsCache = createLimitsCache({
|
|
3001
|
+
file: COPILOT_LIMITS_CACHE_FILE,
|
|
3002
|
+
key: "copilot",
|
|
3003
|
+
maxAgeMs: COPILOT_LIMITS_CACHE_MAX_AGE_MS,
|
|
3004
|
+
fields: { plan_name: "string" },
|
|
3005
|
+
// Windows carry `reset_at` as an ISO string built from the monthly
|
|
3006
|
+
// quota_reset_date.
|
|
3007
|
+
windowKeys: ["primary_window", "secondary_window"],
|
|
3008
|
+
identityField: "identity",
|
|
3009
|
+
identityRequired: true,
|
|
3010
|
+
});
|
|
3011
|
+
|
|
3598
3012
|
async function resolveCopilotCacheIdentity({
|
|
3599
3013
|
home = os.homedir(),
|
|
3600
3014
|
platform = process.platform,
|
|
@@ -3609,84 +3023,67 @@ async function resolveCopilotCacheIdentity({
|
|
|
3609
3023
|
}
|
|
3610
3024
|
}
|
|
3611
3025
|
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3026
|
+
// The signed-in Google account partitions the cache (zcode login-identity
|
|
3027
|
+
// precedent): the email decoded from the local id_token JWT is non-secret and is
|
|
3028
|
+
// the same id the payload already surfaces as account_email. GEMINI_HOME can aim
|
|
3029
|
+
// the credential lookup at a different installation, but the cache stays keyed by
|
|
3030
|
+
// account — a read requires the stored identity to match the account the current
|
|
3031
|
+
// credentials resolve to, and a write without an identity is refused: an
|
|
3032
|
+
// unreadable login must never pin another account's bars in place.
|
|
3033
|
+
const geminiLimitsCache = createLimitsCache({
|
|
3034
|
+
file: GEMINI_LIMITS_CACHE_FILE,
|
|
3035
|
+
key: "gemini",
|
|
3036
|
+
maxAgeMs: GEMINI_LIMITS_CACHE_MAX_AGE_MS,
|
|
3037
|
+
fields: { account_email: "string", account_plan: "string" },
|
|
3038
|
+
// Windows carry `reset_at` as an ISO string (the Code Assist bucket's own
|
|
3039
|
+
// resetTime — daily for free-tier quotas).
|
|
3040
|
+
windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
|
|
3041
|
+
identityField: "identity",
|
|
3042
|
+
identityRequired: true,
|
|
3043
|
+
});
|
|
3615
3044
|
|
|
3616
|
-
|
|
3617
|
-
|
|
3045
|
+
// Failure-only identity re-derivation for the cache fallback: when the provider
|
|
3046
|
+
// slot's result was produced by the outer timeout race, the fetcher's own
|
|
3047
|
+
// identity field is lost, so decode the (still-local) id_token once to learn
|
|
3048
|
+
// who we are serving for.
|
|
3049
|
+
function resolveGeminiCacheIdentity({ home, env } = {}) {
|
|
3050
|
+
try {
|
|
3051
|
+
return decodeJwtPayload(loadGeminiCredentials({ home, env })?.id_token)?.email || null;
|
|
3052
|
+
} catch (_error) {
|
|
3053
|
+
return null;
|
|
3054
|
+
}
|
|
3618
3055
|
}
|
|
3619
3056
|
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3057
|
+
const devinLimitsCache = createLimitsCache({
|
|
3058
|
+
file: DEVIN_LIMITS_CACHE_FILE,
|
|
3059
|
+
key: "devin",
|
|
3060
|
+
maxAgeMs: DEVIN_LIMITS_CACHE_MAX_AGE_MS,
|
|
3061
|
+
fields: { plan_label: "string" },
|
|
3062
|
+
windowKeys: ["primary_window", "secondary_window"],
|
|
3063
|
+
// Partition by the current credential-file revision, including on timeout
|
|
3064
|
+
// fallback. Legacy unpartitioned entries are deliberately ignored.
|
|
3065
|
+
identityField: "identity",
|
|
3066
|
+
identityRequired: true,
|
|
3067
|
+
});
|
|
3068
|
+
|
|
3069
|
+
function resolveDevinLimitsCachePath({ home } = {}) {
|
|
3070
|
+
return devinLimitsCache.path({ home });
|
|
3629
3071
|
}
|
|
3630
3072
|
|
|
3631
|
-
function
|
|
3632
|
-
|
|
3633
|
-
{ nowMs = Date.now(), maxAgeMs = DEVIN_LIMITS_CACHE_MAX_AGE_MS } = {},
|
|
3634
|
-
) {
|
|
3635
|
-
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
3636
|
-
if (!Number.isFinite(cachedAtMs)) return null;
|
|
3637
|
-
if (cachedAtMs > nowMs + 60_000) return null;
|
|
3638
|
-
if (nowMs - cachedAtMs > maxAgeMs) return null;
|
|
3639
|
-
|
|
3640
|
-
const cached = {
|
|
3641
|
-
configured: true,
|
|
3642
|
-
error: null,
|
|
3643
|
-
plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
|
|
3644
|
-
primary_window: isDevinCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
3645
|
-
secondary_window: isDevinCacheWindowUsable(raw?.secondary_window, { nowMs }) ? raw.secondary_window : null,
|
|
3646
|
-
stale: true,
|
|
3647
|
-
cached_at: raw.cached_at,
|
|
3648
|
-
};
|
|
3649
|
-
return hasDevinWindow(cached) ? cached : null;
|
|
3073
|
+
function hasDevinWindow(limits) {
|
|
3074
|
+
return Boolean(limits?.primary_window || limits?.secondary_window);
|
|
3650
3075
|
}
|
|
3651
3076
|
|
|
3652
|
-
// Match the current credential-file revision, including on timeout fallback.
|
|
3653
|
-
// Legacy unpartitioned entries are deliberately ignored.
|
|
3654
3077
|
function readDevinLimitsCache({ home, nowMs = Date.now(), identity } = {}) {
|
|
3655
|
-
|
|
3656
|
-
try {
|
|
3657
|
-
const parsed = JSON.parse(fs.readFileSync(resolveDevinLimitsCachePath({ home }), "utf8"));
|
|
3658
|
-
if (parsed?.devin?.identity !== identity) return null;
|
|
3659
|
-
return normalizeDevinCachedLimits(parsed.devin, { nowMs });
|
|
3660
|
-
} catch (_error) {
|
|
3661
|
-
return null;
|
|
3662
|
-
}
|
|
3078
|
+
return devinLimitsCache.read({ home, nowMs, identity });
|
|
3663
3079
|
}
|
|
3664
3080
|
|
|
3665
3081
|
function writeDevinLimitsCache(limits, { home, nowMs = Date.now(), identity } = {}) {
|
|
3666
|
-
|
|
3667
|
-
if (!limits?.configured || limits.error || !hasDevinWindow(limits)) return;
|
|
3668
|
-
const cachePath = resolveDevinLimitsCachePath({ home });
|
|
3669
|
-
const payload = {
|
|
3670
|
-
devin: {
|
|
3671
|
-
identity,
|
|
3672
|
-
plan_label: limits.plan_label || null,
|
|
3673
|
-
primary_window: limits.primary_window || null,
|
|
3674
|
-
secondary_window: limits.secondary_window || null,
|
|
3675
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
3676
|
-
},
|
|
3677
|
-
};
|
|
3678
|
-
try {
|
|
3679
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
3680
|
-
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
3681
|
-
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
3682
|
-
fs.renameSync(tmpPath, cachePath);
|
|
3683
|
-
} catch (_error) {}
|
|
3082
|
+
devinLimitsCache.write(limits, { home, nowMs, identity });
|
|
3684
3083
|
}
|
|
3685
3084
|
|
|
3686
3085
|
function clearDevinLimitsCache({ home } = {}) {
|
|
3687
|
-
|
|
3688
|
-
fs.rmSync(resolveDevinLimitsCachePath({ home }), { force: true });
|
|
3689
|
-
} catch (_error) {}
|
|
3086
|
+
devinLimitsCache.clear({ home });
|
|
3690
3087
|
}
|
|
3691
3088
|
|
|
3692
3089
|
// Only transient failures fall back to the disk cache. Requests without a
|
|
@@ -5635,41 +5032,23 @@ async function fetchUsageLimitsUncached({
|
|
|
5635
5032
|
// "fetch failed" (mirrors Codex / Claude / OpenCode Go). A session-expiry error
|
|
5636
5033
|
// (401/403 — "re-login in Cursor") is actionable and must not be hidden behind
|
|
5637
5034
|
// stale cache, so it passes through untouched.
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
cursorWithCache = {
|
|
5645
|
-
...cursor,
|
|
5646
|
-
stale: false,
|
|
5647
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
5648
|
-
};
|
|
5649
|
-
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
5650
|
-
writeCursorLimitsCache(cursorWithCache, {
|
|
5651
|
-
home,
|
|
5652
|
-
nowMs,
|
|
5653
|
-
identity: typeof cursor.cache_identity === "string"
|
|
5654
|
-
? cursor.cache_identity
|
|
5655
|
-
: resolveCursorCacheIdentity({ home, env, trackerDir }),
|
|
5656
|
-
});
|
|
5657
|
-
}
|
|
5658
|
-
} else {
|
|
5035
|
+
const cursorWithCache = await withLimitsCacheFallback(cursor, {
|
|
5036
|
+
cache: cursorLimitsCache,
|
|
5037
|
+
nowMs,
|
|
5038
|
+
home,
|
|
5039
|
+
env,
|
|
5040
|
+
isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
|
|
5659
5041
|
// The slot's error object loses the fetcher's identity field when the outer
|
|
5660
|
-
// timeout race wins (the common failure mode this cache exists for), so
|
|
5661
|
-
// re-
|
|
5662
|
-
|
|
5042
|
+
// timeout race wins (the common failure mode this cache exists for), so the
|
|
5043
|
+
// read path re-derives it from the local state DB.
|
|
5044
|
+
resolveWriteIdentity: (slot) => typeof slot.cache_identity === "string"
|
|
5045
|
+
? slot.cache_identity
|
|
5046
|
+
: resolveCursorCacheIdentity({ home, env }),
|
|
5047
|
+
resolveReadIdentity: () => typeof cursor.cache_identity === "string"
|
|
5663
5048
|
? cursor.cache_identity
|
|
5664
|
-
: resolveCursorCacheIdentity({ home, env, trackerDir })
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
}
|
|
5668
|
-
// cache_identity / auth_error are internal cache-plumbing markers, not payload.
|
|
5669
|
-
if (cursorWithCache && ("cache_identity" in cursorWithCache || "auth_error" in cursorWithCache)) {
|
|
5670
|
-
const { cache_identity: _identity, auth_error: _authError, ...cursorPayload } = cursorWithCache;
|
|
5671
|
-
cursorWithCache = cursorPayload;
|
|
5672
|
-
}
|
|
5049
|
+
: resolveCursorCacheIdentity({ home, env, trackerDir }),
|
|
5050
|
+
passthrough: (slot) => slot.auth_error === true,
|
|
5051
|
+
});
|
|
5673
5052
|
|
|
5674
5053
|
// Grok: persist last successful windows so a transient timeout/5xx on the
|
|
5675
5054
|
// cli-chat-proxy.grok.com route serves stale bars instead of a flashing
|
|
@@ -5677,43 +5056,26 @@ async function fetchUsageLimitsUncached({
|
|
|
5677
5056
|
// (GROK_AUTH_REQUIRED / GROK_REAUTH_REQUIRED — "run `grok login`") is
|
|
5678
5057
|
// actionable and must not be hidden behind stale cache, so it passes
|
|
5679
5058
|
// through untouched.
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
grokWithCache = {
|
|
5687
|
-
...grok,
|
|
5688
|
-
stale: false,
|
|
5689
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
5690
|
-
};
|
|
5059
|
+
const grokWithCache = await withLimitsCacheFallback(grok, {
|
|
5060
|
+
cache: grokLimitsCache,
|
|
5061
|
+
nowMs,
|
|
5062
|
+
home,
|
|
5063
|
+
env,
|
|
5064
|
+
isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
|
|
5691
5065
|
// Write only under the identity the fetch itself carried: re-reading
|
|
5692
5066
|
// auth.json here could attribute a finished fetch's windows to an account
|
|
5693
5067
|
// that logged in while the request was in flight (opaque → JWT switch).
|
|
5694
5068
|
// No fetch-side identity (opaque token) → refuse the write (fail-closed).
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
}
|
|
5702
|
-
} else {
|
|
5703
|
-
// The slot's error object loses the fetcher's identity field when the outer
|
|
5704
|
-
// timeout race wins (the common failure mode this cache exists for), so
|
|
5705
|
-
// re-derive it from auth.json on this failure-only path.
|
|
5706
|
-
const identity = typeof grok.cache_identity === "string"
|
|
5069
|
+
resolveWriteIdentity: (slot) => typeof slot.cache_identity === "string"
|
|
5070
|
+
? slot.cache_identity
|
|
5071
|
+
: null,
|
|
5072
|
+
// Re-derive from auth.json: the timeout race strips the fetcher's own
|
|
5073
|
+
// identity field.
|
|
5074
|
+
resolveReadIdentity: () => typeof grok.cache_identity === "string"
|
|
5707
5075
|
? grok.cache_identity
|
|
5708
|
-
: resolveGrokCacheIdentity({ home, env })
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
}
|
|
5712
|
-
// cache_identity / auth_error are internal cache-plumbing markers, not payload.
|
|
5713
|
-
if (grokWithCache && ("cache_identity" in grokWithCache || "auth_error" in grokWithCache)) {
|
|
5714
|
-
const { cache_identity: _identity, auth_error: _authError, ...grokPayload } = grokWithCache;
|
|
5715
|
-
grokWithCache = grokPayload;
|
|
5716
|
-
}
|
|
5076
|
+
: resolveGrokCacheIdentity({ home, env }),
|
|
5077
|
+
passthrough: (slot) => slot.auth_error === true,
|
|
5078
|
+
});
|
|
5717
5079
|
|
|
5718
5080
|
const cursorLimits = cursorSandDuplicatesGrokPool(cursorWithCache, grokWithCache)
|
|
5719
5081
|
? { ...cursorWithCache, quaternary_window: null }
|
|
@@ -5725,38 +5087,32 @@ async function fetchUsageLimitsUncached({
|
|
|
5725
5087
|
// passes through untouched. The otel probe fields ride along on every slot
|
|
5726
5088
|
// result, so a cached overlay spreads the live object first and lets the cached
|
|
5727
5089
|
// entry win only the limit fields.
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
}
|
|
5755
|
-
// cache_identity / auth_error are internal cache-plumbing markers, not payload.
|
|
5756
|
-
if (copilotWithCache && ("cache_identity" in copilotWithCache || "auth_error" in copilotWithCache)) {
|
|
5757
|
-
const { cache_identity: _identity, auth_error: _authError, ...copilotPayload } = copilotWithCache;
|
|
5758
|
-
copilotWithCache = copilotPayload;
|
|
5759
|
-
}
|
|
5090
|
+
const copilotWithCache = await withLimitsCacheFallback(copilot, {
|
|
5091
|
+
cache: copilotLimitsCache,
|
|
5092
|
+
nowMs,
|
|
5093
|
+
home,
|
|
5094
|
+
env,
|
|
5095
|
+
isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
|
|
5096
|
+
// Only the login captured with the request can own its windows: an
|
|
5097
|
+
// unknown login must never be promoted to a newly signed-in account, and
|
|
5098
|
+
// a fresh read of the auth store must still agree — a login that switched
|
|
5099
|
+
// while the request was in flight refuses the write.
|
|
5100
|
+
resolveWriteIdentity: async (slot) => {
|
|
5101
|
+
const identity = typeof slot.cache_identity === "string" ? slot.cache_identity : null;
|
|
5102
|
+
if (!identity) return null;
|
|
5103
|
+
const current = await resolveCopilotCacheIdentity({ home, platform, securityRunner });
|
|
5104
|
+
return identity === current ? identity : null;
|
|
5105
|
+
},
|
|
5106
|
+
beforeRead: async () => {
|
|
5107
|
+
// Re-read the current login even when the failed request carries an
|
|
5108
|
+
// identity: the account may have changed while the request was in flight.
|
|
5109
|
+
const identity = await resolveCopilotCacheIdentity({ home, platform, securityRunner });
|
|
5110
|
+
// Sample after the async auth lookup too: a slow poll can straddle a reset.
|
|
5111
|
+
return { identity, nowMs: Date.now() };
|
|
5112
|
+
},
|
|
5113
|
+
passthrough: (slot) => slot.auth_error === true,
|
|
5114
|
+
merge: "overlay",
|
|
5115
|
+
});
|
|
5760
5116
|
|
|
5761
5117
|
// Kimi: persist last successful windows so a transient timeout/5xx on the
|
|
5762
5118
|
// api.kimi.com / api.kimi.ai coding routes serves stale bars instead of a
|
|
@@ -5764,103 +5120,23 @@ async function fetchUsageLimitsUncached({
|
|
|
5764
5120
|
// errors ("Not logged in to Kimi" / "token_expired") and actionable profile
|
|
5765
5121
|
// misconfigurations must not be hidden behind stale cache, so they pass
|
|
5766
5122
|
// through untouched.
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
writeKimiLimitsCache(kimiWithCache, {
|
|
5780
|
-
home,
|
|
5781
|
-
nowMs,
|
|
5782
|
-
identity: typeof kimi.cache_identity === "string"
|
|
5783
|
-
? kimi.cache_identity
|
|
5784
|
-
: resolveKimiCacheIdentity({ home, env }),
|
|
5785
|
-
});
|
|
5786
|
-
}
|
|
5787
|
-
} else {
|
|
5788
|
-
// The slot's error object loses the fetcher's identity field when the outer
|
|
5789
|
-
// timeout race wins (the common failure mode this cache exists for), so
|
|
5790
|
-
// re-derive it from the local Kimi profile on this failure-only path.
|
|
5791
|
-
const identity = typeof kimi.cache_identity === "string"
|
|
5123
|
+
const kimiWithCache = await withLimitsCacheFallback(kimi, {
|
|
5124
|
+
cache: kimiLimitsCache,
|
|
5125
|
+
nowMs,
|
|
5126
|
+
home,
|
|
5127
|
+
env,
|
|
5128
|
+
isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
|
|
5129
|
+
resolveWriteIdentity: (slot) => typeof slot.cache_identity === "string"
|
|
5130
|
+
? slot.cache_identity
|
|
5131
|
+
: resolveKimiCacheIdentity({ home, env }),
|
|
5132
|
+
// Re-derive from the local Kimi profile: the timeout race strips the
|
|
5133
|
+
// fetcher's own identity field.
|
|
5134
|
+
resolveReadIdentity: () => typeof kimi.cache_identity === "string"
|
|
5792
5135
|
? kimi.cache_identity
|
|
5793
|
-
: resolveKimiCacheIdentity({ home, env })
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
}
|
|
5797
|
-
// cache_identity / auth_error / config_error are internal cache-plumbing
|
|
5798
|
-
// markers, not payload.
|
|
5799
|
-
if (kimiWithCache && ("cache_identity" in kimiWithCache || "auth_error" in kimiWithCache || "config_error" in kimiWithCache)) {
|
|
5800
|
-
const {
|
|
5801
|
-
cache_identity: _kimiIdentity,
|
|
5802
|
-
auth_error: _kimiAuthError,
|
|
5803
|
-
config_error: _kimiConfigError,
|
|
5804
|
-
...kimiPayload
|
|
5805
|
-
} = kimiWithCache;
|
|
5806
|
-
kimiWithCache = kimiPayload;
|
|
5807
|
-
}
|
|
5808
|
-
|
|
5809
|
-
// Gemini: persist last successful windows so a transient timeout/5xx on the
|
|
5810
|
-
// cloudcode-pa.googleapis.com routes serves stale bars instead of a flashing
|
|
5811
|
-
// "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Auth errors (a dead
|
|
5812
|
-
// OAuth session, or the unsupported api-key / vertex-ai auth modes) are
|
|
5813
|
-
// actionable and must not be hidden behind stale cache, so they pass through
|
|
5814
|
-
// untouched.
|
|
5815
|
-
let geminiWithCache;
|
|
5816
|
-
if (!gemini || gemini.configured === false) {
|
|
5817
|
-
geminiWithCache = gemini;
|
|
5818
|
-
} else if (gemini.auth_error === true) {
|
|
5819
|
-
geminiWithCache = gemini;
|
|
5820
|
-
} else if (!gemini.error && hasGeminiWindow(gemini)) {
|
|
5821
|
-
geminiWithCache = {
|
|
5822
|
-
...gemini,
|
|
5823
|
-
stale: false,
|
|
5824
|
-
cached_at: new Date(nowMs).toISOString(),
|
|
5825
|
-
};
|
|
5826
|
-
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
5827
|
-
// A slow in-flight fetch must not clobber the on-disk entry of an
|
|
5828
|
-
// account that has since been switched away: only write when the
|
|
5829
|
-
// snapshot's identity still matches the current credentials.
|
|
5830
|
-
const currentIdentity = resolveGeminiCacheIdentity({ home, env });
|
|
5831
|
-
const snapshotIdentity = typeof gemini.cache_identity === "string"
|
|
5832
|
-
? gemini.cache_identity
|
|
5833
|
-
: currentIdentity;
|
|
5834
|
-
if (snapshotIdentity && snapshotIdentity === currentIdentity) {
|
|
5835
|
-
writeGeminiLimitsCache(geminiWithCache, {
|
|
5836
|
-
home,
|
|
5837
|
-
nowMs,
|
|
5838
|
-
identity: snapshotIdentity,
|
|
5839
|
-
});
|
|
5840
|
-
}
|
|
5841
|
-
}
|
|
5842
|
-
} else {
|
|
5843
|
-
// The slot's error object loses the fetcher's identity field when the outer
|
|
5844
|
-
// timeout race wins (the common failure mode this cache exists for). Always
|
|
5845
|
-
// re-derive from the CURRENT local credentials — the request may have
|
|
5846
|
-
// started under an account that has since been switched away, in which
|
|
5847
|
-
// case the read's identity gate refuses the old entry. Revalidate the auth
|
|
5848
|
-
// mode too: a mid-flight switch to api-key/vertex-ai never reached the
|
|
5849
|
-
// punch-through branch above, but stale OAuth bars would still mislead.
|
|
5850
|
-
const selectedType = loadGeminiSettings({ home, env })?.security?.auth?.selectedType ?? null;
|
|
5851
|
-
if (selectedType === "api-key" || selectedType === "vertex-ai") {
|
|
5852
|
-
geminiWithCache = gemini;
|
|
5853
|
-
} else {
|
|
5854
|
-
const identity = resolveGeminiCacheIdentity({ home, env });
|
|
5855
|
-
const cached = readGeminiLimitsCache({ home, nowMs, identity });
|
|
5856
|
-
geminiWithCache = cached || gemini;
|
|
5857
|
-
}
|
|
5858
|
-
}
|
|
5859
|
-
// cache_identity / auth_error are internal cache-plumbing markers, not payload.
|
|
5860
|
-
if (geminiWithCache && ("cache_identity" in geminiWithCache || "auth_error" in geminiWithCache)) {
|
|
5861
|
-
const { cache_identity: _identity, auth_error: _authError, ...geminiPayload } = geminiWithCache;
|
|
5862
|
-
geminiWithCache = geminiPayload;
|
|
5863
|
-
}
|
|
5136
|
+
: resolveKimiCacheIdentity({ home, env }),
|
|
5137
|
+
passthrough: (slot) => slot.auth_error === true || slot.config_error === true,
|
|
5138
|
+
markers: ["cache_identity", "auth_error", "config_error"],
|
|
5139
|
+
});
|
|
5864
5140
|
|
|
5865
5141
|
// Kiro: the slot runs the local kiro-cli (its own 2s/20s command budgets,
|
|
5866
5142
|
// never rejects), so the failure modes are CLI timeouts, unparseable panels
|
|
@@ -5874,28 +5150,22 @@ async function fetchUsageLimitsUncached({
|
|
|
5874
5150
|
// account switch mid-flight refuses the write instead of stamping the
|
|
5875
5151
|
// previous account's panel under the new login.
|
|
5876
5152
|
const kiroAuthBlocked = isKiroAuthError(kiro?.error);
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
5887
|
-
const identity = typeof kiro.cache_identity === "string"
|
|
5888
|
-
? kiro.cache_identity
|
|
5153
|
+
const kiroWithCache = await withLimitsCacheFallback(kiro, {
|
|
5154
|
+
cache: kiroLimitsCache,
|
|
5155
|
+
nowMs,
|
|
5156
|
+
home,
|
|
5157
|
+
env,
|
|
5158
|
+
isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
|
|
5159
|
+
resolveWriteIdentity: (slot) => {
|
|
5160
|
+
const identity = typeof slot.cache_identity === "string"
|
|
5161
|
+
? slot.cache_identity
|
|
5889
5162
|
: resolveKiroAuthIdentity({ home });
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
}
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
} else {
|
|
5897
|
-
const cached = readKiroLimitsCache({
|
|
5898
|
-
home,
|
|
5163
|
+
return identity && identity === resolveKiroAuthIdentity({ home })
|
|
5164
|
+
? identity
|
|
5165
|
+
: null;
|
|
5166
|
+
},
|
|
5167
|
+
passthrough: (slot) => kiroAuthBlocked || !slot.error,
|
|
5168
|
+
beforeRead: () => ({
|
|
5899
5169
|
// Gate against the wall clock, not the aggregate's entry stamp: a slow
|
|
5900
5170
|
// failed CLI read can straddle a reset, and an expired window must not
|
|
5901
5171
|
// pass the reset gate on a timestamp from before the read started.
|
|
@@ -5903,16 +5173,53 @@ async function fetchUsageLimitsUncached({
|
|
|
5903
5173
|
// The current login, never the fetch-time capture — a switch that
|
|
5904
5174
|
// happened mid-flight must not match the previous account's entry.
|
|
5905
5175
|
identity: resolveKiroAuthIdentity({ home }),
|
|
5906
|
-
})
|
|
5176
|
+
}),
|
|
5907
5177
|
// The failed slot already carries the credits-sidecar fields the fetcher
|
|
5908
5178
|
// merged in; the cached windows overlay on top of them.
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5179
|
+
merge: "overlay",
|
|
5180
|
+
markers: ["cache_identity"],
|
|
5181
|
+
});
|
|
5182
|
+
|
|
5183
|
+
// Gemini: persist last successful windows so a transient timeout/5xx on the
|
|
5184
|
+
// cloudcode-pa.googleapis.com routes serves stale bars instead of a flashing
|
|
5185
|
+
// "fetch failed" (mirrors Cursor / Codex / OpenCode Go). Auth errors (a dead
|
|
5186
|
+
// OAuth session, or the unsupported api-key / vertex-ai auth modes) are
|
|
5187
|
+
// actionable and must not be hidden behind stale cache, so they pass through
|
|
5188
|
+
// untouched.
|
|
5189
|
+
const geminiWithCache = await withLimitsCacheFallback(gemini, {
|
|
5190
|
+
cache: geminiLimitsCache,
|
|
5191
|
+
nowMs,
|
|
5192
|
+
home,
|
|
5193
|
+
env,
|
|
5194
|
+
isCurrentGeneration: () => cacheGeneration === usageLimitsCacheGeneration,
|
|
5195
|
+
// A slow in-flight fetch must not clobber the on-disk entry of an
|
|
5196
|
+
// account that has since been switched away: only write when the
|
|
5197
|
+
// snapshot's own identity still matches the current credentials. An
|
|
5198
|
+
// identity-less snapshot (the id_token read at fetch start carried no
|
|
5199
|
+
// email) cannot be attributed to anyone — never promote it to the
|
|
5200
|
+
// account now holding the credentials.
|
|
5201
|
+
resolveWriteIdentity: (slot) => {
|
|
5202
|
+
const currentIdentity = resolveGeminiCacheIdentity({ home, env });
|
|
5203
|
+
const snapshotIdentity = typeof slot.cache_identity === "string"
|
|
5204
|
+
? slot.cache_identity
|
|
5205
|
+
: null;
|
|
5206
|
+
return snapshotIdentity && snapshotIdentity === currentIdentity
|
|
5207
|
+
? snapshotIdentity
|
|
5208
|
+
: null;
|
|
5209
|
+
},
|
|
5210
|
+
passthrough: (slot) => slot.auth_error === true,
|
|
5211
|
+
beforeRead: () => {
|
|
5212
|
+
// Always re-derive from the CURRENT local credentials — the request may
|
|
5213
|
+
// have started under an account that has since been switched away, in
|
|
5214
|
+
// which case the read's identity gate refuses the old entry. Revalidate
|
|
5215
|
+
// the auth mode too: a mid-flight switch to api-key/vertex-ai never
|
|
5216
|
+
// reached the punch-through branch above, but stale OAuth bars would
|
|
5217
|
+
// still mislead.
|
|
5218
|
+
const selectedType = loadGeminiSettings({ home, env })?.security?.auth?.selectedType ?? null;
|
|
5219
|
+
if (selectedType === "api-key" || selectedType === "vertex-ai") return null;
|
|
5220
|
+
return { identity: resolveGeminiCacheIdentity({ home, env }) };
|
|
5221
|
+
},
|
|
5222
|
+
});
|
|
5916
5223
|
|
|
5917
5224
|
const data = {
|
|
5918
5225
|
fetched_at: new Date(nowMs).toISOString(),
|