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
|
@@ -22587,7 +22587,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
|
|
|
22587
22587
|
import { dirname as dirname4, join as join2 } from "node:path";
|
|
22588
22588
|
|
|
22589
22589
|
// src/build-info.ts
|
|
22590
|
-
var VERSION = "0.16.
|
|
22590
|
+
var VERSION = "0.16.23";
|
|
22591
22591
|
|
|
22592
22592
|
// src/cli/resolve-version.ts
|
|
22593
22593
|
function readPackageVersion() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
|
-
"version": "0.16.
|
|
4
|
-
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw
|
|
3
|
+
"version": "0.16.24",
|
|
4
|
+
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw \u2014 no API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"switchroom": "./dist/cli/switchroom.js"
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"build": "node scripts/build.mjs",
|
|
24
24
|
"build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
|
|
25
25
|
"pretest": "npm run build",
|
|
26
|
-
"test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
|
|
26
|
+
"test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
27
27
|
"test:vitest": "vitest run",
|
|
28
|
-
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
|
|
28
|
+
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
29
29
|
"test:watch": "vitest",
|
|
30
30
|
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
|
|
31
31
|
"lint:tsc": "tsc --noEmit",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"bcryptjs": "^3.0.3",
|
|
48
48
|
"chalk": "^5.4.0",
|
|
49
49
|
"commander": "^13.1.0",
|
|
50
|
-
"grammy": "^1.
|
|
50
|
+
"grammy": "^1.44",
|
|
51
51
|
"handlebars": "^4.7.8",
|
|
52
52
|
"posthog-node": "^5.29.2",
|
|
53
53
|
"yaml": "^2.7.0",
|
|
@@ -34,7 +34,7 @@ import { isSilentFlushMarker } from './turn-flush-safety.js'
|
|
|
34
34
|
|
|
35
35
|
export const MIN_INITIAL_CHARS = 50
|
|
36
36
|
export const DEFAULT_THROTTLE_MS = 1000
|
|
37
|
-
const TELEGRAM_MAX_CHARS =
|
|
37
|
+
const TELEGRAM_MAX_CHARS = 32768
|
|
38
38
|
|
|
39
39
|
/** Called when a late sendMessage/edit resolves after a new turn has started. */
|
|
40
40
|
export type OnSupersededCallback = (params: {
|
|
@@ -55,11 +55,13 @@ export interface AnswerStreamConfig {
|
|
|
55
55
|
replyToMessageId?: number
|
|
56
56
|
|
|
57
57
|
// ── Transport callbacks ────────────────────────────────────────────────
|
|
58
|
+
// Both callbacks ship the RAW assistant transcript markdown — the gateway
|
|
59
|
+
// wrapper sends it via the rich-message path (`sendRichMessage` /
|
|
60
|
+
// `editMessageText({ markdown })`, #2669). No render step happens here.
|
|
58
61
|
sendMessage: (
|
|
59
62
|
chatId: string,
|
|
60
63
|
text: string,
|
|
61
64
|
params?: {
|
|
62
|
-
parse_mode?: 'HTML'
|
|
63
65
|
message_thread_id?: number
|
|
64
66
|
link_preview_options?: { is_disabled: boolean }
|
|
65
67
|
reply_parameters?: { message_id: number }
|
|
@@ -86,31 +88,12 @@ export interface AnswerStreamConfig {
|
|
|
86
88
|
messageId: number,
|
|
87
89
|
text: string,
|
|
88
90
|
params?: {
|
|
89
|
-
parse_mode?: 'HTML'
|
|
90
91
|
message_thread_id?: number
|
|
91
92
|
link_preview_options?: { is_disabled: boolean }
|
|
92
93
|
},
|
|
93
94
|
) => Promise<unknown>
|
|
94
95
|
deleteMessage?: (chatId: string, messageId: number) => Promise<unknown>
|
|
95
96
|
|
|
96
|
-
/**
|
|
97
|
-
* Render the raw assistant transcript text into the wire format used for
|
|
98
|
-
* `parse_mode: 'HTML'` sends/edits. Every OTHER outbound lane in the gateway
|
|
99
|
-
* (handleStreamReply, the reply handler, the turn-flush backstop, the PTY
|
|
100
|
-
* partial handler) converts markdown → Telegram HTML via
|
|
101
|
-
* `sanitizeTelegramHtml(markdownToHtml(...))` before sending. The
|
|
102
|
-
* answer-stream lane historically shipped the RAW transcript under
|
|
103
|
-
* `parse_mode: 'HTML'`, so `**bold**` reached the user as literal asterisks
|
|
104
|
-
* and agent narration read as unformatted text.
|
|
105
|
-
*
|
|
106
|
-
* Injected as a dependency (mirroring `renderText` on the PTY partial
|
|
107
|
-
* handler) rather than hard-importing `format`/`html-sanitize` here, so this
|
|
108
|
-
* module stays free of a grammy/format dependency and remains fully
|
|
109
|
-
* testable. When absent, text is sent verbatim — preserving the old
|
|
110
|
-
* behaviour for callers (and tests) that don't wire it.
|
|
111
|
-
*/
|
|
112
|
-
renderText?: (text: string) => string
|
|
113
|
-
|
|
114
97
|
/** Called when a late edit/send resolves but this stream has been superseded. */
|
|
115
98
|
onSuperseded?: OnSupersededCallback
|
|
116
99
|
log?: (msg: string) => void
|
|
@@ -197,7 +180,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
197
180
|
replyToMessageId,
|
|
198
181
|
sendMessage,
|
|
199
182
|
editMessageText,
|
|
200
|
-
renderText,
|
|
201
183
|
onSuperseded,
|
|
202
184
|
log,
|
|
203
185
|
warn,
|
|
@@ -207,13 +189,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
207
189
|
recordOutbound,
|
|
208
190
|
} = config
|
|
209
191
|
|
|
210
|
-
/**
|
|
211
|
-
* Convert raw transcript text to the wire format before any
|
|
212
|
-
* `parse_mode: 'HTML'` send/edit. Falls back to the verbatim text when no
|
|
213
|
-
* renderer is injected (old behaviour / unwired tests).
|
|
214
|
-
*/
|
|
215
|
-
const render = (text: string): string => (renderText != null ? renderText(text) : text)
|
|
216
|
-
|
|
217
192
|
const effectiveThrottle = Math.max(250, throttleMs)
|
|
218
193
|
|
|
219
194
|
// Stream state
|
|
@@ -255,19 +230,17 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
255
230
|
}
|
|
256
231
|
|
|
257
232
|
async function sendOrEditViaMessage(trimmed: string, gen: number, prevText: string): Promise<void> {
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
const rendered = render(trimmed)
|
|
233
|
+
// Ship the RAW transcript markdown — the gateway wrapper sends it via the
|
|
234
|
+
// rich-message path (#2669), so `**bold**` renders correctly without any
|
|
235
|
+
// render step here.
|
|
262
236
|
if (typeof streamMsgId === 'number') {
|
|
263
237
|
// Edit existing message
|
|
264
238
|
const editParams: Parameters<typeof editMessageText>[3] = {
|
|
265
|
-
parse_mode: 'HTML',
|
|
266
239
|
link_preview_options: { is_disabled: true },
|
|
267
240
|
}
|
|
268
241
|
if (threadId != null) editParams.message_thread_id = threadId
|
|
269
242
|
try {
|
|
270
|
-
await editMessageText(chatId, streamMsgId,
|
|
243
|
+
await editMessageText(chatId, streamMsgId, trimmed, editParams)
|
|
271
244
|
onMetric?.({ kind: 'answer_lane_update', chatId, messageId: streamMsgId, charCount: trimmed.length, transport: 'edit' })
|
|
272
245
|
} catch (err) {
|
|
273
246
|
const msg = err instanceof Error ? err.message : String(err)
|
|
@@ -290,13 +263,12 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
290
263
|
// preview opener — silent (no device ping). The turn-end materialize
|
|
291
264
|
// path uses 'materialize' to allow the platform-default ping.
|
|
292
265
|
const sendParams: Parameters<typeof sendMessage>[2] = {
|
|
293
|
-
parse_mode: 'HTML',
|
|
294
266
|
link_preview_options: { is_disabled: true },
|
|
295
267
|
purpose: 'stream',
|
|
296
268
|
}
|
|
297
269
|
if (threadId != null) sendParams.message_thread_id = threadId
|
|
298
270
|
if (replyToMessageId != null) sendParams.reply_parameters = { message_id: replyToMessageId }
|
|
299
|
-
const sent = await sendMessage(chatId,
|
|
271
|
+
const sent = await sendMessage(chatId, trimmed, sendParams)
|
|
300
272
|
const sentId = sent?.message_id
|
|
301
273
|
if (typeof sentId !== 'number' || !Number.isFinite(sentId)) {
|
|
302
274
|
warn?.('answer-stream: sendMessage returned no message_id')
|
|
@@ -444,7 +416,6 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
444
416
|
// answer surface — beat 5 of the conversational-pacing contract
|
|
445
417
|
// — and MUST ping so the user knows the answer landed.
|
|
446
418
|
const sendParams: Parameters<typeof sendMessage>[2] = {
|
|
447
|
-
parse_mode: 'HTML',
|
|
448
419
|
link_preview_options: { is_disabled: true },
|
|
449
420
|
purpose: 'materialize',
|
|
450
421
|
}
|
|
@@ -454,11 +425,10 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
|
|
|
454
425
|
// nested quote that looks wrong.
|
|
455
426
|
|
|
456
427
|
try {
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
const sent = await sendMessage(chatId, render(textToSend), sendParams)
|
|
428
|
+
// Ship the raw transcript markdown — the gateway wrapper sends it via
|
|
429
|
+
// the rich-message path (#2669). The dedup / silent-marker / history
|
|
430
|
+
// checks above all run on the same raw textToSend.
|
|
431
|
+
const sent = await sendMessage(chatId, textToSend, sendParams)
|
|
462
432
|
const sentId = sent?.message_id
|
|
463
433
|
if (typeof sentId === 'number' && Number.isFinite(sentId)) {
|
|
464
434
|
streamMsgId = sentId
|
|
@@ -243,7 +243,7 @@ function displayLabel(label: string, opts: SnapshotRenderOpts): string {
|
|
|
243
243
|
function groupHeader(health: AccountHealth, count: number): string {
|
|
244
244
|
const emoji = HEALTH_EMOJI[health];
|
|
245
245
|
const title = HEALTH_TITLE[health];
|
|
246
|
-
return `${emoji}
|
|
246
|
+
return `${emoji} **${title}** (${count})`;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
const HEALTH_EMOJI: Record<AccountHealth, string> = {
|
|
@@ -284,10 +284,10 @@ function renderAccountRow(
|
|
|
284
284
|
|
|
285
285
|
if (!snap.quota) {
|
|
286
286
|
lines.push(
|
|
287
|
-
`${marker}
|
|
287
|
+
`${marker}\`${escapeHtml(label)}\` _quota probe failed_`,
|
|
288
288
|
);
|
|
289
289
|
if (snap.quotaError) {
|
|
290
|
-
lines.push(`
|
|
290
|
+
lines.push(` _${escapeHtml(snap.quotaError)}_`);
|
|
291
291
|
}
|
|
292
292
|
return lines;
|
|
293
293
|
}
|
|
@@ -297,7 +297,7 @@ function renderAccountRow(
|
|
|
297
297
|
// it as a data-quality gap, never a confident "0% / 0%".
|
|
298
298
|
if (isProbeThin(q)) {
|
|
299
299
|
lines.push(
|
|
300
|
-
`${marker}
|
|
300
|
+
`${marker}\`${escapeHtml(label)}\` _quota unknown (thin probe)_`,
|
|
301
301
|
);
|
|
302
302
|
return lines;
|
|
303
303
|
}
|
|
@@ -307,7 +307,7 @@ function renderAccountRow(
|
|
|
307
307
|
const fiveStr = fmtPct(norm.fiveHourUtilizationPct);
|
|
308
308
|
const sevenStr = fmtPct(norm.sevenDayUtilizationPct);
|
|
309
309
|
lines.push(
|
|
310
|
-
`${marker}
|
|
310
|
+
`${marker}\`${escapeHtml(label)}\` ${fiveStr} / ${sevenStr}`,
|
|
311
311
|
);
|
|
312
312
|
|
|
313
313
|
const health = classifyHealth(snap, now);
|
|
@@ -318,7 +318,7 @@ function renderAccountRow(
|
|
|
318
318
|
const reset = win === '5h' ? q.fiveHourResetAt : q.sevenDayResetAt;
|
|
319
319
|
const winLabel = win === '5h' ? '5-hour' : '7-day';
|
|
320
320
|
lines.push(
|
|
321
|
-
`
|
|
321
|
+
` _quota exhausted — back ${formatAbsolute(reset, tz)} (in ${formatRelative(reset, now)}, ${winLabel} cap)_`,
|
|
322
322
|
);
|
|
323
323
|
return lines;
|
|
324
324
|
}
|
|
@@ -337,13 +337,13 @@ function renderAccountRow(
|
|
|
337
337
|
const sevenSeg = q.sevenDayResetAt
|
|
338
338
|
? `7d resets ${formatAbsolute(q.sevenDayResetAt, tz)} (in ${formatRelative(q.sevenDayResetAt, now)})`
|
|
339
339
|
: '7d resets —';
|
|
340
|
-
lines.push(`
|
|
341
|
-
lines.push(`
|
|
340
|
+
lines.push(` _${fiveFirst ? fiveSeg : sevenSeg}_`);
|
|
341
|
+
lines.push(` _${fiveFirst ? sevenSeg : fiveSeg}_`);
|
|
342
342
|
// Informational overage annotation: if out_of_credits (no overage headroom),
|
|
343
343
|
// surface it as a sub-line on a healthy/throttling row — NOT a blocked badge.
|
|
344
344
|
if (q.overageDisabledReason != null && OVERAGE_EXHAUSTED_REASONS.has(q.overageDisabledReason)) {
|
|
345
345
|
lines.push(
|
|
346
|
-
`
|
|
346
|
+
` _overage off (${escapeHtml(q.overageDisabledReason)}) — serving from quota_`,
|
|
347
347
|
);
|
|
348
348
|
}
|
|
349
349
|
return lines;
|
|
@@ -380,7 +380,7 @@ export function renderAuthSnapshotFormat2(
|
|
|
380
380
|
): string {
|
|
381
381
|
const now = opts.now ?? new Date();
|
|
382
382
|
const lines: string[] = [];
|
|
383
|
-
lines.push('🔋
|
|
383
|
+
lines.push('🔋 **Auth — fleet status**');
|
|
384
384
|
|
|
385
385
|
// Group by health. Render BLOCKED first (it's the urgent action),
|
|
386
386
|
// then THROTTLING (potential next problem), then HEALTHY (good
|
|
@@ -410,16 +410,16 @@ export function renderAuthSnapshotFormat2(
|
|
|
410
410
|
|
|
411
411
|
lines.push('');
|
|
412
412
|
lines.push('────────────────────────────');
|
|
413
|
-
lines.push(
|
|
413
|
+
lines.push(`_${recommendation(snapshots, now, opts.demo ?? false)}_`);
|
|
414
414
|
// #2495 Change 2 — a failed probe-on-open renders an explicit "cached Nm
|
|
415
415
|
// ago" warning, never a false live stamp. The degraded variant takes
|
|
416
416
|
// precedence over the live stamp.
|
|
417
417
|
if (opts.staleCachedAtMs != null) {
|
|
418
|
-
lines.push(
|
|
418
|
+
lines.push(`_⚠ cached ${formatAgeStamp(opts.staleCachedAtMs, now)}_`);
|
|
419
419
|
} else if (opts.liveProbedAtMs != null) {
|
|
420
|
-
lines.push(
|
|
420
|
+
lines.push(`_Live · refreshed ${formatAgeStamp(opts.liveProbedAtMs, now)}_`);
|
|
421
421
|
} else {
|
|
422
|
-
lines.push('
|
|
422
|
+
lines.push('_Live_');
|
|
423
423
|
}
|
|
424
424
|
return lines.join('\n');
|
|
425
425
|
}
|
|
@@ -615,10 +615,10 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
615
615
|
// same per-account row + earliest-recovery helpers the /auth table uses so
|
|
616
616
|
// the formatting stays consistent with the rest of the auth surface.
|
|
617
617
|
lines.push(
|
|
618
|
-
`🔴
|
|
618
|
+
`🔴 **All accounts blocked · ${headerLimit} on ${escapeHtml(input.oldLabel)}**`,
|
|
619
619
|
);
|
|
620
620
|
lines.push('');
|
|
621
|
-
lines.push(`Triggered by: agent
|
|
621
|
+
lines.push(`Triggered by: agent **${escapeHtml(input.triggerAgent)}**`);
|
|
622
622
|
|
|
623
623
|
const fleet = input.fleetSnapshots ?? [];
|
|
624
624
|
if (fleet.length > 0) {
|
|
@@ -639,7 +639,7 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
639
639
|
if (earliest) {
|
|
640
640
|
lines.push('');
|
|
641
641
|
lines.push(
|
|
642
|
-
`Earliest recovery:
|
|
642
|
+
`Earliest recovery: \`${escapeHtml(earliest.label)}\` ` +
|
|
643
643
|
`${formatAbsolute(earliest.at, tz)} (in ${formatRelative(earliest.at, now)})`,
|
|
644
644
|
);
|
|
645
645
|
}
|
|
@@ -655,28 +655,28 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
655
655
|
}
|
|
656
656
|
lines.push('');
|
|
657
657
|
lines.push(
|
|
658
|
-
`Run
|
|
659
|
-
`or
|
|
658
|
+
`Run \`/auth add <label>\` to attach another subscription, ` +
|
|
659
|
+
`or \`/auth refresh\` to re-probe.`,
|
|
660
660
|
);
|
|
661
661
|
return lines.join('\n');
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
// Successful swap.
|
|
665
665
|
lines.push(
|
|
666
|
-
`✓
|
|
666
|
+
`✓ **Switched fleet · ${headerLimit} on ${escapeHtml(input.oldLabel)}**`,
|
|
667
667
|
);
|
|
668
668
|
lines.push('');
|
|
669
669
|
lines.push(
|
|
670
|
-
|
|
670
|
+
`\`${escapeHtml(input.oldLabel)}\` → \`${escapeHtml(input.newLabel)}\``,
|
|
671
671
|
);
|
|
672
|
-
lines.push(`Triggered by: agent
|
|
672
|
+
lines.push(`Triggered by: agent **${escapeHtml(input.triggerAgent)}**`);
|
|
673
673
|
lines.push('');
|
|
674
674
|
|
|
675
675
|
if (input.oldQuota) {
|
|
676
676
|
const recovery = recoveryAtFor(input.oldQuota);
|
|
677
677
|
if (recovery) {
|
|
678
678
|
lines.push(
|
|
679
|
-
|
|
679
|
+
`\`${escapeHtml(input.oldLabel)}\` recovers ` +
|
|
680
680
|
`${formatAbsolute(recovery, tz)} (in ${formatRelative(recovery, now)})`,
|
|
681
681
|
);
|
|
682
682
|
}
|
|
@@ -688,13 +688,13 @@ export function renderFallbackAnnouncement(input: FallbackAnnouncementInput): st
|
|
|
688
688
|
const hasHeadroom =
|
|
689
689
|
input.newQuota.fiveHourUtilizationPct < THROTTLING_THRESHOLD_PCT &&
|
|
690
690
|
input.newQuota.sevenDayUtilizationPct < THROTTLING_THRESHOLD_PCT;
|
|
691
|
-
const headroomStr = hasHeadroom ? '
|
|
691
|
+
const headroomStr = hasHeadroom ? '_(plenty of headroom)_' : '_(near limit — watch this)_';
|
|
692
692
|
lines.push(
|
|
693
|
-
|
|
693
|
+
`\`${escapeHtml(input.newLabel)}\` now: ${fiveStr} of 5h · ${sevenStr} of 7d ${headroomStr}`,
|
|
694
694
|
);
|
|
695
695
|
} else {
|
|
696
696
|
lines.push(
|
|
697
|
-
|
|
697
|
+
`_(quota probe for new account is pending — will reflect on next /auth)_`,
|
|
698
698
|
);
|
|
699
699
|
}
|
|
700
700
|
|
|
@@ -808,10 +808,7 @@ function switchPriority(s: AccountSnapshot, now: Date = new Date()): number {
|
|
|
808
808
|
// ── shared HTML escape ───────────────────────────────────────────────
|
|
809
809
|
|
|
810
810
|
function escapeHtml(s: string): string {
|
|
811
|
-
return s
|
|
812
|
-
.replace(/&/g, '&')
|
|
813
|
-
.replace(/</g, '<')
|
|
814
|
-
.replace(/>/g, '>');
|
|
811
|
+
return s.replace(/([\\`*_~=\[\]|])/g, '\\$1');
|
|
815
812
|
}
|
|
816
813
|
|
|
817
814
|
// ── snapshot assembly helper ─────────────────────────────────────────
|
|
@@ -54,19 +54,14 @@ import {
|
|
|
54
54
|
*/
|
|
55
55
|
export function renderFallbackFailureNotice(triggerAgent: string, reason: string): string {
|
|
56
56
|
return (
|
|
57
|
-
`⚠️
|
|
57
|
+
`⚠️ **Auto-failover could not run** (trigger: **${escFailureHtml(triggerAgent)}**)\n` +
|
|
58
58
|
`${escFailureHtml(reason)}\n\n` +
|
|
59
|
-
|
|
59
|
+
`_Switch manually with \`/auth use <label>\`, or \`/auth\` for fleet status._`
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function escFailureHtml(s: string): string {
|
|
64
|
-
return s
|
|
65
|
-
.replace(/&/g, '&')
|
|
66
|
-
.replace(/</g, '<')
|
|
67
|
-
.replace(/>/g, '>')
|
|
68
|
-
.replace(/"/g, '"')
|
|
69
|
-
.replace(/'/g, ''');
|
|
64
|
+
return s.replace(/([\\`*_~=\[\]|])/g, '\\$1');
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
/**
|
|
@@ -214,7 +209,7 @@ export async function runFleetAutoFallback(
|
|
|
214
209
|
if (!oldSnap) {
|
|
215
210
|
return {
|
|
216
211
|
kind: 'no-old-active',
|
|
217
|
-
announcement: '
|
|
212
|
+
announcement: '_Auto-fallback skipped: no active account in broker state._',
|
|
218
213
|
};
|
|
219
214
|
}
|
|
220
215
|
|
|
@@ -232,8 +227,8 @@ export async function runFleetAutoFallback(
|
|
|
232
227
|
oldLabel: oldSnap.label,
|
|
233
228
|
oldQuota: oldSnap.quota,
|
|
234
229
|
announcement:
|
|
235
|
-
|
|
236
|
-
`(${pctSummary(oldSnap.quota)}). Stale event
|
|
230
|
+
`_Auto-fallback skipped: ${oldSnap.label} probed healthy ` +
|
|
231
|
+
`(${pctSummary(oldSnap.quota)}). Stale event?_`,
|
|
237
232
|
};
|
|
238
233
|
}
|
|
239
234
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared formatters for Telegram status cards.
|
|
3
3
|
*
|
|
4
|
+
* Since the Bot API 10.1 rich-message migration (#2669) the cards emit raw
|
|
5
|
+
* GFM markdown (sent via `sendRichMessage` / `editMessageText({ markdown })`),
|
|
6
|
+
* not HTML. `escapeMarkdown` (re-exported from `format.ts`) is the
|
|
7
|
+
* dynamic-value escaper cards use where they used to call `escapeHtml`.
|
|
8
|
+
*
|
|
4
9
|
* Both the main progress card (rendered via `stream-reply-handler.ts`)
|
|
5
|
-
* and the pinned worker card (`subagent-watcher.ts`) emit
|
|
6
|
-
*
|
|
10
|
+
* and the pinned worker card (`subagent-watcher.ts`) emit to Telegram;
|
|
11
|
+
* before issue #94 each had its own private copies with subtly
|
|
7
12
|
* different conventions:
|
|
8
13
|
*
|
|
9
14
|
* - `formatDuration(500)` → progress-card returned `500ms`, watcher
|
|
@@ -16,14 +21,24 @@
|
|
|
16
21
|
* in both modules. Centralising removes one more piece of drift the
|
|
17
22
|
* reviewer has to verify.
|
|
18
23
|
*
|
|
19
|
-
* Keep this module dependency-free. It's imported by every
|
|
20
|
-
* surface and must not pull in plugin or gateway state.
|
|
24
|
+
* Keep this module nearly dependency-free. It's imported by every
|
|
25
|
+
* card-render surface and must not pull in plugin or gateway state. The
|
|
26
|
+
* single exception is the `escapeMarkdown` re-export from `format.ts`,
|
|
27
|
+
* which is itself state-free.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { escapeMarkdown } from './format.js'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Re-export so card surfaces have one import for the markdown escaper they
|
|
34
|
+
* use when interpolating dynamic values into a hand-built markdown card.
|
|
21
35
|
*/
|
|
36
|
+
export { escapeMarkdown }
|
|
22
37
|
|
|
23
38
|
/**
|
|
24
39
|
* Render a millisecond duration as `<n>ms` for sub-second values, or
|
|
25
|
-
* `MM:SS` thereafter. Output
|
|
26
|
-
*
|
|
40
|
+
* `MM:SS` thereafter. Output contains no markdown-special characters, so
|
|
41
|
+
* callers can interpolate it into a markdown card without escaping.
|
|
27
42
|
*
|
|
28
43
|
* formatDuration(0) → "0ms"
|
|
29
44
|
* formatDuration(500) → "500ms"
|
|
@@ -45,17 +60,6 @@ export function formatDuration(ms: number): string {
|
|
|
45
60
|
return `${m.toString().padStart(2, '0')}:${r.toString().padStart(2, '0')}`;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
/**
|
|
49
|
-
* Escape `&`, `<`, `>` for safe interpolation into Telegram HTML
|
|
50
|
-
* messages. Telegram's HTML parser is strict — an unescaped `<` is
|
|
51
|
-
* read as a tag opener and rejects the message with
|
|
52
|
-
* "can't parse entities" (see #101 for the cascade that crashed the
|
|
53
|
-
* gateway into a restart loop).
|
|
54
|
-
*/
|
|
55
|
-
export function escapeHtml(s: string): string {
|
|
56
|
-
return s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' })[c]!);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
63
|
/** Truncate to at most `n` characters, replacing the last char with `…`. */
|
|
60
64
|
export function truncate(s: string, n: number): string {
|
|
61
65
|
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
@@ -65,13 +69,12 @@ export function truncate(s: string, n: number): string {
|
|
|
65
69
|
* Strip Markdown markup from a single line, leaving plain prose.
|
|
66
70
|
*
|
|
67
71
|
* Worker narration is authored as Markdown — the model writes `**bold**`,
|
|
68
|
-
* `` `code` ``, `- bullets`, `# headings`. The status cards
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
+
* `` `code` ``, `- bullets`, `# headings`. The status cards have a fixed
|
|
73
|
+
* structure (steps, durations) and interleave narration as plain prose, so
|
|
74
|
+
* we strip the author's inline markup here so the narration reads as clean
|
|
75
|
+
* prose and never collides with the card's own markdown structure.
|
|
72
76
|
*
|
|
73
|
-
* Run this BEFORE `truncate` + `
|
|
74
|
-
* stripper never touches `<`/`>`/`&`, so escaping stays the last step.)
|
|
77
|
+
* Run this BEFORE `truncate` + `escapeMarkdown`: clean → measure → escape.
|
|
75
78
|
*/
|
|
76
79
|
export function stripMarkdown(s: string): string {
|
|
77
80
|
let out = s;
|
|
@@ -107,8 +110,8 @@ function isRuleLine(s: string): boolean {
|
|
|
107
110
|
* Clean a worker's multi-line result/narration into a single plain-text
|
|
108
111
|
* paragraph for a card's finished body. Drops fenced code blocks and
|
|
109
112
|
* horizontal rules, strips per-line Markdown, then space-joins what's left.
|
|
110
|
-
* Output is plain text — the caller still truncates + escapes
|
|
111
|
-
* interpolating into
|
|
113
|
+
* Output is plain text — the caller still truncates + escapes (escapeMarkdown)
|
|
114
|
+
* before interpolating into the card's markdown body.
|
|
112
115
|
*/
|
|
113
116
|
export function cleanWorkerResultParagraph(s: string): string {
|
|
114
117
|
const kept: string[] = [];
|
|
@@ -157,7 +157,7 @@ export function evaluateCreditState(args: {
|
|
|
157
157
|
if (!currentIsFatal && prevIsFatal) {
|
|
158
158
|
return {
|
|
159
159
|
kind: "notify",
|
|
160
|
-
message: `✅
|
|
160
|
+
message: `✅ **${escapeHtml(agentName)}**: credits restored — agent should resume normal operation.`,
|
|
161
161
|
newState: { lastNotifiedReason: null, lastNotifiedAt: now },
|
|
162
162
|
transition: "exited",
|
|
163
163
|
};
|
|
@@ -193,12 +193,12 @@ export function evaluateCreditState(args: {
|
|
|
193
193
|
function buildEntryMessage(agentName: string, reason: string): string {
|
|
194
194
|
const desc = humanizeReason(reason);
|
|
195
195
|
return [
|
|
196
|
-
`⚠️
|
|
196
|
+
`⚠️ **${escapeHtml(agentName)}**: ${desc}`,
|
|
197
197
|
``,
|
|
198
198
|
`Cron tasks and inbound replies will fail until this is resolved. Check`,
|
|
199
|
-
`your subscription or pre-paid usage at
|
|
199
|
+
`your subscription or pre-paid usage at [console.anthropic.com](https://console.anthropic.com).`,
|
|
200
200
|
``,
|
|
201
|
-
|
|
201
|
+
`_Source: Claude CLI cache (cachedExtraUsageDisabledReason=${escapeHtml(reason)})_`,
|
|
202
202
|
].join("\n");
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -218,12 +218,7 @@ function humanizeReason(reason: string): string {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
function escapeHtml(s: string): string {
|
|
221
|
-
return s
|
|
222
|
-
.replace(/&/g, "&")
|
|
223
|
-
.replace(/</g, "<")
|
|
224
|
-
.replace(/>/g, ">")
|
|
225
|
-
.replace(/"/g, """)
|
|
226
|
-
.replace(/'/g, "'");
|
|
221
|
+
return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
227
222
|
}
|
|
228
223
|
|
|
229
224
|
// ─── State persistence ───────────────────────────────────────────────────────
|