mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-25 01:13:31 +00:00
fix(telegram): delete orphaned preview message after fallback send
When the answer lane in lane-delivery.ts fails to finalize a preview via edit and falls back to sendPayload(), the previously-flushed preview message was left visible. The user sees both the preview and the final message — the classic 'message appears twice, one disappears' behavior. The fallback send path now captures the preview message ID before stopping the draft lane and deletes it after successful delivery. Closes #38365 Related: #37702, #38434, #33308, #33453
This commit is contained in:
@@ -171,6 +171,13 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
||||
params.activePreviewLifecycleByLane[laneName] = "complete";
|
||||
params.retainPreviewOnCleanupByLane[laneName] = true;
|
||||
};
|
||||
const clearActivePreviewState = (laneName: LaneName, lane: DraftLaneState) => {
|
||||
lane.stream?.forceNewMessage?.();
|
||||
lane.lastPartialText = "";
|
||||
lane.hasStreamedMessage = false;
|
||||
params.activePreviewLifecycleByLane[laneName] = "transient";
|
||||
params.retainPreviewOnCleanupByLane[laneName] = false;
|
||||
};
|
||||
const isDraftPreviewLane = (lane: DraftLaneState) => lane.stream?.previewMode?.() === "draft";
|
||||
const canMaterializeDraftFinal = (
|
||||
lane: DraftLaneState,
|
||||
@@ -529,8 +536,20 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
||||
`telegram: preview final too long for edit (${text.length} > ${params.draftMaxChars}); falling back to standard send`,
|
||||
);
|
||||
}
|
||||
const previewMessageIdBeforeFallback = lane.stream?.messageId();
|
||||
await params.stopDraftLane(lane);
|
||||
const previewMessageIdAfterStop = previewMessageIdBeforeFallback ?? lane.stream?.messageId();
|
||||
const delivered = await params.sendPayload(params.applyTextToPayload(payload, text));
|
||||
if (delivered && typeof previewMessageIdAfterStop === "number") {
|
||||
try {
|
||||
await params.deletePreviewMessage(previewMessageIdAfterStop);
|
||||
clearActivePreviewState(laneName, lane);
|
||||
} catch (err) {
|
||||
params.log(
|
||||
`telegram: ${laneName} fallback send orphaned preview cleanup failed (${previewMessageIdAfterStop}): ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return delivered ? "sent" : "skipped";
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ function createHarness(params?: {
|
||||
const deletePreviewMessage = vi.fn().mockResolvedValue(undefined);
|
||||
const log = vi.fn();
|
||||
const markDelivered = vi.fn();
|
||||
const activePreviewLifecycleByLane = { answer: "transient", reasoning: "transient" } as const;
|
||||
const retainPreviewOnCleanupByLane = { answer: false, reasoning: false } as const;
|
||||
const activePreviewLifecycleByLane = { answer: "transient", reasoning: "transient" };
|
||||
const retainPreviewOnCleanupByLane = { answer: false, reasoning: false };
|
||||
const archivedAnswerPreviews: Array<{
|
||||
messageId: number;
|
||||
textSnapshot: string;
|
||||
@@ -53,8 +53,8 @@ function createHarness(params?: {
|
||||
const deliverLaneText = createLaneTextDeliverer({
|
||||
lanes,
|
||||
archivedAnswerPreviews,
|
||||
activePreviewLifecycleByLane: { ...activePreviewLifecycleByLane },
|
||||
retainPreviewOnCleanupByLane: { ...retainPreviewOnCleanupByLane },
|
||||
activePreviewLifecycleByLane,
|
||||
retainPreviewOnCleanupByLane,
|
||||
draftMaxChars: params?.draftMaxChars ?? 4_096,
|
||||
applyTextToPayload: (payload: ReplyPayload, text: string) => ({ ...payload, text }),
|
||||
sendPayload,
|
||||
@@ -81,6 +81,8 @@ function createHarness(params?: {
|
||||
log,
|
||||
markDelivered,
|
||||
archivedAnswerPreviews,
|
||||
activePreviewLifecycleByLane,
|
||||
retainPreviewOnCleanupByLane,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -473,6 +475,29 @@ describe("createLaneTextDeliverer", () => {
|
||||
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555);
|
||||
});
|
||||
|
||||
it("clears active preview state after successful fallback orphan cleanup", async () => {
|
||||
const harness = createHarness({
|
||||
answerMessageId: 5555,
|
||||
answerHasStreamedMessage: true,
|
||||
answerLastPartialText: "Partial streaming...",
|
||||
});
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Final with media",
|
||||
payload: { text: "Final with media", mediaUrl: "file:///tmp/example.png" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("sent");
|
||||
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555);
|
||||
expect(harness.answer.stream?.forceNewMessage).toHaveBeenCalledTimes(1);
|
||||
expect(harness.lanes.answer.hasStreamedMessage).toBe(false);
|
||||
expect(harness.lanes.answer.lastPartialText).toBe("");
|
||||
expect(harness.activePreviewLifecycleByLane.answer).toBe("transient");
|
||||
expect(harness.retainPreviewOnCleanupByLane.answer).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the active preview when an archived final edit target is missing", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
harness.archivedAnswerPreviews.push({
|
||||
|
||||
Reference in New Issue
Block a user