fix(telegram): fall back when current preview is missing

This commit is contained in:
Ayaan Zaidi
2026-03-10 09:31:11 +05:30
parent 38d5183f38
commit 95bf8ed8ee
3 changed files with 64 additions and 14 deletions

View File

@@ -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);
});
});

View File

@@ -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<PreviewEditResult> => {
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<PreviewEditResult> => {
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<PreviewEditResult> => {
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,
);
};

View File

@@ -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 () => {