switchroom 0.14.0 → 0.14.1

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.
@@ -28823,6 +28823,115 @@ var init_boot_issue_cache = __esm(() => {
28823
28823
  EMPTY_CACHE = { schema: 1, probes: {} };
28824
28824
  });
28825
28825
 
28826
+ // gateway/config-snapshot.ts
28827
+ import { createHash as createHash2 } from "crypto";
28828
+ import { existsSync as existsSync26, readFileSync as readFileSync25, writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, renameSync as renameSync10 } from "fs";
28829
+ import { dirname as dirname10 } from "path";
28830
+ function hashStringArray(items) {
28831
+ if (!items || items.length === 0)
28832
+ return null;
28833
+ const sorted = [...items].sort();
28834
+ const raw = createHash2("sha256").update(sorted.join("\x00")).digest("hex");
28835
+ return raw.slice(0, 12);
28836
+ }
28837
+ function normalizeModel(model) {
28838
+ if (!model || model.trim().length === 0)
28839
+ return null;
28840
+ return model.trim().toLowerCase();
28841
+ }
28842
+ function captureConfigSnapshot(input) {
28843
+ return {
28844
+ schema: 1,
28845
+ capturedAtMs: (input.now ?? Date.now)(),
28846
+ model: normalizeModel(input.model),
28847
+ toolsHash: hashStringArray(input.toolsAllow ?? null),
28848
+ skillsHash: hashStringArray(input.skills ?? null),
28849
+ memoryBackend: input.memoryCollection?.trim() || null
28850
+ };
28851
+ }
28852
+ function diffSnapshots(current, previous) {
28853
+ if (previous === null)
28854
+ return [];
28855
+ const changes = [];
28856
+ if (current.model !== previous.model) {
28857
+ changes.push({ field: "model", from: previous.model, to: current.model });
28858
+ }
28859
+ if (current.toolsHash !== previous.toolsHash) {
28860
+ changes.push({ field: "tools", from: previous.toolsHash, to: current.toolsHash });
28861
+ }
28862
+ if (current.skillsHash !== previous.skillsHash) {
28863
+ changes.push({ field: "skills", from: previous.skillsHash, to: current.skillsHash });
28864
+ }
28865
+ if (current.memoryBackend !== previous.memoryBackend) {
28866
+ changes.push({
28867
+ field: "memoryBackend",
28868
+ from: previous.memoryBackend,
28869
+ to: current.memoryBackend
28870
+ });
28871
+ }
28872
+ return changes;
28873
+ }
28874
+ function renderConfigChangeDim(dim) {
28875
+ switch (dim.field) {
28876
+ case "model": {
28877
+ const from = escapeHtml8(dim.from ?? "(default)");
28878
+ const to = escapeHtml8(dim.to ?? "(default)");
28879
+ return `\u2699\ufe0f <b>Config</b> model: ${from} \u2192 ${to}`;
28880
+ }
28881
+ case "memoryBackend": {
28882
+ const from = escapeHtml8(dim.from ?? "(default)");
28883
+ const to = escapeHtml8(dim.to ?? "(default)");
28884
+ return `\u2699\ufe0f <b>Config</b> memory backend: ${from} \u2192 ${to}`;
28885
+ }
28886
+ case "tools":
28887
+ return `\u2699\ufe0f <b>Config</b> tools allowlist changed \u2014 run /status for details`;
28888
+ case "skills":
28889
+ return `\u2699\ufe0f <b>Config</b> skills changed \u2014 run /status for details`;
28890
+ }
28891
+ }
28892
+ function loadSnapshot(path, now = Date.now) {
28893
+ if (!existsSync26(path))
28894
+ return null;
28895
+ let raw;
28896
+ try {
28897
+ raw = readFileSync25(path, "utf-8");
28898
+ } catch {
28899
+ return null;
28900
+ }
28901
+ let parsed;
28902
+ try {
28903
+ parsed = JSON.parse(raw);
28904
+ } catch {
28905
+ try {
28906
+ renameSync10(path, `${path}.corrupt-${now()}`);
28907
+ } catch {}
28908
+ return null;
28909
+ }
28910
+ const obj = parsed;
28911
+ if (!obj || obj.schema !== 1)
28912
+ return null;
28913
+ if (typeof obj.capturedAtMs !== "number" || !("model" in obj) || !("toolsHash" in obj) || !("skillsHash" in obj) || !("memoryBackend" in obj)) {
28914
+ return null;
28915
+ }
28916
+ return {
28917
+ schema: 1,
28918
+ capturedAtMs: obj.capturedAtMs,
28919
+ model: obj.model ?? null,
28920
+ toolsHash: obj.toolsHash ?? null,
28921
+ skillsHash: obj.skillsHash ?? null,
28922
+ memoryBackend: obj.memoryBackend ?? null
28923
+ };
28924
+ }
28925
+ function persistSnapshot(path, snapshot) {
28926
+ try {
28927
+ mkdirSync13(dirname10(path), { recursive: true });
28928
+ const tmp = `${path}.tmp`;
28929
+ writeFileSync15(tmp, JSON.stringify(snapshot), { mode: 384 });
28930
+ renameSync10(tmp, path);
28931
+ } catch {}
28932
+ }
28933
+ var init_config_snapshot = () => {};
28934
+
28826
28935
  // gateway/boot-card.ts
28827
28936
  var exports_boot_card = {};
