fix(client): suppress stale task notifications between turns in receive_response()#791
Open
qozle wants to merge 1 commit intoanthropics:mainfrom
Open
fix(client): suppress stale task notifications between turns in receive_response()#791qozle wants to merge 1 commit intoanthropics:mainfrom
qozle wants to merge 1 commit intoanthropics:mainfrom
Conversation
…e_response() When a background task (spawned via run_in_background=True) completed between turns, its TaskNotificationMessage sat in the message buffer and was the first thing yielded by the next receive_response() call. This caused the notification to appear before the actual Turn N+1 response — and in some cases caused the model to respond to the stale task context instead of the new user prompt. Fix: ClaudeSDKClient now tracks which turn each background task was started in (_task_turn_map). receive_response() defers any task lifecycle events that arrive before the first non-task message of the current turn. When the first substantive message arrives, deferred events are flushed — unless the event is a TaskNotificationMessage for a task started in an earlier turn, in which case it is discarded as stale cross-turn noise. Notifications that arrive AFTER the first AssistantMessage (mid-turn) are still yielded normally. Notifications for tasks with no recorded start (unknown task_id) are yielded as current-turn (safe default). Map entries are cleaned up when a notification is processed to prevent unbounded growth on long-lived clients. The raw receive_messages() stream is unchanged: callers who need every event regardless of turn boundaries should use that method instead. Closes anthropics#788
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.
Fixes #788.
What happened
When a background task (spawned via
run_in_background=True) completed between turns, itsTaskNotificationMessagesat in the shared message buffer. The nextreceive_response()call would yield it as the first message — before any response to the new query. In some cases this caused the model to respond to the stale task context instead of the user's new prompt.Root cause
ClaudeSDKClientuses a single persistentanyio.MemoryObjectReceiveStreamfor the lifetime of the connection.receive_response()stops iterating after aResultMessage, but the buffer keeps accumulating messages. A background task that completes between Turn N's result and Turn N+1's first response injects its notification into that buffer with no turn marker.Fix
Two additions to
ClaudeSDKClient:_current_turn: int— incremented on everyResultMessageseen byreceive_response()_task_turn_map: dict[str, int]— mapstask_idto the turn it was started inIn
receive_response(), task lifecycle events (TaskStartedMessage,TaskNotificationMessage) that arrive before the first non-task message of the current turn are deferred. Once the first substantive message arrives:TaskNotificationMessageentries for tasks started in a previous turn are discarded (stale cross-turn noise).Task completions that arrive mid-turn (after the first
AssistantMessage) are still yielded. The map entry is removed after the notification is processed to avoid unbounded growth on long-lived clients.receive_messages()is not changed — callers that need every event unfiltered should use that method.Tests
Four new cases in
TestReceiveResponseTaskNotificationHygiene:AssistantMessage→ yieldedtask_id(noTaskStartedMessageseen) → yielded (safe default)All 440 existing tests pass.