mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:50:46 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user