fix(telegram): warn on selected quote tool progress

This commit is contained in:
Peter Steinberger
2026-05-03 16:13:38 +01:00
parent b336efdd9c
commit e2c8db2cad
5 changed files with 173 additions and 2 deletions

View File

@@ -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.

View File

@@ -302,7 +302,7 @@ curl "https://api.telegram.org/bot<bot_token>/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.
<Note>
`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.
</Note>
For text-only replies:

View File

@@ -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:

View File

@@ -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: {

View File

@@ -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<string, unknown> | 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<string, unknown> | 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,