switchroom 0.16.23 → 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.
Files changed (138) hide show
  1. package/dist/agent-scheduler/index.js +80 -80
  2. package/dist/auth-broker/index.js +80 -80
  3. package/dist/cli/autoaccept-poll.js +8 -8
  4. package/dist/cli/drive-write-pretool.mjs +10 -10
  5. package/dist/cli/notion-write-pretool.mjs +82 -82
  6. package/dist/cli/self-improve-apply-guard-pretool.mjs +6 -0
  7. package/dist/cli/skill-validate-pretool.mjs +2936 -119
  8. package/dist/cli/switchroom.js +804 -465
  9. package/dist/host-control/main.js +169 -163
  10. package/dist/vault/approvals/kernel-server.js +82 -82
  11. package/dist/vault/broker/server.js +83 -83
  12. package/package.json +4 -4
  13. package/telegram-plugin/answer-stream.ts +20 -49
  14. package/telegram-plugin/auth-snapshot-format.ts +27 -30
  15. package/telegram-plugin/auto-fallback-fleet.ts +6 -11
  16. package/telegram-plugin/bridge/bridge.ts +1 -1
  17. package/telegram-plugin/card-format.ts +28 -25
  18. package/telegram-plugin/credits-watch.ts +5 -10
  19. package/telegram-plugin/dist/bridge/bridge.js +113 -113
  20. package/telegram-plugin/dist/gateway/gateway.js +2085 -2102
  21. package/telegram-plugin/dist/server.js +161 -161
  22. package/telegram-plugin/draft-stream.ts +4 -4
  23. package/telegram-plugin/format.ts +427 -680
  24. package/telegram-plugin/gateway/approval-callback.ts +2 -3
  25. package/telegram-plugin/gateway/approval-card.test.ts +17 -4
  26. package/telegram-plugin/gateway/approval-card.ts +16 -6
  27. package/telegram-plugin/gateway/approvals-commands.ts +18 -24
  28. package/telegram-plugin/gateway/auth-command.ts +74 -74
  29. package/telegram-plugin/gateway/auth-line.ts +5 -15
  30. package/telegram-plugin/gateway/boot-card.ts +20 -22
  31. package/telegram-plugin/gateway/boot-version.ts +3 -2
  32. package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
  33. package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
  34. package/telegram-plugin/gateway/config-snapshot.ts +9 -9
  35. package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
  36. package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
  37. package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
  38. package/telegram-plugin/gateway/drive-write-approval.test.ts +10 -10
  39. package/telegram-plugin/gateway/drive-write-approval.ts +14 -8
  40. package/telegram-plugin/gateway/effort-command.ts +17 -17
  41. package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
  42. package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
  43. package/telegram-plugin/gateway/gateway.ts +881 -633
  44. package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
  45. package/telegram-plugin/gateway/inject-handler.ts +5 -5
  46. package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
  47. package/telegram-plugin/gateway/ipc-server.ts +39 -6
  48. package/telegram-plugin/gateway/linear-activity.ts +16 -14
  49. package/telegram-plugin/gateway/linear-setup.ts +1 -1
  50. package/telegram-plugin/gateway/model-command.ts +25 -25
  51. package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
  52. package/telegram-plugin/gateway/permission-timeout.ts +76 -0
  53. package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
  54. package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
  55. package/telegram-plugin/issues-card.ts +6 -7
  56. package/telegram-plugin/model-unavailable.ts +8 -12
  57. package/telegram-plugin/operator-events-history.ts +1 -1
  58. package/telegram-plugin/operator-events.ts +24 -28
  59. package/telegram-plugin/package.json +1 -1
  60. package/telegram-plugin/pending-work-progress.ts +36 -36
  61. package/telegram-plugin/permission-title.ts +39 -20
  62. package/telegram-plugin/pty-partial-handler.ts +5 -13
  63. package/telegram-plugin/quota-check.ts +5 -5
  64. package/telegram-plugin/quota-watch.ts +13 -18
  65. package/telegram-plugin/recent-outbound-dedup.ts +5 -5
  66. package/telegram-plugin/registry/turns-schema.ts +43 -3
  67. package/telegram-plugin/retry-api-call.ts +42 -7
  68. package/telegram-plugin/rich-send.ts +86 -0
  69. package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
  70. package/telegram-plugin/secret-detect/vault-error.ts +29 -22
  71. package/telegram-plugin/shared/bot-runtime.ts +29 -7
  72. package/telegram-plugin/silence-poke.ts +26 -69
  73. package/telegram-plugin/silent-reply-anchor.ts +9 -2
  74. package/telegram-plugin/slot-banner-driver.ts +9 -6
  75. package/telegram-plugin/slot-banner.ts +5 -8
  76. package/telegram-plugin/status-no-truncate.ts +11 -5
  77. package/telegram-plugin/steering.ts +0 -4
  78. package/telegram-plugin/stream-controller.ts +59 -62
  79. package/telegram-plugin/stream-reply-handler.ts +49 -98
  80. package/telegram-plugin/subagent-watcher.ts +2 -2
  81. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
  82. package/telegram-plugin/tests/answer-stream.test.ts +54 -63
  83. package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
  84. package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
  85. package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
  86. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
  87. package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
  88. package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
  89. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  90. package/telegram-plugin/tests/bot-api.harness.ts +23 -1
  91. package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
  92. package/telegram-plugin/tests/card-format.test.ts +6 -4
  93. package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
  94. package/telegram-plugin/tests/credits-watch.test.ts +5 -5
  95. package/telegram-plugin/tests/fake-bot-api.ts +58 -4
  96. package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
  97. package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
  98. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
  99. package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +6 -2
  100. package/telegram-plugin/tests/issues-card.test.ts +15 -12
  101. package/telegram-plugin/tests/length-error-classify.test.ts +131 -0
  102. package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
  103. package/telegram-plugin/tests/model-command.test.ts +2 -2
  104. package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
  105. package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
  106. package/telegram-plugin/tests/operator-events.test.ts +7 -9
  107. package/telegram-plugin/tests/paragraph-normalizer.test.ts +273 -0
  108. package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
  109. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +12 -2
  110. package/telegram-plugin/tests/permission-timeout.test.ts +77 -0
  111. package/telegram-plugin/tests/permission-title.test.ts +88 -41
  112. package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
  113. package/telegram-plugin/tests/quota-check.test.ts +3 -3
  114. package/telegram-plugin/tests/quota-watch.test.ts +8 -4
  115. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
  116. package/telegram-plugin/tests/silence-poke.test.ts +75 -112
  117. package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
  118. package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
  119. package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
  120. package/telegram-plugin/tests/slot-banner.test.ts +9 -6
  121. package/telegram-plugin/tests/status-accent.test.ts +29 -32
  122. package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts → stream-controller-parse-fallback.test.ts} +40 -42
  123. package/telegram-plugin/tests/stream-controller.test.ts +63 -52
  124. package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
  125. package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
  126. package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
  127. package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
  128. package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
  129. package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
  130. package/telegram-plugin/tests/welcome-text.test.ts +72 -65
  131. package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
  132. package/telegram-plugin/text-voice-scrub.ts +8 -11
  133. package/telegram-plugin/tool-activity-summary.ts +29 -29
  134. package/telegram-plugin/welcome-text.ts +82 -83
  135. package/telegram-plugin/worker-activity-feed.ts +2 -3
  136. package/telegram-plugin/html-sanitize.ts +0 -244
  137. package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
  138. package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
@@ -177,7 +177,7 @@ import {
177
177
  STATUS_LINE_MAX,
178
178
  NESTED_PREFIX,
179
179
  } from './status-no-truncate.js'
180
- import { escapeHtml, stripMarkdown, truncate } from './card-format.js'
180
+ import { escapeMarkdown, stripMarkdown, truncate } from './card-format.js'
181
181
  import { isTelegramSurfaceTool } from './tool-names.js'
182
182
 
183
183
  /**
@@ -264,11 +264,11 @@ export function renderActivityHeader(
264
264
  ): [string, string] {
265
265
  const toolWord = toolCount === 1 ? 'tool' : 'tools'
266
266
  const elapsed = formatFeedElapsed(elapsedMs)
267
- const descPart = description.length > 0 ? ` · <i>${escapeHtml(description)}</i>` : ''
268
- const line1 = `${emoji} <b>${escapeHtml(label)}</b>${descPart}`
267
+ const descPart = description.length > 0 ? ` · _${escapeMarkdown(description)}_` : ''
268
+ const line1 = `${emoji} **${escapeMarkdown(label)}**${descPart}`
269
269
  const line2 = state === 'running'
270
- ? `<i>${elapsed} · ${toolCount} ${toolWord}</i>`
271
- : `<i>${state} · ${toolCount} ${toolWord} · ${elapsed}</i>`
270
+ ? `_${elapsed} · ${toolCount} ${toolWord}_`
271
+ : `_${state} · ${toolCount} ${toolWord} · ${elapsed}_`
272
272
  return [line1, line2]
273
273
  }
274
274
 
@@ -286,14 +286,14 @@ export function formatFeedElapsed(ms: number): string {
286
286
  // 1. stripMarkdown(raw)
287
287
  // 2. .replace(/\s+/g, ' ').trim()
288
288
  // 3. truncate(_, STATUS_LINE_MAX)
289
- // 4. escapeHtml(_) ← escape is ALWAYS the last per-line op.
289
+ // 4. escapeMarkdown(_) ← escape is ALWAYS the last per-line op.
290
290
  // Escaping last is load-bearing: clipping an already-escaped string can split
291
- // an HTML entity (&amp;&amp), which Telegram's HTML parser rejects.
291
+ // a markdown escape (\*\), which renders wrong.
292
292
 
293
- /** Clean + clip + escape a single raw step line. Returns ready-to-wrap HTML. */
293
+ /** Clean + clip + escape a single raw step line. Returns ready-to-wrap markdown. */
294
294
  function escapeStepLine(raw: string): string {
295
295
  const cleaned = stripMarkdown(raw).replace(/\s+/g, ' ').trim()
296
- return escapeHtml(truncate(cleaned, STATUS_LINE_MAX))
296
+ return escapeMarkdown(truncate(cleaned, STATUS_LINE_MAX))
297
297
  }
