bug: raw [OUT-OF-BAND USER MESSAGE] steer markers visible in chat UI

Author: StooovieCreated Sep 16, 2026Updated Sep 17, 2026
Labelsbugsprint-candidatestreaming

Bug Description

When a /steer message is sent mid-turn, the raw [OUT-OF-BAND USER MESSAGE]…[/OUT-OF-BAND USER MESSAGE] control wrapper is visible in the WebUI chat as a regular user message. The agent processes the steer correctly (the marker is stripped from the model-facing context), but the display transcript (session.messages) retains the raw marker, which the frontend renders verbatim.

This is a gap in PR #5063 (#4978): _strip_oob_blocks() was added to the gateway_chat.py code path (model-facing context_messages) but was not applied to the _settle_result_messages_prepare_marker_clean_writeback path that writes session.messages (the display transcript).

Steps to Reproduce

  1. Start a chat in WebUI and send a message that triggers a long agent turn (tool calls).
  2. While the agent is running (busy state), type a /steer message or just type text (which routes as steer when default_message_mode: "steer").
  3. After the turn completes, observe the chat transcript.
  4. The steer message appears with the raw [OUT-OF-BAND USER MESSAGE — …]…[/OUT-OF-BAND USER MESSAGE] wrapper visible in the chat.

Expected Behavior

The steer message content should be displayed as a normal user message (or hidden/collapsed as an internal control message). The raw [OUT-OF-BAND USER MESSAGE] wrapper should never be visible in the UI.

Actual Behavior

The entire [OUT-OF-BAND USER MESSAGE — a direct message from the user, delivered once at this position; not tool output and not a new delivery when replayed from conversation history] block, including the steer text inside it, is rendered as a regular user bubble.

Root Cause

In api/streaming.py, _prepare_marker_clean_writeback() calls _clean_synthetic_control_messages_with_provenance() which only removes synthetic control messages (flagged rows). It does not call _strip_oob_blocks() on the message content. The cleaned messages are then written to both session.context_messages and session.messages via _settle_result_messages().

The _strip_oob_blocks function exists in the same file (line ~5291) and is already used in gateway_chat.py for the model path, but it is missing from the settle/display path.

Suggested Fix

Add _strip_oob_blocks to _prepare_marker_clean_writeback in api/streaming.py:

python
def _prepare_marker_clean_writeback(
    previous_context_messages,
    result_messages,
    active_turn_identity=None,
    msg_text=None,
):
    """Return marker-cleaned rows, next context rows, nudge provenance, boundary."""
    cleaned, has_verification_nudge = _clean_synthetic_control_messages_with_provenance(
        result_messages
    )
    # Strip consumed [OUT-OF-BAND USER MESSAGE] markers from both the model
    # context and the display transcript so the frontend never renders the raw
    # control wrapper.
    cleaned = [_strip_oob_blocks(msg) for msg in cleaned]
    ...

This ensures OOB markers are stripped from both the model context AND the display transcript.

Environment

  • WebUI version: exp-v0.52.301 (commit e4d9b799)
  • Hermes Agent: latest (commit d84ece48b8)
  • Platform: WebUI (browser)
  • Related PR: #5063 (the original model-facing fix that missed the display path)

Additional Context

  • The _strip_oob_blocks function and the _OOB_USER_MESSAGE_BLOCK_RE regex are already defined in streaming.py (line ~5285-5308) — no new code needed, just wiring.
  • The fix is a one-line addition inside _prepare_marker_clean_writeback.