switchroom 0.16.24 → 0.16.28

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 (28) hide show
  1. package/dist/cli/switchroom.js +135 -37
  2. package/dist/host-control/main.js +13 -7
  3. package/package.json +2 -2
  4. package/telegram-plugin/answer-stream.ts +7 -6
  5. package/telegram-plugin/bridge/bridge.ts +1 -1
  6. package/telegram-plugin/dist/bridge/bridge.js +1 -1
  7. package/telegram-plugin/dist/gateway/gateway.js +255 -30
  8. package/telegram-plugin/dist/server.js +1 -1
  9. package/telegram-plugin/format.ts +335 -27
  10. package/telegram-plugin/gateway/drive-write-approval.test.ts +10 -10
  11. package/telegram-plugin/gateway/drive-write-approval.ts +14 -8
  12. package/telegram-plugin/gateway/gateway.ts +169 -12
  13. package/telegram-plugin/gateway/ipc-server.ts +11 -6
  14. package/telegram-plugin/gateway/permission-timeout.ts +76 -0
  15. package/telegram-plugin/permission-title.ts +3 -0
  16. package/telegram-plugin/retry-api-call.ts +27 -0
  17. package/telegram-plugin/rich-send.ts +29 -0
  18. package/telegram-plugin/shared/bot-runtime.ts +6 -1
  19. package/telegram-plugin/silent-reply-anchor.ts +9 -2
  20. package/telegram-plugin/status-no-truncate.ts +11 -5
  21. package/telegram-plugin/stream-reply-handler.ts +9 -1
  22. package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +6 -2
  23. package/telegram-plugin/tests/length-error-classify.test.ts +131 -0
  24. package/telegram-plugin/tests/paragraph-normalizer.test.ts +273 -0
  25. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +12 -2
  26. package/telegram-plugin/tests/permission-timeout.test.ts +77 -0
  27. package/telegram-plugin/tests/permission-title.test.ts +43 -0
  28. package/telegram-plugin/tests/poll-health.test.ts +64 -0
