fix(telegram): use message previews in DMs

This commit is contained in:
Ayaan Zaidi
2026-03-08 21:06:25 +05:30
committed by Ayaan Zaidi
parent 95dff166cb
commit d4ab731746
5 changed files with 23 additions and 18 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
- Mattermost replies: keep `root_id` pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda. - Mattermost replies: keep `root_id` pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.
- Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan. - Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan.
- Android/Play distribution: remove self-update, background location, `screen.record`, and background mic capture from the Android app, narrow the foreground service to `dataSync` only, and clean up the legacy `location.enabledMode=always` preference migration. (#39660) Thanks @obviyus. - Android/Play distribution: remove self-update, background location, `screen.record`, and background mic capture from the Android app, narrow the foreground service to `dataSync` only, and clean up the legacy `location.enabledMode=always` preference migration. (#39660) Thanks @obviyus.
- Telegram/DM partial streaming: keep DM preview lanes on real message edits instead of native draft materialization so final replies no longer flash a second duplicate copy before collapsing back to one.
- macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH. - macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
- macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv. - macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
- macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek. - macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.

View File

@@ -232,10 +232,10 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
## Feature reference ## Feature reference
<AccordionGroup> <AccordionGroup>
<Accordion title="Live stream preview (native drafts + message edits)"> <Accordion title="Live stream preview (message edits)">
OpenClaw can stream partial replies in real time: OpenClaw can stream partial replies in real time:
- direct chats: Telegram native draft streaming via `sendMessageDraft` - direct chats: preview message + `editMessageText`
- groups/topics: preview message + `editMessageText` - groups/topics: preview message + `editMessageText`
Requirement: Requirement:
@@ -244,11 +244,9 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
- `progress` maps to `partial` on Telegram (compat with cross-channel naming) - `progress` maps to `partial` on Telegram (compat with cross-channel naming)
- legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped - legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped
Telegram enabled `sendMessageDraft` for all bots in Bot API 9.5 (March 1, 2026).
For text-only replies: For text-only replies:
- DM: OpenClaw updates the draft in place (no extra preview message) - DM: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
- group/topic: OpenClaw keeps the same preview message and performs a final edit in place (no second message) - group/topic: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message. For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message.
@@ -872,7 +870,7 @@ Primary reference:
- `channels.telegram.textChunkLimit`: outbound chunk size (chars). - `channels.telegram.textChunkLimit`: outbound chunk size (chars).
- `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. - `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true). - `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true).
- `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `partial`; `progress` maps to `partial`; `block` is legacy preview mode compatibility). In DMs, `partial` uses native `sendMessageDraft` when available. - `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `partial`; `progress` maps to `partial`; `block` is legacy preview mode compatibility). Telegram preview streaming uses a single preview message that is edited in place.
- `channels.telegram.mediaMaxMb`: inbound/outbound Telegram media cap (MB, default: 100). - `channels.telegram.mediaMaxMb`: inbound/outbound Telegram media cap (MB, default: 100).
- `channels.telegram.retry`: retry policy for Telegram send helpers (CLI/tools/actions) on recoverable outbound API errors (attempts, minDelayMs, maxDelayMs, jitter). - `channels.telegram.retry`: retry policy for Telegram send helpers (CLI/tools/actions) on recoverable outbound API errors (attempts, minDelayMs, maxDelayMs, jitter).
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to enabled on Node 22+, with WSL2 defaulting to disabled. - `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to enabled on Node 22+, with WSL2 defaulting to disabled.

View File

@@ -138,7 +138,7 @@ Legacy key migration:
Telegram: Telegram:
- Uses Bot API `sendMessageDraft` in DMs when available, and `sendMessage` + `editMessageText` for group/topic preview updates. - Uses `sendMessage` + `editMessageText` preview updates across DMs and group/topics.
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming). - Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
- `/reasoning stream` can write reasoning to preview. - `/reasoning stream` can write reasoning to preview.

View File

