gateway: suppress status/lifecycle messages in group chats - #25748
PeterRosdahl wants to merge 2 commits into
Conversation
Two code paths in gateway/run.py emit internal status messages (interrupt/queue/steer acks, lifecycle/warn callbacks via _status_callback_sync, gateway shutdown/restart notices) through the same adapter.send path as normal agent replies. In group chats this leaks gateway internals to other participants — e.g. iMessage groups where multiple humans share the thread see '⚡ Interrupting current task…' or '⚠️ No response from provider for Ns…' as if the agent sent them. Fix centrally in the adapter layer instead of regexing specific strings out of the chat stream: - Add keyword-only 'kind' param to BaseAdapter._send_with_retry, default 'reply'. Status sends use kind='status'. - Add SendResult.suppressed flag so callers can distinguish a policy-suppressed send from a delivery failure. - Add _should_suppress_status() policy hook on BaseAdapter — default returns False (fail-open). Per-platform adapters override to suppress in group chats when gateway.show_status_in_groups is False (the default). - Tag all three status call sites in gateway/run.py: the interrupt/queue/steer-ack block, _status_callback_sync (now routed through _send_with_retry instead of raw adapter.send), and _notify_active_sessions_of_shutdown (both per-session and home-channel sends). Adds tests/gateway/test_status_kind_adapter_policy.py covering: default kind='reply' always delivers, kind='status' + suppress=True is suppressed without an outbound send, kind='status' + suppress=False delivers, policy-hook exceptions fail open to send, and source-level anti-regression checks that the three call sites still pass kind= 'status'. tests/gateway/ green (79 passed, no existing tests broken).
|
Update pushed: 6427b7f\n\nEvidence:\n- Made BasePlatformAdapter._send_with_retry kind keyword-only.\n- Added BlueBubbles adapter status suppression for iMessage group chats by default; DMs still receive status; opt-out via show_status_in_groups / BLUEBUBBLES_SHOW_STATUS_IN_GROUPS.\n- Added adapter-level regression tests proving group status is suppressed while normal replies and DM status still deliver.\n- Tests: python -m pytest tests/gateway/test_status_kind_adapter_policy.py tests/gateway/test_bluebubbles_status_suppression.py tests/gateway/test_send_retry.py -q -o 'addopts=' => 48 passed.\n- py_compile on changed Python files passed. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the group-chat delivery problem; the busy-ack and shutdown paths still leak on current main (gateway/run.py:5621-5632, gateway/run.py:5786-5869).
Problems
- The status-callback hunk is based on an older path. Current main filters status text through
_prepare_gateway_status_messageand_send_or_update_status_coro(gateway/run.py:17812-17845; helper atgateway/run.py:440-485). Porting the PR's direct_send_with_retryreplacement would bypass the current all-chat noise filter from57864d07edf5d029a6b1f1b3714bbeea89f0a6d8. - The BlueBubbles policy returns
Trueafter its own lookup error (gateway/platforms/bluebubbles.py:702-709in the PR), which suppresses a message rather than failing open as the base hook promises (gateway/platforms/base.py:2518-2530in the PR). BLUEBUBBLES_SHOW_STATUS_IN_GROUPSis a new non-secret behavior setting (gateway/platforms/bluebubbles.py:137in the PR); repository policy directs behavioral settings toconfig.yaml(AGENTS.md:102-107).
Suggested changes
- Preserve the current status preparation/update path, add the group policy after it, and test current busy/shutdown call paths behaviorally.
- Return
Falseon BlueBubbles lookup failure and configure the opt-out throughPlatformConfig.extra.
Automated hermes-sweeper review.
| _status_adapter.send( | ||
| _status_chat_id, | ||
| message, | ||
| _status_adapter._send_with_retry( |
There was a problem hiding this comment.
Current main's status callback first calls _prepare_gateway_status_message and then _send_or_update_status_coro (gateway/run.py:17812-17833). Preserve that shared filter/update route when porting this policy; replacing it with this direct send would bypass the current all-chat lifecycle-noise protection.
| "[bluebubbles] get_chat_info failed during status policy check; dropping kind=status: %s", | ||
| exc, | ||
| ) | ||
| return True |
There was a problem hiding this comment.
This converts a chat-info failure into suppression, contrary to the base policy's stated fail-open contract. Return False here so an unavailable lookup still delivers the user-facing status message.
| # group chats. Default False because every iMessage send is a permanent, | ||
| # un-editable bubble visible to every member. DMs still receive status. | ||
| self.show_status_in_groups = _truthy( | ||
| os.getenv("BLUEBUBBLES_SHOW_STATUS_IN_GROUPS"), |
There was a problem hiding this comment.
Please keep this behavior setting in the configured PlatformConfig.extra path only rather than adding a new environment-variable override; AGENTS.md:102-107 reserves behavioral settings for config.yaml.
Problem
Two code paths in
gateway/run.pyemit internal gateway status through the same adapter send path as normal agent replies, which leaks them into group chats:gateway/run.py~line 2600) —⚡ Interrupting current task…,⏳ Queued for the next turn…,⏩ Steered into current run…_status_callback_sync(gateway/run.py~line 14638) —lifecycleandwarnevents fromAIAgent._emit_status/_emit_warning, including⚠️ No response from provider for Ns…, compression warnings, etc._notify_active_sessions_of_shutdown(gateway/run.py~line 2703) —⚠️ Gateway shutting down — …On platforms with shared group chats (iMessage groups, Telegram groups, Discord channels, Slack rooms) other humans in the thread see these as if the agent sent them. Spotted in production on iMessage: multiple users in the same group thread were seeing
⚡ Interrupting current task (iteration 2/90, running: session_search). I'll respond to your message shortly.whenever anyone interrupted a busy session.Fix
Centralize the suppression in the adapter layer — don't regex specific strings out of the chat stream.
kind: str = "reply"toBaseAdapter._send_with_retry. Status-class sends usekind="status".SendResult.suppressed: boolso callers can distinguish a policy-suppressed send from a delivery failure.BaseAdapter._should_suppress_status()policy hook. Default returnsFalse(fail-open — existing behavior preserved on platforms that haven't opted in). Per-platform adapters override to suppress in group chats whengateway.show_status_in_groupsisFalse(the default for opting platforms).gateway/run.pywithkind="status":_status_callback_sync(now routed through_send_with_retryinstead of rawadapter.send)_notify_active_sessions_of_shutdown— both per-session and home-channel sendsTests
New
tests/gateway/test_status_kind_adapter_policy.py(9 tests, all passing):kind="reply"always delivers, regardless of policy.kind="status"with_should_suppress_status() → Truereturns a suppressedSendResultwithout an outbound send.kind="status"with_should_suppress_status() → Falsedelivers normally (DM case).kindis keyword-only aftermax_retries/base_delay(no positional-arg breakage on existing callers).gateway/run.pystill passkind="status".No existing tests broken.
Out of scope
Per-platform override implementations (e.g. iMessage adapter checking
chat_type == "group") live in their own platform adapters / plugins. This PR ships only the corekindplumbing + policy hook + default-off behavior, so upstream platforms keep their current behavior until they opt in.