From d9ed2c425a1b522319ac4af078cf3385997e6c64 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Thu, 26 Feb 2026 16:13:24 +0530 Subject: [PATCH] fix(telegram): prime final preview before stop flush --- src/telegram/bot-message-dispatch.test.ts | 35 +++++++++++++++++++++++ src/telegram/lane-delivery.ts | 5 ++++ 2 files changed, 40 insertions(+) diff --git a/src/telegram/bot-message-dispatch.test.ts b/src/telegram/bot-message-dispatch.test.ts index 7e82adafec2..7f62a77ae16 100644 --- a/src/telegram/bot-message-dispatch.test.ts +++ b/src/telegram/bot-message-dispatch.test.ts @@ -381,6 +381,41 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(draftStream.stop).toHaveBeenCalled(); }); + it("primes stop() with final text when pending partial is below initial threshold", async () => { + let answerMessageId: number | undefined; + const answerDraftStream = { + update: vi.fn(), + flush: vi.fn().mockResolvedValue(undefined), + messageId: vi.fn().mockImplementation(() => answerMessageId), + clear: vi.fn().mockResolvedValue(undefined), + stop: vi.fn().mockImplementation(async () => { + answerMessageId = 777; + }), + forceNewMessage: vi.fn(), + }; + const reasoningDraftStream = createDraftStream(); + createTelegramDraftStream + .mockImplementationOnce(() => answerDraftStream) + .mockImplementationOnce(() => reasoningDraftStream); + dispatchReplyWithBufferedBlockDispatcher.mockImplementation( + async ({ dispatcherOptions, replyOptions }) => { + await replyOptions?.onPartialReply?.({ text: "no" }); + await dispatcherOptions.deliver({ text: "no problem" }, { kind: "final" }); + return { queuedFinal: true }; + }, + ); + deliverReplies.mockResolvedValue({ delivered: true }); + editMessageTelegram.mockResolvedValue({ ok: true, chatId: "123", messageId: "777" }); + + await dispatchWithContext({ context: createContext() }); + + expect(answerDraftStream.update).toHaveBeenCalledWith("no"); + expect(answerDraftStream.update).toHaveBeenLastCalledWith("no problem"); + expect(editMessageTelegram).toHaveBeenCalledWith(123, 777, "no problem", expect.any(Object)); + expect(deliverReplies).not.toHaveBeenCalled(); + expect(answerDraftStream.stop).toHaveBeenCalled(); + }); + it("does not overwrite finalized preview when additional final payloads are sent", async () => { const draftStream = createDraftStream(999); createTelegramDraftStream.mockReturnValue(draftStream); diff --git a/src/telegram/lane-delivery.ts b/src/telegram/lane-delivery.ts index 91aa59dc888..64902ac3911 100644 --- a/src/telegram/lane-delivery.ts +++ b/src/telegram/lane-delivery.ts @@ -123,6 +123,11 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { const hadPreviewMessage = typeof previewMessageIdOverride === "number" || typeof lanePreviewMessageId === "number"; if (stopBeforeEdit) { + if (!hadPreviewMessage && context === "final") { + // If debounce prevented the first preview, replace stale pending partial text + // before final stop() flush sends the first visible preview. + lane.stream.update(text); + } await params.stopDraftLane(lane); } const previewMessageId =