Skip to content

feat(slack): forward reaction_added/removed to gateway hooks - #33111

Closed
johnkattenhorn wants to merge 1 commit into
NousResearch:mainfrom
johnkattenhorn:reaction-events
Closed

feat(slack): forward reaction_added/removed to gateway hooks#33111
johnkattenhorn wants to merge 1 commit into
NousResearch:mainfrom
johnkattenhorn:reaction-events

Conversation

@johnkattenhorn

Copy link
Copy Markdown
Contributor

Summary

  • Slack adapter now subscribes to reaction_added and reaction_removed events and forwards them, with a resolved permalink for message targets, to a new set_reaction_handler callback on BasePlatformAdapter.
  • Gateway wires that callback to HookRegistry.emit, exposing reaction:added / reaction:removed events to user hooks alongside the existing agent:* / command:* families.
  • Documents the new events + adds a worked :task: reaction → webhook example.

Why

There's currently no way to receive reaction events in a hook handler without locally patching the adapter. Concrete use case: emoji-driven task capture — react with :task: on a Slack message and have the bot push it to Todoist / Linear / your own tracker.

Event payload

Key Notes
platform "slack"
reaction emoji name without colons (e.g. task)
user_id reactor's user ID
item_user_id author of the reacted-to message
item_type message / file / file_comment
channel_id for item_type == "message"
message_ts for item_type == "message"
permalink resolved via chat.getPermalink, best-effort
event_ts Slack event timestamp
raw_event original event dict for advanced consumers

Slack app config required

  • Bot OAuth scope reactions:read
  • Bot event subscriptions: reaction_added and reaction_removed
  • Bot must be a member of the channel (DMs work without an explicit /invite)

Adapter coverage

Slack only here. set_reaction_handler is a no-op for adapters that don't call it, so Discord and Telegram can opt in later as separate PRs without changing this surface.

Tests

Not added in this PR — the change is additive, syntax-checked, and tests/gateway/platforms/ has no Slack file today. Happy to add unit tests for the dispatch path (_forward_reaction_event → handler, _handle_reaction_eventhooks.emit) in a follow-up if it's a merge prerequisite.

Local testing

Patched into a running Hermes v0.14.0 / Slack adapter container, reacted in a channel the bot is in, confirmed reaction:added fires with the documented payload + a working permalink. Removed reaction → reaction:removed fires symmetrically.

Out of scope

  • Discord / Telegram reaction support
  • Reaction-driven agent-loop injection (i.e. treating a reaction as a synthetic message). Decided to leave that to userspace hooks for now; the raw event + permalink in the hook context is enough for capture scenarios without coupling to the agent loop.

Test plan

  • Reviewer can read the diff against main (4 files, +153 lines)
  • Optionally apply locally + add reactions:read scope + subscribe to reaction_added/reaction_removed in a Slack app config, then add a hook subscribing to reaction:added and verify it fires on emoji reactions

Adds first-class support for reaction events in the gateway hook system.
The Slack adapter now subscribes to `reaction_added` and `reaction_removed`,
resolves a permalink for message targets, and forwards the normalised
event to a new `set_reaction_handler` callback on `BasePlatformAdapter`.

The gateway wires that callback to `HookRegistry.emit` so user-authored
hooks can subscribe to `reaction:added` / `reaction:removed` with the
same shape as existing `agent:*` and `command:*` events.

Why
---
Some workflows want to capture the *act of reacting* (e.g. emoji-driven
task capture: react with `:task:` -> push to an external tracker).
There's currently no way to receive reaction events in a hook handler
unless you patch the adapter locally.

What's emitted
--------------
`reaction:added` and `reaction:removed` events with context:

  platform        -> "slack"
  reaction        -> emoji name without colons ("task")
  user_id         -> reactor's Slack user ID
  item_user_id    -> author of the target message
  item_type       -> "message" / "file" / "file_comment"
  channel_id      -> target channel (None for non-message items)
  message_ts      -> target ts (None for non-message items)
  permalink       -> resolved via chat.getPermalink (best effort)
  event_ts        -> Slack event timestamp
  raw_event       -> the original event dict for advanced consumers

