Skip to content

fix(interruption): tolerate websocket teardown race on session.close write - #6893

Open
BhaskarKapri07 wants to merge 1 commit into
livekit:mainfrom
BhaskarKapri07:fix/interruption-ws-teardown-race
Open

fix(interruption): tolerate websocket teardown race on session.close write#6893
BhaskarKapri07 wants to merge 1 commit into
livekit:mainfrom
BhaskarKapri07:fix/interruption-ws-teardown-race

Conversation

@BhaskarKapri07

Copy link
Copy Markdown

Summary

Fixes an unhandled ClientConnectionResetError: Cannot write to closing transport that escapes the adaptive interruption (barge-in) WebSocket during teardown, surfacing as spurious error reports and silently killing barge-in detection for the affected activity.

Problem

InterruptionWebSocketStream.send_task writes a final session.close frame once the audio input channel closes. If the transport is already closing at that moment β€” the inference server won the close race, or the local shutdown path called ws.close() first β€” aiohttp's writer guard raises:

aiohttp/_websocket/writer.py:79 in send_frame
  raise ClientConnectionResetError("Cannot write to closing transport")
aiohttp/client_ws.py:245 in send_str
livekit/agents/inference/interruption.py:819 in send_task

Because ClientConnectionResetError is not an APIError, neither _main_task's retry/emit-error path nor AudioRecognition._interruption_task's except APIError: return swallows it. It escapes as an unhandled asyncio task exception whenever an agent activity is torn down around the interruption stream (embedded agent handoffs, call end) β€” and the adaptive barge-in capability for that activity silently stops.

Observed in production on LiveKit Cloud: 3 occurrences in ~3 weeks, all at the session.close write during teardown, all with zero impact on the call itself β€” pure crash-report noise plus lost false-interruption filtering.

Fix

  • send_task: skip the session.close write when ws.closed, and swallow the connection reset when the peer wins the close race. The close message is best-effort β€” if the transport is already gone, there is nothing worth sending.
  • _run finally: cancel the send/recv/forward tasks before ws.close(), so the local close can no longer arm the writer guard while send_task is mid-write. This removes the local half of the race; the try/except covers the remaining peer-initiated half.
  • Mid-stream audio send failures intentionally still surface as an unrecoverable detector error (new test documents this) β€” only the final benign teardown write is suppressed.

Tests

  • tests/test_interruption/test_interruption_failover.py::TestWsConnectionReset::test_session_close_reset_is_ignored_during_teardown β€” the teardown close race produces no error event and no task exception.
  • tests/test_interruption/test_interruption_failover.py::TestWsConnectionReset::test_audio_send_reset_fails_instead_of_hanging β€” a mid-stream send reset surfaces as exactly one unrecoverable error.

Validated locally: full tests/test_interruption/ suite (17 tests) green, ruff check and ruff format --check clean, scripts/check_types.py (strict mypy) reports no issues.

…write

send_task writes a final session.close frame once the audio input channel
closes. If the transport is already closing at that moment β€” the inference
server won the close race, or the local shutdown path called ws.close()
first β€” aiohttp's writer guard raises ClientConnectionResetError
("Cannot write to closing transport"). That exception is not an APIError,
so neither _main_task's retry/emit path nor AudioRecognition's
except APIError swallows it, and it escapes as an unhandled asyncio task
exception during agent-activity teardown (e.g. embedded agent handoffs,
call end), surfacing as spurious error reports. The barge-in capability
for that activity silently stops.

- send_task: skip the session.close write when ws.closed and swallow the
  connection reset when the peer wins the close race (best-effort).
- _run finally: cancel the tasks before ws.close(), so the local close
  cannot arm the writer guard while send_task is mid-write.
- Mid-stream audio send failures intentionally still surface as an
  unrecoverable detector error (documented by test).
@BhaskarKapri07
BhaskarKapri07 requested a review from a team as a code owner August 18, 2026 16:51

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

βœ… Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant