Skip to content

Commit 57da466

Browse files
anyechClawsistantOpenClaw Assistantthewilloftheshadowclawsweeper[bot]
authored
Fix Discord verbose tool progress delivery (#80042)
Summary: - The PR changes Discord reply delivery, sanitizer, and queued follow-up auto-reply paths so explicit verbose tool-progress payloads are delivered while final assistant replies still use the privacy sanitizer. - Reproducibility: yes. source-level: current main strips tool-looking Discord payload text at the front-chann ... ds compaction events in queued follow-up runs. I did not run a live Discord repro in this read-only review. Automerge notes: - Ran the ClawSweeper repair loop before final review. - Included post-review commit in the final squash: fix: gate queued follow-up progress when verbose is off - Included post-review commit in the final squash: fix: preserve queued verbose progress under preview suppression - Included post-review commit in the final squash: ci: rerun discord verbose progress PR - Included post-review commit in the final squash: fix: preserve Discord verbose progress after rebase - Included post-review commit in the final squash: fix: serialize discord queued progress - Included post-review commit in the final squash: Fix Discord verbose tool progress delivery Validation: - ClawSweeper review passed for head fd845e7. - Required merge gates passed before the squash merge. Prepared head SHA: fd845e7 Review: #80042 (comment) Co-authored-by: Clawsistant <clawsistant@users.noreply.github.com> Co-authored-by: anyech <anyech@gmail.com> Co-authored-by: OpenClaw Assistant <assistant@openclaw.local> Co-authored-by: Shadow <hi@shadowing.dev> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: thewilloftheshadow Co-authored-by: thewilloftheshadow <35580099+thewilloftheshadow@users.noreply.github.com>
1 parent 127f3f8 commit 57da466

10 files changed

Lines changed: 555 additions & 19 deletions

extensions/discord/src/monitor/agent-components.dispatch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ export async function dispatchDiscordComponentEvent(params: {
312312
},
313313
},
314314
delivery: {
315-
deliver: async (payload) => {
315+
deliver: async (payload, info) => {
316316
const replyToId = replyReference.use();
317317
await deliverDiscordReply({
318318
cfg: ctx.cfg,
@@ -333,6 +333,7 @@ export async function dispatchDiscordComponentEvent(params: {
333333
tableMode,
334334
chunkMode: resolveChunkMode(ctx.cfg, "discord", accountId),
335335
mediaLocalRoots,
336+
kind: info.kind,
336337
});
337338
replyReference.markSent();
338339
},

extensions/discord/src/monitor/message-handler.process.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@ export async function processDiscordMessage(
640640
sessionKey: ctxPayload.SessionKey,
641641
threadBindings,
642642
mediaLocalRoots,
643+
kind: info.kind,
643644
});
644645
return true;
645646
},
@@ -678,6 +679,7 @@ export async function processDiscordMessage(
678679
sessionKey: ctxPayload.SessionKey,
679680
threadBindings,
680681
mediaLocalRoots,
682+
kind: info.kind,
681683
});
682684
return true;
683685
},
@@ -716,6 +718,7 @@ export async function processDiscordMessage(
716718
sessionKey: ctxPayload.SessionKey,
717719
threadBindings,
718720
mediaLocalRoots,
721+
kind: info.kind,
719722
});
720723
replyReference.markSent();
721724
if (isFinal) {

extensions/discord/src/monitor/reply-delivery.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ describe("deliverDiscordReply", () => {
119119
textLimit: 2000,
120120
replyToId: "reply-1",
121121
replyToMode: "all",
122+
kind: "final",
122123
});
123124

124125
const params = firstDeliverParams();
@@ -151,10 +152,30 @@ describe("deliverDiscordReply", () => {
151152
runtime,
152153
cfg,
153154
textLimit: 2000,
155+
kind: "final",
154156
}),
155157
).rejects.toThrow("discord final reply produced no delivered message for channel:101");
156158
});
157159

