switchroom 0.14.80 → 0.14.82

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.
@@ -6932,7 +6932,7 @@ var require_public_api = __commonJS((exports) => {
6932
6932
  });
6933
6933
 
6934
6934
  // src/agent-scheduler/index.ts
6935
- import { resolve as resolve4, join } from "node:path";
6935
+ import { resolve as resolve4, join as join2 } from "node:path";
6936
6936
 
6937
6937
  // src/config/loader.ts
6938
6938
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
@@ -12295,6 +12295,581 @@ class JsonlAuditSink {
12295
12295
  close() {}
12296
12296
  }
12297
12297
 
12298
+ // src/scheduler/quota-preflight.ts
12299
+ function decideQuotaPreflight(state) {
12300
+ const accounts = state.accounts ?? [];
12301
+ if (accounts.length === 0) {
12302
+ return { defer: false, reason: "no accounts in broker state" };
12303
+ }
12304
+ const healthy = accounts.filter((a) => !a.exhausted);
12305
+ if (healthy.length > 0) {
12306
+ return {
12307
+ defer: false,
12308
+ reason: `${healthy.length}/${accounts.length} account(s) healthy`
12309
+ };
12310
+ }
12311
+ return { defer: true, reason: `all ${accounts.length} account(s) exhausted` };
12312
+ }
12313
+
12314
+ // src/auth/broker/client.ts
12315
+ import * as net from "node:net";
12316
+ import { homedir as homedir2 } from "node:os";
12317
+ import { randomUUID } from "node:crypto";
12318
+ import { join } from "node:path";
12319
+
12320
+ // src/auth/broker/protocol.ts
12321
+ var MAX_FRAME_BYTES = 64 * 1024;
12322
+ var PROTOCOL_VERSION = 1;
12323
+ var ProviderNameSchema = exports_external.enum(["anthropic", "google", "microsoft"]);
12324
+ var GetCredentialsRequestSchema = exports_external.object({
12325
+ v: exports_external.literal(PROTOCOL_VERSION),
12326
+ op: exports_external.literal("get-credentials"),
12327
+ id: exports_external.string().min(1),
12328
+ provider: ProviderNameSchema.optional()
12329
+ });
12330
+ var ListStateRequestSchema = exports_external.object({
12331
+ v: exports_external.literal(PROTOCOL_VERSION),
12332
+ op: exports_external.literal("list-state"),
12333
+ id: exports_external.string().min(1)
12334
+ });
12335
+ var SetActiveRequestSchema = exports_external.object({
12336
+ v: exports_external.literal(PROTOCOL_VERSION),
12337
+ op: exports_external.literal("set-active"),
12338
+ id: exports_external.string().min(1),
12339
+ account: exports_external.string().min(1),
12340
+ provider: ProviderNameSchema.optional()
12341
+ });
12342
+ var MarkExhaustedRequestSchema = exports_external.object({
12343
+ v: exports_external.literal(PROTOCOL_VERSION),
12344
+ op: exports_external.literal("mark-exhausted"),
12345
+ id: exports_external.string().min(1),
12346
+ until: exports_external.number().int().positive().optional()
12347
+ });
12348
+ var RefreshAccountRequestSchema = exports_external.object({
12349
+ v: exports_external.literal(PROTOCOL_VERSION),
12350
+ op: exports_external.literal("refresh-account"),
12351
+ id: exports_external.string().min(1),
12352
+ account: exports_external.string().min(1),
12353
+ provider: ProviderNameSchema.optional()
12354
+ });
12355
+ var AnthropicCredentialsSchema = exports_external.object({
12356
+ claudeAiOauth: exports_external.object({
12357
+ accessToken: exports_external.string(),
12358
+ refreshToken: exports_external.string().optional(),
12359
+ expiresAt: exports_external.number().optional(),
12360
+ scopes: exports_external.array(exports_external.string()).optional(),
12361
+ subscriptionType: exports_external.string().optional(),
12362
+ rateLimitTier: exports_external.string().optional()
12363
+ })
12364
+ });
12365
+ var GoogleCredentialsSchema = exports_external.object({
12366
+ googleOauth: exports_external.object({
12367
+ accessToken: exports_external.string(),
12368
+ refreshToken: exports_external.string(),
12369
+ expiresAt: exports_external.number(),
12370
+ scope: exports_external.string(),
12371
+ clientId: exports_external.string(),
12372
+ accountEmail: exports_external.string(),
12373
+ tokenType: exports_external.literal("Bearer")
12374
+ })
12375
+ });
12376
+ var MicrosoftCredentialsSchema = exports_external.object({
12377
+ microsoftOauth: exports_external.object({
12378
+ accessToken: exports_external.string(),
12379
+ refreshToken: exports_external.string(),
12380
+ expiresAt: exports_external.number(),
12381
+ scope: exports_external.string(),
12382
+ clientId: exports_external.string(),
12383
+ accountEmail: exports_external.string(),
12384
+ tokenType: exports_external.literal("Bearer"),
12385
+ tenantId: exports_external.string(),
12386
+ accountType: exports_external.enum(["personal", "work"]),
12387
+ homeAccountId: exports_external.string()
12388
+ })
12389
+ });
12390
+ var ProviderCredentialsSchema = exports_external.union([
12391
+ AnthropicCredentialsSchema,
12392
+ GoogleCredentialsSchema,
12393
+ MicrosoftCredentialsSchema
12394
+ ]);
12395
+ var AddAccountRequestSchema = exports_external.object({
12396
+ v: exports_external.literal(PROTOCOL_VERSION),
12397
+ op: exports_external.literal("add-account"),
12398
+ id: exports_external.string().min(1),
12399
+ label: exports_external.string().min(1),
12400
+ provider: ProviderNameSchema.optional(),
12401
+ credentials: ProviderCredentialsSchema,
12402
+ replace: exports_external.boolean().optional()
12403
+ });
12404
+ var RmAccountRequestSchema = exports_external.object({
12405
+ v: exports_external.literal(PROTOCOL_VERSION),
12406
+ op: exports_external.literal("rm-account"),
12407
+ id: exports_external.string().min(1),
12408
+ label: exports_external.string().min(1),
12409
+ provider: ProviderNameSchema.optional()
12410
+ });
12411
+ var SetOverrideRequestSchema = exports_external.object({
12412
+ v: exports_external.literal(PROTOCOL_VERSION),
12413
+ op: exports_external.literal("set-override"),
12414
+ id: exports_external.string().min(1),
12415
+ agent: exports_external.string().min(1),
12416
+ account: exports_external.string().min(1).nullable()
12417
+ });
12418
+ var ListGoogleAccountsRequestSchema = exports_external.object({
12419
+ v: exports_external.literal(PROTOCOL_VERSION),
12420
+ op: exports_external.literal("list-google-accounts"),
12421
+ id: exports_external.string().min(1)
12422
+ });
12423
+ var ListMicrosoftAccountsRequestSchema = exports_external.object({
12424
+ v: exports_external.literal(PROTOCOL_VERSION),
12425
+ op: exports_external.literal("list-microsoft-accounts"),
12426
+ id: exports_external.string().min(1)
12427
+ });
12428
+ var ProbeQuotaRequestSchema = exports_external.object({
12429
+ v: exports_external.literal(PROTOCOL_VERSION),
12430
+ op: exports_external.literal("probe-quota"),
12431
+ id: exports_external.string().min(1),
12432
+ accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
12433
+ timeoutMs: exports_external.number().int().positive().max(60000).optional()
12434
+ });
12435
+ var RequestSchema = exports_external.discriminatedUnion("op", [
12436
+ GetCredentialsRequestSchema,
12437
+ ListStateRequestSchema,
12438
+ SetActiveRequestSchema,
12439
+ MarkExhaustedRequestSchema,
12440
+ RefreshAccountRequestSchema,
12441
+ AddAccountRequestSchema,
12442
+ RmAccountRequestSchema,
12443
+ SetOverrideRequestSchema,
12444
+ ListGoogleAccountsRequestSchema,
12445
+ ListMicrosoftAccountsRequestSchema,
12446
+ ProbeQuotaRequestSchema
12447
+ ]);
12448
+ var GetCredentialsDataSchema = exports_external.object({
12449
+ account: exports_external.string(),
12450
+ credentials: exports_external.unknown(),
12451
+ expiresAt: exports_external.number().optional()
12452
+ });
12453
+ var AccountStateSchema = exports_external.object({
12454
+ label: exports_external.string(),
12455
+ expiresAt: exports_external.number().optional(),
12456
+ exhausted: exports_external.boolean(),
12457
+ exhausted_until: exports_external.number().optional(),
12458
+ threshold_violations: exports_external.number().int().nonnegative().optional(),
12459
+ last_refreshed_at: exports_external.number().optional()
12460
+ });
12461
+ var AgentStateSchema = exports_external.object({
12462
+ name: exports_external.string(),
12463
+ account: exports_external.string(),
12464
+ override: exports_external.string().nullable()
12465
+ });
12466
+ var ConsumerStateSchema = exports_external.object({
12467
+ name: exports_external.string(),
12468
+ account: exports_external.string(),
12469
+ last_seen_at: exports_external.number().nullable()
12470
+ });
12471
+ var ListStateDataSchema = exports_external.object({
12472
+ active: exports_external.string(),
12473
+ fallback_order: exports_external.array(exports_external.string()),
12474
+ accounts: exports_external.array(AccountStateSchema),
12475
+ agents: exports_external.array(AgentStateSchema),
12476
+ consumers: exports_external.array(ConsumerStateSchema)
12477
+ });
12478
+ var SetActiveDataSchema = exports_external.object({
12479
+ active: exports_external.string(),
12480
+ fanned: exports_external.array(exports_external.string())
12481
+ });
12482
+ var MarkExhaustedDataSchema = exports_external.object({
12483
+ account: exports_external.string(),
12484
+ rolled: exports_external.array(exports_external.string()),
12485
+ rolledTo: exports_external.string().nullable().optional()
12486
+ });
12487
+ var RefreshAccountDataSchema = exports_external.object({
12488
+ account: exports_external.string(),
12489
+ expiresAt: exports_external.number().optional()
12490
+ });
12491
+ var AddAccountDataSchema = exports_external.object({
12492
+ label: exports_external.string(),
12493
+ expiresAt: exports_external.number().optional()
12494
+ });
12495
+ var RmAccountDataSchema = exports_external.object({
12496
+ label: exports_external.string()
12497
+ });
12498
+ var SetOverrideDataSchema = exports_external.object({
12499
+ agent: exports_external.string(),
12500
+ account: exports_external.string().nullable()
12501
+ });
12502
+ var GoogleAccountStateSchema = exports_external.object({
12503
+ account: exports_external.string(),
12504
+ expiresAt: exports_external.number(),
12505
+ scope: exports_external.string(),
12506
+ clientId: exports_external.string()
12507
+ });
12508
+ var ListGoogleAccountsDataSchema = exports_external.object({
12509
+ accounts: exports_external.array(GoogleAccountStateSchema)
12510
+ });
12511
+ var MicrosoftAccountStateSchema = exports_external.object({
12512
+ account: exports_external.string(),
12513
+ expiresAt: exports_external.number(),
12514
+ scope: exports_external.string(),
12515
+ clientId: exports_external.string(),
12516
+ accountType: exports_external.enum(["personal", "work"])
12517
+ });
12518
+ var ListMicrosoftAccountsDataSchema = exports_external.object({
12519
+ accounts: exports_external.array(MicrosoftAccountStateSchema)
12520
+ });
12521
+ var ErrorBodySchema = exports_external.object({
12522
+ code: exports_external.enum([
12523
+ "FORBIDDEN",
12524
+ "INVALID_ARGS",
12525
+ "UNKNOWN_VERB",
12526
+ "VERSION_MISMATCH",
12527
+ "ACCOUNT_NOT_FOUND",
12528
+ "ACCOUNT_ALREADY_EXISTS",
12529
+ "CONFIG_INVALID",
12530
+ "DRIFT_DETECTED",
12531
+ "REFRESH_FAILED",
12532
+ "INTERNAL"
12533
+ ]),
12534
+ message: exports_external.string()
12535
+ });
12536
+ var SuccessResponseSchema = exports_external.object({
12537
+ v: exports_external.literal(PROTOCOL_VERSION),
12538
+ id: exports_external.string(),
12539
+ ok: exports_external.literal(true),
12540
+ data: exports_external.unknown()
12541
+ });
12542
+ var ErrorResponseSchema = exports_external.object({
12543
+ v: exports_external.literal(PROTOCOL_VERSION),
12544
+ id: exports_external.string(),
12545
+ ok: exports_external.literal(false),
12546
+ error: ErrorBodySchema
12547
+ });
12548
+ var ResponseSchema = exports_external.discriminatedUnion("ok", [
12549
+ SuccessResponseSchema,
12550
+ ErrorResponseSchema
12551
+ ]);
12552
+ function encodeRequest(req) {
12553
+ const line = JSON.stringify(RequestSchema.parse(req)) + `
12554
+ `;
12555
+ if (Buffer.byteLength(line, "utf-8") > MAX_FRAME_BYTES) {
12556
+ throw new Error(`auth-broker request exceeds MAX_FRAME_BYTES (${MAX_FRAME_BYTES})`);
12557
+ }
12558
+ return line;
12559
+ }
12560
+ function decodeResponse(line) {
12561
+ const trimmed = line.endsWith(`
12562
+ `) ? line.slice(0, -1) : line;
12563
+ let parsed;
12564
+ try {
12565
+ parsed = JSON.parse(trimmed);
12566
+ } catch {
12567
+ throw new Error("auth-broker response is not valid JSON");
12568
+ }
12569
+ return ResponseSchema.parse(parsed);
12570
+ }
12571
+
12572
+ // src/auth/broker/client.ts
12573
+ var DEFAULT_TIMEOUT_MS = 5000;
12574
+ function reviveDate(v) {
12575
+ if (v == null)
12576
+ return null;
12577
+ if (v instanceof Date)
12578
+ return Number.isNaN(v.getTime()) ? null : v;
12579
+ const d = new Date(v);
12580
+ return Number.isNaN(d.getTime()) ? null : d;
12581
+ }
12582
+ function operatorSocketPath(home2 = homedir2()) {
12583
+ return join(home2, ".switchroom", "state", "auth-broker-operator", "sock");
12584
+ }
12585
+ function resolveAuthBrokerSocketPath(opts) {
12586
+ if (opts?.socket)
12587
+ return opts.socket;
12588
+ const env = process.env.SWITCHROOM_AUTH_BROKER_SOCKET;
12589
+ if (env && env.length > 0)
12590
+ return env;
12591
+ return operatorSocketPath(opts?.home);
12592
+ }
12593
+
12594
+ class AuthBrokerError extends Error {
12595
+ code;
12596
+ constructor(code, message) {
12597
+ super(message);
12598
+ this.code = code;
12599
+ this.name = "AuthBrokerError";
12600
+ }
12601
+ }
12602
+
12603
+ class AuthBrokerUnreachableError extends Error {
12604
+ reason;
12605
+ socketPath;
12606
+ constructor(reason, socketPath) {
12607
+ super(`auth-broker unreachable at ${socketPath}: ${reason}. ` + `The broker may be down; existing credentials remain valid until expiry.`);
12608
+ this.reason = reason;
12609
+ this.socketPath = socketPath;
12610
+ this.name = "AuthBrokerUnreachableError";
12611
+ }
12612
+ }
12613
+
12614
+ class AuthBrokerClient {
12615
+ socketPath;
12616
+ timeoutMs;
12617
+ socket = null;
12618
+ connecting = null;
12619
+ buffer = "";
12620
+ pending = new Map;
12621
+ closed = false;
12622
+ constructor(opts = {}) {
12623
+ this.socketPath = resolveAuthBrokerSocketPath(opts);
12624
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12625
+ }
12626
+ getSocketPath() {
12627
+ return this.socketPath;
12628
+ }
12629
+ async close() {
12630
+ this.closed = true;
12631
+ const sock = this.socket;
12632
+ this.socket = null;
12633
+ this.connecting = null;
12634
+ for (const [, p] of this.pending) {
12635
+ clearTimeout(p.timer);
12636
+ p.reject(new Error("auth-broker client closed"));
12637
+ }
12638
+ this.pending.clear();
12639
+ if (sock) {
12640
+ sock.destroy();
12641
+ }
12642
+ }
12643
+ async getCredentials(provider) {
12644
+ const base = {
12645
+ v: PROTOCOL_VERSION,
12646
+ id: randomUUID(),
12647
+ op: "get-credentials"
12648
+ };
12649
+ const req = provider !== undefined ? { ...base, provider } : base;
12650
+ const data = await this.send(req);
12651
+ return data;
12652
+ }
12653
+ async listState() {
12654
+ const data = await this.send({
12655
+ v: PROTOCOL_VERSION,
12656
+ id: randomUUID(),
12657
+ op: "list-state"
12658
+ });
12659
+ return data;
12660
+ }
12661
+ async listGoogleAccounts() {
12662
+ const data = await this.send({
12663
+ v: PROTOCOL_VERSION,
12664
+ id: randomUUID(),
12665
+ op: "list-google-accounts"
12666
+ });
12667
+ return data;
12668
+ }
12669
+ async listMicrosoftAccounts() {
12670
+ const data = await this.send({
12671
+ v: PROTOCOL_VERSION,
12672
+ id: randomUUID(),
12673
+ op: "list-microsoft-accounts"
12674
+ });
12675
+ return data;
12676
+ }
12677
+ async probeQuota(accounts, timeoutMs) {
12678
+ const data = await this.send({
12679
+ v: PROTOCOL_VERSION,
12680
+ id: randomUUID(),
12681
+ op: "probe-quota",
12682
+ accounts: [...accounts],
12683
+ ...timeoutMs !== undefined ? { timeoutMs } : {}
12684
+ });
12685
+ const parsed = data;
12686
+ for (const entry of parsed.results) {
12687
+ if (entry.result.ok) {
12688
+ entry.result.data.fiveHourResetAt = reviveDate(entry.result.data.fiveHourResetAt);
12689
+ entry.result.data.sevenDayResetAt = reviveDate(entry.result.data.sevenDayResetAt);
12690
+ }
12691
+ }
12692
+ return parsed;
12693
+ }
12694
+ async setActive(account) {
12695
+ const data = await this.send({
12696
+ v: PROTOCOL_VERSION,
12697
+ id: randomUUID(),
12698
+ op: "set-active",
12699
+ account
12700
+ });
12701
+ return data;
12702
+ }
12703
+ async markExhausted(until) {
12704
+ const req = until !== undefined ? { v: PROTOCOL_VERSION, id: randomUUID(), op: "mark-exhausted", until } : { v: PROTOCOL_VERSION, id: randomUUID(), op: "mark-exhausted" };
12705
+ const data = await this.send(req);
12706
+ return data;
12707
+ }
12708
+ async refreshAccount(account) {
12709
+ const data = await this.send({
12710
+ v: PROTOCOL_VERSION,
12711
+ id: randomUUID(),
12712
+ op: "refresh-account",
12713
+ account
12714
+ });
12715
+ return data;
12716
+ }
12717
+ async addAccount(label, credentials, replace, provider) {
12718
+ const base = {
12719
+ v: PROTOCOL_VERSION,
12720
+ id: randomUUID(),
12721
+ op: "add-account",
12722
+ label,
12723
+ credentials
12724
+ };
12725
+ const withReplace = replace ? { ...base, replace: true } : base;
12726
+ const req = provider !== undefined ? { ...withReplace, provider } : withReplace;
12727
+ const data = await this.send(req);
12728
+ return data;
12729
+ }
12730
+ async rmAccount(label, provider) {
12731
+ const base = {
12732
+ v: PROTOCOL_VERSION,
12733
+ id: randomUUID(),
12734
+ op: "rm-account",
12735
+ label
12736
+ };
12737
+ const req = provider !== undefined ? { ...base, provider } : base;
12738
+ const data = await this.send(req);
12739
+ return data;
12740
+ }
12741
+ async setOverride(agent, account) {
12742
+ const data = await this.send({
12743
+ v: PROTOCOL_VERSION,
12744
+ id: randomUUID(),
12745
+ op: "set-override",
12746
+ agent,
12747
+ account
12748
+ });
12749
+ return data;
12750
+ }
12751
+ async ensureConnected() {
12752
+ if (this.closed) {
12753
+ throw new Error("auth-broker client is closed");
12754
+ }
12755
+ if (this.socket && !this.socket.destroyed)
12756
+ return this.socket;
12757
+ if (this.connecting)
12758
+ return this.connecting;
12759
+ this.connecting = new Promise((resolve4, reject) => {
12760
+ const sock = new net.Socket;
12761
+ const onError = (err) => {
12762
+ sock.removeAllListeners();
12763
+ sock.destroy();
12764
+ const code = err.code ?? "ERR";
12765
+ let reason;
12766
+ if (code === "ENOENT")
12767
+ reason = "socket file not found";
12768
+ else if (code === "ECONNREFUSED")
12769
+ reason = "connection refused";
12770
+ else if (code === "EACCES")
12771
+ reason = "access denied";
12772
+ else
12773
+ reason = err.message;
12774
+ reject(new AuthBrokerUnreachableError(reason, this.socketPath));
12775
+ };
12776
+ sock.once("error", onError);
12777
+ sock.once("connect", () => {
12778
+ sock.removeListener("error", onError);
12779
+ sock.on("data", (chunk) => this.onData(chunk));
12780
+ sock.on("error", (err) => this.onSocketError(err));
12781
+ sock.on("close", () => this.onSocketClose());
12782
+ this.socket = sock;
12783
+ resolve4(sock);
12784
+ });
12785
+ sock.connect({ path: this.socketPath });
12786
+ });
12787
+ try {
12788
+ return await this.connecting;
12789
+ } finally {
12790
+ this.connecting = null;
12791
+ }
12792
+ }
12793
+ onData(chunk) {
12794
+ this.buffer += chunk.toString("utf8");
12795
+ let idx;
12796
+ while ((idx = this.buffer.indexOf(`
12797
+ `)) !== -1) {
12798
+ const line = this.buffer.slice(0, idx);
12799
+ this.buffer = this.buffer.slice(idx + 1);
12800
+ if (line.length === 0)
12801
+ continue;
12802
+ let resp;
12803
+ try {
12804
+ resp = decodeResponse(line);
12805
+ } catch (err) {
12806
+ const msg = `unparseable auth-broker response: ${err instanceof Error ? err.message : String(err)}`;
12807
+ this.failAll(new AuthBrokerUnreachableError(msg, this.socketPath));
12808
+ return;
12809
+ }
12810
+ const p = this.pending.get(resp.id);
12811
+ if (!p) {
12812
+ continue;
12813
+ }
12814
+ this.pending.delete(resp.id);
12815
+ clearTimeout(p.timer);
12816
+ p.resolve(resp);
12817
+ }
12818
+ }
12819
+ onSocketError(err) {
12820
+ this.failAll(new AuthBrokerUnreachableError(err.message, this.socketPath));
12821
+ if (this.socket) {
12822
+ this.socket.destroy();
12823
+ this.socket = null;
12824
+ }
12825
+ }
12826
+ onSocketClose() {
12827
+ if (this.pending.size > 0) {
12828
+ this.failAll(new AuthBrokerUnreachableError("connection closed mid-request", this.socketPath));
12829
+ }
12830
+ this.socket = null;
12831
+ }
12832
+ failAll(err) {
12833
+ for (const [, p] of this.pending) {
12834
+ clearTimeout(p.timer);
12835
+ p.reject(err);
12836
+ }
12837
+ this.pending.clear();
12838
+ }
12839
+ async send(req) {
12840
+ const sock = await this.ensureConnected();
12841
+ const id = req.id;
12842
+ const frame = encodeRequest(req);
12843
+ return new Promise((resolve4, reject) => {
12844
+ const timer = setTimeout(() => {
12845
+ this.pending.delete(id);
12846
+ reject(new AuthBrokerUnreachableError(`request ${req.op} timed out after ${this.timeoutMs}ms`, this.socketPath));
12847
+ }, this.timeoutMs);
12848
+ this.pending.set(id, {
12849
+ resolve: (resp) => {
12850
+ if (resp.ok) {
12851
+ resolve4(resp.data);
12852
+ } else {
12853
+ reject(new AuthBrokerError(resp.error.code, resp.error.message));
12854
+ }
12855
+ },
12856
+ reject,
12857
+ timer
12858
+ });
12859
+ sock.write(frame, (err) => {
12860
+ if (err) {
12861
+ const p = this.pending.get(id);
12862
+ if (p) {
12863
+ clearTimeout(p.timer);
12864
+ this.pending.delete(id);
12865
+ }
12866
+ reject(new AuthBrokerUnreachableError(`failed to send ${req.op}: ${err.message}`, this.socketPath));
12867
+ }
12868
+ });
12869
+ });
12870
+ }
12871
+ }
12872
+
12298
12873
  // src/agent-scheduler/ipc-client.ts
