Skip to content

Commit 847c0c0

Browse files
committed
fix(telegram): warn on selected quote tool progress
1 parent f1300d3 commit 847c0c0

5 files changed

Lines changed: 173 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
2525
### Fixes
2626

2727
- 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.
28+
- 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.
2829
- 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.
2930
- 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.
3031
- Agents/models: forward model `maxTokens` as the default output-token limit for OpenAI-compatible Responses and Completions transports when no runtime override is provided, preventing provider defaults from silently truncating larger outputs. (#76645) Thanks @joeyfrasier.

docs/channels/telegram.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
302302
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.
303303

304304
<Note>
305-
`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.
305+
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.
306306
</Note>
307307

308308
For text-only replies:

docs/concepts/streaming.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ Supported surfaces:
194194
- **Mattermost** already folds tool activity into its single draft preview post (see above).
195195
- 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.
196196
- 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`.
197-
- 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.
197+
- 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.
198198

199199
Example:
200200

extensions/telegram/src/doctor.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import {
66
collectTelegramEmptyAllowlistExtraWarnings,
77
collectTelegramGroupPolicyWarnings,
88
collectTelegramMissingEnvTokenWarnings,
9+
collectTelegramSelectedQuoteToolProgressWarnings,
910
maybeRepairTelegramApiRoots,
1011
maybeRepairTelegramAllowFromUsernames,
1112
scanTelegramBotEndpointApiRoots,
1213
scanTelegramInvalidAllowFromEntries,
14+
scanTelegramSelectedQuoteToolProgressWarnings,
1315
telegramDoctor,
1416
} from "./doctor.js";
1517

@@ -329,6 +331,112 @@ describe("telegram doctor", () => {
329331
]);
330332
});
331333

