Visitar URL original
fix: prevent control callback starvation when message consumers stall by MonikaDvorackova · Pull Request #1481 · anthropics/claude-agent-sdk-python · GitHub
Skip to content

fix: prevent control callback starvation when message consumers stall - #1481

Open
MonikaDvorackova wants to merge 4 commits into
anthropics:mainfrom
MonikaDvorackova:fix/control-routing-backpressure
Open

MonikaDvorackova wants to merge 4 commits into
anthropics:mainfrom
MonikaDvorackova:fix/control-routing-backpressure

Conversation

@MonikaDvorackova

Copy link
Copy Markdown

Prevents control callback starvation when consumers stop reading the SDK message stream. Replaces the bounded message buffer with an unbounded AnyIO stream and adds regression tests for hook routing and message ordering.

Validation: 4 tests passed (asyncio and trio); Ruff passed.

Trade-off: memory usage may grow if messages remain unconsumed. This change does not fix stale ResultMessage attribution and is independent of #1464.

Related: #1463, #558.

AI-assisted implementation and tests; local validation performed by contributor.

@MonikaDvorackova
MonikaDvorackova marked this pull request as ready for review November 9, 2026 12:10

@mangeshraut712 mangeshraut712 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diagnosis is right. _read_messages does await self._message_send.send(message) for regular frames, so once the 100-slot buffer fills, the same loop that dispatches control_request frames (via _spawn_control_request_handler) is parked, and hooks/permissions behind the burst never get answered. The tests reproduce exactly that, and asserting that order is preserved after the control round-trip is a nice touch.

self._message_send, self._message_receive = anyio.create_memory_object_stream[
dict[str, Any]
](max_buffer_size=100)
](max_buffer_size=math.inf)

@mangeshraut712 mangeshraut712 Nov 10, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two consequences of going fully unbounded that might be worth handling here:

  1. Lost backpressure is now silent. Previously a stalled consumer eventually blocked the reader, which stopped draining the CLI's stdout and throttled the subprocess. Now a host that stops reading after a ResultMessage but keeps the client alive (or a slow UI with include_partial_messages, which produces many stream_event frames) grows memory with nothing in the logs. A one-time logger.warning when self._message_send.statistics().current_buffer_used crosses a high-water mark (say 1,000) would keep the deadlock fix and still make the leak diagnosable.
  2. report_mirror_error still uses send_nowait with a # buffer-full edge case handler, and its docstring says messages are "dropped rather than back-pressuring the read loop". With math.inf, that branch can't trigger any more. Updating the docstring (or removing the handler) would keep the two in sync.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed both points in commit aaafbbc: added a one-time warning at 1,000 unread messages with regression coverage, and updated report_mirror_error documentation and error handling. All 6 backpressure tests pass under asyncio and trio; Ruff lint and formatting checks pass. Thanks for the review.

This branch has not been deployed

No deployments
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