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 = "";