SIGN IN SIGN UP

fix(chat): carry a steered message's attachments into the live turn

A message steered into a running turn rendered as text alone while the turn
streamed. The image only appeared after closing and reopening the conversation,
which reads the agent's own transcript instead.

The bytes reached the agent all along — the wire carries the hydrated draft —
but `FeedbackItem`, the note that is recorded and broadcast, held only `text`,
and `text` is the composer's display form, which collapses attachments into
words. The note now carries what was sent as well, projected by
`user_blocks_from_prompt` after hydration: the same contract an ordinary
prompt's `user_message` already follows, so a steered image and a prompted one
travel identically. Nothing in the renderer changed —
`extractUserImagesFromBlocks` already turns image blocks on a user turn into the
attachment strip. Widening those broadcast blocks back into content blocks is
now one shared helper, since the viewer's `user_message` echo and an adopted
steered message have to agree on the result.

That also restores the suppression that keeps such a message from rendering
twice. `suppressPersistedSteeredPrompts` matches the persisted copy on content
and its key folds in full image data, so keying the live copy on `text` alone
could never match an image steer; the duplicate surfaced whenever the detail
refetched mid-turn. Both keys are built from the same blocks now.

The retained bytes are bounded per turn. `MAX_FEEDBACK_CHARS` bounds a note's
text and leans on "the count scales with human typing", an argument that does
not survive base64 image data, so without an aggregate bound a run of image
steers grows `SessionState.feedback` — and every snapshot rebuilt from it —
without limit. The check sits at the single authorized writer, sharing one
critical section with the append, so concurrent steers and a replay node are
bounded alike. Past the budget the note still delivers and simply records no
blocks, falling back to its text: the agent has already been handed the content,
so refusing the message would be the wrong trade.

A text-only steer is unchanged end to end, pinned by its own assertion.
X
xintaofei committed
e137f0ea2de853a34e6bfb9798d438759724219c
Parent: 35fe8af