Skip to content

bug(slack): mentions_bot filter misses canonical <@U|handle> label form, breaking bot-to-bot relays #800

@howie

Description

@howie

Summary

Under slack.allow_bot_messages = "mentions", bot-to-bot messages that correctly use the canonical Slack user-mention syntax with a handle label (<@U...|handle>) are silently dropped by the gateway because mentions_bot is computed via a literal substring check that only matches the bare <@U...> form. Human re-tag (which Slack stores as the bare form) works, so the bug is invisible until multi-bot autonomous relays are tested.

Verified against main HEAD 8d312a3 on 2026-05-12.

Repro

Channel with two bots A and B (both invited, allow_bot_messages="mentions", trusted_bot_ids containing both).

  1. Bot A posts via chat.postMessage:
    <@U_bot_B|handle_b> please run the next step.
    
  2. Slack pushes a message event (subtype bot_message) to bot B's gateway.
  3. Bot B logs (with RUST_LOG=openab=debug):
    openab::slack: message event received channel_id="C..." has_thread=true
      is_bot=true is_dm=false subtype="" mentions_bot=false text="…"
    
  4. Message is dropped before openab::adapter: processing message. Bot B never reacts.

Same payload but human-authored (text <@U_bot_B> please run the next step.) produces mentions_bot=true and forwards correctly.

conversations.replies against the same message confirms the canonical mention is present in both forms of evidence:

{
  "text": "... <@U_bot_B|handle_b> please ...",
  "blocks": [{
    "type": "rich_text",
    "elements": [{
      "type": "rich_text_section",
      "elements": [{ "type": "user", "user_id": "U_bot_B" }, ...]
    }]
  }]
}

So this is not a missing-event / missing-scope / missing-subscription problem — the event arrives at the gateway with valid canonical mention data.

Expected Behavior

mentions_bot should be true for any message event whose text (or structured blocks) contains a Slack canonical user-mention referencing the receiving bot, regardless of which of the two wire forms (<@U_self> or <@U_self|handle>) the sender used. Under allow_bot_messages="mentions", the event should then be forwarded to openab::adapter: processing message exactly as it currently is for the bare-form / human-authored case.

Concretely for the repro above: bot B should react to bot A's <@U_bot_B|handle_b> exactly as it would react to <@U_bot_B>, removing the need to instruct bots to ask a human to re-tag peers.

Root cause

src/slack.rs:582-584:

let mentions_bot = bot_uid_opt
    .as_ref()
    .is_some_and(|bot_uid| msg_text.contains(&format!("<@{bot_uid}>")));

Slack's user-mention wire format has two interchangeable shapes:

  • <@U...> — what humans typing @handle produce; what chat.postMessage stores when caller wrote <@U...>.
  • <@U...|handle> — what chat.postMessage stores when caller wrote the label form, which is the recommended canonical form when bots address each other (most multi-bot frameworks teach this so the receiving renderer can display the handle even when user-resolution lookups fail).

The contains("<@U...>") check only matches shape 1, so shape 2 is silently treated as not-a-mention.

Suggested fix

Match both shapes — the next byte after the user id must be > or |:

let mentions_bot = bot_uid_opt.as_ref().is_some_and(|bot_uid| {
    let prefix = format!("<@{bot_uid}");
    msg_text.match_indices(&prefix).any(|(i, _)| {
        let next = msg_text.as_bytes().get(i + prefix.len());
        matches!(next, Some(b'>') | Some(b'|'))
    })
});

Or use a regex / a small parser. The app_mention branch at src/slack.rs:531-573 does not need the same fix — app_mention events carry the mentioned user in a structured field. But app_mention is not fired for subtype=bot_message, which is precisely why the message-event path is the one bot-to-bot relies on.

Impact

Multi-bot deployments that follow standard bot-protocol guidance ("always use canonical <@U|handle> form when addressing peers") fall into a silent deadlock: messages render correctly to humans but never trigger the addressed bot. The common workaround in the wild is to instruct bots to ask a human to re-tag — frequently documented as a "Slack platform limitation" in downstream operator docs, but it is actually this filter bug.

Happy to send a PR if you'd like — fix is small enough that I'd rather defer to maintainer preference on the matching strategy (regex vs hand-rolled).

Metadata

Metadata

Assignees

No one assigned

    Labels

    b2bbugSomething isn't workingp1High — address this sprint

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions