tokentracker-cli 1.1.10 → 1.1.12
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/README.md +2 -1
- package/dashboard/dist/assets/{AchievementBadge-CH97d6cg.js → AchievementBadge-bxsSwozZ.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-Dwvj09sF.js → AchievementsPage-CCGsTNky.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-D6HHK1f-.js → AchievementsSection-a-ucN3O9.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Bpr6rY9l.js → BadgeDetailModal-CNCCgDRL.js} +1 -1
- package/dashboard/dist/assets/BotAnimated-DYrmIQt7.js +1 -0
- package/dashboard/dist/assets/{Card-38v0-L6e.js → Card-615UteEP.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Bj0e0ye0.js → CommandPalette-BzmRKASJ.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-IoQLJCe2.js → CommunityStatsModal-DdRpM1bD.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-B7uK0Wjr.js → ConfirmModal-BQIPBiA5.js} +1 -1
- package/dashboard/dist/assets/Controls-Cup1Mj2U.js +1 -0
- package/dashboard/dist/assets/DashboardPage-DF1bFyHx.js +1 -0
- package/dashboard/dist/assets/{DevicePage-DE-NSyZg.js → DevicePage-CC31pv5N.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DB6ZRDR3.js → DialogClose-CaYoc9xQ.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DZLOEWil.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CgwmTJf3.js → DialogTitle-BViOiFtR.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CMjPtMSB.js → FadeIn-BQYqxLXW.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-D5cSnbcg.js → HeaderGithubStar-CpGwcrZV.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CFV8yLxb.js → HoverTooltip--hTrUe1c.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Ctu-GWJQ.js → IpCheckPage-gPQntnIE.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-PAIF034o.js → LandingPage-Cw72oDTD.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-D3CsY-nP.js → LeaderboardAvatar-CnqlbA5v.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-qE8DD-Ia.js → LeaderboardPage-CvdG-SGy.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BY3uaIdF.js → LeaderboardProfileModal-CqzEW_FY.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-CwX77U5Z.js → LeaderboardProfilePage-DCjJ7yF3.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-BlJgvFay.js → LimitsPage-DMGI3mL9.js} +2 -2
- package/dashboard/dist/assets/{LocalOnlyNotice-CLFKSu0V.js → LocalOnlyNotice-CpGpv1oF.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-Bdp2gihl.js → LoginCard-B5W06Jrz.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-o_t6uvYP.js → LoginModal-B4Y8XRkn.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-Cc3ZHUU8.js → LoginPage-BLN-PdCh.js} +1 -1
- package/dashboard/dist/assets/PetPage-BvY7fR8_.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-CGJBBnIf.js → PopoverPopup-C4_m01Pc.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-1OG4Kbkl.js → ProviderIcon-LgUYzw52.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DbfS1gfH.js → ResetPasswordPage-Dgzafk9_.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DPKl-28R.js → SegmentedControl-CMWf089j.js} +1 -1
- package/dashboard/dist/assets/{Select-BTA6mSut.js → Select-BNFp1Pww.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-CX5Br8hd.js → SelectItemText-Jz-4OEyW.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-CaTOLkT7.js → ServiceStatusPage-AEW89tSP.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-Ixp6Fseo.js → SessionsPage-DTW1sFcW.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-2z_a9T_C.js +2 -0
- package/dashboard/dist/assets/{SkillsPage-BUUfMFpU.js → SkillsPage-BkPEwhXO.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DnfKnQhr.js → TokenGalaxy-Cs5wfOxe.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-D0wMtL6Z.js → ToolbarRootContext-B3pyxVI-.js} +1 -1
- package/dashboard/dist/assets/TrendMonitor-BmSRt3h3.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-Bfod5aSH.js → WidgetsPage-BN5ePVqf.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-fYftPA8s.js → WrappedPage-DZTG3lCh.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-4IDwIR3i.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-CAQ_i-1v.css +1 -0
- package/dashboard/dist/assets/{agent-logos-C0O8Lnmv.js → agent-logos-DQmv0J3b.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-Bs0RpChC.js → arrow-up-right-BbkRcWv3.js} +1 -1
- package/dashboard/dist/assets/{check-Dj5Wi1I7.js → check-Dz72GL-k.js} +1 -1
- package/dashboard/dist/assets/{composite-BeHO1nUS.js → composite-COAFYYB2.js} +1 -1
- package/dashboard/dist/assets/{copy-Dgp1NqhD.js → copy-wEl0WhF0.js} +1 -1
- package/dashboard/dist/assets/{download-D1JugY5p.js → download-W7dmHuLF.js} +1 -1
- package/dashboard/dist/assets/{external-link-ow0tAluE.js → external-link-B-pvBbFP.js} +1 -1
- package/dashboard/dist/assets/{flame-Obus_5g4.js → flame-y6R5Plz2.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-yToZ6yvw.js → format-tokens-CkWutEDc.js} +1 -1
- package/dashboard/dist/assets/{icons-Da1IxY6v.js → icons-C_ECRc6a.js} +1 -1
- package/dashboard/dist/assets/{index-0hD-aFe2.js → index-B1Am3t0P.js} +1 -1
- package/dashboard/dist/assets/{index-BtmxTdj8.js → index-BzmW4EmI.js} +1 -1
- package/dashboard/dist/assets/{index-DduY8rpi.js → index-Dtl4UhBX.js} +1 -1
- package/dashboard/dist/assets/{info-CQjQ1whr.js → info-CCfzQ-S6.js} +1 -1
- package/dashboard/dist/assets/{link-2-BGYfWbPb.js → link-2-DvFi_t5y.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-WArW9cmn.js → loader-circle-BglSN88W.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-Bhde1l0n.js → maximize-2-B7Nzrb8n.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-BRxUZvoy.js → modifiers.esm-CsJhvsvC.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-2ZT8ACJS.js → monitor-smartphone-DpicOpX8.js} +1 -1
- package/dashboard/dist/assets/{plus-C_cg6gJE.js → plus-C40ubjK3.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CM8dzzcI.js → provider-display-E2soZfeg.js} +2 -2
- package/dashboard/dist/assets/{react-C3vDx5Sr.js → react-BseCgHfW.js} +1 -1
- package/dashboard/dist/assets/{search-Vd9we7KU.js → search-Bu5sWW2G.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CHxubvN1.js → skills-api-BBF6YhPQ.js} +1 -1
- package/dashboard/dist/assets/skins-Mc0sI20C.js +1 -0
- package/dashboard/dist/assets/{sortable.esm-rLR8Wj9r.js → sortable.esm-CLXvOgJi.js} +1 -1
- package/dashboard/dist/assets/{store-rdi5mnme.js → store-DdObzMvx.js} +1 -1
- package/dashboard/dist/assets/{terminal-xEPZhi6v.js → terminal-CRqmFLSX.js} +1 -1
- package/dashboard/dist/assets/{trash-Cad8nQc4.js → trash-Dknznc_u.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-SMq01IVW.js → use-community-stats-ZvEm6sF4.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-CSQ3Ypu3.js → use-latest-request-guard-QHLPnnSW.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DaaT6TL9.js → use-limits-display-prefs-CRg1sYpO.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-g58JllvT.js → use-ordered-list-NpIo40Y4.js} +1 -1
- package/dashboard/dist/assets/use-pet-settings-BUroRCzh.js +2 -0
- package/dashboard/dist/assets/use-reduced-motion-CDg8lAbR.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DyATLZ-L.js → use-session-efficiency-pref-aQVRBt9R.js} +1 -1
- package/dashboard/dist/assets/{use-transform-XYEzPgA-.js → use-transform-BP8NiCzk.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CvIocz6v.js +1 -0
- package/dashboard/dist/assets/{useCurrency-4xq0xUFx.js → useCurrency-CyksuFrj.js} +1 -1
- package/dashboard/dist/assets/useTokenFormat-CUwCluPw.js +1 -0
- package/dashboard/dist/assets/{zap-InLQedtD.js → zap-BcWplbpF.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 +3 -2
- package/src/commands/init.js +1 -1
- package/src/commands/sync.js +71 -34
- package/src/lib/claude-categorizer.js +4 -7
- package/src/lib/claude-user-identity.js +56 -0
- package/src/lib/cloud-sync-prefs.js +14 -0
- package/src/lib/cursor-store.js +636 -57
- package/src/lib/grok-limits.js +17 -4
- package/src/lib/kimi-profile.js +61 -0
- package/src/lib/local-api.js +17 -11
- package/src/lib/multi-install-parser.js +3 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +91 -39
- package/src/lib/usage-limits.js +128 -79
- package/dashboard/dist/assets/BotAnimated-srF64s63.js +0 -1
- package/dashboard/dist/assets/ClawdAnimated-C9a1u9lq.js +0 -2
- package/dashboard/dist/assets/Controls-D5XuFAlI.js +0 -1
- package/dashboard/dist/assets/DashboardPage-Ddb8k4DR.js +0 -1
- package/dashboard/dist/assets/DialogDescription-CaRzgdvP.js +0 -1
- package/dashboard/dist/assets/PetPage-DFaWnKsI.js +0 -1
- package/dashboard/dist/assets/SettingsPage-BmeQse01.js +0 -2
- package/dashboard/dist/assets/TrendMonitor-D3WafvUS.js +0 -19
- package/dashboard/dist/assets/__vite-browser-external-B98Yok9C.js +0 -45
- package/dashboard/dist/assets/__vite-browser-external-Bg0J-a5P.css +0 -1
- package/dashboard/dist/assets/skins-C8EyB85e.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-Dgdv5SSP.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-DL260plD.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-B6fHArH6.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { claudeUserIdentity, collectClaudeForkAliases } = require("./claude-user-identity");
|
|
1
2
|
const fs = require("node:fs/promises");
|
|
2
3
|
const fssync = require("node:fs");
|
|
3
4
|
const os = require("node:os");
|
|
@@ -982,6 +983,35 @@ async function parseClaudeIncremental({
|
|
|
982
983
|
const prevHashes = Array.isArray(cursors.claudeHashes) ? cursors.claudeHashes : [];
|
|
983
984
|
const seenMessageHashes = new Set(prevHashes);
|
|
984
985
|
const defaultSource = normalizeSourceInput(source) || "claude";
|
|
986
|
+
// Resolve all changed-file lineage before counting: fork files may sort first.
|
|
987
|
+
const forkTimes = new Map();
|
|
988
|
+
const forkAliases = await collectClaudeForkAliases(files, cursors,
|
|
989
|
+
(obj) => { if (typeof obj.timestamp === "string") forkTimes.set(obj.uuid, obj.timestamp); });
|
|
990
|
+
{
|
|
991
|
+
// Correct only copies whose old UUID identities prove they were counted.
|
|
992
|
+
// Preserve all token totals and history whose source files were removed.
|
|
993
|
+
const families = new Map();
|
|
994
|
+
const familyTimes = new Map();
|
|
995
|
+
for (const [uuid, ts] of forkTimes) familyTimes.set(claudeUserIdentity({ uuid }, forkAliases), ts);
|
|
996
|
+
for (const hash of prevHashes) {
|
|
997
|
+
if (typeof hash !== "string" || !hash.startsWith("u:")) continue;
|
|
998
|
+
const canonical = claudeUserIdentity({ uuid: hash.slice(2) }, forkAliases);
|
|
999
|
+
families.set(canonical, (families.get(canonical) || 0) + 1);
|
|
1000
|
+
seenMessageHashes.delete(hash);
|
|
1001
|
+
seenMessageHashes.add(canonical);
|
|
1002
|
+
}
|
|
1003
|
+
for (const [canonical, count] of families) {
|
|
1004
|
+
if (count < 2) continue;
|
|
1005
|
+
const ts = familyTimes.get(canonical);
|
|
1006
|
+
const hour = ts ? toUtcHalfHourStart(ts) : null;
|
|
1007
|
+
if (!hour) continue;
|
|
1008
|
+
const key = bucketKey(defaultSource, DEFAULT_MODEL, hour);
|
|
1009
|
+
const bucket = hourlyState.buckets[key];
|
|
1010
|
+
if (!bucket || bucket.totals.conversation_count < count - 1) continue;
|
|
1011
|
+
bucket.totals.conversation_count -= count - 1;
|
|
1012
|
+
touchedBuckets.add(key);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
985
1015
|
|
|
986
1016
|
if (!cursors.files || typeof cursors.files !== "object") {
|
|
987
1017
|
cursors.files = {};
|
|
@@ -1155,11 +1185,13 @@ async function parseClaudeIncremental({
|
|
|
1155
1185
|
projectRef,
|
|
1156
1186
|
projectKey,
|
|
1157
1187
|
seenMessageHashes,
|
|
1188
|
+
forkAliases,
|
|
1158
1189
|
});
|
|
1159
1190
|
|
|
1160
1191
|
cursors.files[key] = {
|
|
1161
1192
|
inode,
|
|
1162
1193
|
offset: result.endOffset,
|
|
1194
|
+
claudeForkIndexed: true,
|
|
1163
1195
|
updatedAt: new Date().toISOString(),
|
|
1164
1196
|
...(fileId ? { fileId, fileIdSize: st.size } : {}),
|
|
1165
1197
|
...(projectEnabled
|
|
@@ -1198,6 +1230,7 @@ async function parseClaudeIncremental({
|
|
|
1198
1230
|
cursors.projectHourly = projectState;
|
|
1199
1231
|
}
|
|
1200
1232
|
// Persist message hashes for cross-sync dedup; cap at 100k entries to bound size.
|
|
1233
|
+
cursors.claudeForkAliases = forkAliases;
|
|
1201
1234
|
const allHashes = Array.from(seenMessageHashes);
|
|
1202
1235
|
cursors.claudeHashes =
|
|
1203
1236
|
allHashes.length > 100_000 ? allHashes.slice(allHashes.length - 100_000) : allHashes;
|
|
@@ -2678,6 +2711,7 @@ async function parseClaudeFile({
|
|
|
2678
2711
|
projectRef,
|
|
2679
2712
|
projectKey,
|
|
2680
2713
|
seenMessageHashes,
|
|
2714
|
+
forkAliases,
|
|
2681
2715
|
}) {
|
|
2682
2716
|
const st = fileStat || (await fs.stat(filePath).catch(() => null));
|
|
2683
2717
|
if (!st || !st.isFile()) return { endOffset: startOffset, eventsAggregated: 0 };
|
|
@@ -2712,15 +2746,9 @@ async function parseClaudeFile({
|
|
|
2712
2746
|
typeof content === "string" ||
|
|
2713
2747
|
(Array.isArray(content) && content.some((b) => b?.type === "text"));
|
|
2714
2748
|
if (hasText) {
|
|
2715
|
-
//
|
|
2716
|
-
//
|
|
2717
|
-
|
|
2718
|
-
// guarantee from claudeMessageDedupKey; user lines have no
|
|
2719
|
-
// message.id, so the line uuid is the identity.
|
|
2720
|
-
const userKey =
|
|
2721
|
-
seenMessageHashes && typeof userObj?.uuid === "string" && userObj.uuid
|
|
2722
|
-
? `u:${userObj.uuid}`
|
|
2723
|
-
: null;
|
|
2749
|
+
// Copied user rows share their original identity through the full
|
|
2750
|
+
// fork lineage. Token dedup remains independent.
|
|
2751
|
+
const userKey = seenMessageHashes ? claudeUserIdentity(userObj, forkAliases) : null;
|
|
2724
2752
|
if (!userKey || !seenMessageHashes.has(userKey)) {
|
|
2725
2753
|
if (userKey) seenMessageHashes.add(userKey);
|
|
2726
2754
|
const userTs = typeof userObj?.timestamp === "string" ? userObj.timestamp : null;
|
|
@@ -3670,10 +3698,9 @@ function deriveOpencodeMessageFingerprint({ msg, totals, source }) {
|
|
|
3670
3698
|
return crypto.createHash("sha256").update(raw).digest("base64url").slice(0, 22);
|
|
3671
3699
|
}
|
|
3672
3700
|
|
|
3673
|
-
// `fingerprint -> messageKey
|
|
3674
|
-
//
|
|
3675
|
-
//
|
|
3676
|
-
// persisted buckets once. Tombstoned copies never claim ownership themselves.
|
|
3701
|
+
// `fingerprint -> Set<messageKey>` for counted messages in this cursor namespace.
|
|
3702
|
+
// Same-session duplicates are all retained as owners; the first session remains
|
|
3703
|
+
// canonical for cross-session fork copies. Tombstoned copies never claim ownership.
|
|
3677
3704
|
function buildOpencodeFingerprintIndex(messageIndex, wantedFingerprints = null) {
|
|
3678
3705
|
const byFingerprint = new Map();
|
|
3679
3706
|
if (!messageIndex || typeof messageIndex !== "object") return byFingerprint;
|
|
@@ -3681,12 +3708,9 @@ function buildOpencodeFingerprintIndex(messageIndex, wantedFingerprints = null)
|
|
|
3681
3708
|
const entry = messageIndex[key];
|
|
3682
3709
|
if (entry?.dedupedForkCopy === true) continue;
|
|
3683
3710
|
const fingerprint = entry && typeof entry.fingerprint === "string" ? entry.fingerprint : null;
|
|
3684
|
-
if (
|
|
3685
|
-
fingerprint
|
|
3686
|
-
|
|
3687
|
-
!byFingerprint.has(fingerprint)
|
|
3688
|
-
) {
|
|
3689
|
-
byFingerprint.set(fingerprint, key);
|
|
3711
|
+
if (fingerprint && (!wantedFingerprints || wantedFingerprints.has(fingerprint))) {
|
|
3712
|
+
if (!byFingerprint.has(fingerprint)) byFingerprint.set(fingerprint, new Set());
|
|
3713
|
+
byFingerprint.get(fingerprint).add(key);
|
|
3690
3714
|
}
|
|
3691
3715
|
}
|
|
3692
3716
|
return byFingerprint;
|
|
@@ -3704,12 +3728,18 @@ function opencodeMessageKeySession(messageKey) {
|
|
|
3704
3728
|
// must not delete usage.
|
|
3705
3729
|
function isOpencodeForkCopy(fingerprintIndex, fingerprint, messageKey) {
|
|
3706
3730
|
if (!fingerprint || !fingerprintIndex) return false;
|
|
3707
|
-
const
|
|
3708
|
-
if (!
|
|
3709
|
-
const ownerSession = opencodeMessageKeySession(owner);
|
|
3731
|
+
const owners = fingerprintIndex.get(fingerprint);
|
|
3732
|
+
if (!owners) return false;
|
|
3710
3733
|
const session = opencodeMessageKeySession(messageKey);
|
|
3711
|
-
if (!
|
|
3712
|
-
|
|
3734
|
+
if (!session) return false;
|
|
3735
|
+
let hasCrossSessionOwner = false;
|
|
3736
|
+
for (const owner of owners instanceof Set ? owners : [owners]) {
|
|
3737
|
+
const ownerSession = opencodeMessageKeySession(owner);
|
|
3738
|
+
if (!ownerSession) continue;
|
|
3739
|
+
if (owner === messageKey) return false;
|
|
3740
|
+
if (ownerSession !== session) hasCrossSessionOwner = true;
|
|
3741
|
+
}
|
|
3742
|
+
return hasCrossSessionOwner;
|
|
3713
3743
|
}
|
|
3714
3744
|
|
|
3715
3745
|
function normalizeOpencodeAttribution(raw) {
|
|
@@ -3787,8 +3817,16 @@ function recordOpencodeMessage({
|
|
|
3787
3817
|
prevDeduped === Boolean(dedupedForkCopy)
|
|
3788
3818
|
) return;
|
|
3789
3819
|
|
|
3790
|
-
if (
|
|
3791
|
-
|
|
3820
|
+
if (
|
|
3821
|
+
fingerprintIndex &&
|
|
3822
|
+
prevFingerprint &&
|
|
3823
|
+
(prevFingerprint !== nextFingerprint || dedupedForkCopy)
|
|
3824
|
+
) {
|
|
3825
|
+
const owners = fingerprintIndex.get(prevFingerprint);
|
|
3826
|
+
if (owners instanceof Set) {
|
|
3827
|
+
owners.delete(messageKey);
|
|
3828
|
+
if (owners.size === 0) fingerprintIndex.delete(prevFingerprint);
|
|
3829
|
+
} else if (owners === messageKey) {
|
|
3792
3830
|
fingerprintIndex.delete(prevFingerprint);
|
|
3793
3831
|
}
|
|
3794
3832
|
}
|
|
@@ -3797,13 +3835,12 @@ function recordOpencodeMessage({
|
|
|
3797
3835
|
if (nextAttribution) entry.attribution = encodeOpencodeAttribution(nextAttribution);
|
|
3798
3836
|
if (dedupedForkCopy) entry.dedupedForkCopy = true;
|
|
3799
3837
|
messageIndex[messageKey] = entry;
|
|
3800
|
-
if (
|
|
3801
|
-
fingerprintIndex
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
fingerprintIndex.set(nextFingerprint, messageKey);
|
|
3838
|
+
if (fingerprintIndex && nextFingerprint && !dedupedForkCopy) {
|
|
3839
|
+
if (!fingerprintIndex.has(nextFingerprint)) {
|
|
3840
|
+
fingerprintIndex.set(nextFingerprint, new Set());
|
|
3841
|
+
}
|
|
3842
|
+
const owners = fingerprintIndex.get(nextFingerprint);
|
|
3843
|
+
if (owners instanceof Set) owners.add(messageKey);
|
|
3807
3844
|
}
|
|
3808
3845
|
}
|
|
3809
3846
|
|
|
@@ -5542,6 +5579,8 @@ async function parseOpencodeDbIncremental({
|
|
|
5542
5579
|
source,
|
|
5543
5580
|
cursorKey,
|
|
5544
5581
|
publicRepoResolver,
|
|
5582
|
+
opencodeCursorStore,
|
|
5583
|
+
opencodeCursorNamespace = "flat",
|
|
5545
5584
|
}) {
|
|
5546
5585
|
await ensureDir(path.dirname(queuePath));
|
|
5547
5586
|
let messagesProcessed = 0;
|
|
@@ -5557,22 +5596,35 @@ async function parseOpencodeDbIncremental({
|
|
|
5557
5596
|
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
5558
5597
|
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
5559
5598
|
const cursorNamespace = typeof cursorKey === "string" && cursorKey.length > 0 ? cursorKey : "opencode";
|
|
5560
|
-
const opencodeState = normalizeOpencodeState(cursors?.[cursorNamespace]);
|
|
5561
|
-
const messageIndex = opencodeState.messages;
|
|
5562
5599
|
const touchedBuckets = new Set();
|
|
5563
5600
|
const defaultSource = normalizeSourceInput(source) || "opencode";
|
|
5601
|
+
const candidateMessageKeys = new Set();
|
|
5564
5602
|
const candidateFingerprints = new Set();
|
|
5565
5603
|
for (const entry of messages) {
|
|
5604
|
+
const msgForKey = { ...entry?.data };
|
|
5605
|
+
if (entry?.id && !msgForKey.id) msgForKey.id = entry.id;
|
|
5606
|
+
if (entry?.sessionID && !msgForKey.sessionID) msgForKey.sessionID = entry.sessionID;
|
|
5607
|
+
const messageKey = deriveOpencodeMessageKey(msgForKey, null);
|
|
5608
|
+
if (messageKey) candidateMessageKeys.add(messageKey);
|
|
5566
5609
|
const totals = normalizeOpencodeTokens(entry?.data?.tokens);
|
|
5567
5610
|
const fingerprint = totals
|
|
5568
5611
|
? deriveOpencodeMessageFingerprint({ msg: entry.data, totals, source: defaultSource })
|
|
5569
5612
|
: null;
|
|
5570
5613
|
if (fingerprint) candidateFingerprints.add(fingerprint);
|
|
5571
5614
|
}
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5615
|
+
const loaded = cursorNamespace === "opencode" && opencodeCursorStore
|
|
5616
|
+
? await opencodeCursorStore.loadOpencodeMessagesForBatch({
|
|
5617
|
+
namespace: opencodeCursorNamespace,
|
|
5618
|
+
messageKeys: candidateMessageKeys,
|
|
5619
|
+
fingerprints: candidateFingerprints,
|
|
5620
|
+
})
|
|
5621
|
+
: null;
|
|
5622
|
+
const opencodeState = normalizeOpencodeState(cursors?.[cursorNamespace]);
|
|
5623
|
+
const messageIndex = opencodeState.messages;
|
|
5624
|
+
// Direct parser callers and the legacy single-file store retain the in-memory
|
|
5625
|
+
// fallback; cursor-store generations provide the bounded reverse index.
|
|
5626
|
+
const fingerprintIndex = loaded?.fingerprintIndex ||
|
|
5627
|
+
buildOpencodeFingerprintIndex(messageIndex, candidateFingerprints);
|
|
5576
5628
|
|
|
5577
5629
|
for (let idx = 0; idx < messages.length; idx++) {
|
|
5578
5630
|
const entry = messages[idx];
|
package/src/lib/usage-limits.js
CHANGED
|
@@ -36,6 +36,7 @@ const { fetchQoderLimits, fetchQoderCnLimits } = require("./qoder-limits");
|
|
|
36
36
|
const { fetchArkCodingPlanLimits } = require("./ark-coding-plan-limits");
|
|
37
37
|
const { fetchArkAgentPlanLimits } = require("./ark-agent-plan-limits");
|
|
38
38
|
const { fetchProviderServiceStatus } = require("./provider-status");
|
|
39
|
+
const { resolveKimiProfile } = require("./kimi-profile");
|
|
39
40
|
const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
|
|
40
41
|
const {
|
|
41
42
|
runCommand,
|
|
@@ -48,14 +49,17 @@ const execFileAsync = promisify(cp.execFile);
|
|
|
48
49
|
// 2-minute in-memory cache. It also expires early at the earliest upcoming window
|
|
49
50
|
// reset in the cached data (see cacheExpiresAtMs), floored so a provider reporting
|
|
50
51
|
// a reset "right now" can't turn every poll into a full upstream round.
|
|
51
|
-
// Partitioned by the Devin opt-in selection
|
|
52
|
-
// off is never served (or joined onto) a response
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
// Partitioned by the Devin opt-in selection AND the Antigravity quota opt-out, so
|
|
53
|
+
// a request made while Devin is off is never served (or joined onto) a response
|
|
54
|
+
// fetched while it was on, and an aggregate fetched while Antigravity was enabled
|
|
55
|
+
// is never served after the opt-out is set. Key = `${devin}-${antigravity}`.
|
|
56
|
+
const USAGE_LIMITS_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
|
|
57
|
+
const cacheBySelection = {};
|
|
58
|
+
for (const key of USAGE_LIMITS_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
|
|
59
|
+
function usageLimitsSelectionKey(options) {
|
|
60
|
+
const devin = options?.devinEnabled === true ? "on" : "off";
|
|
61
|
+
const antigravity = isAntigravityQuotaDisabled() ? "off" : "on";
|
|
62
|
+
return `${devin}-${antigravity}`;
|
|
59
63
|
}
|
|
60
64
|
const CACHE_TTL_MS = 2 * 60 * 1000;
|
|
61
65
|
// Must stay below the macOS app's post-reset re-fetch grace (10s in
|
|
@@ -297,16 +301,21 @@ async function fetchClaudeUsageLimits(accessToken, { fetchImpl = fetch, maxAttem
|
|
|
297
301
|
throw new Error(`Claude API returned ${res.status}`);
|
|
298
302
|
}
|
|
299
303
|
const body = await res.json();
|
|
300
|
-
return
|
|
301
|
-
five_hour: body.five_hour ?? null,
|
|
302
|
-
seven_day: body.seven_day ?? null,
|
|
303
|
-
seven_day_opus: body.seven_day_opus ?? null,
|
|
304
|
-
weekly_scoped: extractClaudeScopedWeekly(body),
|
|
305
|
-
extra_usage: body.extra_usage ?? null,
|
|
306
|
-
};
|
|
304
|
+
return mapClaudeUsageBody(body);
|
|
307
305
|
}
|
|
308
306
|
}
|
|
309
307
|
|
|
308
|
+
/** Map an /api/oauth/usage body (live, or Claude Code's cached copy) onto the limits shape. */
|
|
309
|
+
function mapClaudeUsageBody(body) {
|
|
310
|
+
return {
|
|
311
|
+
five_hour: body?.five_hour ?? null,
|
|
312
|
+
seven_day: body?.seven_day ?? null,
|
|
313
|
+
seven_day_opus: body?.seven_day_opus ?? null,
|
|
314
|
+
weekly_scoped: extractClaudeScopedWeekly(body),
|
|
315
|
+
extra_usage: body?.extra_usage ?? null,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
310
319
|
// Classify a wham window by `limit_window_seconds` rather than its slot name.
|
|
311
320
|
// 18000s = 5h session window. 604800s = 7d weekly window. Free-tier accounts only get a
|
|
312
321
|
// weekly window, often delivered in the `primary_window` slot — naive position-based
|
|
@@ -777,28 +786,9 @@ async function fetchCursorLimits({ home, fetchImpl = fetch } = {}) {
|
|
|
777
786
|
}
|
|
778
787
|
}
|
|
779
788
|
|
|
780
|
-
function
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const base = home || os.homedir();
|
|
784
|
-
// Prefer the official Kimi Code (@moonshot-ai/kimi-code, ~/.kimi-code) when it
|
|
785
|
-
// holds a login — its credential file shape (kimi-code.json) and the
|
|
786
|
-
// auth/usages endpoints are identical to the legacy kimi-cli (~/.kimi), so the
|
|
787
|
-
// existing fetch path works unchanged. Fall back to legacy when kimi-code has
|
|
788
|
-
// no credentials, keeping old kimi-cli users untouched.
|
|
789
|
-
const explicitCode = typeof env?.KIMI_CODE_HOME === "string" ? env.KIMI_CODE_HOME.trim() : "";
|
|
790
|
-
const codeHome = explicitCode ? path.resolve(explicitCode) : path.join(base, ".kimi-code");
|
|
791
|
-
const codeCredsPath = path.join(codeHome, "credentials", "kimi-code.json");
|
|
792
|
-
try {
|
|
793
|
-
const raw = fs.readFileSync(codeCredsPath, "utf8").trim();
|
|
794
|
-
if (raw && JSON.parse(raw)?.access_token) return codeHome;
|
|
795
|
-
} catch { /* missing / empty / corrupt — fall through to legacy */ }
|
|
796
|
-
return path.join(base, ".kimi");
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function loadKimiCredentials({ home, env } = {}) {
|
|
800
|
-
const kimiHome = resolveKimiHome({ home, env });
|
|
801
|
-
const credsPath = path.join(kimiHome, "credentials", "kimi-code.json");
|
|
789
|
+
function loadKimiCredentials({ home, env, profile = resolveKimiProfile({ home, env }) } = {}) {
|
|
790
|
+
if (profile.error) return null;
|
|
791
|
+
const { credsPath } = profile;
|
|
802
792
|
if (!fs.existsSync(credsPath)) return null;
|
|
803
793
|
try {
|
|
804
794
|
return JSON.parse(fs.readFileSync(credsPath, "utf8"));
|
|
@@ -807,15 +797,10 @@ function loadKimiCredentials({ home, env } = {}) {
|
|
|
807
797
|
}
|
|
808
798
|
}
|
|
809
799
|
|
|
810
|
-
function saveKimiCredentials(creds, { home, env } = {}) {
|
|
811
|
-
const
|
|
812
|
-
const credsPath = path.join(kimiHome, "credentials", "kimi-code.json");
|
|
800
|
+
function saveKimiCredentials(creds, { home, env, profile = resolveKimiProfile({ home, env }) } = {}) {
|
|
801
|
+
const { credsPath } = profile;
|
|
813
802
|
fs.mkdirSync(path.dirname(credsPath), { recursive: true });
|
|
814
|
-
fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2));
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function hasKimiConfig({ home, env } = {}) {
|
|
818
|
-
return fs.existsSync(path.join(resolveKimiHome({ home, env }), "config.toml"));
|
|
803
|
+
fs.writeFileSync(credsPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
819
804
|
}
|
|
820
805
|
|
|
821
806
|
function kimiNumber(value) {
|
|
@@ -867,7 +852,7 @@ function kimiCredentialsExpired(creds, nowMs = Date.now()) {
|
|
|
867
852
|
return expiresAt * 1000 <= nowMs + 30_000;
|
|
868
853
|
}
|
|
869
854
|
|
|
870
|
-
async function refreshKimiAccessToken({ refreshToken,
|
|
855
|
+
async function refreshKimiAccessToken({ refreshToken, profile, fetchImpl = fetch } = {}) {
|
|
871
856
|
if (typeof refreshToken !== "string" || !refreshToken.trim()) {
|
|
872
857
|
throw new Error("Not logged in to Kimi. Run 'kimi' in Terminal to authenticate.");
|
|
873
858
|
}
|
|
@@ -878,8 +863,9 @@ async function refreshKimiAccessToken({ refreshToken, home, env, fetchImpl = fet
|
|
|
878
863
|
refresh_token: refreshToken,
|
|
879
864
|
});
|
|
880
865
|
|
|
881
|
-
const res = await fetchImpl(
|
|
866
|
+
const res = await fetchImpl(`${profile.oauthHost}/api/oauth/token`, {
|
|
882
867
|
method: "POST",
|
|
868
|
+
redirect: "error",
|
|
883
869
|
headers: {
|
|
884
870
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
885
871
|
"X-Msh-Platform": "kimi_cli",
|
|
@@ -907,13 +893,14 @@ async function refreshKimiAccessToken({ refreshToken, home, env, fetchImpl = fet
|
|
|
907
893
|
token_type: String(json.token_type || "Bearer"),
|
|
908
894
|
expires_in: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 900,
|
|
909
895
|
};
|
|
910
|
-
saveKimiCredentials(next, {
|
|
896
|
+
saveKimiCredentials(next, { profile });
|
|
911
897
|
return next.access_token;
|
|
912
898
|
}
|
|
913
899
|
|
|
914
|
-
async function fetchKimiUsage(accessToken, { fetchImpl = fetch } = {}) {
|
|
915
|
-
const res = await fetchImpl(
|
|
900
|
+
async function fetchKimiUsage(accessToken, { profile, fetchImpl = fetch } = {}) {
|
|
901
|
+
const res = await fetchImpl(`${profile.baseUrl}/usages`, {
|
|
916
902
|
method: "GET",
|
|
903
|
+
redirect: "error",
|
|
917
904
|
headers: {
|
|
918
905
|
Authorization: `Bearer ${accessToken}`,
|
|
919
906
|
Accept: "application/json",
|
|
@@ -929,10 +916,12 @@ async function fetchKimiUsage(accessToken, { fetchImpl = fetch } = {}) {
|
|
|
929
916
|
}
|
|
930
917
|
|
|
931
918
|
async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
|
|
932
|
-
|
|
919
|
+
const profile = resolveKimiProfile({ home, env });
|
|
920
|
+
if (!profile.configured) {
|
|
933
921
|
return { configured: false };
|
|
934
922
|
}
|
|
935
|
-
|
|
923
|
+
if (profile.error) return { configured: true, error: profile.error };
|
|
924
|
+
const creds = loadKimiCredentials({ profile });
|
|
936
925
|
let accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
|
|
937
926
|
if (!accessToken) {
|
|
938
927
|
return { configured: false };
|
|
@@ -941,23 +930,21 @@ async function fetchKimiLimits({ home, env, fetchImpl = fetch } = {}) {
|
|
|
941
930
|
if (kimiCredentialsExpired(creds) && creds?.refresh_token) {
|
|
942
931
|
accessToken = await refreshKimiAccessToken({
|
|
943
932
|
refreshToken: creds.refresh_token,
|
|
944
|
-
|
|
945
|
-
env,
|
|
933
|
+
profile,
|
|
946
934
|
fetchImpl,
|
|
947
935
|
});
|
|
948
936
|
}
|
|
949
937
|
let body;
|
|
950
938
|
try {
|
|
951
|
-
body = await fetchKimiUsage(accessToken, { fetchImpl });
|
|
939
|
+
body = await fetchKimiUsage(accessToken, { profile, fetchImpl });
|
|
952
940
|
} catch (error) {
|
|
953
941
|
if (error?.message === "token_expired" && creds?.refresh_token) {
|
|
954
942
|
accessToken = await refreshKimiAccessToken({
|
|
955
943
|
refreshToken: creds.refresh_token,
|
|
956
|
-
|
|
957
|
-
env,
|
|
944
|
+
profile,
|
|
958
945
|
fetchImpl,
|
|
959
946
|
});
|
|
960
|
-
body = await fetchKimiUsage(accessToken, { fetchImpl });
|
|
947
|
+
body = await fetchKimiUsage(accessToken, { profile, fetchImpl });
|
|
961
948
|
} else {
|
|
962
949
|
throw error;
|
|
963
950
|
}
|
|
@@ -2262,7 +2249,12 @@ function normalizeAntigravityCachedLimits(raw, { nowMs = Date.now() } = {}) {
|
|
|
2262
2249
|
return hasAntigravityWindow(cached) ? cached : null;
|
|
2263
2250
|
}
|
|
2264
2251
|
|
|
2252
|
+
function isAntigravityQuotaDisabled() {
|
|
2253
|
+
return process.env.TOKENTRACKER_DISABLE_ANTIGRAVITY_QUOTA === "1";
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2265
2256
|
function readAntigravityLimitsCache({ home, nowMs = Date.now() } = {}) {
|
|
2257
|
+
if (isAntigravityQuotaDisabled()) return null;
|
|
2266
2258
|
const cachePath = resolveAntigravityLimitsCachePath({ home });
|
|
2267
2259
|
try {
|
|
2268
2260
|
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
@@ -2368,7 +2360,9 @@ function readClaudeLimitsCache({
|
|
|
2368
2360
|
maxAgeMs = CLAUDE_LIMITS_CACHE_MAX_AGE_MS,
|
|
2369
2361
|
stale = true,
|
|
2370
2362
|
} = {}) {
|
|
2371
|
-
return
|
|
2363
|
+
return pickNewestClaudeSnapshot(readClaudeLimitsCacheCandidates({ home }).map((raw) => (
|
|
2364
|
+
normalizeClaudeCachedLimits(raw, { nowMs, maxAgeMs, stale })
|
|
2365
|
+
)));
|
|
2372
2366
|
}
|
|
2373
2367
|
|
|
2374
2368
|
// A cached snapshot stops being "fresh" the moment any of its windows crosses the
|
|
@@ -2412,16 +2406,60 @@ function claudeCacheAwaitsNewWindow(raw, { home, nowMs } = {}) {
|
|
|
2412
2406
|
}
|
|
2413
2407
|
}
|
|
2414
2408
|
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2409
|
+
// Claude Code keeps its last /api/oauth/usage response in its global config
|
|
2410
|
+
// (`cachedUsageUtilization` in ~/.claude.json). That endpoint is throttled hard and its
|
|
2411
|
+
// budget is shared with Claude Code itself, so reading this copy costs no request. Only
|
|
2412
|
+
// usage figures are read — no credentials. The path deliberately ignores CLAUDE_CONFIG_DIR:
|
|
2413
|
+
// readClaudeCodeOauthToken reads the default profile's credentials (default Keychain item,
|
|
2414
|
+
// ~/.claude/.credentials.json), and the cache must come from that same profile so it can
|
|
2415
|
+
// never describe a different account than the token this process would query with.
|
|
2416
|
+
function resolveClaudeCodeGlobalConfigPath({ home } = {}) {
|
|
2417
|
+
return path.join(home || os.homedir(), ".claude.json");
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
/**
|
|
2421
|
+
* Claude Code's cached usage as a raw cache entry (same shape as our own disk cache),
|
|
2422
|
+
* or null when absent, unparsable, or recorded for a different account than the one
|
|
2423
|
+
* Claude Code is currently signed in to.
|
|
2424
|
+
*/
|
|
2425
|
+
function readClaudeCodeUsageCacheRaw({ home } = {}) {
|
|
2426
|
+
try {
|
|
2427
|
+
const config = JSON.parse(fs.readFileSync(resolveClaudeCodeGlobalConfigPath({ home }), "utf8"));
|
|
2428
|
+
const cached = config?.cachedUsageUtilization;
|
|
2429
|
+
const fetchedAtMs = Number(cached?.fetchedAtMs);
|
|
2430
|
+
if (!cached?.utilization || typeof cached.utilization !== "object" || !Number.isFinite(fetchedAtMs)) return null;
|
|
2431
|
+
const cachedAccount = typeof cached.accountUuid === "string" ? cached.accountUuid : null;
|
|
2432
|
+
const currentAccount = typeof config?.oauthAccount?.accountUuid === "string" ? config.oauthAccount.accountUuid : null;
|
|
2433
|
+
if (!cachedAccount || !currentAccount || cachedAccount !== currentAccount) return null;
|
|
2434
|
+
return { ...mapClaudeUsageBody(cached.utilization), cached_at: new Date(fetchedAtMs).toISOString() };
|
|
2435
|
+
} catch (_error) {
|
|
2418
2436
|
return null;
|
|
2419
2437
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
});
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
/** Raw snapshots from both sources: our own disk cache and Claude Code's cached read. */
|
|
2441
|
+
function readClaudeLimitsCacheCandidates({ home } = {}) {
|
|
2442
|
+
return [readClaudeLimitsCacheRaw({ home }), readClaudeCodeUsageCacheRaw({ home })].filter(Boolean);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
/** Newest of the already-validated snapshots, by cached_at. */
|
|
2446
|
+
function pickNewestClaudeSnapshot(snapshots) {
|
|
2447
|
+
let newest = null;
|
|
2448
|
+
for (const snapshot of snapshots) {
|
|
2449
|
+
if (!snapshot) continue;
|
|
2450
|
+
if (!newest || parseTimeMs(snapshot.cached_at) > parseTimeMs(newest.cached_at)) newest = snapshot;
|
|
2451
|
+
}
|
|
2452
|
+
return newest;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// Each source is validated on its own before picking the newest, so a newer snapshot
|
|
2456
|
+
// whose windows are all unusable can never displace an older usable one.
|
|
2457
|
+
function readFreshClaudeLimitsCache({ home, nowMs = Date.now() } = {}) {
|
|
2458
|
+
return pickNewestClaudeSnapshot(readClaudeLimitsCacheCandidates({ home }).map((raw) => (
|
|
2459
|
+
claudeCacheCrossedReset(raw, { nowMs }) || claudeCacheAwaitsNewWindow(raw, { home, nowMs })
|
|
2460
|
+
? null
|
|
2461
|
+
: normalizeClaudeCachedLimits(raw, { nowMs, maxAgeMs: CLAUDE_LIMITS_CACHE_FRESH_TTL_MS, stale: false })
|
|
2462
|
+
)));
|
|
2425
2463
|
}
|
|
2426
2464
|
|
|
2427
2465
|
function writeClaudeLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
|
|
@@ -3256,6 +3294,7 @@ function loadAntigravityCredentials({
|
|
|
3256
3294
|
securityRunner,
|
|
3257
3295
|
nowMs = Date.now(),
|
|
3258
3296
|
} = {}) {
|
|
3297
|
+
if (isAntigravityQuotaDisabled()) return null;
|
|
3259
3298
|
const candidates = collectAntigravityFileCredentials({ home });
|
|
3260
3299
|
if (platform === "darwin" || typeof securityRunner === "function") {
|
|
3261
3300
|
const parsed = parseAntigravityCredentialPayload(readAntigravityKeychainRaw({ securityRunner }));
|
|
@@ -3403,6 +3442,7 @@ async function fetchAntigravityRemoteLimits({
|
|
|
3403
3442
|
signal,
|
|
3404
3443
|
creds,
|
|
3405
3444
|
} = {}) {
|
|
3445
|
+
if (isAntigravityQuotaDisabled()) return null;
|
|
3406
3446
|
const resolvedCreds = creds !== undefined
|
|
3407
3447
|
? creds
|
|
3408
3448
|
: loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
|
|
@@ -3497,6 +3537,7 @@ async function fetchAntigravityLimits({
|
|
|
3497
3537
|
securityRunner,
|
|
3498
3538
|
signal,
|
|
3499
3539
|
} = {}) {
|
|
3540
|
+
if (isAntigravityQuotaDisabled()) return { configured: false, error: null };
|
|
3500
3541
|
const creds = loadAntigravityCredentials({ home, platform, securityRunner, nowMs });
|
|
3501
3542
|
const startedAtMs = performance.now();
|
|
3502
3543
|
// min(this step's ceiling, budget left after reserving the fallback guard).
|
|
@@ -3721,7 +3762,8 @@ function withPlanLabel(obj, raw, brand) {
|
|
|
3721
3762
|
// hammered). Survives an external resetUsageLimitsCache() (refresh=1 path in
|
|
3722
3763
|
// local-api.js): a refresh arriving while a fetch is already running reuses that
|
|
3723
3764
|
// in-flight fetch and returns its result.
|
|
3724
|
-
const
|
|
3765
|
+
const inFlightBySelection = {};
|
|
3766
|
+
for (const key of USAGE_LIMITS_SELECTION_KEYS) inFlightBySelection[key] = null;
|
|
3725
3767
|
|
|
3726
3768
|
// Codex stamps reset_at as unix seconds; every other provider (and Claude's
|
|
3727
3769
|
// resets_at) uses ISO strings. Numbers that look like epoch milliseconds are
|
|
@@ -3759,19 +3801,19 @@ function cacheExpiresAtMs(data, fetchedAtMs) {
|
|
|
3759
3801
|
}
|
|
3760
3802
|
|
|
3761
3803
|
async function getUsageLimits(options = {}) {
|
|
3762
|
-
const selection =
|
|
3763
|
-
const cache =
|
|
3804
|
+
const selection = usageLimitsSelectionKey(options);
|
|
3805
|
+
const cache = cacheBySelection[selection];
|
|
3764
3806
|
const nowMs = Date.now();
|
|
3765
3807
|
if (cache.data && nowMs < cache.expiresAtMs) {
|
|
3766
3808
|
return cache.data;
|
|
3767
3809
|
}
|
|
3768
|
-
if (
|
|
3769
|
-
return
|
|
3810
|
+
if (inFlightBySelection[selection]) {
|
|
3811
|
+
return inFlightBySelection[selection];
|
|
3770
3812
|
}
|
|
3771
|
-
const promise = fetchUsageLimitsUncached(options).finally(() => {
|
|
3772
|
-
if (
|
|
3813
|
+
const promise = fetchUsageLimitsUncached({ ...options, selectionKey: selection }).finally(() => {
|
|
3814
|
+
if (inFlightBySelection[selection] === promise) inFlightBySelection[selection] = null;
|
|
3773
3815
|
});
|
|
3774
|
-
|
|
3816
|
+
inFlightBySelection[selection] = promise;
|
|
3775
3817
|
return promise;
|
|
3776
3818
|
}
|
|
3777
3819
|
|
|
@@ -3787,6 +3829,10 @@ async function fetchUsageLimitsUncached({
|
|
|
3787
3829
|
providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
|
|
3788
3830
|
forceRefresh = false,
|
|
3789
3831
|
devinEnabled = false,
|
|
3832
|
+
// Selection key fixed by getUsageLimits before the fetch began. Re-deriving it
|
|
3833
|
+
// here would let an opt-out flipped mid-fetch store a pre-opt-out result in the
|
|
3834
|
+
// post-opt-out slot.
|
|
3835
|
+
selectionKey,
|
|
3790
3836
|
} = {}) {
|
|
3791
3837
|
const nowMs = Date.now();
|
|
3792
3838
|
|
|
@@ -3840,7 +3886,8 @@ async function fetchUsageLimitsUncached({
|
|
|
3840
3886
|
: null;
|
|
3841
3887
|
// Also avoid cross-process hammering after a recent successful read: embedded-server
|
|
3842
3888
|
// restarts and background polls read the disk cache instead of spending another Claude
|
|
3843
|
-
// OAuth usage request.
|
|
3889
|
+
// OAuth usage request. Claude Code's own cached read counts too, so a session that just
|
|
3890
|
+
// refreshed its usage spares this process a call against the same throttled budget. An explicit user refresh (refresh=1 → forceRefresh) punches
|
|
3844
3891
|
// through this cache — but never through the 429 cooldown above, which is exactly the
|
|
3845
3892
|
// hammering the cooldown exists to prevent.
|
|
3846
3893
|
const freshClaudeCache = claudeToken && !forceRefresh
|
|
@@ -4255,7 +4302,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4255
4302
|
};
|
|
4256
4303
|
}
|
|
4257
4304
|
|
|
4258
|
-
|
|
4305
|
+
cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
|
|
4259
4306
|
data,
|
|
4260
4307
|
expiresAtMs: cacheExpiresAtMs(data, nowMs),
|
|
4261
4308
|
};
|
|
@@ -4263,8 +4310,9 @@ async function fetchUsageLimitsUncached({
|
|
|
4263
4310
|
}
|
|
4264
4311
|
|
|
4265
4312
|
function resetUsageLimitsCache() {
|
|
4266
|
-
|
|
4267
|
-
|
|
4313
|
+
for (const key of USAGE_LIMITS_SELECTION_KEYS) {
|
|
4314
|
+
cacheBySelection[key] = { data: null, expiresAtMs: 0 };
|
|
4315
|
+
}
|
|
4268
4316
|
}
|
|
4269
4317
|
|
|
4270
4318
|
module.exports = {
|
|
@@ -4275,6 +4323,7 @@ module.exports = {
|
|
|
4275
4323
|
runCommand,
|
|
4276
4324
|
extractGeminiOauthClientCredentials,
|
|
4277
4325
|
loadKimiCredentials,
|
|
4326
|
+
fetchKimiLimits,
|
|
4278
4327
|
normalizeCursorUsageSummary,
|
|
4279
4328
|
normalizeCursorSandUsageStatus,
|
|
4280
4329
|
normalizeGeminiQuotaResponse,
|