switchroom 0.14.0 → 0.14.2

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
- import {
29159
- readFileSync as readFileSync31,
29160
- writeFileSync as writeFileSync19,
29161
- existsSync as existsSync32,
29162
- renameSync as renameSync11,
29163
- mkdirSync as mkdirSync18,
29652
+ import {
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) {
@@ -29999,366 +30493,53 @@ function registerApprovalsCommands(bot, opts) {
29999
30493
  return;
30000
30494
  }
30001
30495
  if (decisions.length === 0) {
30002
- await ctx.reply(agentFilter ? `No active approvals for <code>${escapeHtml12(agentFilter)}</code>.` : "No active approvals.", { parse_mode: "HTML" });
30496
+ await ctx.reply(agentFilter ? `No active approvals for <code>${escapeHtml14(agentFilter)}</code>.` : "No active approvals.", { parse_mode: "HTML" });
30003
30497
  return;
30004
30498
  }
30005
30499
  const byAgent = new Map;
30006
30500
  for (const d of decisions)
30007
30501
  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(`
30502
+ const summary = Array.from(byAgent.entries()).map(([a, n]) => `\u2022 <b>${escapeHtml14(a)}</b>: ${n}`).join(`
30253
30503
  `);
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(`
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(`
30276
30508
  `);
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}`
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";
@@ -31790,6 +31971,26 @@ function describeToolUse(toolName, input) {
31790
31971
  return "Working\u2026";
31791
31972
  }
31792
31973
  }
31974
+ var MIRROR_MAX_LINES = 6;
31975
+ function appendActivityLine(lines, toolName, input) {
31976
+ const line = describeToolUse(toolName, input);
31977
+ if (line == null)
31978
+ return null;
31979
+ if (lines.length === 0 || lines[lines.length - 1] !== line) {
31980
+ lines.push(line);
31981
+ }
31982
+ return renderActivityFeed(lines);
31983
+ }
31984
+ function renderActivityFeed(lines) {
31985
+ if (lines.length === 0)
31986
+ return null;
31987
+ const shown = lines.slice(-MIRROR_MAX_LINES);
31988
+ const hidden = lines.length - shown.length;
31989
+ const body = shown.map((l) => `\u00b7 ${l}`).join(`
31990
+ `);
31991
+ return hidden > 0 ? `\u00b7 +${hidden} earlier\u2026
31992
+ ${body}` : body;
31993
+ }
31793
31994
 
31794
31995
  // tool-labels.ts
31795
31996
  var MAX_LABEL_CHARS = 60;
@@ -38520,6 +38721,7 @@ function isSilentFlushMarker(text) {
38520
38721
  }
38521
38722
  return SILENT_MARKERS.has(trimmed.toUpperCase());
38522
38723
  }
38724
+ var TRIVIAL_CONFIRMATIONS = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
38523
38725
 
38524
38726
  // answer-stream.ts
38525
38727
  var MIN_INITIAL_CHARS = 50;
@@ -40488,15 +40690,11 @@ function renderOperatorEvent(ev) {
40488
40690
  text: [
40489
40691
  `\uD83D\uDCB3 <b>Credit balance too low</b> for <b>${agent}</b>.`,
40490
40692
  detail ? `<i>${detail}</i>` : "",
40491
- `Swap to another account slot or add a new one.`
40693
+ `Use <code>/auth use &lt;label&gt;</code> to switch account slot or <code>/auth add</code> to add one.`
40492
40694
  ].filter(Boolean).join(`
40493
40695
  `),
40494
40696
  keyboard: {
40495
40697
  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
40698
  [{ text: "\u23f3 Wait", callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }]
40501
40699
  ]
40502
40700
  }
@@ -40506,15 +40704,11 @@ function renderOperatorEvent(ev) {
40506
40704
  text: [
40507
40705
  `\u26a0\ufe0f <b>Quota exhausted</b> for <b>${agent}</b>.`,
40508
40706
  detail ? `<i>${detail}</i>` : "",
40509
- `All account slots are at the usage limit. Switchroom will auto-fallback when another slot is available.`
40707
+ `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
40708
  ].filter(Boolean).join(`
40511
40709
  `),
40512
40710
  keyboard: {
40513
40711
  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
40712
  [{ text: "\u23f3 Wait", callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }]
40519
40713
  ]
40520
40714
  }
@@ -41812,6 +42006,27 @@ function isSilentFlushMarker2(text) {
41812
42006
  }
41813
42007
  return SILENT_MARKERS2.has(trimmed.toUpperCase());
41814
42008
  }
