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.
- package/dist/auth-broker/index.js +73 -11
- package/dist/cli/switchroom.js +1448 -1415
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +33 -0
- package/profiles/default/CLAUDE.md.hbs +13 -4
- package/telegram-plugin/dist/gateway/gateway.js +461 -22
- package/telegram-plugin/gateway/gateway.ts +115 -13
- package/telegram-plugin/gateway/model-command.ts +193 -7
- package/telegram-plugin/tests/model-command.test.ts +144 -0
- package/telegram-plugin/uat/scenarios/jtbd-model-command-dm.test.ts +93 -0
|
@@ -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
|
-
|
|
14034
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
14228
|
-
|
|
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
|
-
|
|
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;
|