bug: raw [OUT-OF-BAND USER MESSAGE] steer markers visible in chat UI
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
- Start a chat in WebUI and send a message that triggers a long agent turn (tool calls).
- While the agent is running (busy state), type a
/steermessage or just type text (which routes as steer whendefault_message_mode: "steer"). - After the turn completes, observe the chat transcript.
- 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:
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_blocksfunction and the_OOB_USER_MESSAGE_BLOCK_REregex are already defined instreaming.py(line ~5285-5308) — no new code needed, just wiring. - The fix is a one-line addition inside
_prepare_marker_clean_writeback.
Source: nesquena/hermes-webui