12299
12874
  import { createConnection } from "node:net";
12300
12875
  function createInjectIpcClient(options) {
@@ -12734,12 +13309,55 @@ function readRecentFires(jsonlPath) {
12734
13309
  }
12735
13310
 
12736
13311
  // src/agent-scheduler/index.ts
13312
+ var DEFAULT_MAX_QUOTA_DEFER_ATTEMPTS = 3;
13313
+ function defaultQuotaDeferBackoffMs(attempt) {
13314
+ return [60000, 180000, 300000][attempt] ?? 300000;
13315
+ }
12737
13316
  function registerAgentSchedule(opts) {
12738
13317
  const tasks = [];
12739
13318
  const now = opts.now ?? Date.now;
13319
+ const maxAttempts = opts.maxQuotaDeferAttempts ?? DEFAULT_MAX_QUOTA_DEFER_ATTEMPTS;
13320
+ const backoff = opts.quotaDeferBackoffMs ?? defaultQuotaDeferBackoffMs;
13321
+ const scheduleRetry = opts.scheduleRetry ?? ((fn, ms) => {
13322
+ const t = setTimeout(fn, ms);
13323
+ if (typeof t.unref === "function")
13324
+ t.unref();
13325
+ return { cancel: () => clearTimeout(t) };
13326
+ });
12740
13327
  for (const entry of opts.entries) {
12741
- const task = opts.cronLib.schedule(entry.cron, () => {
13328
+ const pendingRetries = new Set;
13329
+ const attemptFire = async (attempt) => {
12742
13330
  const startedAt = now();
13331
+ if (opts.quotaGate) {
13332
+ let decision;
13333
+ try {
13334
+ decision = await opts.quotaGate(entry.agent);
13335
+ } catch {
13336
+ decision = { defer: false, reason: "quota gate error (fail-open)" };
13337
+ }
13338
+ if (decision.defer) {
13339
+ const more = attempt + 1 < maxAttempts;
13340
+ const summary2 = `deferred (quota): ${decision.reason} ` + `[attempt ${attempt + 1}/${maxAttempts}]` + (more ? "" : " — giving up; will re-run on next scheduled occurrence");
13341
+ opts.sink.recordFire({
13342
+ agent: entry.agent,
13343
+ scheduleIndex: entry.scheduleIndex,
13344
+ promptKey: entry.promptKey,
13345
+ exitCode: -2,
13346
+ outputSummary: summary2,
13347
+ startedAt,
13348
+ finishedAt: now()
13349
+ });
13350
+ if (more) {
13351
+ let handle;
13352
+ handle = scheduleRetry(() => {
13353
+ pendingRetries.delete(handle);
13354
+ attemptFire(attempt + 1);
13355
+ }, backoff(attempt));
13356
+ pendingRetries.add(handle);
13357
+ }
13358
+ return;
13359
+ }
13360
+ }
12743
13361
  let delivered = false;
12744
13362
  let summary = "";
12745
13363
  try {
@@ -12760,8 +13378,19 @@ function registerAgentSchedule(opts) {
12760
13378
  startedAt,
12761
13379
  finishedAt
12762
13380
  });
13381
+ };
13382
+ const task = opts.cronLib.schedule(entry.cron, () => attemptFire(0));
13383
+ tasks.push({
13384
+ entry,
13385
+ task: {
13386
+ stop: () => {
13387
+ for (const h of pendingRetries)
13388
+ h.cancel();
13389
+ pendingRetries.clear();
13390
+ task.stop();
13391
+ }
13392
+ }
12763
13393
  });
12764
- tasks.push({ entry, task });
12765
13394
  }
12766
13395
  return tasks;
12767
13396
  }
@@ -12785,7 +13414,7 @@ async function main() {
12785
13414
  }
12786
13415
  const configPath = process.env.SWITCHROOM_CONFIG ?? "/state/config/switchroom.yaml";
12787
13416
  const stateDir = process.env.TELEGRAM_STATE_DIR ?? "/state/agent/telegram";
12788
- const socketPath = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join(stateDir, "gateway.sock");
13417
+ const socketPath = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join2(stateDir, "gateway.sock");
12789
13418
  const jsonlPath = process.env.SWITCHROOM_AGENT_SCHEDULER_JSONL ?? "/state/agent/scheduler.jsonl";
12790
13419
  const lockPath = process.env.SWITCHROOM_AGENT_SCHEDULER_LOCK ?? "/state/agent/scheduler.lock";
12791
13420
  const lock = acquireLock(lockPath);
@@ -12913,12 +13542,22 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
12913
13542
  }
12914
13543
  }
12915
13544
  const cronLib = __require("node-cron");
13545
+ const quotaPreflightEnabled = process.env.SWITCHROOM_DISABLE_CRON_QUOTA_PREFLIGHT !== "1";
13546
+ const quotaGate = quotaPreflightEnabled ? async () => {
13547
+ const client = new AuthBrokerClient;
13548
+ try {
13549
+ return decideQuotaPreflight(await client.listState());
13550
+ } finally {
13551
+ await client.close().catch(() => {});
13552
+ }
13553
+ } : undefined;
12916
13554
  const tasks = registerAgentSchedule({
12917
13555
  entries,
12918
13556
  channel,
12919
13557
  sink,
12920
13558
  cronLib,
12921
- dispatcher
13559
+ dispatcher,
13560
+ ...quotaGate ? { quotaGate } : {}
12922
13561
  });
12923
13562
  process.stdout.write(`agent-scheduler: ${agentName} registered ${tasks.length} task(s); ` + `chat=${channel.chatId} thread=${channel.threadId ?? "(none)"} ` + `socket=${socketPath} jsonl=${jsonlPath}
12924
13563
  `);
@@ -12286,6 +12286,35 @@ async function fetchQuota(opts) {
12286
12286
  return parsed;
12287
12287
  }
12288
12288
 
12289
+ // src/auth/broker/consumer-quota-sensor.ts
12290
+ var EXHAUSTION_PCT = 99.5;
12291
+ var DEFAULT_CONSUMER_PROBE_INTERVAL_MS = 10 * 60 * 1000;
12292
+ function quotaIndicatesExhaustion(result) {
12293
+ if (!result.ok)
12294
+ return { exhausted: false, until: null };
12295
+ const d = result.data;
12296
+ const fiveBlocked = d.fiveHourUtilizationPct >= EXHAUSTION_PCT;
12297
+ const sevenBlocked = d.sevenDayUtilizationPct >= EXHAUSTION_PCT;
12298
+ if (!fiveBlocked && !sevenBlocked)
12299
+ return { exhausted: false, until: null };
12300
+ const fiveReset = fiveBlocked ? d.fiveHourResetAt?.getTime() ?? null : null;
12301
+ const sevenReset = sevenBlocked ? d.sevenDayResetAt?.getTime() ?? null : null;
12302
+ const candidates = [fiveReset, sevenReset].filter((x) => x != null);
12303
+ const until = candidates.length > 0 ? Math.max(...candidates) : null;
12304
+ return { exhausted: true, until };
12305
+ }
12306
+ function resolveConsumerProbeIntervalMs(env) {
12307
+ if (env.SWITCHROOM_DISABLE_CONSUMER_QUOTA_PROBE === "1")
12308
+ return 0;
12309
+ const raw = env.SWITCHROOM_CONSUMER_QUOTA_PROBE_MS;
12310
+ if (raw !== undefined) {
12311
+ const n = Number(raw);
12312
+ if (Number.isFinite(n) && n >= 0)
12313
+ return n;
12314
+ }
12315
+ return DEFAULT_CONSUMER_PROBE_INTERVAL_MS;
12316
+ }
12317
+
12289
12318
  // src/util/atomic.ts
12290
12319
  import { randomBytes } from "node:crypto";
12291
12320
  import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
@@ -13483,6 +13512,8 @@ class AuthBroker {
13483
13512
  config;
13484
13513
  listeners = new Map;
13485
13514
  refreshTimer = null;
13515
+ consumerProbeTimer = null;
13516
+ fetchQuotaImpl;
13486
13517
  stateDir;
13487
13518
  socketRoot;
13488
13519
  home;
@@ -13506,6 +13537,7 @@ class AuthBroker {
13506
13537
  this.now = opts.now ?? nowMs;
13507
13538
  this.operatorUid = opts.operatorUid;
13508
13539
  this.fetcher = opts.fetcher;
13540
+ this.fetchQuotaImpl = opts._testFetchQuota ?? fetchQuota;
13509
13541
  this.stateDir = opts.stateDir ?? resolve7(this.homeRoot(), ".switchroom", "state", "auth-broker");
13510
13542
  this.socketRoot = opts.socketRoot ?? AUTH_BROKER_ROOT;
13511
13543
  this.providers = new ProviderRegistry;
@@ -13552,6 +13584,16 @@ class AuthBroker {
13552
13584
  });
13553
13585
  }, REFRESH_TICK_INTERVAL_MS);
