From c979ed3a3a8746607b974a6290dce9496b20d770 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 18:43:11 -0700 Subject: [PATCH] fix(channels): pass raw progress detail to drafts --- CHANGELOG.md | 1 + .../monitor/message-handler.process.test.ts | 33 +++++++++++++++++++ .../src/monitor/message-handler.process.ts | 16 +++++---- .../matrix/src/matrix/monitor/handler.ts | 15 +++++---- extensions/msteams/src/reply-dispatcher.ts | 16 +++++---- .../src/monitor/message-handler/dispatch.ts | 15 +++++---- .../telegram/src/bot-message-dispatch.ts | 15 +++++---- src/auto-reply/get-reply-options.types.ts | 1 + .../reply/agent-runner-execution.test.ts | 33 +++++++++++++++++++ .../reply/agent-runner-execution.ts | 1 + src/plugin-sdk/channel-streaming.test.ts | 10 ++++++ 11 files changed, 126 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20d004dabc1..4b3092c00da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai - Channels/CLI: keep `openclaw channels list --json` usable when provider usage fetching fails, and report per-provider usage errors without aborting the channel list. Refs #67595. - Agents/messaging: deliver distinct final commentary after same-target `message` tool sends while still deduping text/media already sent by the tool, so short closing remarks are no longer silently dropped. Fixes #76915. Thanks @hclsys. - Agents/messaging: preserve string thread IDs when matching message-tool reply dedupe routes, avoiding precision loss on numeric-looking topic IDs before channel plugin comparison. Thanks @vincentkoc. +- Channels/streaming: honor `agents.defaults.toolProgressDetail: "raw"` in Slack, Discord, Telegram, Matrix, and Microsoft Teams progress drafts, so tool-start lines include raw command/detail output when debugging. Thanks @vincentkoc. - OpenAI Codex: honor `auth.order.openai-codex` when starting app-server clients without an explicit auth profile, so status/model probes and implicit startup use the configured Codex account instead of falling back to the default profile. Thanks @vincentkoc. - OpenAI Codex: let SSRF-guarded provider requests inherit OpenClaw's undici IPv4/IPv6 fallback policy, so ChatGPT-backed Codex runs recover on IPv4-working hosts when DNS still returns unreachable IPv6 addresses. Fixes #76857. Thanks @jplavoiemtl and @SymbolStar. - 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. diff --git a/extensions/discord/src/monitor/message-handler.process.test.ts b/extensions/discord/src/monitor/message-handler.process.test.ts index 4f51b681940..77e8e170de6 100644 --- a/extensions/discord/src/monitor/message-handler.process.test.ts +++ b/extensions/discord/src/monitor/message-handler.process.test.ts @@ -102,6 +102,7 @@ type DispatchInboundParams = { name?: string; phase?: string; args?: Record; + detailMode?: "explain" | "raw"; }) => Promise | void; onItemEvent?: (payload: { progressText?: string; @@ -1534,6 +1535,38 @@ describe("processDiscordMessage draft streaming", () => { ); }); + it("uses raw tool-progress detail in Discord progress drafts", async () => { + const draftStream = createMockDraftStreamForTest(); + + dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => { + await params?.replyOptions?.onToolStart?.({ + name: "exec", + phase: "start", + args: { command: "pnpm test -- --watch=false" }, + detailMode: "raw", + }); + await params?.replyOptions?.onItemEvent?.({ progressText: "done" }); + return createNoQueuedDispatchResult(); + }); + + const ctx = await createAutomaticSourceDeliveryContext({ + discordConfig: { + streaming: { + mode: "progress", + progress: { + label: "Shelling", + }, + }, + }, + }); + + await runProcessDiscordMessage(ctx); + + expect(draftStream.update).toHaveBeenCalledWith( + "Shelling\n🛠️ Exec: run tests, `pnpm test -- --watch=false`\n• done", + ); + }); + it("keeps Discord progress lines across assistant boundaries", async () => { const draftStream = createMockDraftStreamForTest(); diff --git a/extensions/discord/src/monitor/message-handler.process.ts b/extensions/discord/src/monitor/message-handler.process.ts index 414226ab79d..4d5162a1b32 100644 --- a/extensions/discord/src/monitor/message-handler.process.ts +++ b/extensions/discord/src/monitor/message-handler.process.ts @@ -91,6 +91,7 @@ type ToolStartPayload = { name?: string; phase?: string; args?: Record; + detailMode?: "explain" | "raw"; }; function readToolStringArg(args: Record, key: string): string | undefined { @@ -668,12 +669,15 @@ export async function processDiscordMessage( await maybeBindStatusReactionsToToolReaction(payload); await statusReactions.setTool(payload.name); await draftPreview.pushToolProgress( - formatChannelProgressDraftLine({ - event: "tool", - name: payload.name, - phase: payload.phase, - args: payload.args, - }), + formatChannelProgressDraftLine( + { + event: "tool", + name: payload.name, + phase: payload.phase, + args: payload.args, + }, + payload.detailMode ? { detailMode: payload.detailMode } : undefined, + ), { toolName: payload.name }, ); }, diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index d648629442d..7fc2bee3cb0 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -1580,12 +1580,15 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam onToolStart: async (payload) => { const toolName = payload.name?.trim(); await pushPreviewToolProgress( - formatChannelProgressDraftLine({ - event: "tool", - name: toolName, - phase: payload.phase, - args: payload.args, - }), + formatChannelProgressDraftLine( + { + event: "tool", + name: toolName, + phase: payload.phase, + args: payload.args, + }, + payload.detailMode ? { detailMode: payload.detailMode } : undefined, + ), { toolName }, ); }, diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index c4600510974..07cceda2822 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -381,14 +381,18 @@ export function createMSTeamsReplyDispatcher(params: { name?: string; phase?: string; args?: Record; + detailMode?: "explain" | "raw"; }) => { await streamController.pushProgressLine( - formatChannelProgressDraftLine({ - event: "tool", - name: payload.name, - phase: payload.phase, - args: payload.args, - }), + formatChannelProgressDraftLine( + { + event: "tool", + name: payload.name, + phase: payload.phase, + args: payload.args, + }, + payload.detailMode ? { detailMode: payload.detailMode } : undefined, + ), { toolName: payload.name }, ); }, diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 20c8abddbb1..4f9700af265 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -1084,12 +1084,15 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag await statusReactions.setTool(payload.name); } await pushPreviewToolProgress( - formatChannelProgressDraftLine({ - event: "tool", - name: payload.name, - phase: payload.phase, - args: payload.args, - }), + formatChannelProgressDraftLine( + { + event: "tool", + name: payload.name, + phase: payload.phase, + args: payload.args, + }, + payload.detailMode ? { detailMode: payload.detailMode } : undefined, + ), { toolName: payload.name }, ); }, diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index b7ce0ad8bb8..ffda126b665 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -1174,12 +1174,15 @@ export const dispatchTelegramMessage = async ({ await statusReactionController.setTool(toolName); } await pushPreviewToolProgress( - formatChannelProgressDraftLine({ - event: "tool", - name: toolName, - phase: payload.phase, - args: payload.args, - }), + formatChannelProgressDraftLine( + { + event: "tool", + name: toolName, + phase: payload.phase, + args: payload.args, + }, + payload.detailMode ? { detailMode: payload.detailMode } : undefined, + ), { toolName }, ); }, diff --git a/src/auto-reply/get-reply-options.types.ts b/src/auto-reply/get-reply-options.types.ts index 26c7c430a56..c620a11943d 100644 --- a/src/auto-reply/get-reply-options.types.ts +++ b/src/auto-reply/get-reply-options.types.ts @@ -85,6 +85,7 @@ export type GetReplyOptions = { name?: string; phase?: string; args?: Record; + detailMode?: "explain" | "raw"; }) => Promise | void; /** Called when a concrete work item starts, updates, or completes. */ onItemEvent?: (payload: { diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index 2fbb1e32bcc..c855e21282d 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -1142,6 +1142,39 @@ describe("runAgentTurnWithFallback", () => { }); }); + it("forwards raw tool progress detail mode to tool-start reply options", async () => { + const onToolStart = vi.fn(); + state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + await params.onAgentEvent?.({ + stream: "tool", + data: { + name: "exec", + phase: "start", + args: { command: "pnpm test -- --watch=false" }, + }, + }); + return { payloads: [{ text: "final" }], meta: {} }; + }); + + const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); + const result = await runAgentTurnWithFallback({ + ...createMinimalRunAgentTurnParams({ + opts: { + onToolStart, + } satisfies GetReplyOptions, + }), + toolProgressDetail: "raw", + }); + + expect(result.kind).toBe("success"); + expect(onToolStart).toHaveBeenCalledWith({ + name: "exec", + phase: "start", + args: { command: "pnpm test -- --watch=false" }, + detailMode: "raw", + }); + }); + it("publishes Codex app-server telemetry to agent event subscribers", async () => { const agentEvents = await import("../../infra/agent-events.js"); const emitAgentEvent = vi.mocked(agentEvents.emitAgentEvent); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 4ed5d086d1b..f80869bed86 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -1537,6 +1537,7 @@ export async function runAgentTurnWithFallback(params: { evt.data.args && typeof evt.data.args === "object" ? (evt.data.args as Record) : undefined, + detailMode: params.toolProgressDetail, }); } } diff --git a/src/plugin-sdk/channel-streaming.test.ts b/src/plugin-sdk/channel-streaming.test.ts index 51426647cc9..91f31ffebe5 100644 --- a/src/plugin-sdk/channel-streaming.test.ts +++ b/src/plugin-sdk/channel-streaming.test.ts @@ -208,6 +208,16 @@ describe("channel-streaming", () => { modified: ["/tmp/demo/index.html", "/tmp/demo/style.css"], }), ).toBe("🩹 Apply Patch: /tmp/demo/{index.html, style.css}"); + expect( + formatChannelProgressDraftLine( + { + event: "tool", + name: "exec", + args: { command: "pnpm test -- --watch=false" }, + }, + { detailMode: "raw" }, + ), + ).toBe("🛠️ Exec: run tests, `pnpm test -- --watch=false`"); }); it("starts progress drafts after five seconds or a second work event", async () => {