From 95bf8ed8ee5a702a5fc2ffa1ce4ccf1d7810e0f3 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 10 Mar 2026 09:31:11 +0530 Subject: [PATCH] fix(telegram): fall back when current preview is missing --- src/telegram/bot-message-dispatch.test.ts | 4 +- src/telegram/lane-delivery-text-deliverer.ts | 35 ++++++++++++++---- src/telegram/lane-delivery.test.ts | 39 +++++++++++++++++--- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/telegram/bot-message-dispatch.test.ts b/src/telegram/bot-message-dispatch.test.ts index 09f33ad22fe..d5d4f88be1b 100644 --- a/src/telegram/bot-message-dispatch.test.ts +++ b/src/telegram/bot-message-dispatch.test.ts @@ -1958,7 +1958,7 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(finalTextSentViaDeliverReplies).toBe(true); }); - it("keeps preview when Telegram reports the final edit target missing", async () => { + it("falls back when Telegram reports the current final edit target missing", async () => { const draftStream = createDraftStream(999); createTelegramDraftStream.mockReturnValue(draftStream); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( @@ -1980,6 +1980,6 @@ describe("dispatchTelegramMessage draft streaming", () => { (r: { text?: string }) => r.text === "Final answer", ), ); - expect(finalTextSentViaDeliverReplies).toBe(false); + expect(finalTextSentViaDeliverReplies).toBe(true); }); }); diff --git a/src/telegram/lane-delivery-text-deliverer.ts b/src/telegram/lane-delivery-text-deliverer.ts index 463abb8967f..691096ddd68 100644 --- a/src/telegram/lane-delivery-text-deliverer.ts +++ b/src/telegram/lane-delivery-text-deliverer.ts @@ -25,8 +25,9 @@ function isMessageNotModifiedError(err: unknown): boolean { } /** - * Returns true when Telegram reports the target message no longer exists. - * In this case the preview is gone and a fallback send is safe (no duplicate). + * Returns true when Telegram rejects an edit because the target message can no + * longer be resolved or edited. The caller still needs preview context to + * decide whether to retain a different visible preview or fall back to send. */ function isMissingPreviewMessageError(err: unknown): boolean { return MESSAGE_NOT_FOUND_RE.test(extractErrorText(err)); @@ -207,6 +208,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { updateLaneSnapshot: boolean; lane: DraftLaneState; finalTextAlreadyLanded: boolean; + retainAlternatePreviewOnMissingTarget: boolean; }): Promise => { try { await params.editPreview({ @@ -244,11 +246,17 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { return "fallback"; } if (isMissingPreviewMessageError(err)) { + if (args.retainAlternatePreviewOnMissingTarget) { + params.log( + `telegram: ${args.laneName} preview final edit target missing; keeping alternate preview without fallback (${String(err)})`, + ); + params.markDelivered(); + return "retained"; + } params.log( - `telegram: ${args.laneName} preview final edit target missing; keeping existing preview without fallback (${String(err)})`, + `telegram: ${args.laneName} preview final edit target missing with no alternate preview; falling back to standard send (${String(err)})`, ); - params.markDelivered(); - return "retained"; + return "fallback"; } if (isRecoverableTelegramNetworkError(err, { allowMessageMatch: true })) { params.log( @@ -281,7 +289,11 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { previewMessageId: previewMessageIdOverride, previewTextSnapshot, }: TryUpdatePreviewParams): Promise => { - const editPreview = (messageId: number, finalTextAlreadyLanded: boolean) => + const editPreview = ( + messageId: number, + finalTextAlreadyLanded: boolean, + retainAlternatePreviewOnMissingTarget: boolean, + ) => tryEditPreviewMessage({ laneName, messageId, @@ -291,11 +303,13 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { updateLaneSnapshot, lane, finalTextAlreadyLanded, + retainAlternatePreviewOnMissingTarget, }); const finalizePreview = ( previewMessageId: number, finalTextAlreadyLanded: boolean, hadPreviewMessage: boolean, + retainAlternatePreviewOnMissingTarget = false, ): PreviewEditResult | Promise => { const currentPreviewText = previewTextSnapshot ?? getLanePreviewText(lane); const shouldSkipRegressive = shouldSkipRegressivePreviewUpdate({ @@ -308,7 +322,11 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { params.markDelivered(); return "edited"; } - return editPreview(previewMessageId, finalTextAlreadyLanded); + return editPreview( + previewMessageId, + finalTextAlreadyLanded, + retainAlternatePreviewOnMissingTarget, + ); }; if (!lane.stream) { return "fallback"; @@ -346,10 +364,13 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { if (typeof previewTargetAfterStop.previewMessageId !== "number") { return "fallback"; } + const activePreviewMessageId = lane.stream?.messageId(); return finalizePreview( previewTargetAfterStop.previewMessageId, false, previewTargetAfterStop.hadPreviewMessage, + typeof activePreviewMessageId === "number" && + activePreviewMessageId !== previewTargetAfterStop.previewMessageId, ); }; diff --git a/src/telegram/lane-delivery.test.ts b/src/telegram/lane-delivery.test.ts index 31d412c26b1..f33401875f8 100644 --- a/src/telegram/lane-delivery.test.ts +++ b/src/telegram/lane-delivery.test.ts @@ -193,7 +193,7 @@ describe("createLaneTextDeliverer", () => { ); }); - it("keeps preview when Telegram reports the final edit target missing", async () => { + it("falls back when Telegram reports the current final edit target missing", async () => { const harness = createHarness({ answerMessageId: 999 }); harness.editPreview.mockRejectedValue(new Error("400: Bad Request: message to edit not found")); @@ -204,11 +204,13 @@ describe("createLaneTextDeliverer", () => { infoKind: "final", }); - expect(result).toBe("preview-retained"); + expect(result).toBe("sent"); expect(harness.editPreview).toHaveBeenCalledTimes(1); - expect(harness.sendPayload).not.toHaveBeenCalled(); + expect(harness.sendPayload).toHaveBeenCalledWith( + expect.objectContaining({ text: "Hello final" }), + ); expect(harness.log).toHaveBeenCalledWith( - expect.stringContaining("edit target missing; keeping existing preview without fallback"), + expect.stringContaining("edit target missing with no alternate preview; falling back"), ); }); @@ -445,7 +447,7 @@ describe("createLaneTextDeliverer", () => { expect(harness.sendPayload).toHaveBeenCalledTimes(1); }); - it("keeps archived preview when its final edit target is missing", async () => { + it("falls back when an archived preview edit target is missing and no alternate preview exists", async () => { const harness = createHarness(); harness.archivedAnswerPreviews.push({ messageId: 5555, @@ -461,9 +463,36 @@ describe("createLaneTextDeliverer", () => { infoKind: "final", }); + expect(harness.editPreview).toHaveBeenCalledTimes(1); + expect(harness.sendPayload).toHaveBeenCalledWith( + expect.objectContaining({ text: "Complete final answer" }), + ); + expect(result).toBe("sent"); + expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555); + }); + + it("keeps the active preview when an archived final edit target is missing", async () => { + const harness = createHarness({ answerMessageId: 999 }); + harness.archivedAnswerPreviews.push({ + messageId: 5555, + textSnapshot: "Partial streaming...", + deleteIfUnused: true, + }); + harness.editPreview.mockRejectedValue(new Error("400: Bad Request: message to edit not found")); + + const result = await harness.deliverLaneText({ + laneName: "answer", + text: "Complete final answer", + payload: { text: "Complete final answer" }, + infoKind: "final", + }); + expect(harness.editPreview).toHaveBeenCalledTimes(1); expect(harness.sendPayload).not.toHaveBeenCalled(); expect(result).toBe("preview-retained"); + expect(harness.log).toHaveBeenCalledWith( + expect.stringContaining("edit target missing; keeping alternate preview without fallback"), + ); }); it("deletes consumed boundary previews after fallback final send", async () => {