fix(claude_code): streaming Stage 6 emits message_complete with response (2.0.2)#204
Merged
Merged
Conversation
… (2.0.2)
Streaming Stage 6 calls with provider=claude_code_cli failed with
``APIError("Stream ended without message_complete")`` for every
session. Symptom in a Geny pipeline run:
→ s06_api
✗ s06_api: Stream ended without message_complete
Pipeline error: Stream ended without message_complete
Root cause: ``ClaudeCodeCLIClient.create_message_stream`` passed the
translator's bare ``{"type": "message_complete"}`` straight through.
The s06_api default stage's ``_call_streaming`` reads
``chunk["response"]`` to build the assistant message; with no
``response`` field on the envelope, ``response`` stayed ``None`` and
the post-loop guard raised. Anthropic / OpenAI / Google clients all
emit ``{"type": "message_complete", "response": APIResponse}`` — the
CLI client was the lone outlier.
Fix: accumulate text / thinking / tool_use blocks + the final
``result`` envelope's usage as canonical events flow, then yield one
terminal ``message_complete`` carrying an assembled APIResponse.
Mirrors the contract of every SDK client and reuses the same parsing
logic ``assemble_response_from_stream_json`` uses for the
non-streaming path. Per-line ``text_delta`` / ``content_block_stop``
/ ``result`` events still pass through unchanged.
Suppresses the translator's bare ``message_complete`` so callers
don't see a half-populated event before the real one — there's only
ever one terminal envelope.
### Test
``test_create_message_stream_message_complete_carries_response``
asserts a single terminal envelope with the assembled response
(text / stop_reason / usage / model). The existing
``test_create_message_stream_yields_text_deltas`` continues to pass
(per-line deltas + final result + complete all still surface).
2 tasks
CocoRoF
added a commit
to CocoRoF/Geny
that referenced
this pull request
May 19, 2026
#807) geny-executor 2.0.2 fixes ``ClaudeCodeCLIClient.create_message_stream`` to emit a populated ``message_complete`` envelope. Without this upgrade, every session using ``provider=claude_code_cli`` for Stage 6 fails at first turn with: → s06_api ✗ s06_api: Stream ended without message_complete Pipeline error: Stream ended without message_complete Reported by the user after picking Claude Code (CLI) in the Stage 6 provider strip. See CocoRoF/geny-executor#204 for the full root-cause analysis. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Streaming Stage 6 calls with
provider=claude_code_clifailed withAPIError("Stream ended without message_complete")for every session. Symptom in a Geny pipeline run:Root cause
ClaudeCodeCLIClient.create_message_streampassed the translator's bare{"type": "message_complete"}straight through. The s06_api default stage's_call_streamingreadschunk["response"]to build the assistant message; with noresponsefield on the envelope,responsestayedNoneand the post-loop guard raised:Anthropic / OpenAI / Google clients all emit
{"type": "message_complete", "response": APIResponse}— the CLI client was the lone outlier.Fix
create_message_streamnow accumulates text / thinking / tool_use blocks + the finalresultenvelope's usage as canonical events flow, then yields one terminalmessage_completecarrying an assembledAPIResponse. Mirrors the contract of every SDK client and reuses the same parsing logicassemble_response_from_stream_jsonuses for the non-streaming path. Per-linetext_delta/content_block_stop/resultevents still pass through unchanged.Suppresses the translator's bare
message_completeso callers don't see a half-populated event before the real one — there's only ever one terminal envelope.Test plan
test_create_message_stream_message_complete_carries_response(new) — single terminal envelope, populated response, stop_reason, usage, modeltest_create_message_stream_yields_text_deltas(existing) — per-line deltas + final result + complete all still surfacetests/llm_client/suite — 184/184 passRelease
2.0.2. Migration: none — strictly additive change. Code that didn't useclaude_code_clifor s06 sees no difference; code that did was broken before this PR and works after.