fix(mattermost): set lastSentText after network success; wait for in-flight tick in flush

Race condition: lastSentText was set synchronously before the async send/patch
completed, so a failed request was treated as delivered and subsequent ticks
skipped retrying. flushPendingPatch also didn't wait for in-flight interval ticks,
causing it to exit early (text === lastSentText guard) when a tick had just fired
but hadn't resolved yet, leaving streamMessageId null and forcing final delivery
to send a new post instead of patching the streamed one.

Fixes:
- schedulePatch interval: set lastSentText only after successful send/patch
- flushPendingPatch: wait up to 2s for in-flight patchSending before proceeding
- flushPendingPatch: set lastSentText after network success, not before
This commit is contained in:
teconomix 2026-03-18 07:55:37 +00:00
parent ee6d984950
commit 33678bb973

View File

@ -1427,9 +1427,17 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
const flushPendingPatch = async () => {
stopPatchInterval();
if (!blockStreamingClient) return;
// Wait for any in-flight interval tick to settle before flushing.
// Without this, an interval tick that set lastSentText synchronously but hasn't
// completed the async send yet would cause flushPendingPatch to exit early
// (text === lastSentText guard), leaving streamMessageId null and causing
// final delivery to fall through to a new post instead of patching in place.
const deadline = Date.now() + 2000;
while (patchSending && Date.now() < deadline) {
await new Promise<void>((r) => setTimeout(r, 20));
}
const text = pendingPatchText;
if (!text || text === lastSentText) return;
lastSentText = text;
if (!streamMessageId) {
try {
const result = await sendMessageMattermost(to, text, {
@ -1437,6 +1445,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
replyToId: effectiveReplyToId,
});
streamMessageId = result.messageId;
lastSentText = text;
runtime.log?.(`stream-patch started ${streamMessageId}`);
} catch (err) {
logVerboseMessage(`mattermost stream-patch flush send failed: ${String(err)}`);
@ -1447,6 +1456,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
postId: streamMessageId,
message: text,
});
lastSentText = text;
runtime.log?.(`stream-patch flushed ${streamMessageId}`);
} catch (err) {
logVerboseMessage(`mattermost stream-patch flush failed: ${String(err)}`);
@ -1461,7 +1471,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
patchInterval = setInterval(() => {
const text = pendingPatchText;
if (!text || text === lastSentText || patchSending) return;
lastSentText = text;
patchSending = true;
void (async () => {
try {
@ -1472,6 +1481,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
replyToId: effectiveReplyToId,
});
streamMessageId = result.messageId;
lastSentText = text;
runtime.log?.(`stream-patch started ${streamMessageId}`);
} catch (err) {
logVerboseMessage(`mattermost stream-patch send failed: ${String(err)}`);
@ -1482,6 +1492,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
postId: streamMessageId,
message: text,
});
lastSentText = text;
runtime.log?.(`stream-patch edited ${streamMessageId}`);
} catch (err) {
logVerboseMessage(`mattermost stream-patch edit failed: ${String(err)}`);
@ -1546,7 +1557,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
});
return;
}
// Successful final patch: reset all streaming state.
streamMessageId = null;
pendingPatchText = "";
lastSentText = "";
patchSending = false;
return;
}