From 0ecdda1433e0694f33a5907e8289629ac650ba63 Mon Sep 17 00:00:00 2001 From: teconomix Date: Fri, 20 Mar 2026 07:15:22 +0000 Subject: [PATCH] fix(mattermost): wait for in-flight preview send in onSettled cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the first preview POST is still in flight (patchSending=true, streamMessageId=null), the previous onSettled check was skipped entirely — the POST would resolve after cleanup and leave an orphaned preview post with no interval to clear it. Fix: trigger cleanup when either streamMessageId is set OR patchSending is true. Stop the interval immediately, clear pending state, then wait up to 3s for patchSending to clear before capturing the final streamMessageId and deleting the post. --- .../mattermost/src/mattermost/monitor.ts | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 391ccb890e9..299470bff1f 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -1704,16 +1704,31 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} // the reply pipeline produces no final payload (e.g. messaging-tool sends that // are suppressed, or empty/heartbeat responses). Without this, onPartialReply // can create a Mattermost post that is never deleted or patched with final text. - if (streamMessageId && blockStreamingClient) { + // + // We must also handle the race where the first preview POST is still in flight + // (patchSending=true, streamMessageId=null): stopPatchInterval() prevents new + // ticks, and the async cleanup waits for patchSending to clear so it can capture + // the messageId that the in-flight send will set. + if ((streamMessageId || patchSending) && blockStreamingClient) { stopPatchInterval(); - const orphanId = streamMessageId; - streamMessageId = null; pendingPatchText = ""; lastSentText = ""; - patchSending = false; - void deleteMattermostPost(blockStreamingClient, orphanId).catch(() => { - // Best-effort — the run is already complete. - }); + const client = blockStreamingClient; + void (async () => { + // Wait for any in-flight send/patch to settle so we get the final messageId. + const deadline = Date.now() + 3000; + while (patchSending && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 50)); + } + patchSending = false; + const orphanId = streamMessageId; + streamMessageId = null; + if (orphanId) { + await deleteMattermostPost(client, orphanId).catch(() => { + // Best-effort — the run is already complete. + }); + } + })(); } }, run: () =>