Please read this first
Describe the bug
Two PTY startup paths can leak resources on ordinary caller cancellation before registration.
Blaxel
pty_exec_start creates an aiohttp.ClientSession, opens the WebSocket, starts the reader task and sends the command before inserting the entry into _pty_sessions.
Its cleanup handlers are:
except asyncio.TimeoutError:
if not registered:
await self._terminate_pty_entry(entry)
...
except Exception:
if not registered:
await self._terminate_pty_entry(entry)
...
asyncio.CancelledError derives from BaseException, so cancellation during ws_connect, command send, or a contended _pty_lock acquisition bypasses both handlers. The entry is not in the registry, so pty_terminate_all() cannot recover it.
Concrete resources at risk are the WebSocket, aiohttp.ClientSession, and reader task.
UnixLocal
Once create_subprocess_exec succeeds, the process entry, pump task(s), and wait task exist. Registration then awaits:
async with self._pty_lock:
...
self._pty_processes[process_id] = entry
There is no cancellation cleanup around this ownership transfer. If the map lock is contended and the caller is cancelled, the process entry and background tasks remain unregistered and unreachable from pty_terminate_all().
The TTY creation path has a related gap: its fd-close handler catches only Exception around create_subprocess_exec, so cancellation during that await can also bypass the PTY fd cleanup.
Deterministic control-flow reproduction
I modeled the exact ownership boundaries without provider/network dependencies:
blaxel_current_cleaned=False
blaxel_reference_cleaned=True
unix_current_cleaned=False
unix_reference_cleaned=True
V46_PTY_STARTUP_CANCELLATION_REPRO=PASS
For Blaxel, cancellation bypasses the Exception handlers. For UnixLocal, cancellation while waiting for a held map lock exits after local resource creation but before registration.
Other backends checked
I checked all seven PTY implementations rather than generalizing the claim:
- Docker catches
BaseException and attempts cleanup of an unregistered entry.
- Cloudflare has an explicit
CancelledError cleanup path.
- Daytona catches
BaseException and shields startup cleanup.
- E2B has an explicit
CancelledError cleanup path.
- Modal has an explicit
CancelledError cleanup path.
So the direct single-cancellation claim here is intentionally limited to UnixLocal and Blaxel.
Expected behavior
Once a process/provider resource exists, either:
- registration commits and the session registry owns cleanup; or
- cancellation before registration settles cleanup before propagating.
A narrow fix can reuse the cancellation-settled cleanup primitive discussed in #4747.
For Blaxel, add a cancellation/BaseException path that cleanup-settles the unregistered entry before re-raising.
For UnixLocal, track whether registration committed and cleanup-settle the entry if cancellation occurs before insertion. The TTY fd-close handler should also cover BaseException so cancellation cannot bypass descriptor cleanup.
Debug information
- Repository:
openai/openai-agents-python
- Main SHA:
89c02c828ee8510fe9a84ee6675608193aa13b02
- Provider integration execution: not required / not claimed
- Reproduction type: deterministic asyncio ownership/control-flow
Please read this first
pty_exec_start cancellation, unregistered PTY cleanup, Blaxel WebSocket cancellation, UnixLocal PTY cancellation, and PTY registration cancellation. I did not find an issue for this pre-registration ownership boundary.mainat89c02c828ee8510fe9a84ee6675608193aa13b02.Describe the bug
Two PTY startup paths can leak resources on ordinary caller cancellation before registration.
Blaxel
pty_exec_startcreates anaiohttp.ClientSession, opens the WebSocket, starts the reader task and sends the command before inserting the entry into_pty_sessions.Its cleanup handlers are:
asyncio.CancelledErrorderives fromBaseException, so cancellation duringws_connect, command send, or a contended_pty_lockacquisition bypasses both handlers. The entry is not in the registry, sopty_terminate_all()cannot recover it.Concrete resources at risk are the WebSocket,
aiohttp.ClientSession, and reader task.UnixLocal
Once
create_subprocess_execsucceeds, the process entry, pump task(s), and wait task exist. Registration then awaits:There is no cancellation cleanup around this ownership transfer. If the map lock is contended and the caller is cancelled, the process entry and background tasks remain unregistered and unreachable from
pty_terminate_all().The TTY creation path has a related gap: its fd-close handler catches only
Exceptionaroundcreate_subprocess_exec, so cancellation during that await can also bypass the PTY fd cleanup.Deterministic control-flow reproduction
I modeled the exact ownership boundaries without provider/network dependencies:
For Blaxel, cancellation bypasses the
Exceptionhandlers. For UnixLocal, cancellation while waiting for a held map lock exits after local resource creation but before registration.Other backends checked
I checked all seven PTY implementations rather than generalizing the claim:
BaseExceptionand attempts cleanup of an unregistered entry.CancelledErrorcleanup path.BaseExceptionand shields startup cleanup.CancelledErrorcleanup path.CancelledErrorcleanup path.So the direct single-cancellation claim here is intentionally limited to UnixLocal and Blaxel.
Expected behavior
Once a process/provider resource exists, either:
A narrow fix can reuse the cancellation-settled cleanup primitive discussed in #4747.
For Blaxel, add a cancellation/BaseException path that cleanup-settles the unregistered entry before re-raising.
For UnixLocal, track whether registration committed and cleanup-settle the entry if cancellation occurs before insertion. The TTY fd-close handler should also cover
BaseExceptionso cancellation cannot bypass descriptor cleanup.Debug information
openai/openai-agents-python89c02c828ee8510fe9a84ee6675608193aa13b02