Skip to content

Fix: prevent MessageParseError from killing SDK message stream#81

Merged
RichardAtCT merged 2 commits intomainfrom
claude/run-claude-command-fJFwS
Feb 21, 2026
Merged

Fix: prevent MessageParseError from killing SDK message stream#81
RichardAtCT merged 2 commits intomainfrom
claude/run-claude-command-fJFwS

Conversation

@RichardAtCT
Copy link
Copy Markdown
Owner

@RichardAtCT RichardAtCT commented Feb 21, 2026

Summary

  • Fixed a critical bug where unknown SDK message types (e.g. rate_limit_event) caused MessageParseError to propagate through and permanently terminate the receive_messages() async generator
  • This caused all subsequent messages to be lost, including ResultMessage — resulting in empty session_id, zero cost, and non-resumable sessions
  • Added StreamEvent session_id fallback as defense-in-depth

Root Cause

The SDK's receive_messages() generator calls parse_message(data) inside a yield expression. When parse_message raises MessageParseError for unknown types like rate_limit_event, the exception propagates out of the async generator. In Python, this permanently terminates the generator — even if the caller catches the exception and tries to continue iterating.

The previous code caught MessageParseError from the caller side and called continue, but this couldn't save the already-dead generator. The next __anext__() call received StopAsyncIteration, ending the loop before ResultMessage could arrive.

Fix

Instead of using client.receive_response() (which wraps the fragile generator), iterate over client._query.receive_messages() directly (yields raw dicts) and call parse_message() ourselves with error handling inside the loop. Parse errors for unknown message types are now caught before they can kill the generator.

Before / After

Before After
session_id "" (empty) db7aa2b6-4dfa-4cfb-b268-d678ec082a91
cost 0.0 0.11
Session resumable No Yes
rate_limit_event Kills generator Skipped gracefully

Test plan

  • 380 existing tests pass
  • 5 new tests for StreamEvent session_id fallback
  • Live-tested: bot receives messages, extracts session_id and cost, sessions persist in SQLite
  • Formatting checks pass (black, isort)

claude and others added 2 commits February 21, 2026 07:29
…ge has empty session_id

When Claude CLI returns an empty session_id in the ResultMessage, sessions
become non-resumable. This adds a fallback that checks StreamEvent messages
(which also carry session_id) to recover the session ID and enable session
resumption.

https://claude.ai/code/session_01WAbujHDo1pYht6sEZXinv4
The SDK's receive_messages() async generator was being terminated when
encountering unknown message types (e.g. rate_limit_event). In Python,
when an exception propagates out of an async generator, the generator
dies permanently — causing all subsequent messages to be lost, including
the ResultMessage that carries session_id and cost data.

Fix: iterate over client._query.receive_messages() directly (raw dicts)
and call parse_message() ourselves with error handling inside the loop.
Unknown message types are now skipped without killing the generator.

Also adds the StreamEvent session_id fallback (original PR intent) and
comprehensive tests for both the fallback logic and parse error recovery.

Before: session_id="", cost=0.0, sessions not resumable
After:  session_id properly extracted, cost tracked, sessions resumable
@RichardAtCT RichardAtCT changed the title Add fallback session ID extraction from StreamEvent messages Fix: prevent MessageParseError from killing SDK message stream Feb 21, 2026
@RichardAtCT RichardAtCT merged commit d6573a6 into main Feb 21, 2026
1 check passed
@RichardAtCT RichardAtCT deleted the claude/run-claude-command-fJFwS branch February 21, 2026 07:53
@RichardAtCT RichardAtCT mentioned this pull request Feb 21, 2026
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