dsh-agy 0.3.2 → 0.4.1
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 +21 -10
- package/lib/{accounts-CKWybAfc.mjs → accounts-BjdDqUq1.mjs} +425 -76
- package/lib/accounts-BjdDqUq1.mjs.map +1 -0
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +4 -2
- package/lib/cli/index.mjs.map +1 -1
- package/lib/client.js +406 -36
- package/lib/index.mjs +2 -2
- package/lib/{plugin-common-B5dyfnMH.mjs → plugin-common-B7OSfnyl.mjs} +354 -24
- package/lib/plugin-common-B7OSfnyl.mjs.map +1 -0
- package/lib/{translate-BQu8HLvB.mjs → translate-YhGDUuVa.mjs} +157 -23
- package/lib/translate-YhGDUuVa.mjs.map +1 -0
- package/lib/web/plugin.d.mts +28 -1
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +63 -25
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +4 -3
- package/lib/accounts-CKWybAfc.mjs.map +0 -1
- package/lib/plugin-common-B5dyfnMH.mjs.map +0 -1
- package/lib/translate-BQu8HLvB.mjs.map +0 -1
|
@@ -544,6 +544,87 @@ function resolveMasterKeyCodec(dshHome) {
|
|
|
544
544
|
};
|
|
545
545
|
}
|
|
546
546
|
//#endregion
|
|
547
|
+
//#region src/store/paths.ts
|
|
548
|
+
/**
|
|
549
|
+
* The `agy` data folder: every data file this plugin creates lives under
|
|
550
|
+
* `$DSH_HOME/agy/` instead of scattered across the DSH home.
|
|
551
|
+
*
|
|
552
|
+
* Why one folder: agy owned five JSON files sitting directly in `$DSH_HOME`
|
|
553
|
+
* (accounts, stats, models, thinking, fingerprint override) next to the host's
|
|
554
|
+
* own files. A folder is the boundary a user can see, back up, or delete whole.
|
|
555
|
+
*
|
|
556
|
+
* Migration is a ONE-SHOT ATOMIC RENAME, not a read-merge with lazy fallback:
|
|
557
|
+
*
|
|
558
|
+
* - `agy/` sits on the same filesystem as `$DSH_HOME`, so `rename` is atomic
|
|
559
|
+
* and preserves the 0600 mode.
|
|
560
|
+
* - A read-merge would need every store to carry two paths, two file locks,
|
|
561
|
+
* and per-store merge logic — and the merge itself can still lose updates
|
|
562
|
+
* (accounts mutate, stats counts).
|
|
563
|
+
* - A lazy fallback reads the old path forever, which defeats the point: the
|
|
564
|
+
* old file would never leave the home directory.
|
|
565
|
+
*
|
|
566
|
+
* Version-skew window contract: after a migration an OLD build recreates its
|
|
567
|
+
* legacy file, and its writes are invisible to the new layout — the both-files
|
|
568
|
+
* warning in `migrateToAgyDir` is the detector for exactly that (it fires when
|
|
569
|
+
* a later process finds both files present). Local SOP re-syncs all profiles at
|
|
570
|
+
* once, so the window is one app restart; npm users restart every dsh surface
|
|
571
|
+
* per the release note. Accounts logged in by an old CLI during skew need one
|
|
572
|
+
* re-login — enumerable, one-time, accepted in exchange for zero merge code.
|
|
573
|
+
*/
|
|
574
|
+
/** The directory under `$DSH_HOME` holding every agy data file. */
|
|
575
|
+
function agyDataDir(dshHome = resolveDshHome()) {
|
|
576
|
+
return join(dshHome, "agy");
|
|
577
|
+
}
|
|
578
|
+
/** A data file inside the agy directory. */
|
|
579
|
+
function agyDataFile(name, dshHome = resolveDshHome()) {
|
|
580
|
+
return join(agyDataDir(dshHome), name);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* One-time legacy -> `agy/` migration for one data file.
|
|
584
|
+
*
|
|
585
|
+
* 1. New file already present: warn-once (via `skew`) when the legacy file is
|
|
586
|
+
* ALSO present — an old-version process is still writing the old path.
|
|
587
|
+
* Return the new path regardless: the new layout wins once it exists.
|
|
588
|
+
* 2. No legacy file: fresh install (or already migrated), nothing to do.
|
|
589
|
+
* 3. Otherwise create the directory and rename. On ANY failure (`agy/` cannot
|
|
590
|
+
* be created, EXDEV, permissions, a locked target on Windows) fall back to
|
|
591
|
+
* the LEGACY path for this process: for accounts, the old layout with the
|
|
592
|
+
* real pool beats a new empty one, and `skew` surfaces the reason instead
|
|
593
|
+
* of hiding it.
|
|
594
|
+
*
|
|
595
|
+
* The caller decides what `skew` becomes; `plugin-common.ts` and the CLI log
|
|
596
|
+
* it once per process at most.
|
|
597
|
+
*/
|
|
598
|
+
function migrateToAgyDir(name, dshHome = resolveDshHome()) {
|
|
599
|
+
const legacy = join(dshHome, name);
|
|
600
|
+
const file = agyDataFile(name, dshHome);
|
|
601
|
+
if (existsSync(file)) return {
|
|
602
|
+
file,
|
|
603
|
+
migrated: false,
|
|
604
|
+
skew: existsSync(legacy)
|
|
605
|
+
};
|
|
606
|
+
if (!existsSync(legacy)) return {
|
|
607
|
+
file,
|
|
608
|
+
migrated: false,
|
|
609
|
+
skew: false
|
|
610
|
+
};
|
|
611
|
+
try {
|
|
612
|
+
mkdirSync(agyDataDir(dshHome), { recursive: true });
|
|
613
|
+
renameSync(legacy, file);
|
|
614
|
+
return {
|
|
615
|
+
file,
|
|
616
|
+
migrated: true,
|
|
617
|
+
skew: false
|
|
618
|
+
};
|
|
619
|
+
} catch {
|
|
620
|
+
return {
|
|
621
|
+
file: legacy,
|
|
622
|
+
migrated: false,
|
|
623
|
+
skew: true
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
//#endregion
|
|
547
628
|
//#region src/runtime/rotation.ts
|
|
548
629
|
const BACKOFF_TIERS_MS = [
|
|
549
630
|
5e3,
|
|
@@ -1091,14 +1172,23 @@ function earliestResetTime(a, b) {
|
|
|
1091
1172
|
* counts Claude and GPT under one third-party counter, so the weekly budget
|
|
1092
1173
|
* belongs to both. No model-id prefix rule can recover that split, which is why
|
|
1093
1174
|
* the mapping lives here rather than in `modelFamilyOf`.
|
|
1175
|
+
*
|
|
1176
|
+
* Window-aware as of #56: a bucket id resolves a family only when the window it
|
|
1177
|
+
* describes is one `CachedQuota` can hold AND the id agrees with the token (see
|
|
1178
|
+
* `agreedWindowKind`). The prefix and the window were two independent decisions
|
|
1179
|
+
* once, and an unrecognized window still claimed a family.
|
|
1094
1180
|
*/
|
|
1095
|
-
function familiesForBucketId(bucketId) {
|
|
1181
|
+
function familiesForBucketId(bucketId, window) {
|
|
1096
1182
|
const id = bucketId.toLowerCase();
|
|
1097
|
-
|
|
1098
|
-
if (id.startsWith("
|
|
1099
|
-
if (id.startsWith("
|
|
1100
|
-
if (id.startsWith("
|
|
1101
|
-
|
|
1183
|
+
const families = [];
|
|
1184
|
+
if (id.startsWith("gemini-")) families.push("google");
|
|
1185
|
+
else if (id.startsWith("3p-") || id.startsWith("third_party-")) families.push("anthropic", "openai");
|
|
1186
|
+
else if (id.startsWith("claude-")) families.push("anthropic");
|
|
1187
|
+
else if (id.startsWith("gpt-") || id.startsWith("openai-")) families.push("openai");
|
|
1188
|
+
if (families.length === 0) return [];
|
|
1189
|
+
const declared = window === void 0 ? advertisedWindowOf(id) : window.toLowerCase();
|
|
1190
|
+
if (declared === void 0 || agreedWindowKind(id, declared) === void 0) return [];
|
|
1191
|
+
return families;
|
|
1102
1192
|
}
|
|
1103
1193
|
/**
|
|
1104
1194
|
* The families a group's LABEL covers, consulted only when no bucket id was
|
|
@@ -1124,7 +1214,6 @@ function familiesForGroupName(name) {
|
|
|
1124
1214
|
if (claude && gpt) families.push("anthropic", "openai");
|
|
1125
1215
|
else if (claude) families.push("anthropic");
|
|
1126
1216
|
else if (gpt) families.push("openai");
|
|
1127
|
-
else if (families.length === 0 && (label.includes("3p") || label.includes("third"))) families.push("anthropic", "openai");
|
|
1128
1217
|
return families;
|
|
1129
1218
|
}
|
|
1130
1219
|
/**
|
|
@@ -1145,13 +1234,44 @@ function familiesForGroupName(name) {
|
|
|
1145
1234
|
function windowKind(window) {
|
|
1146
1235
|
return QUOTA_WINDOWS[window.toLowerCase()]?.kind;
|
|
1147
1236
|
}
|
|
1237
|
+
/** The window token a bucket id advertises, when it ends with one upstream names. */
|
|
1238
|
+
function advertisedWindowOf(bucketId) {
|
|
1239
|
+
const id = bucketId.toLowerCase();
|
|
1240
|
+
const token = id.slice(id.lastIndexOf("-") + 1);
|
|
1241
|
+
return QUOTA_WINDOWS[token] ? token : void 0;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* The field a window belongs in, decided from an AGREED (id, token) pair — or
|
|
1245
|
+
* `undefined` when the pair cannot be agreed.
|
|
1246
|
+
*
|
|
1247
|
+
* The family used to come from the bucket id's prefix while the field came from
|
|
1248
|
+
* the window token, as two independent decisions nothing checked against each
|
|
1249
|
+
* other. A bucket whose window the cache cannot hold still claimed the family,
|
|
1250
|
+
* and its reading was then filed by whichever field the TOKEN named. `gemini-weekly`
|
|
1251
|
+
* declaring `5h` is the shape that puts a multi-day weekly fraction into
|
|
1252
|
+
* `remainingFraction` (the 5-hour field), where `isFamilyDrained` and
|
|
1253
|
+
* `parseFutureResetMs` block the account for days on a window they misread.
|
|
1254
|
+
*
|
|
1255
|
+
* Refusing an unagreed pair is deliberately the strict option: the permissive
|
|
1256
|
+
* mapping is exactly the coupling that let two window vocabularies drift into
|
|
1257
|
+
* destroying a measurement. A bucket id that advertises no recognizable token is
|
|
1258
|
+
* held to the declared one alone, so a rename does not silently disable the cache.
|
|
1259
|
+
*/
|
|
1260
|
+
function agreedWindowKind(bucketId, window) {
|
|
1261
|
+
const declared = window.toLowerCase();
|
|
1262
|
+
const kind = windowKind(declared);
|
|
1263
|
+
if (kind === void 0) return void 0;
|
|
1264
|
+
const advertised = advertisedWindowOf(bucketId);
|
|
1265
|
+
if (advertised !== void 0 && advertised !== declared) return void 0;
|
|
1266
|
+
return kind;
|
|
1267
|
+
}
|
|
1148
1268
|
/** The families one group belongs to: its bucket ids first, its label second. */
|
|
1149
1269
|
function familiesForGroup(group) {
|
|
1150
1270
|
const families = [];
|
|
1151
1271
|
const add = (family) => {
|
|
1152
1272
|
if (!families.includes(family)) families.push(family);
|
|
1153
1273
|
};
|
|
1154
|
-
for (const window of group.windows) for (const family of familiesForBucketId(window.bucketId)) add(family);
|
|
1274
|
+
for (const window of group.windows) for (const family of familiesForBucketId(window.bucketId, window.window)) add(family);
|
|
1155
1275
|
if (families.length === 0) for (const family of familiesForGroupName(group.name)) add(family);
|
|
1156
1276
|
return families;
|
|
1157
1277
|
}
|
|
@@ -1163,9 +1283,16 @@ function familiesForGroup(group) {
|
|
|
1163
1283
|
* `remainingFraction` and `resetTime`, with no window field at all, which is why
|
|
1164
1284
|
* a weekly limit used to be invisible to rotation.
|
|
1165
1285
|
*
|
|
1166
|
-
* Two groups can land on one family (upstream may split `3p` later), so
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1286
|
+
* Two groups can land on one family (upstream may split `3p` later), so the
|
|
1287
|
+
* family record keeps the MOST pressured reading of each window across those
|
|
1288
|
+
* groups (`Math.min` below) rather than letting the last group win — the same
|
|
1289
|
+
* rule the per-model merge uses.
|
|
1290
|
+
*
|
|
1291
|
+
* WITHIN one group the rule is deliberately different: `??=` keeps upstream's
|
|
1292
|
+
* FIRST bucket of each window, because a group is upstream's own 5h + weekly
|
|
1293
|
+
* pair and a duplicate would otherwise depend on upstream's ordering. "Most
|
|
1294
|
+
* pressured" therefore applies to two GROUPS landing on one family, not to two
|
|
1295
|
+
* buckets inside one group.
|
|
1169
1296
|
*/
|
|
1170
1297
|
function ingestQuotaGroups(groups) {
|
|
1171
1298
|
const families = /* @__PURE__ */ new Map();
|
|
@@ -1175,7 +1302,7 @@ function ingestQuotaGroups(groups) {
|
|
|
1175
1302
|
let rolling;
|
|
1176
1303
|
let weekly;
|
|
1177
1304
|
for (const window of group.windows) {
|
|
1178
|
-
const kind =
|
|
1305
|
+
const kind = agreedWindowKind(window.bucketId, window.window);
|
|
1179
1306
|
if (kind === "rolling") rolling ??= window;
|
|
1180
1307
|
else if (kind === "weekly") weekly ??= window;
|
|
1181
1308
|
}
|
|
@@ -1248,21 +1375,70 @@ function ingestFamilyQuotas(discovered, groups, previous) {
|
|
|
1248
1375
|
}
|
|
1249
1376
|
return Object.fromEntries(families);
|
|
1250
1377
|
}
|
|
1251
|
-
/**
|
|
1252
|
-
|
|
1378
|
+
/**
|
|
1379
|
+
* The lowest fraction a record reports across its LIVE windows — its overall
|
|
1380
|
+
* pressure.
|
|
1381
|
+
*
|
|
1382
|
+
* A window whose reset has already passed describes a window that no longer
|
|
1383
|
+
* exists, so it does not count — the same rule `isQuotaStale` applies when it
|
|
1384
|
+
* sizes the TTL from the selected record. Scoring a dead window would let one
|
|
1385
|
+
* family's expired weekly reading win the selection and represent the account,
|
|
1386
|
+
* masking another family's live pressure (issue #69: a dead weekly at 0.004
|
|
1387
|
+
* beat a live 0.2 and stretched the refresh TTL from 5 to 15 minutes).
|
|
1388
|
+
*
|
|
1389
|
+
* `undefined` means the record carries no usable LIVE reading at all, which is
|
|
1390
|
+
* deliberately different from a measured zero: only a real number can size a
|
|
1391
|
+
* refresh interval.
|
|
1392
|
+
*/
|
|
1393
|
+
function pressureScore(entry, now) {
|
|
1394
|
+
const fractions = [];
|
|
1395
|
+
if (typeof entry.remainingFraction === "number" && Number.isFinite(entry.remainingFraction) && !resetInPast(entry.resetTime, now)) fractions.push(entry.remainingFraction);
|
|
1396
|
+
if (typeof entry.weeklyFraction === "number" && Number.isFinite(entry.weeklyFraction) && !resetInPast(entry.weeklyResetTime, now)) fractions.push(entry.weeklyFraction);
|
|
1397
|
+
return fractions.length === 0 ? void 0 : Math.min(...fractions);
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* The quota record for one family, or the most-pressured family when the model
|
|
1401
|
+
* is unknown.
|
|
1402
|
+
*
|
|
1403
|
+
* "Most pressured" compares BOTH windows and reports the record whose LOWEST
|
|
1404
|
+
* window is the lowest of all of them. That matters because `isQuotaStale` sizes
|
|
1405
|
+
* the refresh TTL from whichever record this returns: a family at 0.9 / weekly
|
|
1406
|
+
* 0.004 is more pressured than one at 0.2, and comparing the 5-hour fraction
|
|
1407
|
+
* alone picked the 0.2 record — so the weekly-driven 60s TTL was never applied
|
|
1408
|
+
* to the family actually being requested and a spent week was discovered late.
|
|
1409
|
+
*
|
|
1410
|
+
* A record carrying ONLY a weekly reading participates for the same reason: #48
|
|
1411
|
+
* produces that shape legitimately (the anthropic/openai entries of a
|
|
1412
|
+
* Gemini-only pool), and the old `typeof entry.remainingFraction !== 'number'`
|
|
1413
|
+
* guard skipped it outright, leaving the week invisible to TTL sizing.
|
|
1414
|
+
*
|
|
1415
|
+
* Selection considers LIVE windows only (`pressureScore` drops a window whose
|
|
1416
|
+
* reset has passed) — but the with-family path returns the record as-is either
|
|
1417
|
+
* way, because its consumers guard each window individually.
|
|
1418
|
+
*
|
|
1419
|
+
* @param account - the account whose cache to read.
|
|
1420
|
+
* @param family - the exact family's record; omit to select the most pressured.
|
|
1421
|
+
* @param now - current time (Unix ms), which decides whether a window is live.
|
|
1422
|
+
*/
|
|
1423
|
+
function familyQuotaFor(account, family, now = Date.now()) {
|
|
1253
1424
|
const cache = account.cachedQuota ?? {};
|
|
1254
1425
|
if (family) return cache[family];
|
|
1255
1426
|
let worst;
|
|
1427
|
+
let worstScore;
|
|
1256
1428
|
for (const entry of Object.values(cache)) {
|
|
1257
|
-
|
|
1258
|
-
if (
|
|
1429
|
+
const score = pressureScore(entry, now);
|
|
1430
|
+
if (score === void 0) continue;
|
|
1431
|
+
if (worstScore === void 0 || score < worstScore) {
|
|
1432
|
+
worst = entry;
|
|
1433
|
+
worstScore = score;
|
|
1434
|
+
}
|
|
1259
1435
|
}
|
|
1260
1436
|
return worst;
|
|
1261
1437
|
}
|
|
1262
1438
|
/** Whether the account's quota cache needs a refresh (missing, or past its health-based TTL). */
|
|
1263
1439
|
function isQuotaStale(account, now = Date.now()) {
|
|
1264
1440
|
if (!account.cachedQuota || !account.cachedQuotaUpdatedAt) return true;
|
|
1265
|
-
const mostPressured = familyQuotaFor(account);
|
|
1441
|
+
const mostPressured = familyQuotaFor(account, void 0, now);
|
|
1266
1442
|
const ttl = computeSoftQuotaCacheTtlMs(resetInPast(mostPressured?.resetTime, now) ? void 0 : mostPressured?.remainingFraction, resetInPast(mostPressured?.weeklyResetTime, now) ? void 0 : mostPressured?.weeklyFraction);
|
|
1267
1443
|
return now - account.cachedQuotaUpdatedAt > ttl;
|
|
1268
1444
|
}
|
|
@@ -1296,26 +1472,6 @@ function resetInPast(resetTime, now) {
|
|
|
1296
1472
|
return !Number.isNaN(reset) && reset <= now;
|
|
1297
1473
|
}
|
|
1298
1474
|
/**
|
|
1299
|
-
* Whether the requested family on this account is soft-quota-exhausted.
|
|
1300
|
-
*
|
|
1301
|
-
* BOTH windows are checked, because they refill on different clocks. The 5-hour
|
|
1302
|
-
* bucket can be nearly empty while the weekly budget is untouched, and the
|
|
1303
|
-
* reverse — a spent weekly budget stays spent across four 5-hour refills. Before
|
|
1304
|
-
* this, only the 5-hour reading was consulted, so an account whose week was over
|
|
1305
|
-
* kept being selected until a real request failed.
|
|
1306
|
-
*
|
|
1307
|
-
* A window whose reset has already passed is IGNORED rather than read: its
|
|
1308
|
-
* fraction describes a window that no longer exists, so the account stays
|
|
1309
|
-
* selectable until the next measurement replaces the stale value.
|
|
1310
|
-
*/
|
|
1311
|
-
function isFamilyDrained(account, family, now = Date.now()) {
|
|
1312
|
-
const quota = familyQuotaFor(account, family);
|
|
1313
|
-
if (!quota) return false;
|
|
1314
|
-
if (typeof quota.remainingFraction === "number" && !resetInPast(quota.resetTime, now) && quota.remainingFraction < .15) return true;
|
|
1315
|
-
if (typeof quota.weeklyFraction === "number" && !resetInPast(quota.weeklyResetTime, now) && quota.weeklyFraction <= .01) return true;
|
|
1316
|
-
return false;
|
|
1317
|
-
}
|
|
1318
|
-
/**
|
|
1319
1475
|
* Required drain rate: headroomFraction / remainingHours — how fast the
|
|
1320
1476
|
* family's remaining quota must be consumed to avoid expiring unused at its
|
|
1321
1477
|
* reset (mirrors AuthStorage.#computeWindowRequiredDrain with a daily window).
|
|
@@ -1350,6 +1506,32 @@ function parseFutureResetMs(resetTime, now) {
|
|
|
1350
1506
|
return reset;
|
|
1351
1507
|
}
|
|
1352
1508
|
/**
|
|
1509
|
+
* Whether the requested family on this account is effectively quota-exhausted
|
|
1510
|
+
* for account STEERING (affinity pins, the active-account preference): an
|
|
1511
|
+
* explicit quota-exhausted cooldown in force, a spent 5-hour window
|
|
1512
|
+
* (`remainingFraction <= 0` with a future reset), or a weekly window at or
|
|
1513
|
+
* below `WEEKLY_QUOTA_THRESHOLD` — a spent week refills in days, so its rule
|
|
1514
|
+
* mirrors `isFamilyDrained`'s weekly branch rather than the block wall.
|
|
1515
|
+
*
|
|
1516
|
+
* Deliberately a DIFFERENT predicate from the one in `pickAccount`'s `!picked`
|
|
1517
|
+
* branch, which applies a strict zero to BOTH windows to mirror
|
|
1518
|
+
* `rankPoolCandidates`' block wall when classifying `AgyPoolBlockedError`; and
|
|
1519
|
+
* deliberately NARROWER than `isFamilyDrained`'s 5-hour rule
|
|
1520
|
+
* (`SOFT_QUOTA_THRESHOLD`): account prioritization holds a chosen account
|
|
1521
|
+
* until it is actually spent, not until ranking would prefer another one.
|
|
1522
|
+
*/
|
|
1523
|
+
function isFamilyQuotaExhausted(account, family, now = Date.now()) {
|
|
1524
|
+
if (account.cooldownReason === "quota-exhausted" && (account.coolingDownUntil ?? 0) > now) return true;
|
|
1525
|
+
const quota = familyQuotaFor(account, family);
|
|
1526
|
+
if (!quota) return false;
|
|
1527
|
+
const spent = (fraction, resetTime, threshold = 0) => {
|
|
1528
|
+
if (typeof fraction !== "number" || fraction > threshold || !resetTime) return false;
|
|
1529
|
+
const resetMs = parseFutureResetMs(resetTime, now);
|
|
1530
|
+
return resetMs !== void 0 && resetMs > now;
|
|
1531
|
+
};
|
|
1532
|
+
return spent(quota.remainingFraction, quota.resetTime, 0) || spent(quota.weeklyFraction, quota.weeklyResetTime, .01);
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1353
1535
|
* Rank pool candidates for one request, mirroring AuthStorage's antigravity
|
|
1354
1536
|
* ordering: unblocked first (earliest unblock time among blocked), hot windows
|
|
1355
1537
|
* last, measured usage before unmeasured, required-drain descending, then
|
|
@@ -1361,10 +1543,12 @@ function rankPoolCandidates(entries, modelId, now = Date.now(), startIndex = 0)
|
|
|
1361
1543
|
const activePos = entries.findIndex((e) => e.index === startIndex);
|
|
1362
1544
|
const clampedStart = activePos >= 0 ? activePos : 0;
|
|
1363
1545
|
const candidates = (entries.length === 0 ? [] : [...entries.slice(clampedStart), ...entries.slice(0, clampedStart)]).map(({ account, index }, orderPos) => {
|
|
1364
|
-
const quota = familyQuotaFor(account, family);
|
|
1365
|
-
const
|
|
1546
|
+
const quota = familyQuotaFor(account, family, now);
|
|
1547
|
+
const live5h = quota !== void 0 && !resetInPast(quota.resetTime, now);
|
|
1548
|
+
const liveWeekly = quota !== void 0 && !resetInPast(quota.weeklyResetTime, now);
|
|
1549
|
+
const remaining = live5h ? quota.remainingFraction : void 0;
|
|
1366
1550
|
const used = typeof remaining === "number" ? Math.min(Math.max(1 - remaining, 0), 1) : void 0;
|
|
1367
|
-
const weeklyRemaining = quota
|
|
1551
|
+
const weeklyRemaining = liveWeekly ? quota.weeklyFraction : void 0;
|
|
1368
1552
|
const weeklyUsed = typeof weeklyRemaining === "number" ? Math.min(Math.max(1 - weeklyRemaining, 0), 1) : void 0;
|
|
1369
1553
|
let blockedUntil = null;
|
|
1370
1554
|
if (account.coolingDownUntil && account.coolingDownUntil > now) blockedUntil = account.coolingDownUntil;
|
|
@@ -1436,18 +1620,17 @@ const DEFAULT_FINGERPRINT_DATA = {
|
|
|
1436
1620
|
],
|
|
1437
1621
|
ideTypes: ["ANTIGRAVITY"]
|
|
1438
1622
|
};
|
|
1439
|
-
const USER_OVERRIDE_FILE = "agy-fingerprint-data.json";
|
|
1440
1623
|
/**
|
|
1441
|
-
* Effective fingerprint data: a user override at `$DSH_HOME/agy-fingerprint-data.json`
|
|
1624
|
+
* Effective fingerprint data: a user override at `$DSH_HOME/agy/agy-fingerprint-data.json`
|
|
1442
1625
|
* wins when present and parseable (hot-updatable without a code release — the
|
|
1443
|
-
* bundled copy is compiled in), otherwise the bundled defaults.
|
|
1626
|
+
* bundled copy is compiled in), otherwise the bundled defaults. The legacy
|
|
1627
|
+
* `$DSH_HOME/agy-fingerprint-data.json` location is migrated once by rename.
|
|
1444
1628
|
*/
|
|
1445
1629
|
let cachedData = null;
|
|
1446
1630
|
function getFingerprintData() {
|
|
1447
1631
|
if (cachedData) return cachedData;
|
|
1448
1632
|
try {
|
|
1449
|
-
const
|
|
1450
|
-
const overrideFile = join(dshHome, USER_OVERRIDE_FILE);
|
|
1633
|
+
const overrideFile = migrateToAgyDir("agy-fingerprint-data.json", process.env.DSH_HOME ? process.env.DSH_HOME : join(process.env.HOME ?? ".", ".dsh")).file;
|
|
1451
1634
|
if (existsSync(overrideFile)) {
|
|
1452
1635
|
const parsed = JSON.parse(readFileSync(overrideFile, "utf8"));
|
|
1453
1636
|
if (parsed && Array.isArray(parsed.versionPool) && parsed.versionPool.length > 0) {
|
|
@@ -1545,6 +1728,30 @@ function fingerprintMode() {
|
|
|
1545
1728
|
return process.env.DSH_AGY_FINGERPRINT_MODE === "stable" ? "stable" : "dynamic";
|
|
1546
1729
|
}
|
|
1547
1730
|
/**
|
|
1731
|
+
* Fraction-per-hour per window bucket, from two snapshots.
|
|
1732
|
+
*
|
|
1733
|
+
* Pure and exported for a direct unit test. Rules:
|
|
1734
|
+
* - the FIRST sample yields nothing (no baseline yet), and so does a pair
|
|
1735
|
+
* closer than `MIN_BURN_SAMPLE_MS` — two probes seconds apart measure the
|
|
1736
|
+
* probe jitter, not the burn;
|
|
1737
|
+
* - a RISING fraction is a reset (or refresh noise), never negative burn;
|
|
1738
|
+
* - a bucket with no baseline simply has no rate yet.
|
|
1739
|
+
*
|
|
1740
|
+
* Returns null when no bucket produced a rate, so the caller can keep the
|
|
1741
|
+
* previous rates instead of overwriting them with an empty read.
|
|
1742
|
+
*/
|
|
1743
|
+
function sampleLimitBurn(prev, next) {
|
|
1744
|
+
if (prev === void 0) return null;
|
|
1745
|
+
const elapsedH = (next.at - prev.at) / 36e5;
|
|
1746
|
+
if (elapsedH < 3e5 / 36e5) return null;
|
|
1747
|
+
const perHour = {};
|
|
1748
|
+
for (const [bucketId, fraction] of Object.entries(next.fractions)) {
|
|
1749
|
+
const before = prev.fractions[bucketId];
|
|
1750
|
+
if (before !== void 0 && before > fraction) perHour[bucketId] = (before - fraction) / elapsedH;
|
|
1751
|
+
}
|
|
1752
|
+
return Object.keys(perHour).length > 0 ? perHour : null;
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1548
1755
|
* Resolve the impersonation headers for one request from the account's
|
|
1549
1756
|
* persistent fingerprint (stable identity).
|
|
1550
1757
|
*
|
|
@@ -1553,7 +1760,7 @@ function fingerprintMode() {
|
|
|
1553
1760
|
* deliberately DETERMINISTIC rather than randomized: a per-request platform or
|
|
1554
1761
|
* version would make one account appear to be several different machines, which
|
|
1555
1762
|
* is the anomaly this identity exists to avoid. It reads
|
|
1556
|
-
* {@link getFingerprintData} so a `$DSH_HOME/agy-fingerprint-data.json` override
|
|
1763
|
+
* {@link getFingerprintData} so a `$DSH_HOME/agy/agy-fingerprint-data.json` override
|
|
1557
1764
|
* still applies on this path.
|
|
1558
1765
|
*/
|
|
1559
1766
|
function impersonationHeadersFor(account) {
|
|
@@ -1575,6 +1782,22 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1575
1782
|
refreshInFlight = /* @__PURE__ */ new Map();
|
|
1576
1783
|
/** In-flight quota fetches keyed by account: concurrent selections share one fetchAvailableModels call. */
|
|
1577
1784
|
quotaRefreshInFlight = /* @__PURE__ */ new Map();
|
|
1785
|
+
/**
|
|
1786
|
+
* In-flight `retrieveUserQuotaSummary` probes keyed by account.
|
|
1787
|
+
*
|
|
1788
|
+
* ONE promise per account per cycle, shared by BOTH consumers:
|
|
1789
|
+
* `refreshLimits` (display, writes `cachedLimits`) and `refreshQuotaCache`
|
|
1790
|
+
* (scheduling, writes `cachedQuota`). Since #48 both read the same endpoint for
|
|
1791
|
+
* the same account, so a stale account paid two round trips where one would
|
|
1792
|
+
* do; `quotaRefreshInFlight` de-duplicated only WITHIN the scheduling path.
|
|
1793
|
+
* Sharing makes the two caches agree on what upstream actually said.
|
|
1794
|
+
*
|
|
1795
|
+
* It carries the FETCH and nothing else: the invariant that `refreshLimits`
|
|
1796
|
+
* must never write `cachedQuota` (which `rankPoolCandidates` turns into a
|
|
1797
|
+
* `blockedUntil`) is about WHERE a result lands, and each path still decides
|
|
1798
|
+
* that for itself.
|
|
1799
|
+
*/
|
|
1800
|
+
quotaSummaryInFlight = /* @__PURE__ */ new Map();
|
|
1578
1801
|
failureCounts = /* @__PURE__ */ new Map();
|
|
1579
1802
|
/** Accounts whose request-time project discovery already failed (no retry per request). */
|
|
1580
1803
|
projectRetryFailed = /* @__PURE__ */ new Set();
|
|
@@ -1642,6 +1865,62 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1642
1865
|
at: now
|
|
1643
1866
|
});
|
|
1644
1867
|
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Which accounts have upstream requests in flight, for the UI's live line.
|
|
1870
|
+
*
|
|
1871
|
+
* The join runs HERE, not in the RPC layer: the in-flight map is keyed by
|
|
1872
|
+
* `accountKey` (id ?? email ?? idx-<refresh>), a derivation the management
|
|
1873
|
+
* surface must not have to replicate — its own email-first key would miss
|
|
1874
|
+
* every account carrying both fields. Counts go through `inFlightCount`, so
|
|
1875
|
+
* an entry leaked past its TTL reads as idle instead of busy forever.
|
|
1876
|
+
*/
|
|
1877
|
+
async inFlightAccounts() {
|
|
1878
|
+
const storage = await this.store.load();
|
|
1879
|
+
const now = Date.now();
|
|
1880
|
+
const busy = [];
|
|
1881
|
+
for (const [index, account] of storage.accounts.entries()) {
|
|
1882
|
+
const count = this.inFlightCount(this.accountKey(account), now);
|
|
1883
|
+
if (count > 0) busy.push({
|
|
1884
|
+
index,
|
|
1885
|
+
email: account.email ?? null,
|
|
1886
|
+
count
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
return busy;
|
|
1890
|
+
}
|
|
1891
|
+
/** Last fraction snapshot per account, for `sampleLimitBurn` (memory only). */
|
|
1892
|
+
limitFractions = /* @__PURE__ */ new Map();
|
|
1893
|
+
/** Latest burn rate per account: bucketId -> fraction/hour (memory only). */
|
|
1894
|
+
limitBurn = /* @__PURE__ */ new Map();
|
|
1895
|
+
noteLimitSample(key, groups, at) {
|
|
1896
|
+
const fractions = {};
|
|
1897
|
+
for (const group of groups) for (const window of group.windows) if (window.remainingFraction !== null) fractions[window.bucketId] = window.remainingFraction;
|
|
1898
|
+
const next = {
|
|
1899
|
+
at,
|
|
1900
|
+
fractions
|
|
1901
|
+
};
|
|
1902
|
+
const burn = sampleLimitBurn(this.limitFractions.get(key), next);
|
|
1903
|
+
if (burn !== null) this.limitBurn.set(key, { perHour: burn });
|
|
1904
|
+
this.limitFractions.set(key, next);
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Burn rates per account INDEX, for the `account.limits` reply.
|
|
1908
|
+
*
|
|
1909
|
+
* The index join runs here, like `inFlightAccounts`: the rate map is keyed
|
|
1910
|
+
* by `accountKey`, a derivation the management surface must not replicate.
|
|
1911
|
+
*/
|
|
1912
|
+
async limitBurnRates() {
|
|
1913
|
+
const storage = await this.store.load();
|
|
1914
|
+
const out = [];
|
|
1915
|
+
for (const [index, account] of storage.accounts.entries()) {
|
|
1916
|
+
const entry = this.limitBurn.get(this.accountKey(account));
|
|
1917
|
+
if (entry !== void 0) out.push({
|
|
1918
|
+
index,
|
|
1919
|
+
perHour: entry.perHour
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
return out;
|
|
1923
|
+
}
|
|
1645
1924
|
/** The map key for a conversation; anonymous callers share one bucket. */
|
|
1646
1925
|
conversationKeyFor(conversationKey) {
|
|
1647
1926
|
const trimmed = conversationKey?.trim();
|
|
@@ -1682,6 +1961,28 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1682
1961
|
clearAffinityForAccount(accountKey) {
|
|
1683
1962
|
for (const [conversation, pin] of this.affinity) if (pin.key === accountKey) this.affinity.delete(conversation);
|
|
1684
1963
|
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Clear all session affinity pins.
|
|
1966
|
+
*
|
|
1967
|
+
* Called when an account is explicitly activated by the user, so all
|
|
1968
|
+
* ongoing and upcoming conversations immediately steer to the newly
|
|
1969
|
+
* activated account rather than staying pinned to the previous one.
|
|
1970
|
+
*/
|
|
1971
|
+
clearAllAffinity() {
|
|
1972
|
+
this.affinity.clear();
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Explicitly activate an account by index: sets `storage.activeIndex = index`,
|
|
1976
|
+
* clears session affinity pins so conversations immediately switch to the
|
|
1977
|
+
* activated account, and resets transient failure counts.
|
|
1978
|
+
*/
|
|
1979
|
+
async activateAccount(index) {
|
|
1980
|
+
await this.store.mutate((storage) => {
|
|
1981
|
+
if (index < 0 || index >= storage.accounts.length) throw new Error("account not found");
|
|
1982
|
+
storage.activeIndex = index;
|
|
1983
|
+
});
|
|
1984
|
+
this.clearAllAffinity();
|
|
1985
|
+
}
|
|
1685
1986
|
constructor(options) {
|
|
1686
1987
|
this.store = options.store;
|
|
1687
1988
|
this.onRotate = options.onRotate;
|
|
@@ -1786,6 +2087,49 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1786
2087
|
return this.refreshToken(key, account, cached);
|
|
1787
2088
|
}
|
|
1788
2089
|
/**
|
|
2090
|
+
* Probe `retrieveUserQuotaSummary` for one account, sharing ONE in-flight
|
|
2091
|
+
* promise with every other caller in this cycle.
|
|
2092
|
+
*
|
|
2093
|
+
* Returns `[]` on any failure — the fetch itself already answers a dead
|
|
2094
|
+
* endpoint with an empty list — so both consumers read "no groups" the same
|
|
2095
|
+
* way. The promise is dropped as soon as it settles, so the NEXT cycle
|
|
2096
|
+
* re-probes.
|
|
2097
|
+
*
|
|
2098
|
+
* The account's proxy is honoured here or the call would egress direct and
|
|
2099
|
+
* leak the account's real IP, and one timeout bounds the whole endpoint
|
|
2100
|
+
* fallback chain.
|
|
2101
|
+
*/
|
|
2102
|
+
probeQuotaSummary(account) {
|
|
2103
|
+
const key = this.accountKey(account);
|
|
2104
|
+
const existing = this.quotaSummaryInFlight.get(key);
|
|
2105
|
+
if (existing) return existing;
|
|
2106
|
+
const probe = (async () => {
|
|
2107
|
+
try {
|
|
2108
|
+
const auth = await this.accessTokenFor(account);
|
|
2109
|
+
if (!auth) return [];
|
|
2110
|
+
const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
|
|
2111
|
+
const routed = accountFetch({ proxyUrl: account.proxy });
|
|
2112
|
+
const bounded = (input, init) => {
|
|
2113
|
+
const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
|
|
2114
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
2115
|
+
return routed(input, {
|
|
2116
|
+
...init,
|
|
2117
|
+
signal
|
|
2118
|
+
});
|
|
2119
|
+
};
|
|
2120
|
+
return await fetchQuotaSummary(auth.access, account.projectId, bounded);
|
|
2121
|
+
} catch {
|
|
2122
|
+
return [];
|
|
2123
|
+
}
|
|
2124
|
+
})();
|
|
2125
|
+
this.quotaSummaryInFlight.set(key, probe);
|
|
2126
|
+
const release = () => {
|
|
2127
|
+
if (this.quotaSummaryInFlight.get(key) === probe) this.quotaSummaryInFlight.delete(key);
|
|
2128
|
+
};
|
|
2129
|
+
probe.then(release, release);
|
|
2130
|
+
return probe;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
1789
2133
|
* Refresh the display-only 5h/weekly windows for every enabled account.
|
|
1790
2134
|
*
|
|
1791
2135
|
* SEPARATE from `refreshQuotaCache`, and deliberately so. That method writes
|
|
@@ -1833,22 +2177,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1833
2177
|
const probes = await Promise.all(targets.map(async (account) => {
|
|
1834
2178
|
const key = this.accountKey(account);
|
|
1835
2179
|
try {
|
|
1836
|
-
const
|
|
1837
|
-
if (!auth) return {
|
|
1838
|
-
key,
|
|
1839
|
-
ok: false
|
|
1840
|
-
};
|
|
1841
|
-
const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
|
|
1842
|
-
const routed = accountFetch({ proxyUrl: account.proxy });
|
|
1843
|
-
const bounded = (input, init) => {
|
|
1844
|
-
const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
|
|
1845
|
-
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
1846
|
-
return routed(input, {
|
|
1847
|
-
...init,
|
|
1848
|
-
signal
|
|
1849
|
-
});
|
|
1850
|
-
};
|
|
1851
|
-
const groups = await fetchQuotaSummary(auth.access, account.projectId, bounded);
|
|
2180
|
+
const groups = await this.probeQuotaSummary(account);
|
|
1852
2181
|
if (groups.length === 0) return {
|
|
1853
2182
|
key,
|
|
1854
2183
|
ok: false
|
|
@@ -1876,11 +2205,17 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1876
2205
|
if (updates.length === 0) return result;
|
|
1877
2206
|
for (const update of updates) {
|
|
1878
2207
|
const target = storage.accounts.find((candidate) => this.accountKey(candidate) === update.key);
|
|
1879
|
-
if (target)
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2208
|
+
if (target) {
|
|
2209
|
+
target.cachedLimits = {
|
|
2210
|
+
groups: update.groups,
|
|
2211
|
+
updatedAt: update.updatedAt
|
|
2212
|
+
};
|
|
2213
|
+
this.noteLimitSample(update.key, update.groups, update.updatedAt);
|
|
2214
|
+
}
|
|
1883
2215
|
}
|
|
2216
|
+
const live = new Set(storage.accounts.map((account) => this.accountKey(account)));
|
|
2217
|
+
for (const key of this.limitFractions.keys()) if (!live.has(key)) this.limitFractions.delete(key);
|
|
2218
|
+
for (const key of this.limitBurn.keys()) if (!live.has(key)) this.limitBurn.delete(key);
|
|
1884
2219
|
try {
|
|
1885
2220
|
await this.store.mutate((s) => {
|
|
1886
2221
|
for (const update of updates) {
|
|
@@ -1911,7 +2246,6 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1911
2246
|
const auth = await this.accessTokenFor(account);
|
|
1912
2247
|
if (!auth) return null;
|
|
1913
2248
|
const { fetchAvailableModels } = await import("./models-Qn0-sh6X.mjs").then((n) => n.r);
|
|
1914
|
-
const { fetchQuotaSummary } = await import("./quota-summary-adID82I2.mjs");
|
|
1915
2249
|
const routed = accountFetch({ proxyUrl: account.proxy });
|
|
1916
2250
|
const bounded = (input, init) => {
|
|
1917
2251
|
const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
|
|
@@ -1921,7 +2255,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1921
2255
|
signal
|
|
1922
2256
|
});
|
|
1923
2257
|
};
|
|
1924
|
-
const [discovered, groups] = await Promise.all([fetchAvailableModels(auth.access, account.projectId, bounded),
|
|
2258
|
+
const [discovered, groups] = await Promise.all([fetchAvailableModels(auth.access, account.projectId, bounded), this.probeQuotaSummary(account)]);
|
|
1925
2259
|
const quotas = ingestFamilyQuotas(discovered, groups, account.cachedQuota);
|
|
1926
2260
|
return {
|
|
1927
2261
|
key,
|
|
@@ -1962,7 +2296,8 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1962
2296
|
}
|
|
1963
2297
|
/**
|
|
1964
2298
|
* Pick the account for one request: the affinity pin wins while it is fresh,
|
|
1965
|
-
* healthy, and not
|
|
2299
|
+
* healthy, and not quota-exhausted for the requested model; the explicitly
|
|
2300
|
+
* activated account wins next while it is usable; otherwise the pool is
|
|
1966
2301
|
* ranked by family-scoped usage (OMP-aligned) and the best candidate wins.
|
|
1967
2302
|
*/
|
|
1968
2303
|
async pickAccount(storage, model, conversationKey) {
|
|
@@ -1975,7 +2310,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1975
2310
|
const lastIndex = storage.accounts.findIndex((a) => this.accountKey(a) === pinnedKey);
|
|
1976
2311
|
if (lastIndex !== -1) {
|
|
1977
2312
|
const last = storage.accounts[lastIndex];
|
|
1978
|
-
if (last.enabled !== false && !isCoolingDown(last, now) && !isFamilyRateLimited(last, familyKey, now) && !
|
|
2313
|
+
if (last.enabled !== false && !isCoolingDown(last, now) && !isFamilyRateLimited(last, familyKey, now) && !isFamilyQuotaExhausted(last, family, now)) return {
|
|
1979
2314
|
account: last,
|
|
1980
2315
|
index: lastIndex
|
|
1981
2316
|
};
|
|
@@ -1986,12 +2321,18 @@ var AgySessionManager = class AgySessionManager {
|
|
|
1986
2321
|
index
|
|
1987
2322
|
})).filter(({ account }) => account.enabled !== false);
|
|
1988
2323
|
if (eligible.length === 0) return void 0;
|
|
2324
|
+
const active = storage.accounts[storage.activeIndex];
|
|
2325
|
+
const isActiveUsable = (acc) => acc !== void 0 && acc.enabled !== false && !isCoolingDown(acc, now) && !isFamilyRateLimited(acc, familyKey, now) && !isFamilyQuotaExhausted(acc, family, now);
|
|
2326
|
+
if (isActiveUsable(active) && this.inFlightCount(this.accountKey(active), now) < 3) return {
|
|
2327
|
+
account: active,
|
|
2328
|
+
index: storage.activeIndex
|
|
2329
|
+
};
|
|
1989
2330
|
const ranked = rankPoolCandidates(eligible, model, now, storage.activeIndex);
|
|
1990
2331
|
const picked = ranked.find((candidate) => candidate.blockedUntil === null && this.inFlightCount(this.accountKey(candidate.account), now) < 3) ?? ranked.find((candidate) => candidate.blockedUntil === null);
|
|
1991
2332
|
if (!picked) {
|
|
1992
2333
|
const quotaExhausted = (account) => {
|
|
1993
2334
|
if (account.cooldownReason === "quota-exhausted" && (account.coolingDownUntil ?? 0) > now) return true;
|
|
1994
|
-
const quota = familyQuotaFor(account, family);
|
|
2335
|
+
const quota = familyQuotaFor(account, family, now);
|
|
1995
2336
|
if (!quota) return false;
|
|
1996
2337
|
const spent = (fraction, resetTime) => {
|
|
1997
2338
|
if (typeof fraction !== "number" || fraction > 0 || !resetTime) return false;
|
|
@@ -2168,6 +2509,13 @@ var AgySessionManager = class AgySessionManager {
|
|
|
2168
2509
|
if (decision.action === "revoke") {
|
|
2169
2510
|
this.tokenCache.delete(key);
|
|
2170
2511
|
this.failureCounts.delete(key);
|
|
2512
|
+
const currentIndex = storage.accounts.findIndex((a) => this.accountKey(a) === key);
|
|
2513
|
+
const nextIndex = pickNextAccountIndex(storage.accounts, currentIndex >= 0 ? currentIndex : storage.activeIndex, Date.now());
|
|
2514
|
+
if (nextIndex !== storage.activeIndex) {
|
|
2515
|
+
storage.activeIndex = nextIndex;
|
|
2516
|
+
nextIndexToRotate = nextIndex;
|
|
2517
|
+
}
|
|
2518
|
+
this.clearAffinityForAccount(key);
|
|
2171
2519
|
return;
|
|
2172
2520
|
}
|
|
2173
2521
|
if (kind === "rate-limit" && info?.rateLimitCategory !== "soft_rate_limit") {
|
|
@@ -2183,7 +2531,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
2183
2531
|
}
|
|
2184
2532
|
}
|
|
2185
2533
|
}
|
|
2186
|
-
if (decision.action === "rotate") {
|
|
2534
|
+
if (decision.action === "rotate" || decision.action === "cool") {
|
|
2187
2535
|
const currentIndex = storage.accounts.findIndex((a) => this.accountKey(a) === key);
|
|
2188
2536
|
const familyKey = familyKeyOf(info?.model);
|
|
2189
2537
|
const nextIndex = pickNextAccountIndex(storage.accounts, currentIndex >= 0 ? currentIndex : storage.activeIndex, Date.now(), familyKey);
|
|
@@ -2201,7 +2549,8 @@ var AgySessionManager = class AgySessionManager {
|
|
|
2201
2549
|
source: "chat",
|
|
2202
2550
|
ok: false,
|
|
2203
2551
|
rotated: true,
|
|
2204
|
-
poolEvent: true
|
|
2552
|
+
poolEvent: true,
|
|
2553
|
+
reason: kind
|
|
2205
2554
|
});
|
|
2206
2555
|
}
|
|
2207
2556
|
}
|
|
@@ -2241,7 +2590,7 @@ var AgySessionManager = class AgySessionManager {
|
|
|
2241
2590
|
};
|
|
2242
2591
|
try {
|
|
2243
2592
|
const account = session.account.email ?? session.account.id;
|
|
2244
|
-
const { toAgyRequestBody } = await import("./translate-
|
|
2593
|
+
const { toAgyRequestBody } = await import("./translate-YhGDUuVa.mjs").then((n) => n.n);
|
|
2245
2594
|
const { fetchAgyFirstOk } = await import("./constants-BpyRirvk.mjs").then((n) => n.f);
|
|
2246
2595
|
const { parseAgySse } = await import("./parse-Bs4rPkHA.mjs").then((n) => n.n);
|
|
2247
2596
|
const requestId = generateAntigravityRequestId();
|
|
@@ -2682,6 +3031,6 @@ var JsonAccountStore = class {
|
|
|
2682
3031
|
}
|
|
2683
3032
|
};
|
|
2684
3033
|
//#endregion
|
|
2685
|
-
export {
|
|
3034
|
+
export { isSessionAccumulationOverflow as S, resolveDshHome as _, generateFingerprint as a, classifyHttpError as b, resolveAntigravityVersionBounded as c, migrateToAgyDir as d, MASTER_KEY_REF as f, persistMasterKey as g, loadMasterKey as h, isAgyDisabled as i, clearExpiredState as l, deriveKey as m, maskProxyUrl as n, recordFingerprintVersion as o, createAesGcmCodec as p, AgySessionManager as r, resolveAntigravityVersion as s, JsonAccountStore as t, pickProbeProxyUrl as u, resolveMasterKeyCodec as v, describeFetchError as x, classifyFetchError as y };
|
|
2686
3035
|
|
|
2687
|
-
//# sourceMappingURL=accounts-
|
|
3036
|
+
//# sourceMappingURL=accounts-BjdDqUq1.mjs.map
|