From a505be78ab4407e9da5ed8ce650a4de6b94af850 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 00:54:49 +0000 Subject: [PATCH] fix(telegram): land #38906 from @gambletan Landed from contributor PR #38906 by @gambletan. Co-authored-by: gambletan --- CHANGELOG.md | 1 + src/telegram/bot-message-dispatch.test.ts | 6 ++++-- src/telegram/bot-message-dispatch.ts | 9 ++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a1a2b5014..999a833c0e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -321,6 +321,7 @@ Docs: https://docs.openclaw.ai - Exec approvals/allow-always shell scripts: persist and match script paths for wrapper invocations like `bash scripts/foo.sh` while still blocking `-c`/`-s` wrapper bypasses. Landed from contributor PR #35137 by @yuweuii. Thanks @yuweuii. - Queue/followup dedupe across drain restarts: dedupe queued redelivery `message_id` values after queue recreation so busy-session followups no longer duplicate on replayed inbound events. Landed from contributor PR #33168 by @rylena. Thanks @rylena. - Telegram/preview-final edit idempotence: treat `message is not modified` errors during preview finalization as delivered so partial-stream final replies do not fall back to duplicate sends. Landed from contributor PR #34983 by @HOYALIM. Thanks @HOYALIM. +- Telegram/DM streaming transport parity: use message preview transport for all DM streaming lanes so final delivery can edit the active preview instead of sending duplicate finals. Landed from contributor PR #38906 by @gambletan. Thanks @gambletan. ## 2026.3.2 diff --git a/src/telegram/bot-message-dispatch.test.ts b/src/telegram/bot-message-dispatch.test.ts index 001660b6aa1..ddec14d60e2 100644 --- a/src/telegram/bot-message-dispatch.test.ts +++ b/src/telegram/bot-message-dispatch.test.ts @@ -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 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( async ({ dispatcherOptions, replyOptions }) => { @@ -1187,10 +1187,12 @@ describe("dispatchTelegramMessage draft streaming", () => { await dispatchWithContext({ context: createReasoningStreamContext(), streamMode: "partial" }); expect(createTelegramDraftStream).toHaveBeenCalledTimes(2); + // Both answer (call[0]) and reasoning (call[1]) lanes should use message + // transport in DMs to prevent duplicate messages. (Fixes #33453) expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual( expect.objectContaining({ thread: { id: 777, scope: "dm" }, - previewTransport: "auto", + previewTransport: "message", }), ); expect(createTelegramDraftStream.mock.calls[1]?.[0]).toEqual( diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index 859a35688f6..42640546e24 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -194,15 +194,18 @@ export const dispatchTelegramMessage = async ({ const archivedAnswerPreviews: ArchivedPreview[] = []; const archivedReasoningPreviewIds: number[] = []; const createDraftLane = (laneName: LaneName, enabled: boolean): DraftLaneState => { - const useMessagePreviewTransportForDmReasoning = - laneName === "reasoning" && threadSpec?.scope === "dm" && canStreamAnswerDraft; + // Use message transport (sendMessage + editMessageText) for all lanes in + // DMs so that streamMessageId is tracked. Draft transport doesn't track a + // messageId, causing resolvePreviewTarget() to miss the preview on final + // delivery — which sends a duplicate message. (Fixes #33453) + const useMessagePreviewTransportForDm = threadSpec?.scope === "dm" && canStreamAnswerDraft; const stream = enabled ? createTelegramDraftStream({ api: bot.api, chatId, maxChars: draftMaxChars, thread: threadSpec, - previewTransport: useMessagePreviewTransportForDmReasoning ? "message" : "auto", + previewTransport: useMessagePreviewTransportForDm ? "message" : "auto", replyToMessageId: draftReplyToMessageId, minInitialChars: draftMinInitialChars, renderText: renderDraftPreview,