fix(slack): humanize inbound user mentions + ground bot identity - #55340
fix(slack): humanize inbound user mentions + ground bot identity#55340benbarclay wants to merge 1 commit into
Conversation
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
2 |
unresolved-import |
1 |
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:3069: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/gateway/test_slack_mention_humanization.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
✅ Fixed issues (1):
| Rule | Count |
|---|---|
invalid-assignment |
1 |
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`
Unchanged: 6173 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Slack delivers user mentions as opaque IDs (<@u123>). The adapter stripped only the bot's OWN mention and passed every other participant's <@uid> to the agent raw. With no name map and no knowledge of its own handle, the agent could read a human's mention as a self-mention and reply to messages merely addressed to that person — it would treat a participant's mention as if it named the bot. Discord never hits this because it feeds the agent message.clean_content (IDs already rendered as names). Two cooperating fixes: - _humanize_user_mentions: rewrite remaining <@uid> (and <@uid|label>) tokens in the trigger text, thread context, and reply-to text to @DisplayName via the cached _resolve_user_name (one users.info lookup per distinct user per process). The Slack equivalent of clean_content. - _build_identity_prompt: an ephemeral system-prompt line naming the bot's own Slack handle ("you are @x; a mention of any other participant is not a mention of you"), injected through the per-turn channel_prompt seam — applied at API-call time, never persisted to history, so per-conversation prompt caching is preserved. Bot display name is captured per-workspace at connect time (multi-workspace safe, cleared on reconnect). Tests in tests/gateway/test_slack_mention_humanization.py cover single / multiple / labelled / repeated / unresolvable / no-op mention cases and the identity-prompt builder (per-team name preference, empty-before-connect). Prove-fail verified: all 9 fail with the adapter change stashed, pass with it; the existing 256 Slack tests stay green.
6e008db to
4a150a0
Compare
tonydwb
left a comment
There was a problem hiding this comment.
LGTM. Well-documented fix for the Slack bot identity confusion bug. Humanizes user mentions and grounds the bot's own identity via ephemeral channel_prompt. Clean with tests.
Code Review SummaryVerdict: Approved Replaces raw ✅ Looks Good
Reviewed by Hermes Agent |
Slack delivers user mentions as opaque IDs (<@u123>). The agent had no way to tell one participant from another — or from itself — so it could misread a mention of a human as a self-mention and answer messages addressed to that person (the "bot thinks it's @Someone-Else" bug). Two cooperating fixes: - _humanize_user_mentions rewrites remaining <@uid> tokens (the bot's own mention is stripped earlier) to @DisplayName in the trigger text and reply_to_text — the Slack equivalent of Discord's clean_content. Handles the labelled <@uid|handle> form; unresolvable IDs fall back to the raw ID. - _build_identity_prompt injects an ephemeral per-turn system-prompt line via the channel_prompt seam (applied at API-call time, never persisted — prompt caching preserved) naming the bot's own workspace handle (per-team in multi-workspace installs) so the agent has a positive "that's me" anchor. Salvaged from #55340 by @benbarclay, rebased over the workspace-scoped user-name cache (team_id-aware resolution) on main.
Slack delivers user mentions as opaque IDs (<@u123>). The agent had no way to tell one participant from another — or from itself — so it could misread a mention of a human as a self-mention and answer messages addressed to that person (the "bot thinks it's @Someone-Else" bug). Two cooperating fixes: - _humanize_user_mentions rewrites remaining <@uid> tokens (the bot's own mention is stripped earlier) to @DisplayName in the trigger text and reply_to_text — the Slack equivalent of Discord's clean_content. Handles the labelled <@uid|handle> form; unresolvable IDs fall back to the raw ID. - _build_identity_prompt injects an ephemeral per-turn system-prompt line via the channel_prompt seam (applied at API-call time, never persisted — prompt caching preserved) naming the bot's own workspace handle (per-team in multi-workspace installs) so the agent has a positive "that's me" anchor. Salvaged from #55340 by @benbarclay, rebased over the workspace-scoped user-name cache (team_id-aware resolution) on main.
|
Merged via #69320 — your commit was cherry-picked onto current main with your authorship preserved in git history: your mention humanization + bot identity grounding was cherry-picked directly. Thanks for the contribution! |
|
@benbarclay I may have found a secondary regression.
That instruction conflicts with valid unmentioned messages the Slack adapter has already chosen to route, including:
The gateway delivers these messages correctly, but the model is simultaneously told not to treat them as directed at it. Because this is natural-language guidance rather than a hard routing gate, the result is intermittent: the agent sometimes proceeds based on thread context and sometimes refuses to act without another explicit mention. I reproduced this on current A narrow fix would preserve the identity protection without overriding channel routing, maybe like the following:
|
|
After reviewing the original PR more closely and tracing the current Slack routing logic, I want to refine my earlier comment. The original failure this PR aims to resolve appears narrower than free-response behavior:
That means free-response channels were not the original bug surface. They became collateral damage because
I also do not think conditioning that existing sentence only on The intended behavior seems to be:
Hermes now has related adapter-level logic through My revised view is that routing should remain the adapter’s responsibility. The prompt should provide identity grounding only, for example:
Then, for auto-followed threads in mention-required channels, the adapter can suppress only messages that begin by mentioning another participant without also mentioning the bot. This preserves normal unmentioned thread follow-ups without changing free-response channels. |
Slack delivers user mentions as opaque IDs (<@u123>). The agent had no way to tell one participant from another — or from itself — so it could misread a mention of a human as a self-mention and answer messages addressed to that person (the "bot thinks it's @Someone-Else" bug). Two cooperating fixes: - _humanize_user_mentions rewrites remaining <@uid> tokens (the bot's own mention is stripped earlier) to @DisplayName in the trigger text and reply_to_text — the Slack equivalent of Discord's clean_content. Handles the labelled <@uid|handle> form; unresolvable IDs fall back to the raw ID. - _build_identity_prompt injects an ephemeral per-turn system-prompt line via the channel_prompt seam (applied at API-call time, never persisted — prompt caching preserved) naming the bot's own workspace handle (per-team in multi-workspace installs) so the agent has a positive "that's me" anchor. Salvaged from NousResearch#55340 by @benbarclay, rebased over the workspace-scoped user-name cache (team_id-aware resolution) on main.
Infographic
Problem
Slack delivers user mentions as opaque IDs (
<@U123>). The adapter stripped only the bot's own mention and passed every other participant's<@U…>token to the agent raw:With no name map and no knowledge of its own handle, the agent receives something like:
In a thread the bot is already participating in (correctly auto-responding to follow-ups), the model has no way to tell that
<@U07BOB>is a human and not itself — so it reads the mention as directed at it and replies. This is the reported symptom: the bot treats a participant's mention as if it named the bot, and even insists it was "explicitly mentioned." Discord never hits this because it feeds the agentmessage.clean_content(IDs already rendered as@DisplayName); Slack was the outlier.Fix (two cooperating parts)
1. Humanize inbound mentions —
_humanize_user_mentions()rewrites remaining<@UID>and<@UID|label>tokens in the trigger text, thread context, and reply-to text to@DisplayName, resolved via the cached_resolve_user_name()(oneusers.infolookup per distinct user per process). Unresolvable IDs fall back to the bare ID rather than emptying the mention. This is the Slack equivalent of Discord'sclean_content.2. Ground the bot's identity —
_build_identity_prompt()returns a line naming the bot's own Slack handle:It's injected through the per-turn
channel_promptephemeral seam — applied at API-call time, never persisted to history, so per-conversation prompt caching is preserved. The bot display name is captured per-workspace at connect time (multi-workspace safe; cleared on reconnect alongside the existing_bot_user_idreset).Tests
tests/gateway/test_slack_mention_humanization.py:<@UID|handle>), repeated, unresolvable→ID fallback, and no-mention no-opProve-fail verified: all 9 fail with the adapter change stashed, all 9 pass with it. The existing 256 Slack tests stay green. An out-of-test E2E run against the real
_humanize_user_mentions/_build_identity_promptconfirms a human's<@UID>is rendered to@DisplayNameand the identity anchor names the bot's own handle.Reinstall caveat
None — this is a pure runtime change. No manifest/scope change, so no Slack app reinstall is required; it takes effect on the next gateway restart.