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).
- Bot A posts via
chat.postMessage:
<@U_bot_B|handle_b> please run the next step.
- Slack pushes a
message event (subtype bot_message) to bot B's gateway.
- 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="…"
- 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:
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).
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 becausementions_botis 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
mainHEAD8d312a3on 2026-05-12.Repro
Channel with two bots A and B (both invited,
allow_bot_messages="mentions",trusted_bot_idscontaining both).chat.postMessage:messageevent (subtypebot_message) to bot B's gateway.RUST_LOG=openab=debug):openab::adapter: processing message. Bot B never reacts.Same payload but human-authored (text
<@U_bot_B> please run the next step.) producesmentions_bot=trueand forwards correctly.conversations.repliesagainst 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_botshould betruefor anymessageevent whosetext(or structuredblocks) 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. Underallow_bot_messages="mentions", the event should then be forwarded toopenab::adapter: processing messageexactly 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:Slack's user-mention wire format has two interchangeable shapes:
<@U...>— what humans typing@handleproduce; whatchat.postMessagestores when caller wrote<@U...>.<@U...|handle>— whatchat.postMessagestores 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|:Or use a regex / a small parser. The
app_mentionbranch atsrc/slack.rs:531-573does not need the same fix —app_mentionevents carry the mentioned user in a structured field. Butapp_mentionis not fired forsubtype=bot_message, which is precisely why themessage-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).