298
298
 
299
299
  /**
@@ -317,10 +317,10 @@ export function renderStepFeed(
317
317
  if (steps.length === 0) return
318
318
  const shown = steps.slice(-STATUS_ROLLING_LINES)
319
319
  const hidden = steps.length - shown.length
320
- if (hidden > 0) out.push(`<i>✓ +${hidden} earlier…</i>`)
320
+ if (hidden > 0) out.push(`_✓ +${hidden} earlier…_`)
321
321
  const lastIdx = shown.length - 1
322
322
  shown.forEach((s, i) => {
323
- out.push(!allDone && i === lastIdx ? `<b>→ ${s}${liveSuffix}</b>` : `<i>✓ ${s}</i>`)
323
+ out.push(!allDone && i === lastIdx ? `**→ ${s}${liveSuffix}**` : `_✓ ${s}_`)
324
324
  })
325
325
  }
326
326
 
@@ -399,18 +399,18 @@ export function renderStatusCard(opts: StatusCardOpts): string | null {
399
399
  // Parent lines all render done — the live → step lives in the nested block.
400
400
  const shownParent = steps.slice(-STATUS_ROLLING_LINES)
401
401
  const hiddenParent = steps.length - shownParent.length
402
- if (hiddenParent > 0) out.push(`<i>✓ +${hiddenParent} earlier…</i>`)
403
- for (const s of shownParent) out.push(`<i>✓ ${s}</i>`)
402
+ if (hiddenParent > 0) out.push(`_✓ +${hiddenParent} earlier…_`)
403
+ for (const s of shownParent) out.push(`_✓ ${s}_`)
404
404
  // Child block.
405
405
  const shownChild = children.slice(-STATUS_ROLLING_LINES)
406
406
  const hiddenChild = children.length - shownChild.length
407
- if (hiddenChild > 0) out.push(`${NESTED_PREFIX}<i>+${hiddenChild} earlier…</i>`)
407
+ if (hiddenChild > 0) out.push(`${NESTED_PREFIX}_+${hiddenChild} earlier…_`)
408
408
  const lastChildIdx = shownChild.length - 1
409
409
  shownChild.forEach((s, i) => {
410
410
  out.push(
411
411
  i === lastChildIdx && !final
412
- ? `${NESTED_PREFIX}<b>→ ${s}${liveSuffix}</b>`
413
- : `${NESTED_PREFIX}<i>${s}</i>`,
412
+ ? `${NESTED_PREFIX}**→ ${s}${liveSuffix}**`
413
+ : `${NESTED_PREFIX}_${s}_`,
414
414
  )
415
415
  })
416
416
  } else {
@@ -418,12 +418,12 @@ export function renderStatusCard(opts: StatusCardOpts): string | null {
418
418
  }
419
419
 
420
420
  if (final && stepCount != null && stepCount > 0) {
421
- out.push(`<i>✓ ${stepCount} steps</i>`)
421
+ out.push(`_✓ ${stepCount} steps_`)
422
422
  }
423
423
 
424
424
  if (result != null && result.text.length > 0) {
425
425
  out.push(WORKER_RESULT_RULE)
426
- out.push(`${result.emoji} <i>${escapeHtml(truncate(result.text, WORKER_RESULT_MAX))}</i>`)
426
+ out.push(`${result.emoji} _${escapeMarkdown(truncate(result.text, WORKER_RESULT_MAX))}_`)
427
427
  }
428
428
 
429
429
  // out always carries the two header lines, so it is never empty — but guard
@@ -444,7 +444,7 @@ const WORKER_RESULT_MAX = 320
444
444
  * drops the oldest body bullets one at a time, re-inserting a `+N earlier…`
445
445
  * marker, until the card fits STATUS_CARD_CHAR_BUDGET. In the extreme case
446
446
  * (a single newest bullet that is itself oversized) it truncates the RAW
447
- * newest text, THEN escapes, THEN wraps — never slicing already-escaped HTML.
447
+ * newest text, THEN escapes, THEN wraps — never slicing already-escaped markdown.
448
448
  */
