Skip to content

Commit d1280a3

Browse files
obviyussteipete
andauthored
fix(discord): preserve room event history until delivery (#82573)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent d7ad12d commit d1280a3

13 files changed

Lines changed: 503 additions & 23 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
4242
- QQBot: treat only explicit truthy `QQBOT_DEBUG` values as enabling debug logs, so false-like values such as `0` no longer expose debug output. Fixes #82644. (#82697) Thanks @leno23.
4343
- Agents/session_status: resolve implicit no-arg status lookups against the live run session, so `/think` changes report the current thinking level instead of stale sandbox state. Fixes #82669. (#82696) Thanks @leno23.
4444
- Discord: keep progress drafts visible for message-tool-only guild replies under the default coding tool profile. Fixes #82747. Thanks @eliranwong.
45+
- Discord: keep unmentioned room-event history until a visible Discord send succeeds, so quiet ambient context does not disappear before message-tool delivery. (#82573) Thanks @obviyus.
4546
- CLI/setup: order the model/auth provider picker as OpenAI, Anthropic, xAI, Google, then the remaining providers alphabetically.
4647
- Diagnostics/usage/voice-call: treat explicit zero and non-finite limits as empty results and reject invalid voice-call numeric CLI flags. Fixes #82646, #82650, #82651, and #82653. (#82679) Thanks @leno23.
4748
- CLI/config: avoid redundant startup config/plugin checks for the guided `openclaw config` flow and show progress while source checkout CLI artifacts build or load.

extensions/discord/src/actions/handle-action.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const handleDiscordActionMock = vi
66
.spyOn(runtimeModule, "handleDiscordAction")
77
.mockResolvedValue({ content: [], details: { ok: true } });
88
const { handleDiscordMessageAction } = await import("./handle-action.js");
9+
const { beginDiscordInboundEventDeliveryCorrelation } =
10+
await import("../inbound-event-delivery.js");
911

1012
function discordConfig(actions?: Record<string, boolean>): OpenClawConfig {
1113
return {
@@ -201,6 +203,68 @@ describe("handleDiscordMessageAction", () => {
201203
});
202204
});
203205

206+
it("notifies inbound event delivery after message sends", async () => {
207+
const markDelivered = vi.fn();
208+
const end = beginDiscordInboundEventDeliveryCorrelation(
209+
"agent:main:discord:channel:c1",
210+
{
211+
outboundTo: "channel:c1",
212+
outboundAccountId: "default",
213+
markInboundEventDelivered: markDelivered,
214+
},
215+
{ inboundEventKind: "room_event" },
216+
);
217+
218+
try {
219+
await handleDiscordMessageAction({
220+
action: "send",
221+
params: {
222+
to: "channel:c1",
223+
message: "hello",
224+
},
225+
cfg: discordConfig(),
226+
accountId: "default",
227+
sessionKey: "agent:main:discord:channel:c1",
228+
inboundEventKind: "room_event",
229+
});
230+
} finally {
231+
end();
232+
}
233+
234+
expect(markDelivered).toHaveBeenCalledTimes(1);
235+
});
236+
237+
it("notifies inbound event delivery after visible message actions", async () => {
238+
const markDelivered = vi.fn();
239+
const end = beginDiscordInboundEventDeliveryCorrelation(
240+
"agent:main:discord:channel:c1",
241+
{
242+
outboundTo: "channel:c1",
243+
outboundAccountId: "default",
244+
markInboundEventDelivered: markDelivered,
245+
},
246+
{ inboundEventKind: "room_event" },
247+
);
248+
249+
try {
250+
await handleDiscordMessageAction({
251+
action: "upload-file",
252+
params: {
253+
to: "channel:c1",
254+
filePath: "/tmp/image.png",
255+
},
256+
cfg: discordConfig(),
257+
accountId: "default",
258+
sessionKey: "agent:main:discord:channel:c1",
259+
inboundEventKind: "room_event",
260+
});
261+
} finally {
262+
end();
263+
}
264+
265+
expect(markDelivered).toHaveBeenCalledTimes(1);
266+
});
267+
204268
it("maps upload-file to Discord sendMessage with media read context", async () => {
205269
const mediaReadFile = vi.fn(async () => Buffer.from("image"));
206270
const mediaAccess = {

extensions/discord/src/actions/handle-action.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "openclaw/plugin-sdk/interactive-runtime";
1414
import { normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/string-coerce-runtime";
1515
import { handleDiscordAction } from "../../action-runtime-api.js";
16+
import { notifyDiscordInboundEventOutboundSuccess } from "../inbound-event-delivery.js";
1617
import {
1718
buildDiscordInteractiveComponents,
1819
buildDiscordPresentationComponents,
@@ -45,6 +46,8 @@ export async function handleDiscordMessageAction(
4546
| "mediaAccess"
4647
| "mediaLocalRoots"
4748
| "mediaReadFile"
49+
| "sessionKey"
50+
| "inboundEventKind"
4851
>,
4952
): Promise<AgentToolResult<unknown>> {
5053
const { action, params, cfg } = ctx;
@@ -54,6 +57,13 @@ export async function handleDiscordMessageAction(
5457
mediaLocalRoots: ctx.mediaLocalRoots,
5558
mediaReadFile: ctx.mediaReadFile,
5659
} as const;
60+
const notifyVisibleOutbound = (to: string, fallbackSessionKey?: string) =>
61+
notifyDiscordInboundEventOutboundSuccess({
62+
sessionKey: ctx.sessionKey ?? fallbackSessionKey ?? undefined,
63+
to,
64+
accountId,
65+
inboundEventKind: ctx.inboundEventKind,
66+
});
5767

5868
const readTarget = () => {
5969
const target =
@@ -106,7 +116,7 @@ export async function handleDiscordMessageAction(
106116
const sessionKey = readStringParam(params, "__sessionKey");
107117
const agentId = readStringParam(params, "__agentId");
108118
const threadName = readStringParam(params, "threadName");
109-
return await handleDiscordAction(
119+
const result = await handleDiscordAction(
110120
{
111121
action: "sendMessage",
112122
accountId: accountId ?? undefined,
@@ -127,6 +137,8 @@ export async function handleDiscordMessageAction(
127137
cfg,
128138
actionOptions,
129139
);
140+
notifyVisibleOutbound(to, sessionKey);
141+
return result;
130142
}
131143

132144
if (action === "upload-file") {
@@ -147,7 +159,7 @@ export async function handleDiscordMessageAction(
147159
const suppressEmbeds = readBooleanParam(params, "suppressEmbeds");
148160
const sessionKey = readStringParam(params, "__sessionKey");
149161
const agentId = readStringParam(params, "__agentId");
150-
return await handleDiscordAction(
162+
const result = await handleDiscordAction(
151163
{
152164
action: "sendMessage",
153165
accountId: accountId ?? undefined,
@@ -164,6 +176,8 @@ export async function handleDiscordMessageAction(
164176
cfg,
165177
actionOptions,
166178
);
179+
notifyVisibleOutbound(to, sessionKey);
180+
return result;
167181
}
168182

169183
if (action === "poll") {
@@ -177,7 +191,7 @@ export async function handleDiscordMessageAction(
177191
integer: true,
178192
strict: true,
179193
});
180-
return await handleDiscordAction(
194+
const result = await handleDiscordAction(
181195
{
182196
action: "poll",
183197
accountId: accountId ?? undefined,
@@ -191,6 +205,8 @@ export async function handleDiscordMessageAction(
191205
cfg,
192206
actionOptions,
193207
);
208+
notifyVisibleOutbound(to);
209+
return result;
194210
}
195211

196212
if (action === "react") {
@@ -315,7 +331,7 @@ export async function handleDiscordMessageAction(
315331
integer: true,
316332
});
317333
const appliedTags = readStringArrayParam(params, "appliedTags");
318-
return await handleDiscordAction(
334+
const result = await handleDiscordAction(
319335
{
320336
action: "threadCreate",
321337
accountId: accountId ?? undefined,
@@ -329,25 +345,30 @@ export async function handleDiscordMessageAction(
329345
cfg,
330346
actionOptions,
331347
);
348+
notifyVisibleOutbound(resolveChannelId());
349+
return result;
332350
}
333351

334352
if (action === "sticker") {
353+
const to = readStringParam(params, "to", { required: true });
335354
const stickerIds =
336355
readStringArrayParam(params, "stickerId", {
337356
required: true,
338357
label: "sticker-id",
339358
}) ?? [];
340-
return await handleDiscordAction(
359+
const result = await handleDiscordAction(
341360
{
342361
action: "sticker",
343362
accountId: accountId ?? undefined,
344-
to: readStringParam(params, "to", { required: true }),
363+
to,
345364
stickerIds,
346365
content: readStringParam(params, "message"),
347366
},
348367
cfg,
349368
actionOptions,
350369
);
370+
notifyVisibleOutbound(to);
371+
return result;
351372
}
352373

353374
if (action === "set-presence") {
@@ -371,6 +392,9 @@ export async function handleDiscordMessageAction(
371392
resolveChannelId,
372393
});
373394
if (adminResult !== undefined) {
395+
if (action === "thread-reply") {
396+
notifyVisibleOutbound(readStringParam(params, "threadId") ?? readTarget());
397+
}
374398
return adminResult;
375399
}
376400

extensions/discord/src/channel-actions.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,33 @@ describe("discordMessageActions", () => {
414414
});
415415
});
416416

417+
it("prepares inbound event delivery metadata for durable core sends", async () => {
418+
const prepared = await discordMessageActions.prepareSendPayload?.({
419+
ctx: {
420+
channel: "discord",
421+
action: "send",
422+
cfg: {} as OpenClawConfig,
423+
params: {},
424+
sessionKey: "agent:main:discord:channel:c1",
425+
inboundEventKind: "room_event",
426+
},
427+
to: "channel:123",
428+
payload: { text: "hello" },
429+
});
430+
431+
expect(prepared).toEqual({
432+
text: "hello",
433+
channelData: {
434+
discord: {
435+
__openclawInboundEventDelivery: {
436+
sessionKey: "agent:main:discord:channel:c1",
437+
inboundEventKind: "room_event",
438+
},
439+
},
440+
},
441+
});
442+
});
443+
417444
it("keeps non-serializable Discord component sends on the legacy action path", async () => {
418445
const prepared = await discordMessageActions.prepareSendPayload?.({
419446
ctx: {

extensions/discord/src/channel-actions.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
1010
import { inspectDiscordAccount } from "./account-inspect.js";
1111
import { createDiscordActionGate, listDiscordAccountIds } from "./accounts.js";
1212
import { readDiscordComponentSpec } from "./components.js";
13+
import { withDiscordInboundEventDeliveryMetadata } from "./inbound-event-delivery.js";
1314

1415
let discordChannelActionsRuntimePromise:
1516
| Promise<typeof import("./channel-actions.runtime.js")>
@@ -180,6 +181,10 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
180181
if (ctx.action !== "send") {
181182
return null;
182183
}
184+
const payloadWithDeliveryMetadata = withDiscordInboundEventDeliveryMetadata(payload, {
185+
sessionKey: ctx.sessionKey,
186+
inboundEventKind: ctx.inboundEventKind,
187+
});
183188
const rawComponents = ctx.params.components;
184189
if (typeof rawComponents === "function") {
185190
return null;
@@ -195,18 +200,18 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
195200
}
196201
const filename = normalizeOptionalString(ctx.params.filename);
197202
if (!componentSpec && !nativeComponents && !embeds?.length && !filename) {
198-
return payload;
203+
return payloadWithDeliveryMetadata;
199204
}
200205
const discordData =
201-
payload.channelData?.discord &&
202-
typeof payload.channelData.discord === "object" &&
203-
!Array.isArray(payload.channelData.discord)
204-
? (payload.channelData.discord as Record<string, unknown>)
206+
payloadWithDeliveryMetadata.channelData?.discord &&
207+
typeof payloadWithDeliveryMetadata.channelData.discord === "object" &&
208+
!Array.isArray(payloadWithDeliveryMetadata.channelData.discord)
209+
? (payloadWithDeliveryMetadata.channelData.discord as Record<string, unknown>)
205210
: {};
206211
return {
207-
...payload,
212+
...payloadWithDeliveryMetadata,
208213
channelData: {
209-
...payload.channelData,
214+
...payloadWithDeliveryMetadata.channelData,
210215
discord: {
211216
...discordData,
212217
...(componentSpec ? { components: componentSpec } : {}),
@@ -227,6 +232,8 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
227232
mediaAccess,
228233
mediaLocalRoots,
229234
mediaReadFile,
235+
sessionKey,
236+
inboundEventKind,
230237
}) => {
231238
return await (
232239
await loadDiscordChannelActionsRuntime()
@@ -240,6 +247,8 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
240247
mediaAccess,
241248
mediaLocalRoots,
242249
mediaReadFile,
250+
...(sessionKey ? { sessionKey } : {}),
251+
...(inboundEventKind ? { inboundEventKind } : {}),
243252
});
244253
},
245254
};

0 commit comments

Comments
 (0)