fix: protege limpeza da sessão após desconexão

This commit is contained in:
alex alves
2026-11-03 10:31:04 -03:00
parent 5c35fd4e33
commit 82553d86c7
3 changed files with 76 additions and 72 deletions

View File

@@ -5,6 +5,7 @@ description = "Realtime voice assistant with WebSocket audio gateway, streaming
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"anyio>=4.15.1,<5",
"deepagents>=0.7.26", "deepagents>=0.7.26",
"fastapi>=0.141.1", "fastapi>=0.141.1",
"httpx>=0.28,<1", "httpx>=0.28,<1",

View File

@@ -35,6 +35,7 @@ Architecture:
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
import anyio
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -1044,93 +1045,93 @@ async def audio_socket(websocket: WebSocket) -> None:
pass pass
finally: finally:
# Stop Gateway-owned finalization before provider cleanup. with anyio.CancelScope(shield=True):
try: # Stop Gateway-owned finalization before provider cleanup.
await cancel_finalization_task()
except Exception:
logger.exception(
"Failed to cancel finalization task. session_id=%s",
session.session_id if session else None,
)
# Cancel and await the task owned by VoicePipeline.
if pipeline is not None:
try: try:
await pipeline.cancel_turn_task() await cancel_finalization_task()
except Exception: except Exception:
logger.exception( logger.exception(
"Failed to cancel voice turn task. session_id=%s", "Failed to cancel finalization task. session_id=%s",
session.session_id if session else None, session.session_id if session else None,
) )
# A task may have been created before registration failed. # Cancel and await the task owned by VoicePipeline.
# Handle it independently if the pipeline does not own it. if pipeline is not None:
if stt_events_task is not None:
task = stt_events_task
if (
pipeline is None
or pipeline.turn_task is not task
):
try: try:
if not task.done(): await pipeline.cancel_turn_task()
task.cancel()
try:
await asyncio.wait_for(
asyncio.shield(task),
timeout=5.0,
)
except asyncio.CancelledError:
if not task.cancelled():
raise
except TimeoutError:
logger.error(
"Unregistered STT task cleanup timed out. "
"session_id=%s",
session.session_id if session else None,
)
except Exception: except Exception:
logger.exception( logger.exception(
"Failed to clean up unregistered STT task. " "Failed to cancel voice turn task. session_id=%s",
"session_id=%s",
session.session_id if session else None, session.session_id if session else None,
) )
if task.done(): # Handle tasks created before pipeline registration.
stt_events_task = None if stt_events_task is not None:
# Abort STT without waiting for a final transcript. task = stt_events_task
if stt is not None:
if (
pipeline is None
or pipeline.turn_task is not task
):
try:
if not task.done():
task.cancel()
try:
await asyncio.wait_for(
asyncio.shield(task),
timeout=5.0,
)
except asyncio.CancelledError:
if not task.cancelled():
raise
except TimeoutError:
logger.error(
"Unregistered STT task cleanup timed out. "
"session_id=%s",
session.session_id if session else None,
)
except Exception:
logger.exception(
"Failed to clean up unregistered STT task. "
"session_id=%s",
session.session_id if session else None,
)
if task.done():
stt_events_task = None
# Abort STT without waiting for a final transcript.
if stt is not None:
try:
await stt.abort_session()
except Exception:
logger.exception(
"Failed to abort STT session. session_id=%s",
session.session_id if session else None,
)
# Release session and semantic turn state.
try: try:
await stt.abort_session() if pipeline is not None:
pipeline.close_session()
elif session is not None:
session.close()
except Exception: except Exception:
logger.exception( logger.exception(
"Failed to abort STT session. session_id=%s", "Failed to close voice session. session_id=%s",
session.session_id if session else None, session.session_id if session else None,
) )
# Release session and semantic turn state. # Release provider-owned HTTP connections.
try: if agent is not None:
if pipeline is not None: try:
pipeline.close_session() await close_optional_provider(agent)
elif session is not None: except Exception:
session.close() logger.exception(
except Exception: "Failed to close Agent provider. session_id=%s",
logger.exception( session.session_id if session else None,
"Failed to close voice session. session_id=%s", )
session.session_id if session else None, finally:
) agent = None
# Release provider-owned HTTP connections.
if agent is not None:
try:
await close_optional_provider(agent)
except Exception:
logger.exception(
"Failed to close Agent provider. session_id=%s",
session.session_id if session else None,
)
finally:
agent = None

2
uv.lock generated
View File

@@ -2924,6 +2924,7 @@ name = "voice-assistant"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "anyio" },
{ name = "deepagents" }, { name = "deepagents" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx" }, { name = "httpx" },
@@ -2946,6 +2947,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "anyio", specifier = ">=4.15.1,<5" },
{ name = "deepagents", specifier = ">=0.7.26" }, { name = "deepagents", specifier = ">=0.7.26" },
{ name = "fastapi", specifier = ">=0.141.1" }, { name = "fastapi", specifier = ">=0.141.1" },
{ name = "httpx", specifier = ">=0.28,<1" }, { name = "httpx", specifier = ">=0.28,<1" },