160+
it("preserves explicit tool progress payloads at the tool delivery boundary", async () => {
161+
await deliverDiscordReply({
162+
replies: [{ text: "🛠️ Exec: `echo visible`" }],
163+
target: "channel:101",
164+
token: "token",
165+
accountId: "default",
166+
runtime,
167+
cfg,
168+
textLimit: 2000,
169+
kind: "tool",
170+
});
171+
172+
expect(sendDurableMessageBatchMock).toHaveBeenCalledWith(
173+
expect.objectContaining({
174+
payloads: [{ text: "🛠️ Exec: `echo visible`" }],
175+
}),
176+
);
177+
});
178+
158179
it("strips internal execution trace lines at the final Discord send boundary", async () => {
159180
await deliverDiscordReply({
160181
replies: [
@@ -177,6 +198,7 @@ describe("deliverDiscordReply", () => {
177198
runtime,
178199
cfg,
179200
textLimit: 2000,
201+
kind: "final",
180202
});
181203

182204
expect(firstDeliverParams().payloads).toEqual([{ text: "Visible reply." }]);
@@ -196,6 +218,7 @@ describe("deliverDiscordReply", () => {
196218
runtime,
197219
cfg,
198220
textLimit: 2000,
221+
kind: "final",
199222
});
200223

201224
expect(firstDeliverParams().payloads).toEqual([
@@ -235,6 +258,7 @@ describe("deliverDiscordReply", () => {
235258
runtime,
236259
cfg,
237260
textLimit: 2000,
261+
kind: "final",
238262
});
239263

240264
expect(firstDeliverParams().payloads).toEqual([{ channelData, text: undefined }]);
@@ -264,6 +288,7 @@ describe("deliverDiscordReply", () => {
264288
runtime,
265289
cfg,
266290
textLimit: 2000,
291+
kind: "final",
267292
});
268293

269294
expect(firstDeliverParams().payloads).toEqual([{ presentation, text: undefined }]);
@@ -280,6 +305,7 @@ describe("deliverDiscordReply", () => {
280305
runtime,
281306
cfg,
282307
textLimit: 2000,
308+
kind: "final",
283309
});
284310

285311
expect(firstDeliverParams().payloads).toEqual([{ text }]);
@@ -301,6 +327,7 @@ describe("deliverDiscordReply", () => {
301327
runtime,
302328
cfg,
303329
textLimit: 2000,
330+
kind: "final",
304331
});
305332

306333
expect(firstDeliverParams().payloads).toEqual([{ text }]);
@@ -334,6 +361,7 @@ describe("deliverDiscordReply", () => {
334361
maxLinesPerMessage: 7,
335362
tableMode: "off",
336363
chunkMode: "newline",
364+
kind: "final",
337365
});
338366

339367
expect(firstDeliverParams().cfg).toBe(baseCfg);
@@ -363,6 +391,7 @@ describe("deliverDiscordReply", () => {
363391
textLimit: 2000,
364392
replyToMode: "off",
365393
mediaLocalRoots: ["/tmp/openclaw-media"],
394+
kind: "final",
366395
});
367396

368397
const params = firstDeliverParams();
@@ -381,6 +410,7 @@ describe("deliverDiscordReply", () => {
381410
cfg,
382411
textLimit: 2000,
383412
replyToId: "reply-1",
413+
kind: "final",
384414
});
385415

386416
const deps = firstDeliverParams().deps!;
@@ -429,6 +459,7 @@ describe("deliverDiscordReply", () => {
429459
replyToId: "reply-1",
430460
sessionKey: "agent:main:subagent:child",
431461
threadBindings,
462+
kind: "final",
432463
});
433464

434465
const params = firstDeliverParams();

extensions/discord/src/monitor/reply-delivery.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,12 @@ export async function deliverDiscordReply(params: {
172172
sessionKey?: string;
173173
threadBindings?: DiscordThreadBindingLookup;
174174
mediaLocalRoots?: readonly string[];
175+
kind: "tool" | "block" | "final";
175176
}) {
176177
void params.runtime;
177178

178179
const delivery = resolveDiscordDeliveryOptions(params);
179-
const payloads = sanitizeDiscordFrontChannelReplyPayloads(params.replies);
180+
const payloads = sanitizeDiscordFrontChannelReplyPayloads(params.replies, { kind: params.kind });
180181
if (payloads.length === 0) {
181182
return;
182183
}

extensions/discord/src/monitor/reply-safety.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,16 @@ export function sanitizeDiscordFrontChannelText(text: string): string {
7171

7272
export function sanitizeDiscordFrontChannelReplyPayloads(
7373
payloads: readonly ReplyPayload[],
74+
options: { kind?: "tool" | "block" | "final" } = {},
7475
): ReplyPayload[] {
76+
const preserveVerboseToolProgress = options.kind === "tool";
7577
const safePayloads: ReplyPayload[] = [];
7678
for (const payload of payloads) {
7779
const safeText =
7880
typeof payload.text === "string"
79-
? sanitizeDiscordFrontChannelText(payload.text)
81+
? preserveVerboseToolProgress
82+
? collapseExcessBlankLines(sanitizeAssistantVisibleText(payload.text)).trim()
83+
: sanitizeDiscordFrontChannelText(payload.text)
8084
: payload.text;
8185
const nextPayload =
8286
safeText === payload.text

src/auto-reply/reply/agent-runner.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,7 @@ export async function runReplyAgent(params: {
11931193
storePath,
11941194
defaultModel,
11951195
agentCfgContextTokens,
1196+
toolProgressDetail,
11961197
});
11971198

11981199
if (activeRunQueueAction === "drop") {
@@ -1415,6 +1416,7 @@ export async function runReplyAgent(params: {
14151416
storePath,
14161417
defaultModel,
14171418
agentCfgContextTokens,
1419+
toolProgressDetail,
14181420
});
14191421

14201422
let responseUsageLine: string | undefined;

src/auto-reply/reply/dispatch-from-config.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,7 +2024,7 @@ describe("dispatchReplyFromConfig", () => {
20242024
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
20252025
});
20262026

2027-
it("keeps verbose tool summaries suppressed for channel message-tool-only turns", async () => {
2027+
it("delivers verbose tool summaries for Discord channel message-tool-only turns", async () => {
20282028
setNoAbort();
20292029
sessionStoreMocks.currentEntry = {
20302030
sessionId: "s1",
@@ -2056,7 +2056,7 @@ describe("dispatchReplyFromConfig", () => {
20562056
});
20572057

20582058
expect(result.sourceReplyDeliveryMode).toBe("message_tool_only");
2059-
expect(dispatcher.sendToolResult).not.toHaveBeenCalled();
2059+
expect(dispatcher.sendToolResult).toHaveBeenCalledWith({ text: "🛠️ `pwd (agent)`" });
20602060
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
20612061
});
20622062

@@ -4947,6 +4947,39 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
49474947
expect(dispatcher.sendBlockReply).not.toHaveBeenCalled();
49484948
});
49494949

4950+
it("delivers verbose tool progress in message-tool-only mode", async () => {
4951+
setNoAbort();
4952+
sessionStoreMocks.currentEntry = {
4953+
sessionId: "s1",
4954+
updatedAt: 0,
4955+
sendPolicy: "allow",
4956+
verboseLevel: "on",
4957+
};
4958+
const dispatcher = createDispatcher();
4959+
const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => {
4960+
await opts?.onToolResult?.({ text: "🛠️ Exec: echo post-restart" });
4961+
return { text: "NO_REPLY" } satisfies ReplyPayload;
4962+
});
4963+
const ctx = buildTestCtx({ SessionKey: "test:session", ChatType: "channel" });
4964+
4965+
const result = await dispatchReplyFromConfig({
4966+
ctx,
4967+
cfg: emptyConfig,
4968+
dispatcher,
4969+
replyResolver,
4970+
replyOptions: {
4971+
sourceReplyDeliveryMode: "message_tool_only",
4972+
},
4973+
});
4974+
4975+
expect(result.queuedFinal).toBe(false);
4976+
expect(result.sourceReplyDeliveryMode).toBe("message_tool_only");
4977+
expect(dispatcher.sendToolResult).toHaveBeenCalledWith(
4978+
expect.objectContaining({ text: "🛠️ Exec: echo post-restart" }),
4979+
);
4980+
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
4981+
});
4982+
49504983
it("delivers marked runtime failure notices in message-tool-only mode", async () => {
49514984
setNoAbort();
49524985
sessionStoreMocks.currentEntry = {

src/auto-reply/reply/dispatch-from-config.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,7 @@ export async function dispatchReplyFromConfig(
10461046
const shouldSendToolStartStatuses = false;
10471047
const shouldDeliverVerboseProgressDespiteSourceSuppression = () =>
10481048
suppressAutomaticSourceDelivery &&
1049-
chatType === "direct" &&
1049+
sourceReplyDeliveryMode === "message_tool_only" &&
10501050
ctx.InboundEventKind !== "room_event" &&
10511051
!sendPolicyDenied &&
10521052
shouldEmitVerboseProgress() &&
@@ -1214,7 +1214,7 @@ export async function dispatchReplyFromConfig(
12141214
return parts.join("\n\n").trim() || "Planning next steps.";
12151215
};
12161216
const maybeSendWorkingStatus = async (label: string): Promise<void> => {
1217-
if (suppressDelivery && !shouldDeliverVerboseProgressDespiteSourceSuppression()) {
1217+
if (shouldSuppressProgressDelivery()) {
12181218
return;
12191219
}
12201220
const normalizedLabel = normalizeWorkingLabel(label);
@@ -1244,7 +1244,7 @@ export async function dispatchReplyFromConfig(
12441244
steps?: string[];
12451245
}): Promise<void> => {
12461246
if (
1247-
(suppressDelivery && !shouldDeliverVerboseProgressDespiteSourceSuppression()) ||
1247+
shouldSuppressProgressDelivery() ||
12481248
!shouldEmitVerboseProgress() ||
12491249
!shouldSendVerboseProgressMessages
12501250
) {
@@ -1349,6 +1349,9 @@ export async function dispatchReplyFromConfig(
13491349
params.replyOptions?.suppressDefaultToolProgressMessages === true;
13501350
const shouldSuppressDefaultToolProgressMessages = () =>
13511351
suppressDefaultToolProgressMessages && !shouldEmitVerboseProgress();
1352+
const shouldSuppressProgressDelivery = () =>
1353+
sendPolicyDenied ||
1354+
(suppressDelivery && !shouldDeliverVerboseProgressDespiteSourceSuppression());
13521355
const onToolResultFromReplyOptions = params.replyOptions?.onToolResult;
13531356
const onPlanUpdateFromReplyOptions = params.replyOptions?.onPlanUpdate;
13541357
const onApprovalEventFromReplyOptions = params.replyOptions?.onApprovalEvent;
@@ -1420,7 +1423,7 @@ export async function dispatchReplyFromConfig(
14201423
if (!suppressAutomaticSourceDelivery) {
14211424
await onToolResultFromReplyOptions?.(payload);
14221425
}
1423-
if (suppressDelivery && !shouldDeliverVerboseProgressDespiteSourceSuppression()) {
1426+
if (shouldSuppressProgressDelivery()) {
14241427
return;
14251428
}
14261429
const ttsPayload = await maybeApplyTtsToReplyPayload({

0 commit comments

Comments
 (0)