Commit Graph
1242 Commits
Author SHA1 Message Date
Dave HortonandClaude Opus 5.5 baf97dabc2 fix(gather): clear a finalized deepgram interim word so UtteranceEnd is not deferred forever (#1588)
* 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>
2026-09-29 17:45:27 -04:00
javibooklineandClaude Fable 5.1 ebcc3e3a8a fix: speechmatics enable_entities channel variable name typo (#1587)
The speechmatics recognizer option transcription_config.enable_entities
was exported to freeswitch as SPEECHMATICS_ENABLE_ENTTIES, but
mod_speechmatics_transcribe reads SPEECHMATICS_ENABLE_ENTITIES, so the
option never reached the StartRecognition request and Speechmatics always
ran with its default. Rename the variable so the option takes effect.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 07:19:43 -04:00
Ben Younes 74510b0bef fix: retry ws opening-handshake timeout under the default ct policy (#1565) (#1576)
The ws v8 library throws a plain Error('Opening handshake has timed out') with no
.code and name 'Error' when the handshake timeout (JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
default 1500ms) elapses. BaseRequestor._shouldRetry classified retryable errors only
by .code/.name/.statusCode, so this error matched neither the ct nor rt bucket and was
never retried under the default rp=ct policy — the call failed immediately on the first
attempt, silently bypassing maxReconnects.

Match the handshake-timeout message in the ct bucket so a transient slow WS upgrade
(e.g. a serverless/edge backend cold start) is retried as intended. Adds tape coverage
for both the default-ct retry path and the rp=4xx no-retry path.
2026-09-13 11:49:40 +01:00
Ben Younes 1d1d16e221 fix: return tts:tokens-result when tts:tokens command is missing id (#1548) (#1577)
_lccTtsTokens silently returned on a missing `id`, leaving the WS client with
no feedback — no tts:tokens-result, no audio, only a server-side info log.
Mirror the existing missing-`tokens` handling and reply with status 'failed' /
reason 'missing id' so callers that omit `id` (e.g. the Python SDK) get an
actionable result.
2026-09-09 15:54:07 +01:00
83db903bc6 fix: fire sip:refer actionHook when the far end BYEs before any NOTIFY (#1258) (#1580)
* 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>
2026-09-09 15:14:51 +01:00
Rehan Sanjay VenkatesanandClaude Opus 5 a4e7afac83 fix(status): don't re-send pre-transfer call statuses after a transfer (#1584)
A call transferred between feature servers arrives at the receiving server as a
fresh INVITE, so the session runs through trying, ringing, early-media and
answered again as it accepts the REFER. The application was already told all of
that by the server that first handled the call, so it sees a second round of
events for a call it believes is long since answered.

Skip only the application-facing status callback for those four statuses when
the session is a transferred call. callInfo, the redis call record and
record-all-calls still run exactly as before, so the call record stays accurate
and answering a transferred call still starts recording.

Fixes #1392

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 14:43:32 +01:00
Hoan Luu HuuandClaude Opus 5 6cc3dc45f2 Fix/siprec survives fs transfer (#1586)
* 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>
2026-09-09 07:45:30 +01:00
RehuzandClaude Opus 5 f29aa0faef fix(tts): send stream_resumed to the same hook path as the other stream events (#1585)
_onTtsStreamingResume asked for "streaming-event" while stream_open,
stream_paused, stream_closed and user_interruption all ask for
"/streaming-event".

That one missing slash decides whether the event reaches an HTTP application at
all. HttpRequestor joins a hook path to the app's baseUrl only when it is
relative, and relative means starting with a slash:

  _isRelativeUrl(u) { return typeof u === 'string' && u.startsWith('/'); }
  const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url;

"streaming-event" is neither relative nor absolute, so it is passed through
unjoined and never lands on the application. The call site catches and logs the
failure rather than raising it, so an application simply sees stream_paused with
no matching stream_resumed and nothing anywhere says why.

Present since the verb was introduced in #994. Found by reading the file, so
there is no issue to close.

Tests: the two new cases fail against the unpatched line and pass with it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 11:32:26 +01:00
Dave HortonandClaude Opus 5 a6663198da ci: authenticate to AWS via GitHub OIDC using the role_arn credential path (#1582)
This repo is public and held a long-lived AWS access key as repository secrets
(set 2023-11-22). It is replaced with short-lived credentials from the GitHub OIDC
provider; no AWS key and no account id remain in the repo.

create-test-db.js wrote {access_key_id, secret_access_key, aws_region} into the test
database as the aws speech credential, which sends speech-utils' getAwsAuthToken down
its access-key branch and calls GetSessionToken -- rejected by AWS for session
credentials. The role_arn branch calls AssumeRole instead, which accepts them, and is
already plumbed through db-utils.js, call-session.js and stt-task.js. The pinned
speech-utils 0.2.30 already supports it, so no dependency change is needed.

Fork pull requests receive neither secrets nor an OIDC token, so the credentials step
is guarded by a condition; the AWS tests then skip for forks exactly as they do today.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 13:53:14 -04:00
Hoan Luu HuuandClaude Opus 5 db18b558ce add sip_reason_header to status Callback- #178 (#1579)
* feat: surface the SIP Reason header on call status events

Carriers fronting ISDN/E1 PRI trunks put the authoritative disconnect cause in
an RFC 3326 Reason header rather than in the SIP status line, e.g.

    SIP/2.0 408 Request Timeout
    Reason: Q.850 ;cause=18

Q.850 cause 18 is "no user responding" - nobody answered, not a platform fault.
Different causes also arrive under the same SIP status (503 with cause=38
network out of order, or cause=41 temporary failure), so the status code alone
cannot classify the outcome of the call.

drachtio relays the header intact and it is present on the response object, but
the feature server only read the status line from it, so the cause was lost at
the application boundary and never reached the call status webhook.

Rather than extract the header at each emit site, carry the SIP message that
caused the status change on the callStatusChange event and derive from it in one
place, so provisional responses, the 200, final failures, BYE and CANCEL are all
covered by the same code and future headers cost one line.

Note this partly overlaps the existing _extractCustomHeaders/sip_headers
passthrough: that already exposes a Reason header arriving on a BYE, but it only
runs on the hangup path, so nothing covered the outbound INVITE failure
responses where a Q.850 cause matters most.  sip_reason_header is a dedicated,
documented field that behaves the same on every status event.

sip_reason keeps meaning the status line phrase, and no key is added when there
is no Reason header, so existing consumers see an unchanged payload.

Adds a test:unit script so the smoke test runs without the docker testbed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: note that the Reason header arrives re-serialized, not verbatim

Verified live end-to-end against a cluster, capturing both the external leg and
the leg into the feature server:

    14.226.234.142 -> 10.0.197.31:5060   Reason: Q.850 ;cause=31   (as sent)
    10.0.197.31:5060 -> :5070            Reason: Q.850;cause=31    (to fs)

Proxying re-serializes the header, normalizing the optional whitespace RFC 3326
permits around ';'. The same normalization appears in customer captures from an
unrelated deployment, so this is drachtio behaviour, not cluster-specific.

sip_reason_header is therefore the header as this process received it, not the
carrier's exact bytes - worth stating outright, since the spacing inconsistency
is exactly what consumers ask about, and a consumer who string-matches on the
spaced form would silently never match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: clear a stale Reason header in redis, pass it on the alloc path, run the tests in CI

Three problems found reviewing the earlier commits.

1. The redis call record kept a stale header forever. updateCallStatus assigns
   unconditionally, but toJSON only emits truthy values, and the same object is
   written to the redis hash with hmset - a MERGE. An absent key therefore left
   the previous status change's header in place, readable via GET /Calls/:sid:
   a leg that got 183 + "Reason: Q.850;cause=31", then answered on a clean 200
   and completed on a plain BYE, reported a Q.850 temporary-failure cause for a
   call that ended normally. The webhooks were right; only the call record was
   wrong. Storing absence as '' overwrites it. The existing "does not linger"
   test passed throughout because it only exercised toJSON in memory, so this
   adds one that pins what survives the redis filter (verified: it fails
   without the fix).

2. The endpoint-allocation failure path already extracted the Reason header and
   relayed it to the SBC, but called _notifyCallStatusChange without it - so a
   FreeSWITCH 488 with "Reason: Q.850;cause=88 INCOMPATIBLE_DESTINATION" told
   the SBC the cause and the application nothing, which is exactly the case
   this feature exists to expose. That error is an fsmrf object rather than a
   SipMessage, so it cannot go through msg (the guard in
   reasonHeaderFromSipMessage would silently return undefined); the event now
   takes an explicit sipReasonHeader for callers holding the value already.

3. The test:unit script added with these tests was never wired into CI - the
   workflow runs jslint and npm test, and npm test enumerates its files
   explicitly and skips test/unit entirely, so the suite would have rotted
   unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: clear the stale header on the CallSession path too, and keep the CANCEL

Both from PR review.

1. The previous fix only worked for SingleDialer. Storing '' on the instance is
   enough for callers that write the instance itself, but CallSession writes
   toJSON(), and toJSON() drops falsy values on purpose so the key stays out of
   the webhook payload - so '' never reached hmset and the stale header
   survived. Reproduced:

       after 183, toJSON has: Q.850;cause=31
       raw instance value:    ""
       instance   -> redis has key: true    (SingleDialer, clears)
       toJSON()   -> redis has key: false   (CallSession, stale persists)

   The split is by session class, not call direction: place-outdial is required
   only by dial.js, so SingleDialer covers dial-verb child legs while inbound,
   REST-created and adulting all inherit CallSession._notifyCallStatusChange -
   including the REST outdial path this feature was written for.

   Adds CallInfo.toRedisJSON(), a named projection for the merge-semantics
   store, so the divergence from toJSON() lives in one documented place rather
   than being rediscovered at each call site. The unit test asserted against the
   raw instance, which is why it passed while the real path was broken; it now
   goes through both writers and fails if either regresses.

2. The caller-abandoned race dropped the CANCEL. middleware.js had it in hand
   and discarded it, so the constructor's _onCancel() passed nothing whenever
   the CANCEL beat the application fetch - the header landed or not depending on
   timing. Worth closing because the 487 and its 'Request Terminated' phrase are
   both ours, so every abandoned inbound call looks identical: a
   Reason: SIP;cause=200;text="Call completed elsewhere" is what separates a
   forked branch losing the race from a caller who gave up, and today both are
   just no-answer.

   Confirmed on a deployed srf (5.0.27) that this is not a no-op:
   copyUASHeaderToUACForOnlyCancel forwards a hardcoded ['Reason', 'X-Reason']
   when proxying a CANCEL, so the header does reach us.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: keep the Reason header out of the redis call record

Reversing the earlier approach after a closer look at who reads what.

The header was reaching the redis call record simply because CallInfo feeds two
sinks with opposite semantics: the status webhook is an EVENT (each POST is
independent, an absent key means "not in this event") while the redis record is
STATE written with hmset, a MERGE (an absent key means "keep what was there").
sipReasonHeader is the first field here that can legitimately go from set back
to unset - sipStatus and sipReason are only ever overwritten - so it was the
first to expose the mismatch, and two rounds of fixes had to chase it because
the two writers project from different bases.

Rather than keep managing that, exclude it: redis holds calls that are still
live, where there is usually no interesting cause yet, and by the time there is
one the call is over. Call history is served from RecentCalls (the CDR), which
is what the webapp reads - not GET /Calls. So the field earned very little there
while costing an invariant that has to be remembered forever.

Worth being explicit that this is NOT simply a revert: dropping the '' would
only have stopped the empty value being written. When the header is PRESENT it
still reached redis through both writers - toJSON includes it, and SingleDialer
writes the instance's own properties - and was then never cleared. Keeping it out
takes the same machinery as clearing it, just inverted, so this is a choice about
where the field belongs rather than a saving.

WEBHOOK_ONLY_FIELDS names that intent in one place, with the reasoning, so the
next field with the same property has somewhere obvious to go. The test asserts
both writers, since they project from different bases and checking one would
pass while the other still wrote the field (verified: it fails if the exclusion
is removed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: apply the redis projection at the boundary, and drop dead adulting plumbing

From PR review.

1. There was a THIRD call-record writer. Asking each call site to remember the
   projection was the wrong shape, and review found the proof: this class writes
   the record from the status change, the recording flag AND (private only)
   _persistConferenceState, and the last one still passed toJSON() straight
   through. A conferenced caller whose BYE carried Reason: Q.850;cause=16 would
   have that written into the call hash on the next conference-state persist,
   where hmset merges and nothing can ever clear it.

   Fixed by wrapping updateCallStatus once where it is bound, so every write is
   projected and a newly added writer cannot bypass it by forgetting to ask. The
   call sites go back to passing plain shapes. This is a class of miss that a
   unit test cannot catch - the contract test passed the whole time - so the fix
   is structural rather than another assertion.

2. The msg/byeReq parameters added to AdultingCallSession were dead code. The
   only path that reacts to the far-end BYE there is the inline
   sd.dlg.on('destroy') handler, which calls _callReleased() and discards the
   request; _hangup is reachable only from CallSession.hangup() (LCC), which
   passes nothing. The Reason header on that leg does reach the webhook, via
   SingleDialer's own destroy handler - so the plumbing was not just unused but
   misleading about which path carries it. Removed, with a comment at the handler
   recording where the event actually comes from so it does not get re-added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: correct the projection comment in SingleDialer

Copied verbatim from CallSession, where the list of writers (status change,
recording flag, conference state) is accurate. SingleDialer has one writer, so
the comment described code that is not there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 07:35:38 -04:00
Hoan Luu Huu f35b694877 update speech utils v1.0.10 (#1574) 2026-07-29 07:15:51 -04:00
Dave Horton 389c65e2d8 0.9.8 v0.9.8 2026-07-20 08:34:52 -04:00
Hoan Luu HuuandClaude Opus 4.8 cdc9070488 feat: support srtpEncryption on dial verb for sip URI targets (#1568)
* 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>
2026-07-20 07:23:48 -04:00
Joe Heung 9973670a6c feat: surface in-dialog hold/un-hold to the application (#1564)
Applications currently have no way to observe a mid-call hold/un-hold.
The feature-server handles the in-dialog re-INVITE internally (re-modifies
the endpoint, returns 200) but never notifies the app, so a transcription
or agent-assist app can't pause/resume when a caller is held or retrieved.

This detects the hold state transition on a re-INVITE and delivers it over
the existing sipRequestWithinDialogHook channel as a verb:hook, plus emits
a local 'hold'/'unhold' event so in-process tasks (e.g. transcribe) can react.

- call-session.js: ordinary calls — reuse isOnhold() (a=sendonly/a=inactive).
- siprec-call-session.js: SIPREC overrides _onReinvite, so a separate handler
  is required. SIPREC recording streams are sendonly by nature, so a stream
  flipping to a=inactive is treated as the hold signal; the full multipart
  body (SDP + rs-metadata) is always forwarded. Fires on transition only;
  other re-INVITEs are delivered as event:"reinvite".

Delivery uses requestor.request('verb:hook', ...) — the same channel
_handleRefer uses — rather than a task-bound performHook, so events are not
dropped if the current task has ended. Enable per call with:
  {"verb":"config","sipRequestWithinDialogHook":"/hold-events"}

Confirmed live against Cisco CUBE (Cisco-SIPGateway/IOS-17.12.7b) over SIPREC:
both recording streams flip sendonly<->inactive on hold/resume, each
transition emitted correctly.

The whole feature is opt-in behind the JAMBONES_HOLD_UNHOLD_EVENTS env var;
when it is unset, _notifyHoldState / _notifySipRecReinvite are no-ops (neither
the local event nor the hook fires).

Reinstates the feature previously reverted in #1563. The revert was due to the
unit test failing in the full suite: lib/config.js captures the env var once at
load time, and earlier suite files require config/session modules (flag unset)
before this test could set it, so the enabled-path assertions saw a disabled
feature. The test now force-reloads config + the session modules with the flag
set (and restores the module cache afterward), so it is robust to suite load order.
2026-07-14 20:22:34 -04:00
Sam Machin fa93259683 kill s2s sessions (#1566)
* kill s2s sessions

* undo
2026-07-14 09:18:25 -04:00
Dave Horton 5fe73b0051 Revert "feat: surface in-dialog hold/un-hold to the application (#1560)" (#1563)
This reverts commit edc6d6b208.
2026-07-09 09:44:06 -04:00
Joe Heung edc6d6b208 feat: surface in-dialog hold/un-hold to the application (#1560)
Applications currently have no way to observe a mid-call hold/un-hold.
The feature-server handles the in-dialog re-INVITE internally (re-modifies
the endpoint, returns 200) but never notifies the app, so a transcription
or agent-assist app can't pause/resume when a caller is held or retrieved.

This detects the hold state transition on a re-INVITE and delivers it over
the existing sipRequestWithinDialogHook channel as a verb:hook, plus emits
a local 'hold'/'unhold' event so in-process tasks (e.g. transcribe) can react.

- call-session.js: ordinary calls — reuse isOnhold() (a=sendonly/a=inactive).
- siprec-call-session.js: SIPREC overrides _onReinvite, so a separate handler
  is required. SIPREC recording streams are sendonly by nature, so a stream
  flipping to a=inactive is treated as the hold signal; the full multipart
  body (SDP + rs-metadata) is always forwarded. Fires on transition only;
  other re-INVITEs are delivered as event:"reinvite".

Delivery uses requestor.request('verb:hook', ...) — the same channel
_handleRefer uses — rather than a task-bound performHook, so events are not
dropped if the current task has ended. Enable per call with:
  {"verb":"config","sipRequestWithinDialogHook":"/hold-events"}

Confirmed live against Cisco CUBE (Cisco-SIPGateway/IOS-17.12.7b) over SIPREC:
both recording streams flip sendonly<->inactive on hold/resume, each
transition emitted correctly.

The whole feature is opt-in behind the JAMBONES_HOLD_UNHOLD_EVENTS env var;
when it is unset, _notifyHoldState / _notifySipRecReinvite are no-ops (neither
the local event nor the hook fires).
2026-07-09 09:29:19 -04:00
Dave HortonandClaude Opus 4.8 01827ff955 fix(deps): drop conflicting utf-8-validate/bufferutil optionalDependencies
The optionalDependencies pinned utf-8-validate ^6.0.3, but ws@8 only
accepts ^5.0.2 as its optional peer and ibm-watson's websocket dep
hard-requires ^5.0.2. That 5.x-vs-6.x split forced a dual-version tree
that npm 10 and npm 11 lay out differently, so a lockfile generated with
npm 11 (local) failed 'npm ci' under npm 10 (CI, node 20) with
'Missing: utf-8-validate@5.0.10 from lock file'. The 6.x pin never
accelerated ws anyway. Removing it collapses to a single utf-8-validate
5.0.10 that both npm majors resolve identically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.9.7
2026-07-09 08:06:44 -04:00
Dave Horton e17f559a08 bump version 2026-07-09 07:34:30 -04:00
Sam Machin 521a89e5f3 Update gather.js (#1557)
* Update gather.js

* use spread
2026-06-23 07:38:20 -04:00
Hoan Luu Huu 81601a290d update realtimedb-helper (#1555)
* update realtimedb-helper

* update realtimedb-helper
2026-06-15 21:24:29 -04:00
Hoan Luu Huu 44fbd91053 fixed listern verb terminated due to background task listen failure,teardown wrong handler as well (#1554) 2026-06-05 07:48:44 -04:00
Sam Machin d84690dfb1 add custom nye headers to completed hook (#1552) 2026-05-21 10:02:28 -04:00
Hoan Luu Huu c1a7f1146a sync google s2s from jambonz version 10 (#1551) 2026-05-20 20:50:15 -04:00
Hoan Luu Huu cb581f9be7 support recognizer.googleOptions.parentPath (#1546)
* support recognizer.googleOptions.parentPath

* update vẻb specification

* wip
2026-05-13 08:33:51 -04:00
Hoan Luu Huu 423c070900 fixed HTTP llm.toolHook does not support openAI (#1547) 2026-05-06 08:47:04 -07:00
Hoan Luu Huu 4b8fc65cdb support houndify ws (#1540) 0.9.7-rc1 v0.9.7-rc1 2026-04-23 07:19:39 -04:00
Hoan Luu Huu 570641fe07 update drachtio srf 5.0.21 (#1537) 2026-04-13 21:52:14 -04:00
Dave Horton 4f373d2fbc bump version v0.9.6 2026-03-31 07:43:27 -04:00
Sam MachinandDave Horton 24d9740618 add x-reason to sip-decline (#1518)
* add x-ver to sip-decline

* lint

* Update sip_decline.js

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
0.9.6
2026-03-29 16:19:15 -04:00
39746598b5 add null check for eventHook (#1534)
* 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>
2026-03-29 16:13:35 -04:00
Sam MachinandDave Horton 315eb98d86 add sp_sid to alerts (#1533)
* add sp_sid to alerts

* bump time-series

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
2026-03-29 16:07:08 -04:00
Dave Horton df30496dac fix uncaught exception referencing this.ep in freeswitch hangup scenario (#1532) 2026-03-27 08:31:32 -04:00
Sam Machin 5d6751782a Fix/hangup call (#1530)
* Update error.js

* Update error.js
2026-03-26 08:20:28 -04:00
rhondahollisandrhonda hollis 6147ec3f6a ensure sbcCallId is added to callInfo (#1529)
Co-authored-by: rhonda hollis <rhonda@jambonz.org>
2026-03-25 17:00:05 -04:00
Ed Robbins 18a13971ca respond to re-INVITE during race condition. (#1527) v0.9.6-rc6 2026-03-20 10:41:07 -04:00
Dave Horton 5bd1c53f7d fixed faulty commit https://github.com/jambonz/jambonz-feature-server/pull/1528 2026-03-20 09:17:00 -04:00
Anton Voylenko 5a759791f9 chore: bump node (#1528) 2026-03-20 08:57:16 -04:00
Sam Machin 1f5fa8d49e Update gather.js (#1526) 2026-03-19 15:29:03 -04:00
Anton Voylenko cf0b392c99 Append sip realm for rest dial (#1525)
* feat: append sip realm for rest dial

* fix: check account sip realm
2026-03-17 07:30:21 -04:00
Anton Voylenko 68339ced0b fix: conference mute and mute status (#1218) v0.9.6-rc5 2026-03-12 07:55:49 -04:00
Ed Robbins 1560efaf03 remove sensitive data from log statement (#1522) v0.9.6-rc4 2026-03-05 10:04:20 -05:00
Dave Horton 782ce8154e update drachtio-srf (#1521) 2026-03-05 09:33:59 -05:00
Dave Horton 0267acf9e1 anchor media on dial if we are recording (#1520) 2026-02-23 18:13:25 -05:00
rhondahollisandrhonda hollis 9090006703 update drachtio-srf to v5.0.19 (#1519)
Co-authored-by: rhonda hollis <rhonda@jambonz.org>
2026-02-20 16:19:28 -05:00
Hoan Luu HuuandDave Horton d7beaa1b7b Feat/dialogflow cx (#7) (#1516)
* wip

* wip

* wip

* wip

* logging

* wip

* wip

* wip

* update docker env to latest freeswitch

* wip

* lint

* wip

* support dialogflowcx

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
v0.9.6-rc3
2026-02-12 07:46:36 -05:00
Dave Horton 45d0ca87af update drachtio-srf (#1515) v0.9.6-rc2 2026-02-09 10:31:43 -05:00
Sam Machin bd435dfff9 add deep copy (#1511)
* escape tag data in listen

* deep copy call data for listen
2026-02-03 10:44:26 -05:00
Sam Machin b598cd94ae escape tag data in listen (#1510) 2026-02-03 07:35:15 -05:00
Matt HertogsandClaude Sonnet 4.5 ceb9a7a3bd Fix boostAudioSignal parameter in Update Call REST API (#1490)
Corrects the parameter passed to _lccBoostAudioSignal to use
opts.boostAudioSignal instead of the entire opts object, ensuring
the boostAudioSignal option works correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
v0.9.6-rc1
2026-01-29 13:58:14 -05:00