13554
13586
  this.refreshTimer.unref();
13587
+ const probeMs = resolveConsumerProbeIntervalMs(process.env);
13588
+ const hasConsumers = (this.config.auth?.consumers ?? []).length > 0;
13589
+ if (probeMs > 0 && hasConsumers) {
13590
+ this.consumerProbeTimer = setInterval(() => {
13591
+ this.consumerQuotaProbeTick().catch((err) => {
13592
+ this.logErr(`consumer-quota-probe threw: ${err.message}`);
13593
+ });
13594
+ }, probeMs);
13595
+ this.consumerProbeTimer.unref();
13596
+ }
13555
13597
  }
13556
13598
  const fanned = this.fanoutAll();
13557
13599
  if (fanned.length > 0) {
@@ -13578,6 +13620,10 @@ class AuthBroker {
13578
13620
  clearInterval(this.refreshTimer);
13579
13621
  this.refreshTimer = null;
13580
13622
  }
13623
+ if (this.consumerProbeTimer) {
13624
+ clearInterval(this.consumerProbeTimer);
13625
+ this.consumerProbeTimer = null;
13626
+ }
13581
13627
  for (const [sock, lis] of this.listeners) {
13582
13628
  try {
13583
13629
  lis.server.close();
@@ -13995,7 +14041,7 @@ class AuthBroker {
13995
14041
  this.audit({ op: "probe-quota", identity: identity2, account: label, ok: false, error: "missing-credentials" });
13996
14042
  return { label, result: result2 };
13997
14043
  }
13998
- const result = await fetchQuota({ accessToken: token, timeoutMs });
14044
+ const result = await this.fetchQuotaImpl({ accessToken: token, timeoutMs });
13999
14045
  this.audit({
14000
14046
  op: "probe-quota",
14001
14047
  identity: identity2,
@@ -14019,6 +14065,34 @@ class AuthBroker {
14019
14065
  }));
14020
14066
  socket.write(encodeSuccess(id, { results }));
14021
14067
  }
14068
+ async consumerQuotaProbeTick() {
14069
+ const accounts = Array.from(new Set((this.config.auth?.consumers ?? []).map((c) => c.account)));
14070
+ for (const label of accounts) {
14071
+ const creds = readAccountCredentials(label, this.home);
14072
+ const token = creds?.claudeAiOauth?.accessToken;
14073
+ if (!token)
14074
+ continue;
14075
+ let result;
14076
+ try {
14077
+ result = await this.fetchQuotaImpl({ accessToken: token });
14078
+ } catch (err) {
14079
+ this.logErr(`consumer-quota-probe ${label}: ${err.message}`);
14080
+ continue;
14081
+ }
14082
+ const decision = quotaIndicatesExhaustion(result);
14083
+ if (!decision.exhausted)
14084
+ continue;
14085
+ const exhaustedUntil = decision.until ?? this.now() + MARK_EXHAUSTED_DEFAULT_MS;
14086
+ const existing = this.quota[label]?.exhausted_until;
14087
+ if (existing !== undefined && existing >= exhaustedUntil)
14088
+ continue;
14089
+ this.quota[label] = { exhausted_until: exhaustedUntil };
14090
+ this.persistQuota();
14091
+ this.audit({ op: "mark-exhausted", identity: { kind: "operator" }, account: label, ok: true });
14092
+ process.stdout.write(`auth-broker: consumer-quota-sensor marked ${label} exhausted until ${new Date(exhaustedUntil).toISOString()} — consumer(s) fail over
14093
+ `);
14094
+ }
14095
+ }
14022
14096
  async opSetActive(socket, id, identity2, account) {
14023
14097
  if (!this.isAdmin(identity2)) {
14024
14098
  this.audit({ op: "set-active", identity: identity2, account, ok: false, error: "FORBIDDEN" });
@@ -49700,8 +49700,8 @@ var {
49700
49700
  } = import__.default;
49701
49701
 
49702
49702
  // src/build-info.ts
49703
- var VERSION = "0.14.80";
49704
- var COMMIT_SHA = "1198bdb5";
49703
+ var VERSION = "0.14.82";
49704
+ var COMMIT_SHA = "91bc41d1";
49705
49705
 
49706
49706
  // src/cli/agent.ts
49707
49707
  init_source();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.80",
3
+ "version": "0.14.82",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52810,11 +52810,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52810
52810
  }
52811
52811
 
52812
52812
  // ../src/build-info.ts
52813
- var VERSION = "0.14.80";
52814
- var COMMIT_SHA = "1198bdb5";
52815
- var COMMIT_DATE = "2026-06-07T09:36:25+10:00";
52813
+ var VERSION = "0.14.82";
52814
+ var COMMIT_SHA = "91bc41d1";
52815
+ var COMMIT_DATE = "2026-06-07T12:22:49+10:00";
52816
52816
  var LATEST_PR = null;
52817
- var COMMITS_AHEAD_OF_TAG = 1;
52817
+ var COMMITS_AHEAD_OF_TAG = 2;
52818
52818
 
52819
52819
  // gateway/boot-version.ts
52820
52820
  function formatRelativeAgo(iso) {