switchroom 0.16.24 → 0.16.27

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.
@@ -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";
56167
+ var VERSION = "0.16.27";
56168
+ var COMMIT_SHA = "68a4feed";
56169
+ var COMMIT_DATE = "2026-06-30T14:02:23+10:00";
56005
56170
  var LATEST_PR = null;
56006
- var COMMITS_AHEAD_OF_TAG = 6;
56171
+ var COMMITS_AHEAD_OF_TAG = 2;
56007
56172
 
56008
56173
  // gateway/boot-version.ts
56009
56174
  function formatRelativeAgo(iso) {
@@ -58946,7 +59111,6 @@ function logOutbound(path, chatId, messageId, chars, extra, opts) {
58946
59111
  var STATUS_QUERY_RE = /^\s*status\??\s*$/i;
58947
59112
  var PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
58948
59113
  var pendingPermissions = new Map;
58949
- var PERMISSION_TTL_MS = 600000;
58950
59114
  var PERMISSION_NO_REPEAT_ENABLED = process.env.SWITCHROOM_PERMISSION_NO_REPEAT !== "0";
58951
59115
  var PERMISSION_DUPLICATE_WINDOW_MS = 3600000;
58952
59116
  var permissionTimeoutSignatures = new Map;
@@ -59188,14 +59352,16 @@ var pendingStateReaper = setInterval(() => {
59188
59352
  pendingVaultOps.delete(k);
59189
59353
  }
59190
59354
  for (const [k, v] of pendingPermissions) {
59191
- if (now - v.startedAt > PERMISSION_TTL_MS) {
59192
- const timeoutMinutes = Math.round(PERMISSION_TTL_MS / 60000);
59355
+ const ttl = ttlForTool(v.tool_name);
59356
+ if (now - v.startedAt > ttl) {
59357
+ const timeoutMinutes = Math.round(ttl / 60000);
59193
59358
  dispatchPermissionVerdict({
59194
59359
  type: "permission",
59195
59360
  requestId: k,
59196
59361
  behavior: "deny",
59197
59362
  message: timeoutDenyMessage(timeoutMinutes)
59198
59363
  });
59364
+ stripTimedOutPermissionCards(v.card_text, v.cards);
59199
59365
  resumeReactionAfterVerdict();
59200
59366
  postPermissionResumeMessage({
59201
59367
  behavior: "deny",
@@ -59837,6 +60003,11 @@ function buildPermissionActionRow(requestId, showAlways) {
59837
60003
  kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
59838
60004
  return kb;
59839
60005
  }
60006
+ async function stripTimedOutPermissionCards(cardText, cards) {
60007
+ for (const edit of buildTimedOutCardEdits(cardText, cards)) {
60008
+ await swallowingApiCall(() => bot.api.editMessageText(edit.chatId, edit.messageId, richMessage2(edit.text), {}), { chat_id: edit.chatId, verb: "permission_timeout.strip" });
60009
+ }
60010
+ }
59840
60011
  function dispatchPermissionVerdict(ev) {
59841
60012
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
59842
60013
  const delivered = ipcServer.sendToAgent(selfAgent, ev);
@@ -60094,13 +60265,13 @@ var ipcServer = createIpcServer({
60094
60265
  return;
60095
60266
  }
60096
60267
  }
60097
- pendingPermissions.set(requestId, { tool_name: toolName, description, input_preview: inputPreview, startedAt: Date.now() });
60098
60268
  const text2 = formatPermissionCardBody({
60099
60269
  toolName,
60100
60270
  inputPreview,
60101
60271
  description,
60102
60272
  agentName: _client.agentName
60103
60273
  });
60274
+ pendingPermissions.set(requestId, { tool_name: toolName, description, input_preview: inputPreview, startedAt: Date.now(), card_text: text2, cards: [] });
60104
60275
  const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null;
60105
60276
  const keyboard = buildPermissionActionRow(requestId, showAlways);
60106
60277
  const activeTurn = currentTurn;
@@ -60109,7 +60280,12 @@ var ipcServer = createIpcServer({
60109
60280
  retryWithThreadFallback(robustApiCall, (tid) => bot.api.sendRichMessage(chatId, richMessage2(text2), {
60110
60281
  reply_markup: keyboard,
60111
60282
  ...tid != null ? { message_thread_id: tid } : {}
60112
- }), { threadId, chat_id: chatId, verb: "permission_request" }).catch((e) => {
60283
+ }), { threadId, chat_id: chatId, verb: "permission_request" }).then((sent) => {
60284
+ const pend = pendingPermissions.get(requestId);
60285
+ if (pend && sent && typeof sent.message_id === "number") {
60286
+ pend.cards.push({ chatId, messageId: sent.message_id });
60287
+ }
60288
+ }).catch((e) => {
60113
60289
  process.stderr.write(`telegram gateway: permission_request send to ${chatId} failed: ${e}
60114
60290
  `);
60115
60291
  });
@@ -60757,7 +60933,7 @@ async function executeReply(args) {
60757
60933
  const rawText = args.text;
60758
60934
  if (rawText == null || rawText === "")
60759
60935
  throw new Error("reply: text is required and cannot be empty");
60760
- let text2 = repairEscapedWhitespace(rawText);
60936
+ let text2 = normalizeParagraphBreaks(repairEscapedWhitespace(rawText));
60761
60937
  text2 = redactOutboundText(text2, "reply");
60762
60938
  {
60763
60939
  const scrub = scrubVoice(text2);
@@ -61102,6 +61278,24 @@ ${url}`;
61102
61278
  delete richOpts.link_preview_options;
61103
61279
  return lockedBot.api.sendRichMessage(chat_id, richMessage2(chunks[i]), richOpts);
61104
61280
  };
61281
+ const sendChunkResplit = async (opts) => {
61282
+ const subPieces = splitMarkdownChunks(chunks[i], RICH_MESSAGE_MAX_CHARS2);
61283
+ const pieces = subPieces.length > 1 ? subPieces : hardSliceToCap(chunks[i], RICH_MESSAGE_MAX_CHARS2);
61284
+ for (let p = 0;p < pieces.length; p++) {
61285
+ let sent;
61286
+ if (literalText) {
61287
+ sent = await lockedBot.api.sendMessage(chat_id, pieces[p], opts);
61288
+ } else {
61289
+ const ro = { ...opts };
61290
+ delete ro.link_preview_options;
61291
+ sent = await lockedBot.api.sendRichMessage(chat_id, richMessage2(pieces[p]), ro);
61292
+ }
61293
+ sentIds.push(sent.message_id);
61294
+ logOutbound("reply", chat_id, sent.message_id, pieces[p].length, `chunk=${i + 1}/${chunks.length} resplit=${p + 1}/${pieces.length}`);
61295
+ }
61296
+ process.stderr.write(`telegram gateway: rich body too long \u2014 re-split chunk ${i + 1}/${chunks.length} into ${pieces.length} piece(s)
61297
+ `);
61298
+ };
61105
61299
  try {
61106
61300
  const sent = await robustApiCall(() => sendChunk(sendOpts), { threadId, chat_id });
61107
61301
  sentIds.push(sent.message_id);
@@ -61115,11 +61309,15 @@ ${url}`;
61115
61309
  const sent = await sendChunk(retryOpts);
61116
61310
  sentIds.push(sent.message_id);
61117
61311
  } catch (retryErr) {
61118
- if (isHtmlParseRejectError(retryErr))
61312
+ if (isMessageTooLongError(retryErr))
61313
+ await sendChunkResplit(retryOpts);
61314
+ else if (isHtmlParseRejectError(retryErr))
61119
61315
  await sendChunkPlainText(retryOpts);
61120
61316
  else
61121
61317
  throw retryErr;
61122
61318
  }
61319
+ } else if (isMessageTooLongError(err)) {
61320
+ await sendChunkResplit(sendOpts);
61123
61321
  } else if (isHtmlParseRejectError(err)) {
61124
61322
  await sendChunkPlainText(sendOpts);
61125
61323
  } else {
@@ -61369,6 +61567,7 @@ async function executeStreamReply(args) {
61369
61567
  bot: lockedBot,
61370
61568
  retry: robustApiCall,
61371
61569
  repairEscapedWhitespace,
61570
+ normalizeParagraphBreaks,
61372
61571
  assertAllowedChat,
61373
61572
  resolveThreadId,
61374
61573
  disableLinkPreview: access.disableLinkPreview !== false,
@@ -62199,6 +62398,8 @@ async function executeEditMessage(args) {
62199
62398
  const editFormat = args.format ?? (editAccess.parseMode ?? "html");
62200
62399
  const editLiteralText = editFormat === "text";
62201
62400
  let editRawText = repairEscapedWhitespace(args.text);
62401
+ if (!editLiteralText)
62402
+ editRawText = normalizeParagraphBreaks(editRawText);
62202
62403
  editRawText = redactOutboundText(editRawText, "edit_message");
62203
62404
  {
62204
62405
  const scrub = scrubVoice(editRawText);
@@ -64353,7 +64554,7 @@ function switchroomExecCombined(args, timeoutMs = 15000) {
64353
64554
  shell: "/bin/bash"
64354
64555
  });
64355
64556
  }
64356
- function formatSwitchroomOutput(output, maxLen = 4000) {
64557
+ function formatSwitchroomOutput(output, maxLen = RICH_MESSAGE_MAX_CHARS2) {
64357
64558
  const trimmed = output.trim();
64358
64559
  if (trimmed.length <= maxLen)
64359
64560
  return trimmed;
@@ -68699,6 +68900,10 @@ ${editLabel}` : editLabel
68699
68900
  });
68700
68901
  return;
68701
68902
  }
68903
+ if (!pendingPermissions.has(request_id)) {
68904
+ await ctx.answerCallbackQuery({ text: STALE_TAP_NOTICE }).catch(() => {});
68905
+ return;
68906
+ }
68702
68907
  clearPermissionTimeoutSuppression("operator answered a permission card");
68703
68908
  const pd = pendingPermissions.get(request_id);
68704
68909
  const resumeAction = pd ? naturalAction(pd.tool_name, pd.input_preview) : "";
@@ -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: {