fix(mattermost): compute replyTargetDiverged before flush; deliver-before-delete for divergent thread; fix disableBlockStreaming undefined

Three fixes from latest Codex review:

1. Compute replyTargetDiverged before flushPendingPatch: previously the flush
   always ran first, potentially creating a preview post under effectiveReplyToId
   even when the final payload would land in a different thread. Now flush is
   skipped when the target diverges, avoiding a transient post in the wrong thread.

2. Divergent-thread cleanup order: when replyTargetDiverged, deliver the correct
   message first and delete the orphaned preview only afterward. This matches the
   same pattern as the fallback path — if delivery fails, the user keeps the
   partial preview rather than losing all visible output.

3. disableBlockStreaming: changed fallback from false to undefined so accounts
   without an explicit blockStreaming setting preserve the agent blockStreamingDefault
   instead of having block streaming forced on.
This commit is contained in:
teconomix 2026-03-18 14:43:22 +00:00
parent 276e5d735b
commit e1e572b9ca

View File

@ -1528,23 +1528,24 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
deliver: async (payload: ReplyPayload, info) => {
const isFinal = info.kind === "final";
// Flush any pending partial-reply patch before final delivery.
if (isFinal && blockStreamingClient) {
// Compute reply target divergence before flushing, so we don't
// accidentally create a preview post in the wrong thread on flush.
const finalReplyToId = resolveMattermostReplyRootId({
threadRootId: effectiveReplyToId,
replyToId: payload.replyToId,
});
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();
}
// Final + streaming active: patch the streamed message with authoritative
// complete text, or fall back to a new message (with orphan cleanup).
// If the final payload carries an explicit replyToId that differs from
// the one the streaming post was created under, skip the in-place patch
// and fall through to normal delivery so the reply lands in the right thread.
const finalReplyToId = resolveMattermostReplyRootId({
threadRootId: effectiveReplyToId,
replyToId: payload.replyToId,
});
const streamReplyToId = effectiveReplyToId;
const replyTargetDiverged =
finalReplyToId !== streamReplyToId && payload.replyToId != null;
// (When replyTargetDiverged the preview is cleaned up further below.)
if (isFinal && streamMessageId && payload.text && !replyTargetDiverged) {
const text = core.channel.text.convertMarkdownTables(payload.text, tableMode);
try {
@ -1596,22 +1597,39 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
if (isFinal) {
stopPatchInterval();
// If the reply target diverged and we have an orphaned stream post,
// attempt to delete it before normal delivery creates the correct post.
if (replyTargetDiverged && streamMessageId) {
const orphanId = streamMessageId;
streamMessageId = null;
try {
await deleteMattermostPost(blockStreamingClient!, orphanId);
} catch {
// Ignore — delivering to the correct thread takes priority.
}
} else {
streamMessageId = null;
}
// Capture and clear the stream ID so normal delivery below can proceed.
// If the reply target diverged we hold the orphan ID and delete it
// *after* the replacement message is successfully sent (see below).
const orphanedStreamId = replyTargetDiverged ? streamMessageId : null;
streamMessageId = null;
pendingPatchText = "";
lastSentText = "";
patchSending = false;
if (!orphanedStreamId) {
// No divergence — fall through to normal delivery.
} else {
// Divergent target: deliver to the correct thread first, then clean
// up the orphan. If delivery fails the user keeps the partial preview.
await deliverMattermostReplyPayload({
core,
cfg,
payload,
to,
accountId: account.accountId,
agentId: route.agentId,
replyToId: finalReplyToId,
textLimit,
tableMode,
sendMessage: sendMessageMattermost,
});
try {
await deleteMattermostPost(blockStreamingClient!, orphanedStreamId);
} catch {
// Ignore — the complete message was already delivered.
}
return;
}
}
// Normal delivery — streaming not active or non-final partial.
@ -1664,7 +1682,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
? true
: typeof account.blockStreaming === "boolean"
? !account.blockStreaming
: false,
: undefined,
onModelSelected,
},
}),