diff --git a/CHANGELOG.md b/CHANGELOG.md index 1290abde641..dae426d06f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -179,6 +179,7 @@ Docs: https://docs.openclaw.ai - CLI: strip generic OSC terminal escape payloads from sanitized output fields, preventing clipboard/title escape bodies from leaking into commitment tables and other terminal-safe text. Thanks @shakkernerd. - Codex app-server: match connector-backed plugin approval elicitations by stable connector id so enabled destructive actions no longer fall through to display-name-only rejection. - Build: replace selected build utility `tsx` preloads with Node native type stripping so Node 26 build paths no longer emit `DEP0205` module loader deprecation warnings. (#78584) Thanks @keshavbotagent. +- Channels/loop-guard: enforce shared per-pair bot loop protection in the core channel-turn kernel, with Discord, Slack, Matrix, and Google Chat supplying bot-pair facts where they can reliably identify accepted bot-authored messages. The generic guard keys on `(scope, conversation, participant pair)`, suppresses every additional bot-to-bot event in either direction once a pair crosses the configured budget, and lifts suppression after `cooldownSeconds`. Defaults are `maxEventsPerWindow: 20`, `windowSeconds: 60`, and `cooldownSeconds: 60` whenever a channel lets bot-authored messages reach dispatch; they can be set globally via `channels.defaults.botLoopProtection` and overridden per channel/account or supported per-conversation config. Fixes #58789. Thanks @pandadev66. - Media generation: honor configured music and video generation timeouts when tool calls omit `timeoutMs`, matching image generation behavior. (#80687) - CLI/update/status: label beta-channel plugin fallback and model-pricing refresh failures as warnings, keeping mixed beta/latest plugin cohorts visible without making core update or Gateway reachability look failed. Fixes #80689. Thanks @BKF-Gitty. - Doctor/plugins: relink managed npm plugin `openclaw` peer dependencies during `doctor --fix`, while refusing to follow package-local `node_modules` symlinks outside the plugin package. (#77412) Thanks @TheCrazyLex. diff --git a/docs/channels/bot-loop-protection.md b/docs/channels/bot-loop-protection.md new file mode 100644 index 00000000000..c48537ffd00 --- /dev/null +++ b/docs/channels/bot-loop-protection.md @@ -0,0 +1,131 @@ +--- +summary: "Bot-to-bot loop protection defaults and channel overrides" +read_when: + - Configuring bot-authored channel messages + - Tuning bot-to-bot loop protection +title: "Bot loop protection" +sidebarTitle: "Bot loop protection" +--- + +# Bot loop protection + +OpenClaw can accept messages written by other bots on channels that support `allowBots`. +When that path is enabled, pair loop protection prevents two bot identities from +replying to each other indefinitely. + +The guard is enforced by the core channel-turn kernel. Each supporting channel +maps its own inbound event into generic facts: account or scope, conversation id, +sender bot id, and receiver bot id. Core then tracks the participant pair in both +directions, applies a sliding-window budget, and suppresses the pair during a +cooldown after the budget is exceeded. + +## Defaults + +Pair loop protection is active when a channel lets bot-authored messages reach +dispatch. Built-in defaults are: + +- `maxEventsPerWindow: 20` - a bot pair can exchange 20 events within the window +- `windowSeconds: 60` - sliding window length +- `cooldownSeconds: 60` - suppression time after the pair exceeds the budget + +The guard does not affect normal human-authored messages, single-bot deployments, +self-message filtering, or one-shot bot replies that stay under the budget. + +## Configure shared defaults + +Set `channels.defaults.botLoopProtection` once to give every supporting channel +the same baseline. Channel and account overrides can still tune individual +surfaces. + +```json5 +{ + channels: { + defaults: { + botLoopProtection: { + maxEventsPerWindow: 20, + windowSeconds: 60, + cooldownSeconds: 60, + }, + }, + }, +} +``` + +Set `enabled: false` only when your channel policy intentionally allows +bot-to-bot conversations without automatic suppression. + +## Override per channel or account + +Supporting channels layer their own config over the shared default. Precedence is: + +- `channels...botLoopProtection`, when the channel supports per-conversation overrides +- `channels..accounts..botLoopProtection`, when the channel supports accounts +- `channels..botLoopProtection`, when the channel supports top-level defaults +- `channels.defaults.botLoopProtection` +- built-in defaults + +```json5 +{ + channels: { + defaults: { + botLoopProtection: { + maxEventsPerWindow: 20, + }, + }, + discord: { + botLoopProtection: { + maxEventsPerWindow: 8, + }, + accounts: { + molty: { + allowBots: "mentions", + botLoopProtection: { + maxEventsPerWindow: 5, + cooldownSeconds: 90, + }, + }, + }, + }, + slack: { + allowBots: "mentions", + botLoopProtection: { + maxEventsPerWindow: 8, + }, + }, + matrix: { + allowBots: "mentions", + groups: { + "!roomid:example.org": { + botLoopProtection: { + maxEventsPerWindow: 5, + }, + }, + }, + }, + googlechat: { + allowBots: true, + groups: { + "spaces/AAAA": { + botLoopProtection: { + maxEventsPerWindow: 5, + }, + }, + }, + }, + }, +} +``` + +## Channel support + +- Discord: native `author.bot` facts, keyed by Discord account, channel, and bot pair. +- Slack: native `bot_id` facts for accepted bot-authored messages, keyed by Slack account, channel, and bot pair. +- Matrix: configured Matrix bot accounts, keyed by Matrix account, room, and configured bot pair. +- Google Chat: native `sender.type=BOT` facts for accepted bot-authored messages, keyed by account, space, and bot pair. + +Channels that do not expose a reliable inbound bot identity keep using their +normal self-message and access-policy filters. They should not opt into this +guard until they can identify both participants in the bot pair. + +See [SDK runtime](/plugins/sdk-runtime#reusable-runtime-utilities) for plugin +implementation details. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 0b78a2dce1f..9aeca1cb624 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -1569,10 +1569,39 @@ openclaw logs --follow If you set `channels.discord.allowBots=true`, use strict mention and allowlist rules to avoid loop behavior. Prefer `channels.discord.allowBots="mentions"` to only accept bot messages that mention the bot. + OpenClaw also ships shared [bot loop protection](/channels/bot-loop-protection). Whenever `allowBots` lets bot-authored messages reach dispatch, Discord maps the inbound event to `(account, channel, bot pair)` facts and the generic pair guard suppresses the pair after it crosses the configured event budget. The guard prevents runaway two-bot loops that previously had to be stopped by Discord rate limits; it does not affect single-bot deployments or one-shot bot replies that stay under the budget. + + Default settings (active when `allowBots` is set): + + - `maxEventsPerWindow: 20` -- bot pair can exchange 20 messages within the sliding window + - `windowSeconds: 60` -- sliding window length + - `cooldownSeconds: 60` -- once the budget trips, every additional bot-to-bot message in either direction is dropped for one minute + + Configure the shared default once under `channels.defaults.botLoopProtection`, then override Discord when a legitimate workflow needs more headroom. Precedence is: + + - `channels.discord.accounts..botLoopProtection` + - `channels.discord.botLoopProtection` + - `channels.defaults.botLoopProtection` + - built-in defaults + + Discord uses the generic `maxEventsPerWindow`, `windowSeconds`, and `cooldownSeconds` keys. + ```json5 { channels: { + defaults: { + botLoopProtection: { + maxEventsPerWindow: 20, + windowSeconds: 60, + cooldownSeconds: 60, + }, + }, discord: { + // Optional Discord-wide override. Account blocks override individual + // fields and inherit omitted fields from here. + botLoopProtection: { + maxEventsPerWindow: 4, + }, accounts: { mantis: { // Mantis listens to other bots only when they mention her. @@ -1585,6 +1614,12 @@ openclaw logs --follow // Lets Molty write "@Mantis" and send a real Discord mention. Mantis: "MANTIS_DISCORD_USER_ID", }, + botLoopProtection: { + // Allow up to five messages per minute before suppressing the pair. + maxEventsPerWindow: 5, + windowSeconds: 60, + cooldownSeconds: 90, + }, }, }, }, diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md index 4b16e209019..ad294d5e588 100644 --- a/docs/channels/googlechat.md +++ b/docs/channels/googlechat.md @@ -185,6 +185,7 @@ Use these identifiers for delivery and allowlists: audience: "https://gateway.example.com/googlechat", webhookPath: "/googlechat", botUser: "users/1234567890", // optional; helps mention detection + allowBots: false, dm: { policy: "pairing", allowFrom: ["users/1234567890"], @@ -216,6 +217,7 @@ Notes: - Message actions expose `send` for text and `upload-file` for explicit attachment sends. `upload-file` accepts `media` / `filePath` / `path` plus optional `message`, `filename`, and thread targeting. - `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth). - Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`). +- Bot-authored Google Chat messages are ignored by default. If you intentionally set `allowBots: true`, accepted bot-authored messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override with `channels.googlechat.botLoopProtection` or `channels.googlechat.groups..botLoopProtection` when one space needs a different budget. Secrets reference details: [Secrets Management](/gateway/secrets). diff --git a/docs/channels/index.md b/docs/channels/index.md index 14908431f1c..6bd3d13b7ab 100644 --- a/docs/channels/index.md +++ b/docs/channels/index.md @@ -18,6 +18,9 @@ Text is supported everywhere; media and reactions vary by channel. - WhatsApp setup is install-on-demand: onboarding can show the setup flow before the plugin package is installed, and the Gateway loads the external ClawHub/npm plugin only when the channel is actually active. +- Channels that accept bot-authored inbound messages can use shared + [bot loop protection](/channels/bot-loop-protection) to prevent bot pairs from + replying to each other indefinitely. ## Supported channels diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index e31488ca847..6a5158d3b93 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -266,6 +266,7 @@ Use `allowBots` when you intentionally want inter-agent Matrix traffic: - `allowBots: true` accepts messages from other configured Matrix bot accounts in allowed rooms and DMs. - `allowBots: "mentions"` accepts those messages only when they visibly mention this bot in rooms. DMs are still allowed. - `groups..allowBots` overrides the account-level setting for one room. +- Accepted configured-bot messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override with `channels.matrix.botLoopProtection` or `channels.matrix.groups..botLoopProtection` when one room needs a different budget. - OpenClaw still ignores messages from the same Matrix user ID to avoid self-reply loops. - Matrix does not expose a native bot flag here; OpenClaw treats "bot-authored" as "sent by another configured Matrix account on this OpenClaw gateway". diff --git a/docs/channels/slack.md b/docs/channels/slack.md index 66bb9d170de..4c46555b8bf 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -920,6 +920,8 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r `allowBots` is conservative for channels and private channels: bot-authored room messages are accepted only when the sending bot is explicitly listed in that room's `users` allowlist, or when at least one explicit Slack owner ID from `channels.slack.allowFrom` is currently a room member. Wildcards and display-name owner entries do not satisfy owner presence. Owner presence uses Slack `conversations.members`; make sure the app has the matching read scope for the room type (`channels:read` for public channels, `groups:read` for private channels). If the member lookup fails, OpenClaw drops the bot-authored room message. + Accepted bot-authored Slack messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection` for the default budget, then override with `channels.slack.botLoopProtection` or `channels.slack.channels..botLoopProtection` when a workspace or channel needs a different limit. + diff --git a/docs/gateway/config-channels.md b/docs/gateway/config-channels.md index 59bdfe45749..aa742ab0b70 100644 --- a/docs/gateway/config-channels.md +++ b/docs/gateway/config-channels.md @@ -335,6 +335,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat - Use `user:` (DM) or `channel:` (guild channel) for delivery targets; bare numeric IDs are rejected. - Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged name (no `#`). Prefer guild IDs. - Bot-authored messages are ignored by default. `allowBots: true` enables them; use `allowBots: "mentions"` to only accept bot messages that mention the bot (own messages still filtered). +- Channels that support bot-authored inbound messages can use shared [bot loop protection](/channels/bot-loop-protection). Set `channels.defaults.botLoopProtection` for baseline pair budgets, then override the channel or account only when one surface needs different limits. - `channels.discord.guilds..ignoreOtherMentions` (and channel overrides) drops messages that mention another user or role but not the bot (excluding @everyone/@here). - `channels.discord.mentionAliases` maps stable outbound `@handle` text to Discord user IDs before sending, so known teammates can be mentioned deterministically even when the transient directory cache is empty. Per-account overrides live under `channels.discord.accounts..mentionAliases`. - `maxLinesPerMessage` (default 17) splits tall messages even when under 2000 chars. diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md index 9f853248c06..f0e0147e25e 100644 --- a/docs/plugins/sdk-runtime.md +++ b/docs/plugins/sdk-runtime.md @@ -54,6 +54,46 @@ Internal OpenClaw runtime code has the same direction: load config once at the C Provider and channel execution paths must use the active runtime config snapshot, not a file snapshot returned for config readback or editing. File snapshots preserve source values such as SecretRef markers for UI and writes; provider callbacks need the resolved runtime view. When a helper may be called with either the active source snapshot or the active runtime snapshot, route through `selectApplicableRuntimeConfig()` before reading credentials. +## Reusable runtime utilities + +Use the channel-turn `botLoopProtection` facts for bot-authored inbound messages. Core applies the shared in-memory sliding-window guard before session record and dispatch, without tying the policy to one channel. The guard tracks `(scopeId, conversationId, participant pair)` keys, counts both directions of a pair together, applies a cooldown once the window budget is exceeded, and prunes inactive entries opportunistically. + +Channel plugins that expose this behavior to operators should prefer the shared `channels.defaults.botLoopProtection` shape for baseline budgets, then layer channel/provider-specific overrides on top. The shared config uses seconds because it is user-facing: + +```typescript +type ChannelBotLoopProtectionConfig = { + enabled?: boolean; + maxEventsPerWindow?: number; + windowSeconds?: number; + cooldownSeconds?: number; +}; +``` + +Pass normalized bot-pair facts with the resolved turn. Core resolves defaults, unit conversion, and `enabled` semantics: + +```typescript +return { + channel: "example", + routeSessionKey, + storePath, + ctxPayload, + recordInboundSession, + runDispatch, + botLoopProtection: { + scopeId: "account-1", + conversationId: "channel-1", + senderId: "bot-a", + receiverId: "bot-b", + config: channelConfig.botLoopProtection, + defaultsConfig: runtimeConfig.channels?.defaults?.botLoopProtection, + defaultEnabled: allowBotsMode !== "off", + }, +}; +``` + +Use `openclaw/plugin-sdk/pair-loop-guard-runtime` directly only for custom +two-party event loops that do not go through the shared channel-turn kernel. + ## Runtime namespaces