fix(realtime): release the websocket when the initial session config fails - #4161
fix(realtime): release the websocket when the initial session config fails#4161LeSingh1 wants to merge 1 commit into
Conversation
seratch
left a comment
There was a problem hiding this comment.
Thanks for the focused fix. The leak is real, and cleaning up when the initial session update fails is the correct direction.
Before we merge this, please make the existing single-connection invariant effective across the whole connection attempt. Two connect() calls can currently pass the assertions before either assigns _websocket; if A's initial send is pending, B installs newer connection state, and A then fails, A's unconditional self.close() can close B's socket and listener task. Please mark an attempt active before the first await, reject a second attempt before it acquires resources, and clear that guard on every failure path.
Please also add a controlled interleaving test that pauses A's initial send, verifies B is rejected without opening another socket, then fails A and confirms its original exception and resource cleanup.
|
Closing this PR in favor of #4189; your contribution is included in the commit. |
Summary
OpenAIRealtimeWebSocketModel.connect()opens the websocket and starts the listener task, then sends the initial session config. If that send fails, neither the socket nor the task is released.This is reachable from ordinary agent configuration, not just transport errors:
_tools_to_session_toolsraisesUserError("Tool ... is unsupported. Must be a function tool.")for any non-FunctionTool, andRealtimeSession._get_updated_model_settings_from_agentdoes not reject hosted tools beforehand. So a realtime agent configured with, say, aWebSearchToolleaks a live connection.ensure_tool_choice_supports_backendand aConnectionClosedon send reach the same point.The caller cannot clean up after it either.
RealtimeSession.__aenter__only callsremove_listener, and Python does not invoke__aexit__when__aenter__raises, so the usualasync with await runner.run() as session:form gives no cleanup hook. On top of the leaked socket and background task,connect()opens withassert self._websocket is None, so the model instance can never be reconnected.Fix: wrap the initial
_update_session_configcall andawait self.close()before re-raising, restoring the same clean state thattest_connect_websocket_failure_propagatesalready asserts for a failure one step earlier..agents/references/realtime-session-lifecycle.mdstates this ownership rule; this is the untested half of it.Test plan
Added
test_connect_session_config_failure_releases_websocket: connects with aWebSearchToolininitial_model_settings, asserts theUserErrorpropagates, and asserts_websocket is None,_websocket_task is None, and thatclose()was awaited on the socket.Fails on
main(assert <AsyncMock> is None), passes with the fix. Fulltests/realtime/suite: 378 passed..agents/skills/code-change-verification/scripts/run.shpassed.Issue number
N/A — found while auditing connection ownership on realtime failure paths.
Checks
.agents/skills/code-change-verification/scripts/run.sh/reviewbefore submitting this PR