switchroom 0.16.15 → 0.16.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.
@@ -51692,8 +51692,8 @@ import { existsSync, readFileSync } from "node:fs";
51692
51692
  import { dirname, join } from "node:path";
51693
51693
 
51694
51694
  // src/build-info.ts
51695
- var VERSION = "0.16.15";
51696
- var COMMIT_SHA = "a9c59169";
51695
+ var VERSION = "0.16.17";
51696
+ var COMMIT_SHA = "de202ab1";
51697
51697
 
51698
51698
  // src/cli/resolve-version.ts
51699
51699
  function readPackageVersion() {
@@ -77596,11 +77596,19 @@ async function injectInbound(agentsDir, agentName, chatId, threadId, text, promp
77596
77596
  });
77597
77597
  }
77598
77598
  async function handleHermesRest(method, pathname, config) {
77599
- if (method === "GET" && pathname === "/api/sessions") {
77599
+ if (method === "GET" && (pathname === "/api/sessions" || pathname === "/api/profiles/sessions")) {
77600
77600
  const agents = await handleGetAgents(config);
77601
- const agentsDir = resolveAgentsDir(config);
77602
77601
  const sessions = agents.map((a) => toHermesSession(a, agentLiveness(config, a.name)));
77603
- return { status: 200, body: { sessions } };
77602
+ return {
77603
+ status: 200,
77604
+ body: {
77605
+ sessions,
77606
+ total: sessions.length,
77607
+ limit: sessions.length,
77608
+ offset: 0,
77609
+ profile_totals: { default: sessions.length }
77610
+ }
77611
+ };
77604
77612
  }
77605
77613
  const sessionMatch = pathname.match(/^\/api\/sessions\/([^/]+)$/);
77606
77614
  if (method === "GET" && sessionMatch) {
@@ -77639,15 +77647,38 @@ async function handleHermesRest(method, pathname, config) {
77639
77647
  };
77640
77648
  }
77641
77649
  if (method === "GET" && pathname === "/api/config") {
77642
- const agentNames = Object.keys(config.agents ?? {});
77643
77650
  return {
77644
77651
  status: 200,
77645
77652
  body: {
77646
77653
  provider: "switchroom",
77647
- agents: agentNames
77654
+ model: null,
77655
+ context_length: null,
77656
+ system_prompt: null
77648
77657
  }
77649
77658
  };
77650
77659
  }
77660
+ if (method === "GET" && (pathname === "/api/config/defaults" || pathname === "/api/config/schema")) {
77661
+ return { status: 200, body: {} };
77662
+ }
77663
+ if (method === "GET" && pathname === "/api/model/info") {
77664
+ return {
77665
+ status: 200,
77666
+ body: {
77667
+ model: "claude",
77668
+ provider: "switchroom",
77669
+ capabilities: {}
77670
+ }
77671
+ };
77672
+ }
77673
+ if (method === "GET" && pathname.startsWith("/api/logs")) {
77674
+ return { status: 200, body: { file: "gateway.log", lines: [] } };
77675
+ }
77676
+ if (method === "GET" && (pathname.startsWith("/api/cron") || pathname.startsWith("/api/messaging") || pathname.startsWith("/api/profiles") || pathname === "/api/memory/providers")) {
77677
+ if (pathname.includes("sessions")) {
77678
+ return { status: 200, body: { sessions: [], total: 0, limit: 0, offset: 0 } };
77679
+ }
77680
+ return { status: 200, body: {} };
77681
+ }
77651
77682
  return null;
77652
77683
  }
77653
77684
  function sendEvent(ctx, type, sessionId, payload) {
@@ -22587,7 +22587,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
22587
22587
  import { dirname as dirname4, join as join2 } from "node:path";
22588
22588
 
22589
22589
  // src/build-info.ts
22590
- var VERSION = "0.16.15";
22590
+ var VERSION = "0.16.17";
22591
22591
 
22592
22592
  // src/cli/resolve-version.ts
22593
22593
  function readPackageVersion() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.16.15",
3
+ "version": "0.16.16",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -93,6 +93,24 @@ export interface AnswerStreamConfig {
93
93
  ) => Promise<unknown>
94
94
  deleteMessage?: (chatId: string, messageId: number) => Promise<unknown>
95
95
 
96
+ /**
97
+ * Render the raw assistant transcript text into the wire format used for
98
+ * `parse_mode: 'HTML'` sends/edits. Every OTHER outbound lane in the gateway
99
+ * (handleStreamReply, the reply handler, the turn-flush backstop, the PTY
100
+ * partial handler) converts markdown → Telegram HTML via
101
+ * `sanitizeTelegramHtml(markdownToHtml(...))` before sending. The
102
+ * answer-stream lane historically shipped the RAW transcript under
103
+ * `parse_mode: 'HTML'`, so `**bold**` reached the user as literal asterisks
104
+ * and agent narration read as unformatted text.
105
+ *
106
+ * Injected as a dependency (mirroring `renderText` on the PTY partial
107
+ * handler) rather than hard-importing `format`/`html-sanitize` here, so this
108
+ * module stays free of a grammy/format dependency and remains fully
109
+ * testable. When absent, text is sent verbatim — preserving the old
110
+ * behaviour for callers (and tests) that don't wire it.
111
+ */
112
+ renderText?: (text: string) => string
113
+
96
114
  /** Called when a late edit/send resolves but this stream has been superseded. */
97
115
  onSuperseded?: OnSupersededCallback
98
116
  log?: (msg: string) => void
@@ -179,6 +197,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
179
197
  replyToMessageId,
180
198
  sendMessage,
181
199
  editMessageText,
200
+ renderText,
182
201
  onSuperseded,
183
202
  log,
184
203
  warn,
@@ -188,6 +207,13 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
188
207
  recordOutbound,
189
208
  } = config
190
209
 
210
+ /**
211
+ * Convert raw transcript text to the wire format before any
212
+ * `parse_mode: 'HTML'` send/edit. Falls back to the verbatim text when no
213
+ * renderer is injected (old behaviour / unwired tests).
214
+ */
215
+ const render = (text: string): string => (renderText != null ? renderText(text) : text)
216
+
191
217
  const effectiveThrottle = Math.max(250, throttleMs)
192
218
 
193
219
  // Stream state
@@ -229,6 +255,10 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
229
255
  }
