switchroom 0.16.23 → 0.16.27
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 +80 -80
- package/dist/auth-broker/index.js +80 -80
- package/dist/cli/autoaccept-poll.js +8 -8
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/notion-write-pretool.mjs +82 -82
- package/dist/cli/self-improve-apply-guard-pretool.mjs +6 -0
- package/dist/cli/skill-validate-pretool.mjs +2936 -119
- package/dist/cli/switchroom.js +804 -465
- package/dist/host-control/main.js +169 -163
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +83 -83
- package/package.json +4 -4
- package/telegram-plugin/answer-stream.ts +20 -49
- package/telegram-plugin/auth-snapshot-format.ts +27 -30
- package/telegram-plugin/auto-fallback-fleet.ts +6 -11
- package/telegram-plugin/bridge/bridge.ts +1 -1
- package/telegram-plugin/card-format.ts +28 -25
- package/telegram-plugin/credits-watch.ts +5 -10
- package/telegram-plugin/dist/bridge/bridge.js +113 -113
- package/telegram-plugin/dist/gateway/gateway.js +2085 -2102
- package/telegram-plugin/dist/server.js +161 -161
- package/telegram-plugin/draft-stream.ts +4 -4
- package/telegram-plugin/format.ts +427 -680
- package/telegram-plugin/gateway/approval-callback.ts +2 -3
- package/telegram-plugin/gateway/approval-card.test.ts +17 -4
- package/telegram-plugin/gateway/approval-card.ts +16 -6
- package/telegram-plugin/gateway/approvals-commands.ts +18 -24
- package/telegram-plugin/gateway/auth-command.ts +74 -74
- package/telegram-plugin/gateway/auth-line.ts +5 -15
- package/telegram-plugin/gateway/boot-card.ts +20 -22
- package/telegram-plugin/gateway/boot-version.ts +3 -2
- package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
- package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
- package/telegram-plugin/gateway/config-snapshot.ts +9 -9
- package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
- package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
- package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
- package/telegram-plugin/gateway/drive-write-approval.test.ts +10 -10
- package/telegram-plugin/gateway/drive-write-approval.ts +14 -8
- package/telegram-plugin/gateway/effort-command.ts +17 -17
- package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
- package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
- package/telegram-plugin/gateway/gateway.ts +881 -633
- package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
- package/telegram-plugin/gateway/inject-handler.ts +5 -5
- package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
- package/telegram-plugin/gateway/ipc-server.ts +39 -6
- package/telegram-plugin/gateway/linear-activity.ts +16 -14
- package/telegram-plugin/gateway/linear-setup.ts +1 -1
- package/telegram-plugin/gateway/model-command.ts +25 -25
- package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
- package/telegram-plugin/gateway/permission-timeout.ts +76 -0
- package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
- package/telegram-plugin/issues-card.ts +6 -7
- package/telegram-plugin/model-unavailable.ts +8 -12
- package/telegram-plugin/operator-events-history.ts +1 -1
- package/telegram-plugin/operator-events.ts +24 -28
- package/telegram-plugin/package.json +1 -1
- package/telegram-plugin/pending-work-progress.ts +36 -36
- package/telegram-plugin/permission-title.ts +39 -20
- package/telegram-plugin/pty-partial-handler.ts +5 -13
- package/telegram-plugin/quota-check.ts +5 -5
- package/telegram-plugin/quota-watch.ts +13 -18
- package/telegram-plugin/recent-outbound-dedup.ts +5 -5
- package/telegram-plugin/registry/turns-schema.ts +43 -3
- package/telegram-plugin/retry-api-call.ts +42 -7
- package/telegram-plugin/rich-send.ts +86 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
- package/telegram-plugin/secret-detect/vault-error.ts +29 -22
- package/telegram-plugin/shared/bot-runtime.ts +29 -7
- package/telegram-plugin/silence-poke.ts +26 -69
- package/telegram-plugin/silent-reply-anchor.ts +9 -2
- package/telegram-plugin/slot-banner-driver.ts +9 -6
- package/telegram-plugin/slot-banner.ts +5 -8
- package/telegram-plugin/status-no-truncate.ts +11 -5
- package/telegram-plugin/steering.ts +0 -4
- package/telegram-plugin/stream-controller.ts +59 -62
- package/telegram-plugin/stream-reply-handler.ts +49 -98
- package/telegram-plugin/subagent-watcher.ts +2 -2
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
- package/telegram-plugin/tests/answer-stream.test.ts +54 -63
- package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
- package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/bot-api.harness.ts +23 -1
- package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
- package/telegram-plugin/tests/card-format.test.ts +6 -4
- package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
- package/telegram-plugin/tests/credits-watch.test.ts +5 -5
- package/telegram-plugin/tests/fake-bot-api.ts +58 -4
- package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
- package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
- package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +6 -2
- package/telegram-plugin/tests/issues-card.test.ts +15 -12
- package/telegram-plugin/tests/length-error-classify.test.ts +131 -0
- package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
- package/telegram-plugin/tests/model-command.test.ts +2 -2
- package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
- package/telegram-plugin/tests/operator-events.test.ts +7 -9
- package/telegram-plugin/tests/paragraph-normalizer.test.ts +273 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +12 -2
- package/telegram-plugin/tests/permission-timeout.test.ts +77 -0
- package/telegram-plugin/tests/permission-title.test.ts +88 -41
- package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
- package/telegram-plugin/tests/quota-check.test.ts +3 -3
- package/telegram-plugin/tests/quota-watch.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
- package/telegram-plugin/tests/silence-poke.test.ts +75 -112
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
- package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
- package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
- package/telegram-plugin/tests/slot-banner.test.ts +9 -6
- package/telegram-plugin/tests/status-accent.test.ts +29 -32
- package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts → stream-controller-parse-fallback.test.ts} +40 -42
- package/telegram-plugin/tests/stream-controller.test.ts +63 -52
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
- package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
- package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
- package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
- package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
- package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
- package/telegram-plugin/tests/welcome-text.test.ts +72 -65
- package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
- package/telegram-plugin/text-voice-scrub.ts +8 -11
- package/telegram-plugin/tool-activity-summary.ts +29 -29
- package/telegram-plugin/welcome-text.ts +82 -83
- package/telegram-plugin/worker-activity-feed.ts +2 -3
- package/telegram-plugin/html-sanitize.ts +0 -244
- package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
|
@@ -127,10 +127,17 @@ export interface VaultErrorRendering {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
function htmlEscape(s: string): string {
|
|
130
|
-
return s
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Make a string safe to interpolate INSIDE a `code span` without escaping —
|
|
135
|
+
* content there is literal, so markdown-escaping a key would leak visible
|
|
136
|
+
* backslashes (e.g. `my\_key`). Only a backtick can prematurely close the
|
|
137
|
+
* span, so split it with a zero-width space (#2669).
|
|
138
|
+
*/
|
|
139
|
+
function codeSpanSafe(s: string): string {
|
|
140
|
+
return s.replace(/`/g, "`");
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
/**
|
|
@@ -173,13 +180,13 @@ export function renderVaultCliError(
|
|
|
173
180
|
return {
|
|
174
181
|
suppressRaw: true,
|
|
175
182
|
html:
|
|
176
|
-
`⚠️
|
|
183
|
+
`⚠️ **New vault key — operator approval required.**\n` +
|
|
177
184
|
(key
|
|
178
|
-
? `The agent tried to save
|
|
185
|
+
? `The agent tried to save \`${codeSpanSafe(key)}\`, but `
|
|
179
186
|
: `The agent tried to save a new key, but `) +
|
|
180
187
|
`agents can only rotate existing keys via the broker; introducing ` +
|
|
181
188
|
`a new key needs an operator action.\n\n` +
|
|
182
|
-
|
|
189
|
+
`**Ask the agent** to call its \`vault_request_save\` ` +
|
|
183
190
|
`tool — it'll render an approval card in this chat with ` +
|
|
184
191
|
`[✅ Save once] [🚫 Discard] [✏️ Rename] buttons. One tap saves the ` +
|
|
185
192
|
`secret to the vault; no re-paste needed.`,
|
|
@@ -194,12 +201,12 @@ export function renderVaultCliError(
|
|
|
194
201
|
return {
|
|
195
202
|
suppressRaw: true,
|
|
196
203
|
html:
|
|
197
|
-
`⚠️
|
|
204
|
+
`⚠️ **Vault broker isn't reachable.**\n` +
|
|
198
205
|
`From inside the agent sandbox there's no fallback path. ` +
|
|
199
206
|
`Telegram-native broker recovery is tracked as a follow-up — ` +
|
|
200
207
|
`for now, on the host:\n` +
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
"```\nswitchroom vault broker status\n```\n" +
|
|
209
|
+
`_Or, if the broker is wedged: \`docker compose -p switchroom restart vault-broker\`._`,
|
|
203
210
|
};
|
|
204
211
|
case "broker_denied":
|
|
205
212
|
// Telegram-native grant flow shipped in #969 P2b + #1012:
|
|
@@ -210,14 +217,14 @@ export function renderVaultCliError(
|
|
|
210
217
|
return {
|
|
211
218
|
suppressRaw: true,
|
|
212
219
|
html:
|
|
213
|
-
`⚠️
|
|
220
|
+
`⚠️ **Vault broker refused the request.**\n` +
|
|
214
221
|
(key
|
|
215
|
-
? `The agent isn't authorized to access
|
|
222
|
+
? `The agent isn't authorized to access \`${codeSpanSafe(key)}\`. `
|
|
216
223
|
: `The agent isn't authorized to access this key. `) +
|
|
217
224
|
`Grant access in two taps:\n` +
|
|
218
|
-
`•
|
|
225
|
+
`• \`/vault audit <agent>\` in this chat → tap ` +
|
|
219
226
|
`[🔓 Allow${key ? ` ${htmlEscape(key)}` : ""}] on the recent denial, OR\n` +
|
|
220
|
-
`• ask the agent to call
|
|
227
|
+
`• ask the agent to call \`vault_request_access\` — ` +
|
|
221
228
|
`an approval card lands here with [✅ Approve] / [🚫 Deny].`,
|
|
222
229
|
};
|
|
223
230
|
case "other":
|
|
@@ -234,36 +241,36 @@ function renderSandboxContextSuggestion(
|
|
|
234
241
|
verb: "set" | "get" | "list" | "init" | "remove" | "save",
|
|
235
242
|
key: string | undefined,
|
|
236
243
|
): string {
|
|
237
|
-
const head = `⚠️
|
|
244
|
+
const head = `⚠️ **Direct vault file IO isn't available from inside the agent sandbox.**\n`;
|
|
238
245
|
switch (verb) {
|
|
239
246
|
case "set":
|
|
240
247
|
case "save":
|
|
241
248
|
return (
|
|
242
249
|
head +
|
|
243
250
|
(key
|
|
244
|
-
?
|
|
245
|
-
`tool for
|
|
251
|
+
? `**Ask the agent** to call its \`vault_request_save\` ` +
|
|
252
|
+
`tool for \`${codeSpanSafe(key)}\` — an approval card ` +
|
|
246
253
|
`lands here with [✅ Save once] / [🚫 Discard] / [✏️ Rename] buttons.`
|
|
247
|
-
:
|
|
254
|
+
: `**Ask the agent** to call its \`vault_request_save\` ` +
|
|
248
255
|
`tool — an approval card lands here with [✅ Save once] / [🚫 Discard] / [✏️ Rename] buttons.`)
|
|
249
256
|
);
|
|
250
257
|
case "get":
|
|
251
258
|
return (
|
|
252
259
|
head +
|
|
253
|
-
`From this chat:
|
|
260
|
+
`From this chat: \`/vault get${key ? ` ${codeSpanSafe(key)}` : " <key>"}\``
|
|
254
261
|
);
|
|
255
262
|
case "list":
|
|
256
|
-
return head + `From this chat:
|
|
263
|
+
return head + `From this chat: \`/vault list\``;
|
|
257
264
|
case "remove":
|
|
258
265
|
return (
|
|
259
266
|
head +
|
|
260
267
|
`Use the operator host CLI for now — Telegram-native delete is tracked as a follow-up. ` +
|
|
261
|
-
|
|
268
|
+
`_(Removing a vault key is a rare, irreversible operation; an in-chat confirmation card is on the punch list.)_`
|
|
262
269
|
);
|
|
263
270
|
case "init":
|
|
264
271
|
return (
|
|
265
272
|
head +
|
|
266
|
-
`Vault bootstrap is a one-time host-shell step:
|
|
273
|
+
`Vault bootstrap is a one-time host-shell step: \`switchroom vault init\`.`
|
|
267
274
|
);
|
|
268
275
|
}
|
|
269
276
|
}
|
|
@@ -29,6 +29,7 @@ import { createHash } from 'crypto'
|
|
|
29
29
|
import { AsyncLocalStorage } from 'async_hooks'
|
|
30
30
|
import { clearStaleTelegramPollingState } from '../startup-reset.js'
|
|
31
31
|
import { createRetryApiCall } from '../retry-api-call.js'
|
|
32
|
+
import { RICH_MESSAGE_MAX_CHARS } from '../format.js'
|
|
32
33
|
|
|
33
34
|
// ─── tg-post tag plumbing ─────────────────────────────────────────────────
|
|
34
35
|
|
|
@@ -154,21 +155,36 @@ export function createRobustApiCall() {
|
|
|
154
155
|
})
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
// ───
|
|
158
|
+
// ─── Markdown escape helpers (#2669) ──────────────────────────────────────
|
|
158
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Escape GFM-markdown specials in a dynamic value interpolated into prose.
|
|
162
|
+
* Kept under the legacy `escapeHtmlForTg` name so callers don't churn.
|
|
163
|
+
*/
|
|
159
164
|
export function escapeHtmlForTg(text: string): string {
|
|
160
|
-
return text.replace(
|
|
165
|
+
return text.replace(/([\\`*_~=\[\]|])/g, '\\$1')
|
|
161
166
|
}
|
|
162
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Wrap CLI / command output in a fenced code block. Inside a fence the
|
|
170
|
+
* content is literal (no escaping), so we pass `text` through verbatim —
|
|
171
|
+
* except a fence-closing ``` sequence in the content, which we defuse so it
|
|
172
|
+
* can't terminate the block early.
|
|
173
|
+
*/
|
|
163
174
|
export function preBlock(text: string): string {
|
|
164
|
-
|
|
175
|
+
const safe = text.replace(/```/g, '```')
|
|
176
|
+
return '```\n' + safe + '\n```'
|
|
165
177
|
}
|
|
166
178
|
|
|
167
179
|
export function stripAnsi(text: string): string {
|
|
168
180
|
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
169
181
|
}
|
|
170
182
|
|
|
171
|
-
|
|
183
|
+
// Default truncation budget for CLI output bound for Telegram. Post-#2669 the
|
|
184
|
+
// rich-message wire cap is RICH_MESSAGE_MAX_CHARS (32768), not the legacy 4096
|
|
185
|
+
// plain-text limit; the preBlock fence framing (~8 chars) easily fits the
|
|
186
|
+
// remaining headroom.
|
|
187
|
+
export function formatSwitchroomOutput(output: string, maxLen = RICH_MESSAGE_MAX_CHARS): string {
|
|
172
188
|
const trimmed = output.trim()
|
|
173
189
|
if (trimmed.length <= maxLen) return trimmed
|
|
174
190
|
return trimmed.slice(0, maxLen - 20) + '\n... (truncated)'
|
|
@@ -278,11 +294,17 @@ export function makeSwitchroomReply(
|
|
|
278
294
|
): Promise<void> {
|
|
279
295
|
const chatId = String(ctx.chat!.id)
|
|
280
296
|
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id)
|
|
281
|
-
|
|
297
|
+
const opts = {
|
|
282
298
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
283
|
-
...(options.html ? { parse_mode: 'HTML' as const, link_preview_options: { is_disabled: true } } : {}),
|
|
284
299
|
...(options.reply_markup ? { reply_markup: options.reply_markup } : {}),
|
|
285
|
-
}
|
|
300
|
+
}
|
|
301
|
+
// #2669: `options.html:true` now means "render `text` as GFM markdown via
|
|
302
|
+
// the rich-message path" (legacy field name kept). Plain otherwise.
|
|
303
|
+
if (options.html) {
|
|
304
|
+
await ctx.replyWithRichMessage({ markdown: text }, opts)
|
|
305
|
+
} else {
|
|
306
|
+
await ctx.reply(text, opts)
|
|
307
|
+
}
|
|
286
308
|
}
|
|
287
309
|
}
|
|
288
310
|
|
|
@@ -377,17 +377,20 @@ export function silenceMsForKey(key: string, now: number): number | null {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
380
|
+
* Framework-fallback text for the 300s silence threshold. Returns `null` for
|
|
381
|
+
* the pure-stall cases (the "still working / still thinking" / "running <Tool>
|
|
382
|
+
* for Nm" notice) — that timer-fired stall ping was a stop-gap from before the
|
|
383
|
+
* live-updating reply/draft carried the progress beats natively, and the
|
|
384
|
+
* operator has retired it (it's the exact cadence-based "still working" update
|
|
385
|
+
* the conversational-pacing RFC's Anti-patterns section bans). The 300s
|
|
386
|
+
* fallback's surviving job — unwedging a turn that produced no output at all —
|
|
387
|
+
* is the gateway's turn-teardown after this call, NOT a user-visible message,
|
|
388
|
+
* so dropping the stall send keeps the safety net's real work intact (see
|
|
389
|
+
* `reference/rfcs/conversational-pacing.md` § Silence-poke fallback).
|
|
384
390
|
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* 2. The verb is `working` by default, `thinking` only when the session
|
|
389
|
-
* stream has emitted a `kind: 'thinking'` event in the last 30s. Picked
|
|
390
|
-
* by the caller via `fallbackKind`; this helper just formats.
|
|
391
|
+
* The ONE case that still returns a string is `blockedOnApproval`: the turn is
|
|
392
|
+
* parked on an approval card waiting for YOUR tap. That is not a stall — it
|
|
393
|
+
* tells the user the ball is in their court — so it keeps pinging.
|
|
391
394
|
*
|
|
392
395
|
* Extracted from the gateway's `onFrameworkFallback` callback so the wording
|
|
393
396
|
* can be snapshot-tested in isolation. CC-4 in `docs/status-ask-cause-classes.md`.
|
|
@@ -397,73 +400,27 @@ export function formatFrameworkFallbackText(
|
|
|
397
400
|
silenceMs: number,
|
|
398
401
|
inFlightTools: ToolSnapshot[] = [],
|
|
399
402
|
blockedOnApproval = false,
|
|
400
|
-
): string {
|
|
403
|
+
): string | null {
|
|
401
404
|
const minutes = Math.max(1, Math.round(silenceMs / 60_000))
|
|
402
405
|
// The turn isn't stalled — it's parked on an approval card waiting for YOUR
|
|
403
406
|
// tap (the dominant live "wedge" class is benign approval-latency, not a
|
|
404
407
|
// hang). Saying "still working…" here actively lies; name the real blocker so
|
|
405
408
|
// the operator knows the ball is in their court. Takes precedence over the
|
|
406
|
-
// in-flight-tool framing (a tool awaiting approval isn't "running").
|
|
409
|
+
// in-flight-tool framing (a tool awaiting approval isn't "running"). This is
|
|
410
|
+
// the only branch that still emits a user-visible message.
|
|
407
411
|
if (blockedOnApproval) {
|
|
408
412
|
return `waiting for your approval — tap Approve or Deny on the card above (${minutes} min)`
|
|
409
413
|
}
|
|
410
|
-
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
// and look like a leak when surfaced to a user. When the tool name
|
|
421
|
-
// matches that shape AND a human-friendly label is available, drop
|
|
422
|
-
// the raw name and lead with the label instead:
|
|
423
|
-
// Searching memory for 4m (no update from agent in 5 min)
|
|
424
|
-
// Built-in tool names (Grep, Read, Bash) stay as-is — they ARE
|
|
425
|
-
// human-readable, and the label is supplementary detail (e.g. the
|
|
426
|
-
// search pattern) that reads naturally after the verb.
|
|
427
|
-
if (inFlightTools.length > 0) {
|
|
428
|
-
const longest = inFlightTools[0]!
|
|
429
|
-
const dur = formatDurationShort(longest.durationMs)
|
|
430
|
-
const labelTail = longest.label && longest.label.length > 0
|
|
431
|
-
? ` ${truncateLabel(longest.label)}`
|
|
432
|
-
: ''
|
|
433
|
-
const more = inFlightTools.length > 1
|
|
434
|
-
? ` + ${inFlightTools.length - 1} more`
|
|
435
|
-
: ''
|
|
436
|
-
const isMcpRawName = /^mcp__/.test(longest.name)
|
|
437
|
-
if (isMcpRawName && labelTail !== '') {
|
|
438
|
-
// Label-only: "Searching memory for 4m (…)". Drop the raw
|
|
439
|
-
// `mcp__server__tool` and the leading "running" because the
|
|
440
|
-
// label already reads as a gerund phrase.
|
|
441
|
-
return `${truncateLabel(longest.label!)}${more} for ${dur} ${suffix}`
|
|
442
|
-
}
|
|
443
|
-
return `running ${longest.name}${labelTail}${more} for ${dur} ${suffix}`
|
|
444
|
-
}
|
|
445
|
-
return fallbackKind === 'thinking'
|
|
446
|
-
? `still thinking… ${suffix}`
|
|
447
|
-
: `still working… ${suffix}`
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** Compact m/s rendering for the fallback message. Anything under a
|
|
451
|
-
* minute reads as `${s}s`, otherwise `${m}m`. Always rounds toward the
|
|
452
|
-
* user-honest direction — "4m" for 4m 30s, "5m" for 4m 45s. */
|
|
453
|
-
function formatDurationShort(ms: number): string {
|
|
454
|
-
const totalSec = Math.max(0, Math.round(ms / 1000))
|
|
455
|
-
if (totalSec < 60) return `${totalSec}s`
|
|
456
|
-
const minutes = Math.round(totalSec / 60)
|
|
457
|
-
return `${minutes}m`
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/** Telegram lines are short on mobile. Clip the label to keep the
|
|
461
|
-
* fallback message readable. Truncation point is generous (60 chars)
|
|
462
|
-
* because tool labels are pre-truncated by `toolLabel()` already. */
|
|
463
|
-
function truncateLabel(label: string): string {
|
|
464
|
-
const MAX = 60
|
|
465
|
-
if (label.length <= MAX) return label
|
|
466
|
-
return label.slice(0, MAX - 1) + '…'
|
|
414
|
+
// Stop-gap retired: the "still working… (no update from agent in N min)" /
|
|
415
|
+
// "running <Tool> for Nm" stall notice (including the #1292 tool-aware
|
|
416
|
+
// enrichment) no longer sends. The live draft + the model's own pacing beats
|
|
417
|
+
// carry progress; a timer-fired stall ping on top of that is the banned
|
|
418
|
+
// cadence-based update. `fallbackKind` and `inFlightTools` are now unused for
|
|
419
|
+
// the stall path but kept on the signature so the gateway's call sites — and
|
|
420
|
+
// the deterministic update-status / unwedge paths around them — are untouched.
|
|
421
|
+
void fallbackKind
|
|
422
|
+
void inFlightTools
|
|
423
|
+
return null
|
|
467
424
|
}
|
|
468
425
|
|
|
469
426
|
/** Snapshot in-flight tools sorted longest-running first — for the honest
|
|
@@ -38,8 +38,15 @@
|
|
|
38
38
|
* the safety net off; reverts to per-reply fresh send.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
import { RICH_MESSAGE_MAX_CHARS } from './format.js'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Telegram rich-message text limit. The accumulator stays under this before
|
|
45
|
+
* it rolls to a fresh anchor. Post-#2669 every reply renders as GFM markdown
|
|
46
|
+
* via `sendRichMessage`, so the cap is `RICH_MESSAGE_MAX_CHARS` (32768), not
|
|
47
|
+
* the legacy 4096 plain-text limit.
|
|
48
|
+
*/
|
|
49
|
+
export const TELEGRAM_MSG_CAP = RICH_MESSAGE_MAX_CHARS
|
|
43
50
|
|
|
44
51
|
export interface SilentReplyAnchorDecisionInput {
|
|
45
52
|
/** True when the model passed `disable_notification: true` for
|
|
@@ -31,10 +31,15 @@ export interface BannerBotApi {
|
|
|
31
31
|
text: string,
|
|
32
32
|
opts?: Record<string, unknown>,
|
|
33
33
|
): Promise<{ message_id: number }>;
|
|
34
|
+
sendRichMessage(
|
|
35
|
+
chat_id: string | number,
|
|
36
|
+
rich_message: { markdown: string },
|
|
37
|
+
opts?: Record<string, unknown>,
|
|
38
|
+
): Promise<{ message_id: number }>;
|
|
34
39
|
editMessageText(
|
|
35
40
|
chat_id: string | number,
|
|
36
41
|
message_id: number,
|
|
37
|
-
text: string,
|
|
42
|
+
text: string | { markdown: string },
|
|
38
43
|
opts?: Record<string, unknown>,
|
|
39
44
|
): Promise<unknown>;
|
|
40
45
|
pinChatMessage(
|
|
@@ -107,9 +112,8 @@ export async function refreshBanner(
|
|
|
107
112
|
if (action.kind === 'pin') {
|
|
108
113
|
let sent: { message_id: number };
|
|
109
114
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
link_preview_options: { is_disabled: true },
|
|
115
|
+
// sendRichMessage doesn't accept link_preview_options — omit it.
|
|
116
|
+
sent = await args.bot.api.sendRichMessage(args.ownerChatId, { markdown: action.text }, {
|
|
113
117
|
// OAuth slot banner is a status notice — silence the open ping.
|
|
114
118
|
// (the pin below is already silent; the edit path doesn't ping.)
|
|
115
119
|
disable_notification: true,
|
|
@@ -135,9 +139,8 @@ export async function refreshBanner(
|
|
|
135
139
|
await args.bot.api.editMessageText(
|
|
136
140
|
args.ownerChatId,
|
|
137
141
|
action.messageId,
|
|
138
|
-
action.text,
|
|
142
|
+
{ markdown: action.text },
|
|
139
143
|
{
|
|
140
|
-
parse_mode: 'HTML',
|
|
141
144
|
link_preview_options: { is_disabled: true },
|
|
142
145
|
},
|
|
143
146
|
);
|
|
@@ -72,15 +72,12 @@ export function formatBannerHtml(
|
|
|
72
72
|
defaultSlot: string,
|
|
73
73
|
): string {
|
|
74
74
|
return [
|
|
75
|
-
`📌
|
|
76
|
-
|
|
75
|
+
`📌 **${escapeMarkdown(agentName)}** is running on slot \`${currentSlot}\``,
|
|
76
|
+
`_(failover from \`${defaultSlot}\`)_`,
|
|
77
77
|
].join(' ');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.replace(/</g, '<')
|
|
84
|
-
.replace(/>/g, '>')
|
|
85
|
-
.replace(/"/g, '"');
|
|
80
|
+
/** Local GFM-markdown escaper (keeps this module dependency-free). */
|
|
81
|
+
function escapeMarkdown(text: string): string {
|
|
82
|
+
return text.replace(/([\\`*_~=\[\]|])/g, '\\$1');
|
|
86
83
|
}
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* holds the tuning constants that primitive (and its internal helpers)
|
|
9
9
|
* read, so a forked renderer never re-derives them.
|
|
10
10
|
*
|
|
11
|
+
* Wire cap: since the Bot API 10.1 rich-message migration (#2669) every card
|
|
12
|
+
* renders as GFM markdown via `sendRichMessage`, whose limit is
|
|
13
|
+
* `RICH_MESSAGE_MAX_CHARS` (32768), not the legacy 4096 plain-text cap.
|
|
14
|
+
*
|
|
11
15
|
* The former `SWITCHROOM_STATUS_NO_TRUNCATE` feature flag was retired:
|
|
12
16
|
* rolling-window-with-char-budget is now the only behaviour. The per-line
|
|
13
17
|
* cap (`STATUS_LINE_MAX`) and rolling window (`STATUS_ROLLING_LINES`) apply
|
|
@@ -15,6 +19,8 @@
|
|
|
15
19
|
* (`STATUS_CARD_CHAR_BUDGET`) is the wire-limit backstop.
|
|
16
20
|
*/
|
|
17
21
|
|
|
22
|
+
import { RICH_MESSAGE_MAX_CHARS } from './format.js'
|
|
23
|
+
|
|
18
24
|
/**
|
|
19
25
|
* Number of trailing narrative/step lines shown in the rolling window.
|
|
20
26
|
* The feed is a fixed-height rolling window: oldest drops off as new arrive.
|
|
@@ -30,15 +36,15 @@ export const STATUS_ROLLING_LINES = 5
|
|
|
30
36
|
export const STATUS_LINE_MAX = 200
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
|
-
* The safe char budget for a rendered Telegram status card.
|
|
34
|
-
* cap is
|
|
35
|
-
*
|
|
36
|
-
*
|
|
39
|
+
* The safe char budget for a rendered Telegram status card. The rich-message
|
|
40
|
+
* wire cap is `RICH_MESSAGE_MAX_CHARS` (32768) post-#2669 — the legacy 4096
|
|
41
|
+
* plain-text cap no longer applies on the rich path. We use the constant
|
|
42
|
+
* directly as the backstop.
|
|
37
43
|
*
|
|
38
44
|
* With STATUS_ROLLING_LINES=5 lines each ≤ STATUS_LINE_MAX this backstop
|
|
39
45
|
* effectively never fires in practice, but is kept as a wire-limit safety net.
|
|
40
46
|
*/
|
|
41
|
-
export const STATUS_CARD_CHAR_BUDGET =
|
|
47
|
+
export const STATUS_CARD_CHAR_BUDGET = RICH_MESSAGE_MAX_CHARS
|
|
42
48
|
|
|
43
49
|
/** Indent marker for a nested (foreground sub-agent) step line. */
|
|
44
50
|
export const NESTED_PREFIX = ' ↳ '
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* These helpers are pure so server.ts stays testable without standing up
|
|
10
10
|
* grammy.
|
|
11
11
|
*/
|
|
12
|
-
import { escapeHtml } from './format.js'
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Detect and strip the `/queue ` or `/q ` prefix.
|
|
@@ -181,6 +180,3 @@ export function buildChannelMetaAttributes(opts: ChannelMetaAttributeOptions): s
|
|
|
181
180
|
}
|
|
182
181
|
return parts.length === 0 ? '' : ' ' + parts.join(' ')
|
|
183
182
|
}
|
|
184
|
-
|
|
185
|
-
/** Re-export so callers that want HTML-escape for body content don't need a second import. */
|
|
186
|
-
export { escapeHtml }
|