449
449
  function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
450
450
  const { final = false, liveSuffix = '', stepCount, result } = opts
@@ -454,10 +454,10 @@ function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
454
454
 
455
455
  // Fixed footer/result lines (always kept).
456
456
  const footerLines: string[] = []
457
- if (final && stepCount != null && stepCount > 0) footerLines.push(`<i>✓ ${stepCount} steps</i>`)
457
+ if (final && stepCount != null && stepCount > 0) footerLines.push(`_✓ ${stepCount} steps_`)
458
458
  if (result != null && result.text.length > 0) {
459
459
  footerLines.push(WORKER_RESULT_RULE)
460
- footerLines.push(`${result.emoji} <i>${escapeHtml(truncate(result.text, WORKER_RESULT_MAX))}</i>`)
460
+ footerLines.push(`${result.emoji} _${escapeMarkdown(truncate(result.text, WORKER_RESULT_MAX))}_`)
461
461
  }
462
462
  const fixedCost = [...headerLines, ...footerLines].join('\n').length
463
463
 
@@ -468,17 +468,17 @@ function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
468
468
  const prefix = hasChildren ? NESTED_PREFIX : ''
469
469
 
470
470
  const buildBullet = (esc: string, isLast: boolean): string =>
471
- !final && isLast ? `${prefix}<b>→ ${esc}${liveSuffix}</b>` : `${prefix}<i>${esc}</i>`
471
+ !final && isLast ? `${prefix}**→ ${esc}${liveSuffix}**` : `${prefix}_${esc}_`
472
472
 
473
473
  // Parent-collapsed marker line when we are dropping children but parent steps exist.
474
474
  const parentMarker =
475
- hasChildren && rawSteps.length > 0 ? `<i>✓ +${rawSteps.length} earlier…</i>` : null
475
+ hasChildren && rawSteps.length > 0 ? `_✓ +${rawSteps.length} earlier…_` : null
476
476
 
