feat(slack): handle inbound reaction_added events - #44508
Closed
harrisonmedmedmetrics wants to merge 1 commit into
Closed
feat(slack): handle inbound reaction_added events#44508harrisonmedmedmetrics wants to merge 1 commit into
harrisonmedmedmetrics wants to merge 1 commit into
Conversation
The Slack adapter registered handlers for message, app_mention, and file
events but had no handler for reaction_added. Slack delivers the event over
Socket Mode, but with no @app.event("reaction_added") binding Bolt silently
drops it — so reaction-driven workflows (e.g. react ✅ on a
card to approve it) never fire no matter how the app is reinstalled.
Add a reaction_added handler that routes approval emojis through the normal
_handle_slack_message pipeline as a synthetic mention, inheriting
allowed-channel gating, channel_prompt injection, session keying, and the
reaction lifecycle. Also ack reaction_removed to silence unhandled-request
logging. Includes unit tests covering dispatch on approval emoji and
non-dispatch for non-approval / bot's own / non-message-item / non-allowed
channel, plus survival of thread-text fetch failure.
teknium1
reviewed
Jul 14, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for identifying the reaction-event gap. Current main still registers reaction_added but no-ops it at plugins/platforms/slack/adapter.py:1121-1127, so the feature request remains live.
Problems
- The PR targets
gateway/platforms/slack.py, but Slack was migrated toplugins/platforms/slack/adapter.pyby560010547. This needs a port rather than a direct cherry-pick. gateway/platforms/slack.py:2215hard-codes the synthetic event aschannel_type="channel". Current routing only infers D-prefixed channels as DMs when that field is absent (plugins/platforms/slack/adapter.py:2759-2763) and emits source type from that result (:3163-3176), so a DM reaction would be classified as a group interaction.- The current no-op handlers were deliberately added in
82d570165for acknowledgement only. The agent-triggering approval contract needs maintainer confirmation during salvage.
Suggested changes
- Port the implementation and tests to the bundled Slack plugin path.
- Preserve/derive conversation type and add DM/MPIM source and session-scope coverage.
Automated hermes-sweeper review.
| "text": instruction, | ||
| "user": reactor, | ||
| "channel": channel_id, | ||
| "channel_type": "channel", |
Contributor
There was a problem hiding this comment.
This misclassifies reactions in DMs: current Slack code only infers im for D-prefixed channels when channel_type is absent, then uses the result for source/session routing. Omit this field when unavailable or derive the actual conversation type, and add DM/MPIM coverage.
teknium1
pushed a commit
that referenced
this pull request
Jul 23, 2026
Slack reaction_added events were explicitly acked and dropped, so a user reacting to a bot message (👍 to approve, ✅ to acknowledge) produced nothing. Forward them through the normal message pipeline as synthesized MessageEvents whose text is the reaction emoji (translated to unicode for common names), keeping the downstream auth gate, thread-context fetch, dedup, and skill routing unchanged. - Self-reactions and non-message items are dropped; reactions on messages not sent by this bot are dropped (Feishu-adapter parity). - The reacted-to message's thread parent becomes the synthesized thread_ts so the reaction lands in the same session as a reply would. - Manifest gains reactions:read scope + reaction_added bot event. Salvaged from PR #29916 by @bpross. Related: #33111, #44508, #45265 (same cluster).
teknium1
added a commit
that referenced
this pull request
Jul 23, 2026
… handoff Build the full reaction pipeline on top of the #29916 base: - Opt-in gate: slack.reaction_triggers (default OFF — reaction events stay acked-and-dropped so busy channels don't wake the agent on every emoji). 'true' routes reactions on the bot's OWN messages; an explicit emoji-name list routes those emojis from any message (handoff flows). - reaction_removed events now route too, distinguished by the cross-platform text convention reaction:added:<emoji> / reaction:removed:<emoji> (matches the Feishu and Photon adapters, so agents and skills see one shape everywhere). - Authorization: the reactor becomes the synthesized message's user, so the early _is_user_authorized gate and allowed_channels whitelist apply exactly as for typed messages. _hermes_force_process only skips the mention requirement (a reaction on the bot's own message is definitionally addressed to the bot), mirroring Feishu/Photon. - Gateway hooks (#33111 by @johnkattenhorn): every human reaction on a message item fires reaction:added / reaction:removed through the new BasePlatformAdapter.set_reaction_handler → GatewayRunner ._handle_reaction_event → HookRegistry.emit, independent of the routing opt-in. Documented in hooks.md. - Channel handoff (#45265 by @Kev-fs): slack.reaction_trigger_target routes the reaction turn to a configured channel (top-level via _hermes_no_thread_response + reply-anchor suppression in gateway/platforms/base.py) or C123:<ts> thread. - Manifest: reaction_removed event subscription added alongside reaction_added/reactions:read. - Docs: slack.md Reaction Triggers section; hooks.md event table rows. Also credits #44508 by @harrisonmedmedmetrics (inbound reaction_added handling — same plumbing class, superseded by this consolidated shape). Co-authored-by: johnkattenhorn <john.kattenhorn.personal@gmail.com> Co-authored-by: Kev-fs <kevin@fleetsmarts.net> Co-authored-by: harrisonmedmedmetrics <harrison@medmetricsrx.com>
teknium1
pushed a commit
that referenced
this pull request
Jul 23, 2026
Slack reaction_added events were explicitly acked and dropped, so a user reacting to a bot message (👍 to approve, ✅ to acknowledge) produced nothing. Forward them through the normal message pipeline as synthesized MessageEvents whose text is the reaction emoji (translated to unicode for common names), keeping the downstream auth gate, thread-context fetch, dedup, and skill routing unchanged. - Self-reactions and non-message items are dropped; reactions on messages not sent by this bot are dropped (Feishu-adapter parity). - The reacted-to message's thread parent becomes the synthesized thread_ts so the reaction lands in the same session as a reply would. - Manifest gains reactions:read scope + reaction_added bot event. Salvaged from PR #29916 by @bpross. Related: #33111, #44508, #45265 (same cluster).
teknium1
added a commit
that referenced
this pull request
Jul 23, 2026
… handoff Build the full reaction pipeline on top of the #29916 base: - Opt-in gate: slack.reaction_triggers (default OFF — reaction events stay acked-and-dropped so busy channels don't wake the agent on every emoji). 'true' routes reactions on the bot's OWN messages; an explicit emoji-name list routes those emojis from any message (handoff flows). - reaction_removed events now route too, distinguished by the cross-platform text convention reaction:added:<emoji> / reaction:removed:<emoji> (matches the Feishu and Photon adapters, so agents and skills see one shape everywhere). - Authorization: the reactor becomes the synthesized message's user, so the early _is_user_authorized gate and allowed_channels whitelist apply exactly as for typed messages. _hermes_force_process only skips the mention requirement (a reaction on the bot's own message is definitionally addressed to the bot), mirroring Feishu/Photon. - Gateway hooks (#33111 by @johnkattenhorn): every human reaction on a message item fires reaction:added / reaction:removed through the new BasePlatformAdapter.set_reaction_handler → GatewayRunner ._handle_reaction_event → HookRegistry.emit, independent of the routing opt-in. Documented in hooks.md. - Channel handoff (#45265 by @Kev-fs): slack.reaction_trigger_target routes the reaction turn to a configured channel (top-level via _hermes_no_thread_response + reply-anchor suppression in gateway/platforms/base.py) or C123:<ts> thread. - Manifest: reaction_removed event subscription added alongside reaction_added/reactions:read. - Docs: slack.md Reaction Triggers section; hooks.md event table rows. Also credits #44508 by @harrisonmedmedmetrics (inbound reaction_added handling — same plumbing class, superseded by this consolidated shape). Co-authored-by: johnkattenhorn <john.kattenhorn.personal@gmail.com> Co-authored-by: Kev-fs <kevin@fleetsmarts.net> Co-authored-by: harrisonmedmedmetrics <harrison@medmetricsrx.com>
teknium1
pushed a commit
that referenced
this pull request
Jul 23, 2026
Slack reaction_added events were explicitly acked and dropped, so a user reacting to a bot message (👍 to approve, ✅ to acknowledge) produced nothing. Forward them through the normal message pipeline as synthesized MessageEvents whose text is the reaction emoji (translated to unicode for common names), keeping the downstream auth gate, thread-context fetch, dedup, and skill routing unchanged. - Self-reactions and non-message items are dropped; reactions on messages not sent by this bot are dropped (Feishu-adapter parity). - The reacted-to message's thread parent becomes the synthesized thread_ts so the reaction lands in the same session as a reply would. - Manifest gains reactions:read scope + reaction_added bot event. Salvaged from PR #29916 by @bpross. Related: #33111, #44508, #45265 (same cluster).
teknium1
added a commit
that referenced
this pull request
Jul 23, 2026
… handoff Build the full reaction pipeline on top of the #29916 base: - Opt-in gate: slack.reaction_triggers (default OFF — reaction events stay acked-and-dropped so busy channels don't wake the agent on every emoji). 'true' routes reactions on the bot's OWN messages; an explicit emoji-name list routes those emojis from any message (handoff flows). - reaction_removed events now route too, distinguished by the cross-platform text convention reaction:added:<emoji> / reaction:removed:<emoji> (matches the Feishu and Photon adapters, so agents and skills see one shape everywhere). - Authorization: the reactor becomes the synthesized message's user, so the early _is_user_authorized gate and allowed_channels whitelist apply exactly as for typed messages. _hermes_force_process only skips the mention requirement (a reaction on the bot's own message is definitionally addressed to the bot), mirroring Feishu/Photon. - Gateway hooks (#33111 by @johnkattenhorn): every human reaction on a message item fires reaction:added / reaction:removed through the new BasePlatformAdapter.set_reaction_handler → GatewayRunner ._handle_reaction_event → HookRegistry.emit, independent of the routing opt-in. Documented in hooks.md. - Channel handoff (#45265 by @Kev-fs): slack.reaction_trigger_target routes the reaction turn to a configured channel (top-level via _hermes_no_thread_response + reply-anchor suppression in gateway/platforms/base.py) or C123:<ts> thread. - Manifest: reaction_removed event subscription added alongside reaction_added/reactions:read. - Docs: slack.md Reaction Triggers section; hooks.md event table rows. Also credits #44508 by @harrisonmedmedmetrics (inbound reaction_added handling — same plumbing class, superseded by this consolidated shape). Co-authored-by: johnkattenhorn <john.kattenhorn.personal@gmail.com> Co-authored-by: Kev-fs <kevin@fleetsmarts.net> Co-authored-by: harrisonmedmedmetrics <harrison@medmetricsrx.com>
teknium1
pushed a commit
that referenced
this pull request
Jul 23, 2026
Slack reaction_added events were explicitly acked and dropped, so a user reacting to a bot message (👍 to approve, ✅ to acknowledge) produced nothing. Forward them through the normal message pipeline as synthesized MessageEvents whose text is the reaction emoji (translated to unicode for common names), keeping the downstream auth gate, thread-context fetch, dedup, and skill routing unchanged. - Self-reactions and non-message items are dropped; reactions on messages not sent by this bot are dropped (Feishu-adapter parity). - The reacted-to message's thread parent becomes the synthesized thread_ts so the reaction lands in the same session as a reply would. - Manifest gains reactions:read scope + reaction_added bot event. Salvaged from PR #29916 by @bpross. Related: #33111, #44508, #45265 (same cluster).
teknium1
added a commit
that referenced
this pull request
Jul 23, 2026
… handoff Build the full reaction pipeline on top of the #29916 base: - Opt-in gate: slack.reaction_triggers (default OFF — reaction events stay acked-and-dropped so busy channels don't wake the agent on every emoji). 'true' routes reactions on the bot's OWN messages; an explicit emoji-name list routes those emojis from any message (handoff flows). - reaction_removed events now route too, distinguished by the cross-platform text convention reaction:added:<emoji> / reaction:removed:<emoji> (matches the Feishu and Photon adapters, so agents and skills see one shape everywhere). - Authorization: the reactor becomes the synthesized message's user, so the early _is_user_authorized gate and allowed_channels whitelist apply exactly as for typed messages. _hermes_force_process only skips the mention requirement (a reaction on the bot's own message is definitionally addressed to the bot), mirroring Feishu/Photon. - Gateway hooks (#33111 by @johnkattenhorn): every human reaction on a message item fires reaction:added / reaction:removed through the new BasePlatformAdapter.set_reaction_handler → GatewayRunner ._handle_reaction_event → HookRegistry.emit, independent of the routing opt-in. Documented in hooks.md. - Channel handoff (#45265 by @Kev-fs): slack.reaction_trigger_target routes the reaction turn to a configured channel (top-level via _hermes_no_thread_response + reply-anchor suppression in gateway/platforms/base.py) or C123:<ts> thread. - Manifest: reaction_removed event subscription added alongside reaction_added/reactions:read. - Docs: slack.md Reaction Triggers section; hooks.md event table rows. Also credits #44508 by @harrisonmedmedmetrics (inbound reaction_added handling — same plumbing class, superseded by this consolidated shape). Co-authored-by: johnkattenhorn <john.kattenhorn.personal@gmail.com> Co-authored-by: Kev-fs <kevin@fleetsmarts.net> Co-authored-by: harrisonmedmedmetrics <harrison@medmetricsrx.com>
Contributor
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
Slack reaction_added events were explicitly acked and dropped, so a user reacting to a bot message (👍 to approve, ✅ to acknowledge) produced nothing. Forward them through the normal message pipeline as synthesized MessageEvents whose text is the reaction emoji (translated to unicode for common names), keeping the downstream auth gate, thread-context fetch, dedup, and skill routing unchanged. - Self-reactions and non-message items are dropped; reactions on messages not sent by this bot are dropped (Feishu-adapter parity). - The reacted-to message's thread parent becomes the synthesized thread_ts so the reaction lands in the same session as a reply would. - Manifest gains reactions:read scope + reaction_added bot event. Salvaged from PR NousResearch#29916 by @bpross. Related: NousResearch#33111, NousResearch#44508, NousResearch#45265 (same cluster).
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
… handoff Build the full reaction pipeline on top of the NousResearch#29916 base: - Opt-in gate: slack.reaction_triggers (default OFF — reaction events stay acked-and-dropped so busy channels don't wake the agent on every emoji). 'true' routes reactions on the bot's OWN messages; an explicit emoji-name list routes those emojis from any message (handoff flows). - reaction_removed events now route too, distinguished by the cross-platform text convention reaction:added:<emoji> / reaction:removed:<emoji> (matches the Feishu and Photon adapters, so agents and skills see one shape everywhere). - Authorization: the reactor becomes the synthesized message's user, so the early _is_user_authorized gate and allowed_channels whitelist apply exactly as for typed messages. _hermes_force_process only skips the mention requirement (a reaction on the bot's own message is definitionally addressed to the bot), mirroring Feishu/Photon. - Gateway hooks (NousResearch#33111 by @johnkattenhorn): every human reaction on a message item fires reaction:added / reaction:removed through the new BasePlatformAdapter.set_reaction_handler → GatewayRunner ._handle_reaction_event → HookRegistry.emit, independent of the routing opt-in. Documented in hooks.md. - Channel handoff (NousResearch#45265 by @Kev-fs): slack.reaction_trigger_target routes the reaction turn to a configured channel (top-level via _hermes_no_thread_response + reply-anchor suppression in gateway/platforms/base.py) or C123:<ts> thread. - Manifest: reaction_removed event subscription added alongside reaction_added/reactions:read. - Docs: slack.md Reaction Triggers section; hooks.md event table rows. Also credits NousResearch#44508 by @harrisonmedmedmetrics (inbound reaction_added handling — same plumbing class, superseded by this consolidated shape). Co-authored-by: johnkattenhorn <john.kattenhorn.personal@gmail.com> Co-authored-by: Kev-fs <kevin@fleetsmarts.net> Co-authored-by: harrisonmedmedmetrics <harrison@medmetricsrx.com>
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
- john.kattenhorn.personal@gmail.com -> johnkattenhorn (NousResearch#33111) - harrison@medmetricsrx.com -> harrisonmedmedmetrics (NousResearch#44508) - kevin@fleetsmarts.net -> Kev-fs (NousResearch#45265)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Slack adapter (
gateway/platforms/slack.py) registers@self._app.event(...)handlers formessage,app_mention, and the file lifecycle events — but none forreaction_added. In Socket Mode, Slack delivers thereaction_addedevent over the websocket, but with no handler bound, Bolt silently drops it (at most an "unhandled request" log).The practical effect: reaction-driven workflows never fire. A common pattern — react ✅ on a bot-posted card to approve/trigger an action — does nothing, and there's no clear signal why. Users typically chase this on the Slack side (re-adding the
reactions:readscope, re-subscribing the event, reinstalling the app) when in fact the scope/subscription are correct and the gap is purely that Hermes never listens for the event.Fix
Add a
reaction_addedhandler that routes approval emojis through the existing_handle_slack_messagepipeline by synthesizing a mention-shaped event. Reusing that pipeline means the reaction path inherits, for free:_slack_allowed_channels())channel_promptinjection and channel skillsDesign details:
white_check_mark,heavy_check_mark,ballot_box_with_check. Other reactions are ignored.<@bot_uid>) so the existingrequire_mentiongate passes naturally — no special-casing inside_handle_slack_message.tsis the reaction'sevent_ts, not the reacted message's ts (theMessageDeduplicatorkeys on ts and would otherwise drop it as a duplicate of the already-seen card).thread_tsis set to the reacted message ts so the reply threads under the card.reaction_removedso Bolt stops logging unhandled-request warnings for it.Tests
New
TestReactionAddedclass intests/gateway/test_slack.py:_fetch_thread_parent_textraising (still dispatches)Full Slack suite green locally: 200 passed (194 existing + 6 new).
py_compileclean.Notes
+208 / -0across two files; no existing behavior changed (purely additive event handlers + the new method).slack.approval_reactions) if preferred.