Summary
In Control UI WebChat, when an agent loop produces multiple text → tool_call → tool_result → text → tool_call → ... cycles, each new assistant text block is rendered as a separate visible message card containing the cumulative text from all prior assistant text blocks in the same turn. The gateway-side persistence is correct (separate, non-cumulative blocks); only the WebChat renderer is at fault.
Reproduced on 2026.4.24. Likely older too — possibly older than 2026.4.20. Survives the 4.24 fix #71371 and the 4.23 fix #70921.
Evidence
Single user turn, agent loop with 3 narration-before-tool blocks. JSONL entries from the session transcript (timestamps and content trimmed for clarity; sequential, no other assistant entries in between):
| ts (HH:MM:SS) |
role |
content length |
content preview |
T+00.000 |
assistant |
184 chars |
"Now I need to create an updated …configuration:" |
T+05.229 |
assistant |
87 chars |
"Now let me upload this configuration to …" |
T+27.040 |
assistant |
95 chars |
"Let me try a different approach - I'll use the … CLI to …" |
Note each persisted block is only the new chunk — 184/87/95 chars respectively, not 184/271/366. So the gateway is correct.
What WebChat shows for the same turn (paraphrased; three distinct message cards at three distinct timestamps):
- Card 1: block 1 text (184 chars)
- Card 2: block 1 text + block 2 text (cumulative ~271 chars, no separator between them — note the missing space at the join)
- Card 3: block 1 + 2 + 3 text (cumulative ~366 chars)
The cumulative-without-separator pattern (e.g. "...configuration:Now let me upload...") suggests the renderer is not doing per-card delta tracking — it treats each new assistant block as another append to the current card's text accumulator, but also inserts a new card boundary, so old text leaks into each new bubble.
Where (suspected)
Control UI WebChat client bundle under dist/control-ui/assets/. The assistant-card append/reset logic is the surface — specifically whichever code listens for the gateway's stream events and decides when to close the current card and open a new one. A tool_call arriving after a text block should reset the text accumulator so the next text block lands in a fresh card.
Reproduction
- From WebChat, send a prompt that requires multiple sequential tool calls with narration between them (e.g. "do a multi-step task that needs you to ssh somewhere, then upload a file, then verify").
- Observe the visible cards while the run is in progress (this is not a history-refresh artifact — duplicates appear during live streaming and persist).
- Hard-refresh the browser tab — the duplicates disappear and the correct sequence loads from the JSONL. Confirms the bug is purely client-side render state.
Severity
Low (correctness) / High (UX). Transcript and memory are both unaffected, but the visible chat becomes very noisy during long-running tool loops.
Note
This is adjacent to but distinct from #71371 (optimistic tail message flicker on history refresh). That fix kept tails visible; this bug is about resetting them on tool-block boundaries.
Summary
In Control UI WebChat, when an agent loop produces multiple
text → tool_call → tool_result → text → tool_call → ...cycles, each new assistant text block is rendered as a separate visible message card containing the cumulative text from all prior assistant text blocks in the same turn. The gateway-side persistence is correct (separate, non-cumulative blocks); only the WebChat renderer is at fault.Reproduced on 2026.4.24. Likely older too — possibly older than 2026.4.20. Survives the 4.24 fix #71371 and the 4.23 fix #70921.
Evidence
Single user turn, agent loop with 3 narration-before-tool blocks. JSONL entries from the session transcript (timestamps and content trimmed for clarity; sequential, no other assistant entries in between):
T+00.000T+05.229T+27.040Note each persisted block is only the new chunk — 184/87/95 chars respectively, not 184/271/366. So the gateway is correct.
What WebChat shows for the same turn (paraphrased; three distinct message cards at three distinct timestamps):
The cumulative-without-separator pattern (e.g.
"...configuration:Now let me upload...") suggests the renderer is not doing per-card delta tracking — it treats each new assistant block as another append to the current card's text accumulator, but also inserts a new card boundary, so old text leaks into each new bubble.Where (suspected)
Control UI WebChat client bundle under
dist/control-ui/assets/. The assistant-card append/reset logic is the surface — specifically whichever code listens for the gateway's stream events and decides when to close the current card and open a new one. Atool_callarriving after atextblock should reset the text accumulator so the next text block lands in a fresh card.Reproduction
Severity
Low (correctness) / High (UX). Transcript and memory are both unaffected, but the visible chat becomes very noisy during long-running tool loops.
Note
This is adjacent to but distinct from #71371 (optimistic tail message flicker on history refresh). That fix kept tails visible; this bug is about resetting them on tool-block boundaries.