diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 0f7b6ac7074..5587b598bc3 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -582,6 +582,7 @@ Default slash command settings: OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. - `channels.discord.streaming` controls preview streaming (`off` | `partial` | `block` | `progress`, default: `off`). + - Default stays `off` because Discord preview edits can hit rate limits quickly, especially when multiple bots or gateways share the same account or guild traffic. - `progress` is accepted for cross-channel consistency and maps to `partial` on Discord. - `channels.discord.streamMode` is a legacy alias and is auto-migrated. - `partial` edits a single preview message as tokens arrive. diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 0e21e35538f..2a619a3a628 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -179,7 +179,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat historyLimit: 50, replyToMode: "first", // off | first | all linkPreview: true, - streaming: "partial", // off | partial | block | progress (default: off) + streaming: "partial", // off | partial | block | progress (default: off; opt in explicitly to avoid preview-edit rate limits) actions: { reactions: true, sendMessage: true }, reactionNotifications: "own", // off | own | all mediaMaxMb: 100, diff --git a/extensions/discord/src/monitor/message-handler.process.test.ts b/extensions/discord/src/monitor/message-handler.process.test.ts index fb0f0311a04..bb3e47c1287 100644 --- a/extensions/discord/src/monitor/message-handler.process.test.ts +++ b/extensions/discord/src/monitor/message-handler.process.test.ts @@ -537,6 +537,12 @@ describe("processDiscordMessage draft streaming", () => { expectSinglePreviewEdit(); }); + it("keeps preview streaming off by default when streaming is unset", async () => { + await runSingleChunkFinalScenario({ maxLinesPerMessage: 5 }); + expect(editMessageDiscord).not.toHaveBeenCalled(); + expect(deliverDiscordReply).toHaveBeenCalledTimes(1); + }); + it("falls back to standard send when final needs multiple chunks", async () => { await runSingleChunkFinalScenario({ streamMode: "partial", maxLinesPerMessage: 1 }); diff --git a/src/commands/doctor-legacy-config.test.ts b/src/commands/doctor-legacy-config.test.ts index 89bc93bbcc7..8f4bf74e684 100644 --- a/src/commands/doctor-legacy-config.test.ts +++ b/src/commands/doctor-legacy-config.test.ts @@ -32,6 +32,23 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => { ]); }); + it("explains why discord preview streaming stays off when legacy config resolves to off", () => { + const res = normalizeCompatibilityConfigValues({ + channels: { + discord: { + streamMode: "off", + }, + }, + }); + + expect(res.config.channels?.discord?.streaming).toBe("off"); + expect(res.config.channels?.discord?.streamMode).toBeUndefined(); + expect(res.changes).toEqual([ + "Moved channels.discord.streamMode → channels.discord.streaming (off).", + 'channels.discord.streaming remains off by default to avoid Discord preview-edit rate limits; set channels.discord.streaming="partial" to opt in explicitly.', + ]); + }); + it("normalizes slack boolean streaming aliases to enum and native streaming", () => { const res = normalizeCompatibilityConfigValues({ channels: { diff --git a/src/commands/doctor-legacy-config.ts b/src/commands/doctor-legacy-config.ts index 36c86bc0315..58eff1d0382 100644 --- a/src/commands/doctor-legacy-config.ts +++ b/src/commands/doctor-legacy-config.ts @@ -142,6 +142,11 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): { `Normalized ${params.pathPrefix}.streaming (${beforeStreaming}) → (${resolved}).`, ); } + if (params.pathPrefix.startsWith("channels.discord") && resolved === "off") { + changes.push( + `${params.pathPrefix}.streaming remains off by default to avoid Discord preview-edit rate limits; set ${params.pathPrefix}.streaming="partial" to opt in explicitly.`, + ); + } return { entry: updated, changed }; }; diff --git a/src/config/discord-preview-streaming.test.ts b/src/config/discord-preview-streaming.test.ts new file mode 100644 index 00000000000..1673ea5a82a --- /dev/null +++ b/src/config/discord-preview-streaming.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { resolveDiscordPreviewStreamMode } from "./discord-preview-streaming.js"; + +describe("resolveDiscordPreviewStreamMode", () => { + it("defaults to off when unset", () => { + expect(resolveDiscordPreviewStreamMode({})).toBe("off"); + }); + + it("preserves explicit off", () => { + expect(resolveDiscordPreviewStreamMode({ streaming: "off" })).toBe("off"); + expect(resolveDiscordPreviewStreamMode({ streamMode: "off" })).toBe("off"); + expect(resolveDiscordPreviewStreamMode({ streaming: false })).toBe("off"); + }); +}); diff --git a/src/config/discord-preview-streaming.ts b/src/config/discord-preview-streaming.ts index 79d7f8fd9b9..37e96e08bf3 100644 --- a/src/config/discord-preview-streaming.ts +++ b/src/config/discord-preview-streaming.ts @@ -104,6 +104,9 @@ export function resolveDiscordPreviewStreamMode( if (typeof params.streaming === "boolean") { return params.streaming ? "partial" : "off"; } + // Discord preview streaming edits can hit aggressive rate limits, especially + // when multiple gateways or multiple bots share the same account/server. Keep + // the default off unless the operator opts in explicitly. return "off"; }