switchroom 0.19.15 → 0.19.16

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 (27) hide show
  1. package/dist/cli/switchroom.js +1 -1
  2. package/dist/host-control/main.js +1 -1
  3. package/package.json +1 -1
  4. package/telegram-plugin/dist/bridge/bridge.js +30 -1
  5. package/telegram-plugin/dist/gateway/gateway.js +693 -433
  6. package/telegram-plugin/dist/server.js +30 -1
  7. package/telegram-plugin/gateway/background-shell-liveness.ts +65 -0
  8. package/telegram-plugin/gateway/gateway.ts +7 -58
  9. package/telegram-plugin/gateway/outbound-send-path.ts +25 -23
  10. package/telegram-plugin/gateway/outbox-listen-markup.ts +67 -0
  11. package/telegram-plugin/gateway/outbox-sweep.ts +92 -18
  12. package/telegram-plugin/gateway/rich-message-handler.ts +10 -4
  13. package/telegram-plugin/gateway/silence-poke-session-event.ts +89 -0
  14. package/telegram-plugin/session-tail.ts +88 -1
  15. package/telegram-plugin/silence-poke.ts +118 -1
  16. package/telegram-plugin/tests/background-shell-liveness.test.ts +72 -0
  17. package/telegram-plugin/tests/feed-survival.test.ts +7 -1
  18. package/telegram-plugin/tests/fixtures/bg-shell-liveness-3519.jsonl +3 -0
  19. package/telegram-plugin/tests/forwarded-rich-message-coalesce.test.ts +290 -0
  20. package/telegram-plugin/tests/outbox-sweep-listen-button.test.ts +253 -0
  21. package/telegram-plugin/tests/session-tail.test.ts +91 -1
  22. package/telegram-plugin/tests/silence-poke.test.ts +280 -0
  23. package/telegram-plugin/tests/tts-normalize.test.ts +66 -0
  24. package/telegram-plugin/tests/voice-normalize-text.test.ts +82 -1
  25. package/telegram-plugin/tts-normalize.ts +12 -0
  26. package/telegram-plugin/voice-normalize-text.ts +100 -0
  27. package/telegram-plugin/voice-ondemand.ts +71 -0