42009
+ var TRIVIAL_CONFIRMATIONS2 = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
42010
+ function isTrivialConfirmationLine(line) {
42011
+ let t = line.trim();
42012
+ if (t.length === 0 || t.length > 8)
42013
+ return false;
42014
+ if (/\W$/.test(t))
42015
+ t = t.slice(0, -1);
42016
+ return TRIVIAL_CONFIRMATIONS2.has(t.toUpperCase());
42017
+ }
42018
+ function isCompositeSilentNoise(text) {
42019
+ if (typeof text !== "string")
42020
+ return false;
42021
+ const lines = text.split(`
42022
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
42023
+ if (lines.length === 0)
42024
+ return false;
42025
+ const hasMarker = lines.some((l) => isSilentFlushMarker2(l));
42026
+ if (!hasMarker)
42027
+ return false;
42028
+ return lines.every((l) => isSilentFlushMarker2(l) || isTrivialConfirmationLine(l));
42029
+ }
41815
42030
  function decideTurnFlush(input) {
41816
42031
  const flushEnabled = input.flushEnabled !== false;
41817
42032
  if (!flushEnabled)
@@ -41826,6 +42041,8 @@ function decideTurnFlush(input) {
41826
42041
  return { kind: "skip", reason: "empty-text" };
41827
42042
  if (isSilentFlushMarker2(joined))
41828
42043
  return { kind: "skip", reason: "silent-marker" };
42044
+ if (isCompositeSilentNoise(joined))
42045
+ return { kind: "skip", reason: "silent-marker" };
41829
42046
  return { kind: "flush", text: joined };
41830
42047
  }
41831
42048
  function isTurnFlushSafetyEnabled(env = process.env) {
@@ -48729,7 +48946,7 @@ function determineRestartReason(opts) {
48729
48946
  init_boot_card();
48730
48947
 
48731
48948
  // gateway/update-announce.ts
48732
- import { existsSync as existsSync26, mkdirSync as mkdirSync13, openSync as openSync3, closeSync as closeSync3, readFileSync as readFileSync25 } from "node:fs";
48949
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, openSync as openSync3, closeSync as closeSync3, readFileSync as readFileSync26 } from "node:fs";
48733
48950
  import { join as join24 } from "node:path";
48734
48951
  import { homedir as homedir10 } from "node:os";
48735
48952
 
@@ -48843,8 +49060,8 @@ function readAndFilter(raw, filters, limit) {
48843
49060
  var DEFAULT_LOOKBACK_MS = 10 * 60 * 1000;
48844
49061
  function readLastTerminalUpdateAudit(opts = {}) {
48845
49062
  const path = opts.auditLogPath ?? defaultAuditLogPath();
48846
- const exists = opts.exists ?? existsSync26;
48847
- const readFile = opts.readFile ?? ((p) => readFileSync25(p, "utf-8"));
49063
+ const exists = opts.exists ?? existsSync27;
49064
+ const readFile = opts.readFile ?? ((p) => readFileSync26(p, "utf-8"));
48848
49065
  if (!exists(path))
48849
49066
  return null;
48850
49067
  let raw;
@@ -48908,7 +49125,7 @@ function claimUpdateAnnouncement(requestId, opts = {}) {
48908
49125
  const stateDir = opts.stateDir ?? process.env.TELEGRAM_STATE_DIR ?? join24(homedir10(), ".switchroom");
48909
49126
  const dir = join24(stateDir, "update-announced");
48910
49127
  try {
48911
- mkdirSync13(dir, { recursive: true });
49128
+ mkdirSync14(dir, { recursive: true });
48912
49129
  } catch {
48913
49130
  return false;
48914
49131
  }
@@ -48932,7 +49149,7 @@ function maybeRenderUpdateAnnouncement(opts = {}) {
48932
49149
  }
48933
49150
 
48934
49151
  // issues-card.ts
48935
- import { readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
49152
+ import { readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
48936
49153
  var SEVERITY_EMOJI = {
48937
49154
  info: "\u2139\ufe0f",
48938
49155
  warn: "\u26a0\ufe0f",
@@ -49024,7 +49241,7 @@ function extractRetryAfterSecs(err) {
49024
49241
  var COOLDOWN_JITTER_MS = 500;
49025
49242
  function readPersistedMessageId(path, log) {
49026
49243
  try {
49027
- const raw = readFileSync26(path, "utf8");
49244
+ const raw = readFileSync27(path, "utf8");
49028
49245
  const parsed = JSON.parse(raw);
49029
49246
  const v = parsed.messageId;
49030
49247
  if (typeof v === "number" && Number.isInteger(v) && v > 0)
@@ -49040,7 +49257,7 @@ function readPersistedMessageId(path, log) {
49040
49257
  }
49041
49258
  function writePersistedMessageId(path, messageId, log) {
49042
49259
  try {
49043
- writeFileSync15(path, JSON.stringify({ messageId }) + `
49260
+ writeFileSync16(path, JSON.stringify({ messageId }) + `
49044
49261
  `, { mode: 384 });
49045
49262
  } catch (err) {
49046
49263
  log(`issues-card: persist write failed (${err.message})`);
@@ -49133,21 +49350,21 @@ function createIssuesCardHandle(opts) {
49133
49350
  }
49134
49351
 
49135
49352
  // issues-watcher.ts
49136
- import { existsSync as existsSync28, statSync as statSync8 } from "node:fs";
49353
+ import { existsSync as existsSync29, statSync as statSync8 } from "node:fs";
49137
49354
  import { join as join26 } from "node:path";
49138
49355
 
49139
49356
  // ../src/issues/store.ts
49140
49357
  import {
49141
49358
  closeSync as closeSync4,
49142
- existsSync as existsSync27,
49143
- mkdirSync as mkdirSync14,
49359
+ existsSync as existsSync28,
49360
+ mkdirSync as mkdirSync15,
49144
49361
  openSync as openSync4,
49145
49362
  readdirSync as readdirSync5,
49146
- readFileSync as readFileSync27,
49147
- renameSync as renameSync10,
49363
+ readFileSync as readFileSync28,
49364
+ renameSync as renameSync11,
49148
49365
  statSync as statSync7,
49149
49366
  unlinkSync as unlinkSync10,
49150
- writeFileSync as writeFileSync16,
49367
+ writeFileSync as writeFileSync17,
49151
49368
  writeSync
49152
49369
  } from "node:fs";
49153
49370
  import { join as join25 } from "node:path";
@@ -49167,11 +49384,11 @@ var ISSUES_FILE = "issues.jsonl";
49167
49384
  var ISSUES_LOCK = "issues.lock";
49168
49385
  function readAll(stateDir) {
49169
49386
  const path = join25(stateDir, ISSUES_FILE);
49170
- if (!existsSync27(path))
49387
+ if (!existsSync28(path))
49171
49388
  return [];
49172
49389
  let raw;
49173
49390
  try {
49174
- raw = readFileSync27(path, "utf-8");
49391
+ raw = readFileSync28(path, "utf-8");
49175
49392
  } catch {
49176
49393
  return [];
49177
49394
  }
@@ -49203,7 +49420,7 @@ function list(stateDir, opts = {}) {
49203
49420
  });
49204
49421
  }
49205
49422
  function resolve6(stateDir, fingerprint, nowFn = Date.now) {
49206
- if (!existsSync27(join25(stateDir, ISSUES_FILE)))
49423
+ if (!existsSync28(join25(stateDir, ISSUES_FILE)))
49207
49424
  return 0;
49208
49425
  return withLock(stateDir, () => {
49209
49426
  const all = readAll(stateDir);
@@ -49227,8 +49444,8 @@ function writeAll(stateDir, events) {
49227
49444
  const body = events.length === 0 ? "" : events.map((e) => JSON.stringify(e)).join(`
49228
49445
  `) + `
49229
49446
  `;
49230
- writeFileSync16(tmp, body, "utf-8");
49231
- renameSync10(tmp, path);
49447
+ writeFileSync17(tmp, body, "utf-8");
49448
+ renameSync11(tmp, path);
49232
49449
  }
49233
49450
  var ORPHAN_TMP_TTL_MS = 60000;
49234
49451
  var TMP_PREFIX = `${ISSUES_FILE}.tmp-`;
@@ -49290,7 +49507,7 @@ function withLock(stateDir, fn) {
49290
49507
  function tryStealStaleLock(lockPath) {
49291
49508
  let pidStr;
49292
49509
  try {
49293
- pidStr = readFileSync27(lockPath, "utf-8").trim();
49510
+ pidStr = readFileSync28(lockPath, "utf-8").trim();
49294
49511
  } catch {
49295
49512
  return true;
49296
49513
  }
@@ -49388,7 +49605,7 @@ function startIssuesWatcher(opts) {
49388
49605
  };
49389
49606
  }
49390
49607
  function defaultSignatureProvider(path) {
49391
- if (!existsSync28(path))
49608
+ if (!existsSync29(path))
49392
49609
  return null;
49393
49610
  try {
49394
49611
  const stat = statSync8(path);
@@ -49628,7 +49845,7 @@ function skillBasenameFromPath2(input) {
49628
49845
  }
49629
49846
 
49630
49847
  // credits-watch.ts
49631
- import { readFileSync as readFileSync28, writeFileSync as writeFileSync17, existsSync as existsSync29, mkdirSync as mkdirSync15 } from "fs";
49848
+ import { readFileSync as readFileSync29, writeFileSync as writeFileSync18, existsSync as existsSync30, mkdirSync as mkdirSync16 } from "fs";
49632
49849
  import { join as join27 } from "path";
49633
49850
  var STATE_FILE = "credits-watch.json";
49634
49851
  var FATAL_REASONS = new Set([
@@ -49642,11 +49859,11 @@ function emptyCreditState() {
49642
49859
  }
49643
49860
  function readClaudeJsonOverage(claudeConfigDir) {
49644
49861
  const path = join27(claudeConfigDir, ".claude.json");
49645
- if (!existsSync29(path))
49862
+ if (!existsSync30(path))
49646
49863
  return null;
49647
49864
  let raw;
49648
49865
  try {
49649
- raw = readFileSync28(path, "utf-8");
49866
+ raw = readFileSync29(path, "utf-8");
49650
49867
  } catch {
49651
49868
  return null;
49652
49869
  }
@@ -49727,10 +49944,10 @@ function escapeHtml10(s) {
49727
49944
  }
49728
49945
  function loadCreditState(stateDir) {
49729
49946
  const path = join27(stateDir, STATE_FILE);
49730
- if (!existsSync29(path))
49947
+ if (!existsSync30(path))
49731
49948
  return emptyCreditState();
49732
49949
  try {
49733
- const raw = readFileSync28(path, "utf-8");
49950
+ const raw = readFileSync29(path, "utf-8");
49734
49951
  const parsed = JSON.parse(raw);
49735
49952
  if (parsed && typeof parsed === "object" && (parsed.lastNotifiedReason === null || typeof parsed.lastNotifiedReason === "string") && typeof parsed.lastNotifiedAt === "number" && Number.isFinite(parsed.lastNotifiedAt)) {
49736
49953
  return {
@@ -49742,36 +49959,161 @@ function loadCreditState(stateDir) {
49742
49959
  return emptyCreditState();
49743
49960
  }
49744
49961
  function saveCreditState(stateDir, state4) {
49745
- mkdirSync15(stateDir, { recursive: true });
49962
+ mkdirSync16(stateDir, { recursive: true });
49746
49963
  const path = join27(stateDir, STATE_FILE);
49747
- writeFileSync17(path, JSON.stringify(state4, null, 2) + `
49964
+ writeFileSync18(path, JSON.stringify(state4, null, 2) + `
49965
+ `, { mode: 384 });
49966
+ }
49967
+
49968
+ // quota-watch.ts
49969
+ import { readFileSync as readFileSync30, writeFileSync as writeFileSync19, existsSync as existsSync31, mkdirSync as mkdirSync17 } from "fs";
49970
+ import { join as join28 } from "path";
49971
+ var STATE_FILE2 = "quota-watch.json";
49972
+ function emptyQuotaWatchState() {
49973
+ return {};
49974
+ }
49975
+ function emptyAccountState() {
49976
+ return { lastNotifiedHealth: null, lastNotifiedAt: 0 };
49977
+ }
49978
+ function evaluateQuotaWatchAccount(args) {
49979
+ const { agentName: agentName3, snap, prev, now } = args;
49980
+ const label = snap.label;
49981
+ const currentHealth = classifyHealth(snap);
49982
+ if (currentHealth === "unknown" || currentHealth === "blocked") {
49983
+ return { kind: "skip", accountLabel: label, reason: `${currentHealth}-not-our-domain` };
49984
+ }
49985
+ const prevHealth = prev.lastNotifiedHealth ?? "healthy";
49986
+ if (currentHealth === prevHealth) {
49987
+ return { kind: "skip", accountLabel: label, reason: "steady-state" };
49988
+ }
49989
+ if (currentHealth === "throttling" && prevHealth === "healthy") {
49990
+ const newState = {
49991
+ lastNotifiedHealth: "throttling",
49992
+ lastNotifiedAt: now
49993
+ };
49994
+ return {
49995
+ kind: "notify",
49996
+ accountLabel: label,
49997
+ message: buildThrottlingMessage(agentName3, snap),
49998
+ newAccountState: newState,
49999
+ transition: "entered-throttling"
50000
+ };
50001
+ }
50002
+ if (currentHealth === "healthy" && prevHealth === "throttling") {
50003
+ const newState = {
50004
+ lastNotifiedHealth: "healthy",
50005
+ lastNotifiedAt: now
50006
+ };
50007
+ return {
50008
+ kind: "notify",
50009
+ accountLabel: label,
50010
+ message: buildRecoveryMessage(agentName3, snap),
50011
+ newAccountState: newState,
50012
+ transition: "recovered-to-healthy"
50013
+ };
50014
+ }
50015
+ return { kind: "skip", accountLabel: label, reason: "no-matching-transition" };
50016
+ }
50017
+ function buildThrottlingMessage(agentName3, snap) {
50018
+ const q = snap.quota;
50019
+ const fiveStr = fmtPct(q.fiveHourUtilizationPct);
50020
+ const sevenStr = fmtPct(q.sevenDayUtilizationPct);
50021
+ const max = Math.max(q.fiveHourUtilizationPct, q.sevenDayUtilizationPct);
50022
+ const win = max === q.fiveHourUtilizationPct ? "5h" : "7d";
50023
+ const winLabel = win === "5h" ? "5-hour" : "7-day";
50024
+ const resetAt = win === "5h" ? q.fiveHourResetAt : q.sevenDayResetAt;
50025
+ const resetStr = resetAt ? ` \u00b7 refills in ${formatRelative(resetAt, new Date)}` : "";
50026
+ const activeNote = snap.isActive ? "" : `
50027
+ This is a non-active account. Consider <code>/auth use ${escapeHtml11(snap.label)}</code> to switch, or keep it as a fallback reserve.`;
50028
+ const altNote = snap.isActive ? `
50029
+ Consider <code>/auth use &lt;other-account&gt;</code> if you have a healthier account, or wait for the ${winLabel} window to refill${resetStr}.` : "";
50030
+ return [
50031
+ `\uD83D\uDFE1 <b>Quota approaching limit</b> \u2014 <code>${escapeHtml11(snap.label)}</code>`,
50032
+ ``,
50033
+ `${fiveStr} of 5h \u00b7 ${sevenStr} of 7d`,
50034
+ `Binding window: ${winLabel}${resetStr}`,
50035
+ `${activeNote}${altNote}`,
50036
+ ``,
50037
+ `<i>Threshold: ${THROTTLING_THRESHOLD_PCT}% on either window. Source: broker quota cache.</i>`,
50038
+ `<i>Run /auth for full fleet status or /usage for the active account.</i>`
50039
+ ].join(`
50040
+ `).replace(/\n\n\n+/g, `
50041
+
50042
+ `).trim();
50043
+ }
50044
+ function buildRecoveryMessage(agentName3, snap) {
50045
+ const q = snap.quota;
50046
+ const utilLine = q ? `Current: ${fmtPct(q.fiveHourUtilizationPct)} of 5h \u00b7 ${fmtPct(q.sevenDayUtilizationPct)} of 7d` : "Current quota data unavailable.";
50047
+ return [
50048
+ `\uD83D\uDFE2 <b>Quota back in healthy range</b> \u2014 <code>${escapeHtml11(snap.label)}</code>`,
50049
+ ``,
50050
+ utilLine,
50051
+ ``,
50052
+ `<i>Below ${THROTTLING_THRESHOLD_PCT}% on both windows.</i>`
50053
+ ].join(`
50054
+ `);
50055
+ }
50056
+ function escapeHtml11(s) {
50057
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
50058
+ }
50059
+ function loadQuotaWatchState(stateDir) {
50060
+ const path = join28(stateDir, STATE_FILE2);
50061
+ if (!existsSync31(path))
50062
+ return emptyQuotaWatchState();
50063
+ try {
50064
+ const raw = readFileSync30(path, "utf-8");
50065
+ const parsed = JSON.parse(raw);
50066
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
50067
+ return emptyQuotaWatchState();
50068
+ }
50069
+ const result = {};
50070
+ for (const [key, val] of Object.entries(parsed)) {
50071
+ 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)) {
50072
+ result[key] = val;
50073
+ }
50074
+ }
50075
+ return result;
50076
+ } catch {
50077
+ return emptyQuotaWatchState();
50078
+ }
50079
+ }
50080
+ function saveQuotaWatchState(stateDir, state4) {
50081
+ mkdirSync17(stateDir, { recursive: true });
50082
+ const path = join28(stateDir, STATE_FILE2);
50083
+ writeFileSync19(path, JSON.stringify(state4, null, 2) + `
49748
50084
  `, { mode: 384 });
49749
50085
  }
50086
+ function patchQuotaWatchState(current, accountLabel, accountState) {
50087
+ return { ...current, [accountLabel]: accountState };
50088
+ }
50089
+
50090
+ // gateway/gateway.ts
50091
+ init_auth_snapshot_format();
49750
50092
 
49751
50093
  // gateway/turn-active-marker.ts
49752
50094
  import {
49753
50095
  closeSync as closeSync5,
49754
- existsSync as existsSync30,
49755
- mkdirSync as mkdirSync16,
50096
+ existsSync as existsSync32,
50097
+ mkdirSync as mkdirSync18,
49756
50098
  openSync as openSync5,
49757
- readFileSync as readFileSync29,
50099
+ readFileSync as readFileSync31,
49758
50100
  statSync as statSync9,
49759
50101
  unlinkSync as unlinkSync11,
49760
50102
  utimesSync as utimesSync2,
49761
- writeFileSync as writeFileSync18
50103
+ writeFileSync as writeFileSync20
49762
50104
  } from "node:fs";
49763
- import { join as join28 } from "node:path";
50105
+ import { join as join29 } from "node:path";
49764
50106
  var TURN_ACTIVE_MARKER_FILE2 = "turn-active.json";
49765
50107
  function writeTurnActiveMarker(stateDir, marker) {
49766
50108
  try {
49767
- mkdirSync16(stateDir, { recursive: true });
49768
- writeFileSync18(join28(stateDir, TURN_ACTIVE_MARKER_FILE2), JSON.stringify(marker, null, 2) + `
50109
+ mkdirSync18(stateDir, { recursive: true });
50110
+ writeFileSync20(join29(stateDir, TURN_ACTIVE_MARKER_FILE2), JSON.stringify(marker, null, 2) + `
49769
50111
  `, { mode: 384 });
49770
50112
  } catch {}
49771
50113
  }
49772
50114
  function touchTurnActiveMarker2(stateDir) {
49773
- const path = join28(stateDir, TURN_ACTIVE_MARKER_FILE2);
49774
- if (!existsSync30(path))
50115
+ const path = join29(stateDir, TURN_ACTIVE_MARKER_FILE2);
50116
+ if (!existsSync32(path))
49775
50117
  return;
49776
50118
  const now = new Date;
49777
50119
  try {
@@ -49785,12 +50127,12 @@ function touchTurnActiveMarker2(stateDir) {
49785
50127
  }
49786
50128
  function removeTurnActiveMarker(stateDir) {
49787
50129
  try {
49788
- unlinkSync11(join28(stateDir, TURN_ACTIVE_MARKER_FILE2));
50130
+ unlinkSync11(join29(stateDir, TURN_ACTIVE_MARKER_FILE2));
49789
50131
  } catch {}
49790
50132
  }
49791
50133
  function sweepStaleTurnActiveMarker(stateDir, opts) {
49792
- const path = join28(stateDir, TURN_ACTIVE_MARKER_FILE2);
49793
- if (!existsSync30(path))
50134
+ const path = join29(stateDir, TURN_ACTIVE_MARKER_FILE2);
50135
+ if (!existsSync32(path))
49794
50136
  return false;
49795
50137
  const now = opts.now ?? Date.now();
49796
50138
  try {
@@ -49802,7 +50144,7 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
49802
50144
  return false;
49803
50145
  let payload = null;
49804
50146
  try {
49805
- payload = readFileSync29(path, "utf8");
50147
+ payload = readFileSync31(path, "utf8");
49806
50148
  } catch {}
49807
50149
  unlinkSync11(path);
49808
50150
  if (opts.onRemove) {
@@ -49821,10 +50163,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
49821
50163
  }
49822
50164
 
49823
50165
  // ../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;
50166
+ var VERSION = "0.14.2";
50167
+ var COMMIT_SHA = "3c7d0238";
50168
+ var COMMIT_DATE = "2026-05-28T07:47:55Z";
50169
+ var LATEST_PR = 1958;
49828
50170
  var COMMITS_AHEAD_OF_TAG = 0;
49829
50171
 
49830
50172
  // gateway/boot-version.ts
@@ -49898,11 +50240,11 @@ init_peercred();
49898
50240
  import * as net4 from "node:net";
49899
50241
  import * as fs2 from "node:fs";
49900
50242
  import { homedir as homedir11 } from "node:os";
49901
- import { join as join29 } from "node:path";
50243
+ import { join as join30 } from "node:path";
49902
50244
  var DEFAULT_TIMEOUT_MS4 = 2000;
49903
50245
  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");
50246
+ var LEGACY_SOCKET_PATH2 = join30(homedir11(), ".switchroom", "vault-broker.sock");
50247
+ var OPERATOR_SOCKET_PATH2 = join30(homedir11(), ".switchroom", "broker-operator", "sock");
49906
50248
  function defaultBrokerSocketPath2() {
49907
50249
  if (fs2.existsSync(OPERATOR_SOCKET_PATH2))
49908
50250
  return OPERATOR_SOCKET_PATH2;
@@ -50124,8 +50466,8 @@ function resolveVaultApprovalPosture(broker) {
50124
50466
  }
50125
50467
 
50126
50468
  // registry/turns-schema.ts
50127
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync17 } from "fs";
50128
- import { join as join30 } from "path";
50469
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync19 } from "fs";
50470
+ import { join as join31 } from "path";
50129
50471
  var DatabaseClass2 = null;
50130
50472
  function loadDatabaseClass2() {
50131
50473
  if (DatabaseClass2 != null)
@@ -50184,9 +50526,9 @@ function applySchema(db2) {
50184
50526
  }
50185
50527
  function openTurnsDb(agentDir) {
50186
50528
  const Database = loadDatabaseClass2();
50187
- const dir = join30(agentDir, "telegram");
50188
- mkdirSync17(dir, { recursive: true, mode: 448 });
50189
- const path = join30(dir, "registry.db");
50529
+ const dir = join31(agentDir, "telegram");
50530
+ mkdirSync19(dir, { recursive: true, mode: 448 });
50531
+ const path = join31(dir, "registry.db");
50190
50532
  const db2 = new Database(path, { create: true });
50191
50533
  applySchema(db2);
50192
50534
  try {
@@ -50327,11 +50669,11 @@ installGlobalErrorHandlers();
50327
50669
  process.on("beforeExit", () => {
50328
50670
  shutdownAnalytics();
50329
50671
  });
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");
50672
+ var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join33(homedir12(), ".claude", "channels", "telegram");
50673
+ var ACCESS_FILE = join33(STATE_DIR, "access.json");
50674
+ var APPROVED_DIR = join33(STATE_DIR, "approved");
50675
+ var ENV_FILE = join33(STATE_DIR, ".env");
50676
+ var INBOX_DIR = join33(STATE_DIR, "inbox");
50335
50677
  function triggerSelfRestart(targetAgent, reason, delayMs = 300) {
50336
50678
  const isDocker = process.env.SWITCHROOM_RUNTIME === "docker";
50337
50679
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME;
@@ -50396,7 +50738,7 @@ function formatBootVersion() {
50396
50738
  }
50397
50739
  try {
50398
50740
  chmodSync5(ENV_FILE, 384);
50399
- for (const line of readFileSync32(ENV_FILE, "utf8").split(`
50741
+ for (const line of readFileSync34(ENV_FILE, "utf8").split(`
50400
50742
  `)) {
50401
50743
  const m = line.match(/^(\w+)=(.*)$/);
50402
50744
  if (m && process.env[m[1]] === undefined)
@@ -50449,7 +50791,7 @@ installTgPostLogger(bot);
50449
50791
  var _rawSendMessageDraft = bot.api.raw.sendMessageDraft;
50450
50792
  var GRAMMY_VERSION = (() => {
50451
50793
  try {
50452
- const raw = readFileSync32(new URL("../../node_modules/grammy/package.json", import.meta.url), "utf8");
50794
+ const raw = readFileSync34(new URL("../../node_modules/grammy/package.json", import.meta.url), "utf8");
50453
50795
  return JSON.parse(raw).version ?? "unknown";
50454
50796
  } catch {
50455
50797
  return "unknown";
@@ -50522,7 +50864,7 @@ function assertSendable(f) {
50522
50864
  } catch {
50523
50865
  throw new Error(`refusing to send file \u2014 cannot resolve real path: ${f}`);
50524
50866
  }
50525
- const inbox = join32(stateReal, "inbox");
50867
+ const inbox = join33(stateReal, "inbox");
50526
50868
  if (real.startsWith(stateReal + sep3) && !real.startsWith(inbox + sep3)) {
50527
50869
  throw new Error(`refusing to send channel state: ${f}`);
50528
50870
  }
@@ -50541,7 +50883,7 @@ function assertSendable(f) {
50541
50883
  }
50542
50884
  function readAccessFile() {
50543
50885
  try {
50544
- const raw = readFileSync32(ACCESS_FILE, "utf8");
50886
+ const raw = readFileSync34(ACCESS_FILE, "utf8");
50545
50887
  const parsed = JSON.parse(raw);
50546
50888
  const allowFrom = validateStringArray("allowFrom", parsed.allowFrom ?? []);
50547
50889
  const groups = {};
@@ -50575,7 +50917,7 @@ function readAccessFile() {
50575
50917
  if (err.code === "ENOENT")
50576
50918
  return defaultAccess();
50577
50919
  try {
50578
- renameSync12(ACCESS_FILE, `${ACCESS_FILE}.corrupt-${Date.now()}`);
50920
+ renameSync13(ACCESS_FILE, `${ACCESS_FILE}.corrupt-${Date.now()}`);
50579
50921
  } catch {}
50580
50922
  process.stderr.write(`telegram gateway: access.json is corrupt, moved aside. Starting fresh.
50581
50923
  `);
@@ -50606,11 +50948,11 @@ function assertAllowedChat(chat_id) {
50606
50948
  function saveAccess(a) {
50607
50949
  if (STATIC)
50608
50950
  return;
50609
- mkdirSync21(STATE_DIR, { recursive: true, mode: 448 });
50951
+ mkdirSync23(STATE_DIR, { recursive: true, mode: 448 });
50610
50952
  const tmp = ACCESS_FILE + ".tmp";
50611
- writeFileSync21(tmp, JSON.stringify(a, null, 2) + `
50953
+ writeFileSync23(tmp, JSON.stringify(a, null, 2) + `
50612
50954
  `, { mode: 384 });
50613
- renameSync12(tmp, ACCESS_FILE);
50955
+ renameSync13(tmp, ACCESS_FILE);
50614
50956
  }
50615
50957
  function pruneExpired(a) {
50616
50958
  const now = Date.now();
@@ -50628,7 +50970,7 @@ var HISTORY_ENABLED = HISTORY_ACCESS.historyEnabled !== false;
50628
50970
  if (HISTORY_ENABLED) {
50629
50971
  try {
50630
50972
  initHistory(STATE_DIR, HISTORY_ACCESS.historyRetentionDays ?? 30);
50631
- process.stderr.write(`telegram gateway: history capture enabled at ${join32(STATE_DIR, "history.db")}
50973
+ process.stderr.write(`telegram gateway: history capture enabled at ${join33(STATE_DIR, "history.db")}
50632
50974
  `);
50633
50975
  } catch (err) {
50634
50976
  process.stderr.write(`telegram gateway: history init failed (${err.message}) \u2014 capture disabled
@@ -50645,10 +50987,10 @@ try {
50645
50987
  process.stderr.write(`telegram gateway: turn-registry boot-reaper stamped ${reaped} orphaned turn(s) as ended_via='restart'
50646
50988
  `);
50647
50989
  } else {
50648
- process.stderr.write(`telegram gateway: turn-registry initialized at ${join32(agentDir, "telegram", "registry.db")}
50990
+ process.stderr.write(`telegram gateway: turn-registry initialized at ${join33(agentDir, "telegram", "registry.db")}
50649
50991
  `);
50650
50992
  }
50651
- const pendingEnvPath = join32(agentDir, ".pending-turn.env");
50993
+ const pendingEnvPath = join33(agentDir, ".pending-turn.env");
50652
50994
  try {
50653
50995
  const pending2 = findMostRecentInterruptedTurn(turnsDb);
50654
50996
  if (pending2 != null) {
@@ -50662,13 +51004,13 @@ try {
50662
51004
  `SWITCHROOM_PENDING_STARTED_AT=${pending2.started_at}`
50663
51005
  ];
50664
51006
  const pendingEnvTmp = `${pendingEnvPath}.tmp-${process.pid}`;
50665
- writeFileSync21(pendingEnvTmp, lines.join(`
51007
+ writeFileSync23(pendingEnvTmp, lines.join(`
50666
51008
  `) + `
50667
51009
  `, { mode: 384 });
50668
- renameSync12(pendingEnvTmp, pendingEnvPath);
51010
+ renameSync13(pendingEnvTmp, pendingEnvPath);
50669
51011
  process.stderr.write(`telegram gateway: pending-turn env written to ${pendingEnvPath} turnKey=${pending2.turn_key} endedVia=${pending2.ended_via ?? "open"}
50670
51012
  `);
50671
- } else if (existsSync34(pendingEnvPath)) {
51013
+ } else if (existsSync36(pendingEnvPath)) {
50672
51014
  rmSync4(pendingEnvPath, { force: true });
50673
51015
  process.stderr.write(`telegram gateway: pending-turn env cleared (clean previous shutdown)
50674
51016
  `);
@@ -50722,7 +51064,7 @@ function checkApprovals() {
50722
51064
  return;
50723
51065
  }
50724
51066
  for (const senderId of files) {
50725
- const file = join32(APPROVED_DIR, senderId);
51067
+ const file = join33(APPROVED_DIR, senderId);
50726
51068
  bot.api.sendMessage(senderId, "Paired! Say hi to Claude.").then(() => rmSync4(file, { force: true }), (err) => {
50727
51069
  process.stderr.write(`telegram gateway: failed to send approval confirm: ${err}
50728
51070
  `);
@@ -51606,11 +51948,11 @@ var unpinProgressCardForChat = null;
51606
51948
  var getPinnedProgressCardMessageId = null;
51607
51949
  var completeProgressCardTurn = null;
51608
51950
  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");
51951
+ var SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join33(STATE_DIR, "gateway.sock");
51952
+ mkdirSync23(STATE_DIR, { recursive: true, mode: 448 });
51953
+ var GATEWAY_PID_PATH = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join33(STATE_DIR, "gateway.pid.json");
51954
+ var GATEWAY_SESSION_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_SESSION_MARKER ?? join33(STATE_DIR, "gateway-session.json");
51955
+ var GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join33(STATE_DIR, "clean-shutdown.json");
51614
51956
  var GATEWAY_STARTED_AT_MS = Date.now();
51615
51957
  var BOOT_CARD_ENABLED = process.env.SWITCHROOM_BOOT_CARD !== "false";
51616
51958
  var activeBootCard = null;
@@ -51639,7 +51981,7 @@ function ensureIssuesCard(chatId, threadId) {
51639
51981
  bot: botApi,
51640
51982
  log: (msg) => process.stderr.write(`telegram gateway: ${msg}
51641
51983
  `),
51642
- persistPath: join32(stateDir, "issues-card.json")
51984
+ persistPath: join33(stateDir, "issues-card.json")
51643
51985
  });
51644
51986
  activeIssuesWatcher = startIssuesWatcher({
51645
51987
  stateDir,
@@ -51808,13 +52150,13 @@ startTimer2({
51808
52150
  }
51809
52151
  });
51810
52152
  var inboundSpool = STATIC ? undefined : createInboundSpool({
51811
- path: join32(STATE_DIR, "inbound-spool.jsonl"),
52153
+ path: join33(STATE_DIR, "inbound-spool.jsonl"),
51812
52154
  fs: {
51813
52155
  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),
52156
+ readFileSync: (p) => readFileSync34(p, "utf8"),
52157
+ writeFileSync: (p, d) => writeFileSync23(p, d),
52158
+ renameSync: (a, b) => renameSync13(a, b),
52159
+ existsSync: (p) => existsSync36(p),
51818
52160
  statSizeSync: (p) => statSync13(p).size
51819
52161
  }
51820
52162
  });
@@ -51927,11 +52269,12 @@ var ipcServer = createIpcServer({
51927
52269
  return;
51928
52270
  }
51929
52271
  })();
52272
+ const resolvedAgentDirForCard = agentDir ?? (process.env.TELEGRAM_STATE_DIR ? __require("path").dirname(process.env.TELEGRAM_STATE_DIR) : "/tmp");
51930
52273
  startBootCard(chatId, threadId, botApiForCard, {
51931
52274
  agentName: agentDisplayName,
51932
52275
  agentSlug,
51933
52276
  version: formatBootVersion(),
51934
- agentDir: agentDir ?? (process.env.TELEGRAM_STATE_DIR ? __require("path").dirname(process.env.TELEGRAM_STATE_DIR) : "/tmp"),
52277
+ agentDir: resolvedAgentDirForCard,
51935
52278
  gatewayInfo: { pid: process.pid, startedAtMs: GATEWAY_STARTED_AT_MS },
51936
52279
  restartReason: reason,
51937
52280
  restartAgeMs: markerAgeMs,
@@ -51940,6 +52283,7 @@ var ipcServer = createIpcServer({
51940
52283
  probeQuotaViaBroker: (t) => probeQuotaForBootCard(agentSlug, t),
51941
52284
  tmuxSupervisor: process.env.SWITCHROOM_TMUX_SUPERVISOR === "1",
51942
52285
  dockerMode: process.env.SWITCHROOM_RUNTIME === "docker",
52286
+ configSnapshotPath: join33(resolvedAgentDirForCard, ".config-snapshot.json"),
51943
52287
  ...updateOutcomeLine ? { updateOutcomeLine } : {}
51944
52288
  }, ackMsgId).then((handle) => {
51945
52289
  activeBootCard = handle;
@@ -53304,11 +53648,11 @@ async function executeSendGif(rawArgs) {
53304
53648
  };
53305
53649
  }
53306
53650
  async function publishToTelegraph(text, shortName, authorName) {
53307
- const accountPath = join32(STATE_DIR, "telegraph-account.json");
53651
+ const accountPath = join33(STATE_DIR, "telegraph-account.json");
53308
53652
  let account = null;
53309
53653
  try {
53310
- if (existsSync34(accountPath)) {
53311
- const raw = readFileSync32(accountPath, "utf-8");
53654
+ if (existsSync36(accountPath)) {
53655
+ const raw = readFileSync34(accountPath, "utf-8");
53312
53656
  const parsed = JSON.parse(raw);
53313
53657
  if (parsed.shortName && parsed.accessToken) {
53314
53658
  account = parsed;
@@ -53327,8 +53671,8 @@ async function publishToTelegraph(text, shortName, authorName) {
53327
53671
  }
53328
53672
  account = created.value;
53329
53673
  try {
53330
- mkdirSync21(STATE_DIR, { recursive: true, mode: 448 });
53331
- writeFileSync21(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
53674
+ mkdirSync23(STATE_DIR, { recursive: true, mode: 448 });
53675
+ writeFileSync23(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
53332
53676
  } catch (err) {
53333
53677
  process.stderr.write(`telegram gateway: telegraph cache write failed: ${err.message}
53334
53678
  `);
@@ -53570,9 +53914,9 @@ async function executeDownloadAttachment(args) {
53570
53914
  fileUniqueId: file.file_unique_id,
53571
53915
  now: Date.now()
53572
53916
  });
53573
- mkdirSync21(INBOX_DIR, { recursive: true, mode: 448 });
53917
+ mkdirSync23(INBOX_DIR, { recursive: true, mode: 448 });
53574
53918
  assertInsideInbox(INBOX_DIR, dlPath);
53575
- writeFileSync21(dlPath, buf, { mode: 384 });
53919
+ writeFileSync23(dlPath, buf, { mode: 384 });
53576
53920
  return { content: [{ type: "text", text: dlPath }] };
53577
53921
  }
53578
53922
  async function executeEditMessage(args) {
@@ -53899,6 +54243,7 @@ function handleSessionEvent(ev) {
53899
54243
  activityInFlight: null,
53900
54244
  activityPendingRender: null,
53901
54245
  activityLastSentRender: null,
54246
+ mirrorLines: [],
53902
54247
  answerStream: null,
53903
54248
  isDm: isDmChatId(ev.chatId)
53904
54249
  };
@@ -53969,7 +54314,7 @@ function handleSessionEvent(ev) {
53969
54314
  }
53970
54315
  }
53971
54316
  if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
53972
- const rendered = DRAFT_MIRROR_ENABLED ? describeToolUse(name, ev.input) : registerAndRender(turn.toolActivity, name);
54317
+ const rendered = DRAFT_MIRROR_ENABLED ? appendActivityLine(turn.mirrorLines, name, ev.input) : registerAndRender(turn.toolActivity, name);
53973
54318
  if (rendered != null) {
53974
54319
  turn.activityPendingRender = rendered;
53975
54320
  if (turn.activityInFlight == null) {
@@ -55344,14 +55689,14 @@ function restartMarkerPath() {
55344
55689
  const agentDir = resolveAgentDirFromEnv();
55345
55690
  if (!agentDir)
55346
55691
  return null;
55347
- return join32(agentDir, "restart-pending.json");
55692
+ return join33(agentDir, "restart-pending.json");
55348
55693
  }
55349
55694
  function writeRestartMarker(marker) {
55350
55695
  const p = restartMarkerPath();
55351
55696
  if (!p)
55352
55697
  return;
55353
55698
  try {
55354
- writeFileSync21(p, JSON.stringify(marker));
55699
+ writeFileSync23(p, JSON.stringify(marker));
55355
55700
  lastPlannedRestartAt = Date.now();
55356
55701
  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
55702
  `);
@@ -55370,7 +55715,7 @@ function readRestartMarker() {
55370
55715
  if (!p)
55371
55716
  return null;
55372
55717
  try {
55373
- return JSON.parse(readFileSync32(p, "utf8"));
55718
+ return JSON.parse(readFileSync34(p, "utf8"));
55374
55719
  } catch {
55375
55720
  return null;
55376
55721
  }
@@ -55468,7 +55813,7 @@ var _dockerReachable;
55468
55813
  function isDockerReachable() {
55469
55814
  if (_dockerReachable !== undefined)
55470
55815
  return _dockerReachable;
55471
- if (!existsSync34("/var/run/docker.sock")) {
55816
+ if (!existsSync36("/var/run/docker.sock")) {
55472
55817
  _dockerReachable = false;
55473
55818
  return _dockerReachable;
55474
55819
  }
@@ -55485,12 +55830,12 @@ function _resetDockerReachableCache() {
55485
55830
  }
55486
55831
  function spawnSwitchroomDetached(args, onFailure) {
55487
55832
  const fullArgs = SWITCHROOM_CONFIG ? ["--config", SWITCHROOM_CONFIG, ...args] : args;
55488
- const logPath = join32(STATE_DIR, "detached-spawn.log");
55833
+ const logPath = join33(STATE_DIR, "detached-spawn.log");
55489
55834
  let outFd = null;
55490
55835
  try {
55491
- mkdirSync21(STATE_DIR, { recursive: true });
55836
+ mkdirSync23(STATE_DIR, { recursive: true });
55492
55837
  outFd = openSync8(logPath, "a");
55493
- writeFileSync21(logPath, `
55838
+ writeFileSync23(logPath, `
55494
55839
  [${new Date().toISOString()}] spawn ${SWITCHROOM_CLI} ${fullArgs.join(" ")}
55495
55840
  `, { flag: "a" });
55496
55841
  } catch {}
@@ -55516,7 +55861,7 @@ function spawnSwitchroomDetached(args, onFailure) {
55516
55861
  return;
55517
55862
  let tail = "";
55518
55863
  try {
55519
- const full = readFileSync32(logPath, "utf8");
55864
+ const full = readFileSync34(logPath, "utf8");
55520
55865
  tail = full.split(`
55521
55866
  `).slice(-30).join(`
55522
55867
  `).trim();
@@ -55858,10 +56203,10 @@ bot.use(async (ctx, next) => {
55858
56203
  });
55859
56204
  function readRecentDenialsForAgent(agentName3, windowMs, limit) {
55860
56205
  try {
55861
- const auditPath = join32(homedir12(), ".switchroom", "vault-audit.log");
55862
- if (!existsSync34(auditPath))
56206
+ const auditPath = join33(homedir12(), ".switchroom", "vault-audit.log");
56207
+ if (!existsSync36(auditPath))
55863
56208
  return [];
55864
- const raw = readFileSync32(auditPath, "utf8");
56209
+ const raw = readFileSync34(auditPath, "utf8");
55865
56210
  return recentDenialsFromAuditLog(raw, { agentName: agentName3, windowMs, limit });
55866
56211
  } catch {
55867
56212
  return [];
@@ -55912,7 +56257,7 @@ async function buildAgentMetadata(agentName3) {
55912
56257
  try {
55913
56258
  const agentDir = resolveAgentDirFromEnv();
55914
56259
  if (agentDir) {
55915
- const raw = readFileSync32(join32(agentDir, ".claude", ".claude.json"), "utf8");
56260
+ const raw = readFileSync34(join33(agentDir, ".claude", ".claude.json"), "utf8");
55916
56261
  claudeJson = JSON.parse(raw);
55917
56262
  }
55918
56263
  } catch {}
@@ -56126,9 +56471,9 @@ bot.command("restart", async (ctx) => {
56126
56471
  function flushAgentHandoff(agentDir) {
56127
56472
  let removed = 0;
56128
56473
  for (const fname of [".handoff.md", ".handoff-topic"]) {
56129
- const p = join32(agentDir, fname);
56474
+ const p = join33(agentDir, fname);
56130
56475
  try {
56131
- if (existsSync34(p)) {
56476
+ if (existsSync36(p)) {
56132
56477
  unlinkSync14(p);
56133
56478
  removed++;
56134
56479
  }
@@ -56184,7 +56529,7 @@ async function handleNewOrResetCommand(ctx, kind) {
56184
56529
  writeRestartMarker({ chat_id: chatId, thread_id: threadId ?? null, ack_message_id: ackId, ts: Date.now() });
56185
56530
  if (agentDir != null) {
56186
56531
  try {
56187
- writeFileSync21(join32(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
56532
+ writeFileSync23(join33(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
56188
56533
  `, "utf8");
56189
56534
  } catch (err) {
56190
56535
  process.stderr.write(`telegram gateway: failed to write force-fresh marker: ${err}
@@ -56543,16 +56888,16 @@ bot.command("interrupt", async (ctx) => {
56543
56888
  await runSwitchroomCommand(ctx, ["agent", "interrupt", name], `interrupt ${name}`);
56544
56889
  });
56545
56890
  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)
56891
+ readFileSync: (p, enc) => readFileSync34(p, enc),
56892
+ writeFileSync: (p, data, opts) => writeFileSync23(p, data, opts),
56893
+ existsSync: (p) => existsSync36(p),
56894
+ mkdirSync: (p, opts) => mkdirSync23(p, opts),
56895
+ joinPath: (...parts) => join33(...parts)
56551
56896
  };
56552
56897
  var FLEET_FALLBACK_DEDUP_MS = 30000;
56553
56898
  function isAuthBrokerSocketReachable() {
56554
56899
  try {
56555
- return existsSync34(resolveAuthBrokerSocketPath2());
56900
+ return existsSync36(resolveAuthBrokerSocketPath2());
56556
56901
  } catch {
56557
56902
  return false;
56558
56903
  }
@@ -56613,7 +56958,7 @@ async function runCreditWatch() {
56613
56958
  if (!agentDir)
56614
56959
  return;
56615
56960
  const agentName3 = getMyAgentName();
56616
- const claudeConfigDir = join32(agentDir, ".claude");
56961
+ const claudeConfigDir = join33(agentDir, ".claude");
56617
56962
  const stateDir = STATE_DIR;
56618
56963
  const reason = readClaudeJsonOverage(claudeConfigDir);
56619
56964
  const prev = loadCreditState(stateDir);
@@ -56640,6 +56985,99 @@ async function runCreditWatch() {
56640
56985
  `);
56641
56986
  }
56642
56987
  }
56988
+ async function runQuotaWatch() {
56989
+ const agentName3 = getMyAgentName();
56990
+ const stateDir = STATE_DIR;
56991
+ const brokerClient = await getAuthBrokerClient(agentName3);
56992
+ if (!brokerClient) {
56993
+ process.stderr.write(`telegram gateway: quota-watch: broker client unavailable \u2014 skipping
56994
+ `);
56995
+ return;
56996
+ }
56997
+ let listStateData;
56998
+ try {
56999
+ listStateData = await brokerClient.listState();
57000
+ } catch (err) {
57001
+ process.stderr.write(`telegram gateway: quota-watch: listState failed: ${err}
57002
+ `);
57003
+ return;
57004
+ }
57005
+ if (!listStateData.accounts || listStateData.accounts.length === 0) {
57006
+ return;
57007
+ }
57008
+ const snapshots = buildSnapshotsFromCachedState(listStateData);
57009
+ let watchState = loadQuotaWatchState(stateDir);
57010
+ const now = Date.now();
57011
+ const access = loadAccess();
57012
+ const pendingTransitions = [];
57013
+ const labelToSnapIndex = new Map(snapshots.map((s, i) => [s.label, i]));
57014
+ for (const snap of snapshots) {
57015
+ const prev = watchState[snap.label] ?? emptyAccountState();
57016
+ const decision = evaluateQuotaWatchAccount({ agentName: agentName3, snap, prev, now });
57017
+ if (decision.kind !== "skip") {
57018
+ pendingTransitions.push({
57019
+ accountLabel: snap.label,
57020
+ snapIndex: labelToSnapIndex.get(snap.label) ?? -1,
57021
+ decision
57022
+ });
57023
+ }
57024
+ }
57025
+ if (pendingTransitions.length === 0) {
57026
+ return;
57027
+ }
57028
+ const crossingLabels = pendingTransitions.map((t) => t.accountLabel);
57029
+ let freshProbeMap = new Map;
57030
+ try {
57031
+ const probeData = await brokerClient.probeQuota(crossingLabels, 8000);
57032
+ for (const entry of probeData.results) {
57033
+ freshProbeMap.set(entry.label, entry.result);
57034
+ }
57035
+ } catch (err) {
57036
+ process.stderr.write(`telegram gateway: quota-watch: probe for crossing accounts failed: ${err}
57037
+ `);
57038
+ }
57039
+ let mutatedState = watchState;
57040
+ const notifications = [];
57041
+ for (const { accountLabel, snapIndex, decision } of pendingTransitions) {
57042
+ const freshResult = freshProbeMap.get(accountLabel);
57043
+ let enrichedDecision = decision;
57044
+ if (decision.kind !== "notify")
57045
+ continue;
57046
+ if (freshResult && freshResult.ok && snapIndex >= 0) {
57047
+ const enrichedSnap = { ...snapshots[snapIndex], quota: freshResult.data };
57048
+ const prev = watchState[accountLabel] ?? emptyAccountState();
57049
+ const re = evaluateQuotaWatchAccount({ agentName: agentName3, snap: enrichedSnap, prev, now });
57050
+ if (re.kind === "notify" && re.transition === decision.transition) {
57051
+ enrichedDecision = re;
57052
+ } else if (re.kind === "skip") {
57053
+ continue;
57054
+ }
57055
+ }
57056
+ if (enrichedDecision.kind !== "notify")
57057
+ continue;
57058
+ notifications.push({ message: enrichedDecision.message, accountLabel });
57059
+ mutatedState = patchQuotaWatchState(mutatedState, accountLabel, enrichedDecision.newAccountState);
57060
+ }
57061
+ if (notifications.length === 0) {
57062
+ return;
57063
+ }
57064
+ for (const { message, accountLabel } of notifications) {
57065
+ for (const chat_id of access.allowFrom) {
57066
+ await swallowingApiCall(() => bot.api.sendMessage(chat_id, message, {
57067
+ parse_mode: "HTML",
57068
+ link_preview_options: { is_disabled: true }
57069
+ }), { chat_id, verb: "quota-watch.notify" });
57070
+ }
57071
+ process.stderr.write(`telegram gateway: quota-watch: notified transition for account=${accountLabel}
57072
+ `);
57073
+ }
57074
+ try {
57075
+ saveQuotaWatchState(stateDir, mutatedState);
57076
+ } catch (err) {
57077
+ process.stderr.write(`telegram gateway: quota-watch state persist failed: ${err}
57078
+ `);
57079
+ }
57080
+ }
56643
57081
  bot.command("auth", async (ctx) => {
56644
57082
  const authSenderId = String(ctx.from?.id ?? "");
56645
57083
  const authOperatorPrivate = ctx.chat?.type === "private" && loadAccess().allowFrom.includes(authSenderId);
@@ -56823,10 +57261,10 @@ async function handleVaultRecentDenialCallback(ctx, data) {
56823
57261
  return;
56824
57262
  }
56825
57263
  const { token, id } = result;
56826
- const tokenPath = join32(homedir12(), ".switchroom", "agents", agentName3, ".vault-token");
57264
+ const tokenPath = join33(homedir12(), ".switchroom", "agents", agentName3, ".vault-token");
56827
57265
  try {
56828
- mkdirSync21(join32(homedir12(), ".switchroom", "agents", agentName3), { recursive: true });
56829
- writeFileSync21(tokenPath, token, { mode: 384 });
57266
+ mkdirSync23(join33(homedir12(), ".switchroom", "agents", agentName3), { recursive: true });
57267
+ writeFileSync23(tokenPath, token, { mode: 384 });
56830
57268
  } catch (err) {
56831
57269
  await switchroomReply(ctx, `<b>Grant created (${escapeHtmlForTg(id)}) but token write failed:</b> ${escapeHtmlForTg(String(err))}
56832
57270
  <i>Recover with: <code>switchroom vault grant ${escapeHtmlForTg(agentName3)} --keys ${escapeHtmlForTg(keyName)} --duration 30d</code> on the host.</i>`, { html: true });
@@ -56902,10 +57340,10 @@ async function performVaultAccessApproval(ctx, pending2, stageId, senderId, atte
56902
57340
  return;
56903
57341
  }
56904
57342
  const { token, id } = result;
56905
- const tokenPath = join32(homedir12(), ".switchroom", "agents", pending2.agent, ".vault-token");
57343
+ const tokenPath = join33(homedir12(), ".switchroom", "agents", pending2.agent, ".vault-token");
56906
57344
  try {
56907
- mkdirSync21(join32(homedir12(), ".switchroom", "agents", pending2.agent), { recursive: true });
56908
- writeFileSync21(tokenPath, token, { mode: 384 });
57345
+ mkdirSync23(join33(homedir12(), ".switchroom", "agents", pending2.agent), { recursive: true });
57346
+ writeFileSync23(tokenPath, token, { mode: 384 });
56909
57347
  } catch (err) {
56910
57348
  await switchroomReply(ctx, `<b>Grant created (${escapeHtmlForTg(id)}) but token write failed:</b> ${escapeHtmlForTg(String(err))}
56911
57349
  <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 +57818,10 @@ async function executeGrantWizard(ctx, chatId, state4) {
57380
57818
  return;
57381
57819
  }
57382
57820
  const { token, id } = result;
57383
- const tokenPath = join32(homedir12(), ".switchroom", "agents", state4.agent, ".vault-token");
57821
+ const tokenPath = join33(homedir12(), ".switchroom", "agents", state4.agent, ".vault-token");
57384
57822
  try {
57385
- mkdirSync21(join32(homedir12(), ".switchroom", "agents", state4.agent), { recursive: true });
57386
- writeFileSync21(tokenPath, token, { mode: 384 });
57823
+ mkdirSync23(join33(homedir12(), ".switchroom", "agents", state4.agent), { recursive: true });
57824
+ writeFileSync23(tokenPath, token, { mode: 384 });
57387
57825
  } catch (err) {
57388
57826
  await switchroomReply(ctx, `<b>Grant created but token write failed:</b> ${escapeHtmlForTg(String(err))}`, { html: true });
57389
57827
  return;
@@ -57731,15 +58169,6 @@ async function handleOperatorEventCallback(ctx, data) {
57731
58169
  }
57732
58170
  return;
57733
58171
  }
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
58172
  default: {
57744
58173
  await ctx.answerCallbackQuery({ text: `Unknown action: ${action}` }).catch(() => {});
57745
58174
  return;
@@ -57823,8 +58252,8 @@ async function handleAuthDashboardCallback(ctx) {
57823
58252
  return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
57824
58253
  });
57825
58254
  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);
58255
+ const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState4, buildSnapshotKeyboard: buildSnapshotKeyboard3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
58256
+ const snapshots = buildSnapshotsFromState4(state4, quotas);
57828
58257
  const text = renderAuthSnapshotFormat23(snapshots, {
57829
58258
  tz,
57830
58259
  now: new Date,
@@ -58219,9 +58648,9 @@ bot.command("usage", async (ctx) => {
58219
58648
  const hit = probeResp.results.find((r) => r.label === a.label);
58220
58649
  return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
58221
58650
  });
58222
- const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
58651
+ const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState4 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
58223
58652
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
58224
- const snapshots = buildSnapshotsFromState3(state4, quotas);
58653
+ const snapshots = buildSnapshotsFromState4(state4, quotas);
58225
58654
  const text = renderAuthSnapshotFormat23(snapshots, {
58226
58655
  tz,
58227
58656
  now: new Date,
@@ -58240,7 +58669,7 @@ bot.command("usage", async (ctx) => {
58240
58669
  await switchroomReply(ctx, "<b>/usage:</b> cannot resolve agent dir.", { html: true });
58241
58670
  return;
58242
58671
  }
58243
- const result = await fetchQuota2({ claudeConfigDir: join32(agentDir, ".claude") });
58672
+ const result = await fetchQuota2({ claudeConfigDir: join33(agentDir, ".claude") });
58244
58673
  if (!result.ok) {
58245
58674
  await switchroomReply(ctx, `<b>/usage:</b> ${escapeHtmlForTg(result.reason)}`, { html: true });
58246
58675
  return;
@@ -58743,9 +59172,9 @@ bot.on("message:photo", async (ctx) => {
58743
59172
  fileUniqueId: best.file_unique_id,
58744
59173
  now: Date.now()
58745
59174
  });
58746
- mkdirSync21(INBOX_DIR, { recursive: true, mode: 448 });
59175
+ mkdirSync23(INBOX_DIR, { recursive: true, mode: 448 });
58747
59176
  assertInsideInbox(INBOX_DIR, dlPath);
58748
- writeFileSync21(dlPath, buf, { mode: 384 });
59177
+ writeFileSync23(dlPath, buf, { mode: 384 });
58749
59178
  return dlPath;
58750
59179
  } catch (err) {
58751
59180
  const msg = err instanceof Error ? err.message : "unknown error";
@@ -58785,8 +59214,8 @@ async function maybeTranscribeVoice(fileId, mimeType, language) {
58785
59214
  let apiKey = null;
58786
59215
  try {
58787
59216
  const path = __require("path").join(__require("os").homedir(), ".switchroom", "openai-api-key");
58788
- if (existsSync34(path)) {
58789
- apiKey = readFileSync32(path, "utf-8").trim();
59217
+ if (existsSync36(path)) {
59218
+ apiKey = readFileSync34(path, "utf-8").trim();
58790
59219
  }
58791
59220
  } catch (err) {
58792
59221
  process.stderr.write(`telegram gateway: voice-in: failed to read api key: ${err.message}
@@ -59642,11 +60071,12 @@ var didOneTimeSetup = false;
59642
60071
  return;
59643
60072
  }
59644
60073
  })();
60074
+ const resolvedAgentDirForBootCard = agentDir ?? join33(homedir12(), ".switchroom", "agents", agentSlug);
59645
60075
  const handle = await startBootCard(chatId, threadId, botApiForCard, {
59646
60076
  agentName: agentDisplayName,
59647
60077
  agentSlug,
59648
60078
  version: formatBootVersion(),
59649
- agentDir: agentDir ?? join32(homedir12(), ".switchroom", "agents", agentSlug),
60079
+ agentDir: resolvedAgentDirForBootCard,
59650
60080
  gatewayInfo: { pid: process.pid, startedAtMs: GATEWAY_STARTED_AT_MS },
59651
60081
  restartReason: reason,
59652
60082
  restartAgeMs: markerAgeMs,
@@ -59655,6 +60085,7 @@ var didOneTimeSetup = false;
59655
60085
  probeQuotaViaBroker: (t) => probeQuotaForBootCard(agentSlug, t),
59656
60086
  tmuxSupervisor: process.env.SWITCHROOM_TMUX_SUPERVISOR === "1",
59657
60087
  dockerMode: process.env.SWITCHROOM_RUNTIME === "docker",
60088
+ configSnapshotPath: join33(resolvedAgentDirForBootCard, ".config-snapshot.json"),
59658
60089
  ...updateOutcomeLine ? { updateOutcomeLine } : {}
59659
60090
  }, ackMsgId);
59660
60091
  activeBootCard = handle;
@@ -59694,6 +60125,21 @@ var didOneTimeSetup = false;
59694
60125
  });
59695
60126
  }, CREDIT_WATCH_POLL_MS).unref();
59696
60127
  }
60128
+ const QUOTA_WATCH_POLL_MS = Number(process.env.SWITCHROOM_QUOTA_WATCH_POLL_MS ?? 900000);
60129
+ if (QUOTA_WATCH_POLL_MS > 0) {
60130
+ setTimeout(() => {
60131
+ runQuotaWatch().catch((err) => {
60132
+ process.stderr.write(`telegram gateway: quota-watch initial run failed: ${err}
60133
+ `);
60134
+ });
60135
+ }, 30000);
60136
+ setInterval(() => {
60137
+ runQuotaWatch().catch((err) => {
60138
+ process.stderr.write(`telegram gateway: quota-watch scheduled run failed: ${err}
60139
+ `);
60140
+ });
60141
+ }, QUOTA_WATCH_POLL_MS).unref();
60142
+ }
59697
60143
  const RESTART_WATCHDOG_POLL_MS = Number(process.env.SWITCHROOM_RESTART_WATCHDOG_POLL_MS ?? 30000);
59698
60144
  const watchdogAgentName = process.env.SWITCHROOM_AGENT_NAME;
59699
60145
  const watchdogDockerMode = process.env.SWITCHROOM_RUNTIME === "docker";