Start Python SDK turn subscriptions at their attachment point (#44400)

## What changed

Make turn event delivery depend on when each consumer attaches:

- Handles returned by `thread.turn(...)` receive events from when the request is sent, including events received before the response arrives.
- Other handles and explicit low-level registrations receive only events from when they join. Remove replay of completed items, token usage, and terminal events; use `thread.read(include_turns=True)` for earlier history.
- Retain unread events for active subscribers and pending starts, release unused turn state, and close the returned subscription when an async start is cancelled.

Preserve the existing low-level subscription when repeated `turn_start(...)` calls return the same turn.

## Testing

Update coverage for late joins, request-time buffering, completion or transport failure before a start returns, async cancellation cleanup, and repeated low-level starts. Adjust external-message integration tests to allow handles to collect different items while receiving the same final response.

GitOrigin-RevId: 3b7f1268df1193dee88385006ae9510101086c24
This commit is contained in:
Ahmed Ibrahim
2026-09-10 03:40:55 +00:00
committed by copyberry
parent ea53c8d4f7
commit ddea03ad04
10 changed files with 195 additions and 153 deletions
+5 -2
View File
@@ -82,7 +82,10 @@ def test_external_message_joins_active_turn_with_tool_authority(tmp_path) -> Non
harness.responses.wait_for_requests(1)
joined = thread.turn(ExternalMessage(tool_name="notifications", content=content))
result = consumers.submit(joined.run).result(timeout=15)
assert original_result.result(timeout=15) == result
first = original_result.result(timeout=15)
assert first.final_response == result.final_response
assert first.items[0].root.type == "userMessage"
assert all(item.root.type != "userMessage" for item in result.items)
assert codex._client._router._turn_states == {}
requests = harness.responses.requests()
@@ -151,7 +154,7 @@ def test_async_external_message_allows_both_handles_to_consume(tmp_path) -> None
first, second = await asyncio.wait_for(
asyncio.gather(original_result, joined.run()), timeout=15
)
assert first == second
assert (first.final_response, first.usage) == (second.final_response, second.usage)
assert first.final_response == "Update processed"
assert first.usage is not None
assert codex._client._sync._router._turn_states == {}