switchroom 0.16.14 → 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 +260 -9
- 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 +265 -72
- package/telegram-plugin/gateway/feed-open-gate.ts +54 -1
- package/telegram-plugin/gateway/gateway.ts +176 -86
- package/telegram-plugin/gateway/model-command.ts +22 -1
- package/telegram-plugin/gateway/turn-typing-loop.ts +102 -0
- package/telegram-plugin/permission-title.ts +1 -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/model-command.test.ts +7 -1
- package/telegram-plugin/tests/tool-activity-summary.test.ts +20 -0
- package/telegram-plugin/tests/turn-typing-loop.test.ts +124 -0
|
@@ -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`;
|
|
@@ -46272,7 +46434,7 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
46272
46434
|
`).map((l) => l.trim()).find((l) => /set model|switched/i.test(l)) ?? `Switched to ${friendlyName} (session)`;
|
|
46273
46435
|
return {
|
|
46274
46436
|
answer: confirmation,
|
|
46275
|
-
reply: await
|
|
46437
|
+
reply: await menuWithBannerStatic(deps, `\u2705 ${deps.escapeHtml(confirmation)}`),
|
|
46276
46438
|
selectedModel: srName
|
|
46277
46439
|
};
|
|
46278
46440
|
}
|
|
@@ -46334,6 +46496,14 @@ async function menuWithBanner(deps, banner) {
|
|
|
46334
46496
|
...fresh.keyboard ? { keyboard: fresh.keyboard } : {}
|
|
46335
46497
|
};
|
|
46336
46498
|
}
|
|
46499
|
+
async function menuWithBannerStatic(deps, banner) {
|
|
46500
|
+
const v1 = await handleModelCommand({ kind: "show" }, deps);
|
|
46501
|
+
return {
|
|
46502
|
+
text: [banner, "", v1.text].join(`
|
|
46503
|
+
`),
|
|
46504
|
+
html: true
|
|
46505
|
+
};
|
|
46506
|
+
}
|
|
46337
46507
|
|
|
46338
46508
|
// ../src/agents/model-picker.ts
|
|
46339
46509
|
var HEADER_RE = /Select model/;
|
|
@@ -50469,6 +50639,17 @@ function decideFeedReopen(input) {
|
|
|
50469
50639
|
}
|
|
50470
50640
|
|
|
50471
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
|
+
}
|
|
50472
50653
|
function mayOpenActivityCard(input) {
|
|
50473
50654
|
if (input.crossTurnAnswerDelivered)
|
|
50474
50655
|
return false;
|
|
@@ -54753,6 +54934,7 @@ var ARG_SUMMARY_LINE_MAX = 180;
|
|
|
54753
54934
|
var MCP_TOOL_DESCRIPTIONS = {
|
|
54754
54935
|
"mcp__agent-config__config_get": "Read its own merged config",
|
|
54755
54936
|
"mcp__agent-config__cron_list": "List its own scheduled tasks",
|
|
54937
|
+
"mcp__agent-config__cron_doctor": "Health-check its own cron schedule",
|
|
54756
54938
|
"mcp__agent-config__skill_list": "List its own installed skills",
|
|
54757
54939
|
"mcp__agent-config__audit_tail": "Read its own recent tool-call audit log",
|
|
54758
54940
|
"mcp__agent-config__peers_list": "List the other agents on this instance",
|
|
@@ -56064,10 +56246,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
56064
56246
|
}
|
|
56065
56247
|
|
|
56066
56248
|
// ../src/build-info.ts
|
|
56067
|
-
var VERSION = "0.16.
|
|
56068
|
-
var COMMIT_SHA = "
|
|
56069
|
-
var COMMIT_DATE = "2026-06-
|
|
56070
|
-
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;
|
|
56071
56253
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
56072
56254
|
|
|
56073
56255
|
// gateway/boot-version.ts
|
|
@@ -58067,7 +58249,7 @@ var FEED_LIVENESS_OPEN_ENABLED = process.env.SWITCHROOM_FEED_LIVENESS_OPEN !== "
|
|
|
58067
58249
|
var FEED_LIVENESS_OPEN_MS = (() => {
|
|
58068
58250
|
const raw = process.env.SWITCHROOM_FEED_LIVENESS_OPEN_MS;
|
|
58069
58251
|
const n = raw ? Number(raw) : NaN;
|
|
58070
|
-
return Number.isFinite(n) && n > 0 ? n :
|
|
58252
|
+
return Number.isFinite(n) && n > 0 ? n : 1200;
|
|
58071
58253
|
})();
|
|
58072
58254
|
var POST_ANSWER_LIVENESS_STALE_MS = parsePostAnswerLivenessMs(process.env.SWITCHROOM_POST_ANSWER_LIVENESS_STALE_MS) || 30000;
|
|
58073
58255
|
function formatFeedElapsed3(ms) {
|
|
@@ -58475,6 +58657,7 @@ function purgeReactionTracking(key, endingTurn) {
|
|
|
58475
58657
|
const threadId = threadPart === "_" || threadPart === "" ? null : Number(threadPart);
|
|
58476
58658
|
stopTurnTypingLoop(chatId, Number.isFinite(threadId) ? threadId : null);
|
|
58477
58659
|
}
|
|
58660
|
+
stopEarlyLivenessOpen(key);
|
|
58478
58661
|
if (msgInfo) {
|
|
58479
58662
|
const agentDir = resolveAgentDirFromEnv();
|
|
58480
58663
|
if (agentDir != null)
|
|
@@ -58673,34 +58856,13 @@ function maybeIdleClear() {
|
|
|
58673
58856
|
idleClearDispatching = true;
|
|
58674
58857
|
process.stderr.write(`telegram gateway: idle auto-/clear for ${agentName3} (idle >= ${Math.round(idleClearMs / 60000)}m)
|
|
58675
58858
|
`);
|
|
58676
|
-
injectSlashCommand(agentName3, "/clear").
|
|
58677
|
-
postIdleClearNotice(idleClearMs);
|
|
58678
|
-
}).catch((err) => {
|
|
58859
|
+
injectSlashCommand(agentName3, "/clear").catch((err) => {
|
|
58679
58860
|
process.stderr.write(`telegram gateway: idle /clear inject failed for ${agentName3}: ${err instanceof Error ? err.message : String(err)}
|
|
58680
58861
|
`);
|
|
58681
58862
|
}).finally(() => {
|
|
58682
58863
|
idleClearDispatching = false;
|
|
58683
58864
|
});
|
|
58684
58865
|
}
|
|
58685
|
-
async function postIdleClearNotice(idleClearMs) {
|
|
58686
|
-
try {
|
|
58687
|
-
const chatId = loadAccess().allowFrom[0];
|
|
58688
|
-
if (!chatId)
|
|
58689
|
-
return;
|
|
58690
|
-
const threadId = topicForRecipient({
|
|
58691
|
-
recipientChatId: chatId,
|
|
58692
|
-
resolvedTopic: resolveAgentOutboundTopic({ kind: "compact-watchdog" }) ?? chatThreadMap.get(chatId),
|
|
58693
|
-
supergroupChatId: resolveAgentSupergroupChatId()
|
|
58694
|
-
});
|
|
58695
|
-
const hrs = Math.round(idleClearMs / 3600000 * 10) / 10;
|
|
58696
|
-
const text2 = `\uD83E\uDDF9 <b>Cleared after ${hrs}h idle</b> \u2014 fresh slate next message; ` + `long-term memory is in Hindsight.`;
|
|
58697
|
-
await swallowingApiCall(() => bot.api.sendMessage(chatId, text2, {
|
|
58698
|
-
parse_mode: "HTML",
|
|
58699
|
-
disable_notification: true,
|
|
58700
|
-
...threadId != null ? { message_thread_id: threadId } : {}
|
|
58701
|
-
}), { chat_id: chatId, verb: "idleAutoClear.notice" });
|
|
58702
|
-
} catch {}
|
|
58703
|
-
}
|
|
58704
58866
|
async function postCompactCard(occ, cap) {
|
|
58705
58867
|
try {
|
|
58706
58868
|
const chatId = loadAccess().allowFrom[0];
|
|
@@ -58974,24 +59136,18 @@ function stopTypingLoop(chat_id, thread_id = null) {
|
|
|
58974
59136
|
typingRetryTimers.delete(key);
|
|
58975
59137
|
}
|
|
58976
59138
|
}
|
|
58977
|
-
var
|
|
58978
|
-
|
|
58979
|
-
|
|
58980
|
-
const key = chatKey2(chat_id, thread_id);
|
|
58981
|
-
const sendOpts = thread_id != null ? { message_thread_id: thread_id } : undefined;
|
|
58982
|
-
const send = () => {
|
|
59139
|
+
var turnTypingLoop = createTurnTypingLoop({
|
|
59140
|
+
sendChatAction: (chat_id, thread_id) => {
|
|
59141
|
+
const sendOpts = thread_id != null ? { message_thread_id: thread_id } : undefined;
|
|
58983
59142
|
bot.api.sendChatAction(chat_id, "typing", sendOpts).catch(() => {});
|
|
58984
|
-
}
|
|
58985
|
-
|
|
58986
|
-
|
|
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);
|
|
58987
59148
|
}
|
|
58988
59149
|
function stopTurnTypingLoop(chat_id, thread_id = null) {
|
|
58989
|
-
|
|
58990
|
-
const iv = turnTypingIntervals.get(key);
|
|
58991
|
-
if (iv) {
|
|
58992
|
-
clearInterval(iv);
|
|
58993
|
-
turnTypingIntervals.delete(key);
|
|
58994
|
-
}
|
|
59150
|
+
turnTypingLoop.stop(chat_id, thread_id);
|
|
58995
59151
|
}
|
|
58996
59152
|
var typingWrapper = createTypingWrapper({
|
|
58997
59153
|
startTypingLoop,
|
|
@@ -62610,6 +62766,62 @@ async function drainActivitySummary(turn, producer = "tool", openFlags) {
|
|
|
62610
62766
|
turn.activityInFlight = null;
|
|
62611
62767
|
}
|
|
62612
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
|
+
}
|
|
62613
62825
|
function feedHeartbeatTick() {
|
|
62614
62826
|
const turn = currentTurn;
|
|
62615
62827
|
if (turn == null)
|
|
@@ -62650,29 +62862,7 @@ function feedHeartbeatTick() {
|
|
|
62650
62862
|
return;
|
|
62651
62863
|
}
|
|
62652
62864
|
if (turn.mirrorLines.length === 0) {
|
|
62653
|
-
|
|
62654
|
-
return;
|
|
62655
|
-
const age = Date.now() - turn.startedAt;
|
|
62656
|
-
if (age < FEED_LIVENESS_OPEN_MS)
|
|
62657
|
-
return;
|
|
62658
|
-
const livenessHeader = {
|
|
62659
|
-
label: "Agent",
|
|
62660
|
-
elapsedMs: age,
|
|
62661
|
-
toolCount: 0,
|
|
62662
|
-
state: "running"
|
|
62663
|
-
};
|
|
62664
|
-
const rendered2 = renderActivityFeedWithNested(["Working\u2026"], [], false, ` \xB7 ${formatFeedElapsed3(age)}`, undefined, livenessHeader);
|
|
62665
|
-
if (rendered2 == null)
|
|
62666
|
-
return;
|
|
62667
|
-
turn.activityPendingRender = rendered2;
|
|
62668
|
-
const ea2 = emissionAuthorityFor(turn);
|
|
62669
|
-
cardDrainGate(turn, ea2, () => {
|
|
62670
|
-
if (ea2.mayDrain(turn)) {
|
|
62671
|
-
ea2.openOrEditCard("liveness", () => {
|
|
62672
|
-
turn.activityInFlight = drainActivitySummary(turn, "liveness");
|
|
62673
|
-
});
|
|
62674
|
-
}
|
|
62675
|
-
});
|
|
62865
|
+
openLivenessFeedIfDue(turn);
|
|
62676
62866
|
return;
|
|
62677
62867
|
}
|
|
62678
62868
|
if (turn.activityMessageId == null)
|
|
@@ -62804,6 +62994,7 @@ function handleSessionEvent(ev) {
|
|
|
62804
62994
|
};
|
|
62805
62995
|
setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
|
|
62806
62996
|
markIdleActivity();
|
|
62997
|
+
scheduleEarlyLivenessOpen(next);
|
|
62807
62998
|
process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
|
|
62808
62999
|
`);
|
|
62809
63000
|
rememberRecentTurn(next);
|
|
@@ -62942,6 +63133,7 @@ function handleSessionEvent(ev) {
|
|
|
62942
63133
|
chatId: turn.sessionChatId,
|
|
62943
63134
|
threadId: turn.sessionThreadId,
|
|
62944
63135
|
minInitialChars: ANSWER_LANE.minInitialChars,
|
|
63136
|
+
renderText: (text2) => sanitizeTelegramHtml2(markdownToHtml(text2)),
|
|
62945
63137
|
sendMessage: async (chatId, text2, params) => {
|
|
62946
63138
|
const tid = params?.message_thread_id;
|
|
62947
63139
|
const silent = params?.purpose !== "materialize";
|
|
@@ -69501,9 +69693,10 @@ async function shutdown(signal) {
|
|
|
69501
69693
|
for (const iv of [...typingIntervals.values()])
|
|
69502
69694
|
clearInterval(iv);
|
|
69503
69695
|
typingIntervals.clear();
|
|
69504
|
-
|
|
69505
|
-
|
|
69506
|
-
|
|
69696
|
+
turnTypingLoop.stopAll();
|
|
69697
|
+
for (const t of [...earlyLivenessOpenTimers.values()])
|
|
69698
|
+
clearTimeout(t);
|
|
69699
|
+
earlyLivenessOpenTimers.clear();
|
|
69507
69700
|
for (const t of [...typingRetryTimers.values()])
|
|
69508
69701
|
clearTimeout(t);
|
|
69509
69702
|
typingRetryTimers.clear();
|
|
@@ -80,10 +80,63 @@
|
|
|
80
80
|
* already received.
|
|
81
81
|
*/
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Inputs for the liveness early-open WHEN-decision (`shouldEarlyOpenLiveness`).
|
|
85
|
+
* Separate from the OPEN-gate levers above: those answer *may* a card open at
|
|
86
|
+
* all (reply-is-last / cross-turn); this answers *is it time yet* for the
|
|
87
|
+
* minimal "Working…" placeholder on a 0-label turn. Both must say yes — the
|
|
88
|
+
* caller routes the actual open through `mayOpenActivityCard` after this clears.
|
|
89
|
+
*/
|
|
90
|
+
export interface EarlyLivenessOpenInput {
|
|
91
|
+
/** Feature flag (`SWITCHROOM_FEED_LIVENESS_OPEN`). Off ⇒ never early-open. */
|
|
92
|
+
enabled: boolean
|
|
93
|
+
/** Turn age in ms (`now - turn.startedAt`). Must be ≥ `thresholdMs`. */
|
|
94
|
+
ageMs: number
|
|
95
|
+
/** The early-open threshold (`FEED_LIVENESS_OPEN_MS`). */
|
|
96
|
+
thresholdMs: number
|
|
97
|
+
/** Count of surfaced tool steps this turn (`turn.mirrorLines.length`). >0 ⇒ a
|
|
98
|
+
* real label already drives the labelled-feed heartbeat; the placeholder must
|
|
99
|
+
* not fight it, so it never opens (unless `forceNarrative` — see below). */
|
|
100
|
+
mirrorLineCount: number
|
|
101
|
+
/** Single in-place card transport id. Non-null ⇒ a card is already OPEN, so
|
|
102
|
+
* this is a maintain/no-op, not a fresh OPEN. */
|
|
103
|
+
activityMessageId: number | null
|
|
104
|
+
/** The session chat id. `null` ⇒ no surface to open on. */
|
|
105
|
+
sessionChatId: string | null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pure: is the minimal "Working…" liveness placeholder due to OPEN for a 0-label
|
|
110
|
+
* turn? True iff the feature is on, the turn has a target chat, it has been alive
|
|
111
|
+
* past the threshold, and NO card is already open. Once `mirrorLineCount > 0` a
|
|
112
|
+
* real tool label drives the feed, EXCEPT the edge where narration staged
|
|
113
|
+
* `mirrorLines` but no card opened yet (`activityMessageId == null`) — there the
|
|
114
|
+
* accumulated narration should still render on the early open, so it is allowed.
|
|
115
|
+
*
|
|
116
|
+
* This is the WHEN gate. The caller still routes the OPEN through
|
|
117
|
+
* `mayOpenActivityCard` (lever 1/4) so a card never opens below a delivered
|
|
118
|
+
* answer or on a cross-turn synthetic surface. Two callers consult this — the
|
|
119
|
+
* enqueue-time early-open timer and the 6 s heartbeat — and because an already-
|
|
120
|
+
* open card returns false here (and the drain EDITs rather than re-OPENs), they
|
|
121
|
+
* can never double-open.
|
|
122
|
+
*/
|
|
123
|
+
export function shouldEarlyOpenLiveness(input: EarlyLivenessOpenInput): boolean {
|
|
124
|
+
if (!input.enabled) return false
|
|
125
|
+
if (input.sessionChatId == null) return false
|
|
126
|
+
// A card is already open → maintain via the drain's EDIT path, not a fresh
|
|
127
|
+
// OPEN. Never double-open. (Reaching past here implies `activityMessageId ==
|
|
128
|
+
// null`, so a non-zero `mirrorLineCount` is the "narration staged but no card
|
|
129
|
+
// opened yet" edge — allowed below so the accumulated narration renders.)
|
|
130
|
+
if (input.activityMessageId != null) return false
|
|
131
|
+
if (input.ageMs < input.thresholdMs) return false
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
83
135
|
/** Which producer triggered this drain — determines lever-5 OPEN eligibility. */
|
|
84
136
|
export type FeedOpenProducer =
|
|
85
137
|
/** Narrative SHOW (producer A): plain assistant text, no tool, no time
|
|
86
|
-
* threshold.
|
|
138
|
+
* threshold. Pre-answer it is OPEN-eligible (lever 5 INERT); after a
|
|
139
|
+
* substantive final answer it is blocked by lever 1. */
|
|
87
140
|
| 'narrative'
|
|
88
141
|
/** Tool label (producer B): the model dispatched a tool. OPEN-eligible unless a
|
|
89
142
|
* substantive final already landed (lever 1).
|