From 99d169ec67d6bf15a474447a8b078bdba822785c Mon Sep 17 00:00:00 2001 From: arys Date: Mon, 31 Aug 2026 01:03:47 +0500 Subject: [PATCH] fix: stop the no-answer retry from racing the dialplan hangup + fix event catalog validation Two bugs found via a live test with two real registered browser softphones: 1. retry_escalation_no_answer() re-resolved the client channel via _resolve_handoff_channel() -> a live AMI CoreShowChannels round-trip that can take ~10s. The new mvpcc-transfer dialplan wait window (MusicOnHold, also ~10s, added to give the backend time to redirect before the final Hangup) was consistently LOST to this exact same duration: the backend's AMI Redirect fired against a channel the dialplan had already hung up ('Channel does not exist: PJSIP/...', confirmed in escalation timeline). Fixed by reusing the actively-maintained AsteriskCallLinkRow.channel_name directly (unchanged for a PJSIP channel across Redirect between contexts of the same call) instead of re-discovering it, falling back to the slow path only if that field is empty. 2. VoiceEventIn.event_type is a pydantic Literal restricted to 6 legacy values (call.started/ivr.completed/...). None of the Phase 2 event catalog names (AgentReserved/AgentRinging/AgentNoAnswer/AgentConnected/ TransferCompleted/TransferFailed) were ever in it, so every single _emit_escalation_event() call has been failing with 422 since Phase 2 shipped (swallowed silently by the broad except there) - confirmed by calling app._emit_voice_event() directly against the running service. Extended the Literal to include all six. --- services/asterisk_bridge_service/voice_ai.py | 6 +++++- services/shared/models.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/services/asterisk_bridge_service/voice_ai.py b/services/asterisk_bridge_service/voice_ai.py index 1eed1a8..393b3ab 100644 --- a/services/asterisk_bridge_service/voice_ai.py +++ b/services/asterisk_bridge_service/voice_ai.py @@ -938,7 +938,11 @@ def retry_escalation_no_answer(session, *, call_id: str, dial_outcome: str) -> N ) attempted_ids = json.loads(escalation.attempted_agent_ids_json or "[]") - channel = _resolve_handoff_channel(session, link) + # Use the already-known, actively-maintained channel name directly instead of + # _resolve_handoff_channel()'s live AMI CoreShowChannels re-discovery: that + # round-trip can take ~10s, which races (and loses) against the dialplan's + # own short MusicOnHold-then-hangup wait window for this exact retry path. + channel = str(link.channel_name or "").strip() or _resolve_handoff_channel(session, link) required_skills = json.loads(escalation.required_skills_json or "[]") next_agent = _reserve_routing_agent( call_id=call_id, diff --git a/services/shared/models.py b/services/shared/models.py index 00b2bd9..4c8f470 100644 --- a/services/shared/models.py +++ b/services/shared/models.py @@ -363,6 +363,12 @@ class VoiceEventIn(BaseModel): "recording.ready", "call.connected", "call.transferred", + "AgentReserved", + "AgentRinging", + "AgentNoAnswer", + "AgentConnected", + "TransferCompleted", + "TransferFailed", ] call_id: str interaction_id: str | None = None -- 2.54.0