switchroom 0.15.2 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12363,6 +12363,47 @@ function resolveConsumerProbeIntervalMs(env) {
12363
12363
  return DEFAULT_CONSUMER_PROBE_INTERVAL_MS;
12364
12364
  }
12365
12365
 
12366
+ // src/auth/broker/account-eligibility.ts
12367
+ var WALL_PCT = 99.5;
12368
+ var HEALTHY_CLEAR_PCT = 80;
12369
+ var SNAPSHOT_STALE_AGE_MS = 24 * 60 * 60 * 1000;
12370
+ function snapshotFresh(s, now, maxAgeMs = SNAPSHOT_STALE_AGE_MS) {
12371
+ return !!s && now - s.capturedAt <= maxAgeMs && s.capturedAt <= now + 60000;
12372
+ }
12373
+ function snapshotWalled(s) {
12374
+ return s.fiveHourUtilizationPct >= WALL_PCT || s.sevenDayUtilizationPct >= WALL_PCT;
12375
+ }
12376
+ function snapshotClearlyHealthy(s) {
12377
+ return s.fiveHourUtilizationPct < HEALTHY_CLEAR_PCT && s.sevenDayUtilizationPct < HEALTHY_CLEAR_PCT;
12378
+ }
12379
+ function isAccountBlocked(opts) {
12380
+ const { mark, snapshot, now } = opts;
12381
+ if (snapshotFresh(snapshot, now)) {
12382
+ const markedAt = mark?.marked_at ?? 0;
12383
+ if (snapshot.capturedAt >= markedAt) {
12384
+ return snapshotWalled(snapshot);
12385
+ }
12386
+ }
12387
+ return mark !== undefined && mark.exhausted_until > now;
12388
+ }
12389
+ function snapshotShouldClearMark(snapshot, mark, now) {
12390
+ if (!mark)
12391
+ return false;
12392
+ if (!snapshotFresh(snapshot, now))
12393
+ return false;
12394
+ if (snapshot.capturedAt < (mark.marked_at ?? 0))
12395
+ return false;
12396
+ return snapshotClearlyHealthy(snapshot);
12397
+ }
12398
+ function clampMarkExpiry(opts) {
12399
+ const { proposedUntil, now, shortMs, snapshot } = opts;
12400
+ const shortCeil = now + shortMs;
12401
+ if (proposedUntil <= shortCeil)
12402
+ return proposedUntil;
12403
+ const liveContradictsWeeklyWall = snapshotFresh(snapshot, now) && snapshot.sevenDayUtilizationPct < WALL_PCT;
12404
+ return liveContradictsWeeklyWall ? shortCeil : proposedUntil;
12405
+ }
12406
+
12366
12407
  // src/util/atomic.ts
12367
12408
  import { randomBytes } from "node:crypto";
12368
12409
  import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
@@ -14030,8 +14071,11 @@ class AuthBroker {
14030
14071
  return this.accountWithFailover(account);
14031
14072
  }
14032
14073
  isAccountExhausted(account) {
14033
- const q = this.quota[account];
14034
- return q !== undefined && q.exhausted_until > this.now();
14074
+ return isAccountBlocked({
14075
+ mark: this.quota[account],
14076
+ snapshot: this.lastQuotaCache[account],
14077
+ now: this.now()
14078
+ });
14035
14079
  }
