switchroom 0.19.4 → 0.19.5

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 (32) hide show
  1. package/dist/auth-broker/index.js +7 -3
  2. package/dist/cli/autoaccept-poll.js +8 -2
  3. package/dist/cli/switchroom.js +20 -5
  4. package/dist/host-control/main.js +1 -1
  5. package/package.json +1 -1
  6. package/profiles/_base/start.sh.hbs +67 -4
  7. package/telegram-plugin/dist/gateway/gateway.js +524 -293
  8. package/telegram-plugin/gateway/command-format.ts +253 -0
  9. package/telegram-plugin/gateway/gateway-heartbeat.ts +72 -0
  10. package/telegram-plugin/gateway/gateway.ts +97 -255
  11. package/telegram-plugin/gateway/hang-restart-decision.ts +189 -0
  12. package/telegram-plugin/gateway/liveness-wiring.ts +35 -1
  13. package/telegram-plugin/gateway/session-model-file.ts +13 -0
  14. package/telegram-plugin/gateway/stream-render.ts +18 -1
  15. package/telegram-plugin/gateway/turn-active-marker.ts +29 -17
  16. package/telegram-plugin/gateway/worker-feed-dispatch.ts +139 -0
  17. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +87 -36
  18. package/telegram-plugin/hooks/silent-end-scan.mjs +263 -3
  19. package/telegram-plugin/render/line-start-guard.ts +76 -4
  20. package/telegram-plugin/rich-send.ts +8 -1
  21. package/telegram-plugin/tests/command-format.test.ts +212 -0
  22. package/telegram-plugin/tests/gateway-heartbeat.test.ts +70 -0
  23. package/telegram-plugin/tests/hang-restart-decision.test.ts +146 -0
  24. package/telegram-plugin/tests/hang-restart-marker-integration.test.ts +98 -0
  25. package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +86 -0
  26. package/telegram-plugin/tests/render/heading-guard.test.ts +114 -0
  27. package/telegram-plugin/tests/render/rich-corpus-seam-regression.test.ts +76 -0
  28. package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +63 -0
  29. package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +60 -16
  30. package/telegram-plugin/tests/silent-end-single-writer-election.test.ts +193 -0
  31. package/telegram-plugin/tests/silent-end.test.ts +60 -5
  32. package/telegram-plugin/tests/worker-feed-origin-race-defer.test.ts +321 -0
@@ -8674,10 +8674,42 @@ function guardAccidentalBlockConstructs(text) {
8674
8674
  }
8675
8675
  return out;
8676
8676
  }
8677
- var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST;
8677
+ function escapeAccidentalHeadingLine(line) {
8678
+ return line.replace(ACCIDENTAL_HEADING, "$1\\$2");
8679
+ }
8680
+ function guardAccidentalHeading(text) {
8681
+ if (!text.includes("#"))
8682
+ return text;
8683
+ const segments = splitProtectedSegments(text);
8684
+ let out = "";
8685
+ let atLineStart = true;
8686
+ for (const seg of segments) {
8687
+ if (seg.code) {
8688
+ out += seg.text;
8689
+ atLineStart = seg.text.endsWith(`
8690
+ `);
8691
+ continue;
8692
+ }
8693
+ const lines = seg.text.split(`
8694
+ `);
8695
+ for (let k = 0;k < lines.length; k++) {
8696
+ const lineIsAtStart = k === 0 ? atLineStart : true;
8697
+ const processed = lineIsAtStart ? escapeAccidentalHeadingLine(lines[k]) : lines[k];
8698
+ out += processed;
8699
+ if (k < lines.length - 1)
8700
+ out += `
8701
+ `;
8702
+ }
8703
+ atLineStart = seg.text.endsWith(`
8704
+ `);
8705
+ }
8706
+ return out;
8707
+ }
8708
+ var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST, ACCIDENTAL_HEADING;
8678
8709
  var init_line_start_guard = __esm(() => {
8679
8710
  ACCIDENTAL_BLOCKQUOTE = /^>[0-9=]/;
8680
8711
  ACCIDENTAL_ORDERED_LIST = /^(\d{4,})([.)])(\s|$)/;
8712
+ ACCIDENTAL_HEADING = /^([ \t]{0,3})(#{1,6})(?=[^\s#])/;
8681
8713
  });
8682
8714
 
8683
8715
  // render/inline-pairs-guard.ts
