Skip to content

Commit 7619a2d

Browse files
fix(slack): finalize TTS hook ownership
Co-authored-by: Rishi Tamrakar <[email protected]>
1 parent 7d824f6 commit 7619a2d

2 files changed

Lines changed: 96 additions & 15 deletions

File tree

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,8 @@ vi.mock("../config.runtime.js", () => ({
834834

835835
vi.mock("../replies.js", () => ({
836836
createSlackReplyDeliveryPlan: () => ({
837-
peekThreadTs: () => mockedReplyThreadTsSequence?.[0] ?? mockedReplyThreadTs,
837+
peekThreadTs: () =>
838+
mockedReplyThreadTsSequence ? mockedReplyThreadTsSequence[0] : mockedReplyThreadTs,
838839
nextThreadTs: () =>
839840
mockedReplyThreadTsSequence ? mockedReplyThreadTsSequence.shift() : mockedReplyThreadTs,
840841
markSent: () => {},
@@ -3094,6 +3095,51 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
30943095
]);
30953096
});
30963097

3098+
it("defers hooks and suppresses duplicate TTS finals when flush creates the preview id", async () => {
3099+
let flushed = false;
3100+
const draftStream = {
3101+
...createDraftStreamStub(),
3102+
flush: vi.fn(async () => {
3103+
flushed = true;
3104+
}),
3105+
clear: vi.fn(noopAsync),
3106+
discardPending: vi.fn(noopAsync),
3107+
seal: vi.fn(noopAsync),
3108+
messageId: () => (flushed ? "171234.567" : undefined),
3109+
};
3110+
createSlackDraftStreamMock.mockReturnValueOnce(draftStream);
3111+
finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined);
3112+
mockedSlackIsThreadReply = false;
3113+
mockedReplyThreadTsSequence = [undefined, undefined];
3114+
const payload = {
3115+
text: "Spoken answer",
3116+
mediaUrl: "https://example.com/tts.mp3",
3117+
audioAsVoice: true,
3118+
spokenText: "Spoken answer",
3119+
ttsSupplement: { spokenText: "Spoken answer" },
3120+
};
3121+
mockedDispatchSequence = [
3122+
{ kind: "final", payload },
3123+
{ kind: "final", payload },
3124+
];
3125+
3126+
await dispatchPreparedSlackMessage(
3127+
createPreparedSlackMessage({
3128+
message: { thread_ts: undefined },
3129+
replyToMode: "first",
3130+
}),
3131+
);
3132+
3133+
expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1);
3134+
expect(deliverRepliesMock).toHaveBeenCalledTimes(1);
3135+
const delivered = requireRecord(
3136+
requireMockCall(deliverRepliesMock, 0, "deliver replies")[0],
3137+
"deliver replies params",
3138+
);
3139+
expectRecordFields(delivered, { replyThreadTs: THREAD_TS });
3140+
expect(emitSlackMessageSentHooksMock).not.toHaveBeenCalled();
3141+
});
3142+
30973143
it("suppresses duplicate TTS supplement finals after preview finalization", async () => {
30983144
const draftStream = {
30993145
...createDraftStreamStub(),
@@ -3104,6 +3150,8 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
31043150
};
31053151
createSlackDraftStreamMock.mockReturnValueOnce(draftStream);
31063152
finalizeSlackPreviewEditMock.mockResolvedValueOnce(undefined);
3153+
mockedSlackIsThreadReply = false;
3154+
mockedReplyThreadTsSequence = [undefined];
31073155
const payload = {
31083156
text: "Spoken answer",
31093157
mediaUrl: "https://example.com/tts.mp3",
@@ -3116,14 +3164,20 @@ describe("dispatchPreparedSlackMessage preview fallback", () => {
31163164
{ kind: "final", payload },
31173165
];
31183166

3119-
await dispatchPreparedSlackMessage(createPreparedSlackMessage());
3167+
await dispatchPreparedSlackMessage(
3168+
createPreparedSlackMessage({
3169+
message: { thread_ts: undefined },
3170+
replyToMode: "first",
3171+
}),
3172+
);
31203173

31213174
expect(finalizeSlackPreviewEditMock).toHaveBeenCalledTimes(1);
31223175
expect(deliverRepliesMock).toHaveBeenCalledTimes(1);
31233176
const delivered = requireRecord(
31243177
requireMockCall(deliverRepliesMock, 0, "deliver replies")[0],
31253178
"deliver replies params",
31263179
);
3180+
expectRecordFields(delivered, { replyThreadTs: THREAD_TS });
31273181
expect(delivered.replies).toEqual([
31283182
{
31293183
mediaUrl: "https://example.com/tts.mp3",

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

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,19 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
805805
}
806806
};
807807
const deliveryTracker = createSlackEventDeliveryTracker();
808+
const markPreviewPayloadDelivered = (params: {
809+
kind: ReplyDispatchKind;
810+
payload: ReplyPayload;
811+
threadTs: string | undefined;
812+
}) => {
813+
deliveryTracker.markDelivered(params);
814+
// Single-use reply modes move later same-turn payloads off the preview
815+
// thread, so protect both delivery keys from duplicates.
816+
const nextThreadTs = replyPlan.peekThreadTs();
817+
if (nextThreadTs !== params.threadTs) {
818+
deliveryTracker.markDelivered({ ...params, threadTs: nextThreadTs });
819+
}
820+
};
808821
const resolveDeliveryThreadTs = (params: {
809822
kind: ReplyDispatchKind;
810823
forcedThreadTs?: string;
@@ -908,7 +921,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
908921
payload: ReplyPayload;
909922
kind: ReplyDispatchKind;
910923
forcedThreadTs?: string;
911-
}): Promise<void> => {
924+
}): Promise<string | undefined> => {
912925
if (params.payload.isReasoning === true) {
913926
return;
914927
}
@@ -925,7 +938,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
925938
})
926939
) {
927940
logVerbose("slack: suppressed duplicate normal delivery within the same turn");
928-
return;
941+
return deliveryReplyThreadTs;
929942
}
930943
await deliverReplies({
931944
cfg: ctx.cfg,
@@ -958,6 +971,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
958971
payload: params.payload,
959972
threadTs: deliveryReplyThreadTs,
960973
});
974+
return deliveryReplyThreadTs;
961975
};
962976

963977
const deliverBufferedStreamFallback = async (params: {
@@ -1311,7 +1325,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
13111325
kind: info.kind,
13121326
forcedThreadTs: finalThreadTs,
13131327
});
1314-
deliveryTracker.markDelivered({ kind: info.kind, payload, threadTs: finalThreadTs });
1328+
markPreviewPayloadDelivered({ kind: info.kind, payload, threadTs: finalThreadTs });
13151329
return;
13161330
}
13171331
}
@@ -1363,14 +1377,16 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
13631377
...(edit.blocks?.length ? { blocks: edit.blocks } : {}),
13641378
threadTs: edit.threadTs,
13651379
});
1366-
emitSlackMessageSentHooks({
1367-
...messageSentHookContext,
1368-
to: messageSentHookTarget,
1369-
accountId: account.accountId,
1370-
content: trimmedFinalText,
1371-
success: true,
1372-
messageId: preview.messageId,
1373-
});
1380+
if (!ttsSupplement) {
1381+
emitSlackMessageSentHooks({
1382+
...messageSentHookContext,
1383+
to: messageSentHookTarget,
1384+
accountId: account.accountId,
1385+
content: trimmedFinalText,
1386+
success: true,
1387+
messageId: preview.messageId,
1388+
});
1389+
}
13741390
draftPreviewCommitted = true;
13751391
observedFinalReplyDelivery = true;
13761392
},
@@ -1382,14 +1398,25 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
13821398
const finalThreadTs = usedReplyThreadTs ?? statusThreadTs;
13831399
observedReplyDelivery = true;
13841400
replyPlan.markSent();
1385-
deliveryTracker.markDelivered({ kind: info.kind, payload, threadTs: finalThreadTs });
1401+
// Supplemental TTS media is the terminal delivery for the logical
1402+
// payload. Marking the preview first would suppress that media send.
1403+
if (!ttsSupplement) {
1404+
markPreviewPayloadDelivered({ kind: info.kind, payload, threadTs: finalThreadTs });
1405+
}
13861406
},
13871407
buildSupplementalPayload: () =>
13881408
ttsSupplement ? buildTtsSupplementMediaPayload(payload) : undefined,
13891409
deliverSupplemental: async (supplementalPayload) => {
1390-
await deliverNormally({
1410+
const previewThreadTs = usedReplyThreadTs ?? statusThreadTs;
1411+
const supplementalThreadTs = await deliverNormally({
13911412
payload: supplementalPayload,
13921413
kind: info.kind,
1414+
forcedThreadTs: previewThreadTs,
1415+
});
1416+
markPreviewPayloadDelivered({
1417+
kind: info.kind,
1418+
payload,
1419+
threadTs: supplementalThreadTs,
13931420
});
13941421
},
13951422
logPreviewEditFailure: (err) => {

0 commit comments

Comments
 (0)