diff --git a/src/telegram/bot-message-dispatch.test.ts b/src/telegram/bot-message-dispatch.test.ts index 07d4e10b408..d0369e3635f 100644 --- a/src/telegram/bot-message-dispatch.test.ts +++ b/src/telegram/bot-message-dispatch.test.ts @@ -687,6 +687,49 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(deliverReplies).not.toHaveBeenCalled(); }); + it("updates reasoning preview for reasoning block payloads instead of sending duplicates", async () => { + setupDraftStreams({ answerMessageId: 999, reasoningMessageId: 111 }); + dispatchReplyWithBufferedBlockDispatcher.mockImplementation( + async ({ dispatcherOptions, replyOptions }) => { + await replyOptions?.onReasoningStream?.({ + text: "Reasoning:\nIf I count r in strawberry, I see positions 3, 8, and", + }); + await replyOptions?.onReasoningEnd?.(); + await replyOptions?.onPartialReply?.({ text: "3" }); + await dispatcherOptions.deliver({ text: "3" }, { kind: "final" }); + await dispatcherOptions.deliver( + { + text: "Reasoning:\nIf I count r in strawberry, I see positions 3, 8, and 9. So the total is 3.", + }, + { kind: "block" }, + ); + return { queuedFinal: true }; + }, + ); + deliverReplies.mockResolvedValue({ delivered: true }); + editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "999" }); + + await dispatchWithContext({ context: createContext(), streamMode: "partial" }); + + expect(editMessageTelegram).toHaveBeenNthCalledWith(1, 123, 999, "3", expect.any(Object)); + expect(editMessageTelegram).toHaveBeenNthCalledWith( + 2, + 123, + 111, + "Reasoning:\nIf I count r in strawberry, I see positions 3, 8, and 9. So the total is 3.", + expect.any(Object), + ); + expect(deliverReplies).not.toHaveBeenCalledWith( + expect.objectContaining({ + replies: [ + expect.objectContaining({ + text: expect.stringContaining("Reasoning:\nIf I count r in strawberry"), + }), + ], + }), + ); + }); + it("splits reasoning preview only when next reasoning block starts in partial mode", async () => { const { reasoningDraftStream } = setupDraftStreams({ answerMessageId: 999, diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index d9057f0572b..cecfd9795e4 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -399,6 +399,49 @@ export const dispatchTelegramMessage = async ({ return false; } }; + const tryEditExistingPreviewForLane = async (params: { + lane: DraftLaneState; + laneName: "answer" | "reasoning"; + finalText: string; + previewButtons?: TelegramInlineButtons; + }): Promise => { + const { lane, laneName, finalText, previewButtons } = params; + if (!lane.stream) { + return false; + } + const previewMessageId = lane.stream.messageId(); + if (typeof previewMessageId !== "number") { + return false; + } + const currentPreviewText = streamMode === "block" ? lane.draftText : lane.lastPartialText; + if ( + currentPreviewText && + currentPreviewText.startsWith(finalText) && + finalText.length < currentPreviewText.length + ) { + // Avoid regressive punctuation/wording flicker from occasional shorter finals. + deliveryState.delivered = true; + return true; + } + try { + await editMessageTelegram(chatId, previewMessageId, finalText, { + api: bot.api, + cfg, + accountId: route.accountId, + linkPreview: telegramCfg.linkPreview, + buttons: previewButtons, + }); + lane.lastPartialText = finalText; + lane.draftText = finalText; + deliveryState.delivered = true; + return true; + } catch (err) { + logVerbose( + `telegram: ${laneName} preview update failed; falling back to standard send (${String(err)})`, + ); + return false; + } + }; let queuedFinal = false; try { @@ -408,21 +451,21 @@ export const dispatchTelegramMessage = async ({ dispatcherOptions: { ...prefixOptions, deliver: async (payload, info) => { + const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; + const finalText = payload.text; + const reasoningMessage = isReasoningMessage(finalText); + const previewButtons = ( + payload.channelData?.telegram as { buttons?: TelegramInlineButtons } | undefined + )?.buttons; + const canFinalizeViaPreviewEdit = + !hasMedia && + typeof finalText === "string" && + finalText.length > 0 && + finalText.length <= draftMaxChars && + !payload.isError; if (info.kind === "final") { await flushDraftLane(answerLane); await flushDraftLane(reasoningLane); - const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0; - const finalText = payload.text; - const reasoningMessage = isReasoningMessage(finalText); - const previewButtons = ( - payload.channelData?.telegram as { buttons?: TelegramInlineButtons } | undefined - )?.buttons; - const canFinalizeViaPreviewEdit = - !hasMedia && - typeof finalText === "string" && - finalText.length > 0 && - finalText.length <= draftMaxChars && - !payload.isError; if (canFinalizeViaPreviewEdit && reasoningMessage) { const finalizedReasoning = await tryFinalizePreviewForLane({ lane: reasoningLane, @@ -459,6 +502,17 @@ export const dispatchTelegramMessage = async ({ await answerLane.stream?.stop(); await reasoningLane.stream?.stop(); } + if (info.kind !== "final" && canFinalizeViaPreviewEdit && reasoningMessage) { + const updatedReasoning = await tryEditExistingPreviewForLane({ + lane: reasoningLane, + laneName: "reasoning", + finalText, + previewButtons, + }); + if (updatedReasoning) { + return; + } + } const result = await deliverReplies({ ...deliveryBaseOptions, replies: [payload],