fix(voice): clean up tasks when streams close early - #4060
fix(voice): clean up tasks when streams close early#4060GautamSharma99 wants to merge 6 commits into
Conversation
seratch
left a comment
There was a problem hiding this comment.
Thanks for the contribution. The underlying lifecycle bug is real, and wrapping the iterator lifecycles in try/finally while draining owned tasks is the right direction.
Before we can merge this, please make the STT shutdown ordering ownership-safe. close() currently finishes the active span before awaiting WebSocket close; while that await is suspended, _handle_events() can process a queued transcription completion and start a new span that remains unfinished when the tasks are cancelled. Please stop and drain the session-owned work before the final span cleanup, and add a controlled interleaving test for this ordering.
Please also preserve the original consumer cancellation or exception if cleanup fails, with a negative-path regression test. Scope the guarantee to explicit aclose(), cancellation, and normal exhaustion; a bare async for break does not synchronously await generator close.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a4bcfac3f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Addressed the first review feedback in d340172:
The focused voice/STT suite passes (55 tests), and the full format, lint, typecheck, and test stack passes locally. |
|
@seratch
The focused voice/STT suite passes with 55 tests, and the full format, lint, typecheck, and test suite passes locally. Could you please take another look? |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d340172737
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
seratch
left a comment
There was a problem hiding this comment.
Thanks for the update. The STT shutdown ordering, replacement-span interleaving coverage, exception preservation, and clarified aclose() scope now address the previous review.
One lifecycle regression remains. StreamedAudioResult.stream() records session_ended only after yielding it. If the consumer immediately calls aclose(), GeneratorExit enters the exception cleanup path before saw_session_end is set, so the producer is cancelled instead of being allowed to finish normally. Please record terminal delivery before the yield, preserve graceful producer completion during finalization, and add a controlled regression test for this exact ordering.
Also, when cleanup fails while a primary consumer exception is active, please log only a static warning without attaching the cleanup exception. Its implicit __context__ otherwise causes verbose logging to include the primary consumer traceback. Keep this as one simplified finalization path rather than adding another logging mode.
|
Thanks for catching these remaining cases. Addressed in
The focused voice and STT suite passes with 52 tests, the sensitive-logging collector tests pass, and the full format, lint, typecheck, and test stack passes locally. |
|
Can you resolve the conflicts as well? |
|
Merged the current upstream |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a4a009a097
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
seratch
left a comment
There was a problem hiding this comment.
Can you fix all the review comments by codex first?
Address Codex review on the terminal stream finalization cleanup paths. Both StreamedAudioResult.stream() and OpenAISTTTranscriptionSession. transcribe_turns() run cleanup in a finally that swallows secondary exceptions to preserve the primary consumer exception. That also swallowed asyncio.CancelledError: when a caller cancels or wraps aclose() in asyncio.wait_for while cleanup is still awaiting (shielding the producer or closing the websocket), the injected GeneratorExit had already marked the primary exception active, so a later CancelledError was treated as secondary and dropped. The caller then observed a successful close even though cleanup was cancelled. Always re-raise CancelledError from the cleanup handler so cancellation and timeouts propagate, while still swallowing non-cancellation cleanup errors to preserve the primary exception. Add regressions that cancel cleanup with a primary exception active and assert the cancellation propagates; both fail before this change.
|
Thanks for the review — both Codex comments are fixed in 876e263. Root cause. Both cleanup paths ( Fix. The cleanup handler now always re-raises except BaseException as cleanup_exception:
if isinstance(cleanup_exception, asyncio.CancelledError) or (
not primary_exception_active
):
raise
# log + swallow non-cancellation cleanup errorTests. Added a regression for each path that cancels cleanup with a primary exception active and asserts the cancellation propagates. Both fail before this change (the STT one surfaced
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 876e263a1a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| except BaseException: | ||
| primary_exception_active = True | ||
| raise |
There was a problem hiding this comment.
Surface terminal producer errors on close
When a consumer stops as soon as it receives session_ended and calls aclose()/breaks, this treats the injected GeneratorExit as the primary exception. If the producer or a TTS task has just queued session_ended and then fails, the post-finally _check_errors() is skipped and any shielded producer exception is suppressed as a cleanup failure, so the caller observes a successful close even though stream() is the public terminal error boundary. GeneratorExit after a delivered terminal event should not hide those producer/task errors.
AGENTS.md reference: AGENTS.md:L126-L126
Useful? React with 👍 / 👎.
|
Can you resolve the conflicts and remaining review comments? |
|
Hi @GautamSharma99 and @seratch, I completed the remaining work for this PR:
Verification: I don't have push access to this fork, so the changes are in a PR against If you merge that into |
|
Thanks again for working on this; it is merged as #4131 |
This pull request fixes voice background-task and tracing cleanup when a consumer explicitly closes or cancels a voice async iterator.
Closes #4051.
Problem
StreamedAudioResult.stream()andOpenAISTTTranscriptionSession.transcribe_turns()previously ran cleanup only after normal iterator exhaustion or from cancellation handling around a queue read. Callingaclose()while either concrete async generator was suspended at a successfulyieldskipped trailing cleanup.The cleanup helpers also requested task cancellation without draining owned work. Callers could therefore return before producer, dispatcher, synthesis, websocket, listener, or STT processing tasks had run their finalizers.
Two additional ordering hazards existed:
StreamedAudioResult.stream()recordedsession_endedonly after yielding it. Immediateaclose()injectedGeneratorExitbefore the flag changed, so cleanup cancelled the producer rather than allowing normal trace and session finalization.Finally, attaching a secondary cleanup exception to a warning could render its implicit context, including the primary consumer traceback, when diagnostic logging was enabled.
Changes
aclose().session_endeddelivery before yielding the event.task_done()accounting when the STT iterator closes at a yield.Regression coverage
Controlled
asyncio.Eventinterleavings assert that:session_endedremains pending until the producer completes normally and does not cancel it;CancelledErroror an exception injected at a successful yield;Scope
The deterministic cleanup guarantee covers normal exhaustion, cancellation, and explicit
aclose(). A bareasync forbreakdoes not synchronously await async-generator close, so this pull request does not claim synchronous cleanup at the point of a bare break.Compatibility
This is an internal lifecycle fix against the v0.19.1 behavior boundary. It does not change public signatures, event formats, provider payloads, configuration, or persisted state.
Validation
make formatmake lintmake typecheckmake tests