From a3a969be5d22aedf2551d5e8ec7bcdd43a3cd69d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Jul 2026 01:40:55 -0400 Subject: [PATCH] fix(cli): avoid duplicate commentary delivery (#115596) --- src/auto-reply/get-reply-options.types.ts | 1 + .../reply/agent-runner-cli-candidate.ts | 2 + ...nt-runner-execution-cli-commentary.test.ts | 1 + .../dispatch-from-config.prepare-execution.ts | 13 +++-- ...ispatch-from-config.progress.test-utils.ts | 55 +++++++++++++++++++ 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/auto-reply/get-reply-options.types.ts b/src/auto-reply/get-reply-options.types.ts index 2cb5cb47bce4..450977d40c36 100644 --- a/src/auto-reply/get-reply-options.types.ts +++ b/src/auto-reply/get-reply-options.types.ts @@ -209,6 +209,7 @@ export type GetReplyOptions = { meta?: string; approvalId?: string; approvalSlug?: string; + suppressDurableProgress?: true; }) => Promise | ProgressCallbackResult; /** * Called when the utility-model narration of the in-progress turn changes. diff --git a/src/auto-reply/reply/agent-runner-cli-candidate.ts b/src/auto-reply/reply/agent-runner-cli-candidate.ts index 8c4a118c849a..d201993dbc55 100644 --- a/src/auto-reply/reply/agent-runner-cli-candidate.ts +++ b/src/auto-reply/reply/agent-runner-cli-candidate.ts @@ -257,6 +257,8 @@ export async function runCliFallbackCandidate(params: { itemId: payload.itemId, kind: "preamble", progressText: payload.text, + // The block bridge owns durability; this event remains a progress preview. + ...(bridgeCliDurableCommentary ? { suppressDurableProgress: true } : {}), }), ); } diff --git a/src/auto-reply/reply/agent-runner-execution-cli-commentary.test.ts b/src/auto-reply/reply/agent-runner-execution-cli-commentary.test.ts index d513ccdccfa4..f1438cdb2591 100644 --- a/src/auto-reply/reply/agent-runner-execution-cli-commentary.test.ts +++ b/src/auto-reply/reply/agent-runner-execution-cli-commentary.test.ts @@ -188,6 +188,7 @@ describe("executeAgentTurn: CLI durable commentary", () => { itemId: "commentary-durable-1", kind: "preamble", progressText: "The durable findings live here.", + suppressDurableProgress: true, }); }); expect(result.kind).toBe("success"); diff --git a/src/auto-reply/reply/dispatch-from-config.prepare-execution.ts b/src/auto-reply/reply/dispatch-from-config.prepare-execution.ts index 0919627a98e4..70910cd46702 100644 --- a/src/auto-reply/reply/dispatch-from-config.prepare-execution.ts +++ b/src/auto-reply/reply/dispatch-from-config.prepare-execution.ts @@ -422,14 +422,18 @@ export async function prepareDispatchExecution(state: ChooseDispatchRouteReadySt suppressAutomaticSourceDelivery && allowSuppressedSourceProgressCallbacks && canForwardItemEvents; + const shouldDeliverDurableCommentaryProgress = ( + payload: Parameters>[0], + ) => + deliverStandaloneCommentaryProgress && + payload.kind === "preamble" && + payload.suppressDurableProgress !== true; const forwardItemEvent = canForwardItemEvents ? wrapProgressCallback(params.replyOptions?.onItemEvent, { ...itemEventForwardingOptions, waitForDirectBlockReplyDelivery: true, onForward: (payload) => - preserveProgressCallbackStartOrder && - deliverStandaloneCommentaryProgress && - payload.kind === "preamble" + preserveProgressCallbackStartOrder && shouldDeliverDurableCommentaryProgress(payload) ? noteCommentaryProgress(payload) : undefined, onVisible: (payload) => { @@ -453,8 +457,7 @@ export async function prepareDispatchExecution(state: ChooseDispatchRouteReadySt } if ( (!forwardItemEvent || !preserveProgressCallbackStartOrder) && - deliverStandaloneCommentaryProgress && - payload.kind === "preamble" + shouldDeliverDurableCommentaryProgress(payload) ) { await noteCommentaryProgress(payload); } diff --git a/src/auto-reply/reply/dispatch-from-config.progress.test-utils.ts b/src/auto-reply/reply/dispatch-from-config.progress.test-utils.ts index c9e302909dc8..611c9aab003a 100644 --- a/src/auto-reply/reply/dispatch-from-config.progress.test-utils.ts +++ b/src/auto-reply/reply/dispatch-from-config.progress.test-utils.ts @@ -92,6 +92,61 @@ describe("dispatchReplyFromConfig", () => { expect(activeDuringOffRun).toBe(false); }); + it("keeps block-owned commentary out of the standalone durable progress lane", async () => { + setNoAbort(); + sessionStoreMocks.currentEntry = { + verboseLevel: "on", + }; + const dispatcher = createDispatcher(); + const ctx = buildTestCtx({ + Provider: "discord", + Surface: "discord", + ChatType: "direct", + }); + const onItemEvent = vi.fn(); + + const replyResolver = async ( + _ctx: MsgContext, + opts?: GetReplyOptions, + _cfg?: OpenClawConfig, + ) => { + await opts?.onItemEvent?.({ + itemId: "commentary-1", + kind: "preamble", + progressText: "Inspecting the dispatch path.", + suppressDurableProgress: true, + }); + await opts?.onBlockReply?.({ text: "Inspecting the dispatch path." }); + return { text: "Done." } satisfies ReplyPayload; + }; + + await dispatchReplyFromConfig({ + ctx, + cfg: emptyConfig, + dispatcher, + replyResolver, + replyOptions: { + suppressDefaultToolProgressMessages: true, + commentaryProgressEnabled: true, + progressPreambleEnabled: true, + commentaryPayloadsEnabled: true, + onItemEvent, + }, + }); + + expect(onItemEvent).toHaveBeenCalledExactlyOnceWith({ + itemId: "commentary-1", + kind: "preamble", + progressText: "Inspecting the dispatch path.", + suppressDurableProgress: true, + }); + expect(dispatcher.sendToolResult).not.toHaveBeenCalled(); + expect(dispatcher.sendBlockReply).toHaveBeenCalledExactlyOnceWith({ + text: "Inspecting the dispatch path.", + }); + expect(dispatcher.sendFinalReply).toHaveBeenCalledExactlyOnceWith({ text: "Done." }); + }); + it("forwards channel-owned group progress callbacks while source delivery is suppressed", async () => { setNoAbort(); sessionStoreMocks.currentEntry = {