switchroom 0.19.3 → 0.19.4

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.
@@ -18839,6 +18839,27 @@ function parseQuotaHeaders(headers, now = new Date) {
18839
18839
  }
18840
18840
  };
18841
18841
  }
18842
+ function extractApiErrorMessage(bodyText) {
18843
+ if (!bodyText || bodyText.trim().length === 0)
18844
+ return null;
18845
+ try {
18846
+ const parsed = JSON.parse(bodyText);
18847
+ const msg = parsed?.error?.message;
18848
+ if (typeof msg === "string" && msg.trim().length > 0)
18849
+ return msg.trim();
18850
+ } catch {}
18851
+ return bodyText.trim().slice(0, 500);
18852
+ }
18853
+ function isEntitlementDisabledMessage(message) {
18854
+ if (!message)
18855
+ return false;
18856
+ const m = message.toLowerCase();
18857
+ if (!m.includes("disabled"))
18858
+ return false;
18859
+ const mentionsCode = m.includes("claude code") || m.includes("claude subscription");
18860
+ const mentionsOrgOrSub = m.includes("organization") || m.includes("subscription") || m.includes(" org ");
18861
+ return mentionsCode && mentionsOrgOrSub;
18862
+ }
18842
18863
  async function fetchQuota(opts) {
18843
18864
  const token = opts.accessToken?.trim();
18844
18865
  if (!token || token.length === 0) {
@@ -18874,13 +18895,28 @@ async function fetchQuota(opts) {
18874
18895
  }
18875
18896
  return { ok: false, reason: `quota probe network error: ${msg}` };
18876
18897
  }
18877
- clearTimeout(timeout);
18878
18898
  const parsed = parseQuotaHeaders(resp.headers);
18879
- if (parsed.ok)
18899
+ if (parsed.ok) {
18900
+ clearTimeout(timeout);
18880
18901
  return parsed;
18902
+ }
18881
18903
  if (!resp.ok) {
18882
- return { ok: false, reason: `HTTP ${resp.status} from Anthropic (${parsed.reason})` };
18904
+ let bodyText = "";
18905
+ try {
18906
+ bodyText = await resp.text();
18907
+ } catch {}
18908
+ clearTimeout(timeout);
18909
+ const apiErrorMessage = extractApiErrorMessage(bodyText);
18910
+ const entitlement = resp.status === 403 && isEntitlementDisabledMessage(apiErrorMessage);
18911
+ return {
18912
+ ok: false,
18913
+ reason: `HTTP ${resp.status} from Anthropic (${parsed.reason})`,
18914
+ httpStatus: resp.status,
18915
+ ...apiErrorMessage ? { apiErrorMessage } : {},
18916
+ failureKind: entitlement ? "entitlement_blocked" : "other"
18917
+ };
18883
18918
  }
18919
+ clearTimeout(timeout);
18884
18920
  return parsed;
18885
18921
  }
18886
18922
 
@@ -18912,7 +18948,9 @@ function snapshotClearlyHealthy(s) {
18912
18948
  return s.fiveHourUtilizationPct < HEALTHY_CLEAR_PCT && s.sevenDayUtilizationPct < HEALTHY_CLEAR_PCT;
18913
18949
  }
18914
18950
  function accountEligibility(opts) {
18915
- const { mark, snapshot, now, allowOverage = false } = opts;
18951
+ const { mark, snapshot, now, allowOverage = false, entitlementBlocked = false } = opts;
18952
+ if (entitlementBlocked)
18953
+ return "blocked";
18916
18954
  if (snapshotFresh(snapshot, now) && snapshotWalled(snapshot) && !overageLiftsWall(snapshot, allowOverage)) {
18917
18955
  return "blocked";
18918
18956
  }
@@ -21170,9 +21208,51 @@ class AuthBroker {
21170
21208
  mark: this.exhaustionMarkOf(account),
21171
21209
  snapshot: this.lastQuotaCache[account],
21172
21210
  now: this.now(),
21173
- allowOverage: this.isOverageAllowed(account)
21211
+ allowOverage: this.isOverageAllowed(account),
21212
+ entitlementBlocked: this.isAccountEntitlementBlocked(account)
21174
21213
  });
