Skip to content

fix(aspnetcore): hang up and warn when the OpenAI live sideband closes mid-call - #42

Merged
MatthewHsu1 merged 2 commits into
mainfrom
fm/agentcore-live-session-hangup
Oct 22, 2026
Merged

MatthewHsu1 merged 2 commits into
mainfrom
fm/agentcore-live-session-hangup

Conversation

@MatthewHsu1

@MatthewHsu1 MatthewHsu1 commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Intent

yeah no luck on the phone, just build it now

Context: this is the build of a plan to move Spirit's phone line to the direct SIP path GoTo -> Asterisk -> OpenAI. Live tests proved a SIP REFER from Asterisk to GoTo with Refer-To sip:@ is accepted (202, NOTIFY 200) and moves the caller. GoTo's web-calls blind-transfer API rejects the SIP X-ref id (404), so the plan is that the AI session sends the REFER (AgentCore's LiveReferTransfer, used when the host registers no TransferCommand handler) and Asterisk passes it on to GoTo. Separately, the OpenAI live session's audio stopped silently at about 218 s and 278 s in two tests, and AgentCore neither hung the call up nor logged anything, so callers sat in silence. The AgentCore part of the plan: in OpenAiLiveCall, when the sideband receive loop ends on a null message and on a LiveEvent.Closed, hang the call up and finish instead of only finishing, and log a Warning naming the call id and cause on both paths; tests for both. No transfer change. The full OpenAI REFER -> Asterisk -> GoTo chain is not yet proven by a live call.

What Changed

  • OpenAiLiveCall: when the sideband receive loop ends with no Closed event (sideband_closed), the call now hangs up and then finishes (HangUpAndFinishAsync, reason Faulted). Before, it only finished, so the phone line stayed open in silence.
  • OpenAiLiveLog: new Warning SessionEndedByOpenAi (EventId 329). It names the call id and the cause, and is logged on that sideband-closed path.
  • Tests in OpenAiLiveCallEndTests: a sideband that closes mid-call hangs up once and logs one Warning with sideband_closed. A LiveEvent.Closed (a caller hang-up, close_requested) still finishes the call with reason CallerHungUp, with no hang-up request and no warning.

Risk Assessment

✅ Low: The production change is small: only the sideband-closed (null message) path now hangs up and logs Warning 329. The Closed branch is back to the original FinishAsync with no log, which matches the user's round-1 decision. The only gap is a minor loss of test coverage.

Testing

I drove three call scenarios through the real AgentCore web host. The host got a signed webhook, attached a fake OpenAI sideband, and sent its call-control requests to a recording fake. For each run I saved the hangup requests, the log lines, and the end reason. Sideband close, mid-call or at once, gives exactly one hangup and one Warning 329, also when the host stops after it. A recorded caller hang-up gives no hangup and no warning. The base code fails the sideband scenarios, so the drivers catch the bug. A real OpenAI/SIP phone call was not possible: there is no OpenAI key or SIP trunk here. One run ran the whole AspNetCore test project by accident (all passed). The temporary driver file is deleted, and the worktree is clean. This is not a UI change, so there are no screenshots.

  • Live validation: ✅ go - 3 of 4 scenarios driven live against the product
Scenario Result Live Evidence
OpenAI sideband closes mid-call with no session.closed: AgentCore POSTs /hangup once, logs Warning 329 with call id and sideband_closed, and ends the call Faulted/sideband_closed ✅ pass live ~/.no-mistakes/evidence/01M5JR93DWYJWBNFR7Z1NA20J3/s1-sideband-closes-mid-call.txt (base code: before-fix/s1 shows no hangup and no warning)
Caller hangs up (recorded session.closed close_requested): call ends CallerHungUp with no hangup request and no Warning 329 (round-1 decision) ✅ pass live ~/.no-mistakes/evidence/01M5JR93DWYJWBNFR7Z1NA20J3/s2-caller-hangup-session-closed.txt
Adversarial: sideband dies right after session.started, then the host shuts down: exactly one hangup, one warning, one ConversationEnded ✅ pass live ~/.no-mistakes/evidence/01M5JR93DWYJWBNFR7Z1NA20J3/s3-sideband-closes-at-once-then-stop.txt
Real phone call through GoTo -> Asterisk -> OpenAI where OpenAI audio stops silently: the caller gets hung up ⏸️ untested no This needs a real OpenAI Realtime SIP session (OPENAI API key and webhook secret), the GoTo/Asterisk SIP trunk, and a phone. None of them are here, and the audio stall cannot be forced on demand. The…
Evidence: Sideband closes mid-call (fixed code): one hangup + Warning 329

POST /v1/live/sessions/rtc_s1/accept POST /v1/live/sessions/rtc_s1/hangup [Warning] SessionEndedByOpenAi (329): the session of the call rtc_s1 ended on OpenAI's side (sideband_closed); the call is hung up. reason=Faulted callId=rtc_s1 cause=sideband_closed

POST /v1/main/call webhook -> 200
--- requests the host sent to (fake) OpenAI call control ---
POST /v1/live/sessions/rtc_s1/accept
POST /v1/live/sessions/rtc_s1/hangup
--- OpenAiLive log lines (Information and above) ---
[Warning] SessionEndedByOpenAi (329) AgentCore.OpenAiLive: the session of the call rtc_s1 ended on OpenAI's side (sideband_closed); the call is hung up.
--- ConversationEnded hook ---
reason=Faulted callId=rtc_s1 cause=sideband_closed
Evidence: Caller hang-up via session.closed: no hangup, no warning
POST /v1/main/call webhook -> 200
hangup requests before host stop: 0
--- requests the host sent to (fake) OpenAI call control ---
POST /v1/live/sessions/rtc_s2/accept
--- OpenAiLive log lines (Information and above) ---
--- ConversationEnded hook ---
reason=CallerHungUp callId=rtc_s2 cause=close_requested
Evidence: Sideband closes at once then host stops: still exactly one hangup
POST /v1/main/call webhook -> 200
--- requests the host sent to (fake) OpenAI call control ---
POST /v1/live/sessions/rtc_s3/accept
POST /v1/live/sessions/rtc_s3/hangup
--- OpenAiLive log lines (Information and above) ---
[Warning] SessionEndedByOpenAi (329) AgentCore.OpenAiLive: the session of the call rtc_s3 ended on OpenAI's side (sideband_closed); the call is hung up.
--- ConversationEnded hook ---
reason=Faulted callId=rtc_s3 cause=sideband_closed
  • Evidence: Same scenarios on base code (before fix): no hangup, no warning (local file: ~/.no-mistakes/evidence/01M5JR93DWYJWBNFR7Z1NA20J3/before-fix)
Evidence: Targeted test run on the target commit
Passed!  - Failed:     0, Passed:    13, Skipped:     0, Total:    13, Duration: 1 s - AgentCore.AspNetCore.Tests.dll (net10.0)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ src/AgentCore.AspNetCore/Vendors/OpenAiLive/Call/OpenAiLiveCall.cs:274 - The code maps LiveEvent.Closed to ConversationEndReason.CallerHungUp, and the old test was named 'ACallerHangUpBeforeTheAckFinishesTheCallWithNoHangUp'. So OpenAI's session.closed is also how a normal caller hang-up arrives. With this change, every normal caller hang-up now does three things: (1) it logs the new Warning 329 'ended on OpenAI's side ... the call is hung up', which is the wrong label for a caller who left; (2) it POSTs /hangup for a call that is already gone; (3) it logs HangupFailed (316) when OpenAI answers that POST with non-2xx (LiveCallFinish.cs:32). The intent asks for the hang-up and the warning on Closed, so the behavior itself conforms. The question is the log noise and the wording: should the warning text be neutral (for example 'OpenAI closed the session ({Cause})'), or should Warning 329 and HangupFailed be skipped when the cause is a normal caller hang-up? The same new log call is at OpenAiLiveCall.cs:177 (null-message path). There the wording is accurate.

🔧 Fix applied.
1 info still open:

  • ℹ️ tests/AgentCore.AspNetCore.Tests/Vendors/OpenAiLive/OpenAiLiveCallEndTests.cs:143 - The round-1 fix rewrote the old test ACallerHangUpBeforeTheAckFinishesTheCallWithNoHangUp. It now calls RunningLiveCall.StartAsync without EndsAfterTheFirstAskYaml. The old test ran with that plan, so an engine end was waiting for the ack when the Closed event came. It proved that the waiting end does not hang up a caller who already left. Without the plan, nothing in the test could start a hang-up except the Closed branch, so that race is no longer covered. The user asked to restore the original assertions. Fix: pass EndsAfterTheFirstAskYaml again, e.g. StartAsync(new StallOnCueChatClient(), [hook], EndsAfterTheFirstAskYaml, warned.CreateLogger("test")). Keep the new CallerHungUp/close_requested and zero-warning checks.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 3 of 4 scenarios driven live against the product
Scenario Result Live Evidence
OpenAI sideband closes mid-call with no session.closed: AgentCore POSTs /hangup once, logs Warning 329 with call id and sideband_closed, and ends the call Faulted/sideband_closed ✅ pass live ~/.no-mistakes/evidence/01M5JR93DWYJWBNFR7Z1NA20J3/s1-sideband-closes-mid-call.txt (base code: before-fix/s1 shows no hangup and no warning)
Caller hangs up (recorded session.closed close_requested): call ends CallerHungUp with no hangup request and no Warning 329 (round-1 decision) ✅ pass live ~/.no-mistakes/evidence/01M5JR93DWYJWBNFR7Z1NA20J3/s2-caller-hangup-session-closed.txt
Adversarial: sideband dies right after session.started, then the host shuts down: exactly one hangup, one warning, one ConversationEnded ✅ pass live ~/.no-mistakes/evidence/01M5JR93DWYJWBNFR7Z1NA20J3/s3-sideband-closes-at-once-then-stop.txt
Real phone call through GoTo -> Asterisk -> OpenAI where OpenAI audio stops silently: the caller gets hung up ⏸️ untested no This needs a real OpenAI Realtime SIP session (OPENAI API key and webhook secret), the GoTo/Asterisk SIP trunk, and a phone. None of them are here, and the audio stall cannot be forced on demand. The…
  • Temporary xUnit driver tests/AgentCore.AspNetCore.Tests/Vendors/OpenAiLive/ZzScratchHangupEvidence.cs (deleted after the run). It starts the real ASP.NET host (OpenAiLiveHost, Kestrel on 127.0.0.1:0), posts a signed incoming-call webhook to /v1/main/call, feeds the recorded p1-a sideband events, and records every request to the fake OpenAI call-control API plus the real ILoggerFactory output
  • dotnet test tests/AgentCore.AspNetCore.Tests --filter "FullyQualifiedName~ZzScratchHangupEvidence|FullyQualifiedName~OpenAiLiveCallEndTests" on the target commit: 13 passed
  • Regression check: put OpenAiLiveCall.cs back to base 0c6f06c and ran the driver again. S1 and S3 failed with no hangup request and no warning. Restored the file with git checkout
  • An earlier run of dotnet test tests/AgentCore.AspNetCore.Tests ignored the class filter and ran the whole AspNetCore test project: 862 passed
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Release

Packages ship by v* tag; the latest is v0.17.0. This fix should ship as v0.17.1 (bug fix, no API change). Not tagged here.

Note on LiveEvent.Closed

The recorded Closed reasons (for example close_requested) cannot tell a caller hang-up from an AI-side end. So the Closed path is unchanged (finishes as CallerHungUp, no hang-up request, no warning). Only the sideband-closed path (null message, no Closed event) hangs up and logs Warning 329.

A Closed event or a sideband that ends mid-call now hangs the call up
and logs a warning with the cause, so callers no longer sit in silence.
@MatthewHsu1
MatthewHsu1 merged commit b48ea59 into main Oct 22, 2026
1 check passed
@MatthewHsu1
MatthewHsu1 deleted the fm/agentcore-live-session-hangup branch October 22, 2026 21:47
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.

1 participant