Skip to content

fix(sse): keep a clean empty Claude end_turn - #15505

Merged
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.52from
HouMinXi:fix/15488-empty-end-turn
Oct 6, 2026
Merged

diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.52from
HouMinXi:fix/15488-empty-end-turn

Conversation

@HouMinXi

@HouMinXi HouMinXi commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • A streamed Claude turn that finishes with message_stop, stop_reason end_turn or stop_sequence, and no content block was turned into 502 Claude returned an empty response (no content block). Claude Code's prompt-suggestion request asks for a short guess or nothing, so an empty answer is valid.
  • message_delta.delta.stop_reason is now kept on the stream lifecycle. The flush check leaves that finished empty turn alone. content_filter, a missing stop reason, a lifecycle that never reaches message_stop, and a zero-byte stream still abort.
  • Related to fix(claude): a clean Claude end_turn with no content block becomes a 502 (breaks Claude Code prompt suggestions) #15488

Validation

  • node --import tsx/esm --test tests/unit/stream-claude-empty-end-turn.test.ts tests/unit/stream-empty-response-retry-96ef4a.test.ts — 12 pass, 0 fail
  • Disabling the stopReason write makes only the wiring test fail. The decision tests stay green.
  • Four LOCAL forge runs on the uncommitted diff, backend review-default. All four had 0 CONFIRMED. Round 2 was PASS with clean 2. Rounds 1, 3, and 4 were PENDING because of UNCERTAIN notes: a guessed top-level stop_reason (fixtures in this repo use delta.stop_reason), a null payload (the type check already returns), and a claim that max_tokens / tool_use should also pass. Those two stay failures on purpose. One note was an excerpt line count.

Tests Added Or Updated

  • tests/unit/stream-claude-empty-end-turn.test.ts
    • end_turn and stop_sequence with a complete lifecycle do not abort
    • content_filter, a missing stop reason, a missing message_stop, and a zero-byte stream still abort
    • message_delta stores delta.stop_reason, and that stored value is what the decision reads

Coverage Notes

  • The tests call the decision and the lifecycle update. They do not open a live Claude stream.

Reviewer Notes

A streamed Claude turn that reaches message_stop with end_turn or
stop_sequence and no content block is a real empty answer. The flush
check aborted every lifecycle that had no content block, including that
one. content_filter, a missing stop, a missing message_stop, and a
zero-byte stream still abort.

Related to diegosouzapw#15488.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
@HouMinXi
HouMinXi requested a review from diegosouzapw as a code owner October 4, 2026 05:27
The stream change needs its own changelog fragment so release
aggregation picks it up.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants