Skip to content

fix(claude_code): streaming Stage 6 emits message_complete with response (2.0.2)#204

Merged
CocoRoF merged 1 commit into
mainfrom
fix/claude-code-streaming-message-complete
May 19, 2026
Merged

fix(claude_code): streaming Stage 6 emits message_complete with response (2.0.2)#204
CocoRoF merged 1 commit into
mainfrom
fix/claude-code-streaming-message-complete

Conversation

@CocoRoF
Copy link
Copy Markdown
Owner

@CocoRoF CocoRoF commented May 19, 2026

Summary

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:

async for chunk in stream:
    if chunk_type == "message_complete":
        response = chunk["response"]
    elif chunk_type == "text_delta" and chunk.get("text"):
        state.add_event("text.delta", {"text": chunk["text"]})

if response is None:
    raise APIError("Stream ended without message_complete", ...)

Anthropic / OpenAI / Google clients all emit {"type": "message_complete", "response": APIResponse} — the CLI client was the lone outlier.

Fix

create_message_stream now accumulates text / thinking / tool_use blocks + the final result envelope's usage as canonical events flow, then yields 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 plan

  • test_create_message_stream_message_complete_carries_response (new) — single terminal envelope, populated response, stop_reason, usage, model
  • test_create_message_stream_yields_text_deltas (existing) — per-line deltas + final result + complete all still surface
  • Full tests/llm_client/ suite — 184/184 pass
  • Manual verify on Geny prod once 2.0.2 is published + pinned

Release

2.0.2. Migration: none — strictly additive change. Code that didn't use claude_code_cli for s06 sees no difference; code that did was broken before this PR and works after.

… (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).
@CocoRoF CocoRoF merged commit 3779e5a into main May 19, 2026
5 of 6 checks passed
@CocoRoF CocoRoF deleted the fix/claude-code-streaming-message-complete branch May 19, 2026 01:26
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>
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.

1 participant