From 374c92947afb66623682168ef31aea0f2a07ebbd Mon Sep 17 00:00:00 2001 From: teconomix Date: Fri, 20 Mar 2026 08:21:53 +0000 Subject: [PATCH] fix(mattermost): latch send failures; include patchInterval in onSettled cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: 1. Failure latch (Codex ID=2964357928): add previewSendFailed boolean that is set true in both the initial-send and patch-edit catch blocks (alongside stopPatchInterval). schedulePatch() checks it before re-arming the interval, so subsequent onPartialReply calls during a run with a permanent failure (missing permission, DM-creation error) no longer recreate the timer and retry indefinitely. 2. patchInterval in onSettled guard (Codex ID=2964357925): onSettled now triggers cleanup when patchInterval is non-null, even if streamMessageId and patchSending are still falsy. This covers the window between schedulePatch arming the interval and the first 200ms tick flipping patchSending — if the run ends in that window (same-target messaging-tool sends, empty/heartbeat replies), the interval is now stopped and the pending state is cleared. --- .../mattermost/src/mattermost/monitor.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 299470bff1f..cdddf595c1e 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -1409,6 +1409,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} let lastSentText = ""; let patchInterval: ReturnType | null = null; let patchSending = false; // prevents concurrent network calls + // Latches true after the first send/edit failure to prevent the interval + // from being re-armed by a later onPartialReply call (ID=2964357928). + let previewSendFailed = false; const STREAM_PATCH_INTERVAL_MS = 200; // Edit-in-place streaming is opt-in: only activate when blockStreaming is @@ -1475,6 +1478,8 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const schedulePatch = (fullText: string) => { if (!blockStreamingClient) return; + // Do not re-arm if a permanent send/edit failure has been latched. + if (previewSendFailed) return; pendingPatchText = fullText; if (patchInterval) return; patchInterval = setInterval(() => { @@ -1499,9 +1504,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} runtime.log?.(`stream-patch started ${streamMessageId}`); } catch (err) { logVerboseMessage(`mattermost stream-patch send failed: ${String(err)}`); - // Stop retrying on initial-send failure (e.g. missing post permission, - // DM-channel creation failure). Without this the interval keeps firing - // every 200 ms and flooding the API/logs for the rest of the response. + // Latch the failure so schedulePatch() does not re-arm the interval + // on subsequent onPartialReply calls (which would retry indefinitely). + previewSendFailed = true; stopPatchInterval(); } } else { @@ -1514,9 +1519,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} runtime.log?.(`stream-patch edited ${streamMessageId}`); } catch (err) { logVerboseMessage(`mattermost stream-patch edit failed: ${String(err)}`); - // Stop retrying on patch failure — the post may not be editable - // (missing edit_post permission, deleted, etc.). The preview stays - // frozen; final delivery will patch or replace it via deliver(). + // Latch the failure so schedulePatch() does not re-arm the interval + // on subsequent onPartialReply calls. + previewSendFailed = true; stopPatchInterval(); } } @@ -1709,7 +1714,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} // (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) { + if ((streamMessageId || patchSending || patchInterval) && blockStreamingClient) { stopPatchInterval(); pendingPatchText = ""; lastSentText = "";