Skip to content

gateway: suppress status/lifecycle messages in group chats - #25748

Open
PeterRosdahl wants to merge 2 commits into
NousResearch:mainfrom
PeterRosdahl:fix/gateway-status-suppression-in-groups
Open

PeterRosdahl wants to merge 2 commits into
NousResearch:mainfrom
PeterRosdahl:fix/gateway-status-suppression-in-groups

Conversation

@PeterRosdahl

Copy link
Copy Markdown

Problem

Two code paths in gateway/run.py emit internal gateway status through the same adapter send path as normal agent replies, which leaks them into group chats:

  1. Interrupt / queue / steer acks (gateway/run.py ~line 2600) — ⚡ Interrupting current task…, ⏳ Queued for the next turn…, ⏩ Steered into current run…
  2. Agent _status_callback_sync (gateway/run.py ~line 14638) — lifecycle and warn events from AIAgent._emit_status / _emit_warning, including ⚠️ No response from provider for Ns…, compression warnings, etc.
  3. _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.

  • Add keyword-only kind: str = "reply" to BaseAdapter._send_with_retry. Status-class sends use kind="status".
  • Add SendResult.suppressed: bool so callers can distinguish a policy-suppressed send from a delivery failure.
  • Add BaseAdapter._should_suppress_status() policy hook. Default returns False (fail-open — existing behavior preserved on platforms that haven't opted in). Per-platform adapters override to suppress in group chats when gateway.show_status_in_groups is False (the default for opting platforms).
  • Tag all three status call sites in gateway/run.py with kind="status":
    • The interrupt/queue/steer-ack block
    • _status_callback_sync (now routed through _send_with_retry instead of raw adapter.send)
    • _notify_active_sessions_of_shutdown — both per-session and home-channel sends
  • If the policy hook itself raises, fail open and deliver — a bug in policy code must never silently swallow user-facing output.

Tests

New tests/gateway/test_status_kind_adapter_policy.py (9 tests, all passing):

  • Default kind="reply" always delivers, regardless of policy.
  • kind="status" with _should_suppress_status() → True returns a suppressed SendResult without an outbound send.
  • kind="status" with _should_suppress_status() → False delivers normally (DM case).
  • Policy-hook exceptions fail open to send.
  • kind is keyword-only after max_retries / base_delay (no positional-arg breakage on existing callers).
  • Source-level anti-regression checks that the three call sites in gateway/run.py still pass kind="status".
tests/gateway/test_status_kind_adapter_policy.py ............................. 9 passed in 1.31s
tests/gateway/{test_send_retry,test_busy_session_ack,test_session_split_brain_11016,test_clean_shutdown_marker}.py ... 70 passed in 2.32s

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 core kind plumbing + policy hook + default-off behavior, so upstream platforms keep their current behavior until they opt in.

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).
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery labels May 14, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

Related to existing PRs addressing the same lifecycle-message suppression surface: #24519, #23119, #20040. Root feature request: #19572. Consider consolidating — there are now four open PRs for the same problem area.

@PeterRosdahl

Copy link
Copy Markdown
Author

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 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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_message and _send_or_update_status_coro (gateway/run.py:17812-17845; helper at gateway/run.py:440-485). Porting the PR's direct _send_with_retry replacement would bypass the current all-chat noise filter from 57864d07edf5d029a6b1f1b3714bbeea89f0a6d8.
  • The BlueBubbles policy returns True after its own lookup error (gateway/platforms/bluebubbles.py:702-709 in the PR), which suppresses a message rather than failing open as the base hook promises (gateway/platforms/base.py:2518-2530 in the PR).
  • BLUEBUBBLES_SHOW_STATUS_IN_GROUPS is a new non-secret behavior setting (gateway/platforms/bluebubbles.py:137 in the PR); repository policy directs behavioral settings to config.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 False on BlueBubbles lookup failure and configure the opt-out through PlatformConfig.extra.

Automated hermes-sweeper review.

Comment thread gateway/run.py
_status_adapter.send(
_status_chat_id,
message,
_status_adapter._send_with_retry(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 13, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users 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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants