Skip to content

A cut Anthropic stream that opened a text block still emits a content-free assistant row #595

Description

@philcunliffe

Residual from PR #592's round-3 review, deliberately deferred there so the shape of the emptiness predicate is a recorded decision rather than a reviewer's aside.

The case

isEmptyCutRow (hypaware-core/plugins-workspace/openclaw/src/projector.js) tests content.length === 0. An Anthropic stream cut between content_block_start {type:'text',text:''} and the first content_block_delta yields:

content: [{"type":"text","text":""}]   mk=b8501a26

which is not length-0, so the row is emitted with a live message_index and zero information.

Why it is the same hazard class, not a lesser one

ordinalFallbackMatch keys on message_index + role + a time window and never consults content, so any assistant row at that index is a candidate; the match key only decides whether the content pass matches first. So this row can acquire another turn's native message_id exactly like the rows #592 removes.

Why it was deferred rather than fixed

Reachability is genuinely low: the window between content_block_start and the first text_delta is milliseconds, unlike the gap before content_block_start where prefill latency lives. No failing test demands it.

What a fix needs to be careful about

Round 2 argued that widening the emptiness predicate risks over-reach, citing a thinking block with empty text but a real signature, and a tool_use block with {} input, as real evidence that must keep recording. Round 3 checked both and found neither blocks a precise fix:

  • a tool_use block from content_block_start carries a real id and name
  • a thinking block's signature is unreachable here anyway: reconstructAssistantMessage handles text_delta/thinking_delta/input_json_delta and has no signature_delta case, so a signature could only arrive on content_block_start, where Anthropic sends ""

So a predicate restricted to { type: 'text', text: '' } blocks appears safe. That should be verified rather than taken from this issue, and the decision recorded, since it widens a floor that deletes rows.

Severity: low. Depends on #592 landing first (isEmptyCutRow does not exist on master until then).

Metadata

Metadata

Assignees

No one assigned

    Labels

    neutral:fixDelegate this issue to neutral for an autonomous fix attempt (reproduce -> fix -> PR)neutral:stuckneutral attempted this but cannot complete it autonomously — needs a human

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions