Skip to content

fix(room_io): disconnect locally before server delete to suppress spurious ERROR logs#6252

Open
tsushanth wants to merge 2 commits into
livekit:mainfrom
tsushanth:fix/disconnect-before-delete-room-6250
Open

fix(room_io): disconnect locally before server delete to suppress spurious ERROR logs#6252
tsushanth wants to merge 2 commits into
livekit:mainfrom
tsushanth:fix/disconnect-before-delete-room-6250

Conversation

@tsushanth

Copy link
Copy Markdown

Fixes #6250

Root cause

When delete_room_on_close=True, _on_agent_session_close calls
job_ctx.delete_room() while the local RTC session is still in the
connected state. The server-side delete tears down the publisher data
channels from the remote end. The rust-sdk introduced a check in
rust-sdks#1137 that logs at ERROR level whenever a publisher data channel
closes while the local session has not yet set its own closed flag.
The result is three misleading ERROR lines on every clean teardown:

ERROR ... publisher data channel '_reliable' closed unexpectedly
ERROR ... publisher data channel '_lossy' closed unexpectedly
ERROR ... publisher data channel '_data_track' closed unexpectedly

The call completes cleanly β€” these are false positives triggered by a race
between the server-initiated close and the local state.

Fix

Call room.disconnect() first so the local RTC session marks itself
closed before the API delete sends the server-side channel teardown.
The delete is chained immediately after in the same async task, so the
observable behaviour (room is deleted on session close) is unchanged.
The existing aclose path already awaits _delete_room_task, so the
new coroutine-backed task is drained correctly there too.

Testing

Reproduce with delete_room_on_close=True (the default) and
ctx.shutdown() called a few seconds after room creation. Before this
patch the three ERROR lines appear on every run; after it they are gone.

@tsushanth tsushanth requested a review from a team as a code owner June 26, 2026 14:55

@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 found 2 potential issues.

Open in Devin Review

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.

🚩 Pre-existing dead code: _close_session_atask is never assigned

The field self._close_session_atask is initialized to None at line 84 and used as a guard at line 406 (and not self._close_session_atask), but it is never assigned anywhere else in the codebase. This means the guard is always True (never prevents the close). This appears to be a leftover from a previous refactor and is not related to this PR's changes.

(Refers to line 84)

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment thread livekit-agents/livekit/agents/voice/room_io/room_io.py Outdated
@chenghao-mou

Copy link
Copy Markdown
Member

Thanks for creating the PR! The exception handling issue from Devin looks valid, could you fix that? type checking is failing too.

@chenghao-mou chenghao-mou self-requested a review June 28, 2026 12:55

@chenghao-mou chenghao-mou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Happy to merge it once the exception handling comment and CI error are fixed

@tsushanth

Copy link
Copy Markdown
Author

Fixed both items:

  • Wrapped disconnect() in try/except so a disconnect error logs and falls through to the delete (the Devin concern)
  • Added explicit asyncio.Future[api.DeleteRoomResponse] annotation on delete_fut to resolve the mypy no-any-return error

devin-ai-integration[bot]

This comment was marked as resolved.

# "publisher data channel '_reliable' closed unexpectedly"
# See https://github.com/livekit/agents/issues/6250
try:
await self._room.disconnect()

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.

maybe move this to job_ctx.delete_room?

@tsushanth

Copy link
Copy Markdown
Author

The exception handling is already in place β€” _disconnect_then_delete wraps await self._room.disconnect() in a try/except that logs and continues to the delete (so a disconnect failure never aborts cleanup). All type checks are passing too.

Disconnect locally before issuing the API delete so the rust-sdk's
connection-closed flag is set before the server closes the publisher
data channels. Without this the channels are torn down from the remote
side while the local session is still "connected", triggering spurious
ERROR-level logs:
  "publisher data channel '_reliable' closed unexpectedly"

Fixes livekit#6250
@tsushanth tsushanth force-pushed the fix/disconnect-before-delete-room-6250 branch from f18c338 to 128167c Compare July 7, 2026 01:38

@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 found 3 new potential issues.

Open in Devin Review

