switchroom 0.19.2 → 0.19.3

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.
Files changed (60) hide show
  1. package/dist/agent-scheduler/index.js +2 -0
  2. package/dist/auth-broker/index.js +13 -0
  3. package/dist/cli/autoaccept-poll.js +2 -0
  4. package/dist/cli/drive-write-pretool.mjs +2 -0
  5. package/dist/cli/ms-365-write-pretool.mjs +2 -0
  6. package/dist/cli/switchroom.js +404 -245
  7. package/dist/host-control/main.js +1 -1
  8. package/package.json +1 -1
  9. package/profiles/default/CLAUDE.md.hbs +8 -0
  10. package/skills/mental-model-curator/SKILL.md +68 -2
  11. package/telegram-plugin/auth-snapshot-format.ts +104 -12
  12. package/telegram-plugin/dist/bridge/bridge.js +8 -2
  13. package/telegram-plugin/dist/gateway/gateway.js +1194 -794
  14. package/telegram-plugin/dist/server.js +8 -2
  15. package/telegram-plugin/flushed-turn-supersede.ts +117 -13
  16. package/telegram-plugin/gateway/auth-add-flow.ts +215 -6
  17. package/telegram-plugin/gateway/auth-command.ts +138 -5
  18. package/telegram-plugin/gateway/gateway.ts +68 -101
  19. package/telegram-plugin/gateway/inbound-interceptors.ts +13 -3
  20. package/telegram-plugin/gateway/model-command.ts +203 -1
  21. package/telegram-plugin/gateway/outbound-send-path.ts +68 -15
  22. package/telegram-plugin/gateway/session-model-source.ts +90 -10
  23. package/telegram-plugin/gateway/stream-render.ts +22 -5
  24. package/telegram-plugin/quota-bar-format.ts +60 -12
  25. package/telegram-plugin/reply-owner-resolve.ts +76 -11
  26. package/telegram-plugin/session-tail.ts +27 -3
  27. package/telegram-plugin/tests/auth-add-flow.test.ts +367 -5
  28. package/telegram-plugin/tests/auth-snapshot-format.test.ts +41 -0
  29. package/telegram-plugin/tests/flushed-turn-supersede.test.ts +117 -0
  30. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +185 -29
  31. package/telegram-plugin/tests/model-command.test.ts +220 -0
  32. package/telegram-plugin/tests/reply-owner-resolve.test.ts +257 -13
  33. package/telegram-plugin/tests/send-reply-golden.test.ts +154 -0
  34. package/telegram-plugin/tests/session-model-source.test.ts +142 -0
  35. package/telegram-plugin/tests/session-tail-first-attach.test.ts +115 -2
  36. package/vendor/hindsight-memory/CHANGELOG.md +102 -0
  37. package/vendor/hindsight-memory/README.md +2 -1
  38. package/vendor/hindsight-memory/hooks/hooks.json +12 -0
  39. package/vendor/hindsight-memory/scripts/directive_verify.py +100 -3
  40. package/vendor/hindsight-memory/scripts/lib/config.py +150 -1
  41. package/vendor/hindsight-memory/scripts/lib/content.py +55 -5
  42. package/vendor/hindsight-memory/scripts/lib/directives.py +152 -15
  43. package/vendor/hindsight-memory/scripts/lib/parallel_recall.py +142 -0
  44. package/vendor/hindsight-memory/scripts/lib/state.py +31 -0
  45. package/vendor/hindsight-memory/scripts/recall.py +789 -143
  46. package/vendor/hindsight-memory/scripts/reconcile_tail.py +22 -1
  47. package/vendor/hindsight-memory/scripts/retain.py +71 -2
  48. package/vendor/hindsight-memory/scripts/subagent_retain.py +501 -0
  49. package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +169 -0
  50. package/vendor/hindsight-memory/scripts/tests/test_directives.py +177 -0
  51. package/vendor/hindsight-memory/scripts/tests/test_lesson_tagging.py +200 -0
  52. package/vendor/hindsight-memory/scripts/tests/test_recall_context_turns_default.py +200 -0
  53. package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +477 -0
  54. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +51 -0
  55. package/vendor/hindsight-memory/scripts/tests/test_recall_parallel_deadline.py +409 -0
  56. package/vendor/hindsight-memory/scripts/tests/test_recall_tag_weights.py +96 -0
  57. package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +413 -0
  58. package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +49 -0
  59. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +439 -0
  60. package/vendor/hindsight-memory/settings.json +3 -1
@@ -7196,6 +7196,67 @@ var init_format = __esm(() => {
7196
7196
  ];
7197
7197
  });
7198
7198
 
7199
+ // ../src/auth/quota.ts
7200
+ function isProbeThin(q) {
7201
+ return q.fiveHourUtilPresent === false && q.sevenDayUtilPresent === false;
7202
+ }
7203
+ function refillNormalizedUtils(q, now) {
7204
+ const nowMs = now.getTime();
7205
+ const fiveHourRefilled = q.fiveHourResetAt != null && q.fiveHourResetAt.getTime() <= nowMs;
7206
+ const sevenDayRefilled = q.sevenDayResetAt != null && q.sevenDayResetAt.getTime() <= nowMs;
7207
+ return {
7208
+ fiveHourUtilizationPct: fiveHourRefilled ? 0 : q.fiveHourUtilizationPct,
7209
+ sevenDayUtilizationPct: sevenDayRefilled ? 0 : q.sevenDayUtilizationPct,
7210
+ fiveHourRefilled,
7211
+ sevenDayRefilled
7212
+ };
7213
+ }
7214
+
7215
+ // demo-mask.ts
7216
+ function stableAssign(input, cache, pool, overflow) {
7217
+ const existing = cache.get(input);
7218
+ if (existing !== undefined)
7219
+ return existing;
7220
+ const index = cache.size;
7221
+ const value = index < pool.length ? pool[index] : overflow(index);
7222
+ cache.set(input, value);
7223
+ return value;
7224
+ }
7225
+ function maskEmail(label) {
7226
+ return stableAssign(label, emailCache, EMAIL_POOL, (i) => `demo${i + 1}@example.com`);
7227
+ }
7228
+ function maskUsername(tag) {
7229
+ return stableAssign(tag, usernameCache, USERNAME_POOL, (i) => `@demo_user${i + 1}`);
7230
+ }
7231
+ function maskVaultKey(name) {
7232
+ return stableAssign(name, vaultKeyCache, [], (i) => `demo/secret-${i + 1}`);
7233
+ }
7234
+ var EMAIL_POOL, emailCache, USERNAME_POOL, usernameCache, vaultKeyCache;
7235
+ var init_demo_mask = __esm(() => {
7236
+ EMAIL_POOL = [
7237
+ "ada@example.com",
7238
+ "grace@example.com",
7239
+ "linus@example.com",
7240
+ "hopper@example.com",
7241
+ "turing@example.com",
7242
+ "lovelace@example.com",
7243
+ "dijkstra@example.com",
7244
+ "knuth@example.com",
7245
+ "ritchie@example.com",
7246
+ "torvalds@example.com"
7247
+ ];
7248
+ emailCache = new Map;
7249
+ USERNAME_POOL = [
7250
+ "@demo_user",
7251
+ "@demo_user2",
7252
+ "@demo_user3",
7253
+ "@demo_user4",
7254
+ "@demo_user5"
7255
+ ];
7256
+ usernameCache = new Map;
7257
+ vaultKeyCache = new Map;
7258
+ });
7259
+
7199
7260
  // text-voice-scrub.ts
