Realtime: a failing model.close() leaves every waiting event iterator blocked forever
Please read this first
- Have you read the docs? Yes.
- Have you searched for related issues? Yes. #3284 fixed
close()leaving event iterators blocked on the successful close path; this is the failure path of the same contract.
Describe the bug
When RealtimeSession.close() runs and the model's close() raises, _cleanup has already removed the session as the model's listener and set _closing, but it never reaches the lines that set _closed and call _wake_event_iterators(). A consumer parked in async for event in session then waits forever: no event can reach the queue any more (_put_event refuses events while closing and the listener is gone), and __aiter__ only terminates on _closed, on _model_stream_ended, or on a wake-up sentinel that was never sent.
The retry design is deliberate and stays intact (_closed stays False so a second close() closes the model again, as test_concurrent_close_callers_share_failure_and_retry pins). But the task that called close() is usually not the task that iterates, and async with session: surfaces the close error from __aexit__ with nothing telling the application that its consumer task is now stuck. The lifecycle reference lists "wake or terminate every waiter on close" as the required release for event iterators.
Debug information
- Agents SDK version:
mainat 9f6f6b10 (also present in v0.22.2) - Python version: 3.12.13
- Operating system: macOS 15
- Model and model provider: any
RealtimeModelwhoseclose()raises; reproduced withScriptedRealtimeModel(close_error=...)fromagents.realtime.testing.OpenAIRealtimeWebSocketModel.close()re-raises WebSocket close failures. - Does the issue reproduce with the latest Agents SDK release? Yes
- Does the issue occur consistently or intermittently? Consistently
Repro steps
Self-contained, no network.
import asyncio
from agents.realtime.agent import RealtimeAgent
from agents.realtime.session import RealtimeSession
from agents.realtime.testing import ScriptedRealtimeModel
async def main():
model = ScriptedRealtimeModel(close_error=RuntimeError("close failed"), strict=False)
session = RealtimeSession(model, RealtimeAgent(name="agent"), None)
await session.enter()
async def consume():
async for _ in session:
pass
consumer = asyncio.create_task(consume())
await asyncio.sleep(0.05) # Let the consumer park on the event queue.
try:
await session.close()
except RuntimeError as error:
print("close() raised:", error)
try:
await asyncio.wait_for(consumer, timeout=1)
print("consumer ended")
except asyncio.TimeoutError:
print("consumer still blocked after close() raised")
consumer.cancel()
asyncio.run(main())Output on main:
close() raised: close failed
consumer still blocked after close() raisedExpected behavior
close() raised: close failed
consumer endedclose() still raises and the session stays retryable, but the parked consumer is released: waiting iterators are woken when the model close fails, and an iterator that resumes afterwards drains whatever was queued before the close and then ends instead of waiting for events that can no longer arrive. I have a fix with regression tests ready and will open a PR referencing this issue.
Source: openai/openai-agents-python