334+
it("warns when selected quote replies can suppress Telegram tool-progress preview", async () => {
335+
const cfg = {
336+
channels: {
337+
telegram: {
338+
replyToMode: "first",
339+
},
340+
},
341+
} as unknown as OpenClawConfig;
342+
343+
const hits = scanTelegramSelectedQuoteToolProgressWarnings(cfg);
344+
expect(hits).toEqual([{ path: "channels.telegram", replyToMode: "first" }]);
345+
346+
const warnings = collectTelegramSelectedQuoteToolProgressWarnings({ hits });
347+
expect(warnings[0]).toContain("selected quote replies");
348+
expect(warnings[0]).toContain('"Working..." tool-progress preview');
349+
expect(warnings[0]).toContain("Current-message replies without selected quote text");
350+
expect(warnings[1]).toContain("streaming.preview.toolProgress: false");
351+
expect(
352+
await telegramDoctor.collectPreviewWarnings?.({
353+
cfg,
354+
doctorFixCommand: "openclaw doctor --fix",
355+
}),
356+
).toEqual(expect.arrayContaining([expect.stringContaining("selected quote replies")]));
357+
});
358+
359+
it("warns for the implicit default Telegram account when accounts is empty", () => {
360+
const cfg = {
361+
channels: {
362+
telegram: {
363+
replyToMode: "all",
364+
accounts: {},
365+
},
366+
},
367+
} as unknown as OpenClawConfig;
368+
369+
expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([
370+
{ path: "channels.telegram", replyToMode: "all" },
371+
]);
372+
});
373+
374+
it("uses merged Telegram account config for selected quote tool-progress warnings", () => {
375+
listTelegramAccountIdsMock.mockReturnValue(["work", "quiet"]);
376+
const cfg = {
377+
channels: {
378+
telegram: {
379+
replyToMode: "batched",
380+
accounts: {
381+
work: {},
382+
quiet: {
383+
replyToMode: "off",
384+
},
385+
},
386+
},
387+
},
388+
} as unknown as OpenClawConfig;
389+
390+
expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([
391+
{ path: "channels.telegram.accounts.work", replyToMode: "batched" },
392+
]);
393+
});
394+
395+
it("skips selected quote tool-progress warning when preview progress is disabled", () => {
396+
const cfg = {
397+
channels: {
398+
telegram: {
399+
replyToMode: "first",
400+
streaming: {
401+
preview: {
402+
toolProgress: false,
403+
},
404+
},
405+
},
406+
},
407+
} as unknown as OpenClawConfig;
408+
409+
expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([]);
410+
});
411+
412+
it("skips selected quote tool-progress warning when preview streaming is off or block streaming owns delivery", () => {
413+
expect(
414+
scanTelegramSelectedQuoteToolProgressWarnings({
415+
channels: {
416+
telegram: {
417+
replyToMode: "first",
418+
streaming: false,
419+
},
420+
},
421+
} as unknown as OpenClawConfig),
422+
).toEqual([]);
423+
424+
expect(
425+
scanTelegramSelectedQuoteToolProgressWarnings({
426+
channels: {
427+
telegram: {
428+
replyToMode: "first",
429+
},
430+
},
431+
agents: {
432+
defaults: {
433+
blockStreamingDefault: "on",
434+
},
435+
},
436+
} as unknown as OpenClawConfig),
437+
).toEqual([]);
438+
});
439+
332440
it("wires apiRoot preview warnings and repair through the doctor adapter", async () => {
333441
const cfg = {
334442
channels: {

extensions/telegram/src/doctor.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import {
22
type ChannelDoctorAdapter,
33
type ChannelDoctorEmptyAllowlistAccountContext,
44
} from "openclaw/plugin-sdk/channel-contract";
5+
import {
6+
resolveChannelStreamingBlockEnabled,
7+
resolveChannelStreamingPreviewToolProgress,
8+
} from "openclaw/plugin-sdk/channel-streaming";
59
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
610
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
711
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
812
import { inspectTelegramAccount } from "./account-inspect.js";
913
import {
1014
listTelegramAccountIds,
15+
mergeTelegramAccountConfig,
1116
resolveDefaultTelegramAccountId,
1217
resolveTelegramAccount,
1318
} from "./accounts.js";
@@ -18,8 +23,10 @@ import {
1823
legacyConfigRules as TELEGRAM_LEGACY_CONFIG_RULES,
1924
normalizeCompatibilityConfig as normalizeTelegramCompatibilityConfig,
2025
} from "./doctor-contract.js";
26+
import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js";
2127

2228
type TelegramAllowFromInvalidHit = { path: string; entry: string };
29+
type TelegramSelectedQuoteToolProgressHit = { path: string; replyToMode: string };
2330
type TelegramApiRootBotEndpointHit = {
2431
path: string;
2532
pathSegments: string[];
@@ -196,6 +203,58 @@ export function collectTelegramApiRootWarnings(params: {
196203
];
197204
}
198205

206+
function formatTelegramAccountConfigPath(cfg: OpenClawConfig, accountId: string): string {
207+
const telegram = asObjectRecord((cfg.channels as Record<string, unknown> | undefined)?.telegram);
208+
const accounts = asObjectRecord(telegram?.accounts);
209+
if (!accounts || Object.keys(accounts).length === 0) {
210+
return "channels.telegram";
211+
}
212+
return accountId === "default" ? "channels.telegram" : `channels.telegram.accounts.${accountId}`;
213+
}
214+
215+
export function scanTelegramSelectedQuoteToolProgressWarnings(
216+
cfg: OpenClawConfig,
217+
): TelegramSelectedQuoteToolProgressHit[] {
218+
if (!asObjectRecord((cfg.channels as Record<string, unknown> | undefined)?.telegram)) {
219+
return [];
220+
}
221+
return listTelegramAccountIds(cfg).flatMap((accountId) => {
222+
const account = mergeTelegramAccountConfig(cfg, accountId);
223+
const replyToMode = account.replyToMode ?? "off";
224+
if (replyToMode === "off") {
225+
return [];
226+
}
227+
if (resolveTelegramPreviewStreamMode(account) === "off") {
228+
return [];
229+
}
230+
const blockStreamingEnabled =
231+
resolveChannelStreamingBlockEnabled(account) ??
232+
cfg.agents?.defaults?.blockStreamingDefault === "on";
233+
if (blockStreamingEnabled || !resolveChannelStreamingPreviewToolProgress(account)) {
234+
return [];
235+
}
236+
return [
237+
{
238+
path: formatTelegramAccountConfigPath(cfg, accountId),
239+
replyToMode,
240+
},
241+
];
242+
});
243+
}
244+
245+
export function collectTelegramSelectedQuoteToolProgressWarnings(params: {
246+
hits: TelegramSelectedQuoteToolProgressHit[];
247+
}): string[] {
248+
if (params.hits.length === 0) {
249+
return [];
250+
}
251+
const sample = params.hits[0] ?? { path: "channels.telegram", replyToMode: "first" };
252+
return [
253+
`- ${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.`,
254+
'- 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.',
255+
];
256+
}
257+
199258
export function maybeRepairTelegramApiRoots(cfg: OpenClawConfig): {
200259
config: OpenClawConfig;
201260
changes: string[];
@@ -506,6 +565,9 @@ export const telegramDoctor: ChannelDoctorAdapter = {
506565
hits: scanTelegramBotEndpointApiRoots(cfg),
507566
doctorFixCommand,
508567
}),
568+
...collectTelegramSelectedQuoteToolProgressWarnings({
569+
hits: scanTelegramSelectedQuoteToolProgressWarnings(cfg),
570+
}),
509571
],
510572
repairConfig: async ({ cfg }) => await repairTelegramConfig({ cfg }),
511573
collectEmptyAllowlistExtraWarnings: collectTelegramEmptyAllowlistExtraWarnings,

0 commit comments

Comments
 (0)