hermes - 💡(How to fix) Fix feat(discord): reaction-based option selection for clarify tool (👍/👎 + 1️⃣2️⃣3️⃣4️⃣)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

This is especially noticeable because:

  • The clarify tool already structures responses as discrete options
  • Discord users are trained by decades of reaction-based poll/selection bots
  • Typing "1" or "Other" in a busy channel feels clunky vs a single click

Code Example

discord:
  reaction_options: true            # master toggle
  reaction_choice_cleanup: true     # remove reactions after selection
  reaction_choice_timeout: 120      # seconds before cleaning up stale options

---

# 1) Reaction option state tracking
_reaction_options: Dict[str, ReactionPoll]  # message_id -> poll state

@dataclass
class ReactionPoll:
    options: List[str]         # ["Yes", "No"] or ["Option 1", "Option 2", ...]
    emoji_map: Dict[str, int]  # {"👍": 0, "👎": 1} etc.
    user_id: str               # only count reactions from this user
    session_key: str           # to route the response back
    created_at: float
    timeout_task: asyncio.Task

# 2) Reaction listener
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
    # Ignore bot's own reactions
    if payload.user_id == self._client.user.id:
        return
    msg_id = str(payload.message_id)
    poll = self._reaction_options.get(msg_id)
    if not poll:
        return
    # Only count the intended user's reactions
    if str(payload.user_id) != poll.user_id:
        return
    emoji = str(payload.emoji)
    if emoji not in poll.emoji_map:
        return
    # Route the response back through the gateway
    selected = poll.options[poll.emoji_map[emoji]]
    await self._submit_reaction_response(poll, selected)
RAW_BUFFERClick to expand / collapse

Feature: Discord reaction-based option selection for the clarify tool

Problem

On Discord, when Hermes uses the clarify tool to ask a yes/no question or present multiple choices (up to 4 options), users must type out their answer — either the full text or "1" / "A". This is friction compared to how users naturally interact with Discord bots, where clicking a reaction or button to select an option is the expected UX.

This is especially noticeable because:

  • The clarify tool already structures responses as discrete options
  • Discord users are trained by decades of reaction-based poll/selection bots
  • Typing "1" or "Other" in a busy channel feels clunky vs a single click

Proposed Solution

Add a configurable reaction-based selection system for Discord (opt-in via a skill or config flag) with two tiers:

Tier 1 — Yes/No reactions (clarify with yes/no or binary questions)

When the clarify tool posts a yes/no question, automatically add:

  • 👍 — as a positive response ("Yes", "Option 1", "A")
  • 👎 — as a negative response ("No", "Option 2", "B")

When a user clicks either reaction, it's equivalent to typing that answer. The reactions are cleaned up after selection.

Tier 2 — Numbered multiple choice (up to 4 options)

When clarify presents 2-4 choices, automatically add numbered emoji reactions:

  • 1️⃣ — Choice 1
  • 2️⃣ — Choice 2
  • 3️⃣ — Choice 3
  • 4️⃣ — Choice 4

The Discord adapter listens for on_raw_reaction_add, maps the emoji to the corresponding option, and feeds the selection back as if the user typed it.

Design Constraints

  1. Only the intended user's reactions count — reactions from other users or from the bot itself are ignored. This is critical for multi-user channels.
  2. Single selection — once a reaction is detected, either lock further interaction or remove reactions.
  3. Timeout — if no reaction is received within a configurable period (e.g. 60s matching the existing approvals.timeout), clean up.
  4. Existing message compatibility — if the user does type their answer, that should still work and also trigger reaction cleanup.
  5. Opt-in — this should be behind a config flag or skill, not forced on all Discord users. Proposed config:
discord:
  reaction_options: true            # master toggle
  reaction_choice_cleanup: true     # remove reactions after selection
  reaction_choice_timeout: 120      # seconds before cleaning up stale options

Implementation Sketch

The Discord adapter already has:

  • Button component handling (_component_check_auth at line 4418+) — for exec approvals
  • Reaction support (reactions: true for read receipts)
  • Full discord.py event subscription

New components needed in DiscordAdapter:

# 1) Reaction option state tracking
_reaction_options: Dict[str, ReactionPoll]  # message_id -> poll state

@dataclass
class ReactionPoll:
    options: List[str]         # ["Yes", "No"] or ["Option 1", "Option 2", ...]
    emoji_map: Dict[str, int]  # {"👍": 0, "👎": 1} etc.
    user_id: str               # only count reactions from this user
    session_key: str           # to route the response back
    created_at: float
    timeout_task: asyncio.Task

# 2) Reaction listener
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
    # Ignore bot's own reactions
    if payload.user_id == self._client.user.id:
        return
    msg_id = str(payload.message_id)
    poll = self._reaction_options.get(msg_id)
    if not poll:
        return
    # Only count the intended user's reactions
    if str(payload.user_id) != poll.user_id:
        return
    emoji = str(payload.emoji)
    if emoji not in poll.emoji_map:
        return
    # Route the response back through the gateway
    selected = poll.options[poll.emoji_map[emoji]]
    await self._submit_reaction_response(poll, selected)

How It Connects to the clarify Tool

The clarify tool (in the gateway's run.py) produces a structured message with choices. The point of integration is after the clarify message is sent — the gateway's send/display layer passes the choice metadata along so the Discord adapter can attach reactions.

Alternatively, this could be implemented as a Hermes skill (discord-reaction-options) that intercepts clarify messages on the Discord platform and manages the reaction lifecycle. This keeps the logic out of the core gateway and makes it opt-in.

Prior Art

  • Discord reaction roles bots — decades of precedent for reaction-based selection
  • Telegram inline keyboards (reply_markup) — Hermes already uses inline keyboards on Telegram for yes/no approvals
  • Existing Discord buttons — the adapter already has button views for exec approvals (lines 4418-4806). Reactions are a simpler, lower-effort alternative to Discord UI Components for this use case.
  • #8372 — Slack reactions toggle (existing config precedent for platform reactions)
  • #13942 — Telegram reaction event routing (existing pattern for consuming platform reactions as signals)

Why Reactions Over Discord Buttons?

Discord UI Components (buttons) would be the "proper" modern approach but:

  • Buttons require a discord.ui.View posted with the message — this means modifying the send() path to optionally attach views, which is a bigger change
  • Reactions work with any existing message without changing the send pipeline
  • Reactions are universally understood by Discord users
  • Buttons can be added later as a v2 enhancement

Open Questions

  1. Should this be a built-in feature of the Discord adapter or a separate installable skill?
  2. Should it support alphabetical emoji (🇦 🇧 🇨 🇩) in addition to numbered (1️⃣ 2️⃣ 3️⃣ 4️⃣)?
  3. How should the reaction selection interact with the existing ephemeral_system_ttl cleanup — should selected polls delete the original clarify message?
  4. Should the original choice text stay embedded in the response after selection (e.g. "Selected: Yes") for channel context?

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING