Skip to content

Commit fb1dfd4

Browse files
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>
1 parent cf47580 commit fb1dfd4

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,42 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
12381238
expect(draftStream.clear).not.toHaveBeenCalled();
12391239
});
12401240

1241+
it("does not reuse draft cleanup after a normally delivered final reply", async () => {
1242+
const draftStream = {
1243+
...createDraftStreamStub(),
1244+
flush: vi.fn(noopAsync),
1245+
clear: vi.fn(noopAsync),
1246+
discardPending: vi.fn(noopAsync),
1247+
seal: vi.fn(noopAsync),
1248+
};
1249+
createSlackDraftStreamMock.mockReturnValueOnce(draftStream);
1250+
mockedDispatchSequence = [
1251+
{
1252+
kind: "final",
1253+
payload: { text: "answer", mediaUrl: "https://example.com/final.png" },
1254+
},
1255+
{ kind: "final", payload: { text: "late cleanup failed", isError: true } },
1256+
];
1257+
1258+
await dispatchPreparedSlackMessage(createPreparedSlackMessage());
1259+
1260+
expect(finalizeSlackPreviewEditMock).not.toHaveBeenCalled();
1261+
expect(deliverRepliesMock).toHaveBeenCalledTimes(2);
1262+
expect(draftStream.clear).toHaveBeenCalledTimes(1);
1263+
const firstDelivered = requireRecord(
1264+
requireMockCall(deliverRepliesMock, 0, "deliver replies")[0],
1265+
"deliver replies params",
1266+
);
1267+
expect(firstDelivered.replies).toEqual([
1268+
{ text: "answer", mediaUrl: "https://example.com/final.png" },
1269+
]);
1270+
const lateDelivered = requireRecord(
1271+
requireMockCall(deliverRepliesMock, 1, "deliver replies")[0],
1272+
"deliver replies params",
1273+
);
1274+
expect(lateDelivered.replies).toEqual([{ text: "late cleanup failed", isError: true }]);
1275+
});
1276+
12411277
it("suppresses block streaming when Slack draft preview streaming is active", async () => {
12421278
mockedBlockStreamingEnabled = true;
12431279

extensions/slack/src/monitor/message-handler/dispatch.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
591591
let usedReplyThreadTs: string | undefined;
592592
let usedBlockReplyThreadTs: string | undefined;
593593
let observedReplyDelivery = false;
594+
let observedFinalReplyDelivery = false;
594595
const deliveryTracker = createSlackEventDeliveryTracker();
595596
const resolveDeliveryThreadTs = (params: {
596597
kind: ReplyDispatchKind;
@@ -693,6 +694,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
693694
...(slackMessageMetadata ? { metadata: slackMessageMetadata } : {}),
694695
});
695696
observedReplyDelivery = true;
697+
if (params.kind === "final") {
698+
observedFinalReplyDelivery = true;
699+
}
696700
const deliveredThreadTs = resolveDeliveredSlackReplyThreadTs({
697701
replyToMode: replyDeliveryMode,
698702
payloadReplyToId: params.payload.replyToId,
@@ -720,6 +724,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
720724
return false;
721725
}
722726
replyPlan.markSent();
727+
if (params.kind === "final") {
728+
observedFinalReplyDelivery = true;
729+
}
723730
deliveryTracker.markDelivered({
724731
kind: params.kind,
725732
payload: params.payload,
@@ -798,6 +805,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
798805
// the SDK reports a real Slack response.
799806
if (streamSession.delivered) {
800807
observedReplyDelivery = true;
808+
if (params.kind === "final") {
809+
observedFinalReplyDelivery = true;
810+
}
801811
}
802812
rememberDeliveredThreadTs(params.kind, streamThreadTs);
803813
replyPlan.markSent();
@@ -829,6 +839,9 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
829839
// optimistic "done" status until Slack acknowledges a flush.
830840
if (streamSession.delivered) {
831841
observedReplyDelivery = true;
842+
if (params.kind === "final") {
843+
observedFinalReplyDelivery = true;
844+
}
832845
}
833846
deliveryTracker.markDelivered({
834847
kind: params.kind,
@@ -915,6 +928,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
915928
ttsSupplement?.visibleTextAlreadyDelivered !== true &&
916929
Boolean(draftStream) &&
917930
!draftPreviewCommitted &&
931+
!observedFinalReplyDelivery &&
918932
previewStreamingEnabled &&
919933
!payload.text?.trim();
920934

@@ -923,6 +937,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
923937
ttsSupplement &&
924938
draftStream &&
925939
!draftPreviewCommitted &&
940+
!observedFinalReplyDelivery &&
926941
previewStreamingEnabled &&
927942
!payload.isError &&
928943
trimmedFinalText.length > 0
@@ -970,6 +985,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
970985
return;
971986
}
972987
draftPreviewCommitted = true;
988+
observedFinalReplyDelivery = true;
973989
observedReplyDelivery = true;
974990
replyPlan.markSent();
975991
await deliverNormally({
@@ -987,7 +1003,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
9871003
payload,
9881004
adapter: defineFinalizableLivePreviewAdapter({
9891005
draft:
990-
draftStream && !draftPreviewCommitted
1006+
draftStream && !draftPreviewCommitted && !observedFinalReplyDelivery
9911007
? {
9921008
flush: draftStream.flush,
9931009
clear: draftStream.clear,
@@ -1030,11 +1046,13 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
10301046
threadTs: edit.threadTs,
10311047
});
10321048
draftPreviewCommitted = true;
1049+
observedFinalReplyDelivery = true;
10331050
},
10341051
onPreviewFinalized: (_preview) => {
10351052
// The preview edit promotes the draft message into the final answer.
10361053
// Later same-turn payloads must not let fallback cleanup clear it.
10371054
draftPreviewCommitted = true;
1055+
observedFinalReplyDelivery = true;
10381056
const finalThreadTs = usedReplyThreadTs ?? statusThreadTs;
10391057
observedReplyDelivery = true;
10401058
replyPlan.markSent();

0 commit comments

Comments
 (0)