You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Have you searched for related issues? Yes. I did not find an issue covering PTY entries that are removed from the registry before cancellation-settled backend teardown.
Once the pop commits, pty_terminate_all() can no longer reach that entry. If the caller is cancelled while _terminate_pty_entry() is suspended, teardown is cancelled too and there is no registry owner left to retry it.
Blaxel is the clearest concrete resource leak: its terminate path awaits cleanup that closes the PTY/WebSocket-side resources and the aiohttp.ClientSession. Other backends also have awaitful teardown paths, although I would not claim identical runtime exposure for every provider without provider-specific integration tests.
Why this matters
Registry removal transfers ownership of cleanup away from the session map. After that point, caller cancellation should be allowed to cancel delivery of the result, but should not be able to abandon resource teardown.
The repository already has the stronger ownership pattern in sandbox/session/mount_lifecycle.py::_settle_mount_transition: create and own the work task, await a separate completion task under asyncio.shield, keep waiting across repeated caller cancellations, consume the work result, and only then propagate the caller's cancellation state.
Deterministic regression shape
This does not require a real provider. A unit test can monkeypatch termination so it blocks after starting:
On the current ownership shape, the final assertion fails: cancellation propagates into the awaited terminate coroutine after the entry has already become unreachable from the registry.
A dependency-free control-flow reproduction of the same transaction gives:
registry removed: yes
cancellation during teardown: yes
cleanup completed: no
registry can recover entry: no
A settled-owner version modeled after _settle_mount_transition survives repeated cancellation and propagates cancellation only after cleanup completes.
Expected behavior
Once PTY registry removal commits, backend teardown should have an owner that survives caller cancellation until cleanup reaches a terminal state.
A narrow implementation could factor a PTY cleanup helper analogous to _settle_mount_transition rather than sprinkling bare asyncio.shield() calls that leave background task ownership ambiguous.
During review of #4745 I initially treated a repeated-cancellation version of this as part of that PR's new final-tail path. Comparing against the exact PR base showed that this post-pop terminate-await ownership boundary already exists on main, so I corrected the review and withdrew it as a blocker on that PR.
PR #4745 has a separate, PR-specific single-cancellation regression around its newly-added tail-drain await; that is distinct from this pre-existing lifecycle-hardening issue.
Debug information
Repository: openai/openai-agents-python
Main SHA: 89c02c828ee8510fe9a84ee6675608193aa13b02
Provider/model: n/a for the deterministic ownership repro
Network/provider integration execution for this report: not required / not claimed
Second manifestation: pty_terminate_all() clears ownership before batch cleanup
The same ownership problem exists in every current pty_terminate_all() implementation. All seven backends copy the entries, clear the registry, and then await teardown sequentially:
If caller cancellation lands while cleaning the first entry, the current cleanup is cancelled and every later entry is already unreachable from the registry, so their cleanup never starts.
A deterministic two-entry control-flow reproduction gives:
registry cleared: yes
cancellation during cleanup of A: yes
cleanup completed: none
cleanup of B started: no
A settled-owner batch preserving the current sequential order cleans A and B first, then re-propagates the caller's cancellation.
This means the shared helper should own an arbitrary cleanup operation, so finalizers can settle one terminate call and pty_terminate_all() can settle a sequential cleanup_all() coroutine.
Backend scope verified on main
At 89c02c828ee8510fe9a84ee6675608193aa13b02:
Unix local: non-TTY finished finalization has awaitful teardown; TTY schedules fd close and returns without the same post-pop await.
E2B: a finished entry with an exit code returns immediately from _terminate_pty_entry; do not claim that finished-finalizer path is exposed by this exact mechanism.
Modal: awaitful process/read-task teardown.
pty_terminate_all() is broader: because it can include running entries, all seven batch implementations need cancellation-settled ownership after the registry is cleared.
Candidate implementation contract
A narrow helper can mirror the repository's existing mount_lifecycle._settle_mount_transition pattern:
Local reference checks cover normal completion, single cancellation, repeated cancellation, cleanup-exception preservation, and sequential batch cleanup under cancellation: 5/5 PASS.
This remains an implementation proposal; no upstream patch or provider integration execution is claimed.
Please read this first
mainat89c02c828ee8510fe9a84ee6675608193aa13b02. It is pre-existing and is not being reported as a regression from PR fix: keep a utf-8 character that straddles two pty collection windows #4745.Describe the bug
The PTY finalizers remove a finished entry from the session registry and then await backend teardown:
Once the
popcommits,pty_terminate_all()can no longer reach that entry. If the caller is cancelled while_terminate_pty_entry()is suspended, teardown is cancelled too and there is no registry owner left to retry it.Blaxel is the clearest concrete resource leak: its terminate path awaits cleanup that closes the PTY/WebSocket-side resources and the
aiohttp.ClientSession. Other backends also have awaitful teardown paths, although I would not claim identical runtime exposure for every provider without provider-specific integration tests.Why this matters
Registry removal transfers ownership of cleanup away from the session map. After that point, caller cancellation should be allowed to cancel delivery of the result, but should not be able to abandon resource teardown.
The repository already has the stronger ownership pattern in
sandbox/session/mount_lifecycle.py::_settle_mount_transition: create and own the work task, await a separate completion task underasyncio.shield, keep waiting across repeated caller cancellations, consume the work result, and only then propagate the caller's cancellation state.Deterministic regression shape
This does not require a real provider. A unit test can monkeypatch termination so it blocks after starting:
On the current ownership shape, the final assertion fails: cancellation propagates into the awaited terminate coroutine after the entry has already become unreachable from the registry.
A dependency-free control-flow reproduction of the same transaction gives:
A settled-owner version modeled after
_settle_mount_transitionsurvives repeated cancellation and propagates cancellation only after cleanup completes.Expected behavior
Once PTY registry removal commits, backend teardown should have an owner that survives caller cancellation until cleanup reaches a terminal state.
A narrow implementation could factor a PTY cleanup helper analogous to
_settle_mount_transitionrather than sprinkling bareasyncio.shield()calls that leave background task ownership ambiguous.Relationship to PR #4745
During review of #4745 I initially treated a repeated-cancellation version of this as part of that PR's new final-tail path. Comparing against the exact PR base showed that this post-pop terminate-await ownership boundary already exists on
main, so I corrected the review and withdrew it as a blocker on that PR.PR #4745 has a separate, PR-specific single-cancellation regression around its newly-added tail-drain await; that is distinct from this pre-existing lifecycle-hardening issue.
Debug information
openai/openai-agents-python89c02c828ee8510fe9a84ee6675608193aa13b02Second manifestation:
pty_terminate_all()clears ownership before batch cleanupThe same ownership problem exists in every current
pty_terminate_all()implementation. All seven backends copy the entries, clear the registry, and then await teardown sequentially:If caller cancellation lands while cleaning the first entry, the current cleanup is cancelled and every later entry is already unreachable from the registry, so their cleanup never starts.
A deterministic two-entry control-flow reproduction gives:
A settled-owner batch preserving the current sequential order cleans A and B first, then re-propagates the caller's cancellation.
This means the shared helper should own an arbitrary cleanup operation, so finalizers can settle one terminate call and
pty_terminate_all()can settle a sequentialcleanup_all()coroutine.Backend scope verified on
mainAt
89c02c828ee8510fe9a84ee6675608193aa13b02:aiohttp.ClientSessionteardown; clearest concrete resource leak._terminate_pty_entry; do not claim that finished-finalizer path is exposed by this exact mechanism.pty_terminate_all()is broader: because it can include running entries, all seven batch implementations need cancellation-settled ownership after the registry is cleared.Candidate implementation contract
A narrow helper can mirror the repository's existing
mount_lifecycle._settle_mount_transitionpattern:Local reference checks cover normal completion, single cancellation, repeated cancellation, cleanup-exception preservation, and sequential batch cleanup under cancellation: 5/5 PASS.
This remains an implementation proposal; no upstream patch or provider integration execution is claimed.