switchroom 0.14.81 → 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
  `);
@@ -49700,8 +49700,8 @@ var {
49700
49700
  } = import__.default;
49701
49701
 
49702
49702
  // src/build-info.ts
49703
- var VERSION = "0.14.81";
49704
- var COMMIT_SHA = "4ac9cc7d";
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.81",
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,9 +52810,9 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52810
52810
  }
52811
52811
 
52812
52812
  // ../src/build-info.ts
52813
- var VERSION = "0.14.81";
52814
- var COMMIT_SHA = "4ac9cc7d";
52815
- var COMMIT_DATE = "2026-06-07T10:43:55+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
52817
  var COMMITS_AHEAD_OF_TAG = 2;
52818
52818