21175
21214
  }
21215
+ isAccountEntitlementBlocked(account) {
21216
+ return this.quota[account]?.entitlement_blocked === true;
21217
+ }
21218
+ markEntitlementBlocked(label, result) {
21219
+ if (result.ok || result.failureKind !== "entitlement_blocked")
21220
+ return;
21221
+ if (this.quota[label]?.entitlement_blocked === true)
21222
+ return;
21223
+ this.quota[label] = {
21224
+ ...this.quota[label],
21225
+ entitlement_blocked: true,
21226
+ entitlement_blocked_at: this.now(),
21227
+ ...result.apiErrorMessage ? { entitlement_blocked_reason: result.apiErrorMessage } : {}
21228
+ };
21229
+ this.persistQuota();
21230
+ this.audit({
21231
+ op: "mark-exhausted",
21232
+ identity: { kind: "operator" },
21233
+ account: label,
21234
+ accountKind: "claude",
21235
+ ok: true,
21236
+ reason: "entitlement-403"
21237
+ });
21238
+ process.stdout.write(`auth-broker: ${label} returned entitlement-403 (Claude Code access disabled) — marked entitlement_blocked${result.apiErrorMessage ? ` (${result.apiErrorMessage})` : ""}
21239
+ `);
21240
+ }
21241
+ clearEntitlementBlocked(label) {
21242
+ const entry = this.quota[label];
21243
+ if (!entry?.entitlement_blocked)
21244
+ return;
21245
+ const {
21246
+ entitlement_blocked,
21247
+ entitlement_blocked_at,
21248
+ entitlement_blocked_reason,
21249
+ ...rest
21250
+ } = entry;
21251
+ this.quota[label] = rest;
21252
+ this.persistQuota();
21253
+ process.stdout.write(`auth-broker: live probe of ${label} succeeded — cleared entitlement_blocked mark
21254
+ `);
21255
+ }
21176
21256
  premiumWallMarkOf(account) {
21177
21257
  const q = this.quota[account];
21178
21258
  if (!q || q.premium_walled_until === undefined)
@@ -21329,7 +21409,8 @@ class AuthBroker {
21329
21409
  mark: this.exhaustionMarkOf(account),
21330
21410
  snapshot,
21331
21411
  now,
21332
- allowOverage: true
21412
+ allowOverage: true,
21413
+ entitlementBlocked: this.isAccountEntitlementBlocked(account)
21333
21414
  }) === "eligible";
21334
21415
  }
21335
21416
  accountEligibilityOf(account) {
@@ -21339,7 +21420,8 @@ class AuthBroker {
21339
21420
  mark: this.exhaustionMarkOf(account),
21340
21421
  snapshot,
21341
21422
  now: this.now(),
21342
- allowOverage
21423
+ allowOverage,
21424
+ entitlementBlocked: this.isAccountEntitlementBlocked(account)
21343
21425
  });
