switchroom 0.19.19 → 0.19.22

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 (53) hide show
  1. package/dist/auth-broker/index.js +53 -0
  2. package/dist/cli/switchroom.js +2444 -1264
  3. package/dist/host-control/main.js +54 -1
  4. package/dist/vault/approvals/kernel-server.js +53 -0
  5. package/dist/vault/broker/server.js +53 -0
  6. package/package.json +4 -2
  7. package/skills/switchroom-release/SKILL.md +103 -20
  8. package/telegram-plugin/card-format.ts +92 -3
  9. package/telegram-plugin/dist/gateway/gateway.js +769 -172
  10. package/telegram-plugin/edit-flood-fuse.ts +477 -0
  11. package/telegram-plugin/format.ts +19 -7
  12. package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
  13. package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
  14. package/telegram-plugin/gateway/gateway.ts +66 -56
  15. package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
  16. package/telegram-plugin/gateway/narrative-lane.ts +49 -3
  17. package/telegram-plugin/gateway/status-pin-api.ts +145 -0
  18. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +325 -45
  19. package/telegram-plugin/retry-api-call.ts +15 -2
  20. package/telegram-plugin/send-gate.ts +1 -1
  21. package/telegram-plugin/status-no-truncate.ts +64 -1
  22. package/telegram-plugin/status-pin-driver.ts +50 -27
  23. package/telegram-plugin/status-pin.ts +43 -5
  24. package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
  25. package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
  26. package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +101 -0
  27. package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
  28. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  29. package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
  30. package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
  31. package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
  32. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
  33. package/telegram-plugin/tests/status-pin.test.ts +106 -5
  34. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
  35. package/telegram-plugin/tests/tool-activity-summary.test.ts +19 -10
  36. package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
  37. package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
  38. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
  39. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
  40. package/telegram-plugin/tool-activity-summary.ts +85 -13
  41. package/telegram-plugin/worker-activity-feed.ts +5 -1
  42. package/vendor/hindsight-memory/scripts/drain_pending.py +193 -25
  43. package/vendor/hindsight-memory/scripts/lib/pending.py +84 -5
  44. package/vendor/hindsight-memory/scripts/lib/retain_split.py +21 -10
  45. package/vendor/hindsight-memory/scripts/recall.py +74 -5
  46. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +158 -4
  47. package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
  48. package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
  49. package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
  50. package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
  51. package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
  52. package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +19 -11
  53. package/vendor/hindsight-memory/tests/test_drain_pending.py +28 -2
@@ -7379,7 +7379,17 @@ var init_text_voice_scrub = __esm(() => {
7379
7379
  });
7380
7380
 
7381
7381
  // card-format.ts
