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:
parent
82e3ac21ee
commit
4f7bd48458
@ -188,6 +188,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan.
|
||||
- Mattermost replies: keep `root_id` pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.
|
||||
- Telegram/DM partial streaming: keep DM preview lanes on real message edits instead of native draft materialization so final replies no longer flash a second duplicate copy before collapsing back to one.
|
||||
- Telegram/DM fallback preview cleanup: delete orphaned answer previews after fallback final sends and clear the consumed preview state so later assistant-message boundaries do not archive deleted preview ids or force duplicate fallback sends. (#39455) Thanks @eveiljuice.
|
||||
- macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
|
||||
- macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
|
||||
- macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user