switchroom 0.16.22 ā 0.16.24
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/self-improve-apply-guard-pretool.mjs +6 -0
- package/dist/cli/skill-validate-pretool.mjs +2865 -48
- package/dist/cli/switchroom.js +338 -92
- package/dist/host-control/main.js +1 -1
- package/package.json +5 -5
- package/telegram-plugin/answer-stream.ts +13 -43
- package/telegram-plugin/auth-snapshot-format.ts +27 -30
- package/telegram-plugin/auto-fallback-fleet.ts +6 -11
- package/telegram-plugin/card-format.ts +28 -25
- package/telegram-plugin/credits-watch.ts +5 -10
- package/telegram-plugin/dist/gateway/gateway.js +1670 -1892
- package/telegram-plugin/draft-stream.ts +4 -4
- package/telegram-plugin/format.ts +101 -662
- 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/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 +751 -624
- 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 +28 -0
- 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/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 +36 -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 +44 -2
- package/telegram-plugin/retry-api-call.ts +15 -7
- package/telegram-plugin/rich-send.ts +57 -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 +23 -6
- package/telegram-plugin/silence-poke.ts +26 -69
- package/telegram-plugin/slot-banner-driver.ts +9 -6
- package/telegram-plugin/slot-banner.ts +5 -8
- package/telegram-plugin/steering.ts +0 -4
- package/telegram-plugin/stream-controller.ts +59 -62
- package/telegram-plugin/stream-reply-handler.ts +40 -97
- 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/issues-card.test.ts +15 -12
- 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/pending-work-progress.test.ts +20 -21
- package/telegram-plugin/tests/permission-title.test.ts +45 -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
|
@@ -189,8 +189,8 @@ const REPLY_TO_TEXT_MAX = 200
|
|
|
189
189
|
const SILENT_END_FALLBACK_TEXT =
|
|
190
190
|
'ā ļø The agent finished working but didnāt send a reply ā your last ' +
|
|
191
191
|
'message may not have been answered. Please try asking again.'
|
|
192
|
-
import {
|
|
193
|
-
import {
|
|
192
|
+
import { splitMarkdownChunks, repairEscapedWhitespace, escapeMarkdown, RICH_MESSAGE_MAX_CHARS } from '../format.js'
|
|
193
|
+
import { richMessage } from '../rich-send.js'
|
|
194
194
|
import { scrubVoice } from '../text-voice-scrub.js'
|
|
195
195
|
import {
|
|
196
196
|
validateInlineKeyboard,
|
|
@@ -384,6 +384,18 @@ import {
|
|
|
384
384
|
buildVaultSaveFailedInbound,
|
|
385
385
|
buildVaultSaveDiscardedInbound,
|
|
386
386
|
} from './vault-grant-inbound-builders.js'
|
|
387
|
+
import {
|
|
388
|
+
parseSkillProposalCallback,
|
|
389
|
+
buildSkillProposalApplyInbound,
|
|
390
|
+
renderSkillProposalCard,
|
|
391
|
+
skillProposalKeyboard,
|
|
392
|
+
} from './skill-proposal-card.js'
|
|
393
|
+
import {
|
|
394
|
+
getProposal as getSkillProposal,
|
|
395
|
+
setProposalStatus as setSkillProposalStatus,
|
|
396
|
+
enqueueProposal as enqueueSkillProposal,
|
|
397
|
+
isSuppressed as isSkillProposalSuppressed,
|
|
398
|
+
} from '../../src/self-improve/skill-proposals.js'
|
|
387
399
|
import { decideSubagentHandback } from './subagent-handback-inbound-builder.js'
|
|
388
400
|
import {
|
|
389
401
|
decideSubagentProgress,
|
|
@@ -408,6 +420,7 @@ import type {
|
|
|
408
420
|
InjectInboundMessage,
|
|
409
421
|
SendOutboundMessage,
|
|
410
422
|
QuotaWallDetectedMessage,
|
|
423
|
+
PostSkillProposalMessage,
|
|
411
424
|
PermissionEvent,
|
|
412
425
|
} from './ipc-protocol.js'
|
|
413
426
|
import { DebounceBuffer, HourCap, buildReactionInboundMeta, buildReactionInboundText, evaluateTriggerCandidate, isGroupChat, resolveReactionsConfig, truncatePreview, type PendingReaction, type ReactionBatch, type ReactionsResolvedConfig } from './reaction-trigger.js'
|
|
@@ -934,7 +947,8 @@ function defaultAccess(): Access {
|
|
|
934
947
|
return { dmPolicy: 'pairing', allowFrom: [], groups: {}, pending: {} }
|
|
935
948
|
}
|
|
936
949
|
|
|
937
|
-
|
|
950
|
+
// Rich-message wire cap (#2669): the rich path allows 32768 UTF-8 chars.
|
|
951
|
+
const MAX_CHUNK_LIMIT = RICH_MESSAGE_MAX_CHARS
|
|
938
952
|
const MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024
|
|
939
953
|
|
|
940
954
|
function assertSendable(f: string): void {
|
|
@@ -1922,7 +1936,6 @@ interface OutstandingCompactCard {
|
|
|
1922
1936
|
let compactNotifyState: CompactNotifyState = idleCompactNotifyState()
|
|
1923
1937
|
let outstandingCompactCard: OutstandingCompactCard | null = null
|
|
1924
1938
|
const activeDraftStreams = new Map<string, DraftStreamHandle>()
|
|
1925
|
-
const activeDraftParseModes = new Map<string, 'HTML' | 'MarkdownV2' | undefined>()
|
|
1926
1939
|
const suppressPtyPreview = new Set<string>()
|
|
1927
1940
|
const lastPtyPreviewByChat = new Map<string, string>()
|
|
1928
1941
|
const progressUpdateLastSent = new Map<string, number>()
|
|
@@ -3662,15 +3675,14 @@ async function postCompactCard(occ: number, cap: number): Promise<void> {
|
|
|
3662
3675
|
supergroupChatId: resolveAgentSupergroupChatId(),
|
|
3663
3676
|
});
|
|
3664
3677
|
const text =
|
|
3665
|
-
`šļø
|
|
3678
|
+
`šļø **Context compaction**\n` +
|
|
3666
3679
|
`Working context hit ~${occ.toLocaleString()} tokens ` +
|
|
3667
|
-
`(cap ${cap.toLocaleString()}) ā running
|
|
3680
|
+
`(cap ${cap.toLocaleString()}) ā running \`/compact\`. ` +
|
|
3668
3681
|
`Older detail moves to Hindsight; I'll confirm here once the ` +
|
|
3669
3682
|
`context has shrunk (may take a turn or two).`;
|
|
3670
3683
|
const sent = await swallowingApiCall(
|
|
3671
3684
|
() =>
|
|
3672
|
-
bot.api.
|
|
3673
|
-
parse_mode: 'HTML',
|
|
3685
|
+
bot.api.sendRichMessage(chatId, richMessage(text), {
|
|
3674
3686
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
3675
3687
|
}),
|
|
3676
3688
|
{ chat_id: chatId, verb: 'proactiveCompact.start' },
|
|
@@ -3720,7 +3732,7 @@ async function resolveCompactCard(
|
|
|
3720
3732
|
let text: string;
|
|
3721
3733
|
if (kind === 'finished') {
|
|
3722
3734
|
text =
|
|
3723
|
-
`ā
|
|
3735
|
+
`ā
**Context compacted**\n` +
|
|
3724
3736
|
`Working context reduced` +
|
|
3725
3737
|
(occNow != null
|
|
3726
3738
|
? ` (~${card.occAtStart.toLocaleString()} ā ` +
|
|
@@ -3729,19 +3741,18 @@ async function resolveCompactCard(
|
|
|
3729
3741
|
`. Hindsight retains the detail.`;
|
|
3730
3742
|
} else if (kind === 'superseded') {
|
|
3731
3743
|
text =
|
|
3732
|
-
`ā©ļø
|
|
3744
|
+
`ā©ļø **Context compaction superseded**\n` +
|
|
3733
3745
|
`A newer compaction started before this one confirmed.`;
|
|
3734
3746
|
} else {
|
|
3735
3747
|
text =
|
|
3736
|
-
`ā ļø
|
|
3737
|
-
|
|
3748
|
+
`ā ļø **Compaction issued**\n` +
|
|
3749
|
+
`\`/compact\` was requested but the context isn't ` +
|
|
3738
3750
|
`confirmed reduced yet. Native compaction and Hindsight still apply.`;
|
|
3739
3751
|
}
|
|
3740
3752
|
try {
|
|
3741
3753
|
await swallowingApiCall(
|
|
3742
3754
|
() =>
|
|
3743
|
-
bot.api.editMessageText(card.chatId, card.messageId, text, {
|
|
3744
|
-
parse_mode: 'HTML',
|
|
3755
|
+
bot.api.editMessageText(card.chatId, card.messageId, richMessage(text), {
|
|
3745
3756
|
}),
|
|
3746
3757
|
{ chat_id: card.chatId, verb: `proactiveCompact.${kind}` },
|
|
3747
3758
|
);
|
|
@@ -3945,8 +3956,7 @@ function postPermissionResumeMessage(opts: {
|
|
|
3945
3956
|
// allow-raw-bot-api: wrapped in swallowingApiCall (retry policy); thread-aware send
|
|
3946
3957
|
void swallowingApiCall(
|
|
3947
3958
|
() =>
|
|
3948
|
-
bot.api.
|
|
3949
|
-
parse_mode: 'HTML',
|
|
3959
|
+
bot.api.sendRichMessage(chatId, richMessage(text), {
|
|
3950
3960
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
3951
3961
|
}),
|
|
3952
3962
|
{ chat_id: chatId, verb: 'permission-resume', ...(threadId != null ? { threadId } : {}) },
|
|
@@ -4033,21 +4043,6 @@ function chunk(text: string, limit: number, mode: 'length' | 'newline'): string[
|
|
|
4033
4043
|
return out
|
|
4034
4044
|
}
|
|
4035
4045
|
|
|
4036
|
-
function escapeMarkdownV2(text: string): string {
|
|
4037
|
-
const specialChars = /[_*\[\]()~`>#+\-=|{}.!\\]/g
|
|
4038
|
-
const parts: string[] = []
|
|
4039
|
-
let last = 0
|
|
4040
|
-
const codeRe = /(```[\s\S]*?```|`[^`\n]+`)/g
|
|
4041
|
-
let m: RegExpExecArray | null
|
|
4042
|
-
while ((m = codeRe.exec(text)) !== null) {
|
|
4043
|
-
if (m.index > last) parts.push(text.slice(last, m.index).replace(specialChars, '\\$&'))
|
|
4044
|
-
parts.push(m[0])
|
|
4045
|
-
last = m.index + m[0].length
|
|
4046
|
-
}
|
|
4047
|
-
if (last < text.length) parts.push(text.slice(last).replace(specialChars, '\\$&'))
|
|
4048
|
-
return parts.join('')
|
|
4049
|
-
}
|
|
4050
|
-
|
|
4051
4046
|
// āāā Typing indicator āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
4052
4047
|
// All four state maps re-keyed from `chat_id` to `chatKey(chat, thread)`
|
|
4053
4048
|
// in PR3 of the supergroup-mode rollout. In supergroup mode one agent
|
|
@@ -4193,14 +4188,17 @@ function wrapBootCardApi(
|
|
|
4193
4188
|
verb: 'boot-card',
|
|
4194
4189
|
...(threadId != null ? { threadId } : {}),
|
|
4195
4190
|
})
|
|
4191
|
+
// #2669: card bodies are raw GFM markdown ā send them via the
|
|
4192
|
+
// rich-message path. The card modules pass plain `opts` (no parse_mode).
|
|
4196
4193
|
return {
|
|
4197
4194
|
sendMessage: async (cid, text, sendOpts) => {
|
|
4198
4195
|
const sent = await robustApiCall(
|
|
4199
4196
|
() =>
|
|
4200
|
-
|
|
4197
|
+
// allow-raw-bot-api: sendRichMessage routed through robustApiCall (not in the THREAD_NOT_FOUND blast pattern)
|
|
4198
|
+
lockedBot.api.sendRichMessage(
|
|
4201
4199
|
cid,
|
|
4202
|
-
text,
|
|
4203
|
-
sendOpts as Parameters<typeof lockedBot.api.
|
|
4200
|
+
richMessage(text),
|
|
4201
|
+
sendOpts as Parameters<typeof lockedBot.api.sendRichMessage>[2],
|
|
4204
4202
|
),
|
|
4205
4203
|
opts(cid),
|
|
4206
4204
|
)
|
|
@@ -4212,7 +4210,7 @@ function wrapBootCardApi(
|
|
|
4212
4210
|
lockedBot.api.editMessageText(
|
|
4213
4211
|
cid,
|
|
4214
4212
|
mid,
|
|
4215
|
-
text,
|
|
4213
|
+
richMessage(text),
|
|
4216
4214
|
editOpts as Parameters<typeof lockedBot.api.editMessageText>[3],
|
|
4217
4215
|
),
|
|
4218
4216
|
opts(cid),
|
|
@@ -4226,7 +4224,7 @@ function wrapBootCardApi(
|
|
|
4226
4224
|
type EditOpts = Parameters<Api['editMessageText']>[3]
|
|
4227
4225
|
try {
|
|
4228
4226
|
// allow-raw-bot-api: boot-card edit-in-place probe ā must detect a deleted target, which the shared retry policy swallows.
|
|
4229
|
-
await lockedBot.api.editMessageText(cid, mid, text, editOpts as EditOpts)
|
|
4227
|
+
await lockedBot.api.editMessageText(cid, mid, richMessage(text), editOpts as EditOpts)
|
|
4230
4228
|
return 'edited'
|
|
4231
4229
|
} catch (err) {
|
|
4232
4230
|
const desc =
|
|
@@ -4256,14 +4254,16 @@ function wrapIssuesCardApi(
|
|
|
4256
4254
|
verb: 'issues-card',
|
|
4257
4255
|
...(threadId != null ? { threadId } : {}),
|
|
4258
4256
|
})
|
|
4257
|
+
// #2669: issues-card body is raw GFM markdown ā send via the rich path.
|
|
4259
4258
|
return {
|
|
4260
4259
|
sendMessage: async (cid, text, sendOpts) => {
|
|
4261
4260
|
const sent = await robustApiCall(
|
|
4262
4261
|
() =>
|
|
4263
|
-
|
|
4262
|
+
// allow-raw-bot-api: sendRichMessage routed through robustApiCall (not in the THREAD_NOT_FOUND blast pattern)
|
|
4263
|
+
lockedBot.api.sendRichMessage(
|
|
4264
4264
|
cid,
|
|
4265
|
-
text,
|
|
4266
|
-
sendOpts as Parameters<typeof lockedBot.api.
|
|
4265
|
+
richMessage(text),
|
|
4266
|
+
sendOpts as Parameters<typeof lockedBot.api.sendRichMessage>[2],
|
|
4267
4267
|
),
|
|
4268
4268
|
opts(cid),
|
|
4269
4269
|
)
|
|
@@ -4275,7 +4275,7 @@ function wrapIssuesCardApi(
|
|
|
4275
4275
|
lockedBot.api.editMessageText(
|
|
4276
4276
|
cid,
|
|
4277
4277
|
mid,
|
|
4278
|
-
text,
|
|
4278
|
+
richMessage(text),
|
|
4279
4279
|
editOpts as Parameters<typeof lockedBot.api.editMessageText>[3],
|
|
4280
4280
|
),
|
|
4281
4281
|
opts(cid),
|
|
@@ -5320,7 +5320,6 @@ function emitGatewayOperatorEvent(event: OperatorEvent): void {
|
|
|
5320
5320
|
// grammy's Other<...> opts type is generated and stricter than our
|
|
5321
5321
|
// call shape; runtime accepts both. Cast through unknown.
|
|
5322
5322
|
const opts = {
|
|
5323
|
-
parse_mode: 'HTML' as const,
|
|
5324
5323
|
...(renderedKeyboard ? { reply_markup: renderedKeyboard } : {}),
|
|
5325
5324
|
...(opEventThread != null ? { message_thread_id: opEventThread } : {}),
|
|
5326
5325
|
}
|
|
@@ -5328,7 +5327,7 @@ function emitGatewayOperatorEvent(event: OperatorEvent): void {
|
|
|
5328
5327
|
// very next line is what unlocks the raw bot.api call.
|
|
5329
5328
|
// Opts now includes message_thread_id when supergroup mode is on.
|
|
5330
5329
|
// allow-raw-bot-api: operator-event broadcast loop; topic-aware opts
|
|
5331
|
-
void bot.api.
|
|
5330
|
+
void bot.api.sendRichMessage(chat_id, richMessage(renderedText), opts as never).catch(e => {
|
|
5332
5331
|
process.stderr.write(
|
|
5333
5332
|
`telegram gateway: operator-event send to ${chat_id} failed agent=${agent} kind=${kind}: ${e}\n`,
|
|
5334
5333
|
)
|
|
@@ -5356,9 +5355,7 @@ function postLegacyBanner(
|
|
|
5356
5355
|
retryWithThreadFallback(
|
|
5357
5356
|
robustApiCall,
|
|
5358
5357
|
(tid) =>
|
|
5359
|
-
lockedBot.api.
|
|
5360
|
-
parse_mode: 'HTML',
|
|
5361
|
-
link_preview_options: { is_disabled: true },
|
|
5358
|
+
lockedBot.api.sendRichMessage(chatId, richMessage(text), {
|
|
5362
5359
|
...(tid != null ? { message_thread_id: tid } : {}),
|
|
5363
5360
|
...(ackMessageId != null ? { reply_parameters: { message_id: ackMessageId } } : {}),
|
|
5364
5361
|
}),
|
|
@@ -5612,11 +5609,14 @@ function ensureIssuesCard(chatId: string, threadId: number | undefined): void {
|
|
|
5612
5609
|
|
|
5613
5610
|
// #1122: framework safety-net for "model is silent to the user for >5min."
|
|
5614
5611
|
// Starts a single setInterval poll that walks active turns; at 300s of
|
|
5615
|
-
// silence the framework
|
|
5616
|
-
// still thinkingā¦" message
|
|
5617
|
-
//
|
|
5618
|
-
//
|
|
5619
|
-
//
|
|
5612
|
+
// silence the framework unwedges the turn. The generic "still working⦠/
|
|
5613
|
+
// still thinkingā¦" stall message it used to send was a stop-gap from
|
|
5614
|
+
// before the live draft carried the progress beats and has been retired
|
|
5615
|
+
// (the stall-stop-gap PR) ā the surviving user-visible sends are the
|
|
5616
|
+
// deterministic update_apply phase line and the approval-blocked re-ping.
|
|
5617
|
+
// The model-targeted nudge ladder (ack/soft/firm) and the 60s awareness
|
|
5618
|
+
// ping were retired earlier once the live-updating reply/draft took over
|
|
5619
|
+
// the pacing job ā only this single unwedge fallback remains. Honours
|
|
5620
5620
|
// SWITCHROOM_DISABLE_SILENCE_POKE=1 kill switch (no-op if set).
|
|
5621
5621
|
// Set when this gateway dispatches an `update_apply` to hostd that
|
|
5622
5622
|
// returns `started`; cleared when the dispatch poll resolves (terminal
|
|
@@ -5752,6 +5752,14 @@ silencePoke.startTimer({
|
|
|
5752
5752
|
ctx.inFlightTools,
|
|
5753
5753
|
blockedOnApproval,
|
|
5754
5754
|
)
|
|
5755
|
+
// The stall-notice stop-gap is retired: `formatFrameworkFallbackText`
|
|
5756
|
+
// returns null for the pure-liveness "still working / running <Tool>"
|
|
5757
|
+
// beat (the only string it still emits is the approval-blocked re-ping).
|
|
5758
|
+
// The early quiet floor beat existed ONLY to surface that stall notice, so
|
|
5759
|
+
// with the notice gone there is nothing to send here unless the turn is
|
|
5760
|
+
// parked on an approval card. Skip silently otherwise; the live draft +
|
|
5761
|
+
// the model's own pacing beats carry progress.
|
|
5762
|
+
if (text == null) return
|
|
5755
5763
|
try {
|
|
5756
5764
|
await robustApiCall(
|
|
5757
5765
|
() => bot.api.sendMessage(ctx.chatId, text, {
|
|
@@ -5849,7 +5857,10 @@ silencePoke.startTimer({
|
|
|
5849
5857
|
)
|
|
5850
5858
|
}
|
|
5851
5859
|
// #2527: log the actual poke fire with structured data before sending,
|
|
5852
|
-
// so the event is visible even if the send fails.
|
|
5860
|
+
// so the event is visible even if the send fails. The stall-notice
|
|
5861
|
+
// stop-gap is retired (the user-visible send below is now gated), but the
|
|
5862
|
+
// fire event is still logged so observability + the unwedge accounting are
|
|
5863
|
+
// unchanged ā mirrors PR #2641 (drop the chat badge, keep the log).
|
|
5853
5864
|
logStreamingEvent({
|
|
5854
5865
|
kind: 'silence_poke_fire',
|
|
5855
5866
|
chatId: ctx.chatId,
|
|
@@ -5857,24 +5868,34 @@ silencePoke.startTimer({
|
|
|
5857
5868
|
silenceMs: ctx.silenceMs,
|
|
5858
5869
|
fallbackKind: ctx.fallbackKind,
|
|
5859
5870
|
})
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5871
|
+
// Send a user-visible message ONLY when there's something honest to say:
|
|
5872
|
+
// - a deterministic in-flight update_apply phase (`text` set above), or
|
|
5873
|
+
// - the approval-blocked re-ping (`formatFrameworkFallbackText` returns a
|
|
5874
|
+
// string only for that case).
|
|
5875
|
+
// The generic "still working⦠(no update from agent in N min)" stall notice
|
|
5876
|
+
// is retired ā `formatFrameworkFallbackText` returns null for it, so `text`
|
|
5877
|
+
// stays null and we skip the send. The turn-teardown below (the unwedge ā
|
|
5878
|
+
// the one job the live draft can't do, per the conversational-pacing RFC)
|
|
5879
|
+
// still runs unconditionally.
|
|
5880
|
+
if (text != null) {
|
|
5881
|
+
try {
|
|
5882
|
+
await robustApiCall(
|
|
5883
|
+
() => bot.api.sendMessage(ctx.chatId, text, {
|
|
5884
|
+
...(ctx.threadId != null ? { message_thread_id: ctx.threadId } : {}),
|
|
5885
|
+
// Conditional: when the turn is parked on an approval card, this
|
|
5886
|
+
// fallback carries a user-gating re-ping ("waiting for your
|
|
5887
|
+
// approval ā tap Approve or Deny ā¦") ā that must PING, because the
|
|
5888
|
+
// user is the one being waited on. The deterministic update-status
|
|
5889
|
+
// line stays SILENT (a status surface). Gate on `blockedOnApproval`.
|
|
5890
|
+
disable_notification: blockedOnApproval ? false : true,
|
|
5891
|
+
}),
|
|
5892
|
+
{ chat_id: ctx.chatId, ...(ctx.threadId != null ? { threadId: ctx.threadId } : {}) },
|
|
5893
|
+
)
|
|
5894
|
+
} catch (err) {
|
|
5895
|
+
process.stderr.write(
|
|
5896
|
+
`silence-poke fallback sendMessage failed chat=${ctx.chatId} thread=${ctx.threadId}: ${err}\n`,
|
|
5897
|
+
)
|
|
5898
|
+
}
|
|
5878
5899
|
}
|
|
5879
5900
|
// #1122 follow-up: end the wedged turn AFTER the fallback fires.
|
|
5880
5901
|
// Without this, `activeTurnStartedAt` stays set, every subsequent
|
|
@@ -6197,19 +6218,18 @@ _deliveryConfirmSweep.unref?.()
|
|
|
6197
6218
|
// `SWITCHROOM_DISABLE_PENDING_PROGRESS=1`.
|
|
6198
6219
|
pendingProgress.startTimer({
|
|
6199
6220
|
editMessage: async (ctx) => {
|
|
6200
|
-
// #
|
|
6201
|
-
// the
|
|
6202
|
-
//
|
|
6203
|
-
//
|
|
6204
|
-
//
|
|
6205
|
-
const editOpts = ctx.parseMode != null ? { parse_mode: ctx.parseMode } : undefined
|
|
6221
|
+
// #2669: re-edit via the SAME shape the anchor was sent with. A rich
|
|
6222
|
+
// anchor (the reply tool's default) re-edits via the rich-markdown
|
|
6223
|
+
// path so the body keeps rendering its markdown; a literal
|
|
6224
|
+
// `format:'text'` anchor re-edits as a plain string. This is the
|
|
6225
|
+
// single-rich-path successor to the #1698 parse-mode contract.
|
|
6206
6226
|
await swallowingApiCall(
|
|
6207
6227
|
() =>
|
|
6208
6228
|
lockedBot.api.editMessageText(
|
|
6209
6229
|
ctx.chatId,
|
|
6210
6230
|
ctx.messageId,
|
|
6211
|
-
ctx.newText,
|
|
6212
|
-
|
|
6231
|
+
ctx.literalText ? ctx.newText : richMessage(ctx.newText),
|
|
6232
|
+
undefined,
|
|
6213
6233
|
),
|
|
6214
6234
|
{
|
|
6215
6235
|
chat_id: ctx.chatId,
|
|
@@ -6792,7 +6812,6 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
6792
6812
|
activeTurnStartedAt,
|
|
6793
6813
|
claudeBusyKeys,
|
|
6794
6814
|
activeDraftStreams,
|
|
6795
|
-
activeDraftParseModes,
|
|
6796
6815
|
clearActiveReactions: () => {
|
|
6797
6816
|
const ad = resolveAgentDirFromEnv()
|
|
6798
6817
|
if (ad) clearActiveReactions(ad)
|
|
@@ -6981,8 +7000,8 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
6981
7000
|
// Natural-language card body ā a plain sentence ("Gymbro wants to
|
|
6982
7001
|
// edit: supplement-log.md" + a why-line), never a raw tool id.
|
|
6983
7002
|
// The operator sees what is being requested and why at a glance.
|
|
6984
|
-
// Mirrors the `vault_request_access` card layout. Sent
|
|
6985
|
-
//
|
|
7003
|
+
// Mirrors the `vault_request_access` card layout. Sent via the
|
|
7004
|
+
// rich-markdown path below.
|
|
6986
7005
|
const text = formatPermissionCardBody({
|
|
6987
7006
|
toolName,
|
|
6988
7007
|
inputPreview,
|
|
@@ -7009,8 +7028,8 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
7009
7028
|
const activeTurn = currentTurn
|
|
7010
7029
|
const targets = resolvePermissionCardTargets()
|
|
7011
7030
|
for (const { chatId, threadId } of targets) {
|
|
7012
|
-
//
|
|
7013
|
-
//
|
|
7031
|
+
// The rich-markdown path pairs with formatPermissionCardBody (#1790)
|
|
7032
|
+
// so its bold/italic render. retryWithThreadFallback: if the topic was
|
|
7014
7033
|
// deleted/recreated (stale thread id ā 400 "message thread not
|
|
7015
7034
|
// found"), re-send thread-less into the main chat so the card still
|
|
7016
7035
|
// ARRIVES rather than vanishing ā 10-min TTL auto-deny ā wedge.
|
|
@@ -7018,8 +7037,7 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
7018
7037
|
void retryWithThreadFallback<{ message_id: number }>(
|
|
7019
7038
|
robustApiCall,
|
|
7020
7039
|
(tid) =>
|
|
7021
|
-
bot.api.
|
|
7022
|
-
parse_mode: 'HTML',
|
|
7040
|
+
bot.api.sendRichMessage(chatId, richMessage(text), {
|
|
7023
7041
|
reply_markup: keyboard,
|
|
7024
7042
|
...(tid != null ? { message_thread_id: tid } : {}),
|
|
7025
7043
|
}),
|
|
@@ -7198,8 +7216,7 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
7198
7216
|
try {
|
|
7199
7217
|
const sent = await robustApiCall(
|
|
7200
7218
|
() =>
|
|
7201
|
-
bot.api.
|
|
7202
|
-
parse_mode: 'HTML',
|
|
7219
|
+
bot.api.sendRichMessage(args.chatId, richMessage(args.text), {
|
|
7203
7220
|
...(args.threadId !== undefined
|
|
7204
7221
|
? { message_thread_id: args.threadId }
|
|
7205
7222
|
: {}),
|
|
@@ -7349,8 +7366,8 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
7349
7366
|
try {
|
|
7350
7367
|
const sent = await robustApiCall(
|
|
7351
7368
|
() =>
|
|
7352
|
-
bot
|
|
7353
|
-
|
|
7369
|
+
// allow-raw-bot-api: sendRichMessage routed through robustApiCall (not in the THREAD_NOT_FOUND blast pattern)
|
|
7370
|
+
bot.api.sendRichMessage(args.chatId, richMessage(args.text), {
|
|
7354
7371
|
...(args.threadId !== undefined
|
|
7355
7372
|
? { message_thread_id: args.threadId }
|
|
7356
7373
|
: {}),
|
|
@@ -7374,8 +7391,7 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
7374
7391
|
try {
|
|
7375
7392
|
await robustApiCall(
|
|
7376
7393
|
() =>
|
|
7377
|
-
bot.api.editMessageText(args.chatId, args.messageId, args.text, {
|
|
7378
|
-
parse_mode: 'HTML',
|
|
7394
|
+
bot.api.editMessageText(args.chatId, args.messageId, richMessage(args.text), {
|
|
7379
7395
|
// Strip the inline keyboard on a terminal/interim edit so the
|
|
7380
7396
|
// [Approve]/[Deny] buttons stop being tappable on a resolved card.
|
|
7381
7397
|
...(args.stripKeyboard ? { reply_markup: { inline_keyboard: [] } } : {}),
|
|
@@ -7446,8 +7462,7 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
7446
7462
|
try {
|
|
7447
7463
|
await robustApiCall(
|
|
7448
7464
|
() =>
|
|
7449
|
-
bot.api.editMessageText(args.chatId, args.messageId, args.text, {
|
|
7450
|
-
parse_mode: 'HTML',
|
|
7465
|
+
bot.api.editMessageText(args.chatId, args.messageId, richMessage(args.text), {
|
|
7451
7466
|
// Finalize is terminal ā drop the keyboard so buttons are gone.
|
|
7452
7467
|
...(args.stripKeyboard ? { reply_markup: { inline_keyboard: [] } } : {}),
|
|
7453
7468
|
}),
|
|
@@ -7544,15 +7559,18 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
7544
7559
|
return
|
|
7545
7560
|
}
|
|
7546
7561
|
const threadId = msg.threadId
|
|
7547
|
-
|
|
7562
|
+
// #2669: literal `text` mode ā plain send; everything else ā rich markdown.
|
|
7563
|
+
const literalText = msg.parseMode === 'text'
|
|
7564
|
+
const outOpts = {
|
|
7565
|
+
...(threadId != null && threadId !== 1 ? { message_thread_id: threadId } : {}),
|
|
7566
|
+
}
|
|
7548
7567
|
// allow-raw-bot-api: wrapped in swallowingApiCall (retry policy); thread-aware send.
|
|
7549
7568
|
// General topic (thread 1) sends omit message_thread_id per the outbound convention.
|
|
7550
7569
|
void swallowingApiCall(
|
|
7551
|
-
() =>
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
}),
|
|
7570
|
+
(): Promise<{ message_id: number }> =>
|
|
7571
|
+
literalText
|
|
7572
|
+
? bot.api.sendMessage(msg.chatId, msg.text, outOpts)
|
|
7573
|
+
: bot.api.sendRichMessage(msg.chatId, richMessage(msg.text), outOpts),
|
|
7556
7574
|
{ chat_id: msg.chatId, verb: 'cron-action-send', ...(threadId != null ? { threadId } : {}) },
|
|
7557
7575
|
)
|
|
7558
7576
|
process.stderr.write(
|
|
@@ -7580,6 +7598,78 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
7580
7598
|
void fireFleetAutoFallback(msg.agentName, untilMs)
|
|
7581
7599
|
},
|
|
7582
7600
|
|
|
7601
|
+
// #2670 one-tap self-improvement ā persist a skill-improvement proposal and
|
|
7602
|
+
// post its Approve/Dismiss card. The store transition + apply-injection on
|
|
7603
|
+
// Approve are owned by handleSkillProposalCallback (so a gateway restart
|
|
7604
|
+
// between post and tap still resolves correctly). Placed AFTER
|
|
7605
|
+
// onQuotaWallDetected so it doesn't fall inside the source slice that
|
|
7606
|
+
// send-outbound-wiring.test.ts takes between onSendOutbound and
|
|
7607
|
+
// onQuotaWallDetected.
|
|
7608
|
+
onPostSkillProposal(_client: IpcClient, msg: PostSkillProposalMessage) {
|
|
7609
|
+
const self = process.env.SWITCHROOM_AGENT_NAME
|
|
7610
|
+
if (self && msg.agentName !== self) {
|
|
7611
|
+
process.stderr.write(
|
|
7612
|
+
`telegram gateway: post_skill_proposal rejected ā agent mismatch (${msg.agentName} != ${self})\n`,
|
|
7613
|
+
)
|
|
7614
|
+
return
|
|
7615
|
+
}
|
|
7616
|
+
try {
|
|
7617
|
+
assertAllowedChat(msg.chatId)
|
|
7618
|
+
} catch (err) {
|
|
7619
|
+
process.stderr.write(
|
|
7620
|
+
`telegram gateway: post_skill_proposal rejected ā ${(err as Error).message}\n`,
|
|
7621
|
+
)
|
|
7622
|
+
return
|
|
7623
|
+
}
|
|
7624
|
+
const stateDir = process.env.TELEGRAM_STATE_DIR
|
|
7625
|
+
if (stateDir == null || stateDir.length === 0) {
|
|
7626
|
+
process.stderr.write(`telegram gateway: post_skill_proposal: TELEGRAM_STATE_DIR unset, skipping\n`)
|
|
7627
|
+
return
|
|
7628
|
+
}
|
|
7629
|
+
// Dedup against still-live rejection fingerprints ā never re-surface a
|
|
7630
|
+
// proposal the operator already dismissed.
|
|
7631
|
+
if (isSkillProposalSuppressed(stateDir, {
|
|
7632
|
+
lesson: msg.lesson,
|
|
7633
|
+
draft: msg.draft,
|
|
7634
|
+
skill_slug: msg.skillSlug,
|
|
7635
|
+
})) {
|
|
7636
|
+
process.stderr.write(
|
|
7637
|
+
`telegram gateway: post_skill_proposal suppressed (rejected before) slug=${msg.skillSlug}\n`,
|
|
7638
|
+
)
|
|
7639
|
+
return
|
|
7640
|
+
}
|
|
7641
|
+
const proposal = enqueueSkillProposal(stateDir, {
|
|
7642
|
+
skill_slug: msg.skillSlug,
|
|
7643
|
+
is_new: msg.isNew,
|
|
7644
|
+
lesson: msg.lesson,
|
|
7645
|
+
draft: msg.draft,
|
|
7646
|
+
evidence: msg.evidence,
|
|
7647
|
+
chat_id: Number(msg.chatId),
|
|
7648
|
+
})
|
|
7649
|
+
const cardText = renderSkillProposalCard({
|
|
7650
|
+
id: proposal.id,
|
|
7651
|
+
skill_slug: proposal.skill_slug,
|
|
7652
|
+
is_new: proposal.is_new,
|
|
7653
|
+
lesson: proposal.lesson,
|
|
7654
|
+
evidence: proposal.evidence,
|
|
7655
|
+
skill_md: proposal.draft['SKILL.md'],
|
|
7656
|
+
})
|
|
7657
|
+
const threadId = msg.threadId
|
|
7658
|
+
void swallowingApiCall(
|
|
7659
|
+
() =>
|
|
7660
|
+
bot.api.sendMessage(msg.chatId, cardText, {
|
|
7661
|
+
parse_mode: 'HTML',
|
|
7662
|
+
reply_markup: skillProposalKeyboard(proposal.id),
|
|
7663
|
+
...(threadId != null && threadId !== 1 ? { message_thread_id: threadId } : {}),
|
|
7664
|
+
}),
|
|
7665
|
+
{ chat_id: msg.chatId, verb: 'skill-proposal-card', ...(threadId != null ? { threadId } : {}) },
|
|
7666
|
+
)
|
|
7667
|
+
process.stderr.write(
|
|
7668
|
+
`telegram gateway: post_skill_proposal agent=${msg.agentName} chat=${msg.chatId} ` +
|
|
7669
|
+
`proposal=${proposal.id} slug=${proposal.skill_slug} new=${proposal.is_new}\n`,
|
|
7670
|
+
)
|
|
7671
|
+
},
|
|
7672
|
+
|
|
7583
7673
|
log: (msg) => process.stderr.write(`telegram gateway: ipc ā ${msg}\n`),
|
|
7584
7674
|
})
|
|
7585
7675
|
|
|
@@ -7891,8 +7981,8 @@ function notifyLinearAuthDead(info: { agent: string; reason: LinearAuthDeadReaso
|
|
|
7891
7981
|
const text = buildLinearAuthDeadMessage(info.agent, info.reason)
|
|
7892
7982
|
await swallowingApiCall(
|
|
7893
7983
|
() =>
|
|
7894
|
-
bot
|
|
7895
|
-
|
|
7984
|
+
// allow-raw-bot-api: wrapped in swallowingApiCall (retry policy)
|
|
7985
|
+
bot.api.sendRichMessage(chatId, richMessage(text), {
|
|
7896
7986
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
7897
7987
|
}),
|
|
7898
7988
|
{ chat_id: chatId, verb: 'linearAuthDead' },
|
|
@@ -8004,11 +8094,9 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8004
8094
|
// value, exactly like the voice scrub that follows.
|
|
8005
8095
|
text = redactOutboundText(text, 'reply')
|
|
8006
8096
|
// Voice scrub (#1683): replace em / en dashes with commas / periods.
|
|
8007
|
-
// Runs BEFORE outboundDedup so retries see the scrubbed key, and
|
|
8008
|
-
//
|
|
8009
|
-
//
|
|
8010
|
-
// converter would have already escaped/parked code, and the scrub
|
|
8011
|
-
// would see only the parked placeholders). Kill switch:
|
|
8097
|
+
// Runs BEFORE outboundDedup so retries see the scrubbed key, and on the
|
|
8098
|
+
// raw markdown text (the scrubber does its own code-region parking so
|
|
8099
|
+
// dashes inside fences/inline code are preserved). Kill switch:
|
|
8012
8100
|
// `SWITCHROOM_DISABLE_VOICE_SCRUB=1`.
|
|
8013
8101
|
{
|
|
8014
8102
|
const scrub = scrubVoice(text)
|
|
@@ -8220,24 +8308,19 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8220
8308
|
// Replace the local text with a one-line link. The first line
|
|
8221
8309
|
// is the chosen title (so the user sees the topic at a glance);
|
|
8222
8310
|
// the second line is the URL Telegram will Instant-View.
|
|
8223
|
-
text =
|
|
8311
|
+
text = `**${escapeMarkdown(title)}**\n${url}`
|
|
8224
8312
|
}
|
|
8225
8313
|
// url null ā fall through and chunk; the user still gets the
|
|
8226
8314
|
// long reply, just split across messages.
|
|
8227
8315
|
}
|
|
8228
8316
|
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
effectiveText = escapeMarkdownV2(text)
|
|
8237
|
-
} else {
|
|
8238
|
-
parseMode = undefined
|
|
8239
|
-
effectiveText = text
|
|
8240
|
-
}
|
|
8317
|
+
// Single rich-markdown path (#2669). The only fork is `format:'text'`,
|
|
8318
|
+
// a literal/no-markdown send: the body bypasses the rich parser entirely
|
|
8319
|
+
// (plain `sendMessage`, no rich wrapper). Everything else ā the default ā
|
|
8320
|
+
// ships the raw GFM markdown via `sendRichMessage`. `effectiveText` is the
|
|
8321
|
+
// raw text either way (no HTML/MarkdownV2 rendering happens here anymore).
|
|
8322
|
+
const literalText = format === 'text'
|
|
8323
|
+
const effectiveText: string = text
|
|
8241
8324
|
|
|
8242
8325
|
assertAllowedChat(chat_id)
|
|
8243
8326
|
|
|
@@ -8299,11 +8382,11 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8299
8382
|
}
|
|
8300
8383
|
}
|
|
8301
8384
|
|
|
8302
|
-
const limit = Math.max(1, Math.min(access.textChunkLimit ??
|
|
8385
|
+
const limit = Math.max(1, Math.min(access.textChunkLimit ?? RICH_MESSAGE_MAX_CHARS, MAX_CHUNK_LIMIT))
|
|
8303
8386
|
const replyMode = access.replyToMode ?? 'first'
|
|
8304
|
-
const chunks =
|
|
8305
|
-
?
|
|
8306
|
-
:
|
|
8387
|
+
const chunks = literalText
|
|
8388
|
+
? chunk(effectiveText, limit, access.chunkMode ?? 'length')
|
|
8389
|
+
: splitMarkdownChunks(effectiveText, limit)
|
|
8307
8390
|
const sentIds: number[] = []
|
|
8308
8391
|
|
|
8309
8392
|
// #271: validate inline_keyboard and namespace any callback_data with
|
|
@@ -8335,7 +8418,6 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8335
8418
|
await openStream.finalize().catch(() => {})
|
|
8336
8419
|
previewMessageId = openStream.getMessageId()
|
|
8337
8420
|
activeDraftStreams.delete(replySKey)
|
|
8338
|
-
activeDraftParseModes.delete(replySKey)
|
|
8339
8421
|
lastPtyPreviewByChat.delete(replySKey)
|
|
8340
8422
|
}
|
|
8341
8423
|
|
|
@@ -8446,13 +8528,11 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8446
8528
|
})
|
|
8447
8529
|
if (decision.kind === 'edit-anchor') {
|
|
8448
8530
|
const editParams: {
|
|
8449
|
-
parse_mode?: 'HTML' | 'MarkdownV2'
|
|
8450
8531
|
message_thread_id?: number
|
|
8451
8532
|
link_preview_options?: { is_disabled: boolean }
|
|
8452
8533
|
} = {
|
|
8453
8534
|
link_preview_options: { is_disabled: disableLinkPreview },
|
|
8454
8535
|
}
|
|
8455
|
-
if (parseMode != null) editParams.parse_mode = parseMode
|
|
8456
8536
|
if (threadId != null) editParams.message_thread_id = threadId
|
|
8457
8537
|
try {
|
|
8458
8538
|
await robustApiCall(
|
|
@@ -8460,7 +8540,7 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8460
8540
|
lockedBot.api.editMessageText(
|
|
8461
8541
|
chat_id,
|
|
8462
8542
|
decision.messageId,
|
|
8463
|
-
decision.mergedText,
|
|
8543
|
+
literalText ? decision.mergedText : richMessage(decision.mergedText),
|
|
8464
8544
|
editParams,
|
|
8465
8545
|
),
|
|
8466
8546
|
{
|
|
@@ -8504,7 +8584,7 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8504
8584
|
pendingProgress.noteOutbound(statusKey(chat_id, threadId), {
|
|
8505
8585
|
messageId: decision.messageId,
|
|
8506
8586
|
text: decision.mergedText,
|
|
8507
|
-
|
|
8587
|
+
literalText,
|
|
8508
8588
|
})
|
|
8509
8589
|
if (HISTORY_ENABLED) {
|
|
8510
8590
|
try {
|
|
@@ -8591,7 +8671,6 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8591
8671
|
},
|
|
8592
8672
|
}
|
|
8593
8673
|
: {}),
|
|
8594
|
-
...(parseMode ? { parse_mode: parseMode } : {}),
|
|
8595
8674
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
8596
8675
|
...(disableLinkPreview ? { link_preview_options: { is_disabled: true } } : {}),
|
|
8597
8676
|
...(replyMarkup != null && isLastChunk ? { reply_markup: replyMarkup } : {}),
|
|
@@ -8599,14 +8678,17 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8599
8678
|
...(disableNotification ? { disable_notification: true } : {}),
|
|
8600
8679
|
}
|
|
8601
8680
|
|
|
8681
|
+
// The body argument for send/edit: a rich-markdown wrapper on the
|
|
8682
|
+
// default path, or the literal string for `format:'text'` (#2669).
|
|
8683
|
+
const richOrPlain = (s: string) => (literalText ? s : richMessage(s))
|
|
8684
|
+
|
|
8602
8685
|
if (i === 0 && previewMessageId != null) {
|
|
8603
8686
|
const editOpts: Record<string, unknown> = {}
|
|
8604
|
-
if (parseMode) editOpts.parse_mode = parseMode
|
|
8605
8687
|
if (disableLinkPreview) editOpts.link_preview_options = { is_disabled: true }
|
|
8606
8688
|
if (replyMarkup != null && isLastChunk) editOpts.reply_markup = replyMarkup
|
|
8607
8689
|
try {
|
|
8608
8690
|
await robustApiCall(
|
|
8609
|
-
() => lockedBot.api.editMessageText(chat_id, previewMessageId!, chunks[i], editOpts),
|
|
8691
|
+
() => lockedBot.api.editMessageText(chat_id, previewMessageId!, richOrPlain(chunks[i]), editOpts),
|
|
8610
8692
|
{ threadId, chat_id },
|
|
8611
8693
|
)
|
|
8612
8694
|
sentIds.push(previewMessageId!)
|
|
@@ -8625,37 +8707,45 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8625
8707
|
}
|
|
8626
8708
|
}
|
|
8627
8709
|
|
|
8628
|
-
// Last-resort: resend this chunk as plain text (
|
|
8629
|
-
// Keeps thread / reply / markup
|
|
8630
|
-
// sacrificed. Used when Telegram
|
|
8631
|
-
// unformatted answer than a
|
|
8710
|
+
// Last-resort: resend this chunk as plain text (no rich wrapper, so
|
|
8711
|
+
// the markdown parser never runs). Keeps thread / reply / markup
|
|
8712
|
+
// params; only the formatting is sacrificed. Used when Telegram
|
|
8713
|
+
// rejects our markdown ā better an unformatted answer than a
|
|
8714
|
+
// vanished one. The raw markdown source is itself readable prose, so
|
|
8715
|
+
// we send it verbatim rather than strip anything.
|
|
8632
8716
|
const sendChunkPlainText = async (opts: Record<string, unknown>): Promise<void> => {
|
|
8633
|
-
const plainOpts = { ...opts }
|
|
8634
|
-
delete (plainOpts as { parse_mode?: unknown }).parse_mode
|
|
8635
|
-
// A chunk that was pure markup (no text content) strips to ''.
|
|
8636
|
-
// Sending '' is a Telegram 400 "message text is empty" ā i.e.
|
|
8637
|
-
// the answer would still vanish, in the exact path that exists
|
|
8638
|
-
// to prevent that. Substitute an honest placeholder so the
|
|
8639
|
-
// user at least sees that a fragment was unrenderable.
|
|
8640
|
-
const stripped = telegramHtmlToPlainText(chunks[i])
|
|
8641
8717
|
const plain =
|
|
8642
|
-
|
|
8643
|
-
?
|
|
8644
|
-
: 'ā ļø (a
|
|
8645
|
-
// allow-raw-bot-api: plaintext last-resort fallback; wrapping would re-enter the parse
|
|
8646
|
-
const sent = await lockedBot.api.sendMessage(chat_id, plain,
|
|
8718
|
+
chunks[i].length > 0
|
|
8719
|
+
? chunks[i]
|
|
8720
|
+
: 'ā ļø (a fragment could not be rendered for Telegram)'
|
|
8721
|
+
// allow-raw-bot-api: plaintext last-resort fallback; wrapping would re-enter the parse policy that just rejected the payload
|
|
8722
|
+
const sent = await lockedBot.api.sendMessage(chat_id, plain, opts as never)
|
|
8647
8723
|
sentIds.push(sent.message_id)
|
|
8648
8724
|
logOutbound('reply', chat_id, sent.message_id, plain.length, `chunk=${i + 1}/${chunks.length} plaintext-fallback`)
|
|
8649
8725
|
process.stderr.write(
|
|
8650
|
-
`telegram gateway:
|
|
8726
|
+
`telegram gateway: markdown parse-reject ā resent chunk ${i + 1}/${chunks.length} as plain text\n`,
|
|
8651
8727
|
)
|
|
8652
8728
|
}
|
|
8653
8729
|
|
|
8730
|
+
// Literal `format:'text'` sends bypass the rich parser entirely
|
|
8731
|
+
// (plain sendMessage, no markdown). The default path ships rich
|
|
8732
|
+
// markdown via sendRichMessage. Both resolve to a Message with a
|
|
8733
|
+
// message_id, which is all the caller reads.
|
|
8734
|
+
const sendChunk = (opts: Record<string, unknown>): Promise<{ message_id: number }> => {
|
|
8735
|
+
if (literalText) {
|
|
8736
|
+
// allow-raw-bot-api: literal format:'text' send; the chunk-loop's own THREAD_NOT_FOUND + parse-reject handling wraps this
|
|
8737
|
+
return lockedBot.api.sendMessage(chat_id, chunks[i], opts as never)
|
|
8738
|
+
}
|
|
8739
|
+
// sendRichMessage does NOT accept link_preview_options (rich messages
|
|
8740
|
+
// control previews via entity detection) ā drop it for the rich path.
|
|
8741
|
+
const richOpts = { ...opts }
|
|
8742
|
+
delete (richOpts as { link_preview_options?: unknown }).link_preview_options
|
|
8743
|
+
// allow-raw-bot-api: sendRichMessage is not in the THREAD_NOT_FOUND blast pattern; thread fallback handled in the catch below
|
|
8744
|
+
return lockedBot.api.sendRichMessage(chat_id, richMessage(chunks[i]), richOpts as never)
|
|
8745
|
+
}
|
|
8746
|
+
|
|
8654
8747
|
try {
|
|
8655
|
-
const sent = await robustApiCall(
|
|
8656
|
-
() => lockedBot.api.sendMessage(chat_id, chunks[i], sendOpts as never),
|
|
8657
|
-
{ threadId, chat_id },
|
|
8658
|
-
)
|
|
8748
|
+
const sent = await robustApiCall(() => sendChunk(sendOpts), { threadId, chat_id })
|
|
8659
8749
|
sentIds.push(sent.message_id)
|
|
8660
8750
|
logOutbound('reply', chat_id, sent.message_id, chunks[i].length, `chunk=${i + 1}/${chunks.length}`)
|
|
8661
8751
|
} catch (err) {
|
|
@@ -8664,11 +8754,10 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8664
8754
|
const retryOpts = { ...sendOpts }
|
|
8665
8755
|
delete (retryOpts as any).message_thread_id
|
|
8666
8756
|
try {
|
|
8667
|
-
|
|
8668
|
-
const sent = await lockedBot.api.sendMessage(chat_id, chunks[i], retryOpts as never)
|
|
8757
|
+
const sent = await sendChunk(retryOpts)
|
|
8669
8758
|
sentIds.push(sent.message_id)
|
|
8670
8759
|
} catch (retryErr) {
|
|
8671
|
-
// Thread dropped, but the
|
|
8760
|
+
// Thread dropped, but the markdown is also unparseable ā go plain.
|
|
8672
8761
|
if (isHtmlParseRejectError(retryErr)) await sendChunkPlainText(retryOpts)
|
|
8673
8762
|
else throw retryErr
|
|
8674
8763
|
}
|
|
@@ -8716,7 +8805,7 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
8716
8805
|
pendingProgress.noteOutbound(statusKey(chat_id, threadId), {
|
|
8717
8806
|
messageId: anchorMsgId,
|
|
8718
8807
|
text: chunks[chunks.length - 1],
|
|
8719
|
-
|
|
8808
|
+
literalText,
|
|
8720
8809
|
})
|
|
8721
8810
|
}
|
|
8722
8811
|
}
|
|
@@ -9161,7 +9250,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
|
|
|
9161
9250
|
...(streamReplyMarkup != null ? { reply_markup: streamReplyMarkup } : {}),
|
|
9162
9251
|
...(args.disable_notification === true ? { disable_notification: true } : {}),
|
|
9163
9252
|
},
|
|
9164
|
-
{ activeDraftStreams,
|
|
9253
|
+
{ activeDraftStreams, suppressPtyPreview },
|
|
9165
9254
|
{
|
|
9166
9255
|
// grammy's Bot<Context, Api<RawApi>> has a wider api shape than the
|
|
9167
9256
|
// local StreamBotApi interface, but is structurally compatible at
|
|
@@ -9169,8 +9258,6 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
|
|
|
9169
9258
|
// bypass the structural-typing strictness.
|
|
9170
9259
|
bot: lockedBot as unknown as { api: import('../stream-controller.js').StreamBotApi },
|
|
9171
9260
|
retry: robustApiCall,
|
|
9172
|
-
markdownToHtml,
|
|
9173
|
-
escapeMarkdownV2,
|
|
9174
9261
|
repairEscapedWhitespace,
|
|
9175
9262
|
assertAllowedChat,
|
|
9176
9263
|
resolveThreadId,
|
|
@@ -9257,16 +9344,11 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
|
|
|
9257
9344
|
// async dispatch, the framework edits THIS message in place at
|
|
9258
9345
|
// intervals.
|
|
9259
9346
|
//
|
|
9260
|
-
// #
|
|
9261
|
-
// the cross-turn edit tick
|
|
9262
|
-
//
|
|
9263
|
-
// "still working (Nm)" tick edits HTML content as plain text and
|
|
9264
|
-
// <b>/<code>/<a> render as literal tags.
|
|
9347
|
+
// #2669 ā capture whether the stream-reply-handler sent literally
|
|
9348
|
+
// (format:'text') so the cross-turn edit tick re-edits with the same
|
|
9349
|
+
// shape. The default rich-markdown path re-edits via the rich path.
|
|
9265
9350
|
const streamFormat = (args.format as string | undefined) ?? (access.parseMode ?? 'html')
|
|
9266
|
-
const
|
|
9267
|
-
streamFormat === 'html' ? 'HTML'
|
|
9268
|
-
: streamFormat === 'markdownv2' ? 'MarkdownV2'
|
|
9269
|
-
: undefined
|
|
9351
|
+
const streamLiteralText = streamFormat === 'text'
|
|
9270
9352
|
// #1760 primary fix ā clear any stale prior-turn ticker before
|
|
9271
9353
|
// re-anchoring on stream_reply done. See the matching comment at
|
|
9272
9354
|
// the executeReply finalize site.
|
|
@@ -9274,7 +9356,7 @@ async function executeStreamReply(args: Record<string, unknown>): Promise<unknow
|
|
|
9274
9356
|
pendingProgress.noteOutbound(statusKey(sChatId, sThreadId), {
|
|
9275
9357
|
messageId: result.messageId,
|
|
9276
9358
|
text: args.text as string,
|
|
9277
|
-
|
|
9359
|
+
literalText: streamLiteralText,
|
|
9278
9360
|
})
|
|
9279
9361
|
}
|
|
9280
9362
|
// #1664 ā mark the turn's final answer as delivered. For stream_reply a
|
|
@@ -9434,19 +9516,22 @@ async function executeProgressUpdate(args: Record<string, unknown>): Promise<unk
|
|
|
9434
9516
|
// Otherwise fall through to message-send below.
|
|
9435
9517
|
}
|
|
9436
9518
|
|
|
9437
|
-
// Send plain message (no quote-reply)
|
|
9519
|
+
// Send plain message (no quote-reply). Single rich-markdown path (#2669):
|
|
9520
|
+
// `parseMode:'text'` config opts into a literal plain send; everything
|
|
9521
|
+
// else ships raw GFM markdown via sendRichMessage.
|
|
9438
9522
|
const access = loadAccess()
|
|
9439
|
-
const
|
|
9440
|
-
const parseMode = configParseMode === 'html' ? 'HTML' : undefined
|
|
9441
|
-
const effectiveText = configParseMode === 'html' ? markdownToHtml(text) : text
|
|
9523
|
+
const literalText = (access.parseMode ?? 'html') === 'text'
|
|
9442
9524
|
|
|
9443
9525
|
const sendOpts = {
|
|
9444
|
-
...(parseMode ? { parse_mode: parseMode } : {}),
|
|
9445
9526
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
9446
9527
|
}
|
|
9447
9528
|
|
|
9448
9529
|
const sent = await robustApiCall(
|
|
9449
|
-
()
|
|
9530
|
+
(): Promise<{ message_id: number }> =>
|
|
9531
|
+
literalText
|
|
9532
|
+
// allow-raw-bot-api: literal progress-update send routed through robustApiCall
|
|
9533
|
+
? lockedBot.api.sendMessage(chat_id, text, sendOpts as never)
|
|
9534
|
+
: lockedBot.api.sendRichMessage(chat_id, richMessage(text), sendOpts as never),
|
|
9450
9535
|
{ verb: 'sendMessage', chat_id, threadId },
|
|
9451
9536
|
)
|
|
9452
9537
|
|
|
@@ -9537,7 +9622,6 @@ async function executeAskUser(rawArgs: Record<string, unknown>): Promise<unknown
|
|
|
9537
9622
|
// alive (button tap turns into "ā
<choice>" inline) rather than a
|
|
9538
9623
|
// graveyard of unanswered questions in the chat history.
|
|
9539
9624
|
const sendOpts: Record<string, unknown> = {
|
|
9540
|
-
parse_mode: 'HTML',
|
|
9541
9625
|
reply_markup: keyboard,
|
|
9542
9626
|
}
|
|
9543
9627
|
if (threadId != null) sendOpts.message_thread_id = threadId
|
|
@@ -9553,7 +9637,7 @@ async function executeAskUser(rawArgs: Record<string, unknown>): Promise<unknown
|
|
|
9553
9637
|
const opts = { ...sendOpts }
|
|
9554
9638
|
if (tid == null) delete opts.message_thread_id
|
|
9555
9639
|
else opts.message_thread_id = tid
|
|
9556
|
-
return lockedBot.api.
|
|
9640
|
+
return lockedBot.api.sendRichMessage(args.chatId, richMessage(args.question), opts)
|
|
9557
9641
|
},
|
|
9558
9642
|
{ threadId, chat_id: args.chatId, verb: 'ask_user.sendMessage' },
|
|
9559
9643
|
)
|
|
@@ -9581,8 +9665,7 @@ async function executeAskUser(rawArgs: Record<string, unknown>): Promise<unknown
|
|
|
9581
9665
|
lockedBot.api.editMessageText(
|
|
9582
9666
|
args.chatId,
|
|
9583
9667
|
entry.messageId!,
|
|
9584
|
-
`${args.question}\n\
|
|
9585
|
-
{ parse_mode: 'HTML' },
|
|
9668
|
+
richMessage(`${args.question}\n\n_ā± no response within ${Math.round(args.timeoutMs / 1000)}s_`),
|
|
9586
9669
|
),
|
|
9587
9670
|
{
|
|
9588
9671
|
chat_id: args.chatId,
|
|
@@ -9816,13 +9899,13 @@ function buildVaultRequestSaveKeyboard(stageId: string): { inline_keyboard: Arra
|
|
|
9816
9899
|
|
|
9817
9900
|
function renderVaultRequestSaveCard(req: PendingVaultRequestSave, agentSlug: string): string {
|
|
9818
9901
|
const lines: string[] = []
|
|
9819
|
-
lines.push(`š
|
|
9820
|
-
lines.push(`key:
|
|
9902
|
+
lines.push(`š **${escapeHtmlForTg(agentSlug)}** wants to save a secret`)
|
|
9903
|
+
lines.push(`key: \`${req.key}\``)
|
|
9821
9904
|
if (req.why && req.why.length > 0) {
|
|
9822
|
-
lines.push(`why:
|
|
9905
|
+
lines.push(`why: _${escapeHtmlForTg(req.why)}_`)
|
|
9823
9906
|
}
|
|
9824
9907
|
lines.push('')
|
|
9825
|
-
lines.push(
|
|
9908
|
+
lines.push(`_Tap Save to write to the host vault, Rename to change the key name, or Discard to drop it. The value is held in this chat's gateway memory until you decide._`)
|
|
9826
9909
|
return lines.join('\n')
|
|
9827
9910
|
}
|
|
9828
9911
|
|
|
@@ -9885,8 +9968,7 @@ async function executeVaultRequestSave(args: Record<string, unknown>): Promise<{
|
|
|
9885
9968
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
9886
9969
|
robustApiCall,
|
|
9887
9970
|
(tid) =>
|
|
9888
|
-
lockedBot.api.
|
|
9889
|
-
parse_mode: 'HTML',
|
|
9971
|
+
lockedBot.api.sendRichMessage(chat_id, richMessage(text), {
|
|
9890
9972
|
reply_markup: buildVaultRequestSaveKeyboard(stageId),
|
|
9891
9973
|
...(tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}),
|
|
9892
9974
|
}),
|
|
@@ -9959,13 +10041,13 @@ function buildSecretRequestKeyboard(stageId: string): { inline_keyboard: Array<A
|
|
|
9959
10041
|
|
|
9960
10042
|
function renderSecretRequestCard(req: PendingSecretRequest): string {
|
|
9961
10043
|
const lines: string[] = [
|
|
9962
|
-
`š
|
|
9963
|
-
|
|
10044
|
+
`š **${escapeHtmlForTg(req.agent)}** needs a secret:`,
|
|
10045
|
+
`\`${req.key}\``,
|
|
9964
10046
|
]
|
|
9965
|
-
if (req.reason) lines.push(
|
|
10047
|
+
if (req.reason) lines.push(`_${escapeHtmlForTg(req.reason)}_`)
|
|
9966
10048
|
lines.push(
|
|
9967
10049
|
'',
|
|
9968
|
-
'Tap
|
|
10050
|
+
'Tap **Provide securely**, then send the value as your next message. Iāll delete it instantly and store it in the vault ā it is never shown in chat or to the agent.',
|
|
9969
10051
|
)
|
|
9970
10052
|
return lines.join('\n')
|
|
9971
10053
|
}
|
|
@@ -10005,8 +10087,7 @@ async function executeRequestSecret(args: Record<string, unknown>): Promise<{ co
|
|
|
10005
10087
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
10006
10088
|
robustApiCall,
|
|
10007
10089
|
(tid) =>
|
|
10008
|
-
lockedBot.api.
|
|
10009
|
-
parse_mode: 'HTML',
|
|
10090
|
+
lockedBot.api.sendRichMessage(chat_id, richMessage(text), {
|
|
10010
10091
|
reply_markup: buildSecretRequestKeyboard(stageId),
|
|
10011
10092
|
...(tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}),
|
|
10012
10093
|
}),
|
|
@@ -10069,8 +10150,8 @@ async function captureProvidedSecret(
|
|
|
10069
10150
|
if (!write.ok) {
|
|
10070
10151
|
const parsed = parseVaultCliError(write.output)
|
|
10071
10152
|
const rendered = renderVaultCliError(parsed, { verb: 'save', key: armed.key })
|
|
10072
|
-
const body = rendered.suppressRaw ? rendered.html : `ā ļø vault write failed:\n
|
|
10073
|
-
await switchroomReply(ctx, `${body}\n\
|
|
10153
|
+
const body = rendered.suppressRaw ? rendered.html : `ā ļø vault write failed:\n\`\`\`\n${write.output}\n\`\`\``
|
|
10154
|
+
await switchroomReply(ctx, `${body}\n\n_The secret was NOT saved. The agent can re-request with \`request_secret\`._`, { html: true })
|
|
10074
10155
|
// Wake the agent too (review #2047 finding #3): it ended its turn
|
|
10075
10156
|
// waiting for a synthetic inbound; without this it stalls silently.
|
|
10076
10157
|
const fts = Date.now()
|
|
@@ -10097,7 +10178,7 @@ async function captureProvidedSecret(
|
|
|
10097
10178
|
|
|
10098
10179
|
await switchroomReply(
|
|
10099
10180
|
ctx,
|
|
10100
|
-
`ā
saved as
|
|
10181
|
+
`ā
saved as \`vault:${escapeHtmlForTg(armed.key)}\` (masked: \`${escapeHtmlForTg(maskToken(value))}\`). The agent can now reference it.`,
|
|
10101
10182
|
{ html: true },
|
|
10102
10183
|
)
|
|
10103
10184
|
|
|
@@ -10166,8 +10247,8 @@ async function handleSecretRequestCallback(ctx: Context, data: string): Promise<
|
|
|
10166
10247
|
.editMessageText(
|
|
10167
10248
|
pending.chat_id,
|
|
10168
10249
|
pending.card_message_id,
|
|
10169
|
-
`š Send the value for
|
|
10170
|
-
{
|
|
10250
|
+
richMessage(`š Send the value for \`${pending.key}\` as your next message ā a single message, exactly as-is (don't add other text). Iāll delete it instantly and store it in the vault.`),
|
|
10251
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
10171
10252
|
)
|
|
10172
10253
|
.catch(() => {})
|
|
10173
10254
|
}
|
|
@@ -10180,8 +10261,7 @@ async function handleSecretRequestCallback(ctx: Context, data: string): Promise<
|
|
|
10180
10261
|
await ctx.answerCallbackQuery({ text: 'Declined.' }).catch(() => {})
|
|
10181
10262
|
if (pending.card_message_id != null) {
|
|
10182
10263
|
await ctx.api
|
|
10183
|
-
.editMessageText(pending.chat_id, pending.card_message_id, `š« Declined ā
|
|
10184
|
-
parse_mode: 'HTML',
|
|
10264
|
+
.editMessageText(pending.chat_id, pending.card_message_id, richMessage(`š« Declined ā \`${pending.key}\` not provided.`), {
|
|
10185
10265
|
reply_markup: { inline_keyboard: [] },
|
|
10186
10266
|
})
|
|
10187
10267
|
.catch(() => {})
|
|
@@ -10233,9 +10313,9 @@ function renderVaultRequestAccessCard(req: PendingVaultRequestAccess): string {
|
|
|
10233
10313
|
const scopeLabel = req.scope === 'write' ? 'write' : 'read'
|
|
10234
10314
|
const days = Math.round(req.ttl_seconds / 86400)
|
|
10235
10315
|
const durationLabel = days >= 1 ? `${days}d` : `${Math.round(req.ttl_seconds / 3600)}h`
|
|
10236
|
-
lines.push(`š
|
|
10237
|
-
lines.push(`key:
|
|
10238
|
-
lines.push(`scope:
|
|
10316
|
+
lines.push(`š **${escapeHtmlForTg(req.agent)}** wants vault access`)
|
|
10317
|
+
lines.push(`key: \`${req.key}\``)
|
|
10318
|
+
lines.push(`scope: \`${scopeLabel}\` Ā· duration: \`${durationLabel}\``)
|
|
10239
10319
|
// #1790 ā always render the why-line, even when the agent omitted
|
|
10240
10320
|
// `reason`. Rendering "not provided" makes a missing rationale
|
|
10241
10321
|
// visibly an agent-side failure (the tool description nudges the
|
|
@@ -10244,12 +10324,12 @@ function renderVaultRequestAccessCard(req: PendingVaultRequestAccess): string {
|
|
|
10244
10324
|
// template choice, which the operator couldn't tell apart from a
|
|
10245
10325
|
// legitimate "no reason needed" case.
|
|
10246
10326
|
if (req.reason && req.reason.length > 0) {
|
|
10247
|
-
lines.push(`why:
|
|
10327
|
+
lines.push(`why: _${escapeHtmlForTg(req.reason)}_`)
|
|
10248
10328
|
} else {
|
|
10249
|
-
lines.push(`why:
|
|
10329
|
+
lines.push(`why: _not provided_`)
|
|
10250
10330
|
}
|
|
10251
10331
|
lines.push('')
|
|
10252
|
-
lines.push(
|
|
10332
|
+
lines.push(`_Tap Approve to mint a scoped grant token (same flow as \`switchroom vault grant\`). Tap Deny to refuse ā the agent will receive a denial result._`)
|
|
10253
10333
|
return lines.join('\n')
|
|
10254
10334
|
}
|
|
10255
10335
|
|
|
@@ -10355,8 +10435,7 @@ async function executeVaultRequestAccess(args: Record<string, unknown>): Promise
|
|
|
10355
10435
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
10356
10436
|
robustApiCall,
|
|
10357
10437
|
(tid) =>
|
|
10358
|
-
lockedBot.api.
|
|
10359
|
-
parse_mode: 'HTML',
|
|
10438
|
+
lockedBot.api.sendRichMessage(chat_id, richMessage(text), {
|
|
10360
10439
|
reply_markup: buildVaultRequestAccessKeyboard(stageId),
|
|
10361
10440
|
...(tid != null && Number.isFinite(tid) ? { message_thread_id: tid } : {}),
|
|
10362
10441
|
}),
|
|
@@ -10425,8 +10504,10 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
|
|
|
10425
10504
|
if (args.text == null || args.text === '') throw new Error('edit_message: text is required and cannot be empty')
|
|
10426
10505
|
assertAllowedChat(String(args.chat_id ?? ''))
|
|
10427
10506
|
const editAccess = loadAccess()
|
|
10428
|
-
const
|
|
10429
|
-
|
|
10507
|
+
const editFormat = (args.format as string | undefined) ?? (editAccess.parseMode ?? 'html')
|
|
10508
|
+
// Single rich-markdown path (#2669): `format:'text'` edits as a literal
|
|
10509
|
+
// plain string; everything else edits via the rich-markdown path.
|
|
10510
|
+
const editLiteralText = editFormat === 'text'
|
|
10430
10511
|
let editRawText = repairEscapedWhitespace(args.text as string)
|
|
10431
10512
|
// Outbound secret scrub (#2044): an edit must not re-introduce a raw
|
|
10432
10513
|
// secret into a live bubble or the history row. Mask before scrub/send.
|
|
@@ -10447,22 +10528,10 @@ async function executeEditMessage(args: Record<string, unknown>): Promise<unknow
|
|
|
10447
10528
|
})
|
|
10448
10529
|
}
|
|
10449
10530
|
}
|
|
10450
|
-
let editParseMode: 'HTML' | 'MarkdownV2' | undefined
|
|
10451
|
-
let editText: string
|
|
10452
|
-
if (editFormat === 'html') {
|
|
10453
|
-
editParseMode = 'HTML'
|
|
10454
|
-
editText = markdownToHtml(editRawText)
|
|
10455
|
-
} else if (editFormat === 'markdownv2') {
|
|
10456
|
-
editParseMode = 'MarkdownV2'
|
|
10457
|
-
editText = escapeMarkdownV2(editRawText)
|
|
10458
|
-
} else {
|
|
10459
|
-
editParseMode = undefined
|
|
10460
|
-
editText = editRawText
|
|
10461
|
-
}
|
|
10462
10531
|
const edited = await robustApiCall(
|
|
10463
10532
|
() => lockedBot.api.editMessageText(
|
|
10464
|
-
String(args.chat_id ?? ''), Number(args.message_id),
|
|
10465
|
-
|
|
10533
|
+
String(args.chat_id ?? ''), Number(args.message_id),
|
|
10534
|
+
editLiteralText ? editRawText : richMessage(editRawText),
|
|
10466
10535
|
),
|
|
10467
10536
|
)
|
|
10468
10537
|
const id = typeof edited === 'object' && edited ? (edited as any).message_id : args.message_id
|
|
@@ -10696,7 +10765,6 @@ function closeActivityLane(chatId: string, threadId: number | undefined): void {
|
|
|
10696
10765
|
const stream = activeDraftStreams.get(key)
|
|
10697
10766
|
if (stream == null) return
|
|
10698
10767
|
activeDraftStreams.delete(key)
|
|
10699
|
-
activeDraftParseModes.delete(key)
|
|
10700
10768
|
void stream.finalize().catch(() => {})
|
|
10701
10769
|
}
|
|
10702
10770
|
|
|
@@ -10708,7 +10776,6 @@ function closeProgressLane(chatId: string, threadId: number | undefined): void {
|
|
|
10708
10776
|
for (const [key, stream] of activeDraftStreams) {
|
|
10709
10777
|
if (key.startsWith(prefix)) {
|
|
10710
10778
|
activeDraftStreams.delete(key)
|
|
10711
|
-
activeDraftParseModes.delete(key)
|
|
10712
10779
|
void stream.finalize().catch(() => {})
|
|
10713
10780
|
}
|
|
10714
10781
|
}
|
|
@@ -10916,7 +10983,7 @@ async function drainActivitySummary(
|
|
|
10916
10983
|
break
|
|
10917
10984
|
}
|
|
10918
10985
|
// `renderActivityFeed` already emitted ready Telegram HTML with per-line
|
|
10919
|
-
// markup (
|
|
10986
|
+
// markup (**ā current** / _ā done_) and escaped each label's
|
|
10920
10987
|
// <,>,& itself (#1942 class) ā send verbatim, do NOT re-escape or
|
|
10921
10988
|
// re-wrap (double-escaping would surface literal tags).
|
|
10922
10989
|
const html = target
|
|
@@ -10932,9 +10999,9 @@ async function drainActivitySummary(
|
|
|
10932
10999
|
try {
|
|
10933
11000
|
if (turn.activityMessageId == null) {
|
|
10934
11001
|
const sent = await robustApiCall(
|
|
10935
|
-
|
|
11002
|
+
// allow-raw-bot-api: sendRichMessage routed through robustApiCall (not in the THREAD_NOT_FOUND blast pattern)
|
|
11003
|
+
() => bot.api.sendRichMessage(chat, richMessage(html), {
|
|
10936
11004
|
...(thread != null ? { message_thread_id: thread } : {}),
|
|
10937
|
-
parse_mode: 'HTML',
|
|
10938
11005
|
disable_notification: true,
|
|
10939
11006
|
...replyAnchor,
|
|
10940
11007
|
}),
|
|
@@ -10945,7 +11012,7 @@ async function drainActivitySummary(
|
|
|
10945
11012
|
} else {
|
|
10946
11013
|
const id = turn.activityMessageId
|
|
10947
11014
|
await robustApiCall(
|
|
10948
|
-
() => bot.api.editMessageText(chat, id, html, {
|
|
11015
|
+
() => bot.api.editMessageText(chat, id, richMessage(html), {}),
|
|
10949
11016
|
{ chat_id: chat, ...(thread != null ? { threadId: thread } : {}), verb: 'activity-summary.edit' },
|
|
10950
11017
|
)
|
|
10951
11018
|
}
|
|
@@ -11257,7 +11324,7 @@ function clearActivitySummary(turn: CurrentTurn, finalHtmlOverride?: string | nu
|
|
|
11257
11324
|
if (finalHtml == null) return
|
|
11258
11325
|
try {
|
|
11259
11326
|
await robustApiCall(
|
|
11260
|
-
() => bot.api.editMessageText(chat, id, finalHtml, {
|
|
11327
|
+
() => bot.api.editMessageText(chat, id, richMessage(finalHtml), {}),
|
|
11261
11328
|
{ chat_id: chat, ...(thread != null ? { threadId: thread } : {}), verb: 'activity-summary.finalize' },
|
|
11262
11329
|
)
|
|
11263
11330
|
} catch (err) {
|
|
@@ -11781,15 +11848,10 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
11781
11848
|
// (no flash). The draft transport is permanently retired ā both modes
|
|
11782
11849
|
// use sendMessage + editMessageText for any message that does open.
|
|
11783
11850
|
minInitialChars: ANSWER_LANE.minInitialChars,
|
|
11784
|
-
//
|
|
11785
|
-
//
|
|
11786
|
-
//
|
|
11787
|
-
//
|
|
11788
|
-
// shipped raw text ā `**bold**` arrived as literal asterisks and
|
|
11789
|
-
// agent narration read unformatted (the answer-stream-raw-markdown
|
|
11790
|
-
// bug). Injected as a dependency (same pattern as the PTY partial
|
|
11791
|
-
// handler's `renderText`) so answer-stream.ts stays format-free.
|
|
11792
|
-
renderText: (text: string) => sanitizeTelegramHtml(markdownToHtml(text)),
|
|
11851
|
+
// #2669: the answer-stream ships the RAW transcript markdown; the
|
|
11852
|
+
// sendMessage/editMessageText wrappers below send it via the
|
|
11853
|
+
// rich-message path (`sendRichMessage` / `editMessageText({ markdown })`),
|
|
11854
|
+
// matching every other outbound lane. No render step needed.
|
|
11793
11855
|
// #1075: route through robustApiCall so flood-wait,
|
|
11794
11856
|
// benign-400, and THREAD_NOT_FOUND are handled uniformly
|
|
11795
11857
|
// instead of crashing the answer-stream loop on a deleted
|
|
@@ -11825,13 +11887,11 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
11825
11887
|
const silent = params?.purpose !== 'materialize'
|
|
11826
11888
|
const msg = await robustApiCall(
|
|
11827
11889
|
() =>
|
|
11828
|
-
bot
|
|
11829
|
-
|
|
11890
|
+
// allow-raw-bot-api: sendRichMessage is not in the THREAD_NOT_FOUND blast pattern; answer-stream tolerates failures via its own try/catch
|
|
11891
|
+
// sendRichMessage doesn't accept link_preview_options ā omit it.
|
|
11892
|
+
bot.api.sendRichMessage(chatId, richMessage(text), {
|
|
11830
11893
|
disable_notification: silent,
|
|
11831
11894
|
...(tid != null ? { message_thread_id: tid } : {}),
|
|
11832
|
-
...(params?.link_preview_options != null
|
|
11833
|
-
? { link_preview_options: params.link_preview_options }
|
|
11834
|
-
: {}),
|
|
11835
11895
|
...(params?.reply_parameters != null
|
|
11836
11896
|
? { reply_parameters: params.reply_parameters }
|
|
11837
11897
|
: {}),
|
|
@@ -11848,8 +11908,7 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
11848
11908
|
const tid = params?.message_thread_id
|
|
11849
11909
|
return robustApiCall(
|
|
11850
11910
|
() =>
|
|
11851
|
-
bot.api.editMessageText(chatId, messageId, text, {
|
|
11852
|
-
parse_mode: params?.parse_mode,
|
|
11911
|
+
bot.api.editMessageText(chatId, messageId, richMessage(text), {
|
|
11853
11912
|
...(tid != null ? { message_thread_id: tid } : {}),
|
|
11854
11913
|
...(params?.link_preview_options != null
|
|
11855
11914
|
? { link_preview_options: params.link_preview_options }
|
|
@@ -11945,16 +12004,16 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
11945
12004
|
lastContextExhaustionWarningAt = now
|
|
11946
12005
|
process.stderr.write(`telegram gateway: context exhaustion detected ā notifying user\n`)
|
|
11947
12006
|
const warnOpts = {
|
|
11948
|
-
parse_mode: 'HTML' as const,
|
|
11949
12007
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
11950
12008
|
}
|
|
11951
12009
|
// #1075: thread-id-bearing, fire-and-forget ā swallow on
|
|
11952
12010
|
// THREAD_NOT_FOUND so a deleted topic doesn't crash the gateway.
|
|
11953
12011
|
void swallowingApiCall(
|
|
11954
12012
|
() =>
|
|
11955
|
-
bot
|
|
12013
|
+
// allow-raw-bot-api: wrapped in swallowingApiCall (retry policy)
|
|
12014
|
+
bot.api.sendRichMessage(
|
|
11956
12015
|
chatId,
|
|
11957
|
-
'ā ļø
|
|
12016
|
+
richMessage('ā ļø **Context window full** ā send \`/restart\` to start a fresh session.'),
|
|
11958
12017
|
warnOpts,
|
|
11959
12018
|
),
|
|
11960
12019
|
{
|
|
@@ -12313,7 +12372,6 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
12313
12372
|
for (const [key] of activeDraftStreams) {
|
|
12314
12373
|
if (key.startsWith(suppressPrefix)) {
|
|
12315
12374
|
activeDraftStreams.delete(key)
|
|
12316
|
-
activeDraftParseModes.delete(key)
|
|
12317
12375
|
}
|
|
12318
12376
|
}
|
|
12319
12377
|
// Unpin without editing the message so no orphaned card lingers.
|
|
@@ -12414,9 +12472,9 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
12414
12472
|
// that fires when the model emits raw transcript text WITHOUT
|
|
12415
12473
|
// calling reply / stream_reply. That captured text bypasses
|
|
12416
12474
|
// PR #1683's executeReply scrub site entirely and is delivered
|
|
12417
|
-
// via
|
|
12418
|
-
//
|
|
12419
|
-
//
|
|
12475
|
+
// via the rich-message path directly. Scrub the capturedText on the
|
|
12476
|
+
// raw markdown so em-dashes never reach the wire. Kill switch:
|
|
12477
|
+
// SWITCHROOM_DISABLE_VOICE_SCRUB.
|
|
12420
12478
|
{
|
|
12421
12479
|
const scrub = scrubVoice(capturedText)
|
|
12422
12480
|
if (scrub.replaced > 0) {
|
|
@@ -12517,13 +12575,11 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
12517
12575
|
`(chat=${backstopChatId} cardMsgId=${backstopCardMessageId ?? 'none'})\n`,
|
|
12518
12576
|
)
|
|
12519
12577
|
const sendOpts = {
|
|
12520
|
-
parse_mode: 'HTML' as const,
|
|
12521
12578
|
...(backstopThreadId != null ? { message_thread_id: backstopThreadId } : {}),
|
|
12522
12579
|
link_preview_options: { is_disabled: true },
|
|
12523
12580
|
}
|
|
12524
|
-
const
|
|
12525
|
-
const
|
|
12526
|
-
const htmlChunks = splitHtmlChunks(renderedText, limit)
|
|
12581
|
+
const limit = RICH_MESSAGE_MAX_CHARS
|
|
12582
|
+
const htmlChunks = splitMarkdownChunks(capturedText, limit)
|
|
12527
12583
|
const sentIds: number[] = []
|
|
12528
12584
|
try {
|
|
12529
12585
|
// #654 deterministic double-message fix. If the progress
|
|
@@ -12552,7 +12608,7 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
12552
12608
|
bot.api.editMessageText(
|
|
12553
12609
|
backstopChatId,
|
|
12554
12610
|
backstopCardMessageId,
|
|
12555
|
-
htmlChunks[0],
|
|
12611
|
+
richMessage(htmlChunks[0]),
|
|
12556
12612
|
sendOpts,
|
|
12557
12613
|
),
|
|
12558
12614
|
{
|
|
@@ -12578,11 +12634,10 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
12578
12634
|
robustApiCall,
|
|
12579
12635
|
(tid) => {
|
|
12580
12636
|
const opts = {
|
|
12581
|
-
parse_mode: 'HTML' as const,
|
|
12582
12637
|
link_preview_options: { is_disabled: true },
|
|
12583
12638
|
...(tid != null ? { message_thread_id: tid } : {}),
|
|
12584
12639
|
}
|
|
12585
|
-
return bot.api.
|
|
12640
|
+
return bot.api.sendRichMessage(backstopChatId, richMessage(c), opts)
|
|
12586
12641
|
},
|
|
12587
12642
|
{
|
|
12588
12643
|
threadId: liveThreadId,
|
|
@@ -12832,7 +12887,6 @@ function handlePtyPartial(text: string): void {
|
|
|
12832
12887
|
currentSessionThreadId: turn?.sessionThreadId,
|
|
12833
12888
|
pendingPtyPartial: pendingPtyPartial != null ? { text: pendingPtyPartial } : null,
|
|
12834
12889
|
activeDraftStreams,
|
|
12835
|
-
activeDraftParseModes,
|
|
12836
12890
|
suppressPtyPreview,
|
|
12837
12891
|
lastPtyPreviewByChat,
|
|
12838
12892
|
}
|
|
@@ -12841,7 +12895,6 @@ function handlePtyPartial(text: string): void {
|
|
|
12841
12895
|
// interface; runtime-compatible. Cast through never.
|
|
12842
12896
|
bot: bot as never,
|
|
12843
12897
|
retry: robustApiCall,
|
|
12844
|
-
renderText: markdownToHtml,
|
|
12845
12898
|
logEvent: logStreamingEvent,
|
|
12846
12899
|
onStreamSend: (chatId, messageId, charCount) => {
|
|
12847
12900
|
logOutbound('pty_preview', chatId, messageId, charCount, 'initial_send')
|
|
@@ -12876,13 +12929,11 @@ function handlePtyActivity(text: string): void {
|
|
|
12876
12929
|
format: 'text',
|
|
12877
12930
|
lane: 'activity',
|
|
12878
12931
|
},
|
|
12879
|
-
{ activeDraftStreams
|
|
12932
|
+
{ activeDraftStreams },
|
|
12880
12933
|
{
|
|
12881
12934
|
// See StreamBotApi cast comment above (line ~2522 area).
|
|
12882
12935
|
bot: bot as never,
|
|
12883
12936
|
retry: robustApiCall,
|
|
12884
|
-
markdownToHtml,
|
|
12885
|
-
escapeMarkdownV2,
|
|
12886
12937
|
repairEscapedWhitespace,
|
|
12887
12938
|
assertAllowedChat,
|
|
12888
12939
|
resolveThreadId,
|
|
@@ -13573,9 +13624,9 @@ async function handleInbound(
|
|
|
13573
13624
|
cleanAuthAddScratchDir(pendingAdd.scratchDir)
|
|
13574
13625
|
await switchroomReply(
|
|
13575
13626
|
ctx,
|
|
13576
|
-
`ā Account
|
|
13627
|
+
`ā Account \`${pendingAdd.label}\` added.\n` +
|
|
13577
13628
|
`The fleet's active account hasn't changed. Send ` +
|
|
13578
|
-
|
|
13629
|
+
`\`/auth use ${escapeHtmlForTg(pendingAdd.label)}\` to switch to it.`,
|
|
13579
13630
|
{ html: true },
|
|
13580
13631
|
)
|
|
13581
13632
|
} catch (brokerErr) {
|
|
@@ -13585,7 +13636,7 @@ async function handleInbound(
|
|
|
13585
13636
|
cleanAuthAddScratchDir(pendingAdd.scratchDir)
|
|
13586
13637
|
await switchroomReply(
|
|
13587
13638
|
ctx,
|
|
13588
|
-
|
|
13639
|
+
`**/auth add failed at broker:** ${escapeHtmlForTg((brokerErr as Error)?.message ?? String(brokerErr))}`,
|
|
13589
13640
|
{ html: true },
|
|
13590
13641
|
)
|
|
13591
13642
|
}
|
|
@@ -13594,7 +13645,7 @@ async function handleInbound(
|
|
|
13594
13645
|
// failure paths (timeout, child exit, stdin broken).
|
|
13595
13646
|
await switchroomReply(
|
|
13596
13647
|
ctx,
|
|
13597
|
-
|
|
13648
|
+
`**/auth add code failed:** ${escapeHtmlForTg((err as Error)?.message ?? String(err))}`,
|
|
13598
13649
|
{ html: true },
|
|
13599
13650
|
)
|
|
13600
13651
|
}
|
|
@@ -13616,7 +13667,7 @@ async function handleInbound(
|
|
|
13616
13667
|
pendingReauthFlows.delete(interceptKey)
|
|
13617
13668
|
const { result, errorText } = execAuthCode(pendingReauth.agent, text.trim())
|
|
13618
13669
|
if (errorText) {
|
|
13619
|
-
await switchroomReply(ctx,
|
|
13670
|
+
await switchroomReply(ctx, `**auth code failed:**\n${preBlock(formatSwitchroomOutput(errorText))}`, { html: true })
|
|
13620
13671
|
} else if (result) {
|
|
13621
13672
|
const outcomeMsg = renderAuthCodeOutcome(result.outcome)
|
|
13622
13673
|
if (outcomeMsg) {
|
|
@@ -13668,7 +13719,7 @@ async function handleInbound(
|
|
|
13668
13719
|
if (result.ok) {
|
|
13669
13720
|
await switchroomReply(ctx, 'š Vault broker unlocked.', { html: true })
|
|
13670
13721
|
} else {
|
|
13671
|
-
await switchroomReply(ctx,
|
|
13722
|
+
await switchroomReply(ctx, `**vault unlock failed:** ${escapeHtmlForTg(result.msg ?? 'unknown error')}`, { html: true })
|
|
13672
13723
|
}
|
|
13673
13724
|
} else if (pendingVault.kind === 'passphrase-for-deferred') {
|
|
13674
13725
|
// Issue #44: passphrase entered after tapping "š Unlock vault &
|
|
@@ -13717,8 +13768,8 @@ async function handleInbound(
|
|
|
13717
13768
|
.editMessageText(
|
|
13718
13769
|
item.cardChatId,
|
|
13719
13770
|
item.cardMessageId,
|
|
13720
|
-
`ā
|
|
13721
|
-
{
|
|
13771
|
+
richMessage(`ā _Vault unlocked, but this access-request card expired or was denied before you replied. Ask the agent to re-issue if still needed._`),
|
|
13772
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
13722
13773
|
)
|
|
13723
13774
|
.catch(() => {})
|
|
13724
13775
|
continue
|
|
@@ -13732,7 +13783,7 @@ async function handleInbound(
|
|
|
13732
13783
|
if (ttlSeconds === null) {
|
|
13733
13784
|
// Re-set state so user can try again
|
|
13734
13785
|
pendingVaultOps.set(chat_id, { ...pendingVault, startedAt: Date.now() })
|
|
13735
|
-
await switchroomReply(ctx, 'ā ļø Invalid duration. Use
|
|
13786
|
+
await switchroomReply(ctx, 'ā ļø Invalid duration. Use \`Nd\` (days) or \`Nh\` (hours), e.g. \`30d\` or \`12h\`.', { html: true })
|
|
13736
13787
|
return
|
|
13737
13788
|
}
|
|
13738
13789
|
const newState = { ...pendingVault, ttlSeconds, awaitingCustomDuration: false }
|
|
@@ -13759,7 +13810,7 @@ async function handleInbound(
|
|
|
13759
13810
|
if (!VAULT_KEY_REGEX.test(newKey)) {
|
|
13760
13811
|
// Re-arm the pending state so the user can try again.
|
|
13761
13812
|
pendingVaultOps.set(chat_id, { ...pendingVault, startedAt: Date.now() })
|
|
13762
|
-
await switchroomReply(ctx, `ā ļø Key must match
|
|
13813
|
+
await switchroomReply(ctx, `ā ļø Key must match \`${VAULT_KEY_REGEX_LABEL}\`. Send a different name.`, { html: true })
|
|
13763
13814
|
return
|
|
13764
13815
|
}
|
|
13765
13816
|
staged.key = newKey
|
|
@@ -13770,8 +13821,8 @@ async function handleInbound(
|
|
|
13770
13821
|
.editMessageText(
|
|
13771
13822
|
staged.chat_id,
|
|
13772
13823
|
staged.card_message_id,
|
|
13773
|
-
renderVaultRequestSaveCard(staged, staged.agent),
|
|
13774
|
-
{
|
|
13824
|
+
richMessage(renderVaultRequestSaveCard(staged, staged.agent)),
|
|
13825
|
+
{ reply_markup: buildVaultRequestSaveKeyboard(pendingVault.stageId) },
|
|
13775
13826
|
)
|
|
13776
13827
|
.catch(() => {})
|
|
13777
13828
|
}
|
|
@@ -13806,7 +13857,7 @@ async function handleInbound(
|
|
|
13806
13857
|
if (verb === 'stash' || verb === 'rename') {
|
|
13807
13858
|
const cached = vaultPassphraseCache.get(chat_id)
|
|
13808
13859
|
if (!cached || cached.expiresAt <= Date.now()) {
|
|
13809
|
-
await switchroomReply(ctx, 'No vault passphrase cached. Run
|
|
13860
|
+
await switchroomReply(ctx, 'No vault passphrase cached. Run \`/vault list\` first (or any /vault command) to unlock, then re-send \`stash NAME\`.', { html: true })
|
|
13810
13861
|
return
|
|
13811
13862
|
}
|
|
13812
13863
|
const slugBase = arg && arg.length > 0 ? arg : staged.detection.suggested_slug
|
|
@@ -13824,14 +13875,14 @@ async function handleInbound(
|
|
|
13824
13875
|
const rendered = renderVaultCliError(parsed, { verb: 'save', key: slug })
|
|
13825
13876
|
const body = rendered.suppressRaw
|
|
13826
13877
|
? rendered.html
|
|
13827
|
-
:
|
|
13878
|
+
: `**vault write failed:**\n${preBlock(write.output)}`
|
|
13828
13879
|
await switchroomReply(ctx, body, { html: true })
|
|
13829
13880
|
return
|
|
13830
13881
|
}
|
|
13831
13882
|
secretStaging.delete(staged.chat_id, staged.message_id)
|
|
13832
13883
|
if (msgId != null) await deleteSensitiveMessage(chat_id, msgId, 'stash command')
|
|
13833
13884
|
await deleteSensitiveMessage(chat_id, staged.message_id, 'detected secret')
|
|
13834
|
-
await switchroomReply(ctx, `ā
stored as
|
|
13885
|
+
await switchroomReply(ctx, `ā
stored as \`vault:${slug}\` (masked: \`${maskToken(staged.detection.matched_text)}\`)`, { html: true })
|
|
13835
13886
|
return
|
|
13836
13887
|
}
|
|
13837
13888
|
}
|
|
@@ -13921,11 +13972,11 @@ async function handleInbound(
|
|
|
13921
13972
|
await deleteSensitiveMessage(chat_id, msgId, 'detected secret')
|
|
13922
13973
|
}
|
|
13923
13974
|
const lines = pipeRes.stored.map((s) =>
|
|
13924
|
-
`ā¢
|
|
13975
|
+
`⢠\`${s.masked}\` ā \`vault:${s.actual_slug}\``,
|
|
13925
13976
|
)
|
|
13926
13977
|
await switchroomReply(
|
|
13927
13978
|
ctx,
|
|
13928
|
-
[`š captured ${pipeRes.stored.length} secret${pipeRes.stored.length === 1 ? '' : 's'}:`, ...lines, '', 'reply
|
|
13979
|
+
[`š captured ${pipeRes.stored.length} secret${pipeRes.stored.length === 1 ? '' : 's'}:`, ...lines, '', 'reply \`rename X\` or \`forget\`.'].join('\n'),
|
|
13929
13980
|
{ html: true },
|
|
13930
13981
|
)
|
|
13931
13982
|
for (const s of pipeRes.stored) {
|
|
@@ -13978,7 +14029,7 @@ async function handleInbound(
|
|
|
13978
14029
|
const top = pipeRes.ambiguous[0]!
|
|
13979
14030
|
await switchroomReply(
|
|
13980
14031
|
ctx,
|
|
13981
|
-
`š looks like a high-entropy string (rule:
|
|
14032
|
+
`š looks like a high-entropy string (rule: \`${top.rule_id}\`). reply \`stash NAME\` to store in vault, or \`ignore\`.`,
|
|
13982
14033
|
{ html: true },
|
|
13983
14034
|
)
|
|
13984
14035
|
// For ambiguous, we do NOT delete the message or rewrite ā let the
|
|
@@ -14127,7 +14178,6 @@ async function handleInbound(
|
|
|
14127
14178
|
// content bug. Delete from the map FIRST so any concurrent
|
|
14128
14179
|
// reads can't see the stale stream while we await.
|
|
14129
14180
|
activeDraftStreams.delete(sKey)
|
|
14130
|
-
activeDraftParseModes.delete(sKey)
|
|
14131
14181
|
await priorStream.finalize().catch(() => {})
|
|
14132
14182
|
}
|
|
14133
14183
|
suppressPtyPreview.delete(sKey)
|
|
@@ -14647,12 +14697,15 @@ function stripAnsi(text: string): string {
|
|
|
14647
14697
|
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
14648
14698
|
}
|
|
14649
14699
|
|
|
14700
|
+
// #2669: escape GFM-markdown specials in dynamic values interpolated into
|
|
14701
|
+
// rich-message bodies (kept under the legacy name to avoid churn).
|
|
14650
14702
|
function escapeHtmlForTg(text: string): string {
|
|
14651
|
-
return text.replace(
|
|
14703
|
+
return text.replace(/([\\`*_~=\[\]|])/g, '\\$1')
|
|
14652
14704
|
}
|
|
14653
14705
|
|
|
14706
|
+
// Wrap CLI/command output in a fenced code block (content is literal there).
|
|
14654
14707
|
function preBlock(text: string): string {
|
|
14655
|
-
return '
|
|
14708
|
+
return '```\n' + text.replace(/```/g, '`ā``') + '\n```'
|
|
14656
14709
|
}
|
|
14657
14710
|
|
|
14658
14711
|
type SwitchroomReplyMarkup =
|
|
@@ -14682,11 +14735,17 @@ async function switchroomReply(
|
|
|
14682
14735
|
? slashCommandReplyOpts(ctx, options.classification)
|
|
14683
14736
|
: {}
|
|
14684
14737
|
const threadId = routedOpts.message_thread_id ?? baseThreadId
|
|
14685
|
-
|
|
14738
|
+
const replyOpts = {
|
|
14686
14739
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
14687
|
-
...(options.html ? { parse_mode: 'HTML' as const, link_preview_options: { is_disabled: true } } : {}),
|
|
14688
14740
|
...(options.reply_markup ? { reply_markup: options.reply_markup } : {}),
|
|
14689
|
-
}
|
|
14741
|
+
}
|
|
14742
|
+
// #2669: `options.html` now means "render `text` as GFM markdown via the
|
|
14743
|
+
// rich-message path" (legacy field name kept). Plain otherwise.
|
|
14744
|
+
if (options.html) {
|
|
14745
|
+
await ctx.replyWithRichMessage(richMessage(text), replyOpts)
|
|
14746
|
+
} else {
|
|
14747
|
+
await ctx.reply(text, replyOpts)
|
|
14748
|
+
}
|
|
14690
14749
|
}
|
|
14691
14750
|
|
|
14692
14751
|
/**
|
|
@@ -14725,10 +14784,11 @@ async function deleteSensitiveMessage(
|
|
|
14725
14784
|
// the stderr log is the audit trail.
|
|
14726
14785
|
await swallowingApiCall(
|
|
14727
14786
|
() =>
|
|
14728
|
-
bot
|
|
14787
|
+
// allow-raw-bot-api: wrapped in swallowingApiCall (retry policy)
|
|
14788
|
+
bot.api.sendRichMessage(
|
|
14729
14789
|
chatId,
|
|
14730
|
-
`ā ļø
|
|
14731
|
-
{
|
|
14790
|
+
richMessage(`ā ļø **Could not auto-delete message containing your ${escapeHtmlForTg(reason)}.**\n\nPlease delete message \`${msgId}\` manually so the value is not retained in chat history.\n\n_Reason: ${escapeHtmlForTg(msg)}_`),
|
|
14791
|
+
{},
|
|
14732
14792
|
),
|
|
14733
14793
|
{ chat_id: chatId, verb: 'deleteSensitiveMessage.warning' },
|
|
14734
14794
|
)
|
|
@@ -15233,7 +15293,7 @@ function notifyDetachedFailure(
|
|
|
15233
15293
|
clearRestartMarker()
|
|
15234
15294
|
const snippet = tail ? tail.slice(-800) : '(no output captured)'
|
|
15235
15295
|
const text =
|
|
15236
|
-
`ā
|
|
15296
|
+
`ā **${escapeHtmlForTg(label)} failed** (exit ${code}):\n` +
|
|
15237
15297
|
preBlock(snippet)
|
|
15238
15298
|
// Fire-and-forget ā we're off the command-handler context and don't
|
|
15239
15299
|
// have an await to block on anyway.
|
|
@@ -15242,9 +15302,7 @@ function notifyDetachedFailure(
|
|
|
15242
15302
|
// failure handler.
|
|
15243
15303
|
void swallowingApiCall(
|
|
15244
15304
|
() =>
|
|
15245
|
-
lockedBot.api.
|
|
15246
|
-
parse_mode: 'HTML',
|
|
15247
|
-
link_preview_options: { is_disabled: true },
|
|
15305
|
+
lockedBot.api.sendRichMessage(chatId, richMessage(text), {
|
|
15248
15306
|
// Detached restart/update child-failure notice ā status, not
|
|
15249
15307
|
// the user's answer. Silence the open ping.
|
|
15250
15308
|
disable_notification: true,
|
|
@@ -15353,8 +15411,8 @@ function formatAuthOutputForTelegram(output: string): { text: string; url: strin
|
|
|
15353
15411
|
return true
|
|
15354
15412
|
})
|
|
15355
15413
|
const rendered = body.map(line => {
|
|
15356
|
-
if (line.startsWith('Started Claude auth') || line.startsWith('Auth session already running')) return
|
|
15357
|
-
if (line.startsWith('Open this URL')) return
|
|
15414
|
+
if (line.startsWith('Started Claude auth') || line.startsWith('Auth session already running')) return `**${escapeHtmlForTg(line)}**`
|
|
15415
|
+
if (line.startsWith('Open this URL')) return `_${escapeHtmlForTg(line)}_`
|
|
15358
15416
|
return escapeHtmlForTg(line)
|
|
15359
15417
|
})
|
|
15360
15418
|
// Mobile-native post-script. Two paths depending on which Anthropic
|
|
@@ -15378,11 +15436,11 @@ function formatAuthOutputForTelegram(output: string): { text: string; url: strin
|
|
|
15378
15436
|
// the same outcome with no API constraint. See PR #30.
|
|
15379
15437
|
rendered.push(
|
|
15380
15438
|
'',
|
|
15381
|
-
'š Tap
|
|
15439
|
+
'š Tap **š Open Claude auth** below, then **reply with the browser code**.',
|
|
15382
15440
|
'',
|
|
15383
|
-
'
|
|
15441
|
+
'_Wrong Anthropic account getting authorized? Long-press the URL below and choose "Copy Link" or "Open in Browser" ā lands in your main browser where the right account is signed in, bypassing Telegram\'s in-app browser cookies._',
|
|
15384
15442
|
'',
|
|
15385
|
-
|
|
15443
|
+
url,
|
|
15386
15444
|
)
|
|
15387
15445
|
return { text: rendered.join('\n'), url }
|
|
15388
15446
|
}
|
|
@@ -15474,7 +15532,7 @@ async function runSwitchroomAuthCommand(ctx: Context, args: string[], label: str
|
|
|
15474
15532
|
if (error.message?.includes('ENOENT')) { await switchroomReply(ctx, 'switchroom CLI not found.', { html: true }); return }
|
|
15475
15533
|
if (error.message?.includes('ETIMEDOUT') || error.message?.includes('timed out')) { await switchroomReply(ctx, `${label}: timed out`); return }
|
|
15476
15534
|
const detail = stripAnsi(error.stderr?.trim() || error.message || 'unknown error')
|
|
15477
|
-
await switchroomReply(ctx,
|
|
15535
|
+
await switchroomReply(ctx, `**${escapeHtmlForTg(label)} failed:**\n${preBlock(formatSwitchroomOutput(detail))}`, { html: true })
|
|
15478
15536
|
}
|
|
15479
15537
|
}
|
|
15480
15538
|
|
|
@@ -15521,7 +15579,7 @@ function renderVaultOpFailure(
|
|
|
15521
15579
|
const verb = verbLabel === 'delete' ? 'remove' : verbLabel
|
|
15522
15580
|
const rendered = renderVaultCliError(parsed, { verb, key })
|
|
15523
15581
|
if (rendered.suppressRaw) return rendered.html
|
|
15524
|
-
return
|
|
15582
|
+
return `**vault ${verbLabel} failed:**\n${preBlock(cliOutput)}`
|
|
15525
15583
|
}
|
|
15526
15584
|
|
|
15527
15585
|
async function executeVaultOp(ctx: Context, chatId: string, op: 'list' | 'get' | 'set' | 'delete', key: string | undefined, passphrase: string, setValue: string | undefined): Promise<void> {
|
|
@@ -15530,24 +15588,24 @@ async function executeVaultOp(ctx: Context, chatId: string, op: 'list' | 'get' |
|
|
|
15530
15588
|
if (!r.ok) { await switchroomReply(ctx, renderVaultOpFailure('list', r.output, undefined), { html: true }); return }
|
|
15531
15589
|
const keys = r.output.split('\n').filter(Boolean)
|
|
15532
15590
|
if (keys.length === 0) { await switchroomReply(ctx, 'Vault is empty.', { html: true }) }
|
|
15533
|
-
else { await switchroomReply(ctx,
|
|
15591
|
+
else { await switchroomReply(ctx, `**Vault keys (${keys.length}):**\n${keys.map(k => `⢠\`${k}\``).join('\n')}`, { html: true }) }
|
|
15534
15592
|
} else if (op === 'get') {
|
|
15535
15593
|
const r = runVaultCli(['get', key!], passphrase)
|
|
15536
15594
|
if (!r.ok) { await switchroomReply(ctx, renderVaultOpFailure('get', r.output, key), { html: true }); return }
|
|
15537
|
-
await switchroomReply(ctx,
|
|
15595
|
+
await switchroomReply(ctx, `\`${key!}\` =\n\`${r.output}\``, { html: true })
|
|
15538
15596
|
} else if (op === 'set') {
|
|
15539
15597
|
if (setValue === undefined) {
|
|
15540
15598
|
pendingVaultOps.set(chatId, { kind: 'value', op: 'set', key: key!, passphrase, startedAt: Date.now() })
|
|
15541
|
-
await switchroomReply(ctx, `Send the value for
|
|
15599
|
+
await switchroomReply(ctx, `Send the value for \`${key!}\`.`, { html: true })
|
|
15542
15600
|
return
|
|
15543
15601
|
}
|
|
15544
15602
|
const r = runVaultCli(['set', key!], passphrase, setValue)
|
|
15545
15603
|
if (!r.ok) { await switchroomReply(ctx, renderVaultOpFailure('set', r.output, key), { html: true }) }
|
|
15546
|
-
else { await switchroomReply(ctx, `ā
|
|
15604
|
+
else { await switchroomReply(ctx, `ā
\`${key!}\` saved to vault.`, { html: true }) }
|
|
15547
15605
|
} else if (op === 'delete') {
|
|
15548
15606
|
const r = runVaultCli(['remove', key!], passphrase)
|
|
15549
15607
|
if (!r.ok) { await switchroomReply(ctx, renderVaultOpFailure('delete', r.output, key), { html: true }) }
|
|
15550
|
-
else { await switchroomReply(ctx, `ā
|
|
15608
|
+
else { await switchroomReply(ctx, `ā
\`${key!}\` removed from vault.`, { html: true }) }
|
|
15551
15609
|
}
|
|
15552
15610
|
}
|
|
15553
15611
|
|
|
@@ -15598,8 +15656,8 @@ async function dispatchShortVerbViaHostd(
|
|
|
15598
15656
|
if (hostdResp.result === 'started') {
|
|
15599
15657
|
await switchroomReply(
|
|
15600
15658
|
ctx,
|
|
15601
|
-
`š
|
|
15602
|
-
`(request_id
|
|
15659
|
+
`š **${escapeHtmlForTg(label)}** dispatched via hostd ` +
|
|
15660
|
+
`(request_id=\`${hostdResp.request_id}\`). ` +
|
|
15603
15661
|
`Check audit log for completion.`,
|
|
15604
15662
|
{ html: true },
|
|
15605
15663
|
)
|
|
@@ -15613,7 +15671,7 @@ async function dispatchShortVerbViaHostd(
|
|
|
15613
15671
|
'(no error tail returned)'
|
|
15614
15672
|
await switchroomReply(
|
|
15615
15673
|
ctx,
|
|
15616
|
-
`ā
|
|
15674
|
+
`ā **${escapeHtmlForTg(label)} failed via hostd** ` +
|
|
15617
15675
|
`(result=${escapeHtmlForTg(hostdResp.result)}):\n` +
|
|
15618
15676
|
preBlock(stripAnsi(errBody)),
|
|
15619
15677
|
{ html: true },
|
|
@@ -15640,7 +15698,7 @@ async function runSwitchroomCommand(
|
|
|
15640
15698
|
if (error.message?.includes('ENOENT')) { await switchroomReply(ctx, 'switchroom CLI not found.', { html: true, classification }); return }
|
|
15641
15699
|
if (error.message?.includes('ETIMEDOUT') || error.message?.includes('timed out')) { await switchroomReply(ctx, `${label}: timed out`, { classification }); return }
|
|
15642
15700
|
const detail = stripAnsi(error.stderr?.trim() || error.message || 'unknown error')
|
|
15643
|
-
await switchroomReply(ctx,
|
|
15701
|
+
await switchroomReply(ctx, `**${escapeHtmlForTg(label)} failed:**\n${preBlock(formatSwitchroomOutput(detail))}`, { html: true, classification })
|
|
15644
15702
|
}
|
|
15645
15703
|
}
|
|
15646
15704
|
|
|
@@ -15666,16 +15724,16 @@ function statusIcon(status: string): string {
|
|
|
15666
15724
|
function renderAuthCodeOutcome(outcome: AuthCodeOutcome | null | undefined): string | null {
|
|
15667
15725
|
if (!outcome || outcome.kind === 'success') return null
|
|
15668
15726
|
const tail = outcome.paneTailText
|
|
15669
|
-
? `\
|
|
15727
|
+
? `\n_${escapeHtmlForTg(outcome.paneTailText)}_`
|
|
15670
15728
|
: ''
|
|
15671
15729
|
switch (outcome.kind) {
|
|
15672
15730
|
case 'invalid-code':
|
|
15673
15731
|
case 'expired-code':
|
|
15674
|
-
return `Code rejected by Claude ā tap
|
|
15732
|
+
return `Code rejected by Claude ā tap **Restart flow** for a fresh URL.${tail}`
|
|
15675
15733
|
case 'pane-not-ready':
|
|
15676
|
-
return `Auth pane not ready ā tap
|
|
15734
|
+
return `Auth pane not ready ā tap **Retry**.`
|
|
15677
15735
|
case 'timeout':
|
|
15678
|
-
return `Still waiting after 2 min ā tap
|
|
15736
|
+
return `Still waiting after 2 min ā tap **Retry** or check \`switchroom auth list\`.${tail}`
|
|
15679
15737
|
}
|
|
15680
15738
|
}
|
|
15681
15739
|
|
|
@@ -15727,7 +15785,7 @@ async function runSwitchroomCommandFormatted(ctx: Context, args: string[], label
|
|
|
15727
15785
|
} catch (err: unknown) {
|
|
15728
15786
|
const error = err as { stderr?: string; message?: string }
|
|
15729
15787
|
const detail = stripAnsi(error.stderr?.trim() || error.message || 'unknown error')
|
|
15730
|
-
await switchroomReply(ctx,
|
|
15788
|
+
await switchroomReply(ctx, `**${escapeHtmlForTg(label)} failed:**\n${preBlock(formatSwitchroomOutput(detail))}`, { html: true })
|
|
15731
15789
|
}
|
|
15732
15790
|
}
|
|
15733
15791
|
|
|
@@ -15762,8 +15820,8 @@ bot.use(async (ctx, next) => {
|
|
|
15762
15820
|
const nameHtml = escapeHtmlForTg(myName)
|
|
15763
15821
|
const notFlagged =
|
|
15764
15822
|
decision.reason === 'other-agent'
|
|
15765
|
-
? `ā ļø
|
|
15766
|
-
: `ā ļø
|
|
15823
|
+
? `ā ļø \`${cmdHtml}\` targeting another agent is an admin operation ā this agent (\`${nameHtml}\`) isn't admin-flagged. Run it from an admin agent, or set \`admin: true\` for this agent in switchroom.yaml. (Self-restart is allowed: send \`/restart\` with no arg.)`
|
|
15824
|
+
: `ā ļø \`${cmdHtml}\` is an admin command ā this agent (\`${nameHtml}\`) isn't admin-flagged. Run it from an admin agent, or set \`admin: true\` for this agent in switchroom.yaml.`
|
|
15767
15825
|
if (!AGENT_ADMIN) {
|
|
15768
15826
|
// Unchanged behaviour: a non-admin agent never executes admin
|
|
15769
15827
|
// verbs locally and must not forward them to Claude.
|
|
@@ -15793,7 +15851,7 @@ bot.use(async (ctx, next) => {
|
|
|
15793
15851
|
)
|
|
15794
15852
|
await switchroomReply(
|
|
15795
15853
|
ctx,
|
|
15796
|
-
`ā ļø
|
|
15854
|
+
`ā ļø \`${cmdHtml}\` is a fleet-admin command ā it is **operator-private**. Send it as a direct message to me from your operator account (a private chat where your Telegram ID is on the access allowlist), not in a group or forum.`,
|
|
15797
15855
|
{ html: true },
|
|
15798
15856
|
)
|
|
15799
15857
|
return
|
|
@@ -16018,12 +16076,12 @@ bot.command('start', async ctx => {
|
|
|
16018
16076
|
const gated = dmCommandGate(ctx)
|
|
16019
16077
|
if (!gated) return
|
|
16020
16078
|
const disabled = gated.access.dmPolicy === 'disabled'
|
|
16021
|
-
await ctx.
|
|
16079
|
+
await ctx.replyWithRichMessage(richMessage(buildStartText(getMyAgentName(), disabled)))
|
|
16022
16080
|
})
|
|
16023
16081
|
|
|
16024
16082
|
bot.command('help', async ctx => {
|
|
16025
16083
|
if (!dmCommandGate(ctx)) return
|
|
16026
|
-
await ctx.
|
|
16084
|
+
await ctx.replyWithRichMessage(richMessage(buildHelpText(getMyAgentName())))
|
|
16027
16085
|
})
|
|
16028
16086
|
|
|
16029
16087
|
bot.command('status', async ctx => {
|
|
@@ -16038,12 +16096,12 @@ bot.command('status', async ctx => {
|
|
|
16038
16096
|
const demo = hasDemoFlag(getCommandArgs(ctx))
|
|
16039
16097
|
const userTag = from.username ? `@${from.username}` : senderId
|
|
16040
16098
|
const meta = await buildAgentMetadata(getMyAgentName())
|
|
16041
|
-
await ctx.
|
|
16099
|
+
await ctx.replyWithRichMessage(richMessage(buildStatusPairedText({ user: userTag, meta, demo })))
|
|
16042
16100
|
return
|
|
16043
16101
|
}
|
|
16044
16102
|
for (const [code, p] of Object.entries(access.pending)) {
|
|
16045
16103
|
if (p.senderId === senderId) {
|
|
16046
|
-
await ctx.
|
|
16104
|
+
await ctx.replyWithRichMessage(richMessage(buildStatusPendingText(code)))
|
|
16047
16105
|
return
|
|
16048
16106
|
}
|
|
16049
16107
|
}
|
|
@@ -16056,11 +16114,11 @@ bot.command('agents', async ctx => {
|
|
|
16056
16114
|
type AgentListResp = { agents: Array<{ name: string; status: string; uptime: string; template: string; topic_name: string; topic_emoji?: string }> }
|
|
16057
16115
|
const data = switchroomExecJson<AgentListResp>(['agent', 'list'])
|
|
16058
16116
|
if (!data) return null
|
|
16059
|
-
if (data.agents.length === 0) return '
|
|
16060
|
-
const lines = ['
|
|
16117
|
+
if (data.agents.length === 0) return '_No agents defined_'
|
|
16118
|
+
const lines = ['**Agents**']
|
|
16061
16119
|
for (const a of data.agents) {
|
|
16062
|
-
lines.push(`${statusIcon(a.status)}
|
|
16063
|
-
lines.push(`
|
|
16120
|
+
lines.push(`${statusIcon(a.status)} **${escapeHtmlForTg(a.name)}** Ā· ${escapeHtmlForTg(a.status)} Ā· ${escapeHtmlForTg(a.uptime)}`)
|
|
16121
|
+
lines.push(` _${escapeHtmlForTg(a.template)} ā ${escapeHtmlForTg(a.topic_name)}${a.topic_emoji ? ' ' + a.topic_emoji : ''}_`)
|
|
16064
16122
|
}
|
|
16065
16123
|
return lines.join('\n')
|
|
16066
16124
|
})
|
|
@@ -16335,8 +16393,7 @@ bot.command('restart', async ctx => {
|
|
|
16335
16393
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
16336
16394
|
robustApiCall,
|
|
16337
16395
|
(tid) =>
|
|
16338
|
-
lockedBot.api.
|
|
16339
|
-
parse_mode: 'HTML', link_preview_options: { is_disabled: true },
|
|
16396
|
+
lockedBot.api.sendRichMessage(chatId, richMessage(ackText), {
|
|
16340
16397
|
// Restart acknowledgement is a status notice ā silence the
|
|
16341
16398
|
// open ping (the "restarted ā ready" follow-up is what matters).
|
|
16342
16399
|
disable_notification: true,
|
|
@@ -16378,7 +16435,7 @@ bot.command('restart', async ctx => {
|
|
|
16378
16435
|
clearRestartMarker()
|
|
16379
16436
|
await switchroomReply(
|
|
16380
16437
|
ctx,
|
|
16381
|
-
`ā
|
|
16438
|
+
`ā **restart ${escapeHtmlForTg(name)} failed via hostd** ` +
|
|
16382
16439
|
`(result=${escapeHtmlForTg(hostdResp.result)}):\n` +
|
|
16383
16440
|
preBlock(hostdResp.error ?? '(no error message)'),
|
|
16384
16441
|
{ html: true },
|
|
@@ -16435,7 +16492,7 @@ async function handleNewCommand(ctx: Context): Promise<void> {
|
|
|
16435
16492
|
await switchroomReply(
|
|
16436
16493
|
ctx,
|
|
16437
16494
|
`/${kind} only supports a single agent ā āallā would leave other agents with stale handoff briefings. ` +
|
|
16438
|
-
`Run /${kind} from each agentās own topic, or use
|
|
16495
|
+
`Run /${kind} from each agentās own topic, or use \`switchroom agent restart all\` if you just want a plain restart without flushing sessions.`,
|
|
16439
16496
|
{ html: true },
|
|
16440
16497
|
)
|
|
16441
16498
|
return
|
|
@@ -16446,9 +16503,9 @@ async function handleNewCommand(ctx: Context): Promise<void> {
|
|
|
16446
16503
|
if (!isSelfTargetingCommand(name)) {
|
|
16447
16504
|
await switchroomReply(
|
|
16448
16505
|
ctx,
|
|
16449
|
-
`/${kind} only supports the current agent (
|
|
16506
|
+
`/${kind} only supports the current agent (**${escapeHtmlForTg(getMyAgentName())}**). ` +
|
|
16450
16507
|
`To restart another agent with a fresh session, run /${kind} from its own topic, ` +
|
|
16451
|
-
`or use
|
|
16508
|
+
`or use \`switchroom agent restart ${escapeHtmlForTg(name)}\`.`,
|
|
16452
16509
|
{ html: true },
|
|
16453
16510
|
)
|
|
16454
16511
|
return
|
|
@@ -16460,7 +16517,7 @@ async function handleNewCommand(ctx: Context): Promise<void> {
|
|
|
16460
16517
|
if (existing && Date.now() - existing.ts < 15_000) {
|
|
16461
16518
|
await switchroomReply(
|
|
16462
16519
|
ctx,
|
|
16463
|
-
`ā³ Restart of
|
|
16520
|
+
`ā³ Restart of **${escapeHtmlForTg(name)}** already in progress (${Math.round((Date.now() - existing.ts) / 1000)}s ago) ā ignoring duplicate /${kind}.`,
|
|
16464
16521
|
{ html: true },
|
|
16465
16522
|
)
|
|
16466
16523
|
return
|
|
@@ -16479,8 +16536,7 @@ async function handleNewCommand(ctx: Context): Promise<void> {
|
|
|
16479
16536
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
16480
16537
|
robustApiCall,
|
|
16481
16538
|
(tid) =>
|
|
16482
|
-
lockedBot.api.
|
|
16483
|
-
parse_mode: 'HTML', link_preview_options: { is_disabled: true },
|
|
16539
|
+
lockedBot.api.sendRichMessage(chatId, richMessage(ackText), {
|
|
16484
16540
|
// /new /reset acknowledgement is a status notice ā silence the
|
|
16485
16541
|
// open ping (the post-restart greeting card is what matters).
|
|
16486
16542
|
disable_notification: true,
|
|
@@ -16534,7 +16590,7 @@ async function handleNewCommand(ctx: Context): Promise<void> {
|
|
|
16534
16590
|
clearRestartMarker()
|
|
16535
16591
|
await switchroomReply(
|
|
16536
16592
|
ctx,
|
|
16537
|
-
`ā
|
|
16593
|
+
`ā **${escapeHtmlForTg(kind)} ${escapeHtmlForTg(name)} failed via hostd** ` +
|
|
16538
16594
|
`(result=${escapeHtmlForTg(hostdResp.result)}):\n` +
|
|
16539
16595
|
preBlock(hostdResp.error ?? '(no error message)'),
|
|
16540
16596
|
{ html: true },
|
|
@@ -16555,7 +16611,7 @@ bot.command('update', async ctx => {
|
|
|
16555
16611
|
await runSwitchroomCommand(ctx, ['update', '--check'], 'update --check')
|
|
16556
16612
|
await switchroomReply(
|
|
16557
16613
|
ctx,
|
|
16558
|
-
'Reply with
|
|
16614
|
+
'Reply with \`/update apply\` to execute, or \`/update apply --skip-images\` to skip the image pull.',
|
|
16559
16615
|
{ html: true },
|
|
16560
16616
|
)
|
|
16561
16617
|
return
|
|
@@ -16567,7 +16623,7 @@ bot.command('update', async ctx => {
|
|
|
16567
16623
|
if (tokens[0] !== 'apply' && tokens[0] !== '--apply') {
|
|
16568
16624
|
await switchroomReply(
|
|
16569
16625
|
ctx,
|
|
16570
|
-
'Usage:
|
|
16626
|
+
'Usage: \`/update\` (dry-run) or \`/update apply [--skip-images] [--rebuild]\`',
|
|
16571
16627
|
{ html: true },
|
|
16572
16628
|
)
|
|
16573
16629
|
return
|
|
@@ -16581,8 +16637,8 @@ bot.command('update', async ctx => {
|
|
|
16581
16637
|
if (!ALLOWED_FLAGS.has(tok)) {
|
|
16582
16638
|
await switchroomReply(
|
|
16583
16639
|
ctx,
|
|
16584
|
-
`Refusing to pass unknown flag:
|
|
16585
|
-
`Allowed:
|
|
16640
|
+
`Refusing to pass unknown flag: \`${tok}\`. ` +
|
|
16641
|
+
`Allowed: \`--skip-images\`, \`--rebuild\`.`,
|
|
16586
16642
|
{ html: true },
|
|
16587
16643
|
)
|
|
16588
16644
|
return
|
|
@@ -16615,16 +16671,16 @@ bot.command('update', async ctx => {
|
|
|
16615
16671
|
// hostd hasn't been installed yet, or its daemon is down.
|
|
16616
16672
|
await switchroomReply(
|
|
16617
16673
|
ctx,
|
|
16618
|
-
`ā
|
|
16619
|
-
`for agent
|
|
16674
|
+
`ā **/update apply** needs \`hostd\`, but its socket ` +
|
|
16675
|
+
`for agent \`${myAgentName}\` isn't ` +
|
|
16620
16676
|
`bound and in-container docker access isn't available either.\n\n` +
|
|
16621
|
-
|
|
16677
|
+
`\`host_control.enabled\` is on in \`switchroom.yaml\`, ` +
|
|
16622
16678
|
`so hostd is the expected dispatch path ā but no socket at ` +
|
|
16623
|
-
|
|
16679
|
+
`\`/run/switchroom/hostd/${escapeHtmlForTg(myAgentName)}/sock\`. ` +
|
|
16624
16680
|
`On a fresh docker install this usually means hostd hasn't been ` +
|
|
16625
16681
|
`installed yet.\n\n` +
|
|
16626
|
-
`Run
|
|
16627
|
-
`start the daemon, then retry. Send
|
|
16682
|
+
`Run \`switchroom hostd install\` on the host to install + ` +
|
|
16683
|
+
`start the daemon, then retry. Send \`/upgradestatus\` to ` +
|
|
16628
16684
|
`re-check the daemon state from here.`,
|
|
16629
16685
|
{ html: true },
|
|
16630
16686
|
)
|
|
@@ -16634,14 +16690,14 @@ bot.command('update', async ctx => {
|
|
|
16634
16690
|
// pick one of: host CLI, or enable hostd.
|
|
16635
16691
|
await switchroomReply(
|
|
16636
16692
|
ctx,
|
|
16637
|
-
`ā
|
|
16638
|
-
`container, but it's not available (no
|
|
16639
|
-
`PATH, no
|
|
16640
|
-
|
|
16641
|
-
`Either run
|
|
16642
|
-
`set
|
|
16643
|
-
|
|
16644
|
-
|
|
16693
|
+
`ā **/update apply** needs docker access from inside the agent ` +
|
|
16694
|
+
`container, but it's not available (no \`docker\` binary on ` +
|
|
16695
|
+
`PATH, no \`/var/run/docker.sock\` mount) and ` +
|
|
16696
|
+
`\`host_control.enabled\` is off.\n\n` +
|
|
16697
|
+
`Either run \`switchroom update\` from the host shell, or ` +
|
|
16698
|
+
`set \`host_control.enabled: true\` in ` +
|
|
16699
|
+
`\`switchroom.yaml\` and run ` +
|
|
16700
|
+
`\`switchroom hostd install\` on the host so this verb ` +
|
|
16645
16701
|
`can dispatch through the host-side daemon.`,
|
|
16646
16702
|
{ html: true },
|
|
16647
16703
|
)
|
|
@@ -16671,9 +16727,9 @@ bot.command('update', async ctx => {
|
|
|
16671
16727
|
// /restart handler (gateway.ts ~6273) so the boot-card lookup
|
|
16672
16728
|
// (gateway.ts ~10393) finds chat_id + ack_message_id in the marker.
|
|
16673
16729
|
const ackText =
|
|
16674
|
-
`š
|
|
16675
|
-
'
|
|
16676
|
-
...passthrough.map((t) =>
|
|
16730
|
+
`š **update started** ā running ${[
|
|
16731
|
+
'\`switchroom update\`',
|
|
16732
|
+
...passthrough.map((t) => `\`${t}\``),
|
|
16677
16733
|
].join(' ')}\n` +
|
|
16678
16734
|
`\nThe gateway will restart as part of the recreate step; watch ` +
|
|
16679
16735
|
`for the post-restart greeting card to confirm completion.`
|
|
@@ -16683,9 +16739,7 @@ bot.command('update', async ctx => {
|
|
|
16683
16739
|
const sent = await retryWithThreadFallback<{ message_id: number }>(
|
|
16684
16740
|
robustApiCall,
|
|
16685
16741
|
(tid) =>
|
|
16686
|
-
lockedBot.api.
|
|
16687
|
-
parse_mode: 'HTML',
|
|
16688
|
-
link_preview_options: { is_disabled: true },
|
|
16742
|
+
lockedBot.api.sendRichMessage(chatId, richMessage(ackText), {
|
|
16689
16743
|
// "update started" acknowledgement is a status notice ā silence
|
|
16690
16744
|
// the open ping (the post-restart greeting card is what matters).
|
|
16691
16745
|
disable_notification: true,
|
|
@@ -16778,15 +16832,14 @@ bot.command('update', async ctx => {
|
|
|
16778
16832
|
terminal.stdout_tail ??
|
|
16779
16833
|
'(no error tail returned)'
|
|
16780
16834
|
const editedText =
|
|
16781
|
-
`š
|
|
16835
|
+
`š **update started** ā **FAILED** via hostd ` +
|
|
16782
16836
|
`(result=${escapeHtmlForTg(terminal.result)}):\n` +
|
|
16783
16837
|
preBlock(errBody)
|
|
16784
16838
|
if (ackId != null) {
|
|
16785
16839
|
try {
|
|
16786
16840
|
await robustApiCall(
|
|
16787
16841
|
() =>
|
|
16788
|
-
lockedBot.api.editMessageText(chatId, ackId!, editedText, {
|
|
16789
|
-
parse_mode: 'HTML',
|
|
16842
|
+
lockedBot.api.editMessageText(chatId, ackId!, richMessage(editedText), {
|
|
16790
16843
|
link_preview_options: { is_disabled: true },
|
|
16791
16844
|
}),
|
|
16792
16845
|
{ verb: 'update.poll.editAck' },
|
|
@@ -16814,7 +16867,7 @@ bot.command('update', async ctx => {
|
|
|
16814
16867
|
clearRestartMarker()
|
|
16815
16868
|
await switchroomReply(
|
|
16816
16869
|
ctx,
|
|
16817
|
-
`ā
|
|
16870
|
+
`ā **/update apply failed via hostd** ` +
|
|
16818
16871
|
`(result=${escapeHtmlForTg(hostdResp.result)}):\n` +
|
|
16819
16872
|
preBlock(hostdResp.error ?? '(no error message)'),
|
|
16820
16873
|
{ html: true },
|
|
@@ -16841,8 +16894,8 @@ bot.command('upgrade', async ctx => {
|
|
|
16841
16894
|
if (!isAuthorizedSender(ctx)) return
|
|
16842
16895
|
await switchroomReply(
|
|
16843
16896
|
ctx,
|
|
16844
|
-
'Did you mean
|
|
16845
|
-
'Or
|
|
16897
|
+
'Did you mean \`/upgradestatus\` (no hyphen ā Telegram slash-command grammar)? ' +
|
|
16898
|
+
'Or \`/update\` to plan, \`/update apply\` to execute.',
|
|
16846
16899
|
{ html: true },
|
|
16847
16900
|
)
|
|
16848
16901
|
})
|
|
@@ -16858,7 +16911,7 @@ bot.command('audit', async ctx => {
|
|
|
16858
16911
|
if (arg === '' || arg === 'help' || arg === '--help') {
|
|
16859
16912
|
await switchroomReply(
|
|
16860
16913
|
ctx,
|
|
16861
|
-
'Usage:
|
|
16914
|
+
'Usage: \`/audit hostd [--tail N] [--agent <name>] [--op <verb>] [--error] [--verbose]\`',
|
|
16862
16915
|
{ html: true },
|
|
16863
16916
|
)
|
|
16864
16917
|
return
|
|
@@ -16868,8 +16921,8 @@ bot.command('audit', async ctx => {
|
|
|
16868
16921
|
if (sub !== 'hostd') {
|
|
16869
16922
|
await switchroomReply(
|
|
16870
16923
|
ctx,
|
|
16871
|
-
`Unknown audit target
|
|
16872
|
-
`Supported:
|
|
16924
|
+
`Unknown audit target \`${sub ?? ''}\`. ` +
|
|
16925
|
+
`Supported: \`hostd\`.`,
|
|
16873
16926
|
{ html: true },
|
|
16874
16927
|
)
|
|
16875
16928
|
return
|
|
@@ -16889,22 +16942,22 @@ bot.command('audit', async ctx => {
|
|
|
16889
16942
|
if (t === '--tail' || t === '--agent' || t === '--op') {
|
|
16890
16943
|
const v = tokens[++i]
|
|
16891
16944
|
if (v == null) {
|
|
16892
|
-
await switchroomReply(ctx, `Flag
|
|
16945
|
+
await switchroomReply(ctx, `Flag \`${t}\` requires a value.`, { html: true })
|
|
16893
16946
|
return
|
|
16894
16947
|
}
|
|
16895
16948
|
if (t === '--tail' && !/^[0-9]{1,4}$/.test(v)) {
|
|
16896
|
-
await switchroomReply(ctx,
|
|
16949
|
+
await switchroomReply(ctx, `\`--tail\` must be an integer (1-9999).`, { html: true })
|
|
16897
16950
|
return
|
|
16898
16951
|
}
|
|
16899
16952
|
if (t === '--agent' && !/^[a-z][a-z0-9-]{0,62}$/i.test(v)) {
|
|
16900
|
-
await switchroomReply(ctx,
|
|
16953
|
+
await switchroomReply(ctx, `\`--agent\` name has an invalid shape.`, { html: true })
|
|
16901
16954
|
return
|
|
16902
16955
|
}
|
|
16903
16956
|
if (t === '--op' && !ALLOWED_OPS.has(v)) {
|
|
16904
16957
|
await switchroomReply(
|
|
16905
16958
|
ctx,
|
|
16906
|
-
`Unknown hostd verb
|
|
16907
|
-
`Known: ${[...ALLOWED_OPS].sort().map(o =>
|
|
16959
|
+
`Unknown hostd verb \`${v}\`. ` +
|
|
16960
|
+
`Known: ${[...ALLOWED_OPS].sort().map(o => `\`${o}\``).join(', ')}.`,
|
|
16908
16961
|
{ html: true },
|
|
16909
16962
|
)
|
|
16910
16963
|
return
|
|
@@ -16914,8 +16967,8 @@ bot.command('audit', async ctx => {
|
|
|
16914
16967
|
}
|
|
16915
16968
|
await switchroomReply(
|
|
16916
16969
|
ctx,
|
|
16917
|
-
`Unknown flag
|
|
16918
|
-
`Allowed:
|
|
16970
|
+
`Unknown flag \`${t}\`. ` +
|
|
16971
|
+
`Allowed: \`--tail\`, \`--agent\`, \`--op\`, \`--error\`, \`--verbose\`.`,
|
|
16919
16972
|
{ html: true },
|
|
16920
16973
|
)
|
|
16921
16974
|
return
|
|
@@ -16964,7 +17017,7 @@ async function handlePermissionSlash(ctx: Context, behavior: 'allow' | 'deny'):
|
|
|
16964
17017
|
if (!details) {
|
|
16965
17018
|
await switchroomReply(
|
|
16966
17019
|
ctx,
|
|
16967
|
-
`No pending permission for id
|
|
17020
|
+
`No pending permission for id \`${request_id}\`. It may have already been answered or timed out.`,
|
|
16968
17021
|
{ html: true },
|
|
16969
17022
|
)
|
|
16970
17023
|
return
|
|
@@ -16983,10 +17036,10 @@ async function handlePermissionSlash(ctx: Context, behavior: 'allow' | 'deny'):
|
|
|
16983
17036
|
`[telegram gateway] slash-${behavior} request_id=${request_id} tool=${details.tool_name} by=${senderId}\n`,
|
|
16984
17037
|
)
|
|
16985
17038
|
const lbl = behavior === 'allow' ? 'ā
Allowed' : 'ā Denied'
|
|
16986
|
-
const suffix = details.tool_name ? ` (
|
|
17039
|
+
const suffix = details.tool_name ? ` (\`${details.tool_name}\`)` : ''
|
|
16987
17040
|
await switchroomReply(
|
|
16988
17041
|
ctx,
|
|
16989
|
-
`${lbl}${suffix} via /${behavior}
|
|
17042
|
+
`${lbl}${suffix} via /${behavior} \`${request_id}\``,
|
|
16990
17043
|
{ html: true },
|
|
16991
17044
|
)
|
|
16992
17045
|
}
|
|
@@ -17064,9 +17117,9 @@ bot.command('pending', async ctx => {
|
|
|
17064
17117
|
await switchroomReply(ctx, 'No pending permission prompts.')
|
|
17065
17118
|
return
|
|
17066
17119
|
}
|
|
17067
|
-
const lines: string[] = ['
|
|
17120
|
+
const lines: string[] = ['**Pending permission prompts**']
|
|
17068
17121
|
for (const [id, details] of pendingPermissions.entries()) {
|
|
17069
|
-
lines.push(`ā¢
|
|
17122
|
+
lines.push(`⢠\`${id}\` ā ${escapeHtmlForTg(details.tool_name)}`)
|
|
17070
17123
|
}
|
|
17071
17124
|
await switchroomReply(ctx, lines.join('\n'), { html: true })
|
|
17072
17125
|
})
|
|
@@ -17266,7 +17319,8 @@ function broadcastFleetFallbackFailure(triggerAgent: string, reason: string): vo
|
|
|
17266
17319
|
const html = renderFallbackFailureNotice(triggerAgent, reason)
|
|
17267
17320
|
for (const chat_id of access.allowFrom) {
|
|
17268
17321
|
void swallowingApiCall(
|
|
17269
|
-
|
|
17322
|
+
// allow-raw-bot-api: wrapped in swallowingApiCall (retry policy)
|
|
17323
|
+
() => bot.api.sendRichMessage(chat_id, richMessage(html), {}),
|
|
17270
17324
|
{ chat_id, verb: 'fleet-fallback:failure-notify' },
|
|
17271
17325
|
)
|
|
17272
17326
|
}
|
|
@@ -17356,10 +17410,11 @@ async function doFireFleetAutoFallback(triggerAgent: string, untilMs?: number):
|
|
|
17356
17410
|
if (access.allowFrom.length === 0) return outcome.kind === 'switched'
|
|
17357
17411
|
// Account-switch / all-blocked announcement is a system status notice,
|
|
17358
17412
|
// not the user's answer ā silence the open ping.
|
|
17359
|
-
const opts = {
|
|
17413
|
+
const opts = { disable_notification: true }
|
|
17360
17414
|
for (const chat_id of access.allowFrom) {
|
|
17361
17415
|
void swallowingApiCall(
|
|
17362
|
-
|
|
17416
|
+
// allow-raw-bot-api: wrapped in swallowingApiCall (retry policy)
|
|
17417
|
+
() => bot.api.sendRichMessage(chat_id, richMessage(outcome.announcement), opts),
|
|
17363
17418
|
{ chat_id, verb: 'fleet-fallback:notify' },
|
|
17364
17419
|
)
|
|
17365
17420
|
}
|
|
@@ -17447,9 +17502,7 @@ async function runCreditWatch(): Promise<void> {
|
|
|
17447
17502
|
// contract enforced by scripts/check-bot-api-wrapping.sh (#1075).
|
|
17448
17503
|
await swallowingApiCall(
|
|
17449
17504
|
() =>
|
|
17450
|
-
bot.api.
|
|
17451
|
-
parse_mode: 'HTML',
|
|
17452
|
-
link_preview_options: { is_disabled: true },
|
|
17505
|
+
bot.api.sendRichMessage(chat_id, richMessage(decision.message), {
|
|
17453
17506
|
// Credit/quota warning is a system status notice ā silence the
|
|
17454
17507
|
// open ping (the user isn't waiting to tap anything).
|
|
17455
17508
|
disable_notification: true,
|
|
@@ -17589,9 +17642,7 @@ async function runQuotaWatch(opts: { bootTick?: boolean } = {}): Promise<void> {
|
|
|
17589
17642
|
}
|
|
17590
17643
|
await swallowingApiCall(
|
|
17591
17644
|
() =>
|
|
17592
|
-
bot.api.
|
|
17593
|
-
parse_mode: 'HTML',
|
|
17594
|
-
link_preview_options: { is_disabled: true },
|
|
17645
|
+
bot.api.sendRichMessage(chat_id, richMessage(fleetDecision.message), {
|
|
17595
17646
|
// Quota status notice ā silence the open ping.
|
|
17596
17647
|
disable_notification: true,
|
|
17597
17648
|
}),
|
|
@@ -17812,9 +17863,7 @@ async function runQuotaWatch(opts: { bootTick?: boolean } = {}): Promise<void> {
|
|
|
17812
17863
|
// contract enforced by scripts/check-bot-api-wrapping.sh (#1075).
|
|
17813
17864
|
await swallowingApiCall(
|
|
17814
17865
|
() =>
|
|
17815
|
-
bot.api.
|
|
17816
|
-
parse_mode: 'HTML',
|
|
17817
|
-
link_preview_options: { is_disabled: true },
|
|
17866
|
+
bot.api.sendRichMessage(chat_id, richMessage(message), {
|
|
17818
17867
|
// Quota throttling status notice ā silence the open ping.
|
|
17819
17868
|
disable_notification: true,
|
|
17820
17869
|
}),
|
|
@@ -17845,8 +17894,7 @@ async function editMicrosoftConnectCard(
|
|
|
17845
17894
|
): Promise<void> {
|
|
17846
17895
|
await robustApiCall(
|
|
17847
17896
|
// allow-raw-bot-api: background connect-card edit by message id (no thread_id; not the reply chunk loop)
|
|
17848
|
-
() => bot.api.editMessageText(chatId, messageId, html, {
|
|
17849
|
-
parse_mode: 'HTML',
|
|
17897
|
+
() => bot.api.editMessageText(chatId, messageId, richMessage(html), {
|
|
17850
17898
|
link_preview_options: { is_disabled: true },
|
|
17851
17899
|
}),
|
|
17852
17900
|
{ chat_id: String(chatId), verb: 'microsoftConnectCard' },
|
|
@@ -17874,21 +17922,21 @@ async function finalizeMicrosoftConnect(key: string): Promise<void> {
|
|
|
17874
17922
|
let html: string
|
|
17875
17923
|
if (result.kind === 'connected') {
|
|
17876
17924
|
html =
|
|
17877
|
-
`ā
|
|
17925
|
+
`ā
**Connected** \`${result.account}\` ` +
|
|
17878
17926
|
`(${result.accountType}).\n\n` +
|
|
17879
|
-
`To let
|
|
17880
|
-
|
|
17927
|
+
`To let **${escapeHtmlForTg(agentName)}** use it, run on the host:\n` +
|
|
17928
|
+
`\`switchroom auth microsoft enable ${escapeHtmlForTg(result.account)} ${escapeHtmlForTg(agentName)}\`\n` +
|
|
17881
17929
|
`then restart ${escapeHtmlForTg(agentName)}.`
|
|
17882
17930
|
} else if (result.kind === 'no-refresh-token') {
|
|
17883
17931
|
html =
|
|
17884
17932
|
`ā Microsoft returned no refresh token (the account would expire in ~1h), ` +
|
|
17885
|
-
`so it was not registered. Try
|
|
17933
|
+
`so it was not registered. Try \`/connect microsoft\` again, or ` +
|
|
17886
17934
|
`connect from the host CLI.`
|
|
17887
17935
|
} else {
|
|
17888
17936
|
html =
|
|
17889
|
-
`ā
|
|
17890
|
-
|
|
17891
|
-
`from the host:
|
|
17937
|
+
`ā **Connect failed:** ${escapeHtmlForTg(result.message)}\n\n` +
|
|
17938
|
+
`_Work/school accounts can't use the phone flow at /common ā connect those ` +
|
|
17939
|
+
`from the host: \`switchroom auth microsoft account add <email>\`._`
|
|
17892
17940
|
}
|
|
17893
17941
|
await editMicrosoftConnectCard(flow.cardChatId, flow.cardMessageId, html)
|
|
17894
17942
|
}
|
|
@@ -17918,7 +17966,7 @@ bot.command('connect', async ctx => {
|
|
|
17918
17966
|
)
|
|
17919
17967
|
await switchroomReply(
|
|
17920
17968
|
ctx,
|
|
17921
|
-
`ā ļø
|
|
17969
|
+
`ā ļø \`/connect\` links account credentials ā it is **operator-private**. ` +
|
|
17922
17970
|
`Send it as a direct message to me from your operator account (a private chat where ` +
|
|
17923
17971
|
`your Telegram ID is on the access allowlist), not in a group or forum.`,
|
|
17924
17972
|
{ html: true },
|
|
@@ -17945,8 +17993,8 @@ bot.command('connect', async ctx => {
|
|
|
17945
17993
|
if (!isAuthAdmin({ isAdmin })) {
|
|
17946
17994
|
await switchroomReply(
|
|
17947
17995
|
ctx,
|
|
17948
|
-
|
|
17949
|
-
|
|
17996
|
+
`**Not authorized.** \`/connect\` requires this agent to have ` +
|
|
17997
|
+
`\`admin: true\` in switchroom.yaml.`,
|
|
17950
17998
|
{ html: true },
|
|
17951
17999
|
)
|
|
17952
18000
|
return
|
|
@@ -17955,7 +18003,7 @@ bot.command('connect', async ctx => {
|
|
|
17955
18003
|
if (arg === 'cancel') {
|
|
17956
18004
|
const existing = pendingMicrosoftConnectFlows.get(key)
|
|
17957
18005
|
if (!existing) {
|
|
17958
|
-
await switchroomReply(ctx, '
|
|
18006
|
+
await switchroomReply(ctx, '_No pending connect flow in this chat._', { html: true })
|
|
17959
18007
|
return
|
|
17960
18008
|
}
|
|
17961
18009
|
existing.cancelled = true
|
|
@@ -17967,9 +18015,9 @@ bot.command('connect', async ctx => {
|
|
|
17967
18015
|
if (arg !== '' && arg !== 'microsoft' && arg !== 'ms') {
|
|
17968
18016
|
await switchroomReply(
|
|
17969
18017
|
ctx,
|
|
17970
|
-
|
|
17971
|
-
`(Outlook / Office 365), or
|
|
17972
|
-
|
|
18018
|
+
`**Usage:** \`/connect microsoft\` to link a Microsoft account ` +
|
|
18019
|
+
`(Outlook / Office 365), or \`/connect cancel\`.\n` +
|
|
18020
|
+
`_Google accounts are connected from the host CLI._`,
|
|
17973
18021
|
{ html: true },
|
|
17974
18022
|
)
|
|
17975
18023
|
return
|
|
@@ -17978,8 +18026,8 @@ bot.command('connect', async ctx => {
|
|
|
17978
18026
|
if (pendingMicrosoftConnectFlows.has(key)) {
|
|
17979
18027
|
await switchroomReply(
|
|
17980
18028
|
ctx,
|
|
17981
|
-
'
|
|
17982
|
-
'or send
|
|
18029
|
+
'_A Microsoft connect flow is already in progress here. Finish it on your phone, ' +
|
|
18030
|
+
'or send \`/connect cancel\`._',
|
|
17983
18031
|
{ html: true },
|
|
17984
18032
|
)
|
|
17985
18033
|
return
|
|
@@ -17999,9 +18047,9 @@ bot.command('connect', async ctx => {
|
|
|
17999
18047
|
if (started.kind === 'byo-vault') {
|
|
18000
18048
|
await switchroomReply(
|
|
18001
18049
|
ctx,
|
|
18002
|
-
|
|
18003
|
-
`app (
|
|
18004
|
-
`Run
|
|
18050
|
+
`**Can't connect from chat:** this install uses a vaulted custom Microsoft ` +
|
|
18051
|
+
`app (\`${started.ref}\`) only the host CLI can read. ` +
|
|
18052
|
+
`Run \`switchroom auth microsoft account add <email>\` on the host.`,
|
|
18005
18053
|
{ html: true },
|
|
18006
18054
|
)
|
|
18007
18055
|
return
|
|
@@ -18009,7 +18057,7 @@ bot.command('connect', async ctx => {
|
|
|
18009
18057
|
if (started.kind === 'error') {
|
|
18010
18058
|
await switchroomReply(
|
|
18011
18059
|
ctx,
|
|
18012
|
-
|
|
18060
|
+
`**/connect failed:** ${escapeHtmlForTg(started.message)}`,
|
|
18013
18061
|
{ html: true },
|
|
18014
18062
|
)
|
|
18015
18063
|
return
|
|
@@ -18017,21 +18065,21 @@ bot.command('connect', async ctx => {
|
|
|
18017
18065
|
|
|
18018
18066
|
const appNote =
|
|
18019
18067
|
started.source === 'default'
|
|
18020
|
-
? "
|
|
18021
|
-
: '
|
|
18068
|
+
? "_Using switchroom's shipped Microsoft app._"
|
|
18069
|
+
: '_Using your configured Microsoft app._'
|
|
18022
18070
|
const keyboard = new InlineKeyboard()
|
|
18023
18071
|
.url('š Sign in to Microsoft', started.device.verification_uri)
|
|
18024
18072
|
.row()
|
|
18025
18073
|
.text('ā Cancel', `cn:cancel:${key}`)
|
|
18026
|
-
const sent = await ctx.
|
|
18027
|
-
|
|
18028
|
-
|
|
18029
|
-
`
|
|
18074
|
+
const sent = await ctx.replyWithRichMessage(
|
|
18075
|
+
richMessage(
|
|
18076
|
+
`š **Connect a Microsoft account**\n\n` +
|
|
18077
|
+
`1. Tap **Sign in to Microsoft** below.\n` +
|
|
18078
|
+
`2. Enter this code: \`${started.device.user_code}\`\n` +
|
|
18030
18079
|
`3. Approve the requested permissions (Mail, Calendar, Files).\n\n` +
|
|
18031
18080
|
`I'll confirm here once it's connected (within ~15 min).\n${appNote}`,
|
|
18081
|
+
),
|
|
18032
18082
|
{
|
|
18033
|
-
parse_mode: 'HTML',
|
|
18034
|
-
link_preview_options: { is_disabled: true },
|
|
18035
18083
|
reply_markup: keyboard,
|
|
18036
18084
|
...(ctx.message?.message_thread_id != null
|
|
18037
18085
|
? { message_thread_id: ctx.message.message_thread_id }
|
|
@@ -18082,7 +18130,7 @@ bot.command("auth", async ctx => {
|
|
|
18082
18130
|
)
|
|
18083
18131
|
await switchroomReply(
|
|
18084
18132
|
ctx,
|
|
18085
|
-
`ā ļø
|
|
18133
|
+
`ā ļø \`/auth\` manages account credentials ā it is **operator-private**. Send it as a direct message to me from your operator account (a private chat where your Telegram ID is on the access allowlist), not in a group or forum.`,
|
|
18086
18134
|
{ html: true },
|
|
18087
18135
|
).catch(() => {})
|
|
18088
18136
|
}
|
|
@@ -18121,10 +18169,10 @@ bot.command("auth", async ctx => {
|
|
|
18121
18169
|
if (!isAuthAdmin({ isAdmin })) {
|
|
18122
18170
|
await switchroomReply(
|
|
18123
18171
|
ctx,
|
|
18124
|
-
|
|
18125
|
-
`Set
|
|
18126
|
-
`(the same flag that gates
|
|
18127
|
-
|
|
18172
|
+
`**Not authorized.** \`/auth ${parsed.kind}\` is admin-only.\n` +
|
|
18173
|
+
`Set \`admin: true\` on this agent in switchroom.yaml to unlock ` +
|
|
18174
|
+
`(the same flag that gates \`/agents\`, \`/restart\`, ` +
|
|
18175
|
+
`\`/update\` etc.).`,
|
|
18128
18176
|
{ html: true },
|
|
18129
18177
|
)
|
|
18130
18178
|
return
|
|
@@ -18137,7 +18185,7 @@ bot.command("auth", async ctx => {
|
|
|
18137
18185
|
if (parsed.kind === 'cancel') {
|
|
18138
18186
|
const existing = pendingAuthAddFlows.get(authAddKey)
|
|
18139
18187
|
if (!existing) {
|
|
18140
|
-
await switchroomReply(ctx, "
|
|
18188
|
+
await switchroomReply(ctx, "_No pending \`/auth add\` flow in this chat._", { html: true })
|
|
18141
18189
|
return
|
|
18142
18190
|
}
|
|
18143
18191
|
cancelAccountAuthSession(existing)
|
|
@@ -18149,8 +18197,8 @@ bot.command("auth", async ctx => {
|
|
|
18149
18197
|
if (pendingAuthAddFlows.has(authAddKey)) {
|
|
18150
18198
|
await switchroomReply(
|
|
18151
18199
|
ctx,
|
|
18152
|
-
"
|
|
18153
|
-
"Finish the paste, or send
|
|
18200
|
+
"_An \`/auth add\` flow is already in progress for this chat. " +
|
|
18201
|
+
"Finish the paste, or send \`/auth cancel\` to abort._",
|
|
18154
18202
|
{ html: true },
|
|
18155
18203
|
)
|
|
18156
18204
|
return
|
|
@@ -18166,17 +18214,17 @@ bot.command("auth", async ctx => {
|
|
|
18166
18214
|
})
|
|
18167
18215
|
await switchroomReply(
|
|
18168
18216
|
ctx,
|
|
18169
|
-
|
|
18217
|
+
`**Adding account** \`${parsed.label}\`\n\n` +
|
|
18170
18218
|
`1. Open this URL on your phone:\n${loginUrl}\n\n` +
|
|
18171
18219
|
`2. Log into Anthropic, copy the code Claude shows.\n` +
|
|
18172
18220
|
`3. Paste it back here.\n\n` +
|
|
18173
|
-
`Send
|
|
18221
|
+
`Send \`/auth cancel\` to abort.`,
|
|
18174
18222
|
{ html: true },
|
|
18175
18223
|
)
|
|
18176
18224
|
} catch (err) {
|
|
18177
18225
|
await switchroomReply(
|
|
18178
18226
|
ctx,
|
|
18179
|
-
|
|
18227
|
+
`**/auth add failed:** ${escapeHtmlForTg((err as Error)?.message ?? String(err))}`,
|
|
18180
18228
|
{ html: true },
|
|
18181
18229
|
)
|
|
18182
18230
|
}
|
|
@@ -18185,7 +18233,7 @@ bot.command("auth", async ctx => {
|
|
|
18185
18233
|
|
|
18186
18234
|
const client = await getAuthBrokerClient(currentAgent)
|
|
18187
18235
|
if (!client) {
|
|
18188
|
-
await switchroomReply(ctx, "
|
|
18236
|
+
await switchroomReply(ctx, "**/auth unavailable:** auth-broker client is not loaded (post-RFC-H rewire in progress?).", { html: true })
|
|
18189
18237
|
return
|
|
18190
18238
|
}
|
|
18191
18239
|
const reply = await handleAuthCommand(parsed, {
|
|
@@ -18437,7 +18485,7 @@ async function handleVaultRecentDenialCallback(ctx: Context, data: string): Prom
|
|
|
18437
18485
|
return
|
|
18438
18486
|
}
|
|
18439
18487
|
if (result.kind === 'error') {
|
|
18440
|
-
await switchroomReply(ctx,
|
|
18488
|
+
await switchroomReply(ctx, `**mint_grant failed:** ${escapeHtmlForTg(result.msg)}`, { html: true })
|
|
18441
18489
|
return
|
|
18442
18490
|
}
|
|
18443
18491
|
// Write the token to the agent's .vault-token file ā same flow as the
|
|
@@ -18451,10 +18499,10 @@ async function handleVaultRecentDenialCallback(ctx: Context, data: string): Prom
|
|
|
18451
18499
|
} catch (err) {
|
|
18452
18500
|
await switchroomReply(
|
|
18453
18501
|
ctx,
|
|
18454
|
-
|
|
18502
|
+
`**Grant created (${escapeHtmlForTg(id)}) but token write failed:** ` +
|
|
18455
18503
|
`${escapeHtmlForTg(String(err))}\n` +
|
|
18456
|
-
|
|
18457
|
-
`--keys ${escapeHtmlForTg(keyName)} --duration 30d
|
|
18504
|
+
`_Recover with: \`switchroom vault grant ${escapeHtmlForTg(agentName)} ` +
|
|
18505
|
+
`--keys ${escapeHtmlForTg(keyName)} --duration 30d\` on the host._`,
|
|
18458
18506
|
{ html: true },
|
|
18459
18507
|
)
|
|
18460
18508
|
return
|
|
@@ -18479,14 +18527,13 @@ async function handleVaultRecentDenialCallback(ctx: Context, data: string): Prom
|
|
|
18479
18527
|
? escapeHtmlForTg(sourceMsg.text)
|
|
18480
18528
|
: ''
|
|
18481
18529
|
const statusLine =
|
|
18482
|
-
`\n\nā
|
|
18483
|
-
|
|
18484
|
-
`(grant
|
|
18530
|
+
`\n\nā
**${escapeHtmlForTg(agentName)}** granted read access to ` +
|
|
18531
|
+
`\`${keyName}\` for 30 days ` +
|
|
18532
|
+
`(grant \`${id}\`). ` +
|
|
18485
18533
|
`Re-run /vault audit to act on remaining denials.`
|
|
18486
18534
|
await finalizeCallback(ctx, {
|
|
18487
18535
|
ackText: 'ā
Grant minted',
|
|
18488
18536
|
newText: baseText ? `${baseText}${statusLine}` : statusLine,
|
|
18489
|
-
parseMode: 'HTML',
|
|
18490
18537
|
// No synthInbound ā operator-only flow. The granted agent picks
|
|
18491
18538
|
// up the token via .vault-token file on next CLI invocation; no
|
|
18492
18539
|
// turn-wake needed.
|
|
@@ -18553,11 +18600,11 @@ async function performVaultAccessApproval(
|
|
|
18553
18600
|
.editMessageText(
|
|
18554
18601
|
pending.chat_id,
|
|
18555
18602
|
pending.card_message_id,
|
|
18556
|
-
`ā¹ļø
|
|
18557
|
-
|
|
18558
|
-
|
|
18559
|
-
`The agent can read it directly
|
|
18560
|
-
{
|
|
18603
|
+
`ā¹ļø **${escapeHtmlForTg(pending.agent)}** already has standing-ACL access to ` +
|
|
18604
|
+
`\`${pending.key}\` (schedule.secrets[]). ` +
|
|
18605
|
+
`**No grant minted** ā a token would shadow the standing ACL. ` +
|
|
18606
|
+
richMessage(`The agent can read it directly.`),
|
|
18607
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18561
18608
|
)
|
|
18562
18609
|
.catch(() => {})
|
|
18563
18610
|
}
|
|
@@ -18651,8 +18698,8 @@ async function performVaultAccessApproval(
|
|
|
18651
18698
|
.editMessageText(
|
|
18652
18699
|
pending.chat_id,
|
|
18653
18700
|
pending.card_message_id,
|
|
18654
|
-
|
|
18655
|
-
{
|
|
18701
|
+
richMessage(`**mint_grant failed:** ${escapeHtmlForTg(result.msg)}`),
|
|
18702
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18656
18703
|
)
|
|
18657
18704
|
.catch(() => {})
|
|
18658
18705
|
}
|
|
@@ -18667,10 +18714,10 @@ async function performVaultAccessApproval(
|
|
|
18667
18714
|
} catch (err) {
|
|
18668
18715
|
await switchroomReply(
|
|
18669
18716
|
ctx,
|
|
18670
|
-
|
|
18717
|
+
`**Grant created (${escapeHtmlForTg(id)}) but token write failed:** ` +
|
|
18671
18718
|
`${escapeHtmlForTg(String(err))}\n` +
|
|
18672
|
-
|
|
18673
|
-
`--keys ${escapeHtmlForTg(pending.key)} --duration ${Math.round(pending.ttl_seconds / 86400)}d
|
|
18719
|
+
`_Recover with: \`switchroom vault grant ${escapeHtmlForTg(pending.agent)} ` +
|
|
18720
|
+
`--keys ${escapeHtmlForTg(pending.key)} --duration ${Math.round(pending.ttl_seconds / 86400)}d\` on the host._`,
|
|
18674
18721
|
{ html: true },
|
|
18675
18722
|
)
|
|
18676
18723
|
return
|
|
@@ -18681,16 +18728,16 @@ async function performVaultAccessApproval(
|
|
|
18681
18728
|
const days = Math.round(pending.ttl_seconds / 86400)
|
|
18682
18729
|
const footer =
|
|
18683
18730
|
VAULT_APPROVAL_AUTH_MODE === 'telegram-id'
|
|
18684
|
-
? `\
|
|
18731
|
+
? `\n_Approver verified by Telegram identity ā broker auto-unlocked at startup._`
|
|
18685
18732
|
: ''
|
|
18686
18733
|
await ctx.api
|
|
18687
18734
|
.editMessageText(
|
|
18688
18735
|
pending.chat_id,
|
|
18689
18736
|
pending.card_message_id,
|
|
18690
|
-
`ā
Granted
|
|
18691
|
-
|
|
18692
|
-
`(grant
|
|
18693
|
-
{
|
|
18737
|
+
`ā
Granted **${escapeHtmlForTg(pending.agent)}** ${pending.scope} access to ` +
|
|
18738
|
+
`\`${pending.key}\` for ${days}d. ` +
|
|
18739
|
+
richMessage(`(grant \`${id}\`)` + footer),
|
|
18740
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18694
18741
|
)
|
|
18695
18742
|
.catch(() => {})
|
|
18696
18743
|
}
|
|
@@ -18734,6 +18781,97 @@ async function performVaultAccessApproval(
|
|
|
18734
18781
|
)
|
|
18735
18782
|
}
|
|
18736
18783
|
|
|
18784
|
+
/**
|
|
18785
|
+
* #2670 ā handle an Approve / Dismiss tap on a one-tap skill-improvement
|
|
18786
|
+
* proposal card.
|
|
18787
|
+
*
|
|
18788
|
+
* Approve ā mark the proposal approved + inject a synthetic
|
|
18789
|
+
* `skill_proposal_apply` turn instructing the live agent to
|
|
18790
|
+
* write the stored draft through `skill_*_personal` (so the
|
|
18791
|
+
* secret-scan pipeline runs; agent never self-applies).
|
|
18792
|
+
* Dismiss ā mark rejected + write a rejection fingerprint so the weekly
|
|
18793
|
+
* synthesis cron doesn't re-propose it.
|
|
18794
|
+
*/
|
|
18795
|
+
async function handleSkillProposalCallback(ctx: Context, data: string): Promise<void> {
|
|
18796
|
+
const senderId = String(ctx.from?.id ?? '')
|
|
18797
|
+
const access = loadAccess()
|
|
18798
|
+
if (!access.allowFrom.includes(senderId)) {
|
|
18799
|
+
await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
|
|
18800
|
+
return
|
|
18801
|
+
}
|
|
18802
|
+
const parsed = parseSkillProposalCallback(data)
|
|
18803
|
+
if (parsed == null) {
|
|
18804
|
+
await ctx.answerCallbackQuery({ text: 'Bad request' }).catch(() => {})
|
|
18805
|
+
return
|
|
18806
|
+
}
|
|
18807
|
+
const stateDir = process.env.TELEGRAM_STATE_DIR
|
|
18808
|
+
const agent = process.env.SWITCHROOM_AGENT_NAME ?? ''
|
|
18809
|
+
if (stateDir == null || stateDir.length === 0) {
|
|
18810
|
+
await ctx.answerCallbackQuery({ text: 'State dir unset ā cannot apply.' }).catch(() => {})
|
|
18811
|
+
return
|
|
18812
|
+
}
|
|
18813
|
+
const proposal = getSkillProposal(stateDir, parsed.id)
|
|
18814
|
+
if (proposal == null) {
|
|
18815
|
+
await ctx.answerCallbackQuery({ text: 'Proposal expired or already actioned.' }).catch(() => {})
|
|
18816
|
+
if (ctx.callbackQuery?.message) {
|
|
18817
|
+
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }).catch(() => {})
|
|
18818
|
+
}
|
|
18819
|
+
return
|
|
18820
|
+
}
|
|
18821
|
+
if (proposal.status !== 'pending') {
|
|
18822
|
+
await ctx.answerCallbackQuery({ text: `Already ${proposal.status}.` }).catch(() => {})
|
|
18823
|
+
if (ctx.callbackQuery?.message) {
|
|
18824
|
+
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }).catch(() => {})
|
|
18825
|
+
}
|
|
18826
|
+
return
|
|
18827
|
+
}
|
|
18828
|
+
|
|
18829
|
+
const cbChatId = String(ctx.chat?.id ?? ctx.from?.id ?? '')
|
|
18830
|
+
const cbThreadId = resolveThreadId(cbChatId, ctx.callbackQuery?.message?.message_thread_id)
|
|
18831
|
+
|
|
18832
|
+
if (parsed.action === 'deny') {
|
|
18833
|
+
setSkillProposalStatus(stateDir, parsed.id, 'rejected')
|
|
18834
|
+
await ctx.answerCallbackQuery({ text: 'š« Dismissed ā wonāt be proposed again.' }).catch(() => {})
|
|
18835
|
+
if (ctx.callbackQuery?.message && 'text' in ctx.callbackQuery.message) {
|
|
18836
|
+
await ctx
|
|
18837
|
+
.editMessageText(
|
|
18838
|
+
`${escapeHtmlForTg(ctx.callbackQuery.message.text ?? '')}\n\nš« <i>Dismissed.</i>`,
|
|
18839
|
+
{ parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } },
|
|
18840
|
+
)
|
|
18841
|
+
.catch(() => {})
|
|
18842
|
+
}
|
|
18843
|
+
return
|
|
18844
|
+
}
|
|
18845
|
+
|
|
18846
|
+
// Approve.
|
|
18847
|
+
setSkillProposalStatus(stateDir, parsed.id, 'approved')
|
|
18848
|
+
const synthetic = buildSkillProposalApplyInbound({
|
|
18849
|
+
ctx: {
|
|
18850
|
+
agent,
|
|
18851
|
+
chat_id: cbChatId,
|
|
18852
|
+
...(cbThreadId != null ? { threadId: cbThreadId } : {}),
|
|
18853
|
+
},
|
|
18854
|
+
proposalId: proposal.id,
|
|
18855
|
+
skillSlug: proposal.skill_slug,
|
|
18856
|
+
isNew: proposal.is_new,
|
|
18857
|
+
operatorId: senderId,
|
|
18858
|
+
})
|
|
18859
|
+
const delivered = deliverResumeSyntheticOrBuffer(agent, synthetic)
|
|
18860
|
+
await ctx.answerCallbackQuery({ text: 'ā
Applying the skillā¦' }).catch(() => {})
|
|
18861
|
+
if (ctx.callbackQuery?.message && 'text' in ctx.callbackQuery.message) {
|
|
18862
|
+
await ctx
|
|
18863
|
+
.editMessageText(
|
|
18864
|
+
`${escapeHtmlForTg(ctx.callbackQuery.message.text ?? '')}\n\nā
<i>Approved ā applying.</i>`,
|
|
18865
|
+
{ parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } },
|
|
18866
|
+
)
|
|
18867
|
+
.catch(() => {})
|
|
18868
|
+
}
|
|
18869
|
+
process.stderr.write(
|
|
18870
|
+
`telegram gateway: skill_proposal_apply injection agent=${agent} ` +
|
|
18871
|
+
`proposal=${proposal.id} slug=${proposal.skill_slug} delivered=${delivered}\n`,
|
|
18872
|
+
)
|
|
18873
|
+
}
|
|
18874
|
+
|
|
18737
18875
|
async function handleVaultRequestAccessCallback(ctx: Context, data: string): Promise<void> {
|
|
18738
18876
|
const senderId = String(ctx.from?.id ?? '')
|
|
18739
18877
|
const access = loadAccess()
|
|
@@ -18756,8 +18894,8 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
|
|
|
18756
18894
|
.editMessageText(
|
|
18757
18895
|
ctx.callbackQuery.message.chat.id,
|
|
18758
18896
|
ctx.callbackQuery.message.message_id,
|
|
18759
|
-
'ā
|
|
18760
|
-
{
|
|
18897
|
+
richMessage('ā _This access-request card expired before you tapped. Ask the agent to re-issue if the need still stands._'),
|
|
18898
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18761
18899
|
)
|
|
18762
18900
|
.catch(() => {})
|
|
18763
18901
|
}
|
|
@@ -18772,8 +18910,8 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
|
|
|
18772
18910
|
.editMessageText(
|
|
18773
18911
|
pending.chat_id,
|
|
18774
18912
|
pending.card_message_id,
|
|
18775
|
-
`š«
|
|
18776
|
-
{
|
|
18913
|
+
richMessage(`š« _Denied. **${escapeHtmlForTg(pending.agent)}** will not get access to \`${pending.key}\`._`),
|
|
18914
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18777
18915
|
)
|
|
18778
18916
|
.catch(() => {})
|
|
18779
18917
|
}
|
|
@@ -18837,8 +18975,8 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
|
|
|
18837
18975
|
.editMessageText(
|
|
18838
18976
|
pending.chat_id,
|
|
18839
18977
|
pending.card_message_id,
|
|
18840
|
-
`ā
Approved by @${escapeHtmlForTg(username)} ā minting
|
|
18841
|
-
{
|
|
18978
|
+
richMessage(`ā
Approved by @${escapeHtmlForTg(username)} ā mintingā¦`),
|
|
18979
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18842
18980
|
)
|
|
18843
18981
|
.catch(() => {})
|
|
18844
18982
|
}
|
|
@@ -18894,13 +19032,13 @@ async function handleVaultRequestAccessCallback(ctx: Context, data: string): Pro
|
|
|
18894
19032
|
pending.chat_id,
|
|
18895
19033
|
pending.card_message_id,
|
|
18896
19034
|
joiningBatch
|
|
18897
|
-
? `š
|
|
19035
|
+
? `š **Queued behind an earlier card.** Type your passphrase as your next message ā it covers **${items.length}** pending approvals in this chat (one entry mints all grants, no re-type per card).`
|
|
18898
19036
|
: isAdminOnly
|
|
18899
|
-
? `š
|
|
18900
|
-
|
|
18901
|
-
: `š
|
|
18902
|
-
|
|
18903
|
-
{
|
|
19037
|
+
? `š **Admin-only credential.** \`${pending.key}\` requires your vault passphrase to grant ā reply with it as your next message and we'll mint the grant for **${escapeHtmlForTg(pending.agent)}**, then delete the passphrase message.\n\n` +
|
|
19038
|
+
`_The passphrase is what proves it's you: an agent can never mint this key on its own._`
|
|
19039
|
+
: `š **Vault is locked.** Reply with your passphrase as your next message ā we'll unlock, mint the grant for **${escapeHtmlForTg(pending.agent)}**, and delete the passphrase message in one step.\n\n` +
|
|
19040
|
+
richMessage(`_Mint authority stays operator-only: the broker only accepts the grant when the passphrase matches._`),
|
|
19041
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18904
19042
|
)
|
|
18905
19043
|
.catch(() => {})
|
|
18906
19044
|
return
|
|
@@ -18937,8 +19075,8 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
18937
19075
|
.editMessageText(
|
|
18938
19076
|
ctx.callbackQuery.message.chat.id,
|
|
18939
19077
|
ctx.callbackQuery.message.message_id,
|
|
18940
|
-
'ā
|
|
18941
|
-
{
|
|
19078
|
+
richMessage('ā _This vault-save card expired before you tapped. Ask the agent to re-issue if you still want to save._'),
|
|
19079
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18942
19080
|
)
|
|
18943
19081
|
.catch(() => {})
|
|
18944
19082
|
}
|
|
@@ -18953,8 +19091,8 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
18953
19091
|
.editMessageText(
|
|
18954
19092
|
pending.chat_id,
|
|
18955
19093
|
pending.card_message_id,
|
|
18956
|
-
`š«
|
|
18957
|
-
{
|
|
19094
|
+
richMessage(`š« _Discarded. The secret was not written to the vault._`),
|
|
19095
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
18958
19096
|
)
|
|
18959
19097
|
.catch(() => {})
|
|
18960
19098
|
}
|
|
@@ -19003,12 +19141,11 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
19003
19141
|
? escapeHtmlForTg(sourceMsg.text)
|
|
19004
19142
|
: ''
|
|
19005
19143
|
const statusLine =
|
|
19006
|
-
`\n\nāļø
|
|
19007
|
-
`The current proposed key is
|
|
19144
|
+
`\n\nāļø **Rename mode** ā send the new key name as your next message. ` +
|
|
19145
|
+
`The current proposed key is \`${pending.key}\`.`
|
|
19008
19146
|
await finalizeCallback(ctx, {
|
|
19009
19147
|
ackText: 'Send the new key name as your next message.',
|
|
19010
19148
|
newText: baseText ? `${baseText}${statusLine}` : statusLine,
|
|
19011
|
-
parseMode: 'HTML',
|
|
19012
19149
|
})
|
|
19013
19150
|
return
|
|
19014
19151
|
}
|
|
@@ -19047,8 +19184,8 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
19047
19184
|
.editMessageText(
|
|
19048
19185
|
pending.chat_id,
|
|
19049
19186
|
pending.card_message_id,
|
|
19050
|
-
`š
|
|
19051
|
-
{
|
|
19187
|
+
richMessage(`š **Passphrase not cached for this chat.** Run \`/vault unlock\` (or any /vault command) to cache it, then tap Save again on the next card.`),
|
|
19188
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
19052
19189
|
)
|
|
19053
19190
|
.catch(() => {})
|
|
19054
19191
|
}
|
|
@@ -19069,14 +19206,14 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
19069
19206
|
const rendered = renderVaultCliError(parsed, { verb: 'save', key: pending.key })
|
|
19070
19207
|
const body = rendered.suppressRaw
|
|
19071
19208
|
? rendered.html
|
|
19072
|
-
: `ā ļø vault write failed:\n
|
|
19209
|
+
: `ā ļø vault write failed:\n\`\`\`\n${write.output}\n\`\`\``
|
|
19073
19210
|
if (pending.card_message_id != null) {
|
|
19074
19211
|
await ctx.api
|
|
19075
19212
|
.editMessageText(
|
|
19076
19213
|
pending.chat_id,
|
|
19077
19214
|
pending.card_message_id,
|
|
19078
|
-
`${body}\n\
|
|
19079
|
-
{
|
|
19215
|
+
richMessage(`${body}\n\n_Tap a fresh card after fixing the underlying issue._`),
|
|
19216
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
19080
19217
|
)
|
|
19081
19218
|
.catch(() => {})
|
|
19082
19219
|
}
|
|
@@ -19114,8 +19251,8 @@ async function handleVaultRequestSaveCallback(ctx: Context, data: string): Promi
|
|
|
19114
19251
|
.editMessageText(
|
|
19115
19252
|
pending.chat_id,
|
|
19116
19253
|
pending.card_message_id,
|
|
19117
|
-
`ā
saved as
|
|
19118
|
-
{
|
|
19254
|
+
richMessage(`ā
saved as \`vault:${escapeHtmlForTg(pending.key)}\` (masked: \`${escapeHtmlForTg(maskToken(pending.value))}\`)\n_The agent can now reference this as \`vault:${escapeHtmlForTg(pending.key)}\`._`),
|
|
19255
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
19119
19256
|
)
|
|
19120
19257
|
.catch(() => {})
|
|
19121
19258
|
}
|
|
@@ -19238,8 +19375,8 @@ async function handleVaultDeferCallback(ctx: Context, data: string): Promise<voi
|
|
|
19238
19375
|
await ctx.answerCallbackQuery({ text: 'Send your passphraseā¦' }).catch(() => {})
|
|
19239
19376
|
await ctx
|
|
19240
19377
|
.editMessageText(
|
|
19241
|
-
'š Send your vault passphrase as your next message ā we\'ll save the held secret automatically and delete the passphrase message.',
|
|
19242
|
-
{
|
|
19378
|
+
richMessage('š Send your vault passphrase as your next message ā we\'ll save the held secret automatically and delete the passphrase message.'),
|
|
19379
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
19243
19380
|
)
|
|
19244
19381
|
.catch(() => {})
|
|
19245
19382
|
return
|
|
@@ -19324,7 +19461,7 @@ async function startGrantWizardStep1(ctx: Context, chatId: string): Promise<void
|
|
|
19324
19461
|
return
|
|
19325
19462
|
}
|
|
19326
19463
|
const kb = buildGrantAgentKeyboard(agents)
|
|
19327
|
-
const sent = await switchroomReply(ctx, '
|
|
19464
|
+
const sent = await switchroomReply(ctx, '**Grant capability token ā Step 1/3**\n\nWhich agent?', { html: true, reply_markup: kb })
|
|
19328
19465
|
const wizardMsgId = (sent as unknown as { message_id?: number })?.message_id
|
|
19329
19466
|
pendingVaultOps.set(chatId, {
|
|
19330
19467
|
kind: 'grant-wizard',
|
|
@@ -19343,16 +19480,16 @@ async function grantWizardStep2(ctx: Context, chatId: string, agent: string, wiz
|
|
|
19343
19480
|
return
|
|
19344
19481
|
}
|
|
19345
19482
|
if (keys.length === 0) {
|
|
19346
|
-
await switchroomReply(ctx, 'ā ļø No vault keys found. Add secrets first with
|
|
19483
|
+
await switchroomReply(ctx, 'ā ļø No vault keys found. Add secrets first with \`/vault set\`.', { html: true })
|
|
19347
19484
|
pendingVaultOps.delete(chatId)
|
|
19348
19485
|
return
|
|
19349
19486
|
}
|
|
19350
19487
|
const selected = new Set<string>()
|
|
19351
19488
|
const kb = buildGrantKeysKeyboard(keys, selected)
|
|
19352
|
-
const text =
|
|
19489
|
+
const text = `**Grant capability token ā Step 2/3**\n\nWhich keys for \`${agent}\`?\n_Tap to toggle; tap Continue when done._`
|
|
19353
19490
|
if (wizardMsgId != null) {
|
|
19354
19491
|
// allow-raw-bot-api: vault grant wizard step 2/3; already .catch-swallows, tap-driven UI re-renders on retry
|
|
19355
|
-
await ctx.api.editMessageText(chatId, wizardMsgId, text, {
|
|
19492
|
+
await ctx.api.editMessageText(chatId, wizardMsgId, richMessage(text), { reply_markup: kb }).catch(() => {})
|
|
19356
19493
|
} else {
|
|
19357
19494
|
const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb })
|
|
19358
19495
|
wizardMsgId = (sent as unknown as { message_id?: number })?.message_id
|
|
@@ -19371,12 +19508,12 @@ async function grantWizardStep2(ctx: Context, chatId: string, agent: string, wiz
|
|
|
19371
19508
|
/** Advance grant wizard to step 3 (pick duration). */
|
|
19372
19509
|
async function grantWizardStep3(ctx: Context, chatId: string, state: Extract<PendingVaultOp, { kind: 'grant-wizard' }>): Promise<void> {
|
|
19373
19510
|
const kb = buildGrantDurationKeyboard()
|
|
19374
|
-
const keyList = state.selectedKeys!.map(k => `ā¢
|
|
19375
|
-
const text =
|
|
19511
|
+
const keyList = state.selectedKeys!.map(k => `⢠\`${k}\``).join('\n')
|
|
19512
|
+
const text = `**Grant capability token ā Step 3/3**\n\nKeys for \`${state.agent!}\`:\n${keyList}\n\nHow long should this grant be valid?`
|
|
19376
19513
|
const msgId = state.wizardMsgId
|
|
19377
19514
|
if (msgId != null) {
|
|
19378
19515
|
// allow-raw-bot-api: vault grant wizard step 3/3 (TTL select); already .catch-swallows, tap-driven UI re-renders on retry
|
|
19379
|
-
await ctx.api.editMessageText(chatId, msgId, text, {
|
|
19516
|
+
await ctx.api.editMessageText(chatId, msgId, richMessage(text), { reply_markup: kb }).catch(() => {})
|
|
19380
19517
|
} else {
|
|
19381
19518
|
const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb })
|
|
19382
19519
|
state.wizardMsgId = (sent as unknown as { message_id?: number })?.message_id
|
|
@@ -19388,20 +19525,20 @@ async function grantWizardStep3(ctx: Context, chatId: string, state: Extract<Pen
|
|
|
19388
19525
|
async function grantWizardConfirm(ctx: Context, chatId: string, state: Extract<PendingVaultOp, { kind: 'grant-wizard' }>): Promise<void> {
|
|
19389
19526
|
const kb = buildGrantConfirmKeyboard()
|
|
19390
19527
|
const expiresLabel = formatGrantExpiry(state.ttlSeconds!)
|
|
19391
|
-
const keyList = state.selectedKeys!.map(k => `ā¢
|
|
19528
|
+
const keyList = state.selectedKeys!.map(k => `⢠\`${k}\``).join('\n')
|
|
19392
19529
|
const text = [
|
|
19393
|
-
'
|
|
19530
|
+
'**Confirm grant**',
|
|
19394
19531
|
'',
|
|
19395
|
-
`Agent:
|
|
19532
|
+
`Agent: \`${state.agent!}\``,
|
|
19396
19533
|
`Keys:\n${keyList}`,
|
|
19397
|
-
`Expires:
|
|
19534
|
+
`Expires: **${escapeHtmlForTg(expiresLabel)}**`,
|
|
19398
19535
|
'',
|
|
19399
|
-
'Tap
|
|
19536
|
+
'Tap **Generate** to mint the token.',
|
|
19400
19537
|
].join('\n')
|
|
19401
19538
|
const msgId = state.wizardMsgId
|
|
19402
19539
|
if (msgId != null) {
|
|
19403
19540
|
// allow-raw-bot-api: vault grant wizard confirm step; already .catch-swallows, tap-driven UI re-renders on retry
|
|
19404
|
-
await ctx.api.editMessageText(chatId, msgId, text, {
|
|
19541
|
+
await ctx.api.editMessageText(chatId, msgId, richMessage(text), { reply_markup: kb }).catch(() => {})
|
|
19405
19542
|
} else {
|
|
19406
19543
|
const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb })
|
|
19407
19544
|
state.wizardMsgId = (sent as unknown as { message_id?: number })?.message_id
|
|
@@ -19460,7 +19597,7 @@ async function executeGrantWizard(ctx: Context, chatId: string, state: Extract<P
|
|
|
19460
19597
|
return
|
|
19461
19598
|
}
|
|
19462
19599
|
if (result.kind === 'error') {
|
|
19463
|
-
await switchroomReply(ctx,
|
|
19600
|
+
await switchroomReply(ctx, `**mint_grant failed:** ${escapeHtmlForTg(result.msg)}`, { html: true })
|
|
19464
19601
|
return
|
|
19465
19602
|
}
|
|
19466
19603
|
// Write token to the agent's .vault-token file
|
|
@@ -19470,7 +19607,7 @@ async function executeGrantWizard(ctx: Context, chatId: string, state: Extract<P
|
|
|
19470
19607
|
mkdirSync(join(homedir(), '.switchroom', 'agents', state.agent!), { recursive: true })
|
|
19471
19608
|
writeFileSync(tokenPath, token, { mode: 0o600 })
|
|
19472
19609
|
} catch (err) {
|
|
19473
|
-
await switchroomReply(ctx,
|
|
19610
|
+
await switchroomReply(ctx, `**Grant created but token write failed:** ${escapeHtmlForTg(String(err))}`, { html: true })
|
|
19474
19611
|
return
|
|
19475
19612
|
}
|
|
19476
19613
|
// Collapse wizard message to just the outcome.
|
|
@@ -19481,12 +19618,11 @@ async function executeGrantWizard(ctx: Context, chatId: string, state: Extract<P
|
|
|
19481
19618
|
// Strip the keyboard atomically with the success text via the
|
|
19482
19619
|
// finalizeCallback helper.
|
|
19483
19620
|
const msgId = state.wizardMsgId
|
|
19484
|
-
const successText = `ā
Grant
|
|
19621
|
+
const successText = `ā
Grant \`${id}\` created. Written to \`~/.switchroom/agents/${escapeHtmlForTg(state.agent!)}/.vault-token\``
|
|
19485
19622
|
if (msgId != null) {
|
|
19486
19623
|
await finalizeCallback(ctx, {
|
|
19487
19624
|
ackText: 'ā
Grant created',
|
|
19488
19625
|
newText: successText,
|
|
19489
|
-
parseMode: 'HTML',
|
|
19490
19626
|
// No synthInbound ā operator-only flow.
|
|
19491
19627
|
})
|
|
19492
19628
|
} else {
|
|
@@ -19537,15 +19673,14 @@ async function handleVaultGrantCallback(ctx: Context, data: string): Promise<voi
|
|
|
19537
19673
|
return
|
|
19538
19674
|
}
|
|
19539
19675
|
const cardText =
|
|
19540
|
-
`š Revoke
|
|
19541
|
-
`Agent:
|
|
19542
|
-
`Keys:
|
|
19676
|
+
`š Revoke \`${grantId}\`?\n` +
|
|
19677
|
+
`Agent: **${escapeHtmlForTg(grant.agent_slug)}**\n` +
|
|
19678
|
+
`Keys: \`${escapeHtmlForTg(grant.key_allow.join(', '))}\``
|
|
19543
19679
|
const confirmKeyboard = new InlineKeyboard()
|
|
19544
19680
|
.text('ā
Confirm Revoke', `vg:confirm:${grantId}`)
|
|
19545
19681
|
.text('ā Cancel', `vg:cancel:${grantId}`)
|
|
19546
19682
|
await ctx.answerCallbackQuery().catch(() => {})
|
|
19547
|
-
await ctx.editMessageText(cardText, {
|
|
19548
|
-
parse_mode: 'HTML',
|
|
19683
|
+
await ctx.editMessageText(richMessage(cardText), {
|
|
19549
19684
|
reply_markup: confirmKeyboard,
|
|
19550
19685
|
}).catch(async () => {
|
|
19551
19686
|
const chatId = String(ctx.chat?.id ?? ctx.from?.id ?? '')
|
|
@@ -19554,8 +19689,7 @@ async function handleVaultGrantCallback(ctx: Context, data: string): Promise<voi
|
|
|
19554
19689
|
// #1075: thread-id-bearing ā swallow on THREAD_NOT_FOUND.
|
|
19555
19690
|
await swallowingApiCall(
|
|
19556
19691
|
() =>
|
|
19557
|
-
bot.api.
|
|
19558
|
-
parse_mode: 'HTML',
|
|
19692
|
+
bot.api.sendRichMessage(chatId, richMessage(cardText), {
|
|
19559
19693
|
reply_markup: confirmKeyboard,
|
|
19560
19694
|
...(threadId != null ? { message_thread_id: threadId } : {}),
|
|
19561
19695
|
}),
|
|
@@ -19584,8 +19718,8 @@ async function handleVaultGrantCallback(ctx: Context, data: string): Promise<voi
|
|
|
19584
19718
|
}
|
|
19585
19719
|
await ctx.answerCallbackQuery({ text: 'ā
Revoked' }).catch(() => {})
|
|
19586
19720
|
await ctx.editMessageText(
|
|
19587
|
-
`ā
Grant
|
|
19588
|
-
{
|
|
19721
|
+
richMessage(`ā
Grant \`${grantId}\` revoked. Token file removed.`),
|
|
19722
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
19589
19723
|
).catch(() => {})
|
|
19590
19724
|
return
|
|
19591
19725
|
}
|
|
@@ -19700,8 +19834,8 @@ async function handleVaultGrantCallback(ctx: Context, data: string): Promise<voi
|
|
|
19700
19834
|
// `.catch(() => {})` would swallow the failure silently ā same
|
|
19701
19835
|
// hazard PR #1158 caught on the operator-event card.
|
|
19702
19836
|
await ctx.editMessageText(
|
|
19703
|
-
escapeHtmlForTg(msg.text) + '\n\
|
|
19704
|
-
{
|
|
19837
|
+
richMessage(escapeHtmlForTg(msg.text) + '\n\n_Send a duration like \`30d\` or \`12h\`:_'),
|
|
19838
|
+
{ reply_markup: buildGrantDurationKeyboard() },
|
|
19705
19839
|
).catch(() => {})
|
|
19706
19840
|
}
|
|
19707
19841
|
await ackSilently()
|
|
@@ -19803,15 +19937,14 @@ async function executeDeferredSecretSave(
|
|
|
19803
19937
|
const rendered = renderVaultCliError(parsed, { verb: "save", key: slug })
|
|
19804
19938
|
const body = rendered.suppressRaw
|
|
19805
19939
|
? rendered.html
|
|
19806
|
-
: `ā ļø vault write failed:\n
|
|
19940
|
+
: `ā ļø vault write failed:\n\`\`\`\n${write.output}\n\`\`\``
|
|
19807
19941
|
if (cardMessageId != null) {
|
|
19808
19942
|
await ctx.api
|
|
19809
19943
|
.editMessageText(
|
|
19810
19944
|
deferred.chat_id,
|
|
19811
19945
|
cardMessageId,
|
|
19812
|
-
`${body}\n\nRe-tap to retry
|
|
19946
|
+
richMessage(`${body}\n\nRe-tap to retry.`),
|
|
19813
19947
|
{
|
|
19814
|
-
parse_mode: 'HTML',
|
|
19815
19948
|
reply_markup: buildDeferredSecretKeyboard(deferKey).inline_keyboard.length > 0
|
|
19816
19949
|
? buildDeferredSecretKeyboard(deferKey)
|
|
19817
19950
|
: undefined,
|
|
@@ -19829,8 +19962,8 @@ async function executeDeferredSecretSave(
|
|
|
19829
19962
|
.editMessageText(
|
|
19830
19963
|
deferred.chat_id,
|
|
19831
19964
|
cardMessageId,
|
|
19832
|
-
`ā
stored as
|
|
19833
|
-
{
|
|
19965
|
+
richMessage(`ā
stored as \`vault:${slug}\` (masked: \`${masked}\`)\n\nReply \`rename NEW_NAME\` to relabel.`),
|
|
19966
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
19834
19967
|
)
|
|
19835
19968
|
.catch(() => {})
|
|
19836
19969
|
}
|
|
@@ -19907,11 +20040,10 @@ async function handleOperatorEventCallback(ctx: Context, data: string): Promise<
|
|
|
19907
20040
|
case 'dismiss': {
|
|
19908
20041
|
// #1150 audit P1: was strip-only. Now appends a status line so
|
|
19909
20042
|
// scrollback shows the dismissal.
|
|
19910
|
-
const status = `\n\nā
|
|
20043
|
+
const status = `\n\nā _Dismissed by operator._`
|
|
19911
20044
|
await finalizeCallback(ctx, {
|
|
19912
20045
|
ackText: 'Dismissed',
|
|
19913
20046
|
newText: sourceMsgText ? `${sourceMsgText}${status}` : status,
|
|
19914
|
-
parseMode: 'HTML',
|
|
19915
20047
|
// No synthInbound ā dismiss is operator-only, no model in loop.
|
|
19916
20048
|
})
|
|
19917
20049
|
return
|
|
@@ -19922,11 +20054,10 @@ async function handleOperatorEventCallback(ctx: Context, data: string): Promise<
|
|
|
19922
20054
|
// #1150 audit P1: was reply + editMessageReplyMarkup (two
|
|
19923
20055
|
// separate edits). Atomic via finalizeCallback now ā the
|
|
19924
20056
|
// status line is the announcement, no separate reply needed.
|
|
19925
|
-
const status = `\n\nš
|
|
20057
|
+
const status = `\n\nš _**${escapeHtmlForTg(agent)}** restart requested by operator._`
|
|
19926
20058
|
await finalizeCallback(ctx, {
|
|
19927
20059
|
ackText: `Restarting ${agent}ā¦`,
|
|
19928
20060
|
newText: sourceMsgText ? `${sourceMsgText}${status}` : status,
|
|
19929
|
-
parseMode: 'HTML',
|
|
19930
20061
|
})
|
|
19931
20062
|
} else {
|
|
19932
20063
|
// Failure-path: leave the keyboard tappable so the operator
|
|
@@ -19936,11 +20067,9 @@ async function handleOperatorEventCallback(ctx: Context, data: string): Promise<
|
|
|
19936
20067
|
const isDocker = process.env.SWITCHROOM_RUNTIME === 'docker'
|
|
19937
20068
|
const detail = isDocker
|
|
19938
20069
|
? `cross-agent restart is not supported under docker. ` +
|
|
19939
|
-
`Restart from the host:
|
|
20070
|
+
`Restart from the host: \`docker compose -p switchroom restart agent-${agent}\`.`
|
|
19940
20071
|
: 'restart trigger failed'
|
|
19941
|
-
await ctx.
|
|
19942
|
-
parse_mode: 'HTML',
|
|
19943
|
-
})
|
|
20072
|
+
await ctx.replyWithRichMessage(richMessage(`**Restart failed for ${agent}:** ${detail}`))
|
|
19944
20073
|
}
|
|
19945
20074
|
return
|
|
19946
20075
|
}
|
|
@@ -19951,11 +20080,10 @@ async function handleOperatorEventCallback(ctx: Context, data: string): Promise<
|
|
|
19951
20080
|
// for the login URL state. Strip the keyboard and append a status
|
|
19952
20081
|
// line; the new reauth-flow's own messages appear below the
|
|
19953
20082
|
// collapsed card.
|
|
19954
|
-
const status = `\n\nš
|
|
20083
|
+
const status = `\n\nš _Reauth started for **${escapeHtmlForTg(agent)}** ā follow the login URL below._`
|
|
19955
20084
|
await finalizeCallback(ctx, {
|
|
19956
20085
|
ackText: `Starting reauth for ${agent}ā¦`,
|
|
19957
20086
|
newText: sourceMsgText ? `${sourceMsgText}${status}` : status,
|
|
19958
|
-
parseMode: 'HTML',
|
|
19959
20087
|
synthInbound: async () => {
|
|
19960
20088
|
await runSwitchroomAuthCommand(ctx, ['auth', 'reauth', agent], `auth reauth ${agent}`)
|
|
19961
20089
|
// PR3 supergroup-mode: key by (chat, thread) so an OAuth code
|
|
@@ -19980,11 +20108,10 @@ async function handleOperatorEventCallback(ctx: Context, data: string): Promise<
|
|
|
19980
20108
|
// path systemd-only.
|
|
19981
20109
|
const isDocker = process.env.SWITCHROOM_RUNTIME === 'docker'
|
|
19982
20110
|
if (isDocker) {
|
|
19983
|
-
await ctx.
|
|
19984
|
-
|
|
19985
|
-
`Run from the host:
|
|
19986
|
-
|
|
19987
|
-
)
|
|
20111
|
+
await ctx.replyWithRichMessage(richMessage(
|
|
20112
|
+
`_Inline log fetch is not available under docker mode (no docker.sock in agent containers). ` +
|
|
20113
|
+
`Run from the host: \`docker logs --since 30m --tail 30 switchroom-${agent}\`_`,
|
|
20114
|
+
))
|
|
19988
20115
|
return
|
|
19989
20116
|
}
|
|
19990
20117
|
try {
|
|
@@ -19994,17 +20121,15 @@ async function handleOperatorEventCallback(ctx: Context, data: string): Promise<
|
|
|
19994
20121
|
{ encoding: 'utf-8', timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'] },
|
|
19995
20122
|
) as string
|
|
19996
20123
|
const trimmed = out.trim().slice(-3500)
|
|
19997
|
-
await ctx.
|
|
20124
|
+
await ctx.replyWithRichMessage(richMessage(
|
|
19998
20125
|
trimmed
|
|
19999
|
-
?
|
|
20000
|
-
:
|
|
20001
|
-
|
|
20002
|
-
)
|
|
20126
|
+
? `\`\`\`\n${trimmed.replace(/```/g, '`ā``')}\n\`\`\``
|
|
20127
|
+
: `_No logs for ${agent}._`,
|
|
20128
|
+
))
|
|
20003
20129
|
} catch (err) {
|
|
20004
|
-
await ctx.
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
)
|
|
20130
|
+
await ctx.replyWithRichMessage(richMessage(
|
|
20131
|
+
`**logs failed:** ${escapeHtmlForTg((err as Error).message)}`,
|
|
20132
|
+
))
|
|
20008
20133
|
}
|
|
20009
20134
|
return
|
|
20010
20135
|
}
|
|
@@ -20058,10 +20183,12 @@ async function handleAuthDashboardCallback(ctx: Context): Promise<void> {
|
|
|
20058
20183
|
bot.api.editMessageText(
|
|
20059
20184
|
msg.chat.id,
|
|
20060
20185
|
msg.message_id,
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20186
|
+
richMessage(
|
|
20187
|
+
`**Active account ā** \`${result.active}\`\n` +
|
|
20188
|
+
`_Re-mirrored credentials for ${result.fanned.length} agent${result.fanned.length === 1 ? '' : 's'}._\n\n` +
|
|
20189
|
+
`_Tap /auth to see updated quota for the new active account._`,
|
|
20190
|
+
),
|
|
20191
|
+
{},
|
|
20065
20192
|
),
|
|
20066
20193
|
{ chat_id: String(msg.chat.id), verb: 'auth:use:edit' },
|
|
20067
20194
|
)
|
|
@@ -20140,8 +20267,7 @@ async function handleAuthDashboardCallback(ctx: Context): Promise<void> {
|
|
|
20140
20267
|
if (msg) {
|
|
20141
20268
|
await swallowingApiCall(
|
|
20142
20269
|
() =>
|
|
20143
|
-
bot.api.editMessageText(msg.chat.id, msg.message_id, text, {
|
|
20144
|
-
parse_mode: 'HTML',
|
|
20270
|
+
bot.api.editMessageText(msg.chat.id, msg.message_id, richMessage(text), {
|
|
20145
20271
|
reply_markup: { inline_keyboard },
|
|
20146
20272
|
}),
|
|
20147
20273
|
{ chat_id: String(msg.chat.id), verb: 'auth:refresh:edit' },
|
|
@@ -20189,17 +20315,17 @@ bot.command('vault', async ctx => {
|
|
|
20189
20315
|
const key = args[1]
|
|
20190
20316
|
if (!sub || sub === 'help') {
|
|
20191
20317
|
await switchroomReply(ctx, [
|
|
20192
|
-
'
|
|
20318
|
+
'**Vault commands**',
|
|
20193
20319
|
'/vault list ā list all secret keys',
|
|
20194
|
-
'/vault get
|
|
20195
|
-
'/vault set
|
|
20196
|
-
'/vault delete
|
|
20320
|
+
'/vault get <key> ā read a secret value',
|
|
20321
|
+
'/vault set <key> ā set a secret',
|
|
20322
|
+
'/vault delete <key> ā remove a secret',
|
|
20197
20323
|
'/vault status ā show broker state',
|
|
20198
20324
|
'/vault unlock ā unlock the broker (prompts for passphrase)',
|
|
20199
20325
|
'/vault lock ā lock the broker',
|
|
20200
20326
|
'/vault grant ā mint a capability token (inline wizard)',
|
|
20201
20327
|
'/vault grants [agent] ā list active capability grants (tap to revoke)',
|
|
20202
|
-
'/vault audit
|
|
20328
|
+
'/vault audit <agent> ā unified view of an agent\'s vault access',
|
|
20203
20329
|
'',
|
|
20204
20330
|
'Your passphrase is cached in memory for 30 min after first use.',
|
|
20205
20331
|
].join('\n'), { html: true })
|
|
@@ -20212,7 +20338,7 @@ bot.command('vault', async ctx => {
|
|
|
20212
20338
|
if (sub === 'audit') {
|
|
20213
20339
|
const targetAgent = args[1]
|
|
20214
20340
|
if (!targetAgent) {
|
|
20215
|
-
await switchroomReply(ctx, 'Usage: /vault audit
|
|
20341
|
+
await switchroomReply(ctx, 'Usage: /vault audit <agent>', { html: true })
|
|
20216
20342
|
return
|
|
20217
20343
|
}
|
|
20218
20344
|
// Sanity-check the agent name shape ā same charset constraint as
|
|
@@ -20270,48 +20396,48 @@ bot.command('vault', async ctx => {
|
|
|
20270
20396
|
}
|
|
20271
20397
|
|
|
20272
20398
|
// 3. Compose the rendered audit view.
|
|
20273
|
-
const lines: string[] = [
|
|
20399
|
+
const lines: string[] = [`**š ${escapeHtmlForTg(targetAgent)} ā vault access**`, '']
|
|
20274
20400
|
|
|
20275
20401
|
if (grantsError) {
|
|
20276
|
-
lines.push(
|
|
20402
|
+
lines.push(`_Grants: ā ļø ${escapeHtmlForTg(grantsError)}_`)
|
|
20277
20403
|
lines.push('')
|
|
20278
20404
|
} else {
|
|
20279
|
-
lines.push('
|
|
20405
|
+
lines.push('**Read grants:**')
|
|
20280
20406
|
if (readGrants.length === 0) {
|
|
20281
|
-
lines.push('
|
|
20407
|
+
lines.push(' _none_')
|
|
20282
20408
|
} else {
|
|
20283
20409
|
for (const g of readGrants) {
|
|
20284
20410
|
const expiry = g.expiresAt
|
|
20285
20411
|
? new Date(g.expiresAt * 1000).toISOString().slice(0, 10)
|
|
20286
20412
|
: 'no expiry'
|
|
20287
20413
|
const keysJoined = g.keys.join(', ')
|
|
20288
|
-
lines.push(`
|
|
20414
|
+
lines.push(` \`${g.id}\` Ā· ${escapeHtmlForTg(keysJoined)} Ā· expires ${expiry}`)
|
|
20289
20415
|
}
|
|
20290
20416
|
}
|
|
20291
20417
|
lines.push('')
|
|
20292
|
-
lines.push('
|
|
20418
|
+
lines.push('**Write grants:**')
|
|
20293
20419
|
if (writeGrants.length === 0) {
|
|
20294
|
-
lines.push('
|
|
20420
|
+
lines.push(' _none_')
|
|
20295
20421
|
} else {
|
|
20296
20422
|
for (const g of writeGrants) {
|
|
20297
20423
|
const expiry = g.expiresAt
|
|
20298
20424
|
? new Date(g.expiresAt * 1000).toISOString().slice(0, 10)
|
|
20299
20425
|
: 'no expiry'
|
|
20300
20426
|
const keysJoined = g.keys.join(', ')
|
|
20301
|
-
lines.push(`
|
|
20427
|
+
lines.push(` \`${g.id}\` Ā· ${escapeHtmlForTg(keysJoined)} Ā· expires ${expiry}`)
|
|
20302
20428
|
}
|
|
20303
20429
|
}
|
|
20304
20430
|
lines.push('')
|
|
20305
20431
|
}
|
|
20306
20432
|
|
|
20307
|
-
lines.push('
|
|
20433
|
+
lines.push('**Cron-declared secrets:**')
|
|
20308
20434
|
if (configError) {
|
|
20309
|
-
lines.push(`
|
|
20435
|
+
lines.push(` _ā ļø ${escapeHtmlForTg(configError)}_`)
|
|
20310
20436
|
} else if (cronEntries.length === 0) {
|
|
20311
|
-
lines.push('
|
|
20437
|
+
lines.push(' _none_')
|
|
20312
20438
|
} else {
|
|
20313
20439
|
for (const entry of cronEntries) {
|
|
20314
|
-
const cronLabel = entry.cron ? ` (
|
|
20440
|
+
const cronLabel = entry.cron ? ` (\`${entry.cron}\`)` : ''
|
|
20315
20441
|
const keysJoined = entry.secrets.join(', ')
|
|
20316
20442
|
lines.push(` schedule[${entry.index}]${cronLabel}: ${escapeHtmlForTg(keysJoined)}`)
|
|
20317
20443
|
}
|
|
@@ -20326,11 +20452,11 @@ bot.command('vault', async ctx => {
|
|
|
20326
20452
|
const denials = readRecentDenialsForAgent(targetAgent, 7 * 24 * 60 * 60 * 1000, 5)
|
|
20327
20453
|
const denialKeyboard = new InlineKeyboard()
|
|
20328
20454
|
if (denials.length > 0) {
|
|
20329
|
-
lines.push('
|
|
20455
|
+
lines.push('**Recent denials (last 7d):**')
|
|
20330
20456
|
for (const d of denials) {
|
|
20331
20457
|
const when = new Date(d.lastSeenMs).toISOString().slice(0, 16).replace('T', ' ')
|
|
20332
20458
|
lines.push(
|
|
20333
|
-
`
|
|
20459
|
+
` \`${d.key}\` Ā· ${d.count}Ć Ā· last ${when}`,
|
|
20334
20460
|
)
|
|
20335
20461
|
// callback_data must stay ⤠64 bytes. Format:
|
|
20336
20462
|
// vrd:<agent>:<key>
|
|
@@ -20348,17 +20474,15 @@ bot.command('vault', async ctx => {
|
|
|
20348
20474
|
}
|
|
20349
20475
|
|
|
20350
20476
|
lines.push(
|
|
20351
|
-
|
|
20477
|
+
`_Summary: ${readGrants.length} read grant${readGrants.length === 1 ? '' : 's'}, ` +
|
|
20352
20478
|
`${writeGrants.length} write grant${writeGrants.length === 1 ? '' : 's'}, ` +
|
|
20353
20479
|
`${cronEntries.length} cron entr${cronEntries.length === 1 ? 'y' : 'ies'}, ` +
|
|
20354
|
-
`${denials.length} recent denial${denials.length === 1 ? '' : 's'}
|
|
20480
|
+
`${denials.length} recent denial${denials.length === 1 ? '' : 's'}._`,
|
|
20355
20481
|
)
|
|
20356
20482
|
|
|
20357
20483
|
const chatIdAudit = String(ctx.chat!.id)
|
|
20358
20484
|
const threadIdAudit = resolveThreadId(chatIdAudit, ctx.message?.message_thread_id)
|
|
20359
|
-
await ctx.
|
|
20360
|
-
parse_mode: 'HTML',
|
|
20361
|
-
link_preview_options: { is_disabled: true },
|
|
20485
|
+
await ctx.replyWithRichMessage(richMessage(lines.join('\n')), {
|
|
20362
20486
|
...(denials.length > 0 ? { reply_markup: denialKeyboard } : {}),
|
|
20363
20487
|
...(threadIdAudit != null ? { message_thread_id: threadIdAudit } : {}),
|
|
20364
20488
|
})
|
|
@@ -20379,7 +20503,7 @@ bot.command('vault', async ctx => {
|
|
|
20379
20503
|
}
|
|
20380
20504
|
const { grants } = result
|
|
20381
20505
|
if (grants.length === 0) {
|
|
20382
|
-
const filterNote = agentFilter ? ` for
|
|
20506
|
+
const filterNote = agentFilter ? ` for \`${agentFilter}\`` : ''
|
|
20383
20507
|
await switchroomReply(ctx, `š No active grants${filterNote}.`, { html: true })
|
|
20384
20508
|
return
|
|
20385
20509
|
}
|
|
@@ -20391,16 +20515,16 @@ bot.command('vault', async ctx => {
|
|
|
20391
20515
|
byAgent.set(g.agent_slug, list)
|
|
20392
20516
|
}
|
|
20393
20517
|
// Build message text (grouped) + inline keyboard (one [Revoke] per grant per row)
|
|
20394
|
-
const lines: string[] = ['
|
|
20518
|
+
const lines: string[] = ['**š Active grants**', '']
|
|
20395
20519
|
const keyboard = new InlineKeyboard()
|
|
20396
20520
|
for (const [agentName, agentGrants] of byAgent) {
|
|
20397
|
-
lines.push(
|
|
20521
|
+
lines.push(`**${escapeHtmlForTg(agentName)}:**`)
|
|
20398
20522
|
for (const g of agentGrants) {
|
|
20399
20523
|
const keys = g.key_allow.join(', ')
|
|
20400
20524
|
const expiry = g.expires_at
|
|
20401
20525
|
? new Date(g.expires_at * 1000).toISOString().slice(0, 10)
|
|
20402
20526
|
: 'no expiry'
|
|
20403
|
-
lines.push(`ā¢
|
|
20527
|
+
lines.push(`⢠\`${g.id}\` ā ${escapeHtmlForTg(keys)}, expires ${expiry}`)
|
|
20404
20528
|
// callback_data: vg:revoke:<id> ā max 64 bytes; grant IDs are "vg_" + 6 chars = 9 chars total ā well within limit
|
|
20405
20529
|
keyboard.text(`š Revoke ${g.id}`, `vg:revoke:${g.id}`).row()
|
|
20406
20530
|
}
|
|
@@ -20408,9 +20532,7 @@ bot.command('vault', async ctx => {
|
|
|
20408
20532
|
}
|
|
20409
20533
|
const chatId2 = String(ctx.chat!.id)
|
|
20410
20534
|
const threadId2 = resolveThreadId(chatId2, ctx.message?.message_thread_id)
|
|
20411
|
-
await ctx.
|
|
20412
|
-
parse_mode: 'HTML',
|
|
20413
|
-
link_preview_options: { is_disabled: true },
|
|
20535
|
+
await ctx.replyWithRichMessage(richMessage(lines.join('\n')), {
|
|
20414
20536
|
reply_markup: keyboard,
|
|
20415
20537
|
...(threadId2 != null ? { message_thread_id: threadId2 } : {}),
|
|
20416
20538
|
})
|
|
@@ -20460,11 +20582,11 @@ bot.command('vault', async ctx => {
|
|
|
20460
20582
|
}
|
|
20461
20583
|
|
|
20462
20584
|
if (!['list', 'get', 'set', 'delete', 'remove'].includes(sub)) {
|
|
20463
|
-
await switchroomReply(ctx, `Unknown vault subcommand:
|
|
20585
|
+
await switchroomReply(ctx, `Unknown vault subcommand: \`${sub}\`. Try /vault help`, { html: true })
|
|
20464
20586
|
return
|
|
20465
20587
|
}
|
|
20466
|
-
if ((sub === 'get' || sub === 'delete' || sub === 'remove') && !key) { await switchroomReply(ctx, `Usage: /vault ${sub}
|
|
20467
|
-
if (sub === 'set' && !key) { await switchroomReply(ctx, 'Usage: /vault set
|
|
20588
|
+
if ((sub === 'get' || sub === 'delete' || sub === 'remove') && !key) { await switchroomReply(ctx, `Usage: /vault ${sub} <key>`, { html: true }); return }
|
|
20589
|
+
if (sub === 'set' && !key) { await switchroomReply(ctx, 'Usage: /vault set <key>', { html: true }); return }
|
|
20468
20590
|
|
|
20469
20591
|
const cached = vaultPassphraseCache.get(chatId)
|
|
20470
20592
|
const passphrase = cached && cached.expiresAt > Date.now() ? cached.passphrase : undefined
|
|
@@ -20551,9 +20673,9 @@ bot.command('issues', async ctx => {
|
|
|
20551
20673
|
await switchroomReply(
|
|
20552
20674
|
ctx,
|
|
20553
20675
|
[
|
|
20554
|
-
`This will resolve
|
|
20676
|
+
`This will resolve **${n} ${noun}**. To confirm, run:`,
|
|
20555
20677
|
'',
|
|
20556
|
-
'
|
|
20678
|
+
'\`/issues clear --confirm\`',
|
|
20557
20679
|
].join('\n'),
|
|
20558
20680
|
{ html: true },
|
|
20559
20681
|
)
|
|
@@ -20573,7 +20695,7 @@ bot.command('issues', async ctx => {
|
|
|
20573
20695
|
}
|
|
20574
20696
|
await switchroomReply(
|
|
20575
20697
|
ctx,
|
|
20576
|
-
'Usage:
|
|
20698
|
+
'Usage: \`/issues\` | \`/issues resolve <fingerprint>\` | \`/issues clear [--confirm]\`',
|
|
20577
20699
|
{ html: true },
|
|
20578
20700
|
)
|
|
20579
20701
|
})
|
|
@@ -20633,12 +20755,12 @@ bot.command('usage', async ctx => {
|
|
|
20633
20755
|
// socket bind, etc.). Same shape /usage shipped with originally.
|
|
20634
20756
|
const agentDir = resolveAgentDirFromEnv()
|
|
20635
20757
|
if (!agentDir) {
|
|
20636
|
-
await switchroomReply(ctx, '
|
|
20758
|
+
await switchroomReply(ctx, '**/usage:** cannot resolve agent dir.', { html: true })
|
|
20637
20759
|
return
|
|
20638
20760
|
}
|
|
20639
20761
|
const result = await fetchQuota({ claudeConfigDir: join(agentDir, '.claude') })
|
|
20640
20762
|
if (!result.ok) {
|
|
20641
|
-
await switchroomReply(ctx,
|
|
20763
|
+
await switchroomReply(ctx, `**/usage:** ${escapeHtmlForTg(result.reason)}`, { html: true })
|
|
20642
20764
|
return
|
|
20643
20765
|
}
|
|
20644
20766
|
await switchroomReply(ctx, formatQuotaBlock(result.data), { html: true })
|
|
@@ -20694,7 +20816,7 @@ async function renderFleetDoctor(ctx: Context): Promise<void> {
|
|
|
20694
20816
|
return
|
|
20695
20817
|
}
|
|
20696
20818
|
if (resp.result === 'denied') {
|
|
20697
|
-
await switchroomReply(ctx, `š©ŗ
|
|
20819
|
+
await switchroomReply(ctx, `𩺠**Whole-fleet doctor denied by hostd:**\n${preBlock(formatSwitchroomOutput(resp.error ?? 'admin required'))}`, { html: true })
|
|
20698
20820
|
return
|
|
20699
20821
|
}
|
|
20700
20822
|
// `completed` (including `switchroom doctor` exit 1 = "found
|
|
@@ -20712,7 +20834,7 @@ bot.command('doctor', async ctx => {
|
|
|
20712
20834
|
// approval card ā doctor is read-only). Everyone else keeps the
|
|
20713
20835
|
// original zero-extra-tap in-container behaviour.
|
|
20714
20836
|
if (AGENT_ADMIN && hostdWillBeUsed(getMyAgentName())) {
|
|
20715
|
-
await switchroomReply(ctx, 'š©ŗ
|
|
20837
|
+
await switchroomReply(ctx, '𩺠**Doctor** ā which scope?', {
|
|
20716
20838
|
html: true,
|
|
20717
20839
|
reply_markup: buildDoctorScopeKeyboard(),
|
|
20718
20840
|
})
|
|
@@ -20720,7 +20842,7 @@ bot.command('doctor', async ctx => {
|
|
|
20720
20842
|
}
|
|
20721
20843
|
await renderSelfDoctor(ctx)
|
|
20722
20844
|
} catch (err: unknown) {
|
|
20723
|
-
await switchroomReply(ctx,
|
|
20845
|
+
await switchroomReply(ctx, `**doctor failed:**\n${preBlock(formatSwitchroomOutput((err as any).message ?? 'unknown error'))}`, { html: true })
|
|
20724
20846
|
}
|
|
20725
20847
|
})
|
|
20726
20848
|
|
|
@@ -20770,7 +20892,7 @@ bot.command('version', async ctx => {
|
|
|
20770
20892
|
if (!trimmed) { await switchroomReply(ctx, 'version: no output'); return }
|
|
20771
20893
|
await switchroomReply(ctx, preBlock(formatSwitchroomOutput(trimmed)), { html: true })
|
|
20772
20894
|
} catch (err: unknown) {
|
|
20773
|
-
await switchroomReply(ctx,
|
|
20895
|
+
await switchroomReply(ctx, `**version failed:**\n${preBlock(formatSwitchroomOutput((err as any).message ?? 'unknown error'))}`, { html: true })
|
|
20774
20896
|
}
|
|
20775
20897
|
})
|
|
20776
20898
|
|
|
@@ -20793,7 +20915,7 @@ bot.command('whoami', async ctx => {
|
|
|
20793
20915
|
catch { card = preBlock(formatSwitchroomOutput(trimmed || 'whoami: no output')) }
|
|
20794
20916
|
await switchroomReply(ctx, card, { html: true })
|
|
20795
20917
|
} catch (err: unknown) {
|
|
20796
|
-
await switchroomReply(ctx,
|
|
20918
|
+
await switchroomReply(ctx, `**whoami failed:**\n${preBlock(formatSwitchroomOutput((err as any).message ?? 'unknown error'))}`, { html: true })
|
|
20797
20919
|
}
|
|
20798
20920
|
})
|
|
20799
20921
|
|
|
@@ -20811,7 +20933,7 @@ function formatWhoamiCard(v: {
|
|
|
20811
20933
|
const esc = escapeHtmlForTg
|
|
20812
20934
|
const yn = (b?: boolean) => (b ? 'ā' : 'ā')
|
|
20813
20935
|
const lines: string[] = []
|
|
20814
|
-
lines.push(`š¤
|
|
20936
|
+
lines.push(`š¤ **${esc(v.name ?? '?')}** Ā· ${esc(v.tier ?? 'standard')}`)
|
|
20815
20937
|
if (v.persona) lines.push(esc(v.persona))
|
|
20816
20938
|
if (v.model) lines.push(`Model: ${esc(v.model)}`)
|
|
20817
20939
|
const allow = v.tools?.allow ?? []
|
|
@@ -20882,8 +21004,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
20882
21004
|
try {
|
|
20883
21005
|
const outcome = await handleEffortMenuCallback(data, buildEffortDeps())
|
|
20884
21006
|
await ctx
|
|
20885
|
-
.editMessageText(outcome.reply.text, {
|
|
20886
|
-
parse_mode: 'HTML',
|
|
21007
|
+
.editMessageText(richMessage(outcome.reply.text), {
|
|
20887
21008
|
reply_markup: effortMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] },
|
|
20888
21009
|
})
|
|
20889
21010
|
.catch(() => {})
|
|
@@ -20908,8 +21029,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
20908
21029
|
if (process.env.SWITCHROOM_MODEL_MENU === '0') {
|
|
20909
21030
|
await ctx.answerCallbackQuery({ text: 'Model menu is disabled (SWITCHROOM_MODEL_MENU=0).' }).catch(() => {})
|
|
20910
21031
|
await ctx
|
|
20911
|
-
.editMessageText('Model menu is disabled on this agent. Use
|
|
20912
|
-
parse_mode: 'HTML',
|
|
21032
|
+
.editMessageText(richMessage('Model menu is disabled on this agent. Use \`/model <name>\`.'), {
|
|
20913
21033
|
reply_markup: { inline_keyboard: [] },
|
|
20914
21034
|
})
|
|
20915
21035
|
.catch(() => {})
|
|
@@ -20953,7 +21073,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
20953
21073
|
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
20954
21074
|
const srLabel = escapeHtmlForTg(srFriendlyLabel(data.slice(MODEL_CALLBACK_SR.length)))
|
|
20955
21075
|
await ctx
|
|
20956
|
-
.editMessageText(`ā³ Switching session to
|
|
21076
|
+
.editMessageText(richMessage(`ā³ Switching session to **${srLabel}**ā¦`), { reply_markup: { inline_keyboard: [] } })
|
|
20957
21077
|
.catch(() => {})
|
|
20958
21078
|
didInterimSrEdit = true
|
|
20959
21079
|
}
|
|
@@ -20982,8 +21102,8 @@ bot.on('callback_query:data', async ctx => {
|
|
|
20982
21102
|
// Replace the menu with a restart notice (no buttons ā session is ending).
|
|
20983
21103
|
await ctx
|
|
20984
21104
|
.editMessageText(
|
|
20985
|
-
`š Switching from
|
|
20986
|
-
{
|
|
21105
|
+
richMessage(`š Switching from **${escapeHtmlForTg(prevSessionModel!)}** back to Claude ā restarting session cleanly. Claude will be ready in ~30s.`),
|
|
21106
|
+
{ reply_markup: { inline_keyboard: [] } },
|
|
20987
21107
|
)
|
|
20988
21108
|
.catch(() => {})
|
|
20989
21109
|
// Write the restart marker so the post-restart boot card edits into this chat.
|
|
@@ -21001,8 +21121,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21001
21121
|
}
|
|
21002
21122
|
|
|
21003
21123
|
await ctx
|
|
21004
|
-
.editMessageText(outcome.reply.text, {
|
|
21005
|
-
parse_mode: 'HTML',
|
|
21124
|
+
.editMessageText(richMessage(outcome.reply.text), {
|
|
21006
21125
|
reply_markup: modelMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] },
|
|
21007
21126
|
})
|
|
21008
21127
|
.catch(() => {})
|
|
@@ -21099,8 +21218,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21099
21218
|
editCard: async (args) => {
|
|
21100
21219
|
try {
|
|
21101
21220
|
await robustApiCall(() =>
|
|
21102
|
-
bot.api.editMessageText(args.chatId, args.messageId, args.text, {
|
|
21103
|
-
parse_mode: 'HTML',
|
|
21221
|
+
bot.api.editMessageText(args.chatId, args.messageId, richMessage(args.text), {
|
|
21104
21222
|
// Resolved on tap ā strip the keyboard so it can't be re-tapped.
|
|
21105
21223
|
...(args.stripKeyboard ? { reply_markup: { inline_keyboard: [] } } : {}),
|
|
21106
21224
|
}),
|
|
@@ -21185,7 +21303,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21185
21303
|
if (scope === 'fleet') await renderFleetDoctor(ctx)
|
|
21186
21304
|
else await renderSelfDoctor(ctx)
|
|
21187
21305
|
} catch (err: unknown) {
|
|
21188
|
-
await switchroomReply(ctx,
|
|
21306
|
+
await switchroomReply(ctx, `**doctor failed:**\n${preBlock(formatSwitchroomOutput((err as any).message ?? 'unknown error'))}`, { html: true })
|
|
21189
21307
|
}
|
|
21190
21308
|
return
|
|
21191
21309
|
}
|
|
@@ -21224,6 +21342,16 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21224
21342
|
return
|
|
21225
21343
|
}
|
|
21226
21344
|
|
|
21345
|
+
// #2670: one-tap skill-improvement proposal card.
|
|
21346
|
+
// skprop:approve:<id> ā apply the stored draft via the personal-skill
|
|
21347
|
+
// write pipeline (inject a turn; agent writes it)
|
|
21348
|
+
// skprop:deny:<id> ā dismiss + record a rejection fingerprint so it
|
|
21349
|
+
// isn't re-proposed
|
|
21350
|
+
if (data.startsWith('skprop:')) {
|
|
21351
|
+
await handleSkillProposalCallback(ctx, data)
|
|
21352
|
+
return
|
|
21353
|
+
}
|
|
21354
|
+
|
|
21227
21355
|
// Issue #969 P2b: vault recent-denial one-tap approval.
|
|
21228
21356
|
// vrd:<agent>:<key> ā mint a 30-day read-grant for the agent + key
|
|
21229
21357
|
// Posted by /vault audit <agent> in the "Recent denials" section.
|
|
@@ -21279,8 +21407,7 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21279
21407
|
if (sourceMsg && 'text' in sourceMsg && sourceMsg.text != null) {
|
|
21280
21408
|
try {
|
|
21281
21409
|
await ctx.editMessageText(
|
|
21282
|
-
`${escapeHtmlForTg(sourceMsg.text)}\n\nā
|
|
21283
|
-
{ parse_mode: 'HTML' },
|
|
21410
|
+
richMessage(`${escapeHtmlForTg(sourceMsg.text)}\n\nā
**${escapeHtmlForTg(choice)}**`),
|
|
21284
21411
|
)
|
|
21285
21412
|
} catch {
|
|
21286
21413
|
/* edit-failed is fine ā the answer-callback below still acks */
|
|
@@ -21658,13 +21785,13 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21658
21785
|
: ''
|
|
21659
21786
|
const editLabel = ok
|
|
21660
21787
|
? (legacyNote
|
|
21661
|
-
? `ā
|
|
21788
|
+
? `ā
**${escapeHtmlForTg(agentName)} can now ${escapeHtmlForTg(grantPhrase)}** without asking (legacy path); restart agent for full effect`
|
|
21662
21789
|
: restartScheduled
|
|
21663
|
-
? `ā
|
|
21664
|
-
: `ā
|
|
21790
|
+
? `ā
**${escapeHtmlForTg(agentName)} can now ${escapeHtmlForTg(grantPhrase)}** without asking ā applying now (restarting to take effect).`
|
|
21791
|
+
: `ā
**${escapeHtmlForTg(agentName)} can now ${escapeHtmlForTg(grantPhrase)}** without asking; restart agent for full effect`)
|
|
21665
21792
|
: (editLockHint
|
|
21666
|
-
? `ā ļø
|
|
21667
|
-
: `ā ļø
|
|
21793
|
+
? `ā ļø **Allowed for now ā "always" did NOT save.** Config edits are locked; enable \`hostd.config_edit_enabled\`.`
|
|
21794
|
+
: `ā ļø **Allowed for now ā "always" did NOT save.** It will ask again after restart. Check gateway log.`)
|
|
21668
21795
|
// #1150 audit: route through finalizeCallback so the keyboard
|
|
21669
21796
|
// strips alongside the status-line edit. The in-flight verdict was
|
|
21670
21797
|
// ALREADY dispatched above (independently of this host round-trip)
|
|
@@ -21673,7 +21800,6 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21673
21800
|
await finalizeCallback(ctx, {
|
|
21674
21801
|
ackText: ackText.slice(0, 200),
|
|
21675
21802
|
newText: baseText ? `${baseText}\n\n${editLabel}` : editLabel,
|
|
21676
|
-
parseMode: 'HTML',
|
|
21677
21803
|
})
|
|
21678
21804
|
return
|
|
21679
21805
|
}
|
|
@@ -21718,10 +21844,10 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21718
21844
|
? 'ā Denied'
|
|
21719
21845
|
: (windowGranted ? `ā
Allowed (${scopedMins} min)` : 'ā
Allowed once')
|
|
21720
21846
|
const htmlLabel = behavior === 'deny'
|
|
21721
|
-
? 'ā
|
|
21847
|
+
? 'ā **Denied**'
|
|
21722
21848
|
: (windowGranted
|
|
21723
|
-
? `ā
|
|
21724
|
-
: 'ā
|
|
21849
|
+
? `ā
**Allowed ā won't ask again about ${escapeHtmlForTg(timeBox!.breadth)} for ${scopedMins} min**`
|
|
21850
|
+
: 'ā
**Allowed once**')
|
|
21725
21851
|
// HTML-escape the source text ā same hazard as the scope-commit and
|
|
21726
21852
|
// recent-denial paths above. The permission card body
|
|
21727
21853
|
// (formatPermissionCardBody) appends claude-supplied `description`
|
|
@@ -21742,7 +21868,6 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21742
21868
|
await finalizeCallback(ctx, {
|
|
21743
21869
|
ackText: ackText.slice(0, 200),
|
|
21744
21870
|
newText: baseText ? `${baseText}\n\n${htmlLabel}` : htmlLabel,
|
|
21745
|
-
parseMode: 'HTML',
|
|
21746
21871
|
synthInbound: () => {
|
|
21747
21872
|
// No `rule` ā the bridge does NOT cache this (truly once on the bridge);
|
|
21748
21873
|
// any 30-min stickiness lives only in scopedGrants (recorded above).
|
|
@@ -23633,14 +23758,16 @@ void (async () => {
|
|
|
23633
23758
|
const orphanStatusEnabled = isOrphanSubagentStatusEnabled(process.env.SWITCHROOM_ORPHAN_SUBAGENT_STATUS)
|
|
23634
23759
|
workerActivityFeed?.stop()
|
|
23635
23760
|
workerActivityFeed = createWorkerActivityFeed({
|
|
23761
|
+
// #2669: worker-feed body is raw GFM markdown ā send via rich.
|
|
23636
23762
|
bot: {
|
|
23637
23763
|
sendMessage: async (cid, text, sendOpts) => {
|
|
23638
23764
|
const sent = await robustApiCall(
|
|
23639
23765
|
() =>
|
|
23640
|
-
|
|
23766
|
+
// allow-raw-bot-api: sendRichMessage routed through robustApiCall (not in the THREAD_NOT_FOUND blast pattern)
|
|
23767
|
+
lockedBot.api.sendRichMessage(
|
|
23641
23768
|
cid,
|
|
23642
|
-
text,
|
|
23643
|
-
sendOpts as Parameters<typeof lockedBot.api.
|
|
23769
|
+
richMessage(text),
|
|
23770
|
+
sendOpts as Parameters<typeof lockedBot.api.sendRichMessage>[2],
|
|
23644
23771
|
),
|
|
23645
23772
|
{ chat_id: cid, verb: 'worker-feed' },
|
|
23646
23773
|
)
|
|
@@ -23652,7 +23779,7 @@ void (async () => {
|
|
|
23652
23779
|
lockedBot.api.editMessageText(
|
|
23653
23780
|
cid,
|
|
23654
23781
|
mid,
|
|
23655
|
-
text,
|
|
23782
|
+
richMessage(text),
|
|
23656
23783
|
editOpts as Parameters<typeof lockedBot.api.editMessageText>[3],
|
|
23657
23784
|
),
|
|
23658
23785
|
{ chat_id: cid, verb: 'worker-feed' },
|