switchroom 0.16.15 → 0.16.20
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 +100 -17
- 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.20";
|
|
51696
|
+
var COMMIT_SHA = "6ea6e873";
|
|
51697
51697
|
|
|
51698
51698
|
// src/cli/resolve-version.ts
|
|
51699
51699
|
function readPackageVersion() {
|
|
@@ -77519,11 +77519,23 @@ function agentLiveness(config, agentName) {
|
|
|
77519
77519
|
return "offline";
|
|
77520
77520
|
}
|
|
77521
77521
|
function toHermesSession(agent, liveness) {
|
|
77522
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
77522
77523
|
return {
|
|
77523
77524
|
id: agent.name,
|
|
77524
77525
|
name: agent.name,
|
|
77526
|
+
title: agent.name,
|
|
77525
77527
|
status: liveness,
|
|
77526
77528
|
model: "claude",
|
|
77529
|
+
is_active: liveness === "active",
|
|
77530
|
+
started_at: nowSec,
|
|
77531
|
+
last_active: nowSec,
|
|
77532
|
+
ended_at: null,
|
|
77533
|
+
input_tokens: 0,
|
|
77534
|
+
output_tokens: 0,
|
|
77535
|
+
message_count: 0,
|
|
77536
|
+
tool_call_count: 0,
|
|
77537
|
+
preview: null,
|
|
77538
|
+
source: "switchroom",
|
|
77527
77539
|
created_at: null,
|
|
77528
77540
|
updated_at: null,
|
|
77529
77541
|
quota: agent.primaryAccount ? { used_pct: null, slot: agent.primaryAccount } : null
|
|
@@ -77596,11 +77608,19 @@ async function injectInbound(agentsDir, agentName, chatId, threadId, text, promp
|
|
|
77596
77608
|
});
|
|
77597
77609
|
}
|
|
77598
77610
|
async function handleHermesRest(method, pathname, config) {
|
|
77599
|
-
if (method === "GET" && pathname === "/api/sessions") {
|
|
77611
|
+
if (method === "GET" && (pathname === "/api/sessions" || pathname === "/api/profiles/sessions")) {
|
|
77600
77612
|
const agents = await handleGetAgents(config);
|
|
77601
|
-
const agentsDir = resolveAgentsDir(config);
|
|
77602
77613
|
const sessions = agents.map((a) => toHermesSession(a, agentLiveness(config, a.name)));
|
|
77603
|
-
return {
|
|
77614
|
+
return {
|
|
77615
|
+
status: 200,
|
|
77616
|
+
body: {
|
|
77617
|
+
sessions,
|
|
77618
|
+
total: sessions.length,
|
|
77619
|
+
limit: sessions.length,
|
|
77620
|
+
offset: 0,
|
|
77621
|
+
profile_totals: { default: sessions.length }
|
|
77622
|
+
}
|
|
77623
|
+
};
|
|
77604
77624
|
}
|
|
77605
77625
|
const sessionMatch = pathname.match(/^\/api\/sessions\/([^/]+)$/);
|
|
77606
77626
|
if (method === "GET" && sessionMatch) {
|
|
@@ -77624,30 +77644,81 @@ async function handleHermesRest(method, pathname, config) {
|
|
|
77624
77644
|
return { status: 200, body: { history: result.turns ?? [] } };
|
|
77625
77645
|
}
|
|
77626
77646
|
if (method === "GET" && pathname === "/api/status") {
|
|
77627
|
-
const agents = await handleGetAgents(config);
|
|
77628
|
-
const fleet = agents.map((a) => ({
|
|
77629
|
-
name: a.name,
|
|
77630
|
-
status: agentLiveness(config, a.name)
|
|
77631
|
-
}));
|
|
77632
77647
|
return {
|
|
77633
77648
|
status: 200,
|
|
77634
77649
|
body: {
|
|
77635
|
-
|
|
77636
|
-
|
|
77637
|
-
|
|
77650
|
+
version: "switchroom",
|
|
77651
|
+
gateway_running: true,
|
|
77652
|
+
gateway_platforms: {},
|
|
77653
|
+
gateway_state: "ready",
|
|
77654
|
+
config_version: 0,
|
|
77655
|
+
latest_config_version: 0,
|
|
77656
|
+
hermes_home: "switchroom",
|
|
77657
|
+
release_date: null,
|
|
77658
|
+
active_sessions: 0,
|
|
77659
|
+
config_path: null,
|
|
77660
|
+
env_path: null,
|
|
77661
|
+
gateway_exit_reason: null,
|
|
77662
|
+
gateway_health_url: null,
|
|
77663
|
+
gateway_pid: null,
|
|
77664
|
+
gateway_updated_at: null
|
|
77638
77665
|
}
|
|
77639
77666
|
};
|
|
77640
77667
|
}
|
|
77641
77668
|
if (method === "GET" && pathname === "/api/config") {
|
|
77642
|
-
const agentNames = Object.keys(config.agents ?? {});
|
|
77643
77669
|
return {
|
|
77644
77670
|
status: 200,
|
|
77645
77671
|
body: {
|
|
77646
77672
|
provider: "switchroom",
|
|
77647
|
-
|
|
77673
|
+
model: null,
|
|
77674
|
+
context_length: null,
|
|
77675
|
+
system_prompt: null
|
|
77676
|
+
}
|
|
77677
|
+
};
|
|
77678
|
+
}
|
|
77679
|
+
if (method === "GET" && (pathname === "/api/config/defaults" || pathname === "/api/config/schema")) {
|
|
77680
|
+
return { status: 200, body: {} };
|
|
77681
|
+
}
|
|
77682
|
+
if (method === "GET" && pathname === "/api/model/info") {
|
|
77683
|
+
return {
|
|
77684
|
+
status: 200,
|
|
77685
|
+
body: {
|
|
77686
|
+
model: "claude",
|
|
77687
|
+
provider: "switchroom",
|
|
77688
|
+
capabilities: {}
|
|
77648
77689
|
}
|
|
77649
77690
|
};
|
|
77650
77691
|
}
|
|
77692
|
+
if (method === "GET" && pathname.startsWith("/api/logs")) {
|
|
77693
|
+
return { status: 200, body: { file: "gateway.log", lines: [] } };
|
|
77694
|
+
}
|
|
77695
|
+
if (method === "GET" && pathname.startsWith("/api/cron")) {
|
|
77696
|
+
if (pathname === "/api/cron/jobs")
|
|
77697
|
+
return { status: 200, body: [] };
|
|
77698
|
+
if (pathname.includes("/runs"))
|
|
77699
|
+
return { status: 200, body: { runs: [] } };
|
|
77700
|
+
if (pathname.includes("sessions")) {
|
|
77701
|
+
return { status: 200, body: { sessions: [], total: 0, limit: 0, offset: 0 } };
|
|
77702
|
+
}
|
|
77703
|
+
return { status: 200, body: {} };
|
|
77704
|
+
}
|
|
77705
|
+
if (method === "GET" && pathname.startsWith("/api/messaging")) {
|
|
77706
|
+
if (pathname === "/api/messaging/platforms")
|
|
77707
|
+
return { status: 200, body: { platforms: [] } };
|
|
77708
|
+
return { status: 200, body: {} };
|
|
77709
|
+
}
|
|
77710
|
+
if (method === "GET" && pathname.startsWith("/api/profiles")) {
|
|
77711
|
+
if (pathname.includes("sessions")) {
|
|
77712
|
+
return { status: 200, body: { sessions: [], total: 0, limit: 0, offset: 0 } };
|
|
77713
|
+
}
|
|
77714
|
+
return { status: 200, body: {} };
|
|
77715
|
+
}
|
|
77716
|
+
if (method === "GET" && pathname === "/api/memory/providers") {
|
|
77717
|
+
return { status: 200, body: {} };
|
|
77718
|
+
}
|
|
77719
|
+
if (method === "GET" && pathname === "/api/fs/default-cwd") {
|
|
77720
|
+
return { status: 200, body: { cwd: null, branch: null } };
|
|
77721
|
+
}
|
|
77651
77722
|
return null;
|
|
77652
77723
|
}
|
|
77653
77724
|
function sendEvent(ctx, type, sessionId, payload) {
|
|
@@ -77806,9 +77877,13 @@ async function onHermesMessage(ctx, raw) {
|
|
|
77806
77877
|
sendResponse(ctx, rpcErr(id, -32603, result.error ?? "inject failed"));
|
|
77807
77878
|
break;
|
|
77808
77879
|
}
|
|
77880
|
+
sendEvent(ctx, "message.delta", sessionId, {
|
|
77881
|
+
text: `*(Prompt sent to ${sessionId} \u2014 response will appear in Telegram)*`,
|
|
77882
|
+
prompt_key: promptKey
|
|
77883
|
+
});
|
|
77809
77884
|
sendEvent(ctx, "message.complete", sessionId, {
|
|
77810
|
-
|
|
77811
|
-
|
|
77885
|
+
text: `*(Prompt sent to ${sessionId} \u2014 response will appear in Telegram)*`,
|
|
77886
|
+
prompt_key: promptKey
|
|
77812
77887
|
});
|
|
77813
77888
|
sendResponse(ctx, rpcOk(id, { ok: true, prompt_key: promptKey }));
|
|
77814
77889
|
break;
|
|
@@ -77834,6 +77909,14 @@ async function onHermesMessage(ctx, raw) {
|
|
|
77834
77909
|
sendResponse(ctx, rpcOk(id, { ok: true }));
|
|
77835
77910
|
break;
|
|
77836
77911
|
}
|
|
77912
|
+
case "setup.status": {
|
|
77913
|
+
sendResponse(ctx, rpcOk(id, { provider_configured: true }));
|
|
77914
|
+
break;
|
|
77915
|
+
}
|
|
77916
|
+
case "setup.runtime_check": {
|
|
77917
|
+
sendResponse(ctx, rpcOk(id, { ok: true }));
|
|
77918
|
+
break;
|
|
77919
|
+
}
|
|
77837
77920
|
case "approval.request":
|
|
77838
77921
|
case "approval.respond":
|
|
77839
77922
|
case "sudo.request":
|
|
@@ -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.20";
|
|
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
|