Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions charts/openab/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,11 @@ agents:
trustedBotIds: []
# maxBotTurns: soft cap on consecutive bot turns per thread before
# the bot stops auto-replying. A human message resets the counter.
# Default 20 (Rust-side `default_max_bot_turns()`). Raise for long
# Default 100 (Rust-side `default_max_bot_turns()`). Raise for long
# multi-agent collaborations; lower to throttle runaway loops more
# aggressively. Hard cap remains 100 regardless (compiled-in).
# maxBotTurns: 20
# aggressively. Compiled-in hard cap of 1000 consecutive bot messages
# is always enforced (not configurable).
# maxBotTurns: 100
# messageProcessingMode: "per-message" (default) | "per-thread" | "per-lane"
# per-thread: all senders in a thread share one batch → one ACP turn per turn boundary
# per-lane: each (thread, sender) batches independently → no silent-drop risk
Expand Down Expand Up @@ -189,10 +190,11 @@ agents:
allowUserMessages: "involved"
# maxBotTurns: soft cap on consecutive bot turns per thread before
# the bot stops auto-replying. A human message resets the counter.
# Default 20 (Rust-side `default_max_bot_turns()`). Raise for long
# Default 100 (Rust-side `default_max_bot_turns()`). Raise for long
# multi-agent collaborations; lower to throttle runaway loops more
# aggressively. Hard cap remains 100 regardless (compiled-in).
# maxBotTurns: 20
# aggressively. Compiled-in hard cap of 1000 consecutive bot messages
# is always enforced (not configurable).
# maxBotTurns: 100
# messageProcessingMode: "per-message" (default) | "per-thread" | "per-lane"
# per-thread: all senders in a thread share one batch → one ACP turn per turn boundary
# per-lane: each (thread, sender) batches independently → no silent-drop risk
Expand Down
2 changes: 1 addition & 1 deletion config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ allowed_channels = ["1234567890"] # ↑ omitted + non-empty list → auto-
# "mentions" = always require @mention
# "multibot-mentions" = like "involved", but require @mention
# once another bot has posted in the thread
# max_bot_turns = 20 # soft cap on consecutive bot turns per thread (human msg resets)
# max_bot_turns = 100 # soft cap on consecutive bot turns per thread (human msg resets)

# [gateway]
# url = "ws://openab-gateway:8080/ws" # WebSocket URL of the custom gateway
Expand Down
4 changes: 2 additions & 2 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Discord adapter. Requires a Discord bot token.
| `trusted_bot_ids` | string[] | `[]` | When non-empty, only these bot IDs pass the bot gate. Empty = any bot (mode permitting). Ignored when `allow_bot_messages = "off"`. |
| `allow_user_messages` | string | `"involved"` | `"involved"` — reply in threads bot has participated in without @mention; channel messages require @mention; DMs always process. `"mentions"` — always require @mention. `"multibot-mentions"` — like `"involved"`, but require @mention once another bot has posted in the thread. |
| `allow_dm` | bool | `false` | `true` = respond to Discord DMs; `false` = ignore DMs. `allowed_users` still applies in DMs. Each DM user consumes one session slot. |
| `max_bot_turns` | u32 | `20` | Max consecutive bot turns per thread before throttling. Human message resets the counter. Note: when `allow_bot_messages = "all"`, a separate hardcoded cap of 10 (`MAX_CONSECUTIVE_BOT_TURNS`) stops bot replies regardless of this value. |
| `max_bot_turns` | u32 | `100` | Max consecutive bot turns per thread before throttling (soft limit). Human message resets the counter. A compiled-in hard cap of 1000 consecutive bot messages is always enforced. |

---

Expand All @@ -59,7 +59,7 @@ Slack adapter using Socket Mode. Requires both a Bot User OAuth Token and an App
| `allow_bot_messages` | string | `"off"` | Same as Discord. |
| `trusted_bot_ids` | string[] | `[]` | Slack Bot User IDs (`U...`). Find via: click bot profile → Copy member ID. |
| `allow_user_messages` | string | `"involved"` | Same as Discord. |
| `max_bot_turns` | u32 | `20` | Same as Discord. |
| `max_bot_turns` | u32 | `100` | Same as Discord. |

---

Expand Down
8 changes: 4 additions & 4 deletions docs/discord.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,16 +235,16 @@ allow_bot_messages = "mentions"

To prevent runaway bot-to-bot loops, OpenAB enforces two layers of protection:

- **Soft limit** (`max_bot_turns`, default: 20) — total bot messages in a thread without human intervention. When reached, the bot sends a one-time warning and stops responding. A human message in the thread resets the counter.
- **Hard limit** (100, not configurable) — absolute cap on bot turns between human interventions. When reached, bot-to-bot conversation stops until a human replies.
- **Soft limit** (`max_bot_turns`, default: 100) — total bot messages in a thread without human intervention. When reached, the bot sends a one-time warning and stops responding. A human message in the thread resets the counter.
- **Hard limit** (1000, not configurable) — cap on consecutive bot messages in `allow_bot_messages = "all"` mode. When reached, bot-to-bot conversation stops until a human replies.

Both limits count **all** bot messages in the thread, including the bot's own replies. In a two-bot ping-pong with `max_bot_turns = 20`, each bot sends ~10 messages before the limit triggers.
Both limits count **all** bot messages in the thread, including the bot's own replies. In a two-bot ping-pong with `max_bot_turns = 100`, each bot sends ~50 messages before the limit triggers.