@@ -8727,6 +8759,7 @@ var init_inline_pairs_guard = __esm(() => {
8727
8759
  function guardAccidentalFormatting(markdown) {
8728
8760
  let out = markdown;
8729
8761
  out = guardAccidentalEmphasis(out);
8762
+ out = guardAccidentalHeading(out);
8730
8763
  out = guardAccidentalBlockConstructs(out);
8731
8764
  out = guardAccidentalInlinePairs(out);
8732
8765
  out = guardDollarMath(out);
@@ -37338,7 +37371,7 @@ __export(exports_tmux2, {
37338
37371
  captureAgentPane: () => captureAgentPane2
37339
37372
  });
37340
37373
  import { execFileSync as execFileSync8 } from "node:child_process";
37341
- import { chmodSync as chmodSync13, mkdirSync as mkdirSync45, readdirSync as readdirSync12, statSync as statSync18, unlinkSync as unlinkSync25, writeFileSync as writeFileSync46 } from "node:fs";
37374
+ import { chmodSync as chmodSync13, mkdirSync as mkdirSync46, readdirSync as readdirSync12, statSync as statSync18, unlinkSync as unlinkSync25, writeFileSync as writeFileSync47 } from "node:fs";
37342
37375
  import { resolve as resolve12 } from "node:path";
37343
37376
  function captureAgentPane2(opts) {
37344
37377
  const { agentName: agentName3, agentDir, reason } = opts;
@@ -37350,7 +37383,7 @@ function captureAgentPane2(opts) {
37350
37383
  const reasonSlug = sanitizeReason2(reason);
37351
37384
  const outPath = resolve12(outDir, `${ts}-${reasonSlug}.txt`);
37352
37385
  try {
37353
- mkdirSync45(outDir, { recursive: true, mode: 448 });
37386
+ mkdirSync46(outDir, { recursive: true, mode: 448 });
37354
37387
  } catch (err) {
37355
37388
  const msg = `mkdir crash-reports failed: ${err.message}`;
37356
37389
  console.error(`[tmux-capture] ${agentName3}: ${msg}`);
@@ -37389,7 +37422,7 @@ function captureAgentPane2(opts) {
37389
37422
  ` + `
37390
37423
  `;
37391
37424
  try {
37392
- writeFileSync46(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
37425
+ writeFileSync47(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
37393
37426
  mode: 384
37394
37427
  });
37395
37428
  } catch (err) {
@@ -38210,7 +38243,7 @@ function buildGrantedKeyboard(scope) {
38210
38243
  const btn = scopeToOpenInDriveButton(scope);
38211
38244
  if (btn === null)
38212
38245
  return;
38213
- return new import_grammy13.InlineKeyboard().url(btn.text, btn.url);
38246
+ return new import_grammy14.InlineKeyboard().url(btn.text, btn.url);
38214
38247
  }
38215
38248
  async function handleApprovalCallback(ctx, data) {
38216
38249
  const parsed = parseApprovalCallback(data);
@@ -38256,11 +38289,11 @@ async function handleApprovalCallback(ctx, data) {
38256
38289
  } catch {}
38257
38290
  await ctx.answerCallbackQuery({ text: granted ? "Approved" : "Denied" });
38258
38291
  }
38259
- var import_grammy13;
38292
+ var import_grammy14;
38260
38293
  var init_approval_callback = __esm(() => {
38261
38294
  init_approval_card();
38262
38295
  init_client3();
38263
- import_grammy13 = __toESM(require_mod2(), 1);
38296
+ import_grammy14 = __toESM(require_mod2(), 1);
38264
38297
  });
38265
38298
 
38266
38299
  // ../src/telegram/materialize-bot-token.ts
@@ -38433,14 +38466,14 @@ var init_approvals_commands = __esm(() => {
38433
38466
  });
38434
38467
 
38435
38468
  // gateway/gateway.ts
38436
- var import_grammy14 = __toESM(require_mod(), 1);
38469
+ var import_grammy15 = __toESM(require_mod(), 1);
38437
38470
  var import_runner3 = __toESM(require_mod3(), 1);
38438
38471
  import { randomBytes as randomBytes10, createHash as createHash4 } from "crypto";
38439
38472
  import { execFileSync as execFileSync9, execSync as execSync2, spawn as spawn2 } from "child_process";
38440
38473
  import {
38441
38474
  readFileSync as readFileSync58,
38442
- writeFileSync as writeFileSync47,
38443
- mkdirSync as mkdirSync46,
38475
+ writeFileSync as writeFileSync48,
38476
+ mkdirSync as mkdirSync47,
38444
38477
  readdirSync as readdirSync13,
38445
38478
  rmSync as rmSync6,
38446
38479
  statSync as statSync19,
@@ -38454,7 +38487,7 @@ import {
38454
38487
  appendFileSync as appendFileSync7
38455
38488
  } from "fs";
38456
38489
  import { homedir as homedir19 } from "os";
38457
- import { join as join59, sep as sep4, basename as basename15 } from "path";
38490
+ import { join as join60, sep as sep4, basename as basename15 } from "path";
38458
38491
 
38459
38492
  // plugin-logger.ts
38460
38493
  import { appendFileSync, mkdirSync, renameSync, statSync, existsSync } from "fs";
@@ -50451,6 +50484,25 @@ function sweepPermissionTtl(deps) {
50451
50484
  return expired;
50452
50485
  }
50453
50486
 
50487
+ // gateway/hang-restart-decision.ts
50488
+ var HUMAN_WAIT_TOOLS = new Set(["ask_user"]);
50489
+ var BACKGROUND_TOOLS = new Set(["Task", "Agent"]);
50490
+ var HANG_PROTECTED_EXACT = new Set([
50491
+ "Task",
50492
+ "Agent",
50493
+ "Bash",
50494
+ "WebFetch",
50495
+ "WebSearch"
50496
+ ]);
50497
+ var DEFAULT_HANG_STALENESS_MS = 300000;
50498
+ function hangStalenessMs(env = process.env) {
50499
+ const raw = env.TURN_HANG_SECS;
50500
+ const n = Number(raw);
50501
+ if (!Number.isFinite(n) || n <= 0)
50502
+ return DEFAULT_HANG_STALENESS_MS;
50503
+ return Math.floor(n * 1000);
50504
+ }
50505
+
50454
50506
  // gateway/missed-approvals-store.ts
50455
50507
  import { readFileSync as readFileSync13, writeFileSync as writeFileSync11, unlinkSync as unlinkSync7 } from "node:fs";
50456
50508
  import { join as join13 } from "node:path";
@@ -73377,15 +73429,142 @@ function backOffOpenInline2(text4, cut) {
73377
73429
  return earliest;
73378
73430
  }
73379
73431
 
73432
+ // gateway/command-format.ts
73433
+ init_format();
73434
+ var import_grammy8 = __toESM(require_mod2(), 1);
73435
+ function formatSwitchroomOutput(output, maxLen = RICH_MESSAGE_MAX_CHARS) {
73436
+ const trimmed = output.trim();
73437
+ if (trimmed.length <= maxLen)
73438
+ return trimmed;
73439
+ return trimmed.slice(0, maxLen - 20) + `
73440
+ ... (truncated)`;
73441
+ }
73442
+ function stripAnsi4(text4) {
73443
+ return text4.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
73444
+ }
73445
+ function escapeHtmlForTg2(text4) {
73446
+ return text4.replace(/([\\`*_~=\[\]|])/g, "\\$1");
73447
+ }
73448
+ function preBlock(text4) {
73449
+ return "```\n" + text4.replace(/```/g, "`\u200b``") + "\n```";
73450
+ }
73451
+ function getCommandArgs(ctx) {
73452
+ const fromMatch = typeof ctx.match === "string" ? ctx.match.trim() : "";
73453
+ if (fromMatch)
73454
+ return fromMatch;
73455
+ const text4 = ctx.msg?.text ?? ctx.message?.text ?? "";
73456
+ const m = text4.match(/^\/\S+\s+([\s\S]*)$/);
73457
+ return m ? m[1].trim() : "";
73458
+ }
73459
+ function hasDemoFlag(args) {
73460
+ return /(?:^|\s)demo$/i.test(args.trim());
73461
+ }
73462
+ function assertSafeAgentName(name) {
73463
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name) && name !== "all") {
73464
+ throw new Error(`invalid agent name: ${name}`);
73465
+ }
73466
+ }
73467
+ function formatAuthOutputForTelegram(output) {
73468
+ const trimmed = stripAnsi4(output).trim();
73469
+ const url = trimmed.match(/https:\/\/\S+/)?.[0] ?? null;
73470
+ const lines = trimmed.split(/\n+/).map((l) => l.trim()).filter(Boolean);
73471
+ if (!url)
73472
+ return { text: preBlock(formatSwitchroomOutput(trimmed)), url: null };
73473
+ const body = lines.filter((line) => {
73474
+ if (line === url)
73475
+ return false;
73476
+ if (line.startsWith("switchroom auth code"))
73477
+ return false;
73478
+ if (line.startsWith("switchroom auth cancel"))
73479
+ return false;
73480
+ if (line.startsWith("Use 'tmux attach"))
73481
+ return false;
73482
+ if (line.startsWith("After Claude shows you a browser code"))
73483
+ return false;
73484
+ if (line.startsWith("Then finish with:"))
73485
+ return false;
73486
+ if (line.startsWith("Cancel with:"))
73487
+ return false;
73488
+ return true;
73489
+ });
73490
+ const rendered = body.map((line) => {
73491
+ if (line.startsWith("Started Claude auth") || line.startsWith("Auth session already running"))
73492
+ return `**${escapeHtmlForTg2(line)}**`;
73493
+ if (line.startsWith("Open this URL"))
73494
+ return `_${escapeHtmlForTg2(line)}_`;
73495
+ return escapeHtmlForTg2(line);
73496
+ });
73497
+ rendered.push("", "\uD83D\uDC47 Tap **\uD83D\uDD10 Open Claude auth** below, then **reply with the browser code**.", "", `_Wrong Anthropic account getting authorized? Long-press the URL below and choose "Copy Link" or "Open in Browser" \u2014 lands in your main browser where the right account is signed in, bypassing Telegram's in-app browser cookies._`, "", url);
73498
+ return { text: rendered.join(`
73499
+ `), url };
73500
+ }
73501
+ function buildAuthUrlKeyboard(authorizeUrl) {
73502
+ return new import_grammy8.InlineKeyboard().url("\uD83D\uDD10 Open Claude auth", authorizeUrl);
73503
+ }
73504
+ function buildDeferredSecretKeyboard(deferKey) {
73505
+ const unlockData = `vd:unlock:${deferKey}`;
73506
+ const cancelData = `vd:cancel:${deferKey}`;
73507
+ if (unlockData.length > 64 || cancelData.length > 64) {
73508
+ process.stderr.write(`telegram gateway: callback_data overflow \u2014 deferKey=${deferKey} unlockLen=${unlockData.length} cancelLen=${cancelData.length}
73509
+ `);
73510
+ throw new Error(`callback_data overflow: deferKey too long (${deferKey.length} chars)`);
73511
+ }
73512
+ return new import_grammy8.InlineKeyboard().text("\uD83D\uDD13 Unlock vault & save", unlockData).text("\uD83D\uDDD1 Discard", cancelData);
73513
+ }
73514
+ function renderVaultOpFailure(verbLabel, cliOutput, key) {
73515
+ const parsed = parseVaultCliError(cliOutput);
73516
+ const verb = verbLabel === "delete" ? "remove" : verbLabel;
73517
+ const rendered = renderVaultCliError(parsed, { verb, key });
73518
+ if (rendered.suppressRaw)
73519
+ return rendered.html;
73520
+ return `**vault ${verbLabel} failed:**
73521
+ ${preBlock(cliOutput)}`;
73522
+ }
73523
+ function statusIcon(status) {
73524
+ if (status === "active" || status === "running")
73525
+ return "\uD83D\uDFE2";
73526
+ if (status === "inactive" || status === "stopped" || status === "dead")
73527
+ return "\uD83D\uDD34";
73528
+ if (status === "failed")
73529
+ return "\u26a0\ufe0f";
73530
+ return "\u26aa";
73531
+ }
73532
+ function renderAuthCodeOutcome(outcome) {
73533
+ if (!outcome || outcome.kind === "success")
73534
+ return null;
73535
+ const tail = outcome.paneTailText ? `
73536
+ _${escapeHtmlForTg2(outcome.paneTailText)}_` : "";
73537
+ switch (outcome.kind) {
73538
+ case "invalid-code":
73539
+ case "expired-code":
73540
+ return `Code rejected by Claude \u2014 tap **Restart flow** for a fresh URL.${tail}`;
73541
+ case "pane-not-ready":
73542
+ return `Auth pane not ready \u2014 tap **Retry**.`;
73543
+ case "timeout":
73544
+ return `Still waiting after 2 min \u2014 tap **Retry** or check \`switchroom auth list\`.${tail}`;
73545
+ }
73546
+ }
73547
+ function buildDoctorScopeKeyboard() {
73548
+ return new import_grammy8.InlineKeyboard().text("\uD83E\uDE7A Whole fleet", "dr:fleet").text("\uD83E\uDE7A This agent", "dr:self");
73549
+ }
73550
+ function formatDoctorReport(raw) {
73551
+ const trimmed = stripAnsi4(raw).trim();
73552
+ if (!trimmed)
73553
+ return "doctor: no output";
73554
+ const pretty = trimmed.replace(/^( *)\u2713 /gm, "$1\uD83D\uDFE2 ").replace(/^( *)\u2717 /gm, "$1\uD83D\uDD34 ").replace(/^( *)! /gm, "$1\uD83D\uDFE1 ");
73555
+ return preBlock(formatSwitchroomOutput(pretty));
73556
+ }
73557
+
73380
73558
  // rich-send.ts
73381
73559
  init_dollar_math_guard();
73382
73560
  init_emphasis_guard();
73383
73561
  init_line_start_guard();
73384
73562
  init_inline_pairs_guard();
73385
- var import_grammy8 = __toESM(require_mod2(), 1);
73563
+ var import_grammy9 = __toESM(require_mod2(), 1);
73386
73564
  function guardAccidentalFormatting2(markdown) {
73387
73565
  let out = markdown;
73388
73566
  out = guardAccidentalEmphasis(out);
73567
+ out = guardAccidentalHeading(out);
73389
73568
  out = guardAccidentalBlockConstructs(out);
73390
73569
  out = guardAccidentalInlinePairs(out);
73391
73570
  out = guardDollarMath(out);
@@ -73537,7 +73716,7 @@ function scrubVoice2(text4) {
73537
73716
  // gateway/outbound-send-path.ts
73538
73717
  init_format();
73539
73718
  init_text_voice_scrub();
73540
- var import_grammy9 = __toESM(require_mod2(), 1);
73719
+ var import_grammy10 = __toESM(require_mod2(), 1);
73541
73720
  import { statSync as statSync9 } from "fs";
73542
73721
  import { extname } from "path";
73543
73722
 
@@ -74326,7 +74505,7 @@ async function sendReplyChunks(deps, state3) {
74326
74505
  return { threadId, previewMessageId };
74327
74506
  }
74328
74507
  function classifyReadBackError(err) {
74329
- if (err instanceof import_grammy9.GrammyError && err.error_code === 400) {
74508
+ if (err instanceof import_grammy10.GrammyError && err.error_code === 400) {
74330
74509
  const d = (err.description || "").toLowerCase();
74331
74510
  if (d.includes("message to edit not found"))
74332
74511
  return "absent";
@@ -74901,7 +75080,7 @@ ${url}`;
74901
75080
  opts.message_thread_id = tid;
74902
75081
  else
74903
75082
  delete opts.message_thread_id;
74904
- return lockedBot.api.sendVoice(chat_id, new import_grammy9.InputFile(Buffer.from(oggBytes)), opts);
75083
+ return lockedBot.api.sendVoice(chat_id, new import_grammy10.InputFile(Buffer.from(oggBytes)), opts);
74905
75084
  }, { threadId, chat_id, verb: "sendVoice" });
74906
75085
  sentIds.push(sentVoice.message_id);
74907
75086
  logOutbound("reply", chat_id, sentVoice.message_id, voiceOutPlan?.ttsChunks[v]?.length ?? 0, `voice-note=${v + 1}/${voiceOggs.length}`);
@@ -74974,12 +75153,12 @@ ${url}`;
74974
75153
  ...replyParams,
74975
75154
  ...tid != null ? { message_thread_id: tid } : {}
74976
75155
  };
74977
- return lockedBot.api.sendDocument(chat_id, new import_grammy9.InputFile(f), baseOpts);
75156
+ return lockedBot.api.sendDocument(chat_id, new import_grammy10.InputFile(f), baseOpts);
74978
75157
  }, { threadId, chat_id, verb: "sendDocument" });
74979
75158
  if (allPhotos) {
74980
75159
  const media = files.map((f) => ({
74981
75160
  type: "photo",
74982
- media: new import_grammy9.InputFile(f)
75161
+ media: new import_grammy10.InputFile(f)
74983
75162
  }));
74984
75163
  let sent;
74985
75164
  try {
@@ -75012,7 +75191,7 @@ ${url}`;
75012
75191
  sentIds.push(m.message_id);
75013
75192
  } else {
75014
75193
  for (const f of files) {
75015
- const input = new import_grammy9.InputFile(f);
75194
+ const input = new import_grammy10.InputFile(f);
75016
75195
  const isPhoto = sendableAsPhoto(f);
75017
75196
  let sent;
75018
75197
  try {
@@ -76202,6 +76381,15 @@ function removeTurnActiveMarker(stateDir) {
76202
76381
  unlinkSync13(join31(stateDir, TURN_ACTIVE_MARKER_FILE));
76203
76382
  } catch {}
76204
76383
  }
76384
+ function readTurnActiveMarkerAgeMs(stateDir, now) {
76385
+ const path2 = join31(stateDir, TURN_ACTIVE_MARKER_FILE);
76386
+ try {
76387
+ const st = statSync10(path2);
76388
+ return (now ?? Date.now()) - st.mtimeMs;
76389
+ } catch {
76390
+ return null;
76391
+ }
76392
+ }
76205
76393
 
76206
76394
  // gateway/turn-end-gate-backstop.ts
76207
76395
  function withTurnEndGateBackstop(key, endingTurn, body, deps) {
@@ -77149,10 +77337,14 @@ function handleSessionEvent(deps, ev) {
77149
77337
  ended_via: outboundMetrics.outboundCount > 0 ? "reply" : "silent"
77150
77338
  });
77151
77339
  if (turnEndDecision === "reprompt") {
77340
+ const gatewayCapturedEmpty = turn.capturedText.join(`
77341
+
77342
+ `).trim().length === 0;
77343
+ const proseMinChars = !turn.replyCalled && gatewayCapturedEmpty ? 1 : CAPTURED_PROSE_MIN_CHARS;
77152
77344
  const proseDecision = CAPTURED_PROSE_DELIVERY_ENABLED ? decideCapturedProseDelivery({
77153
77345
  turnKey: tKey,
77154
77346
  turnId: turn.turnId,
77155
- minChars: CAPTURED_PROSE_MIN_CHARS
77347
+ minChars: proseMinChars
77156
77348
  }) : { deliver: false, reason: "no-state" };
77157
77349
  if (proseDecision.deliver && proseDecision.text != null) {
77158
77350
  process.stderr.write(`telegram gateway: captured-prose delivery engaged on first silent-end chat=${chatId} turnKey=${tKey} (#3227)
@@ -78555,7 +78747,7 @@ async function loadFromAuthBroker(options = {}) {
78555
78747
  }
78556
78748
 
78557
78749
  // gateway/folder-picker-handler.ts
78558
- var import_grammy10 = __toESM(require_mod2(), 1);
78750
+ var import_grammy11 = __toESM(require_mod2(), 1);
78559
78751
 
78560
78752
  // ../src/drive/folder-picker.ts
78561
78753
  var DRIVE_ID_RE2 = /^[A-Za-z0-9_-]+$/;
@@ -78874,7 +79066,7 @@ async function recordGrantAndConfirm(ctx, deps, parsed) {
78874
79066
  const confirmText = `\u2705 Granted ${deps.agentName} access to folder ${parsed.folder_id}
78875
79067
  ` + `Scope: \`${scope}\`
78876
79068
  ` + `Revoke with: \`/approvals revoke ${decisionId}\``;
78877
- const kb = new import_grammy10.InlineKeyboard().url("\uD83D\uDCD6 Open in Drive", `https://drive.google.com/drive/folders/${parsed.folder_id}`);
79069
+ const kb = new import_grammy11.InlineKeyboard().url("\uD83D\uDCD6 Open in Drive", `https://drive.google.com/drive/folders/${parsed.folder_id}`);
78878
79070
  try {
78879
79071
  await ctx.editMessageText({ markdown: confirmText }, {
78880
79072
  reply_markup: kb
@@ -78883,7 +79075,7 @@ async function recordGrantAndConfirm(ctx, deps, parsed) {
78883
79075
  await ctx.answerCallbackQuery({ text: "Allowed" });
78884
79076
  }
78885
79077
  function toKeyboard(spec) {
78886
- const kb = new import_grammy10.InlineKeyboard;
79078
+ const kb = new import_grammy11.InlineKeyboard;
78887
79079
  for (let r = 0;r < spec.rows.length; r++) {
78888
79080
  const row = spec.rows[r];
78889
79081
  for (const btn of row) {
@@ -80346,6 +80538,9 @@ function writeSessionModelFile(agentDir, model, configuredDefaultAtWrite) {
80346
80538
  if (!isValidModelArg(model)) {
80347
80539
  throw new Error(`refusing to persist non-canonical session model token: ${JSON.stringify(model)}`);
80348
80540
  }
80541
+ try {
80542
+ rmSync4(join35(agentDir, SESSION_MODEL_BOOT_ATTEMPTS_FILE), { force: true });
80543
+ } catch {}
80349
80544
  atomicWrite(join35(agentDir, SESSION_MODEL_FILE), serializeSessionModel({ model, configuredDefaultAtWrite, ts: Date.now() }));
80350
80545
  }
80351
80546
  function readSessionModelFileRaw(agentDir) {
@@ -81151,20 +81346,20 @@ function registerOpsInfoCommands(bot, deps) {
81151
81346
  isAuthorizedSender,
81152
81347
  getMyAgentName,
81153
81348
  switchroomReply,
81154
- buildDoctorScopeKeyboard,
81349
+ buildDoctorScopeKeyboard: buildDoctorScopeKeyboard2,
81155
81350
  renderSelfDoctor,
81156
- preBlock,
81157
- formatSwitchroomOutput,
81158
- getCommandArgs,
81159
- assertSafeAgentName,
81351
+ preBlock: preBlock2,
81352
+ formatSwitchroomOutput: formatSwitchroomOutput2,
81353
+ getCommandArgs: getCommandArgs2,
81354
+ assertSafeAgentName: assertSafeAgentName2,
81160
81355
  runSwitchroomCommand,
81161
81356
  switchroomExecCombined,
81162
- stripAnsi: stripAnsi4,
81163
- hasDemoFlag,
81164
- escapeHtmlForTg: escapeHtmlForTg2
81357
+ stripAnsi: stripAnsi5,
81358
+ hasDemoFlag: hasDemoFlag2,
81359
+ escapeHtmlForTg: escapeHtmlForTg3
81165
81360
  } = deps;
81166
81361
  function formatWhoamiCard(v, demo = false) {
81167
- const esc = escapeHtmlForTg2;
81362
+ const esc = escapeHtmlForTg3;
81168
81363
  const yn = (b) => b ? "\u2713" : "\u2717";
81169
81364
  const lines = [];
81170
81365
  lines.push(`\uD83D\uDC64 **${esc(v.name ?? "?")}** \u00b7 ${esc(v.tier ?? "standard")}`);
@@ -81196,20 +81391,20 @@ function registerOpsInfoCommands(bot, deps) {
81196
81391
  if (AGENT_ADMIN && hostdWillBeUsed(getMyAgentName())) {
81197
81392
  await switchroomReply(ctx, "\uD83E\uDE7A **Doctor** \u2014 which scope?", {
81198
81393
  html: true,
81199
- reply_markup: buildDoctorScopeKeyboard()
81394
+ reply_markup: buildDoctorScopeKeyboard2()
81200
81395
  });
81201
81396
  return;
81202
81397
  }
81203
81398
  await renderSelfDoctor(ctx);
81204
81399
  } catch (err) {
81205
81400
  await switchroomReply(ctx, `**doctor failed:**
81206
- ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
81401
+ ${preBlock2(formatSwitchroomOutput2(err.message ?? "unknown error"))}`, { html: true });
81207
81402
  }
81208
81403
  });
81209
81404
  bot.command("grant", async (ctx) => {
81210
81405
  if (!isAuthorizedSender(ctx))
81211
81406
  return;
81212
- const parts = getCommandArgs(ctx).split(/\s+/).filter(Boolean);
81407
+ const parts = getCommandArgs2(ctx).split(/\s+/).filter(Boolean);
81213
81408
  if (parts.length === 0) {
81214
81409
  await switchroomReply(ctx, "Usage: /grant <tool> or /grant <agent> <tool>");
81215
81410
  return;
@@ -81224,7 +81419,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81224
81419
  tool = parts.slice(1).join(" ");
81225
81420
  }
81226
81421
  try {
81227
- assertSafeAgentName(agentName3);
81422
+ assertSafeAgentName2(agentName3);
81228
81423
  } catch {
81229
81424
  await switchroomReply(ctx, "Invalid agent name.");
81230
81425
  return;
@@ -81234,7 +81429,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81234
81429
  bot.command("dangerous", async (ctx) => {
81235
81430
  if (!isAuthorizedSender(ctx))
81236
81431
  return;
81237
- const parts = getCommandArgs(ctx).split(/\s+/).filter(Boolean);
81432
+ const parts = getCommandArgs2(ctx).split(/\s+/).filter(Boolean);
81238
81433
  let agentName3;
81239
81434
  let off = false;
81240
81435
  if (parts.length === 0) {
@@ -81248,7 +81443,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81248
81443
  off = true;
81249
81444
  }
81250
81445
  try {
81251
- assertSafeAgentName(agentName3);
81446
+ assertSafeAgentName2(agentName3);
81252
81447
  } catch {
81253
81448
  await switchroomReply(ctx, "Invalid agent name.");
81254
81449
  return;
@@ -81263,7 +81458,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81263
81458
  return;
81264
81459
  const agentName3 = (typeof ctx.match === "string" ? ctx.match : "").trim() || getMyAgentName();
81265
81460
  try {
81266
- assertSafeAgentName(agentName3);
81461
+ assertSafeAgentName2(agentName3);
81267
81462
  } catch {
81268
81463
  await switchroomReply(ctx, "Invalid agent name.");
81269
81464
  return;
@@ -81280,21 +81475,21 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81280
81475
  } catch (err) {
81281
81476
  output = err.stdout ?? err.message ?? "version failed";
81282
81477
  }
81283
- const trimmed = stripAnsi4(output).trim();
81478
+ const trimmed = stripAnsi5(output).trim();
81284
81479
  if (!trimmed) {
81285
81480
  await switchroomReply(ctx, "version: no output");
81286
81481
  return;
81287
81482
  }
81288
- await switchroomReply(ctx, preBlock(formatSwitchroomOutput(trimmed)), { html: true });
81483
+ await switchroomReply(ctx, preBlock2(formatSwitchroomOutput2(trimmed)), { html: true });
81289
81484
  } catch (err) {
81290
81485
  await switchroomReply(ctx, `**version failed:**
81291
- ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
81486
+ ${preBlock2(formatSwitchroomOutput2(err.message ?? "unknown error"))}`, { html: true });
81292
81487
  }
81293
81488
  });
81294
81489
  bot.command("whoami", async (ctx) => {
81295
81490
  if (!isAuthorizedSender(ctx))
81296
81491
  return;
81297
- const demo = hasDemoFlag(getCommandArgs(ctx));
81492
+ const demo = hasDemoFlag2(getCommandArgs2(ctx));
81298
81493
  try {
81299
81494
  let raw;
81300
81495
  try {
@@ -81302,18 +81497,18 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
81302
81497
  } catch (err) {
81303
81498
  raw = err.stdout ?? err.message ?? "whoami failed";
81304
81499
  }
81305
- const trimmed = stripAnsi4(raw).trim();
81500
+ const trimmed = stripAnsi5(raw).trim();
81306
81501
  let card;
81307
81502
  try {
81308
81503
  card = formatWhoamiCard(JSON.parse(trimmed.split(`
81309
81504
  `).pop() ?? trimmed), demo);
81310
81505
  } catch {
81311
- card = preBlock(formatSwitchroomOutput(trimmed || "whoami: no output"));
81506
+ card = preBlock2(formatSwitchroomOutput2(trimmed || "whoami: no output"));
81312
81507
  }
81313
81508
  await switchroomReply(ctx, card, { html: true });
81314
81509
  } catch (err) {
81315
81510
  await switchroomReply(ctx, `**whoami failed:**
81316
- ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
81511
+ ${preBlock2(formatSwitchroomOutput2(err.message ?? "unknown error"))}`, { html: true });
81317
81512
  }
81318
81513
  });
81319
81514
  bot.command("commands", async (ctx) => {
@@ -84359,7 +84554,7 @@ async function handleRequestMs365Approval(client3, msg, deps) {
84359
84554
 
84360
84555
  // gateway/diff-preview-card.ts
84361
84556
  init_format();
84362
- var import_grammy11 = __toESM(require_mod2(), 1);
84557
+ var import_grammy12 = __toESM(require_mod2(), 1);
84363
84558
  var REQUEST_ID_RE = /^[0-9a-f]{32}$/;
84364
84559
  var PENDING_FILE_ID_SENTINEL = "pending-create";
84365
84560
  function buildDiffPreviewCard(input) {
@@ -84377,7 +84572,7 @@ function buildDiffPreviewCard(input) {
84377
84572
  }
84378
84573
  const text4 = bodyLines.join(`
84379
84574
  `);
84380
- const kb = new import_grammy11.InlineKeyboard;
84575
+ const kb = new import_grammy12.InlineKeyboard;
84381
84576
  const ROW_BREAK_AFTER = [
84382
84577
  "apply_suggestion"
84383
84578
  ];
@@ -87221,6 +87416,49 @@ function purgeStaleTurnsForChat(chatId, keys, purger, isStale = () => true) {
87221
87416
  return { purged };
87222
87417
  }
87223
87418
 
87419
+ // gateway/hang-restart-decision.ts
87420
+ var HUMAN_WAIT_TOOLS2 = new Set(["ask_user"]);
87421
+ var BACKGROUND_TOOLS2 = new Set(["Task", "Agent"]);
87422
+ function classifyToolClass(toolName, opts = {}) {
87423
+ if (opts.awaitingApproval === true)
87424
+ return "human";
87425
+ if (HUMAN_WAIT_TOOLS2.has(toolName))
87426
+ return "human";
87427
+ if (BACKGROUND_TOOLS2.has(toolName))
87428
+ return "background";
87429
+ if (toolName === "Bash" && opts.backgroundBash === true)
87430
+ return "background";
87431
+ return "standard";
87432
+ }
87433
+ var HANG_PROTECTED_EXACT2 = new Set([
87434
+ "Task",
87435
+ "Agent",
87436
+ "Bash",
87437
+ "WebFetch",
87438
+ "WebSearch"
87439
+ ]);
87440
+ var HANG_PROTECTED_SUBSTRINGS = ["research", "perplexity", "crawl", "deep_research", "webkite"];
87441
+ function isHangRestartProtectedTool(toolName) {
87442
+ if (HANG_PROTECTED_EXACT2.has(toolName))
87443
+ return true;
87444
+ if (classifyToolClass(toolName) !== "standard")
87445
+ return true;
87446
+ const lower = toolName.toLowerCase();
87447
+ return HANG_PROTECTED_SUBSTRINGS.some((s) => lower.includes(s));
87448
+ }
87449
+ function decideHangRestart(input) {
87450
+ if (input.inFlightToolNames.length === 0)
87451
+ return { restart: false, reason: "not-mid-tool" };
87452
+ const protectedName = input.inFlightToolNames.find(isHangRestartProtectedTool);
87453
+ if (protectedName != null) {
87454
+ return { restart: false, reason: `protected-long-tool:${protectedName}` };
87455
+ }
87456
+ if (input.markerAgeMs != null && input.markerAgeMs < input.stalenessThresholdMs) {
87457
+ return { restart: false, reason: "marker-advancing" };
87458
+ }
87459
+ return { restart: true, reason: "mid-tool-marker-stale" };
87460
+ }
87461
+
87224
87462
  // gateway/update-status-line.ts
87225
87463
  function latestHostdPhase(tail) {
87226
87464
  if (!tail)
@@ -87276,7 +87514,8 @@ function buildSilencePokeOptions(deps) {
87276
87514
  getPendingInboundBuffer,
87277
87515
  trackRedeliveredInbound,
87278
87516
  closeActivityLane,
87279
- closeProgressLane
87517
+ closeProgressLane,
87518
+ hangRestart
87280
87519
  } = deps;
87281
87520
  return {
87282
87521
  thresholdsMs: { fallback: SILENCE_FALLBACK_MS, fallbackHardCeiling: SILENCE_FALLBACK_HARD_MS, floor: SILENCE_FLOOR_MS },
@@ -87324,6 +87563,21 @@ function buildSilencePokeOptions(deps) {
87324
87563
  endTurn2(ctx.key);
87325
87564
  return;
87326
87565
  }
87566
+ if (hangRestart != null && ctx.inFlightTools.length > 0) {
87567
+ const markerAgeMs = readTurnActiveMarkerAgeMs(STATE_DIR);
87568
+ const inFlightToolNames = ctx.inFlightTools.map((t) => t.name);
87569
+ const decision = decideHangRestart({
87570
+ inFlightToolNames,
87571
+ markerAgeMs,
87572
+ stalenessThresholdMs: hangRestart.stalenessThresholdMs
87573
+ });
87574
+ process.stderr.write(`telegram gateway: [hang-watchdog] fallback mid-tool chat=${ctx.chatId} ` + `thread=${ctx.threadId ?? "-"} silence_ms=${ctx.silenceMs} ` + `tools=${inFlightToolNames.join(",")} marker_age_ms=${markerAgeMs ?? "null"} ` + `restart=${decision.restart} reason=${decision.reason}
87575
+ `);
87576
+ if (decision.restart) {
87577
+ hangRestart.request(decision.reason, markerAgeMs ?? ctx.silenceMs);
87578
+ return;
87579
+ }
87580
+ }
87327
87581
  let text4 = null;
87328
87582
  const blockedOnApproval = activeStatusReactions.get(statusKey(ctx.chatId, ctx.threadId))?.isAwaiting() ?? false;
87329
87583
  const upd = getInFlightUpdate();
@@ -93121,7 +93375,7 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
93121
93375
  return false;
93122
93376
  }
93123
93377
  }
93124
- function readTurnActiveMarkerAgeMs(stateDir, now) {
93378
+ function readTurnActiveMarkerAgeMs2(stateDir, now) {
93125
93379
  const path2 = join56(stateDir, TURN_ACTIVE_MARKER_FILE2);
93126
93380
  try {
93127
93381
  const st = statSync16(path2);
@@ -93134,11 +93388,36 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
93134
93388
  return markerAgeMs ?? now - turnStartedAt;
93135
93389
  }
93136
93390
 
93391
+ // gateway/gateway-heartbeat.ts
93392
+ import { mkdirSync as mkdirSync44, utimesSync as utimesSync3, writeFileSync as writeFileSync45 } from "node:fs";
93393
+ import { join as join57 } from "node:path";
93394
+ var GATEWAY_HEARTBEAT_FILE = "gateway-heartbeat";
93395
+ var GATEWAY_HEARTBEAT_INTERVAL_MS = 15000;
93396
+ function touchGatewayHeartbeat(stateDir) {
93397
+ const path2 = join57(stateDir, GATEWAY_HEARTBEAT_FILE);
93398
+ const now = new Date;
93399
+ try {
93400
+ utimesSync3(path2, now, now);
93401
+ } catch {
93402
+ try {
93403
+ mkdirSync44(stateDir, { recursive: true });
93404
+ writeFileSync45(path2, `${Date.now()}
93405
+ `, { mode: 384 });
93406
+ } catch {}
93407
+ }
93408
+ }
93409
+ function startGatewayHeartbeat(stateDir, intervalMs = GATEWAY_HEARTBEAT_INTERVAL_MS) {
93410
+ touchGatewayHeartbeat(stateDir);
93411
+ const timer3 = setInterval(() => touchGatewayHeartbeat(stateDir), intervalMs);
93412
+ timer3.unref?.();
93413
+ return timer3;
93414
+ }
93415
+
93137
93416
  // ../src/build-info.ts
93138
- var VERSION = "0.19.4";
93139
- var COMMIT_SHA = "7cdf5428";
93140
- var COMMIT_DATE = "2026-07-20T11:09:10+10:00";
93141
- var LATEST_PR = 3457;
93417
+ var VERSION = "0.19.5";
93418
+ var COMMIT_SHA = "c5feaef8";
93419
+ var COMMIT_DATE = "2026-07-20T10:20:34Z";
93420
+ var LATEST_PR = 3474;
93142
93421
  var COMMITS_AHEAD_OF_TAG = 0;
93143
93422
 
93144
93423
  // gateway/boot-version.ts
@@ -93183,10 +93462,10 @@ function composeBootVersionString(inputs) {
93183
93462
  }
93184
93463
 
93185
93464
  // gateway/unhandled-rejection-policy.ts
93186
- var import_grammy12 = __toESM(require_mod2(), 1);
93465
+ var import_grammy13 = __toESM(require_mod2(), 1);
93187
93466
  function classifyRejection(err, opts = {}) {
93188
- const isGrammy = opts.isGrammyError != null ? opts.isGrammyError(err) : err instanceof import_grammy12.GrammyError;
93189
- const isHttp = opts.isHttpError != null ? opts.isHttpError(err) : err instanceof import_grammy12.HttpError;
93467
+ const isGrammy = opts.isGrammyError != null ? opts.isGrammyError(err) : err instanceof import_grammy13.GrammyError;
93468
+ const isHttp = opts.isHttpError != null ? opts.isHttpError(err) : err instanceof import_grammy13.HttpError;
93190
93469
  if (isHttp)
93191
93470
  return "log_only";
93192
93471
  if (err instanceof Error && err.message === FLOOD_WAIT_ACTIVE)
@@ -93216,11 +93495,11 @@ init_peercred();
93216
93495
  import * as net5 from "node:net";
93217
93496
  import * as fs2 from "node:fs";
93218
93497
  import { homedir as homedir18 } from "node:os";
93219
- import { join as join57 } from "node:path";
93498
+ import { join as join58 } from "node:path";
93220
93499
  var DEFAULT_TIMEOUT_MS4 = 2000;
93221
93500
  var UNLOCK_TIMEOUT_MS = 30000;
93222
- var LEGACY_SOCKET_PATH2 = join57(homedir18(), ".switchroom", "vault-broker.sock");
93223
- var OPERATOR_SOCKET_PATH2 = join57(homedir18(), ".switchroom", "broker-operator", "sock");
93501
+ var LEGACY_SOCKET_PATH2 = join58(homedir18(), ".switchroom", "vault-broker.sock");
93502
+ var OPERATOR_SOCKET_PATH2 = join58(homedir18(), ".switchroom", "broker-operator", "sock");
93224
93503
  function defaultBrokerSocketPath2() {
93225
93504
  if (fs2.existsSync(OPERATOR_SOCKET_PATH2))
93226
93505
  return OPERATOR_SOCKET_PATH2;
@@ -94102,8 +94381,8 @@ function resolveVaultApprovalPosture(broker) {
94102
94381
  }
94103
94382
 
94104
94383
  // registry/turns-schema.ts
94105
- import { chmodSync as chmodSync12, mkdirSync as mkdirSync44 } from "fs";
94106
- import { join as join58 } from "path";
94384
+ import { chmodSync as chmodSync12, mkdirSync as mkdirSync45 } from "fs";
94385
+ import { join as join59 } from "path";
94107
94386
  var DatabaseClass3 = null;
94108
94387
  function loadDatabaseClass3() {
94109
94388
  if (DatabaseClass3 != null)
@@ -94175,9 +94454,9 @@ function applySchema(db3) {
94175
94454
  }
94176
94455
  function openTurnsDb(agentDir) {
94177
94456
  const Database = loadDatabaseClass3();
94178
- const dir = join58(agentDir, "telegram");
94179
- mkdirSync44(dir, { recursive: true, mode: 448 });
94180
- const path2 = join58(dir, "registry.db");
94457
+ const dir = join59(agentDir, "telegram");
94458
+ mkdirSync45(dir, { recursive: true, mode: 448 });
94459
+ const path2 = join59(dir, "registry.db");
94181
94460
  const db3 = new Database(path2, { create: true });
94182
94461
  applySchema(db3);
94183
94462
  try {
@@ -94521,7 +94800,7 @@ function selectResumeBuilder(endedVia, opts) {
94521
94800
  }
94522
94801
 
94523
94802
  // gateway/bridge-dead-watchdog.ts
94524
- import { readFileSync as readFileSync56, writeFileSync as writeFileSync45, renameSync as renameSync20, unlinkSync as unlinkSync24 } from "node:fs";
94803
+ import { readFileSync as readFileSync56, writeFileSync as writeFileSync46, renameSync as renameSync20, unlinkSync as unlinkSync24 } from "node:fs";
94525
94804
 
94526
94805
  // gateway/cron-session.ts
94527
94806
  var CRON_IDENTITY_SUFFIX2 = "-cron";
@@ -94575,7 +94854,7 @@ function readFreshCrashLogTail(path2, opts = {}) {
94575
94854
  }
94576
94855
  function writeBridgeDeadEscalationMarker(path2, marker) {
94577
94856
  const tmp = `${path2}.tmp-${process.pid}-${Date.now()}`;
94578
- writeFileSync45(tmp, JSON.stringify(marker), "utf8");
94857
+ writeFileSync46(tmp, JSON.stringify(marker), "utf8");
94579
94858
  renameSync20(tmp, path2);
94580
94859
  }
94581
94860
  function consumeBridgeDeadEscalationMarker(path2, nowMs3 = Date.now(), maxAgeMs = ESCALATION_MARKER_MAX_AGE_MS) {
@@ -94900,6 +95179,46 @@ function handleWorkerResume(feed, agentId, log) {
94900
95179
  }
94901
95180
  log(`telegram gateway: worker ${agentId} card RE-SURFACED \u2014 resumed via SendMessage after a genuine terminal (issue #3373)`);
94902
95181
  }
95182
+ function decideWorkerFeedOriginDefer(input) {
95183
+ const { originResolved, cardExists, priorDeferrals, maxDeferrals } = input;
95184
+ if (originResolved || cardExists)
95185
+ return { defer: false, deferrals: 0 };
95186
+ const deferrals = priorDeferrals + 1;
95187
+ if (deferrals >= maxDeferrals)
95188
+ return { defer: false, deferrals };
95189
+ return { defer: true, deferrals };
95190
+ }
95191
+ function decideWorkerFeedDestination(input) {
95192
+ const { origin, cardExists, priorDeferrals, maxDeferrals, fleetChatId, stampChatId, stampThreadId, ownerDm } = input;
95193
+ const originResolved = origin != null;
95194
+ const { defer, deferrals } = decideWorkerFeedOriginDefer({
95195
+ originResolved,
95196
+ cardExists,
95197
+ priorDeferrals,
95198
+ maxDeferrals
95199
+ });
95200
+ if (defer)
95201
+ return { action: "defer", deferrals };
95202
+ const exhausted = !originResolved && !cardExists;
95203
+ const usingStampFallback = fleetChatId.length === 0;
95204
+ const workerFleetChatId = usingStampFallback ? stampChatId ?? fleetChatId : fleetChatId;
95205
+ const fallbackThreadId = usingStampFallback ? stampThreadId : undefined;
95206
+ if (origin != null && origin.chatId.length > 0) {
95207
+ return { action: "paint", chatId: origin.chatId, threadId: origin.threadId, deferrals, exhausted, ownerDmFallback: false };
95208
+ }
95209
+ if (workerFleetChatId.length > 0) {
95210
+ return { action: "paint", chatId: workerFleetChatId, threadId: fallbackThreadId, deferrals, exhausted, ownerDmFallback: false };
95211
+ }
95212
+ const ownerDmFallback = origin == null && workerFleetChatId.length === 0 && ownerDm.length > 0;
95213
+ return {
95214
+ action: "paint",
95215
+ chatId: ownerDm,
95216
+ threadId: origin?.threadId ?? fallbackThreadId,
95217
+ deferrals,
95218
+ exhausted,
95219
+ ownerDmFallback
95220
+ };
95221
+ }
94903
95222
  function resolveWorkerFeedDispatch(sub, watcherDescription, entryBackground) {
94904
95223
  return {
94905
95224
  isBackground: sub?.background ?? entryBackground ?? false,
@@ -94952,7 +95271,7 @@ if (isGatewayMain) {
94952
95271
  shutdownAnalytics();
94953
95272
  });
94954
95273
  }
94955
- var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join59(homedir19(), ".claude", "channels", "telegram");
95274
+ var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join60(homedir19(), ".claude", "channels", "telegram");
94956
95275
  var permCardStore = createPermissionCardStore(STATE_DIR);
94957
95276
  var BLOCKED_APPROVALS_DIR = process.env.SWITCHROOM_BLOCKED_APPROVALS_DIR ?? "/state/blocked-approvals";
94958
95277
  var AGENT_NAME = process.env.SWITCHROOM_AGENT_NAME ?? "agent";
@@ -95052,11 +95371,11 @@ function scheduleAlwaysAllowPersistDrain() {
95052
95371
  }, ALWAYS_ALLOW_DRAIN_INTERVAL_MS);
95053
95372
  timer3.unref?.();
95054
95373
  }
95055
- var ACCESS_FILE = join59(STATE_DIR, "access.json");
95056
- var APPROVED_DIR = join59(STATE_DIR, "approved");
95057
- var ENV_FILE = join59(STATE_DIR, ".env");
95058
- var INBOX_DIR = join59(STATE_DIR, "inbox");
95059
- var PEOPLE_FILE = join59(STATE_DIR, "people.json");
95374
+ var ACCESS_FILE = join60(STATE_DIR, "access.json");
95375
+ var APPROVED_DIR = join60(STATE_DIR, "approved");
95376
+ var ENV_FILE = join60(STATE_DIR, ".env");
95377
+ var INBOX_DIR = join60(STATE_DIR, "inbox");
95378
+ var PEOPLE_FILE = join60(STATE_DIR, "people.json");
95060
95379
  function triggerSelfRestart(targetAgent, reason, delayMs = 300) {
95061
95380
  const isDocker = process.env.SWITCHROOM_RUNTIME === "docker";
95062
95381
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME;
@@ -95205,7 +95524,7 @@ function assertSendable(f) {
95205
95524
  } catch {
95206
95525
  throw new Error(`refusing to send file \u2014 cannot resolve real path: ${f}`);
95207
95526
  }
95208
- const inbox = join59(stateReal, "inbox");
95527
+ const inbox = join60(stateReal, "inbox");
95209
95528
  if (real.startsWith(stateReal + sep4) && !real.startsWith(inbox + sep4)) {
95210
95529
  throw new Error(`refusing to send channel state: ${f}`);
95211
95530
  }
@@ -95308,9 +95627,9 @@ function assertAllowedChat(chat_id) {
95308
95627
  function saveAccess(a) {
95309
95628
  if (STATIC)
95310
95629
  return;
95311
- mkdirSync46(STATE_DIR, { recursive: true, mode: 448 });
95630
+ mkdirSync47(STATE_DIR, { recursive: true, mode: 448 });
95312
95631
  const tmp = ACCESS_FILE + ".tmp";
95313
- writeFileSync47(tmp, JSON.stringify(a, null, 2) + `
95632
+ writeFileSync48(tmp, JSON.stringify(a, null, 2) + `
95314
95633
  `, { mode: 384 });
95315
95634
  renameSync21(tmp, ACCESS_FILE);
95316
95635
  }
@@ -95330,7 +95649,7 @@ var HISTORY_ENABLED = HISTORY_ACCESS.historyEnabled !== false;
95330
95649
  if (isGatewayMain && HISTORY_ENABLED) {
95331
95650
  try {
95332
95651
  initHistory(STATE_DIR, HISTORY_ACCESS.historyRetentionDays ?? 30);
95333
- process.stderr.write(`telegram gateway: history capture enabled at ${join59(STATE_DIR, "history.db")}
95652
+ process.stderr.write(`telegram gateway: history capture enabled at ${join60(STATE_DIR, "history.db")}
95334
95653
  `);
95335
95654
  } catch (err) {
95336
95655
  process.stderr.write(`telegram gateway: history init failed (${err.message}) \u2014 capture disabled
@@ -95349,7 +95668,7 @@ if (isGatewayMain)
95349
95668
  let markerTurnKey = null;
95350
95669
  let markerAgeMs = null;
95351
95670
  try {
95352
- const markerPath = join59(STATE_DIR, TURN_ACTIVE_MARKER_FILE2);
95671
+ const markerPath = join60(STATE_DIR, TURN_ACTIVE_MARKER_FILE2);
95353
95672
  if (existsSync54(markerPath)) {
95354
95673
  const st = statSync19(markerPath);
95355
95674
  markerAgeMs = Date.now() - st.mtimeMs;
@@ -95374,10 +95693,10 @@ if (isGatewayMain)
95374
95693
  process.stderr.write(`telegram gateway: turn-registry boot-reaper stamped ${reaped} orphaned turn(s)` + `${timeoutTurnKey ? ` (turnKey=${timeoutTurnKey} as 'timeout', markerAgeMs=${markerAgeMs})` : " as 'restart'"}
95375
95694
  `);
95376
95695
  } else {
95377
- process.stderr.write(`telegram gateway: turn-registry initialized at ${join59(agentDir, "telegram", "registry.db")}
95696
+ process.stderr.write(`telegram gateway: turn-registry initialized at ${join60(agentDir, "telegram", "registry.db")}
95378
95697
  `);
95379
95698
  }
95380
- const bridgeDeadMarker = consumeBridgeDeadEscalationMarker(join59(STATE_DIR, "bridge-dead-escalation.json"));
95699
+ const bridgeDeadMarker = consumeBridgeDeadEscalationMarker(join60(STATE_DIR, "bridge-dead-escalation.json"));
95381
95700
  if (bridgeDeadMarker != null) {
95382
95701
  bridgeDeadPriorStreak = bridgeDeadMarker.count ?? 1;
95383
95702
  process.stderr.write(`telegram gateway: boot: prior restart was a bridge-dead escalation (reason=${bridgeDeadMarker.reason}` + `, consecutive=${bridgeDeadPriorStreak}` + `${bridgeDeadMarker.crashTail ? `, crashTail=${bridgeDeadMarker.crashTail}` : ""})
@@ -95390,7 +95709,7 @@ if (isGatewayMain)
95390
95709
  const pending2 = findLatestTurnIfInterrupted(turnsDb);
95391
95710
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
95392
95711
  if (pending2 != null && selfAgent) {
95393
- const bootResumeMarkerPath = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join59(STATE_DIR, "clean-shutdown.json");
95712
+ const bootResumeMarkerPath = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join60(STATE_DIR, "clean-shutdown.json");
95394
95713
  const bootResumeCleanMarker = readCleanShutdownMarker(bootResumeMarkerPath);
95395
95714
  const bootResumeForceAlways = process.env.SWITCHROOM_BOOT_RESUME_ALWAYS === "1";
95396
95715
  const bootResumeMode = parseBootResumeMode(process.env.SWITCHROOM_BOOT_RESUME);
@@ -95503,7 +95822,7 @@ if (isGatewayMain)
95503
95822
  `);
95504
95823
  }
95505
95824
  }
95506
- const pendingEnvPath = join59(agentDir, ".pending-turn.env");
95825
+ const pendingEnvPath = join60(agentDir, ".pending-turn.env");
95507
95826
  try {
95508
95827
  if (pending2 != null) {
95509
95828
  const lines = [
@@ -95517,7 +95836,7 @@ if (isGatewayMain)
95517
95836
  pending2.interrupt_reason != null ? `SWITCHROOM_PENDING_INTERRUPT_REASON=${pending2.interrupt_reason}` : `SWITCHROOM_PENDING_INTERRUPT_REASON=`
95518
95837
  ];
95519
95838
  const pendingEnvTmp = `${pendingEnvPath}.tmp-${process.pid}`;
95520
- writeFileSync47(pendingEnvTmp, lines.join(`
95839
+ writeFileSync48(pendingEnvTmp, lines.join(`
95521
95840
  `) + `
95522
95841
  `, { mode: 384 });
95523
95842
  renameSync21(pendingEnvTmp, pendingEnvPath);
@@ -95564,26 +95883,31 @@ var WORKER_FEED_GROUP_MESSAGE_LIFETIME_CAP_MS = (() => {
95564
95883
  return Number.isFinite(v) && v > 0 ? v : 60 * 60000;
95565
95884
  })();
95566
95885
  var workerFeedOwnerDmFallbackLogged = new Set;
95567
- function resolveWorkerFeedChat(agentId, fleetChatId) {
95886
+ function noteWorkerFeedOwnerDmFallback(agentId) {
95887
+ if (workerFeedOwnerDmFallbackLogged.has(agentId))
95888
+ return;
95889
+ workerFeedOwnerDmFallbackLogged.add(agentId);
95890
+ if (workerFeedOwnerDmFallbackLogged.size > WORKER_FEED_FALLBACK_LOG_CAP) {
95891
+ const oldest = workerFeedOwnerDmFallbackLogged.values().next().value;
95892
+ if (oldest != null)
95893
+ workerFeedOwnerDmFallbackLogged.delete(oldest);
95894
+ }
95895
+ process.stderr.write(`telegram gateway: worker-feed origin unresolved agent=${agentId} \u2014 routing card to owner DM
95896
+ `);
95897
+ }
95898
+ var workerFeedOriginDeferrals = new Map;
95899
+ var WORKER_FEED_ORIGIN_DEFER_MAX = 10;
95900
+ function resolveWorkerFeedChat(agentId, fleetChatId, fallbackThreadId) {
95568
95901
  const origin = resolveSubagentOriginChat(agentId);
95569
95902
  if (origin != null && origin.chatId.length > 0)
95570
95903
  return origin;
95571
95904
  if (fleetChatId.length > 0)
95572
- return { chatId: fleetChatId };
95905
+ return { chatId: fleetChatId, threadId: fallbackThreadId };
95573
95906
  const ownerDm = loadAccess().allowFrom[0] ?? "";
95574
95907
  if (origin == null && fleetChatId.length === 0 && ownerDm.length > 0) {
95575
- if (!workerFeedOwnerDmFallbackLogged.has(agentId)) {
95576
- workerFeedOwnerDmFallbackLogged.add(agentId);
95577
- if (workerFeedOwnerDmFallbackLogged.size > WORKER_FEED_FALLBACK_LOG_CAP) {
95578
- const oldest = workerFeedOwnerDmFallbackLogged.values().next().value;
95579
- if (oldest != null)
95580
- workerFeedOwnerDmFallbackLogged.delete(oldest);
95581
- }
95582
- process.stderr.write(`telegram gateway: worker-feed origin unresolved agent=${agentId} \u2014 routing card to owner DM
95583
- `);
95584
- }
95908
+ noteWorkerFeedOwnerDmFallback(agentId);
95585
95909
  }
95586
- return { chatId: ownerDm, threadId: origin?.threadId };
95910
+ return { chatId: ownerDm, threadId: origin?.threadId ?? fallbackThreadId };
95587
95911
  }
95588
95912
  var REGISTRY_REAPER_INTERVAL_MS = 6 * 60 * 60 * 1000;
95589
95913
  function runHistoryReaperNow(reason) {
@@ -95626,7 +95950,7 @@ function checkApprovals() {
95626
95950
  return;
95627
95951
  }
95628
95952
  for (const senderId of files) {
95629
- const file = join59(APPROVED_DIR, senderId);
95953
+ const file = join60(APPROVED_DIR, senderId);
95630
95954
  bot.api.sendMessage(senderId, "Paired! Say hi to Claude.").then(() => rmSync6(file, { force: true }), (err) => {
95631
95955
  process.stderr.write(`telegram gateway: failed to send approval confirm: ${err}
95632
95956
  `);
@@ -95636,6 +95960,8 @@ function checkApprovals() {
95636
95960
  }
95637
95961
  if (isGatewayMain && !STATIC)
95638
95962
  setInterval(checkApprovals, 5000).unref();
95963
+ if (isGatewayMain && !STATIC)
95964
+ startGatewayHeartbeat(STATE_DIR);
95639
95965
  var IDLE_CLEAR_CHECK_MS = Number(process.env.SWITCHROOM_IDLE_CLEAR_CHECK_MS ?? 60000);
95640
95966
  if (isGatewayMain && !STATIC && IDLE_CLEAR_CHECK_MS > 0)
95641
95967
  setInterval(maybeIdleClear, IDLE_CLEAR_CHECK_MS).unref();
@@ -95797,10 +96123,10 @@ function noteAgentOutputAt(key, ts) {
95797
96123
  lastAgentOutputAt.delete(oldest);
95798
96124
  }
95799
96125
  }
95800
- var OBLIGATION_STORE_PATH = join59(STATE_DIR, "obligations.json");
96126
+ var OBLIGATION_STORE_PATH = join60(STATE_DIR, "obligations.json");
95801
96127
  var obligationStoreFs = {
95802
96128
  readFileSync: (p) => readFileSync58(p, "utf8"),
95803
- writeFileSync: (p, d) => writeFileSync47(p, d),
96129
+ writeFileSync: (p, d) => writeFileSync48(p, d),
95804
96130
  renameSync: (a, b) => renameSync21(a, b),
95805
96131
  existsSync: (p) => existsSync54(p)
95806
96132
  };
@@ -95854,7 +96180,7 @@ function turnInFlightMachineOnly() {
95854
96180
  function liveTurnAgeMs(now) {
95855
96181
  if (currentTurn === null)
95856
96182
  return null;
95857
- return effectiveTurnAgeMs(readTurnActiveMarkerAgeMs(STATE_DIR, now), currentTurn.startedAt, now);
96183
+ return effectiveTurnAgeMs(readTurnActiveMarkerAgeMs2(STATE_DIR, now), currentTurn.startedAt, now);
95858
96184
  }
95859
96185
  function oldestPendingApprovalAgeMs(now) {
95860
96186
  let oldest = null;
@@ -97061,7 +97387,7 @@ function wrapBootCardApi(threadId) {
97061
97387
  await lockedBot.api.editMessageText(cid, mid, richMessage2(text5), editOpts);
97062
97388
  return "edited";
97063
97389
  } catch (err) {
97064
- const desc = err instanceof import_grammy14.GrammyError ? err.description : err instanceof Error ? err.message : String(err);
97390
+ const desc = err instanceof import_grammy15.GrammyError ? err.description : err instanceof Error ? err.message : String(err);
97065
97391
  if (typeof desc === "string" && desc.toLowerCase().includes("not modified"))
97066
97392
  return "edited";
97067
97393
  return "gone";
@@ -98141,26 +98467,26 @@ var statusPinState = new Map;
98141
98467
  var statusPinChatIds = new Map;
98142
98468
  var statusPinPinnedAt = new Map;
98143
98469
  var statusPinRightsCache = new PinRightsCache2;
98144
- var STATUS_PIN_STORE_PATH = join59(STATE_DIR, "status-pins.json");
98470
+ var STATUS_PIN_STORE_PATH = join60(STATE_DIR, "status-pins.json");
98145
98471
  var statusPinStoreFs = {
98146
98472
  readFileSync: (p) => readFileSync58(p, "utf8"),
98147
- writeFileSync: (p, d) => writeFileSync47(p, d),
98473
+ writeFileSync: (p, d) => writeFileSync48(p, d),
98148
98474
  renameSync: (a, b) => renameSync21(a, b),
98149
98475
  existsSync: (p) => existsSync54(p)
98150
98476
  };
98151
98477
  var statusPinPersistEnabled = !STATIC && PIN_STATUS_WHILE_WORKING;
98152
- var ACTIVITY_CARD_STORE_PATH = join59(STATE_DIR, "activity-cards-pending.json");
98478
+ var ACTIVITY_CARD_STORE_PATH = join60(STATE_DIR, "activity-cards-pending.json");
98153
98479
  var activityCardStoreFs = {
98154
98480
  readFileSync: (p) => readFileSync58(p, "utf8"),
98155
- writeFileSync: (p, d) => writeFileSync47(p, d),
98481
+ writeFileSync: (p, d) => writeFileSync48(p, d),
98156
98482
  renameSync: (a, b) => renameSync21(a, b),
98157
98483
  existsSync: (p) => existsSync54(p)
98158
98484
  };
98159
98485
  var activityCardPersistEnabled = !STATIC;
98160
- var QUEUED_CARD_STORE_PATH = join59(STATE_DIR, "queued-cards-pending.json");
98486
+ var QUEUED_CARD_STORE_PATH = join60(STATE_DIR, "queued-cards-pending.json");
98161
98487
  var queuedCardStoreFs = {
98162
98488
  readFileSync: (p) => readFileSync58(p, "utf8"),
98163
- writeFileSync: (p, d) => writeFileSync47(p, d),
98489
+ writeFileSync: (p, d) => writeFileSync48(p, d),
98164
98490
  renameSync: (a, b) => renameSync21(a, b),
98165
98491
  existsSync: (p) => existsSync54(p)
98166
98492
  };
@@ -98535,12 +98861,12 @@ var getPinnedProgressCardMessageId = null;
98535
98861
  var completeProgressCardTurn = null;
98536
98862
  var subagentWatcher = null;
98537
98863
  var workerActivityFeed = null;
98538
- var SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join59(STATE_DIR, "gateway.sock");
98864
+ var SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join60(STATE_DIR, "gateway.sock");
98539
98865
  if (isGatewayMain)
98540
- mkdirSync46(STATE_DIR, { recursive: true, mode: 448 });
98541
- var GATEWAY_PID_PATH = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join59(STATE_DIR, "gateway.pid.json");
98542
- var GATEWAY_SESSION_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_SESSION_MARKER ?? join59(STATE_DIR, "gateway-session.json");
98543
- var GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join59(STATE_DIR, "clean-shutdown.json");
98866
+ mkdirSync47(STATE_DIR, { recursive: true, mode: 448 });
98867
+ var GATEWAY_PID_PATH = process.env.SWITCHROOM_GATEWAY_PID_FILE ?? join60(STATE_DIR, "gateway.pid.json");
98868
+ var GATEWAY_SESSION_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_SESSION_MARKER ?? join60(STATE_DIR, "gateway-session.json");
98869
+ var GATEWAY_CLEAN_SHUTDOWN_MARKER_PATH = process.env.SWITCHROOM_GATEWAY_CLEAN_SHUTDOWN_MARKER ?? join60(STATE_DIR, "clean-shutdown.json");
98544
98870
  var GATEWAY_STARTED_AT_MS = Date.now();
98545
98871
  var BOOT_CARD_ENABLED = process.env.SWITCHROOM_BOOT_CARD !== "false";
98546
98872
  var activeBootCard = null;
@@ -98569,7 +98895,7 @@ function ensureIssuesCard(chatId, threadId) {
98569
98895
  bot: botApi,
98570
98896
  log: (msg) => process.stderr.write(`telegram gateway: ${msg}
98571
98897
  `),
98572
- persistPath: join59(stateDir, "issues-card.json")
98898
+ persistPath: join60(stateDir, "issues-card.json")
98573
98899
  });
98574
98900
  activeIssuesWatcher = startIssuesWatcher({
98575
98901
  stateDir,
@@ -98688,7 +99014,13 @@ function gatewayLivenessWiringDeps() {
98688
99014
  getPendingInboundBuffer: () => pendingInboundBuffer,
98689
99015
  trackRedeliveredInbound,
98690
99016
  closeActivityLane,
98691
- closeProgressLane
99017
+ closeProgressLane,
99018
+ hangRestart: {
99019
+ stalenessThresholdMs: hangStalenessMs(),
99020
+ request: (reason, idleMs) => {
99021
+ triggerSelfRestart(getMyAgentName(), `hang-watchdog:${reason} idle=${Math.round(idleMs)}ms`);
99022
+ }
99023
+ }
98692
99024
  };
98693
99025
  }
98694
99026
  if (isGatewayMain)
@@ -98760,11 +99092,11 @@ if (isGatewayMain)
98760
99092
  var inboundSpool;
98761
99093
  if (isGatewayMain)
98762
99094
  inboundSpool = STATIC ? undefined : createInboundSpool({
98763
- path: join59(STATE_DIR, "inbound-spool.jsonl"),
99095
+ path: join60(STATE_DIR, "inbound-spool.jsonl"),
98764
99096
  fs: {
98765
99097
  appendFileSync: (p, d) => appendFileSync7(p, d),
98766
99098
  readFileSync: (p) => readFileSync58(p, "utf8"),
98767
- writeFileSync: (p, d) => writeFileSync47(p, d),
99099
+ writeFileSync: (p, d) => writeFileSync48(p, d),
98768
99100
  renameSync: (a, b) => renameSync21(a, b),
98769
99101
  existsSync: (p) => existsSync54(p),
98770
99102
  statSizeSync: (p) => statSync19(p).size
@@ -98794,7 +99126,7 @@ var pendingInboundBuffer = createPendingInboundBuffer({
98794
99126
  function agentHasInFlightBackgroundWork(now) {
98795
99127
  if (countRunningWorkers() > 0)
98796
99128
  return true;
98797
- const ageMs = readTurnActiveMarkerAgeMs(STATE_DIR, now);
99129
+ const ageMs = readTurnActiveMarkerAgeMs2(STATE_DIR, now);
98798
99130
  return ageMs != null && ageMs < TURN_ACTIVE_MARKER_FRESH_MS;
98799
99131
  }
98800
99132
  async function deliverCapturedProse2(args) {
@@ -98827,7 +99159,7 @@ async function maybeRedeliverUndeliveredAnswer() {
98827
99159
  let transcriptText;
98828
99160
  try {
98829
99161
  const projectsDir = getProjectsDirForCwd();
98830
- const path2 = join59(projectsDir, `${sessionId}.jsonl`);
99162
+ const path2 = join60(projectsDir, `${sessionId}.jsonl`);
98831
99163
  if (!existsSync54(path2)) {
98832
99164
  process.stderr.write(`telegram gateway: crash-redelivery \u2014 transcript not found for turnKey=${turn.turn_key} session=${sessionId} (${path2}); skipping
98833
99165
  `);
@@ -98930,7 +99262,7 @@ if (isGatewayMain && inboundSpool != null) {
98930
99262
  }
98931
99263
  var pendingPermissionBuffer = createPendingPermissionBuffer();
98932
99264
  function buildPermissionActionRow(requestId, showAlways) {
98933
- const kb = new import_grammy14.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow", `perm:allow:${requestId}`);
99265
+ const kb = new import_grammy15.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow", `perm:allow:${requestId}`);
98934
99266
  if (showAlways)
98935
99267
  kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
98936
99268
  return kb;
@@ -98992,8 +99324,8 @@ var bridgeDeadWatchdog = createBridgeDeadWatchdog({
98992
99324
  isSessionAlive: () => findAgentProcessInContainer2()?.comm === "claude",
98993
99325
  isShuttingDown: () => shuttingDown,
98994
99326
  escalate: (reason) => triggerSelfRestart(process.env.SWITCHROOM_AGENT_NAME ?? "", reason, 1500),
98995
- crashLogPath: join59(STATE_DIR, "bridge-crash.log"),
98996
- markerPath: join59(STATE_DIR, "bridge-dead-escalation.json"),
99327
+ crashLogPath: join60(STATE_DIR, "bridge-crash.log"),
99328
+ markerPath: join60(STATE_DIR, "bridge-dead-escalation.json"),
98997
99329
  log: (line) => process.stderr.write(`${line}
98998
99330
  `),
98999
99331
  priorStreak: bridgeDeadPriorStreak,
@@ -99099,8 +99431,8 @@ if (isGatewayMain)
99099
99431
  probeQuotaViaBroker: (t) => probeQuotaForBootCard(agentSlug, t),
99100
99432
  tmuxSupervisor: process.env.SWITCHROOM_TMUX_SUPERVISOR === "1",
99101
99433
  dockerMode: process.env.SWITCHROOM_RUNTIME === "docker",
99102
- configSnapshotPath: join59(resolvedAgentDirForCard, ".config-snapshot.json"),
99103
- bootCardStatePath: join59(resolvedAgentDirForCard, ".boot-card-msgid.json"),
99434
+ configSnapshotPath: join60(resolvedAgentDirForCard, ".config-snapshot.json"),
99435
+ bootCardStatePath: join60(resolvedAgentDirForCard, ".boot-card-msgid.json"),
99104
99436
  floodStatePath: FLOOD_STATE_PATH,
99105
99437
  ...updateOutcomeLine ? { updateOutcomeLine } : {}
99106
99438
  }, ackMsgId).then((handle) => {
@@ -99453,7 +99785,7 @@ if (isGatewayMain)
99453
99785
  },
99454
99786
  async onRequestConfigApproval(client3, msg) {
99455
99787
  const { handleRequestConfigApproval: handleRequestConfigApproval2 } = await Promise.resolve().then(() => (init_config_approval_handler(), exports_config_approval_handler));
99456
- const { InlineKeyboard: InlineKeyboard7 } = await Promise.resolve().then(() => __toESM(require_mod(), 1));
99788
+ const { InlineKeyboard: InlineKeyboard8 } = await Promise.resolve().then(() => __toESM(require_mod(), 1));
99457
99789
  await handleRequestConfigApproval2(client3, msg, {
99458
99790
  agentName: getMyAgentName(),
99459
99791
  loadTargetChat: () => {
@@ -99475,7 +99807,7 @@ if (isGatewayMain)
99475
99807
  ...cfgTopic != null ? { threadId: cfgTopic } : {}
99476
99808
  };
99477
99809
  },
99478
- buildKeyboard: (requestId, epoch) => new InlineKeyboard7().text("\u2705 Approve", `cfg:${requestId}:${epoch}:approve`).text("\uD83D\uDEAB Deny", `cfg:${requestId}:${epoch}:deny`),
99810
+ buildKeyboard: (requestId, epoch) => new InlineKeyboard8().text("\u2705 Approve", `cfg:${requestId}:${epoch}:approve`).text("\uD83D\uDEAB Deny", `cfg:${requestId}:${epoch}:deny`),
99479
99811
  postCard: async (args) => {
99480
99812
  try {
99481
99813
  const sent = await robustApiCall(() => bot.api.sendRichMessage(args.chatId, richMessage2(args.text), {
@@ -99504,7 +99836,7 @@ if (isGatewayMain)
99504
99836
  }
99505
99837
  },
99506
99838
  postAttachment: async (args) => {
99507
- const input = new import_grammy14.InputFile(Buffer.from(args.content, "utf8"), args.filename);
99839
+ const input = new import_grammy15.InputFile(Buffer.from(args.content, "utf8"), args.filename);
99508
99840
  await robustApiCall(() => bot.api.sendDocument(args.chatId, input, {
99509
99841
  ...args.threadId !== undefined ? { message_thread_id: args.threadId } : {}
99510
99842
  }), {
@@ -99782,7 +100114,7 @@ if (isGatewayMain)
99782
100114
  const receiverUid = receiverUidRaw ? Number(receiverUidRaw) : NaN;
99783
100115
  if (Number.isInteger(receiverUid))
99784
100116
  allowedUids.push(receiverUid);
99785
- const socketPath = join59(STATE_DIR, "webhook.sock");
100117
+ const socketPath = join60(STATE_DIR, "webhook.sock");
99786
100118
  const webhookInject = (agentName3, inbound) => {
99787
100119
  const msg = inbound;
99788
100120
  const delivered = ipcServer.sendToAgent(agentName3, msg);
@@ -100010,9 +100342,9 @@ function redactOutboundText(text5, site) {
100010
100342
  var VOICE_OUT_DEFAULT_CHUNK_CHARS = 600;
100011
100343
  var VOICE_OUT_HARD_CHUNK_CAP = 4096;
100012
100344
  var voiceOnDemandCache = new VoiceOnDemandCache({
100013
- persistPath: join59(STATE_DIR, "voice-ondemand.json")
100345
+ persistPath: join60(STATE_DIR, "voice-ondemand.json")
100014
100346
  });
100015
- var VOICE_CACHE_DIR = join59(STATE_DIR, "voice-cache");
100347
+ var VOICE_CACHE_DIR = join60(STATE_DIR, "voice-cache");
100016
100348
  var voicePreSynthQueue = new PreSynthQueue({
100017
100349
  runJob: async (job) => {
100018
100350
  const sidecarToken = await materializeSidecarToken2();
@@ -100278,7 +100610,7 @@ async function executeAskUser(rawArgs) {
100278
100610
  }
100279
100611
  }
100280
100612
  const askId = generateAskId();
100281
- const keyboard = new import_grammy14.InlineKeyboard;
100613
+ const keyboard = new import_grammy15.InlineKeyboard;
100282
100614
  for (let i = 0;i < args.options.length; i++) {
100283
100615
  keyboard.text(args.options[i], encodeAskCallback(askId, i));
100284
100616
  if (i < args.options.length - 1)
@@ -100413,7 +100745,7 @@ async function executeSendGif(rawArgs) {
100413
100745
  };
100414
100746
  }
100415
100747
  async function publishToTelegraph(text5, shortName, authorName) {
100416
- const accountPath = join59(STATE_DIR, "telegraph-account.json");
100748
+ const accountPath = join60(STATE_DIR, "telegraph-account.json");
100417
100749
  let account = null;
100418
100750
  try {
100419
100751
  if (existsSync54(accountPath)) {
@@ -100436,8 +100768,8 @@ async function publishToTelegraph(text5, shortName, authorName) {
100436
100768
  }
100437
100769
  account = created.value;
100438
100770
  try {
100439
- mkdirSync46(STATE_DIR, { recursive: true, mode: 448 });
100440
- writeFileSync47(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
100771
+ mkdirSync47(STATE_DIR, { recursive: true, mode: 448 });
100772
+ writeFileSync48(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
100441
100773
  } catch (err) {
100442
100774
  process.stderr.write(`telegram gateway: telegraph cache write failed: ${err.message}
100443
100775
  `);
@@ -100592,9 +100924,9 @@ async function executeDownloadAttachment(args) {
100592
100924
  fileUniqueId: file.file_unique_id,
100593
100925
  now: Date.now()
100594
100926
  });
100595
- mkdirSync46(INBOX_DIR, { recursive: true, mode: 448 });
100927
+ mkdirSync47(INBOX_DIR, { recursive: true, mode: 448 });
100596
100928
  assertInsideInbox2(INBOX_DIR, dlPath);
100597
- writeFileSync47(dlPath, buf, { mode: 384 });
100929
+ writeFileSync48(dlPath, buf, { mode: 384 });
100598
100930
  return { content: [{ type: "text", text: dlPath }] };
100599
100931
  }
100600
100932
  async function executeEditMessage(args) {
@@ -101909,24 +102241,8 @@ function switchroomExecCombined(args, timeoutMs = 15000) {
101909
102241
  shell: "/bin/bash"
101910
102242
  });
101911
102243
  }
101912
- function formatSwitchroomOutput(output, maxLen = RICH_MESSAGE_MAX_CHARS2) {
101913
- const trimmed = output.trim();
101914
- if (trimmed.length <= maxLen)
101915
- return trimmed;
101916
- return trimmed.slice(0, maxLen - 20) + `
101917
- ... (truncated)`;
101918
- }
101919
- function stripAnsi4(text5) {
101920
- return text5.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
101921
- }
101922
- function escapeHtmlForTg2(text5) {
101923
- return text5.replace(/([\\`*_~=\[\]|])/g, "\\$1");
101924
- }
101925
102244
  var buttonConfirmParseModeWarned = new Set;
101926
102245
  var buttonConfirmSingleUseWarned = new Set;
101927
- function preBlock(text5) {
101928
- return "```\n" + text5.replace(/```/g, "`\u200B``") + "\n```";
101929
- }
101930
102246
  async function switchroomReply(ctx, text5, options = {}) {
101931
102247
  const chatId = String(ctx.chat.id);
101932
102248
  const baseThreadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
@@ -101956,22 +102272,6 @@ Please delete message \`${msgId}\` manually so the value is not retained in chat
101956
102272
  _Reason: ${escapeHtmlForTg2(msg)}_`), {}), { chat_id: chatId, verb: "deleteSensitiveMessage.warning" });
101957
102273
  }
101958
102274
  }
101959
- function getCommandArgs(ctx) {
101960
- const fromMatch = typeof ctx.match === "string" ? ctx.match.trim() : "";
101961
- if (fromMatch)
101962
- return fromMatch;
101963
- const text5 = ctx.msg?.text ?? ctx.message?.text ?? "";
101964
- const m = text5.match(/^\/\S+\s+([\s\S]*)$/);
101965
- return m ? m[1].trim() : "";
101966
- }
101967
- function hasDemoFlag(args) {
101968
- return /(?:^|\s)demo$/i.test(args.trim());
101969
- }
101970
- function assertSafeAgentName(name) {
101971
- if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name) && name !== "all") {
101972
- throw new Error(`invalid agent name: ${name}`);
101973
- }
101974
- }
101975
102275
  function getMyAgentName() {
101976
102276
  const fromEnv = process.env.SWITCHROOM_AGENT_NAME;
101977
102277
  if (fromEnv && fromEnv.trim().length > 0)
@@ -101989,14 +102289,14 @@ function restartMarkerPath() {
101989
102289
  const agentDir = resolveAgentDirFromEnv();
101990
102290
  if (!agentDir)
101991
102291
  return null;
101992
- return join59(agentDir, "restart-pending.json");
102292
+ return join60(agentDir, "restart-pending.json");
101993
102293
  }
101994
102294
  function writeRestartMarker(marker) {
101995
102295
  const p = restartMarkerPath();
101996
102296
  if (!p)
101997
102297
  return;
101998
102298
  try {
101999
- writeFileSync47(p, JSON.stringify(marker));
102299
+ writeFileSync48(p, JSON.stringify(marker));
102000
102300
  lastPlannedRestartAt = Date.now();
102001
102301
  process.stderr.write(`telegram gateway: restart-marker: write chat_id=${marker.chat_id} thread_id=${marker.thread_id ?? "-"} ack=${marker.ack_message_id ?? "-"} path=${p}
102002
102302
  `);
@@ -102181,12 +102481,12 @@ function _resetDockerReachableCache() {
102181
102481
  }
102182
102482
  function spawnSwitchroomDetached(args, onFailure) {
102183
102483
  const fullArgs = SWITCHROOM_CONFIG ? ["--config", SWITCHROOM_CONFIG, ...args] : args;
102184
- const logPath = join59(STATE_DIR, "detached-spawn.log");
102484
+ const logPath = join60(STATE_DIR, "detached-spawn.log");
102185
102485
  let outFd = null;
102186
102486
  try {
102187
- mkdirSync46(STATE_DIR, { recursive: true });
102487
+ mkdirSync47(STATE_DIR, { recursive: true });
102188
102488
  outFd = openSync12(logPath, "a");
102189
- writeFileSync47(logPath, `
102489
+ writeFileSync48(logPath, `
102190
102490
  [${new Date().toISOString()}] spawn ${SWITCHROOM_CLI} ${fullArgs.join(" ")}
102191
102491
  `, { flag: "a" });
102192
102492
  } catch {}
@@ -102286,53 +102586,6 @@ function scheduleGrantRestart(agentName3, chatId, threadId, reason) {
102286
102586
  }
102287
102587
  return decision;
102288
102588
  }
102289
- function formatAuthOutputForTelegram(output) {
102290
- const trimmed = stripAnsi4(output).trim();
102291
- const url = trimmed.match(/https:\/\/\S+/)?.[0] ?? null;
102292
- const lines = trimmed.split(/\n+/).map((l) => l.trim()).filter(Boolean);
102293
- if (!url)
102294
- return { text: preBlock(formatSwitchroomOutput(trimmed)), url: null };
102295
- const body = lines.filter((line) => {
102296
- if (line === url)
102297
- return false;
102298
- if (line.startsWith("switchroom auth code"))
102299
- return false;
102300
- if (line.startsWith("switchroom auth cancel"))
102301
- return false;
102302
- if (line.startsWith("Use 'tmux attach"))
102303
- return false;
102304
- if (line.startsWith("After Claude shows you a browser code"))
102305
- return false;
102306
- if (line.startsWith("Then finish with:"))
102307
- return false;
102308
- if (line.startsWith("Cancel with:"))
102309
- return false;
102310
- return true;
102311
- });
102312
- const rendered = body.map((line) => {
102313
- if (line.startsWith("Started Claude auth") || line.startsWith("Auth session already running"))
102314
- return `**${escapeHtmlForTg2(line)}**`;
102315
- if (line.startsWith("Open this URL"))
102316
- return `_${escapeHtmlForTg2(line)}_`;
102317
- return escapeHtmlForTg2(line);
102318
- });
102319
- rendered.push("", "\uD83D\uDC47 Tap **\uD83D\uDD10 Open Claude auth** below, then **reply with the browser code**.", "", `_Wrong Anthropic account getting authorized? Long-press the URL below and choose "Copy Link" or "Open in Browser" \u2014 lands in your main browser where the right account is signed in, bypassing Telegram's in-app browser cookies._`, "", url);
102320
- return { text: rendered.join(`
102321
- `), url };
102322
- }
102323
- function buildAuthUrlKeyboard(authorizeUrl) {
102324
- return new import_grammy14.InlineKeyboard().url("\uD83D\uDD10 Open Claude auth", authorizeUrl);
102325
- }
102326
- function buildDeferredSecretKeyboard(deferKey) {
102327
- const unlockData = `vd:unlock:${deferKey}`;
102328
- const cancelData = `vd:cancel:${deferKey}`;
102329
- if (unlockData.length > 64 || cancelData.length > 64) {
102330
- process.stderr.write(`telegram gateway: callback_data overflow \u2014 deferKey=${deferKey} unlockLen=${unlockData.length} cancelLen=${cancelData.length}
102331
- `);
102332
- throw new Error(`callback_data overflow: deferKey too long (${deferKey.length} chars)`);
102333
- }
102334
- return new import_grammy14.InlineKeyboard().text("\uD83D\uDD13 Unlock vault & save", unlockData).text("\uD83D\uDDD1 Discard", cancelData);
102335
- }
102336
102589
  async function runSwitchroomAuthCommand(ctx, args, label) {
102337
102590
  try {
102338
102591
  const output = switchroomExecCombined(args, 30000);
@@ -102383,15 +102636,6 @@ function runVaultCli(args, passphrase, stdinValue) {
102383
102636
  `).trim() };
102384
102637
  }
102385
102638
  }
102386
- function renderVaultOpFailure(verbLabel, cliOutput, key) {
102387
- const parsed = parseVaultCliError2(cliOutput);
102388
- const verb = verbLabel === "delete" ? "remove" : verbLabel;
102389
- const rendered = renderVaultCliError2(parsed, { verb, key });
102390
- if (rendered.suppressRaw)
102391
- return rendered.html;
102392
- return `**vault ${verbLabel} failed:**
102393
- ${preBlock(cliOutput)}`;
102394
- }
102395
102639
  async function executeVaultOp(ctx, chatId, op, key, passphrase, setValue) {
102396
102640
  if (op === "list") {
102397
102641
  const r = runVaultCli(["list"], passphrase);
@@ -102495,30 +102739,6 @@ function switchroomExecJson(args) {
102495
102739
  return null;
102496
102740
  }
102497
102741
  }
102498
- function statusIcon(status) {
102499
- if (status === "active" || status === "running")
102500
- return "\uD83D\uDFE2";
102501
- if (status === "inactive" || status === "stopped" || status === "dead")
102502
- return "\uD83D\uDD34";
102503
- if (status === "failed")
102504
- return "\u26A0\uFE0F";
102505
- return "\u26AA";
102506
- }
102507
- function renderAuthCodeOutcome(outcome) {
102508
- if (!outcome || outcome.kind === "success")
102509
- return null;
102510
- const tail = outcome.paneTailText ? `
102511
- _${escapeHtmlForTg2(outcome.paneTailText)}_` : "";
102512
- switch (outcome.kind) {
102513
- case "invalid-code":
102514
- case "expired-code":
102515
- return `Code rejected by Claude \u2014 tap **Restart flow** for a fresh URL.${tail}`;
102516
- case "pane-not-ready":
102517
- return `Auth pane not ready \u2014 tap **Retry**.`;
102518
- case "timeout":
102519
- return `Still waiting after 2 min \u2014 tap **Retry** or check \`switchroom auth list\`.${tail}`;
102520
- }
102521
- }
102522
102742
  function execAuthCode(agent, code2) {
102523
102743
  try {
102524
102744
  const output = switchroomExec(["auth", "code", agent, code2, "--json"], 150000);
@@ -102554,7 +102774,7 @@ ${preBlock(formatSwitchroomOutput(detail))}`, { html: true });
102554
102774
  }
102555
102775
  function readRecentDenialsForAgent(agentName3, windowMs, limit) {
102556
102776
  try {
102557
- const auditPath = join59(homedir19(), ".switchroom", "vault-audit.log");
102777
+ const auditPath = join60(homedir19(), ".switchroom", "vault-audit.log");
102558
102778
  if (!existsSync54(auditPath))
102559
102779
  return [];
102560
102780
  const raw = readFileSync58(auditPath, "utf8");
@@ -102608,7 +102828,7 @@ async function buildAgentMetadata(agentName3) {
102608
102828
  try {
102609
102829
  const agentDir = resolveAgentDirFromEnv();
102610
102830
  if (agentDir) {
102611
- const raw = readFileSync58(join59(agentDir, ".claude", ".claude.json"), "utf8");
102831
+ const raw = readFileSync58(join60(agentDir, ".claude", ".claude.json"), "utf8");
102612
102832
  claudeJson = JSON.parse(raw);
102613
102833
  }
102614
102834
  } catch {}
@@ -102737,7 +102957,7 @@ function buildModelDeps(restartCtx) {
102737
102957
  try {
102738
102958
  const agentDir = resolveAgentDirFromEnv();
102739
102959
  if (agentDir) {
102740
- const local = await fetchQuota2({ claudeConfigDir: join59(agentDir, ".claude") });
102960
+ const local = await fetchQuota2({ claudeConfigDir: join60(agentDir, ".claude") });
102741
102961
  if (local.ok)
102742
102962
  return formatQuotaLine2(local.data);
102743
102963
  }
@@ -102832,7 +103052,7 @@ function buildModelDeps(restartCtx) {
102832
103052
  function modelMenuReplyMarkup(reply) {
102833
103053
  if (!reply.keyboard)
102834
103054
  return;
102835
- const kb = new import_grammy14.InlineKeyboard;
103055
+ const kb = new import_grammy15.InlineKeyboard;
102836
103056
  for (const row of reply.keyboard) {
102837
103057
  for (const btn of row)
102838
103058
  kb.text(btn.text, btn.callback_data);
@@ -102972,7 +103192,7 @@ function buildEffortDeps() {
102972
103192
  function effortMenuReplyMarkup(reply) {
102973
103193
  if (!reply.keyboard)
102974
103194
  return;
102975
- const kb = new import_grammy14.InlineKeyboard;
103195
+ const kb = new import_grammy15.InlineKeyboard;
102976
103196
  for (const row of reply.keyboard) {
102977
103197
  for (const btn of row)
102978
103198
  kb.text(btn.text, btn.callback_data);
@@ -102983,7 +103203,7 @@ function effortMenuReplyMarkup(reply) {
102983
103203
  function flushAgentHandoff(agentDir) {
102984
103204
  let removed = 0;
102985
103205
  for (const fname of [".handoff.md", ".handoff-topic"]) {
102986
- const p = join59(agentDir, fname);
103206
+ const p = join60(agentDir, fname);
102987
103207
  try {
102988
103208
  if (existsSync54(p)) {
102989
103209
  unlinkSync26(p);
@@ -103041,7 +103261,7 @@ async function handleNewCommand(ctx) {
103041
103261
  writeRestartMarker({ chat_id: chatId, thread_id: threadId ?? null, ack_message_id: ackId, ts: Date.now() });
103042
103262
  if (agentDir != null) {
103043
103263
  try {
103044
- writeFileSync47(join59(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
103264
+ writeFileSync48(join60(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
103045
103265
  `, "utf8");
103046
103266
  } catch (err) {
103047
103267
  process.stderr.write(`telegram gateway: failed to write force-fresh marker: ${err}
@@ -103155,10 +103375,10 @@ function buildFolderPickerDeps() {
103155
103375
  }
103156
103376
  var lockoutOps = {
103157
103377
  readFileSync: (p, enc) => readFileSync58(p, enc),
103158
- writeFileSync: (p, data, opts) => writeFileSync47(p, data, opts),
103378
+ writeFileSync: (p, data, opts) => writeFileSync48(p, data, opts),
103159
103379
  existsSync: (p) => existsSync54(p),
103160
- mkdirSync: (p, opts) => mkdirSync46(p, opts),
103161
- joinPath: (...parts) => join59(...parts)
103380
+ mkdirSync: (p, opts) => mkdirSync47(p, opts),
103381
+ joinPath: (...parts) => join60(...parts)
103162
103382
  };
103163
103383
  var FLEET_FALLBACK_DEDUP_MS = 30000;
103164
103384
  function isAuthBrokerSocketReachable() {
@@ -103418,7 +103638,7 @@ async function runCreditWatch() {
103418
103638
  if (!agentDir)
103419
103639
  return;
103420
103640
  const agentName3 = getMyAgentName();
103421
- const claudeConfigDir = join59(agentDir, ".claude");
103641
+ const claudeConfigDir = join60(agentDir, ".claude");
103422
103642
  const stateDir = STATE_DIR;
103423
103643
  const reason = readClaudeJsonOverage(claudeConfigDir);
103424
103644
  const prev = loadCreditState(stateDir);
@@ -103738,16 +103958,6 @@ async function probeQuotaForBootCard(agent, timeoutMs) {
103738
103958
  }
103739
103959
  var callbackQueryHandlers;
103740
103960
  var cardToolHandlers;
103741
- function buildDoctorScopeKeyboard() {
103742
- return new import_grammy14.InlineKeyboard().text("\uD83E\uDE7A Whole fleet", "dr:fleet").text("\uD83E\uDE7A This agent", "dr:self");
103743
- }
103744
- function formatDoctorReport(raw) {
103745
- const trimmed = stripAnsi4(raw).trim();
103746
- if (!trimmed)
103747
- return "doctor: no output";
103748
- const pretty = trimmed.replace(/^( *)\u2713 /gm, "$1\uD83D\uDFE2 ").replace(/^( *)\u2717 /gm, "$1\uD83D\uDD34 ").replace(/^( *)! /gm, "$1\uD83D\uDFE1 ");
103749
- return preBlock(formatSwitchroomOutput(pretty));
103750
- }
103751
103961
  async function renderSelfDoctor(ctx) {
103752
103962
  let output;
103753
103963
  try {
@@ -104635,7 +104845,7 @@ _Google accounts are connected from the host CLI._`, { html: true });
104635
104845
  return;
104636
104846
  }
104637
104847
  const appNote = started.source === "default" ? "_Using switchroom's shipped Microsoft app._" : "_Using your configured Microsoft app._";
104638
- const keyboard = new import_grammy14.InlineKeyboard().url("\uD83D\uDD10 Sign in to Microsoft", started.device.verification_uri).row().text("\u2716 Cancel", `cn:cancel:${key}`);
104848
+ const keyboard = new import_grammy15.InlineKeyboard().url("\uD83D\uDD10 Sign in to Microsoft", started.device.verification_uri).row().text("\u2716 Cancel", `cn:cancel:${key}`);
104639
104849
  const sent = await ctx.replyWithRichMessage(richMessage2(`\uD83D\uDD17 **Connect a Microsoft account**
104640
104850
 
104641
104851
  ` + `1. Tap **Sign in to Microsoft** below.
@@ -104771,7 +104981,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
104771
104981
  tz: process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ
104772
104982
  });
104773
104983
  if (reply.keyboard && reply.keyboard.length > 0) {
104774
- const kb = new import_grammy14.InlineKeyboard;
104984
+ const kb = new import_grammy15.InlineKeyboard;
104775
104985
  for (let i = 0;i < reply.keyboard.length; i++) {
104776
104986
  const row = reply.keyboard[i];
104777
104987
  for (const b of row) {
@@ -104913,7 +105123,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
104913
105123
  }
104914
105124
  lines.push("");
104915
105125
  const denials = readRecentDenialsForAgent(targetAgent, 604800000, 5);
104916
- const denialKeyboard = new import_grammy14.InlineKeyboard;
105126
+ const denialKeyboard = new import_grammy15.InlineKeyboard;
104917
105127
  if (denials.length > 0) {
104918
105128
  lines.push("**Recent denials (last 7d):**");
104919
105129
  for (const d of denials) {
@@ -104961,7 +105171,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
104961
105171
  byAgent.set(g.agent_slug, list3);
104962
105172
  }
104963
105173
  const lines = ["**\uD83D\uDCDC Active grants**", ""];
104964
- const keyboard = new import_grammy14.InlineKeyboard;
105174
+ const keyboard = new import_grammy15.InlineKeyboard;
104965
105175
  for (const [agentName3, agentGrants] of byAgent) {
104966
105176
  lines.push(`**${escapeHtmlForTg2(agentName3)}:**`);
104967
105177
  for (const g of agentGrants) {
@@ -105158,7 +105368,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
105158
105368
  if (ctx.chat?.type !== "private") {
105159
105369
  kbRows = kbRows.filter((row) => !row.some((b) => b.callbackData?.startsWith("auth:use:")));
105160
105370
  }
105161
- const keyboard = new import_grammy14.InlineKeyboard;
105371
+ const keyboard = new import_grammy15.InlineKeyboard;
105162
105372
  kbRows.forEach((row, ri) => {
105163
105373
  if (ri > 0)
105164
105374
  keyboard.row();
@@ -105184,7 +105394,7 @@ Send \`/auth ${parsed.provider} cancel\` to abort.`, { html: true });
105184
105394
  await switchroomReply(ctx, "**/usage:** cannot resolve agent dir.", { html: true });
105185
105395
  return;
105186
105396
  }
105187
- const result = await fetchQuota2({ claudeConfigDir: join59(agentDir, ".claude") });
105397
+ const result = await fetchQuota2({ claudeConfigDir: join60(agentDir, ".claude") });
105188
105398
  if (!result.ok) {
105189
105399
  await switchroomReply(ctx, `**/usage:** ${escapeHtmlForTg2(result.reason)}`, { html: true });
105190
105400
  return;
@@ -105510,7 +105720,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
105510
105720
  await ctx.answerCallbackQuery({ text: "No always-allow rule for this tool." }).catch(() => {});
105511
105721
  return;
105512
105722
  }
105513
- keyboard = new import_grammy14.InlineKeyboard().text("\u2190 Back", `perm:back:${request_id}`);
105723
+ keyboard = new import_grammy15.InlineKeyboard().text("\u2190 Back", `perm:back:${request_id}`);
105514
105724
  if (choices.specific)
105515
105725
  keyboard.text(choices.specific.buttonLabel, `perm:asn:${request_id}`);
105516
105726
  keyboard.text(`${choices.broad.buttonLabel} \u26A0\uFE0F`, `perm:asb:${request_id}`);
@@ -105812,7 +106022,7 @@ ${labelWithResume}` : labelWithResume,
105812
106022
  verb: "voice-ondemand.sendVoice",
105813
106023
  ...threadId != null ? { threadId } : {}
105814
106024
  }),
105815
- sendVoiceByUpload: (chatId, audio, sendOpts, threadId) => robustApiCall(() => bot2.api.sendVoice(chatId, new import_grammy14.InputFile(Buffer.from(audio)), sendOpts), {
106025
+ sendVoiceByUpload: (chatId, audio, sendOpts, threadId) => robustApiCall(() => bot2.api.sendVoice(chatId, new import_grammy15.InputFile(Buffer.from(audio)), sendOpts), {
105816
106026
  chat_id: chatId,
105817
106027
  verb: "voice-ondemand.sendVoice",
105818
106028
  ...threadId != null ? { threadId } : {}
@@ -106071,7 +106281,7 @@ async function initGatewayBot() {
106071
106281
  `);
106072
106282
  process.exit(1);
106073
106283
  }
106074
- bot = new import_grammy14.Bot(TOKEN);
106284
+ bot = new import_grammy15.Bot(TOKEN);
106075
106285
  installTgPostLogger(bot);
106076
106286
  installUpdateTap(bot, (line) => process.stderr.write(line));
106077
106287
  bot.api.config.use(async (prev, method, payload, signal) => {
@@ -106425,7 +106635,7 @@ async function startGateway() {
106425
106635
  return;
106426
106636
  }
106427
106637
  })();
106428
- const resolvedAgentDirForBootCard = agentDir ?? join59(homedir19(), ".switchroom", "agents", agentSlug);
106638
+ const resolvedAgentDirForBootCard = agentDir ?? join60(homedir19(), ".switchroom", "agents", agentSlug);
106429
106639
  const handle = await startBootCard(chatId, threadId, botApiForCard, {
106430
106640
  agentName: agentDisplayName,
106431
106641
  agentSlug,
@@ -106439,8 +106649,8 @@ async function startGateway() {
106439
106649
  probeQuotaViaBroker: (t) => probeQuotaForBootCard(agentSlug, t),
106440
106650
  tmuxSupervisor: process.env.SWITCHROOM_TMUX_SUPERVISOR === "1",
106441
106651
  dockerMode: process.env.SWITCHROOM_RUNTIME === "docker",
106442
- configSnapshotPath: join59(resolvedAgentDirForBootCard, ".config-snapshot.json"),
106443
- bootCardStatePath: join59(resolvedAgentDirForBootCard, ".boot-card-msgid.json"),
106652
+ configSnapshotPath: join60(resolvedAgentDirForBootCard, ".config-snapshot.json"),
106653
+ bootCardStatePath: join60(resolvedAgentDirForBootCard, ".boot-card-msgid.json"),
106444
106654
  floodStatePath: FLOOD_STATE_PATH,
106445
106655
  ...updateOutcomeLine ? { updateOutcomeLine } : {}
106446
106656
  }, ackMsgId);
@@ -106471,7 +106681,7 @@ async function startGateway() {
106471
106681
  try {
106472
106682
  const smAgentDir = resolveAgentDirFromEnv();
106473
106683
  if (smAgentDir) {
106474
- const activePath = join59(smAgentDir, ".active-session-model");
106684
+ const activePath = join60(smAgentDir, ".active-session-model");
106475
106685
  if (existsSync54(activePath)) {
106476
106686
  try {
106477
106687
  const launched = readFileSync58(activePath, "utf8").trim();
@@ -106507,12 +106717,12 @@ async function startGateway() {
106507
106717
  deliverModelSwitchBootNotice({
106508
106718
  ...modelBootCardDeps,
106509
106719
  confirmation,
106510
- hasSessionModelAlert: existsSync54(join59(smAgentDir, ".session-model-alert"))
106720
+ hasSessionModelAlert: existsSync54(join60(smAgentDir, ".session-model-alert"))
106511
106721
  });
106512
106722
  }
106513
106723
  } catch {}
106514
106724
  }
106515
- const activeEffortPath = join59(smAgentDir, ".active-session-effort");
106725
+ const activeEffortPath = join60(smAgentDir, ".active-session-effort");
106516
106726
  if (existsSync54(activeEffortPath)) {
106517
106727
  try {
106518
106728
  const launchedEffort = readFileSync58(activeEffortPath, "utf8").trim();
@@ -106520,7 +106730,7 @@ async function startGateway() {
106520
106730
  sessionEffortOverride = launchedEffort.length > 0 && launchedEffort !== configuredEffort ? launchedEffort : null;
106521
106731
  } catch {}
106522
106732
  }
106523
- const alertPath = join59(smAgentDir, ".session-model-alert");
106733
+ const alertPath = join60(smAgentDir, ".session-model-alert");
106524
106734
  if (existsSync54(alertPath)) {
106525
106735
  let alertText = null;
106526
106736
  try {
@@ -106700,6 +106910,7 @@ async function startGateway() {
106700
106910
  `);
106701
106911
  },
106702
106912
  onTerminalCleanup: (agentId) => {
106913
+ workerFeedOriginDeferrals.delete(agentId);
106703
106914
  try {
106704
106915
  workerActivityFeed?.terminate(agentId);
106705
106916
  } catch (err) {
@@ -106708,6 +106919,7 @@ async function startGateway() {
106708
106919
  }
106709
106920
  },
106710
106921
  onFinish: ({ agentId, outcome, description, resultText, toolCount, totalTokens, durationMs, background: entryBackground }) => {
106922
+ workerFeedOriginDeferrals.delete(agentId);
106711
106923
  deferredDoneReactions.promote();
106712
106924
  let fleetChatId = "";
106713
106925
  try {
@@ -106928,9 +107140,28 @@ async function startGateway() {
106928
107140
  stampTurn.subagentActivityAt = Date.now();
106929
107141
  }
106930
107142
  if (workerFeedEnabled) {
106931
- const wk = resolveWorkerFeedChat(agentId, fleetChatId);
106932
- const wkChat = wk.chatId;
106933
- workerActivityFeed?.update(agentId, wkChat, {
107143
+ const dest = decideWorkerFeedDestination({
107144
+ origin: resolveSubagentOriginChat(agentId),
107145
+ cardExists: workerActivityFeed?.has(agentId) === true,
107146
+ priorDeferrals: workerFeedOriginDeferrals.get(agentId) ?? 0,
107147
+ maxDeferrals: WORKER_FEED_ORIGIN_DEFER_MAX,
107148
+ fleetChatId,
107149
+ stampChatId: stampTurn?.sessionChatId,
107150
+ stampThreadId: stampTurn?.sessionThreadId,
107151
+ ownerDm: loadAccess().allowFrom[0] ?? ""
107152
+ });
107153
+ if (dest.action === "defer") {
107154
+ workerFeedOriginDeferrals.set(agentId, dest.deferrals);
107155
+ return;
107156
+ }
107157
+ workerFeedOriginDeferrals.delete(agentId);
107158
+ if (dest.exhausted) {
107159
+ process.stderr.write(`telegram gateway: worker-feed origin backfill never linked agent=${agentId} after ${dest.deferrals} deferrals \u2014 painting card
107160
+ `);
107161
+ }
107162
+ if (dest.ownerDmFallback)
107163
+ noteWorkerFeedOwnerDmFallback(agentId);
107164
+ workerActivityFeed?.update(agentId, dest.chatId, {
106934
107165
  description: dispatch.feedDescription,
106935
107166
  lastTool,
106936
107167
  toolCount,
@@ -106939,7 +107170,7 @@ async function startGateway() {
106939
107170
  state: "running",
106940
107171
  model: feedModel,
106941
107172
  totalTokens
106942
- }, wk.threadId);
107173
+ }, dest.threadId);
106943
107174
  return;
106944
107175
  }
106945
107176
  const progressOrigin = resolveSubagentOriginChat(agentId);
@@ -107004,7 +107235,7 @@ async function startGateway() {
107004
107235
  await runnerHandle.task();
107005
107236
  return;
107006
107237
  } catch (err) {
107007
- if (err instanceof import_grammy14.GrammyError && err.error_code === 409) {
107238
+ if (err instanceof import_grammy15.GrammyError && err.error_code === 409) {
107008
107239
  const delay = Math.min(1000 * attempt, 15000);
107009
107240
  const agentName3 = process.env.SWITCHROOM_AGENT_NAME ?? "-";
107010
107241
  process.stderr.write(`telegram gateway: poll.409.detected attempt=${attempt} retry_in_ms=${delay} agent=${agentName3}