@@ -1171,7 +1171,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
}, },
); );
it("uses message preview transport for DM reasoning lane when answer preview lane is active", async () => { it("uses message preview transport for all DM lanes when streaming is active", async () => {
setupDraftStreams({ answerMessageId: 999, reasoningMessageId: 111 }); setupDraftStreams({ answerMessageId: 999, reasoningMessageId: 111 });
dispatchReplyWithBufferedBlockDispatcher.mockImplementation( dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
async ({ dispatcherOptions, replyOptions }) => { async ({ dispatcherOptions, replyOptions }) => {
@@ -1190,7 +1190,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual( expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual(
expect.objectContaining({ expect.objectContaining({
thread: { id: 777, scope: "dm" }, thread: { id: 777, scope: "dm" },
previewTransport: "auto", previewTransport: "message",
}), }),
); );
expect(createTelegramDraftStream.mock.calls[1]?.[0]).toEqual( expect(createTelegramDraftStream.mock.calls[1]?.[0]).toEqual(
@@ -1201,9 +1201,8 @@ describe("dispatchTelegramMessage draft streaming", () => {
); );
}); });
it("materializes DM answer draft final without sending a duplicate final message", async () => { it("finalizes DM answer preview in place without materializing or sending a duplicate", async () => {
const answerDraftStream = createTestDraftStream({ previewMode: "draft" }); const answerDraftStream = createDraftStream(321);
answerDraftStream.materialize.mockResolvedValue(321);
const reasoningDraftStream = createDraftStream(111); const reasoningDraftStream = createDraftStream(111);
createTelegramDraftStream createTelegramDraftStream
.mockImplementationOnce(() => answerDraftStream) .mockImplementationOnce(() => answerDraftStream)
@@ -1222,12 +1221,17 @@ describe("dispatchTelegramMessage draft streaming", () => {
expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual( expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual(
expect.objectContaining({ expect.objectContaining({
thread: { id: 777, scope: "dm" }, thread: { id: 777, scope: "dm" },
previewTransport: "auto", previewTransport: "message",
}), }),
); );
expect(answerDraftStream.materialize).toHaveBeenCalledTimes(1); expect(answerDraftStream.materialize).not.toHaveBeenCalled();
expect(deliverReplies).not.toHaveBeenCalled(); expect(deliverReplies).not.toHaveBeenCalled();
expect(editMessageTelegram).not.toHaveBeenCalled(); expect(editMessageTelegram).toHaveBeenCalledWith(
123,
321,
"Checking the directory...",
expect.any(Object),
);
}); });
it("keeps reasoning and answer streaming in separate preview lanes", async () => { it("keeps reasoning and answer streaming in separate preview lanes", async () => {

View File

@@ -190,19 +190,21 @@ export const dispatchTelegramMessage = async ({
const draftReplyToMessageId = const draftReplyToMessageId =
replyToMode !== "off" && typeof msg.message_id === "number" ? msg.message_id : undefined; replyToMode !== "off" && typeof msg.message_id === "number" ? msg.message_id : undefined;
const draftMinInitialChars = DRAFT_MIN_INITIAL_CHARS; const draftMinInitialChars = DRAFT_MIN_INITIAL_CHARS;
// Keep DM preview lanes on real message transport. Native draft previews still
// require a draft->message materialize hop, and that overlap keeps reintroducing
// a visible duplicate flash at finalize time.
const useMessagePreviewTransportForDm = threadSpec?.scope === "dm" && canStreamAnswerDraft;
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId); const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
const archivedAnswerPreviews: ArchivedPreview[] = []; const archivedAnswerPreviews: ArchivedPreview[] = [];
const archivedReasoningPreviewIds: number[] = []; const archivedReasoningPreviewIds: number[] = [];
const createDraftLane = (laneName: LaneName, enabled: boolean): DraftLaneState => { const createDraftLane = (laneName: LaneName, enabled: boolean): DraftLaneState => {
const useMessagePreviewTransportForDmReasoning =
laneName === "reasoning" && threadSpec?.scope === "dm" && canStreamAnswerDraft;
const stream = enabled const stream = enabled
? createTelegramDraftStream({ ? createTelegramDraftStream({
api: bot.api, api: bot.api,
chatId, chatId,
maxChars: draftMaxChars, maxChars: draftMaxChars,
thread: threadSpec, thread: threadSpec,
previewTransport: useMessagePreviewTransportForDmReasoning ? "message" : "auto", previewTransport: useMessagePreviewTransportForDm ? "message" : "auto",
replyToMessageId: draftReplyToMessageId, replyToMessageId: draftReplyToMessageId,
minInitialChars: draftMinInitialChars, minInitialChars: draftMinInitialChars,
renderText: renderDraftPreview, renderText: renderDraftPreview,