fix(discord): harden read message results (#82276)

Validate Discord read message results before normalizing channel history, preserving the Discord array contract while replacing opaque map crashes with a clear boundary error.

Fixes #82252.
This commit is contained in:
Peter Steinberger
2026-05-15 20:42:41 +01:00
committed by GitHub
parent 8ba0bb2a8a
commit 4add9bab77
3 changed files with 34 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
- Plugin releases: require external package compatibility metadata in the npm plugin publish plan, matching the ClawHub package contract before packages ship.
- Agents/OpenAI-compatible: honor per-model `max_completion_tokens`/`max_tokens` params in embedded OpenAI-completions runs so high-token Kimi-style routes keep their configured completion cap. Fixes #82230. Thanks @albert-zen.
- Agents/local: install a local gateway request scope around trusted `openclaw agent --local` runs, so subagent completion announces can use in-process gateway dispatch without crashing. Fixes #82140. Thanks @Kushmaro.
- Discord: validate message-read results before normalizing channel history and report unexpected payloads with a Discord boundary error instead of `map is not a function`. Fixes #82252. Thanks @jessewunderlich.
- Telegram/active-memory: run blocking memory recall through the Telegram provider for direct-message turns even when the hook context carries the raw chat id, preventing embedded recall from launching against an invalid numeric channel. Fixes #82177. Thanks @cslash-zz.
- Control UI/WebChat: keep optimistic image messages from embedding large inline `data:` previews and preserve image-only user turns in chat history, avoiding browser stack overflows when sending image attachments. Fixes #82182. Thanks @ExploreSheep.
- Agents/media: preserve message-tool-only delivery for generated music and video completion handoffs, so group/channel completions do not finish without posting the generated attachment.

View File

@@ -24,6 +24,29 @@ function parseDiscordMessageLink(link: string) {
};
}
function describeDiscordMessageListResult(value: unknown): string {
if (Array.isArray(value)) {
return "array";
}
if (value === null) {
return "null";
}
if (value && typeof value === "object") {
const keys = Object.keys(value).toSorted();
return keys.length ? `object with keys ${keys.join(", ")}` : "object";
}
return typeof value;
}
function assertDiscordMessageListResult(value: unknown): Array<unknown> {
if (Array.isArray(value)) {
return value;
}
throw new Error(
`Discord message read returned ${describeDiscordMessageListResult(value)} instead of an array.`,
);
}
export async function handleDiscordMessageManagementAction(ctx: DiscordMessagingActionContext) {
switch (ctx.action) {
case "permissions": {
@@ -80,10 +103,8 @@ export async function handleDiscordMessageManagementAction(ctx: DiscordMessaging
after: readStringParam(ctx.params, "after"),
around: readStringParam(ctx.params, "around"),
};
const messages = await discordMessagingActionRuntime.readMessagesDiscord(
channelId,
query,
ctx.withOpts(),
const messages = assertDiscordMessageListResult(
await discordMessagingActionRuntime.readMessagesDiscord(channelId, query, ctx.withOpts()),
);
return jsonResult({
ok: true,

View File

@@ -389,6 +389,14 @@ describe("handleDiscordMessagingAction", () => {
expect(payload.messages[0].timestampUtc).toBe(new Date(expectedMs).toISOString());
});
it("rejects unexpected readMessages payloads with a boundary error", async () => {
readMessagesDiscord.mockResolvedValueOnce({ ok: true } as never);
await expect(
handleMessagingAction("readMessages", { channelId: "C1" }, enableAllActions),
).rejects.toThrow("Discord message read returned object with keys ok instead of an array.");
});
it("threads provided cfg into readMessages calls", async () => {
const cfg = {
channels: {