230
256
 
231
257
  async function sendOrEditViaMessage(trimmed: string, gen: number, prevText: string): Promise<void> {
258
+ // Convert raw transcript markdown → Telegram HTML before sending under
259
+ // parse_mode: 'HTML'. Without this, `**bold**` ships as literal asterisks
260
+ // (the answer-stream-raw-markdown bug). Mirrors every other outbound lane.
261
+ const rendered = render(trimmed)
232
262
  if (typeof streamMsgId === 'number') {
233
263
  // Edit existing message
234
264
  const editParams: Parameters<typeof editMessageText>[3] = {
@@ -237,7 +267,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
237
267
  }
238
268
  if (threadId != null) editParams.message_thread_id = threadId
239
269
  try {
240
- await editMessageText(chatId, streamMsgId, trimmed, editParams)
270
+ await editMessageText(chatId, streamMsgId, rendered, editParams)
241
271
  onMetric?.({ kind: 'answer_lane_update', chatId, messageId: streamMsgId, charCount: trimmed.length, transport: 'edit' })
242
272
  } catch (err) {
243
273
  const msg = err instanceof Error ? err.message : String(err)
@@ -266,7 +296,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
266
296
  }
267
297
  if (threadId != null) sendParams.message_thread_id = threadId
268
298
  if (replyToMessageId != null) sendParams.reply_parameters = { message_id: replyToMessageId }
269
- const sent = await sendMessage(chatId, trimmed, sendParams)
299
+ const sent = await sendMessage(chatId, rendered, sendParams)
270
300
  const sentId = sent?.message_id
271
301
  if (typeof sentId !== 'number' || !Number.isFinite(sentId)) {
272
302
  warn?.('answer-stream: sendMessage returned no message_id')
@@ -424,7 +454,11 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
424
454
  // nested quote that looks wrong.
425
455
 
426
456
  try {
427
- const sent = await sendMessage(chatId, textToSend, sendParams)
457
+ // Render markdown Telegram HTML for the wire send. The dedup /
458
+ // silent-marker / history checks above all run on the raw textToSend
459
+ // (so they match the comparisons the other lanes make on raw text);
460
+ // only the actual outbound payload is converted.
461
+ const sent = await sendMessage(chatId, render(textToSend), sendParams)
428
462
  const sentId = sent?.message_id
429
463
  if (typeof sentId === 'number' && Number.isFinite(sentId)) {
430
464
  streamMsgId = sentId
@@ -34032,6 +34032,40 @@ function createTypingWrapper(deps) {
34032
34032
  };
34033
34033
  }
34034
34034
 
34035
+ // gateway/turn-typing-loop.ts
34036
+ function createTurnTypingLoop(deps) {
34037
+ const refreshMs = deps.refreshMs ?? 4000;
34038
+ const intervals = new Map;
34039
+ function stop(chatId, threadId = null) {
34040
+ const key = deps.chatKey(chatId, threadId);
34041
+ const iv = intervals.get(key);
34042
+ if (iv != null) {
34043
+ clearInterval(iv);
34044
+ intervals.delete(key);
34045
+ }
34046
+ }
34047
+ function start(chatId, threadId = null) {
34048
+ stop(chatId, threadId);
34049
+ const key = deps.chatKey(chatId, threadId);
34050
+ const send = () => deps.sendChatAction(chatId, threadId);
34051
+ send();
34052
+ const iv = setInterval(send, refreshMs);
34053
+ iv.unref?.();
34054
+ intervals.set(key, iv);
34055
+ }
34056
+ function stopAll() {
34057
+ for (const iv of [...intervals.values()])
34058
+ clearInterval(iv);
34059
+ intervals.clear();
34060
+ }
34061
+ return {
34062
+ start,
34063
+ stop,
34064
+ stopAll,
34065
+ activeCount: () => intervals.size
34066
+ };
34067
+ }
34068
+
34035
34069
  // draft-stream.ts
34036
34070
  var TELEGRAM_MAX_CHARS = 4096;
34037
34071
  var DEFAULT_DM_THROTTLE_MS = 400;
@@ -40362,6 +40396,7 @@ function createAnswerStream(config) {
40362
40396
  replyToMessageId,
40363
40397
  sendMessage,
40364
40398
  editMessageText,
40399
+ renderText,
40365
40400
  onSuperseded,
40366
40401
  log,
40367
40402
  warn,
@@ -40370,6 +40405,7 @@ function createAnswerStream(config) {
40370
40405
  recordDedup,
40371
40406
  recordOutbound
40372
40407
  } = config;
40408
+ const render = (text) => renderText != null ? renderText(text) : text;
40373
40409
  const effectiveThrottle = Math.max(250, throttleMs);
40374
40410
  let streamMsgId;
40375
40411
  let pendingText = null;
@@ -40404,6 +40440,7 @@ function createAnswerStream(config) {
40404
40440
  }
40405
40441
  }
40406
40442
  async function sendOrEditViaMessage(trimmed, gen, prevText) {
40443
+ const rendered = render(trimmed);
40407
40444
  if (typeof streamMsgId === "number") {
40408
40445
  const editParams = {
40409
40446
  parse_mode: "HTML",
@@ -40412,7 +40449,7 @@ function createAnswerStream(config) {
40412
40449
  if (threadId != null)
40413
40450
  editParams.message_thread_id = threadId;
40414
40451
  try {
40415
- await editMessageText(chatId, streamMsgId, trimmed, editParams);
40452
+ await editMessageText(chatId, streamMsgId, rendered, editParams);
40416
40453
  onMetric?.({ kind: "answer_lane_update", chatId, messageId: streamMsgId, charCount: trimmed.length, transport: "edit" });
40417
40454
  } catch (err) {
40418
40455
  const msg = err instanceof Error ? err.message : String(err);
@@ -40437,7 +40474,7 @@ function createAnswerStream(config) {
40437
40474
  sendParams.message_thread_id = threadId;
40438
40475
  if (replyToMessageId != null)
40439
40476
  sendParams.reply_parameters = { message_id: replyToMessageId };
40440
- const sent = await sendMessage(chatId, trimmed, sendParams);
40477
+ const sent = await sendMessage(chatId, rendered, sendParams);
40441
40478
  const sentId = sent?.message_id;
40442
40479
  if (typeof sentId !== "number" || !Number.isFinite(sentId)) {
40443
40480
  warn?.("answer-stream: sendMessage returned no message_id");
@@ -40552,7 +40589,7 @@ function createAnswerStream(config) {
40552
40589
  if (threadId != null)
40553
40590
  sendParams.message_thread_id = threadId;
40554
40591
  try {
40555
- const sent = await sendMessage(chatId, textToSend, sendParams);
40592
+ const sent = await sendMessage(chatId, render(textToSend), sendParams);
40556
40593
  const sentId = sent?.message_id;
40557
40594
  if (typeof sentId === "number" && Number.isFinite(sentId)) {
40558
40595
  streamMsgId = sentId;
@@ -43708,6 +43745,131 @@ function getOpenTags(html) {
43708
43745
  return tagStack;
43709
43746
  }
43710
43747
 
43748
+ // html-sanitize.ts
43749
+ var ALLOWED_TAGS2 = new Set([
43750
+ "b",
43751
+ "strong",
43752
+ "i",
43753
+ "em",
43754
+ "u",
43755
+ "ins",
43756
+ "s",
43757
+ "strike",
43758
+ "del",
43759
+ "a",
43760
+ "code",
43761
+ "pre",
43762
+ "span",
43763
+ "tg-spoiler",
43764
+ "tg-emoji",
43765
+ "blockquote"
43766
+ ]);
43767
+ var ALLOWED_ATTRS2 = {
43768
+ a: new Set(["href"]),
43769
+ code: new Set(["class"]),
43770
+ span: new Set(["class"]),
43771
+ "tg-emoji": new Set(["emoji-id"]),
43772
+ blockquote: new Set(["expandable"]),
43773
+ pre: new Set(["language"])
43774
+ };
43775
+ var ALLOWED_HREF_SCHEMES2 = /^(?:https?|mailto|tel|tg):/i;
43776
+ function escapeAllHtml2(text) {
43777
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
43778
+ }
43779
+ function sanitizeTelegramHtml2(input) {
43780
+ const out = [];
43781
+ const stack = [];
43782
+ let i = 0;
43783
+ const len = input.length;
43784
+ while (i < len) {
43785
+ const ch = input[i];
43786
+ if (ch === "&") {
43787
+ const m = /^&(?:#\d+|#x[0-9a-f]+|[a-z]+);/i.exec(input.slice(i, i + 12));
43788
+ if (m) {
43789
+ out.push(m[0]);
43790
+ i += m[0].length;
43791
+ } else {
43792
+ out.push("&amp;");
43793
+ i++;
43794
+ }
43795
+ continue;
43796
+ }
43797
+ if (ch !== "<") {
43798
+ out.push(ch);
43799
+ i++;
43800
+ continue;
43801
+ }
43802
+ const tagMatch = /^<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*)>/.exec(input.slice(i));
43803
+ if (!tagMatch) {
43804
+ out.push("&lt;");
43805
+ i++;
43806
+ continue;
43807
+ }
43808
+ const isClose = tagMatch[1] === "/";
43809
+ const tagName = tagMatch[2].toLowerCase();
43810
+ const attrText = tagMatch[3];
43811
+ if (!ALLOWED_TAGS2.has(tagName)) {
43812
+ out.push(escapeAllHtml2(tagMatch[0]));
43813
+ i += tagMatch[0].length;
43814
+ continue;
43815
+ }
43816
+ if (isClose) {
43817
+ const idx = stack.lastIndexOf(tagName);
43818
+ if (idx === -1) {
43819
+ i += tagMatch[0].length;
43820
+ continue;
43821
+ }
43822
+ while (stack.length > idx + 1) {
43823
+ const top = stack.pop();
43824
+ out.push(`</${top}>`);
43825
+ }
43826
+ stack.pop();
43827
+ out.push(`</${tagName}>`);
43828
+ i += tagMatch[0].length;
43829
+ continue;
43830
+ }
43831
+ const cleanAttrs = sanitizeAttrs2(tagName, attrText);
43832
+ out.push(`<${tagName}${cleanAttrs}>`);
43833
+ stack.push(tagName);
43834
+ i += tagMatch[0].length;
43835
+ }
43836
+ while (stack.length > 0) {
43837
+ const top = stack.pop();
43838
+ out.push(`</${top}>`);
43839
+ }
43840
+ return out.join("");
43841
+ }
43842
+ function sanitizeAttrs2(tagName, attrText) {
43843
+ const allowed = ALLOWED_ATTRS2[tagName];
43844
+ if (!allowed || allowed.size === 0)
43845
+ return "";
43846
+ const attrRe = /([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
43847
+ const kept = [];
43848
+ let m;
43849
+ while ((m = attrRe.exec(attrText)) != null) {
43850
+ const name = m[1].toLowerCase();
43851
+ if (!allowed.has(name))
43852
+ continue;
43853
+ const rawValue = m[2] ?? m[3] ?? m[4] ?? "";
43854
+ if (tagName === "a" && name === "href") {
43855
+ const trimmed = rawValue.trim();
43856
+ if (!ALLOWED_HREF_SCHEMES2.test(trimmed))
43857
+ continue;
43858
+ kept.push(`href="${escapeAttrValue2(trimmed)}"`);
43859
+ continue;
43860
+ }
43861
+ if (rawValue.length === 0) {
43862
+ kept.push(name);
43863
+ continue;
43864
+ }
43865
+ kept.push(`${name}="${escapeAttrValue2(rawValue)}"`);
43866
+ }
43867
+ return kept.length > 0 ? " " + kept.join(" ") : "";
43868
+ }
43869
+ function escapeAttrValue2(v) {
43870
+ return v.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
43871
+ }
43872
+
43711
43873
  // text-voice-scrub.ts
43712
43874
  var NULL = "\x00";
43713
43875
  var FENCE_PH = `${NULL}VS_FENCE`;
@@ -50477,6 +50639,17 @@ function decideFeedReopen(input) {
50477
50639
  }
50478
50640
 
50479
50641
  // gateway/feed-open-gate.ts
50642
+ function shouldEarlyOpenLiveness(input) {
50643
+ if (!input.enabled)
50644
+ return false;
50645
+ if (input.sessionChatId == null)
50646
+ return false;
50647
+ if (input.activityMessageId != null)
50648
+ return false;
50649
+ if (input.ageMs < input.thresholdMs)
50650
+ return false;
50651
+ return true;
50652
+ }
50480
50653
  function mayOpenActivityCard(input) {
50481
50654
  if (input.crossTurnAnswerDelivered)
50482
50655
  return false;
@@ -56073,10 +56246,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
56073
56246
  }
56074
56247
 
56075
56248
  // ../src/build-info.ts
56076
- var VERSION = "0.16.15";
56077
- var COMMIT_SHA = "a9c59169";
56078
- var COMMIT_DATE = "2026-06-28T22:48:22Z";
56079
- var LATEST_PR = 2643;
56249
+ var VERSION = "0.16.17";
56250
+ var COMMIT_SHA = "de202ab1";
56251
+ var COMMIT_DATE = "2026-06-29T02:55:27Z";
56252
+ var LATEST_PR = 2652;
56080
56253
  var COMMITS_AHEAD_OF_TAG = 0;
56081
56254
 
56082
56255
  // gateway/boot-version.ts
@@ -58076,7 +58249,7 @@ var FEED_LIVENESS_OPEN_ENABLED = process.env.SWITCHROOM_FEED_LIVENESS_OPEN !== "
58076
58249
  var FEED_LIVENESS_OPEN_MS = (() => {
58077
58250
  const raw = process.env.SWITCHROOM_FEED_LIVENESS_OPEN_MS;
58078
58251
  const n = raw ? Number(raw) : NaN;
58079
- return Number.isFinite(n) && n > 0 ? n : 12000;
58252
+ return Number.isFinite(n) && n > 0 ? n : 1200;
58080
58253
  })();
58081
58254
  var POST_ANSWER_LIVENESS_STALE_MS = parsePostAnswerLivenessMs(process.env.SWITCHROOM_POST_ANSWER_LIVENESS_STALE_MS) || 30000;
58082
58255
  function formatFeedElapsed3(ms) {
@@ -58484,6 +58657,7 @@ function purgeReactionTracking(key, endingTurn) {
58484
58657
  const threadId = threadPart === "_" || threadPart === "" ? null : Number(threadPart);
58485
58658
  stopTurnTypingLoop(chatId, Number.isFinite(threadId) ? threadId : null);
58486
58659
  }
58660
+ stopEarlyLivenessOpen(key);
58487
58661
  if (msgInfo) {
58488
58662
  const agentDir = resolveAgentDirFromEnv();
58489
58663
  if (agentDir != null)
@@ -58682,34 +58856,13 @@ function maybeIdleClear() {
58682
58856
  idleClearDispatching = true;
58683
58857
  process.stderr.write(`telegram gateway: idle auto-/clear for ${agentName3} (idle >= ${Math.round(idleClearMs / 60000)}m)
58684
58858
  `);
58685
- injectSlashCommand(agentName3, "/clear").then(() => {
58686
- postIdleClearNotice(idleClearMs);
58687
- }).catch((err) => {
58859
+ injectSlashCommand(agentName3, "/clear").catch((err) => {
58688
58860
  process.stderr.write(`telegram gateway: idle /clear inject failed for ${agentName3}: ${err instanceof Error ? err.message : String(err)}
58689
58861
  `);
58690
58862
  }).finally(() => {
58691
58863
  idleClearDispatching = false;
58692
58864
  });
58693
58865
  }
58694
- async function postIdleClearNotice(idleClearMs) {
58695
- try {
58696
- const chatId = loadAccess().allowFrom[0];
58697
- if (!chatId)
58698
- return;
58699
- const threadId = topicForRecipient({
58700
- recipientChatId: chatId,
58701
- resolvedTopic: resolveAgentOutboundTopic({ kind: "compact-watchdog" }) ?? chatThreadMap.get(chatId),
58702
- supergroupChatId: resolveAgentSupergroupChatId()
58703
- });
58704
- const hrs = Math.round(idleClearMs / 3600000 * 10) / 10;
58705
- const text2 = `\uD83E\uDDF9 <b>Cleared after ${hrs}h idle</b> \u2014 fresh slate next message; ` + `long-term memory is in Hindsight.`;
58706
- await swallowingApiCall(() => bot.api.sendMessage(chatId, text2, {
58707
- parse_mode: "HTML",
58708
- disable_notification: true,
58709
- ...threadId != null ? { message_thread_id: threadId } : {}
58710
- }), { chat_id: chatId, verb: "idleAutoClear.notice" });
58711
- } catch {}
58712
- }
58713
58866
  async function postCompactCard(occ, cap) {
58714
58867
  try {
58715
58868
  const chatId = loadAccess().allowFrom[0];
@@ -58983,24 +59136,18 @@ function stopTypingLoop(chat_id, thread_id = null) {
58983
59136
  typingRetryTimers.delete(key);
58984
59137
  }
58985
59138
  }
58986
- var turnTypingIntervals = new Map;
58987
- function startTurnTypingLoop(chat_id, thread_id = null) {
58988
- stopTurnTypingLoop(chat_id, thread_id);
58989
- const key = chatKey2(chat_id, thread_id);
58990
- const sendOpts = thread_id != null ? { message_thread_id: thread_id } : undefined;
58991
- const send = () => {
59139
+ var turnTypingLoop = createTurnTypingLoop({
59140
+ sendChatAction: (chat_id, thread_id) => {
59141
+ const sendOpts = thread_id != null ? { message_thread_id: thread_id } : undefined;
58992
59142
  bot.api.sendChatAction(chat_id, "typing", sendOpts).catch(() => {});
58993
- };
58994
- send();
58995
- turnTypingIntervals.set(key, setInterval(send, 4000));
59143
+ },
59144
+ chatKey: (chat_id, thread_id) => chatKey2(chat_id, thread_id)
59145
+ });
59146
+ function startTurnTypingLoop(chat_id, thread_id = null) {
59147
+ turnTypingLoop.start(chat_id, thread_id);
58996
59148
  }
58997
59149
  function stopTurnTypingLoop(chat_id, thread_id = null) {
58998
- const key = chatKey2(chat_id, thread_id);
58999
- const iv = turnTypingIntervals.get(key);
59000
- if (iv) {
59001
- clearInterval(iv);
59002
- turnTypingIntervals.delete(key);
59003
- }
59150
+ turnTypingLoop.stop(chat_id, thread_id);
59004
59151
  }
59005
59152
  var typingWrapper = createTypingWrapper({
59006
59153
  startTypingLoop,
@@ -62619,6 +62766,62 @@ async function drainActivitySummary(turn, producer = "tool", openFlags) {
62619
62766
  turn.activityInFlight = null;
62620
62767
  }
62621
62768
  }
62769
+ function openLivenessFeedIfDue(turn) {
62770
+ const age = Date.now() - turn.startedAt;
62771
+ if (!shouldEarlyOpenLiveness({
62772
+ enabled: FEED_LIVENESS_OPEN_ENABLED,
62773
+ ageMs: age,
62774
+ thresholdMs: FEED_LIVENESS_OPEN_MS,
62775
+ mirrorLineCount: turn.mirrorLines.length,
62776
+ activityMessageId: turn.activityMessageId,
62777
+ sessionChatId: turn.sessionChatId
62778
+ }))
62779
+ return;
62780
+ const lines = turn.mirrorLines.length > 0 ? turn.mirrorLines : ["Working\u2026"];
62781
+ const livenessHeader = {
62782
+ label: "Agent",
62783
+ elapsedMs: age,
62784
+ toolCount: turn.labeledToolCount,
62785
+ state: "running"
62786
+ };
62787
+ const rendered = renderActivityFeedWithNested(lines, [], false, ` \xB7 ${formatFeedElapsed3(age)}`, undefined, livenessHeader);
62788
+ if (rendered == null)
62789
+ return;
62790
+ turn.activityPendingRender = rendered;
62791
+ const ea = emissionAuthorityFor(turn);
62792
+ cardDrainGate(turn, ea, () => {
62793
+ if (ea.mayDrain(turn)) {
62794
+ ea.openOrEditCard("liveness", () => {
62795
+ turn.activityInFlight = drainActivitySummary(turn, "liveness");
62796
+ });
62797
+ }
62798
+ });
62799
+ }
62800
+ var earlyLivenessOpenTimers = new Map;
62801
+ function scheduleEarlyLivenessOpen(turn) {
62802
+ if (STATIC || !FEED_HEARTBEAT_ENABLED || !FEED_LIVENESS_OPEN_ENABLED)
62803
+ return;
62804
+ if (turn.sessionChatId == null)
62805
+ return;
62806
+ const key = statusKey(turn.sessionChatId, turn.sessionThreadId);
62807
+ stopEarlyLivenessOpen(key);
62808
+ const t = setTimeout(() => {
62809
+ earlyLivenessOpenTimers.delete(key);
62810
+ const live = currentTurnMap.get(key);
62811
+ if (live == null || live.turnId !== turn.turnId)
62812
+ return;
62813
+ openLivenessFeedIfDue(live);
62814
+ }, FEED_LIVENESS_OPEN_MS);
62815
+ t.unref?.();
62816
+ earlyLivenessOpenTimers.set(key, t);
62817
+ }
62818
+ function stopEarlyLivenessOpen(key) {
62819
+ const t = earlyLivenessOpenTimers.get(key);
62820
+ if (t != null) {
62821
+ clearTimeout(t);
62822
+ earlyLivenessOpenTimers.delete(key);
62823
+ }
62824
+ }
62622
62825
  function feedHeartbeatTick() {
62623
62826
  const turn = currentTurn;
62624
62827
  if (turn == null)
@@ -62659,29 +62862,7 @@ function feedHeartbeatTick() {
62659
62862
  return;
62660
62863
  }
62661
62864
  if (turn.mirrorLines.length === 0) {
62662
- if (!FEED_LIVENESS_OPEN_ENABLED || turn.sessionChatId == null)
62663
- return;
62664
- const age = Date.now() - turn.startedAt;
62665
- if (age < FEED_LIVENESS_OPEN_MS)
62666
- return;
62667
- const livenessHeader = {
62668
- label: "Agent",
62669
- elapsedMs: age,
62670
- toolCount: 0,
62671
- state: "running"
62672
- };
62673
- const rendered2 = renderActivityFeedWithNested(["Working\u2026"], [], false, ` \xB7 ${formatFeedElapsed3(age)}`, undefined, livenessHeader);
62674
- if (rendered2 == null)
62675
- return;
62676
- turn.activityPendingRender = rendered2;
62677
- const ea2 = emissionAuthorityFor(turn);
62678
- cardDrainGate(turn, ea2, () => {
62679
- if (ea2.mayDrain(turn)) {
62680
- ea2.openOrEditCard("liveness", () => {
62681
- turn.activityInFlight = drainActivitySummary(turn, "liveness");
62682
- });
62683
- }
62684
- });
62865
+ openLivenessFeedIfDue(turn);
62685
62866
  return;
62686
62867
  }
62687
62868
  if (turn.activityMessageId == null)
@@ -62813,6 +62994,7 @@ function handleSessionEvent(ev) {
62813
62994
  };
62814
62995
  setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
62815
62996
  markIdleActivity();
62997
+ scheduleEarlyLivenessOpen(next);
62816
62998
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
62817
62999
  `);
62818
63000
  rememberRecentTurn(next);
@@ -62951,6 +63133,7 @@ function handleSessionEvent(ev) {
62951
63133
  chatId: turn.sessionChatId,
62952
63134
  threadId: turn.sessionThreadId,
62953
63135
  minInitialChars: ANSWER_LANE.minInitialChars,
63136
+ renderText: (text2) => sanitizeTelegramHtml2(markdownToHtml(text2)),
62954
63137
  sendMessage: async (chatId, text2, params) => {
62955
63138
  const tid = params?.message_thread_id;
62956
63139
  const silent = params?.purpose !== "materialize";
@@ -69510,9 +69693,10 @@ async function shutdown(signal) {
69510
69693
  for (const iv of [...typingIntervals.values()])
69511
69694
  clearInterval(iv);
69512
69695
  typingIntervals.clear();
69513
- for (const iv of [...turnTypingIntervals.values()])
69514
- clearInterval(iv);
69515
- turnTypingIntervals.clear();
69696
+ turnTypingLoop.stopAll();
69697
+ for (const t of [...earlyLivenessOpenTimers.values()])
69698
+ clearTimeout(t);
69699
+ earlyLivenessOpenTimers.clear();
69516
69700
  for (const t of [...typingRetryTimers.values()])
69517
69701
  clearTimeout(t);
69518
69702
  typingRetryTimers.clear();