diff --git a/src/telegram/lane-delivery-text-deliverer.ts b/src/telegram/lane-delivery-text-deliverer.ts index 19052c9b193..4bb0392d330 100644 --- a/src/telegram/lane-delivery-text-deliverer.ts +++ b/src/telegram/lane-delivery-text-deliverer.ts @@ -365,13 +365,9 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { context, }); if (typeof previewTargetAfterStop.previewMessageId !== "number") { - if (lane.stream?.sendMayHaveLanded?.()) { - params.log( - `telegram: ${laneName} preview send may have landed despite missing message id; keeping to avoid duplicate`, - ); - params.markDelivered(); - return "retained"; - } + // This is the first preview creation — no prior visible message exists. + // Even if sendMayHaveLanded, prefer fallback over silence: a duplicate + // is better than the user seeing nothing at all. return "fallback"; } return finalizePreview(previewTargetAfterStop.previewMessageId, true, false); @@ -386,7 +382,9 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { context, }); if (typeof previewTargetAfterStop.previewMessageId !== "number") { - if (lane.stream?.sendMayHaveLanded?.()) { + // Only retain when a prior preview is already visible to the user — + // otherwise falling back is safer than silence. + if (lane.hasStreamedMessage && lane.stream?.sendMayHaveLanded?.()) { params.log( `telegram: ${laneName} preview send may have landed despite missing message id; keeping to avoid duplicate`, ); diff --git a/src/telegram/lane-delivery.test.ts b/src/telegram/lane-delivery.test.ts index 2f9da295e9b..52d333c1213 100644 --- a/src/telegram/lane-delivery.test.ts +++ b/src/telegram/lane-delivery.test.ts @@ -538,10 +538,10 @@ describe("createLaneTextDeliverer", () => { ); }); - it("retains preview when sendMayHaveLanded is true and no messageId", async () => { + it("falls back when sendMayHaveLanded is true but no prior visible preview exists", async () => { const stream = createTestDraftStream(); stream.sendMayHaveLanded.mockReturnValue(true); - // No messageId → resolvePreviewTarget returns undefined + // No messageId and no prior preview → nothing visible for user to see const harness = createHarness({ answerStream: stream }); const result = await harness.deliverLaneText({ @@ -551,6 +551,34 @@ describe("createLaneTextDeliverer", () => { infoKind: "final", }); + // Prefer fallback (possible duplicate) over silence (no message at all) + expect(result).toBe("sent"); + expect(harness.sendPayload).toHaveBeenCalledWith( + expect.objectContaining({ text: "Hello final" }), + ); + }); + + it("retains when sendMayHaveLanded is true and a prior preview was visible", async () => { + // Stream has a messageId (visible preview) but loses it after stop + const stream = createTestDraftStream({ messageId: 999 }); + stream.sendMayHaveLanded.mockReturnValue(true); + const harness = createHarness({ + answerStream: stream, + answerHasStreamedMessage: true, + }); + // Simulate messageId lost after stop (e.g. forceNewMessage or timeout) + harness.stopDraftLane.mockImplementation(async (lane: DraftLaneState) => { + stream.setMessageId(undefined); + await lane.stream?.stop(); + }); + + const result = await harness.deliverLaneText({ + laneName: "answer", + text: "Hello final", + payload: { text: "Hello final" }, + infoKind: "final", + }); + expect(result).toBe("preview-retained"); expect(harness.sendPayload).not.toHaveBeenCalled(); expect(harness.log).toHaveBeenCalledWith(