`_strip_oob_markers_from_messages()` matched on marker presence alone and passed
the whole row to `_strip_oob_blocks()`, so a user row that legitimately quotes a
complete `[OUT-OF-BAND USER MESSAGE]…[/OUT-OF-BAND USER MESSAGE]` example lost
that text on the next settlement — and the shortened pre-match row broke
active-turn identity matching, which duplicated the turn.
The transport only ever appends a consumed `/steer` wrapper to the turn's last
tool result, so the scrub now considers `role == "tool"` rows only and rebuilds
just their `content` (shallow copy): user/assistant rows, stable ids, reasoning
metadata, and turn bookkeeping are never touched.
Measured on the focused file, helper restored to the pre-fix body as sabotage:
- pre-fix body: 1 failed, 7 passed — the new regression prints the deleted
example plus 3 user rows instead of 2 (the duplicated turn)
- with the fix: 8 passed
Neighbors: test_context_history_sanitization.py 20 passed; writeback/settle
family (12 files) 120 passed, 5 skipped; media slice (7 files) 197 passed,
18 subtests.
Docs: docs/rfcs/webui-pending-intent-controls.md now records that the transport
wrapper is dropped from the settled display and the persisted context state, is
scoped to the carrier row, and that a transcript stored before this behavior
heals on its next settlement.
The display merge carries earlier turns' rows across verbatim, so a wrapper
persisted by a pre-fix settle would stay rendered for the rest of the session.
Scrub session.messages after the merge — the merge's identity matching still saw
the rows unchanged — so the persisted display copy is marker-free even when the
row was not first written by this turn. Covered by
test_settle_heals_a_transcript_that_already_stored_the_wrapper.
A mid-turn /steer is delivered to the agent as an [OUT-OF-BAND USER MESSAGE ...]
block appended to the turn's last tool result. The model-facing gateway history
always dropped that wrapper, but the writeback that produces session.messages
(rendered verbatim) and session.context_messages kept it, so the frontend showed
the raw control wrapper as a chat bubble.
Strip it in _prepare_marker_clean_writeback, the chokepoint both writebacks are
built from, reusing _strip_oob_blocks. Rows without a marker are returned
untouched so stable ids / reasoning metadata keep riding the same objects and the
scrubbed rows still compare equal to the rows persisted by earlier turns.