28828
28937
  __export(exports_boot_card, {
@@ -28899,6 +29008,12 @@ function renderBootCard(opts) {
28899
29008
  }
28900
29009
  }
28901
29010
  const accountRows = opts.accounts ? renderAuthLine(opts.accounts, agentName3, (opts.now ?? new Date).getTime()) : [];
29011
+ const configChangeRows = [];
29012
+ if (opts.configChanges && opts.configChanges.length > 0) {
29013
+ for (const dim of opts.configChanges) {
29014
+ configChangeRows.push(renderConfigChangeDim(dim));
29015
+ }
29016
+ }
28902
29017
  const sections = [ack];
28903
29018
  if (degradedRows.length > 0)
28904
29019
  sections.push("", ...degradedRows);
@@ -28908,6 +29023,9 @@ function renderBootCard(opts) {
28908
29023
  sections.push("", ...opts.updateOutcomeLine.split(`
28909
29024
  `));
28910
29025
  }
29026
+ if (configChangeRows.length > 0) {
29027
+ sections.push("", ...configChangeRows);
29028
+ }
28911
29029
  if (sections.length === 1)
28912
29030
  return ack;
28913
29031
  return sections.join(`
@@ -29011,6 +29129,38 @@ async function startBootCard(chatId, threadId, bot, opts, ackMessageId, log) {
29011
29129
  applyAndSave(opts.bootIssueCachePath, cache, diff);
29012
29130
  } catch (diffErr) {
29013
29131
  logger2(`telegram gateway: boot-card: issue-dedup diff failed: ${diffErr?.message ?? String(diffErr)}
29132
+ `);
29133
+ }
29134
+ }
29135
+ let configChanges = [];
29136
+ if (opts.configSnapshotPath) {
29137
+ try {
29138
+ const agentSlug = opts.agentSlug ?? opts.agentName;
29139
+ const agentName3 = process.env.SWITCHROOM_AGENT_NAME ?? agentSlug;
29140
+ let currentCfg;
29141
+ try {
29142
+ const loaded = loadConfig();
29143
+ const rawAgent = loaded.agents?.[agentName3] ?? {};
29144
+ const resolved = resolveAgentConfig(loaded.defaults, loaded.profiles, rawAgent);
29145
+ currentCfg = captureConfigSnapshot({
29146
+ agentName: agentName3,
29147
+ model: resolved.model,
29148
+ toolsAllow: resolved.tools?.allow,
29149
+ skills: resolved.skills,
29150
+ memoryCollection: resolved.memory?.collection
29151
+ });
29152
+ } catch {}
29153
+ if (currentCfg != null) {
29154
+ const previousSnapshot = loadSnapshot(opts.configSnapshotPath);
29155
+ configChanges = diffSnapshots(currentCfg, previousSnapshot);
29156
+ persistSnapshot(opts.configSnapshotPath, currentCfg);
29157
+ if (configChanges.length > 0) {
29158
+ logger2(`telegram gateway: boot-card: config-snapshot diff detected ${configChanges.length} change(s): ${configChanges.map((d) => d.field).join(", ")}
29159
+ `);
29160
+ }
29161
+ }
29162
+ } catch (snapErr) {
29163
+ logger2(`telegram gateway: boot-card: config-snapshot diff failed: ${snapErr?.message ?? String(snapErr)}
29014
29164
  `);
29015
29165
  }
29016
29166
  }
@@ -29024,7 +29174,8 @@ async function startBootCard(chatId, threadId, bot, opts, ackMessageId, log) {
29024
29174
  ...accountRows ? { accounts: accountRows } : {},
29025
29175
  ...resolvedRows.length > 0 ? { resolvedRows } : {},
29026
29176
  ...snoozeRows.length > 0 ? { snoozeRows } : {},
29027
- ...opts.updateOutcomeLine ? { updateOutcomeLine: opts.updateOutcomeLine } : {}
29177
+ ...opts.updateOutcomeLine ? { updateOutcomeLine: opts.updateOutcomeLine } : {},
29178
+ ...configChanges.length > 0 ? { configChanges } : {}
29028
29179
  });
29029
29180
  if (currentText !== ackText) {
29030
29181
  try {
@@ -29066,7 +29217,8 @@ async function startBootCard(chatId, threadId, bot, opts, ackMessageId, log) {
29066
29217
  ...accountRows ? { accounts: accountRows } : {},
29067
29218
  ...resolvedRows.length > 0 ? { resolvedRows } : {},
29068
29219
  ...snoozeRows.length > 0 ? { snoozeRows } : {},
29069
- ...opts.updateOutcomeLine ? { updateOutcomeLine: opts.updateOutcomeLine } : {}
29220
+ ...opts.updateOutcomeLine ? { updateOutcomeLine: opts.updateOutcomeLine } : {},
29221
+ ...configChanges.length > 0 ? { configChanges } : {}
29070
29222
  });
29071
29223
  if (updatedText === currentText)
29072
29224
  continue;
@@ -29108,7 +29260,9 @@ var SETTLE_WINDOW_MS = 6000, DOT, PROBE_LABELS, PROBE_KEYS, REASON_EMOJI, REASON
29108
29260
  var init_boot_card = __esm(() => {
29109
29261
  init_boot_probes();
29110
29262
  init_boot_issue_cache();
29263
+ init_config_snapshot();
29111
29264
  init_loader();
29265
+ init_merge();
29112
29266
  DOT = {
29113
29267
  ok: "\uD83D\uDFE2",
29114
29268
  degraded: "\uD83D\uDFE1",
@@ -29150,17 +29304,357 @@ var init_boot_card = __esm(() => {
29150
29304
  };
29151
29305
  });
29152
29306
 
29307
+ // auth-snapshot-format.ts
29308
+ var exports_auth_snapshot_format = {};
29309
+ __export(exports_auth_snapshot_format, {
29310
+ reviveLastQuota: () => reviveLastQuota,
29311
+ renderFallbackAnnouncement: () => renderFallbackAnnouncement2,
29312
+ renderAuthSnapshotFormat2: () => renderAuthSnapshotFormat22,
29313
+ recommendation: () => recommendation2,
29314
+ formatRelative: () => formatRelative2,
29315
+ formatAbsolute: () => formatAbsolute2,
29316
+ fmtPct: () => fmtPct2,
29317
+ classifyHealth: () => classifyHealth2,
29318
+ buildSnapshotsFromState: () => buildSnapshotsFromState2,
29319
+ buildSnapshotsFromCachedState: () => buildSnapshotsFromCachedState,
29320
+ buildSnapshotKeyboard: () => buildSnapshotKeyboard2,
29321
+ bindingWindow: () => bindingWindow3,
29322
+ THROTTLING_THRESHOLD_PCT: () => THROTTLING_THRESHOLD_PCT2
29323
+ });
29324
+ function classifyHealth2(snap) {
29325
+ if (!snap.quota)
29326
+ return "unknown";
29327
+ const q = snap.quota;
29328
+ const max = Math.max(q.fiveHourUtilizationPct, q.sevenDayUtilizationPct);
29329
+ if (max >= 99.5)
29330
+ return "blocked";
29331
+ if (max >= THROTTLING_THRESHOLD_PCT2)
29332
+ return "throttling";
29333
+ return "healthy";
29334
+ }
29335
+ function bindingWindow3(q) {
29336
+ if (q.representativeClaim === "seven_day")
29337
+ return "7d";
29338
+ if (q.representativeClaim === "five_hour")
29339
+ return "5h";
29340
+ return q.sevenDayUtilizationPct >= q.fiveHourUtilizationPct ? "7d" : "5h";
29341
+ }
29342
+ function formatRelative2(target, now = new Date) {
29343
+ if (!target)
29344
+ return "\u2014";
29345
+ const deltaMs = target.getTime() - now.getTime();
29346
+ if (deltaMs <= 0)
29347
+ return "now";
29348
+ const totalMin = Math.round(deltaMs / 60000);
29349
+ if (totalMin < 60)
29350
+ return `${totalMin}m`;
29351
+ const h = Math.floor(totalMin / 60);
29352
+ const m = totalMin % 60;
29353
+ if (h < 24)
29354
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
29355
+ const d = Math.floor(h / 24);
29356
+ const rh = h % 24;
29357
+ return rh > 0 ? `${d}d ${rh}h` : `${d}d`;
29358
+ }
29359
+ function formatAbsolute2(target, tz = "UTC") {
29360
+ if (!target)
29361
+ return "\u2014";
29362
+ return target.toLocaleString("en-US", {
29363
+ timeZone: tz,
29364
+ weekday: "short",
29365
+ hour: "numeric",
29366
+ minute: "2-digit",
29367
+ hour12: true
29368
+ });
29369
+ }
29370
+ function fmtPct2(pct) {
29371
+ return `${Math.round(pct)}%`;
29372
+ }
29373
+ function groupHeader2(health, count) {
29374
+ const emoji = HEALTH_EMOJI2[health];
29375
+ const title = HEALTH_TITLE2[health];
29376
+ return `${emoji} <b>${title}</b> (${count})`;
29377
+ }
29378
+ function renderAccountRow2(snap, opts) {
29379
+ const now = opts.now ?? new Date;
29380
+ const tz = opts.tz ?? "UTC";
29381
+ const lines = [];
29382
+ const marker = snap.isActive ? "\u25cf " : "";
29383
+ if (!snap.quota) {
29384
+ lines.push(`${marker}<code>${escapeHtml12(snap.label)}</code> <i>quota probe failed</i>`);
29385
+ if (snap.quotaError) {
29386
+ lines.push(` <i>${escapeHtml12(snap.quotaError)}</i>`);
29387
+ }
29388
+ return lines;
29389
+ }
29390
+ const q = snap.quota;
29391
+ const fiveStr = fmtPct2(q.fiveHourUtilizationPct);
29392
+ const sevenStr = fmtPct2(q.sevenDayUtilizationPct);
29393
+ lines.push(`${marker}<code>${escapeHtml12(snap.label)}</code> ${fiveStr} / ${sevenStr}`);
29394
+ const health = classifyHealth2(snap);
29395
+ if (health === "blocked") {
29396
+ const win = bindingWindow3(q);
29397
+ const reset2 = win === "5h" ? q.fiveHourResetAt : q.sevenDayResetAt;
29398
+ const winLabel = win === "5h" ? "5-hour" : "7-day";
29399
+ lines.push(` <i>back ${formatAbsolute2(reset2, tz)} (in ${formatRelative2(reset2, now)}, ${winLabel} cap)</i>`);
29400
+ return lines;
29401
+ }
29402
+ const fiveResetIn = q.fiveHourResetAt ? q.fiveHourResetAt.getTime() - now.getTime() : Infinity;
29403
+ const sevenResetIn = q.sevenDayResetAt ? q.sevenDayResetAt.getTime() - now.getTime() : Infinity;
29404
+ const fiveFirst = fiveResetIn <= sevenResetIn;
29405
+ const fiveSeg = q.fiveHourResetAt ? `5h refills ${formatAbsolute2(q.fiveHourResetAt, tz)} (in ${formatRelative2(q.fiveHourResetAt, now)})` : "5h refills \u2014";
29406
+ const sevenSeg = q.sevenDayResetAt ? `7d resets ${formatAbsolute2(q.sevenDayResetAt, tz)} (in ${formatRelative2(q.sevenDayResetAt, now)})` : "7d resets \u2014";
29407
+ lines.push(` <i>${fiveFirst ? fiveSeg : sevenSeg} \u00b7 ${fiveFirst ? sevenSeg : fiveSeg}</i>`);
29408
+ return lines;
29409
+ }
29410
+ function renderAuthSnapshotFormat22(snapshots, opts = {}) {
29411
+ const now = opts.now ?? new Date;
29412
+ const lines = [];
29413
+ lines.push("\uD83D\uDD0B <b>Auth \u2014 fleet status</b>");
29414
+ const order = ["blocked", "throttling", "healthy", "unknown"];
29415
+ const grouped = new Map;
29416
+ for (const s of snapshots) {
29417
+ const h = classifyHealth2(s);
29418
+ if (!grouped.has(h))
29419
+ grouped.set(h, []);
29420
+ grouped.get(h).push(s);
29421
+ }
29422
+ for (const arr of grouped.values()) {
29423
+ arr.sort((a, b) => Number(b.isActive) - Number(a.isActive));
29424
+ }
29425
+ for (const h of order) {
29426
+ const arr = grouped.get(h);
29427
+ if (!arr || arr.length === 0)
29428
+ continue;
29429
+ lines.push("");
29430
+ lines.push(groupHeader2(h, arr.length));
29431
+ for (const s of arr) {
29432
+ for (const ln of renderAccountRow2(s, opts))
29433
+ lines.push(ln);
29434
+ }
29435
+ }
29436
+ lines.push("");
29437
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
29438
+ lines.push(`<i>${recommendation2(snapshots, now)}</i>`);
29439
+ if (opts.liveProbedAtMs != null) {
29440
+ const ageSec = Math.max(0, Math.round((Date.now() - opts.liveProbedAtMs) / 1000));
29441
+ const ageStr = ageSec < 60 ? `${ageSec}s ago` : `${Math.round(ageSec / 60)}m ago`;
29442
+ lines.push(`<i>Live \u00b7 refreshed ${ageStr}</i>`);
29443
+ } else {
29444
+ lines.push("<i>Live</i>");
29445
+ }
29446
+ return lines.join(`
29447
+ `);
29448
+ }
29449
+ function recommendation2(snapshots, now = new Date) {
29450
+ const active = snapshots.find((s) => s.isActive);
29451
+ if (!active)
29452
+ return "No active account set.";
29453
+ const activeHealth = classifyHealth2(active);
29454
+ const others = snapshots.filter((s) => !s.isActive);
29455
+ const healthyAlt = others.find((s) => classifyHealth2(s) === "healthy");
29456
+ if (activeHealth === "healthy") {
29457
+ return `Recommendation: stay on ${active.label}.`;
29458
+ }
29459
+ if (activeHealth === "throttling") {
29460
+ if (healthyAlt) {
29461
+ return `Recommendation: active ${active.label} is throttling. Switch to ${healthyAlt.label} for headroom.`;
29462
+ }
29463
+ return `Recommendation: active ${active.label} is throttling; no healthy alternative \u2014 wait for refill.`;
29464
+ }
29465
+ if (activeHealth === "blocked") {
29466
+ if (healthyAlt) {
29467
+ return `Recommendation: active ${active.label} is BLOCKED \u2014 switch to ${healthyAlt.label} now.`;
29468
+ }
29469
+ const earliestRecovery = pickEarliestRecovery2(snapshots, now);
29470
+ if (earliestRecovery) {
29471
+ return `All accounts blocked. Earliest recovery: ${earliestRecovery.label} in ${formatRelative2(earliestRecovery.at, now)}.`;
29472
+ }
29473
+ return `All accounts blocked. Run /auth add to attach another subscription.`;
29474
+ }
29475
+ return `Active ${active.label}: quota probe failed; broker last_seen unknown.`;
29476
+ }
29477
+ function pickEarliestRecovery2(snapshots, now) {
29478
+ let best = null;
29479
+ for (const s of snapshots) {
29480
+ if (!s.quota)
29481
+ continue;
29482
+ const win = bindingWindow3(s.quota);
29483
+ const at = win === "5h" ? s.quota.fiveHourResetAt : s.quota.sevenDayResetAt;
29484
+ if (!at || at.getTime() <= now.getTime())
29485
+ continue;
29486
+ if (!best || at.getTime() < best.at.getTime()) {
29487
+ best = { label: s.label, at };
29488
+ }
29489
+ }
29490
+ return best;
29491
+ }
29492
+ function renderFallbackAnnouncement2(input) {
29493
+ const now = input.now ?? new Date;
29494
+ const tz = input.tz ?? "UTC";
29495
+ const lines = [];
29496
+ const limitWord = input.oldQuota ? limitWordFor2(input.oldQuota) : "quota";
29497
+ const headerLimit = limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
29498
+ if (!input.newLabel) {
29499
+ lines.push(`\uD83D\uDD34 <b>All accounts blocked \u00b7 ${headerLimit} on ${escapeHtml12(input.oldLabel)}</b>`);
29500
+ lines.push("");
29501
+ lines.push(`Triggered by: agent <b>${escapeHtml12(input.triggerAgent)}</b>`);
29502
+ if (input.oldQuota) {
29503
+ const recovery = recoveryAtFor2(input.oldQuota);
29504
+ if (recovery) {
29505
+ lines.push(`${escapeHtml12(input.oldLabel)} recovers ${formatAbsolute2(recovery, tz)} ` + `(in ${formatRelative2(recovery, now)})`);
29506
+ }
29507
+ }
29508
+ lines.push("");
29509
+ lines.push(`Run <code>/auth add &lt;label&gt;</code> to attach another subscription, ` + `or <code>/auth refresh</code> to re-probe.`);
29510
+ return lines.join(`
29511
+ `);
29512
+ }
29513
+ lines.push(`\u2713 <b>Switched fleet \u00b7 ${headerLimit} on ${escapeHtml12(input.oldLabel)}</b>`);
29514
+ lines.push("");
29515
+ lines.push(`<code>${escapeHtml12(input.oldLabel)}</code> \u2192 <code>${escapeHtml12(input.newLabel)}</code>`);
29516
+ lines.push(`Triggered by: agent <b>${escapeHtml12(input.triggerAgent)}</b>`);
29517
+ lines.push("");
29518
+ if (input.oldQuota) {
29519
+ const recovery = recoveryAtFor2(input.oldQuota);
29520
+ if (recovery) {
29521
+ lines.push(`<code>${escapeHtml12(input.oldLabel)}</code> recovers ` + `${formatAbsolute2(recovery, tz)} (in ${formatRelative2(recovery, now)})`);
29522
+ }
29523
+ }
29524
+ if (input.newQuota) {
29525
+ const fiveStr = fmtPct2(input.newQuota.fiveHourUtilizationPct);
29526
+ const sevenStr = fmtPct2(input.newQuota.sevenDayUtilizationPct);
29527
+ const hasHeadroom = input.newQuota.fiveHourUtilizationPct < THROTTLING_THRESHOLD_PCT2 && input.newQuota.sevenDayUtilizationPct < THROTTLING_THRESHOLD_PCT2;
29528
+ const headroomStr = hasHeadroom ? "<i>(plenty of headroom)</i>" : "<i>(near limit \u2014 watch this)</i>";
29529
+ lines.push(`<code>${escapeHtml12(input.newLabel)}</code> now: ${fiveStr} of 5h \u00b7 ${sevenStr} of 7d ${headroomStr}`);
29530
+ } else {
29531
+ lines.push(`<i>(quota probe for new account is pending \u2014 will reflect on next /auth)</i>`);
29532
+ }
29533
+ return lines.join(`
29534
+ `);
29535
+ }
29536
+ function limitWordFor2(q) {
29537
+ if (q.representativeClaim === "seven_day" && q.sevenDayUtilizationPct >= 99)
29538
+ return "7-day";
29539
+ if (q.representativeClaim === "five_hour" && q.fiveHourUtilizationPct >= 99)
29540
+ return "5-hour";
29541
+ if (q.sevenDayUtilizationPct >= 99)
29542
+ return "7-day";
29543
+ if (q.fiveHourUtilizationPct >= 99)
29544
+ return "5-hour";
29545
+ return q.sevenDayUtilizationPct >= q.fiveHourUtilizationPct ? "7-day" : "5-hour";
29546
+ }
29547
+ function recoveryAtFor2(q) {
29548
+ const word = limitWordFor2(q);
29549
+ if (word === "7-day")
29550
+ return q.sevenDayResetAt;
29551
+ if (word === "5-hour")
29552
+ return q.fiveHourResetAt;
29553
+ if (!q.fiveHourResetAt)
29554
+ return q.sevenDayResetAt;
29555
+ if (!q.sevenDayResetAt)
29556
+ return q.fiveHourResetAt;
29557
+ return q.fiveHourResetAt.getTime() < q.sevenDayResetAt.getTime() ? q.fiveHourResetAt : q.sevenDayResetAt;
29558
+ }
29559
+ function buildSnapshotKeyboard2(snapshots, opts = {}) {
29560
+ const max = opts.maxSwitchButtons ?? 3;
29561
+ const rows = [];
29562
+ const switchTargets = snapshots.filter((s) => !s.isActive).sort((a, b) => switchPriority2(a) - switchPriority2(b)).filter((s) => classifyHealth2(s) !== "blocked" && classifyHealth2(s) !== "unknown").slice(0, max);
29563
+ for (const t of switchTargets) {
29564
+ rows.push([
29565
+ {
29566
+ text: `Switch fleet \u2192 ${t.label}`,
29567
+ callbackData: `auth:use:${t.label}`
29568
+ }
29569
+ ]);
29570
+ }
29571
+ rows.push([
29572
+ { text: "\u21bb Refresh", callbackData: "auth:refresh" },
29573
+ { text: "/usage", insertText: "/usage" },
29574
+ { text: "+ Add", insertText: "/auth add " }
29575
+ ]);
29576
+ return rows;
29577
+ }
29578
+ function switchPriority2(s) {
29579
+ const h = classifyHealth2(s);
29580
+ if (h === "healthy")
29581
+ return 0;
29582
+ if (h === "throttling")
29583
+ return 1;
29584
+ if (h === "unknown")
29585
+ return 2;
29586
+ return 3;
29587
+ }
29588
+ function escapeHtml12(s) {
29589
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
29590
+ }
29591
+ function buildSnapshotsFromState2(state4, quotas) {
29592
+ const out = [];
29593
+ for (let i = 0;i < state4.accounts.length; i++) {
29594
+ const acc = state4.accounts[i];
29595
+ const q = quotas[i];
29596
+ out.push({
29597
+ label: acc.label,
29598
+ isActive: acc.label === state4.active,
29599
+ quota: q && q.ok ? q.data : null,
29600
+ quotaError: q && !q.ok ? q.reason : undefined,
29601
+ expiresAtMs: acc.expiresAt
29602
+ });
29603
+ }
29604
+ return out;
29605
+ }
29606
+ function reviveLastQuota(snap) {
29607
+ if (!snap)
29608
+ return null;
29609
+ return {
29610
+ fiveHourUtilizationPct: snap.fiveHourUtilizationPct,
29611
+ sevenDayUtilizationPct: snap.sevenDayUtilizationPct,
29612
+ fiveHourResetAt: snap.fiveHourResetAt ? new Date(snap.fiveHourResetAt) : null,
29613
+ sevenDayResetAt: snap.sevenDayResetAt ? new Date(snap.sevenDayResetAt) : null,
29614
+ representativeClaim: snap.representativeClaim,
29615
+ overageStatus: snap.overageStatus,
29616
+ overageDisabledReason: snap.overageDisabledReason
29617
+ };
29618
+ }
29619
+ function buildSnapshotsFromCachedState(state4) {
29620
+ return state4.accounts.map((acc) => {
29621
+ const lq = acc.last_quota ?? null;
29622
+ return {
29623
+ label: acc.label,
29624
+ isActive: acc.label === state4.active,
29625
+ quota: reviveLastQuota(lq),
29626
+ quotaError: lq ? undefined : "no cached quota (no probe since broker start)",
29627
+ expiresAtMs: acc.expiresAt
29628
+ };
29629
+ });
29630
+ }
29631
+ var THROTTLING_THRESHOLD_PCT2 = 80, HEALTH_EMOJI2, HEALTH_TITLE2;
29632
+ var init_auth_snapshot_format = __esm(() => {
29633
+ HEALTH_EMOJI2 = {
29634
+ healthy: "\uD83D\uDFE2",
29635
+ throttling: "\uD83D\uDFE1",
29636
+ blocked: "\uD83D\uDD34",
29637
+ unknown: "\u26aa"
29638
+ };
29639
+ HEALTH_TITLE2 = {
29640
+ healthy: "HEALTHY",
29641
+ throttling: "THROTTLING",
29642
+ blocked: "BLOCKED",
29643
+ unknown: "UNKNOWN"
29644
+ };
29645
+ });
29646
+
29153
29647
  // ../src/vault/flock.ts
29154
29648
  var init_flock = () => {};
29155
29649
 
29156
29650
  // ../src/vault/vault.ts
29157
29651
  import { randomBytes as randomBytes5, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
29158
29652
  import {
29159
- readFileSync as readFileSync31,
29160
- writeFileSync as writeFileSync19,
29161
- existsSync as existsSync32,
29162
- renameSync as renameSync11,
29163
- mkdirSync as mkdirSync18,
29653
+ readFileSync as readFileSync33,
29654
+ writeFileSync as writeFileSync21,
29655
+ existsSync as existsSync34,
29656
+ renameSync as renameSync12,
29657
+ mkdirSync as mkdirSync20,
29164
29658
  unlinkSync as unlinkSync12,
29165
29659
  lstatSync,
29166
29660
  realpathSync
@@ -29196,12 +29690,12 @@ function normalizeSecrets(raw) {
29196
29690
  return out;
29197
29691
  }
29198
29692
  function openVault(passphrase, vaultPath) {
29199
- if (!existsSync32(vaultPath)) {
29693
+ if (!existsSync34(vaultPath)) {
29200
29694
  throw new VaultError(`Vault file not found: ${vaultPath}`);
29201
29695
  }
29202
29696
  let vaultFile;
29203
29697
  try {
29204
- vaultFile = JSON.parse(readFileSync31(vaultPath, "utf8"));
29698
+ vaultFile = JSON.parse(readFileSync33(vaultPath, "utf8"));
29205
29699
  } catch {
29206
29700
  throw new VaultError(`Failed to read vault file: ${vaultPath}`);
29207
29701
  }
@@ -29248,14 +29742,14 @@ var init_vault = __esm(() => {
29248
29742
  import {
29249
29743
  chmodSync as chmodSync4,
29250
29744
  closeSync as closeSync7,
29251
- mkdirSync as mkdirSync19,
29745
+ mkdirSync as mkdirSync21,
29252
29746
  mkdtempSync as mkdtempSync2,
29253
29747
  openSync as openSync7,
29254
29748
  rmSync as rmSync3,
29255
29749
  statSync as statSync11,
29256
29750
  writeSync as writeSync2
29257
29751
  } from "node:fs";
29258
- import { join as join31 } from "node:path";
29752
+ import { join as join32 } from "node:path";
29259
29753
  import { tmpdir } from "node:os";
29260
29754
  import { constants as fsConstants } from "node:fs";
29261
29755
  function isVaultReference(value) {
@@ -29307,11 +29801,11 @@ function materializationRoot() {
29307
29801
  return cachedRoot;
29308
29802
  const xdg = process.env.XDG_RUNTIME_DIR;
29309
29803
  if (xdg) {
29310
- const base = join31(xdg, "switchroom", "vault");
29311
- mkdirSync19(base, { recursive: true, mode: 448 });
29312
- cachedRoot = mkdtempSync2(join31(base, "run-"));
29804
+ const base = join32(xdg, "switchroom", "vault");
29805
+ mkdirSync21(base, { recursive: true, mode: 448 });
29806
+ cachedRoot = mkdtempSync2(join32(base, "run-"));
29313
29807
  } else {
29314
- cachedRoot = mkdtempSync2(join31(tmpdir(), "switchroom-vault-"));
29808
+ cachedRoot = mkdtempSync2(join32(tmpdir(), "switchroom-vault-"));
29315
29809
  }
29316
29810
  chmodSync4(cachedRoot, 448);
29317
29811
  return cachedRoot;
@@ -29326,13 +29820,13 @@ function writeFileExclusive(filePath, content) {
29326
29820
  }
29327
29821
  }
29328
29822
  function materializeFilesEntry(key, files) {
29329
- const dir = join31(materializationRoot(), key);
29823
+ const dir = join32(materializationRoot(), key);
29330
29824
  if (materializedDirs.has(dir)) {
29331
29825
  try {
29332
29826
  rmSync3(dir, { recursive: true, force: true });
29333
29827
  } catch {}
29334
29828
  }
29335
- mkdirSync19(dir, { recursive: true, mode: 448 });
29829
+ mkdirSync21(dir, { recursive: true, mode: 448 });
29336
29830
  chmodSync4(dir, 448);
29337
29831
  const st = statSync11(dir);
29338
29832
  if (typeof process.getuid === "function" && st.uid !== process.getuid()) {
@@ -29342,7 +29836,7 @@ function materializeFilesEntry(key, files) {
29342
29836
  if (filename.includes("/") || filename.includes("\\") || filename === ".." || filename === "." || filename.includes("\x00")) {
29343
29837
  throw new Error(`Refusing to materialize vault file with unsafe name: ${filename}`);
29344
29838
  }
29345
- const filePath = join31(dir, filename);
29839
+ const filePath = join32(dir, filename);
29346
29840
  const content = encoding === "base64" ? Buffer.from(value, "base64") : value;
29347
29841
  writeFileExclusive(filePath, content);
29348
29842
  }
@@ -29475,7 +29969,7 @@ __export(exports_materialize_bot_token, {
29475
29969
  materializeBotToken: () => materializeBotToken,
29476
29970
  BotTokenMaterializeError: () => BotTokenMaterializeError
29477
29971
  });
29478
- import { existsSync as existsSync33 } from "node:fs";
29972
+ import { existsSync as existsSync35 } from "node:fs";
29479
29973
  function pickConfiguredToken(config, agentName3) {
29480
29974
  if (agentName3) {
29481
29975
  const agent = config.agents?.[agentName3];
@@ -29489,7 +29983,7 @@ function tryDirectVaultRead(ref, config, passphrase) {
29489
29983
  if (!passphrase)
29490
29984
  return null;
29491
29985
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
29492
- if (!existsSync33(vaultPath))
29986
+ if (!existsSync35(vaultPath))
29493
29987
  return null;
29494
29988
  try {
29495
29989
  const secrets = openVault(passphrase, vaultPath);
@@ -29607,7 +30101,7 @@ function truncateDiffForCard(unifiedDiff, maxLines = 50, maxChars = 3000) {
29607
30101
  }
29608
30102
  return out === unifiedDiff ? out : out + sentinel;
29609
30103
  }
29610
- function escapeHtml11(s) {
30104
+ function escapeHtml13(s) {
29611
30105
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
29612
30106
  }
29613
30107
  function clipReason(reason) {
@@ -29618,10 +30112,10 @@ function clipReason(reason) {
29618
30112
  function buildConfigApprovalCardBody(args) {
29619
30113
  const safeReason = clipReason(args.reason);
29620
30114
  const render = (diff) => `\uD83D\uDEE0 <b>Config edit proposed</b>
29621
- ` + `Agent: <code>${escapeHtml11(args.agentName)}</code>
29622
- ` + `Reason: ${escapeHtml11(safeReason)}
30115
+ ` + `Agent: <code>${escapeHtml13(args.agentName)}</code>
30116
+ ` + `Reason: ${escapeHtml13(safeReason)}
29623
30117
 
29624
- ` + `<pre>${escapeHtml11(diff)}</pre>`;
30118
+ ` + `<pre>${escapeHtml13(diff)}</pre>`;
29625
30119
  return truncateRawToFit({
29626
30120
  raw: args.unifiedDiff,
29627
30121
  render,
@@ -29728,8 +30222,8 @@ async function handleRequestConfigFinalize(_client, msg, deps) {
29728
30222
  }
29729
30223
  pending.delete(msg.requestId);
29730
30224
  const body = msg.outcome === "applied" ? `\u2705 <b>Applied</b>${msg.detail ? `
29731
- ${escapeHtml11(msg.detail)}` : ""}` : `\u26a0\ufe0f <b>Reconcile failed; rolled back</b>${msg.detail ? `
29732
- ${escapeHtml11(msg.detail)}` : ""}`;
30225
+ ${escapeHtml13(msg.detail)}` : ""}` : `\u26a0\ufe0f <b>Reconcile failed; rolled back</b>${msg.detail ? `
30226
+ ${escapeHtml13(msg.detail)}` : ""}`;
29733
30227
  try {
29734
30228
  await deps.editCard({
29735
30229
  chatId: entry.chatId,
@@ -29794,7 +30288,7 @@ __export(exports_tmux, {
29794
30288
  captureAgentPane: () => captureAgentPane
29795
30289
  });
29796
30290
  import { execFileSync as execFileSync4 } from "node:child_process";
29797
- import { mkdirSync as mkdirSync20, readdirSync as readdirSync6, statSync as statSync12, unlinkSync as unlinkSync13, writeFileSync as writeFileSync20 } from "node:fs";
30291
+ import { mkdirSync as mkdirSync22, readdirSync as readdirSync6, statSync as statSync12, unlinkSync as unlinkSync13, writeFileSync as writeFileSync22 } from "node:fs";
29798
30292
  import { resolve as resolve7 } from "node:path";
29799
30293
  function captureAgentPane(opts) {
29800
30294
  const { agentName: agentName3, agentDir, reason } = opts;
@@ -29806,7 +30300,7 @@ function captureAgentPane(opts) {
29806
30300
  const reasonSlug = sanitizeReason(reason);
29807
30301
  const outPath = resolve7(outDir, `${ts}-${reasonSlug}.txt`);
29808
30302
  try {
29809
- mkdirSync20(outDir, { recursive: true, mode: 493 });
30303
+ mkdirSync22(outDir, { recursive: true, mode: 493 });
29810
30304
  } catch (err) {
29811
30305
  const msg = `mkdir crash-reports failed: ${err.message}`;
29812
30306
  console.error(`[tmux-capture] ${agentName3}: ${msg}`);
@@ -29840,7 +30334,7 @@ function captureAgentPane(opts) {
29840
30334
  ` + `
29841
30335
  `;
29842
30336
  try {
29843
- writeFileSync20(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
30337
+ writeFileSync22(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
29844
30338
  mode: 420
29845
30339
  });
29846
30340
  } catch (err) {
@@ -29998,367 +30492,54 @@ function registerApprovalsCommands(bot, opts) {
29998
30492
  await ctx.reply("Approval kernel unreachable (vault broker not running?).");
29999
30493
  return;
30000
30494
  }
30001
- if (decisions.length === 0) {
30002
- await ctx.reply(agentFilter ? `No active approvals for <code>${escapeHtml12(agentFilter)}</code>.` : "No active approvals.", { parse_mode: "HTML" });
30003
- return;
30004
- }
30005
- const byAgent = new Map;
30006
- for (const d of decisions)
30007
- byAgent.set(d.agent_unit, (byAgent.get(d.agent_unit) ?? 0) + 1);
30008
- const summary = Array.from(byAgent.entries()).map(([a, n]) => `\u2022 <b>${escapeHtml12(a)}</b>: ${n}`).join(`
30009
- `);
30010
- const detail = decisions.slice(0, 20).map((d) => {
30011
- const ttl = d.ttl_expires_at === null ? "always" : `until ${new Date(d.ttl_expires_at).toISOString().slice(0, 16).replace("T", " ")}`;
30012
- return `<code>${escapeHtml12(d.id.slice(0, 8))}</code> ` + `${escapeHtml12(d.agent_unit)} \u2192 ` + `<code>${escapeHtml12(d.scope)}</code> ` + `(${escapeHtml12(d.action)}, ${ttl}) ` + `\u00b7 /approvals revoke ${escapeHtml12(d.id)}`;
30013
- }).join(`
30014
- `);
30015
- await ctx.reply(`<b>Active approvals</b>
30016
-
30017
- ${summary}
30018
-
30019
- ${detail}`, {
30020
- parse_mode: "HTML"
30021
- });
30022
- return;
30023
- }
30024
- if (sub === "revoke") {
30025
- const id = args[1];
30026
- if (!id) {
30027
- await ctx.reply("Usage: <code>/approvals revoke &lt;id&gt;</code>", {
30028
- parse_mode: "HTML"
30029
- });
30030
- return;
30031
- }
30032
- const actor = ctx.from?.id?.toString() ?? "unknown";
30033
- const ok = await approvalRevoke(id, actor, "manual /approvals revoke");
30034
- if (ok === null) {
30035
- await ctx.reply("Approval kernel unreachable.");
30036
- return;
30037
- }
30038
- await ctx.reply(ok ? `Revoked <code>${escapeHtml12(id)}</code>.` : `No such active decision <code>${escapeHtml12(id)}</code>.`, { parse_mode: "HTML" });
30039
- return;
30040
- }
30041
- await ctx.reply(`Unknown subcommand <code>${escapeHtml12(sub)}</code>. ` + `Use <code>/approvals list</code> or <code>/approvals revoke &lt;id&gt;</code>. ` + `(<code>add</code> and <code>stats</code> are coming in a follow-up.)`, { parse_mode: "HTML" });
30042
- });
30043
- }
30044
- function escapeHtml12(s) {
30045
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30046
- }
30047
- var init_approvals_commands = __esm(() => {
30048
- init_client3();
30049
- });
30050
-
30051
- // auth-snapshot-format.ts
30052
- var exports_auth_snapshot_format = {};
30053
- __export(exports_auth_snapshot_format, {
30054
- renderFallbackAnnouncement: () => renderFallbackAnnouncement2,
30055
- renderAuthSnapshotFormat2: () => renderAuthSnapshotFormat22,
30056
- recommendation: () => recommendation2,
30057
- formatRelative: () => formatRelative2,
30058
- formatAbsolute: () => formatAbsolute2,
30059
- fmtPct: () => fmtPct2,
30060
- classifyHealth: () => classifyHealth2,
30061
- buildSnapshotsFromState: () => buildSnapshotsFromState2,
30062
- buildSnapshotKeyboard: () => buildSnapshotKeyboard2,
30063
- bindingWindow: () => bindingWindow2,
30064
- THROTTLING_THRESHOLD_PCT: () => THROTTLING_THRESHOLD_PCT2
30065
- });
30066
- function classifyHealth2(snap) {
30067
- if (!snap.quota)
30068
- return "unknown";
30069
- const q = snap.quota;
30070
- const max = Math.max(q.fiveHourUtilizationPct, q.sevenDayUtilizationPct);
30071
- if (max >= 99.5)
30072
- return "blocked";
30073
- if (max >= THROTTLING_THRESHOLD_PCT2)
30074
- return "throttling";
30075
- return "healthy";
30076
- }
30077
- function bindingWindow2(q) {
30078
- if (q.representativeClaim === "seven_day")
30079
- return "7d";
30080
- if (q.representativeClaim === "five_hour")
30081
- return "5h";
30082
- return q.sevenDayUtilizationPct >= q.fiveHourUtilizationPct ? "7d" : "5h";
30083
- }
30084
- function formatRelative2(target, now = new Date) {
30085
- if (!target)
30086
- return "\u2014";
30087
- const deltaMs = target.getTime() - now.getTime();
30088
- if (deltaMs <= 0)
30089
- return "now";
30090
- const totalMin = Math.round(deltaMs / 60000);
30091
- if (totalMin < 60)
30092
- return `${totalMin}m`;
30093
- const h = Math.floor(totalMin / 60);
30094
- const m = totalMin % 60;
30095
- if (h < 24)
30096
- return m > 0 ? `${h}h ${m}m` : `${h}h`;
30097
- const d = Math.floor(h / 24);
30098
- const rh = h % 24;
30099
- return rh > 0 ? `${d}d ${rh}h` : `${d}d`;
30100
- }
30101
- function formatAbsolute2(target, tz = "UTC") {
30102
- if (!target)
30103
- return "\u2014";
30104
- return target.toLocaleString("en-US", {
30105
- timeZone: tz,
30106
- weekday: "short",
30107
- hour: "numeric",
30108
- minute: "2-digit",
30109
- hour12: true
30110
- });
30111
- }
30112
- function fmtPct2(pct) {
30113
- return `${Math.round(pct)}%`;
30114
- }
30115
- function groupHeader2(health, count) {
30116
- const emoji = HEALTH_EMOJI2[health];
30117
- const title = HEALTH_TITLE2[health];
30118
- return `${emoji} <b>${title}</b> (${count})`;
30119
- }
30120
- function renderAccountRow2(snap, opts) {
30121
- const now = opts.now ?? new Date;
30122
- const tz = opts.tz ?? "UTC";
30123
- const lines = [];
30124
- const marker = snap.isActive ? "\u25cf " : "";
30125
- if (!snap.quota) {
30126
- lines.push(`${marker}<code>${escapeHtml13(snap.label)}</code> <i>quota probe failed</i>`);
30127
- if (snap.quotaError) {
30128
- lines.push(` <i>${escapeHtml13(snap.quotaError)}</i>`);
30129
- }
30130
- return lines;
30131
- }
30132
- const q = snap.quota;
30133
- const fiveStr = fmtPct2(q.fiveHourUtilizationPct);
30134
- const sevenStr = fmtPct2(q.sevenDayUtilizationPct);
30135
- lines.push(`${marker}<code>${escapeHtml13(snap.label)}</code> ${fiveStr} / ${sevenStr}`);
30136
- const health = classifyHealth2(snap);
30137
- if (health === "blocked") {
30138
- const win = bindingWindow2(q);
30139
- const reset2 = win === "5h" ? q.fiveHourResetAt : q.sevenDayResetAt;
30140
- const winLabel = win === "5h" ? "5-hour" : "7-day";
30141
- lines.push(` <i>back ${formatAbsolute2(reset2, tz)} (in ${formatRelative2(reset2, now)}, ${winLabel} cap)</i>`);
30142
- return lines;
30143
- }
30144
- const fiveResetIn = q.fiveHourResetAt ? q.fiveHourResetAt.getTime() - now.getTime() : Infinity;
30145
- const sevenResetIn = q.sevenDayResetAt ? q.sevenDayResetAt.getTime() - now.getTime() : Infinity;
30146
- const fiveFirst = fiveResetIn <= sevenResetIn;
30147
- const fiveSeg = q.fiveHourResetAt ? `5h refills ${formatAbsolute2(q.fiveHourResetAt, tz)} (in ${formatRelative2(q.fiveHourResetAt, now)})` : "5h refills \u2014";
30148
- const sevenSeg = q.sevenDayResetAt ? `7d resets ${formatAbsolute2(q.sevenDayResetAt, tz)} (in ${formatRelative2(q.sevenDayResetAt, now)})` : "7d resets \u2014";
30149
- lines.push(` <i>${fiveFirst ? fiveSeg : sevenSeg} \u00b7 ${fiveFirst ? sevenSeg : fiveSeg}</i>`);
30150
- return lines;
30151
- }
30152
- function renderAuthSnapshotFormat22(snapshots, opts = {}) {
30153
- const now = opts.now ?? new Date;
30154
- const lines = [];
30155
- lines.push("\uD83D\uDD0B <b>Auth \u2014 fleet status</b>");
30156
- const order = ["blocked", "throttling", "healthy", "unknown"];
30157
- const grouped = new Map;
30158
- for (const s of snapshots) {
30159
- const h = classifyHealth2(s);
30160
- if (!grouped.has(h))
30161
- grouped.set(h, []);
30162
- grouped.get(h).push(s);
30163
- }
30164
- for (const arr of grouped.values()) {
30165
- arr.sort((a, b) => Number(b.isActive) - Number(a.isActive));
30166
- }
30167
- for (const h of order) {
30168
- const arr = grouped.get(h);
30169
- if (!arr || arr.length === 0)
30170
- continue;
30171
- lines.push("");
30172
- lines.push(groupHeader2(h, arr.length));
30173
- for (const s of arr) {
30174
- for (const ln of renderAccountRow2(s, opts))
30175
- lines.push(ln);
30176
- }
30177
- }
30178
- lines.push("");
30179
- lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
30180
- lines.push(`<i>${recommendation2(snapshots, now)}</i>`);
30181
- if (opts.liveProbedAtMs != null) {
30182
- const ageSec = Math.max(0, Math.round((Date.now() - opts.liveProbedAtMs) / 1000));
30183
- const ageStr = ageSec < 60 ? `${ageSec}s ago` : `${Math.round(ageSec / 60)}m ago`;
30184
- lines.push(`<i>Live \u00b7 refreshed ${ageStr}</i>`);
30185
- } else {
30186
- lines.push("<i>Live</i>");
30187
- }
30188
- return lines.join(`
30189
- `);
30190
- }
30191
- function recommendation2(snapshots, now = new Date) {
30192
- const active = snapshots.find((s) => s.isActive);
30193
- if (!active)
30194
- return "No active account set.";
30195
- const activeHealth = classifyHealth2(active);
30196
- const others = snapshots.filter((s) => !s.isActive);
30197
- const healthyAlt = others.find((s) => classifyHealth2(s) === "healthy");
30198
- if (activeHealth === "healthy") {
30199
- return `Recommendation: stay on ${active.label}.`;
30200
- }
30201
- if (activeHealth === "throttling") {
30202
- if (healthyAlt) {
30203
- return `Recommendation: active ${active.label} is throttling. Switch to ${healthyAlt.label} for headroom.`;
30204
- }
30205
- return `Recommendation: active ${active.label} is throttling; no healthy alternative \u2014 wait for refill.`;
30206
- }
30207
- if (activeHealth === "blocked") {
30208
- if (healthyAlt) {
30209
- return `Recommendation: active ${active.label} is BLOCKED \u2014 switch to ${healthyAlt.label} now.`;
30210
- }
30211
- const earliestRecovery = pickEarliestRecovery2(snapshots, now);
30212
- if (earliestRecovery) {
30213
- return `All accounts blocked. Earliest recovery: ${earliestRecovery.label} in ${formatRelative2(earliestRecovery.at, now)}.`;
30214
- }
30215
- return `All accounts blocked. Run /auth add to attach another subscription.`;
30216
- }
30217
- return `Active ${active.label}: quota probe failed; broker last_seen unknown.`;
30218
- }
30219
- function pickEarliestRecovery2(snapshots, now) {
30220
- let best = null;
30221
- for (const s of snapshots) {
30222
- if (!s.quota)
30223
- continue;
30224
- const win = bindingWindow2(s.quota);
30225
- const at = win === "5h" ? s.quota.fiveHourResetAt : s.quota.sevenDayResetAt;
30226
- if (!at || at.getTime() <= now.getTime())
30227
- continue;
30228
- if (!best || at.getTime() < best.at.getTime()) {
30229
- best = { label: s.label, at };
30230
- }
30231
- }
30232
- return best;
30233
- }
30234
- function renderFallbackAnnouncement2(input) {
30235
- const now = input.now ?? new Date;
30236
- const tz = input.tz ?? "UTC";
30237
- const lines = [];
30238
- const limitWord = input.oldQuota ? limitWordFor2(input.oldQuota) : "quota";
30239
- const headerLimit = limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
30240
- if (!input.newLabel) {
30241
- lines.push(`\uD83D\uDD34 <b>All accounts blocked \u00b7 ${headerLimit} on ${escapeHtml13(input.oldLabel)}</b>`);
30242
- lines.push("");
30243
- lines.push(`Triggered by: agent <b>${escapeHtml13(input.triggerAgent)}</b>`);
30244
- if (input.oldQuota) {
30245
- const recovery = recoveryAtFor2(input.oldQuota);
30246
- if (recovery) {
30247
- lines.push(`${escapeHtml13(input.oldLabel)} recovers ${formatAbsolute2(recovery, tz)} ` + `(in ${formatRelative2(recovery, now)})`);
30248
- }
30249
- }
30250
- lines.push("");
30251
- lines.push(`Run <code>/auth add &lt;label&gt;</code> to attach another subscription, ` + `or <code>/auth refresh</code> to re-probe.`);
30252
- return lines.join(`
30253
- `);
30254
- }
30255
- lines.push(`\u2713 <b>Switched fleet \u00b7 ${headerLimit} on ${escapeHtml13(input.oldLabel)}</b>`);
30256
- lines.push("");
30257
- lines.push(`<code>${escapeHtml13(input.oldLabel)}</code> \u2192 <code>${escapeHtml13(input.newLabel)}</code>`);
30258
- lines.push(`Triggered by: agent <b>${escapeHtml13(input.triggerAgent)}</b>`);
30259
- lines.push("");
30260
- if (input.oldQuota) {
30261
- const recovery = recoveryAtFor2(input.oldQuota);
30262
- if (recovery) {
30263
- lines.push(`<code>${escapeHtml13(input.oldLabel)}</code> recovers ` + `${formatAbsolute2(recovery, tz)} (in ${formatRelative2(recovery, now)})`);
30264
- }
30265
- }
30266
- if (input.newQuota) {
30267
- const fiveStr = fmtPct2(input.newQuota.fiveHourUtilizationPct);
30268
- const sevenStr = fmtPct2(input.newQuota.sevenDayUtilizationPct);
30269
- const hasHeadroom = input.newQuota.fiveHourUtilizationPct < THROTTLING_THRESHOLD_PCT2 && input.newQuota.sevenDayUtilizationPct < THROTTLING_THRESHOLD_PCT2;
30270
- const headroomStr = hasHeadroom ? "<i>(plenty of headroom)</i>" : "<i>(near limit \u2014 watch this)</i>";
30271
- lines.push(`<code>${escapeHtml13(input.newLabel)}</code> now: ${fiveStr} of 5h \u00b7 ${sevenStr} of 7d ${headroomStr}`);
30272
- } else {
30273
- lines.push(`<i>(quota probe for new account is pending \u2014 will reflect on next /auth)</i>`);
30274
- }
30275
- return lines.join(`
30495
+ if (decisions.length === 0) {
30496
+ await ctx.reply(agentFilter ? `No active approvals for <code>${escapeHtml14(agentFilter)}</code>.` : "No active approvals.", { parse_mode: "HTML" });
30497
+ return;
30498
+ }
30499
+ const byAgent = new Map;
30500
+ for (const d of decisions)
30501
+ byAgent.set(d.agent_unit, (byAgent.get(d.agent_unit) ?? 0) + 1);
30502
+ const summary = Array.from(byAgent.entries()).map(([a, n]) => `\u2022 <b>${escapeHtml14(a)}</b>: ${n}`).join(`
30276
30503
  `);
30277
- }
30278
- function limitWordFor2(q) {
30279
- if (q.representativeClaim === "seven_day" && q.sevenDayUtilizationPct >= 99)
30280
- return "7-day";
30281
- if (q.representativeClaim === "five_hour" && q.fiveHourUtilizationPct >= 99)
30282
- return "5-hour";
30283
- if (q.sevenDayUtilizationPct >= 99)
30284
- return "7-day";
30285
- if (q.fiveHourUtilizationPct >= 99)
30286
- return "5-hour";
30287
- return q.sevenDayUtilizationPct >= q.fiveHourUtilizationPct ? "7-day" : "5-hour";
30288
- }
30289
- function recoveryAtFor2(q) {
30290
- const word = limitWordFor2(q);
30291
- if (word === "7-day")
30292
- return q.sevenDayResetAt;
30293
- if (word === "5-hour")
30294
- return q.fiveHourResetAt;
30295
- if (!q.fiveHourResetAt)
30296
- return q.sevenDayResetAt;
30297
- if (!q.sevenDayResetAt)
30298
- return q.fiveHourResetAt;
30299
- return q.fiveHourResetAt.getTime() < q.sevenDayResetAt.getTime() ? q.fiveHourResetAt : q.sevenDayResetAt;
30300
- }
30301
- function buildSnapshotKeyboard2(snapshots, opts = {}) {
30302
- const max = opts.maxSwitchButtons ?? 3;
30303
- const rows = [];
30304
- const switchTargets = snapshots.filter((s) => !s.isActive).sort((a, b) => switchPriority2(a) - switchPriority2(b)).filter((s) => classifyHealth2(s) !== "blocked" && classifyHealth2(s) !== "unknown").slice(0, max);
30305
- for (const t of switchTargets) {
30306
- rows.push([
30307
- {
30308
- text: `Switch fleet \u2192 ${t.label}`,
30309
- callbackData: `auth:use:${t.label}`
30504
+ const detail = decisions.slice(0, 20).map((d) => {
30505
+ const ttl = d.ttl_expires_at === null ? "always" : `until ${new Date(d.ttl_expires_at).toISOString().slice(0, 16).replace("T", " ")}`;
30506
+ return `<code>${escapeHtml14(d.id.slice(0, 8))}</code> ` + `${escapeHtml14(d.agent_unit)} \u2192 ` + `<code>${escapeHtml14(d.scope)}</code> ` + `(${escapeHtml14(d.action)}, ${ttl}) ` + `\u00b7 /approvals revoke ${escapeHtml14(d.id)}`;
30507
+ }).join(`
30508
+ `);
30509
+ await ctx.reply(`<b>Active approvals</b>
30510
+
30511
+ ${summary}
30512
+
30513
+ ${detail}`, {
30514
+ parse_mode: "HTML"
30515
+ });
30516
+ return;
30517
+ }
30518
+ if (sub === "revoke") {
30519
+ const id = args[1];
30520
+ if (!id) {
30521
+ await ctx.reply("Usage: <code>/approvals revoke &lt;id&gt;</code>", {
30522
+ parse_mode: "HTML"
30523
+ });
30524
+ return;
30310
30525
  }
30311
- ]);
30312
- }
30313
- rows.push([
30314
- { text: "\u21bb Refresh", callbackData: "auth:refresh" },
30315
- { text: "/usage", insertText: "/usage" },
30316
- { text: "+ Add", insertText: "/auth add " }
30317
- ]);
30318
- return rows;
30319
- }
30320
- function switchPriority2(s) {
30321
- const h = classifyHealth2(s);
30322
- if (h === "healthy")
30323
- return 0;
30324
- if (h === "throttling")
30325
- return 1;
30326
- if (h === "unknown")
30327
- return 2;
30328
- return 3;
30526
+ const actor = ctx.from?.id?.toString() ?? "unknown";
30527
+ const ok = await approvalRevoke(id, actor, "manual /approvals revoke");
30528
+ if (ok === null) {
30529
+ await ctx.reply("Approval kernel unreachable.");
30530
+ return;
30531
+ }
30532
+ await ctx.reply(ok ? `Revoked <code>${escapeHtml14(id)}</code>.` : `No such active decision <code>${escapeHtml14(id)}</code>.`, { parse_mode: "HTML" });
30533
+ return;
30534
+ }
30535
+ await ctx.reply(`Unknown subcommand <code>${escapeHtml14(sub)}</code>. ` + `Use <code>/approvals list</code> or <code>/approvals revoke &lt;id&gt;</code>. ` + `(<code>add</code> and <code>stats</code> are coming in a follow-up.)`, { parse_mode: "HTML" });
30536
+ });
30329
30537
  }
30330
- function escapeHtml13(s) {
30538
+ function escapeHtml14(s) {
30331
30539
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30332
30540
  }
30333
- function buildSnapshotsFromState2(state4, quotas) {
30334
- const out = [];
30335
- for (let i = 0;i < state4.accounts.length; i++) {
30336
- const acc = state4.accounts[i];
30337
- const q = quotas[i];
30338
- out.push({
30339
- label: acc.label,
30340
- isActive: acc.label === state4.active,
30341
- quota: q && q.ok ? q.data : null,
30342
- quotaError: q && !q.ok ? q.reason : undefined,
30343
- expiresAtMs: acc.expiresAt
30344
- });
30345
- }
30346
- return out;
30347
- }
30348
- var THROTTLING_THRESHOLD_PCT2 = 80, HEALTH_EMOJI2, HEALTH_TITLE2;
30349
- var init_auth_snapshot_format = __esm(() => {
30350
- HEALTH_EMOJI2 = {
30351
- healthy: "\uD83D\uDFE2",
30352
- throttling: "\uD83D\uDFE1",
30353
- blocked: "\uD83D\uDD34",
30354
- unknown: "\u26aa"
30355
- };
30356
- HEALTH_TITLE2 = {
30357
- healthy: "HEALTHY",
30358
- throttling: "THROTTLING",
30359
- blocked: "BLOCKED",
30360
- unknown: "UNKNOWN"
30361
- };
30541
+ var init_approvals_commands = __esm(() => {
30542
+ init_client3();
30362
30543
  });
30363
30544
 
30364
30545
  // gateway/approval-card.ts
@@ -30495,23 +30676,23 @@ var import_runner2 = __toESM(require_mod3(), 1);
30495
30676
  import { randomBytes as randomBytes6 } from "crypto";
30496
30677
  import { execFileSync as execFileSync5, execSync as execSync2, spawn as spawn2 } from "child_process";
30497
30678
  import {
30498
- readFileSync as readFileSync32,
30499
- writeFileSync as writeFileSync21,
30500
- mkdirSync as mkdirSync21,
30679
+ readFileSync as readFileSync34,
30680
+ writeFileSync as writeFileSync23,
30681
+ mkdirSync as mkdirSync23,
30501
30682
  readdirSync as readdirSync7,
30502
30683
  rmSync as rmSync4,
30503
30684
  statSync as statSync13,
30504
- renameSync as renameSync12,
30685
+ renameSync as renameSync13,
30505
30686
  realpathSync as realpathSync2,
30506
30687
  chmodSync as chmodSync5,
30507
30688
  openSync as openSync8,
30508
30689
  closeSync as closeSync8,
30509
- existsSync as existsSync34,
30690
+ existsSync as existsSync36,
30510
30691
  unlinkSync as unlinkSync14,
30511
30692
  appendFileSync as appendFileSync3
30512
30693
  } from "fs";
30513
30694
  import { homedir as homedir12 } from "os";
30514
- import { join as join32, extname, sep as sep3, basename as basename7 } from "path";
30695
+ import { join as join33, extname, sep as sep3, basename as basename7 } from "path";
30515
30696
 
30516
30697
  // plugin-logger.ts
30517
30698
  import { appendFileSync, mkdirSync, renameSync, statSync, existsSync } from "fs";
@@ -38520,6 +38701,7 @@ function isSilentFlushMarker(text) {
38520
38701
  }
38521
38702
  return SILENT_MARKERS.has(trimmed.toUpperCase());
38522
38703
  }
38704
+ var TRIVIAL_CONFIRMATIONS = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
38523
38705
 
38524
38706
  // answer-stream.ts
38525
38707
  var MIN_INITIAL_CHARS = 50;
@@ -40488,15 +40670,11 @@ function renderOperatorEvent(ev) {
40488
40670
  text: [
40489
40671
  `\uD83D\uDCB3 <b>Credit balance too low</b> for <b>${agent}</b>.`,
40490
40672
  detail ? `<i>${detail}</i>` : "",
40491
- `Swap to another account slot or add a new one.`
40673
+ `Use <code>/auth use &lt;label&gt;</code> to switch account slot or <code>/auth add</code> to add one.`
40492
40674
  ].filter(Boolean).join(`
40493
40675
  `),
40494
40676
  keyboard: {
40495
40677
  inline_keyboard: [
40496
- [
40497
- { text: "\uD83D\uDD04 Swap slot", callback_data: `op:swap-slot:${encodeURIComponent(ev.agent)}` },
40498
- { text: "\u2795 Add slot", callback_data: `op:add-slot:${encodeURIComponent(ev.agent)}` }
40499
- ],
40500
40678
  [{ text: "\u23f3 Wait", callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }]
40501
40679
  ]
40502
40680
  }
@@ -40506,15 +40684,11 @@ function renderOperatorEvent(ev) {
40506
40684
  text: [
40507
40685
  `\u26a0\ufe0f <b>Quota exhausted</b> for <b>${agent}</b>.`,
40508
40686
  detail ? `<i>${detail}</i>` : "",
40509
- `All account slots are at the usage limit. Switchroom will auto-fallback when another slot is available.`
40687
+ `All account slots are at the usage limit. Switchroom will auto-fallback when another slot is available. Use <code>/auth use &lt;label&gt;</code> to switch manually.`
40510
40688
  ].filter(Boolean).join(`
40511
40689
  `),
40512
40690
  keyboard: {
40513
40691
  inline_keyboard: [
40514
- [
40515
- { text: "\uD83D\uDD04 Swap slot", callback_data: `op:swap-slot:${encodeURIComponent(ev.agent)}` },
40516
- { text: "\u2795 Add slot", callback_data: `op:add-slot:${encodeURIComponent(ev.agent)}` }
40517
- ],
40518
40692
  [{ text: "\u23f3 Wait", callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }]
40519
40693
  ]
40520
40694
  }
@@ -41812,6 +41986,27 @@ function isSilentFlushMarker2(text) {
41812
41986
  }
41813
41987
  return SILENT_MARKERS2.has(trimmed.toUpperCase());
41814
41988
  }
41989
+ var TRIVIAL_CONFIRMATIONS2 = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
41990
+ function isTrivialConfirmationLine(line) {
41991
+ let t = line.trim();
41992
+ if (t.length === 0 || t.length > 8)
41993
+ return false;
41994
+ if (/\W$/.test(t))
41995
+ t = t.slice(0, -1);
41996
+ return TRIVIAL_CONFIRMATIONS2.has(t.toUpperCase());
41997
+ }
41998
+ function isCompositeSilentNoise(text) {
41999
+ if (typeof text !== "string")
42000
+ return false;
42001
+ const lines = text.split(`
42002
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
42003
+ if (lines.length === 0)
42004
+ return false;
42005
+ const hasMarker = lines.some((l) => isSilentFlushMarker2(l));
42006
+ if (!hasMarker)
42007
+ return false;
42008
+ return lines.every((l) => isSilentFlushMarker2(l) || isTrivialConfirmationLine(l));
42009
+ }
41815
42010
  function decideTurnFlush(input) {
41816
42011
  const flushEnabled = input.flushEnabled !== false;
41817
42012
  if (!flushEnabled)
@@ -41826,6 +42021,8 @@ function decideTurnFlush(input) {
41826
42021
  return { kind: "skip", reason: "empty-text" };
41827
42022
  if (isSilentFlushMarker2(joined))
41828
42023
  return { kind: "skip", reason: "silent-marker" };
42024
+ if (isCompositeSilentNoise(joined))
42025
+ return { kind: "skip", reason: "silent-marker" };
41829
42026
  return { kind: "flush", text: joined };
41830
42027
  }
41831
42028
  function isTurnFlushSafetyEnabled(env = process.env) {
@@ -48729,7 +48926,7 @@ function determineRestartReason(opts) {
48729
48926
  init_boot_card();
48730
48927
 
48731
48928
  // gateway/update-announce.ts
48732
- import { existsSync as existsSync26, mkdirSync as mkdirSync13, openSync as openSync3, closeSync as closeSync3, readFileSync as readFileSync25 } from "node:fs";
48929
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, openSync as openSync3, closeSync as closeSync3, readFileSync as readFileSync26 } from "node:fs";
48733
48930
  import { join as join24 } from "node:path";
48734
48931
  import { homedir as homedir10 } from "node:os";
48735
48932
 
@@ -48843,8 +49040,8 @@ function readAndFilter(raw, filters, limit) {
48843
49040
  var DEFAULT_LOOKBACK_MS = 10 * 60 * 1000;
48844
49041
  function readLastTerminalUpdateAudit(opts = {}) {
48845
49042
  const path = opts.auditLogPath ?? defaultAuditLogPath();
48846
- const exists = opts.exists ?? existsSync26;
48847
- const readFile = opts.readFile ?? ((p) => readFileSync25(p, "utf-8"));
49043
+ const exists = opts.exists ?? existsSync27;
49044
+ const readFile = opts.readFile ?? ((p) => readFileSync26(p, "utf-8"));
48848
49045
  if (!exists(path))
48849
49046
  return null;
48850
49047
  let raw;
@@ -48908,7 +49105,7 @@ function claimUpdateAnnouncement(requestId, opts = {}) {
48908
49105
  const stateDir = opts.stateDir ?? process.env.TELEGRAM_STATE_DIR ?? join24(homedir10(), ".switchroom");
48909
49106
  const dir = join24(stateDir, "update-announced");
48910
49107
  try {
48911
- mkdirSync13(dir, { recursive: true });
49108
+ mkdirSync14(dir, { recursive: true });
48912
49109
  } catch {
48913
49110
  return false;
48914
49111
  }
@@ -48932,7 +49129,7 @@ function maybeRenderUpdateAnnouncement(opts = {}) {
48932
49129
  }
48933
49130
 
48934
49131
  // issues-card.ts
48935
- import { readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
49132
+ import { readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
48936
49133
  var SEVERITY_EMOJI = {
48937
49134
  info: "\u2139\ufe0f",
48938
49135
  warn: "\u26a0\ufe0f",
@@ -49024,7 +49221,7 @@ function extractRetryAfterSecs(err) {
49024
49221
  var COOLDOWN_JITTER_MS = 500;
49025
49222
  function readPersistedMessageId(path, log) {
49026
49223
  try {
49027
- const raw = readFileSync26(path, "utf8");
49224
+ const raw = readFileSync27(path, "utf8");
49028
49225
  const parsed = JSON.parse(raw);
49029
49226
  const v = parsed.messageId;
49030
49227
  if (typeof v === "number" && Number.isInteger(v) && v > 0)
@@ -49040,7 +49237,7 @@ function readPersistedMessageId(path, log) {
49040
49237
  }
49041
49238
  function writePersistedMessageId(path, messageId, log) {
49042
49239
  try {
49043
- writeFileSync15(path, JSON.stringify({ messageId }) + `
49240
+ writeFileSync16(path, JSON.stringify({ messageId }) + `
49044
49241
  `, { mode: 384 });
49045
49242
  } catch (err) {
49046
49243
  log(`issues-card: persist write failed (${err.message})`);
@@ -49133,21 +49330,21 @@ function createIssuesCardHandle(opts) {
49133
49330
  }
49134
49331
 
49135
49332
  // issues-watcher.ts
49136
- import { existsSync as existsSync28, statSync as statSync8 } from "node:fs";
49333
+ import { existsSync as existsSync29, statSync as statSync8 } from "node:fs";
49137
49334
  import { join as join26 } from "node:path";
49138
49335
 
49139
49336
  // ../src/issues/store.ts
49140
49337
  import {
49141
49338
  closeSync as closeSync4,
49142
- existsSync as existsSync27,
49143
- mkdirSync as mkdirSync14,
49339
+ existsSync as existsSync28,
49340
+ mkdirSync as mkdirSync15,
49144
49341
  openSync as openSync4,
49145
49342
  readdirSync as readdirSync5,
49146
- readFileSync as readFileSync27,
49147
- renameSync as renameSync10,
49343
+ readFileSync as readFileSync28,
49344
+ renameSync as renameSync11,
49148
49345
  statSync as statSync7,
49149
49346
  unlinkSync as unlinkSync10,
49150
- writeFileSync as writeFileSync16,
49347
+ writeFileSync as writeFileSync17,
49151
49348
  writeSync
49152
49349
  } from "node:fs";
49153
49350
  import { join as join25 } from "node:path";
@@ -49167,11 +49364,11 @@ var ISSUES_FILE = "issues.jsonl";
49167
49364
  var ISSUES_LOCK = "issues.lock";
49168
49365
  function readAll(stateDir) {
49169
49366
  const path = join25(stateDir, ISSUES_FILE);
49170
- if (!existsSync27(path))
49367
+ if (!existsSync28(path))
49171
49368
  return [];
49172
49369
  let raw;
49173
49370
  try {
49174
- raw = readFileSync27(path, "utf-8");
49371
+ raw = readFileSync28(path, "utf-8");
49175
49372
  } catch {
49176
49373
  return [];
49177
49374
  }
@@ -49203,7 +49400,7 @@ function list(stateDir, opts = {}) {
49203
49400
  });
49204
49401
  }
49205
49402
  function resolve6(stateDir, fingerprint, nowFn = Date.now) {
49206
- if (!existsSync27(join25(stateDir, ISSUES_FILE)))
49403
+ if (!existsSync28(join25(stateDir, ISSUES_FILE)))
49207
49404
  return 0;
49208
49405
  return withLock(stateDir, () => {
49209
49406
  const all = readAll(stateDir);
@@ -49227,8 +49424,8 @@ function writeAll(stateDir, events) {
49227
49424
  const body = events.length === 0 ? "" : events.map((e) => JSON.stringify(e)).join(`
49228
49425
  `) + `
49229
49426
  `;
49230
- writeFileSync16(tmp, body, "utf-8");
49231
- renameSync10(tmp, path);
49427
+ writeFileSync17(tmp, body, "utf-8");
49428
+ renameSync11(tmp, path);
49232
49429
  }
49233
49430
  var ORPHAN_TMP_TTL_MS = 60000;
49234
49431
  var TMP_PREFIX = `${ISSUES_FILE}.tmp-`;
@@ -49290,7 +49487,7 @@ function withLock(stateDir, fn) {
49290
49487
  function tryStealStaleLock(lockPath) {
49291
49488
  let pidStr;
49292
49489
  try {
49293
- pidStr = readFileSync27(lockPath, "utf-8").trim();
49490
+ pidStr = readFileSync28(lockPath, "utf-8").trim();
49294
49491
  } catch {
49295
49492
  return true;
49296
49493
  }
@@ -49388,7 +49585,7 @@ function startIssuesWatcher(opts) {
49388
49585
  };
49389
49586
  }
49390
49587
  function defaultSignatureProvider(path) {
49391
- if (!existsSync28(path))
49588
+ if (!existsSync29(path))
49392
49589
  return null;
49393
49590
  try {
49394
49591
  const stat = statSync8(path);
@@ -49628,7 +49825,7 @@ function skillBasenameFromPath2(input) {
49628
49825
  }
49629
49826
 
49630
49827
  // credits-watch.ts
49631
- import { readFileSync as readFileSync28, writeFileSync as writeFileSync17, existsSync as existsSync29, mkdirSync as mkdirSync15 } from "fs";
49828
+ import { readFileSync as readFileSync29, writeFileSync as writeFileSync18, existsSync as existsSync30, mkdirSync as mkdirSync16 } from "fs";
49632
49829
  import { join as join27 } from "path";
49633
49830
  var STATE_FILE = "credits-watch.json";
49634
49831
  var FATAL_REASONS = new Set([
@@ -49642,11 +49839,11 @@ function emptyCreditState() {
49642
49839
  }
49643
49840
  function readClaudeJsonOverage(claudeConfigDir) {
49644
49841
  const path = join27(claudeConfigDir, ".claude.json");
49645
- if (!existsSync29(path))
49842
+ if (!existsSync30(path))
49646
49843
  return null;
49647
49844
  let raw;
49648
49845
  try {
49649
- raw = readFileSync28(path, "utf-8");
49846
+ raw = readFileSync29(path, "utf-8");
49650
49847
  } catch {
49651
49848
  return null;
49652
49849
  }
@@ -49727,10 +49924,10 @@ function escapeHtml10(s) {
49727
49924
  }
49728
49925
  function loadCreditState(stateDir) {
49729
49926
  const path = join27(stateDir, STATE_FILE);
49730
- if (!existsSync29(path))
49927
+ if (!existsSync30(path))
49731
49928
  return emptyCreditState();
49732
49929
  try {
49733
- const raw = readFileSync28(path, "utf-8");
49930
+ const raw = readFileSync29(path, "utf-8");
49734
49931
  const parsed = JSON.parse(raw);
49735
49932
  if (parsed && typeof parsed === "object" && (parsed.lastNotifiedReason === null || typeof parsed.lastNotifiedReason === "string") && typeof parsed.lastNotifiedAt === "number" && Number.isFinite(parsed.lastNotifiedAt)) {
49736
49933
  return {
@@ -49742,36 +49939,161 @@ function loadCreditState(stateDir) {
49742
49939
  return emptyCreditState();
49743
49940
  }
49744
49941
  function saveCreditState(stateDir, state4) {
49745
- mkdirSync15(stateDir, { recursive: true });
49942
+ mkdirSync16(stateDir, { recursive: true });
49746
49943
  const path = join27(stateDir, STATE_FILE);
49747
- writeFileSync17(path, JSON.stringify(state4, null, 2) + `
49944
+ writeFileSync18(path, JSON.stringify(state4, null, 2) + `
49945
+ `, { mode: 384 });
49946
+ }
49947
+
49948
+ // quota-watch.ts
49949
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync19, existsSync as existsSync31, mkdirSync as mkdirSync17 } from "fs";
49950
+ import { join as join28 } from "path";
49951
+ var STATE_FILE2 = "quota-watch.json";
49952
+ function emptyQuotaWatchState() {
49953
+ return {};
49954
+ }
49955
+ function emptyAccountState() {
49956
+ return { lastNotifiedHealth: null, lastNotifiedAt: 0 };
49957
+ }
49958
+ function evaluateQuotaWatchAccount(args) {
49959
+ const { agentName: agentName3, snap, prev, now } = args;
49960
+ const label = snap.label;
49961
+ const currentHealth = classifyHealth(snap);
49962
+ if (currentHealth === "unknown" || currentHealth === "blocked") {
49963
+ return { kind: "skip", accountLabel: label, reason: `${currentHealth}-not-our-domain` };
49964
+ }
49965
+ const prevHealth = prev.lastNotifiedHealth ?? "healthy";
49966
+ if (currentHealth === prevHealth) {
49967
+ return { kind: "skip", accountLabel: label, reason: "steady-state" };
49968
+ }
49969
+ if (currentHealth === "throttling" && prevHealth === "healthy") {
49970
+ const newState = {
49971
+ lastNotifiedHealth: "throttling",
49972
+ lastNotifiedAt: now
49973
+ };
49974
+ return {
49975
+ kind: "notify",
49976
+ accountLabel: label,
49977
+ message: buildThrottlingMessage(agentName3, snap),
49978
+ newAccountState: newState,
49979
+ transition: "entered-throttling"
49980
+ };
49981
+ }
49982
+ if (currentHealth === "healthy" && prevHealth === "throttling") {
49983
+ const newState = {
49984
+ lastNotifiedHealth: "healthy",
49985
+ lastNotifiedAt: now
49986
+ };
49987
+ return {
49988
+ kind: "notify",
49989
+ accountLabel: label,
49990
+ message: buildRecoveryMessage(agentName3, snap),
49991
+ newAccountState: newState,
49992
+ transition: "recovered-to-healthy"
49993
+ };
49994
+ }
49995
+ return { kind: "skip", accountLabel: label, reason: "no-matching-transition" };
49996
+ }
49997
+ function buildThrottlingMessage(agentName3, snap) {
49998
+ const q = snap.quota;
49999
+ const fiveStr = fmtPct(q.fiveHourUtilizationPct);
50000
+ const sevenStr = fmtPct(q.sevenDayUtilizationPct);
50001
+ const max = Math.max(q.fiveHourUtilizationPct, q.sevenDayUtilizationPct);
50002
+ const win = max === q.fiveHourUtilizationPct ? "5h" : "7d";
50003
+ const winLabel = win === "5h" ? "5-hour" : "7-day";
50004
+ const resetAt = win === "5h" ? q.fiveHourResetAt : q.sevenDayResetAt;
50005
+ const resetStr = resetAt ? ` \u00b7 refills in ${formatRelative(resetAt, new Date)}` : "";
50006
+ const activeNote = snap.isActive ? "" : `
50007
+ This is a non-active account. Consider <code>/auth use ${escapeHtml11(snap.label)}</code> to switch, or keep it as a fallback reserve.`;
50008
+ const altNote = snap.isActive ? `
50009
+ Consider <code>/auth use &lt;other-account&gt;</code> if you have a healthier account, or wait for the ${winLabel} window to refill${resetStr}.` : "";
50010
+ return [
50011
+ `\uD83D\uDFE1 <b>Quota approaching limit</b> \u2014 <code>${escapeHtml11(snap.label)}</code>`,
50012
+ ``,
50013
+ `${fiveStr} of 5h \u00b7 ${sevenStr} of 7d`,
50014
+ `Binding window: ${winLabel}${resetStr}`,
50015
+ `${activeNote}${altNote}`,
50016
+ ``,
50017
+ `<i>Threshold: ${THROTTLING_THRESHOLD_PCT}% on either window. Source: broker quota cache.</i>`,
50018
+ `<i>Run /auth for full fleet status or /usage for the active account.</i>`
50019
+ ].join(`
50020
+ `).replace(/\n\n\n+/g, `
50021
+
50022
+ `).trim();
50023
+ }
50024
+ function buildRecoveryMessage(agentName3, snap) {
50025
+ const q = snap.quota;
50026
+ const utilLine = q ? `Current: ${fmtPct(q.fiveHourUtilizationPct)} of 5h \u00b7 ${fmtPct(q.sevenDayUtilizationPct)} of 7d` : "Current quota data unavailable.";
50027
+ return [
50028
+ `\uD83D\uDFE2 <b>Quota back in healthy range</b> \u2014 <code>${escapeHtml11(snap.label)}</code>`,
50029
+ ``,
50030
+ utilLine,
50031
+ ``,
50032
+ `<i>Below ${THROTTLING_THRESHOLD_PCT}% on both windows.</i>`
50033
+ ].join(`
50034
+ `);
50035
+ }
50036
+ function escapeHtml11(s) {
50037
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
50038
+ }
50039
+ function loadQuotaWatchState(stateDir) {
50040
+ const path = join28(stateDir, STATE_FILE2);
50041
+ if (!existsSync31(path))
50042
+ return emptyQuotaWatchState();
50043
+ try {
50044
+ const raw = readFileSync30(path, "utf-8");
50045
+ const parsed = JSON.parse(raw);
50046
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
50047
+ return emptyQuotaWatchState();
50048
+ }
50049
+ const result = {};
50050
+ for (const [key, val] of Object.entries(parsed)) {
50051
+ if (val && typeof val === "object" && !Array.isArray(val) && (val.lastNotifiedHealth === null || val.lastNotifiedHealth === "healthy" || val.lastNotifiedHealth === "throttling") && typeof val.lastNotifiedAt === "number" && Number.isFinite(val.lastNotifiedAt)) {
50052
+ result[key] = val;
50053
+ }
50054
+ }
50055
+ return result;
50056
+ } catch {
50057
+ return emptyQuotaWatchState();
50058
+ }
50059
+ }
50060
+ function saveQuotaWatchState(stateDir, state4) {
50061
+ mkdirSync17(stateDir, { recursive: true });
50062
+ const path = join28(stateDir, STATE_FILE2);
50063
+ writeFileSync19(path, JSON.stringify(state4, null, 2) + `
49748
50064
  `, { mode: 384 });
49749
50065
  }
50066
+ function patchQuotaWatchState(current, accountLabel, accountState) {
50067
+ return { ...current, [accountLabel]: accountState };
50068
+ }
50069
+
50070
+ // gateway/gateway.ts
50071
+ init_auth_snapshot_format();
49750
50072
 
49751
50073
  // gateway/turn-active-marker.ts
49752
50074
  import {
49753
50075
  closeSync as closeSync5,
49754
- existsSync as existsSync30,
49755
- mkdirSync as mkdirSync16,
50076
+ existsSync as existsSync32,
50077
+ mkdirSync as mkdirSync18,
49756
50078
  openSync as openSync5,
49757
- readFileSync as readFileSync29,
50079
+ readFileSync as readFileSync31,
49758
50080
  statSync as statSync9,
49759
50081
  unlinkSync as unlinkSync11,
49760
50082
  utimesSync as utimesSync2,
49761
- writeFileSync as writeFileSync18
50083
+ writeFileSync as writeFileSync20
49762
50084
  } from "node:fs";
49763
- import { join as join28 } from "node:path";
50085
+ import { join as join29 } from "node:path";
49764
50086
  var TURN_ACTIVE_MARKER_FILE2 = "turn-active.json";
49765
50087
  function writeTurnActiveMarker(stateDir, marker) {
49766
50088
  try {
49767
- mkdirSync16(stateDir, { recursive: true });
49768
- writeFileSync18(join28(stateDir, TURN_ACTIVE_MARKER_FILE2), JSON.stringify(marker, null, 2) + `
50089
+ mkdirSync18(stateDir, { recursive: true });
50090
+ writeFileSync20(join29(stateDir, TURN_ACTIVE_MARKER_FILE2), JSON.stringify(marker, null, 2) + `
49769
50091
  `, { mode: 384 });
49770
50092
  } catch {}
49771
50093
  }
49772
50094
  function touchTurnActiveMarker2(stateDir) {
49773
- const path = join28(stateDir, TURN_ACTIVE_MARKER_FILE2);
49774
- if (!existsSync30(path))
50095
+ const path = join29(stateDir, TURN_ACTIVE_MARKER_FILE2);
50096
+ if (!existsSync32(path))
49775
50097
  return;
49776
50098
  const now = new Date;
49777
50099
  try {
@@ -49785,12 +50107,12 @@ function touchTurnActiveMarker2(stateDir) {
49785
50107
  }
49786
50108
  function removeTurnActiveMarker(stateDir) {
49787
50109
  try {
49788
- unlinkSync11(join28(stateDir, TURN_ACTIVE_MARKER_FILE2));
50110
+ unlinkSync11(join29(stateDir, TURN_ACTIVE_MARKER_FILE2));
49789
50111
  } catch {}
49790
50112
  }
49791
50113
  function sweepStaleTurnActiveMarker(stateDir, opts) {
49792
- const path = join28(stateDir, TURN_ACTIVE_MARKER_FILE2);
49793
- if (!existsSync30(path))
50114
+ const path = join29(stateDir, TURN_ACTIVE_MARKER_FILE2);
50115
+ if (!existsSync32(path))
49794
50116
  return false;
49795
50117
  const now = opts.now ?? Date.now();
49796
50118
  try {
@@ -49802,7 +50124,7 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
49802
50124
  return false;
49803
50125
  let payload = null;
49804
50126
  try {
49805
- payload = readFileSync29(path, "utf8");
50127
+ payload = readFileSync31(path, "utf8");
49806
50128
  } catch {}
49807
50129
  unlinkSync11(path);
49808
50130
  if (opts.onRemove) {
@@ -49821,10 +50143,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
49821
50143
  }
49822
50144
 
49823
50145
  // ../src/build-info.ts
49824
- var VERSION = "0.14.0";
49825
- var COMMIT_SHA = "d7cd6faa";
49826
- var COMMIT_DATE = "2026-05-28T06:28:21Z";
49827
- var LATEST_PR = 1954;
50146
+ var VERSION = "0.14.1";
50147
+ var COMMIT_SHA = "e51a8794";
50148
+ var COMMIT_DATE = "2026-05-28T07:24:12Z";
50149
+ var LATEST_PR = 1956;
49828
50150
  var COMMITS_AHEAD_OF_TAG = 0;
49829
50151
 
49830
50152
  // gateway/boot-version.ts
@@ -49898,11 +50220,11 @@ init_peercred();
49898
50220
  import * as net4 from "node:net";
49899
50221
  import * as fs2 from "node:fs";
49900
50222
  import { homedir as homedir11 } from "node:os";
49901
- import { join as join29 } from "node:path";
50223
+ import { join as join30 } from "node:path";
49902
50224
  var DEFAULT_TIMEOUT_MS4 = 2000;
49903
50225
  var UNLOCK_TIMEOUT_MS = 30000;
49904
- var LEGACY_SOCKET_PATH2 = join29(homedir11(), ".switchroom", "vault-broker.sock");
49905
- var OPERATOR_SOCKET_PATH2 = join29(homedir11(), ".switchroom", "broker-operator", "sock");
50226
+ var LEGACY_SOCKET_PATH2 = join30(homedir11(), ".switchroom", "vault-broker.sock");
50227
+ var OPERATOR_SOCKET_PATH2 = join30(homedir11(), ".switchroom", "broker-operator", "sock");
49906
50228
  function defaultBrokerSocketPath2() {
49907
50229
  if (fs2.existsSync(OPERATOR_SOCKET_PATH2))
49908
50230
  return OPERATOR_SOCKET_PATH2;
@@ -50124,8 +50446,8 @@ function resolveVaultApprovalPosture(broker) {
50124
50446
  }
50125
50447
 
50126
50448
  // registry/turns-schema.ts
50127
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync17 } from "fs";
50128
- import { join as join30 } from "path";
50449
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync19 } from "fs";
50450
+ import { join as join31 } from "path";
50129
50451
  var DatabaseClass2 = null;
50130
50452
  function loadDatabaseClass2() {
50131
50453
  if (DatabaseClass2 != null)
@@ -50184,9 +50506,9 @@ function applySchema(db2) {
50184
50506
  }
50185
50507
  function openTurnsDb(agentDir) {
50186
50508
  const Database = loadDatabaseClass2();
50187
- const dir = join30(agentDir, "telegram");
50188
- mkdirSync17(dir, { recursive: true, mode: 448 });
50189
- const path = join30(dir, "registry.db");
50509
+ const dir = join31(agentDir, "telegram");
50510
+ mkdirSync19(dir, { recursive: true, mode: 448 });
50511
+ const path = join31(dir, "registry.db");
50190
50512
  const db2 = new Database(path, { create: true });
50191
50513
  applySchema(db2);
50192
50514
  try {
@@ -50327,11 +50649,11 @@ installGlobalErrorHandlers();
50327
50649
  process.on("beforeExit", () => {
50328
50650
  shutdownAnalytics();
50329
50651
  });
50330
- var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join32(homedir12(), ".claude", "channels", "telegram");
50331
- var ACCESS_FILE = join32(STATE_DIR, "access.json");
50332
- var APPROVED_DIR = join32(STATE_DIR, "approved");
50333
- var ENV_FILE = join32(STATE_DIR, ".env");
50334
- var INBOX_DIR = join32(STATE_DIR, "inbox");
50652
+ var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join33(homedir12(), ".claude", "channels", "telegram");
50653
+ var ACCESS_FILE = join33(STATE_DIR, "access.json");
50654
+ var APPROVED_DIR = join33(STATE_DIR, "approved");
50655
+ var ENV_FILE = join33(STATE_DIR, ".env");
50656
+ var INBOX_DIR = join33(STATE_DIR, "inbox");
50335
50657
  function triggerSelfRestart(targetAgent, reason, delayMs = 300) {
50336
50658
  const isDocker = process.env.SWITCHROOM_RUNTIME === "docker";
50337
50659
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME;
@@ -50396,7 +50718,7 @@ function formatBootVersion() {
50396
50718
  }
50397
50719
  try {
50398
50720
  chmodSync5(ENV_FILE, 384);
50399
- for (const line of readFileSync32(ENV_FILE, "utf8").split(`
50721
+ for (const line of readFileSync34(ENV_FILE, "utf8").split(`
50400
50722
  `)) {
50401
50723
  const m = line.match(/^(\w+)=(.*)$/);
50402
50724
  if (m && process.env[m[1]] === undefined)
@@ -50449,7 +50771,7 @@ installTgPostLogger(bot);
50449
50771
  var _rawSendMessageDraft = bot.api.raw.sendMessageDraft;
50450
50772
  var GRAMMY_VERSION = (() => {
50451
50773
  try {
50452
- const raw = readFileSync32(new URL("../../node_modules/grammy/package.json", import.meta.url), "utf8");
50774
+ const raw = readFileSync34(new URL("../../node_modules/grammy/package.json", import.meta.url), "utf8");
50453
50775
  return JSON.parse(raw).version ?? "unknown";
50454
50776
  } catch {
50455
50777
  return "unknown";
@@ -50522,7 +50844,7 @@ function assertSendable(f) {
50522
50844
  } catch {
50523
50845
  throw new Error(`refusing to send file \u2014 cannot resolve real path: ${f}`);
50524
50846
  }
50525
- const inbox = join32(stateReal, "inbox");
50847
+ const inbox = join33(stateReal, "inbox");
50526
50848
  if (real.startsWith(stateReal + sep3) && !real.startsWith(inbox + sep3)) {
50527
50849
  throw new Error(`refusing to send channel state: ${f}`);
50528
50850
  }
@@ -50541,7 +50863,7 @@ function assertSendable(f) {
50541
50863
  }
50542
50864
  function readAccessFile() {
50543
50865
  try {
50544
- const raw = readFileSync32(ACCESS_FILE, "utf8");
50866
+ const raw = readFileSync34(ACCESS_FILE, "utf8");
50545
50867
  const parsed = JSON.parse(raw);
50546
50868
  const allowFrom = validateStringArray("allowFrom", parsed.allowFrom ?? []);
50547
50869
  const groups = {};
@@ -50575,7 +50897,7 @@ function readAccessFile() {
50575
50897
  if (err.code === "ENOENT")
50576
50898
  return defaultAccess();
50577
50899
  try {
50578
- renameSync12(ACCESS_FILE, `${ACCESS_FILE}.corrupt-${Date.now()}`);
50900
+ renameSync13(ACCESS_FILE, `${ACCESS_FILE}.corrupt-${Date.now()}`);
50579
50901
  } catch {}
50580
50902
  process.stderr.write(`telegram gateway: access.json is corrupt, moved aside. Starting fresh.
50581
50903
  `);
@@ -50606,11 +50928,11 @@ function assertAllowedChat(chat_id) {
50606
50928
  function saveAccess(a) {
50607
50929
  if (STATIC)
50608
50930
  return;
50609
- mkdirSync21(STATE_DIR, { recursive: true, mode: 448 });
50931
+ mkdirSync23(STATE_DIR, { recursive: true, mode: 448 });
50610
50932
  const tmp = ACCESS_FILE + ".tmp";
50611
- writeFileSync21(tmp, JSON.stringify(a, null, 2) + `
50933
+ writeFileSync23(tmp, JSON.stringify(a, null, 2) + `
50612
50934
  `, { mode: 384 });
50613
- renameSync12(tmp, ACCESS_FILE);
50935
+ renameSync13(tmp, ACCESS_FILE);
50614
50936
  }
50615
50937
  function pruneExpired(a) {
50616
50938
  const now = Date.now();
@@ -50628,7 +50950,7 @@ var HISTORY_ENABLED = HISTORY_ACCESS.historyEnabled !== false;
50628
50950
  if (HISTORY_ENABLED) {
50629
50951
  try {
50630
50952
  initHistory(STATE_DIR, HISTORY_ACCESS.historyRetentionDays ?? 30);
50631
- process.stderr.write(`telegram gateway: history capture enabled at ${join32(STATE_DIR, "history.db")}
50953
+ process.stderr.write(`telegram gateway: history capture enabled at ${join33(STATE_DIR, "history.db")}
50632
50954
  `);
50633
50955
  } catch (err) {
50634
50956
  process.stderr.write(`telegram gateway: history init failed (${err.message}) \u2014 capture disabled
@@ -50645,10 +50967,10 @@ try {
50645
50967
  process.stderr.write(`telegram gateway: turn-registry boot-reaper stamped ${reaped} orphaned turn(s) as ended_via='restart'
50646
50968
  `);
50647
50969
  } else {
50648
- process.stderr.write(`telegram gateway: turn-registry initialized at ${join32(agentDir, "telegram", "registry.db")}
50970
+ process.stderr.write(`telegram gateway: turn-registry initialized at ${join33(agentDir, "telegram", "registry.db")}
50649
50971
  `);
50650
50972
  }
50651
- const pendingEnvPath = join32(agentDir, ".pending-turn.env");
50973
+ const pendingEnvPath = join33(agentDir, ".pending-turn.env");
50652
50974
  try {
50653
50975
  const pending2 = findMostRecentInterruptedTurn(turnsDb);
50654
50976
  if (pending2 != null) {
@@ -50662,13 +50984,13 @@ try {
50662
50984
  `SWITCHROOM_PENDING_STARTED_AT=${pending2.started_at}`
50663
50985
  ];
50664
50986
  const pendingEnvTmp = `${pendingEnvPath}.tmp-${process.pid}`;
50665
- writeFileSync21(pendingEnvTmp, lines.join(`
50987
+ writeFileSync23(pendingEnvTmp, lines.join(`
50666
50988
  `) + `
50667
50989
  `, { mode: 384 });
50668
- renameSync12(pendingEnvTmp, pendingEnvPath);
50990
+ renameSync13(pendingEnvTmp, pendingEnvPath);
50669
50991
  process.stderr.write(`telegram gateway: pending-turn env written to ${pendingEnvPath} turnKey=${pending2.turn_key} endedVia=${pending2.ended_via ?? "open"}
50670
50992
  `);
50671
- } else if (existsSync34(pendingEnvPath)) {
50993
+ } else if (existsSync36(pendingEnvPath)) {
50672
50994
  rmSync4(pendingEnvPath, { force: true });
50673
50995
  process.stderr.write(`telegram gateway: pending-turn env cleared (clean previous shutdown)
50674
50996
  `);
@@ -50722,7 +51044,7 @@ function checkApprovals() {
50722
51044
  return;
50723
51045
  }
50724
51046
  for (const senderId of files) {
50725
- const file = join32(APPROVED_DIR, senderId);
51047
+ const file = join33(APPROVED_DIR, senderId);
50726
51048
  bot.api.sendMessage(senderId, "Paired! Say hi to Claude.").then(() => rmSync4(file, { force: true }), (err) => {
50727
51049
  process.stderr.write(`telegram gateway: failed to send approval confirm: ${err}
50728
51050
  `);
@@ -51606,11 +51928,11 @@ var unpinProgressCardForChat = null;
51606
51928
  var getPinnedProgressCardMessageId = null;
51607
51929
  var completeProgressCardTurn = null;
51608
51930
  var subagentWatcher = null;
51609
- var SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join32(STATE_DIR, "gateway.sock");
51610
- mkdirSync21(STATE_DIR, { recursive: true, mode: 448 });
51611
- var GATEWAY_PID_PATH = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join32(STATE_DIR, "gateway.pid.json");
51612
- var GATEWAY_SESSION_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_SESSION_MARKER ?? join32(STATE_DIR, "gateway-session.json");
51613
- var GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join32(STATE_DIR, "clean-shutdown.json");
51931
+ var SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join33(STATE_DIR, "gateway.sock");
51932
+ mkdirSync23(STATE_DIR, { recursive: true, mode: 448 });
51933
+ var GATEWAY_PID_PATH = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join33(STATE_DIR, "gateway.pid.json");
51934
+ var GATEWAY_SESSION_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_SESSION_MARKER ?? join33(STATE_DIR, "gateway-session.json");
51935
+ var GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join33(STATE_DIR, "clean-shutdown.json");
51614
51936
  var GATEWAY_STARTED_AT_MS = Date.now();
51615
51937
  var BOOT_CARD_ENABLED = process.env.SWITCHROOM_BOOT_CARD !== "false";
51616
51938
  var activeBootCard = null;
@@ -51639,7 +51961,7 @@ function ensureIssuesCard(chatId, threadId) {
51639
51961
  bot: botApi,
51640
51962
  log: (msg) => process.stderr.write(`telegram gateway: ${msg}
51641
51963
  `),
51642
- persistPath: join32(stateDir, "issues-card.json")
51964
+ persistPath: join33(stateDir, "issues-card.json")
51643
51965
  });
51644
51966
  activeIssuesWatcher = startIssuesWatcher({
51645
51967
  stateDir,
@@ -51808,13 +52130,13 @@ startTimer2({
51808
52130
  }
51809
52131
  });
51810
52132
  var inboundSpool = STATIC ? undefined : createInboundSpool({
51811
- path: join32(STATE_DIR, "inbound-spool.jsonl"),
52133
+ path: join33(STATE_DIR, "inbound-spool.jsonl"),
51812
52134
  fs: {
51813
52135
  appendFileSync: (p, d) => appendFileSync3(p, d),
51814
- readFileSync: (p) => readFileSync32(p, "utf8"),
51815
- writeFileSync: (p, d) => writeFileSync21(p, d),
51816
- renameSync: (a, b) => renameSync12(a, b),
51817
- existsSync: (p) => existsSync34(p),
52136
+ readFileSync: (p) => readFileSync34(p, "utf8"),
52137
+ writeFileSync: (p, d) => writeFileSync23(p, d),
52138
+ renameSync: (a, b) => renameSync13(a, b),
52139
+ existsSync: (p) => existsSync36(p),
51818
52140
  statSizeSync: (p) => statSync13(p).size
51819
52141
  }
51820
52142
  });
@@ -51927,11 +52249,12 @@ var ipcServer = createIpcServer({
51927
52249
  return;
51928
52250
  }
51929
52251
  })();
52252
+ const resolvedAgentDirForCard = agentDir ?? (process.env.TELEGRAM_STATE_DIR ? __require("path").dirname(process.env.TELEGRAM_STATE_DIR) : "/tmp");
51930
52253
  startBootCard(chatId, threadId, botApiForCard, {
51931
52254
  agentName: agentDisplayName,
51932
52255
  agentSlug,
51933
52256
  version: formatBootVersion(),
51934
- agentDir: agentDir ?? (process.env.TELEGRAM_STATE_DIR ? __require("path").dirname(process.env.TELEGRAM_STATE_DIR) : "/tmp"),
52257
+ agentDir: resolvedAgentDirForCard,
51935
52258
  gatewayInfo: { pid: process.pid, startedAtMs: GATEWAY_STARTED_AT_MS },
51936
52259
  restartReason: reason,
51937
52260
  restartAgeMs: markerAgeMs,
@@ -51940,6 +52263,7 @@ var ipcServer = createIpcServer({
51940
52263
  probeQuotaViaBroker: (t) => probeQuotaForBootCard(agentSlug, t),
51941
52264
  tmuxSupervisor: process.env.SWITCHROOM_TMUX_SUPERVISOR === "1",
51942
52265
  dockerMode: process.env.SWITCHROOM_RUNTIME === "docker",
52266
+ configSnapshotPath: join33(resolvedAgentDirForCard, ".config-snapshot.json"),
51943
52267
  ...updateOutcomeLine ? { updateOutcomeLine } : {}
51944
52268
  }, ackMsgId).then((handle) => {
51945
52269
  activeBootCard = handle;
@@ -53304,11 +53628,11 @@ async function executeSendGif(rawArgs) {
53304
53628
  };
53305
53629
  }
53306
53630
  async function publishToTelegraph(text, shortName, authorName) {
53307
- const accountPath = join32(STATE_DIR, "telegraph-account.json");
53631
+ const accountPath = join33(STATE_DIR, "telegraph-account.json");
53308
53632
  let account = null;
53309
53633
  try {
53310
- if (existsSync34(accountPath)) {
53311
- const raw = readFileSync32(accountPath, "utf-8");
53634
+ if (existsSync36(accountPath)) {
53635
+ const raw = readFileSync34(accountPath, "utf-8");
53312
53636
  const parsed = JSON.parse(raw);
53313
53637
  if (parsed.shortName && parsed.accessToken) {
53314
53638
  account = parsed;
@@ -53327,8 +53651,8 @@ async function publishToTelegraph(text, shortName, authorName) {
53327
53651
  }
53328
53652
  account = created.value;
53329
53653
  try {
53330
- mkdirSync21(STATE_DIR, { recursive: true, mode: 448 });
53331
- writeFileSync21(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
53654
+ mkdirSync23(STATE_DIR, { recursive: true, mode: 448 });
53655
+ writeFileSync23(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
53332
53656
  } catch (err) {
53333
53657
  process.stderr.write(`telegram gateway: telegraph cache write failed: ${err.message}
53334
53658
  `);
@@ -53570,9 +53894,9 @@ async function executeDownloadAttachment(args) {
53570
53894
  fileUniqueId: file.file_unique_id,
53571
53895
  now: Date.now()
53572
53896
  });
53573
- mkdirSync21(INBOX_DIR, { recursive: true, mode: 448 });
53897
+ mkdirSync23(INBOX_DIR, { recursive: true, mode: 448 });
53574
53898
  assertInsideInbox(INBOX_DIR, dlPath);
53575
- writeFileSync21(dlPath, buf, { mode: 384 });
53899
+ writeFileSync23(dlPath, buf, { mode: 384 });
53576
53900
  return { content: [{ type: "text", text: dlPath }] };
53577
53901
  }
53578
53902
  async function executeEditMessage(args) {
@@ -55344,14 +55668,14 @@ function restartMarkerPath() {
55344
55668
  const agentDir = resolveAgentDirFromEnv();
55345
55669
  if (!agentDir)
55346
55670
  return null;
55347
- return join32(agentDir, "restart-pending.json");
55671
+ return join33(agentDir, "restart-pending.json");
55348
55672
  }
55349
55673
  function writeRestartMarker(marker) {
55350
55674
  const p = restartMarkerPath();
55351
55675
  if (!p)
55352
55676
  return;
55353
55677
  try {
55354
- writeFileSync21(p, JSON.stringify(marker));
55678
+ writeFileSync23(p, JSON.stringify(marker));
55355
55679
  lastPlannedRestartAt = Date.now();
55356
55680
  process.stderr.write(`telegram gateway: restart-marker: write chat_id=${marker.chat_id} thread_id=${marker.thread_id ?? "-"} ack=${marker.ack_message_id ?? "-"} path=${p}
55357
55681
  `);
@@ -55370,7 +55694,7 @@ function readRestartMarker() {
55370
55694
  if (!p)
55371
55695
  return null;
55372
55696
  try {
55373
- return JSON.parse(readFileSync32(p, "utf8"));
55697
+ return JSON.parse(readFileSync34(p, "utf8"));
55374
55698
  } catch {
55375
55699
  return null;
55376
55700
  }
@@ -55468,7 +55792,7 @@ var _dockerReachable;
55468
55792
  function isDockerReachable() {
55469
55793
  if (_dockerReachable !== undefined)
55470
55794
  return _dockerReachable;
55471
- if (!existsSync34("/var/run/docker.sock")) {
55795
+ if (!existsSync36("/var/run/docker.sock")) {
55472
55796
  _dockerReachable = false;
55473
55797
  return _dockerReachable;
55474
55798
  }
@@ -55485,12 +55809,12 @@ function _resetDockerReachableCache() {
55485
55809
  }
55486
55810
  function spawnSwitchroomDetached(args, onFailure) {
55487
55811
  const fullArgs = SWITCHROOM_CONFIG ? ["--config", SWITCHROOM_CONFIG, ...args] : args;
55488
- const logPath = join32(STATE_DIR, "detached-spawn.log");
55812
+ const logPath = join33(STATE_DIR, "detached-spawn.log");
55489
55813
  let outFd = null;
55490
55814
  try {
55491
- mkdirSync21(STATE_DIR, { recursive: true });
55815
+ mkdirSync23(STATE_DIR, { recursive: true });
55492
55816
  outFd = openSync8(logPath, "a");
55493
- writeFileSync21(logPath, `
55817
+ writeFileSync23(logPath, `
55494
55818
  [${new Date().toISOString()}] spawn ${SWITCHROOM_CLI} ${fullArgs.join(" ")}
55495
55819
  `, { flag: "a" });
55496
55820
  } catch {}
@@ -55516,7 +55840,7 @@ function spawnSwitchroomDetached(args, onFailure) {
55516
55840
  return;
55517
55841
  let tail = "";
55518
55842
  try {
55519
- const full = readFileSync32(logPath, "utf8");
55843
+ const full = readFileSync34(logPath, "utf8");
55520
55844
  tail = full.split(`
55521
55845
  `).slice(-30).join(`
55522
55846
  `).trim();
@@ -55858,10 +56182,10 @@ bot.use(async (ctx, next) => {
55858
56182
  });
55859
56183
  function readRecentDenialsForAgent(agentName3, windowMs, limit) {
55860
56184
  try {
55861
- const auditPath = join32(homedir12(), ".switchroom", "vault-audit.log");
55862
- if (!existsSync34(auditPath))
56185
+ const auditPath = join33(homedir12(), ".switchroom", "vault-audit.log");
56186
+ if (!existsSync36(auditPath))
55863
56187
  return [];
55864
- const raw = readFileSync32(auditPath, "utf8");
56188
+ const raw = readFileSync34(auditPath, "utf8");
55865
56189
  return recentDenialsFromAuditLog(raw, { agentName: agentName3, windowMs, limit });
55866
56190
  } catch {
55867
56191
  return [];
@@ -55912,7 +56236,7 @@ async function buildAgentMetadata(agentName3) {
55912
56236
  try {
55913
56237
  const agentDir = resolveAgentDirFromEnv();
55914
56238
  if (agentDir) {
55915
- const raw = readFileSync32(join32(agentDir, ".claude", ".claude.json"), "utf8");
56239
+ const raw = readFileSync34(join33(agentDir, ".claude", ".claude.json"), "utf8");
55916
56240
  claudeJson = JSON.parse(raw);
55917
56241
  }
55918
56242
  } catch {}
@@ -56126,9 +56450,9 @@ bot.command("restart", async (ctx) => {
56126
56450
  function flushAgentHandoff(agentDir) {
56127
56451
  let removed = 0;
56128
56452
  for (const fname of [".handoff.md", ".handoff-topic"]) {
56129
- const p = join32(agentDir, fname);
56453
+ const p = join33(agentDir, fname);
56130
56454
  try {
56131
- if (existsSync34(p)) {
56455
+ if (existsSync36(p)) {
56132
56456
  unlinkSync14(p);
56133
56457
  removed++;
56134
56458
  }
@@ -56184,7 +56508,7 @@ async function handleNewOrResetCommand(ctx, kind) {
56184
56508
  writeRestartMarker({ chat_id: chatId, thread_id: threadId ?? null, ack_message_id: ackId, ts: Date.now() });
56185
56509
  if (agentDir != null) {
56186
56510
  try {
56187
- writeFileSync21(join32(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
56511
+ writeFileSync23(join33(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
56188
56512
  `, "utf8");
56189
56513
  } catch (err) {
56190
56514
  process.stderr.write(`telegram gateway: failed to write force-fresh marker: ${err}
@@ -56543,16 +56867,16 @@ bot.command("interrupt", async (ctx) => {
56543
56867
  await runSwitchroomCommand(ctx, ["agent", "interrupt", name], `interrupt ${name}`);
56544
56868
  });
56545
56869
  var lockoutOps = {
56546
- readFileSync: (p, enc) => readFileSync32(p, enc),
56547
- writeFileSync: (p, data, opts) => writeFileSync21(p, data, opts),
56548
- existsSync: (p) => existsSync34(p),
56549
- mkdirSync: (p, opts) => mkdirSync21(p, opts),
56550
- joinPath: (...parts) => join32(...parts)
56870
+ readFileSync: (p, enc) => readFileSync34(p, enc),
56871
+ writeFileSync: (p, data, opts) => writeFileSync23(p, data, opts),
56872
+ existsSync: (p) => existsSync36(p),
56873
+ mkdirSync: (p, opts) => mkdirSync23(p, opts),
56874
+ joinPath: (...parts) => join33(...parts)
56551
56875
  };
56552
56876
  var FLEET_FALLBACK_DEDUP_MS = 30000;
56553
56877
  function isAuthBrokerSocketReachable() {
56554
56878
  try {
56555
- return existsSync34(resolveAuthBrokerSocketPath2());
56879
+ return existsSync36(resolveAuthBrokerSocketPath2());
56556
56880
  } catch {
56557
56881
  return false;
56558
56882
  }
@@ -56613,7 +56937,7 @@ async function runCreditWatch() {
56613
56937
  if (!agentDir)
56614
56938
  return;
56615
56939
  const agentName3 = getMyAgentName();
56616
- const claudeConfigDir = join32(agentDir, ".claude");
56940
+ const claudeConfigDir = join33(agentDir, ".claude");
56617
56941
  const stateDir = STATE_DIR;
56618
56942
  const reason = readClaudeJsonOverage(claudeConfigDir);
56619
56943
  const prev = loadCreditState(stateDir);
@@ -56640,6 +56964,99 @@ async function runCreditWatch() {
56640
56964
  `);
56641
56965
  }
56642
56966
  }
56967
+ async function runQuotaWatch() {
56968
+ const agentName3 = getMyAgentName();
56969
+ const stateDir = STATE_DIR;
56970
+ const brokerClient = await getAuthBrokerClient(agentName3);
56971
+ if (!brokerClient) {
56972
+ process.stderr.write(`telegram gateway: quota-watch: broker client unavailable \u2014 skipping
56973
+ `);
56974
+ return;
56975
+ }
56976
+ let listStateData;
56977
+ try {
56978
+ listStateData = await brokerClient.listState();
56979
+ } catch (err) {
56980
+ process.stderr.write(`telegram gateway: quota-watch: listState failed: ${err}
56981
+ `);
56982
+ return;
56983
+ }
56984
+ if (!listStateData.accounts || listStateData.accounts.length === 0) {
56985
+ return;
56986
+ }
56987
+ const snapshots = buildSnapshotsFromCachedState(listStateData);
56988
+ let watchState = loadQuotaWatchState(stateDir);
56989
+ const now = Date.now();
56990
+ const access = loadAccess();
56991
+ const pendingTransitions = [];
56992
+ const labelToSnapIndex = new Map(snapshots.map((s, i) => [s.label, i]));
56993
+ for (const snap of snapshots) {
56994
+ const prev = watchState[snap.label] ?? emptyAccountState();
56995
+ const decision = evaluateQuotaWatchAccount({ agentName: agentName3, snap, prev, now });
56996
+ if (decision.kind !== "skip") {
56997
+ pendingTransitions.push({
56998
+ accountLabel: snap.label,
56999
+ snapIndex: labelToSnapIndex.get(snap.label) ?? -1,
57000
+ decision
57001
+ });
57002
+ }
57003
+ }
57004
+ if (pendingTransitions.length === 0) {
57005
+ return;
57006
+ }
57007
+ const crossingLabels = pendingTransitions.map((t) => t.accountLabel);
57008
+ let freshProbeMap = new Map;
57009
+ try {
57010
+ const probeData = await brokerClient.probeQuota(crossingLabels, 8000);
57011
+ for (const entry of probeData.results) {
57012
+ freshProbeMap.set(entry.label, entry.result);
57013
+ }
57014
+ } catch (err) {
57015
+ process.stderr.write(`telegram gateway: quota-watch: probe for crossing accounts failed: ${err}
57016
+ `);
57017
+ }
57018
+ let mutatedState = watchState;
57019
+ const notifications = [];
57020
+ for (const { accountLabel, snapIndex, decision } of pendingTransitions) {
57021
+ const freshResult = freshProbeMap.get(accountLabel);
57022
+ let enrichedDecision = decision;
57023
+ if (decision.kind !== "notify")
57024
+ continue;
57025
+ if (freshResult && freshResult.ok && snapIndex >= 0) {
57026
+ const enrichedSnap = { ...snapshots[snapIndex], quota: freshResult.data };
57027
+ const prev = watchState[accountLabel] ?? emptyAccountState();
57028
+ const re = evaluateQuotaWatchAccount({ agentName: agentName3, snap: enrichedSnap, prev, now });
57029
+ if (re.kind === "notify" && re.transition === decision.transition) {
57030
+ enrichedDecision = re;
57031
+ } else if (re.kind === "skip") {
57032
+ continue;
57033
+ }
57034
+ }
57035
+ if (enrichedDecision.kind !== "notify")
57036
+ continue;
57037
+ notifications.push({ message: enrichedDecision.message, accountLabel });
57038
+ mutatedState = patchQuotaWatchState(mutatedState, accountLabel, enrichedDecision.newAccountState);
57039
+ }
57040
+ if (notifications.length === 0) {
57041
+ return;
57042
+ }
57043
+ for (const { message, accountLabel } of notifications) {
57044
+ for (const chat_id of access.allowFrom) {
57045
+ await swallowingApiCall(() => bot.api.sendMessage(chat_id, message, {
57046
+ parse_mode: "HTML",
57047
+ link_preview_options: { is_disabled: true }
57048
+ }), { chat_id, verb: "quota-watch.notify" });
57049
+ }
57050
+ process.stderr.write(`telegram gateway: quota-watch: notified transition for account=${accountLabel}
57051
+ `);
57052
+ }
57053
+ try {
57054
+ saveQuotaWatchState(stateDir, mutatedState);
57055
+ } catch (err) {
57056
+ process.stderr.write(`telegram gateway: quota-watch state persist failed: ${err}
57057
+ `);
57058
+ }
57059
+ }
56643
57060
  bot.command("auth", async (ctx) => {
56644
57061
  const authSenderId = String(ctx.from?.id ?? "");
56645
57062
  const authOperatorPrivate = ctx.chat?.type === "private" && loadAccess().allowFrom.includes(authSenderId);
@@ -56823,10 +57240,10 @@ async function handleVaultRecentDenialCallback(ctx, data) {
56823
57240
  return;
56824
57241
  }
56825
57242
  const { token, id } = result;
56826
- const tokenPath = join32(homedir12(), ".switchroom", "agents", agentName3, ".vault-token");
57243
+ const tokenPath = join33(homedir12(), ".switchroom", "agents", agentName3, ".vault-token");
56827
57244
  try {
56828
- mkdirSync21(join32(homedir12(), ".switchroom", "agents", agentName3), { recursive: true });
56829
- writeFileSync21(tokenPath, token, { mode: 384 });
57245
+ mkdirSync23(join33(homedir12(), ".switchroom", "agents", agentName3), { recursive: true });
57246
+ writeFileSync23(tokenPath, token, { mode: 384 });
56830
57247
  } catch (err) {
56831
57248
  await switchroomReply(ctx, `<b>Grant created (${escapeHtmlForTg(id)}) but token write failed:</b> ${escapeHtmlForTg(String(err))}
56832
57249
  <i>Recover with: <code>switchroom vault grant ${escapeHtmlForTg(agentName3)} --keys ${escapeHtmlForTg(keyName)} --duration 30d</code> on the host.</i>`, { html: true });
@@ -56902,10 +57319,10 @@ async function performVaultAccessApproval(ctx, pending2, stageId, senderId, atte
56902
57319
  return;
56903
57320
  }
56904
57321
  const { token, id } = result;
56905
- const tokenPath = join32(homedir12(), ".switchroom", "agents", pending2.agent, ".vault-token");
57322
+ const tokenPath = join33(homedir12(), ".switchroom", "agents", pending2.agent, ".vault-token");
56906
57323
  try {
56907
- mkdirSync21(join32(homedir12(), ".switchroom", "agents", pending2.agent), { recursive: true });
56908
- writeFileSync21(tokenPath, token, { mode: 384 });
57324
+ mkdirSync23(join33(homedir12(), ".switchroom", "agents", pending2.agent), { recursive: true });
57325
+ writeFileSync23(tokenPath, token, { mode: 384 });
56909
57326
  } catch (err) {
56910
57327
  await switchroomReply(ctx, `<b>Grant created (${escapeHtmlForTg(id)}) but token write failed:</b> ${escapeHtmlForTg(String(err))}
56911
57328
  <i>Recover with: <code>switchroom vault grant ${escapeHtmlForTg(pending2.agent)} --keys ${escapeHtmlForTg(pending2.key)} --duration ${Math.round(pending2.ttl_seconds / 86400)}d</code> on the host.</i>`, { html: true });
@@ -57380,10 +57797,10 @@ async function executeGrantWizard(ctx, chatId, state4) {
57380
57797
  return;
57381
57798
  }
57382
57799
  const { token, id } = result;
57383
- const tokenPath = join32(homedir12(), ".switchroom", "agents", state4.agent, ".vault-token");
57800
+ const tokenPath = join33(homedir12(), ".switchroom", "agents", state4.agent, ".vault-token");
57384
57801
  try {
57385
- mkdirSync21(join32(homedir12(), ".switchroom", "agents", state4.agent), { recursive: true });
57386
- writeFileSync21(tokenPath, token, { mode: 384 });
57802
+ mkdirSync23(join33(homedir12(), ".switchroom", "agents", state4.agent), { recursive: true });
57803
+ writeFileSync23(tokenPath, token, { mode: 384 });
57387
57804
  } catch (err) {
57388
57805
  await switchroomReply(ctx, `<b>Grant created but token write failed:</b> ${escapeHtmlForTg(String(err))}`, { html: true });
57389
57806
  return;
@@ -57731,15 +58148,6 @@ async function handleOperatorEventCallback(ctx, data) {
57731
58148
  }
57732
58149
  return;
57733
58150
  }
57734
- case "swap-slot":
57735
- case "add-slot": {
57736
- await ctx.answerCallbackQuery({ text: "Phase 4c will wire this" }).catch(() => {});
57737
- const cmd = action === "swap-slot" ? `auth use ${agent} <slot-name>` : `auth add ${agent}`;
57738
- await ctx.reply(`Phase 4c will wire ${action} buttons. Until then, run in terminal: <code>switchroom ${cmd}</code>`, {
57739
- parse_mode: "HTML"
57740
- });
57741
- return;
57742
- }
57743
58151
  default: {
57744
58152
  await ctx.answerCallbackQuery({ text: `Unknown action: ${action}` }).catch(() => {});
57745
58153
  return;
@@ -57823,8 +58231,8 @@ async function handleAuthDashboardCallback(ctx) {
57823
58231
  return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
57824
58232
  });
57825
58233
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
57826
- const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState3, buildSnapshotKeyboard: buildSnapshotKeyboard3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
57827
- const snapshots = buildSnapshotsFromState3(state4, quotas);
58234
+ const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState4, buildSnapshotKeyboard: buildSnapshotKeyboard3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
58235
+ const snapshots = buildSnapshotsFromState4(state4, quotas);
57828
58236
  const text = renderAuthSnapshotFormat23(snapshots, {
57829
58237
  tz,
57830
58238
  now: new Date,
@@ -58219,9 +58627,9 @@ bot.command("usage", async (ctx) => {
58219
58627
  const hit = probeResp.results.find((r) => r.label === a.label);
58220
58628
  return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
58221
58629
  });
58222
- const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
58630
+ const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState4 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
58223
58631
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
58224
- const snapshots = buildSnapshotsFromState3(state4, quotas);
58632
+ const snapshots = buildSnapshotsFromState4(state4, quotas);
58225
58633
  const text = renderAuthSnapshotFormat23(snapshots, {
58226
58634
  tz,
58227
58635
  now: new Date,
@@ -58240,7 +58648,7 @@ bot.command("usage", async (ctx) => {
58240
58648
  await switchroomReply(ctx, "<b>/usage:</b> cannot resolve agent dir.", { html: true });
58241
58649
  return;
58242
58650
  }
58243
- const result = await fetchQuota2({ claudeConfigDir: join32(agentDir, ".claude") });
58651
+ const result = await fetchQuota2({ claudeConfigDir: join33(agentDir, ".claude") });
58244
58652
  if (!result.ok) {
58245
58653
  await switchroomReply(ctx, `<b>/usage:</b> ${escapeHtmlForTg(result.reason)}`, { html: true });
58246
58654
  return;
@@ -58743,9 +59151,9 @@ bot.on("message:photo", async (ctx) => {
58743
59151
  fileUniqueId: best.file_unique_id,
58744
59152
  now: Date.now()
58745
59153
  });
58746
- mkdirSync21(INBOX_DIR, { recursive: true, mode: 448 });
59154
+ mkdirSync23(INBOX_DIR, { recursive: true, mode: 448 });
58747
59155
  assertInsideInbox(INBOX_DIR, dlPath);
58748
- writeFileSync21(dlPath, buf, { mode: 384 });
59156
+ writeFileSync23(dlPath, buf, { mode: 384 });
58749
59157
  return dlPath;
58750
59158
  } catch (err) {
58751
59159
  const msg = err instanceof Error ? err.message : "unknown error";
@@ -58785,8 +59193,8 @@ async function maybeTranscribeVoice(fileId, mimeType, language) {
58785
59193
  let apiKey = null;
58786
59194
  try {
58787
59195
  const path = __require("path").join(__require("os").homedir(), ".switchroom", "openai-api-key");
58788
- if (existsSync34(path)) {
58789
- apiKey = readFileSync32(path, "utf-8").trim();
59196
+ if (existsSync36(path)) {
59197
+ apiKey = readFileSync34(path, "utf-8").trim();
58790
59198
  }
58791
59199
  } catch (err) {
58792
59200
  process.stderr.write(`telegram gateway: voice-in: failed to read api key: ${err.message}
@@ -59642,11 +60050,12 @@ var didOneTimeSetup = false;
59642
60050
  return;
59643
60051
  }
59644
60052
  })();
60053
+ const resolvedAgentDirForBootCard = agentDir ?? join33(homedir12(), ".switchroom", "agents", agentSlug);
59645
60054
  const handle = await startBootCard(chatId, threadId, botApiForCard, {
59646
60055
  agentName: agentDisplayName,
59647
60056
  agentSlug,
59648
60057
  version: formatBootVersion(),
59649
- agentDir: agentDir ?? join32(homedir12(), ".switchroom", "agents", agentSlug),
60058
+ agentDir: resolvedAgentDirForBootCard,
59650
60059
  gatewayInfo: { pid: process.pid, startedAtMs: GATEWAY_STARTED_AT_MS },
59651
60060
  restartReason: reason,
59652
60061
  restartAgeMs: markerAgeMs,
@@ -59655,6 +60064,7 @@ var didOneTimeSetup = false;
59655
60064
  probeQuotaViaBroker: (t) => probeQuotaForBootCard(agentSlug, t),
59656
60065
  tmuxSupervisor: process.env.SWITCHROOM_TMUX_SUPERVISOR === "1",
59657
60066
  dockerMode: process.env.SWITCHROOM_RUNTIME === "docker",
60067
+ configSnapshotPath: join33(resolvedAgentDirForBootCard, ".config-snapshot.json"),
59658
60068
  ...updateOutcomeLine ? { updateOutcomeLine } : {}
59659
60069
  }, ackMsgId);
59660
60070
  activeBootCard = handle;
@@ -59694,6 +60104,21 @@ var didOneTimeSetup = false;
59694
60104
  });
59695
60105
  }, CREDIT_WATCH_POLL_MS).unref();
59696
60106
  }
60107
+ const QUOTA_WATCH_POLL_MS = Number(process.env.SWITCHROOM_QUOTA_WATCH_POLL_MS ?? 900000);
60108
+ if (QUOTA_WATCH_POLL_MS > 0) {
60109
+ setTimeout(() => {
60110
+ runQuotaWatch().catch((err) => {
60111
+ process.stderr.write(`telegram gateway: quota-watch initial run failed: ${err}
60112
+ `);
60113
+ });
60114
+ }, 30000);
60115
+ setInterval(() => {
60116
+ runQuotaWatch().catch((err) => {
60117
+ process.stderr.write(`telegram gateway: quota-watch scheduled run failed: ${err}
60118
+ `);
60119
+ });
60120
+ }, QUOTA_WATCH_POLL_MS).unref();
60121
+ }
59697
60122
  const RESTART_WATCHDOG_POLL_MS = Number(process.env.SWITCHROOM_RESTART_WATCHDOG_POLL_MS ?? 30000);
59698
60123
  const watchdogAgentName = process.env.SWITCHROOM_AGENT_NAME;
59699
60124
  const watchdogDockerMode = process.env.SWITCHROOM_RUNTIME === "docker";