Skip to content

Commit bfb89d3

Browse files
fix(discord): limit implicit reply fanout (#100784)
* fix(discord): limit implicit reply fanout Co-authored-by: qingminlong <[email protected]> * fix(discord): preserve oversized fallback fanout * refactor(discord): make reply metadata per-message * fix(discord): keep reply receipts serializable * docs(changelog): defer Discord entry to release * refactor(discord): model native reply fanout --------- Co-authored-by: qingminlong <[email protected]>
1 parent 06789ca commit bfb89d3

21 files changed

Lines changed: 549 additions & 228 deletions

extensions/discord/src/actions/runtime.messaging.send.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Discord plugin module implements runtime.messaging.send behavior.
22
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
3+
import { createReusableDiscordReplyReference } from "../reply-reference.js";
34
import {
45
assertMediaNotDataUrl,
56
jsonResult,
@@ -250,7 +251,7 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
250251
{
251252
...ctx.withOpts(),
252253
silent,
253-
replyTo: replyTo ?? undefined,
254+
reply: createReusableDiscordReplyReference(replyTo),
254255
sessionKey: sessionKey ?? undefined,
255256
agentId: agentId ?? undefined,
256257
mediaUrl: mediaUrl ?? undefined,
@@ -284,7 +285,7 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
284285
assertMediaNotDataUrl(mediaUrl);
285286
const result = await discordMessagingActionRuntime.sendVoiceMessageDiscord(to, mediaUrl, {
286287
...ctx.withOpts(),
287-
replyTo,
288+
reply: createReusableDiscordReplyReference(replyTo),
288289
silent,
289290
});
290291
return jsonResult(
@@ -303,7 +304,7 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
303304
filename: filename ?? undefined,
304305
mediaLocalRoots: ctx.options?.mediaLocalRoots,
305306
mediaReadFile: ctx.options?.mediaReadFile,
306-
replyTo,
307+
reply: createReusableDiscordReplyReference(replyTo),
307308
components,
308309
embeds,
309310
silent,
@@ -413,7 +414,7 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
413414
mediaUrl,
414415
mediaLocalRoots: ctx.options?.mediaLocalRoots,
415416
mediaReadFile: ctx.options?.mediaReadFile,
416-
replyTo,
417+
reply: createReusableDiscordReplyReference(replyTo),
417418
},
418419
);
419420
return jsonResult({ ok: true, result });

extensions/discord/src/actions/runtime.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,7 +1309,7 @@ describe("handleDiscordMessagingAction", () => {
13091309

13101310
expect(sendVoiceMessageDiscord).toHaveBeenCalledWith("channel:123", "/tmp/voice.mp3", {
13111311
cfg: DISCORD_TEST_CFG,
1312-
replyTo: undefined,
1312+
reply: undefined,
13131313
silent: true,
13141314
});
13151315
expect(sendMessageDiscord).not.toHaveBeenCalled();
@@ -1445,7 +1445,7 @@ describe("handleDiscordMessagingAction", () => {
14451445
filename: undefined,
14461446
mediaLocalRoots: undefined,
14471447
mediaReadFile: undefined,
1448-
replyTo: undefined,
1448+
reply: undefined,
14491449
components: undefined,
14501450
embeds: undefined,
14511451
silent: false,

extensions/discord/src/channel.message-adapter.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe("discord channel message adapter", () => {
9393
});
9494
expect(hoisted.sendMessageDiscordMock).toHaveBeenLastCalledWith("channel:123456", "hello", {
9595
verbose: false,
96-
replyTo: undefined,
96+
reply: undefined,
9797
accountId: "default",
9898
silent: undefined,
9999
cfg: {},
@@ -121,7 +121,7 @@ describe("discord channel message adapter", () => {
121121
mediaAccess: undefined,
122122
mediaLocalRoots: undefined,
123123
mediaReadFile: undefined,
124-
replyTo: undefined,
124+
reply: undefined,
125125
accountId: "default",
126126
silent: undefined,
127127
cfg: {},
@@ -147,7 +147,7 @@ describe("discord channel message adapter", () => {
147147
"payload",
148148
expect.objectContaining({
149149
verbose: false,
150-
replyTo: undefined,
150+
reply: undefined,
151151
accountId: "default",
152152
silent: undefined,
153153
cfg: {},
@@ -199,7 +199,7 @@ describe("discord channel message adapter", () => {
199199
{
200200
verbose: false,
201201
accountId: "default",
202-
replyTo: "reply-1",
202+
reply: { messageId: "reply-1", scope: "all" },
203203
silent: true,
204204
cfg: {},
205205
textLimit: undefined,

extensions/discord/src/channel.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ describe("discordPlugin outbound", () => {
326326
expect(sendOptions.mediaUrl).toBe("/tmp/image.png");
327327
expect(sendOptions.mediaLocalRoots).toEqual(["/tmp/agent-root"]);
328328
expect(sendOptions.mediaReadFile).toBe(mediaReadFile);
329-
expect(sendOptions.replyTo).toBe("reply-123");
329+
expect(sendOptions.reply).toEqual({ messageId: "reply-123", scope: "all" });
330330
expect(result.channel).toBe("discord");
331331
expect(result.messageId).toBe("m1");
332332
});
@@ -353,7 +353,10 @@ describe("discordPlugin outbound", () => {
353353
expect(sendMessageDiscord).toHaveBeenCalledTimes(2);
354354
expect(argAt(sendMessageDiscord, 0, 0)).toBe("channel:thread-123");
355355
expect(argAt(sendMessageDiscord, 0, 1)).toBe("done - tiny cyber-lobster clip incoming");
356-
expect(objectArgAt(sendMessageDiscord, 0, 2).replyTo).toBe("reply-123");
356+
expect(objectArgAt(sendMessageDiscord, 0, 2).reply).toEqual({
357+
messageId: "reply-123",
358+
scope: "all",
359+
});
357360
expect(argAt(sendMessageDiscord, 1, 0)).toBe("channel:thread-123");
358361
expect(argAt(sendMessageDiscord, 1, 1)).toBe("");
359362
expect(objectArgAt(sendMessageDiscord, 1, 2).mediaUrl).toBe("/tmp/molty.mp4");

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ describe("deliverDiscordReply", () => {
515515
const deps = firstDeliverParams().deps!;
516516
await deps.discordVoice("channel:123", "https://example.com/voice.ogg", {
517517
cfg,
518-
replyTo: "reply-1",
518+
reply: { messageId: "reply-1", scope: "all" },
519519
});
520520

521521
expect(firstMockArg(sendVoiceMessageDiscordMock, "sendVoiceMessageDiscord", 0)).toBe(
@@ -527,7 +527,7 @@ describe("deliverDiscordReply", () => {
527527
const voiceOptions = objectArgAt(sendVoiceMessageDiscordMock, 2);
528528
expect(voiceOptions.cfg).toBe(cfg);
529529
expect(voiceOptions.token).toBe("token");
530-
expect(voiceOptions.replyTo).toBe("reply-1");
530+
expect(voiceOptions.reply).toEqual({ messageId: "reply-1", scope: "all" });
531531
});
532532

533533
it("rewrites bound thread replies to parent target plus thread id and persona", async () => {

extensions/discord/src/outbound-adapter.interactive-order.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe("discordOutbound shared interactive ordering", () => {
6767
chunkMode: undefined,
6868
cfg: {},
6969
maxLinesPerMessage: undefined,
70-
replyTo: undefined,
70+
reply: undefined,
7171
silent: undefined,
7272
tableMode: undefined,
7373
textLimit: undefined,

0 commit comments

Comments
 (0)