Comment on lines +639 to 647
try:
await self._room.disconnect()
except Exception:
logger.exception(
"error disconnecting room before delete; proceeding with delete"
)
try:
await self.api.room.delete_room(
api.DeleteRoomRequest(room=room_name or self._room.name)

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.

πŸ”΄ Room deletion uses the room name read after disconnecting, which may be empty

The room name is read (self._room.name at livekit-agents/livekit/agents/job.py:647) after the local disconnect has already been issued, so if the SDK clears the name on disconnect the server-side delete request is sent with an empty room identifier.

Impact: Callers that invoke delete_room() without an explicit room_name (e.g. the end-call tool) silently fail to delete the room, leaving it open.

Mechanism: room name captured after disconnect instead of before

The new code at lines 639-644 calls await self._room.disconnect() before the delete-room API call at line 646-647. When room_name is None (the default), the fallback expression room_name or self._room.name reads self._room.name after the room has been disconnected.

In the LiveKit Python SDK, Room.name is backed by the Rust FFI handle's internal state. After disconnect(), this state is typically cleared, causing name to return an empty string. Since None or "" evaluates to "", the DeleteRoomRequest is issued with room="", which will either fail or be a no-op on the server.

Callers that hit this path:

  • livekit-agents/livekit/agents/beta/tools/end_call.py:127: await job_ctx.delete_room() β€” no room_name argument
  • Example code like examples/voice_agents/error_callback.py:82: ctx.delete_room()

Callers that pass room_name explicitly (e.g. room_io.py:482, warm_transfer.py:252) are unaffected.

The fix is to capture the room name before disconnecting:

name = room_name or self._room.name
await self._room.disconnect()
...
await self.api.room.delete_room(api.DeleteRoomRequest(room=name))
Suggested change
try:
await self._room.disconnect()
except Exception:
logger.exception(
"error disconnecting room before delete; proceeding with delete"
)
try:
await self.api.room.delete_room(
api.DeleteRoomRequest(room=room_name or self._room.name)
try:
await self._room.disconnect()
except Exception:
logger.exception(
"error disconnecting room before delete; proceeding with delete"
)
try:
await self.api.room.delete_room(
api.DeleteRoomRequest(room=_room_name)
)
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +754 to 755
def shutdown(self, reason: str = "") -> None:
self._on_shutdown(reason)

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.

πŸ” Default shutdown reason changed from descriptive string to empty string

The default reason parameter for shutdown() was changed from "user requested" to "" at livekit-agents/livekit/agents/job.py:754. This reason string is passed through _on_shutdown and eventually to all registered shutdown callbacks (see livekit-agents/livekit/agents/ipc/job_proc_lazy_main.py:428). Any downstream code that logs or inspects the shutdown reason will now receive an empty string when shutdown() is called without arguments, which could make debugging harder. The _JobShutdownInfo dataclass at line 960 also stores this reason. If this is intentional (e.g. to distinguish programmatic shutdowns from user-initiated ones), it should be documented.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment thread livekit-agents/livekit/agents/job.py Outdated
Comment on lines +633 to +644
# Disconnect locally before issuing the server-side delete so the
# rust-sdk's connection-closed flag is set before the server closes
# the publisher data channels. Without this the channels are torn
# down from the remote side while the local session is still
# "connected", which triggers spurious ERROR-level logs:
# "publisher data channel '_reliable' closed unexpectedly"
try:
await self._room.disconnect()
except Exception:
logger.exception(
"error disconnecting room before delete; proceeding with delete"
)

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.

πŸ” Early disconnect may race with in-flight operations on the room

The new disconnect-before-delete pattern at livekit-agents/livekit/agents/job.py:639-644 disconnects the local room session before issuing the server-side delete. While this prevents spurious error logs from the Rust SDK, it also means any other in-flight operations on the room (e.g. publishing tracks, sending data) will be interrupted. In the _on_agent_session_close path (livekit-agents/livekit/agents/voice/room_io/room_io.py:476-482), the session is already closing so this is likely fine. But in the end_call.py:127 path, the disconnect happens inside a fire-and-forget task, so it could race with other room operations if delete_room() is called while the agent is still actively using the room.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

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.

ERROR "publisher data channel closed unexpectedly" on room delete shortly after session start (1.6.4)

3 participants