Skip to content

fix(amd): start SIP detection after answer - #6386

Closed
dexhunter wants to merge 2 commits into
livekit:mainfrom
dexhunter:fix/amd-sip-answer-timeout
Closed

fix(amd): start SIP detection after answer#6386
dexhunter wants to merge 2 commits into
livekit:mainfrom
dexhunter:fix/amd-sip-answer-timeout

Conversation

@dexhunter

Copy link
Copy Markdown

Summary

  • start the AMD detection budget when an outbound SIP call becomes active, rather than when its early-media track is subscribed
  • add a separate configurable sip_answer_timeout guard, defaulting to 60 seconds, for calls that never become active
  • keep non-SIP behavior unchanged and add lifecycle, timeout, fallback, and public-export coverage

Fixes #6187.

Why

Carrier early media can subscribe the audio track well before answer. On the current implementation, that consumes the default 20-second detection_timeout during ringback and can emit uncertain / detection_timeout before a late-answering person or voicemail becomes active.

This change preserves detection_timeout as a post-answer AMD budget. The new answer guard independently bounds calls that never reach active.

Evaluation

The deterministic harness models a SIP track subscribed at t=0 and a call becoming active at t=32s:

Metric main This PR
detection_timeout_pre_answer_ms 20,000 ms 0 ms

The metric is virtual-time based, so it is independent of CPU speed. The strict harness also checks the default and custom answer deadlines, duplicate timer starts, task cancellation, non-SIP behavior, legacy fallback, and all classifier verdict paths.

Validation

  • Python 3.10: 27 focused AMD tests passed
  • Python 3.14: 27 focused AMD tests passed
  • make check: formatting, lint, and mypy passed across 617 source files
  • strict evaluator: detection_timeout_pre_answer_ms=0

The deferred-timer and separate-answer-guard direction was identified through autoresearch with Weco. The final patch was reduced and hardened manually against lifecycle and compatibility counterexamples.

@dexhunter
dexhunter requested a review from a team as a code owner July 11, 2026 00:01
@CLAassistant

CLAassistant commented Jul 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@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

@dexhunter

Copy link
Copy Markdown
Author

Re-checked this against main @ 48c17932 today. The conflict is no longer just textual, so I'd rather ask than guess at it.

#6496 arms the detection budget before the publication wait and again at track-up (detector.py has three start_detection_timer() call sites on main now), and tests/test_amd_classifier.py::test_setup_resets_detection_timer_after_track_subscription pins exactly that — assert calls == 1 inside the faked publication wait, assert calls == 2 after _setup returns. This PR's premise is that the budget must not be running while an outbound SIP call is still ringing (#6187), so any rebase of it changes what that test asserts. I don't want to quietly edit a test that was merged deliberately.

I did build the port to check it's workable rather than just asserting it isn't: bound the publication wait directly with asyncio.wait_for(..., timeout), which emits the same uncertain / detection_timeout outcome for the same duration and so preserves #6496's "AMD settles even if audio never arrives" guarantee; bound the answer wait with sip_answer_timeout; arm the detection budget in _start_listening(). That takes start_detection_timer() from three call sites to one. On a fresh worktree against today's main: pytest tests/test_amd_detector.py tests/test_amd_classifier.py37 passed, 1 failed, and the one failure is that #6496 test.

@chenghao-mou — on #6187 you said a separate name was fine rather than repurposing detection_timeout, which is what this does. The open question is only where the pre-publication bound should live:

  1. bound the publication wait directly for the same duration (what I have; the fix: make AMD settle on endpointing backstop by default #6496 test needs updating alongside it), or
  2. keep fix: make AMD settle on endpointing backstop by default #6496's arming and skip it only when the publisher is a SIP participant.

Both are small patches, I just don't think the choice is mine — either way it changes a contract you merged on purpose. Also glad to hold until #6202 lands, since that's rewriting detector.py and tests/test_amd_detector.py as well and rebasing once on top of it beats doing it twice.

@chenghao-mou

Copy link
Copy Markdown
Member

Thanks for the PR and the follow up!

The re-arm of the detection timer was more of a parity port from part of our node.js implementation (https://github.com/livekit/agents-js/blame/c92d739b7d1deefb412255df4f83a993405fe747/agents/src/voice/amd.ts#L413)

I think I have an idea: because create_sip_participant itself has a timeout value, and CreateSIPParticipantRequest also has a ringing_timeout and wait_until_answered, in normal AMD use cases, we actually don't need another sip timeout at AMD level:

Something like this:

CleanShot 2026-07-26 at 02 01 45@2x

what do you think?

@chenghao-mou

Copy link
Copy Markdown
Member

I opened #6580 with some refactoring and with you as the co-author. Let me know if it addresses your issue.

@dexhunter

Copy link
Copy Markdown
Author

@chenghao-mou great, thanks, free feel to close my current ones

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.

AMD: detection_timeout still armed at track subscription poisons long-ring outbound SIP calls (follow-up to #5848)

3 participants