21344
21426
  if (verdict === "eligible" && allowOverage && snapshot && (snapshot.fiveHourUtilizationPct >= WALL_PCT || snapshot.sevenDayUtilizationPct >= WALL_PCT)) {
21345
21427
  process.stdout.write(`auth-broker: ${account} is past the utilization wall but eligible via allow_overage — Anthropic overage billing active (5h=${snapshot.fiveHourUtilizationPct.toFixed(1)}%, 7d=${snapshot.sevenDayUtilizationPct.toFixed(1)}%)
@@ -21356,6 +21438,8 @@ class AuthBroker {
21356
21438
  const result = await this.probeQuotaSingleFlight(account, token);
21357
21439
  if (result.ok)
21358
21440
  this.cacheQuotaSnapshot(account, result);
21441
+ else
21442
+ this.markEntitlementBlocked(account, result);
21359
21443
  } catch {}
21360
21444
  }
21361
21445
  async nextHealthyAccountLive(current, order) {
@@ -21455,7 +21539,6 @@ class AuthBroker {
21455
21539
  expiresAt: creds?.claudeAiOauth?.expiresAt,
21456
21540
  exhausted,
21457
21541
  in_service: inService.has(label),
21458
- entitlement_blocked: false,
21459
21542
  exhausted_until: q?.exhausted_until,
21460
21543
  throttled_until: q?.throttled_until,
21461
21544
  threshold_violations: this.thresholdViolations[label] ?? 0,
@@ -21465,6 +21548,7 @@ class AuthBroker {
21465
21548
  premium_walled_until: q?.premium_walled_until,
21466
21549
  premium_wall_bucket: q?.premium_wall_bucket,
21467
21550
  last_tier_quota: this.lastTierQuotaCache[label] ?? null,
21551
+ entitlement_blocked: q?.entitlement_blocked ?? false,
21468
21552
  usage_ledger: summarizeAccountUsage(this.usageLedger, label, this.now())
21469
21553
  };
21470
21554
  });
@@ -21539,6 +21623,7 @@ class AuthBroker {
21539
21623
  this.cacheQuotaSnapshot(label, result);
21540
21624
  return { label, result, served: "live" };
21541
21625
  }
21626
+ this.markEntitlementBlocked(label, result);
21542
21627
  if (cached) {
21543
21628
  return { label, result: cachedSnapshotToResult(cached), served: "cache", capturedAt: cached.capturedAt };
21544
21629
  }
@@ -21560,6 +21645,7 @@ class AuthBroker {
21560
21645
  cacheQuotaSnapshot(label, result) {
21561
21646
  if (!result.ok)
21562
21647
  return;
21648
+ this.clearEntitlementBlocked(label);
21563
21649
  const snapshot = {
21564
21650
  fiveHourUtilizationPct: result.data.fiveHourUtilizationPct,
21565
21651
  sevenDayUtilizationPct: result.data.sevenDayUtilizationPct,
@@ -21589,6 +21675,8 @@ class AuthBroker {
21589
21675
  const canarySet = this.premiumCanarySet();
21590
21676
  const canaryEnabled = process.env.SWITCHROOM_DISABLE_MODEL_TIER_PROBE !== "1";
21591
21677
  for (const label of listAccounts(this.home)) {
21678
+ if (this.isAccountEntitlementBlocked(label))
21679
+ continue;
21592
21680
  const creds = readAccountCredentials(label, this.home);
21593
21681
  const token = creds?.claudeAiOauth?.accessToken;
21594
21682
  if (!token)
@@ -21601,6 +21689,7 @@ class AuthBroker {
21601
21689
  continue;
21602
21690
  }
21603
21691
  this.cacheQuotaSnapshot(label, result);
21692
+ this.markEntitlementBlocked(label, result);
21604
21693
  probed.push({ label, result });
21605
21694
  if (canaryEnabled && canarySet.has(label)) {
21606
21695
  let tierResult;
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.19.3", COMMIT_SHA = "41896be4";
2123
+ var VERSION = "0.19.4", COMMIT_SHA = "7cdf5428";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -26663,7 +26663,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
26663
26663
  import { dirname as dirname4, join as join7 } from "node:path";
26664
26664
 
26665
26665
  // src/build-info.ts
26666
- var VERSION = "0.19.3";
26666
+ var VERSION = "0.19.4";
26667
26667
 
26668
26668
  // src/cli/resolve-version.ts
26669
26669
  function readPackageVersion() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.19.3",
4
+ "version": "0.19.4",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -93135,10 +93135,10 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
93135
93135
  }
93136
93136
 
93137
93137
  // ../src/build-info.ts
93138
- var VERSION = "0.19.3";
93139
- var COMMIT_SHA = "41896be4";
93140
- var COMMIT_DATE = "2026-07-20T10:37:39+10:00";
93141
- var LATEST_PR = 3456;
93138
+ var VERSION = "0.19.4";
93139
+ var COMMIT_SHA = "7cdf5428";
93140
+ var COMMIT_DATE = "2026-07-20T11:09:10+10:00";
93141
+ var LATEST_PR = 3457;
93142
93142
  var COMMITS_AHEAD_OF_TAG = 0;
93143
93143
 
93144
93144
  // gateway/boot-version.ts