477
477
  for (let drop = 1; drop < escapedBody.length; drop++) {
478
478
  const shown = escapedBody.slice(drop)
479
479
  const lines: string[] = [...headerLines]
480
480
  if (parentMarker != null) lines.push(parentMarker)
481
- lines.push(hasChildren ? `${NESTED_PREFIX}<i>+${drop} earlier…</i>` : `<i>✓ +${drop} earlier…</i>`)
481
+ lines.push(hasChildren ? `${NESTED_PREFIX}_+${drop} earlier…_` : `_✓ +${drop} earlier…_`)
482
482
  const lastIdx = shown.length - 1
483
483
  shown.forEach((esc, i) => lines.push(buildBullet(esc, i === lastIdx)))
484
484
  lines.push(...footerLines)
@@ -491,19 +491,19 @@ function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
491
491
  // escaping can expand the string (& → &amp;).
492
492
  const rawNewest = body.length > 0 ? stripMarkdown(body[body.length - 1]).replace(/\s+/g, ' ').trim() : ''
493
493
  const wrapperOverhead = final
494
- ? (prefix + '<i>✓ </i>').length
495
- : (prefix + '<b>→ </b>').length + liveSuffix.length
494
+ ? (prefix + '_✓ _').length
495
+ : (prefix + '**→ **').length + liveSuffix.length
496
496
  const headerFooterCost =
497
497
  fixedCost + (fixedCost > 0 ? 1 : 0) + (parentMarker != null ? parentMarker.length + 1 : 0)
498
498
  const budget = STATUS_CARD_CHAR_BUDGET - headerFooterCost - wrapperOverhead
499
499
  let raw = rawNewest.slice(0, Math.max(0, budget))
500
- let newest = escapeHtml(raw)
500
+ let newest = escapeMarkdown(raw)
501
501
  while (raw.length > 0 && wrapperOverhead + headerFooterCost + newest.length > STATUS_CARD_CHAR_BUDGET) {
502
502
  const excess = wrapperOverhead + headerFooterCost + newest.length - STATUS_CARD_CHAR_BUDGET
503
503
  raw = raw.slice(0, Math.max(0, raw.length - excess - 1))
504
- newest = escapeHtml(raw)
504
+ newest = escapeMarkdown(raw)
505
505
  }
506
- const newestLine = final ? `${prefix}<i>✓ ${newest}</i>` : `${prefix}<b>→ ${newest}${liveSuffix}</b>`
506
+ const newestLine = final ? `${prefix}_✓ ${newest}_` : `${prefix}**→ ${newest}${liveSuffix}**`
507
507
  const lines: string[] = [...headerLines]
508
508
  if (parentMarker != null) lines.push(parentMarker)
509
509
  lines.push(newestLine)
@@ -5,13 +5,15 @@
5
5
  * 1. single-sourced (no drift between gateway mode and monolith mode)
6
6
  * 2. unit-testable without needing a grammy Context
7
7
  *
8
- * All functions return HTML-safe strings with <b>/<i>/<code> markup ready
9
- * for Telegram's parse_mode=HTML. The <code>…</code> wrapper is deliberate
10
- * for things like command names and agent identifiers, which render as
11
- * monospace inline and avoid Telegram treating them as markdown.
8
+ * All functions return GFM markdown strings (bold, italic, inline code)
9
+ * ready for the rich-message path (#2669). The code-span wrapper is
10
+ * deliberate for command names and agent identifiers, which render as
11
+ * monospace inline (and inside a code span, characters are literal so no
12
+ * escaping is needed).
12
13
  */
13
14
 
14
15
  import { maskUsername } from "./demo-mask.js";
16
+ import { escapeMarkdown } from "./card-format.js";
15
17
 
16
18
  export type AuthSummary = {
17
19
  authenticated: boolean;
@@ -93,14 +95,11 @@ export type AgentMetadata = {
93
95
  live?: StatusProbeRow[];
94
96
  };
95
97
 
96
- // Tiny escaper duplicates the one in gateway.ts / server.ts so this
97
- // module stays dependency-free and easy to test.
98
+ // Markdown escaper for dynamic values interpolated into bold/plain card
99
+ // text (#2669). Kept under the legacy `escapeHtml` name so existing imports
100
+ // don't churn; it now escapes GFM-markdown specials.
98
101
  export function escapeHtml(text: string): string {
99
- return text
100
- .replace(/&/g, "&amp;")
101
- .replace(/</g, "&lt;")
102
- .replace(/>/g, "&gt;")
103
- .replace(/"/g, "&quot;");
102
+ return escapeMarkdown(text);
104
103
  }
105
104
 
106
105
  /**
@@ -137,9 +136,9 @@ export function formatAgentLine(meta: AgentMetadata): string {
137
136
  // restart, when the session reverts to the configured model).
138
137
  const session =
139
138
  meta.sessionModel && meta.sessionModel.length > 0
140
- ? ` · live session: <code>${escapeHtml(meta.sessionModel)}</code>`
139
+ ? ` · live session: \`${meta.sessionModel}\``
141
140
  : "";
142
- return `<b>${escapeHtml(meta.agentName)}</b> · model: <code>${escapeHtml(m)}</code>${session}${topic}`;
141
+ return `**${escapeHtml(meta.agentName)}** · model: \`${m}\`${session}${topic}`;
143
142
  }
144
143
 
145
144
  /**
@@ -150,15 +149,15 @@ export function formatAgentLine(meta: AgentMetadata): string {
150
149
  export function startText(agentName: string, dmDisabled: boolean): string {
151
150
  if (dmDisabled) return "This bot isn't accepting new connections.";
152
151
  return [
153
- `<b>Switchroom</b> — Telegram on your Claude Pro or Max subscription.`,
152
+ `**Switchroom** — Telegram on your Claude Pro or Max subscription.`,
154
153
  ``,
155
- `This bot is the <b>${escapeHtml(agentName)}</b> agent. Pair first, then send messages here and they reach the agent; replies and reactions come back.`,
154
+ `This bot is the **${escapeHtml(agentName)}** agent. Pair first, then send messages here and they reach the agent; replies and reactions come back.`,
156
155
  ``,
157
- `<b>To pair:</b>`,
156
+ `**To pair:**`,
158
157
  `1. DM me anything — you'll get a 6-char code`,
159
- `2. In Claude Code: <code>/telegram:access pair &lt;code&gt;</code>`,
158
+ `2. In Claude Code: \`/telegram:access pair <code>\``,
160
159
  ``,
161
- `After pairing, try <code>/status</code> or <code>/commands</code>.`,
160
+ `After pairing, try \`/status\` or \`/commands\`.`,
162
161
  ].join("\n");
163
162
  }
164
163
 
@@ -168,16 +167,16 @@ export function startText(agentName: string, dmDisabled: boolean): string {
168
167
  */
169
168
  export function helpText(agentName: string): string {
170
169
  return [
171
- `<b>Switchroom</b> — your Pro/Max subscription, wired to Telegram.`,
170
+ `**Switchroom** — your Pro/Max subscription, wired to Telegram.`,
172
171
  ``,
173
- `This bot is the <b>${escapeHtml(agentName)}</b> agent. Text and photos route through to it; replies, reactions and progress cards come back.`,
172
+ `This bot is the **${escapeHtml(agentName)}** agent. Text and photos route through to it; replies, reactions and progress cards come back.`,
174
173
  ``,
175
- `Tool approvals surface as inline buttons (✅ / ❌) or via <code>/approve</code>, <code>/deny</code>, <code>/pending</code>. Start a fresh session with <code>/new</code>, or trim/clear context with <code>/compact</code> / <code>/clear</code>.`,
174
+ `Tool approvals surface as inline buttons (✅ / ❌) or via \`/approve\`, \`/deny\`, \`/pending\`. Start a fresh session with \`/new\`, or trim/clear context with \`/compact\` / \`/clear\`.`,
176
175
  ``,
177
- `<code>/start</code> — pairing instructions`,
178
- `<code>/status</code> — agent, model, auth`,
179
- `<code>/vault audit &lt;agent&gt;</code> — admin: review agent's vault access + one-tap [🔓 Allow] on recent denials`,
180
- `<code>/commands</code> — full command list`,
176
+ `\`/start\` — pairing instructions`,
177
+ `\`/status\` — agent, model, auth`,
178
+ `\`/vault audit <agent>\` — admin: review agent's vault access + one-tap [🔓 Allow] on recent denials`,
179
+ `\`/commands\` — full command list`,
181
180
  ].join("\n");
182
181
  }
183
182
 
@@ -217,7 +216,7 @@ export function statusPairedText(params: {
217
216
  `Agent: ${formatAgentLine(meta)}`,
218
217
  `Auth: ${formatAuthLine(meta.auth)}`,
219
218
  ];
220
- if (meta.status) lines.push(`Status: <code>${escapeHtml(meta.status)}</code>${meta.uptime ? ` · up ${escapeHtml(meta.uptime)}` : ""}`);
219
+ if (meta.status) lines.push(`Status: \`${meta.status}\`${meta.uptime ? ` · up ${escapeHtml(meta.uptime)}` : ""}`);
221
220
 
222
221
  // Live health block — every probe (green and otherwise) so the user
223
222
  // can see at a glance what's working AND what isn't. This is the
@@ -225,10 +224,10 @@ export function statusPairedText(params: {
225
224
  // contract: the boot card is a quiet ack, /status is the dashboard.
226
225
  if (meta.live && meta.live.length > 0) {
227
226
  lines.push("");
228
- lines.push("<b>Health</b>");
227
+ lines.push("**Health**");
229
228
  for (const row of meta.live) {
230
229
  const dot = STATUS_DOT[row.status] ?? STATUS_DOT.fail;
231
- lines.push(`${dot} <b>${escapeHtml(row.label)}</b> ${escapeHtml(row.detail)}`);
230
+ lines.push(`${dot} **${escapeHtml(row.label)}** ${escapeHtml(row.detail)}`);
232
231
  }
233
232
  }
234
233
 
@@ -237,14 +236,14 @@ export function statusPairedText(params: {
237
236
  // Blank separator before the audit block so the reply reads as two
238
237
  // sections: live state up top, config audit below.
239
238
  lines.push("");
240
- if (audit.version) lines.push(`<b>Version</b> ${escapeHtml(audit.version)}`);
241
- if (meta.extendsProfile) lines.push(`<b>Profile</b> ${escapeHtml(meta.extendsProfile)}`);
242
- if (audit.tools) lines.push(`<b>Tools</b> ${escapeHtml(audit.tools)}`);
243
- if (audit.toolsDeny) lines.push(`<b>Deny</b> ${escapeHtml(audit.toolsDeny)}`);
244
- if (audit.skills) lines.push(`<b>Skills</b> ${escapeHtml(audit.skills)}`);
245
- if (audit.limits) lines.push(`<b>Limits</b> ${escapeHtml(audit.limits)}`);
246
- if (audit.channel) lines.push(`<b>Channel</b> ${escapeHtml(audit.channel)}`);
247
- if (audit.memoryBank) lines.push(`<b>Memory</b> ${escapeHtml(audit.memoryBank)}`);
239
+ if (audit.version) lines.push(`**Version** ${escapeHtml(audit.version)}`);
240
+ if (meta.extendsProfile) lines.push(`**Profile** ${escapeHtml(meta.extendsProfile)}`);
241
+ if (audit.tools) lines.push(`**Tools** ${escapeHtml(audit.tools)}`);
242
+ if (audit.toolsDeny) lines.push(`**Deny** ${escapeHtml(audit.toolsDeny)}`);
243
+ if (audit.skills) lines.push(`**Skills** ${escapeHtml(audit.skills)}`);
244
+ if (audit.limits) lines.push(`**Limits** ${escapeHtml(audit.limits)}`);
245
+ if (audit.channel) lines.push(`**Channel** ${escapeHtml(audit.channel)}`);
246
+ if (audit.memoryBank) lines.push(`**Memory** ${escapeHtml(audit.memoryBank)}`);
248
247
  }
249
248
 
250
249
  return lines.join("\n");
@@ -254,7 +253,7 @@ export function statusPairedText(params: {
254
253
  * `/status` when the sender isn't paired yet but has a pending code.
255
254
  */
256
255
  export function statusPendingText(code: string): string {
257
- return `Pending pairing — run in Claude Code:\n\n<code>/telegram:access pair ${escapeHtml(code)}</code>`;
256
+ return `Pending pairing — run in Claude Code:\n\n\`/telegram:access pair ${code}\``;
258
257
  }
259
258
 
260
259
  /**
@@ -367,55 +366,55 @@ export const TELEGRAM_SWITCHROOM_COMMANDS = TELEGRAM_MENU_COMMANDS.slice(3);
367
366
 
368
367
  export function switchroomHelpText(agentName: string): string {
369
368
  return [
370
- `<b>Switchroom bot</b> — commands for the <b>${escapeHtml(agentName)}</b> agent.`,
369
+ `**Switchroom bot** — commands for the **${escapeHtml(agentName)}** agent.`,
371
370
  ``,
372
- `<b>Session &amp; approvals</b>`,
373
- `<code>/new</code> — fresh session (flush handoff, restart)`,
374
- `<code>/compact</code> — compact context (summarize, keep the thread)`,
375
- `<code>/clear</code> — clear context (fresh slate; memory in Hindsight)`,
376
- `<code>/approve [id]</code> — approve pending tool permission`,
377
- `<code>/deny [id]</code> — deny pending tool permission`,
378
- `<code>/pending</code> — list pending permission prompts`,
379
- `<code>/interrupt [name]</code> — interrupt an agent turn`,
371
+ `**Session & approvals**`,
372
+ `\`/new\` — fresh session (flush handoff, restart)`,
373
+ `\`/compact\` — compact context (summarize, keep the thread)`,
374
+ `\`/clear\` — clear context (fresh slate; memory in Hindsight)`,
375
+ `\`/approve [id]\` — approve pending tool permission`,
376
+ `\`/deny [id]\` — deny pending tool permission`,
377
+ `\`/pending\` — list pending permission prompts`,
378
+ `\`/interrupt [name]\` — interrupt an agent turn`,
380
379
  ``,
381
- `<b>Agents</b>`,
382
- `<code>/agents</code> — list all agents`,
383
- `<code>/agentstart [name]</code> — start an agent`,
384
- `<code>/stop [name]</code> — stop an agent`,
385
- `<code>/logs [name] [lines]</code> — show agent logs`,
386
- `<code>/memory &lt;query&gt;</code> — search agent memory`,
380
+ `**Agents**`,
381
+ `\`/agents\` — list all agents`,
382
+ `\`/agentstart [name]\` — start an agent`,
383
+ `\`/stop [name]\` — stop an agent`,
384
+ `\`/logs [name] [lines]\` — show agent logs`,
385
+ `\`/memory <query>\` — search agent memory`,
387
386
  ``,
388
- `<b>Fleet management</b>`,
389
- `<code>/upgradestatus</code> — read-only: CLI version, image age, container age per service`,
390
- `<code>/update</code> — dry-run plan; <code>/update apply</code> — actually pull images, reconcile, restart`,
391
- `<code>/restart [name|all]</code> — bounce agent (drains in-flight turn by default)`,
392
- `<code>/version</code> — show versions + running agent health summary`,
393
- `<code>/whoami</code> — this agent's sandbox: tools, MCP, vault key-names, powers`,
387
+ `**Fleet management**`,
388
+ `\`/upgradestatus\` — read-only: CLI version, image age, container age per service`,
389
+ `\`/update\` — dry-run plan; \`/update apply\` — actually pull images, reconcile, restart`,
390
+ `\`/restart [name|all]\` — bounce agent (drains in-flight turn by default)`,
391
+ `\`/version\` — show versions + running agent health summary`,
392
+ `\`/whoami\` — this agent's sandbox: tools, MCP, vault key-names, powers`,
394
393
  ``,
395
- `<b>Auth &amp; config</b>`,
396
- `<code>/auth</code> — auth status or actions`,
397
- `<code>/auth add [agent]</code> — add a new account slot (fallback pool)`,
398
- `<code>/auth list [agent]</code> — list account slots and health`,
399
- `<code>/auth use [agent] &lt;slot&gt;</code> — switch active slot and restart`,
400
- `<code>/auth rm [agent] &lt;slot&gt; [--force]</code> — remove a slot`,
401
- `<code>/model</code> — show the configured Claude model`,
402
- `<code>/model &lt;name&gt;</code> — switch the live session's model (opus · sonnet · haiku or a full id; until restart)`,
403
- `<code>/effort</code> — show or switch reasoning effort (low · medium · high · xhigh · max; until restart)`,
404
- `<code>/topics</code> — topic-to-agent mappings`,
405
- `<code>/permissions [agent]</code> — show agent permissions`,
406
- `<code>/grant &lt;tool&gt;</code> — grant a tool permission`,
407
- `<code>/dangerous [off]</code> — toggle full tool access`,
408
- `<code>/vault list|get|set|delete</code> — manage encrypted secrets`,
409
- `<code>/vault status</code> — show broker state (locked/unlocked, uptime, key count)`,
410
- `<code>/vault unlock</code> — unlock the broker (prompts for passphrase via Telegram)`,
411
- `<code>/vault lock</code> — lock the broker`,
412
- `<code>/vault grants [agent]</code> — list active capability grants (tap to revoke)`,
413
- `<code>/doctor</code> — health check (deps, services, MCP)`,
414
- `<code>/usage</code> — Pro/Max plan quota (5h + 7d windows)`,
415
- `<code>/inject &lt;slash&gt;</code> — inject a Claude Code REPL slash command (e.g. <code>/inject /cost</code>; allowlisted)`,
416
- `<code>/commands</code> — this help`,
394
+ `**Auth & config**`,
395
+ `\`/auth\` — auth status or actions`,
396
+ `\`/auth add [agent]\` — add a new account slot (fallback pool)`,
397
+ `\`/auth list [agent]\` — list account slots and health`,
398
+ `\`/auth use [agent] <slot>\` — switch active slot and restart`,
399
+ `\`/auth rm [agent] <slot> [--force]\` — remove a slot`,
400
+ `\`/model\` — show the configured Claude model`,
401
+ `\`/model <name>\` — switch the live session's model (opus · sonnet · haiku or a full id; until restart)`,
402
+ `\`/effort\` — show or switch reasoning effort (low · medium · high · xhigh · max; until restart)`,
403
+ `\`/topics\` — topic-to-agent mappings`,
404
+ `\`/permissions [agent]\` — show agent permissions`,
405
+ `\`/grant <tool>\` — grant a tool permission`,
406
+ `\`/dangerous [off]\` — toggle full tool access`,
407
+ `\`/vault list|get|set|delete\` — manage encrypted secrets`,
408
+ `\`/vault status\` — show broker state (locked/unlocked, uptime, key count)`,
409
+ `\`/vault unlock\` — unlock the broker (prompts for passphrase via Telegram)`,
410
+ `\`/vault lock\` — lock the broker`,
411
+ `\`/vault grants [agent]\` — list active capability grants (tap to revoke)`,
412
+ `\`/doctor\` — health check (deps, services, MCP)`,
413
+ `\`/usage\` — Pro/Max plan quota (5h + 7d windows)`,
414
+ `\`/inject <slash>\` — inject a Claude Code REPL slash command (e.g. \`/inject /cost\`; allowlisted)`,
415
+ `\`/commands\` — this help`,
417
416
  ``,
418
- `<i>Tip: <code>/update</code> shows the plan; <code>/update apply</code> executes it; <code>/restart</code> bounces a stuck agent; <code>/version</code> checks what's running.</i>`,
417
+ `_Tip: \`/update\` shows the plan; \`/update apply\` executes it; \`/restart\` bounces a stuck agent; \`/version\` checks what's running._`,
419
418
  ].join("\n");
420
419
  }
421
420
 
@@ -424,10 +423,10 @@ export function switchroomHelpText(agentName: string): string {
424
423
  * Centralized so gateway and monolith agree on wording.
425
424
  */
426
425
  export function restartAckText(agentName: string): string {
427
- return `🔄 Restarting <b>${escapeHtml(agentName)}</b>…`;
426
+ return `🔄 Restarting **${escapeHtml(agentName)}**…`;
428
427
  }
429
428
 
430
429
  export function newSessionAckText(agentName: string, flushedHandoff: boolean): string {
431
430
  const tail = flushedHandoff ? " · flushed handoff" : "";
432
- return `🆕 Started fresh session for <b>${escapeHtml(agentName)}</b>${tail} · restarting…`;
431
+ return `🆕 Started fresh session for **${escapeHtml(agentName)}**${tail} · restarting…`;
433
432
  }
@@ -155,11 +155,11 @@ export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): st
155
155
  })
156
156
  if (card == null) {
157
157
  // Unreachable (header always present) — defensive.
158
- return `🛠 <b>Worker</b> · <i>starting…</i>`
158
+ return `🛠 **Worker** · _starting…_`
159
159
  }
160
160
  if (!finished && steps.length === 0) {
161
161
  // Header-only running render → append the starting placeholder.
162
- return `${card}\n<i>starting…</i>`
162
+ return `${card}\n_starting…_`
163
163
  }
164
164
  return card
165
165
  }
@@ -285,7 +285,6 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
285
285
 
286
286
  function sendOptsFor(h: WorkerHandle): Record<string, unknown> {
287
287
  return {
288
- parse_mode: 'HTML',
289
288
  disable_web_page_preview: true,
290
289
  // Sub-agent progress card is a status surface, never the user's
291
290
  // answer — silence the open ping. (editMessageText ignores