Skip to content

Commit 5c9b693

Browse files
committed
fix(discord): preserve tool progress messages
1 parent bc97182 commit 5c9b693

4 files changed

Lines changed: 105 additions & 4 deletions

File tree

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,10 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
198198
onReplyStart?: () => Promise<void> | void;
199199
}) => ({
200200
dispatcher: {
201-
sendToolResult: vi.fn(() => true),
201+
sendToolResult: vi.fn((payload: unknown) => {
202+
void opts.deliver(payload, { kind: "tool" });
203+
return true;
204+
}),
202205
sendBlockReply: vi.fn((payload: unknown) => {
203206
void opts.deliver(payload, { kind: "block" });
204207
return true;
@@ -983,6 +986,31 @@ describe("processDiscordMessage session routing", () => {
983986
});
984987
});
985988

989+
it("marks default tool progress deliveries so Discord safety scrubber preserves them", async () => {
990+
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
991+
params?.dispatcher.sendToolResult({ text: "📖 Read: `lines 1-10 from file.txt`" });
992+
return { queuedFinal: false, counts: { final: 0, tool: 1, block: 0 } };
993+
});
994+
995+
const ctx = await createAutomaticSourceDeliveryContext({
996+
discordConfig: { streaming: { mode: "off" } },
997+
route: BASE_CHANNEL_ROUTE,
998+
});
999+
1000+
await runProcessDiscordMessage(ctx);
1001+
1002+
expect(deliverDiscordReply).toHaveBeenCalledWith(
1003+
expect.objectContaining({
1004+
replies: [
1005+
expect.objectContaining({
1006+
text: "📖 Read: `lines 1-10 from file.txt`",
1007+
channelData: expect.objectContaining({ openclawDiscordToolProgress: true }),
1008+
}),
1009+
],
1010+
}),
1011+
);
1012+
});
1013+
9861014
it("stores group lastRoute with channel target", async () => {
9871015
const ctx = await createBaseContext({
9881016
baseSessionKey: "agent:main:discord:channel:c1",

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,9 +542,23 @@ export async function processDiscordMessage(
542542
if (isFinal) {
543543
notifyFinalReplyStart();
544544
}
545+
const deliveryPayload: ReplyPayload =
546+
info.kind === "tool"
547+
? {
548+
...payload,
549+
channelData: {
550+
...(payload.channelData &&
551+
typeof payload.channelData === "object" &&
552+
!Array.isArray(payload.channelData)
553+
? payload.channelData
554+
: {}),
555+
openclawDiscordToolProgress: true,
556+
},
557+
}
558+
: payload;
545559
await deliverDiscordReply({
546560
cfg,
547-
replies: [payload],
561+
replies: [deliveryPayload],
548562
target: deliverTarget,
549563
token,
550564
accountId,

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,42 @@ describe("deliverDiscordReply", () => {
150150
);
151151
});
152152

153+
it("preserves marked Discord tool-progress labels at the front-channel boundary", async () => {
154+
await deliverDiscordReply({
155+
replies: [
156+
{
157+
text: "📖 Read: `lines 1-10 from file.txt`",
158+
channelData: { openclawDiscordToolProgress: true },
159+
},
160+
{
161+
text: "🛠️ Exec: `print text`",
162+
channelData: { openclawDiscordToolProgress: true },
163+
},
164+
],
165+
target: "channel:101",
166+
token: "token",
167+
accountId: "default",
168+
runtime,
169+
cfg,
170+
textLimit: 2000,
171+
});
172+
173+
expect(deliverOutboundPayloadsMock).toHaveBeenCalledWith(
174+
expect.objectContaining({
175+
payloads: [
176+
{
177+
text: "📖 Read: `lines 1-10 from file.txt`",
178+
channelData: { openclawDiscordToolProgress: true },
179+
},
180+
{
181+
text: "🛠️ Exec: `print text`",
182+
channelData: { openclawDiscordToolProgress: true },
183+
},
184+
],
185+
}),
186+
);
187+
});
188+
153189
it("drops pure internal trace text while preserving media-only delivery", async () => {
154190
await deliverDiscordReply({
155191
replies: [

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,36 @@ function hasInteractiveOrPresentationBlocks(value: unknown): boolean {
2424
return Array.isArray(record.blocks) && record.blocks.length > 0;
2525
}
2626

27+
function getDiscordToolProgressChannelData(
28+
payload: ReplyPayload,
29+
): Record<string, unknown> | undefined {
30+
const channelData = payload.channelData;
31+
if (!channelData || typeof channelData !== "object" || Array.isArray(channelData)) {
32+
return undefined;
33+
}
34+
return channelData as Record<string, unknown>;
35+
}
36+
37+
function hasNonMarkerChannelData(value: unknown): boolean {
38+
if (!hasNonEmptyRecord(value)) {
39+
return false;
40+
}
41+
return Object.keys(value).some((key) => key !== "openclawDiscordToolProgress");
42+
}
43+
2744
function hasNonTextReplyPayloadContent(payload: ReplyPayload): boolean {
2845
return (
2946
payload.audioAsVoice === true ||
30-
hasNonEmptyRecord(payload.channelData) ||
47+
hasNonMarkerChannelData(payload.channelData) ||
3148
hasInteractiveOrPresentationBlocks(payload.interactive) ||
3249
hasInteractiveOrPresentationBlocks(payload.presentation)
3350
);
3451
}
3552

53+
function shouldPreserveDiscordToolProgressText(payload: ReplyPayload): boolean {
54+
return getDiscordToolProgressChannelData(payload)?.openclawDiscordToolProgress === true;
55+
}
56+
3657
function stripDiscordInternalTraceLines(text: string): string {
3758
let inFence = false;
3859
const kept: string[] = [];
@@ -73,7 +94,9 @@ export function sanitizeDiscordFrontChannelReplyPayloads(
7394
for (const payload of payloads) {
7495
const safeText =
7596
typeof payload.text === "string"
76-
? sanitizeDiscordFrontChannelText(payload.text)
97+
? shouldPreserveDiscordToolProgressText(payload)
98+
? sanitizeAssistantVisibleText(payload.text).trim()
99+
: sanitizeDiscordFrontChannelText(payload.text)
77100
: payload.text;
78101
const nextPayload =
79102
safeText === payload.text

0 commit comments

Comments
 (0)