fix(mattermost): wait for in-flight preview send before divergent delivery; stop retrying failed patches

Two fixes:

1. Divergent-target flush (Codex ID=2962544342): when replyTargetDiverged is true
   the flush is skipped to avoid creating a preview in the wrong thread, but the
   patch interval was not stopped and any in-flight first sendMessageMattermost
   was not awaited. If that send resolved after the divergent delivery returned,
   it created a stray preview post with no cleanup path. Fix: always stop the
   interval and wait for patchSending to settle (up to 2s) even on the divergent
   path, so streamMessageId is populated if the send resolves during this window
   and the orphan cleanup below can capture and delete it.

2. Patch-failure retry storm (Codex ID=2962544347): after a patchMattermostPost
   failure in the schedulePatch interval, streamMessageId remained set and every
   subsequent 200ms tick retried the same failing request, spamming the API until
   final delivery. Fix: call stopPatchInterval() in the catch block so retries
   stop immediately. The preview stays frozen at its last successful text; deliver()
   will patch or replace it when the final reply arrives.
This commit is contained in:
teconomix 2026-03-20 03:53:16 +00:00
parent 59db1c2b67
commit 38f3e76f97

View File

@ -1510,6 +1510,10 @@ 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().
stopPatchInterval();
}
}
} finally {
@ -1537,10 +1541,22 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
const replyTargetDiverged =
finalReplyToId !== effectiveReplyToId && payload.replyToId != null;
// Flush any pending partial-reply patch before final delivery —
// but only when the reply stays in the same thread as the preview post.
if (isFinal && blockStreamingClient && !replyTargetDiverged) {
await flushPendingPatch();
if (isFinal && blockStreamingClient) {
if (replyTargetDiverged) {
// Divergent target: don't flush (we don't want to create a preview in
// the wrong thread), but do stop the interval and wait for any in-flight
// tick/send to settle. This ensures streamMessageId is populated if the
// first sendMessageMattermost resolves during this window, so the orphan
// cleanup below can capture and delete it.
stopPatchInterval();
const deadline = Date.now() + 2000;
while (patchSending && Date.now() < deadline) {
await new Promise<void>((r) => setTimeout(r, 20));
}
} else {
// Same thread: flush pending patches normally.
await flushPendingPatch();
}
}
// Final + streaming active: patch the streamed message with authoritative