@@ -8869,12 +8869,12 @@ var require_mod4 = __commonJS((exports) => {
8869
8869
  // flood-circuit-breaker.ts
8870
8870
  import {
8871
8871
  existsSync as existsSync2,
8872
- readFileSync as readFileSync2,
8873
- writeFileSync as writeFileSync3,
8874
- mkdirSync as mkdirSync4,
8872
+ readFileSync as readFileSync3,
8873
+ writeFileSync as writeFileSync4,
8874
+ mkdirSync as mkdirSync5,
8875
8875
  chmodSync,
8876
8876
  unlinkSync as unlinkSync2,
8877
- renameSync as renameSync4
8877
+ renameSync as renameSync5
8878
8878
  } from "node:fs";
8879
8879
  function floodWaitRemainingMs(state, now) {
8880
8880
  if (!state)
@@ -8884,7 +8884,7 @@ function floodWaitRemainingMs(state, now) {
8884
8884
  function readFloodStateResult(path) {
8885
8885
  let text;
8886
8886
  try {
8887
- text = readFileSync2(path, "utf-8");
8887
+ text = readFileSync3(path, "utf-8");
8888
8888
  } catch (err) {
8889
8889
  const code = err?.code;
8890
8890
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -13554,7 +13554,7 @@ __export(exports_tmux, {
13554
13554
  captureAgentPane: () => captureAgentPane
13555
13555
  });
13556
13556
  import { execFileSync as execFileSync2 } from "node:child_process";
13557
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
13557
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync6, readdirSync as readdirSync2, statSync as statSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
13558
13558
  import { resolve } from "node:path";
13559
13559
  function captureAgentPane(opts) {
13560
13560
  const { agentName, agentDir, reason } = opts;
@@ -13566,7 +13566,7 @@ function captureAgentPane(opts) {
13566
13566
  const reasonSlug = sanitizeReason(reason);
13567
13567
  const outPath = resolve(outDir, `${ts}-${reasonSlug}.txt`);
13568
13568
  try {
13569
- mkdirSync5(outDir, { recursive: true, mode: 448 });
13569
+ mkdirSync6(outDir, { recursive: true, mode: 448 });
13570
13570
  } catch (err) {
13571
13571
  const msg = `mkdir crash-reports failed: ${err.message}`;
13572
13572
  console.error(`[tmux-capture] ${agentName}: ${msg}`);
@@ -13605,7 +13605,7 @@ function captureAgentPane(opts) {
13605
13605
  ` + `
13606
13606
  `;
13607
13607
  try {
13608
- writeFileSync4(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
13608
+ writeFileSync5(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
13609
13609
  mode: 384
13610
13610
  });
13611
13611
  } catch (err) {
@@ -39416,13 +39416,218 @@ class VoiceOnDemandCache {
39416
39416
  }
39417
39417
  }
39418
39418
 
39419
+ // voice-ondemand.ts
39420
+ import { randomBytes as randomBytes2 } from "crypto";
39421
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync3, mkdirSync as mkdirSync3 } from "fs";
39422
+ import { dirname as dirname3 } from "path";
39423
+ var VOICE_ONDEMAND_CALLBACK_PREFIX = "voice:";
39424
+ var VOICE_ONDEMAND_TTL_MS2 = 7 * 24 * 60 * 60 * 1000;
39425
+ var VOICE_ONDEMAND_MAX_ENTRIES2 = 500;
39426
+
39427
+ class VoiceOnDemandCache2 {
39428
+ store = new Map;
39429
+ ttlMs;
39430
+ maxEntries;
39431
+ now;
39432
+ persistPath;
39433
+ constructor(options = {}) {
39434
+ this.ttlMs = options.ttlMs ?? VOICE_ONDEMAND_TTL_MS2;
39435
+ this.maxEntries = options.maxEntries ?? VOICE_ONDEMAND_MAX_ENTRIES2;
39436
+ this.now = options.now ?? Date.now;
39437
+ this.persistPath = options.persistPath;
39438
+ if (this.persistPath !== undefined)
39439
+ this.load();
39440
+ }
39441
+ put(token, payload) {
39442
+ this.store.delete(token);
39443
+ this.store.set(token, {
39444
+ createdAt: this.now(),
39445
+ ...payload,
39446
+ expiresAt: this.now() + this.ttlMs
39447
+ });
39448
+ while (this.store.size > this.maxEntries) {
39449
+ const oldest = this.store.keys().next().value;
39450
+ if (oldest === undefined)
39451
+ break;
39452
+ this.store.delete(oldest);
39453
+ }
39454
+ this.flush();
39455
+ }
39456
+ get(token) {
39457
+ const entry = this.store.get(token);
39458
+ if (entry == null)
39459
+ return null;
39460
+ if (entry.expiresAt <= this.now()) {
39461
+ this.store.delete(token);
39462
+ this.flush();
39463
+ return null;
39464
+ }
39465
+ const { text, voice, speed, filePath, telegramFileId } = entry;
39466
+ return {
39467
+ text,
39468
+ speed,
39469
+ ...voice !== undefined ? { voice } : {},
39470
+ ...filePath !== undefined ? { filePath } : {},
39471
+ ...telegramFileId !== undefined ? { telegramFileId } : {}
39472
+ };
39473
+ }
39474
+ setFilePath(token, filePath) {
39475
+ const entry = this.store.get(token);
39476
+ if (entry == null || entry.expiresAt <= this.now())
39477
+ return;
39478
+ entry.filePath = filePath;
39479
+ this.flush();
39480
+ }
39481
+ setTelegramFileId(token, fileId) {
39482
+ if (fileId.length === 0)
39483
+ return;
39484
+ const entry = this.store.get(token);
39485
+ if (entry == null || entry.expiresAt <= this.now())
39486
+ return;
39487
+ entry.telegramFileId = fileId;
39488
+ this.flush();
39489
+ }
39490
+ prune(tokens) {
39491
+ let changed = false;
39492
+ for (const token of tokens) {
39493
+ if (this.store.delete(token))
39494
+ changed = true;
39495
+ }
39496
+ if (changed)
39497
+ this.flush();
39498
+ }
39499
+ get size() {
39500
+ return this.store.size;
39501
+ }
39502
+ load() {
39503
+ if (this.persistPath === undefined)
39504
+ return;
39505
+ let raw;
39506
+ try {
39507
+ raw = readFileSync2(this.persistPath, "utf8");
39508
+ } catch {
39509
+ return;
39510
+ }
39511
+ try {
39512
+ const parsed = JSON.parse(raw);
39513
+ if (parsed == null || parsed.version !== 1 || typeof parsed.entries !== "object")
39514
+ return;
39515
+ const nowMs = this.now();
39516
+ for (const [token, entry] of Object.entries(parsed.entries)) {
39517
+ if (entry == null || typeof entry.expiresAt !== "number" || typeof entry.text !== "string" || typeof entry.speed !== "number") {
39518
+ continue;
39519
+ }
39520
+ if (entry.expiresAt <= nowMs)
39521
+ continue;
39522
+ this.store.set(token, entry);
39523
+ }
39524
+ while (this.store.size > this.maxEntries) {
39525
+ const oldest = this.store.keys().next().value;
39526
+ if (oldest === undefined)
39527
+ break;
39528
+ this.store.delete(oldest);
39529
+ }
39530
+ } catch (err) {
39531
+ process.stderr.write(`voice-ondemand: failed to parse persisted cache ${this.persistPath}: ${String(err)}
39532
+ `);
39533
+ }
39534
+ }
39535
+ flush() {
39536
+ if (this.persistPath === undefined)
39537
+ return;
39538
+ const doc = {
39539
+ version: 1,
39540
+ entries: Object.fromEntries(this.store)
39541
+ };
39542
+ const tmp = `${this.persistPath}.tmp`;
39543
+ try {
39544
+ mkdirSync3(dirname3(this.persistPath), { recursive: true });
39545
+ writeFileSync2(tmp, JSON.stringify(doc), "utf8");
39546
+ renameSync3(tmp, this.persistPath);
39547
+ } catch (err) {
39548
+ process.stderr.write(`voice-ondemand: failed to persist cache ${this.persistPath}: ${String(err)}
39549
+ `);
39550
+ }
39551
+ }
39552
+ }
39553
+ function mintVoiceOnDemandToken() {
39554
+ return randomBytes2(4).toString("hex");
39555
+ }
39556
+ function isVoiceOnDemandCallback(data) {
39557
+ return data.startsWith(VOICE_ONDEMAND_CALLBACK_PREFIX);
39558
+ }
39559
+ function parseVoiceOnDemandToken(data) {
39560
+ if (!isVoiceOnDemandCallback(data))
39561
+ return null;
39562
+ const token = data.slice(VOICE_ONDEMAND_CALLBACK_PREFIX.length);
39563
+ return token.length > 0 ? token : null;
39564
+ }
39565
+ function buildListenKeyboard(token) {
39566
+ return {
39567
+ inline_keyboard: [
39568
+ [{ text: "\uD83D\uDD0A Listen", callback_data: `${VOICE_ONDEMAND_CALLBACK_PREFIX}${token}` }]
39569
+ ]
39570
+ };
39571
+ }
39572
+ function mayInjectListenButton(rawKeyboard) {
39573
+ if (rawKeyboard == null)
39574
+ return true;
39575
+ return !rawKeyboard.some((row) => Array.isArray(row) && row.length > 0);
39576
+ }
39577
+ function planListenButton(params) {
39578
+ const plan = params.voiceOutPlan;
39579
+ const useOnDemandButton = plan != null && plan.replyMode === "on-demand" && plan.engine === "kokoro";
39580
+ if (!useOnDemandButton)
39581
+ return null;
39582
+ if (!(plan.ttsChunks.length > 0 && plan.ttsChunks[0].length > 0))
39583
+ return null;
39584
+ if (!mayInjectListenButton(params.rawKeyboard))
39585
+ return null;
39586
+ const token = mintVoiceOnDemandToken();
39587
+ return {
39588
+ replyMarkup: buildListenKeyboard(token),
39589
+ token,
39590
+ payload: {
39591
+ text: plan.ttsChunks[0],
39592
+ ...plan.voice != null ? { voice: plan.voice } : {},
39593
+ speed: plan.speed
39594
+ }
39595
+ };
39596
+ }
39597
+
39598
+ // gateway/outbox-listen-markup.ts
39599
+ function makeOutboxListenMarkupResolver(deps) {
39600
+ return (_chatId, _threadId, text) => {
39601
+ const voiceOutPlan = deps.resolveVoiceOutPlan(text);
39602
+ const listenPlan = planListenButton({ voiceOutPlan, rawKeyboard: undefined });
39603
+ if (listenPlan == null)
39604
+ return;
39605
+ const payload = listenPlan.payload;
39606
+ deps.cachePut(listenPlan.token, payload);
39607
+ if (deps.eagerVoiceEnabled()) {
39608
+ deps.enqueuePreSynth({
39609
+ token: listenPlan.token,
39610
+ text: payload.text,
39611
+ ...payload.voice != null ? { voice: payload.voice } : {},
39612
+ speed: payload.speed
39613
+ });
39614
+ }
39615
+ return listenPlan.replyMarkup;
39616
+ };
39617
+ }
39618
+
39419
39619
  // voice-presynth.ts
39420
- import { readdirSync, statSync as statSync2, unlinkSync, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, renameSync as renameSync3 } from "fs";
39620
+ import { readdirSync, statSync as statSync2, unlinkSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, renameSync as renameSync4 } from "fs";
39421
39621
  import { join as join2 } from "path";
39422
39622
  var VOICE_FILE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
39423
39623
  var VOICE_CACHE_MAX_BYTES = 500 * 1024 * 1024;
39424
39624
  var PRESYNTH_MAX_PENDING = 50;
39425
39625
  var VOICE_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
39626
+ function eagerVoiceEnabled() {
39627
+ const kill = process.env.SWITCHROOM_DISABLE_EAGER_VOICE;
39628
+ return !(kill === "1" || kill === "true");
39629
+ }
39630
+
39426
39631
  class PreSynthQueue {
39427
39632
  pending = [];
39428
39633
  running = false;
@@ -39474,11 +39679,11 @@ function voiceCacheFilePath(dir, token) {
39474
39679
  return join2(dir, `${token}.ogg`);
39475
39680
  }
39476
39681
  function writeVoiceCacheFile(dir, token, audio) {
39477
- mkdirSync3(dir, { recursive: true, mode: 448 });
39682
+ mkdirSync4(dir, { recursive: true, mode: 448 });
39478
39683
  const final = voiceCacheFilePath(dir, token);
39479
39684
  const tmp = `${final}.tmp`;
39480
- writeFileSync2(tmp, audio);
39481
- renameSync3(tmp, final);
39685
+ writeFileSync3(tmp, audio);
39686
+ renameSync4(tmp, final);
39482
39687
  return final;
39483
39688
  }
39484
39689
  function sweepVoiceCacheDir(options) {
@@ -39535,6 +39740,49 @@ function sweepVoiceCacheDir(options) {
39535
39740
 
39536
39741
  // voice-normalize-text.ts
39537
39742
  var CODE_BLOCK_PLACEHOLDER = "code block omitted";
39743
+ var HTML_ENTITIES = {
39744
+ amp: "&",
39745
+ lt: "<",
39746
+ gt: ">",
39747
+ quot: '"',
39748
+ apos: "'",
39749
+ nbsp: " "
39750
+ };
39751
+ var METACHAR_SPOKEN = {
39752
+ "#": " hash ",
39753
+ "*": " asterisk ",
39754
+ _: " underscore ",
39755
+ "~": " tilde ",
39756
+ "`": " backtick ",
39757
+ "|": " bar "
39758
+ };
39759
+ function decodeHtmlEntitiesOnce(input) {
39760
+ const toChar = (cp, raw) => {
39761
+ if (!(cp > 0 && cp <= 1114111))
39762
+ return raw;
39763
+ const ch = String.fromCodePoint(cp);
39764
+ return METACHAR_SPOKEN[ch] ?? ch;
39765
+ };
39766
+ return input.replace(/&#x([0-9a-f]+);/gi, (m, hex) => toChar(parseInt(hex, 16), m)).replace(/&#(\d+);/g, (m, dec) => toChar(Number(dec), m)).replace(/&([a-z][a-z0-9]*);/gi, (m, name) => {
39767
+ const ch = HTML_ENTITIES[name.toLowerCase()];
39768
+ if (ch === undefined)
39769
+ return m;
39770
+ return METACHAR_SPOKEN[ch] ?? ch;
39771
+ });
39772
+ }
39773
+ function decodeHtmlEntities(input) {
39774
+ let s = input;
39775
+ for (let i = 0;i < 10; i++) {
39776
+ const next = decodeHtmlEntitiesOnce(s);
39777
+ if (next === s)
39778
+ break;
39779
+ s = next;
39780
+ }
39781
+ return s;
39782
+ }
39783
+ function stripBackslashEscapes(input) {
39784
+ return input.replace(/\\([\s\S])/g, "$1").replace(/\\/g, "");
39785
+ }
39538
39786
  var ONES = [
39539
39787
  "zero",
39540
39788
  "one",
@@ -39701,6 +39949,8 @@ function normalizeForSpeech(input) {
39701
39949
  return "";
39702
39950
  let s = input.replace(/\r\n?/g, `
39703
39951
  `);
39952
+ s = decodeHtmlEntities(s);
39953
+ s = stripBackslashEscapes(s);
39704
39954
  s = s.replace(/[\u{1F000}-\u{1FAFF}\u{1F1E6}-\u{1F1FF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE00}-\u{FE0F}\u{200D}\u{2B50}\u{3030}\u{303D}\u{3297}\u{3299}\u{24C2}]/gu, "");
39705
39955
  s = s.replace(/:([a-z0-9][a-z0-9_+-]*):/gi, " ");
39706
39956
  s = s.replace(/(^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n[ \t]*\2[ \t]*(?=\n|$)/g, `$1${CODE_BLOCK_PLACEHOLDER}.`);
@@ -39800,6 +40050,65 @@ function normalizeForSpeech(input) {
39800
40050
  return s.trim();
39801
40051
  }
39802
40052
 
40053
+ // voice-normalize-text.ts
40054
+ var HTML_ENTITIES2 = {
40055
+ amp: "&",
40056
+ lt: "<",
40057
+ gt: ">",
40058
+ quot: '"',
40059
+ apos: "'",
40060
+ nbsp: " "
40061
+ };
40062
+ var METACHAR_SPOKEN2 = {
40063
+ "#": " hash ",
40064
+ "*": " asterisk ",
40065
+ _: " underscore ",
40066
+ "~": " tilde ",
40067
+ "`": " backtick ",
40068
+ "|": " bar "
40069
+ };
40070
+ function decodeHtmlEntitiesOnce2(input) {
40071
+ const toChar = (cp, raw) => {
40072
+ if (!(cp > 0 && cp <= 1114111))
40073
+ return raw;
40074
+ const ch = String.fromCodePoint(cp);
40075
+ return METACHAR_SPOKEN2[ch] ?? ch;
40076
+ };
40077
+ return input.replace(/&#x([0-9a-f]+);/gi, (m, hex) => toChar(parseInt(hex, 16), m)).replace(/&#(\d+);/g, (m, dec) => toChar(Number(dec), m)).replace(/&([a-z][a-z0-9]*);/gi, (m, name) => {
40078
+ const ch = HTML_ENTITIES2[name.toLowerCase()];
40079
+ if (ch === undefined)
40080
+ return m;
40081
+ return METACHAR_SPOKEN2[ch] ?? ch;
40082
+ });
40083
+ }
40084
+ function decodeHtmlEntities2(input) {
40085
+ let s = input;
40086
+ for (let i = 0;i < 10; i++) {
40087
+ const next = decodeHtmlEntitiesOnce2(s);
40088
+ if (next === s)
40089
+ break;
40090
+ s = next;
40091
+ }
40092
+ return s;
40093
+ }
40094
+ function stripBackslashEscapes2(input) {
40095
+ return input.replace(/\\([\s\S])/g, "$1").replace(/\\/g, "");
40096
+ }
40097
+ var ACRONYMS2 = new Set([
40098
+ "CI",
40099
+ "PR",
40100
+ "API",
40101
+ "URL",
40102
+ "GPU",
40103
+ "CPU",
40104
+ "TTS",
40105
+ "STT",
40106
+ "HTTP",
40107
+ "JSON",
40108
+ "SQL",
40109
+ "UI"
40110
+ ]);
40111
+
39803
40112
  // tts-normalize.ts
39804
40113
  var NULL = "\x00";
39805
40114
  var INLINE_PH = `${NULL}TN_INLINE`;
@@ -39980,6 +40289,8 @@ function normalizeForTts(text) {
39980
40289
  return text;
39981
40290
  let s = text.replace(/\r\n?/g, `
39982
40291
  `);
40292
+ s = decodeHtmlEntities2(s);
40293
+ s = stripBackslashEscapes2(s);
39983
40294
  s = s.replace(/(^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n[ \t]*\2[ \t]*(?=\n|$)/g, "$1code block omitted.");
39984
40295
  s = s.replace(/(^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*$/g, "$1code block omitted.");
39985
40296
  const { parked, parts } = parkInline(s);
@@ -43629,7 +43940,7 @@ async function handleAnimationMessage(ctx, deps) {
43629
43940
  }
43630
43941
 
43631
43942
  // gateway/photo-message-handler.ts
43632
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
43943
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
43633
43944
 
43634
43945
  // attachment-path.ts
43635
43946
  import { join as join4, basename as basename3, resolve as resolve2, sep } from "node:path";
@@ -43690,9 +44001,9 @@ async function handlePhotoMessage(ctx, deps) {
43690
44001
  fileUniqueId: best.file_unique_id,
43691
44002
  now: Date.now()
43692
44003
  });
43693
- mkdirSync6(deps.inboxDir, { recursive: true, mode: 448 });
44004
+ mkdirSync7(deps.inboxDir, { recursive: true, mode: 448 });
43694
44005
  assertInsideInbox(deps.inboxDir, dlPath);
43695
- writeFileSync5(dlPath, buf, { mode: 384 });
44006
+ writeFileSync6(dlPath, buf, { mode: 384 });
43696
44007
  return dlPath;
43697
44008
  } catch (err) {
43698
44009
  const msg = err instanceof Error ? err.message : "unknown error";
@@ -44760,165 +45071,6 @@ async function handleAskCallback(ctx, data, deps) {
44760
45071
  // gateway/voice-ondemand-callback-handler.ts
44761
45072
  import { readFileSync as readFileSync8 } from "node:fs";
44762
45073
 
44763
- // voice-ondemand.ts
44764
- import { randomBytes as randomBytes2 } from "crypto";
44765
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync6, renameSync as renameSync5, mkdirSync as mkdirSync7 } from "fs";
44766
- import { dirname as dirname3 } from "path";
44767
- var VOICE_ONDEMAND_CALLBACK_PREFIX = "voice:";
44768
- var VOICE_ONDEMAND_TTL_MS2 = 7 * 24 * 60 * 60 * 1000;
44769
- var VOICE_ONDEMAND_MAX_ENTRIES2 = 500;
44770
-
44771
- class VoiceOnDemandCache2 {
44772
- store = new Map;
44773
- ttlMs;
44774
- maxEntries;
44775
- now;
44776
- persistPath;
44777
- constructor(options = {}) {
44778
- this.ttlMs = options.ttlMs ?? VOICE_ONDEMAND_TTL_MS2;
44779
- this.maxEntries = options.maxEntries ?? VOICE_ONDEMAND_MAX_ENTRIES2;
44780
- this.now = options.now ?? Date.now;
44781
- this.persistPath = options.persistPath;
44782
- if (this.persistPath !== undefined)
44783
- this.load();
44784
- }
44785
- put(token, payload) {
44786
- this.store.delete(token);
44787
- this.store.set(token, {
44788
- createdAt: this.now(),
44789
- ...payload,
44790
- expiresAt: this.now() + this.ttlMs
44791
- });
44792
- while (this.store.size > this.maxEntries) {
44793
- const oldest = this.store.keys().next().value;
44794
- if (oldest === undefined)
44795
- break;
44796
- this.store.delete(oldest);
44797
- }
44798
- this.flush();
44799
- }
44800
- get(token) {
44801
- const entry = this.store.get(token);
44802
- if (entry == null)
44803
- return null;
44804
- if (entry.expiresAt <= this.now()) {
44805
- this.store.delete(token);
44806
- this.flush();
44807
- return null;
44808
- }
44809
- const { text, voice, speed, filePath, telegramFileId } = entry;
44810
- return {
44811
- text,
44812
- speed,
44813
- ...voice !== undefined ? { voice } : {},
44814
- ...filePath !== undefined ? { filePath } : {},
44815
- ...telegramFileId !== undefined ? { telegramFileId } : {}
44816
- };
44817
- }
44818
- setFilePath(token, filePath) {
44819
- const entry = this.store.get(token);
44820
- if (entry == null || entry.expiresAt <= this.now())
44821
- return;
44822
- entry.filePath = filePath;
44823
- this.flush();
44824
- }
44825
- setTelegramFileId(token, fileId) {
44826
- if (fileId.length === 0)
44827
- return;
44828
- const entry = this.store.get(token);
44829
- if (entry == null || entry.expiresAt <= this.now())
44830
- return;
44831
- entry.telegramFileId = fileId;
44832
- this.flush();
44833
- }
44834
- prune(tokens) {
44835
- let changed = false;
44836
- for (const token of tokens) {
44837
- if (this.store.delete(token))
44838
- changed = true;
44839
- }
44840
- if (changed)
44841
- this.flush();
44842
- }
44843
- get size() {
44844
- return this.store.size;
44845
- }
44846
- load() {
44847
- if (this.persistPath === undefined)
44848
- return;
44849
- let raw;
44850
- try {
44851
- raw = readFileSync4(this.persistPath, "utf8");
44852
- } catch {
44853
- return;
44854
- }
44855
- try {
44856
- const parsed = JSON.parse(raw);
44857
- if (parsed == null || parsed.version !== 1 || typeof parsed.entries !== "object")
44858
- return;
44859
- const nowMs = this.now();
44860
- for (const [token, entry] of Object.entries(parsed.entries)) {
44861
- if (entry == null || typeof entry.expiresAt !== "number" || typeof entry.text !== "string" || typeof entry.speed !== "number") {
44862
- continue;
44863
- }
44864
- if (entry.expiresAt <= nowMs)
44865
- continue;
44866
- this.store.set(token, entry);
44867
- }
44868
- while (this.store.size > this.maxEntries) {
44869
- const oldest = this.store.keys().next().value;
44870
- if (oldest === undefined)
44871
- break;
44872
- this.store.delete(oldest);
44873
- }
44874
- } catch (err) {
44875
- process.stderr.write(`voice-ondemand: failed to parse persisted cache ${this.persistPath}: ${String(err)}
44876
- `);
44877
- }
44878
- }
44879
- flush() {
44880
- if (this.persistPath === undefined)
44881
- return;
44882
- const doc = {
44883
- version: 1,
44884
- entries: Object.fromEntries(this.store)
44885
- };
44886
- const tmp = `${this.persistPath}.tmp`;
44887
- try {
44888
- mkdirSync7(dirname3(this.persistPath), { recursive: true });
44889
- writeFileSync6(tmp, JSON.stringify(doc), "utf8");
44890
- renameSync5(tmp, this.persistPath);
44891
- } catch (err) {
44892
- process.stderr.write(`voice-ondemand: failed to persist cache ${this.persistPath}: ${String(err)}
44893
- `);
44894
- }
44895
- }
44896
- }
44897
- function mintVoiceOnDemandToken() {
44898
- return randomBytes2(4).toString("hex");
44899
- }
44900
- function isVoiceOnDemandCallback(data) {
44901
- return data.startsWith(VOICE_ONDEMAND_CALLBACK_PREFIX);
44902
- }
44903
- function parseVoiceOnDemandToken(data) {
44904
- if (!isVoiceOnDemandCallback(data))
44905
- return null;
44906
- const token = data.slice(VOICE_ONDEMAND_CALLBACK_PREFIX.length);
44907
- return token.length > 0 ? token : null;
44908
- }
44909
- function buildListenKeyboard(token) {
44910
- return {
44911
- inline_keyboard: [
44912
- [{ text: "\uD83D\uDD0A Listen", callback_data: `${VOICE_ONDEMAND_CALLBACK_PREFIX}${token}` }]
44913
- ]
44914
- };
44915
- }
44916
- function mayInjectListenButton(rawKeyboard) {
44917
- if (rawKeyboard == null)
44918
- return true;
44919
- return !rawKeyboard.some((row) => Array.isArray(row) && row.length > 0);
44920
- }
44921
-
44922
45074
  // voice-send.ts
44923
45075
  function extractVoiceFileId(sent) {
44924
45076
  const id = sent?.voice?.file_id;
@@ -45231,6 +45383,8 @@ function normalizeForTts2(text) {
45231
45383
  return text;
45232
45384
  let s = text.replace(/\r\n?/g, `
45233
45385
  `);
45386
+ s = decodeHtmlEntities2(s);
45387
+ s = stripBackslashEscapes2(s);
45234
45388
  s = s.replace(/(^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?\n[ \t]*\2[ \t]*(?=\n|$)/g, "$1code block omitted.");
45235
45389
  s = s.replace(/(^|\n)[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*$/g, "$1code block omitted.");
45236
45390
  const { parked, parts } = parkInline2(s);
@@ -51566,202 +51720,6 @@ function createSessionModelSource(options = {}) {
51566
51720
  };
51567
51721
  }
51568
51722
 
51569
- // tool-labels.ts
51570
- var MAX_LABEL_CHARS = 60;
51571
- var MAX_BASH_CHARS = 40;
51572
- var MAX_DESCRIPTION_CHARS = 160;
51573
- function basename6(p) {
51574
- if (!p)
51575
- return "";
51576
- const parts = p.split("/").filter(Boolean);
51577
- return parts.length > 0 ? parts[parts.length - 1] : p;
51578
- }
51579
- function shortenGrepPath(p) {
51580
- if (!p)
51581
- return "repo";
51582
- const hadTrailingSlash = /\/+$/.test(p);
51583
- const trimmed = p.replace(/\/+$/, "");
51584
- const parts = trimmed.split("/").filter(Boolean);
51585
- if (parts.length === 0)
51586
- return "repo";
51587
- const last = parts[parts.length - 1];
51588
- if (hadTrailingSlash)
51589
- return `${last}/`;
51590
- if (last.startsWith(".") && !last.slice(1).includes("."))
51591
- return last;
51592
- if (!last.includes("."))
51593
- return `${last}/`;
51594
- return last;
51595
- }
51596
- function hostFromUrl(u) {
51597
- if (!u)
51598
- return "";
51599
- try {
51600
- return new URL(u).host;
51601
- } catch {
51602
- return truncate3(u);
51603
- }
51604
- }
51605
- function truncate3(s, n = MAX_LABEL_CHARS) {
51606
- if (s.length <= n)
51607
- return s;
51608
- return s.slice(0, n - 1) + "\u2026";
51609
- }
51610
- function stripHtml(s) {
51611
- return s.replace(/<\/?[a-zA-Z][^>]*>/g, "");
51612
- }
51613
- function firstLine(s) {
51614
- const idx = s.indexOf(`
51615
- `);
51616
- return idx === -1 ? s : s.slice(0, idx);
51617
- }
51618
- function toolLabel(tool, input, preamble, precomputedLabel) {
51619
- if (precomputedLabel && precomputedLabel.trim().length > 0) {
51620
- return truncate3(firstLine(precomputedLabel.trim()), MAX_DESCRIPTION_CHARS);
51621
- }
51622
- if (!input || typeof input !== "object")
51623
- return "";
51624
- const str = (k) => typeof input[k] === "string" ? input[k] : undefined;
51625
- const preambleLabel = () => {
51626
- if (!preamble)
51627
- return null;
51628
- if (preamble.includes(`
51629
- `))
51630
- return null;
51631
- const trimmed = preamble.trim();
51632
- if (!trimmed)
51633
- return null;
51634
- if (trimmed.length > MAX_DESCRIPTION_CHARS)
51635
- return null;
51636
- return trimmed;
51637
- };
51638
- switch (tool) {
51639
- case "Read":
51640
- case "Write":
51641
- case "NotebookEdit":
51642
- case "Edit": {
51643
- const pre = preambleLabel();
51644
- if (pre)
51645
- return pre;
51646
- return truncate3(basename6(str("file_path") ?? ""));
51647
- }
51648
- case "Bash":
51649
- case "BashOutput": {
51650
- const description = str("description");
51651
- if (description)
51652
- return truncate3(firstLine(description), MAX_DESCRIPTION_CHARS);
51653
- const pre = preambleLabel();
51654
- if (pre)
51655
- return pre;
51656
- const cmd = str("command") ?? str("bash_id") ?? "";
51657
- return truncate3(firstLine(cmd), MAX_BASH_CHARS);
51658
- }
51659
- case "KillShell":
51660
- return truncate3(str("shell_id") ?? "");
51661
- case "Glob": {
51662
- const pre = preambleLabel();
51663
- if (pre)
51664
- return pre;
51665
- return truncate3(str("pattern") ?? "");
51666
- }
51667
- case "Grep": {
51668
- const pre = preambleLabel();
51669
- if (pre)
51670
- return pre;
51671
- const pat = str("pattern") ?? "";
51672
- if (!pat)
51673
- return "";
51674
- const path = str("path");
51675
- const where = shortenGrepPath(path ?? "");
51676
- return truncate3(`"${pat}" (in ${where})`);
51677
- }
51678
- case "WebFetch":
51679
- return truncate3(hostFromUrl(str("url") ?? ""));
51680
- case "WebSearch": {
51681
- const q = str("query") ?? "";
51682
- return q ? truncate3(`"${q}"`) : "";
51683
- }
51684
- case "Task":
51685
- case "Agent": {
51686
- const desc = str("description") ?? str("subagent_type") ?? "";
51687
- return truncate3(desc);
51688
- }
51689
- case "TodoWrite":
51690
- case "TaskCreate":
51691
- case "TaskUpdate":
51692
- case "TaskList":
51693
- case "TaskGet":
51694
- case "TaskStop":
51695
- case "TaskOutput":
51696
- return "";
51697
- case "Skill":
51698
- return truncate3(str("skill") ?? "");
51699
- case "SlashCommand":
51700
- return truncate3(str("command") ?? "");
51701
- case "ToolSearch": {
51702
- const q = str("query") ?? "";
51703
- if (!q)
51704
- return "";
51705
- const selectMatch = q.match(/^\s*select\s*:\s*(.+)$/i);
51706
- if (selectMatch) {
51707
- const names = selectMatch[1].split(",").map((n) => n.trim()).filter((n) => n.length > 0).join(", ");
51708
- return truncate3(`Loading schema: ${names}`);
51709
- }
51710
- return truncate3(`Searching tools: ${q}`);
51711
- }
51712
- default:
51713
- if (tool.startsWith("mcp__")) {
51714
- const description = str("description");
51715
- if (description)
51716
- return truncate3(firstLine(stripHtml(description)), MAX_DESCRIPTION_CHARS);
51717
- const label = mcpBaseLabel(tool);
51718
- const query = str("query") ?? str("text") ?? str("name");
51719
- if (label && query) {
51720
- const budget = Math.max(8, MAX_LABEL_CHARS - label.length - 4);
51721
- const preview = truncate3(firstLine(stripHtml(query)), budget);
51722
- return `${label} (${preview})`;
51723
- }
51724
- if (label)
51725
- return truncate3(label);
51726
- }
51727
- for (const k of ["description", "file_path", "path", "url", "query", "pattern", "command"]) {
51728
- const v = str(k);
51729
- if (v != null && v.length > 0) {
51730
- if (k === "file_path" || k === "path")
51731
- return truncate3(basename6(v));
51732
- if (k === "url")
51733
- return truncate3(hostFromUrl(v));
51734
- if (k === "description")
51735
- return truncate3(firstLine(v), MAX_DESCRIPTION_CHARS);
51736
- return truncate3(firstLine(v));
51737
- }
51738
- }
51739
- return "";
51740
- }
51741
- }
51742
- function mcpBaseLabel(tool) {
51743
- if (!tool.startsWith("mcp__"))
51744
- return "";
51745
- const parts = tool.slice("mcp__".length).split("__");
51746
- if (parts.length < 2)
51747
- return "";
51748
- const rawServer = parts[0];
51749
- const action = parts.slice(1).join("__");
51750
- if (!rawServer || !action)
51751
- return "";
51752
- return `${prettifyServer(rawServer)}: ${action}`;
51753
- }
51754
- function prettifyServer(name) {
51755
- const LABELS = {
51756
- "switchroom-telegram": "Telegram"
51757
- };
51758
- if (LABELS[name])
51759
- return LABELS[name];
51760
- if (!name)
51761
- return name;
51762
- return name.charAt(0).toUpperCase() + name.slice(1);
51763
- }
51764
-
51765
51723
  // typing-wrap.ts
51766
51724
  function createTypingWrapper(deps) {
51767
51725
  const debounceMs = deps.debounceMs ?? 500;
@@ -63253,7 +63211,7 @@ function loadInitialFloodWindows(floodStateFilePath, floodWindowsFilePath, now)
63253
63211
  }
63254
63212
 
63255
63213
  // attachment-path.ts
63256
- import { join as join17, basename as basename7, resolve as resolve6, sep as sep2 } from "node:path";
63214
+ import { join as join17, basename as basename6, resolve as resolve6, sep as sep2 } from "node:path";
63257
63215
  function sanitizeExtension2(ext) {
63258
63216
  if (ext == null)
63259
63217
  return "bin";
@@ -63282,7 +63240,7 @@ function assertInsideInbox2(inboxDir, candidatePath) {
63282
63240
  const inboxReal = resolve6(inboxDir);
63283
63241
  const candidateReal = resolve6(candidatePath);
63284
63242
  if (candidateReal !== inboxReal && !candidateReal.startsWith(inboxReal + sep2)) {
63285
- throw new Error(`attachment path escape: ${basename7(candidatePath)} resolved outside ${inboxDir}`);
63243
+ throw new Error(`attachment path escape: ${basename6(candidatePath)} resolved outside ${inboxDir}`);
63286
63244
  }
63287
63245
  }
63288
63246
 
@@ -68095,6 +68053,8 @@ __export(exports_silence_poke, {
68095
68053
  noteThinking: () => noteThinking,
68096
68054
  noteProduction: () => noteProduction,
68097
68055
  noteOutbound: () => noteOutbound2,
68056
+ noteBackgroundShellDead: () => noteBackgroundShellDead,
68057
+ noteBackgroundShellAlive: () => noteBackgroundShellAlive,
68098
68058
  longestInFlightTool: () => longestInFlightTool,
68099
68059
  formatFrameworkFallbackText: () => formatFrameworkFallbackText,
68100
68060
  endTurn: () => endTurn,
@@ -68102,6 +68062,7 @@ __export(exports_silence_poke, {
68102
68062
  __setDepsForTests: () => __setDepsForTests,
68103
68063
  __resetAllForTests: () => __resetAllForTests2,
68104
68064
  __getStateForTests: () => __getStateForTests,
68065
+ __bgMarkerParserConfirmedForTests: () => __bgMarkerParserConfirmedForTests,
68105
68066
  DEFAULT_THRESHOLDS: () => DEFAULT_THRESHOLDS,
68106
68067
  DEFAULT_POLL_INTERVAL_MS: () => DEFAULT_POLL_INTERVAL_MS
68107
68068
  });
@@ -68172,6 +68133,7 @@ var DEFAULT_POLL_INTERVAL_MS = 5000;
68172
68133
  var state2 = new Map;
68173
68134
  var timer = null;
68174
68135
  var activeDeps = null;
68136
+ var bgMarkerParserConfirmed = false;
68175
68137
  function silencePokeEnabled() {
68176
68138
  const v = process.env.SWITCHROOM_DISABLE_SILENCE_POKE;
68177
68139
  return !(v === "1" || v === "true");
@@ -68185,9 +68147,24 @@ function startTurn(key, now) {
68185
68147
  lastThinkingAt: null,
68186
68148
  fallbackFired: false,
68187
68149
  floorFired: false,
68188
- inFlightTools: new Map
68150
+ inFlightTools: new Map,
68151
+ sawBashThisTurn: false,
68152
+ aliveShells: new Set
68189
68153
  });
68190
68154
  }
68155
+ function noteBackgroundShellAlive(key, shellId) {
68156
+ bgMarkerParserConfirmed = true;
68157
+ const s = state2.get(key);
68158
+ if (s == null)
68159
+ return;
68160
+ s.aliveShells.add(shellId);
68161
+ }
68162
+ function noteBackgroundShellDead(key, shellId) {
68163
+ const s = state2.get(key);
68164
+ if (s == null)
68165
+ return;
68166
+ s.aliveShells.delete(shellId);
68167
+ }
68191
68168
  function noteOutbound2(key, now) {
68192
68169
  const s = state2.get(key);
68193
68170
  if (s == null)
@@ -68213,6 +68190,8 @@ function noteToolStart(key, toolUseId, name, label, now) {
68213
68190
  if (s == null)
68214
68191
  return;
68215
68192
  s.inFlightTools.set(toolUseId, { name, startedAt: now, label });
68193
+ if (name === "Bash")
68194
+ s.sawBashThisTurn = true;
68216
68195
  }
68217
68196
  function noteToolEnd(key, toolUseId, _now) {
68218
68197
  const s = state2.get(key);
@@ -68327,6 +68306,10 @@ function tick(now) {
68327
68306
  if (!forceDisable && activeDeps.isLegitimatelyWorking != null) {
68328
68307
  if (activeDeps.isLegitimatelyWorking(key))
68329
68308
  continue;
68309
+ if (s.aliveShells.size > 0)
68310
+ continue;
68311
+ if (!bgMarkerParserConfirmed && s.sawBashThisTurn)
68312
+ continue;
68330
68313
  } else if (!forceDisable && activeDeps.deferFallbackWhileToolInFlight === true && s.inFlightTools.size > 0) {
68331
68314
  continue;
68332
68315
  }
@@ -68409,6 +68392,10 @@ function __getStateForTests(key) {
68409
68392
  function __resetAllForTests2() {
68410
68393
  state2.clear();
68411
68394
  stopTimer();
68395
+ bgMarkerParserConfirmed = false;
68396
+ }
68397
+ function __bgMarkerParserConfirmedForTests() {
68398
+ return bgMarkerParserConfirmed;
68412
68399
  }
68413
68400
 
68414
68401
  // pending-work-progress.ts
@@ -68781,7 +68768,7 @@ function resolveAnswerLaneConfig(input) {
68781
68768
 
68782
68769
  // session-tail.ts
68783
68770
  import { homedir as homedir7 } from "os";
68784
- import { basename as basename8, join as join23 } from "path";
68771
+ import { basename as basename7, join as join23 } from "path";
68785
68772
 
68786
68773
  // operator-events.ts
68787
68774
  init_format();
@@ -69218,6 +69205,29 @@ function extractToolResultErrorText(content3) {
69218
69205
  }
69219
69206
  return "";
69220
69207
  }
69208
+ function parseBackgroundTaskId(obj) {
69209
+ const tur = obj.toolUseResult;
69210
+ if (typeof tur === "object" && tur != null) {
69211
+ const id = tur.backgroundTaskId;
69212
+ if (typeof id === "string" && id.length > 0)
69213
+ return id;
69214
+ }
69215
+ return null;
69216
+ }
69217
+ function parseBackgroundLaunchString(content3) {
69218
+ const text4 = typeof content3 === "string" ? content3 : extractToolResultErrorText(content3);
69219
+ const m = text4.match(/Command running in background with ID: (\w+)/);
69220
+ return m != null ? m[1] : null;
69221
+ }
69222
+ function parseTaskNotification(content3) {
69223
+ if (!content3.includes("<task-notification>"))
69224
+ return null;
69225
+ const idM = content3.match(/<task-id>([^<]+)<\/task-id>/);
69226
+ const stM = content3.match(/<status>([^<]+)<\/status>/);
69227
+ if (idM == null || stM == null)
69228
+ return null;
69229
+ return { taskId: idM[1].trim(), status: stM[1].trim() };
69230
+ }
69221
69231
  function projectAssistantTextBlocks(content3, make) {
69222
69232
  const out = new Map;
69223
69233
  let lastToolUseIdx = -1;
@@ -69314,6 +69324,10 @@ function projectTranscriptLine(line) {
69314
69324
  const op = obj.operation;
69315
69325
  if (op === "enqueue") {
69316
69326
  const content3 = obj.content ?? "";
69327
+ const notif = parseTaskNotification(content3);
69328
+ if (notif != null) {
69329
+ return [{ kind: "task_notification", taskId: notif.taskId, status: notif.status }];
69330
+ }
69317
69331
  const { chatId, messageId, threadId } = parseChannelMeta(content3);
69318
69332
  return [{ kind: "enqueue", chatId, messageId, threadId, rawContent: content3 }];
69319
69333
  }
@@ -69371,6 +69385,7 @@ function projectTranscriptLine(line) {
69371
69385
  const content3 = message?.content;
69372
69386
  if (!Array.isArray(content3))
69373
69387
  return [];
69388
+ const backgroundTaskId = parseBackgroundTaskId(obj);
69374
69389
  const events = [];
69375
69390
  for (const c of content3) {
69376
69391
  if (c.type === "tool_result") {
@@ -69380,7 +69395,8 @@ function projectTranscriptLine(line) {
69380
69395
  toolUseId: c.tool_use_id ?? "",
69381
69396
  toolName: null,
69382
69397
  isError: isError2,
69383
- errorText: isError2 ? extractToolResultErrorText(c.content) : undefined
69398
+ errorText: isError2 ? extractToolResultErrorText(c.content) : undefined,
69399
+ backgroundTaskId: backgroundTaskId ?? parseBackgroundLaunchString(c.content) ?? undefined
69384
69400
  });
69385
69401
  }
69386
69402
  }
@@ -74908,7 +74924,7 @@ function deriveTelegraphTitle2(text4) {
74908
74924
  var VOICE_FILE_TTL_MS2 = 7 * 24 * 60 * 60 * 1000;
74909
74925
  var VOICE_CACHE_MAX_BYTES2 = 500 * 1024 * 1024;
74910
74926
  var VOICE_SWEEP_INTERVAL_MS2 = 60 * 60 * 1000;
74911
- function eagerVoiceEnabled() {
74927
+ function eagerVoiceEnabled2() {
74912
74928
  const kill = process.env.SWITCHROOM_DISABLE_EAGER_VOICE;
74913
74929
  return !(kill === "1" || kill === "true");
74914
74930
  }
@@ -75698,24 +75714,23 @@ ${url}`;
75698
75714
  replyButtonMeta = extractAgentButtonMeta(redactedKeyboard);
75699
75715
  replyMarkup = { inline_keyboard: wrapAgentCallbacks(redactedKeyboard) };
75700
75716
  }
75701
- if (useOnDemandButton && voiceOutPlan.ttsChunks.length > 0 && voiceOutPlan.ttsChunks[0].length > 0) {
75702
- if (!mayInjectListenButton(rawKeyboard)) {
75703
- process.stderr.write(`telegram gateway: voice-out on-demand: agent supplied inline_keyboard \u2014 skipping Listen button (single_use collision gate)
75717
+ if (useOnDemandButton) {
75718
+ const listenPlan = planListenButton({ voiceOutPlan, rawKeyboard });
75719
+ if (listenPlan == null) {
75720
+ const hasSpeakableText = voiceOutPlan.ttsChunks.length > 0 && voiceOutPlan.ttsChunks[0].length > 0;
75721
+ if (hasSpeakableText && !mayInjectListenButton(rawKeyboard)) {
75722
+ process.stderr.write(`telegram gateway: voice-out on-demand: agent supplied inline_keyboard \u2014 skipping Listen button (single_use collision gate)
75704
75723
  `);
75724
+ }
75705
75725
  } else {
75706
- const token = mintVoiceOnDemandToken();
75707
- voiceOnDemandCache.put(token, {
75708
- text: voiceOutPlan.ttsChunks[0],
75709
- ...voiceOutPlan.voice != null ? { voice: voiceOutPlan.voice } : {},
75710
- speed: voiceOutPlan.speed
75711
- });
75712
- replyMarkup = buildListenKeyboard(token);
75713
- if (eagerVoiceEnabled()) {
75726
+ voiceOnDemandCache.put(listenPlan.token, listenPlan.payload);
75727
+ replyMarkup = listenPlan.replyMarkup;
75728
+ if (eagerVoiceEnabled2()) {
75714
75729
  voicePreSynthQueue.enqueue({
75715
- token,
75716
- text: voiceOutPlan.ttsChunks[0],
75717
- ...voiceOutPlan.voice != null ? { voice: voiceOutPlan.voice } : {},
75718
- speed: voiceOutPlan.speed
75730
+ token: listenPlan.token,
75731
+ text: listenPlan.payload.text,
75732
+ ...listenPlan.payload.voice != null ? { voice: listenPlan.payload.voice } : {},
75733
+ speed: listenPlan.payload.speed
75719
75734
  });
75720
75735
  }
75721
75736
  }
@@ -85368,15 +85383,15 @@ function buildMs365CardText(p) {
85368
85383
  const lines = [];
85369
85384
  lines.push(`\uD83D\uDCC4 Microsoft 365 write approval`);
85370
85385
  lines.push("");
85371
- lines.push(`Agent: ${truncate4(p.agentName, 64)}`);
85372
- lines.push(`Tool: ${truncate4(p.toolName.replace(/^mcp__/, ""), 96)}`);
85373
- lines.push(`Item: ${truncate4(p.itemDisplayName, 256)}`);
85386
+ lines.push(`Agent: ${truncate3(p.agentName, 64)}`);
85387
+ lines.push(`Tool: ${truncate3(p.toolName.replace(/^mcp__/, ""), 96)}`);
85388
+ lines.push(`Item: ${truncate3(p.itemDisplayName, 256)}`);
85374
85389
  if (p.itemId !== "(new)") {
85375
- lines.push(`ID: ${truncate4(p.itemId, 96)}`);
85390
+ lines.push(`ID: ${truncate3(p.itemId, 96)}`);
85376
85391
  }
85377
- lines.push(`Account: ${truncate4(p.accountEmail, 96)}`);
85392
+ lines.push(`Account: ${truncate3(p.accountEmail, 96)}`);
85378
85393
  if (p.eventWhen) {
85379
- lines.push(`When: ${truncate4(p.eventWhen, 96)}`);
85394
+ lines.push(`When: ${truncate3(p.eventWhen, 96)}`);
85380
85395
  }
85381
85396
  if (typeof p.sizeBytesBefore === "number" || typeof p.sizeBytesAfter === "number") {
85382
85397
  const before = p.sizeBytesBefore ?? 0;
@@ -85386,27 +85401,27 @@ function buildMs365CardText(p) {
85386
85401
  lines.push(`Size: ${humanBytes(before)} \u2192 ${humanBytes(after)} (${sign}${humanBytes(delta)})`);
85387
85402
  }
85388
85403
  if (p.deepLink) {
85389
- lines.push(`Link: ${truncate4(p.deepLink, 256)}`);
85404
+ lines.push(`Link: ${truncate3(p.deepLink, 256)}`);
85390
85405
  }
85391
85406
  if (p.changes && p.changes.length > 0) {
85392
85407
  lines.push("");
85393
85408
  lines.push("Changes:");
85394
85409
  for (const c of p.changes.slice(0, 8)) {
85395
- const before = c.before !== undefined ? truncate4(c.before, 96) : "(none)";
85396
- const after = c.after !== undefined ? truncate4(c.after, 96) : "(cleared)";
85410
+ const before = c.before !== undefined ? truncate3(c.before, 96) : "(none)";
85411
+ const after = c.after !== undefined ? truncate3(c.after, 96) : "(cleared)";
85397
85412
  lines.push(`\u2022 ${c.field}: ${before} \u2192 ${after}`);
85398
85413
  }
85399
85414
  }
85400
85415
  if (p.agentRationale) {
85401
85416
  lines.push("");
85402
- lines.push(`\uD83D\uDCAC ${truncate4(p.agentRationale, 512)}`);
85417
+ lines.push(`\uD83D\uDCAC ${truncate3(p.agentRationale, 512)}`);
85403
85418
  }
85404
85419
  lines.push("");
85405
85420
  lines.push(p.changes && p.changes.length > 0 ? "\u26a0\ufe0f Attestation (RFC \u00a78 v1.5): the diff above is derived from live Graph state + the mutation payload. Verify before approving." : "\u26a0\ufe0f Weak attestation (RFC \u00a78 v1): operator should click through to verify the actual change before approving. Structural diff coming v1.5.");
85406
85421
  return hardenCardBreaks(lines.join(`
85407
85422
  `));
85408
85423
  }
85409
- function truncate4(s, n) {
85424
+ function truncate3(s, n) {
85410
85425
  const oneLine = s.replace(/[\r\n\t]+/g, " ");
85411
85426
  if (oneLine.length <= n)
85412
85427
  return oneLine;
@@ -88718,6 +88733,246 @@ function buildSilencePokeOptions(deps) {
88718
88733
  };
88719
88734
  }
88720
88735
 
88736
+ // tool-labels.ts
88737
+ var MAX_LABEL_CHARS = 60;
88738
+ var MAX_BASH_CHARS = 40;
88739
+ var MAX_DESCRIPTION_CHARS = 160;
88740
+ function basename8(p) {
88741
+ if (!p)
88742
+ return "";
88743
+ const parts = p.split("/").filter(Boolean);
88744
+ return parts.length > 0 ? parts[parts.length - 1] : p;
88745
+ }
88746
+ function shortenGrepPath(p) {
88747
+ if (!p)
88748
+ return "repo";
88749
+ const hadTrailingSlash = /\/+$/.test(p);
88750
+ const trimmed = p.replace(/\/+$/, "");
88751
+ const parts = trimmed.split("/").filter(Boolean);
88752
+ if (parts.length === 0)
88753
+ return "repo";
88754
+ const last = parts[parts.length - 1];
88755
+ if (hadTrailingSlash)
88756
+ return `${last}/`;
88757
+ if (last.startsWith(".") && !last.slice(1).includes("."))
88758
+ return last;
88759
+ if (!last.includes("."))
88760
+ return `${last}/`;
88761
+ return last;
88762
+ }
88763
+ function hostFromUrl(u) {
88764
+ if (!u)
88765
+ return "";
88766
+ try {
88767
+ return new URL(u).host;
88768
+ } catch {
88769
+ return truncate4(u);
88770
+ }
88771
+ }
88772
+ function truncate4(s, n = MAX_LABEL_CHARS) {
88773
+ if (s.length <= n)
88774
+ return s;
88775
+ return s.slice(0, n - 1) + "\u2026";
88776
+ }
88777
+ function stripHtml(s) {
88778
+ return s.replace(/<\/?[a-zA-Z][^>]*>/g, "");
88779
+ }
88780
+ function firstLine(s) {
88781
+ const idx = s.indexOf(`
88782
+ `);
88783
+ return idx === -1 ? s : s.slice(0, idx);
88784
+ }
88785
+ function toolLabel(tool, input, preamble, precomputedLabel) {
88786
+ if (precomputedLabel && precomputedLabel.trim().length > 0) {
88787
+ return truncate4(firstLine(precomputedLabel.trim()), MAX_DESCRIPTION_CHARS);
88788
+ }
88789
+ if (!input || typeof input !== "object")
88790
+ return "";
88791
+ const str = (k) => typeof input[k] === "string" ? input[k] : undefined;
88792
+ const preambleLabel = () => {
88793
+ if (!preamble)
88794
+ return null;
88795
+ if (preamble.includes(`
88796
+ `))
88797
+ return null;
88798
+ const trimmed = preamble.trim();
88799
+ if (!trimmed)
88800
+ return null;
88801
+ if (trimmed.length > MAX_DESCRIPTION_CHARS)
88802
+ return null;
88803
+ return trimmed;
88804
+ };
88805
+ switch (tool) {
88806
+ case "Read":
88807
+ case "Write":
88808
+ case "NotebookEdit":
88809
+ case "Edit": {
88810
+ const pre = preambleLabel();
88811
+ if (pre)
88812
+ return pre;
88813
+ return truncate4(basename8(str("file_path") ?? ""));
88814
+ }
88815
+ case "Bash":
88816
+ case "BashOutput": {
88817
+ const description = str("description");
88818
+ if (description)
88819
+ return truncate4(firstLine(description), MAX_DESCRIPTION_CHARS);
88820
+ const pre = preambleLabel();
88821
+ if (pre)
88822
+ return pre;
88823
+ const cmd = str("command") ?? str("bash_id") ?? "";
88824
+ return truncate4(firstLine(cmd), MAX_BASH_CHARS);
88825
+ }
88826
+ case "KillShell":
88827
+ return truncate4(str("shell_id") ?? "");
88828
+ case "Glob": {
88829
+ const pre = preambleLabel();
88830
+ if (pre)
88831
+ return pre;
88832
+ return truncate4(str("pattern") ?? "");
88833
+ }
88834
+ case "Grep": {
88835
+ const pre = preambleLabel();
88836
+ if (pre)
88837
+ return pre;
88838
+ const pat = str("pattern") ?? "";
88839
+ if (!pat)
88840
+ return "";
88841
+ const path2 = str("path");
88842
+ const where = shortenGrepPath(path2 ?? "");
88843
+ return truncate4(`"${pat}" (in ${where})`);
88844
+ }
88845
+ case "WebFetch":
88846
+ return truncate4(hostFromUrl(str("url") ?? ""));
88847
+ case "WebSearch": {
88848
+ const q = str("query") ?? "";
88849
+ return q ? truncate4(`"${q}"`) : "";
88850
+ }
88851
+ case "Task":
88852
+ case "Agent": {
88853
+ const desc = str("description") ?? str("subagent_type") ?? "";
88854
+ return truncate4(desc);
88855
+ }
88856
+ case "TodoWrite":
88857
+ case "TaskCreate":
88858
+ case "TaskUpdate":
88859
+ case "TaskList":
88860
+ case "TaskGet":
88861
+ case "TaskStop":
88862
+ case "TaskOutput":
88863
+ return "";
88864
+ case "Skill":
88865
+ return truncate4(str("skill") ?? "");
88866
+ case "SlashCommand":
88867
+ return truncate4(str("command") ?? "");
88868
+ case "ToolSearch": {
88869
+ const q = str("query") ?? "";
88870
+ if (!q)
88871
+ return "";
88872
+ const selectMatch = q.match(/^\s*select\s*:\s*(.+)$/i);
88873
+ if (selectMatch) {
88874
+ const names = selectMatch[1].split(",").map((n) => n.trim()).filter((n) => n.length > 0).join(", ");
88875
+ return truncate4(`Loading schema: ${names}`);
88876
+ }
88877
+ return truncate4(`Searching tools: ${q}`);
88878
+ }
88879
+ default:
88880
+ if (tool.startsWith("mcp__")) {
88881
+ const description = str("description");
88882
+ if (description)
88883
+ return truncate4(firstLine(stripHtml(description)), MAX_DESCRIPTION_CHARS);
88884
+ const label = mcpBaseLabel(tool);
88885
+ const query3 = str("query") ?? str("text") ?? str("name");
88886
+ if (label && query3) {
88887
+ const budget = Math.max(8, MAX_LABEL_CHARS - label.length - 4);
88888
+ const preview = truncate4(firstLine(stripHtml(query3)), budget);
88889
+ return `${label} (${preview})`;
88890
+ }
88891
+ if (label)
88892
+ return truncate4(label);
88893
+ }
88894
+ for (const k of ["description", "file_path", "path", "url", "query", "pattern", "command"]) {
88895
+ const v = str(k);
88896
+ if (v != null && v.length > 0) {
88897
+ if (k === "file_path" || k === "path")
88898
+ return truncate4(basename8(v));
88899
+ if (k === "url")
88900
+ return truncate4(hostFromUrl(v));
88901
+ if (k === "description")
88902
+ return truncate4(firstLine(v), MAX_DESCRIPTION_CHARS);
88903
+ return truncate4(firstLine(v));
88904
+ }
88905
+ }
88906
+ return "";
88907
+ }
88908
+ }
88909
+ function mcpBaseLabel(tool) {
88910
+ if (!tool.startsWith("mcp__"))
88911
+ return "";
88912
+ const parts = tool.slice("mcp__".length).split("__");
88913
+ if (parts.length < 2)
88914
+ return "";
88915
+ const rawServer = parts[0];
88916
+ const action = parts.slice(1).join("__");
88917
+ if (!rawServer || !action)
88918
+ return "";
88919
+ return `${prettifyServer(rawServer)}: ${action}`;
88920
+ }
88921
+ function prettifyServer(name) {
88922
+ const LABELS = {
88923
+ "switchroom-telegram": "Telegram"
88924
+ };
88925
+ if (LABELS[name])
88926
+ return LABELS[name];
88927
+ if (!name)
88928
+ return name;
88929
+ return name.charAt(0).toUpperCase() + name.slice(1);
88930
+ }
88931
+
88932
+ // gateway/background-shell-liveness.ts
88933
+ var TERMINAL_STATUSES = new Set(["completed", "failed", "killed"]);
88934
+ function applyBackgroundShellLiveness(registry, key, ev) {
88935
+ if (ev.kind === "tool_result") {
88936
+ if (ev.backgroundTaskId != null && ev.backgroundTaskId.length > 0) {
88937
+ registry.noteBackgroundShellAlive(key, ev.backgroundTaskId);
88938
+ }
88939
+ return;
88940
+ }
88941
+ if (ev.kind === "task_notification") {
88942
+ if (ev.taskId.length > 0 && TERMINAL_STATUSES.has(ev.status)) {
88943
+ registry.noteBackgroundShellDead(key, ev.taskId);
88944
+ }
88945
+ return;
88946
+ }
88947
+ if (ev.kind === "tool_use" && ev.toolName === "KillShell") {
88948
+ const killId = ev.input?.shell_id;
88949
+ if (typeof killId === "string" && killId.length > 0) {
88950
+ registry.noteBackgroundShellDead(key, killId);
88951
+ }
88952
+ }
88953
+ }
88954
+
88955
+ // gateway/silence-poke-session-event.ts
88956
+ function applySilencePokeSessionEvent(silencePoke, pendingProgress, key, ev) {
88957
+ if (ev.kind === "thinking") {
88958
+ silencePoke.noteThinking(key, Date.now());
88959
+ } else if (ev.kind === "tool_use") {
88960
+ if (ev.toolUseId != null && ev.toolUseId.length > 0 && !isTelegramSurfaceTool(ev.toolName)) {
88961
+ const label = toolLabel(ev.toolName, ev.input, undefined, ev.precomputedLabel);
88962
+ silencePoke.noteToolStart(key, ev.toolUseId, ev.toolName, label.length > 0 ? label : null, Date.now());
88963
+ const evInput = ev.input;
88964
+ if (ev.toolName === "Agent" || ev.toolName === "Task" || ev.toolName === "Bash" && evInput?.run_in_background === true) {
88965
+ pendingProgress.noteAsyncDispatch(key);
88966
+ }
88967
+ }
88968
+ } else if (ev.kind === "tool_result") {
88969
+ if (ev.toolUseId != null && ev.toolUseId.length > 0) {
88970
+ silencePoke.noteToolEnd(key, ev.toolUseId, Date.now());
88971
+ }
88972
+ }
88973
+ applyBackgroundShellLiveness(silencePoke, key, ev);
88974
+ }
88975
+
88721
88976
  // gateway/inbound-delivery-machine-dispatch.ts
88722
88977
  function dispatchEffects(effects, ctx) {
88723
88978
  for (const effect of effects) {
@@ -94446,10 +94701,38 @@ function chatFromTurnKey(turnKey3) {
94446
94701
  return { chatId, threadId: Number.isFinite(t) ? t : null };
94447
94702
  }
94448
94703
  var OUTBOX_SWEEP_INTERVAL_MS = 5000;
94704
+ function createOutboxSend(deps) {
94705
+ return async (chatId, threadId, text4) => {
94706
+ const bot = deps.getBot();
94707
+ if (bot == null)
94708
+ throw new Error("outbox-sweep: bot unavailable");
94709
+ if (text4.length === 0)
94710
+ return;
94711
+ const replyMarkup = deps.resolveReplyMarkup?.(chatId, threadId, text4);
94712
+ let lastId;
94713
+ const chunkCount = Math.ceil(text4.length / 4000);
94714
+ for (let i = 0, idx = 0;i < text4.length; i += 4000, idx++) {
94715
+ const chunk2 = text4.slice(i, i + 4000);
94716
+ const isLast = idx === chunkCount - 1;
94717
+ const res = await retryWithThreadFallback(deps.retry, (tid) => {
94718
+ const base = tid != null ? { message_thread_id: tid } : {};
94719
+ const opts = isLast && replyMarkup != null ? { ...base, reply_markup: replyMarkup } : base;
94720
+ return bot.api.sendMessage(chatId, chunk2, opts);
94721
+ }, { threadId: threadId ?? undefined, chat_id: chatId, verb: "outbox-sweep.sendMessage" });
94722
+ lastId = res?.message_id;
94723
+ }
94724
+ return lastId;
94725
+ };
94726
+ }
94449
94727
  function startOutboxSweep(deps) {
94450
94728
  if (!deps.isGatewayMain || process.env.SWITCHROOM_TG_OUTBOX_DELIVERY === "0")
94451
94729
  return;
94452
94730
  const retry = createRetryApiCall({ log: deps.log });
94731
+ const send = createOutboxSend({
94732
+ getBot: deps.getBot,
94733
+ retry,
94734
+ ...deps.resolveReplyMarkup != null ? { resolveReplyMarkup: deps.resolveReplyMarkup } : {}
94735
+ });
94453
94736
  const tick3 = () => {
94454
94737
  const bot = deps.getBot();
94455
94738
  if (bot == null)
@@ -94457,15 +94740,7 @@ function startOutboxSweep(deps) {
94457
94740
  sweepOutbox({
94458
94741
  stateDir: deps.stateDir,
94459
94742
  log: deps.log,
94460
- send: async (chatId, threadId, text4) => {
94461
- let lastId;
94462
- for (let i = 0;i < text4.length; i += 4000) {
94463
- const chunk2 = text4.slice(i, i + 4000);
94464
- const res = await retryWithThreadFallback(retry, (tid) => bot.api.sendMessage(chatId, chunk2, tid != null ? { message_thread_id: tid } : {}), { threadId: threadId ?? undefined, chat_id: chatId, verb: "outbox-sweep.sendMessage" });
94465
- lastId = res?.message_id;
94466
- }
94467
- return lastId;
94468
- },
94743
+ send,
94469
94744
  textAlreadyDelivered: (chatId, threadId, text4) => deps.dedupCheck(chatId, threadId ?? undefined, text4),
94470
94745
  registryChainLookup: (taskId) => {
94471
94746
  const db3 = deps.getTurnsDb();
@@ -94483,10 +94758,10 @@ function startOutboxSweep(deps) {
94483
94758
  }
94484
94759
 
94485
94760
  // ../src/build-info.ts
94486
- var VERSION = "0.19.15";
94487
- var COMMIT_SHA = "20691874";
94488
- var COMMIT_DATE = "2026-07-24T12:46:35+10:00";
94489
- var LATEST_PR = 3518;
94761
+ var VERSION = "0.19.16";
94762
+ var COMMIT_SHA = "e905f237";
94763
+ var COMMIT_DATE = "2026-07-24T18:26:03+10:00";
94764
+ var LATEST_PR = null;
94490
94765
  var COMMITS_AHEAD_OF_TAG = 0;
94491
94766
 
94492
94767
  // gateway/boot-version.ts
@@ -100144,7 +100419,7 @@ function runDeliveryConfirmSweep() {
100144
100419
  }
100145
100420
  var _deliveryConfirmSweep = isGatewayMain ? setInterval(runDeliveryConfirmSweep, DELIVERY_CONFIRM_SWEEP_MS) : undefined;
100146
100421
  _deliveryConfirmSweep?.unref?.();
100147
- startOutboxSweep({ isGatewayMain, stateDir: STATE_DIR, getBot: () => bot, getTurnsDb: () => turnsDb, dedupCheck: (c, t, x) => outboundDedup.check(c, t, x, Date.now()) != null, log: (l) => process.stderr.write(l) });
100422
+ startOutboxSweep({ isGatewayMain, stateDir: STATE_DIR, getBot: () => bot, getTurnsDb: () => turnsDb, dedupCheck: (c, t, x) => outboundDedup.check(c, t, x, Date.now()) != null, resolveReplyMarkup: makeOutboxListenMarkupResolver({ resolveVoiceOutPlan: (t) => resolveVoiceOutPlan(loadAccess().voice_out, t), cachePut: (token, payload) => voiceOnDemandCache.put(token, payload), eagerVoiceEnabled, enqueuePreSynth: (j) => voicePreSynthQueue.enqueue(j) }), log: (l) => process.stderr.write(l) });
100148
100423
  if (isGatewayMain)
100149
100424
  startTimer2({
100150
100425
  editMessage: async (ctx) => {
@@ -100617,22 +100892,7 @@ if (isGatewayMain)
100617
100892
  notifyHaltBoundaryWaiters();
100618
100893
  if (currentTurn != null) {
100619
100894
  const key = statusKey(currentTurn.sessionChatId, currentTurn.sessionThreadId);
100620
- if (ev.kind === "thinking") {
100621
- noteThinking(key, Date.now());
100622
- } else if (ev.kind === "tool_use") {
100623
- if (ev.toolUseId != null && ev.toolUseId.length > 0 && !isTelegramSurfaceTool2(ev.toolName)) {
100624
- const label = toolLabel(ev.toolName, ev.input, undefined, ev.precomputedLabel);
100625
- noteToolStart(key, ev.toolUseId, ev.toolName, label.length > 0 ? label : null, Date.now());
100626
- const evInput = ev.input;
100627
- if (ev.toolName === "Agent" || ev.toolName === "Task" || ev.toolName === "Bash" && evInput?.run_in_background === true) {
100628
- noteAsyncDispatch(key);
100629
- }
100630
- }
100631
- } else if (ev.kind === "tool_result") {
100632
- if (ev.toolUseId != null && ev.toolUseId.length > 0) {
100633
- noteToolEnd(key, ev.toolUseId, Date.now());
100634
- }
100635
- }
100895
+ applySilencePokeSessionEvent(exports_silence_poke, exports_pending_work_progress, key, ev);
100636
100896
  }
100637
100897
  },
100638
100898
  onPermissionRequest(_client, msg) {
@@ -107131,7 +107391,7 @@ ${labelWithResume}` : labelWithResume,
107131
107391
  handleChecklistUpdate(ctx, "checklist_tasks_added", checklistHandlerDeps);
107132
107392
  });
107133
107393
  bot2.on("message:pinned_message", (ctx) => handlePinnedMessage(ctx, pinnedMessageHandlerDeps));
107134
- bot2.on("message:rich_message", (ctx) => handleRichMessageMessage(ctx, mediaEnvelopeDeps));
107394
+ bot2.on("message:rich_message", (ctx) => handleRichMessageMessage(ctx, { ...mediaEnvelopeDeps, handleInbound: handleInboundCoalesced }));
107135
107395
  installUnhandledMessageCatchAll(bot2, (ctx, text5) => routeInbound(ctx, text5, undefined, undefined, inboundRouterDeps), (line) => process.stderr.write(line));
107136
107396
  bot2.on("message_reaction", (ctx) => {
107137
107397
  handleMessageReaction(ctx);