7382
- function stackCardLines(lines) {
7382
+ function normalizeLineTail(line, collapseSafe) {
7383
+ let out = line.replace(/[ \t\r]+$/, "");
7384
+ if (!collapseSafe || COLLAPSE_SAFE_SEPARATOR.length === 0)
7385
+ return out;
7386
+ while (out.endsWith(COLLAPSE_SAFE_SEPARATOR)) {
7387
+ out = out.slice(0, -COLLAPSE_SAFE_SEPARATOR.length).replace(/[ \t\r]+$/, "");
7388
+ }
7389
+ return out;
7390
+ }
7391
+ function stackCardLines(lines, opts) {
7392
+ const collapseSafe = opts?.collapseSafe === true;
7383
7393
  const pieces = [];
7384
7394
  for (let i = 0;i < lines.length; i++) {
7385
7395
  const line = lines[i];
@@ -7389,11 +7399,14 @@ function stackCardLines(lines) {
7389
7399
  const cur = line.trim();
7390
7400
  const next = lines[i + 1].trim();
7391
7401
  if (cur === "" || next === "") {
7402
+ if (collapseSafe && cur !== "") {
7403
+ pieces[pieces.length - 1] = normalizeLineTail(line, true) + COLLAPSE_SAFE_SEPARATOR;
7404
+ }
7392
7405
  pieces.push(`
7393
7406
  `);
7394
7407
  continue;
7395
7408
  }
7396
- pieces[pieces.length - 1] = line.replace(/[ \t\r]+$/, "");
7409
+ pieces[pieces.length - 1] = normalizeLineTail(line, collapseSafe) + (collapseSafe ? COLLAPSE_SAFE_SEPARATOR : "");
7397
7410
  pieces.push(`
7398
7411
  `);
7399
7412
  }
@@ -7439,6 +7452,7 @@ function cleanWorkerResultParagraph(s) {
7439
7452
  }
7440
7453
  return kept.join(" ").replace(/\s+/g, " ").trim();
7441
7454
  }
7455
+ var COLLAPSE_SAFE_SEPARATOR = "\u00a0";
7442
7456
  var init_card_format = __esm(() => {
7443
7457
  init_format();
7444
7458
  init_text_voice_scrub();
@@ -35381,7 +35395,7 @@ function renderAuthLine(state7, agentName3, now = Date.now()) {
35381
35395
 
35382
35396
  // gateway/quota-cache.ts
35383
35397
  import { existsSync as existsSync45, readFileSync as readFileSync43, writeFileSync as writeFileSync36, mkdirSync as mkdirSync38 } from "fs";
35384
- import { join as join51, dirname as dirname17 } from "path";
35398
+ import { join as join51, dirname as dirname18 } from "path";
35385
35399
  function defaultCachePath() {
35386
35400
  return process.env.SWITCHROOM_QUOTA_CACHE_PATH ?? join51(process.env.HOME ?? "/tmp", ".switchroom", "quota-cache.json");
35387
35401
  }
@@ -35418,7 +35432,7 @@ function writeQuotaCache(result, opts = {}) {
35418
35432
  result
35419
35433
  };
35420
35434
  try {
35421
- mkdirSync38(dirname17(path3), { recursive: true });
35435
+ mkdirSync38(dirname18(path3), { recursive: true });
35422
35436
  writeFileSync36(path3, JSON.stringify(entry, null, 2), { mode: 384 });
35423
35437
  } catch {}
35424
35438
  }
@@ -36291,7 +36305,7 @@ var init_boot_probes = __esm(() => {
36291
36305
 
36292
36306
  // gateway/boot-issue-cache.ts
36293
36307
  import { existsSync as existsSync47, readFileSync as readFileSync45, writeFileSync as writeFileSync37, mkdirSync as mkdirSync39, renameSync as renameSync21 } from "fs";
36294
- import { dirname as dirname18 } from "path";
36308
+ import { dirname as dirname19 } from "path";
36295
36309
  function fingerprintProbe(key, r) {
36296
36310
  if (r.status === "ok")
36297
36311
  return `${key}:ok`;
@@ -36417,7 +36431,7 @@ function applyAndSave(path3, cache, diff) {
36417
36431
  }
36418
36432
  }
36419
36433
  try {
36420
- mkdirSync39(dirname18(path3), { recursive: true });
36434
+ mkdirSync39(dirname19(path3), { recursive: true });
36421
36435
  const tmp = `${path3}.tmp`;
36422
36436
  writeFileSync37(tmp, JSON.stringify(next), { mode: 384 });
36423
36437
  renameSync21(tmp, path3);
@@ -36434,7 +36448,7 @@ var init_boot_issue_cache = __esm(() => {
36434
36448
  // gateway/config-snapshot.ts
36435
36449
  import { createHash as createHash6 } from "crypto";
36436
36450
  import { existsSync as existsSync48, readFileSync as readFileSync46, writeFileSync as writeFileSync38, mkdirSync as mkdirSync40, renameSync as renameSync22 } from "fs";
36437
- import { dirname as dirname19 } from "path";
36451
+ import { dirname as dirname20 } from "path";
36438
36452
  function hashStringArray(items) {
36439
36453
  if (!items || items.length === 0)
36440
36454
  return null;
@@ -36532,7 +36546,7 @@ function loadSnapshot(path3, now = Date.now) {
36532
36546
  }
36533
36547
  function persistSnapshot(path3, snapshot) {
36534
36548
  try {
36535
- mkdirSync40(dirname19(path3), { recursive: true });
36549
+ mkdirSync40(dirname20(path3), { recursive: true });
36536
36550
  const tmp = `${path3}.tmp`;
36537
36551
  writeFileSync38(tmp, JSON.stringify(snapshot), { mode: 384 });
36538
36552
  renameSync22(tmp, path3);
@@ -38714,7 +38728,7 @@ import {
38714
38728
  unlinkSync as unlinkSync28,
38715
38729
  appendFileSync as appendFileSync9
38716
38730
  } from "fs";
38717
- import { homedir as homedir20 } from "os";
38731
+ import { homedir as homedir19 } from "os";
38718
38732
  import { join as join65, sep as sep4, basename as basename17 } from "path";
38719
38733
 
38720
38734
  // plugin-logger.ts
@@ -43330,13 +43344,27 @@ async function interceptVault(p, deps) {
43330
43344
  deps.vaultPassphraseCache.set(p.chat_id, { passphrase, expiresAt: Date.now() + deps.vaultPassphraseTtlMs });
43331
43345
  if (p.msgId != null)
43332
43346
  await deps.deleteSensitiveMessage(p.chat_id, p.msgId, "vault passphrase");
43347
+ const mismatched = [];
43348
+ let mismatchMsg = "";
43333
43349
  for (const item of pendingVault.items) {
43334
43350
  const stagedAccess = deps.pendingVaultRequestAccesses.get(item.stageId);
43335
43351
  if (!stagedAccess) {
43336
43352
  await p.ctx.api.editMessageText(item.cardChatId, item.cardMessageId, richMessage(`\u231b _Vault unlocked, but this access-request card expired or was denied before you replied. Ask the agent to re-issue if still needed._`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
43337
43353
  continue;
43338
43354
  }
43339
- await deps.callbackQueryHandlers().performVaultAccessApproval(p.ctx, stagedAccess, item.stageId, item.senderId, { kind: "passphrase", passphrase });
43355
+ const outcome = await deps.callbackQueryHandlers().performVaultAccessApproval(p.ctx, stagedAccess, item.stageId, item.senderId, { kind: "passphrase", passphrase });
43356
+ if (outcome?.kind === "passphrase-mismatch") {
43357
+ mismatched.push(item);
43358
+ mismatchMsg = outcome.msg;
43359
+ }
43360
+ }
43361
+ if (mismatched.length > 0) {
43362
+ await deps.callbackQueryHandlers().resolveAccessApprovalPassphraseMismatch(p.ctx, {
43363
+ chat_id: p.chat_id,
43364
+ failed: mismatched,
43365
+ priorAttempts: pendingVault.attempts ?? 0,
43366
+ brokerMsg: mismatchMsg
43367
+ });
43340
43368
  }
43341
43369
  } else if (pendingVault.kind === "grant-wizard" && pendingVault.awaitingCustomDuration) {
43342
43370
  const input = p.text.trim();
@@ -43965,13 +43993,27 @@ async function interceptVault2(p, deps) {
43965
43993
  deps.vaultPassphraseCache.set(p.chat_id, { passphrase, expiresAt: Date.now() + deps.vaultPassphraseTtlMs });
43966
43994
  if (p.msgId != null)
43967
43995
  await deps.deleteSensitiveMessage(p.chat_id, p.msgId, "vault passphrase");
43996
+ const mismatched = [];
43997
+ let mismatchMsg = "";
43968
43998
  for (const item of pendingVault.items) {
43969
43999
  const stagedAccess = deps.pendingVaultRequestAccesses.get(item.stageId);
43970
44000
  if (!stagedAccess) {
43971
44001
  await p.ctx.api.editMessageText(item.cardChatId, item.cardMessageId, richMessage(`\u231b _Vault unlocked, but this access-request card expired or was denied before you replied. Ask the agent to re-issue if still needed._`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
43972
44002
  continue;
43973
44003
  }
43974
- await deps.callbackQueryHandlers().performVaultAccessApproval(p.ctx, stagedAccess, item.stageId, item.senderId, { kind: "passphrase", passphrase });
44004
+ const outcome = await deps.callbackQueryHandlers().performVaultAccessApproval(p.ctx, stagedAccess, item.stageId, item.senderId, { kind: "passphrase", passphrase });
44005
+ if (outcome?.kind === "passphrase-mismatch") {
44006
+ mismatched.push(item);
44007
+ mismatchMsg = outcome.msg;
44008
+ }
44009
+ }
44010
+ if (mismatched.length > 0) {
44011
+ await deps.callbackQueryHandlers().resolveAccessApprovalPassphraseMismatch(p.ctx, {
44012
+ chat_id: p.chat_id,
44013
+ failed: mismatched,
44014
+ priorAttempts: pendingVault.attempts ?? 0,
44015
+ brokerMsg: mismatchMsg
44016
+ });
43975
44017
  }
43976
44018
  } else if (pendingVault.kind === "grant-wizard" && pendingVault.awaitingCustomDuration) {
43977
44019
  const input = p.text.trim();
@@ -46435,6 +46477,7 @@ var WORKER_HISTORY_MAX = 6;
46435
46477
  var STATUS_LINE_MAX = 200;
46436
46478
  var STATUS_CARD_CHAR_BUDGET = RICH_MESSAGE_MAX_CHARS;
46437
46479
  var NESTED_PREFIX = " \u21b3 ";
46480
+ var WORKER_STEP_INDENT = "\u2800\u2800\u2800";
46438
46481
 
46439
46482
  // hooks/tool-label-pretool.mjs
46440
46483
  import { readFileSync as readFileSync9, mkdirSync as mkdirSync10, appendFileSync as appendFileSync2, existsSync as existsSync9 } from "node:fs";
@@ -46858,16 +46901,16 @@ function escapeStepLine(raw) {
46858
46901
  const cleaned = stripMarkdown(raw).replace(/\s+/g, " ").trim();
46859
46902
  return escapeMarkdown(truncate(cleaned, STATUS_LINE_MAX));
46860
46903
  }
46861
- function renderStepFeed(out, steps, allDone, liveSuffix = "", window2 = STATUS_ROLLING_LINES) {
46904
+ function renderStepFeed(out, steps, allDone, liveSuffix = "", window2 = STATUS_ROLLING_LINES, indent = "") {
46862
46905
  if (steps.length === 0)
46863
46906
  return;
46864
46907
  const shown = steps.slice(-Math.max(1, window2));
46865
46908
  const hidden = steps.length - shown.length;
46866
46909
  if (hidden > 0)
46867
- out.push(`_\u2713 +${hidden} earlier\u2026_`);
46910
+ out.push(`${indent}_\u2713 +${hidden} earlier\u2026_`);
46868
46911
  const lastIdx = shown.length - 1;
46869
46912
  shown.forEach((s, i) => {
46870
- out.push(!allDone && i === lastIdx ? `**\u2192 ${s}${liveSuffix}**` : `~~_\u2713 ${s}_~~`);
46913
+ out.push(!allDone && i === lastIdx ? `${indent}**\u2192 ${s}${liveSuffix}**` : `${indent}~~_\u2713 ${s}_~~`);
46871
46914
  });
46872
46915
  }
46873
46916
  function renderStatusCard(opts) {
@@ -46907,7 +46950,7 @@ function renderStatusCard(opts) {
46907
46950
  }
46908
46951
  if (out.length === 0)
46909
46952
  return null;
46910
- const joined = stackCardLines(out);
46953
+ const joined = stackCardLines(out, { collapseSafe: true });
46911
46954
  if (joined.length <= STATUS_CARD_CHAR_BUDGET)
46912
46955
  return joined;
46913
46956
  return fitCardToBudget(opts, headerLines);
@@ -46942,7 +46985,7 @@ function fitCardToBudget(opts, headerLines) {
46942
46985
  const lastIdx = shown.length - 1;
46943
46986
  shown.forEach((esc, i) => lines2.push(buildBullet(esc, i === lastIdx)));
46944
46987
  lines2.push(...footerLines);
46945
- const candidate = stackCardLines(lines2);
46988
+ const candidate = stackCardLines(lines2, { collapseSafe: true });
46946
46989
  if (candidate.length <= STATUS_CARD_CHAR_BUDGET)
46947
46990
  return candidate;
46948
46991
  }
@@ -46963,7 +47006,7 @@ function fitCardToBudget(opts, headerLines) {
46963
47006
  lines.push(parentMarker);
46964
47007
  lines.push(newestLine);
46965
47008
  lines.push(...footerLines);
46966
- return stackCardLines(lines);
47009
+ return stackCardLines(lines, { collapseSafe: true });
46967
47010
  }
46968
47011
  function renderActivityFeed(lines, final = false, liveSuffix = "", stepCount, header) {
46969
47012
  if (lines.length === 0 && header == null)
@@ -47016,6 +47059,13 @@ function combinedHistoryDepth(w) {
47016
47059
  return Math.min(WORKER_HISTORY_MAX, Math.max(MIN_WORKER_DEPTH, 7 - w));
47017
47060
  }
47018
47061
  var workerHistoryDepth = combinedHistoryDepth;
47062
+ function glanceLine(rows) {
47063
+ const oldestMs = rows.reduce((m, r) => Math.max(m, r.elapsedMs), 0);
47064
+ const tools = rows.reduce((n, r) => n + r.toolCount, 0);
47065
+ const tok = rows.reduce((n, r) => n + (r.totalTokens ?? 0), 0);
47066
+ const toolWord = tools === 1 ? "tool" : "tools";
47067
+ return `\uD83D\uDEE0 **Workers** \u00b7 _${rows.length} running \u00b7 oldest ${formatFeedElapsed(oldestMs)}` + ` \u00b7 ${tools} ${toolWord}${tokenSegment(tok)}_`;
47068
+ }
47019
47069
  function renderCombinedWorkerFeed(rows, opts) {
47020
47070
  if (rows.length === 0)
47021
47071
  return null;
@@ -47038,22 +47088,22 @@ function renderCombinedWorkerFeed(rows, opts) {
47038
47088
  const shown = rows.slice(0, visibleCount);
47039
47089
  const hidden = rows.length - shown.length;
47040
47090
  const depth = combinedHistoryDepth(shown.length);
47041
- const chrome = [`\uD83D\uDEE0 **Workers** \u00b7 _${rows.length} running_`];
47091
+ const chrome = [glanceLine(rows)];
47042
47092
  const bodyOut = [];
47043
47093
  for (const r of shown) {
47044
47094
  bodyOut.push(rowHeader(r));
47045
47095
  const hist = rowHistory(r);
47046
47096
  if (hist.length === 0) {
47047
- bodyOut.push("\u2192 _starting\u2026_");
47097
+ bodyOut.push(`${WORKER_STEP_INDENT}\u2192 _starting\u2026_`);
47048
47098
  continue;
47049
47099
  }
47050
47100
  const esc = hist.slice(-depth).map(escapeStepLine);
47051
- renderStepFeed(bodyOut, esc, false, "", depth);
47101
+ renderStepFeed(bodyOut, esc, false, "", depth, WORKER_STEP_INDENT);
47052
47102
  }
47053
47103
  const out = [...chrome, ...bodyOut];
47054
47104
  if (hidden > 0)
47055
47105
  out.push(`_+${hidden} more working\u2026_`);
47056
- return { body: stackCardLines(out), bodyLines: bodyOut.length };
47106
+ return { body: stackCardLines(out, { collapseSafe: true }), bodyLines: bodyOut.length };
47057
47107
  };
47058
47108
  let visible = Math.min(rows.length, maxRows);
47059
47109
  let { body, bodyLines } = compose(visible);
@@ -47074,6 +47124,10 @@ function appendActivityLabel(lines, label) {
47074
47124
  }
47075
47125
 
47076
47126
  // send-gate.ts
47127
+ var systemClock = {
47128
+ now: () => Date.now(),
47129
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms))
47130
+ };
47077
47131
  var SEND_GATE_SHED = Symbol.for("switchroom.send-gate.shed");
47078
47132
  function isSendGateShed(value) {
47079
47133
  return value === SEND_GATE_SHED;
@@ -47131,7 +47185,7 @@ function renderWorkerActivity(v, liveSuffix = "") {
47131
47185
  return `\uD83D\uDEE0 **Worker** \u00b7 _starting\u2026_`;
47132
47186
  }
47133
47187
  if (!finished && steps.length === 0) {
47134
- return `${card}
47188
+ return `${card}${COLLAPSE_SAFE_SEPARATOR}
47135
47189
  _starting\u2026_`;
47136
47190
  }
47137
47191
  return card;
@@ -48032,6 +48086,16 @@ function errorDescription(err) {
48032
48086
  function isPinRightsError(err) {
48033
48087
  return errorDescription(err).includes("not enough rights");
48034
48088
  }
48089
+ function isUnpinTerminalError(err) {
48090
+ if (isPinRightsError(err))
48091
+ return true;
48092
+ if (err != null && typeof err === "object") {
48093
+ const code = err.error_code;
48094
+ if (typeof code === "number")
48095
+ return code >= 400 && code < 500 && code !== 429;
48096
+ }
48097
+ return false;
48098
+ }
48035
48099
 
48036
48100
  class PinRightsCache {
48037
48101
  blocked = new Set;
@@ -48055,18 +48119,20 @@ async function reconcilePin(args) {
48055
48119
  if (action.kind === "noop")
48056
48120
  return args.prevState;
48057
48121
  if (action.kind === "unpin") {
48058
- if (!args.rightsCache?.isBlocked(args.chatId)) {
48059
- try {
48060
- await args.api.unpinChatMessage(args.chatId, action.messageId);
48061
- } catch (err) {
48062
- if (args.rightsCache && isPinRightsError(err)) {
48063
- const firstTime = args.rightsCache.block(args.chatId);
48064
- if (firstTime)
48065
- args.onPinRightsDisabled?.(args.chatId);
48066
- } else {
48067
- args.onError?.("unpin", err);
48068
- }
48122
+ if (args.rightsCache?.isBlocked(args.chatId))
48123
+ return null;
48124
+ try {
48125
+ await args.api.unpinChatMessage(args.chatId, action.messageId);
48126
+ } catch (err) {
48127
+ if (args.rightsCache && isPinRightsError(err)) {
48128
+ const firstTime = args.rightsCache.block(args.chatId);
48129
+ if (firstTime)
48130
+ args.onPinRightsDisabled?.(args.chatId);
48131
+ } else {
48132
+ args.onError?.("unpin", err);
48069
48133
  }
48134
+ if (!isUnpinTerminalError(err))
48135
+ return args.prevState;
48070
48136
  }
48071
48137
  return null;
48072
48138
  }
@@ -48498,8 +48564,7 @@ init_rich_send();
48498
48564
  var import_grammy4 = __toESM(require_mod2(), 1);
48499
48565
  import { execFileSync as execFileSync3 } from "child_process";
48500
48566
  import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync9 } from "fs";
48501
- import { homedir as homedir5 } from "os";
48502
- import { join as join13 } from "path";
48567
+ import { dirname as dirname5 } from "path";
48503
48568
  init_client();
48504
48569
 
48505
48570
  // gateway/vault-grant-inbound-builders.ts
@@ -49532,6 +49597,48 @@ var _deprecationSeen = new Set;
49532
49597
 
49533
49598
  // gateway/callback-query-handlers.ts
49534
49599
  var AUTH_REFRESH_THROTTLE_MS = 5000;
49600
+ var MAX_VAULT_PASSPHRASE_ATTEMPTS = 3;
49601
+ function isPassphraseMismatchBrokerError(msg) {
49602
+ const m = msg.toLowerCase();
49603
+ if (!m.includes("passphrase"))
49604
+ return false;
49605
+ return m.includes("does not match") || m.includes("mismatch");
49606
+ }
49607
+ function buildAccessPassphrasePromptText(opts) {
49608
+ const header = `**\uD83D\uDEA8\uD83D\uDD10 ACTION NEEDED: passphrase required**`;
49609
+ if (opts.kind === "retry") {
49610
+ const plural = opts.retryRemaining === 1 ? "attempt" : "attempts";
49611
+ return `${header}
49612
+
49613
+ ` + `Wrong passphrase. ${opts.retryRemaining} ${plural} remaining.
49614
+ ` + `Type your vault passphrase again as your **next message**.
49615
+ ` + (opts.itemCount > 1 ? `One entry covers **${opts.itemCount}** pending approvals in this chat.
49616
+ ` : ``) + `
49617
+ _We delete the passphrase message the moment we read it._`;
49618
+ }
49619
+ if (opts.variant === "batch") {
49620
+ return `${header}
49621
+
49622
+ ` + `Type your vault passphrase as your **next message**.
49623
+ ` + `One entry covers **${opts.itemCount}** pending approvals in this chat, no re-type per card.
49624
+
49625
+ ` + `_We delete the passphrase message the moment we read it._`;
49626
+ }
49627
+ if (opts.variant === "admin-only") {
49628
+ return `${header}
49629
+
49630
+ ` + `\`${opts.key}\` is an **admin-only credential**.
49631
+ ` + `Type your vault passphrase as your **next message** to mint the grant for **${opts.agentEscaped}**.
49632
+
49633
+ ` + `_The passphrase is what proves it's you. An agent can never mint this key on its own. We delete the passphrase message the moment we read it._`;
49634
+ }
49635
+ return `${header}
49636
+
49637
+ ` + `Your vault is locked.
49638
+ ` + `Reply with your passphrase as your **next message** to unlock and mint the grant for **${opts.agentEscaped}**.
49639
+
49640
+ ` + `_Mint authority stays operator-only: the broker only accepts the grant when the passphrase matches. We delete the passphrase message the moment we read it._`;
49641
+ }
49535
49642
  function createCallbackQueryHandlers(deps) {
49536
49643
  const {
49537
49644
  loadAccess,
@@ -49574,6 +49681,10 @@ function createCallbackQueryHandlers(deps) {
49574
49681
  } = deps;
49575
49682
  const bot = deps.bot;
49576
49683
  const lockedBot = deps.lockedBot;
49684
+ const mintGrantViaBroker2 = deps.brokerMintGrant ?? mintGrantViaBroker;
49685
+ const listViaBroker2 = deps.brokerList ?? listViaBroker;
49686
+ const listGrantsViaBroker2 = deps.brokerListGrants ?? listGrantsViaBroker;
49687
+ const vaultTokenFilePath2 = deps.brokerVaultTokenFilePath ?? vaultTokenFilePath;
49577
49688
  async function handleVaultRecentDenialCallback(ctx, data) {
49578
49689
  const senderId = String(ctx.from?.id ?? "");
49579
49690
  const access = loadAccess();
@@ -49596,7 +49707,7 @@ function createCallbackQueryHandlers(deps) {
49596
49707
  return;
49597
49708
  }
49598
49709
  await ctx.answerCallbackQuery({ text: "\u23f3 Minting 30-day read grant\u2026" }).catch(() => {});
49599
- const result = await mintGrantViaBroker({
49710
+ const result = await mintGrantViaBroker2({
49600
49711
  agent: agentName,
49601
49712
  keys: [keyName],
49602
49713
  ttl_seconds: 30 * 24 * 60 * 60,
@@ -49611,9 +49722,9 @@ function createCallbackQueryHandlers(deps) {
49611
49722
  return;
49612
49723
  }
49613
49724
  const { token, id } = result;
49614
- const tokenPath = join13(homedir5(), ".switchroom", "agents", agentName, ".vault-token");
49725
+ const tokenPath = vaultTokenFilePath2(agentName);
49615
49726
  try {
49616
- mkdirSync13(join13(homedir5(), ".switchroom", "agents", agentName), { recursive: true });
49727
+ mkdirSync13(dirname5(tokenPath), { recursive: true });
49617
49728
  writeFileSync9(tokenPath, token, { mode: 384 });
49618
49729
  } catch (err) {
49619
49730
  await switchroomReply(ctx, `**Grant created (${escapeHtmlForTg2(id)}) but token write failed:** ` + `${escapeHtmlForTg2(String(err))}
@@ -49630,25 +49741,93 @@ function createCallbackQueryHandlers(deps) {
49630
49741
  newText: baseText ? `${baseText}${statusLine}` : statusLine
49631
49742
  });
49632
49743
  }
49744
+ async function editResolvedCard(ctx, target, messageId, markdown, label) {
49745
+ if (messageId > 0) {
49746
+ try {
49747
+ await robustApiCall(() => ctx.api.editMessageText(target.chat_id, messageId, richMessage(markdown), {
49748
+ reply_markup: { inline_keyboard: [] }
49749
+ }), { chat_id: target.chat_id, verb: `vault_request_access.${label}_edit` });
49750
+ return;
49751
+ } catch (err) {
49752
+ process.stderr.write(`telegram gateway: vault card resolution edit FAILED (${label}) ` + `chat=${target.chat_id} msg=${messageId}: ${String(err)} \u2014 sending fallback message
49753
+ `);
49754
+ }
49755
+ }
49756
+ try {
49757
+ await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendRichMessage(target.chat_id, richMessage(markdown), {
49758
+ ...tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}
49759
+ }), {
49760
+ threadId: target.threadId,
49761
+ chat_id: target.chat_id,
49762
+ verb: `vault_request_access.${label}_fallback`
49763
+ });
49764
+ } catch (err) {
49765
+ process.stderr.write(`telegram gateway: vault card resolution FALLBACK SEND failed (${label}) ` + `chat=${target.chat_id}: ${String(err)}
49766
+ `);
49767
+ }
49768
+ }
49769
+ async function sendAccessPassphrasePrompt(target, spec) {
49770
+ const promptText = buildAccessPassphrasePromptText(spec);
49771
+ await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendRichMessage(target.chat_id, richMessage(promptText), {
49772
+ ...tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}
49773
+ }), {
49774
+ threadId: target.threadId,
49775
+ chat_id: target.chat_id,
49776
+ verb: "vault_request_access.passphrase_prompt"
49777
+ }).catch((err) => {
49778
+ process.stderr.write(`telegram gateway: vault passphrase prompt send FAILED chat=${target.chat_id} ` + `kind=${spec.kind}: ${String(err)}
49779
+ `);
49780
+ });
49781
+ }
49782
+ async function resolveAccessApprovalPassphraseMismatch(ctx, args) {
49783
+ const { chat_id, failed, priorAttempts, brokerMsg } = args;
49784
+ if (failed.length === 0)
49785
+ return;
49786
+ vaultPassphraseCache.delete(chat_id);
49787
+ const attempts = priorAttempts + 1;
49788
+ const remaining = MAX_VAULT_PASSPHRASE_ATTEMPTS - attempts;
49789
+ const threadId = failed.find((it) => it.threadId != null)?.threadId;
49790
+ process.stderr.write(`telegram gateway: vault_request_access passphrase mismatch chat=${chat_id} ` + `stages=${failed.map((f) => f.stageId).join(",")} attempts=${attempts}/${MAX_VAULT_PASSPHRASE_ATTEMPTS}
49791
+ `);
49792
+ if (remaining > 0) {
49793
+ const open = pendingVaultOps.get(chat_id);
49794
+ const carried = open?.kind === "passphrase-for-access-approve" ? open.items.filter((it) => !failed.some((f) => f.stageId === it.stageId)) : [];
49795
+ const requeued = [...failed, ...carried];
49796
+ pendingVaultOps.set(chat_id, {
49797
+ kind: "passphrase-for-access-approve",
49798
+ items: requeued,
49799
+ attempts,
49800
+ startedAt: Date.now()
49801
+ });
49802
+ await sendAccessPassphrasePrompt({ chat_id, ...threadId != null ? { threadId } : {} }, { kind: "retry", retryRemaining: remaining, itemCount: requeued.length });
49803
+ return;
49804
+ }
49805
+ for (const item of failed) {
49806
+ pendingVaultRequestAccesses.delete(item.stageId);
49807
+ pendingCardStore.remove(item.stageId);
49808
+ await editResolvedCard(ctx, { chat_id: item.cardChatId, ...item.threadId != null ? { threadId: item.threadId } : {} }, item.cardMessageId, `\u274c **Too many wrong passphrase attempts** (${MAX_VAULT_PASSPHRASE_ATTEMPTS}). ` + `This request was cancelled \u2014 ask the agent to re-issue it.
49809
+ ` + `_Broker: ${escapeHtmlForTg2(brokerMsg)}_`, "passphrase_lockout");
49810
+ }
49811
+ }
49633
49812
  async function performVaultAccessApproval(ctx, pending, stageId, senderId, attestation) {
49634
49813
  const brokerAuthOpts = attestation.kind === "passphrase" ? { passphrase: attestation.passphrase } : { attest_via_posture: true };
49635
49814
  if (pending.scope === "read") {
49636
49815
  try {
49637
- const visible = await listViaBroker();
49816
+ const visible = await listViaBroker2();
49638
49817
  if (visible !== null && visible.includes(pending.key)) {
49639
49818
  pendingVaultRequestAccesses.delete(stageId);
49640
49819
  pendingCardStore.remove(stageId);
49641
49820
  if (pending.card_message_id != null) {
49642
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\u2139\ufe0f **${escapeHtmlForTg2(pending.agent)}** already has standing-ACL access to ` + `\`${pending.key}\` (schedule.secrets[]). ` + `**No grant minted** \u2014 a token would shadow the standing ACL. ` + richMessage(`The agent can read it directly.`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
49821
+ await editResolvedCard(ctx, pending, pending.card_message_id, `\u2139\ufe0f **${escapeHtmlForTg2(pending.agent)}** already has standing-ACL access to ` + `\`${pending.key}\` (schedule.secrets[]). ` + `**No grant minted** \u2014 a token would shadow the standing ACL. ` + `The agent can read it directly.`, "standing_acl");
49643
49822
  }
49644
- return;
49823
+ return { kind: "ok" };
49645
49824
  }
49646
49825
  } catch {}
49647
49826
  }
49648
49827
  let existingReadKeys = [];
49649
49828
  let existingWriteKeys = [];
49650
49829
  if (pending.scope === "read" || pending.scope === "write") {
49651
- const list = await listGrantsViaBroker(pending.agent, brokerAuthOpts);
49830
+ const list = await listGrantsViaBroker2(pending.agent, brokerAuthOpts);
49652
49831
  if (list.kind === "ok") {
49653
49832
  const now = Math.floor(Date.now() / 1000);
49654
49833
  const active = list.grants.filter((g) => g.expires_at === null || g.expires_at > now).sort((a, b) => {
@@ -49677,28 +49856,31 @@ function createCallbackQueryHandlers(deps) {
49677
49856
  ...writeKeys.size > 0 ? { write_keys: Array.from(writeKeys) } : {},
49678
49857
  ...brokerAuthOpts
49679
49858
  };
49680
- const result = await mintGrantViaBroker(mintArgs);
49859
+ const result = await mintGrantViaBroker2(mintArgs);
49681
49860
  if (result.kind === "unreachable") {
49682
49861
  await switchroomReply(ctx, `\uD83D\uDD34 Broker unreachable: ${escapeHtmlForTg2(result.msg)}`, { html: true });
49683
- return;
49862
+ return { kind: "failed", msg: result.msg };
49684
49863
  }
49685
49864
  if (result.kind === "error") {
49865
+ if (attestation.kind === "passphrase" && isPassphraseMismatchBrokerError(result.msg)) {
49866
+ return { kind: "passphrase-mismatch", msg: result.msg };
49867
+ }
49686
49868
  pendingVaultRequestAccesses.delete(stageId);
49687
49869
  pendingCardStore.remove(stageId);
49688
49870
  if (pending.card_message_id != null) {
49689
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, richMessage(`**mint_grant failed:** ${escapeHtmlForTg2(result.msg)}`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
49871
+ await editResolvedCard(ctx, pending, pending.card_message_id, `**mint_grant failed:** ${escapeHtmlForTg2(result.msg)}`, "mint_failed");
49690
49872
  }
49691
- return;
49873
+ return { kind: "failed", msg: result.msg };
49692
49874
  }
49693
49875
  const { token, id } = result;
49694
- const tokenPath = join13(homedir5(), ".switchroom", "agents", pending.agent, ".vault-token");
49876
+ const tokenPath = vaultTokenFilePath2(pending.agent);
49695
49877
  try {
49696
- mkdirSync13(join13(homedir5(), ".switchroom", "agents", pending.agent), { recursive: true });
49878
+ mkdirSync13(dirname5(tokenPath), { recursive: true });
49697
49879
  writeFileSync9(tokenPath, token, { mode: 384 });
49698
49880
  } catch (err) {
49699
49881
  await switchroomReply(ctx, `**Grant created (${escapeHtmlForTg2(id)}) but token write failed:** ` + `${escapeHtmlForTg2(String(err))}
49700
49882
  ` + `_Recover with: \`switchroom vault grant ${escapeHtmlForTg2(pending.agent)} ` + `--keys ${escapeHtmlForTg2(pending.key)} --duration ${Math.round(pending.ttl_seconds / 86400)}d\` on the host._`, { html: true });
49701
- return;
49883
+ return { kind: "failed", msg: String(err) };
49702
49884
  }
49703
49885
  pendingVaultRequestAccesses.delete(stageId);
49704
49886
  pendingCardStore.remove(stageId);
@@ -49707,7 +49889,7 @@ function createCallbackQueryHandlers(deps) {
49707
49889
  const reasonNormalized = normalizeGrantReason(pending.reason);
49708
49890
  const footer = getVaultApprovalAuthMode() === "telegram-id" ? `
49709
49891
  _Approver verified by Telegram identity \u2014 broker auto-unlocked at startup._` : "";
49710
- await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, richMessage(buildVaultGrantApprovedCardText({
49892
+ await editResolvedCard(ctx, pending, pending.card_message_id, buildVaultGrantApprovedCardText({
49711
49893
  agentEscaped: escapeHtmlForTg2(pending.agent),
49712
49894
  scope: pending.scope,
49713
49895
  key: pending.key,
@@ -49715,7 +49897,7 @@ _Approver verified by Telegram identity \u2014 broker auto-unlocked at startup._
49715
49897
  grantId: id,
49716
49898
  reasonEscaped: reasonNormalized.length > 0 ? escapeHtmlForTg2(reasonNormalized) : undefined,
49717
49899
  footer
49718
- })), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
49900
+ }), "grant_approved");
49719
49901
  }
49720
49902
  const synthetic = buildVaultGrantApprovedInbound({
49721
49903
  ctx: {
@@ -49733,6 +49915,7 @@ _Approver verified by Telegram identity \u2014 broker auto-unlocked at startup._
49733
49915
  const delivered = deliverResumeSyntheticOrBuffer(pending.agent, synthetic);
49734
49916
  process.stderr.write(`telegram gateway: vault_grant_approved injection agent=${pending.agent} ` + `key=${pending.key} stage=${stageId} delivered=${delivered}
49735
49917
  `);
49918
+ return { kind: "ok" };
49736
49919
  }
49737
49920
  async function handleSkillProposalCallback(ctx, data) {
49738
49921
  const senderId = String(ctx.from?.id ?? "");
@@ -50025,40 +50208,53 @@ _Approver verified by Telegram identity \u2014 broker auto-unlocked at startup._
50025
50208
  stageId,
50026
50209
  cardChatId: pending.chat_id,
50027
50210
  cardMessageId: pending.card_message_id,
50028
- senderId
50211
+ senderId,
50212
+ ...pending.threadId != null ? { threadId: pending.threadId } : {}
50029
50213
  };
50030
50214
  const items = existing?.kind === "passphrase-for-access-approve" ? [...existing.items.filter((it) => it.stageId !== stageId), newItem] : [newItem];
50031
50215
  pendingVaultOps.set(pending.chat_id, {
50032
50216
  kind: "passphrase-for-access-approve",
50033
50217
  items,
50218
+ ...existing?.kind === "passphrase-for-access-approve" && existing.attempts ? { attempts: existing.attempts } : {},
50034
50219
  startedAt: existing?.kind === "passphrase-for-access-approve" ? existing.startedAt : Date.now()
50035
50220
  });
50036
50221
  const joiningBatch = items.length > 1;
50037
50222
  await ctx.answerCallbackQuery({ text: joiningBatch ? `\uD83D\uDD10 Queued \u2014 one passphrase covers ${items.length} cards` : "\uD83D\uDD10 Send your passphrase\u2026" }).catch(() => {});
50038
50223
  await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, richMessage(`\uD83D\uDD10 _Approved \u2014 waiting for your vault passphrase. See the prompt below._`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
50039
- const promptText = joiningBatch ? `**\u26a0\ufe0f\uD83D\uDD10 ACTION NEEDED: passphrase required**
50040
-
50041
- ` + `Type your vault passphrase as your **next message**.
50042
- ` + `One entry covers **${items.length}** pending approvals in this chat, no re-type per card.
50043
-
50044
- ` + `_We delete the passphrase message the moment we read it._` : isAdminOnly ? `**\u26a0\ufe0f\uD83D\uDD10 ACTION NEEDED: passphrase required**
50045
-
50046
- ` + `\`${pending.key}\` is an **admin-only credential**.
50047
- ` + `Type your vault passphrase as your **next message** to mint the grant for **${escapeHtmlForTg2(pending.agent)}**.
50048
-
50049
- ` + `_The passphrase is what proves it's you. An agent can never mint this key on its own. We delete the passphrase message the moment we read it._` : `**\u26a0\ufe0f\uD83D\uDD10 ACTION NEEDED: passphrase required**
50050
-
50051
- ` + `Your vault is locked.
50052
- ` + `Reply with your passphrase as your **next message** to unlock and mint the grant for **${escapeHtmlForTg2(pending.agent)}**.
50053
-
50054
- ` + `_Mint authority stays operator-only: the broker only accepts the grant when the passphrase matches. We delete the passphrase message the moment we read it._`;
50055
- await retryWithThreadFallback(robustApiCall, (tid) => lockedBot.api.sendRichMessage(pending.chat_id, richMessage(promptText), {
50056
- ...tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}
50057
- }), { threadId: pending.threadId, chat_id: pending.chat_id, verb: "vault_request_access.passphrase_prompt" }).catch(() => {});
50224
+ await sendAccessPassphrasePrompt({
50225
+ chat_id: pending.chat_id,
50226
+ ...pending.threadId != null ? { threadId: pending.threadId } : {}
50227
+ }, {
50228
+ kind: "first",
50229
+ variant: joiningBatch ? "batch" : isAdminOnly ? "admin-only" : "locked",
50230
+ itemCount: items.length,
50231
+ agentEscaped: escapeHtmlForTg2(pending.agent),
50232
+ key: pending.key
50233
+ });
50058
50234
  return;
50059
50235
  }
50060
50236
  await ctx.answerCallbackQuery({ text: "\u23f3 Minting grant\u2026" }).catch(() => {});
50061
- await performVaultAccessApproval(ctx, pending, stageId, senderId, { kind: "passphrase", passphrase: cached.passphrase });
50237
+ const outcome = await performVaultAccessApproval(ctx, pending, stageId, senderId, {
50238
+ kind: "passphrase",
50239
+ passphrase: cached.passphrase
50240
+ });
50241
+ if (outcome.kind === "passphrase-mismatch") {
50242
+ const openOp = pendingVaultOps.get(pending.chat_id);
50243
+ await resolveAccessApprovalPassphraseMismatch(ctx, {
50244
+ chat_id: pending.chat_id,
50245
+ failed: [
50246
+ {
50247
+ stageId,
50248
+ cardChatId: pending.chat_id,
50249
+ cardMessageId: pending.card_message_id ?? 0,
50250
+ senderId,
50251
+ ...pending.threadId != null ? { threadId: pending.threadId } : {}
50252
+ }
50253
+ ],
50254
+ priorAttempts: openOp?.kind === "passphrase-for-access-approve" ? openOp.attempts ?? 0 : 0,
50255
+ brokerMsg: outcome.msg
50256
+ });
50257
+ }
50062
50258
  return;
50063
50259
  }
50064
50260
  await ctx.answerCallbackQuery({ text: "Unknown action" }).catch(() => {});
@@ -50399,7 +50595,7 @@ Which agent?`, { html: true, reply_markup: kb });
50399
50595
  });
50400
50596
  }
50401
50597
  async function grantWizardStep2(ctx, chatId, agent, wizardMsgId) {
50402
- const keys = await listViaBroker();
50598
+ const keys = await listViaBroker2();
50403
50599
  if (!keys) {
50404
50600
  await switchroomReply(ctx, "\uD83D\uDD34 Broker is not running (or unreachable). Cannot list vault keys.", { html: true });
50405
50601
  pendingVaultOps.delete(chatId);
@@ -50490,7 +50686,7 @@ ${keyList}`,
50490
50686
  } catch {
50491
50687
  return;
50492
50688
  }
50493
- const result = await mintGrantViaBroker({
50689
+ const result = await mintGrantViaBroker2({
50494
50690
  agent: state.agent,
50495
50691
  keys: state.selectedKeys,
50496
50692
  ttl_seconds: state.ttlSeconds ?? null,
@@ -50505,9 +50701,9 @@ ${keyList}`,
50505
50701
  return;
50506
50702
  }
50507
50703
  const { token, id } = result;
50508
- const tokenPath = join13(homedir5(), ".switchroom", "agents", state.agent, ".vault-token");
50704
+ const tokenPath = vaultTokenFilePath2(state.agent);
50509
50705
  try {
50510
- mkdirSync13(join13(homedir5(), ".switchroom", "agents", state.agent), { recursive: true });
50706
+ mkdirSync13(dirname5(tokenPath), { recursive: true });
50511
50707
  writeFileSync9(tokenPath, token, { mode: 384 });
50512
50708
  } catch (err) {
50513
50709
  await switchroomReply(ctx, `**Grant created but token write failed:** ${escapeHtmlForTg2(String(err))}`, { html: true });
@@ -50534,7 +50730,7 @@ ${keyList}`,
50534
50730
  const revokeMatch = /^vg:revoke:(.+)$/.exec(data);
50535
50731
  if (revokeMatch) {
50536
50732
  const grantId = revokeMatch[1];
50537
- const result = await listGrantsViaBroker(undefined);
50733
+ const result = await listGrantsViaBroker2(undefined);
50538
50734
  if (result.kind !== "ok") {
50539
50735
  await ctx.answerCallbackQuery({ text: "Broker unreachable." }).catch(() => {});
50540
50736
  return;
@@ -50983,6 +51179,7 @@ ${trimmed.replace(/```/g, "`\u200b``")}
50983
51179
  return {
50984
51180
  handleVaultRecentDenialCallback,
50985
51181
  performVaultAccessApproval,
51182
+ resolveAccessApprovalPassphraseMismatch,
50986
51183
  handleSkillProposalCallback,
50987
51184
  handleMentalModelProposeCallback,
50988
51185
  handleVaultRequestAccessCallback,
@@ -51955,7 +52152,7 @@ function pickRecoveredPermissionOrigin(recentTurns, now, maxAgeMs) {
51955
52152
 
51956
52153
  // gateway/approval-hold.ts
51957
52154
  import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync11, readFileSync as readFileSync13, unlinkSync as unlinkSync9, chmodSync as chmodSync4, lstatSync as lstatSync2 } from "node:fs";
51958
- import { join as join16, dirname as dirname5 } from "node:path";
52155
+ import { join as join16, dirname as dirname6 } from "node:path";
51959
52156
  var BLOCKED_APPROVAL_FILE_MODE = 420;
51960
52157
  var BLOCKED_APPROVAL_DIR_MODE = 1023;
51961
52158
  var HELD_RETRY_BACKOFF_MS = 60000;
@@ -51988,7 +52185,7 @@ function createBlockedApprovalStore(dir, agent, fallbackDir) {
51988
52185
  const fallback = fallbackDir != null ? join16(fallbackDir, "blocked-approval.json") : null;
51989
52186
  let active = primary;
51990
52187
  function tryWrite(target, body, dirMode) {
51991
- const parent = dirname5(target);
52188
+ const parent = dirname6(target);
51992
52189
  try {
51993
52190
  mkdirSync14(parent, { recursive: true });
51994
52191
  try {
@@ -62658,9 +62855,249 @@ async function retryWithThreadFallback2(retry, send, opts) {
62658
62855
  }
62659
62856
  }
62660
62857
 
62858
+ // edit-flood-fuse.ts
62859
+ var EDIT_METHODS = new Set([
62860
+ "editMessageText",
62861
+ "editMessageCaption",
62862
+ "editMessageMedia",
62863
+ "editMessageReplyMarkup",
62864
+ "editMessageLiveLocation",
62865
+ "editMessageChecklist"
62866
+ ]);
62867
+ var SEND_METHODS = new Set([
62868
+ "sendMessage",
62869
+ "sendPhoto",
62870
+ "sendDocument",
62871
+ "sendMediaGroup",
62872
+ "sendAnimation",
62873
+ "sendVideo",
62874
+ "sendVoice",
62875
+ "sendAudio",
62876
+ "sendSticker",
62877
+ "sendLocation",
62878
+ "forwardMessage",
62879
+ "forwardMessages",
62880
+ "copyMessage",
62881
+ "copyMessages",
62882
+ "sendRichMessage"
62883
+ ]);
62884
+ var EDIT_FLOOD_FUSE_DEFAULTS = {
62885
+ perMessageMaxPerWindow: 20,
62886
+ perMessageWindowMs: 60000,
62887
+ perChatEditMaxPerWindow: 30,
62888
+ perChatSendMaxPerWindow: 25,
62889
+ perChatWindowMs: 60000,
62890
+ maxDeferMs: 30000,
62891
+ tightenFactor: 0.5,
62892
+ tightenMs: 600000
62893
+ };
62894
+ var DROPPED_RESULT = true;
62895
+ function createEditFloodFuse(config = {}) {
62896
+ const enabled2 = config.enabled ?? true;
62897
+ const clock = config.clock ?? systemClock;
62898
+ const D = EDIT_FLOOD_FUSE_DEFAULTS;
62899
+ const perMessageMax = config.perMessageMaxPerWindow ?? D.perMessageMaxPerWindow;
62900
+ const perMessageWindowMs = config.perMessageWindowMs ?? D.perMessageWindowMs;
62901
+ const perChatEditMax = config.perChatEditMaxPerWindow ?? D.perChatEditMaxPerWindow;
62902
+ const perChatSendMax = config.perChatSendMaxPerWindow ?? D.perChatSendMaxPerWindow;
62903
+ const perChatWindowMs = config.perChatWindowMs ?? D.perChatWindowMs;
62904
+ const maxDeferMs = config.maxDeferMs ?? D.maxDeferMs;
62905
+ const tightenFactor = config.tightenFactor ?? D.tightenFactor;
62906
+ const tightenMs = config.tightenMs ?? D.tightenMs;
62907
+ const onTrip = config.onTrip;
62908
+ const windows = new Map;
62909
+ const counters = { deferred: 0, dropped: 0, superseded: 0, floodObserved: 0 };
62910
+ let tightenedUntil = 0;
62911
+ function isTightened(now) {
62912
+ return tightenedUntil > now;
62913
+ }
62914
+ function ceiling(base, now) {
62915
+ if (!isTightened(now))
62916
+ return base;
62917
+ return Math.max(1, Math.floor(base * tightenFactor));
62918
+ }
62919
+ function win(key) {
62920
+ let w = windows.get(key);
62921
+ if (w === undefined) {
62922
+ w = { ts: [], waiter: null, inflight: 0 };
62923
+ windows.set(key, w);
62924
+ }
62925
+ return w;
62926
+ }
62927
+ function prune(w, now, windowMs) {
62928
+ const cutoff = now - windowMs;
62929
+ while (w.ts.length > 0 && w.ts[0] <= cutoff)
62930
+ w.ts.shift();
62931
+ }
62932
+ let sinceEvict = 0;
62933
+ function evict(now) {
62934
+ if (windows.size < 4096)
62935
+ return;
62936
+ if (++sinceEvict < 1024)
62937
+ return;
62938
+ sinceEvict = 0;
62939
+ const widest = Math.max(perMessageWindowMs, perChatWindowMs);
62940
+ for (const [k, w] of windows) {
62941
+ if (w.waiter === null && w.inflight === 0 && (w.ts.length === 0 || w.ts[w.ts.length - 1] <= now - widest)) {
62942
+ windows.delete(k);
62943
+ }
62944
+ }
62945
+ }
62946
+ function waitFor(w, now, windowMs, max) {
62947
+ prune(w, now, windowMs);
62948
+ if (w.ts.length < max)
62949
+ return 0;
62950
+ return Math.max(1, w.ts[0] + windowMs - now);
62951
+ }
62952
+ function payloadKeys(payload) {
62953
+ const p = payload ?? {};
62954
+ const chat = p.chat_id != null ? String(p.chat_id) : null;
62955
+ const msg = p.message_id != null ? String(p.message_id) : null;
62956
+ return { chat, msg };
62957
+ }
62958
+ function noteFlood(now) {
62959
+ counters.floodObserved++;
62960
+ tightenedUntil = now + tightenMs;
62961
+ }
62962
+ function looksLikeFlood(err) {
62963
+ const e = err;
62964
+ if (e != null && typeof e === "object") {
62965
+ if (e.error_code === 429)
62966
+ return true;
62967
+ if (e.parameters != null && typeof e.parameters.retry_after === "number")
62968
+ return true;
62969
+ }
62970
+ const msg = err instanceof Error ? err.message : String(err ?? "");
62971
+ return /too many requests/i.test(msg) || /retry[ _-]?after/i.test(msg);
62972
+ }
62973
+ function unreserve(key, at) {
62974
+ const w = windows.get(key);
62975
+ if (w === undefined)
62976
+ return;
62977
+ const i = w.ts.lastIndexOf(at);
62978
+ if (i >= 0)
62979
+ w.ts.splice(i, 1);
62980
+ }
62981
+ async function awaitRoom(key, windowMs, max, method, mode, dropGuard) {
62982
+ const w = win(key);
62983
+ const deadline = clock.now() + maxDeferMs;
62984
+ let counted = false;
62985
+ for (;; ) {
62986
+ const now = clock.now();
62987
+ const wait = waitFor(w, now, windowMs, ceiling(max, now));
62988
+ if (wait === 0) {
62989
+ w.ts.push(now);
62990
+ return now;
62991
+ }
62992
+ if (now >= deadline) {
62993
+ if (mode !== "release" && (dropGuard === undefined || dropGuard())) {
62994
+ counters.dropped++;
62995
+ onTrip?.({ method, key, action: "dropped" });
62996
+ return null;
62997
+ }
62998
+ w.ts.push(now);
62999
+ return now;
63000
+ }
63001
+ if (!counted) {
63002
+ counters.deferred++;
63003
+ counted = true;
63004
+ onTrip?.({ method, key, action: "deferred" });
63005
+ }
63006
+ let killed = false;
63007
+ if (mode === "supersede") {
63008
+ w.waiter?.kill();
63009
+ const superseded = new Promise((resolve6) => {
63010
+ w.waiter = { kill: () => {
63011
+ killed = true;
63012
+ resolve6();
63013
+ } };
63014
+ });
63015
+ await Promise.race([clock.sleep(Math.min(wait, deadline - now)), superseded]);
63016
+ if (killed) {
63017
+ counters.superseded++;
63018
+ onTrip?.({ method, key, action: "superseded" });
63019
+ return null;
63020
+ }
63021
+ w.waiter = null;
63022
+ } else {
63023
+ await clock.sleep(Math.min(wait, deadline - now));
63024
+ }
63025
+ }
63026
+ }
63027
+ async function apply(method, payload, next) {
63028
+ if (!enabled2)
63029
+ return next();
63030
+ const isEdit = EDIT_METHODS.has(method);
63031
+ const isSend = SEND_METHODS.has(method);
63032
+ if (!isEdit && !isSend)
63033
+ return runObserved(next);
63034
+ const { chat, msg } = payloadKeys(payload);
63035
+ if (chat == null)
63036
+ return runObserved(next);
63037
+ const now = clock.now();
63038
+ evict(now);
63039
+ if (isEdit) {
63040
+ if (msg == null)
63041
+ return runObserved(next);
63042
+ const msgKey = `m:${chat}:${msg}`;
63043
+ const mw = win(msgKey);
63044
+ mw.inflight++;
63045
+ try {
63046
+ const msgSlot = await awaitRoom(msgKey, perMessageWindowMs, perMessageMax, method, "supersede");
63047
+ if (msgSlot === null)
63048
+ return DROPPED_RESULT;
63049
+ const chatKey3 = `ce:${chat}`;
63050
+ const chatSlot = await awaitRoom(chatKey3, perChatWindowMs, perChatEditMax, method, "drop", () => mw.inflight > 1);
63051
+ if (chatSlot === null) {
63052
+ unreserve(msgKey, msgSlot);
63053
+ return DROPPED_RESULT;
63054
+ }
63055
+ return runObserved(next);
63056
+ } finally {
63057
+ mw.inflight--;
63058
+ }
63059
+ }
63060
+ const chatKey2 = `cs:${chat}`;
63061
+ await awaitRoom(chatKey2, perChatWindowMs, perChatSendMax, method, "release");
63062
+ return runObserved(next);
63063
+ }
63064
+ async function runObserved(next) {
63065
+ try {
63066
+ const res = await next();
63067
+ const r = res;
63068
+ if (r != null && typeof r === "object" && r.ok === false && r.error_code === 429) {
63069
+ noteFlood(clock.now());
63070
+ }
63071
+ return res;
63072
+ } catch (err) {
63073
+ if (looksLikeFlood(err))
63074
+ noteFlood(clock.now());
63075
+ throw err;
63076
+ }
63077
+ }
63078
+ function stats() {
63079
+ const now = clock.now();
63080
+ return {
63081
+ enabled: enabled2,
63082
+ deferred: counters.deferred,
63083
+ dropped: counters.dropped,
63084
+ superseded: counters.superseded,
63085
+ floodObserved: counters.floodObserved,
63086
+ tightened: isTightened(now),
63087
+ perMessageCeiling: ceiling(perMessageMax, now)
63088
+ };
63089
+ }
63090
+ return { apply, stats };
63091
+ }
63092
+ function installEditFloodFuse(bot, config = {}) {
63093
+ const fuse = createEditFloodFuse(config);
63094
+ bot.api.config.use(async (prev, method, payload, signal) => fuse.apply(method, payload, () => prev(method, payload, signal)));
63095
+ return fuse;
63096
+ }
63097
+
62661
63098
  // send-gate.ts
62662
63099
  import { createHash } from "node:crypto";
62663
- var systemClock = {
63100
+ var systemClock2 = {
62664
63101
  now: () => Date.now(),
62665
63102
  sleep: (ms) => new Promise((r) => setTimeout(r, ms))
62666
63103
  };
@@ -62761,7 +63198,7 @@ var SEND_GATE_DEFAULTS = {
62761
63198
  };
62762
63199
  function createSendGate(config) {
62763
63200
  const enabled2 = config.enabled;
62764
- const clock = config.clock ?? systemClock;
63201
+ const clock = config.clock ?? systemClock2;
62765
63202
  const globalPerSec = config.globalPerSec ?? SEND_GATE_DEFAULTS.globalPerSec;
62766
63203
  const globalBurst = config.globalBurst ?? SEND_GATE_DEFAULTS.globalBurst;
62767
63204
  const perChatPerSec = config.perChatPerSec ?? SEND_GATE_DEFAULTS.perChatPerSec;
@@ -63491,7 +63928,7 @@ import {
63491
63928
  unlinkSync as unlinkSync10,
63492
63929
  renameSync as renameSync9
63493
63930
  } from "node:fs";
63494
- import { dirname as dirname6, join as join17 } from "node:path";
63931
+ import { dirname as dirname7, join as join17 } from "node:path";
63495
63932
  var FLOOD_STATE_FILE = "flood-wait.json";
63496
63933
  var FLOOD_STATE_MODE = 420;
63497
63934
  function floodStatePath(stateDir) {
@@ -63544,7 +63981,7 @@ function readFloodState(path2) {
63544
63981
  function writeFloodState(path2, state, log = (l) => process.stderr.write(l)) {
63545
63982
  const payload = JSON.stringify(state);
63546
63983
  try {
63547
- mkdirSync15(dirname6(path2), { recursive: true });
63984
+ mkdirSync15(dirname7(path2), { recursive: true });
63548
63985
  } catch {}
63549
63986
  try {
63550
63987
  writeFileSync12(path2, payload, { mode: FLOOD_STATE_MODE });
@@ -63675,7 +64112,7 @@ function readFloodWindows(path2, now, log = (l) => process.stderr.write(l)) {
63675
64112
  }
63676
64113
  function writeFloodWindow(path2, record, now) {
63677
64114
  try {
63678
- mkdirSync15(dirname6(path2), { recursive: true });
64115
+ mkdirSync15(dirname7(path2), { recursive: true });
63679
64116
  const existing = readFloodWindows(path2, now);
63680
64117
  const byScope = new Map;
63681
64118
  for (const r of existing)
@@ -63850,7 +64287,7 @@ function __resetAllForTests() {
63850
64287
  }
63851
64288
 
63852
64289
  // ../node_modules/.bun/posthog-node@5.29.2/node_modules/posthog-node/dist/extensions/error-tracking/modifiers/module.node.mjs
63853
- import { dirname as dirname7, posix, sep as sep3 } from "path";
64290
+ import { dirname as dirname8, posix, sep as sep3 } from "path";
63854
64291
  function createModulerModifier() {
63855
64292
  const getModuleFromFileName = createGetModuleFromFilename();
63856
64293
  return async (frames) => {
@@ -63859,7 +64296,7 @@ function createModulerModifier() {
63859
64296
  return frames;
63860
64297
  };
63861
64298
  }
63862
- function createGetModuleFromFilename(basePath = process.argv[1] ? dirname7(process.argv[1]) : process.cwd(), isWindows = sep3 === "\\") {
64299
+ function createGetModuleFromFilename(basePath = process.argv[1] ? dirname8(process.argv[1]) : process.cwd(), isWindows = sep3 === "\\") {
63863
64300
  const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
63864
64301
  return (filename) => {
63865
64302
  if (!filename)
@@ -68291,7 +68728,7 @@ class PostHog extends PostHogBackendClient {
68291
68728
 
68292
68729
  // analytics-posthog.ts
68293
68730
  import { existsSync as existsSync13, mkdirSync as mkdirSync16, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "node:fs";
68294
- import { dirname as dirname8, join as join19 } from "node:path";
68731
+ import { dirname as dirname9, join as join19 } from "node:path";
68295
68732
  import { randomUUID as randomUUID2 } from "node:crypto";
68296
68733
  var DEFAULT_KEY = "phc_qKY87cKWZm6ZyCtk7LcRd2cU8Sg42u7Ywhui5stYCegd";
68297
68734
  var DEFAULT_HOST = "https://us.i.posthog.com";
@@ -68330,7 +68767,7 @@ function getDistinctId() {
68330
68767
  const id = randomUUID2();
68331
68768
  cachedDistinctId = id;
68332
68769
  try {
68333
- mkdirSync16(dirname8(fallbackPath), { recursive: true });
68770
+ mkdirSync16(dirname9(fallbackPath), { recursive: true });
68334
68771
  writeFileSync13(fallbackPath, id, "utf-8");
68335
68772
  } catch {}
68336
68773
  return id;
@@ -68403,11 +68840,11 @@ function installGlobalErrorHandlers() {
68403
68840
 
68404
68841
  // runtime-metrics.ts
68405
68842
  import { mkdirSync as mkdirSync18, appendFileSync as appendFileSync3 } from "node:fs";
68406
- import { dirname as dirname10, join as join21 } from "node:path";
68843
+ import { dirname as dirname11, join as join21 } from "node:path";
68407
68844
 
68408
68845
  // analytics-posthog.ts
68409
68846
  import { existsSync as existsSync14, mkdirSync as mkdirSync17, readFileSync as readFileSync16, writeFileSync as writeFileSync14 } from "node:fs";
68410
- import { dirname as dirname9, join as join20 } from "node:path";
68847
+ import { dirname as dirname10, join as join20 } from "node:path";
68411
68848
  import { randomUUID as randomUUID3 } from "node:crypto";
68412
68849
  var DEFAULT_KEY2 = "phc_qKY87cKWZm6ZyCtk7LcRd2cU8Sg42u7Ywhui5stYCegd";
68413
68850
  var DEFAULT_HOST2 = "https://us.i.posthog.com";
@@ -68445,7 +68882,7 @@ function getDistinctId2() {
68445
68882
  const id = randomUUID3();
68446
68883
  cachedDistinctId2 = id;
68447
68884
  try {
68448
- mkdirSync17(dirname9(fallbackPath), { recursive: true });
68885
+ mkdirSync17(dirname10(fallbackPath), { recursive: true });
68449
68886
  writeFileSync14(fallbackPath, id, "utf-8");
68450
68887
  } catch {}
68451
68888
  return id;
@@ -68500,7 +68937,7 @@ function resolveJsonlPath() {
68500
68937
  function appendJsonl(line) {
68501
68938
  const path2 = resolveJsonlPath();
68502
68939
  try {
68503
- mkdirSync18(dirname10(path2), { recursive: true });
68940
+ mkdirSync18(dirname11(path2), { recursive: true });
68504
68941
  appendFileSync3(path2, line + `
68505
68942
  `, "utf-8");
68506
68943
  } catch (err) {
@@ -69169,8 +69606,8 @@ function __resetAllForTests3() {
69169
69606
 
69170
69607
  // silent-end.ts
69171
69608
  import { existsSync as existsSync16, readFileSync as readFileSync17, writeFileSync as writeFileSync15, unlinkSync as unlinkSync11, mkdirSync as mkdirSync19 } from "node:fs";
69172
- import { dirname as dirname11, join as join22 } from "node:path";
69173
- import { homedir as homedir6 } from "node:os";
69609
+ import { dirname as dirname12, join as join22 } from "node:path";
69610
+ import { homedir as homedir5 } from "node:os";
69174
69611
  var SILENT_END_MAX_RETRIES = 2;
69175
69612
  var SILENT_END_STALE_RECORD_MAX_AGE_MS = 30 * 60000;
69176
69613
  function resolveStateDir(deps) {
@@ -69179,7 +69616,7 @@ function resolveStateDir(deps) {
69179
69616
  const env = process.env.TELEGRAM_STATE_DIR;
69180
69617
  if (env != null && env !== "")
69181
69618
  return env;
69182
- const home2 = process.env.HOME ?? homedir6();
69619
+ const home2 = process.env.HOME ?? homedir5();
69183
69620
  return join22(home2, ".claude", "channels", "telegram");
69184
69621
  }
69185
69622
  function resolveStatePath2(deps) {
@@ -69212,7 +69649,7 @@ function writeSilentEndState(args, deps) {
69212
69649
  timestamp: Date.now()
69213
69650
  };
69214
69651
  try {
69215
- mkdirSync19(dirname11(statePath), { recursive: true });
69652
+ mkdirSync19(dirname12(statePath), { recursive: true });
69216
69653
  writeFileSync15(statePath, JSON.stringify(state3), "utf8");
69217
69654
  emitLog(deps, `silent-end: wrote state file turnKey=${args.turnKey} retryCount=${retryCount}
69218
69655
  `);
@@ -69301,7 +69738,7 @@ function resolveAnswerLaneConfig(input) {
69301
69738
  }
69302
69739
 
69303
69740
  // session-tail.ts
69304
- import { homedir as homedir7 } from "os";
69741
+ import { homedir as homedir6 } from "os";
69305
69742
  import { basename as basename8, join as join24 } from "path";
69306
69743
 
69307
69744
  // operator-events.ts
@@ -69704,7 +70141,7 @@ var OPERATOR_ACTIONABLE_KINDS = new Set([
69704
70141
  function sanitizeCwdToProjectName(cwd) {
69705
70142
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
69706
70143
  }
69707
- function getProjectsDirForCwd(cwd = process.cwd(), claudeHome = process.env.CLAUDE_CONFIG_DIR ?? join24(homedir7(), ".claude")) {
70144
+ function getProjectsDirForCwd(cwd = process.cwd(), claudeHome = process.env.CLAUDE_CONFIG_DIR ?? join24(homedir6(), ".claude")) {
69708
70145
  return join24(claudeHome, "projects", sanitizeCwdToProjectName(cwd));
69709
70146
  }
69710
70147
  function parseChannelMeta(content3) {
@@ -71048,7 +71485,7 @@ function defaultAddAccount(label, credentials, opts) {
71048
71485
  // ../src/auth/broker/client.ts
71049
71486
  init_protocol2();
71050
71487
  import * as net3 from "node:net";
71051
- import { homedir as homedir8 } from "node:os";
71488
+ import { homedir as homedir7 } from "node:os";
71052
71489
  import { randomUUID as randomUUID4 } from "node:crypto";
71053
71490
  import { join as join26 } from "node:path";
71054
71491
  var DEFAULT_TIMEOUT_MS3 = 5000;
@@ -71060,7 +71497,7 @@ function reviveDate2(v) {
71060
71497
  const d = new Date(v);
71061
71498
  return Number.isNaN(d.getTime()) ? null : d;
71062
71499
  }
71063
- function operatorSocketPath2(home2 = homedir8()) {
71500
+ function operatorSocketPath2(home2 = homedir7()) {
71064
71501
  return join26(home2, ".switchroom", "state", "auth-broker-operator", "sock");
71065
71502
  }
71066
71503
  function resolveAuthBrokerSocketPath2(opts) {
@@ -71629,7 +72066,7 @@ function resolveExhaustUntil(resetAtMs, now = Date.now()) {
71629
72066
  // gateway/auth-add-flow.ts
71630
72067
  import { execFileSync as execFileSync4 } from "node:child_process";
71631
72068
  import { existsSync as existsSync22, mkdirSync as mkdirSync23, readFileSync as readFileSync21, readdirSync as readdirSync6, rmSync as rmSync5, statSync as statSync9, writeFileSync as writeFileSync19 } from "node:fs";
71632
- import { homedir as homedir9 } from "node:os";
72069
+ import { homedir as homedir8 } from "node:os";
71633
72070
  import { join as join27 } from "node:path";
71634
72071
  import { randomBytes as randomBytes8 } from "node:crypto";
71635
72072
 
@@ -71735,7 +72172,7 @@ function makeAuthAddTmuxOps(tmuxBin = "tmux") {
71735
72172
  };
71736
72173
  }
71737
72174
  var pendingAuthAddFlows = new Map;
71738
- function pickScratchDir(label, home2 = homedir9()) {
72175
+ function pickScratchDir(label, home2 = homedir8()) {
71739
72176
  const suffix = randomBytes8(8).toString("hex");
71740
72177
  return join27(home2, ".switchroom", "accounts", ".in-progress", `${label}-${suffix}`);
71741
72178
  }
@@ -71786,7 +72223,7 @@ async function startAccountAuthSession(label, opts = {}) {
71786
72223
  if (process.env.SWITCHROOM_TMUX_SUPERVISOR !== "1" && !opts.tmuxOps) {
71787
72224
  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).");
71788
72225
  }
71789
- const home2 = opts.home ?? homedir9();
72226
+ const home2 = opts.home ?? homedir8();
71790
72227
  const urlTimeoutMs = opts.urlTimeoutMs ?? 30000;
71791
72228
  const agentName3 = opts.agentName ?? process.env.SWITCHROOM_AGENT_NAME ?? "gateway";
71792
72229
  const tmux = opts.tmuxOps ?? makeAuthAddTmuxOps(opts.tmuxBin);
@@ -74756,7 +75193,7 @@ import {
74756
75193
  appendFileSync as appendFileSync4
74757
75194
  } from "node:fs";
74758
75195
  import { join as join29 } from "node:path";
74759
- import { homedir as homedir10 } from "node:os";
75196
+ import { homedir as homedir9 } from "node:os";
74760
75197
  function sha256Hex(s) {
74761
75198
  return createHash3("sha256").update(s, "utf8").digest("hex");
74762
75199
  }
@@ -74766,7 +75203,7 @@ function resolveStateDir2(explicit) {
74766
75203
  const env = process.env.TELEGRAM_STATE_DIR;
74767
75204
  if (env != null && env !== "")
74768
75205
  return env;
74769
- const home2 = process.env.HOME ?? homedir10();
75206
+ const home2 = process.env.HOME ?? homedir9();
74770
75207
  return join29(home2, ".claude", "channels", "telegram");
74771
75208
  }
74772
75209
  function resolveOutboxDir(stateDir) {
@@ -75620,8 +76057,8 @@ function monotonicNowMs2() {
75620
76057
 
75621
76058
  // silent-end.ts
75622
76059
  import { existsSync as existsSync26, readFileSync as readFileSync24, writeFileSync as writeFileSync22, unlinkSync as unlinkSync13, mkdirSync as mkdirSync27 } from "node:fs";
75623
- import { dirname as dirname12, join as join31 } from "node:path";
75624
- import { homedir as homedir11 } from "node:os";
76060
+ import { dirname as dirname13, join as join31 } from "node:path";
76061
+ import { homedir as homedir10 } from "node:os";
75625
76062
  var SILENT_END_MAX_RETRIES2 = 2;
75626
76063
  var SILENT_END_STALE_RECORD_MAX_AGE_MS2 = 30 * 60000;
75627
76064
  function silentEndFallbackText(turnDurationMs) {
@@ -75634,7 +76071,7 @@ function resolveStateDir3(deps) {
75634
76071
  const env = process.env.TELEGRAM_STATE_DIR;
75635
76072
  if (env != null && env !== "")
75636
76073
  return env;
75637
- const home2 = process.env.HOME ?? homedir11();
76074
+ const home2 = process.env.HOME ?? homedir10();
75638
76075
  return join31(home2, ".claude", "channels", "telegram");
75639
76076
  }
75640
76077
  function resolveStatePath3(deps) {
@@ -75667,7 +76104,7 @@ function writeSilentEndState2(args, deps) {
75667
76104
  timestamp: Date.now()
75668
76105
  };
75669
76106
  try {
75670
- mkdirSync27(dirname12(statePath), { recursive: true });
76107
+ mkdirSync27(dirname13(statePath), { recursive: true });
75671
76108
  writeFileSync22(statePath, JSON.stringify(state3), "utf8");
75672
76109
  emitLog2(deps, `silent-end: wrote state file turnKey=${args.turnKey} retryCount=${retryCount}
75673
76110
  `);
@@ -77261,7 +77698,7 @@ init_rich_send();
77261
77698
 
77262
77699
  // runtime-metrics.ts
77263
77700
  import { mkdirSync as mkdirSync29, appendFileSync as appendFileSync6 } from "node:fs";
77264
- import { dirname as dirname13, join as join33 } from "node:path";
77701
+ import { dirname as dirname14, join as join33 } from "node:path";
77265
77702
  function resolveJsonlPath2() {
77266
77703
  const override = process.env.SWITCHROOM_RUNTIME_METRICS_PATH;
77267
77704
  if (override && override.trim() !== "")
@@ -77272,7 +77709,7 @@ function resolveJsonlPath2() {
77272
77709
  function appendJsonl2(line) {
77273
77710
  const path2 = resolveJsonlPath2();
77274
77711
  try {
77275
- mkdirSync29(dirname13(path2), { recursive: true });
77712
+ mkdirSync29(dirname14(path2), { recursive: true });
77276
77713
  appendFileSync6(path2, line + `
77277
77714
  `, "utf-8");
77278
77715
  } catch (err) {
@@ -79230,7 +79667,16 @@ function createNarrativeLane(deps) {
79230
79667
  reconcileStatusPin(`fg:${statusKey(chat, thread)}`, chat, { pinned: true, messageId: sent.message_id });
79231
79668
  } else {
79232
79669
  const id = turn.activityMessageId;
79233
- await robustApiCall(() => bot.api.editMessageText(chat, id, richMessage(html), {}), { chat_id: chat, ...thread != null ? { threadId: thread } : {}, verb: "activity-summary.edit" });
79670
+ const editRes = await robustApiCall(() => bot.api.editMessageText(chat, id, richMessage(html), {}), {
79671
+ chat_id: chat,
79672
+ ...thread != null ? { threadId: thread } : {},
79673
+ verb: "activity-summary.edit",
79674
+ priorityClass: "cosmetic",
79675
+ messageId: id,
79676
+ editPayload: html
79677
+ });
79678
+ if (isSendGateShed(editRes))
79679
+ break;
79234
79680
  }
79235
79681
  turn.activityLastSentRender = target;
79236
79682
  } catch (err) {
@@ -79431,7 +79877,14 @@ function createNarrativeLane(deps) {
79431
79877
  if (finalHtml == null)
79432
79878
  return;
79433
79879
  try {
79434
- await robustApiCall(() => bot.api.editMessageText(chat, id, richMessage(finalHtml), {}), { chat_id: chat, ...thread != null ? { threadId: thread } : {}, verb: "activity-summary.finalize" });
79880
+ await robustApiCall(() => bot.api.editMessageText(chat, id, richMessage(finalHtml), {}), {
79881
+ chat_id: chat,
79882
+ ...thread != null ? { threadId: thread } : {},
79883
+ verb: "activity-summary.finalize",
79884
+ priorityClass: "useful",
79885
+ messageId: id,
79886
+ editPayload: finalHtml
79887
+ });
79435
79888
  } catch (err) {
79436
79889
  const msg = err instanceof Error ? err.message : String(err);
79437
79890
  const low = msg.toLowerCase();
@@ -79738,12 +80191,12 @@ class AnswerReadyFlushController {
79738
80191
  }
79739
80192
 
79740
80193
  // agent-dir.ts
79741
- import { dirname as dirname14 } from "node:path";
80194
+ import { dirname as dirname15 } from "node:path";
79742
80195
  function resolveAgentDirFromEnv() {
79743
80196
  const state6 = process.env.TELEGRAM_STATE_DIR;
79744
80197
  if (!state6 || state6.trim().length === 0)
79745
80198
  return null;
79746
- return dirname14(state6);
80199
+ return dirname15(state6);
79747
80200
  }
79748
80201
 
79749
80202
  // active-reactions.ts
@@ -82450,7 +82903,7 @@ async function discoverModels(agentName3, opts = {}) {
82450
82903
  }
82451
82904
 
82452
82905
  // ../src/agents/scaffold.ts
82453
- import { dirname as dirname15, isAbsolute, join as join42, relative, resolve as resolve8 } from "node:path";
82906
+ import { dirname as dirname16, isAbsolute, join as join42, relative, resolve as resolve8 } from "node:path";
82454
82907
  init_atomic();
82455
82908
 
82456
82909
  // ../src/agents/agent-uid.ts
@@ -82469,7 +82922,7 @@ init_timezone();
82469
82922
 
82470
82923
  // ../src/cli/agent-config.ts
82471
82924
  import { join as join40 } from "node:path";
82472
- import { homedir as homedir12 } from "node:os";
82925
+ import { homedir as homedir11 } from "node:os";
82473
82926
 
82474
82927
  // ../src/cli/helpers.ts
82475
82928
  init_loader();
@@ -82489,7 +82942,7 @@ var WEBKITE_VAULT_KEYS = new Set([
82489
82942
  init_overlay_loader();
82490
82943
 
82491
82944
  // ../src/cli/agent-config.ts
82492
- var AUDIT_ROOT = join40(homedir12(), ".switchroom", "audit");
82945
+ var AUDIT_ROOT = join40(homedir11(), ".switchroom", "audit");
82493
82946
 
82494
82947
  // ../src/agents/profiles.ts
82495
82948
  var import_handlebars = __toESM(require_lib(), 1);
@@ -82527,6 +82980,47 @@ for (const name of SHARED_FRAGMENTS) {
82527
82980
  }
82528
82981
  }
82529
82982
 
82983
+ // ../src/litellm/timeout-budget.ts
82984
+ var LITELLM_ROUTER_MARGIN_S = 10;
82985
+ var LITELLM_TIMEOUT_TIERS = {
82986
+ interactive: {
82987
+ group: "gpt-oss-20b",
82988
+ localTimeoutS: 90,
82989
+ fallbackGroup: "gpt-oss-20b-openrouter",
82990
+ fallbackTimeoutS: 60
82991
+ },
82992
+ retain: {
82993
+ group: "gpt-oss-20b-retain",
82994
+ localTimeoutS: 200,
82995
+ fallbackGroup: "gpt-oss-20b-retain-openrouter",
82996
+ fallbackTimeoutS: 90
82997
+ },
82998
+ consolidation: {
82999
+ group: "gpt-oss-20b-consolidation",
83000
+ localTimeoutS: 200,
83001
+ fallbackGroup: "gpt-oss-20b-consolidation-openrouter",
83002
+ fallbackTimeoutS: 90
83003
+ }
83004
+ };
83005
+ function litellmChainSeconds(tier) {
83006
+ assertPositive(tier.localTimeoutS, `${tier.group} localTimeoutS`);
83007
+ if (tier.fallbackGroup === null)
83008
+ return tier.localTimeoutS;
83009
+ assertPositive(tier.fallbackTimeoutS, `${tier.fallbackGroup} fallbackTimeoutS`);
83010
+ return tier.localTimeoutS + tier.fallbackTimeoutS;
83011
+ }
83012
+ function minimumClientBudgetSeconds(tier, marginS = LITELLM_ROUTER_MARGIN_S) {
83013
+ return litellmChainSeconds(tier) + marginS;
83014
+ }
83015
+ function clientBudgetSeconds(tier, floorS = 0, marginS = LITELLM_ROUTER_MARGIN_S) {
83016
+ return Math.max(Math.ceil(floorS), minimumClientBudgetSeconds(tier, marginS));
83017
+ }
83018
+ function assertPositive(value, label) {
83019
+ if (!Number.isFinite(value) || value <= 0) {
83020
+ throw new Error(`litellm timeout budget: ${label} must be a positive number, got ${value}`);
83021
+ }
83022
+ }
83023
+
82530
83024
  // ../src/setup/hindsight.ts
82531
83025
  var HINDSIGHT_DEFAULT_API_PORT = 18888;
82532
83026
  var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
@@ -82536,6 +83030,18 @@ var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
82536
83030
  var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
82537
83031
  var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
82538
83032
  var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
83033
+ var HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS = 16384;
83034
+ var HINDSIGHT_RETAIN_MIN_TOKENS_PER_SECOND = 80.6;
83035
+ function hindsightRetainLlmTimeoutSeconds(maxCompletionTokens = HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS, tokensPerSecond = HINDSIGHT_RETAIN_MIN_TOKENS_PER_SECOND) {
83036
+ if (!(maxCompletionTokens > 0) || !(tokensPerSecond > 0)) {
83037
+ throw new Error(`hindsight retain budget: maxCompletionTokens (${maxCompletionTokens}) and ` + `tokensPerSecond (${tokensPerSecond}) must both be positive`);
83038
+ }
83039
+ return Math.ceil(maxCompletionTokens / tokensPerSecond);
83040
+ }
83041
+ function hindsightRetainClientTimeoutSeconds() {
83042
+ return clientBudgetSeconds(LITELLM_TIMEOUT_TIERS.retain, hindsightRetainLlmTimeoutSeconds());
83043
+ }
83044
+ var HINDSIGHT_RETAIN_CLIENT_DEADLINE_S = hindsightRetainClientTimeoutSeconds() + LITELLM_ROUTER_MARGIN_S;
82539
83045
  var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
82540
83046
  var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
82541
83047
  var DOCKER_PROBE_TIMEOUT_MS = 60 * 1000;
@@ -83149,7 +83655,7 @@ init_merge();
83149
83655
  init_timezone();
83150
83656
  var import_yaml4 = __toESM(require_dist(), 1);
83151
83657
  import { readFileSync as readFileSync32, existsSync as existsSync35 } from "node:fs";
83152
- import { homedir as homedir13 } from "node:os";
83658
+ import { homedir as homedir12 } from "node:os";
83153
83659
  import { resolve as resolve9 } from "node:path";
83154
83660
 
83155
83661
  class ConfigError2 extends Error {
@@ -83206,7 +83712,7 @@ function coerceLegacyGoogleWorkspaceKeys2(parsed, filePath) {
83206
83712
  }
83207
83713
  function findConfigFile2(startDir) {
83208
83714
  const envPath = process.env.SWITCHROOM_CONFIG;
83209
- const home2 = homedir13();
83715
+ const home2 = homedir12();
83210
83716
  const userDir = resolve9(home2, ".switchroom");
83211
83717
  const searchPaths = [
83212
83718
  envPath ? resolve9(envPath) : null,
@@ -84156,6 +84662,94 @@ function shouldSweepChatAtBoot(chatId) {
84156
84662
  return n < 0;
84157
84663
  }
84158
84664
 
84665
+ // gateway/boot-sweep-gate.ts
84666
+ function createBootSweepGate(args) {
84667
+ let armed = false;
84668
+ let ready = false;
84669
+ let started = false;
84670
+ const maybeRun = () => {
84671
+ if (started || !armed || !ready)
84672
+ return;
84673
+ started = true;
84674
+ (async () => {
84675
+ try {
84676
+ await args.run();
84677
+ } catch (err) {
84678
+ args.onError?.(err);
84679
+ }
84680
+ })();
84681
+ };
84682
+ return {
84683
+ arm() {
84684
+ armed = true;
84685
+ maybeRun();
84686
+ },
84687
+ botReady() {
84688
+ ready = true;
84689
+ maybeRun();
84690
+ },
84691
+ hasRun() {
84692
+ return started;
84693
+ }
84694
+ };
84695
+ }
84696
+ async function step(name, fn, log) {
84697
+ try {
84698
+ await fn();
84699
+ } catch (err) {
84700
+ const msg = err instanceof Error ? err.message : String(err);
84701
+ try {
84702
+ log(`telegram gateway: boot pin sweep step '${name}' failed: ${msg}
84703
+ `);
84704
+ } catch {}
84705
+ }
84706
+ }
84707
+ async function runBootPinSweepSteps(deps) {
84708
+ const log = deps.log ?? ((l) => process.stderr.write(l));
84709
+ let dmChatIds = [];
84710
+ await step("dm-chat-scan", async () => {
84711
+ dmChatIds = deps.scanDmChatIds();
84712
+ }, log);
84713
+ await step("status-pin-cleanup", deps.statusPinCleanup, log);
84714
+ await step("activity-card-reaper", deps.activityCardReaper, log);
84715
+ await step("queued-card-reaper", deps.queuedCardReaper, log);
84716
+ await step("enable-dm-sweep", async () => deps.enableDmSweep(), log);
84717
+ for (const id of dmChatIds)
84718
+ await step(`dm-sweep:${id}`, () => deps.sweepDm(id), log);
84719
+ }
84720
+
84721
+ // gateway/status-pin-api.ts
84722
+ class BotNotReadyError extends Error {
84723
+ constructor(what) {
84724
+ super(`STATUS_PIN_BOT_NOT_READY: ${what} used before initGatewayBot() assigned ` + `lockedBot \u2014 the boot sweep must run behind the boot-sweep gate`);
84725
+ this.name = "BotNotReadyError";
84726
+ }
84727
+ }
84728
+ function assertBotReady(bot, what) {
84729
+ if (bot == null)
84730
+ throw new BotNotReadyError(what);
84731
+ return bot;
84732
+ }
84733
+
84734
+ class SendGateShedError extends Error {
84735
+ constructor(verb) {
84736
+ super(`STATUS_PIN_SEND_SHED: ${verb} was shed by the send gate and never reached Telegram`);
84737
+ this.name = "SendGateShedError";
84738
+ }
84739
+ }
84740
+ function assertLanded(result, verb) {
84741
+ if (result === SEND_GATE_SHED)
84742
+ throw new SendGateShedError(verb);
84743
+ return result;
84744
+ }
84745
+ function createStatusPinApi(getBot, robust) {
84746
+ const call = (verb, fn, chatId) => robust(() => fn(assertBotReady(getBot(), verb)), { chat_id: chatId, verb }).then((r) => assertLanded(r, verb));
84747
+ return {
84748
+ pinChatMessage: (chat_id, message_id, opts) => call("status-pin.pin", (bot) => bot.api.pinChatMessage(chat_id, message_id, opts), String(chat_id)),
84749
+ unpinChatMessage: (chat_id, message_id) => call("status-pin.unpin", (bot) => bot.api.unpinChatMessage(chat_id, message_id), String(chat_id))
84750
+ };
84751
+ }
84752
+
84159
84753
  // gateway/dm-pin-sweep.ts
84160
84754
  function isDmChatId(chatId) {
84161
84755
  const n = Number(chatId);
@@ -84339,7 +84933,7 @@ function startWebhookIngestServer(opts) {
84339
84933
  // ../src/web/webhook-gateway-record.ts
84340
84934
  import { appendFileSync as appendFileSync8, mkdirSync as mkdirSync35 } from "fs";
84341
84935
  import { join as join47 } from "path";
84342
- import { homedir as homedir15 } from "os";
84936
+ import { homedir as homedir14 } from "os";
84343
84937
 
84344
84938
  // ../src/web/webhook-handler.ts
84345
84939
  import { appendFileSync as appendFileSync7, existsSync as existsSync40, mkdirSync as mkdirSync33, readFileSync as readFileSync34, writeFileSync as writeFileSync30 } from "fs";
@@ -84493,21 +85087,21 @@ function sweepStaleParks(lockPath) {
84493
85087
  } catch {}
84494
85088
  }
84495
85089
  function fsyncAndMeasure(snapshotPath, logPath, tag) {
84496
- let step = "open";
85090
+ let step2 = "open";
84497
85091
  try {
84498
85092
  const fd = fs2.openSync(snapshotPath, "r");
84499
85093
  try {
84500
- step = "fsync";
85094
+ step2 = "fsync";
84501
85095
  fs2.fsyncSync(fd);
84502
- step = "measure";
85096
+ step2 = "measure";
84503
85097
  const size = fs2.fstatSync(fd).size;
84504
- step = "close";
85098
+ step2 = "close";
84505
85099
  return size;
84506
85100
  } finally {
84507
85101
  fs2.closeSync(fd);
84508
85102
  }
84509
85103
  } catch (err) {
84510
- process.stderr.write(`[${tag}] ERROR: could not ${step} snapshot ${snapshotPath}; leaving active log ${logPath} intact to avoid data loss: ${err.message}
85104
+ process.stderr.write(`[${tag}] ERROR: could not ${step2} snapshot ${snapshotPath}; leaving active log ${logPath} intact to avoid data loss: ${err.message}
84511
85105
  `);
84512
85106
  return null;
84513
85107
  }
@@ -84729,7 +85323,7 @@ var throttleIssueWindow = new Map;
84729
85323
  // ../src/web/webhook-dispatch.ts
84730
85324
  import { existsSync as existsSync41, mkdirSync as mkdirSync34, readFileSync as readFileSync35, writeFileSync as writeFileSync31 } from "fs";
84731
85325
  import { join as join46 } from "path";
84732
- import { homedir as homedir14 } from "os";
85326
+ import { homedir as homedir13 } from "os";
84733
85327
 
84734
85328
  // ../src/agent-scheduler/ipc-client.ts
84735
85329
  import { createConnection as createConnection2 } from "node:net";
@@ -85106,7 +85700,7 @@ async function defaultInject(socketPath, agentName3, inbound) {
85106
85700
  }
85107
85701
  function injectWebhookInbound(agent, prompt, ctx, deps = {}) {
85108
85702
  const log = deps.log ?? ((s) => process.stderr.write(s));
85109
- const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join46(homedir14(), ".switchroom", "agents", a));
85703
+ const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join46(homedir13(), ".switchroom", "agents", a));
85110
85704
  const now = (deps.now ?? Date.now)();
85111
85705
  const socketPath = join46(resolveAgentDir(agent), "telegram", "gateway.sock");
85112
85706
  const inbound = {
@@ -85180,7 +85774,7 @@ function evaluateDispatch(args, deps = {}) {
85180
85774
  const log = deps.log ?? ((s) => process.stderr.write(s));
85181
85775
  const now = (deps.now ?? Date.now)();
85182
85776
  const nowDate = deps.nowDate ?? (() => new Date(now));
85183
- const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join46(homedir14(), ".switchroom", "agents", a));
85777
+ const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join46(homedir13(), ".switchroom", "agents", a));
85184
85778
  const cooldownStore = deps.cooldownStore ?? createFileCooldownStore(resolveAgentDir);
85185
85779
  if (!DISPATCH_SOURCES.includes(args.source))
85186
85780
  return 0;
@@ -85258,7 +85852,7 @@ var CONSOLIDATION_COMPLETED_EVENT = "consolidation.completed";
85258
85852
  function recordWebhookEvent(rec, deps = {}) {
85259
85853
  const log = deps.log ?? ((s) => process.stderr.write(s));
85260
85854
  const now = rec.ts || (deps.now ?? Date.now)();
85261
- const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join47(homedir15(), ".switchroom", "agents", a));
85855
+ const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join47(homedir14(), ".switchroom", "agents", a));
85262
85856
  const dedupStore = deps.dedupStore ?? createFileDedupStore(resolveAgentDir);
85263
85857
  const agent = rec.agent;
85264
85858
  const telegramDir = join47(resolveAgentDir(agent), "telegram");
@@ -93053,9 +93647,9 @@ import {
93053
93647
  renameSync as renameSync20
93054
93648
  } from "node:fs";
93055
93649
  import { join as join50, resolve as resolve10 } from "node:path";
93056
- import { homedir as homedir16 } from "node:os";
93650
+ import { homedir as homedir15 } from "node:os";
93057
93651
  function registryDir() {
93058
- return resolve10(process.env.SWITCHROOM_WORKTREE_DIR ?? join50(homedir16(), ".switchroom", "worktrees"));
93652
+ return resolve10(process.env.SWITCHROOM_WORKTREE_DIR ?? join50(homedir15(), ".switchroom", "worktrees"));
93059
93653
  }
93060
93654
  function recordPath(id) {
93061
93655
  return join50(registryDir(), `${id}.json`);
@@ -93238,17 +93832,17 @@ init_boot_card();
93238
93832
  // gateway/update-announce.ts
93239
93833
  import { existsSync as existsSync49, mkdirSync as mkdirSync41, openSync as openSync10, closeSync as closeSync10, readFileSync as readFileSync49 } from "node:fs";
93240
93834
  import { join as join55 } from "node:path";
93241
- import { homedir as homedir18 } from "node:os";
93835
+ import { homedir as homedir17 } from "node:os";
93242
93836
 
93243
93837
  // ../src/host-control/audit-reader.ts
93244
- import { homedir as homedir17 } from "node:os";
93838
+ import { homedir as homedir16 } from "node:os";
93245
93839
  import { join as join54 } from "node:path";
93246
93840
 
93247
93841
  // ../src/host-control/audit-rotation-config.ts
93248
93842
  var DEFAULT_HOSTD_AUDIT_MAX_BYTES = 32 * 1024 * 1024;
93249
93843
 
93250
93844
  // ../src/host-control/audit-reader.ts
93251
- function defaultAuditLogPath(home2 = homedir17()) {
93845
+ function defaultAuditLogPath(home2 = homedir16()) {
93252
93846
  return join54(home2, ".switchroom", "host-control-audit.log");
93253
93847
  }
93254
93848
  var AUDIT_READ_WINDOW_BYTES = 4 * 1024 * 1024;
@@ -93435,7 +94029,7 @@ function renderUpdateOutcomeLine(entry) {
93435
94029
  `);
93436
94030
  }
93437
94031
  function claimUpdateAnnouncement(requestId, opts = {}) {
93438
- const stateDir = opts.stateDir ?? process.env.TELEGRAM_STATE_DIR ?? join55(homedir18(), ".switchroom");
94032
+ const stateDir = opts.stateDir ?? process.env.TELEGRAM_STATE_DIR ?? join55(homedir17(), ".switchroom");
93439
94033
  const dir = join55(stateDir, "update-announced");
93440
94034
  try {
93441
94035
  mkdirSync41(dir, { recursive: true });
@@ -95737,10 +96331,10 @@ function startOutboxSweep(deps) {
95737
96331
  }
95738
96332
 
95739
96333
  // ../src/build-info.ts
95740
- var VERSION2 = "0.19.19";
95741
- var COMMIT_SHA = "0bdb34ec";
95742
- var COMMIT_DATE = "2026-07-26T08:27:14+10:00";
95743
- var LATEST_PR = 3618;
96334
+ var VERSION2 = "0.19.22";
96335
+ var COMMIT_SHA = "50ef9fb5";
96336
+ var COMMIT_DATE = "2026-07-26T06:22:32Z";
96337
+ var LATEST_PR = 3696;
95744
96338
  var COMMITS_AHEAD_OF_TAG = 0;
95745
96339
 
95746
96340
  // gateway/boot-version.ts
@@ -95817,12 +96411,12 @@ init_protocol();
95817
96411
  init_peercred();
95818
96412
  import * as net5 from "node:net";
95819
96413
  import * as fs3 from "node:fs";
95820
- import { homedir as homedir19 } from "node:os";
96414
+ import { homedir as homedir18 } from "node:os";
95821
96415
  import { join as join63 } from "node:path";
95822
96416
  var DEFAULT_TIMEOUT_MS4 = 2000;
95823
96417
  var UNLOCK_TIMEOUT_MS = 30000;
95824
- var LEGACY_SOCKET_PATH2 = join63(homedir19(), ".switchroom", "vault-broker.sock");
95825
- var OPERATOR_SOCKET_PATH2 = join63(homedir19(), ".switchroom", "broker-operator", "sock");
96418
+ var LEGACY_SOCKET_PATH2 = join63(homedir18(), ".switchroom", "vault-broker.sock");
96419
+ var OPERATOR_SOCKET_PATH2 = join63(homedir18(), ".switchroom", "broker-operator", "sock");
95826
96420
  function defaultBrokerSocketPath2() {
95827
96421
  if (fs3.existsSync(OPERATOR_SOCKET_PATH2))
95828
96422
  return OPERATOR_SOCKET_PATH2;
@@ -97596,7 +98190,7 @@ if (isGatewayMain) {
97596
98190
  shutdownAnalytics();
97597
98191
  });
97598
98192
  }
97599
- var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join65(homedir20(), ".claude", "channels", "telegram");
98193
+ var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join65(homedir19(), ".claude", "channels", "telegram");
97600
98194
  var permCardStore = createPermissionCardStore(STATE_DIR);
97601
98195
  var BLOCKED_APPROVALS_DIR = process.env.SWITCHROOM_BLOCKED_APPROVALS_DIR ?? "/state/blocked-approvals";
97602
98196
  var AGENT_NAME = process.env.SWITCHROOM_AGENT_NAME ?? "agent";
@@ -98853,9 +99447,9 @@ function maybePostBusyAck(gateDecision, chatId, threadId) {
98853
99447
  const inFlight = currentTurn;
98854
99448
  const inFlightKey = inFlight != null ? statusKey(inFlight.sessionChatId, inFlight.sessionThreadId) : key;
98855
99449
  const now = Date.now();
98856
- const step = longestInFlightTool(inFlightKey, now);
99450
+ const step2 = longestInFlightTool(inFlightKey, now);
98857
99451
  const midToolCall = toolFlightTracker.isMidToolCall();
98858
- const stepAgeMs = step?.durationMs ?? null;
99452
+ const stepAgeMs = step2?.durationMs ?? null;
98859
99453
  const alreadyAcked = queuedStatusMsgIds.has(key) || busyAckPostedKeys.has(key);
98860
99454
  const fire = shouldPostBusyAck({ gateDecision, midToolCall, stepAgeMs, alreadyAcked });
98861
99455
  if (!fire) {
@@ -98879,10 +99473,10 @@ function maybePostBusyAck(gateDecision, chatId, threadId) {
98879
99473
  busyAckPostedKeys.add(key);
98880
99474
  const text5 = formatBusyAckText({
98881
99475
  gateDecision,
98882
- toolName: step?.name ?? null,
98883
- toolLabel: step?.label ?? null
99476
+ toolName: step2?.name ?? null,
99477
+ toolLabel: step2?.label ?? null
98884
99478
  });
98885
- process.stderr.write(`telegram gateway: mid-flight busy ack chat=${chatId} thread=${threadId ?? "-"} decision=${gateDecision} step=${step?.name ?? "-"} step_age_ms=${step?.durationMs ?? "-"}
99479
+ process.stderr.write(`telegram gateway: mid-flight busy ack chat=${chatId} thread=${threadId ?? "-"} decision=${gateDecision} step=${step2?.name ?? "-"} step_age_ms=${step2?.durationMs ?? "-"}
98886
99480
  `);
98887
99481
  postBusyAck(chatId, threadId, text5);
98888
99482
  }
@@ -100842,10 +101436,7 @@ var TOOL_PIN_TTL_MS = (() => {
100842
101436
  return Number.isFinite(v) && v > 0 ? v : 604800000;
100843
101437
  })();
100844
101438
  function statusPinApi() {
100845
- return {
100846
- pinChatMessage: (chat_id, message_id, opts) => robustApiCall(() => lockedBot.api.pinChatMessage(chat_id, message_id, opts), { chat_id: String(chat_id), verb: "status-pin.pin" }),
100847
- unpinChatMessage: (chat_id, message_id) => robustApiCall(() => lockedBot.api.unpinChatMessage(chat_id, message_id), { chat_id: String(chat_id), verb: "status-pin.unpin" })
100848
- };
101439
+ return createStatusPinApi(() => lockedBot, robustApiCall);
100849
101440
  }
100850
101441
  async function statusPinBootCleanup() {
100851
101442
  if (!statusPinPersistEnabled && !bannerPinPersistEnabled && !toolPinPersistEnabled)
@@ -101165,25 +101756,25 @@ var dmPinSweeper = createDmPinSweeper({
101165
101756
  eligible: () => dmPinSweepEligible,
101166
101757
  log: (line) => process.stderr.write(line)
101167
101758
  });
101168
- async function runBootPinCleanupAndDmSweep() {
101169
- let dmChatIds = [];
101170
- try {
101171
- dmChatIds = collectDmChatIdsFromStores({
101759
+ function runBootPinCleanupAndDmSweep() {
101760
+ return runBootPinSweepSteps({
101761
+ scanDmChatIds: () => collectDmChatIdsFromStores({
101172
101762
  statusPins: statusPinPersistEnabled || bannerPinPersistEnabled || toolPinPersistEnabled ? loadStatusPins(STATUS_PIN_STORE_PATH, statusPinStoreFs) : [],
101173
101763
  activityCards: activityCardPersistEnabled ? loadActivityCards2(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs) : [],
101174
101764
  queuedCards: queuedCardPersistEnabled ? loadQueuedCards(QUEUED_CARD_STORE_PATH, queuedCardStoreFs) : []
101175
- });
101176
- } catch (err) {
101177
- process.stderr.write(`telegram gateway: dm-pin-sweep: store scan failed: ${err.message}
101178
- `);
101179
- }
101180
- await statusPinBootCleanup();
101181
- await activityCardBootReaper();
101182
- await queuedCardBootReaper();
101183
- dmPinSweepEligible = true;
101184
- for (const id of dmChatIds)
101185
- await dmPinSweeper.sweep(id);
101765
+ }),
101766
+ statusPinCleanup: statusPinBootCleanup,
101767
+ activityCardReaper: activityCardBootReaper,
101768
+ queuedCardReaper: queuedCardBootReaper,
101769
+ enableDmSweep: () => {
101770
+ dmPinSweepEligible = true;
101771
+ },
101772
+ sweepDm: (id) => dmPinSweeper.sweep(id),
101773
+ log: (line) => process.stderr.write(line)
101774
+ });
101186
101775
  }
101776
+ var bootPinSweepGate = createBootSweepGate({ run: runBootPinCleanupAndDmSweep, onError: (err) => process.stderr.write(`telegram gateway: boot pin cleanup / DM sweep failed: ${err.message}
101777
+ `) });
101187
101778
  var progressDriver = null;
101188
101779
  var unpinProgressCardForChat = null;
101189
101780
  var getPinnedProgressCardMessageId = null;
@@ -101255,7 +101846,7 @@ if (isGatewayMain) {
101255
101846
  if (carrierAgentDir != null)
101256
101847
  consumeSessionModelCarrierOnHealthyBoot(carrierAgentDir);
101257
101848
  }
101258
- runBootPinCleanupAndDmSweep();
101849
+ bootPinSweepGate.arm();
101259
101850
  } catch (err) {
101260
101851
  process.stderr.write(`telegram gateway: boot.lock_acquire_failed err=${err.message} agent=${SWITCHROOM_AGENT_NAME}
101261
101852
  `);
@@ -101268,7 +101859,7 @@ if (isGatewayMain) {
101268
101859
  if (carrierAgentDir != null)
101269
101860
  consumeSessionModelCarrierOnHealthyBoot(carrierAgentDir);
101270
101861
  }
101271
- runBootPinCleanupAndDmSweep();
101862
+ bootPinSweepGate.arm();
101272
101863
  } catch (writeErr) {
101273
101864
  process.stderr.write(`telegram gateway: writePidFile failed: ${writeErr}
101274
101865
  `);
@@ -105097,7 +105688,7 @@ ${preBlock(formatSwitchroomOutput(detail))}`, { html: true });
105097
105688
  }
105098
105689
  function readRecentDenialsForAgent(agentName3, windowMs, limit) {
105099
105690
  try {
105100
- const auditPath = join65(homedir20(), ".switchroom", "vault-audit.log");
105691
+ const auditPath = join65(homedir19(), ".switchroom", "vault-audit.log");
105101
105692
  if (!existsSync58(auditPath))
105102
105693
  return [];
105103
105694
  const raw = readFileSync58(auditPath, "utf8");
@@ -108608,6 +109199,11 @@ async function initGatewayBot() {
108608
109199
  bot = new import_grammy16.Bot(TOKEN);
108609
109200
  installTgPostLogger(bot);
108610
109201
  installRichMarkdownGuard(bot);
109202
+ installEditFloodFuse(bot, {
109203
+ enabled: process.env.SWITCHROOM_EDIT_FUSE !== "0",
109204
+ onTrip: (i) => process.stderr.write(`edit-flood-fuse ${i.action} method=${i.method} key=${i.key}
109205
+ `)
109206
+ });
108611
109207
  installUpdateTap(bot, (line) => process.stderr.write(line));
108612
109208
  bot.api.config.use(async (prev, method, payload, signal) => {
108613
109209
  try {
@@ -108698,6 +109294,7 @@ async function initGatewayBot() {
108698
109294
  MENTAL_MODEL_PROPOSE_MAX_PER_WINDOW
108699
109295
  });
108700
109296
  registerGatewayHandlers(bot, {});
109297
+ bootPinSweepGate.botReady();
108701
109298
  }
108702
109299
  var didOneTimeSetup = false;
108703
109300
  async function startGateway() {
@@ -108960,7 +109557,7 @@ async function startGateway() {
108960
109557
  return;
108961
109558
  }
108962
109559
  })();
108963
- const resolvedAgentDirForBootCard = agentDir ?? join65(homedir20(), ".switchroom", "agents", agentSlug);
109560
+ const resolvedAgentDirForBootCard = agentDir ?? join65(homedir19(), ".switchroom", "agents", agentSlug);
108964
109561
  const handle = await startBootCard(chatId, threadId, botApiForCard, {
108965
109562
  agentName: agentDisplayName,
108966
109563
  agentSlug,