Add whatsapp.suppress_notifications: drop gateway notices on human-facing platforms - #32550
Open
marcelopaniza wants to merge 2 commits into
Open
Add whatsapp.suppress_notifications: drop gateway notices on human-facing platforms#32550marcelopaniza wants to merge 2 commits into
whatsapp.suppress_notifications: drop gateway notices on human-facing platforms#32550marcelopaniza wants to merge 2 commits into
Conversation
When Hermes drives a WhatsApp number shared with human contacts, gateway-originated notices leak into those chats. Add a per-platform flag, whatsapp.suppress_notifications (default false), that delivers only genuine agent replies and drops every gateway notice. Enforced at the single outbound chokepoint instead of per call site, so new notice types are suppressed automatically: - gateway/platforms/base.py: task-local _delivering_agent_reply ContextVar; split _send_with_retry into a marker-setting wrapper + _send_with_retry_impl (body unchanged). - gateway/platforms/whatsapp.py: drop non-reply sends in send() when whatsapp.suppress_notifications is set (raw-config read; typed-field and interim_assistant_messages fallbacks). - gateway/config.py: PlatformConfig.suppress_notifications (+ to_dict/from_dict). The file-mutation verifier footer is appended to the reply itself, so it bypasses the adapter chokepoint; gate it in the core instead: - run_agent.py: AIAgent._file_mutation_verifier_enabled() returns False when <platform>.suppress_notifications is set (CLI/TUI keep it). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collaborator
…tifications platforms The four interrupt / API-error status strings are assigned to final_response, so they ride the genuine agent-reply path and bypass the adapter notice chokepoint (which only catches separate send() notices). On platforms with suppress_notifications=true (e.g. WhatsApp shared with a human contact) these internal notices leak into the chat. Add AIAgent._suppress_platform_notifications() and gate all four sites in run_conversation() on it: emit "" / None instead of the notice when the platform suppresses. Each site already sets interrupted=True, so the emptied reply is silent -- _normalize_empty_agent_response does not resurrect empty responses on interrupted turns. CLI/TUI and every other platform keep the notices. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
19 tasks
teknium1
reviewed
Jul 13, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for tackling a real gateway-noise problem. The current implementation needs a different classification boundary before it can provide the stated guarantee.
Problems
_send_with_retryis not reply-only: current main uses it for the draining status atgateway/run.py:5301-5316and the busy/queue acknowledgement atgateway/run.py:5600-5652. Marking that helper as a genuine reply would allow those notices through.- Current background-agent output is sent directly at
gateway/run.py:13462-13467; the proposed unmarked-send()guard would suppress that genuine response. - The WhatsApp adapter moved to
plugins/platforms/whatsapp/adapter.pyin5600105478ffde29d7566b45421b100eaa29c4ef, so the patch target no longer exists on main.
Suggested changes
- Use explicit delivery intent at outbound call sites rather than inferring reply-ness from
_send_with_retry, then test status notices and background responses separately. - Port the WhatsApp integration to the bundled plugin and add regression tests for the intended preserved and suppressed paths.
Automated hermes-sweeper review.
| try: | ||
| return await self._send_with_retry_impl( | ||
| chat_id=chat_id, | ||
| content=content, |
Contributor
There was a problem hiding this comment.
_send_with_retry is not reply-only: current main calls it for the draining status (gateway/run.py:5301-5316) and busy/queue acknowledgement (gateway/run.py:5600-5652). Setting this marker for every invocation lets those gateway notices bypass the proposed suppression guard.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a per-platform flag,
whatsapp.suppress_notifications(defaultfalse),that delivers only genuine agent replies on WhatsApp and drops every
gateway-originated "notice" (status/lifecycle, "still working", shutdown,
session-reset, STT hints, runtime footer, "no home channel", kanban pings, cron
headers, etc.).
It's enforced at the single outbound chokepoint rather than per call site, so
new notice types are suppressed automatically.
One class of leak can't be caught at the adapter chokepoint: text that is
appended to the genuine reply rather than sent as a separate message. The
per-turn file-mutation verifier footer (
⚠️ File-mutation verifier: N file(s) were NOT modified…) rides the real reply, so it carries the reply marker andthe chokepoint must let it through. That one is gated in the core instead — see
the
run_agent.pychange below. This is the general pattern for anyreply-embedded developer-facing text: gate it on
<platform>.suppress_notificationswhere it is produced, since the adapter guard only sees standalone sends.
Motivation
When Hermes drives a WhatsApp number that's also used to talk to human
contacts, internal bubbles leak into those human chats and are confusing /
reputationally damaging.
display.platforms.whatsapp.interim_assistant_messages: falseonly covered part of this — many notices calladapter.send()directlyand ignored it, so suppression became per-call-site whack-a-mole and regressed
whenever a new notice type was added.
Approach
Every genuine reply (background path, synchronous path, and slash-command
responses) is delivered through
BasePlatformAdapter._send_with_retry. Gatewaynotices call
adapter.send()directly. We use that asymmetry:contextvars.ContextVar(
_delivering_agent_reply) setTrueonly while inside_send_with_retry(split into a wrapper +
_send_with_retry_impl). Task-local ⇒ no cross-talkbetween concurrent sessions, and it's reset in
finallyso it can't bleedinto later sends in the same task.
WhatsAppAdapter.send(), if the send isnot a marked reply and
whatsapp.suppress_notificationsis set, drop it(return success, log at DEBUG).
This inverts the burden: replies are tagged in one well-tested place; everything
else is treated as a notice. A missed reply tag would be the only risk (it would
drop a real reply), which is why tagging lives at the single shared funnel.
Changes
gateway/platforms/base.py—import contextvars; module-level_delivering_agent_replyContextVar; split_send_with_retryinto amarker-setting wrapper +
_send_with_retry_impl(body unchanged).gateway/platforms/whatsapp.py— guard at the top ofsend()(after theempty-content check).
gateway/config.py—PlatformConfig.suppress_notifications: bool = False(+
to_dict/from_dict).run_agent.py— inAIAgent._file_mutation_verifier_enabled(), returnFalsewhen<platform>.suppress_notificationsis set, so the reply-embeddedverifier footer is dropped on suppressed platforms while CLI/TUI keep it.
See
hermes-suppress-notifications.patch(attached) — validatedgit apply --checkclean againstmain@2517917. (A variant for the v0.14.0 releasetag is in
hermes-suppress-notifications-v0.14.0.patch; the only difference isthe
config.py from_dictcontext, whichmainrewrote to bridgegateway_restart_notificationthroughextra.)Config
The adapter reads the flag from the raw config dict
(
hermes_cli.config.read_raw_config()), so it works for the top-levelwhatsapp:layout that doesn't currently flow throughPlatformConfig.from_dict. The typed field is also populated for theplatforms:layout.Backward compatibility
false⇒ no behavior change for existing users.suppress_notificationsis unset, the guard falls back todisplay.platforms.<platform>.interim_assistant_messages: false, so installsalready relying on that keep their suppression. (Optional — drop if undesired.)
Extending to other platforms
The reply marker is platform-agnostic; enabling
<platform>.suppress_notificationselsewhere is just the same ~10-line guard at the top of that adapter's
send().Testing
_send_with_retry⇒ markerTrue(sent),direct
send()⇒ markerFalse(suppressed when flag on), and the markerresets after
_send_with_retryreturns.PlatformConfig.from_dict({'suppress_notifications': True})round-tripsthrough
to_dict().delivered.
whatsapp.suppress_notifications: true, the file-mutationverifier footer is absent from WhatsApp replies; with the flag off (or on
CLI/TUI) the footer still appends as before.
Notes / open questions for maintainers
suppress_notificationsvsnotices_enabled(inverted) — happy torename.
interim_assistant_messagesbackcompat fallback.send()-wrapper-in-base refactor over per-adapter guards.Update — reply-embedded interrupt notices (2nd commit)
The adapter chokepoint only catches separate
send()notices. Fourinterrupt / API-error strings in
run_conversation()are assigned tofinal_response, so they ride the genuine agent-reply path and bypass thechokepoint — they leaked into WhatsApp as e.g.
Operation interrupted: waiting for model response (3.9s elapsed).Fix (same pattern as the file-mutation footer gate): add
AIAgent._suppress_platform_notifications()and gate all four sites on it,emitting
""/Noneinstead of the notice when the platform suppresses.Every site already sets
interrupted=True, so the emptied reply is silent:_normalize_empty_agent_responsedoes not resurrect empty responses oninterrupted turns. CLI/TUI and other platforms keep the notices.