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.
- package/dist/cli/switchroom.js +38 -7
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/telegram-plugin/answer-stream.ts +37 -3
- package/telegram-plugin/dist/gateway/gateway.js +255 -71
- package/telegram-plugin/gateway/feed-open-gate.ts +54 -1
- package/telegram-plugin/gateway/gateway.ts +176 -86
- package/telegram-plugin/gateway/turn-typing-loop.ts +102 -0
- package/telegram-plugin/tests/answer-stream.test.ts +118 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +21 -6
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +46 -40
- package/telegram-plugin/tests/feed-open-gate.test.ts +75 -1
- package/telegram-plugin/tests/tool-activity-summary.test.ts +20 -0
- package/telegram-plugin/tests/turn-typing-loop.test.ts +124 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -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.
|
|
51696
|
-
var COMMIT_SHA = "
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
22590
|
+
var VERSION = "0.16.17";
|
|
22591
22591
|
|
|
22592
22592
|
// src/cli/resolve-version.ts
|
|
22593
22593
|
function readPackageVersion() {
|
package/package.json
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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("&");
|
|
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("<");
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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.
|
|
56077
|
-
var COMMIT_SHA = "
|
|
56078
|
-
var COMMIT_DATE = "2026-06-
|
|
56079
|
-
var LATEST_PR =
|
|
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 :
|
|
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").
|
|
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
|
|
58987
|
-
|
|
58988
|
-
|
|
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
|
-
|
|
58995
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69514
|
-
|
|
69515
|
-
|
|
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();
|