14036
14080
  accountWithFailover(account) {
14037
14081
  if (!account || !this.isAccountExhausted(account))
@@ -14067,7 +14111,7 @@ class AuthBroker {
14067
14111
  const creds = readAccountCredentials(label, this.home);
14068
14112
  const meta = readAccountMeta(label, this.home);
14069
14113
  const q = this.quota[label];
14070
- const exhausted = q !== undefined && q.exhausted_until > this.now();
14114
+ const exhausted = this.isAccountExhausted(label);
14071
14115
  const lq = this.lastQuotaCache[label];
14072
14116
  return {
14073
14117
  label,
@@ -14142,7 +14186,7 @@ class AuthBroker {
14142
14186
  cacheQuotaSnapshot(label, result) {
14143
14187
  if (!result.ok)
14144
14188
  return;
14145
- this.lastQuotaCache[label] = {
14189
+ const snapshot = {
14146
14190
  fiveHourUtilizationPct: result.data.fiveHourUtilizationPct,
14147
14191
  sevenDayUtilizationPct: result.data.sevenDayUtilizationPct,
14148
14192
  fiveHourResetAt: result.data.fiveHourResetAt?.toISOString() ?? null,
@@ -14152,6 +14196,13 @@ class AuthBroker {
14152
14196
  overageDisabledReason: result.data.overageDisabledReason,
14153
14197
  capturedAt: this.now()
14154
14198
  };
14199
+ this.lastQuotaCache[label] = snapshot;
14200
+ if (snapshotShouldClearMark(snapshot, this.quota[label], this.now())) {
14201
+ delete this.quota[label];
14202
+ this.persistQuota();
14203
+ process.stdout.write(`auth-broker: live probe shows ${label} healthy (5h=${snapshot.fiveHourUtilizationPct}% 7d=${snapshot.sevenDayUtilizationPct}%) — cleared stale exhaustion mark
14204
+ `);
14205
+ }
14155
14206
  }
14156
14207
  async fleetQuotaProbeTick() {
14157
14208
  for (const label of listAccounts(this.home)) {
@@ -14183,14 +14234,21 @@ class AuthBroker {
14183
14234
  this.logErr(`consumer-quota-probe ${label}: ${err.message}`);
14184
14235
  continue;
14185
14236
  }
14237
+ this.cacheQuotaSnapshot(label, result);
14186
14238
  const decision = quotaIndicatesExhaustion(result);
14187
14239
  if (!decision.exhausted)
14188
14240
  continue;
14189
- const exhaustedUntil = decision.until ?? this.now() + MARK_EXHAUSTED_DEFAULT_MS;
14241
+ const now = this.now();
14242
+ const exhaustedUntil = clampMarkExpiry({
14243
+ proposedUntil: decision.until ?? now + MARK_EXHAUSTED_DEFAULT_MS,
14244
+ now,
14245
+ shortMs: MARK_EXHAUSTED_DEFAULT_MS,
14246
+ snapshot: this.lastQuotaCache[label]
14247
+ });
14190
14248
  const existing = this.quota[label]?.exhausted_until;
14191
14249
  if (existing !== undefined && existing >= exhaustedUntil)
14192
14250
  continue;
14193
- this.quota[label] = { exhausted_until: exhaustedUntil };
14251
+ this.quota[label] = { exhausted_until: exhaustedUntil, marked_at: now };
14194
14252
  this.persistQuota();
14195
14253
  this.audit({ op: "mark-exhausted", identity: { kind: "operator" }, account: label, ok: true });
14196
14254
  process.stdout.write(`auth-broker: consumer-quota-sensor marked ${label} exhausted until ${new Date(exhaustedUntil).toISOString()} — consumer(s) fail over
@@ -14224,8 +14282,14 @@ class AuthBroker {
14224
14282
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", "no active account configured"));
14225
14283
  return;
14226
14284
  }
14227
- const exhaustedUntil = until ?? this.now() + MARK_EXHAUSTED_DEFAULT_MS;
14228
- this.quota[account] = { exhausted_until: exhaustedUntil };
14285
+ const now = this.now();
14286
+ const exhaustedUntil = clampMarkExpiry({
14287
+ proposedUntil: until ?? now + MARK_EXHAUSTED_DEFAULT_MS,
14288
+ now,
14289
+ shortMs: MARK_EXHAUSTED_DEFAULT_MS,
14290
+ snapshot: this.lastQuotaCache[account]
14291
+ });
14292
+ this.quota[account] = { exhausted_until: exhaustedUntil, marked_at: now };
14229
14293
  this.persistQuota();
14230
14294
  const rolled = this.fanoutFailoverFor(account);
14231
14295
  const rolledTo = this.nextHealthyAccount(account, this.config.auth?.fallback_order ?? []);
@@ -14752,9 +14816,7 @@ class AuthBroker {
14752
14816
  const cand = order[(start + i) % order.length];
14753
14817
  if (!cand)
14754
14818
  continue;
14755
- const q = this.quota[cand];
14756
- const exhausted = q !== undefined && q.exhausted_until > this.now();
14757
- if (!exhausted && accountExists(cand, this.home))
14819
+ if (!this.isAccountExhausted(cand) && accountExists(cand, this.home))
14758
14820
  return cand;
14759
14821
  }
14760
14822
  return null;