diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f9bfbcfca..e817f456a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/discord/src/actions/runtime.messaging.messages.ts b/extensions/discord/src/actions/runtime.messaging.messages.ts index 7069bb6a17d..35d995f4189 100644 --- a/extensions/discord/src/actions/runtime.messaging.messages.ts +++ b/extensions/discord/src/actions/runtime.messaging.messages.ts @@ -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 { + 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, diff --git a/extensions/discord/src/actions/runtime.test.ts b/extensions/discord/src/actions/runtime.test.ts index 40556848e04..e90e6f245b8 100644 --- a/extensions/discord/src/actions/runtime.test.ts +++ b/extensions/discord/src/actions/runtime.test.ts @@ -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: {