Skip to content

fix(gateway): suppress NO_REPLY/[SILENT] markers on the streaming path - #56099

Merged
teknium1 merged 1 commit into
mainfrom
hermes/hermes-590f13a6
Jul 1, 2026
Merged

fix(gateway): suppress NO_REPLY/[SILENT] markers on the streaming path#56099
teknium1 merged 1 commit into
mainfrom
hermes/hermes-590f13a6

Conversation

@teknium1

@teknium1 teknium1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Infographic

silence-stays-silent

Summary

Intentional-silence markers (NO_REPLY / [SILENT] / SILENT / NO REPLY) are now suppressed on the streaming delivery path, not just the non-streaming one. A user reported a literal NO_REPLY bubble leaking into Slack.

The gateway already suppressed whole-response silence markers via response_filters.is_intentional_silence_response + run.py's whole-response filter — but only on the non-streaming path. GatewayStreamConsumer edits the reply onto the screen delta-by-delta, before that filter runs, so by the time the filter fires the marker is already visible. This affects any streaming-capable adapter (Slack, Telegram, Discord, Matrix, …); Slack has editing on by default, which is where it was caught.

Both streaming call sites (run.py proxy SSE and local) funnel through on_delta, so the fix has a single correct home: the consumer.

Changes

  • gateway/response_filters.py: add is_partial_silence_marker(), the streaming counterpart to is_intentional_silence_response(). Shares the same LIVE_GATEWAY_SILENT_MARKERS set + canonicalization so the two never drift.
  • gateway/stream_consumer.py:
    • Mid-stream hold-back: defer edits while the accumulated buffer is still a prefix of a silence marker, so a partial marker never flashes on an interval tick.
    • Stream-end suppression: on got_done, if the final buffer is exactly a marker, retract any preview (best-effort delete_message, reusing the _try_fresh_final cleanup path) and leave delivery flags False so the gateway's own filter drops it and no fallback send fires.
  • tests/gateway/test_stream_consumer_silence.py: predicate truth table + end-to-end run() suppression (single-shot and token-by-token NO_REPLY, [SILENT] parity, preview retraction, no-delete-support best-effort, prose-passthrough).

No new config, no platform-specific code.

Validation

Before After
Streaming reply of exactly NO_REPLY literal marker delivered to chat suppressed, nothing sent
Partial marker on interval tick flashes on screen held back until resolved
Prose mentioning NO_REPLY delivered delivered (unchanged)

30 tests pass (scripts/run_tests.sh tests/gateway/test_stream_consumer_silence.py). Matches the narrow intentional-silence contract: suppress only when the whole response is an exact marker; never substring-match prose.

Salvaged from #56042 by @benbarclay; authorship preserved via rebase-merge.

The agent emits a bare control marker (NO_REPLY / [SILENT] / …) when it
intentionally chooses not to reply.  The gateway's whole-response filter
(is_intentional_silence_agent_result) suppresses this on the non-streaming
delivery path, but the streaming path (GatewayStreamConsumer) had no silence
awareness: it edited the raw marker onto the screen delta-by-delta and
finalized it BEFORE the whole-response filter could run.  On any
streaming-capable adapter (Slack, Telegram, Discord, …) users saw a literal
'NO_REPLY' message leak into chat.

Fix (contained in the stream consumer + a shared predicate; no new config,
no platform-specific code):

- gateway/response_filters.py: add is_partial_silence_marker() — the
  streaming counterpart to is_intentional_silence_response(), sharing the
  same marker set and canonicalization so the two never drift.
- gateway/stream_consumer.py:
  - Mid-stream hold-back: defer edits while the accumulated buffer is still a
    prefix of a silence marker, so a partial marker never flashes on an
    interval tick.
  - On stream end (got_done): if the final buffer is exactly a marker, retract
    any preview already shown (best-effort delete_message, reusing the
    _try_fresh_final cleanup path) and leave the delivery flags False so the
    gateway's own filter turns the marker into '' and no fallback send fires.

Substantive prose that merely mentions a marker is still delivered normally.

Tests: tests/gateway/test_stream_consumer_silence.py — predicate truth table
+ end-to-end run() suppression (single-shot + token-by-token), preview
retraction, no-delete-support best-effort, [SILENT] parity, and
prose-passthrough. Prove-fail verified by reverting only the consumer change
(the 4 behavioral tests fail: 'NO_REPLY'/'[SILENT]' leaks).
@teknium1
teknium1 merged commit 5f7deeb into main Jul 1, 2026
31 checks passed
@teknium1
teknium1 deleted the hermes/hermes-590f13a6 branch July 1, 2026 06:37
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 1, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: salvage of #56042 (still open) onto current main. Competes/overlaps with OPEN #9956 (shared-normalization refactor targeting the non-streaming path, different mechanism) and OPEN #46970 (adds (silence) marker). Not a duplicate — this is the streaming-path counterpart. Maintainer to pick the canonical silence-suppression consolidation.

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: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