This message was deleted.
# ask-ai
s
This message was deleted.
t
Thanks for reaching out, Aleh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I reviewed the logs you shared. I’ll explain who ended the call and why your voicemail was cut off, and give a few practical steps you can try. Who ended the call • The call was ended by the SIP-side participant (the remote phone / voicemail system). • Evidence: logs show the disconnect reason "CLIENT_INITIATED" and the agent message: ◦ "closing agent session due to participant disconnect ... reason: CLIENT_INITIATED" ◦ "Participant disconnected ... with reason: CLIENT_INITIATED (1)" Why the voicemail was cut off • Your system logged "Voicemail delivery completed, initiating hangup" at 064915.518, but the actual participant disconnect (CLIENT_INITIATED) happens later at 064936.964. That means the remote voicemail/phone endpoint terminated the session (likely while the agent was still sending audio). • Common causes for this behavior: ◦ The answering/voicemail system enforces a maximum recording length and cut the call once that limit was reached. ◦ The voicemail system detected behavior it interprets as unsuitable (continuous audio, no expected pauses) and stopped the recording. ◦ Timing between detection and audio delivery: if the agent starts speaking too early (or too long) after detection, the voicemail system may end the call. Quick recommendations to reduce cut-offs • Add a short pause (1–2s) after detecting the answering machine before playing TTS audio. This lets the remote system finish its prompt. • Shorten the voicemail message to keep it well under common mailbox limits (e.g., <15–20s). • Add natural short pauses inside the message (brief silences) so the remote system doesn’t treat the audio as a continuous stream. • Reduce generated audio length where possible — your usage shows substantial output tokens/audio which can translate to long TTS. • If your agent/framework has options around disconnect behavior (for example not closing the agent session immediately on participant disconnect), review those to ensure the agent gets a chance to finish cleanly. The agent log references
RoomInputOptions.close_on_disconnect
, which can be relevant if you want different behavior. If you want, I can help narrow this down further: • Do you have the exact voicemail duration that reached the remote system? (or the TTS audio duration) • Can you share the TTS text length or token counts you expect for a typical message? • Would you like sample timings (pause lengths and message length) to test against common voicemail systems? Happy to dig in with those details and help you tune the timing so voicemails finish reliably. Sources: Agents telephony integration | LiveKit Documentation
b
Now I have a clear picture. Let me analyze the critical timeline from the logs: 064915.016 "Detected answering machine" — handle = session.generate_reply(...) ← NOT awaited 064915.018 RealtimeModel metrics (0 output audio tokens) — tool-call metrics 064915.518 "Voicemail delivery completed, initiating hangup" — sleep(0.5) done, hangup(handle) called → hangup() runs: update_tools([]) → update_instructions(farewell) → wait_for_playout() 064927.564 RealtimeModel metrics (605 audio tokens) — voicemail generation DONE 064936.964 CLIENT_INITIATED disconnect (Teams hangs up) Teams recorded 21 seconds (beep at ~15.016 → CLIENT_INITIATED at 36.964 = 21.9s). --- Analysis There are 3 suspects in the current code. Before proposing a fix, I need your answers: Questions 1. Is the voicemail audio actually cut at "preapproved for $2900", or is the Teams transcription just a preview? Please play the full 21-second recording. Does it contain more text after "$2900"? This distinguishes whether the speech was interrupted early vs cut near the end. 2. Were voicemails working before commit 59da4d2 (the hangup() refactor)? That commit added await self.update_tools([]) and await self.update_instructions(farewell) before wait_for_playout() in hangup(). The current code calls update_instructions(farewell) at t=0.5s while the voicemail just started generating — for Azure Realtime API, a session.update during active response generation can force a new response or interrupt the current one. This is suspect #1. 3. What does the farewell instruction say? (base_agent_call_ending_farewell_instructions) If it instructs the agent to generate a farewell speech, it may be replacing/competing with the voicemail generation. 4. Are you open to re-applying the VAD fix (turn_detection=None) specifically for the voicemail path? You removed it. Without it, Azure Realtime's server-side VAD can still fire input_speech_started on PSTN echo and interrupt delivery. This is suspect #2. --- My current read on root cause The most likely culprit is hangup(handle) calling update_instructions(farewell) while the voicemail is mid-generation (only 0.5s after generate_reply was called). Azure Realtime treats session.update as a context change that can interrupt the active response. The fix would be to call hangup only after the voicemail playout completes — i.e., await the voicemail handle before calling hangup, rather than passing the unawaited handle into it. Can you answer questions 1-3? That will confirm which path to take.
t
Thanks — this is a great, focused timeline and analysis. I’ll respond to each question you asked and explain what I need from you to confirm the root cause. Brief acknowledgement • I’m responding to the 4 questions in your message about whether the voicemail was cut, whether the refactor commit is responsible, what the farewell instruction contains, and whether you’ll re-enable the VAD change for voicemail. Answers / requests 1) Is the voicemail audio actually cut at "preapproved for $2900", or is the Teams transcription just a preview? • I cannot play the Teams recording from the logs you provided. Please do one of the following so I can confirm whether speech was interrupted or truncated: ◦ Play the saved 21s Teams recording and tell me whether audio continues past the phrase "preapproved for $2900" (timestamps are helpful), or ◦ Share the full Teams recording file, or ◦ Paste the verbatim transcript of the Teams recording with timestamps. • That single check distinguishes "speech interrupted mid-sentence" from "Teams transcription truncated/preview only." 2) Were voicemails working before commit 59da4d2 (the hangup() refactor)? • I don’t have prior-run logs here to confirm. Please tell me: ◦ Did you observe reliably completed voicemails before commit 59da4d2? ◦ If you can, attach or paste a pre-59da4d2 log of a successful voicemail (same code path) so I can compare timing around generate_reply(), the playout completion, and hangup(). 3) What does the farewell instruction say? (
base_agent_call_ending_farewell_instructions
) • Please paste the exact text/content of
base_agent_call_ending_farewell_instructions
. If it contains an instruction to speak a farewell or produce audio, it could be preempting the voicemail generation when
update_instructions(farewell)
runs. 4) Are you open to re-applying the VAD fix (
turn_detection=None
) specifically for the voicemail path? • Tell me Yes/No. If Yes, I can propose a minimal code change to re-enable
turn_detection=None
only for outbound/voicemail flows so server-side VAD won’t trigger during playout. If No, we’ll prioritize avoiding session.update while generation is in-flight. My current read and recommended next step • Your hypothesis is sound: calling
update_instructions(farewell)
inside hangup() while voicemail generation was still in-flight is the most likely cause. Azure Realtime session.update can interrupt active responses. The safer change is to await the voicemail generation/playout to finish before calling any session.update / hangup that writes new instructions. • To confirm and finalize the fix I need the items above (especially item 1 and the farewell text in item 3). Once you provide those I will recommend the exact code change (example: await handle before calling hangup; or defer update_instructions until wait_for_playout returns) and a short test plan. If you want, I can draft the precise code change (small diff) to: 1. Await the generate_reply/playout handle to complete, then 2. Call update_instructions(farewell) and hangup() Let me know which artifacts you can provide first (recording/transcript and the farewell text), and whether you’re open to the VAD change. I’ll then give the exact patch and test steps.
b
Now I have everything. Let me do the deep analysis. --- Deep analysis — root cause identified Log timeline reconstruction t= 0.000s generate_reply(voicemail_msg) ← NOT awaited, handle returned immediately t= 0.500s sleep(0.5) done hangup(handle) called → update_tools([]) ← session.update #1 sent to Azure Realtime → update_instructions("CALL IS NOW ENDING: You are delivering a closing message. Complete your farewell exactly as planned...") ← session.update #2 sent → wait_for_playout() ← blocks here t= 1.570s TTFT — first audio frame, streaming to SIP begins t=12.548s RealtimeModel metrics: 605 audio tokens, 147 text tokens wait_for_playout() returns → _do_teardown() → delete_room() ← SIP severed here t=21.948s CLIENT_INITIATED (Teams hangs up) What the token counts prove ┌─────────────────────┬───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Metric │ Value │ What it means │ ├─────────────────────┼───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ output_text_tokens │ 147 │ Transcript of what was spoken. Full voicemail ~80 words × 1.8 tokens/word ≈ 144 tokens. The full message was generated. │ ├─────────────────────┼───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ output_audio_tokens │ 605 │ Full audio content was generated. │ ├─────────────────────┼───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ tokens_per_second │ 61.32 │ Generation speed. 752 total tokens ÷ 61.32 = 12.26 seconds — matches perfectly with wait_for_playout() at t=12.548s. │ └─────────────────────┴───────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ The full voicemail was generated. This eliminates update_instructions(farewell) as the cause of truncation — the model generated all five sentences regardless. Root cause (high confidence) wait_for_playout() for the RealtimeModel fires when Azure's response.done event arrives (model generation complete), NOT when the audio finishes propagating through the SIP/PSTN pipeline. The audio streams from LiveKit → SIP gateway → PSTN at real-time rate (~1 second/second). Timeline: - Audio starts arriving at Teams at t=1.57s (TTFT) - Full 30-second voicemail finishes arriving at Teams at t≈31.57s - wait_for_playout() returned + delete_room() called at t=12.548s - At room deletion, only ~11 seconds of audio had reached PSTN; ~19 seconds still in SIP pipeline buffer - SIP connection severed → buffer discarded → Teams receives silence → records 21 seconds total (speech + silence), then CLIENT_INITIATED timeout The 21-second recording = ~10s speech (first sentence + second sentence start) + ~11s silence until voicemail system times out. One question before applying the fix Can you play the 21-second recording and confirm: after "preapproved for $2900", is the remainder of the 21 seconds silence (or background noise with no more speech)? If yes, it confirms the PSTN pipeline cut theory with certainty and the fix below is correct. --- The fix Two changes to detected_answering_machine in greetings.py: 1. Await wait_for_playout() before calling hangup() — this ensures update_tools([])/update_instructions(farewell) inside hangup() run AFTER generation, not during. (Eliminates any risk of session.update interference.) 2. Sleep 20 seconds after wait_for_playout() — bridges the gap between response.done (t≈12.5s) and the last audio frame arriving at Teams (t≈31.5s). 20s sleep → room deleted at t≈32.5s → all 30s of audio already in PSTN. handle = self.session.generate_reply(instructions=voicemail_message) # wait_for_playout() fires on Azure's response.done (~12.5s) — NOT when SIP/PSTN # finishes delivering the audio. Audio streams at real-time rate through SIP, so a # ~30s voicemail starting at TTFT=1.5s finishes arriving at Teams at ~31.5s. # We must NOT delete the room until after that point. await handle.wait_for_playout() logger.info( "Voicemail playout complete (interrupted=%s), sleeping for PSTN drain", handle.interrupted, ) await asyncio.sleep(20.0) # SIP/PSTN pipeline drain (~19s of audio still in flight) logger.info("PSTN drain sleep done, proceeding to hangup") await self.hangup(handle) hangup(handle) after the sleep: update_tools/update_instructions fire on a done handle (safe), wait_for_playout() returns instantly, _do_teardown() runs. --- Strongly recommended: re-apply the VAD fix Without turn_detection=None, PSTN echo can still fire input_speech_started and interrupt voicemail delivery. Even if it didn't cause this specific test failure, it's a latent risk on noisy PSTN lines. The fix from the previous session wraps the generate/sleep/hangup in a try/finally and restores TurnDetection afterward. --- One confirmation needed from you: play the recording, confirm it's silence after the first sentence. If yes, I'll apply the exact patch above. If there's additional speech in the recording, the math changes and we adjust the sleep value. will it help?
t
Thanks — this is an excellent, thorough analysis and the root-cause reasoning looks sound. Short answer to your final question • Yes — your proposed fix (await the playout handle, then sleep to allow PSTN drain, then call hangup) will address the issue you identified. It prevents deleting the LiveKit room while audio is still in-flight through the SIP/PSTN pipeline. What I need from you to proceed (single confirmation) • Please play the 21-second Teams recording and confirm whether the audio after the phrase "preapproved for $2900" is only silence (or background noise with no additional speech). ◦ If yes → that confirms the PSTN pipeline cut theory and I’ll provide the exact patch and a short test plan. ◦ If no (there is more speech after that phrase) → paste the exact transcript or attach the recording so we can re-evaluate timing and adjust the sleep duration. Why your fix is correct (brief) • Azure signals response.done when generation is complete, not when all audio has reached the PSTN. Deleting the room at response.done will drop buffered audio still en-route via the SIP gateway. • Awaiting the generate handle ensures no session.update runs while generation is in-flight. Adding a PSTN-drain sleep bridges the real-time delivery gap so the remote voicemail endpoint receives the full audio. Small implementation notes I’ll include in the patch 1. Await the generate handle:
await handle.wait_for_playout()
before calling hangup. 2. Sleep for a configurable drain period (start with 20s as you suggested) — make this configurable so you can tune per carrier. 3. Keep the VAD change (
turn_detection=None
) for the voicemail path if you’re open to it — it reduces risk of server-side VAD falsely triggering during playout. 4. Wrap in try/finally to ensure
turn_detection
is restored and hangup still runs on unexpected errors. If you confirm the recording is silence after "$2900", I’ll provide: • The exact code diff/patch to implement the two changes and add a configurable PSTN drain value. • A short test plan with pass/fail criteria you can run against Teams (or other carriers). Which option do you prefer: confirm the recording now, or paste/attach the Teams file/transcript and I’ll verify it for you?
b
From the LiveKit source code: 1. wait_for_playout() = await asyncio.shield(self._done_fut) — resolves when _mark_done() is called, which happens when all speech_handle._tasks are done. For the RealtimeModel, that's when Azure fires response.done and LiveKit finishes processing audio frames through the output pipeline. Not when audio is received by PSTN. 2. update_instructions does NOT interrupt active speech — it just sends a SessionUpdateEvent (fire-and-forget send_event). The commented-out response-future code confirms it was originally designed to wait for acknowledgment but doesn't now. This takes effect for the next response per OpenAI docs. 3. update_tools also does NOT interrupt — same pattern, just sends session.update. Root cause confirmed: 100% PSTN pipeline drain. The full voicemail IS generated (147 output text tokens = full 80-word message). Room is deleted at t≈12.5s while ~19 seconds of audio is still in the SIP/PSTN buffer. No need to change the order of calls — just add a drain sleep.
t
Thanks — this is an excellent, thorough confirmation. You’ve pinned the root cause correctly and your proposed fix is appropriate. Summary of agreement • Root cause: PSTN pipeline drain — LiveKit’s
wait_for_playout()
resolves when the model’s generation is finished and LiveKit has processed the outgoing audio frames internally, not when those frames have finished traversing the SIP → PSTN pipeline and been played out at the remote voicemail endpoint. Deleting the room at response.done discards audio still in-flight, causing the remote recording to end early. • No ordering changes are required for
update_instructions
/
update_tools
— they do not interrupt the active response in your codepath. The important action is ensuring the room is not deleted until the PSTN drain completes. • Fix: await the playout handle, then sleep long enough to cover the remaining real-time delivery window, then run hangup/teardown. Re-applying the VAD change for voicemail is recommended as a complementary mitigation. Suggested patch (concept) • After you `handle = self.session.generate_reply(...)`: 1. Await model playout completion: ◦
await handle.wait_for_playout()
1. Log and sleep for a configurable PSTN drain: ◦
<http://logger.info|logger.info>("Voicemail playout complete, sleeping for PSTN drain")
◦
await asyncio.sleep(self.config.pstn_drain_seconds or 20.0)
1. Proceed with
await self.hangup(handle)
Implementation notes • Make the drain duration configurable (e.g.,
pstn_drain_seconds
) so you can tune per carrier or region; 20s is a good default based on your timing. • Wrap the generate/await/sleep/hangup in try/finally to ensure cleanup and to restore any temporary VAD settings if you re-enable
turn_detection=None
for voicemail. • Keep logs for TTFT, playout completion, and hangup times so you can measure and refine the drain value. Example test plan 1. Baseline test (current behavior): ◦ Place outbound voicemail to Teams. Record and note the timestamp of beep, last intelligible word, and disconnect. 1. Apply change with pstn_drain_seconds = 20: ◦ Repeat the same voicemail content and compare full recording length and speech completeness. ◦ Expected: remote recording has the full message with no long silence where speech should be. 1. Tunability: ◦ Try 15s and 25s to find minimal safe drain for your carriers. ◦ Run tests across a few carriers (Teams, major PSTN carriers) to ensure compatibility. 1. Edge cases: ◦ Test with short messages (<10s) and very long messages (~45s+) to validate behavior and ensure you don’t unnecessarily prolong calls for short messages. ◦ Test noisy lines or PSTN echo scenarios if VAD is off; reapply the
turn_detection=None
for the voicemail path and verify no premature interruptions. Optional: Re-apply server-side VAD mitigation for voicemail • Re-enabling
turn_detection=None
for the voicemail path reduces the risk of server-side VAD or PSTN echo causing spurious
input_speech_started
events during playout. Wrap its use in try/finally so it’s restored immediately after the voicemail flow completes. If you’d like, I can: • Draft the exact minimal code diff/patch for the change (including config and try/finally) for you to drop into greetings.py. • Provide a concise set of log assertions you can add to validate the timing (TTFT, playout done, sleep start/end, hangup timestamp). Would you like the patch now, or do you want to run the quick confirmation test (play the 21s recording) first?