mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 13:33:43 +00:00
fix(slack): retain delivered final replies during late cleanup
Fix Slack draft cleanup after final-visible delivery. Track when Slack has already delivered a visible final reply and stop reusing the draft finalizer for later same-turn final/error payloads. This keeps the first fallback cleanup for transient previews while preventing late cleanup from deleting a visible answer. Fixes #87363 Co-authored-by: tianxiaochannel-oss88 <tianxiaochannel@gmail.com>
This commit is contained in:
@@ -1238,6 +1238,42 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
|
||||
expect(draftStream.clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not reuse draft cleanup after a normally delivered final reply", async () => {
|
||||
const draftStream = {
|
||||
...createDraftStreamStub(),
|
||||
flush: vi.fn(noopAsync),
|
||||
clear: vi.fn(noopAsync),
|
||||
discardPending: vi.fn(noopAsync),
|
||||
seal: vi.fn(noopAsync),
|
||||
};
|
||||
createSlackDraftStreamMock.mockReturnValueOnce(draftStream);
|
||||
mockedDispatchSequence = [
|
||||
{
|
||||
kind: "final",
|
||||
payload: { text: "answer", mediaUrl: "https://example.com/final.png" },
|
||||
},
|
||||
{ kind: "final", payload: { text: "late cleanup failed", isError: true } },
|
||||
];
|
||||
|
||||
await dispatchPreparedSlackMessage(createPreparedSlackMessage());
|
||||
|
||||
expect(finalizeSlackPreviewEditMock).not.toHaveBeenCalled();
|
||||
expect(deliverRepliesMock).toHaveBeenCalledTimes(2);
|
||||
expect(draftStream.clear).toHaveBeenCalledTimes(1);
|
||||
const firstDelivered = requireRecord(
|
||||
requireMockCall(deliverRepliesMock, 0, "deliver replies")[0],
|
||||
"deliver replies params",
|
||||
);
|
||||
expect(firstDelivered.replies).toEqual([
|
||||
{ text: "answer", mediaUrl: "https://example.com/final.png" },
|
||||
]);
|
||||
const lateDelivered = requireRecord(
|
||||
requireMockCall(deliverRepliesMock, 1, "deliver replies")[0],
|
||||
"deliver replies params",
|
||||
);
|
||||
expect(lateDelivered.replies).toEqual([{ text: "late cleanup failed", isError: true }]);
|
||||
});
|
||||
|
||||
it("suppresses block streaming when Slack draft preview streaming is active", async () => {
|
||||
mockedBlockStreamingEnabled = true;
|
||||
|
||||
|
||||
@@ -591,6 +591,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
let usedReplyThreadTs: string | undefined;
|
||||
let usedBlockReplyThreadTs: string | undefined;
|
||||
let observedReplyDelivery = false;
|
||||
let observedFinalReplyDelivery = false;
|
||||
const deliveryTracker = createSlackEventDeliveryTracker();
|
||||
const resolveDeliveryThreadTs = (params: {
|
||||
kind: ReplyDispatchKind;
|
||||
@@ -693,6 +694,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
...(slackMessageMetadata ? { metadata: slackMessageMetadata } : {}),
|
||||
});
|
||||
observedReplyDelivery = true;
|
||||
if (params.kind === "final") {
|
||||
observedFinalReplyDelivery = true;
|
||||
}
|
||||
const deliveredThreadTs = resolveDeliveredSlackReplyThreadTs({
|
||||
replyToMode: replyDeliveryMode,
|
||||
payloadReplyToId: params.payload.replyToId,
|
||||
@@ -720,6 +724,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
return false;
|
||||
}
|
||||
replyPlan.markSent();
|
||||
if (params.kind === "final") {
|
||||
observedFinalReplyDelivery = true;
|
||||
}
|
||||
deliveryTracker.markDelivered({
|
||||
kind: params.kind,
|
||||
payload: params.payload,
|
||||
@@ -798,6 +805,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
// the SDK reports a real Slack response.
|
||||
if (streamSession.delivered) {
|
||||
observedReplyDelivery = true;
|
||||
if (params.kind === "final") {
|
||||
observedFinalReplyDelivery = true;
|
||||
}
|
||||
}
|
||||
rememberDeliveredThreadTs(params.kind, streamThreadTs);
|
||||
replyPlan.markSent();
|
||||
@@ -829,6 +839,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
// optimistic "done" status until Slack acknowledges a flush.
|
||||
if (streamSession.delivered) {
|
||||
observedReplyDelivery = true;
|
||||
if (params.kind === "final") {
|
||||
observedFinalReplyDelivery = true;
|
||||
}
|
||||
}
|
||||
deliveryTracker.markDelivered({
|
||||
kind: params.kind,
|
||||
@@ -915,6 +928,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
ttsSupplement?.visibleTextAlreadyDelivered !== true &&
|
||||
Boolean(draftStream) &&
|
||||
!draftPreviewCommitted &&
|
||||
!observedFinalReplyDelivery &&
|
||||
previewStreamingEnabled &&
|
||||
!payload.text?.trim();
|
||||
|
||||
@@ -923,6 +937,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
ttsSupplement &&
|
||||
draftStream &&
|
||||
!draftPreviewCommitted &&
|
||||
!observedFinalReplyDelivery &&
|
||||
previewStreamingEnabled &&
|
||||
!payload.isError &&
|
||||
trimmedFinalText.length > 0
|
||||
@@ -970,6 +985,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
return;
|
||||
}
|
||||
draftPreviewCommitted = true;
|
||||
observedFinalReplyDelivery = true;
|
||||
observedReplyDelivery = true;
|
||||
replyPlan.markSent();
|
||||
await deliverNormally({
|
||||
@@ -987,7 +1003,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
payload,
|
||||
adapter: defineFinalizableLivePreviewAdapter({
|
||||
draft:
|
||||
draftStream && !draftPreviewCommitted
|
||||
draftStream && !draftPreviewCommitted && !observedFinalReplyDelivery
|
||||
? {
|
||||
flush: draftStream.flush,
|
||||
clear: draftStream.clear,
|
||||
@@ -1030,11 +1046,13 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
threadTs: edit.threadTs,
|
||||
});
|
||||
draftPreviewCommitted = true;
|
||||
observedFinalReplyDelivery = true;
|
||||
},
|
||||
onPreviewFinalized: (_preview) => {
|
||||
// The preview edit promotes the draft message into the final answer.
|
||||
// Later same-turn payloads must not let fallback cleanup clear it.
|
||||
draftPreviewCommitted = true;
|
||||
observedFinalReplyDelivery = true;
|
||||
const finalThreadTs = usedReplyThreadTs ?? statusThreadTs;
|
||||
observedReplyDelivery = true;
|
||||
replyPlan.markSent();
|
||||
|
||||
Reference in New Issue
Block a user