feat(gateway): add message:received and message:processed hook events - #3769
feat(gateway): add message:received and message:processed hook events#3769jeremiahrthompson wants to merge 2 commits into
Conversation
|
Hey @teknium1 — this PR adds two new lifecycle hooks (message:received and message:processed) to BasePlatformAdapter so all 12 platform adapters get hook support without per-adapter modifications. References issues #3539, #2764, #3434. 30 tests added, all pass. 6822/6834 tests pass (12 pre-existing failures in test_hooks.py unrelated to these changes). Happy to address any feedback. Thanks! |
3333367 to
c6af03b
Compare
|
PR updated — rebased on latest main (0b0c1b3) with 2 clean commits:
Test results: 7165 passed / 7 failed (pre-existing failures in unrelated files: test_api_key_providers.py, test_slack.py, test_cli_tools_command.py, test_delegate.py). Our changes add zero new failures. Rebased cleanly with no adapter modifications — all 12 platforms inherit the hooks automatically. |
3ca2db5 to
11e0522
Compare
Add two new hook events that fire in BasePlatformAdapter for ALL platform adapters without requiring per-adapter modification: - message:received: fires before session registration, hooks can set context['should_process']=False to drop the message silently. Uses asyncio.wait_for with configurable 5s timeout (HERMES_HOOK_TIMEOUT). Fail-open on timeout and exception (hooks never break message delivery). - message:processed: fires after agent response is sent (or on error), includes response text, success flag, and error string. Both hooks use the existing HookRegistry. set_hooks() added to BasePlatformAdapter to wire the registry from GatewayRunner. No adapter files modified — all 12 adapters inherit hook support via BasePlatformAdapter. Ref: NousResearch#3539 NousResearch#2764 NousResearch#3434
Add 30 comprehensive tests covering: - message:received: basic flow, drop, context fields, multiple hooks, should_process identity check, async/sync handlers, fail-open - message:processed: success, error, drop coordination, context fields - Integration: photo batching, concurrent hooks, set_hooks after connect, None hooks passthrough
11e0522 to
d073ea8
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for identifying the shared gateway interception point and adding focused lifecycle coverage.
Problems
- The inbound gating use case is now covered by the supported plugin hook:
gateway/run.py:8885-8924invokespre_gateway_dispatchbefore auth/dispatch, and it supports silentskipandrewrite. This landed in1ef1e4c66989bf409de31bdbe94a0f18f98ac31c. - In this PR,
gateway/platforms/base.py:1180setsmessage:processed.successimmediately after the handler returns, before sending the response. A failed delivery would still report success. Current main tracks actual delivery atgateway/platforms/base.py:4810-4820and derives completion success at5151-5157. gateway/platforms/base.py:1059addsHERMES_HOOK_TIMEOUT;AGENTS.md:102-107requires behavioral settings such as timeouts to live inconfig.yaml.
Suggested changes
- Re-scope the inbound piece around
pre_gateway_dispatchunless a separate directory-hook surface has a distinct need. - If a post-delivery event is retained, rebuild it on the current adapter pipeline using actual delivery outcomes and cover delivery failure, cancellation, and current direct-dispatch paths.
Automated hermes-sweeper review.
| try: | ||
| await asyncio.wait_for( | ||
| self._hooks.emit("message:received", hook_ctx), | ||
| timeout=float(os.getenv("HERMES_HOOK_TIMEOUT", "5")), |
There was a problem hiding this comment.
HERMES_HOOK_TIMEOUT is a new user-facing behavioral timeout. Repository policy requires non-secret timeouts to be configured through config.yaml; please add a config setting and use it for both lifecycle waits.
| # Call the handler (this can take a while with tool calls) | ||
| response = await self._message_handler(event) | ||
| _hook_response = response | ||
| _hook_success = True |
There was a problem hiding this comment.
This marks the event successful before any response delivery. If _send_with_retry later returns an unsuccessful result, message:processed still reports success=True; derive this field from the final delivery outcome instead.
What does this PR do?
Adds two gateway hook events —
message:receivedandmessage:processed— that fire inBasePlatformAdapterfor all 12+ platform adapters without modifying any adapter code.The Problem
Multiple open PRs (#3539, #2764, #3434) independently patch platform adapter source files to answer the same question: "Should the bot respond to this message?" Each creates merge conflicts and maintenance burden. There is no extension point between "adapter receives message" and "agent processes message."
The Solution
Two new hook events in
BasePlatformAdapter.handle_message():message:received— fires before session registration. Hooks can inspect the message and setcontext["should_process"] = Falseto silently drop it. Use cases: ambient-mode classifiers, rate limiting, content filtering, analytics.message:processed— fires after the agent response is sent (or after an error). Includes response text and success/error state. Use cases: thread participation tracking, response analytics, feedback collection, audit logging.Because both hooks are in
BasePlatformAdapter(the universal funnel), they cover Discord, Telegram, Slack, WhatsApp, Signal, Matrix, Email, Mattermost, SMS, HomeAssistant, DingTalk, and Webhook automatically — including any future adapters.Example hook
Design Decisions
handle_message()in BasePlatformAdaptercontext["should_process"] = FalseHERMES_HOOK_TIMEOUTis Falsenot truthinessRelated Issue
Relates to #3539 (Telegram wake-word gating), #2764 (Slack auto-respond threads), #3434 (Discord channel routing) — all solve variants of the same problem by patching adapter source.
Type of Change
Changes Made
gateway/platforms/base.py— Add_hooksattribute,set_hooks()method,_build_hook_metadata()method,message:receivedhook call inhandle_message(),message:processedhook call in_process_message_background()gateway/hooks.py— Documentation update only (new event descriptions in docstring)gateway/run.py— Calladapter.set_hooks(self.hooks)at both adapter initialization sites (lines ~1023, ~1255)tests/gateway/test_message_lifecycle_hooks.py— New file: ~350 lines covering normal flow, drop, timeout, error, multiple hooks, edge casesNo adapter files modified. All 12 adapters inherit hook support automatically via
BasePlatformAdapter. Uses existingself.platform.valuefor platform name string.How to Test
Manual test:
~/.hermes/hooks/test-gate/HOOK.yamlwithevents: ["message:received"]handler.pythat prints context and optionally setsshould_process = Falseshould_process = False, verify message silently droppedrm -rf ~/.hermes/hooks/test-gate/Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — hooks.py docstring updatedcli-config.yaml.exampleif I added/changed config keys — N/A (HERMES_HOOK_TIMEOUT is env-only, optional)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A (hooks architecture unchanged, new events only)Screenshots / Logs