Summary
Mattermost is the only streaming channel whose progress preview overwrites the entire draft on every tool/progress frame instead of merging updates onto stable lines by line identity. Every other channel uses the shared mergeChannelProgressDraftLine line-identity helper; Mattermost does not.
Evidence
mergeChannelProgressDraftLine (and the buildChannelProgressDraftLineForEntry / formatChannelProgressDraftText family in src/channels/progress-draft-compositor.ts) is used by:
src/channels/streaming.ts, src/channels/progress-draft-compositor.ts (core)
extensions/slack/src/monitor/message-handler/dispatch.ts
extensions/matrix/src/matrix/monitor/handler.ts
extensions/msteams/src/reply-stream-controller.ts
extensions/mattermost has zero usages. Its handlers replace the whole preview each frame:
onToolStart → draftStream.update(buildMattermostToolStatusText(...)) — one flat status line, full overwrite (extensions/mattermost/src/mattermost/monitor.ts:1904)
onItemEvent → formatChannelProgressDraftLineForEntry(...) then draftStream.update(progressText) — full overwrite (extensions/mattermost/src/mattermost/monitor.ts:1915)
Impact
Because each frame overwrites the previous text:
- A tool's
start frame and its later update/end frames don't share a line, so progress for one tool replaces (rather than updates) the line for another.
- Interleaved tools produce churn instead of a stable N-line progress block.
- The
progress.maxLines config (which Mattermost already accepts) can't be honored meaningfully, since there is no maintained list of lines to cap.
Proposed fix
Give Mattermost the same line-identity model the other channels already have:
- Maintain a per-turn
progressLines array next to the draft stream.
- In
onToolStart / onItemEvent, build a line with buildChannelProgressDraftLineForEntry(config, { event, itemId, toolCallId, name, phase, ... }) and merge it with mergeChannelProgressDraftLine(progressLines, line, { maxLines: resolveChannelProgressDraftMaxLines(config) }).
- Render the merged set with
formatChannelProgressDraftText(...) and push that as the single draft update.
- Reset the array at
onAssistantMessageStart / onReasoningEnd boundaries (same shape Discord/Slack use).
Line id is already itemId ?? toolCallId, so repeated frames for the same tool/item update one line in place. This is line-level grouping inside the existing single Mattermost preview post — not one post per tool.
Prior art / validation
We run exactly this as a local Dockerfile patch against the prebuilt bundle on 2026.5.28 (streaming.mode: "progress", toolProgress: true) and it works: in a production run, 177/177 tool events carried toolCallId, tool lines merged by id (one line per toolCallId), and item/message frames merged by itemId instead of overwriting. Happy to open a PR porting it to the extensions/mattermost source with tests if maintainers are open to it.
Environment
- OpenClaw: 2026.5.28
- Mattermost channel,
streaming.mode: "progress", progress.toolProgress: true, progress.maxLines: 8
Summary
Mattermost is the only streaming channel whose progress preview overwrites the entire draft on every tool/progress frame instead of merging updates onto stable lines by line identity. Every other channel uses the shared
mergeChannelProgressDraftLineline-identity helper; Mattermost does not.Evidence
mergeChannelProgressDraftLine(and thebuildChannelProgressDraftLineForEntry/formatChannelProgressDraftTextfamily insrc/channels/progress-draft-compositor.ts) is used by:src/channels/streaming.ts,src/channels/progress-draft-compositor.ts(core)extensions/slack/src/monitor/message-handler/dispatch.tsextensions/matrix/src/matrix/monitor/handler.tsextensions/msteams/src/reply-stream-controller.tsextensions/mattermosthas zero usages. Its handlers replace the whole preview each frame:onToolStart→draftStream.update(buildMattermostToolStatusText(...))— one flat status line, full overwrite (extensions/mattermost/src/mattermost/monitor.ts:1904)onItemEvent→formatChannelProgressDraftLineForEntry(...)thendraftStream.update(progressText)— full overwrite (extensions/mattermost/src/mattermost/monitor.ts:1915)Impact
Because each frame overwrites the previous text:
startframe and its laterupdate/endframes don't share a line, so progress for one tool replaces (rather than updates) the line for another.progress.maxLinesconfig (which Mattermost already accepts) can't be honored meaningfully, since there is no maintained list of lines to cap.Proposed fix
Give Mattermost the same line-identity model the other channels already have:
progressLinesarray next to the draft stream.onToolStart/onItemEvent, build a line withbuildChannelProgressDraftLineForEntry(config, { event, itemId, toolCallId, name, phase, ... })and merge it withmergeChannelProgressDraftLine(progressLines, line, { maxLines: resolveChannelProgressDraftMaxLines(config) }).formatChannelProgressDraftText(...)and push that as the single draft update.onAssistantMessageStart/onReasoningEndboundaries (same shape Discord/Slack use).Line id is already
itemId ?? toolCallId, so repeated frames for the same tool/item update one line in place. This is line-level grouping inside the existing single Mattermost preview post — not one post per tool.Prior art / validation
We run exactly this as a local Dockerfile patch against the prebuilt bundle on 2026.5.28 (
streaming.mode: "progress",toolProgress: true) and it works: in a production run, 177/177 tool events carriedtoolCallId, tool lines merged by id (one line pertoolCallId), and item/message frames merged byitemIdinstead of overwriting. Happy to open a PR porting it to theextensions/mattermostsource with tests if maintainers are open to it.Environment
streaming.mode: "progress",progress.toolProgress: true,progress.maxLines: 8