openai / openai/openai-agents-python

Realtime: a failing model.close() leaves every waiting event iterator blocked forever

Open
#5,070 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
29.6k
Forks
4.8k
Avg merge
1d 20h
Merged PRs (30d)
123

Description

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: main at 9f6f6b10 (also present in v0.22.2)
  • Python version: 3.12.13
  • Operating system: macOS 15
  • Model and model provider: any RealtimeModel whose close() raises; reproduced with ScriptedRealtimeModel(close_error=...) from agents.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() raised
Expected behavior
close() raised: close failed
consumer ended

close() 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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in RealtimeSession.close/_cleanup and follow aiter plus _wake_event_iterators; issue #3284 describes the successful-close behavior to compare. Reproduce with ScriptedRealtimeModel(close_error=...), then run the regression covering test_concurrent_close_callers_share_failure_and_retry. Done means close() still raises and remains retryable while parked iterators drain queued events and terminate.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
audio-video-rtc
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.