fix(telegram): recover incomplete preview finalization (#71554)

Fix Telegram partial-stream preview finalization so ambiguous final edit failures fall back to a final send when the visible preview is a strict prefix of the answer.

Includes archived-preview regression coverage and generated config metadata refresh.

Thanks @sahilsatralkar.

Co-authored-by: Sahil Satralkar <62758655+sahilsatralkar@users.noreply.github.com>
This commit is contained in:
Sahil Satralkar
2026-04-25 17:31:10 +05:30
committed by GitHub
parent e25b3c6056
commit 3064ea78ab
7 changed files with 62 additions and 6 deletions

View File

@@ -31,7 +31,7 @@ export const telegramChannelConfigUiHints = {
},
streaming: {
label: "Telegram Streaming Mode",
help: 'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are auto-mapped.',
help: 'Unified Telegram stream preview mode: "off" | "partial" | "block" | "progress" (default: "partial"). "progress" maps to "partial" on Telegram. Legacy boolean/streamMode keys are detected; run doctor --fix to migrate.',
},
"streaming.mode": {
label: "Telegram Streaming Mode",

View File

@@ -38,6 +38,12 @@ function isMissingPreviewMessageError(err: unknown): boolean {
return MESSAGE_NOT_FOUND_RE.test(extractErrorText(err));
}
function isIncompleteFinalPreviewPrefix(previewText: string, finalText: string): boolean {
const preview = previewText.trimEnd();
const final = finalText.trimEnd();
return preview.length > 0 && preview.length < final.length && final.startsWith(preview);
}
export type LaneName = "answer" | "reasoning";
export type DraftLaneState = {
@@ -232,6 +238,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
lane: DraftLaneState;
finalTextAlreadyLanded: boolean;
retainAlternatePreviewOnMissingTarget: boolean;
targetPreviewText: string;
}): Promise<PreviewEditResult> => {
try {
await params.editPreview({
@@ -294,7 +301,14 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
);
return "fallback";
}
// Default: ambiguous error — prefer incomplete over duplicate
if (isIncompleteFinalPreviewPrefix(args.targetPreviewText, args.text)) {
params.log(
`telegram: ${args.laneName} preview final edit failed and existing preview is an incomplete prefix; falling back to standard send (${String(err)})`,
);
return "fallback";
}
// Default: ambiguous error — retain when fallback may duplicate a final
// edit that already landed or when the preview is not known-incomplete.
params.log(
`telegram: ${args.laneName} preview final edit failed with ambiguous error; keeping existing preview to avoid duplicate (${String(err)})`,
);
@@ -324,6 +338,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
messageId: number,
finalTextAlreadyLanded: boolean,
retainAlternatePreviewOnMissingTarget: boolean,
targetPreviewText: string,
) =>
tryEditPreviewMessage({
laneName,
@@ -335,6 +350,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
lane,
finalTextAlreadyLanded,
retainAlternatePreviewOnMissingTarget,
targetPreviewText,
});
const finalizePreview = (
previewMessageId: number,
@@ -357,6 +373,7 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
previewMessageId,
finalTextAlreadyLanded,
retainAlternatePreviewOnMissingTarget,
currentPreviewText,
);
};
if (!lane.stream) {

View File

@@ -231,7 +231,8 @@ describe("createLaneTextDeliverer", () => {
it("retains preview when an existing preview final edit fails with ambiguous error", async () => {
const harness = createHarness({ answerMessageId: 999 });
// Plain Error with no error_code → ambiguous, prefer incomplete over duplicate
// Plain Error with no error_code → ambiguous. Retain unless the preview is
// known to be an incomplete prefix of the final text.
harness.editPreview.mockRejectedValue(new Error("500: preview edit failed"));
await expectFinalPreviewRetained({
@@ -241,6 +242,20 @@ describe("createLaneTextDeliverer", () => {
expect(harness.editPreview).toHaveBeenCalledTimes(1);
});
it("falls back when an ambiguous final edit failure would leave an incomplete preview", async () => {
const harness = createHarness({
answerMessageId: 999,
answerLastPartialText: "Hello fi",
});
harness.editPreview.mockRejectedValue(new Error("500: preview edit failed"));
await expectFinalEditFallbackToSend({
harness,
text: HELLO_FINAL,
expectedLogSnippet: "preview is an incomplete prefix; falling back",
});
});
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"));
@@ -478,6 +493,29 @@ describe("createLaneTextDeliverer", () => {
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555);
});
it("falls back when an archived preview ambiguous final edit would leave an incomplete prefix", async () => {
const harness = createHarness();
harness.archivedAnswerPreviews.push({
messageId: 5555,
textSnapshot: "Hello fi",
deleteIfUnused: true,
});
harness.editPreview.mockRejectedValue(new Error("500: preview edit failed"));
await expectFinalEditFallbackToSend({
harness,
text: HELLO_FINAL,
expectedLogSnippet: "preview is an incomplete prefix; falling back",
});
expect(harness.editPreview).toHaveBeenCalledWith(
expect.objectContaining({
messageId: 5555,
text: HELLO_FINAL,
}),
);
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555);
});
it("keeps the active preview when an archived final edit target is missing", async () => {
const harness = createHarness({ answerMessageId: 999 });
seedArchivedAnswerPreview(harness);