fix(channels): pass raw progress detail to drafts

This commit is contained in:
Vincent Koc
2026-05-03 18:43:11 -07:00
parent 0659c58df8
commit c979ed3a3a
11 changed files with 126 additions and 30 deletions

View File

@@ -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.

View File

@@ -102,6 +102,7 @@ type DispatchInboundParams = {
name?: string;
phase?: string;
args?: Record<string, unknown>;
detailMode?: "explain" | "raw";
}) => Promise<void> | 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();

View File

@@ -91,6 +91,7 @@ type ToolStartPayload = {
name?: string;
phase?: string;
args?: Record<string, unknown>;
detailMode?: "explain" | "raw";
};
function readToolStringArg(args: Record<string, unknown>, 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 },
);
},

View File

@@ -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 },
);
},

View File

@@ -381,14 +381,18 @@ export function createMSTeamsReplyDispatcher(params: {
name?: string;
phase?: string;
args?: Record<string, unknown>;
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 },
);
},

View File

@@ -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 },
);
},

View File

@@ -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 },
);
},

View File

@@ -85,6 +85,7 @@ export type GetReplyOptions = {
name?: string;
phase?: string;
args?: Record<string, unknown>;
detailMode?: "explain" | "raw";
}) => Promise<void> | void;
/** Called when a concrete work item starts, updates, or completes. */
onItemEvent?: (payload: {

View File

@@ -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);

View File

@@ -1537,6 +1537,7 @@ export async function runAgentTurnWithFallback(params: {
evt.data.args && typeof evt.data.args === "object"
? (evt.data.args as Record<string, unknown>)
: undefined,
detailMode: params.toolProgressDetail,
});
}
}

View File

@@ -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 () => {