From d2ba09b3017091408bd52ab353c4b3f59e0c42e0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 16:44:09 -0700 Subject: [PATCH] fix(channels): skip empty progress drafts --- CHANGELOG.md | 1 + .../matrix/src/matrix/monitor/handler.test.ts | 18 +++++++++ .../matrix/src/matrix/monitor/handler.ts | 20 +++++----- .../dispatch.preview-fallback.test.ts | 37 ++++++++++++++++--- .../src/monitor/message-handler/dispatch.ts | 16 ++++---- 5 files changed, 71 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39b09f92781..ceb2c03c44b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys. +- Slack/Matrix: avoid creating blank progress-draft messages when `streaming.progress.label=false` and progress tool lines are disabled. Thanks @vincentkoc. - QA/Matrix: keep the mock OpenAI tool-progress provider aligned with exact-marker Matrix prompts so the hardened live preview scenario still forces a deterministic read before final delivery. Thanks @vincentkoc. - OpenAI/Google Meet: wait for realtime voice `session.updated` before treating the bridge as connected, so Meet joins do not return with audio queued behind an unconfigured realtime session. Thanks @vincentkoc. - Plugins/catalog: merge official external catalog descriptors into partial package channel config metadata, so lagging WeCom/Yuanbao manifests keep their own schema while still exposing host-supplied labels and setup text. Thanks @vincentkoc. diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 026e00bff19..f80a92eaf26 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -2794,6 +2794,24 @@ describe("matrix monitor handler draft streaming", () => { await finish(); }); + it("does not create a blank Matrix progress draft when label and lines are disabled", async () => { + const { dispatch } = createStreamingHarness({ + streaming: "progress", + previewToolProgressEnabled: false, + accountConfig: { + streaming: { mode: "progress", progress: { label: false, toolProgress: false } }, + } as never, + }); + const { opts, finish } = await dispatch(); + + await opts.onItemEvent?.({ progressText: "tool one" }); + await opts.onItemEvent?.({ progressText: "tool two" }); + + expect(opts.suppressDefaultToolProgressMessages).toBe(true); + expect(sendSingleTextMessageMatrixMock).not.toHaveBeenCalled(); + await finish(); + }); + it("keeps partial preview-first finalization on the existing draft when text is unchanged", async () => { const { dispatch, redactEventMock } = createStreamingHarness({ blockStreamingEnabled: true, diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index fa86eb11735..ecea69ab64c 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -1504,15 +1504,17 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam if (!draftStream || !progressDraftStreaming) { return; } - draftStream.update( - formatChannelProgressDraftText({ - entry: progressConfigEntry, - lines: previewToolProgressLines, - seed: progressSeed, - formatLine: formatMatrixToolProgressMarkdownCode, - bullet: "-", - }), - ); + const previewText = formatChannelProgressDraftText({ + entry: progressConfigEntry, + lines: previewToolProgressLines, + seed: progressSeed, + formatLine: formatMatrixToolProgressMarkdownCode, + bullet: "-", + }); + if (!previewText) { + return; + } + draftStream.update(previewText); }; const progressDraftGate = createChannelProgressDraftGate({ onStart: renderProgressDraft, diff --git a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts index 5392517d4c6..e27abfaddd6 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.preview-fallback.test.ts @@ -256,13 +256,17 @@ vi.mock("openclaw/plugin-sdk/channel-streaming", () => ({ }; }, formatChannelProgressDraftText: (params: { - entry?: { streaming?: { progress?: { label?: string; maxLines?: number } } }; + entry?: { streaming?: { progress?: { label?: string | false; maxLines?: number } } }; lines: string[]; - }) => - [ - params.entry?.streaming?.progress?.label ?? "Thinking", + }) => { + const label = params.entry?.streaming?.progress?.label; + return [ + label === false ? undefined : (label ?? "Thinking"), ...params.lines.map((line) => `• ${line}`), - ].join("\n"), + ] + .filter((line): line is string => Boolean(line)) + .join("\n"); + }, resolveChannelProgressDraftMaxLines: (entry?: { streaming?: { progress?: { maxLines?: number } }; }) => entry?.streaming?.progress?.maxLines ?? 8, @@ -731,6 +735,29 @@ describe("dispatchPreparedSlackMessage preview fallback", () => { expect(capturedReplyOptions?.onItemEvent).toBeDefined(); }); + it("does not create a blank Slack progress draft when label and lines are disabled", async () => { + const draftStream = createDraftStreamStub(); + createSlackDraftStreamMock.mockReturnValueOnce(draftStream); + mockedSlackStreamingMode = "progress"; + mockedSlackDraftMode = "status_final"; + mockedDispatchSequence = []; + mockedReplyOptionEvents = [ + { kind: "item", progressText: "tool one" }, + { kind: "item", progressText: "tool two" }, + ]; + + await dispatchPreparedSlackMessage( + createPreparedSlackMessage({ + accountConfig: { + streaming: { mode: "progress", progress: { label: false, toolProgress: false } }, + }, + }), + ); + + expect(capturedReplyOptions?.suppressDefaultToolProgressMessages).toBe(true); + expect(draftStream.update).not.toHaveBeenCalled(); + }); + it("keeps standalone Slack tool progress when partial preview lines are disabled", async () => { mockedSlackStreamingMode = "partial"; mockedSlackDraftMode = "replace"; diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 7a26fd1cd34..126ed919570 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -891,13 +891,15 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag if (!draftStream || streamMode !== "status_final") { return; } - draftStream.update( - formatChannelProgressDraftText({ - entry: account.config, - lines: previewToolProgressLines, - seed: progressSeed, - }), - ); + const previewText = formatChannelProgressDraftText({ + entry: account.config, + lines: previewToolProgressLines, + seed: progressSeed, + }); + if (!previewText) { + return; + } + draftStream.update(previewText); hasStreamedMessage = true; }; const progressDraftGate = createChannelProgressDraftGate({