7200
7261
  function enabled() {
7201
7262
  const v = process.env.SWITCHROOM_DISABLE_VOICE_SCRUB;
@@ -7383,6 +7444,533 @@ var init_card_format = __esm(() => {
7383
7444
  init_text_voice_scrub();
7384
7445
  });
7385
7446
 
7447
+ // auth-snapshot-format.ts
7448
+ var exports_auth_snapshot_format = {};
7449
+ __export(exports_auth_snapshot_format, {
7450
+ zipProbeResults: () => zipProbeResults,
7451
+ reviveLastQuota: () => reviveLastQuota,
7452
+ renderFallbackAnnouncement: () => renderFallbackAnnouncement,
7453
+ renderAuthSnapshotFormat2: () => renderAuthSnapshotFormat2,
7454
+ recommendation: () => recommendation,
7455
+ formatStatusTime: () => formatStatusTime,
7456
+ formatRelative: () => formatRelative,
7457
+ formatAbsolute: () => formatAbsolute,
7458
+ fmtPct: () => fmtPct,
7459
+ deriveUsageFooterFreshness: () => deriveUsageFooterFreshness,
7460
+ classifyHealth: () => classifyHealth,
7461
+ buildSnapshotsFromState: () => buildSnapshotsFromState,
7462
+ buildSnapshotsFromCachedState: () => buildSnapshotsFromCachedState,
7463
+ buildSnapshotKeyboard: () => buildSnapshotKeyboard,
7464
+ blockedReason: () => blockedReason,
7465
+ bindingWindow: () => bindingWindow,
7466
+ THROTTLING_THRESHOLD_PCT: () => THROTTLING_THRESHOLD_PCT
7467
+ });
7468
+ function classifyHealth(snap, now = new Date) {
7469
+ if (!snap.isActive) {
7470
+ if (snap.entitlementBlocked === true)
7471
+ return "org-blocked";
7472
+ if (snap.inService === false)
7473
+ return "retired";
7474
+ }
7475
+ if (!snap.quota)
7476
+ return "unknown";
7477
+ const q = snap.quota;
7478
+ if (isProbeThin(q))
7479
+ return "unknown";
7480
+ const norm = refillNormalizedUtils(q, now);
7481
+ const max = Math.max(norm.fiveHourUtilizationPct, norm.sevenDayUtilizationPct);
7482
+ if (max >= 99.5)
7483
+ return "blocked";
7484
+ if (max >= THROTTLING_THRESHOLD_PCT)
7485
+ return "throttling";
7486
+ return "healthy";
7487
+ }
7488
+ function blockedReason(snap, now = new Date) {
7489
+ if (classifyHealth(snap, now) !== "blocked")
7490
+ return null;
7491
+ return "quota-exhausted";
7492
+ }
7493
+ function bindingWindow(q) {
7494
+ if (q.representativeClaim === "seven_day")
7495
+ return "7d";
7496
+ if (q.representativeClaim === "five_hour")
7497
+ return "5h";
7498
+ return q.sevenDayUtilizationPct >= q.fiveHourUtilizationPct ? "7d" : "5h";
7499
+ }
7500
+ function formatRelative(target, now = new Date) {
7501
+ if (!target)
7502
+ return "\u2014";
7503
+ const deltaMs = target.getTime() - now.getTime();
7504
+ if (deltaMs <= 0)
7505
+ return "now";
7506
+ const totalMin = Math.round(deltaMs / 60000);
7507
+ if (totalMin < 60)
7508
+ return `${totalMin}m`;
7509
+ const h = Math.floor(totalMin / 60);
7510
+ const m = totalMin % 60;
7511
+ if (h < 24)
7512
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
7513
+ const d = Math.floor(h / 24);
7514
+ const rh = h % 24;
7515
+ return rh > 0 ? `${d}d ${rh}h` : `${d}d`;
7516
+ }
7517
+ function formatAbsolute(target, tz = "UTC") {
7518
+ if (!target)
7519
+ return "\u2014";
7520
+ return target.toLocaleString("en-US", {
7521
+ timeZone: tz,
7522
+ weekday: "short",
7523
+ hour: "numeric",
7524
+ minute: "2-digit",
7525
+ hour12: true
7526
+ });
7527
+ }
7528
+ function formatStatusTime(target, now = new Date, tz = "UTC") {
7529
+ if (!target)
7530
+ return "\u2014";
7531
+ const dayFmt = new Intl.DateTimeFormat("en-CA", {
7532
+ timeZone: tz,
7533
+ year: "numeric",
7534
+ month: "2-digit",
7535
+ day: "2-digit"
7536
+ });
7537
+ const timeFmt = new Intl.DateTimeFormat("en-US", {
7538
+ timeZone: tz,
7539
+ hour: "numeric",
7540
+ minute: "2-digit",
7541
+ hour12: true
7542
+ });
7543
+ const timeStr = timeFmt.format(target);
7544
+ if (dayFmt.format(target) === dayFmt.format(now)) {
7545
+ return timeStr;
7546
+ }
7547
+ const withinWeek = target.getTime() - now.getTime() < 7 * 24 * 60 * 60 * 1000;
7548
+ const dateFmt = new Intl.DateTimeFormat("en-US", {
7549
+ timeZone: tz,
7550
+ weekday: "short",
7551
+ ...withinWeek ? {} : { day: "numeric", month: "short" }
7552
+ });
7553
+ return `${dateFmt.format(target)} ${timeStr}`;
7554
+ }
7555
+ function fmtPct(pct) {
7556
+ return `${Math.min(100, Math.round(pct))}%`;
7557
+ }
7558
+ function displayLabel(label, opts) {
7559
+ return opts.demo ? maskEmail(label) : label;
7560
+ }
7561
+ function renderAccountRow(snap, opts) {
7562
+ const now = opts.now ?? new Date;
7563
+ const tz = opts.tz ?? "UTC";
7564
+ const lines = [];
7565
+ const marker = snap.isActive ? "\u25cf " : "";
7566
+ const label = displayLabel(snap.label, opts);
7567
+ const health = classifyHealth(snap, now);
7568
+ if (health === "org-blocked" || health === "retired") {
7569
+ const note = health === "org-blocked" ? "DISABLED (org) \u2014 no fleet routing" : "retired \u2014 removed from fleet rotation";
7570
+ lines.push(`${marker}\`${codeSpanSafe(label)}\` _${note}_`);
7571
+ return lines;
7572
+ }
7573
+ if (!snap.quota) {
7574
+ lines.push(`${marker}\`${codeSpanSafe(label)}\` _quota probe failed_`);
7575
+ if (snap.quotaError) {
7576
+ lines.push(` _${escapeMarkdown(snap.quotaError)}_`);
7577
+ }
7578
+ return lines;
7579
+ }
7580
+ const q = snap.quota;
7581
+ if (isProbeThin(q)) {
7582
+ lines.push(`${marker}\`${codeSpanSafe(label)}\` _quota unknown (thin probe)_`);
7583
+ return lines;
7584
+ }
7585
+ const norm = refillNormalizedUtils(q, now);
7586
+ const fiveStr = fmtPct(norm.fiveHourUtilizationPct);
7587
+ const sevenStr = fmtPct(norm.sevenDayUtilizationPct);
7588
+ lines.push(`${marker}\`${codeSpanSafe(label)}\` ${fiveStr} / ${sevenStr}`);
7589
+ if (health === "blocked") {
7590
+ const win = bindingWindow(q);
7591
+ const reset = win === "5h" ? q.fiveHourResetAt : q.sevenDayResetAt;
7592
+ const winLabel = win === "5h" ? "5-hour" : "7-day";
7593
+ lines.push(reset ? ` _quota exhausted \u2014 back ${formatAbsolute(reset, tz)} (\`in ${formatRelative(reset, now)}\`, ${winLabel} cap)_` : ` _quota exhausted \u2014 ${winLabel} cap, reset time unknown_`);
7594
+ return lines;
7595
+ }
7596
+ const fiveResetIn = q.fiveHourResetAt ? q.fiveHourResetAt.getTime() - now.getTime() : Infinity;
7597
+ const sevenResetIn = q.sevenDayResetAt ? q.sevenDayResetAt.getTime() - now.getTime() : Infinity;
7598
+ const fiveFirst = fiveResetIn <= sevenResetIn;
7599
+ const fiveSeg = q.fiveHourResetAt ? `5h refills ${formatAbsolute(q.fiveHourResetAt, tz)} (\`in ${formatRelative(q.fiveHourResetAt, now)}\`)` : "5h refills \u2014";
7600
+ const sevenSeg = q.sevenDayResetAt ? `7d resets ${formatAbsolute(q.sevenDayResetAt, tz)} (\`in ${formatRelative(q.sevenDayResetAt, now)}\`)` : "7d resets \u2014";
7601
+ lines.push(` _${fiveFirst ? fiveSeg : sevenSeg}_`);
7602
+ lines.push(` _${fiveFirst ? sevenSeg : fiveSeg}_`);
7603
+ if (q.overageDisabledReason != null && OVERAGE_EXHAUSTED_REASONS.has(q.overageDisabledReason)) {
7604
+ lines.push(` _overage off (${escapeMarkdown(q.overageDisabledReason)}) \u2014 serving from quota_`);
7605
+ }
7606
+ return lines;
7607
+ }
7608
+ function formatAgeStamp(atMs, now = new Date) {
7609
+ const ageSec = Math.max(0, Math.round((now.getTime() - atMs) / 1000));
7610
+ return ageSec < 60 ? `${ageSec}s ago` : `${Math.round(ageSec / 60)}m ago`;
7611
+ }
7612
+ function tableCell(s) {
7613
+ return s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
7614
+ }
7615
+ function pctCells(snap, now) {
7616
+ if (!snap.quota)
7617
+ return { five: "\u2014", seven: "\u2014" };
7618
+ if (isProbeThin(snap.quota))
7619
+ return { five: "?", seven: "?" };
7620
+ const norm = refillNormalizedUtils(snap.quota, now);
7621
+ return { five: fmtPct(norm.fiveHourUtilizationPct), seven: fmtPct(norm.sevenDayUtilizationPct) };
7622
+ }
7623
+ function windowResetCell(snap, now, tz, win) {
7624
+ if (!snap.quota) {
7625
+ return snap.quotaError ? `probe failed (${snap.quotaError})` : "\u2014";
7626
+ }
7627
+ if (isProbeThin(snap.quota))
7628
+ return "quota unknown";
7629
+ const reset = win === "5h" ? snap.quota.fiveHourResetAt : snap.quota.sevenDayResetAt;
7630
+ if (!reset)
7631
+ return "\u2014";
7632
+ return `${formatStatusTime(reset, now, tz)} (in ${formatRelative(reset, now)})`;
7633
+ }
7634
+ function renderAuthSnapshotFormat2(snapshots, opts = {}) {
7635
+ const now = opts.now ?? new Date;
7636
+ const tz = opts.tz ?? "UTC";
7637
+ const lines = [];
7638
+ lines.push("\uD83D\uDD0B **Auth \u2014 fleet status**");
7639
+ const ordered = [...snapshots].sort((a, b) => {
7640
+ if (a.isActive !== b.isActive)
7641
+ return a.isActive ? -1 : 1;
7642
+ const r = TABLE_HEALTH_RANK[classifyHealth(a, now)] - TABLE_HEALTH_RANK[classifyHealth(b, now)];
7643
+ if (r !== 0)
7644
+ return r;
7645
+ return a.label.localeCompare(b.label);
7646
+ });
7647
+ if (ordered.length > 0) {
7648
+ lines.push("");
7649
+ lines.push("| State | Account | 5h | 5h resets | 7d | 7d resets |");
7650
+ lines.push("| --- | --- | --- | --- | --- | --- |");
7651
+ for (const s of ordered) {
7652
+ const emoji = HEALTH_EMOJI[classifyHealth(s, now)];
7653
+ const label = displayLabel(s.label, opts);
7654
+ const accountCell = `\`${codeSpanSafe(s.isActive ? `${label} (active)` : label)}\``;
7655
+ const { five, seven } = pctCells(s, now);
7656
+ const fiveReset = windowResetCell(s, now, tz, "5h");
7657
+ const sevenReset = windowResetCell(s, now, tz, "7d");
7658
+ lines.push(`| ${emoji} | ${tableCell(accountCell)} | ${five} | ${tableCell(fiveReset)} | ${seven} | ${tableCell(sevenReset)} |`);
7659
+ }
7660
+ }
7661
+ lines.push("");
7662
+ lines.push(`_${recommendation(snapshots, now, opts.demo ?? false)}_`);
7663
+ if (opts.staleCachedAtMs != null) {
7664
+ lines.push(`_\u26a0 cached ${formatAgeStamp(opts.staleCachedAtMs, now)}_`);
7665
+ } else if (opts.liveProbedAtMs != null) {
7666
+ lines.push(`_Live \u00b7 refreshed ${formatAgeStamp(opts.liveProbedAtMs, now)}_`);
7667
+ } else if (opts.probeFailed) {
7668
+ lines.push("_\u26a0 probe failed \u2014 no live data_");
7669
+ } else {
7670
+ lines.push("_Live_");
7671
+ }
7672
+ return lines.join(`
7673
+ `);
7674
+ }
7675
+ function recommendation(snapshots, now = new Date, demo = false) {
7676
+ const active = snapshots.find((s) => s.isActive);
7677
+ if (!active)
7678
+ return "No active account set.";
7679
+ const activeHealth = classifyHealth(active, now);
7680
+ const inServiceFleet = snapshots.filter((s) => {
7681
+ const h = classifyHealth(s, now);
7682
+ return h !== "retired" && h !== "org-blocked";
7683
+ });
7684
+ const others = inServiceFleet.filter((s) => !s.isActive);
7685
+ const healthyAlt = others.find((s) => classifyHealth(s, now) === "healthy");
7686
+ const lbl = (s) => demo ? maskEmail(s.label) : s.label;
7687
+ const activeLabel = lbl(active);
7688
+ if (activeHealth === "healthy") {
7689
+ return `Recommendation: stay on ${activeLabel}.`;
7690
+ }
7691
+ if (activeHealth === "throttling") {
7692
+ if (healthyAlt) {
7693
+ return `Recommendation: active ${activeLabel} is throttling. Switch to ${lbl(healthyAlt)} for headroom.`;
7694
+ }
7695
+ return `Recommendation: active ${activeLabel} is throttling; no healthy alternative \u2014 wait for refill.`;
7696
+ }
7697
+ if (activeHealth === "blocked") {
7698
+ if (healthyAlt) {
7699
+ return `Recommendation: active ${activeLabel} is BLOCKED \u2014 switch to ${lbl(healthyAlt)} now.`;
7700
+ }
7701
+ return summarizeNoHealthyAlt(inServiceFleet, now, demo);
7702
+ }
7703
+ return `Active ${activeLabel}: quota probe failed; broker last_seen unknown.`;
7704
+ }
7705
+ function summarizeNoHealthyAlt(snapshots, now, demo = false) {
7706
+ const mask = (label) => demo ? maskEmail(label) : label;
7707
+ let throttlingLabel = null;
7708
+ let allTrulyBlocked = true;
7709
+ for (const s of snapshots) {
7710
+ const h = classifyHealth(s, now);
7711
+ if (h === "throttling") {
7712
+ if (!throttlingLabel)
7713
+ throttlingLabel = s.label;
7714
+ allTrulyBlocked = false;
7715
+ } else if (h === "healthy" || h === "unknown") {
7716
+ allTrulyBlocked = false;
7717
+ } else if (h === "blocked" && blockedReason(s, now) === "quota-exhausted") {
7718
+ if (s.quota) {
7719
+ const win = bindingWindow(s.quota);
7720
+ const at = win === "5h" ? s.quota.fiveHourResetAt : s.quota.sevenDayResetAt;
7721
+ if (at && at.getTime() > now.getTime())
7722
+ allTrulyBlocked = false;
7723
+ }
7724
+ }
7725
+ }
7726
+ const earliestRecovery = pickEarliestRecovery(snapshots, now);
7727
+ if (throttlingLabel) {
7728
+ const eta = earliestRecovery ? ` Soonest full refill: ${mask(earliestRecovery.label)} in ${formatRelative(earliestRecovery.at, now)}.` : "";
7729
+ return `No fully-healthy account; ${mask(throttlingLabel)} is throttling but still usable.${eta}`;
7730
+ }
7731
+ if (!allTrulyBlocked) {
7732
+ if (earliestRecovery) {
7733
+ return `All accounts at capacity; soonest refill: ${mask(earliestRecovery.label)} in ${formatRelative(earliestRecovery.at, now)}.`;
7734
+ }
7735
+ return `All accounts at capacity \u2014 waiting on a window refill.`;
7736
+ }
7737
+ if (earliestRecovery) {
7738
+ return `All accounts blocked. Earliest recovery: ${mask(earliestRecovery.label)} in ${formatRelative(earliestRecovery.at, now)}.`;
7739
+ }
7740
+ return `All accounts blocked. Run /auth add to attach another subscription.`;
7741
+ }
7742
+ function pickEarliestRecovery(snapshots, now) {
7743
+ let best = null;
7744
+ for (const s of snapshots) {
7745
+ if (!s.quota)
7746
+ continue;
7747
+ if (isProbeThin(s.quota))
7748
+ continue;
7749
+ const win = bindingWindow(s.quota);
7750
+ const at = win === "5h" ? s.quota.fiveHourResetAt : s.quota.sevenDayResetAt;
7751
+ if (!at || at.getTime() <= now.getTime())
7752
+ continue;
7753
+ if (!best || at.getTime() < best.at.getTime()) {
7754
+ best = { label: s.label, at };
7755
+ }
7756
+ }
7757
+ return best;
7758
+ }
7759
+ function renderFallbackAnnouncement(input) {
7760
+ const now = input.now ?? new Date;
7761
+ const tz = input.tz ?? "UTC";
7762
+ const lines = [];
7763
+ const limitWord = input.oldQuota ? limitWordFor(input.oldQuota) : "quota";
7764
+ const headerLimit = input.cause === "rate-limit" ? "rate limit" : limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
7765
+ if (!input.newLabel) {
7766
+ lines.push(`\uD83D\uDD34 **All accounts blocked \u00b7 ${headerLimit} on ${escapeMarkdown(input.oldLabel)}**`);
7767
+ lines.push("");
7768
+ lines.push(`Triggered by: agent **${escapeMarkdown(input.triggerAgent)}**`);
7769
+ const fleet = input.fleetSnapshots ?? [];
7770
+ if (fleet.length > 0) {
7771
+ lines.push("");
7772
+ const rowOpts = { now, tz };
7773
+ const healthOrder = [
7774
+ "org-blocked",
7775
+ "blocked",
7776
+ "throttling",
7777
+ "healthy",
7778
+ "unknown",
7779
+ "retired"
7780
+ ];
7781
+ const rank = (s) => healthOrder.indexOf(classifyHealth(s, now));
7782
+ const ordered = [...fleet].sort((a, b) => rank(a) - rank(b) || Number(b.isActive) - Number(a.isActive));
7783
+ for (const snap of ordered) {
7784
+ for (const ln of renderAccountRow(snap, rowOpts))
7785
+ lines.push(ln);
7786
+ }
7787
+ const earliest = pickEarliestRecovery(fleet, now);
7788
+ if (earliest) {
7789
+ lines.push("");
7790
+ lines.push(`Earliest recovery: \`${codeSpanSafe(earliest.label)}\` ` + `${formatAbsolute(earliest.at, tz)} (in ${formatRelative(earliest.at, now)})`);
7791
+ }
7792
+ } else {
7793
+ const recovery = (input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ?? input.parsedResetAt ?? null;
7794
+ if (recovery) {
7795
+ lines.push(`${escapeMarkdown(input.oldLabel)} recovers ${formatAbsolute(recovery, tz)} ` + `(in ${formatRelative(recovery, now)})`);
7796
+ }
7797
+ }
7798
+ lines.push("");
7799
+ lines.push(`Run \`/auth add <label>\` to attach another subscription, ` + `or \`/auth refresh\` to re-probe.`);
7800
+ return lines.join(`
7801
+ `);
7802
+ }
7803
+ lines.push(`\u2713 **Switched fleet \u00b7 ${headerLimit} on ${escapeMarkdown(input.oldLabel)}**`);
7804
+ lines.push("");
7805
+ lines.push(`\`${codeSpanSafe(input.oldLabel)}\` \u2192 \`${codeSpanSafe(input.newLabel)}\``);
7806
+ lines.push(`Triggered by: agent **${escapeMarkdown(input.triggerAgent)}**`);
7807
+ lines.push("");
7808
+ {
7809
+ const recovery = (input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ?? input.parsedResetAt ?? null;
7810
+ if (recovery) {
7811
+ lines.push(`\`${codeSpanSafe(input.oldLabel)}\` recovers ` + `${formatAbsolute(recovery, tz)} (in ${formatRelative(recovery, now)})`);
7812
+ }
7813
+ }
7814
+ if (input.newQuota) {
7815
+ const fiveStr = fmtPct(input.newQuota.fiveHourUtilizationPct);
7816
+ const sevenStr = fmtPct(input.newQuota.sevenDayUtilizationPct);
7817
+ const hasHeadroom = input.newQuota.fiveHourUtilizationPct < THROTTLING_THRESHOLD_PCT && input.newQuota.sevenDayUtilizationPct < THROTTLING_THRESHOLD_PCT;
7818
+ const headroomStr = hasHeadroom ? "_(plenty of headroom)_" : "_(near limit \u2014 watch this)_";
7819
+ lines.push(`\`${codeSpanSafe(input.newLabel)}\` now: ${fiveStr} of 5h \u00b7 ${sevenStr} of 7d ${headroomStr}`);
7820
+ } else {
7821
+ lines.push(`_(quota probe for new account is pending \u2014 will reflect on next /auth)_`);
7822
+ }
7823
+ return lines.join(`
7824
+ `);
7825
+ }
7826
+ function limitWordFor(q) {
7827
+ if (q.representativeClaim === "seven_day" && q.sevenDayUtilizationPct >= 99)
7828
+ return "7-day";
7829
+ if (q.representativeClaim === "five_hour" && q.fiveHourUtilizationPct >= 99)
7830
+ return "5-hour";
7831
+ if (q.sevenDayUtilizationPct >= 99)
7832
+ return "7-day";
7833
+ if (q.fiveHourUtilizationPct >= 99)
7834
+ return "5-hour";
7835
+ return q.sevenDayUtilizationPct >= q.fiveHourUtilizationPct ? "7-day" : "5-hour";
7836
+ }
7837
+ function recoveryAtFor(q) {
7838
+ const word = limitWordFor(q);
7839
+ if (word === "7-day")
7840
+ return q.sevenDayResetAt;
7841
+ if (word === "5-hour")
7842
+ return q.fiveHourResetAt;
7843
+ if (!q.fiveHourResetAt)
7844
+ return q.sevenDayResetAt;
7845
+ if (!q.sevenDayResetAt)
7846
+ return q.fiveHourResetAt;
7847
+ return q.fiveHourResetAt.getTime() < q.sevenDayResetAt.getTime() ? q.fiveHourResetAt : q.sevenDayResetAt;
7848
+ }
7849
+ function buildSnapshotKeyboard(snapshots, opts = {}) {
7850
+ const max = opts.maxSwitchButtons ?? 3;
7851
+ const now = opts.now ?? new Date;
7852
+ const rows = [];
7853
+ const switchTargets = snapshots.filter((s) => !s.isActive).sort((a, b) => switchPriority(a, now) - switchPriority(b, now)).filter((s) => {
7854
+ const h = classifyHealth(s, now);
7855
+ return h !== "blocked" && h !== "unknown" && h !== "retired" && h !== "org-blocked";
7856
+ }).slice(0, max);
7857
+ for (const t of switchTargets) {
7858
+ rows.push([
7859
+ {
7860
+ text: `Switch fleet \u2192 ${opts.demo ? maskEmail(t.label) : t.label}`,
7861
+ callbackData: `auth:use:${t.label}`
7862
+ }
7863
+ ]);
7864
+ }
7865
+ rows.push([
7866
+ { text: "\u21bb Refresh", callbackData: opts.demo ? "auth:refresh:demo" : "auth:refresh" },
7867
+ { text: "/usage", insertText: "/usage" },
7868
+ { text: "+ Add", insertText: "/auth add " }
7869
+ ]);
7870
+ return rows;
7871
+ }
7872
+ function switchPriority(s, now = new Date) {
7873
+ const h = classifyHealth(s, now);
7874
+ if (h === "healthy")
7875
+ return 0;
7876
+ if (h === "throttling")
7877
+ return 1;
7878
+ if (h === "unknown")
7879
+ return 2;
7880
+ if (h === "blocked")
7881
+ return 3;
7882
+ return 4;
7883
+ }
7884
+ function zipProbeResults(labels, results) {
7885
+ let staleCachedAtMs;
7886
+ const quotas = labels.map((label) => {
7887
+ const hit = results.find((r) => r.label === label);
7888
+ if (!hit)
7889
+ return { ok: false, reason: "broker returned no result for account" };
7890
+ if (hit.served === "cache" && hit.capturedAt != null) {
7891
+ staleCachedAtMs = staleCachedAtMs == null ? hit.capturedAt : Math.min(staleCachedAtMs, hit.capturedAt);
7892
+ }
7893
+ return hit.result;
7894
+ });
7895
+ return staleCachedAtMs != null ? { quotas, staleCachedAtMs } : { quotas };
7896
+ }
7897
+ function deriveUsageFooterFreshness(results, staleCachedAtMs, liveProbedAtMs) {
7898
+ if (staleCachedAtMs != null)
7899
+ return { staleCachedAtMs };
7900
+ if (results.some((r) => r.result.ok))
7901
+ return { liveProbedAtMs };
7902
+ return { probeFailed: true };
7903
+ }
7904
+ function buildSnapshotsFromState(state, quotas) {
7905
+ const out = [];
7906
+ for (let i = 0;i < state.accounts.length; i++) {
7907
+ const acc = state.accounts[i];
7908
+ const q = quotas[i];
7909
+ out.push({
7910
+ label: acc.label,
7911
+ isActive: acc.label === state.active,
7912
+ quota: q && q.ok ? q.data : null,
7913
+ quotaError: q && !q.ok ? q.reason : undefined,
7914
+ expiresAtMs: acc.expiresAt,
7915
+ inService: acc.in_service,
7916
+ entitlementBlocked: acc.entitlement_blocked
7917
+ });
7918
+ }
7919
+ return out;
7920
+ }
7921
+ function reviveLastQuota(snap) {
7922
+ if (!snap)
7923
+ return null;
7924
+ return {
7925
+ fiveHourUtilizationPct: snap.fiveHourUtilizationPct,
7926
+ sevenDayUtilizationPct: snap.sevenDayUtilizationPct,
7927
+ fiveHourResetAt: snap.fiveHourResetAt ? new Date(snap.fiveHourResetAt) : null,
7928
+ sevenDayResetAt: snap.sevenDayResetAt ? new Date(snap.sevenDayResetAt) : null,
7929
+ representativeClaim: snap.representativeClaim,
7930
+ overageStatus: snap.overageStatus,
7931
+ overageDisabledReason: snap.overageDisabledReason,
7932
+ fiveHourUtilPresent: snap.fiveHourUtilPresent,
7933
+ sevenDayUtilPresent: snap.sevenDayUtilPresent
7934
+ };
7935
+ }
7936
+ function buildSnapshotsFromCachedState(state) {
7937
+ return state.accounts.map((acc) => {
7938
+ const lq = acc.last_quota ?? null;
7939
+ return {
7940
+ label: acc.label,
7941
+ isActive: acc.label === state.active,
7942
+ quota: reviveLastQuota(lq),
7943
+ quotaError: lq ? undefined : "no cached quota (no probe since broker start)",
7944
+ expiresAtMs: acc.expiresAt,
7945
+ capturedAtMs: lq?.capturedAt,
7946
+ inService: acc.in_service,
7947
+ entitlementBlocked: acc.entitlement_blocked
7948
+ };
7949
+ });
7950
+ }
7951
+ var THROTTLING_THRESHOLD_PCT = 80, OVERAGE_EXHAUSTED_REASONS, HEALTH_EMOJI, TABLE_HEALTH_RANK;
7952
+ var init_auth_snapshot_format = __esm(() => {
7953
+ init_demo_mask();
7954
+ init_card_format();
7955
+ OVERAGE_EXHAUSTED_REASONS = new Set(["out_of_credits"]);
7956
+ HEALTH_EMOJI = {
7957
+ healthy: "\uD83D\uDFE2",
7958
+ throttling: "\uD83D\uDFE1",
7959
+ blocked: "\uD83D\uDD34",
7960
+ unknown: "\u26aa",
7961
+ "org-blocked": "\u26d4",
7962
+ retired: "\u26ab"
7963
+ };
7964
+ TABLE_HEALTH_RANK = {
7965
+ "org-blocked": 0,
7966
+ blocked: 1,
7967
+ throttling: 2,
7968
+ unknown: 3,
7969
+ healthy: 4,
7970
+ retired: 5
7971
+ };
7972
+ });
7973
+
7386
7974
  // secret-detect/patterns.ts
7387
7975
  var ANCHORED_PATTERNS, STRUCTURED_PATTERNS, PROVIDER_PATTERNS, ALL_PATTERNS;
7388
7976
  var init_patterns = __esm(() => {
@@ -13034,51 +13622,6 @@ var init_tmux = __esm(() => {
13034
13622
  MAX_BYTES = 10 * 1024 * 1024;
13035
13623
  });
13036
13624
 
13037
- // demo-mask.ts
13038
- function stableAssign(input, cache, pool, overflow) {
13039
- const existing = cache.get(input);
13040
- if (existing !== undefined)
13041
- return existing;
13042
- const index = cache.size;
13043
- const value = index < pool.length ? pool[index] : overflow(index);
13044
- cache.set(input, value);
13045
- return value;
13046
- }
13047
- function maskEmail(label) {
13048
- return stableAssign(label, emailCache, EMAIL_POOL, (i) => `demo${i + 1}@example.com`);
13049
- }
13050
- function maskUsername(tag) {
13051
- return stableAssign(tag, usernameCache, USERNAME_POOL, (i) => `@demo_user${i + 1}`);
13052
- }
13053
- function maskVaultKey(name) {
13054
- return stableAssign(name, vaultKeyCache, [], (i) => `demo/secret-${i + 1}`);
13055
- }
13056
- var EMAIL_POOL, emailCache, USERNAME_POOL, usernameCache, vaultKeyCache;
13057
- var init_demo_mask = __esm(() => {
13058
- EMAIL_POOL = [
13059
- "ada@example.com",
13060
- "grace@example.com",
13061
- "linus@example.com",
13062
- "hopper@example.com",
13063
- "turing@example.com",
13064
- "lovelace@example.com",
13065
- "dijkstra@example.com",
13066
- "knuth@example.com",
13067
- "ritchie@example.com",
13068
- "torvalds@example.com"
13069
- ];
13070
- emailCache = new Map;
13071
- USERNAME_POOL = [
13072
- "@demo_user",
13073
- "@demo_user2",
13074
- "@demo_user3",
13075
- "@demo_user4",
13076
- "@demo_user5"
13077
- ];
13078
- usernameCache = new Map;
13079
- vaultKeyCache = new Map;
13080
- });
13081
-
13082
13625
  // ../node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js
13083
13626
  var require_identity = __commonJS((exports) => {
13084
13627
  var ALIAS = Symbol.for("yaml.alias");
@@ -21894,647 +22437,139 @@ function materializeFilesEntry(key, files) {
21894
22437
  }
21895
22438
  mkdirSync9(dir, { recursive: true, mode: 448 });
21896
22439
  chmodSync3(dir, 448);
21897
- const st = statSync6(dir);
21898
- if (typeof process.getuid === "function" && st.uid !== process.getuid()) {
21899
- throw new Error(`Refusing to materialize vault entry: ${dir} not owned by caller`);
21900
- }
21901
- for (const [filename, { encoding, value }] of Object.entries(files)) {
21902
- if (filename.includes("/") || filename.includes("\\") || filename === ".." || filename === "." || filename.includes("\x00")) {
21903
- throw new Error(`Refusing to materialize vault file with unsafe name: ${filename}`);
21904
- }
21905
- const filePath = join5(dir, filename);
21906
- const content = encoding === "base64" ? Buffer.from(value, "base64") : value;
21907
- writeFileExclusive(filePath, content);
21908
- }
21909
- materializedDirs.add(dir);
21910
- registerCleanupHook();
21911
- return dir;
21912
- }
21913
- function resolveSingleReference(ref, secrets) {
21914
- const entry = secrets[ref.key];
21915
- if (entry === undefined) {
21916
- throw new Error(`Vault secret not found: ${ref.key}`);
21917
- }
21918
- if (ref.filename !== undefined) {
21919
- if (entry.kind !== "files") {
21920
- throw new Error(`Vault reference "vault:${ref.key}#${ref.filename}" expected kind="files", got kind="${entry.kind}".`);
21921
- }
21922
- const file = entry.files[ref.filename];
21923
- if (!file) {
21924
- throw new Error(`Vault secret "${ref.key}" has no file named "${ref.filename}". Available: ${Object.keys(entry.files).join(", ")}`);
21925
- }
21926
- return file.encoding === "base64" ? Buffer.from(file.value, "base64").toString("utf8") : file.value;
21927
- }
21928
- if (entry.kind === "string" || entry.kind === "binary") {
21929
- return entry.value;
21930
- }
21931
- return materializeFilesEntry(ref.key, entry.files);
21932
- }
21933
- function resolveValue(value, secrets) {
21934
- if (typeof value === "string" && isVaultReference(value)) {
21935
- const ref = parseVaultReferenceDetailed(value);
21936
- return resolveSingleReference(ref, secrets);
21937
- }
21938
- if (Array.isArray(value)) {
21939
- return value.map((item) => resolveValue(item, secrets));
21940
- }
21941
- if (value !== null && typeof value === "object") {
21942
- const resolved = {};
21943
- for (const [k, v] of Object.entries(value)) {
21944
- resolved[k] = resolveValue(v, secrets);
21945
- }
21946
- return resolved;
21947
- }
21948
- return value;
21949
- }
21950
- async function resolveVaultReferencesViaBroker(config, brokerOpts) {
21951
- const socketPath = resolveBrokerSocketPath({
21952
- ...brokerOpts,
21953
- vaultBrokerSocket: config.vault?.broker?.socket ? resolvePath(config.vault.broker.socket) : undefined
21954
- });
21955
- const opts = { ...brokerOpts, socket: socketPath };
21956
- const refs = collectVaultRefs(config);
21957
- if (refs.size === 0) {
21958
- return { ok: true, config };
21959
- }
21960
- const brokerSecrets = {};
21961
- let sawDenied = false;
21962
- let sawUnreachable = false;
21963
- let sawLocked = false;
21964
- let sawNotFound = false;
21965
- const resolverAgentSlug = process.env.SWITCHROOM_AGENT_NAME;
21966
- const resolverToken = resolverAgentSlug ? readVaultTokenFile(resolverAgentSlug) ?? undefined : undefined;
21967
- const optsWithToken = resolverToken ? { ...opts, token: resolverToken } : opts;
21968
- for (const key of refs) {
21969
- const result = await getViaBrokerStructured(key, optsWithToken);
21970
- if (result.kind === "ok") {
21971
- brokerSecrets[key] = result.entry;
21972
- } else if (result.kind === "unreachable") {
21973
- sawUnreachable = true;
21974
- break;
21975
- } else if (result.kind === "denied") {
21976
- if (result.code === "LOCKED") {
21977
- sawLocked = true;
21978
- } else {
21979
- sawDenied = true;
21980
- }
21981
- } else if (result.kind === "not_found") {
21982
- sawNotFound = true;
21983
- }
21984
- }
21985
- const resolvedCount = Object.keys(brokerSecrets).length;
21986
- const allResolved = resolvedCount === refs.size;
21987
- if (allResolved) {
21988
- return { ok: true, config: resolveValue(config, brokerSecrets) };
21989
- }
21990
- if (sawUnreachable && !sawDenied && !sawLocked && !sawNotFound) {
21991
- return { ok: false, reason: "unreachable" };
21992
- }
21993
- if (sawLocked && !sawDenied && !sawUnreachable && !sawNotFound) {
21994
- return { ok: false, reason: "locked" };
21995
- }
21996
- if (sawDenied && !sawUnreachable && !sawLocked && !sawNotFound) {
21997
- return { ok: false, reason: "denied" };
21998
- }
21999
- if (sawNotFound && !sawDenied && !sawUnreachable && !sawLocked && resolvedCount > 0) {
22000
- return { ok: true, config: resolveValue(config, brokerSecrets) };
22001
- }
22002
- if (sawNotFound && !sawDenied && !sawUnreachable && !sawLocked && resolvedCount === 0) {
22003
- return { ok: false, reason: "not_found" };
22004
- }
22005
- return { ok: false, reason: "unknown" };
22006
- }
22007
- function collectVaultRefs(value) {
22008
- const keys = new Set;
22009
- function walk(v) {
22010
- if (typeof v === "string" && isVaultReference(v)) {
22011
- const ref = parseVaultReferenceDetailed(v);
22012
- keys.add(ref.key);
22013
- } else if (Array.isArray(v)) {
22014
- for (const item of v)
22015
- walk(item);
22016
- } else if (v !== null && typeof v === "object") {
22017
- for (const val of Object.values(v))
22018
- walk(val);
22019
- }
22020
- }
22021
- walk(value);
22022
- return keys;
22023
- }
22024
- var materializedDirs, cleanupRegistered = false, cachedRoot = null;
22025
- var init_resolver = __esm(() => {
22026
- init_vault();
22027
- init_loader();
22028
- init_client();
22029
- materializedDirs = new Set;
22030
- });
22031
-
22032
- // ../src/auth/quota.ts
22033
- function isProbeThin(q) {
22034
- return q.fiveHourUtilPresent === false && q.sevenDayUtilPresent === false;
22035
- }
22036
- function refillNormalizedUtils(q, now) {
22037
- const nowMs = now.getTime();
22038
- const fiveHourRefilled = q.fiveHourResetAt != null && q.fiveHourResetAt.getTime() <= nowMs;
22039
- const sevenDayRefilled = q.sevenDayResetAt != null && q.sevenDayResetAt.getTime() <= nowMs;
22040
- return {
22041
- fiveHourUtilizationPct: fiveHourRefilled ? 0 : q.fiveHourUtilizationPct,
22042
- sevenDayUtilizationPct: sevenDayRefilled ? 0 : q.sevenDayUtilizationPct,
22043
- fiveHourRefilled,
22044
- sevenDayRefilled
22045
- };
22046
- }
22047
-
22048
- // auth-snapshot-format.ts
22049
- var exports_auth_snapshot_format = {};
22050
- __export(exports_auth_snapshot_format, {
22051
- zipProbeResults: () => zipProbeResults,
22052
- reviveLastQuota: () => reviveLastQuota,
22053
- renderFallbackAnnouncement: () => renderFallbackAnnouncement,
22054
- renderAuthSnapshotFormat2: () => renderAuthSnapshotFormat2,
22055
- recommendation: () => recommendation,
22056
- formatStatusTime: () => formatStatusTime,
22057
- formatRelative: () => formatRelative,
22058
- formatAbsolute: () => formatAbsolute,
22059
- fmtPct: () => fmtPct,
22060
- deriveUsageFooterFreshness: () => deriveUsageFooterFreshness,
22061
- classifyHealth: () => classifyHealth,
22062
- buildSnapshotsFromState: () => buildSnapshotsFromState,
22063
- buildSnapshotsFromCachedState: () => buildSnapshotsFromCachedState,
22064
- buildSnapshotKeyboard: () => buildSnapshotKeyboard,
22065
- blockedReason: () => blockedReason,
22066
- bindingWindow: () => bindingWindow,
22067
- THROTTLING_THRESHOLD_PCT: () => THROTTLING_THRESHOLD_PCT
22068
- });
22069
- function classifyHealth(snap, now = new Date) {
22070
- if (!snap.quota)
22071
- return "unknown";
22072
- const q = snap.quota;
22073
- if (isProbeThin(q))
22074
- return "unknown";
22075
- const norm = refillNormalizedUtils(q, now);
22076
- const max = Math.max(norm.fiveHourUtilizationPct, norm.sevenDayUtilizationPct);
22077
- if (max >= 99.5)
22078
- return "blocked";
22079
- if (max >= THROTTLING_THRESHOLD_PCT)
22080
- return "throttling";
22081
- return "healthy";
22082
- }
22083
- function blockedReason(snap, now = new Date) {
22084
- if (classifyHealth(snap, now) !== "blocked")
22085
- return null;
22086
- return "quota-exhausted";
22087
- }
22088
- function bindingWindow(q) {
22089
- if (q.representativeClaim === "seven_day")
22090
- return "7d";
22091
- if (q.representativeClaim === "five_hour")
22092
- return "5h";
22093
- return q.sevenDayUtilizationPct >= q.fiveHourUtilizationPct ? "7d" : "5h";
22094
- }
22095
- function formatRelative(target, now = new Date) {
22096
- if (!target)
22097
- return "\u2014";
22098
- const deltaMs = target.getTime() - now.getTime();
22099
- if (deltaMs <= 0)
22100
- return "now";
22101
- const totalMin = Math.round(deltaMs / 60000);
22102
- if (totalMin < 60)
22103
- return `${totalMin}m`;
22104
- const h = Math.floor(totalMin / 60);
22105
- const m = totalMin % 60;
22106
- if (h < 24)
22107
- return m > 0 ? `${h}h ${m}m` : `${h}h`;
22108
- const d = Math.floor(h / 24);
22109
- const rh = h % 24;
22110
- return rh > 0 ? `${d}d ${rh}h` : `${d}d`;
22111
- }
22112
- function formatAbsolute(target, tz = "UTC") {
22113
- if (!target)
22114
- return "\u2014";
22115
- return target.toLocaleString("en-US", {
22116
- timeZone: tz,
22117
- weekday: "short",
22118
- hour: "numeric",
22119
- minute: "2-digit",
22120
- hour12: true
22121
- });
22122
- }
22123
- function formatStatusTime(target, now = new Date, tz = "UTC") {
22124
- if (!target)
22125
- return "\u2014";
22126
- const dayFmt = new Intl.DateTimeFormat("en-CA", {
22127
- timeZone: tz,
22128
- year: "numeric",
22129
- month: "2-digit",
22130
- day: "2-digit"
22131
- });
22132
- const timeFmt = new Intl.DateTimeFormat("en-US", {
22133
- timeZone: tz,
22134
- hour: "numeric",
22135
- minute: "2-digit",
22136
- hour12: true
22137
- });
22138
- const timeStr = timeFmt.format(target);
22139
- if (dayFmt.format(target) === dayFmt.format(now)) {
22140
- return timeStr;
22141
- }
22142
- const withinWeek = target.getTime() - now.getTime() < 7 * 24 * 60 * 60 * 1000;
22143
- const dateFmt = new Intl.DateTimeFormat("en-US", {
22144
- timeZone: tz,
22145
- weekday: "short",
22146
- ...withinWeek ? {} : { day: "numeric", month: "short" }
22147
- });
22148
- return `${dateFmt.format(target)} ${timeStr}`;
22149
- }
22150
- function fmtPct(pct) {
22151
- return `${Math.min(100, Math.round(pct))}%`;
22152
- }
22153
- function displayLabel(label, opts) {
22154
- return opts.demo ? maskEmail(label) : label;
22155
- }
22156
- function renderAccountRow(snap, opts) {
22157
- const now = opts.now ?? new Date;
22158
- const tz = opts.tz ?? "UTC";
22159
- const lines = [];
22160
- const marker = snap.isActive ? "\u25cf " : "";
22161
- const label = displayLabel(snap.label, opts);
22162
- if (!snap.quota) {
22163
- lines.push(`${marker}\`${codeSpanSafe(label)}\` _quota probe failed_`);
22164
- if (snap.quotaError) {
22165
- lines.push(` _${escapeMarkdown(snap.quotaError)}_`);
22166
- }
22167
- return lines;
22168
- }
22169
- const q = snap.quota;
22170
- if (isProbeThin(q)) {
22171
- lines.push(`${marker}\`${codeSpanSafe(label)}\` _quota unknown (thin probe)_`);
22172
- return lines;
22173
- }
22174
- const norm = refillNormalizedUtils(q, now);
22175
- const fiveStr = fmtPct(norm.fiveHourUtilizationPct);
22176
- const sevenStr = fmtPct(norm.sevenDayUtilizationPct);
22177
- lines.push(`${marker}\`${codeSpanSafe(label)}\` ${fiveStr} / ${sevenStr}`);
22178
- const health = classifyHealth(snap, now);
22179
- if (health === "blocked") {
22180
- const win = bindingWindow(q);
22181
- const reset = win === "5h" ? q.fiveHourResetAt : q.sevenDayResetAt;
22182
- const winLabel = win === "5h" ? "5-hour" : "7-day";
22183
- lines.push(reset ? ` _quota exhausted \u2014 back ${formatAbsolute(reset, tz)} (\`in ${formatRelative(reset, now)}\`, ${winLabel} cap)_` : ` _quota exhausted \u2014 ${winLabel} cap, reset time unknown_`);
22184
- return lines;
22185
- }
22186
- const fiveResetIn = q.fiveHourResetAt ? q.fiveHourResetAt.getTime() - now.getTime() : Infinity;
22187
- const sevenResetIn = q.sevenDayResetAt ? q.sevenDayResetAt.getTime() - now.getTime() : Infinity;
22188
- const fiveFirst = fiveResetIn <= sevenResetIn;
22189
- const fiveSeg = q.fiveHourResetAt ? `5h refills ${formatAbsolute(q.fiveHourResetAt, tz)} (\`in ${formatRelative(q.fiveHourResetAt, now)}\`)` : "5h refills \u2014";
22190
- const sevenSeg = q.sevenDayResetAt ? `7d resets ${formatAbsolute(q.sevenDayResetAt, tz)} (\`in ${formatRelative(q.sevenDayResetAt, now)}\`)` : "7d resets \u2014";
22191
- lines.push(` _${fiveFirst ? fiveSeg : sevenSeg}_`);
22192
- lines.push(` _${fiveFirst ? sevenSeg : fiveSeg}_`);
22193
- if (q.overageDisabledReason != null && OVERAGE_EXHAUSTED_REASONS.has(q.overageDisabledReason)) {
22194
- lines.push(` _overage off (${escapeMarkdown(q.overageDisabledReason)}) \u2014 serving from quota_`);
22195
- }
22196
- return lines;
22197
- }
22198
- function formatAgeStamp(atMs, now = new Date) {
22199
- const ageSec = Math.max(0, Math.round((now.getTime() - atMs) / 1000));
22200
- return ageSec < 60 ? `${ageSec}s ago` : `${Math.round(ageSec / 60)}m ago`;
22201
- }
22202
- function tableCell(s) {
22203
- return s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
22204
- }
22205
- function pctCells(snap, now) {
22206
- if (!snap.quota)
22207
- return { five: "\u2014", seven: "\u2014" };
22208
- if (isProbeThin(snap.quota))
22209
- return { five: "?", seven: "?" };
22210
- const norm = refillNormalizedUtils(snap.quota, now);
22211
- return { five: fmtPct(norm.fiveHourUtilizationPct), seven: fmtPct(norm.sevenDayUtilizationPct) };
22212
- }
22213
- function windowResetCell(snap, now, tz, win) {
22214
- if (!snap.quota) {
22215
- return snap.quotaError ? `probe failed (${snap.quotaError})` : "\u2014";
22216
- }
22217
- if (isProbeThin(snap.quota))
22218
- return "quota unknown";
22219
- const reset = win === "5h" ? snap.quota.fiveHourResetAt : snap.quota.sevenDayResetAt;
22220
- if (!reset)
22221
- return "\u2014";
22222
- return `${formatStatusTime(reset, now, tz)} (in ${formatRelative(reset, now)})`;
22223
- }
22224
- function renderAuthSnapshotFormat2(snapshots, opts = {}) {
22225
- const now = opts.now ?? new Date;
22226
- const tz = opts.tz ?? "UTC";
22227
- const lines = [];
22228
- lines.push("\uD83D\uDD0B **Auth \u2014 fleet status**");
22229
- const ordered = [...snapshots].sort((a, b) => {
22230
- if (a.isActive !== b.isActive)
22231
- return a.isActive ? -1 : 1;
22232
- const r = TABLE_HEALTH_RANK[classifyHealth(a, now)] - TABLE_HEALTH_RANK[classifyHealth(b, now)];
22233
- if (r !== 0)
22234
- return r;
22235
- return a.label.localeCompare(b.label);
22236
- });
22237
- if (ordered.length > 0) {
22238
- lines.push("");
22239
- lines.push("| State | Account | 5h | 5h resets | 7d | 7d resets |");
22240
- lines.push("| --- | --- | --- | --- | --- | --- |");
22241
- for (const s of ordered) {
22242
- const emoji = HEALTH_EMOJI[classifyHealth(s, now)];
22243
- const label = displayLabel(s.label, opts);
22244
- const accountCell = `\`${codeSpanSafe(s.isActive ? `${label} (active)` : label)}\``;
22245
- const { five, seven } = pctCells(s, now);
22246
- const fiveReset = windowResetCell(s, now, tz, "5h");
22247
- const sevenReset = windowResetCell(s, now, tz, "7d");
22248
- lines.push(`| ${emoji} | ${tableCell(accountCell)} | ${five} | ${tableCell(fiveReset)} | ${seven} | ${tableCell(sevenReset)} |`);
22249
- }
22250
- }
22251
- lines.push("");
22252
- lines.push(`_${recommendation(snapshots, now, opts.demo ?? false)}_`);
22253
- if (opts.staleCachedAtMs != null) {
22254
- lines.push(`_\u26a0 cached ${formatAgeStamp(opts.staleCachedAtMs, now)}_`);
22255
- } else if (opts.liveProbedAtMs != null) {
22256
- lines.push(`_Live \u00b7 refreshed ${formatAgeStamp(opts.liveProbedAtMs, now)}_`);
22257
- } else if (opts.probeFailed) {
22258
- lines.push("_\u26a0 probe failed \u2014 no live data_");
22259
- } else {
22260
- lines.push("_Live_");
22261
- }
22262
- return lines.join(`
22263
- `);
22264
- }
22265
- function recommendation(snapshots, now = new Date, demo = false) {
22266
- const active = snapshots.find((s) => s.isActive);
22267
- if (!active)
22268
- return "No active account set.";
22269
- const activeHealth = classifyHealth(active, now);
22270
- const others = snapshots.filter((s) => !s.isActive);
22271
- const healthyAlt = others.find((s) => classifyHealth(s, now) === "healthy");
22272
- const lbl = (s) => demo ? maskEmail(s.label) : s.label;
22273
- const activeLabel = lbl(active);
22274
- if (activeHealth === "healthy") {
22275
- return `Recommendation: stay on ${activeLabel}.`;
22276
- }
22277
- if (activeHealth === "throttling") {
22278
- if (healthyAlt) {
22279
- return `Recommendation: active ${activeLabel} is throttling. Switch to ${lbl(healthyAlt)} for headroom.`;
22280
- }
22281
- return `Recommendation: active ${activeLabel} is throttling; no healthy alternative \u2014 wait for refill.`;
22440
+ const st = statSync6(dir);
22441
+ if (typeof process.getuid === "function" && st.uid !== process.getuid()) {
22442
+ throw new Error(`Refusing to materialize vault entry: ${dir} not owned by caller`);
22282
22443
  }
22283
- if (activeHealth === "blocked") {
22284
- if (healthyAlt) {
22285
- return `Recommendation: active ${activeLabel} is BLOCKED \u2014 switch to ${lbl(healthyAlt)} now.`;
22444
+ for (const [filename, { encoding, value }] of Object.entries(files)) {
22445
+ if (filename.includes("/") || filename.includes("\\") || filename === ".." || filename === "." || filename.includes("\x00")) {
22446
+ throw new Error(`Refusing to materialize vault file with unsafe name: ${filename}`);
22286
22447
  }
22287
- return summarizeNoHealthyAlt(snapshots, now, demo);
22448
+ const filePath = join5(dir, filename);
22449
+ const content = encoding === "base64" ? Buffer.from(value, "base64") : value;
22450
+ writeFileExclusive(filePath, content);
22288
22451
  }
22289
- return `Active ${activeLabel}: quota probe failed; broker last_seen unknown.`;
22452
+ materializedDirs.add(dir);
22453
+ registerCleanupHook();
22454
+ return dir;
22290
22455
  }
22291
- function summarizeNoHealthyAlt(snapshots, now, demo = false) {
22292
- const mask = (label) => demo ? maskEmail(label) : label;
22293
- let throttlingLabel = null;
22294
- let allTrulyBlocked = true;
22295
- for (const s of snapshots) {
22296
- const h = classifyHealth(s, now);
22297
- if (h === "throttling") {
22298
- if (!throttlingLabel)
22299
- throttlingLabel = s.label;
22300
- allTrulyBlocked = false;
22301
- } else if (h === "healthy" || h === "unknown") {
22302
- allTrulyBlocked = false;
22303
- } else if (h === "blocked" && blockedReason(s, now) === "quota-exhausted") {
22304
- if (s.quota) {
22305
- const win = bindingWindow(s.quota);
22306
- const at = win === "5h" ? s.quota.fiveHourResetAt : s.quota.sevenDayResetAt;
22307
- if (at && at.getTime() > now.getTime())
22308
- allTrulyBlocked = false;
22309
- }
22310
- }
22311
- }
22312
- const earliestRecovery = pickEarliestRecovery(snapshots, now);
22313
- if (throttlingLabel) {
22314
- const eta = earliestRecovery ? ` Soonest full refill: ${mask(earliestRecovery.label)} in ${formatRelative(earliestRecovery.at, now)}.` : "";
22315
- return `No fully-healthy account; ${mask(throttlingLabel)} is throttling but still usable.${eta}`;
22456
+ function resolveSingleReference(ref, secrets) {
22457
+ const entry = secrets[ref.key];
22458
+ if (entry === undefined) {
22459
+ throw new Error(`Vault secret not found: ${ref.key}`);
22316
22460
  }
22317
- if (!allTrulyBlocked) {
22318
- if (earliestRecovery) {
22319
- return `All accounts at capacity; soonest refill: ${mask(earliestRecovery.label)} in ${formatRelative(earliestRecovery.at, now)}.`;
22461
+ if (ref.filename !== undefined) {
22462
+ if (entry.kind !== "files") {
22463
+ throw new Error(`Vault reference "vault:${ref.key}#${ref.filename}" expected kind="files", got kind="${entry.kind}".`);
22320
22464
  }
22321
- return `All accounts at capacity \u2014 waiting on a window refill.`;
22465
+ const file = entry.files[ref.filename];
22466
+ if (!file) {
22467
+ throw new Error(`Vault secret "${ref.key}" has no file named "${ref.filename}". Available: ${Object.keys(entry.files).join(", ")}`);
22468
+ }
22469
+ return file.encoding === "base64" ? Buffer.from(file.value, "base64").toString("utf8") : file.value;
22322
22470
  }
22323
- if (earliestRecovery) {
22324
- return `All accounts blocked. Earliest recovery: ${mask(earliestRecovery.label)} in ${formatRelative(earliestRecovery.at, now)}.`;
22471
+ if (entry.kind === "string" || entry.kind === "binary") {
22472
+ return entry.value;
22325
22473
  }
22326
- return `All accounts blocked. Run /auth add to attach another subscription.`;
22474
+ return materializeFilesEntry(ref.key, entry.files);
22327
22475
  }
22328
- function pickEarliestRecovery(snapshots, now) {
22329
- let best = null;
22330
- for (const s of snapshots) {
22331
- if (!s.quota)
22332
- continue;
22333
- if (isProbeThin(s.quota))
22334
- continue;
22335
- const win = bindingWindow(s.quota);
22336
- const at = win === "5h" ? s.quota.fiveHourResetAt : s.quota.sevenDayResetAt;
22337
- if (!at || at.getTime() <= now.getTime())
22338
- continue;
22339
- if (!best || at.getTime() < best.at.getTime()) {
22340
- best = { label: s.label, at };
22476
+ function resolveValue(value, secrets) {
22477
+ if (typeof value === "string" && isVaultReference(value)) {
22478
+ const ref = parseVaultReferenceDetailed(value);
22479
+ return resolveSingleReference(ref, secrets);
22480
+ }
22481
+ if (Array.isArray(value)) {
22482
+ return value.map((item) => resolveValue(item, secrets));
22483
+ }
22484
+ if (value !== null && typeof value === "object") {
22485
+ const resolved = {};
22486
+ for (const [k, v] of Object.entries(value)) {
22487
+ resolved[k] = resolveValue(v, secrets);
22341
22488
  }
22489
+ return resolved;
22342
22490
  }
22343
- return best;
22491
+ return value;
22344
22492
  }
22345
- function renderFallbackAnnouncement(input) {
22346
- const now = input.now ?? new Date;
22347
- const tz = input.tz ?? "UTC";
22348
- const lines = [];
22349
- const limitWord = input.oldQuota ? limitWordFor(input.oldQuota) : "quota";
22350
- const headerLimit = input.cause === "rate-limit" ? "rate limit" : limitWord === "quota" ? "quota cap" : `${limitWord} limit`;
22351
- if (!input.newLabel) {
22352
- lines.push(`\uD83D\uDD34 **All accounts blocked \u00b7 ${headerLimit} on ${escapeMarkdown(input.oldLabel)}**`);
22353
- lines.push("");
22354
- lines.push(`Triggered by: agent **${escapeMarkdown(input.triggerAgent)}**`);
22355
- const fleet = input.fleetSnapshots ?? [];
22356
- if (fleet.length > 0) {
22357
- lines.push("");
22358
- const rowOpts = { now, tz };
22359
- const healthOrder = ["blocked", "throttling", "healthy", "unknown"];
22360
- const rank = (s) => healthOrder.indexOf(classifyHealth(s, now));
22361
- const ordered = [...fleet].sort((a, b) => rank(a) - rank(b) || Number(b.isActive) - Number(a.isActive));
22362
- for (const snap of ordered) {
22363
- for (const ln of renderAccountRow(snap, rowOpts))
22364
- lines.push(ln);
22365
- }
22366
- const earliest = pickEarliestRecovery(fleet, now);
22367
- if (earliest) {
22368
- lines.push("");
22369
- lines.push(`Earliest recovery: \`${codeSpanSafe(earliest.label)}\` ` + `${formatAbsolute(earliest.at, tz)} (in ${formatRelative(earliest.at, now)})`);
22370
- }
22371
- } else {
22372
- const recovery = (input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ?? input.parsedResetAt ?? null;
22373
- if (recovery) {
22374
- lines.push(`${escapeMarkdown(input.oldLabel)} recovers ${formatAbsolute(recovery, tz)} ` + `(in ${formatRelative(recovery, now)})`);
22493
+ async function resolveVaultReferencesViaBroker(config, brokerOpts) {
22494
+ const socketPath = resolveBrokerSocketPath({
22495
+ ...brokerOpts,
22496
+ vaultBrokerSocket: config.vault?.broker?.socket ? resolvePath(config.vault.broker.socket) : undefined
22497
+ });
22498
+ const opts = { ...brokerOpts, socket: socketPath };
22499
+ const refs = collectVaultRefs(config);
22500
+ if (refs.size === 0) {
22501
+ return { ok: true, config };
22502
+ }
22503
+ const brokerSecrets = {};
22504
+ let sawDenied = false;
22505
+ let sawUnreachable = false;
22506
+ let sawLocked = false;
22507
+ let sawNotFound = false;
22508
+ const resolverAgentSlug = process.env.SWITCHROOM_AGENT_NAME;
22509
+ const resolverToken = resolverAgentSlug ? readVaultTokenFile(resolverAgentSlug) ?? undefined : undefined;
22510
+ const optsWithToken = resolverToken ? { ...opts, token: resolverToken } : opts;
22511
+ for (const key of refs) {
22512
+ const result = await getViaBrokerStructured(key, optsWithToken);
22513
+ if (result.kind === "ok") {
22514
+ brokerSecrets[key] = result.entry;
22515
+ } else if (result.kind === "unreachable") {
22516
+ sawUnreachable = true;
22517
+ break;
22518
+ } else if (result.kind === "denied") {
22519
+ if (result.code === "LOCKED") {
22520
+ sawLocked = true;
22521
+ } else {
22522
+ sawDenied = true;
22375
22523
  }
22524
+ } else if (result.kind === "not_found") {
22525
+ sawNotFound = true;
22376
22526
  }
22377
- lines.push("");
22378
- lines.push(`Run \`/auth add <label>\` to attach another subscription, ` + `or \`/auth refresh\` to re-probe.`);
22379
- return lines.join(`
22380
- `);
22381
22527
  }
22382
- lines.push(`\u2713 **Switched fleet \u00b7 ${headerLimit} on ${escapeMarkdown(input.oldLabel)}**`);
22383
- lines.push("");
22384
- lines.push(`\`${codeSpanSafe(input.oldLabel)}\` \u2192 \`${codeSpanSafe(input.newLabel)}\``);
22385
- lines.push(`Triggered by: agent **${escapeMarkdown(input.triggerAgent)}**`);
22386
- lines.push("");
22387
- {
22388
- const recovery = (input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ?? input.parsedResetAt ?? null;
22389
- if (recovery) {
22390
- lines.push(`\`${codeSpanSafe(input.oldLabel)}\` recovers ` + `${formatAbsolute(recovery, tz)} (in ${formatRelative(recovery, now)})`);
22391
- }
22528
+ const resolvedCount = Object.keys(brokerSecrets).length;
22529
+ const allResolved = resolvedCount === refs.size;
22530
+ if (allResolved) {
22531
+ return { ok: true, config: resolveValue(config, brokerSecrets) };
22392
22532
  }
22393
- if (input.newQuota) {
22394
- const fiveStr = fmtPct(input.newQuota.fiveHourUtilizationPct);
22395
- const sevenStr = fmtPct(input.newQuota.sevenDayUtilizationPct);
22396
- const hasHeadroom = input.newQuota.fiveHourUtilizationPct < THROTTLING_THRESHOLD_PCT && input.newQuota.sevenDayUtilizationPct < THROTTLING_THRESHOLD_PCT;
22397
- const headroomStr = hasHeadroom ? "_(plenty of headroom)_" : "_(near limit \u2014 watch this)_";
22398
- lines.push(`\`${codeSpanSafe(input.newLabel)}\` now: ${fiveStr} of 5h \u00b7 ${sevenStr} of 7d ${headroomStr}`);
22399
- } else {
22400
- lines.push(`_(quota probe for new account is pending \u2014 will reflect on next /auth)_`);
22533
+ if (sawUnreachable && !sawDenied && !sawLocked && !sawNotFound) {
22534
+ return { ok: false, reason: "unreachable" };
22401
22535
  }
22402
- return lines.join(`
22403
- `);
22404
- }
22405
- function limitWordFor(q) {
22406
- if (q.representativeClaim === "seven_day" && q.sevenDayUtilizationPct >= 99)
22407
- return "7-day";
22408
- if (q.representativeClaim === "five_hour" && q.fiveHourUtilizationPct >= 99)
22409
- return "5-hour";
22410
- if (q.sevenDayUtilizationPct >= 99)
22411
- return "7-day";
22412
- if (q.fiveHourUtilizationPct >= 99)
22413
- return "5-hour";
22414
- return q.sevenDayUtilizationPct >= q.fiveHourUtilizationPct ? "7-day" : "5-hour";
22415
- }
22416
- function recoveryAtFor(q) {
22417
- const word = limitWordFor(q);
22418
- if (word === "7-day")
22419
- return q.sevenDayResetAt;
22420
- if (word === "5-hour")
22421
- return q.fiveHourResetAt;
22422
- if (!q.fiveHourResetAt)
22423
- return q.sevenDayResetAt;
22424
- if (!q.sevenDayResetAt)
22425
- return q.fiveHourResetAt;
22426
- return q.fiveHourResetAt.getTime() < q.sevenDayResetAt.getTime() ? q.fiveHourResetAt : q.sevenDayResetAt;
22427
- }
22428
- function buildSnapshotKeyboard(snapshots, opts = {}) {
22429
- const max = opts.maxSwitchButtons ?? 3;
22430
- const now = opts.now ?? new Date;
22431
- const rows = [];
22432
- const switchTargets = snapshots.filter((s) => !s.isActive).sort((a, b) => switchPriority(a, now) - switchPriority(b, now)).filter((s) => classifyHealth(s, now) !== "blocked" && classifyHealth(s, now) !== "unknown").slice(0, max);
22433
- for (const t of switchTargets) {
22434
- rows.push([
22435
- {
22436
- text: `Switch fleet \u2192 ${opts.demo ? maskEmail(t.label) : t.label}`,
22437
- callbackData: `auth:use:${t.label}`
22438
- }
22439
- ]);
22536
+ if (sawLocked && !sawDenied && !sawUnreachable && !sawNotFound) {
22537
+ return { ok: false, reason: "locked" };
22440
22538
  }
22441
- rows.push([
22442
- { text: "\u21bb Refresh", callbackData: opts.demo ? "auth:refresh:demo" : "auth:refresh" },
22443
- { text: "/usage", insertText: "/usage" },
22444
- { text: "+ Add", insertText: "/auth add " }
22445
- ]);
22446
- return rows;
22447
- }
22448
- function switchPriority(s, now = new Date) {
22449
- const h = classifyHealth(s, now);
22450
- if (h === "healthy")
22451
- return 0;
22452
- if (h === "throttling")
22453
- return 1;
22454
- if (h === "unknown")
22455
- return 2;
22456
- return 3;
22539
+ if (sawDenied && !sawUnreachable && !sawLocked && !sawNotFound) {
22540
+ return { ok: false, reason: "denied" };
22541
+ }
22542
+ if (sawNotFound && !sawDenied && !sawUnreachable && !sawLocked && resolvedCount > 0) {
22543
+ return { ok: true, config: resolveValue(config, brokerSecrets) };
22544
+ }
22545
+ if (sawNotFound && !sawDenied && !sawUnreachable && !sawLocked && resolvedCount === 0) {
22546
+ return { ok: false, reason: "not_found" };
22547
+ }
22548
+ return { ok: false, reason: "unknown" };
22457
22549
  }
22458
- function zipProbeResults(labels, results) {
22459
- let staleCachedAtMs;
22460
- const quotas = labels.map((label) => {
22461
- const hit = results.find((r) => r.label === label);
22462
- if (!hit)
22463
- return { ok: false, reason: "broker returned no result for account" };
22464
- if (hit.served === "cache" && hit.capturedAt != null) {
22465
- staleCachedAtMs = staleCachedAtMs == null ? hit.capturedAt : Math.min(staleCachedAtMs, hit.capturedAt);
22550
+ function collectVaultRefs(value) {
22551
+ const keys = new Set;
22552
+ function walk(v) {
22553
+ if (typeof v === "string" && isVaultReference(v)) {
22554
+ const ref = parseVaultReferenceDetailed(v);
22555
+ keys.add(ref.key);
22556
+ } else if (Array.isArray(v)) {
22557
+ for (const item of v)
22558
+ walk(item);
22559
+ } else if (v !== null && typeof v === "object") {
22560
+ for (const val of Object.values(v))
22561
+ walk(val);
22466
22562
  }
22467
- return hit.result;
22468
- });
22469
- return staleCachedAtMs != null ? { quotas, staleCachedAtMs } : { quotas };
22470
- }
22471
- function deriveUsageFooterFreshness(results, staleCachedAtMs, liveProbedAtMs) {
22472
- if (staleCachedAtMs != null)
22473
- return { staleCachedAtMs };
22474
- if (results.some((r) => r.result.ok))
22475
- return { liveProbedAtMs };
22476
- return { probeFailed: true };
22477
- }
22478
- function buildSnapshotsFromState(state, quotas) {
22479
- const out = [];
22480
- for (let i = 0;i < state.accounts.length; i++) {
22481
- const acc = state.accounts[i];
22482
- const q = quotas[i];
22483
- out.push({
22484
- label: acc.label,
22485
- isActive: acc.label === state.active,
22486
- quota: q && q.ok ? q.data : null,
22487
- quotaError: q && !q.ok ? q.reason : undefined,
22488
- expiresAtMs: acc.expiresAt
22489
- });
22490
22563
  }
22491
- return out;
22492
- }
22493
- function reviveLastQuota(snap) {
22494
- if (!snap)
22495
- return null;
22496
- return {
22497
- fiveHourUtilizationPct: snap.fiveHourUtilizationPct,
22498
- sevenDayUtilizationPct: snap.sevenDayUtilizationPct,
22499
- fiveHourResetAt: snap.fiveHourResetAt ? new Date(snap.fiveHourResetAt) : null,
22500
- sevenDayResetAt: snap.sevenDayResetAt ? new Date(snap.sevenDayResetAt) : null,
22501
- representativeClaim: snap.representativeClaim,
22502
- overageStatus: snap.overageStatus,
22503
- overageDisabledReason: snap.overageDisabledReason,
22504
- fiveHourUtilPresent: snap.fiveHourUtilPresent,
22505
- sevenDayUtilPresent: snap.sevenDayUtilPresent
22506
- };
22507
- }
22508
- function buildSnapshotsFromCachedState(state) {
22509
- return state.accounts.map((acc) => {
22510
- const lq = acc.last_quota ?? null;
22511
- return {
22512
- label: acc.label,
22513
- isActive: acc.label === state.active,
22514
- quota: reviveLastQuota(lq),
22515
- quotaError: lq ? undefined : "no cached quota (no probe since broker start)",
22516
- expiresAtMs: acc.expiresAt,
22517
- capturedAtMs: lq?.capturedAt
22518
- };
22519
- });
22564
+ walk(value);
22565
+ return keys;
22520
22566
  }
22521
- var THROTTLING_THRESHOLD_PCT = 80, OVERAGE_EXHAUSTED_REASONS, HEALTH_EMOJI, TABLE_HEALTH_RANK;
22522
- var init_auth_snapshot_format = __esm(() => {
22523
- init_demo_mask();
22524
- init_card_format();
22525
- OVERAGE_EXHAUSTED_REASONS = new Set(["out_of_credits"]);
22526
- HEALTH_EMOJI = {
22527
- healthy: "\uD83D\uDFE2",
22528
- throttling: "\uD83D\uDFE1",
22529
- blocked: "\uD83D\uDD34",
22530
- unknown: "\u26aa"
22531
- };
22532
- TABLE_HEALTH_RANK = {
22533
- blocked: 0,
22534
- throttling: 1,
22535
- unknown: 2,
22536
- healthy: 3
22537
- };
22567
+ var materializedDirs, cleanupRegistered = false, cachedRoot = null;
22568
+ var init_resolver = __esm(() => {
22569
+ init_vault();
22570
+ init_loader();
22571
+ init_client();
22572
+ materializedDirs = new Set;
22538
22573
  });
22539
22574
 
22540
22575
  // ../src/auth/broker/protocol.ts
@@ -22719,6 +22754,8 @@ var init_protocol2 = __esm(() => {
22719
22754
  label: exports_external.string(),
22720
22755
  expiresAt: exports_external.number().optional(),
22721
22756
  exhausted: exports_external.boolean(),
22757
+ in_service: exports_external.boolean().optional(),
22758
+ entitlement_blocked: exports_external.boolean().optional(),
22722
22759
  exhausted_until: exports_external.number().optional(),
22723
22760
  throttled_until: exports_external.number().optional(),
22724
22761
  threshold_violations: exports_external.number().int().nonnegative().optional(),
@@ -36788,6 +36825,12 @@ __export(exports_auth_snapshot_format2, {
36788
36825
  THROTTLING_THRESHOLD_PCT: () => THROTTLING_THRESHOLD_PCT2
36789
36826
  });
36790
36827
  function classifyHealth2(snap, now = new Date) {
36828
+ if (!snap.isActive) {
36829
+ if (snap.entitlementBlocked === true)
36830
+ return "org-blocked";
36831
+ if (snap.inService === false)
36832
+ return "retired";
36833
+ }
36791
36834
  if (!snap.quota)
36792
36835
  return "unknown";
36793
36836
  const q = snap.quota;
@@ -36880,6 +36923,12 @@ function renderAccountRow2(snap, opts) {
36880
36923
  const lines = [];
36881
36924
  const marker = snap.isActive ? "\u25cf " : "";
36882
36925
  const label = displayLabel2(snap.label, opts);
36926
+ const health = classifyHealth2(snap, now);
36927
+ if (health === "org-blocked" || health === "retired") {
36928
+ const note = health === "org-blocked" ? "DISABLED (org) \u2014 no fleet routing" : "retired \u2014 removed from fleet rotation";
36929
+ lines.push(`${marker}\`${codeSpanSafe(label)}\` _${note}_`);
36930
+ return lines;
36931
+ }
36883
36932
  if (!snap.quota) {
36884
36933
  lines.push(`${marker}\`${codeSpanSafe(label)}\` _quota probe failed_`);
36885
36934
  if (snap.quotaError) {
@@ -36896,7 +36945,6 @@ function renderAccountRow2(snap, opts) {
36896
36945
  const fiveStr = fmtPct2(norm.fiveHourUtilizationPct);
36897
36946
  const sevenStr = fmtPct2(norm.sevenDayUtilizationPct);
36898
36947
  lines.push(`${marker}\`${codeSpanSafe(label)}\` ${fiveStr} / ${sevenStr}`);
36899
- const health = classifyHealth2(snap, now);
36900
36948
  if (health === "blocked") {
36901
36949
  const win = bindingWindow3(q);
36902
36950
  const reset2 = win === "5h" ? q.fiveHourResetAt : q.sevenDayResetAt;
@@ -36988,7 +37036,11 @@ function recommendation2(snapshots, now = new Date, demo = false) {
36988
37036
  if (!active)
36989
37037
  return "No active account set.";
36990
37038
  const activeHealth = classifyHealth2(active, now);
36991
- const others = snapshots.filter((s) => !s.isActive);
37039
+ const inServiceFleet = snapshots.filter((s) => {
37040
+ const h = classifyHealth2(s, now);
37041
+ return h !== "retired" && h !== "org-blocked";
37042
+ });
37043
+ const others = inServiceFleet.filter((s) => !s.isActive);
36992
37044
  const healthyAlt = others.find((s) => classifyHealth2(s, now) === "healthy");
36993
37045
  const lbl = (s) => demo ? maskEmail(s.label) : s.label;
36994
37046
  const activeLabel = lbl(active);
@@ -37005,7 +37057,7 @@ function recommendation2(snapshots, now = new Date, demo = false) {
37005
37057
  if (healthyAlt) {
37006
37058
  return `Recommendation: active ${activeLabel} is BLOCKED \u2014 switch to ${lbl(healthyAlt)} now.`;
37007
37059
  }
37008
- return summarizeNoHealthyAlt2(snapshots, now, demo);
37060
+ return summarizeNoHealthyAlt2(inServiceFleet, now, demo);
37009
37061
  }
37010
37062
  return `Active ${activeLabel}: quota probe failed; broker last_seen unknown.`;
37011
37063
  }
@@ -37077,7 +37129,14 @@ function renderFallbackAnnouncement2(input) {
37077
37129
  if (fleet.length > 0) {
37078
37130
  lines.push("");
37079
37131
  const rowOpts = { now, tz };
37080
- const healthOrder = ["blocked", "throttling", "healthy", "unknown"];
37132
+ const healthOrder = [
37133
+ "org-blocked",
37134
+ "blocked",
37135
+ "throttling",
37136
+ "healthy",
37137
+ "unknown",
37138
+ "retired"
37139
+ ];
37081
37140
  const rank = (s) => healthOrder.indexOf(classifyHealth2(s, now));
37082
37141
  const ordered = [...fleet].sort((a, b) => rank(a) - rank(b) || Number(b.isActive) - Number(a.isActive));
37083
37142
  for (const snap of ordered) {
@@ -37150,7 +37209,10 @@ function buildSnapshotKeyboard2(snapshots, opts = {}) {
37150
37209
  const max = opts.maxSwitchButtons ?? 3;
37151
37210
  const now = opts.now ?? new Date;
37152
37211
  const rows = [];
37153
- const switchTargets = snapshots.filter((s) => !s.isActive).sort((a, b) => switchPriority2(a, now) - switchPriority2(b, now)).filter((s) => classifyHealth2(s, now) !== "blocked" && classifyHealth2(s, now) !== "unknown").slice(0, max);
37212
+ const switchTargets = snapshots.filter((s) => !s.isActive).sort((a, b) => switchPriority2(a, now) - switchPriority2(b, now)).filter((s) => {
37213
+ const h = classifyHealth2(s, now);
37214
+ return h !== "blocked" && h !== "unknown" && h !== "retired" && h !== "org-blocked";
37215
+ }).slice(0, max);
37154
37216
  for (const t of switchTargets) {
37155
37217
  rows.push([
37156
37218
  {
@@ -37174,7 +37236,9 @@ function switchPriority2(s, now = new Date) {
37174
37236
  return 1;
37175
37237
  if (h === "unknown")
37176
37238
  return 2;
37177
- return 3;
37239
+ if (h === "blocked")
37240
+ return 3;
37241
+ return 4;
37178
37242
  }
37179
37243
  function zipProbeResults2(labels, results) {
37180
37244
  let staleCachedAtMs;
@@ -37206,7 +37270,9 @@ function buildSnapshotsFromState2(state7, quotas) {
37206
37270
  isActive: acc.label === state7.active,
37207
37271
  quota: q && q.ok ? q.data : null,
37208
37272
  quotaError: q && !q.ok ? q.reason : undefined,
37209
- expiresAtMs: acc.expiresAt
37273
+ expiresAtMs: acc.expiresAt,
37274
+ inService: acc.in_service,
37275
+ entitlementBlocked: acc.entitlement_blocked
37210
37276
  });
37211
37277
  }
37212
37278
  return out;
@@ -37235,7 +37301,9 @@ function buildSnapshotsFromCachedState2(state7) {
37235
37301
  quota: reviveLastQuota2(lq),
37236
37302
  quotaError: lq ? undefined : "no cached quota (no probe since broker start)",
37237
37303
  expiresAtMs: acc.expiresAt,
37238
- capturedAtMs: lq?.capturedAt
37304
+ capturedAtMs: lq?.capturedAt,
37305
+ inService: acc.in_service,
37306
+ entitlementBlocked: acc.entitlement_blocked
37239
37307
  };
37240
37308
  });
37241
37309
  }
@@ -37248,13 +37316,17 @@ var init_auth_snapshot_format2 = __esm(() => {
37248
37316
  healthy: "\uD83D\uDFE2",
37249
37317
  throttling: "\uD83D\uDFE1",
37250
37318
  blocked: "\uD83D\uDD34",
37251
- unknown: "\u26aa"
37319
+ unknown: "\u26aa",
37320
+ "org-blocked": "\u26d4",
37321
+ retired: "\u26ab"
37252
37322
  };
37253
37323
  TABLE_HEALTH_RANK2 = {
37254
- blocked: 0,
37255
- throttling: 1,
37256
- unknown: 2,
37257
- healthy: 3
37324
+ "org-blocked": 0,
37325
+ blocked: 1,
37326
+ throttling: 2,
37327
+ unknown: 3,
37328
+ healthy: 4,
37329
+ retired: 5
37258
37330
  };
37259
37331
  });
37260
37332
 
@@ -37861,9 +37933,13 @@ function buildBar(pct, elapsedFrac) {
37861
37933
  cells[tickIndex] = "\u2503";
37862
37934
  return cells.join("");
37863
37935
  }
37864
- function accountStatus(isActive, exhausted) {
37936
+ function accountStatus(isActive, exhausted, opts = {}) {
37865
37937
  if (isActive)
37866
37938
  return "active";
37939
+ if (opts.entitlementBlocked === true)
37940
+ return "org-disabled";
37941
+ if (opts.inService === false)
37942
+ return "retired";
37867
37943
  if (exhausted)
37868
37944
  return "exhausted";
37869
37945
  return "idle";
@@ -37876,10 +37952,15 @@ function formatWindowRow(window2, pct, resetAt, now = new Date) {
37876
37952
  const timeLeft = formatTimeLeft(resetAt, now);
37877
37953
  return `- ${dot} ${window2} \`[${bar}] ${pctStr} / ${timeLeft}\``;
37878
37954
  }
37879
- function renderQuotaBarAccount(label, isActive, exhausted, quota, now = new Date, demo = false) {
37880
- const status = accountStatus(isActive, exhausted);
37955
+ function renderQuotaBarAccount(label, isActive, exhausted, quota, now = new Date, demo = false, service = {}) {
37956
+ const status = accountStatus(isActive, exhausted, service);
37881
37957
  const displayLabel3 = demo ? maskEmail(label) : label;
37882
- const lines = [`- **${escapeMarkdown(displayLabel3)}** (${status})`];
37958
+ const lines = [`- **${escapeMarkdown(displayLabel3)}** (${STATUS_LABEL[status]})`];
37959
+ if (status === "retired" || status === "org-disabled") {
37960
+ const note = status === "org-disabled" ? "disabled by org \u2014 no fleet routing" : "retired \u2014 removed from fleet rotation";
37961
+ lines.push(`- \u26ab \`${note}\``);
37962
+ return lines;
37963
+ }
37883
37964
  if (!quota || isProbeThin(quota)) {
37884
37965
  const reason = !quota ? "no data \u2014 probe failed" : "no data \u2014 thin probe";
37885
37966
  lines.push(`- \u26a0\ufe0f 5h \`${reason}\``);
@@ -37899,9 +37980,17 @@ function renderQuotaBarBlock(snapshots, exhaustedByLabel, opts = {}) {
37899
37980
  const now = opts.now ?? new Date;
37900
37981
  const demo = opts.demo ?? false;
37901
37982
  const lines = [];
37902
- for (const snap of snapshots) {
37983
+ const outOfService = (s) => !s.isActive && (s.entitlementBlocked === true || s.inService === false);
37984
+ const ordered = [
37985
+ ...snapshots.filter((s) => !outOfService(s)),
37986
+ ...snapshots.filter(outOfService)
37987
+ ];
37988
+ for (const snap of ordered) {
37903
37989
  const exhausted = exhaustedByLabel.get(snap.label) ?? false;
37904
- lines.push(...renderQuotaBarAccount(snap.label, snap.isActive, exhausted, snap.quota, now, demo));
37990
+ lines.push(...renderQuotaBarAccount(snap.label, snap.isActive, exhausted, snap.quota, now, demo, {
37991
+ inService: snap.inService,
37992
+ entitlementBlocked: snap.entitlementBlocked
37993
+ }));
37905
37994
  }
37906
37995
  return lines.join(`
37907
37996
  `);
@@ -37915,7 +38004,9 @@ function renderQuotaBarBlockFromListState(state7, opts = {}) {
37915
38004
  quota: reviveLastQuota(acc.last_quota ?? null),
37916
38005
  quotaError: acc.last_quota ? undefined : "no cached quota (no probe since broker start)",
37917
38006
  expiresAtMs: acc.expiresAt,
37918
- capturedAtMs: acc.last_quota?.capturedAt
38007
+ capturedAtMs: acc.last_quota?.capturedAt,
38008
+ inService: acc.in_service,
38009
+ entitlementBlocked: acc.entitlement_blocked
37919
38010
  }));
37920
38011
  return renderQuotaBarBlock(snapshots, exhaustedByLabel, { now });
37921
38012
  }
@@ -37940,7 +38031,7 @@ function renderUsageCard(snapshots, exhaustedByLabel, opts = {}) {
37940
38031
  return lines.join(`
37941
38032
  `);
37942
38033
  }
37943
- var FIVE_HOUR_MS, SEVEN_DAY_MS, BAR_WIDTH = 10;
38034
+ var FIVE_HOUR_MS, SEVEN_DAY_MS, BAR_WIDTH = 10, STATUS_LABEL;
37944
38035
  var init_quota_bar_format = __esm(() => {
37945
38036
  init_auth_snapshot_format();
37946
38037
  init_card_format();
@@ -37948,6 +38039,13 @@ var init_quota_bar_format = __esm(() => {
37948
38039
  init_external_spend2();
37949
38040
  FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
37950
38041
  SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000;
38042
+ STATUS_LABEL = {
38043
+ active: "active",
38044
+ "org-disabled": "DISABLED (org)",
38045
+ retired: "retired",
38046
+ exhausted: "exhausted",
38047
+ idle: "idle"
38048
+ };
37951
38049
  });
37952
38050
 
37953
38051
  // external-spend.ts
@@ -40267,6 +40365,23 @@ function normalizeForDedup(text) {
40267
40365
 
40268
40366
  // flushed-turn-supersede.ts
40269
40367
  var DEFAULT_SUPERSEDE_TTL_MS = 60000;
40368
+ var SUPERSEDE_MATCH_MIN_CONTAINMENT_CHARS = 32;
40369
+ function normalizeForMatch(text) {
40370
+ return text.replace(/\s+/g, " ").trim();
40371
+ }
40372
+ function flushedAnswerMatchesReply(flushedText, replyText) {
40373
+ const flushed = normalizeForMatch(flushedText);
40374
+ const reply = normalizeForMatch(replyText);
40375
+ if (flushed.length === 0 || reply.length === 0)
40376
+ return false;
40377
+ if (flushed === reply)
40378
+ return true;
40379
+ if (reply.length >= SUPERSEDE_MATCH_MIN_CONTAINMENT_CHARS && flushed.includes(reply))
40380
+ return true;
40381
+ if (flushed.length >= SUPERSEDE_MATCH_MIN_CONTAINMENT_CHARS && reply.includes(flushed))
40382
+ return true;
40383
+ return false;
40384
+ }
40270
40385
  function decideSupersede(record, args) {
40271
40386
  const ttlMs = args.ttlMs ?? DEFAULT_SUPERSEDE_TTL_MS;
40272
40387
  if (record == null)
@@ -40278,7 +40393,15 @@ function decideSupersede(record, args) {
40278
40393
  if (!sameTurn) {
40279
40394
  return { supersede: false, deleteMessageIds: [], reason: "different-turn" };
40280
40395
  }
40281
- return { supersede: true, deleteMessageIds: [...record.messageIds], reason: "supersede" };
40396
+ if (args.replyText != null && !flushedAnswerMatchesReply(record.text, args.replyText)) {
40397
+ return { supersede: false, deleteMessageIds: [], reason: "new-content", recordText: record.text };
40398
+ }
40399
+ return {
40400
+ supersede: true,
40401
+ deleteMessageIds: [...record.messageIds],
40402
+ reason: "supersede",
40403
+ recordText: record.text
40404
+ };
40282
40405
  }
40283
40406
  var NULL_TURN_KEY = "<<null-turn>>";
40284
40407
  function turnKey(turnId) {
@@ -40310,7 +40433,12 @@ class FlushedTurnSupersedeRegistry {
40310
40433
  }
40311
40434
  peek(chatId, threadId, args) {
40312
40435
  const rec = this.entries.get(makeKey2(chatId, threadId))?.get(turnKey(args.liveTurnId));
40313
- return decideSupersede(rec, { liveTurnId: args.liveTurnId, now: args.now, ttlMs: this.ttlMs });
40436
+ return decideSupersede(rec, {
40437
+ liveTurnId: args.liveTurnId,
40438
+ replyText: args.replyText,
40439
+ now: args.now,
40440
+ ttlMs: this.ttlMs
40441
+ });
40314
40442
  }
40315
40443
  take(chatId, threadId, args) {
40316
40444
  const lane = makeKey2(chatId, threadId);
@@ -40899,6 +41027,63 @@ function buildStopReply2(turnInFlight, queuedSessionCmds) {
40899
41027
  `) };
40900
41028
  }
40901
41029
 
41030
+ // gateway/auth-command.ts
41031
+ init_format();
41032
+ init_auth_snapshot_format();
41033
+ init_demo_mask();
41034
+ var pendingAuthRmFlows = new Map;
41035
+ function isAuthAdmin(args) {
41036
+ return args.isAdmin === true;
41037
+ }
41038
+ var REQUIRED_USAGE_SCOPE = "user:profile";
41039
+ function readdPrecheckError(label, replace, labelExists) {
41040
+ if (replace && !labelExists) {
41041
+ return `**/auth readd:** no account named \`${codeSpanSafe(label)}\` to re-auth. ` + `Run \`/auth show\` for the current list, or \`/auth add ${codeSpanSafe(label)}\` to add it fresh.`;
41042
+ }
41043
+ if (!replace && labelExists) {
41044
+ return `**/auth add:** \`${codeSpanSafe(label)}\` already exists. ` + `Use \`/auth readd ${codeSpanSafe(label)}\` to re-authenticate it in place (e.g. to widen scope).`;
41045
+ }
41046
+ return null;
41047
+ }
41048
+ async function runReaddPrecheck(getClient, label, replace) {
41049
+ try {
41050
+ const client = await getClient();
41051
+ if (!client)
41052
+ return null;
41053
+ const state = await client.listState();
41054
+ const exists = state.accounts.some((a) => a.label === label);
41055
+ return readdPrecheckError(label, replace, exists);
41056
+ } catch {
41057
+ return null;
41058
+ }
41059
+ }
41060
+ function formatGrantedScopesReply(scopes) {
41061
+ const list = Array.isArray(scopes) ? scopes.filter((s) => typeof s === "string") : [];
41062
+ const hasUsageScope = list.includes(REQUIRED_USAGE_SCOPE);
41063
+ if (list.length === 0) {
41064
+ return {
41065
+ text: `
41066
+ \u26a0\ufe0f **No scopes reported** on the new token \u2014 could not confirm \`${REQUIRED_USAGE_SCOPE}\`. ` + `7-day usage reporting may not work. Re-run \`/auth readd\` if usage is missing.`,
41067
+ hasUsageScope: false
41068
+ };
41069
+ }
41070
+ const rendered = list.map((s) => `\`${codeSpanSafe(s)}\``).join(", ");
41071
+ if (hasUsageScope) {
41072
+ return {
41073
+ text: `
41074
+ Granted scopes: ${rendered}
41075
+ \u2713 \`${REQUIRED_USAGE_SCOPE}\` present \u2014 7-day usage reporting is unlocked.`,
41076
+ hasUsageScope: true
41077
+ };
41078
+ }
41079
+ return {
41080
+ text: `
41081
+ Granted scopes: ${rendered}
41082
+ ` + `\u26a0\ufe0f **\`${REQUIRED_USAGE_SCOPE}\` is MISSING** \u2014 7-day usage reporting will not work for this account. ` + `This usually means the narrow \`setup-token\` minter was used. Re-run \`/auth readd <label>\` with the broad login to fix.`,
41083
+ hasUsageScope: false
41084
+ };
41085
+ }
41086
+
40902
41087
  // gateway/interrupt-defer.ts
40903
41088
  class ToolFlightTracker2 {
40904
41089
  inFlight = new Set;
@@ -41944,11 +42129,14 @@ async function interceptAuthAdd(p, deps) {
41944
42129
  deps.pendingAuthAddFlows.delete(p.interceptKey);
41945
42130
  try {
41946
42131
  const credentials = await deps.submitAccountAuthCode(pendingAdd, p.text.trim());
42132
+ const replace = pendingAdd.replace === true;
41947
42133
  try {
41948
- await deps.addAccountViaBroker(pendingAdd.label, credentials, { replace: false });
42134
+ await deps.addAccountViaBroker(pendingAdd.label, credentials, { replace });
41949
42135
  deps.cleanAuthAddScratchDir(pendingAdd.scratchDir);
41950
- await deps.switchroomReply(p.ctx, `\u2713 Account \`${pendingAdd.label}\` added.
41951
- The fleet's active account hasn't changed. Send \`/auth use ${deps.escapeHtmlForTg(pendingAdd.label)}\` to switch to it.`, { html: true });
42136
+ const scopeReply = formatGrantedScopesReply(credentials.claudeAiOauth?.scopes);
42137
+ const verb = replace ? "re-authenticated" : "added";
42138
+ await deps.switchroomReply(p.ctx, `\u2713 Account \`${pendingAdd.label}\` ${verb}.
42139
+ The fleet's active account hasn't changed. Send \`/auth use ${deps.escapeHtmlForTg(pendingAdd.label)}\` to switch to it.` + scopeReply.text, { html: true });
41952
42140
  } catch (brokerErr) {
41953
42141
  deps.cleanAuthAddScratchDir(pendingAdd.scratchDir);
41954
42142
  await deps.switchroomReply(p.ctx, `**/auth add failed at broker:** ${deps.escapeHtmlForTg(brokerErr?.message ?? String(brokerErr))}`, { html: true });
@@ -42574,11 +42762,14 @@ async function interceptAuthAdd2(p, deps) {
42574
42762
  deps.pendingAuthAddFlows.delete(p.interceptKey);
42575
42763
  try {
42576
42764
  const credentials = await deps.submitAccountAuthCode(pendingAdd, p.text.trim());
42765
+ const replace = pendingAdd.replace === true;
42577
42766
  try {
42578
- await deps.addAccountViaBroker(pendingAdd.label, credentials, { replace: false });
42767
+ await deps.addAccountViaBroker(pendingAdd.label, credentials, { replace });
42579
42768
  deps.cleanAuthAddScratchDir(pendingAdd.scratchDir);
42580
- await deps.switchroomReply(p.ctx, `\u2713 Account \`${pendingAdd.label}\` added.
42581
- The fleet's active account hasn't changed. Send \`/auth use ${deps.escapeHtmlForTg(pendingAdd.label)}\` to switch to it.`, { html: true });
42769
+ const scopeReply = formatGrantedScopesReply(credentials.claudeAiOauth?.scopes);
42770
+ const verb = replace ? "re-authenticated" : "added";
42771
+ await deps.switchroomReply(p.ctx, `\u2713 Account \`${pendingAdd.label}\` ${verb}.
42772
+ The fleet's active account hasn't changed. Send \`/auth use ${deps.escapeHtmlForTg(pendingAdd.label)}\` to switch to it.` + scopeReply.text, { html: true });
42582
42773
  } catch (brokerErr) {
42583
42774
  deps.cleanAuthAddScratchDir(pendingAdd.scratchDir);
42584
42775
  await deps.switchroomReply(p.ctx, `**/auth add failed at broker:** ${deps.escapeHtmlForTg(brokerErr?.message ?? String(brokerErr))}`, { html: true });
@@ -43539,14 +43730,23 @@ function relaunchErrorReply(deps, model, err) {
43539
43730
  const msg = err instanceof Error ? err.message : String(err);
43540
43731
  return { text: `\u274c Could not schedule model switch: ${deps.escapeHtml(msg)}`, html: true };
43541
43732
  }
43733
+ function unvalidatedIdCaveat(deps, model) {
43734
+ if (!model.trim().toLowerCase().startsWith("claude-"))
43735
+ return null;
43736
+ return `_\`${deps.escapeHtml(model)}\` can't be validated before launch \u2014 if it isn't a real Claude model id, claude will silently serve the configured fallback model instead. I check the first reply and will warn if that happens._`;
43737
+ }
43542
43738
  async function scheduleRelaunchReply(deps, model, reason) {
43543
43739
  try {
43544
43740
  await deps.scheduleModelRelaunch(model, reason);
43545
43741
  } catch (err) {
43546
43742
  return relaunchErrorReply(deps, model, err);
43547
43743
  }
43548
- return { text: [switchingLine(deps, model), PERSIST_NOTE].join(`
43549
- `), html: true };
43744
+ const caveat = unvalidatedIdCaveat(deps, model);
43745
+ return {
43746
+ text: [switchingLine(deps, model), ...caveat ? [caveat] : [], PERSIST_NOTE].join(`
43747
+ `),
43748
+ html: true
43749
+ };
43550
43750
  }
43551
43751
  async function scheduleDefaultRelaunchReply(deps, reason) {
43552
43752
  try {
@@ -50832,16 +51032,28 @@ function formatModelLabel2(model) {
50832
51032
  }
50833
51033
 
50834
51034
  // gateway/session-model-source.ts
50835
- function createSessionModelSource() {
51035
+ function createSessionModelSource(options = {}) {
50836
51036
  let seq = 0;
50837
51037
  let transcript = null;
50838
51038
  let override = null;
51039
+ let overrideUnverified = false;
51040
+ let onDivergence = null;
50839
51041
  return {
50840
- noteTranscriptModel(model) {
51042
+ noteTranscriptModel(model, opts) {
50841
51043
  transcript = { model, seq: ++seq };
51044
+ if (opts?.replayed === true)
51045
+ return;
51046
+ if (override != null && overrideUnverified) {
51047
+ overrideUnverified = false;
51048
+ const matches = options.servedMatchesRequested;
51049
+ if (matches != null && !matches(override.model, model)) {
51050
+ onDivergence?.({ requested: override.model, served: model });
51051
+ }
51052
+ }
50842
51053
  },
50843
- setOverride(model) {
51054
+ setOverride(model, opts) {
50844
51055
  override = model == null ? null : { model, seq: ++seq };
51056
+ overrideUnverified = model != null && opts?.verify === true;
50845
51057
  },
50846
51058
  getOverride() {
50847
51059
  return override?.model ?? null;
@@ -50855,6 +51067,9 @@ function createSessionModelSource() {
50855
51067
  return { model: override.model, source: "override" };
50856
51068
  }
50857
51069
  return { model: transcript.model, source: "transcript" };
51070
+ },
51071
+ setDivergenceHandler(handler) {
51072
+ onDivergence = handler;
50858
51073
  }
50859
51074
  };
50860
51075
  }
@@ -68866,7 +69081,7 @@ init_format();
68866
69081
  init_auth_snapshot_format();
68867
69082
  init_demo_mask();
68868
69083
  var AUTH_RM_CONFIRM_TTL_MS = 60000;
68869
- var pendingAuthRmFlows = new Map;
69084
+ var pendingAuthRmFlows2 = new Map;
68870
69085
  var LABEL_RE = /^[A-Za-z0-9._@+-]+$/;
68871
69086
  var LABEL_MAX = 64;
68872
69087
  function validateAuthAddLabel(label) {
@@ -68914,14 +69129,24 @@ function parseAuthCommand(text4) {
68914
69129
  return { kind: "help", reason: "Usage: /auth use <label>" };
68915
69130
  return { kind: "use", label };
68916
69131
  }
68917
- case "add": {
68918
- const label = parts[1];
69132
+ case "add":
69133
+ case "readd": {
69134
+ const positional = parts.slice(1).filter((t) => !t.startsWith("--"));
69135
+ const flags = new Set(parts.slice(1).filter((t) => t.startsWith("--")).map((t) => t.toLowerCase()));
69136
+ const label = positional[0];
69137
+ const usage = verb === "readd" ? "Usage: /auth readd <label>" : "Usage: /auth add <label>";
68919
69138
  if (!label)
68920
- return { kind: "help", reason: "Usage: /auth add <label>" };
69139
+ return { kind: "help", reason: usage };
68921
69140
  const err = validateAuthAddLabel(label);
68922
69141
  if (err)
68923
69142
  return { kind: "help", reason: err };
68924
- return { kind: "add", label };
69143
+ for (const f of flags) {
69144
+ if (f !== "--replace") {
69145
+ return { kind: "help", reason: `Unknown flag \`${codeSpanSafe(f)}\` for \`/auth ${verb}\`.` };
69146
+ }
69147
+ }
69148
+ const replace2 = verb === "readd" || flags.has("--replace");
69149
+ return { kind: "add", label, replace: replace2 };
68925
69150
  }
68926
69151
  case "cancel":
68927
69152
  return { kind: "cancel" };
@@ -69041,6 +69266,7 @@ async function handleAuthCommand(parsed, ctx) {
69041
69266
  ` + ` \`/auth use <label>\` \u2014 admin: swap the fleet to <label>
69042
69267
  ` + ` \`/auth rotate\` \u2014 admin: cycle to next non-exhausted fallback
69043
69268
  ` + ` \`/auth add <label>\` \u2014 admin: OAuth-add a new Anthropic account from chat
69269
+ ` + ` \`/auth readd <label>\` \u2014 admin: re-auth an EXISTING account in place (widen scope)
69044
69270
  ` + ` \`/auth google add <email>\` \u2014 admin: Telegram-native Google account add/re-auth
69045
69271
  ` + ` \`/auth microsoft add <email>\` \u2014 admin: Telegram-native Microsoft account add/re-auth
69046
69272
  ` + ` \`/auth cancel\` \u2014 abort an \`/auth add\` or provider add in progress
@@ -69195,7 +69421,7 @@ async function handleAuthCommand(parsed, ctx) {
69195
69421
  };
69196
69422
  }
69197
69423
  if (ctx.chatId) {
69198
- pendingAuthRmFlows.set(ctx.chatId, {
69424
+ pendingAuthRmFlows2.set(ctx.chatId, {
69199
69425
  label: parsed.label,
69200
69426
  expiresAt: Date.now() + AUTH_RM_CONFIRM_TTL_MS
69201
69427
  });
@@ -69209,11 +69435,11 @@ async function handleAuthCommand(parsed, ctx) {
69209
69435
  };
69210
69436
  }
69211
69437
  if (parsed.kind === "rm-confirmed") {
69212
- const pending = ctx.chatId ? pendingAuthRmFlows.get(ctx.chatId) : undefined;
69438
+ const pending = ctx.chatId ? pendingAuthRmFlows2.get(ctx.chatId) : undefined;
69213
69439
  const now = Date.now();
69214
69440
  if (!pending || pending.label !== parsed.label || pending.expiresAt <= now) {
69215
69441
  if (ctx.chatId && pending && pending.expiresAt <= now) {
69216
- pendingAuthRmFlows.delete(ctx.chatId);
69442
+ pendingAuthRmFlows2.delete(ctx.chatId);
69217
69443
  }
69218
69444
  return {
69219
69445
  text: `**/auth rm:** no pending confirm for \`${codeSpanSafe(parsed.label)}\` (expired or not started). ` + `Send \`/auth rm ${codeSpanSafe(parsed.label)}\` first.`,
@@ -69221,7 +69447,7 @@ async function handleAuthCommand(parsed, ctx) {
69221
69447
  };
69222
69448
  }
69223
69449
  if (ctx.chatId)
69224
- pendingAuthRmFlows.delete(ctx.chatId);
69450
+ pendingAuthRmFlows2.delete(ctx.chatId);
69225
69451
  try {
69226
69452
  const data = await ctx.client.rmAccount(parsed.label);
69227
69453
  return {
@@ -69311,7 +69537,7 @@ ${failures.map((f) => ` ${f}`).join(`
69311
69537
  html: true
69312
69538
  };
69313
69539
  }
69314
- function isAuthAdmin(args) {
69540
+ function isAuthAdmin2(args) {
69315
69541
  return args.isAdmin === true;
69316
69542
  }
69317
69543
  function isAdmin(ctx) {
@@ -70395,6 +70621,12 @@ function readTokenFromCredentialsFile(credentialsFilePath) {
70395
70621
  }
70396
70622
  }
70397
70623
 
70624
+ // ../src/auth/via-claude.ts
70625
+ var PRE_PASTE_RULES = [
70626
+ { name: "theme", match: /Choose.{1,30}text.{1,30}style/, keys: ["Enter"] },
70627
+ { name: "login-method", match: /Select login method/, keys: ["Enter"] }
70628
+ ];
70629
+
70398
70630
  // gateway/auth-add-flow.ts
70399
70631
  function makeAuthAddTmuxOps(tmuxBin = "tmux") {
70400
70632
  return {
@@ -70420,6 +70652,11 @@ function makeAuthAddTmuxOps(tmuxBin = "tmux") {
70420
70652
  stdio: ["pipe", "pipe", "pipe"]
70421
70653
  });
70422
70654
  },
70655
+ sendKey(socket, session, key) {
70656
+ execFileSync4(tmuxBin, ["-L", socket, "send-keys", "-t", session, key], {
70657
+ stdio: ["pipe", "pipe", "pipe"]
70658
+ });
70659
+ },
70423
70660
  hasSession(socket, session) {
70424
70661
  try {
70425
70662
  execFileSync4(tmuxBin, ["-L", socket, "has-session", "-t", session], {
@@ -70492,10 +70729,11 @@ async function startAccountAuthSession(label, opts = {}) {
70492
70729
  throw new Error('tmux supervisor required for /auth add: SWITCHROOM_TMUX_SUPERVISOR is not set to "1". ' + "Legacy pipe-based setup-token is unsupported (setup-token writes to /dev/tty, not stdout/stderr).");
70493
70730
  }
70494
70731
  const home2 = opts.home ?? homedir9();
70495
- const urlTimeoutMs = opts.urlTimeoutMs ?? 12000;
70732
+ const urlTimeoutMs = opts.urlTimeoutMs ?? 30000;
70496
70733
  const agentName3 = opts.agentName ?? process.env.SWITCHROOM_AGENT_NAME ?? "gateway";
70497
70734
  const tmux = opts.tmuxOps ?? makeAuthAddTmuxOps(opts.tmuxBin);
70498
70735
  const binary = opts.claudeBinary ?? "claude";
70736
+ const mode = opts.mode ?? "via-claude";
70499
70737
  const scratchDir = pickScratchDir(label, home2);
70500
70738
  mkdirSync23(scratchDir, { recursive: true, mode: 448 });
70501
70739
  sweepOrphanSessions(home2, tmux);
@@ -70516,18 +70754,21 @@ ${tmuxSession}`, "utf8");
70516
70754
  sessionEnv["CLAUDE_CONFIG_DIR"] = scratchDir;
70517
70755
  if (process.env.XDG_CONFIG_HOME)
70518
70756
  sessionEnv["XDG_CONFIG_HOME"] = process.env.XDG_CONFIG_HOME;
70757
+ const sessionCmd = mode === "via-claude" ? binary : binary + " setup-token";
70758
+ const minterLabel = mode === "via-claude" ? "claude login" : "claude setup-token";
70519
70759
  try {
70520
- tmux.newSession(tmuxSocket, tmuxSession, sessionEnv, binary + " setup-token");
70760
+ tmux.newSession(tmuxSocket, tmuxSession, sessionEnv, sessionCmd);
70521
70761
  } catch (err) {
70522
70762
  cleanScratchDir(scratchDir);
70523
- throw new Error(`Failed to start tmux session for claude setup-token: ${err.message}`);
70763
+ throw new Error(`Failed to start tmux session for ${minterLabel}: ${err.message}`);
70524
70764
  }
70765
+ const preFired = new Set;
70525
70766
  const loginUrl = await new Promise((resolve7, reject) => {
70526
70767
  const deadline = setTimeout(() => {
70527
70768
  clearInterval(ticker);
70528
70769
  tmux.killSession(tmuxSocket, tmuxSession);
70529
70770
  cleanScratchDir(scratchDir);
70530
- reject(new Error(`claude setup-token did not print an OAuth URL within ${urlTimeoutMs}ms`));
70771
+ reject(new Error(`${minterLabel} did not print an OAuth URL within ${urlTimeoutMs}ms`));
70531
70772
  }, urlTimeoutMs);
70532
70773
  const ticker = setInterval(() => {
70533
70774
  const pane = tmux.capture(tmuxSocket, tmuxSession);
@@ -70535,9 +70776,20 @@ ${tmuxSession}`, "utf8");
70535
70776
  clearTimeout(deadline);
70536
70777
  clearInterval(ticker);
70537
70778
  cleanScratchDir(scratchDir);
70538
- reject(new Error("claude setup-token exited before printing OAuth URL"));
70779
+ reject(new Error(`${minterLabel} exited before printing OAuth URL`));
70539
70780
  return;
70540
70781
  }
70782
+ if (mode === "via-claude") {
70783
+ for (const rule of PRE_PASTE_RULES) {
70784
+ if (preFired.has(rule.name))
70785
+ continue;
70786
+ if (rule.match.test(pane)) {
70787
+ preFired.add(rule.name);
70788
+ for (const key of rule.keys)
70789
+ tmux.sendKey(tmuxSocket, tmuxSession, key);
70790
+ }
70791
+ }
70792
+ }
70541
70793
  const url = parseSetupTokenUrl(pane);
70542
70794
  if (url) {
70543
70795
  clearTimeout(deadline);
@@ -70546,12 +70798,14 @@ ${tmuxSession}`, "utf8");
70546
70798
  }
70547
70799
  }, 500);
70548
70800
  });
70549
- return { loginUrl, scratchDir, tmuxSocket, tmuxSession };
70801
+ return { loginUrl, scratchDir, tmuxSocket, tmuxSession, mode };
70550
70802
  }
70551
70803
  async function submitAccountAuthCode(flow3, code2, opts = {}) {
70552
70804
  const pollIntervalMs = opts.pollIntervalMs ?? 250;
70553
70805
  const pollTimeoutMs = opts.pollTimeoutMs ?? 300000;
70554
70806
  const tmux = opts.tmuxOps ?? makeAuthAddTmuxOps();
70807
+ const mode = flow3.mode ?? "via-claude";
70808
+ const blindEnterDelaysMs = opts.blindEnterDelaysMs ?? (mode === "via-claude" ? [1500, 3000, 5000] : []);
70555
70809
  const credentialsPath = join26(flow3.scratchDir, ".credentials.json");
70556
70810
  try {
70557
70811
  tmux.send(flow3.tmuxSocket, flow3.tmuxSession, code2);
@@ -70559,9 +70813,19 @@ async function submitAccountAuthCode(flow3, code2, opts = {}) {
70559
70813
  cleanScratchDir(flow3.scratchDir);
70560
70814
  throw new Error(`Failed to submit auth code to tmux session: ${err.message}`);
70561
70815
  }
70816
+ const pasteAt = Date.now();
70817
+ const blindEnters = blindEnterDelaysMs.map((d) => ({ at: pasteAt + d, fired: false }));
70562
70818
  const deadline = Date.now() + pollTimeoutMs;
70563
70819
  while (Date.now() < deadline) {
70564
70820
  await new Promise((r) => setTimeout(r, pollIntervalMs));
70821
+ for (const be of blindEnters) {
70822
+ if (!be.fired && Date.now() >= be.at) {
70823
+ be.fired = true;
70824
+ try {
70825
+ tmux.sendKey(flow3.tmuxSocket, flow3.tmuxSession, "Enter");
70826
+ } catch {}
70827
+ }
70828
+ }
70565
70829
  if (existsSync22(credentialsPath)) {
70566
70830
  const token = readTokenFromCredentialsFile(credentialsPath);
70567
70831
  if (token) {
@@ -70591,6 +70855,59 @@ function cancelAccountAuthSession(flow3, tmuxOps) {
70591
70855
  tmux.killSession(flow3.tmuxSocket, flow3.tmuxSession);
70592
70856
  cleanScratchDir(flow3.scratchDir);
70593
70857
  }
70858
+ async function handleAuthAddOrCancel(opts) {
70859
+ const { parsed, isAdmin: isAdmin2, currentAgent, chatId, threadId, reply, escapeHtml: escapeHtml5 } = opts;
70860
+ if (!isAuthAdmin({ isAdmin: isAdmin2 })) {
70861
+ await reply(`**Not authorized.** \`/auth ${parsed.kind}\` is admin-only.
70862
+ ` + `Set \`admin: true\` on this agent in switchroom.yaml to unlock ` + `(the same flag that gates \`/agents\`, \`/restart\`, ` + `\`/update\` etc.).`);
70863
+ return;
70864
+ }
70865
+ const authAddKey = chatKey(chatId, threadId);
70866
+ if (parsed.kind === "cancel") {
70867
+ const existing = pendingAuthAddFlows.get(authAddKey);
70868
+ if (!existing) {
70869
+ await reply("_No pending `/auth add` flow in this chat._");
70870
+ return;
70871
+ }
70872
+ cancelAccountAuthSession(existing);
70873
+ pendingAuthAddFlows.delete(authAddKey);
70874
+ await reply("Cancelled.");
70875
+ return;
70876
+ }
70877
+ if (pendingAuthAddFlows.has(authAddKey)) {
70878
+ await reply("_An `/auth add` flow is already in progress for this chat. " + "Finish the paste, or send `/auth cancel` to abort._");
70879
+ return;
70880
+ }
70881
+ const precheckErr = await runReaddPrecheck(() => getAuthBrokerClient(currentAgent), parsed.label, parsed.replace);
70882
+ if (precheckErr) {
70883
+ await reply(precheckErr);
70884
+ return;
70885
+ }
70886
+ try {
70887
+ const { loginUrl, scratchDir, tmuxSocket, tmuxSession, mode } = await startAccountAuthSession(parsed.label);
70888
+ pendingAuthAddFlows.set(authAddKey, {
70889
+ label: parsed.label,
70890
+ scratchDir,
70891
+ tmuxSocket,
70892
+ tmuxSession,
70893
+ startedAt: Date.now(),
70894
+ replace: parsed.replace,
70895
+ mode
70896
+ });
70897
+ const verbNoun = parsed.replace ? "Re-authenticating account" : "Adding account";
70898
+ await reply(`**${verbNoun}** \`${parsed.label}\`
70899
+
70900
+ ` + `1. Open this URL on your phone:
70901
+ ${loginUrl}
70902
+
70903
+ ` + `2. Log into Anthropic, copy the code Claude shows.
70904
+ ` + `3. Paste it back here.
70905
+
70906
+ ` + `Send \`/auth cancel\` to abort.`);
70907
+ } catch (err) {
70908
+ await reply(`**/auth ${parsed.replace ? "readd" : "add"} failed:** ${escapeHtml5(err?.message ?? String(err))}`);
70909
+ }
70910
+ }
70594
70911
 
70595
70912
  // gateway/auth-loopback-relay.ts
70596
70913
  import { spawn } from "node:child_process";
@@ -73430,6 +73747,23 @@ ${input.newReplyText}`;
73430
73747
 
73431
73748
  // flushed-turn-supersede.ts
73432
73749
  var DEFAULT_SUPERSEDE_TTL_MS2 = 60000;
73750
+ var SUPERSEDE_MATCH_MIN_CONTAINMENT_CHARS2 = 32;
73751
+ function normalizeForMatch2(text4) {
73752
+ return text4.replace(/\s+/g, " ").trim();
73753
+ }
73754
+ function flushedAnswerMatchesReply2(flushedText, replyText) {
73755
+ const flushed = normalizeForMatch2(flushedText);
73756
+ const reply = normalizeForMatch2(replyText);
73757
+ if (flushed.length === 0 || reply.length === 0)
73758
+ return false;
73759
+ if (flushed === reply)
73760
+ return true;
73761
+ if (reply.length >= SUPERSEDE_MATCH_MIN_CONTAINMENT_CHARS2 && flushed.includes(reply))
73762
+ return true;
73763
+ if (flushed.length >= SUPERSEDE_MATCH_MIN_CONTAINMENT_CHARS2 && reply.includes(flushed))
73764
+ return true;
73765
+ return false;
73766
+ }
73433
73767
  function decideSupersede2(record, args) {
73434
73768
  const ttlMs = args.ttlMs ?? DEFAULT_SUPERSEDE_TTL_MS2;
73435
73769
  if (record == null)
@@ -73441,7 +73775,15 @@ function decideSupersede2(record, args) {
73441
73775
  if (!sameTurn) {
73442
73776
  return { supersede: false, deleteMessageIds: [], reason: "different-turn" };
73443
73777
  }
73444
- return { supersede: true, deleteMessageIds: [...record.messageIds], reason: "supersede" };
73778
+ if (args.replyText != null && !flushedAnswerMatchesReply2(record.text, args.replyText)) {
73779
+ return { supersede: false, deleteMessageIds: [], reason: "new-content", recordText: record.text };
73780
+ }
73781
+ return {
73782
+ supersede: true,
73783
+ deleteMessageIds: [...record.messageIds],
73784
+ reason: "supersede",
73785
+ recordText: record.text
73786
+ };
73445
73787
  }
73446
73788
  function decideSupersedeCorrection(input) {
73447
73789
  const eligible = input.flushMessageIds.length === 1 && input.chunkCount === 1 && !input.hasFiles && !input.suppressText && !input.hasOpenPreview;
@@ -73480,7 +73822,12 @@ class FlushedTurnSupersedeRegistry2 {
73480
73822
  }
73481
73823
  peek(chatId, threadId, args) {
73482
73824
  const rec = this.entries.get(makeKey3(chatId, threadId))?.get(turnKey2(args.liveTurnId));
73483
- return decideSupersede2(rec, { liveTurnId: args.liveTurnId, now: args.now, ttlMs: this.ttlMs });
73825
+ return decideSupersede2(rec, {
73826
+ liveTurnId: args.liveTurnId,
73827
+ replyText: args.replyText,
73828
+ now: args.now,
73829
+ ttlMs: this.ttlMs
73830
+ });
73484
73831
  }
73485
73832
  take(chatId, threadId, args) {
73486
73833
  const lane = makeKey3(chatId, threadId);
@@ -73529,7 +73876,9 @@ function decideAnswerLatchSuppression(input) {
73529
73876
  return false;
73530
73877
  if (!input.isLateReply)
73531
73878
  return false;
73532
- return input.ownerAnswerDelivered;
73879
+ if (input.replyMatchesFlushedAnswer === false)
73880
+ return false;
73881
+ return input.ownerAnswerDelivered === "flush";
73533
73882
  }
73534
73883
 
73535
73884
  // telegraph.ts
@@ -74130,31 +74479,40 @@ async function sendReply(deps, req) {
74130
74479
  const replyThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
74131
74480
  const ownerTurn = resolveReplyOwnerTurn(turn, chat_id, args);
74132
74481
  const resolvedTurnId = ownerTurn?.turnId ?? null;
74133
- const decision = flushedTurnSupersede.take(chat_id, replyThreadId, { liveTurnId: resolvedTurnId, now: Date.now() });
74482
+ const decision = flushedTurnSupersede.take(chat_id, replyThreadId, { liveTurnId: resolvedTurnId, replyText: text4, now: Date.now() });
74134
74483
  if (decision.supersede) {
74135
74484
  process.stderr.write(`telegram gateway: reply: superseding flushed turn message(s) ` + `chatId=${chat_id} ids=${JSON.stringify(decision.deleteMessageIds)}
74136
74485
  `);
74137
74486
  supersedeFlushIds = decision.deleteMessageIds;
74138
- if (ownerTurn != null)
74139
- ownerTurn.answerDelivered = true;
74487
+ if (ownerTurn != null) {
74488
+ ownerTurn.answerDelivered = "flush";
74489
+ if (decision.recordText != null)
74490
+ ownerTurn.flushedAnswerText = decision.recordText;
74491
+ }
74140
74492
  } else {
74141
74493
  const replySubstantive = isSubstantiveFinalReply({
74142
74494
  text: rawText,
74143
74495
  disableNotification: args.disable_notification === true
74144
74496
  });
74497
+ const replyMatchesFlushedAnswer = decision.reason === "new-content" ? false : ownerTurn?.flushedAnswerText != null ? flushedAnswerMatchesReply2(ownerTurn.flushedAnswerText, text4) : null;
74145
74498
  const suppressByLatch = decideAnswerLatchSuppression({
74146
74499
  superseded: false,
74147
74500
  replySubstantive,
74148
74501
  isLateReply: turn == null,
74149
- ownerAnswerDelivered: ownerTurn?.answerDelivered ?? false
74502
+ ownerAnswerDelivered: ownerTurn?.answerDelivered ?? false,
74503
+ replyMatchesFlushedAnswer
74150
74504
  });
74505
+ if (decision.reason === "new-content") {
74506
+ process.stderr.write(`telegram gateway: reply: flush supersede declined \u2014 new content (#3429) ` + `chatId=${chat_id} ownerTurnId=${JSON.stringify(resolvedTurnId)}; sending fresh
74507
+ `);
74508
+ }
74151
74509
  if (suppressByLatch) {
74152
74510
  process.stderr.write(`telegram gateway: reply: suppressed by answer-delivered latch ` + `(flush already delivered this turn's answer) chatId=${chat_id} ` + `ownerTurnId=${JSON.stringify(resolvedTurnId)}
74153
74511
  `);
74154
74512
  return { content: [{ type: "text", text: "sent (deduped \u2014 answer already delivered via turn-flush)" }] };
74155
74513
  }
74156
74514
  if (replySubstantive && ownerTurn != null) {
74157
- ownerTurn.answerDelivered = true;
74515
+ ownerTurn.answerDelivered = "reply";
74158
74516
  }
74159
74517
  }
74160
74518
  }
@@ -76129,6 +76487,7 @@ function handleSessionEvent(deps, ev) {
76129
76487
  finalAnswerSubstantive: false,
76130
76488
  finalAnswerEverDelivered: false,
76131
76489
  answerDelivered: false,
76490
+ flushedAnswerText: null,
76132
76491
  endedAt: null,
76133
76492
  firstPingAt: null,
76134
76493
  firstPingWasSubstantive: false,
@@ -76228,7 +76587,7 @@ function handleSessionEvent(deps, ev) {
76228
76587
  if (turn != null) {
76229
76588
  turn.currentModel = ev.model;
76230
76589
  }
76231
- sessionModelSource.noteTranscriptModel(ev.model);
76590
+ sessionModelSource.noteTranscriptModel(ev.model, { replayed: ev.replayed === true });
76232
76591
  return;
76233
76592
  }
76234
76593
  case "usage": {
@@ -76634,7 +76993,8 @@ function handleSessionEvent(deps, ev) {
76634
76993
  }
76635
76994
  turn.finalAnswerDelivered = true;
76636
76995
  turn.finalAnswerSubstantive = true;
76637
- turn.answerDelivered = true;
76996
+ turn.answerDelivered = "flush";
76997
+ turn.flushedAnswerText = capturedText;
76638
76998
  const backstopLatchClaimed = backstopDeliveryLedger.claim(turn.turnId);
76639
76999
  const cardTakeover = progressDriver?.takeOverCard({
76640
77000
  chatId: backstopChatId,
@@ -76706,6 +77066,7 @@ function handleSessionEvent(deps, ev) {
76706
77066
  backstopCtrl.finalize("error");
76707
77067
  backstopDeliveryLedger.release(turn.turnId);
76708
77068
  turn.answerDelivered = false;
77069
+ turn.flushedAnswerText = null;
76709
77070
  } else if (backstopCtrl) {
76710
77071
  backstopCtrl.finalize("done");
76711
77072
  }
@@ -76723,6 +77084,7 @@ function handleSessionEvent(deps, ev) {
76723
77084
  `);
76724
77085
  if (!delivered) {
76725
77086
  turn.answerDelivered = false;
77087
+ turn.flushedAnswerText = null;
76726
77088
  backstopDeliveryLedger.release(turn.turnId);
76727
77089
  if (backstopCtrl)
76728
77090
  backstopCtrl.finalize("error");
@@ -79481,6 +79843,69 @@ function formatModelRelaunchSuppressNotAppliedLog(input) {
79481
79843
  return "telegram gateway: gw /model relaunch NOT-APPLIED \u2014 suppressing not-applied confirmation (a .session-model-alert is present and will be relayed) agent=" + input.agent + " target=" + input.target + `
79482
79844
  `;
79483
79845
  }
79846
+ function resolveModelSwitchBootNotice(input) {
79847
+ const { agent, confirmation, hasSessionModelAlert } = input;
79848
+ if (confirmation.kind === "not-applied" && hasSessionModelAlert) {
79849
+ return {
79850
+ kind: "suppress",
79851
+ log: formatModelRelaunchSuppressNotAppliedLog({ agent, target: confirmation.target })
79852
+ };
79853
+ }
79854
+ return { kind: "card", body: formatModelSwitchConfirmationBody(confirmation) };
79855
+ }
79856
+ function servedModelMatchesRequested(requested, served) {
79857
+ const req = requested.trim().toLowerCase();
79858
+ const srv = served.trim().toLowerCase();
79859
+ if (!srv.startsWith("claude-"))
79860
+ return true;
79861
+ if (MODEL_ALIASES2.includes(req)) {
79862
+ if (req === "default")
79863
+ return true;
79864
+ if (modelFamilyToken(srv) === req)
79865
+ return true;
79866
+ return srv.slice("claude-".length).split("-").includes(req);
79867
+ }
79868
+ if (req.startsWith("claude-")) {
79869
+ return srv === req || srv.startsWith(req + "-");
79870
+ }
79871
+ return true;
79872
+ }
79873
+ function formatServedModelDivergenceLog(input) {
79874
+ return "telegram gateway: gw /model served-model DIVERGENCE agent=" + input.agent + " requested=" + input.requested + " served=" + input.served + ` (--fallback-model substituted: requested id invalid/unknown OR model transiently unavailable)
79875
+ `;
79876
+ }
79877
+ function formatServedModelDivergenceCard(input) {
79878
+ return "\u26a0\ufe0f The first reply was served by `" + input.served + "`, not the requested `" + input.requested + "` \u2014 claude substituted the fallback model. Either the requested id is invalid/unknown, or the model was temporarily unavailable for that call (a transient substitution self-corrects on later replies). If it persists, re-issue `/model <valid id>` or `/model default`. `/status` always shows the model actually serving calls.";
79879
+ }
79880
+ function sendModelBootCard(deps, chat, body, failLabel) {
79881
+ deps.sendCard(chat.chatId, body, {
79882
+ parse_mode: "Markdown",
79883
+ ...chat.threadId != null ? { message_thread_id: chat.threadId } : {}
79884
+ }).catch((err) => deps.log(`telegram gateway: ${failLabel} send failed: ${err?.message ?? String(err)}
79885
+ `));
79886
+ }
79887
+ function buildServedModelDivergenceHandler(deps) {
79888
+ return (d) => {
79889
+ deps.log(formatServedModelDivergenceLog({ agent: deps.agent, requested: d.requested, served: d.served }));
79890
+ if (deps.chat == null)
79891
+ return;
79892
+ sendModelBootCard(deps, deps.chat, formatServedModelDivergenceCard(d), "served-model divergence");
79893
+ };
79894
+ }
79895
+ function deliverModelSwitchBootNotice(deps) {
79896
+ if (deps.chat == null)
79897
+ return;
79898
+ const notice = resolveModelSwitchBootNotice({
79899
+ agent: deps.agent,
79900
+ confirmation: deps.confirmation,
79901
+ hasSessionModelAlert: deps.hasSessionModelAlert
79902
+ });
79903
+ if (notice.kind === "suppress") {
79904
+ deps.log(notice.log);
79905
+ return;
79906
+ }
79907
+ sendModelBootCard(deps, deps.chat, notice.body, "model-switch confirmation");
79908
+ }
79484
79909
  function resolveStaleAwareBusy(input) {
79485
79910
  const turnStale = input.currentTurnActive && input.turnAgeMs !== null && input.turnAgeMs > input.hardTtlMs;
79486
79911
  const approvalLive = input.oldestPendingApprovalAgeMs !== null && input.oldestPendingApprovalAgeMs <= input.hardTtlMs;
@@ -79549,14 +79974,23 @@ function relaunchErrorReply2(deps, model, err) {
79549
79974
  const msg = err instanceof Error ? err.message : String(err);
79550
79975
  return { text: `\u274c Could not schedule model switch: ${deps.escapeHtml(msg)}`, html: true };
79551
79976
  }
79977
+ function unvalidatedIdCaveat2(deps, model) {
79978
+ if (!model.trim().toLowerCase().startsWith("claude-"))
79979
+ return null;
79980
+ return `_\`${deps.escapeHtml(model)}\` can't be validated before launch \u2014 if it isn't a real Claude model id, claude will silently serve the configured fallback model instead. I check the first reply and will warn if that happens._`;
79981
+ }
79552
79982
  async function scheduleRelaunchReply2(deps, model, reason) {
79553
79983
  try {
79554
79984
  await deps.scheduleModelRelaunch(model, reason);
79555
79985
  } catch (err) {
79556
79986
  return relaunchErrorReply2(deps, model, err);
79557
79987
  }
79558
- return { text: [switchingLine2(deps, model), PERSIST_NOTE3].join(`
79559
- `), html: true };
79988
+ const caveat = unvalidatedIdCaveat2(deps, model);
79989
+ return {
79990
+ text: [switchingLine2(deps, model), ...caveat ? [caveat] : [], PERSIST_NOTE3].join(`
79991
+ `),
79992
+ html: true
79993
+ };
79560
79994
  }
79561
79995
  async function scheduleDefaultRelaunchReply2(deps, reason) {
79562
79996
  try {
@@ -92701,10 +93135,10 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
92701
93135
  }
92702
93136
 
92703
93137
  // ../src/build-info.ts
92704
- var VERSION = "0.19.2";
92705
- var COMMIT_SHA = "1fa69736";
92706
- var COMMIT_DATE = "2026-07-19T09:11:44Z";
92707
- var LATEST_PR = 3425;
93138
+ var VERSION = "0.19.3";
93139
+ var COMMIT_SHA = "41896be4";
93140
+ var COMMIT_DATE = "2026-07-20T10:37:39+10:00";
93141
+ var LATEST_PR = 3456;
92708
93142
  var COMMITS_AHEAD_OF_TAG = 0;
92709
93143
 
92710
93144
  // gateway/boot-version.ts
@@ -95522,7 +95956,7 @@ var progressUpdateLastSent = new Map;
95522
95956
  var progressUpdateTurnCount = new Map;
95523
95957
  var currentTurn = null;
95524
95958
  var currentTurnMap = new CurrentTurnMap;
95525
- var sessionModelSource = createSessionModelSource();
95959
+ var sessionModelSource = createSessionModelSource({ servedMatchesRequested: servedModelMatchesRequested });
95526
95960
  var lastActiveTurnChatId;
95527
95961
  function setCurrentTurn(turn, key) {
95528
95962
  currentTurnMap.set(turn, key);
@@ -97314,9 +97748,9 @@ var pendingStateReaper = isGatewayMain ? setInterval(() => {
97314
97748
  if (now - v > 60000)
97315
97749
  lastAuthRefreshAtMs.delete(k);
97316
97750
  }
97317
- for (const [k, v] of pendingAuthRmFlows) {
97751
+ for (const [k, v] of pendingAuthRmFlows2) {
97318
97752
  if (now >= v.expiresAt)
97319
- pendingAuthRmFlows.delete(k);
97753
+ pendingAuthRmFlows2.delete(k);
97320
97754
  }
97321
97755
  pendingVaultOps.sweep(now);
97322
97756
  sweepPermissionTtl({
@@ -104160,7 +104594,7 @@ The gateway will restart as part of the recreate step; watch for the post-restar
104160
104594
  const me = cfg?.agents?.[getMyAgentName()];
104161
104595
  isAdmin2 = me?.admin === true || me?.root === true;
104162
104596
  } catch {}
104163
- if (!isAuthAdmin({ isAdmin: isAdmin2 })) {
104597
+ if (!isAuthAdmin2({ isAdmin: isAdmin2 })) {
104164
104598
  await switchroomReply(ctx, `**Not authorized.** \`/connect\` requires this agent to have \`admin: true\` in switchroom.yaml.`, { html: true });
104165
104599
  return;
104166
104600
  }
@@ -104251,52 +104685,19 @@ ${appNote}`), {
104251
104685
  } catch {}
104252
104686
  const chatId = String(ctx.chat?.id ?? "");
104253
104687
  if (parsed.kind === "add" || parsed.kind === "cancel") {
104254
- if (!isAuthAdmin({ isAdmin: isAdmin2 })) {
104255
- await switchroomReply(ctx, `**Not authorized.** \`/auth ${parsed.kind}\` is admin-only.
104256
- Set \`admin: true\` on this agent in switchroom.yaml to unlock (the same flag that gates \`/agents\`, \`/restart\`, \`/update\` etc.).`, { html: true });
104257
- return;
104258
- }
104259
- const authAddKey = chatKey2(chatId, ctx.message?.message_thread_id ?? null);
104260
- if (parsed.kind === "cancel") {
104261
- const existing = pendingAuthAddFlows.get(authAddKey);
104262
- if (!existing) {
104263
- await switchroomReply(ctx, "_No pending `/auth add` flow in this chat._", { html: true });
104264
- return;
104265
- }
104266
- cancelAccountAuthSession(existing);
104267
- pendingAuthAddFlows.delete(authAddKey);
104268
- await switchroomReply(ctx, "Cancelled.", { html: true });
104269
- return;
104270
- }
104271
- if (pendingAuthAddFlows.has(authAddKey)) {
104272
- await switchroomReply(ctx, "_An `/auth add` flow is already in progress for this chat. Finish the paste, or send `/auth cancel` to abort._", { html: true });
104273
- return;
104274
- }
104275
- try {
104276
- const { loginUrl, scratchDir, tmuxSocket, tmuxSession } = await startAccountAuthSession(parsed.label);
104277
- pendingAuthAddFlows.set(authAddKey, {
104278
- label: parsed.label,
104279
- scratchDir,
104280
- tmuxSocket,
104281
- tmuxSession,
104282
- startedAt: Date.now()
104283
- });
104284
- await switchroomReply(ctx, `**Adding account** \`${parsed.label}\`
104285
-
104286
- 1. Open this URL on your phone:
104287
- ${loginUrl}
104288
-
104289
- 2. Log into Anthropic, copy the code Claude shows.
104290
- 3. Paste it back here.
104291
-
104292
- Send \`/auth cancel\` to abort.`, { html: true });
104293
- } catch (err) {
104294
- await switchroomReply(ctx, `**/auth add failed:** ${escapeHtmlForTg2(err?.message ?? String(err))}`, { html: true });
104295
- }
104688
+ await handleAuthAddOrCancel({
104689
+ parsed,
104690
+ isAdmin: isAdmin2,
104691
+ currentAgent,
104692
+ chatId,
104693
+ threadId: ctx.message?.message_thread_id ?? null,
104694
+ reply: (text6) => switchroomReply(ctx, text6, { html: true }),
104695
+ escapeHtml: escapeHtmlForTg2
104696
+ });
104296
104697
  return;
104297
104698
  }
104298
104699
  if (parsed.kind === "provider-add" || parsed.kind === "provider-cancel") {
104299
- if (!isAuthAdmin({ isAdmin: isAdmin2 })) {
104700
+ if (!isAuthAdmin2({ isAdmin: isAdmin2 })) {
104300
104701
  await switchroomReply(ctx, `**Not authorized.** \`/auth ${parsed.provider}\` is admin-only.
104301
104702
  Set \`admin: true\` on this agent in switchroom.yaml to unlock.`, { html: true });
104302
104703
  return;
@@ -106080,7 +106481,16 @@ async function startGateway() {
106080
106481
  return resolveMainModel(raw ?? undefined);
106081
106482
  })();
106082
106483
  const isApplyBoot = launched.length > 0 && launched !== configured;
106083
- sessionModelSource.setOverride(isApplyBoot ? launched : null);
106484
+ sessionModelSource.setOverride(isApplyBoot ? launched : null, { verify: true });
106485
+ const modelBootCardDeps = {
106486
+ agent: getMyAgentName(),
106487
+ chat: modelSwitchMarkerChat,
106488
+ log: (line) => process.stderr.write(line),
106489
+ sendCard: (chatId, body, opts) => lockedBot.api.sendMessage(chatId, body, opts)
106490
+ };
106491
+ if (isApplyBoot) {
106492
+ sessionModelSource.setDivergenceHandler(buildServedModelDivergenceHandler(modelBootCardDeps));
106493
+ }
106084
106494
  const confirmation = modelSwitchReason != null ? classifyModelSwitchConfirmation({
106085
106495
  reason: modelSwitchReason,
106086
106496
  launched,
@@ -106093,22 +106503,12 @@ async function startGateway() {
106093
106503
  confirmation,
106094
106504
  isApplyBoot
106095
106505
  }));
106096
- if (confirmation != null && modelSwitchMarkerChat) {
106097
- const chat = modelSwitchMarkerChat;
106098
- const hasSessionModelAlert = existsSync54(join59(smAgentDir, ".session-model-alert"));
106099
- if (confirmation.kind === "not-applied" && hasSessionModelAlert) {
106100
- process.stderr.write(formatModelRelaunchSuppressNotAppliedLog({
106101
- agent: getMyAgentName(),
106102
- target: confirmation.target
106103
- }));
106104
- } else {
106105
- const body = formatModelSwitchConfirmationBody(confirmation);
106106
- lockedBot.api.sendMessage(chat.chatId, body, {
106107
- parse_mode: "Markdown",
106108
- ...chat.threadId != null ? { message_thread_id: chat.threadId } : {}
106109
- }).catch((err) => process.stderr.write(`telegram gateway: model-switch confirmation send failed: ${err?.message ?? String(err)}
106110
- `));
106111
- }
106506
+ if (confirmation != null) {
106507
+ deliverModelSwitchBootNotice({
106508
+ ...modelBootCardDeps,
106509
+ confirmation,
106510
+ hasSessionModelAlert: existsSync54(join59(smAgentDir, ".session-model-alert"))
106511
+ });
106112
106512
  }
106113
106513
  } catch {}
106114
106514
  }