Warning messages are sent exactly once (on the exact threshold hit) to prevent warnings from ping-ponging between bots.

```toml
[discord]
max_bot_turns = 30 # default is 20
max_bot_turns = 200 # default is 100
```

### Ice-breaking: teaching bots who's in the room
Expand Down
8 changes: 4 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl<'de> Deserialize<'de> for MessageProcessingMode {
/// Inspired by Hermes Agent's `DISCORD_ALLOW_BOTS` 3-value design:
/// - `Off` (default): ignore all bot messages (safe default, no behavior change)
/// - `Mentions`: only process bot messages that @mention this bot (natural loop breaker)
/// - `All`: process all bot messages (capped at `MAX_CONSECUTIVE_BOT_TURNS`)
/// - `All`: process all bot messages (hard-capped at 1000 consecutive bot turns)
///
/// The bot's own messages are always ignored regardless of this setting.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -143,7 +143,7 @@ pub struct DiscordConfig {
#[serde(default)]
pub allow_user_messages: AllowUsers,
/// Max consecutive bot turns (without human intervention) before throttling.
/// Human message resets the counter. Default: 20.
/// Human message resets the counter. Default: 100.
#[serde(default = "default_max_bot_turns")]
pub max_bot_turns: u32,
/// Allow the bot to respond to Discord direct messages (DMs).
Expand All @@ -161,7 +161,7 @@ pub struct DiscordConfig {
pub max_batch_tokens: usize,
}

fn default_max_bot_turns() -> u32 { 20 }
fn default_max_bot_turns() -> u32 { 100 }
fn default_max_buffered_messages() -> usize { 10 }
fn default_max_batch_tokens() -> usize { 24_000 }

Expand Down Expand Up @@ -217,7 +217,7 @@ pub struct SlackConfig {
#[serde(default)]
pub allow_user_messages: AllowUsers,
/// Max consecutive bot turns (without human intervention) before throttling.
/// Human message resets the counter. Default: 20.
/// Human message resets the counter. Default: 100.
#[serde(default = "default_max_bot_turns")]
pub max_bot_turns: u32,
/// Message dispatch mode. Default: per-message.
Expand Down
5 changes: 3 additions & 2 deletions src/discord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use tracing::{debug, error, info};

/// Hard cap on consecutive bot messages in a channel or thread.
/// Prevents runaway loops between multiple bots in "all" mode.
const MAX_CONSECUTIVE_BOT_TURNS: u8 = 10;
const MAX_CONSECUTIVE_BOT_TURNS: u32 = 1000;

/// Maximum entries in the participation cache before eviction.
const PARTICIPATION_CACHE_MAX: usize = 1000;
Expand Down Expand Up @@ -368,6 +368,7 @@ impl EventHandler for Handler {
AllowBots::Mentions => if !is_mentioned { return; },
AllowBots::All => {
let cap = MAX_CONSECUTIVE_BOT_TURNS as usize;
let limit = std::cmp::min(MAX_CONSECUTIVE_BOT_TURNS, 100) as u8;
let history = ctx.cache.channel_messages(msg.channel_id)
.map(|msgs| {
let mut recent: Vec<_> = msgs.iter()
Expand All @@ -384,7 +385,7 @@ impl EventHandler for Handler {
cached
} else {
match msg.channel_id
.messages(&ctx.http, serenity::builder::GetMessages::new().before(msg.id).limit(MAX_CONSECUTIVE_BOT_TURNS))
.messages(&ctx.http, serenity::builder::GetMessages::new().before(msg.id).limit(limit))
.await
{
Ok(msgs) => msgs,
Expand Down
9 changes: 5 additions & 4 deletions src/slack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ impl ChatAdapter for SlackAdapter {
// --- Socket Mode event loop ---

/// Hard cap on consecutive bot messages in a thread. Prevents runaway loops.
const MAX_CONSECUTIVE_BOT_TURNS: usize = 10;
const MAX_CONSECUTIVE_BOT_TURNS: usize = 1000;

/// Run the Slack adapter using Socket Mode (persistent WebSocket, no public URL needed).
/// Reconnects automatically on disconnect.
Expand Down Expand Up @@ -667,7 +667,8 @@ pub async fn run_slack_adapter(
AllowBots::All => {
// Loop protection: count consecutive bot msgs (fail-closed)
if let Some(thread_ts) = event["thread_ts"].as_str() {
let limit_str = (MAX_CONSECUTIVE_BOT_TURNS + 1).to_string();
let cap = MAX_CONSECUTIVE_BOT_TURNS;
let limit_str = std::cmp::min(cap + 1, 1000).to_string();
match adapter.api_get(
"conversations.replies",
&[
Expand All @@ -685,8 +686,8 @@ pub async fn run_slack_adapter(
|| m["subtype"].as_str() == Some("bot_message")
})
.count();
if consecutive >= MAX_CONSECUTIVE_BOT_TURNS {
warn!("bot turn cap reached ({MAX_CONSECUTIVE_BOT_TURNS}), ignoring");
if consecutive >= cap {
warn!(channel_id, cap, "bot turn cap reached, ignoring");
continue;
}
}
Expand Down
Loading