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
2023-12-11 08:37:36 -05:00
2020-01-07 10:34:03 -05:00
2021-08-30 17:12:00 -04:00
2025-06-28 15:01:09 -04:00
2025-08-21 14:09:20 -04:00
2026-03-20 08:57:16 -04:00
2024-06-28 09:05:05 -04:00

jambonz-feature-server CI

This application implements the core feature server of the jambones platform.

Note: If you are a developer looking to work on the code please read our how-to for that.

Configuration

Configuration is provided via environment variables:

variable meaning required?
AWS_ACCESS_KEY_ID aws access key id, used for TTS/STT as well SNS notifications no
AWS_REGION aws region no
AWS_SECRET_ACCESS_KEY aws secret access key, used per above no
AWS_SNS_TOPIC_ARN aws sns topic arn that scale-in lifecycle notifications will be published to no
DRACHTIO_HOST ip address of drachtio server (typically '127.0.0.1') yes
DRACHTIO_PORT listening port of drachtio server for control connections (typically 9022) yes
DRACHTIO_SECRET shared secret yes
ENABLE_METRICS if 1, metrics will be generated no
ENCRYPTION_SECRET secret for credential encryption(JWT_SECRET is deprecated) yes
GOOGLE_APPLICATION_CREDENTIALS path to gcp service key file yes
HTTP_PORT tcp port to listen on for API requests from jambonz-api-server yes
HTTP_IP IP Address for API requests from jambonz-api-server no
JAMBONES_GATHER_EARLY_HINTS_MATCH if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency no
JAMBONES_FREESWITCH IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$' yes
JAMBONES_HOLD_UNHOLD_EVENTS if set, surface in-dialog hold/un-hold: emits a 'hold'/'unhold' event and, when sipRequestWithinDialogHook is configured, delivers it to the application. Disabled when unset no
JAMBONES_LOGLEVEL log level for application, 'info' or 'debug' no
JAMBONES_MYSQL_HOST mysql host yes
JAMBONES_MYSQL_USER mysql username yes
JAMBONES_MYSQL_PASSWORD mysql password yes
JAMBONES_MYSQL_DATABASE mysql data yes
JAMBONES_MYSQL_CONNECTION_LIMIT mysql connection limit no
JAMBONES_NETWORK_CIDR CIDR of private network that feature server is running in (e.g. '172.31.0.0/16') yes
JAMBONES_REDIS_HOST redis host yes
JAMBONES_REDIS_PORT redis port yes
JAMBONES_SBCS list of IP addresses (on the internal network) of SBCs, comma-separated yes
STATS_HOST ip address of metrics host (usually '127.0.0.1' since telegraf is installed locally no
STATS_PORT listening port for metrics host no
STATS_PROTOCOL 'tcp' or 'udp' no
STATS_TELEGRAF if 1, metrics will be generated in telegraf format no
JAMBONZ_RECORD_WS_BASE_URL recording websocket URL to send the recording audio no
JAMBONZ_RECORD_WS_USERNAME recording websocket username no
JAMBONZ_RECORD_WS_PASSWORD recording websocket password no
ANCHOR_MEDIA_ALWAYS keep media on media server no
JAMBONZ_DISABLE_DIAL_PAI_HEADER control P-Asserted-Identity header on B-Leg no

running under pm2

Typically, this application runs under pm2 using an ecosystem.config.js file similar to this:

module.exports = {
  apps : [
  {
    name: 'jambonz-feature-server',
    cwd: '/home/admin/apps/jambonz-feature-server',
    script: 'app.js',
    instance_var: 'INSTANCE_ID',
    out_file: '/home/admin/.pm2/logs/jambonz-feature-server.log',
    err_file: '/home/admin/.pm2/logs/jambonz-feature-server.log',
    exec_mode: 'fork',
    instances: 1,
    autorestart: true,
    watch: false,
    max_memory_restart: '1G',
    env: {
      NODE_ENV: 'production',
      GOOGLE_APPLICATION_CREDENTIALS: '/home/admin/credentials/gcp.json',
      AWS_ACCESS_KEY_ID: 'XXXXXXXXXXXX',
      AWS_SECRET_ACCESS_KEY: 'YYYYYYYYYYYYYYYYYYYYY',
      AWS_REGION: 'us-west-1',
      ENABLE_METRICS: 1,
      STATS_HOST: '127.0.0.1',
      STATS_PORT: 8125,
      STATS_PROTOCOL: 'tcp',
      STATS_TELEGRAF: 1,
      AWS_SNS_TOPIC_ARN: 'arn:aws:sns:us-west-1:xxxxxxxxxxx:terraform-20201107200347128600000002',
      JAMBONES_NETWORK_CIDR: '172.31.0.0/16',
      JAMBONES_MYSQL_HOST: 'aurora-cluster-jambonz.cluster-yyyyyyyyyyy.us-west-1.rds.amazonaws.com',
      JAMBONES_MYSQL_USER: 'admin',
      JAMBONES_MYSQL_PASSWORD: 'foobarbz',
      JAMBONES_MYSQL_DATABASE: 'jambones',
      JAMBONES_MYSQL_CONNECTION_LIMIT: 10,
      JAMBONES_REDIS_HOST: 'jambonz.zzzzzzz.0001.usw1.cache.amazonaws.com',
      JAMBONES_REDIS_PORT: 6379,
      JAMBONES_LOGLEVEL: 'debug',
      HTTP_PORT: 3000,
      DRACHTIO_HOST: '127.0.0.1',
      DRACHTIO_PORT: 9022,
      DRACHTIO_SECRET: 'sharedsecret',
      JAMBONES_SBCS: '172.31.32.10',
      JAMBONES_FREESWITCH: '127.0.0.1:8021:sharedsecret'
    }
  }]
};

Running the test suite

Please see this.

S
Description
Core telephony feature server for the jambones platform
Readme MIT
10 MiB
Languages
JavaScript 99.9%