Slack app config
----------------
Requires the bot OAuth scope `reactions:read` and the
`reaction_added`/`reaction_removed` bot event subscriptions. Bot must be
a member of the channel where the reaction occurs (DMs work without an
explicit /invite).

Adapter coverage
----------------
Slack only in this PR. The setter on `BasePlatformAdapter` is a no-op
for adapters that don't call the handler, so other platforms can opt in
later without API changes here. Discord and Telegram both support
reactions natively and would be reasonable follow-ups.

Tests
-----
Not added in this PR — the change is additive (no behaviour change for
adapters that don't subscribe), syntax-checked, and the slack adapter
test suite is currently empty (`tests/gateway/platforms/` has no slack
file). Happy to add unit tests for the dispatch path in a follow-up if
the maintainers want them as a prerequisite.

Docs
----
`website/docs/user-guide/features/hooks.md` updated with the two new
events plus a worked example (`:task:` reaction -> webhook).
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/slack Slack app adapter labels May 27, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the concrete Slack automation use case. The feature premise still holds on current main: plugins/platforms/slack/adapter.py:1121-1127 acknowledges both reaction events with no-op handlers, while website/docs/user-guide/features/hooks.md:73-84 has no reaction hook events.

Problems

  • The patch targets removed gateway/platforms/slack.py. Commit 5600105478ffde29d7566b45421b100eaa29c4ef moved it to plugins/platforms/slack/adapter.py; this PR is currently conflicting.
  • The proposed callback is wired only in normal startup/reconnect. Current multiplexed-profile setup has a separate adapter bootstrap at gateway/run.py:8604-8612; it needs the same wiring or profile-owned Slack adapters cannot emit reaction hooks.
  • The diff adds no tests. Existing tests/gateway/test_slack.py:241-242 only checks event registration.

Suggested changes

  • Port the adapter work to plugins/platforms/slack/adapter.py, cover all adapter bootstrap paths, and add focused forwarding/permalink/error-path tests.

Automated hermes-sweeper review.

Comment thread gateway/run.py
@@ -4130,6 +4148,7 @@ async def start(self) -> bool:
adapter.set_fatal_error_handler(self._handle_adapter_fatal_error)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also wire this callback for multiplexed-profile adapters. Current main has a separate bootstrap at gateway/run.py:8604-8612; without equivalent setup there, secondary-profile Slack reaction events will retain no handler.

@@ -1288,6 +1306,48 @@ def _convert_header(m):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add dispatch tests for both event names, normalized payloads, and a failed permalink lookup. Current Slack coverage only asserts registration of the two event names (tests/gateway/test_slack.py:241-242).

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
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 added a commit that referenced this pull request Jul 23, 2026
- john.kattenhorn.personal@gmail.com -> johnkattenhorn (#33111)
- harrison@medmetricsrx.com -> harrisonmedmedmetrics (#44508)
- kevin@fleetsmarts.net -> Kev-fs (#45265)
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 added a commit that referenced this pull request Jul 23, 2026
- john.kattenhorn.personal@gmail.com -> johnkattenhorn (#33111)
- harrison@medmetricsrx.com -> harrisonmedmedmetrics (#44508)
- kevin@fleetsmarts.net -> Kev-fs (#45265)
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 added a commit that referenced this pull request Jul 23, 2026
- john.kattenhorn.personal@gmail.com -> johnkattenhorn (#33111)
- harrison@medmetricsrx.com -> harrisonmedmedmetrics (#44508)
- kevin@fleetsmarts.net -> Kev-fs (#45265)
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 added a commit that referenced this pull request Jul 23, 2026
- john.kattenhorn.personal@gmail.com -> johnkattenhorn (#33111)
- harrison@medmetricsrx.com -> harrisonmedmedmetrics (#44508)
- kevin@fleetsmarts.net -> Kev-fs (#45265)
@teknium1

Copy link
Copy Markdown
Contributor

Closing as superseded by #70195 (merged): the gateway-hook half landed in the consolidated commit with Co-authored-by credit to you.

Thanks for the work — it's credited in #70195's summary.

@teknium1 teknium1 closed this Jul 23, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/slack Slack app adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants