* fix(gather): clear a finalized deepgram interim word so UtteranceEnd is not deferred forever
The #1088 check defers UtteranceEnd while an interim word newer than
last_word_end is pending. The marker was only cleared by a non-empty
is_final, so an interim word that Deepgram later dropped (e.g. line
noise, followed by an empty final) left it set: the gather waited for
the caller to speak again and merged both utterances.
Clear the marker on any Deepgram is_final result whose window
(start + duration) covers it. A word that Deepgram carries into the
next segment still defers UtteranceEnd.
Port of jambonz/feature-server#223.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(gather): return the buffer when a deferred deepgram UtteranceEnd is satisfied
Clearing the unprocessed-word marker only helps if the covering final
arrives before UtteranceEnd. If it arrives after, UtteranceEnd has
already been deferred and Deepgram sends no further UtteranceEnd until
new speech, so the next utterance is still merged in.
Remember a deferred UtteranceEnd, and when a later final clears the
marker, return the buffered transcript once that final has been
handled (setImmediate, since _onTranscription has early returns).
Port of jambonz/feature-server#225.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(gather): only an empty final ends a gather early after a deferred UtteranceEnd
A final with words means the caller resumed talking after the pause;
Deepgram will send a new UtteranceEnd after those words, so keep the
existing flow instead of cutting the caller off mid-sentence (or
skipping the asrTimeout wait). Only a final that drops the pending word
(empty) leaves nothing else to end the gather.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix: fire sip:refer actionHook when the far end BYEs before any NOTIFY
When a REFER is accepted with 202 the verb waits for a NOTIFY carrying the
final status of the referred call, and only fired the actionHook from the
15s timeout callback. If the far end sends BYE straight after the 202 the
call session kills the task, awaitTaskDone() resolves, and exec returned
without ever performing the action.
Perform the action once from exec on every exit path - before the session
tears the requestor down - and de-duplicate it against the final-NOTIFY path.
Fix#1258
Generated by Ora Studio
Vibe coded by ousamabenyounes
Co-Authored-By: Ora Agent <noreply@oratelecom.net>
* fix: keep final_referred_call_status when a BYE races the final NOTIFY (#1580)
A final NOTIFY (status >= 200) whose eventHook round trip is still in flight
when the far end BYEs lost final_referred_call_status on the actionHook: kill()
woke exec(), which memoised the action via _performReferAction({refer_status})
without the final status, so the NOTIFY path's later call carrying it was
deduped away. On main the app received final_referred_call_status here.
Record the final status synchronously in _handleNotify before the eventHook
await, and include it from exec() so the memoised action carries it regardless
of which exit path wins the race. Extract the 200 threshold into a named
constant while touching those lines.
Addresses davehorton's review on #1580.
Generated by Ora Studio
Vibe coded by ousamabenyounes
Co-Authored-By: Ora Agent <noreply@oratelecom.net>
---------
Co-authored-by: Ora Agent <noreply@oratelecom.net>
Co-authored-by: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com>
* fix: carry siprec recording state across a feature server transfer
transferCallToFeatureServer() stored the application, callInfo and remaining tasks,
but not the SIPREC recording state, so the receiving session started at
RecordingOff while the SBC was still recording. From there every live call control
on the recording was rejected locally before an INFO was ever sent: stop and pause
fail the guards in notifyRecordOptions, and a fresh startCallRecording is refused
by the SBC as a duplicate.
Carry recordOptions and the record state through the transfer and restore both on
the receiving session, which also keeps propagateAnswer from issuing a second
startCallRecording for a call that is already being recorded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: add standalone smoke test for siprec across a feature server transfer
Drives the failing flow end to end - answer, start SIPREC, enqueue, create the
agent call on a different feature server, dequeue by callSid - while acting as
the SIPREC recorder, so it can tell whether the recording kept receiving media
across the REFER and whether it was BYEd when the call ended.
Needs two or more feature servers and a real inbound call, so it lives outside
the mocha suite and is run by hand; npm test does not pick it up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: let the siprec smoke test originate both call legs itself
Waiting for a human to dial in with music playing made the test hard to run and
easy to make meaningless (a muted caller sends no RTP, so "the recording is
silent" proves nothing). Add a built-in SIP endpoint that answers and streams a
PCMU tone, and have the test place both legs through createCall, so a run needs
only an account_sid and no carrier, DID or softphone.
That makes the default mode exercise the transfer path in sbc-outbound; the
previous flow is kept as SMOKE_MODE=inbound for the sbc-inbound path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: hand the inherited recording state to one session only
Review of the previous commit turned up two ways the carried state leaks, both
because it was read off the shared application object and left there:
- a call transferred twice kept a stale siprecRecording from the first hop.
transferCallToFeatureServer copies cs.application, and the new guard only
wrote the key, so a call recorded FS1->FS2, stopped there, then moved to FS3
arrived believing it was still recording: startCallRecording rejected as
"already started", stopCallRecording sent an INFO for a session that was
gone.
- child legs got it too. dial passes cs.application straight to
ConfirmCallSession and place-outdial spreads it for the adulting session, so
those sessions came up with recordState=recording_on and the parent's SRS
options while their own dialog had no siprec session at all.
The transferred session now takes the state off the application as it adopts it,
so exactly one session owns it.
Also fixes the smoke test's silence measurement, which was computed per stream
and reported a whole quiet window as a gap - a legitimately silent second stream
failed the run - and splits the README's requirements by mode, since the default
rest mode needs no portal application and no external caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: drop the standalone smoke script, the harness test covers it
smoke-tests/siprec-fs-transfer was a self-contained driver for the cross-feature
-server SIPREC flow, written before the same scenario landed in the smoke-tester
harness as TestVerb_Siprec_SurvivesFeatureServerTransfer (plus a REST-leg
variant for the sbc-outbound path). The harness version is the one that runs in
CI, asserts the recorded audio with Deepgram rather than counting packets, and
is what proved both fixes on a real cluster - keeping a second implementation of
the same flow here only invites the two to drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: support srtpEncryption on dial verb for sip URI targets
Reads the new dial.srtpEncryption option and, for sip: URI targets,
forwards it to sbc-outbound as an X-Jambonz-SRTP header so the outbound
leg negotiates encrypted media (SRTP) — e.g. to a sips:/TLS endpoint
such as LiveKit. No behavior change when the option is unset.
Requires @jambonz/verb-specifications with the srtpEncryption spec
(dependency bump to follow once published) and sbc-outbound honoring
the X-Jambonz-SRTP header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* update verb specification
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* add null check for eventHook
* move guard to superclass, remove logging that adds no value
---------
Co-authored-by: rhonda hollis <rhonda@jambonz.org>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
* fix race condition where gather resolves with speech transcript but timeout timer gets set after the resolve and is left running after gather completes
* remove unneeded line of code
* compare sdp for transcoding
* refactor sdp check for leading codec
* fix reference to epOther
* minor changes
* minor
* fix#1447
* fix security issue
* use convenience getter appIsUsingWebsockets in CallSession
---------
Co-authored-by: Dave Horton <daveh@beachdognet.com>
* fix say verb does not close streaming when finish say
* wip
* wip
* ttsStreamingBuffer reset eventHandlerCount after remove listeners
* only send tokens to module if connected
* wip
* sent stream_open when successfully connected to vendor
* fixed gather does not start timeout on bargin
* with previous change, no need to emit playDone since no where in the code are we listening for it
---------
Co-authored-by: Dave Horton <daveh@beachdognet.com>