switchroom 0.18.13 → 0.18.14
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/agent-scheduler/index.js +49 -9
- package/dist/auth-broker/index.js +111 -7
- package/dist/cli/autoaccept-poll.js +23 -0
- package/dist/cli/drive-write-pretool.mjs +24 -1
- package/dist/cli/foreground-hog-pretool.mjs +264 -0
- package/dist/cli/notion-write-pretool.mjs +0 -1
- package/dist/cli/switchroom.js +35 -6
- package/dist/host-control/main.js +1 -2
- package/dist/vault/approvals/kernel-server.js +0 -1
- package/dist/vault/broker/server.js +0 -1
- package/package.json +1 -1
- package/profiles/coding/CLAUDE.md.hbs +2 -0
- package/profiles/default/CLAUDE.md.hbs +2 -0
- package/skills/switchroom-architecture/telegram.md +0 -1
- package/telegram-plugin/auth-snapshot-format.ts +37 -5
- package/telegram-plugin/auto-fallback-fleet.ts +29 -1
- package/telegram-plugin/bridge/bridge.ts +2 -0
- package/telegram-plugin/dist/bridge/bridge.js +2 -0
- package/telegram-plugin/dist/gateway/gateway.js +620 -67
- package/telegram-plugin/dist/server.js +2 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +1 -0
- package/telegram-plugin/gateway/auth-command.ts +14 -0
- package/telegram-plugin/gateway/forward-origin.ts +235 -0
- package/telegram-plugin/gateway/gateway.ts +224 -10
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +268 -0
- package/telegram-plugin/history.ts +55 -6
- package/telegram-plugin/model-unavailable.ts +20 -2
- package/telegram-plugin/render/rich-render.ts +40 -32
- package/telegram-plugin/stream-controller.ts +3 -2
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +72 -0
- package/telegram-plugin/tests/forward-origin.test.ts +309 -0
- package/telegram-plugin/tests/history.test.ts +157 -0
- package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +6 -4
- package/telegram-plugin/tests/render/rich-render.test.ts +41 -22
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +5 -3
- package/telegram-plugin/tests/status-accent.test.ts +5 -3
- package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +20 -20
- package/telegram-plugin/tests/stream-reply-handler.test.ts +5 -2
- package/telegram-plugin/tests/throttle-tier-wiring.test.ts +290 -0
- package/telegram-plugin/tests/throttle-tier.test.ts +278 -0
- package/telegram-plugin/throttle-tier.ts +226 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +8 -7
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// src/cli/foreground-hog-pretool.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
function gateEnabledFromEnv(env = process.env) {
|
|
4
|
+
const v = env.SWITCHROOM_FOREGROUND_HOG_GATE;
|
|
5
|
+
if (v == null)
|
|
6
|
+
return true;
|
|
7
|
+
const t = v.trim().toLowerCase();
|
|
8
|
+
return !(t === "0" || t === "false" || t === "off" || t === "no");
|
|
9
|
+
}
|
|
10
|
+
function blankQuotedAndHeredocs(src) {
|
|
11
|
+
const out = src.split("");
|
|
12
|
+
const n = src.length;
|
|
13
|
+
const pending = [];
|
|
14
|
+
let i = 0;
|
|
15
|
+
while (i < n) {
|
|
16
|
+
const ch = src[i];
|
|
17
|
+
if (ch === "\\") {
|
|
18
|
+
out[i] = " ";
|
|
19
|
+
if (i + 1 < n)
|
|
20
|
+
out[i + 1] = " ";
|
|
21
|
+
i += 2;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (ch === "'") {
|
|
25
|
+
let j = i + 1;
|
|
26
|
+
while (j < n && src[j] !== "'")
|
|
27
|
+
j++;
|
|
28
|
+
if (j >= n)
|
|
29
|
+
return null;
|
|
30
|
+
for (let k = i;k <= j; k++)
|
|
31
|
+
out[k] = " ";
|
|
32
|
+
i = j + 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (ch === '"') {
|
|
36
|
+
let j = i + 1;
|
|
37
|
+
while (j < n && src[j] !== '"') {
|
|
38
|
+
j += src[j] === "\\" ? 2 : 1;
|
|
39
|
+
}
|
|
40
|
+
if (j >= n)
|
|
41
|
+
return null;
|
|
42
|
+
for (let k = i;k <= j && k < n; k++)
|
|
43
|
+
out[k] = " ";
|
|
44
|
+
i = j + 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === "<" && src[i + 1] === "<") {
|
|
48
|
+
if (src[i + 2] === "<") {
|
|
49
|
+
i += 3;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let j = i + 2;
|
|
53
|
+
let stripTabs = false;
|
|
54
|
+
if (src[j] === "-") {
|
|
55
|
+
stripTabs = true;
|
|
56
|
+
j++;
|
|
57
|
+
}
|
|
58
|
+
while (j < n && (src[j] === " " || src[j] === "\t"))
|
|
59
|
+
j++;
|
|
60
|
+
let quote = "";
|
|
61
|
+
if (src[j] === "'" || src[j] === '"') {
|
|
62
|
+
quote = src[j];
|
|
63
|
+
j++;
|
|
64
|
+
}
|
|
65
|
+
let tag = "";
|
|
66
|
+
while (j < n && /[A-Za-z0-9_]/.test(src[j])) {
|
|
67
|
+
tag += src[j];
|
|
68
|
+
j++;
|
|
69
|
+
}
|
|
70
|
+
if (quote) {
|
|
71
|
+
if (src[j] !== quote)
|
|
72
|
+
return null;
|
|
73
|
+
j++;
|
|
74
|
+
}
|
|
75
|
+
if (!tag)
|
|
76
|
+
return null;
|
|
77
|
+
pending.push({ tag, stripTabs });
|
|
78
|
+
i = j;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === `
|
|
82
|
+
` && pending.length > 0) {
|
|
83
|
+
let j = i + 1;
|
|
84
|
+
while (pending.length > 0) {
|
|
85
|
+
const { tag, stripTabs } = pending.shift();
|
|
86
|
+
let found = false;
|
|
87
|
+
while (j <= n) {
|
|
88
|
+
let eol = src.indexOf(`
|
|
89
|
+
`, j);
|
|
90
|
+
if (eol === -1)
|
|
91
|
+
eol = n;
|
|
92
|
+
const line = src.slice(j, eol);
|
|
93
|
+
const cmp = stripTabs ? line.replace(/^\t+/, "") : line;
|
|
94
|
+
if (cmp === tag) {
|
|
95
|
+
found = true;
|
|
96
|
+
j = eol + 1;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
for (let k = j;k < eol; k++)
|
|
100
|
+
out[k] = " ";
|
|
101
|
+
if (eol === n) {
|
|
102
|
+
j = n + 1;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
j = eol + 1;
|
|
106
|
+
}
|
|
107
|
+
if (!found)
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
i = j;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
return out.join("");
|
|
116
|
+
}
|
|
117
|
+
var LOOP_SLEEP = /\b(?:while|until|for)\b[\s\S]*?\bdo\b[\s\S]*?\bsleep\b[\s\S]*?\bdone\b/;
|
|
118
|
+
function sleepSeconds(token) {
|
|
119
|
+
const m = /^(\d+(?:\.\d+)?)([smhd]?)$/.exec(token);
|
|
120
|
+
if (!m)
|
|
121
|
+
return null;
|
|
122
|
+
const mult = { "": 1, s: 1, m: 60, h: 3600, d: 86400 }[m[2]];
|
|
123
|
+
return Number(m[1]) * mult;
|
|
124
|
+
}
|
|
125
|
+
function hasFollowFlag(args, allowCapitalF) {
|
|
126
|
+
return args.some((a) => a === "--follow" || a.startsWith("--follow=") || /^-[a-zA-Z0-9]+$/.test(a) && (a.includes("f") || allowCapitalF && a.includes("F")));
|
|
127
|
+
}
|
|
128
|
+
function foregroundSegments(command) {
|
|
129
|
+
const cleaned = command.replace(/\d*>&\d*/g, " ");
|
|
130
|
+
const out = [];
|
|
131
|
+
const sep = /&&|\|\||[;|\n&]/g;
|
|
132
|
+
let last = 0;
|
|
133
|
+
let m;
|
|
134
|
+
const push = (seg, backgrounded) => {
|
|
135
|
+
const t = seg.trim();
|
|
136
|
+
if (t && !backgrounded)
|
|
137
|
+
out.push(t);
|
|
138
|
+
};
|
|
139
|
+
while ((m = sep.exec(cleaned)) !== null) {
|
|
140
|
+
push(cleaned.slice(last, m.index), m[0] === "&");
|
|
141
|
+
last = m.index + m[0].length;
|
|
142
|
+
}
|
|
143
|
+
push(cleaned.slice(last), false);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function commandTokens(segment) {
|
|
147
|
+
const SKIP = new Set([
|
|
148
|
+
"if",
|
|
149
|
+
"elif",
|
|
150
|
+
"then",
|
|
151
|
+
"else",
|
|
152
|
+
"do",
|
|
153
|
+
"time",
|
|
154
|
+
"nohup",
|
|
155
|
+
"sudo",
|
|
156
|
+
"command",
|
|
157
|
+
"exec"
|
|
158
|
+
]);
|
|
159
|
+
const tokens = segment.split(/\s+/);
|
|
160
|
+
const out = [];
|
|
161
|
+
let started = false;
|
|
162
|
+
for (const rawTok of tokens) {
|
|
163
|
+
const tok = rawTok.replace(/^[({]+/, "");
|
|
164
|
+
if (!tok)
|
|
165
|
+
continue;
|
|
166
|
+
if (!started) {
|
|
167
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tok))
|
|
168
|
+
continue;
|
|
169
|
+
if (SKIP.has(tok))
|
|
170
|
+
continue;
|
|
171
|
+
started = true;
|
|
172
|
+
}
|
|
173
|
+
out.push(tok);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function matchSegment(segment) {
|
|
178
|
+
const tokens = commandTokens(segment);
|
|
179
|
+
const cmd = tokens[0];
|
|
180
|
+
if (!cmd)
|
|
181
|
+
return null;
|
|
182
|
+
const args = tokens.slice(1);
|
|
183
|
+
if (cmd === "tail" && hasFollowFlag(args, true))
|
|
184
|
+
return "tail -f/--follow";
|
|
185
|
+
if (cmd === "watch")
|
|
186
|
+
return "watch";
|
|
187
|
+
if (cmd === "sleep") {
|
|
188
|
+
const secs = sleepSeconds(args[0] ?? "");
|
|
189
|
+
if (secs !== null && secs > 30)
|
|
190
|
+
return `sleep ${args[0]} (> 30s foreground)`;
|
|
191
|
+
}
|
|
192
|
+
if (cmd === "gh") {
|
|
193
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
194
|
+
if (positionals[0] === "pr" && positionals[1] === "checks" && args.includes("--watch"))
|
|
195
|
+
return "gh pr checks --watch";
|
|
196
|
+
if (positionals[0] === "run" && positionals[1] === "watch")
|
|
197
|
+
return "gh run watch";
|
|
198
|
+
}
|
|
199
|
+
if (cmd === "docker" && args[0] === "logs" && hasFollowFlag(args.slice(1), false))
|
|
200
|
+
return "docker logs -f/--follow";
|
|
201
|
+
if (cmd === "kubectl" && args[0] === "logs" && hasFollowFlag(args.slice(1), false))
|
|
202
|
+
return "kubectl logs -f/--follow";
|
|
203
|
+
if (cmd === "journalctl" && hasFollowFlag(args, false))
|
|
204
|
+
return "journalctl -f";
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function detectForegroundHog(command) {
|
|
208
|
+
const blanked = blankQuotedAndHeredocs(command);
|
|
209
|
+
if (blanked === null)
|
|
210
|
+
return null;
|
|
211
|
+
if (LOOP_SLEEP.test(blanked))
|
|
212
|
+
return "sleep in a while/until/for loop";
|
|
213
|
+
for (const segment of foregroundSegments(blanked)) {
|
|
214
|
+
const hit = matchSegment(segment);
|
|
215
|
+
if (hit)
|
|
216
|
+
return hit;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function denyMessage(pattern) {
|
|
221
|
+
return `Foreground-hog gate: \`${pattern}\` would hold the session foreground for its full duration. ` + "Re-run this exact command with `run_in_background: true` and act on the completion notification, " + "or use a bounded alternative (e.g. `tail -n 200` instead of `tail -f`).";
|
|
222
|
+
}
|
|
223
|
+
function readStdin() {
|
|
224
|
+
try {
|
|
225
|
+
return readFileSync(0, "utf8");
|
|
226
|
+
} catch {
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function allow() {
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
function block(reason) {
|
|
234
|
+
process.stdout.write(JSON.stringify({ decision: "block", reason }));
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
function main() {
|
|
238
|
+
if (!gateEnabledFromEnv())
|
|
239
|
+
allow();
|
|
240
|
+
const raw = readStdin().trim();
|
|
241
|
+
if (!raw)
|
|
242
|
+
allow();
|
|
243
|
+
let event;
|
|
244
|
+
try {
|
|
245
|
+
event = JSON.parse(raw);
|
|
246
|
+
} catch {
|
|
247
|
+
allow();
|
|
248
|
+
}
|
|
249
|
+
if (event.tool_name !== "Bash")
|
|
250
|
+
allow();
|
|
251
|
+
const input = event.tool_input;
|
|
252
|
+
if (!input || typeof input !== "object")
|
|
253
|
+
allow();
|
|
254
|
+
const { run_in_background: runInBackground, command } = input;
|
|
255
|
+
if (runInBackground === true)
|
|
256
|
+
allow();
|
|
257
|
+
if (typeof command !== "string" || command.length === 0)
|
|
258
|
+
allow();
|
|
259
|
+
const pattern = detectForegroundHog(command);
|
|
260
|
+
if (pattern)
|
|
261
|
+
block(denyMessage(pattern));
|
|
262
|
+
allow();
|
|
263
|
+
}
|
|
264
|
+
main();
|
|
@@ -11945,7 +11945,6 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11945
11945
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
11946
11946
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' \u2014 the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
11947
11947
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
11948
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
11949
11948
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI \u2014 " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events \u2014 stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
11950
11949
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
11951
11950
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message \u2014 Reading X, Searching the web for Y, \u2026) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) \u2014 no post-then-delete. Per-agent " + "override; cascades defaults \u2192 profile \u2192 agent (per-key)."),
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.18.
|
|
2123
|
+
var VERSION = "0.18.14", COMMIT_SHA = "b0db04aa";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -13806,7 +13806,6 @@ var init_schema = __esm(() => {
|
|
|
13806
13806
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
13807
13807
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' \u2014 the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
13808
13808
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
13809
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
13810
13809
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI \u2014 " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events \u2014 stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
13811
13810
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
13812
13811
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message \u2014 Reading X, Searching the web for Y, \u2026) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) \u2014 no post-then-delete. Per-agent " + "override; cascades defaults \u2192 profile \u2192 agent (per-key)."),
|
|
@@ -26595,9 +26594,6 @@ function channelsToEnv(agent) {
|
|
|
26595
26594
|
return out;
|
|
26596
26595
|
if (tg.format !== undefined)
|
|
26597
26596
|
out.SWITCHROOM_TG_FORMAT = tg.format;
|
|
26598
|
-
if (tg.rate_limit_ms !== undefined) {
|
|
26599
|
-
out.SWITCHROOM_TG_RATE_LIMIT_MS = String(tg.rate_limit_ms);
|
|
26600
|
-
}
|
|
26601
26597
|
if (tg.stream_mode !== undefined) {
|
|
26602
26598
|
out.SWITCHROOM_TG_STREAM_MODE = tg.stream_mode;
|
|
26603
26599
|
}
|
|
@@ -27883,6 +27879,16 @@ function buildSettingsHooksBlock(p) {
|
|
|
27883
27879
|
}
|
|
27884
27880
|
]
|
|
27885
27881
|
},
|
|
27882
|
+
{
|
|
27883
|
+
matcher: "^Bash$",
|
|
27884
|
+
hooks: [
|
|
27885
|
+
{
|
|
27886
|
+
type: "command",
|
|
27887
|
+
command: wrap("hook:foreground-hog-pretool", `node "${join10(DOCKER_BUNDLED_HOOKS_PATH, "foreground-hog-pretool.mjs")}"`),
|
|
27888
|
+
timeout: 10
|
|
27889
|
+
}
|
|
27890
|
+
]
|
|
27891
|
+
},
|
|
27886
27892
|
{
|
|
27887
27893
|
matcher: "^(Write|Edit|MultiEdit)$",
|
|
27888
27894
|
hooks: [
|
|
@@ -32060,7 +32066,7 @@ function decodeResponse2(line) {
|
|
|
32060
32066
|
}
|
|
32061
32067
|
return ResponseSchema2.parse(parsed);
|
|
32062
32068
|
}
|
|
32063
|
-
var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, ClaimNotificationRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, ClaimNotificationDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
|
|
32069
|
+
var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, MarkThrottledRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, MicrosoftCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ListMicrosoftAccountsRequestSchema, ProbeQuotaRequestSchema, ClaimNotificationRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, MarkThrottledDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, ClaimNotificationDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, MicrosoftAccountStateSchema, ListMicrosoftAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
|
|
32064
32070
|
var init_protocol2 = __esm(() => {
|
|
32065
32071
|
init_zod();
|
|
32066
32072
|
MAX_FRAME_BYTES2 = 64 * 1024;
|
|
@@ -32089,6 +32095,12 @@ var init_protocol2 = __esm(() => {
|
|
|
32089
32095
|
id: exports_external.string().min(1),
|
|
32090
32096
|
until: exports_external.number().int().positive().optional()
|
|
32091
32097
|
});
|
|
32098
|
+
MarkThrottledRequestSchema = exports_external.object({
|
|
32099
|
+
v: exports_external.literal(PROTOCOL_VERSION),
|
|
32100
|
+
op: exports_external.literal("mark-throttled"),
|
|
32101
|
+
id: exports_external.string().min(1),
|
|
32102
|
+
until: exports_external.number().int().positive()
|
|
32103
|
+
});
|
|
32092
32104
|
RefreshAccountRequestSchema = exports_external.object({
|
|
32093
32105
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
32094
32106
|
op: exports_external.literal("refresh-account"),
|
|
@@ -32189,6 +32201,7 @@ var init_protocol2 = __esm(() => {
|
|
|
32189
32201
|
ListStateRequestSchema,
|
|
32190
32202
|
SetActiveRequestSchema,
|
|
32191
32203
|
MarkExhaustedRequestSchema,
|
|
32204
|
+
MarkThrottledRequestSchema,
|
|
32192
32205
|
RefreshAccountRequestSchema,
|
|
32193
32206
|
AddAccountRequestSchema,
|
|
32194
32207
|
RmAccountRequestSchema,
|
|
@@ -32208,6 +32221,7 @@ var init_protocol2 = __esm(() => {
|
|
|
32208
32221
|
expiresAt: exports_external.number().optional(),
|
|
32209
32222
|
exhausted: exports_external.boolean(),
|
|
32210
32223
|
exhausted_until: exports_external.number().optional(),
|
|
32224
|
+
throttled_until: exports_external.number().optional(),
|
|
32211
32225
|
threshold_violations: exports_external.number().int().nonnegative().optional(),
|
|
32212
32226
|
last_refreshed_at: exports_external.number().optional()
|
|
32213
32227
|
});
|
|
@@ -32238,6 +32252,12 @@ var init_protocol2 = __esm(() => {
|
|
|
32238
32252
|
rolled: exports_external.array(exports_external.string()),
|
|
32239
32253
|
rolledTo: exports_external.string().nullable().optional()
|
|
32240
32254
|
});
|
|
32255
|
+
MarkThrottledDataSchema = exports_external.object({
|
|
32256
|
+
account: exports_external.string(),
|
|
32257
|
+
throttled_until: exports_external.number(),
|
|
32258
|
+
escalated: exports_external.boolean(),
|
|
32259
|
+
rolledTo: exports_external.string().nullable().optional()
|
|
32260
|
+
});
|
|
32241
32261
|
RefreshAccountDataSchema = exports_external.object({
|
|
32242
32262
|
account: exports_external.string(),
|
|
32243
32263
|
expiresAt: exports_external.number().optional()
|
|
@@ -32436,6 +32456,15 @@ class AuthBrokerClient {
|
|
|
32436
32456
|
const data = await this.send(req);
|
|
32437
32457
|
return data;
|
|
32438
32458
|
}
|
|
32459
|
+
async markThrottled(until) {
|
|
32460
|
+
const data = await this.send({
|
|
32461
|
+
v: PROTOCOL_VERSION,
|
|
32462
|
+
id: randomUUID(),
|
|
32463
|
+
op: "mark-throttled",
|
|
32464
|
+
until
|
|
32465
|
+
});
|
|
32466
|
+
return data;
|
|
32467
|
+
}
|
|
32439
32468
|
async claimNotification(key, windowMs) {
|
|
32440
32469
|
const data = await this.send({
|
|
32441
32470
|
v: PROTOCOL_VERSION,
|
|
@@ -18824,7 +18824,6 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
18824
18824
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
18825
18825
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
18826
18826
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
18827
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
18828
18827
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
18829
18828
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
18830
18829
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
|
|
@@ -26588,7 +26587,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
|
|
|
26588
26587
|
import { dirname as dirname4, join as join7 } from "node:path";
|
|
26589
26588
|
|
|
26590
26589
|
// src/build-info.ts
|
|
26591
|
-
var VERSION = "0.18.
|
|
26590
|
+
var VERSION = "0.18.14";
|
|
26592
26591
|
|
|
26593
26592
|
// src/cli/resolve-version.ts
|
|
26594
26593
|
function readPackageVersion() {
|
|
@@ -4244,7 +4244,6 @@ var init_schema = __esm(() => {
|
|
|
4244
4244
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
4245
4245
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
4246
4246
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
4247
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
4248
4247
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
4249
4248
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
4250
4249
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
|
|
@@ -4244,7 +4244,6 @@ var init_schema = __esm(() => {
|
|
|
4244
4244
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
4245
4245
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
4246
4246
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
4247
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
4248
4247
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
4249
4248
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
4250
4249
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.14",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -51,6 +51,8 @@ Save proactively: architecture decisions, codebase patterns, conventions, known
|
|
|
51
51
|
|
|
52
52
|
If sub-agents are configured, delegate implementation to `@worker` (background, own worktree) and research to `@researcher` (background). Keep your turns short — dispatch and acknowledge quickly so you stay available for the user.
|
|
53
53
|
|
|
54
|
+
If the user amends in-flight delegated work mid-turn, steer the running worker now (`SendMessage` to the worker by name, or by the agent id from its spawn result) instead of holding the update for handback — and say in your reply whether you folded the update into the running worker or queued it as a separate task; never classify silently. If unsure whether a message amends in-flight work, queue it and say so — queue is the default. If the steer lands too late (worker effectively done), say so and apply the update yourself.
|
|
55
|
+
|
|
54
56
|
{{#if schedule}}
|
|
55
57
|
## Schedule
|
|
56
58
|
You are running on a schedule: `{{schedule}}`. Use scheduled runs to check CI status, review open PRs, or surface stale issues.
|
|
@@ -92,6 +92,8 @@ The main session is for conversation. Execution belongs in sub-agents. Before ma
|
|
|
92
92
|
|
|
93
93
|
**Anti-patterns:** starting a task inline then realizing it's complex mid-way; doing 5+ tool calls "because it's almost done"; polling sub-agent status in a loop.
|
|
94
94
|
|
|
95
|
+
**Steer running workers — don't sit on amendments.** When a mid-turn user message amends work you've already delegated to a running sub-agent, forward the amendment to that worker now — `SendMessage` to the worker by name (or by the agent id from the Agent tool's spawn result, if it has no name) — rather than holding it for handback. If the message is a new independent task, queue it instead; if you're unsure whether it amends the in-flight work, queue it and say so — queue is the default. Either way, state in your reply which you did ("folded your update into the running worker" / "queued as a separate task after the current one") — the steer-or-queue rule is that the classification is always visible in chat, never inferred. If the steer arrives too late (the worker is effectively done), say that and apply the update yourself in the parent.
|
|
96
|
+
|
|
95
97
|
If no sub-agents are configured, do the work yourself.
|
|
96
98
|
|
|
97
99
|
## Session Continuity
|
|
@@ -81,7 +81,6 @@ agents:
|
|
|
81
81
|
| Var | Source | Purpose |
|
|
82
82
|
|-----|--------|---------|
|
|
83
83
|
| `SWITCHROOM_TG_FORMAT` | `channels.telegram.format` | Default reply format |
|
|
84
|
-
| `SWITCHROOM_TG_RATE_LIMIT_MS` | `channels.telegram.rate_limit_ms` | Min delay between outgoing messages |
|
|
85
84
|
| `TELEGRAM_STATE_DIR` | Auto-set by scaffold | Path to `telegram/` dir |
|
|
86
85
|
| `SWITCHROOM_AGENT_NAME` | Auto-set by scaffold | Agent name (used for self-restart detection) |
|
|
87
86
|
| `SWITCHROOM_CONFIG` | Auto-set by scaffold | Path to switchroom.yaml |
|
|
@@ -704,6 +704,22 @@ export interface FallbackAnnouncementInput {
|
|
|
704
704
|
* shows the target's headroom and is unchanged.
|
|
705
705
|
*/
|
|
706
706
|
fleetSnapshots?: AccountSnapshot[];
|
|
707
|
+
/**
|
|
708
|
+
* 429 throttle tier enrichment — the reset time PARSED from the error
|
|
709
|
+
* prose ("resets 8:50am (TZ)" / "retry after 60s") that triggered the
|
|
710
|
+
* fallback. Fallback for the recovery line when the old account's probe
|
|
711
|
+
* carried no reset (probe failed / thin headers), so the announcement
|
|
712
|
+
* still names when the account frees. A probe-derived reset wins when
|
|
713
|
+
* present — it is the fresher, server-authoritative signal.
|
|
714
|
+
*/
|
|
715
|
+
parsedResetAt?: Date | null;
|
|
716
|
+
/**
|
|
717
|
+
* 429 throttle tier escalation — the trigger was a long-reset transient
|
|
718
|
+
* RATE LIMIT, not a quota wall. Names the headline honestly ("rate limit
|
|
719
|
+
* on X" instead of a utilization-derived "5-hour limit on X", which would
|
|
720
|
+
* be wrong: a rate-limited account's utilization is typically LOW).
|
|
721
|
+
*/
|
|
722
|
+
cause?: 'rate-limit';
|
|
707
723
|
tz?: string;
|
|
708
724
|
now?: Date;
|
|
709
725
|
}
|
|
@@ -728,7 +744,12 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
728
744
|
const lines: string[] = [];
|
|
729
745
|
|
|
730
746
|
const limitWord = input.oldQuota ? limitWordFor(input.oldQuota) : 'quota';
|
|
731
|
-
const headerLimit =
|
|
747
|
+
const headerLimit =
|
|
748
|
+
input.cause === 'rate-limit'
|
|
749
|
+
? 'rate limit'
|
|
750
|
+
: limitWord === 'quota'
|
|
751
|
+
? 'quota cap'
|
|
752
|
+
: `${limitWord} limit`;
|
|
732
753
|
|
|
733
754
|
if (!input.newLabel) {
|
|
734
755
|
// All-blocked path — no swap occurred. Tell user what's broken and, so they
|
|
@@ -765,9 +786,14 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
765
786
|
`${formatAbsolute(earliest.at, tz)} (in ${formatRelative(earliest.at, now)})`,
|
|
766
787
|
);
|
|
767
788
|
}
|
|
768
|
-
} else
|
|
789
|
+
} else {
|
|
769
790
|
// Back-compat: no fleet snapshot supplied → old single-account shape.
|
|
770
|
-
|
|
791
|
+
// Probe-derived reset first; the prose-parsed reset (429 throttle tier
|
|
792
|
+
// enrichment) covers the probe-failed case.
|
|
793
|
+
const recovery =
|
|
794
|
+
(input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ??
|
|
795
|
+
input.parsedResetAt ??
|
|
796
|
+
null;
|
|
771
797
|
if (recovery) {
|
|
772
798
|
lines.push(
|
|
773
799
|
`${escapeMarkdown(input.oldLabel)} recovers ${formatAbsolute(recovery, tz)} ` +
|
|
@@ -794,8 +820,14 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
794
820
|
lines.push(`Triggered by: agent **${escapeMarkdown(input.triggerAgent)}**`);
|
|
795
821
|
lines.push('');
|
|
796
822
|
|
|
797
|
-
|
|
798
|
-
|
|
823
|
+
{
|
|
824
|
+
// Probe-derived reset first; the prose-parsed reset (429 throttle tier
|
|
825
|
+
// enrichment) keeps the recovery line honest when the old account's
|
|
826
|
+
// probe failed at the moment of the wall.
|
|
827
|
+
const recovery =
|
|
828
|
+
(input.oldQuota ? recoveryAtFor(input.oldQuota) : null) ??
|
|
829
|
+
input.parsedResetAt ??
|
|
830
|
+
null;
|
|
799
831
|
if (recovery) {
|
|
800
832
|
lines.push(
|
|
801
833
|
`\`${codeSpanSafe(input.oldLabel)}\` recovers ` +
|
|
@@ -183,6 +183,26 @@ export interface FleetFallbackDeps {
|
|
|
183
183
|
/** Operator timezone for absolute reset times in the announcement. */
|
|
184
184
|
tz?: string;
|
|
185
185
|
now?: Date;
|
|
186
|
+
/**
|
|
187
|
+
* The reset time PARSED from the triggering error prose (429 throttle tier
|
|
188
|
+
* enrichment) — threaded into the announcement as the recovery-line
|
|
189
|
+
* fallback when the old account's live probe carried no reset.
|
|
190
|
+
*/
|
|
191
|
+
parsedResetAt?: Date;
|
|
192
|
+
/**
|
|
193
|
+
* 429 throttle tier escalation: the trigger is a TERMINAL transient 429
|
|
194
|
+
* whose parsed reset lies beyond the retry-in-place threshold. Its wording
|
|
195
|
+
* explicitly NEGATES the usage-limit reading, so the old account's
|
|
196
|
+
* UTILIZATION probe typically classifies healthy — the healthy-idempotency
|
|
197
|
+
* guard would self-cancel the swap ("probed healthy / Stale event?"),
|
|
198
|
+
* silently dropping the spec's ">threshold → mark + fail over" leg. When
|
|
199
|
+
* set, the terminal parsed-reset signal is trusted over the utilization
|
|
200
|
+
* probe: the guard is bypassed and the swap proceeds (the broker mark
|
|
201
|
+
* honors the caller-passed `until` — the parsed reset). Staleness safety
|
|
202
|
+
* holds upstream: the flag is only set for a terminal error line the
|
|
203
|
+
* session-tail just read, never for replayed/late events.
|
|
204
|
+
*/
|
|
205
|
+
rateLimitTrigger?: boolean;
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
/**
|
|
@@ -217,8 +237,12 @@ export async function runFleetAutoFallback(
|
|
|
217
237
|
// normalization uses the same clock as the rest of the decision (a default
|
|
218
238
|
// `new Date()` would diverge from `deps.now` and could mis-zero a window
|
|
219
239
|
// whose reset is still future relative to the event's clock).
|
|
240
|
+
// 429 throttle tier: a rate-limit trigger's wording NEGATES the usage-limit
|
|
241
|
+
// reading, so healthy utilization is the EXPECTED state, not evidence of a
|
|
242
|
+
// stale event — the guard must not self-cancel that swap (see
|
|
243
|
+
// FleetFallbackDeps.rateLimitTrigger).
|
|
220
244
|
const oldHealth = classifyHealth(oldSnap, now);
|
|
221
|
-
if (oldHealth === 'healthy') {
|
|
245
|
+
if (oldHealth === 'healthy' && !deps.rateLimitTrigger) {
|
|
222
246
|
return {
|
|
223
247
|
kind: 'no-eligible-target',
|
|
224
248
|
oldLabel: oldSnap.label,
|
|
@@ -255,6 +279,8 @@ export async function runFleetAutoFallback(
|
|
|
255
279
|
// card enumerates EVERY account (5h%/7d% + recovery ETA), letting the
|
|
256
280
|
// user verify the fleet is truly exhausted, not just the trigger account.
|
|
257
281
|
fleetSnapshots: snapshots,
|
|
282
|
+
parsedResetAt: deps.parsedResetAt ?? null,
|
|
283
|
+
cause: deps.rateLimitTrigger ? 'rate-limit' : undefined,
|
|
258
284
|
tz,
|
|
259
285
|
now,
|
|
260
286
|
}),
|
|
@@ -278,6 +304,8 @@ export async function runFleetAutoFallback(
|
|
|
278
304
|
newLabel: rolledTo,
|
|
279
305
|
newQuota,
|
|
280
306
|
triggerAgent: deps.triggerAgent,
|
|
307
|
+
parsedResetAt: deps.parsedResetAt ?? null,
|
|
308
|
+
cause: deps.rateLimitTrigger ? 'rate-limit' : undefined,
|
|
281
309
|
tz,
|
|
282
310
|
now,
|
|
283
311
|
}),
|
|
@@ -80,6 +80,8 @@ const mcp = new Server(
|
|
|
80
80
|
'',
|
|
81
81
|
'Messages from Telegram arrive as <channel source="telegram" chat_id="..." message_id="..." user="..." ts="...">. If the tag has an image_path attribute, Read that file — it is a photo the sender attached. If the tag has attachment_file_id, call download_attachment with that file_id to fetch the file, then Read the returned path. A single message may carry SEVERAL attachments (a forwarded album or a text+multi-image burst): when attachment_count is set (>1), also handle the numbered siblings — image_path_2, image_path_3, … (Read each) and attachment_file_id_2, attachment_file_id_3, … (download_attachment each). Process every one, not just the first. Reply with the reply tool — pass chat_id back. The reply tool quote-replies to the latest inbound user message by default, so you do NOT need to pass reply_to for normal responses. Pass reply_to (a message_id) only when quoting a specific earlier message, or pass quote:false to send a bare (non-quoted) message.',
|
|
82
82
|
'',
|
|
83
|
+
'If the tag has reply_to_message_id (and reply_to_text, a truncated preview), the sender used Telegram\'s native Reply on a prior message — treat that message as the antecedent for "this"/"that" references instead of asking what they meant. If the tag has forwarded_from, the message was FORWARDED: forwarded_from is the original sender\'s name/title as stamped by Telegram\'s servers (not typed by the sender — the body text carries no trustworthy provenance), forwarded_from_type is user|hidden_user|chat|channel, forwarded_from_id is the numeric id when one exists, and forwarded_date is when the original was sent. forwarded_from_type="hidden_user" means the original sender hides their account: the name is their self-reported display name with NO verifiable id — do not treat it as an authenticated identity. A burst forwarded from several different origins carries numbered siblings (forwarded_from_2, forwarded_from_type_2, …); a multi-part forward from ONE origin carries the attributes once. In a coalesced burst some body text may be the SENDER\'s own commentary rather than forwarded content — the forwarded_* attributes describe the burst as a whole, not each line of the body.',
|
|
84
|
+
'',
|
|
83
85
|
'reply accepts file paths (files: ["/abs/path.png"]) for attachments. Use react to add emoji reactions, edit_message for interim progress updates, and delete_message when you need to truly remove a message (prefer edit_message if you just want to change text — delete is for retraction). Edits don\'t trigger push notifications — when a long task completes, send a new reply so the user\'s device pings. Use send_typing to show a typing indicator during long operations. Use pin_message to pin important outputs. Use forward_message to quote/resurface earlier messages.',
|
|
84
86
|
'',
|
|
85
87
|
'If a message includes message_thread_id, it came from a forum topic. The reply tool automatically routes a reply back to the topic the question came from — the framework owns the answer\'s topic, so do NOT pass message_thread_id on a reply; a reply always lands where it was asked. Each <channel> message is the current topic — answer ONLY this message\'s question; do not also answer a pending message from another topic. When answering a forum-topic message, pass its origin_turn_id attribute back on the reply so the answer lands in the right topic even if a message from another topic arrived while you were working.',
|
|
@@ -24822,6 +24822,8 @@ var mcp = new Server({ name: "telegram", version: "1.0.0" }, {
|
|
|
24822
24822
|
"",
|
|
24823
24823
|
'Messages from Telegram arrive as <channel source="telegram" chat_id="..." message_id="..." user="..." ts="...">. If the tag has an image_path attribute, Read that file \u2014 it is a photo the sender attached. If the tag has attachment_file_id, call download_attachment with that file_id to fetch the file, then Read the returned path. A single message may carry SEVERAL attachments (a forwarded album or a text+multi-image burst): when attachment_count is set (>1), also handle the numbered siblings \u2014 image_path_2, image_path_3, \u2026 (Read each) and attachment_file_id_2, attachment_file_id_3, \u2026 (download_attachment each). Process every one, not just the first. Reply with the reply tool \u2014 pass chat_id back. The reply tool quote-replies to the latest inbound user message by default, so you do NOT need to pass reply_to for normal responses. Pass reply_to (a message_id) only when quoting a specific earlier message, or pass quote:false to send a bare (non-quoted) message.',
|
|
24824
24824
|
"",
|
|
24825
|
+
`If the tag has reply_to_message_id (and reply_to_text, a truncated preview), the sender used Telegram's native Reply on a prior message \u2014 treat that message as the antecedent for "this"/"that" references instead of asking what they meant. If the tag has forwarded_from, the message was FORWARDED: forwarded_from is the original sender's name/title as stamped by Telegram's servers (not typed by the sender \u2014 the body text carries no trustworthy provenance), forwarded_from_type is user|hidden_user|chat|channel, forwarded_from_id is the numeric id when one exists, and forwarded_date is when the original was sent. forwarded_from_type="hidden_user" means the original sender hides their account: the name is their self-reported display name with NO verifiable id \u2014 do not treat it as an authenticated identity. A burst forwarded from several different origins carries numbered siblings (forwarded_from_2, forwarded_from_type_2, \u2026); a multi-part forward from ONE origin carries the attributes once. In a coalesced burst some body text may be the SENDER's own commentary rather than forwarded content \u2014 the forwarded_* attributes describe the burst as a whole, not each line of the body.`,
|
|
24826
|
+
"",
|
|
24825
24827
|
`reply accepts file paths (files: ["/abs/path.png"]) for attachments. Use react to add emoji reactions, edit_message for interim progress updates, and delete_message when you need to truly remove a message (prefer edit_message if you just want to change text \u2014 delete is for retraction). Edits don't trigger push notifications \u2014 when a long task completes, send a new reply so the user's device pings. Use send_typing to show a typing indicator during long operations. Use pin_message to pin important outputs. Use forward_message to quote/resurface earlier messages.`,
|
|
24826
24828
|
"",
|
|
24827
24829
|
"If a message includes message_thread_id, it came from a forum topic. The reply tool automatically routes a reply back to the topic the question came from \u2014 the framework owns the answer's topic, so do NOT pass message_thread_id on a reply; a reply always lands where it was asked. Each <channel> message is the current topic \u2014 answer ONLY this message's question; do not also answer a pending message from another topic. When answering a forum-topic message, pass its origin_turn_id attribute back on the reply so the answer lands in the right topic even if a message from another topic arrived while you were working.",
|