Skip to content

feat(telegram): inbound message reactions → agent actions - #54599

Open
thesubtleforces wants to merge 1 commit into
NousResearch:mainfrom
thesubtleforces:feat/telegram-reaction-actions
Open

feat(telegram): inbound message reactions → agent actions#54599
thesubtleforces wants to merge 1 commit into
NousResearch:mainfrom
thesubtleforces:feat/telegram-reaction-actions

Conversation

@thesubtleforces

@thesubtleforces thesubtleforces commented Jun 29, 2026

Copy link
Copy Markdown

Summary

Config-driven reaction actions: a user long-presses a bot message, taps an emoji, and the gateway synthesizes an agent turn from a platforms.<name>.extra.reaction_actions emoji→instruction map. Default-empty config → fully inert and backward-compatible.

Rebuilt on top of the normalized gateway_platform_event pipeline (#64176 and follow-ups) after it landed — the original adapter-level MessageReactionHandler approach from earlier revisions of this PR is gone. The action layer now rides the existing normalize → post-auth boundary rather than adding a parallel reaction path:

How it works

  1. _normalize_reaction_event gains an additive payload field added_emojis — the new_reactionold_reaction diff. Telegram's new_reaction is the message's full current reaction set, so consumers that act once per reaction (unlike state-re-rendering observers) need the delta. Observers get this field too (hooks.md updated).
  2. At the gateway's post-auth boundary (_handle_gateway_platform_event), after the same profile-scoped _is_user_authorized decision that gates observers, a newly-added emoji with a reaction_actions mapping dispatches an agent turn through _handle_message — so session routing, history, and tool policy apply normally. The reacted-to message rides native reply context (reply_to_message_id / reply_to_text via rich_sent_store / reply_to_is_own_message=True).
  3. The adapter catch-all's inertness gate widens minimally: reaction updates proceed when reaction_actions is configured even with no hook subscriber (other update types keep the original skip). With no hook and no configured actions, events still drop before the auth check — the original short-circuit is preserved and pinned by the existing test.
  4. Hook errors and action errors are isolated from each other and from the update loop. Custom-emoji reactions carry ids, not emoji, and are never mapped.

Config

platforms:
  telegram:
    extra:
      reaction_actions:
        "👍": "Mark the task in the reacted-to message as done"
        "🔥": "Escalate the task in the reacted-to message to top priority"
        "😴": "Snooze the reminder in the reacted-to message for an hour"
        "👎": "Cancel or archive the item in the reacted-to message"

Keys must come from Telegram's fixed reaction-emoji set. Documented in website/docs/user-guide/messaging/telegram.md; the gateway_platform_event payload contract row in hooks.md gains added_emojis.

Why this shape

The landed hook is deliberately observer-only (no bot handles, return ignored), so reaction→action can't be built as a plugin — but plenty of single-user deployments want "tap 👍 to complete the task" without writing a plugin at all. A config map at the gateway boundary reuses the pipeline's auth and normalization instead of duplicating either, and stays platform-agnostic (any platform that normalizes reaction events with added_emojis gets actions for free).

Tests

tests/gateway/test_gateway_platform_event_hook.py extended with 14 cases (2 existing exact-payload assertions updated for the additive field); full platform-event + telegram-auth suites: 59 passed.

  • added_emojis diff semantics: excludes pre-existing, re-tap adds nothing, removal yields empty, malformed old_reaction treated as empty
  • Action dispatch: mapped added emoji → agent turn (text, reply context, source); unmapped / pre-existing / removed → no dispatch; observer hook unaffected either way
  • Shared auth: unauthorized reactor blocks hook and action; no-subscriber-but-configured still authorizes before acting; unconfigured + no subscriber drops before auth (original short-circuit pinned)
  • Isolation: hook error doesn't suppress the action; action error never raises into the update loop
  • Fire-site gate: reaction + configured actions normalizes without a subscriber; non-reaction updates keep the original skip

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins platform/telegram Telegram bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P3 Low — cosmetic, nice to have labels Jun 29, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: this joins an open cluster of competing Telegram-inbound-reaction PRs — #24149 (ReactionEvent/ReactionHandler types), #13992 (env-toggled routing), #53814 (telegram:reaction hook event). This PR's distinct angle is a config-driven emoji→action map plus a register_reaction plugin seam. Not a duplicate (different mechanism); flagging the cluster so a maintainer can pick one canonical approach.

@thesubtleforces thesubtleforces left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Approved

@thesubtleforces

Copy link
Copy Markdown
Author

Approved

1 similar comment
@thesubtleforces

Copy link
Copy Markdown
Author

Approved

@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 focused Telegram implementation. The inbound path is still absent from current main, but this version needs changes before it can safely enter the current adapter.

Problems

  • Authorization bypass: plugins/platforms/telegram/adapter.py:7210-7223 calls a plugin handler, and :7272 dispatches an agent event, without the current intake authorization prefilter. Current main documents that _is_user_authorized_from_message() prevents unauthorized prompt injection before event construction (plugins/platforms/telegram/adapter.py:908-916) and calls it in all normal inbound handlers (:7823, :7849, :7869, :8072).
  • The PR body’s telegram.extra.reaction_actions example is not a usable config path: current docs state top-level telegram.extra keys are silently dropped (website/docs/user-guide/messaging/telegram.md:468-481).
  • The body says config is checked before plugins, but :7210-7223 gives a plugin handler priority over a configured action for the same emoji; there is no collision test.
  • register_reaction adds a new generic plugin surface (hermes_cli/plugins.py:581-613) without a consumer. The repository rubric rejects speculative extension points (AGENTS.md:98-101), and the prior member comment notes competing reaction-interface PRs.

Suggested changes

  • Authorize the reaction actor before either plugin dispatch or agent dispatch, with regression tests for both paths.
  • Document platforms.telegram.extra.reaction_actions and test precedence explicitly.
  • Align the plugin seam with the canonical interface selected for the reaction PR cluster.

Automated hermes-sweeper review.

Comment thread plugins/platforms/telegram/adapter.py Outdated
…oundary

Rebuild of the reaction-actions feature on top of the normalized
gateway_platform_event pipeline (NousResearch#64176 and follow-ups), replacing the
original adapter-level MessageReactionHandler approach entirely:

- adapter: expose added_emojis (new_reaction minus old_reaction) as an
  additive payload field — Telegram's new_reaction is the full current
  set, so consumers that act once per reaction need the delta, not the
  state; the catch-all's inertness gate now also admits reaction updates
  when platforms.telegram.extra.reaction_actions is configured, so the
  action layer works without a hook subscriber
- gateway: after the SAME profile-scoped authorization decision that
  gates gateway_platform_event observers, a reaction whose newly-added
  emoji maps in platforms.<name>.extra.reaction_actions synthesizes an
  agent turn through _handle_message — the reacted-to message rides
  native reply context (reply_to_message_id/-text via rich_sent_store,
  reply_to_is_own_message); hook errors and action errors are isolated
  from each other and from the update loop
- default-empty config keeps everything inert (no hook, no actions →
  event still drops before the auth check, preserving the original
  short-circuit)
- docs: hooks.md payload row gains added_emojis; telegram.md gains the
  user-facing Reaction Actions section
- tests: 14 new cases (added_emojis diff semantics, action dispatch,
  shared-auth gating, no-subscriber path, error isolation, fire-site
  gate) + 2 exact-payload assertions updated; full platform-event and
  telegram-auth suites green (59 passed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/telegram Telegram bot 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-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants