diff --git a/CHANGELOG.md b/CHANGELOG.md index b12d6edf5c5..ed5665c10d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Control UI/Sessions: avoid full `sessions.list` reloads for chat-turn `sessions.changed` payloads, so large session stores no longer add multi-second delays while chat responses are being delivered. (#76676) Thanks @VACInc. +- Doctor/Telegram: warn when selected Telegram quote replies can suppress `streaming.preview.toolProgress`, and document the `replyToMode` trade-off without changing runtime delivery. Fixes #73487. Thanks @GodsBoy. - Discord/status: honor explicit `messages.statusReactions.enabled: true` in tool-only guild channels so queued ack reactions can progress through thinking/done lifecycle reactions instead of stopping at the initial emoji. Thanks @Marvinthebored. - Discord/native commands: compare Discord-normalized slash-command descriptions and localized descriptions during reconcile so CJK or multiline command text no longer triggers redundant startup PATCH bursts and rate-limit 429s. Fixes #76587. Thanks @zhengsx. - Agents/OpenAI: omit Chat Completions `reasoning_effort` for `gpt-5.4-mini` only when function tools are present while preserving tool-free Chat and Responses reasoning support, preventing Telegram-routed fallback runs from hanging after OpenAI rejects tool payloads. Fixes #76176. Thanks @ThisIsAdilah and @chinar-amrutkar. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 22852710b6d..b038818edc5 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -302,7 +302,7 @@ curl "https://api.telegram.org/bot/getUpdates" Use `streaming.mode: "off"` only when you want final-only delivery: Telegram preview edits are disabled and generic tool/progress chatter is suppressed instead of being sent as standalone "Working..." messages. Approval prompts, media payloads, and errors still route through normal final delivery. Use `streaming.preview.toolProgress: false` when you only want to keep answer preview edits while hiding the tool-progress status lines. - `streaming.preview.toolProgress` requires `channels.telegram.replyToMode: "off"`. When quote-reply is enabled (`replyToMode: "first"`, `"all"`, or `"batched"`), Telegram requires the final message reference at send time, which is incompatible with preview-edit streaming. The two features are mutually exclusive: tool-progress lines cannot appear in the same preview message that will later be replaced by a quoted final reply. To restore tool-progress visibility, set `replyToMode: "off"`. To suppress the warning while keeping quote-reply, set `streaming.preview.toolProgress: false` to acknowledge the trade-off. + Telegram selected quote replies are the exception. When `replyToMode` is `"first"`, `"all"`, or `"batched"` and the inbound message includes selected quote text, OpenClaw sends the final answer through Telegram's native quote-reply path instead of editing the answer preview, so `streaming.preview.toolProgress` cannot show the short "Working..." lines for that turn. Current-message replies without selected quote text still keep preview streaming. Set `replyToMode: "off"` when tool-progress visibility matters more than native quote replies, or set `streaming.preview.toolProgress: false` to acknowledge the trade-off. For text-only replies: diff --git a/docs/concepts/streaming.md b/docs/concepts/streaming.md index d56a625de0c..e98bbb1ee04 100644 --- a/docs/concepts/streaming.md +++ b/docs/concepts/streaming.md @@ -194,7 +194,7 @@ Supported surfaces: - **Mattermost** already folds tool activity into its single draft preview post (see above). - Tool-progress edits follow the active preview streaming mode; they are skipped when preview streaming is `off` or when block streaming has taken over the message. On Telegram, `streaming.mode: "off"` is final-only: generic progress chatter is also suppressed instead of being delivered as standalone "Working..." messages, while approval prompts, media payloads, and errors still route normally. - To keep preview streaming but hide tool-progress lines, set `streaming.preview.toolProgress` to `false` for that channel. To disable preview edits entirely, set `streaming.mode` to `off`. -- On Telegram specifically, `streaming.preview.toolProgress` requires `channels.telegram.replyToMode: "off"`. Quote-reply needs the final message reference at send time, which is incompatible with preview-edit streaming, so the two are mutually exclusive. See [Telegram channel docs](/channels/telegram) for the full note. +- Telegram selected quote replies are an exception: when `replyToMode` is not `"off"` and selected quote text is present, OpenClaw skips the answer preview stream for that turn so tool-progress preview lines cannot render. Current-message replies without selected quote text still keep preview streaming. See [Telegram channel docs](/channels/telegram) for details. Example: diff --git a/extensions/telegram/src/doctor.test.ts b/extensions/telegram/src/doctor.test.ts index 6fffb881950..1afa5dc2ff4 100644 --- a/extensions/telegram/src/doctor.test.ts +++ b/extensions/telegram/src/doctor.test.ts @@ -6,10 +6,12 @@ import { collectTelegramEmptyAllowlistExtraWarnings, collectTelegramGroupPolicyWarnings, collectTelegramMissingEnvTokenWarnings, + collectTelegramSelectedQuoteToolProgressWarnings, maybeRepairTelegramApiRoots, maybeRepairTelegramAllowFromUsernames, scanTelegramBotEndpointApiRoots, scanTelegramInvalidAllowFromEntries, + scanTelegramSelectedQuoteToolProgressWarnings, telegramDoctor, } from "./doctor.js"; @@ -329,6 +331,112 @@ describe("telegram doctor", () => { ]); }); + it("warns when selected quote replies can suppress Telegram tool-progress preview", async () => { + const cfg = { + channels: { + telegram: { + replyToMode: "first", + }, + }, + } as unknown as OpenClawConfig; + + const hits = scanTelegramSelectedQuoteToolProgressWarnings(cfg); + expect(hits).toEqual([{ path: "channels.telegram", replyToMode: "first" }]); + + const warnings = collectTelegramSelectedQuoteToolProgressWarnings({ hits }); + expect(warnings[0]).toContain("selected quote replies"); + expect(warnings[0]).toContain('"Working..." tool-progress preview'); + expect(warnings[0]).toContain("Current-message replies without selected quote text"); + expect(warnings[1]).toContain("streaming.preview.toolProgress: false"); + expect( + await telegramDoctor.collectPreviewWarnings?.({ + cfg, + doctorFixCommand: "openclaw doctor --fix", + }), + ).toEqual(expect.arrayContaining([expect.stringContaining("selected quote replies")])); + }); + + it("warns for the implicit default Telegram account when accounts is empty", () => { + const cfg = { + channels: { + telegram: { + replyToMode: "all", + accounts: {}, + }, + }, + } as unknown as OpenClawConfig; + + expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([ + { path: "channels.telegram", replyToMode: "all" }, + ]); + }); + + it("uses merged Telegram account config for selected quote tool-progress warnings", () => { + listTelegramAccountIdsMock.mockReturnValue(["work", "quiet"]); + const cfg = { + channels: { + telegram: { + replyToMode: "batched", + accounts: { + work: {}, + quiet: { + replyToMode: "off", + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([ + { path: "channels.telegram.accounts.work", replyToMode: "batched" }, + ]); + }); + + it("skips selected quote tool-progress warning when preview progress is disabled", () => { + const cfg = { + channels: { + telegram: { + replyToMode: "first", + streaming: { + preview: { + toolProgress: false, + }, + }, + }, + }, + } as unknown as OpenClawConfig; + + expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([]); + }); + + it("skips selected quote tool-progress warning when preview streaming is off or block streaming owns delivery", () => { + expect( + scanTelegramSelectedQuoteToolProgressWarnings({ + channels: { + telegram: { + replyToMode: "first", + streaming: false, + }, + }, + } as unknown as OpenClawConfig), + ).toEqual([]); + + expect( + scanTelegramSelectedQuoteToolProgressWarnings({ + channels: { + telegram: { + replyToMode: "first", + }, + }, + agents: { + defaults: { + blockStreamingDefault: "on", + }, + }, + } as unknown as OpenClawConfig), + ).toEqual([]); + }); + it("wires apiRoot preview warnings and repair through the doctor adapter", async () => { const cfg = { channels: { diff --git a/extensions/telegram/src/doctor.ts b/extensions/telegram/src/doctor.ts index a197450df2f..46c9e75c2c6 100644 --- a/extensions/telegram/src/doctor.ts +++ b/extensions/telegram/src/doctor.ts @@ -2,12 +2,17 @@ import { type ChannelDoctorAdapter, type ChannelDoctorEmptyAllowlistAccountContext, } from "openclaw/plugin-sdk/channel-contract"; +import { + resolveChannelStreamingBlockEnabled, + resolveChannelStreamingPreviewToolProgress, +} from "openclaw/plugin-sdk/channel-streaming"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { inspectTelegramAccount } from "./account-inspect.js"; import { listTelegramAccountIds, + mergeTelegramAccountConfig, resolveDefaultTelegramAccountId, resolveTelegramAccount, } from "./accounts.js"; @@ -18,8 +23,10 @@ import { legacyConfigRules as TELEGRAM_LEGACY_CONFIG_RULES, normalizeCompatibilityConfig as normalizeTelegramCompatibilityConfig, } from "./doctor-contract.js"; +import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js"; type TelegramAllowFromInvalidHit = { path: string; entry: string }; +type TelegramSelectedQuoteToolProgressHit = { path: string; replyToMode: string }; type TelegramApiRootBotEndpointHit = { path: string; pathSegments: string[]; @@ -196,6 +203,58 @@ export function collectTelegramApiRootWarnings(params: { ]; } +function formatTelegramAccountConfigPath(cfg: OpenClawConfig, accountId: string): string { + const telegram = asObjectRecord((cfg.channels as Record | undefined)?.telegram); + const accounts = asObjectRecord(telegram?.accounts); + if (!accounts || Object.keys(accounts).length === 0) { + return "channels.telegram"; + } + return accountId === "default" ? "channels.telegram" : `channels.telegram.accounts.${accountId}`; +} + +export function scanTelegramSelectedQuoteToolProgressWarnings( + cfg: OpenClawConfig, +): TelegramSelectedQuoteToolProgressHit[] { + if (!asObjectRecord((cfg.channels as Record | undefined)?.telegram)) { + return []; + } + return listTelegramAccountIds(cfg).flatMap((accountId) => { + const account = mergeTelegramAccountConfig(cfg, accountId); + const replyToMode = account.replyToMode ?? "off"; + if (replyToMode === "off") { + return []; + } + if (resolveTelegramPreviewStreamMode(account) === "off") { + return []; + } + const blockStreamingEnabled = + resolveChannelStreamingBlockEnabled(account) ?? + cfg.agents?.defaults?.blockStreamingDefault === "on"; + if (blockStreamingEnabled || !resolveChannelStreamingPreviewToolProgress(account)) { + return []; + } + return [ + { + path: formatTelegramAccountConfigPath(cfg, accountId), + replyToMode, + }, + ]; + }); +} + +export function collectTelegramSelectedQuoteToolProgressWarnings(params: { + hits: TelegramSelectedQuoteToolProgressHit[]; +}): string[] { + if (params.hits.length === 0) { + return []; + } + const sample = params.hits[0] ?? { path: "channels.telegram", replyToMode: "first" }; + return [ + `- ${sanitizeForLog(sample.path)} has replyToMode: "${sanitizeForLog(sample.replyToMode)}" while Telegram preview tool-progress is enabled. Telegram selected quote replies must send the final answer through the native quote-reply path, so those turns skip the short "Working..." tool-progress preview. Current-message replies without selected quote text still keep preview streaming.`, + '- Set replyToMode: "off" when tool-progress preview matters more than native quote replies, or set streaming.preview.toolProgress: false to keep quote replies and silence this warning.', + ]; +} + export function maybeRepairTelegramApiRoots(cfg: OpenClawConfig): { config: OpenClawConfig; changes: string[]; @@ -506,6 +565,9 @@ export const telegramDoctor: ChannelDoctorAdapter = { hits: scanTelegramBotEndpointApiRoots(cfg), doctorFixCommand, }), + ...collectTelegramSelectedQuoteToolProgressWarnings({ + hits: scanTelegramSelectedQuoteToolProgressWarnings(cfg), + }), ], repairConfig: async ({ cfg }) => await repairTelegramConfig({ cfg }), collectEmptyAllowlistExtraWarnings: collectTelegramEmptyAllowlistExtraWarnings,