@@ -6588,9 +6588,17 @@ function richMessage(markdown) {
6588
6588
  function isParseEntitiesError(err) {
6589
6589
  if (!(err instanceof import_grammy.GrammyError) || err.error_code !== 400)
6590
6590
  return false;
6591
+ if (isLengthError(err))
6592
+ return false;
6591
6593
  const d = (err.description || "").toLowerCase();
6592
6594
  return d.includes("can't parse entities") || d.includes("can\u2019t parse entities") || d.includes("can't parse") || d.includes("can\u2019t parse") || d.includes("parse markdown") || d.includes("parse rich") || d.includes("can't find end of the entity") || d.includes("can\u2019t find end of the entity") || d.includes("unsupported start tag") || d.includes("unclosed start tag") || d.includes("expected end tag");
6593
6595
  }
6596
+ function isLengthError(err) {
6597
+ if (!(err instanceof import_grammy.GrammyError) || err.error_code !== 400)
6598
+ return false;
6599
+ const d = (err.description || "").toLowerCase();
6600
+ return d.includes("rich_message_text_too_long") || d.includes("message_too_long") || d.includes("text_too_long") || d.includes("message is too long") || d.includes("text is too long");
6601
+ }
6594
6602
  var import_grammy;
6595
6603
  var init_rich_send = __esm(() => {
6596
6604
  import_grammy = __toESM(require_mod2(), 1);
@@ -33006,7 +33014,7 @@ init_card_format();
33006
33014
  // status-no-truncate.ts
33007
33015
  var STATUS_ROLLING_LINES = 5;
33008
33016
  var STATUS_LINE_MAX = 200;
33009
- var STATUS_CARD_CHAR_BUDGET = 4000;
33017
+ var STATUS_CARD_CHAR_BUDGET = RICH_MESSAGE_MAX_CHARS;
33010
33018
  var NESTED_PREFIX = " \u21b3 ";
33011
33019
 
33012
33020
  // tool-activity-summary.ts
@@ -33545,6 +33553,23 @@ function isRecentTimeoutDuplicate(timeouts, sig, now, windowMs) {
33545
33553
  const at = timeouts.get(sig);
33546
33554
  return at != null && now - at <= windowMs;
33547
33555
  }
33556
+ var PERMISSION_TTL_MS = 10 * 60000;
33557
+ var HOSTD_PERMISSION_TTL_MS = 30 * 60000;
33558
+ function ttlForTool(toolName) {
33559
+ return toolName && toolName.startsWith("mcp__hostd__") ? HOSTD_PERMISSION_TTL_MS : PERMISSION_TTL_MS;
33560
+ }
33561
+ var TIMED_OUT_FOOTER = `
33562
+
33563
+ \u23f1 Timed out \u2014 re-request to act`;
33564
+ function buildTimedOutCardEdits(cardText, cards) {
33565
+ return cards.map(({ chatId, messageId }) => ({
33566
+ chatId,
33567
+ messageId,
33568
+ text: `${cardText}${TIMED_OUT_FOOTER}`,
33569
+ stripKeyboard: true
33570
+ }));
33571
+ }
33572
+ var STALE_TAP_NOTICE = "This request already resolved (timed out) \u2014 ask again to act.";
33548
33573
 
33549
33574
  // gateway/permission-card-origin.ts
33550
33575
  function pickRecoveredPermissionOrigin(recentTurns, now, maxAgeMs) {
@@ -34452,7 +34477,7 @@ function streamKey2(chatId, threadId, lane, turnKey) {
34452
34477
  }
34453
34478
  async function handleStreamReply(args, state, deps) {
34454
34479
  const chat_id = args.chat_id;
34455
- const rawText = deps.repairEscapedWhitespace(args.text);
34480
+ const rawText = deps.normalizeParagraphBreaks ? deps.normalizeParagraphBreaks(deps.repairEscapedWhitespace(args.text)) : deps.repairEscapedWhitespace(args.text);
34456
34481
  const done = Boolean(args.done);
34457
34482
  const format = args.format ?? deps.defaultFormat;
34458
34483
  if (done) {
@@ -34736,9 +34761,17 @@ async function retryWithThreadFallback(retry, send, opts) {
34736
34761
  function isHtmlParseRejectError(err) {
34737
34762
  if (!(err instanceof import_grammy2.GrammyError) || err.error_code !== 400)
34738
34763
  return false;
34764
+ if (isMessageTooLongError(err))
34765
+ return false;
34739
34766
  const d = (err.description || "").toLowerCase();
34740
34767
  return d.includes("can't parse entities") || d.includes("can\u2019t parse entities") || d.includes("can't parse") || d.includes("can\u2019t parse") || d.includes("parse markdown") || d.includes("parse rich") || d.includes("unsupported start tag") || d.includes("unclosed start tag") || d.includes("can't find end of the entity") || d.includes("can\u2019t find end of the entity") || d.includes("expected end tag");
34741
34768
  }
34769
+ function isMessageTooLongError(err) {
34770
+ if (!(err instanceof import_grammy2.GrammyError) || err.error_code !== 400)
34771
+ return false;
34772
+ const d = (err.description || "").toLowerCase();
34773
+ return d.includes("rich_message_text_too_long") || d.includes("message_too_long") || d.includes("text_too_long") || d.includes("message is too long") || d.includes("text is too long");
34774
+ }
34742
34775
 
34743
34776
  // shared/bot-runtime.ts
34744
34777
  var import_grammy4 = __toESM(require_mod2(), 1);
@@ -39603,7 +39636,7 @@ function decideOverPing(input) {
39603
39636
  }
39604
39637
 
39605
39638
  // silent-reply-anchor.ts
39606
- var TELEGRAM_MSG_CAP = 4000;
39639
+ var TELEGRAM_MSG_CAP = RICH_MESSAGE_MAX_CHARS;
39607
39640
  function enabled2() {
39608
39641
  const v = process.env.SWITCHROOM_DISABLE_SILENT_REPLY_AUTOEDIT;
39609
39642
  return !(v === "1" || v === "true");
@@ -43235,8 +43268,14 @@ function repairEscapedWhitespace(text) {
43235
43268
  if (!/\\[nrt"\\]/.test(text))
43236
43269
  return text;
43237
43270
  const nonce = Math.random().toString(36).slice(2);
43238
- const CODE_MASK_PH = `\x00RM${nonce}_`;
43239
43271
  const BACKSLASH_PH = `\x00BK${nonce}_`;
43272
+ const { masked, restore } = maskCodeRegions(text, nonce);
43273
+ const unescaped = masked.replace(/\\\\/g, BACKSLASH_PH).replace(/\\n/g, `
43274
+ `).replace(/\\r/g, "\r").replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(new RegExp(BACKSLASH_PH.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "\\");
43275
+ return restore(unescaped);
43276
+ }
43277
+ function maskCodeRegions(text, nonce) {
43278
+ const CODE_MASK_PH = `\x00RM${nonce}_`;
43240
43279
  const codeMasks = [];
43241
43280
  const masked = text.replace(/```[\s\S]*?```/g, (m) => {
43242
43281
  const idx = codeMasks.length;
@@ -43247,10 +43286,127 @@ function repairEscapedWhitespace(text) {
43247
43286
  codeMasks.push(m);
43248
43287
  return `${CODE_MASK_PH}${idx}\x00`;
43249
43288
  });
43250
- const unescaped = masked.replace(/\\\\/g, BACKSLASH_PH).replace(/\\n/g, `
43251
- `).replace(/\\r/g, "\r").replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(new RegExp(BACKSLASH_PH.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "\\");
43252
43289
  const restoreRe = new RegExp(`${CODE_MASK_PH.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d+)\x00`, "g");
43253
- return unescaped.replace(restoreRe, (_m, idx) => codeMasks[Number(idx)] ?? _m);
43290
+ const restore = (s) => s.replace(restoreRe, (_m, idx) => codeMasks[Number(idx)] ?? _m);
43291
+ return { masked, restore, placeholder: CODE_MASK_PH };
43292
+ }
43293
+ function normalizeParagraphBreaks(text) {
43294
+ if (!text.includes(`
43295
+ `))
43296
+ return text;
43297
+ const nonce = Math.random().toString(36).slice(2);
43298
+ const { masked, restore, placeholder } = maskCodeRegions(text, nonce);
43299
+ let out = masked.replace(/\n{3,}/g, `
43300
+
43301
+ `);
43302
+ const lines = out.split(`
43303
+ `);
43304
+ const pieces = [];
43305
+ for (let i = 0;i < lines.length; i++) {
43306
+ let line = lines[i];
43307
+ const isLast = i === lines.length - 1;
43308
+ const next = isLast ? "" : lines[i + 1];
43309
+ const promote = !isLast && line.trim() !== "" && next.trim() !== "" && shouldPromoteBreak(line, next, placeholder);
43310
+ if (promote) {
43311
+ line = line.replace(/[ \t\r]+$/, "");
43312
+ }
43313
+ pieces.push(line);
43314
+ if (isLast)
43315
+ break;
43316
+ pieces.push(promote ? `
43317
+ ` : `
43318
+ `);
43319
+ }
43320
+ out = pieces.join("");
43321
+ out = ensureBlockBoundaries(out, placeholder);
43322
+ return restore(out);
43323
+ }
43324
+ function isListItemLine(line) {
43325
+ const t = line.trimStart();
43326
+ return /^[-*+]\s/.test(t) || /^\d+[.)]\s/.test(t);
43327
+ }
43328
+ function isTableRowLine(line) {
43329
+ return /^\s*\|/.test(line);
43330
+ }
43331
+ function isTableDelimiterLine(line) {
43332
+ return /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/.test(line);
43333
+ }
43334
+ function isFenceOpenLine(line, placeholder) {
43335
+ const t = line.trimStart();
43336
+ if (placeholder != null && placeholder.length > 0 && t.startsWith(placeholder))
43337
+ return true;
43338
+ return t.startsWith("```");
43339
+ }
43340
+ function isBlockquoteLine(line) {
43341
+ return line.trimStart().startsWith(">");
43342
+ }
43343
+ function isHeadingLine(line) {
43344
+ return /^#{1,6}\s/.test(line.trimStart());
43345
+ }
43346
+ function ensureBlockBoundaries(text, placeholder) {
43347
+ if (!text.includes(`
43348
+ `))
43349
+ return text;
43350
+ const lines = text.split(`
43351
+ `);
43352
+ const result = [];
43353
+ for (let i = 0;i < lines.length; i++) {
43354
+ const line = lines[i];
43355
+ const prev = result.length > 0 ? result[result.length - 1] : null;
43356
+ const prevNonBlank = prev != null && prev.trim() !== "";
43357
+ const curBlank = line.trim() === "";
43358
+ if (prevNonBlank && !curBlank) {
43359
+ const next = i + 1 < lines.length ? lines[i + 1] : "";
43360
+ const prevIsTable = isTableRowLine(prev);
43361
+ const startsTableHere = !prevIsTable && (line.includes("|") && isTableDelimiterLine(next) || isTableRowLine(line) && isTableDelimiterLine(next));
43362
+ const startsFence = isFenceOpenLine(line, placeholder) && !isFenceOpenLine(prev, placeholder);
43363
+ const startsQuote = isBlockquoteLine(line) && !isBlockquoteLine(prev);
43364
+ const startsHeading = isHeadingLine(line) && !isHeadingLine(prev);
43365
+ if (startsTableHere || startsFence || startsQuote || startsHeading) {
43366
+ result.push("");
43367
+ }
43368
+ }
43369
+ if (prevNonBlank && !curBlank && isListItemLine(prev) && !isListItemLine(line)) {
43370
+ const isIndentedContinuation = /^(\t| {4,})\S/.test(line);
43371
+ const alreadySeparated = result.length > 0 && result[result.length - 1].trim() === "";
43372
+ if (!isIndentedContinuation && !alreadySeparated) {
43373
+ result.push("");
43374
+ }
43375
+ }
43376
+ result.push(line);
43377
+ }
43378
+ return result.join(`
43379
+ `);
43380
+ }
43381
+ function isMarkerLine(line, placeholder) {
43382
+ if (/^ {4,}\S/.test(line))
43383
+ return true;
43384
+ const t = line.trimStart();
43385
+ if (placeholder != null && placeholder.length > 0 && t.startsWith(placeholder))
43386
+ return true;
43387
+ return /^[-*+]\s/.test(t) || /^\d+[.)]\s/.test(t) || t.startsWith(">") || /^#{1,6}\s/.test(t) || t.startsWith("|") || line.includes(" | ") || t.startsWith("```") || /^(-{3,}|\*{3,}|_{3,})\s*$/.test(t);
43388
+ }
43389
+ function shouldPromoteBreak(prev, next, placeholder) {
43390
+ if (isMarkerLine(prev, placeholder) || isMarkerLine(next, placeholder))
43391
+ return false;
43392
+ const nextTrimmed = next.trimStart();
43393
+ if (nextTrimmed.length === 0)
43394
+ return false;
43395
+ const prevTrimmed = prev.trimEnd();
43396
+ const unwrapped = prevTrimmed.replace(/[)"'\u2019\u201d\]]+$/, "");
43397
+ const terminator = unwrapped.slice(-1);
43398
+ return terminator === "." || terminator === "!" || terminator === "?" || terminator === ":";
43399
+ }
43400
+ function hardSliceToCap(text, cap = RICH_MESSAGE_MAX_CHARS2) {
43401
+ if (cap <= 0)
43402
+ return [text];
43403
+ if (text.length <= cap)
43404
+ return [text];
43405
+ const out = [];
43406
+ for (let i = 0;i < text.length; i += cap) {
43407
+ out.push(text.slice(i, i + cap));
43408
+ }
43409
+ return out;
43254
43410
  }
43255
43411
  function splitMarkdownChunks(text, maxLen = RICH_MESSAGE_MAX_CHARS2) {
43256
43412
  if (text.length <= maxLen)
@@ -47227,7 +47383,8 @@ var UpdateApplyRequestSchema = exports_external.object({
47227
47383
  skip_images: exports_external.boolean().optional(),
47228
47384
  rebuild: exports_external.boolean().optional(),
47229
47385
  channel: exports_external.enum(["dev", "rc", "latest"]).nullable().optional(),
47230
- pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).nullable().optional()
47386
+ pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).nullable().optional(),
47387
+ reason: exports_external.string().max(512).optional()
47231
47388
  }).optional()
47232
47389
  });
47233
47390
  var ApplyRequestSchema = exports_external.object({
@@ -47242,21 +47399,24 @@ var RolloutRequestSchema = exports_external.object({
47242
47399
  pin: exports_external.string().regex(/^v\d+\.\d+\.\d+$/),
47243
47400
  agents: exports_external.array(AgentNameSchema2).min(1).optional(),
47244
47401
  skip_web: exports_external.boolean().optional(),
47245
- allow_downgrade: exports_external.boolean().optional()
47402
+ allow_downgrade: exports_external.boolean().optional(),
47403
+ reason: exports_external.string().max(512).optional()
47246
47404
  }).required({ pin: true })
47247
47405
  });
47248
47406
  var AgentStartRequestSchema = exports_external.object({
47249
47407
  ...RequestEnvelope,
47250
47408
  op: exports_external.literal("agent_start"),
47251
47409
  args: exports_external.object({
47252
- name: AgentNameSchema2
47410
+ name: AgentNameSchema2,
47411
+ reason: exports_external.string().max(512).optional()
47253
47412
  })
47254
47413
  });
47255
47414
  var AgentStopRequestSchema = exports_external.object({
47256
47415
  ...RequestEnvelope,
47257
47416
  op: exports_external.literal("agent_stop"),
47258
47417
  args: exports_external.object({
47259
- name: AgentNameSchema2
47418
+ name: AgentNameSchema2,
47419
+ reason: exports_external.string().max(512).optional()
47260
47420
  })
47261
47421
  });
47262
47422
  var AgentLogsRequestSchema = exports_external.object({
@@ -47264,7 +47424,8 @@ var AgentLogsRequestSchema = exports_external.object({
47264
47424
  op: exports_external.literal("agent_logs"),
47265
47425
  args: exports_external.object({
47266
47426
  name: AgentNameSchema2,
47267
- tail: exports_external.number().int().positive().max(2000).optional()
47427
+ tail: exports_external.number().int().positive().max(2000).optional(),
47428
+ reason: exports_external.string().max(512).optional()
47268
47429
  })
47269
47430
  });
47270
47431
  var AgentExecRequestSchema = exports_external.object({
@@ -47272,7 +47433,8 @@ var AgentExecRequestSchema = exports_external.object({
47272
47433
  op: exports_external.literal("agent_exec"),
47273
47434
  args: exports_external.object({
47274
47435
  name: AgentNameSchema2,
47275
- argv: exports_external.array(exports_external.string().min(1)).min(1).max(32)
47436
+ argv: exports_external.array(exports_external.string().min(1)).min(1).max(32),
47437
+ reason: exports_external.string().max(512).optional()
47276
47438
  })
47277
47439
  });
47278
47440
  var DoctorRequestSchema = exports_external.object({
@@ -48480,7 +48642,7 @@ function validateClientMessage(msg) {
48480
48642
  return false;
48481
48643
  if (typeof m.chatId !== "string" || m.chatId.length === 0)
48482
48644
  return false;
48483
- if (typeof m.text !== "string" || m.text.length === 0 || m.text.length > 4096)
48645
+ if (typeof m.text !== "string" || m.text.length === 0 || m.text.length > RICH_MESSAGE_MAX_CHARS)
48484
48646
  return false;
48485
48647
  if (m.threadId !== undefined && (typeof m.threadId !== "number" || !Number.isInteger(m.threadId)))
48486
48648
  return false;
@@ -49044,8 +49206,8 @@ function validateInput(input) {
49044
49206
  var DEFAULT_TTL_MS = 5 * 60 * 1000;
49045
49207
  var MAX_TTL_MS = 30 * 60 * 1000;
49046
49208
  var MIN_TTL_MS = 30 * 1000;
49047
- var TELEGRAM_SENDMESSAGE_LIMIT = 4096;
49048
- var RENDERED_BODY_CAP = 3900;
49209
+ var TELEGRAM_SENDMESSAGE_LIMIT = RICH_MESSAGE_MAX_CHARS;
49210
+ var RENDERED_BODY_CAP = RICH_MESSAGE_MAX_CHARS - 200;
49049
49211
  var OVERSIZE_SENTINEL = `
49050
49212
  [\u2026 preview truncated; open in Drive for full context]`;
49051
49213
  async function handleRequestDriveApproval(client3, msg, deps) {
@@ -54699,6 +54861,9 @@ var MCP_TOOL_DESCRIPTIONS = {
54699
54861
  mcp__hostd__agent_exec: "Run a read-only inspection inside another agent",
54700
54862
  mcp__hostd__update_check: "Check what a fleet-wide update would do",
54701
54863
  mcp__hostd__update_apply: "Apply a fleet-wide update (pull + recreate)",
54864
+ mcp__hostd__rollout: "Roll the fleet to a pinned version",
54865
+ mcp__hostd__config_propose_edit: "Propose an edit to switchroom.yaml",
54866
+ mcp__hostd__get_status: "Read the last fleet-update status",
54702
54867
  mcp__hindsight__recall: "Recall relevant memories",
54703
54868
  mcp__hindsight__retain: "Retain a memory",
54704
54869
  mcp__hindsight__reflect: "Reflect across its memory bank",
@@ -55999,11 +56164,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
55999
56164
  }
56000
56165
 
56001
56166
  // ../src/build-info.ts
56002
- var VERSION = "0.16.23";
56003
- var COMMIT_SHA = "3c17304c";
56004
- var COMMIT_DATE = "2026-06-30T08:50:37+10:00";
56005
- var LATEST_PR = null;
56006
- var COMMITS_AHEAD_OF_TAG = 6;
56167
+ var VERSION = "0.16.28";
56168
+ var COMMIT_SHA = "08b2121e";
56169
+ var COMMIT_DATE = "2026-06-30T04:25:13Z";
56170
+ var LATEST_PR = 2683;
56171
+ var COMMITS_AHEAD_OF_TAG = 0;
56007
56172
 
56008
56173
  // gateway/boot-version.ts
56009
56174
  function formatRelativeAgo(iso) {
@@ -57520,6 +57685,19 @@ var TOPIC_ID = process.env.TELEGRAM_TOPIC_ID ? Number(process.env.TELEGRAM_TOPIC
57520
57685
  var AGENT_ADMIN = process.env.SWITCHROOM_AGENT_ADMIN === "true";
57521
57686
  var bot = new import_grammy11.Bot(TOKEN);
57522
57687
  installTgPostLogger(bot);
57688
+ var lastGetUpdatesHeartbeatMs = Date.now();
57689
+ bot.api.config.use(async (prev, method, payload, signal) => {
57690
+ try {
57691
+ const result = await prev(method, payload, signal);
57692
+ if (method === "getUpdates")
57693
+ lastGetUpdatesHeartbeatMs = Date.now();
57694
+ return result;
57695
+ } catch (err) {
57696
+ if (method === "getUpdates")
57697
+ lastGetUpdatesHeartbeatMs = Date.now();
57698
+ throw err;
57699
+ }
57700
+ });
57523
57701
  var GRAMMY_VERSION = (() => {
57524
57702
  try {
57525
57703
  const raw = readFileSync37(new URL("../../node_modules/grammy/package.json", import.meta.url), "utf8");
@@ -58946,7 +59124,6 @@ function logOutbound(path, chatId, messageId, chars, extra, opts) {
58946
59124
  var STATUS_QUERY_RE = /^\s*status\??\s*$/i;
58947
59125
  var PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
58948
59126
  var pendingPermissions = new Map;
58949
- var PERMISSION_TTL_MS = 600000;
58950
59127
  var PERMISSION_NO_REPEAT_ENABLED = process.env.SWITCHROOM_PERMISSION_NO_REPEAT !== "0";
58951
59128
  var PERMISSION_DUPLICATE_WINDOW_MS = 3600000;
58952
59129
  var permissionTimeoutSignatures = new Map;
@@ -59188,14 +59365,16 @@ var pendingStateReaper = setInterval(() => {
59188
59365
  pendingVaultOps.delete(k);
59189
59366
  }
59190
59367
  for (const [k, v] of pendingPermissions) {
59191
- if (now - v.startedAt > PERMISSION_TTL_MS) {
59192
- const timeoutMinutes = Math.round(PERMISSION_TTL_MS / 60000);
59368
+ const ttl = ttlForTool(v.tool_name);
59369
+ if (now - v.startedAt > ttl) {
59370
+ const timeoutMinutes = Math.round(ttl / 60000);
59193
59371
  dispatchPermissionVerdict({
59194
59372
  type: "permission",
59195
59373
  requestId: k,
59196
59374
  behavior: "deny",
59197
59375
  message: timeoutDenyMessage(timeoutMinutes)
59198
59376
  });
59377
+ stripTimedOutPermissionCards(v.card_text, v.cards);
59199
59378
  resumeReactionAfterVerdict();
59200
59379
  postPermissionResumeMessage({
59201
59380
  behavior: "deny",
@@ -59837,6 +60016,11 @@ function buildPermissionActionRow(requestId, showAlways) {
59837
60016
  kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
59838
60017
  return kb;
59839
60018
  }
60019
+ async function stripTimedOutPermissionCards(cardText, cards) {
60020
+ for (const edit of buildTimedOutCardEdits(cardText, cards)) {
60021
+ await swallowingApiCall(() => bot.api.editMessageText(edit.chatId, edit.messageId, richMessage2(edit.text), {}), { chat_id: edit.chatId, verb: "permission_timeout.strip" });
60022
+ }
60023
+ }
59840
60024
  function dispatchPermissionVerdict(ev) {
59841
60025
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
59842
60026
  const delivered = ipcServer.sendToAgent(selfAgent, ev);
@@ -60094,13 +60278,13 @@ var ipcServer = createIpcServer({
60094
60278
  return;
60095
60279
  }
60096
60280
  }
60097
- pendingPermissions.set(requestId, { tool_name: toolName, description, input_preview: inputPreview, startedAt: Date.now() });
60098
60281
  const text2 = formatPermissionCardBody({
60099
60282
  toolName,
60100
60283
  inputPreview,
60101
60284
  description,
60102
60285
  agentName: _client.agentName
60103
60286
  });
60287
+ pendingPermissions.set(requestId, { tool_name: toolName, description, input_preview: inputPreview, startedAt: Date.now(), card_text: text2, cards: [] });
60104
60288
  const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null;
60105
60289
  const keyboard = buildPermissionActionRow(requestId, showAlways);
60106
60290
  const activeTurn = currentTurn;
@@ -60109,7 +60293,12 @@ var ipcServer = createIpcServer({
60109
60293
  retryWithThreadFallback(robustApiCall, (tid) => bot.api.sendRichMessage(chatId, richMessage2(text2), {
60110
60294
  reply_markup: keyboard,
60111
60295
  ...tid != null ? { message_thread_id: tid } : {}
60112
- }), { threadId, chat_id: chatId, verb: "permission_request" }).catch((e) => {
60296
+ }), { threadId, chat_id: chatId, verb: "permission_request" }).then((sent) => {
60297
+ const pend = pendingPermissions.get(requestId);
60298
+ if (pend && sent && typeof sent.message_id === "number") {
60299
+ pend.cards.push({ chatId, messageId: sent.message_id });
60300
+ }
60301
+ }).catch((e) => {
60113
60302
  process.stderr.write(`telegram gateway: permission_request send to ${chatId} failed: ${e}
60114
60303
  `);
60115
60304
  });
@@ -60757,7 +60946,7 @@ async function executeReply(args) {
60757
60946
  const rawText = args.text;
60758
60947
  if (rawText == null || rawText === "")
60759
60948
  throw new Error("reply: text is required and cannot be empty");
60760
- let text2 = repairEscapedWhitespace(rawText);
60949
+ let text2 = normalizeParagraphBreaks(repairEscapedWhitespace(rawText));
60761
60950
  text2 = redactOutboundText(text2, "reply");
60762
60951
  {
60763
60952
  const scrub = scrubVoice(text2);
@@ -61102,6 +61291,24 @@ ${url}`;
61102
61291
  delete richOpts.link_preview_options;
61103
61292
  return lockedBot.api.sendRichMessage(chat_id, richMessage2(chunks[i]), richOpts);
61104
61293
  };
61294
+ const sendChunkResplit = async (opts) => {
61295
+ const subPieces = splitMarkdownChunks(chunks[i], RICH_MESSAGE_MAX_CHARS2);
61296
+ const pieces = subPieces.length > 1 ? subPieces : hardSliceToCap(chunks[i], RICH_MESSAGE_MAX_CHARS2);
61297
+ for (let p = 0;p < pieces.length; p++) {
61298
+ let sent;
61299
+ if (literalText) {
61300
+ sent = await lockedBot.api.sendMessage(chat_id, pieces[p], opts);
61301
+ } else {
61302
+ const ro = { ...opts };
61303
+ delete ro.link_preview_options;
61304
+ sent = await lockedBot.api.sendRichMessage(chat_id, richMessage2(pieces[p]), ro);
61305
+ }
61306
+ sentIds.push(sent.message_id);
61307
+ logOutbound("reply", chat_id, sent.message_id, pieces[p].length, `chunk=${i + 1}/${chunks.length} resplit=${p + 1}/${pieces.length}`);
61308
+ }
61309
+ process.stderr.write(`telegram gateway: rich body too long \u2014 re-split chunk ${i + 1}/${chunks.length} into ${pieces.length} piece(s)
61310
+ `);
61311
+ };
61105
61312
  try {
61106
61313
  const sent = await robustApiCall(() => sendChunk(sendOpts), { threadId, chat_id });
61107
61314
  sentIds.push(sent.message_id);
@@ -61115,11 +61322,15 @@ ${url}`;
61115
61322
  const sent = await sendChunk(retryOpts);
61116
61323
  sentIds.push(sent.message_id);
61117
61324
  } catch (retryErr) {
61118
- if (isHtmlParseRejectError(retryErr))
61325
+ if (isMessageTooLongError(retryErr))
61326
+ await sendChunkResplit(retryOpts);
61327
+ else if (isHtmlParseRejectError(retryErr))
61119
61328
  await sendChunkPlainText(retryOpts);
61120
61329
  else
61121
61330
  throw retryErr;
61122
61331
  }
61332
+ } else if (isMessageTooLongError(err)) {
61333
+ await sendChunkResplit(sendOpts);
61123
61334
  } else if (isHtmlParseRejectError(err)) {
61124
61335
  await sendChunkPlainText(sendOpts);
61125
61336
  } else {
@@ -61369,6 +61580,7 @@ async function executeStreamReply(args) {
61369
61580
  bot: lockedBot,
61370
61581
  retry: robustApiCall,
61371
61582
  repairEscapedWhitespace,
61583
+ normalizeParagraphBreaks,
61372
61584
  assertAllowedChat,
61373
61585
  resolveThreadId,
61374
61586
  disableLinkPreview: access.disableLinkPreview !== false,
@@ -62199,6 +62411,8 @@ async function executeEditMessage(args) {
62199
62411
  const editFormat = args.format ?? (editAccess.parseMode ?? "html");
62200
62412
  const editLiteralText = editFormat === "text";
62201
62413
  let editRawText = repairEscapedWhitespace(args.text);
62414
+ if (!editLiteralText)
62415
+ editRawText = normalizeParagraphBreaks(editRawText);
62202
62416
  editRawText = redactOutboundText(editRawText, "edit_message");
62203
62417
  {
62204
62418
  const scrub = scrubVoice(editRawText);
@@ -64353,7 +64567,7 @@ function switchroomExecCombined(args, timeoutMs = 15000) {
64353
64567
  shell: "/bin/bash"
64354
64568
  });
64355
64569
  }
64356
- function formatSwitchroomOutput(output, maxLen = 4000) {
64570
+ function formatSwitchroomOutput(output, maxLen = RICH_MESSAGE_MAX_CHARS2) {
64357
64571
  const trimmed = output.trim();
64358
64572
  if (trimmed.length <= maxLen)
64359
64573
  return trimmed;
@@ -68699,6 +68913,10 @@ ${editLabel}` : editLabel
68699
68913
  });
68700
68914
  return;
68701
68915
  }
68916
+ if (!pendingPermissions.has(request_id)) {
68917
+ await ctx.answerCallbackQuery({ text: STALE_TAP_NOTICE }).catch(() => {});
68918
+ return;
68919
+ }
68702
68920
  clearPermissionTimeoutSuppression("operator answered a permission card");
68703
68921
  const pd = pendingPermissions.get(request_id);
68704
68922
  const resumeAction = pd ? naturalAction(pd.tool_name, pd.input_preview) : "";
@@ -69551,7 +69769,14 @@ var POLL_HEALTH_THRESHOLD = Number(process.env.SWITCHROOM_POLL_HEALTH_THRESHOLD
69551
69769
  var pollHealthCheck = null;
69552
69770
  if (POLL_HEALTH_INTERVAL_MS > 0) {
69553
69771
  pollHealthCheck = createPollHealthCheck({
69554
- ping: () => bot.api.getMe(),
69772
+ ping: async () => {
69773
+ await bot.api.getMe();
69774
+ const staleMs = Date.now() - lastGetUpdatesHeartbeatMs;
69775
+ const staleThresholdMs = POLL_HEALTH_INTERVAL_MS * POLL_HEALTH_THRESHOLD;
69776
+ if (staleMs > staleThresholdMs) {
69777
+ throw new Error(`getUpdates heartbeat stale: last seen ${Math.round(staleMs / 1000)}s ago (threshold ${Math.round(staleThresholdMs / 1000)}s) \u2014 runner loop frozen`);
69778
+ }
69779
+ },
69555
69780
  onStall: async () => {
69556
69781
  recoverFromPollStall({ agentName: process.env.SWITCHROOM_AGENT_NAME ?? "-" });
69557
69782
  },
@@ -24388,7 +24388,7 @@ var init_bridge = __esm(async () => {
24388
24388
  },
24389
24389
  {
24390
24390
  name: "stream_reply",
24391
- description: "Post the final answer for this turn. The plugin renders an event-driven progress card (Plan \u2192 Run \u2192 Done with live tool bullets, elapsed time, and status emoji) for free while the turn is in-flight, so you do not need to narrate intermediate progress. Call `stream_reply` exactly once per turn with done=true and the complete answer text. Hard-stops at 4096 chars \u2014 longer text throws; fall back to `reply`, which chunks. Calling with done=false is an error in this environment (the progress card already owns the mid-turn surface). inline_keyboard adds tappable buttons under the final message \u2014 see `reply` for shape and constraints.",
24391
+ description: "Post the final answer for this turn. The plugin renders an event-driven progress card (Plan \u2192 Run \u2192 Done with live tool bullets, elapsed time, and status emoji) for free while the turn is in-flight, so you do not need to narrate intermediate progress. Call `stream_reply` exactly once per turn with done=true and the complete answer text. Hard cap is 32768 chars (the rich-message wire limit) \u2014 longer text is dropped by a defensive guard, so use `reply` for anything that long (it chunks). Calling with done=false is an error in this environment (the progress card already owns the mid-turn surface). inline_keyboard adds tappable buttons under the final message \u2014 see `reply` for shape and constraints.",
24392
24392
  inputSchema: {
24393
24393
  type: "object",
24394
24394
  properties: {