Skip to content

fix: route Slack reaction triggers to target channel - #45265

Closed
Kev-fs wants to merge 2 commits into
NousResearch:mainfrom
Kev-fs:fix/slack-reaction-train-channel
Closed

fix: route Slack reaction triggers to target channel#45265
Kev-fs wants to merge 2 commits into
NousResearch:mainfrom
Kev-fs:fix/slack-reaction-train-channel

Conversation

@Kev-fs

@Kev-fs Kev-fs commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Route configured Slack reaction triggers to a target channel as top-level messages
  • Include a clickable permalink to the original Slack message in the handoff text
  • Add optional reaction source-channel allowlist so Karen can trigger from Dev or Karen Train only
  • Add a Karen workflow note requiring unresolved/duplicate verification before code changes

Test Plan

  • python -m pytest tests/gateway/test_slack.py::TestReactions -q -o 'addopts='
  • python -m pytest tests/gateway/test_slack.py -q -o 'addopts='

@Kev-fs
Kev-fs force-pushed the fix/slack-reaction-train-channel branch from 26d34ad to 9c1ba91 Compare June 12, 2026 23:10
@liuhao1024

Copy link
Copy Markdown
Contributor

A few notes on org-specific constants in the shared Slack adapter:

1. Hardcoded default trigger reactions (_slack_trigger_reactions):
The default "plankton,karen" are team-specific emoji names. Any Hermes user running the Slack adapter gets these defaults — operators who happen to use :plankton: or :karen: for unrelated purposes will get surprise message routing. Consider making the default empty (no triggers unless explicitly configured) or moving the defaults to config.yaml documentation.

2. Org-specific workflow note (_handle_slack_reaction_added, line ~310):

if reaction == "karen":
    source_lines.append(
        "Workflow note: :karen: on #fleetsmarts-dev or #karen-train "
        "is a request to start the code-change workflow..."
    )

This hardcodes #fleetsmarts-dev and #karen-train channel names and a team-specific workflow description into the core Slack adapter. Any Slack user who configures a :karen: trigger will get this note injected — even if their workflow is completely different. This should be user-configurable (e.g. slack.trigger_reaction_workflow_note config key) or removed from the adapter entirely.

3. Default empty means safer: The _slack_trigger_reaction_users() method already defaults to empty (unrestricted). Applying the same pattern to reactions (empty default = no triggers unless configured) would make the adapter safe-by-default for all users.

Overall the implementation is solid — dedup, allowlists, target channel routing, and tests are well done. The concern is purely about shared-core defaults.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/slack Slack app adapter labels Jun 13, 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 implementing a concrete reaction-handoff path. The feature premise remains valid: current main still acknowledges reaction_added without routing it (plugins/platforms/slack/adapter.py:1121-1123).

Problems

  • The implementation is stale after 5600105: Slack now lives in plugins/platforms/slack/adapter.py, and Slack YAML bridging is plugin-owned at plugins/platforms/slack/adapter.py:4485-4519; this diff changes the former inline adapter/core bridge instead.
  • gateway/platforms/slack.py:1724-1727 defaults every workspace to plankton,karen. Triggering agent work must be opt-in, not an organization-specific global default.
  • gateway/platforms/slack.py:1962-1970 injects FleetSmarts channel names and workflow policy into the shared adapter. Please remove that tenant-specific branch or make any note neutral and explicitly configured.
  • The PR adds a reaction-trigger path but does not document the necessary Slack event subscription. The current setup list is website/docs/user-guide/messaging/slack.md:122-133 and does not include reaction_added.

Suggested changes

  • Port the implementation and config bridge into the Slack bundled plugin, make triggers disabled until configured, document the event/configuration, and add tests for the disabled default and YAML bridge.

Automated hermes-sweeper review.

"""Return reaction names that should route the reacted-to message."""
raw = self.config.extra.get("trigger_reactions") or os.getenv(
"SLACK_TRIGGER_REACTIONS",
"plankton,karen",

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 do not make plankton,karen a shared default. This enables agent-triggering behavior for every Slack workspace; use an empty default and require explicit operator configuration.

source_lines.append(f"Original Slack message: {permalink}")
if reaction == "karen":
source_lines.append(
"Workflow note: :karen: on #fleetsmarts-dev or #karen-train "

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.

This injects FleetSmarts-specific channel names and workflow policy into Hermes for every :karen: trigger. Remove it from the shared adapter or make a neutral equivalent explicitly configurable.

@self._app.event("reaction_added")
async def handle_reaction_added(event, say):
pass
await self._handle_slack_reaction_added(event)

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 the corresponding Slack setup/docs update for subscribing to reaction_added; current user guidance lists bot events at website/docs/user-guide/messaging/slack.md:122-133 but does not include it.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state 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 14, 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): your channel-routing/handoff fix was folded into the consolidated commit with Co-authored-by credit.

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 P3 Low — cosmetic, nice to have 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 sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants