CX Unified Agent (Agent Desk) — Complete Architecture & Call Control Guide
Scope: This document covers the Unified Agent (Agent Desk) Angular application — how agents control voice calls, how SIP.js bridges to FreeSWITCH, how CIM messages flow to CCM, and the complete end-to-end call flows from the agent's perspective.
Related Documents:
- [CX Voice Recording + Cisco — Complete Architecture Guide] — Recording architecture, Cisco UCCE/UCCX, JTAPI correlation
- [CX Voice Connector & Outbound Dialer — Architecture Guide] — Voice Connector middleware, Outbound Dialer campaigns, FreeSWITCH integration
---
Table of Contents
---
1. Executive Summary
The Unified Agent (also called Agent Desk) is the Angular-based agent workstation that handles all customer interactions — chat, email, and voice. For voice specifically, the Agent Desk uses SIP.js over WebRTC to register agents with FreeSWITCH and perform call control actions.
1.1 Key Components
|
Component |
Technology |
Role |
|---|---|---|
|
Agent Desk |
Angular 14+ |
UI for agents: accept calls, hold, transfer, consult, conference, manual outbound |
|
SIP.js Wrapper |
SIP.js + custom wrapper ( |
Bridges Angular postMessages to SIP.js INVITE, REFER, BYE |
|
FreeSWITCH |
FreeSWITCH + Lua scripts |
Media server, dialplan routing, call bridging |
|
CCM |
Spring Boot (Conversation Manager) |
Receives CIM messages, manages conversations, routes to MRE |
|
MRE |
Java (Media Routing Engine) |
Agent selection, task routing, queue management |
1.2 What This Document Covers
-
Every call control action an agent can perform and its backend effects
-
How SIP.js commands translate to FreeSWITCH dialplan triggers
-
Complete CIM message structures for voice operations
-
Manual outbound call flow (agent-initiated)
-
Campaign call flow (dialer-initiated, agent receives)
-
End-to-end flows: inbound, outbound, consult, transfer, conference
-
How CCM processes voice events and maintains conversation state
1.3 What This Document Does NOT Cover
-
Voice Connector architecture and ESL commands → See CX Voice Connector & Outbound Dialer — Architecture Guide
-
Outbound Dialer campaign management and contact lifecycle → See CX Voice Connector & Outbound Dialer — Architecture Guide
-
Recording architecture and media server → See CX Voice Recording + Cisco — Complete Architecture Guide
-
Cisco UCCE/UCCX integration → See CX Voice Recording + Cisco — Complete Architecture Guide
---
This section provides the definitive reference for how the Unified Agent (Agent Desk) Angular application interacts with the CX Voice stack. It covers every call control action an agent can perform, how each action triggers FreeSWITCH, how CIM messages are constructed and sent to CCM, and the complete manual outbound call flow.
---
2. Unified Agent Architecture Overview
2.1 The Full Stack
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ UNIFIED AGENT — COMPLETE VOICE STACK │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ AGENT WORKSTATION (Browser) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ UNIFIED AGENT (Angular Application) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ │ │ │
│ │ │ │ Chat/Email │ │ Voice Toolbar │ │ Manual Outbound Dialog │ │ │ │
│ │ │ │ (WebSocket) │ │ (SIP.js) │ │ (Customer Lookup) │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ • Conversations │ │ • Accept/Hold │ │ • Phone number input │ │ │ │
│ │ │ │ • Activities │ │ • Transfer │ │ • Prefix handling │ │ │ │
│ │ │ │ • Wrap-up │ │ • Consult │ │ • Customer search │ │ │ │
│ │ │ │ • CRM screen │ │ • Conference │ │ • Make call button │ │ │ │
│ │ │ └─────────────────┘ └─────────────────┘ └─────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Key Services: │ │ │
│ │ │ • sip.service.ts — All SIP call control, CIM creation, event handling │ │ │
│ │ │ • http.service.ts — REST API calls to CCM, Routing Engine │ │ │
│ │ │ • socket.service.ts — WebSocket for real-time events │ │ │
│ │ │ • cache.service.ts — Agent data, conversations │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ postMessages(command) │ │
│ │ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ WEBRTC PHONE / SIP.js WRAPPER │ │ │
│ │ │ (webrtc-phone/js/wrapper.js) │ │ │
│ │ │ │ │ │
│ │ │ • Receives commands via postMessages() from Angular │ │ │
│ │ │ • Translates to SIP.js: Inviter, Referrer, Session │ │ │
│ │ │ • Manages WebRTC media streams (getUserMedia) │ │ │
│ │ │ • Finesse XMPP integration (BOSH) for agent state │ │ │
│ │ │ • Sends events back to Angular via clientCallback │ │ │
│ │ │ │ │ │
│ │ │ Commands handled: │ │ │
│ │ │ login | logout | makeOBCall | answerCall | releaseCall | holdCall │ │ │
│ │ │ retrieveCall | mute_call | unmute_call | SST | SST_Queue | makeConsult │ │ │
│ │ │ makeConsultQueue | consultTransfer | conference_consult | SendDtmf │ │ │
│ │ │ silentMonitor | bargeIn | convertCall │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ SIP over WebSocket (WSS) │ │
│ │ │ │ │
│ └────────────────────────────────────┼────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────┼────────────────────────────────────────────────┐ │
│ │ FREESWITCH (Media Server) │ │
│ │ │ │ │
│ │ ┌─────────────────────────────────┼─────────────────────────────────────────────┐ │ │
│ │ │ SIP Profile │ ESL (Event Socket Library) │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ ▼ │ ▼ │ │ │
│ │ │ [SIP Stack] │ [Dialplan] │ [Voice Connector] │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ ▼ ▼ ▼ │ ▼ │ │ │
│ │ │ [vcApi.lua] [consult_conf.lua] │ [Java: VcService] │ │ │
│ │ │ [customTransfer.lua] │ │ │ │ │
│ │ │ [cxIvr.lua] [outboundIvr.lua] │ │ HTTP POST /request-agent │ │ │
│ │ │ [eavesdrop_custom.lua] │ │ HTTP POST /cancel-agent │ │ │
│ │ │ [barge.lua] │ │ HTTP POST /ccm-msg/receive │ │ │
│ │ │ │ │ │ │ │
│ │ │ ESL Commands: │ │ ESL Commands: │ │ │
│ │ │ uuid_transfer, uuid_bridge │ │ uuid_transfer, uuid_setvar_multi │ │ │
│ │ │ uuid_broadcast, uuid_kill │ │ uuid_broadcast, sched_hangup │ │ │
│ │ │ conference, record_session │ │ │ │ │
│ │ └─────────────────────────────────┴─────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ HTTP REST │ CIM Messages │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ CCM (Conversation Manager) │ │ │
│ │ │ │ │ │
│ │ │ • Receives CIM: CALL_LEG_STARTED, CALL_LEG_ENDED, CALL_HOLD, CALL_RESUME │ │ │
│ │ │ • Creates/updates conversations │ │ │
│ │ │ • Assigns tasks to Routing Engine │ │ │
│ │ │ • Stores activities in MongoDB │ │ │
│ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
2.2 Two Call Paths
|
Path |
Technology |
Use Case |
Recording |
|---|---|---|---|
|
Cisco-Integrated |
Finesse Desktop (Gadget) + Cisco Phone (BiB) |
Enterprise Cisco UCCE/UCCX deployments |
Cisco BiB → Media Server → Mixer |
|
Native EFCX |
WebRTC/SIP.js in browser |
SaaS, non-Cisco, remote agents |
Media Server inline recording |
The Unified Agent supports both paths simultaneously. The agent logs in via SIP.js wrapper, which registers with FreeSWITCH. For Cisco-integrated deployments, the wrapper also connects to Finesse via XMPP/BOSH for agent state synchronization.
---
3. SIP.js Wrapper & Command Flow
3.1 The postMessages Bridge
The Unified Agent and the SIP.js wrapper communicate via postMessages, a browser window.postMessage API. This decouples the Angular application from the phone implementation.
// From sip.service.ts — any call control action
let command = {
action: "holdCall",
parameter: {
dialogId: "abc-123",
clientCallbackFunction: this.clientCallback
}
};
postMessages(command); // Sends to webrtc-phone iframe/window
// From webrtc-phone/js/wrapper.js — receives commands
window.addEventListener('message', (event) => {
const { action, parameter } = event.data;
switch(action) {
case 'holdCall':
session.invite({ hold: true }); // SIP.js re-INVITE with SDP hold
break;
case 'SST':
session.refer(parameter.numberToTransfer); // SIP REFER
break;
case 'makeConsult':
new Inviter(ua, URI.parse(parameter.numberToConsult)).invite();
break;
// ... etc
}
});
3.2 Complete Command Reference
|
Command |
Sent By |
Wrapper Action |
SIP.js Equivalent |
FreeSWITCH Result |
|---|---|---|---|---|
|
|
|
Registers SIP UA |
|
SIP REGISTER → 200 OK |
|
|
|
Unregisters SIP UA |
|
SIP REGISTER expires |
|
|
|
Invites target |
|
SIP INVITE → originate through gateway |
|
|
|
Accepts invitation |
|
SIP 200 OK → bridge |
|
|
|
Terminates session |
|
SIP BYE → hangup |
|
|
|
Puts on hold |
|
re-INVITE with sendonly |
|
|
|
Resumes |
|
re-INVITE with sendrecv |
|
|
|
Mutes audio |
|
No SIP message |
|
|
|
Unmutes audio |
|
No SIP message |
|
|
|
Blind transfer |
|
SIP REFER → transfer |
|
|
|
Queue transfer |
|
SIP REFER → vcApi.lua |
|
|
|
New consult call |
|
New dialog, customer auto-held |
|
|
|
Queue consult |
|
New dialog to queue |
|
|
|
Complete transfer |
DTMF |
consult_conf.lua CONSULT_TRANSFER |
|
|
|
3-way conference |
DTMF |
consult_conf.lua CONSULT_CONFERENCE |
|
|
|
Sends digits |
|
SIP INFO with DTMF payload |
|
|
|
Eavesdrop |
Custom SIP header |
eavesdrop_custom.lua |
|
|
|
Join call |
Custom SIP header |
barge.lua |
|
|
|
Video on/off |
|
re-INVITE with video SDP |
---
4. Call Control Actions — Complete Deep Dive
4.1 Accept / End / Hold / Resume / Mute
AGENT ANSWERS INCOMING CALL
═══════════════════════════════════════════════════════════════════════════════
[Unified Agent]
│
│ Agent clicks "Accept"
▼
[sip.service.ts — acceptCallOnSip(dialogId, "audio")]
│
│ command = { action: "answerCall", parameter: { dialogId, answerCalltype: "audio" } }
▼
[postMessages(command)]
│
▼
[webrtc-phone wrapper]
│
│ SIP.js: invitation.accept({ media: { constraints: { audio: true } } })
▼
[FreeSWITCH]
│
│ 1. Receives SIP 200 OK
│ 2. Bridges customer channel to agent channel
│ 3. Fires CHANNEL_BRIDGE event
│ 4. Recording continues (if started at ring)
▼
[SIP.js sends event back to Angular]
│
│ event: { event: "dialogState", response: { dialog: { state: "ACTIVE" } } }
▼
[sip.service.ts — handleDialogStateEventFromSip()]
│
│ handleActiveDialogStateEvent()
│ → handleCallActiveEvent()
│ → createCIMMessage(intent: "CALL_LEG_STARTED")
│ → ccmChannelSessionApi()
▼
[CCM]
│
│ Creates VOICE activity on conversation timeline
```
AGENT PUTS CALL ON HOLD
═══════════════════════════════════════════════════════════════════════════════
[Unified Agent]
│
│ Agent clicks "Hold"
▼
[sip.service.ts — holdCallOnSip(dialogId)]
│
│ setTimeout(1550ms) // Debounce to prevent rapid toggle
│ command = { action: "holdCall", parameter: { dialogId } }
▼
[postMessages(command)]
▼
[webrtc-phone wrapper]
│
│ SIP.js: session.invite({ hold: true })
│ Sends re-INVITE with SDP: a=sendonly
▼
[FreeSWITCH]
│
│ Puts agent channel on hold (moh plays)
│ Customer hears hold music
▼
[Finesse / SIP.js event]
│
│ dialog.state = "HELD"
▼
[sip.service.ts — handleHeldDialogStateEvent()]
│
│ handleCallHoldEvent(dialog, "INBOUND")
│ → createCIMMessage(intent: "CALL_HOLD")
│ → ccmChannelSessionApi()
▼
[CCM]
│
│ Adds CALL_HOLD activity to conversation
```
AGENT RESUMES CALL
═══════════════════════════════════════════════════════════════════════════════
[sip.service.ts — resumeCallOnSip(dialogId)]
│
│ command = { action: "retrieveCall", parameter: { dialogId } }
▼
[webrtc-phone wrapper]
│
│ SIP.js: session.invite({ hold: false })
│ Sends re-INVITE with SDP: a=sendrecv
▼
[FreeSWITCH]
│
│ Removes hold, bridges audio
▼
[sip.service.ts — handleActiveDialogStateEvent()]
│
│ handleCallResumeEvent(dialog, "INBOUND")
│ → createCIMMessage(intent: "CALL_RESUME")
│ → ccmChannelSessionApi()
▼
[CCM]
│
│ Adds CALL_RESUME activity
```
4.2 Blind Transfer (Agent or Queue)
BLIND TRANSFER TO ANOTHER AGENT
═══════════════════════════════════════════════════════════════════════════════
[Unified Agent]
│
│ Agent clicks "Transfer" → selects target agent → clicks "Transfer"
▼
[sip.service.ts — directAgentTransferOnSip(["1006"])]
│
│ command = {
│ action: "SST",
│ parameter: {
│ dialogId: activeDialog.id,
│ numberToTransfer: "1006",
│ clientCallbackFunction
│ }
│ }
▼
[postMessages(command)]
▼
[webrtc-phone wrapper]
│
│ SIP.js: session.refer("1006")
│ Sends SIP REFER to FreeSWITCH
▼
[FreeSWITCH]
│
│ 1. Receives REFER with Refer-To: 1006
│ 2. Checks dialplan for extension 1006
│ 3. Matches agent extension dialplan
│ 4. Rings agent 1006
│ 5. When 1006 answers, bridges customer to 1006
│ 6. Sends NOTIFY to original agent (transfer complete)
▼
[SIP.js event to original agent]
│
│ dialog.state = "DROPPED"
│ dialog.callEndReason = "DIRECT_TRANSFERED"
▼
[sip.service.ts — handleDialogDroppedEvent()]
│
│ callType = "DIRECT_TRANSFER"
│ createCIMMessage(intent: "CALL_LEG_ENDED", reasonCode: "DIRECT_TRANSFER")
│ → ccmChannelSessionApi()
▼
[CCM]
│
│ Ends original agent's leg
│ New agent gets new CALL_LEG_STARTED
```
QUEUE TRANSFER (Blind Transfer to Queue)
═══════════════════════════════════════════════════════════════════════════════
[sip.service.ts — directQueueTransferOnSip({ queueId: "support-queue" })]
│
│ command = {
│ action: "SST_Queue",
│ parameter: {
│ dialogId: activeDialog.id,
│ queue: "support-queue",
│ queueType: "ID",
│ numberToTransfer: "99887766" // staticQueueTransferDn prefix
│ }
│ }
▼
[postMessages(command)]
▼
[webrtc-phone wrapper]
│
│ SIP.js: session.refer("99887766-support-queue")
▼
[FreeSWITCH Dialplan]
│
│ Matches: ^99887766-(.*)$
│ → lua vcApi.lua directTransfer
▼
[vcApi.lua]
│
│ 1. Extracts queue from destination_number split
│ 2. Sets sip_h_X-queue = "support-queue"
│ 3. Sets sip_h_X-queueType = "ID"
│ 4. Sets api_hangup_hook = "lua cx_hangup.lua"
│ 5. Parks customer
│ 6. POST /request-agent to Voice Connector
│ 7. Plays hold music
▼
[Voice Connector — VcService]
│
│ Sends ASSIGN_RESOURCE_REQUESTED CIM to CCM
▼
[CCM → Routing Engine]
│
│ Finds available agent in support-queue
▼
[Agent Reserved]
│
│ CCM → Voice Connector → FreeSWITCH uuid_transfer to agent
```
4.3 Consult (Agent or Queue)
CONSULT TO ANOTHER AGENT
═══════════════════════════════════════════════════════════════════════════════
[Unified Agent]
│
│ Agent on call with customer (A1-C1)
│ Agent clicks "Consult" → selects agent 1006 → clicks "Consult"
▼
[sip.service.ts — agentConsultOnSip(["1006"])]
│
│ 1. Validates: no existing consult call, customer still on call
│ 2. command = {
│ action: "makeConsult",
│ parameter: {
│ dialogId: activeDialog.id,
│ numberToConsult: "1006"
│ }
│ }
▼
[postMessages(command)]
▼
[webrtc-phone wrapper]
│
│ SIP.js: new Inviter(ua, URI.parse("1006")).invite()
│ Creates NEW SIP dialog (A1-A2 consult call)
▼
[FreeSWITCH]
│
│ 1. New channel for A1-A2 call
│ 2. A2 phone rings
│ 3. A1's original call with customer is AUTOMATICALLY put on hold
│ (SIP.js wrapper handles this)
▼
[Consult call events]
│
│ SIP.js → Angular: consultCall event
│ dialog.state = "INITIATING" → "INITIATED" → "ALERTING" → "ACTIVE"
▼
[sip.service.ts — handleConsultCallEvent()]
│
│ handleConsultInitiatorFlow():
│ INITIATING → show "Consulting..." notification
│ ACTIVE → customer call auto-held, consult call active
│
│ handleConsultedUserFlow() (for A2):
│ ALERTING → show popup, identify caller as consult
│ ACTIVE → A2 answers, now talking to A1 privately
▼
[CIM for consult leg]
│
│ handleActiveConsultCallEvent()
│ → createCIMMessage(intent: "CALL_LEG_STARTED", callType: "CONSULT")
│ → ccmChannelSessionApi()
```
CONSULT → TRANSFER (Complete the Transfer)
═══════════════════════════════════════════════════════════════════════════════
[Unified Agent]
│
│ A1 is consulting with A2. Customer is on hold.
│ A1 decides to transfer customer to A2.
│ A1 presses DTMF *A (or clicks "Transfer" in UI)
▼
[sip.service.ts — consultTransfer()]
│
│ command = { action: "consultTransfer", parameter: { clientCallbackFunction } }
▼
[webrtc-phone wrapper]
│
│ Sends DTMF *A on the A1-A2 consult call
▼
[FreeSWITCH — consult_conf.lua]
│
│ DTMF binding: bind_meta_app A a s1 lua::consult_conf.lua CONSULT_TRANSFER
│
│ 1. Gets A1 extension, A2 extension, customer number C1
│ 2. Discovers channel UUIDs from FreeSWITCH channels DB
│ 3. Sets hangup_after_bridge=false, park_after_bridge=true
│ 4. Bridges C1 channel to A2 channel: uuid_bridge(c1c1uuid, a2a2uuid)
│ 5. Kills A1's channels: uuid_kill(a1a2uuid, ATTENDED_TRANSFER)
│ 6. Schedules: uuid_kill(a1a1uuid, ATTENDED_TRANSFER) after 2s
│ 7. Sets A2 channel vars: sip_h_X-CALL-ID, sip_h_X-Destination-Number
│ 8. Sends message to A2: "CONSULT_TRANSFER" with customer info
│ 9. Starts new recording for A2 leg
▼
[A1's SIP.js]
│
│ dialog.state = "DROPPED"
│ callEndReason = "ATTENDED_TRANSFER"
▼
[sip.service.ts — handleDialogDroppedEvent()]
│
│ callType = "CONSULT_TRANSFER"
│ createCIMMessage(intent: "CALL_LEG_ENDED")
▼
[A2's SIP.js]
│
│ New dialog with customer becomes ACTIVE
│ dialog.callType = "CONSULT_TRANSFER"
▼
[sip.service.ts — handleActiveConsultTransfer()]
│
│ identifyCustomer() with customerNumber
│ → createCIMMessage(intent: "CALL_LEG_STARTED", callType: "CONSULT_TRANSFER")
```
CONSULT → CONFERENCE (3-Way Call)
═══════════════════════════════════════════════════════════════════════════════
[Unified Agent]
│
│ A1 consulting with A2. Customer on hold.
│ A1 presses DTMF *C (or clicks "Conference")
▼
[sip.service.ts — consultConference()]
│
│ command = { action: "conference_consult", parameter: { dialogId: consultDialog.id } }
▼
[webrtc-phone wrapper]
│
│ Sends DTMF *C on A1-A2 consult call
▼
[FreeSWITCH — consult_conf.lua]
│
│ DTMF binding: bind_meta_app C a s1 lua::consult_conf.lua CONSULT_CONFERENCE
│
│ 1. Discovers all channel UUIDs (A1, A2, C1)
│ 2. Checks if customer call is already a conference
│ • If YES and members > 3 → sendError("LIMIT_REACHED")
│ • If NO → create new conference
│ 3. Creates conference name: {a1c1uuid}-conf
│ 4. Transfers A1-A2 channels into conference: uuid_transfer(a1a2uuid, -both, custom_conf_{name})
│ 5. Transfers C1 channel into conference: uuid_transfer(c1c1uuid, custom_conf_{name})
│ 6. Transfers A1's original channel into conference: uuid_transfer(a1a1uuid, custom_conf_{name})
│ 7. Schedules kill of excess A1-A2 channel: uuid_kill(a1a2uuid, PRE_EMPTED)
│ 8. Updates all participants via send_message.lua
│ 9. Starts new recordings for A1 and A2 legs
▼
[All participants]
│
│ Receive "CONFERENCE" event with conference object
│ dialog.callType = "CONSULT_CONFERENCE"
│ dialog.participants = [A1, A2, C1]
▼
[sip.service.ts — handleConferenceCasesForActiveDialogState()]
│
│ isConferenceCall = true
│ _conferenceParticipantList.next(participants)
│ handleCallActiveEvent() with callType = "CONSULT_CONFERENCE"
```
CONSULT → CANCEL (A2 Hangs Up, Back to Customer)
═══════════════════════════════════════════════════════════════════════════════
[A2 hangs up consult call]
│
▼
[FreeSWITCH]
│
│ A1-A2 call ends
▼
[SIP.js → A1]
│
│ consultDialog.state = "DROPPED"
│ callEndReason = "Canceled" or "NORMAL_CLEARING"
▼
[sip.service.ts — handleConsultInitiatorFlowDroppedState()]
│
│ _consultCallSubActive.next("dropped")
│ consultDialog = undefined
│
│ IF customer call was on hold:
│ resumeCallOnSip(activeDialog.id)
│ → createCIMMessage(intent: "CALL_RESUME")
▼
[A1 resumes with customer]
```
4.4 External Transfer & External Consult
EXTERNAL TRANSFER (Transfer to PSTN Number)
═══════════════════════════════════════════════════════════════════════════════
[sip.service.ts — directTransferOnSipExternalNumbers("+18005551234")]
│
│ command = {
│ action: "SST",
│ parameter: {
│ dialogId: activeDialog.id,
│ numberToTransfer: "99887755-+18005551234" // staticExternalDn + number
│ }
│ }
▼
[postMessages(command)]
▼
[webrtc-phone wrapper]
│
│ SIP.js: session.refer("99887755-+18005551234")
▼
[FreeSWITCH Dialplan]
│
│ Matches: ^99887755-(.*)$
│ → lua customTransfer.lua
▼
[customTransfer.lua]
│
│ 1. Splits destination: prefix + actual number
│ 2. Sets direction = "DIRECT_TRANSFER"
│ 3. Sets sip_h_X-queueType = "", sip_h_X-queue = ""
│ 4. session:transfer(actualNumber, "XML", domain_name)
│ 5. Call bridges to external gateway
▼
[Original agent]
│
│ dialog.callEndReason = "EXTERNAL_DIRECT_TRANSFERED"
│ → CALL_LEG_ENDED CIM
```
EXTERNAL CONSULT (Consult with PSTN Number)
═══════════════════════════════════════════════════════════════════════════════
[sip.service.ts — consultOnSipExternalNumbers("+18005551234")]
│
│ command = {
│ action: "makeConsult",
│ parameter: {
│ dialogId: activeDialog.id,
│ numberToConsult: "99887755-+18005551234"
│ }
│ }
▼
[webrtc-phone wrapper]
│
│ SIP.js: new Inviter(ua, URI.parse("99887755-+18005551234")).invite()
▼
[FreeSWITCH]
│
│ Matches dialplan for external consult
│ Sets sip_h_X-CallType = "EXTERNAL-CONSULT"
│ → customTransfer.lua
│ → bind_meta_app for A and C
│ → When completed: consult_conf.lua with EXTERNAL_ prefix
```
4.5 Silent Monitor & Barge In
SILENT MONITOR (Supervisor Listens Without Agent Knowing)
═══════════════════════════════════════════════════════════════════════════════
[Supervisor Unified Agent]
│
│ Supervisor selects agent → clicks "Silent Monitor"
▼
[sip.service.ts — silentMoniter(agentExtension, customer, serviceIdentifier)]
│
│ 1. makeCXVoiceMrdNotReady() — sets voice MRD to NOT_READY
│ 2. command = {
│ action: "silentMonitor",
│ parameter: {
│ calledNumber: agentExtension,
│ Destination_Number: agentExtension,
│ service_Identifier: serviceIdentifier
│ }
│ }
▼
[postMessages(command)]
▼
[webrtc-phone wrapper]
│
│ Custom SIP INVITE with silent monitor headers
▼
[FreeSWITCH — eavesdrop_custom.lua]
│
│ 1. Discovers agent's active channel UUID
│ 2. Uses eavesdrop application to listen to agent-customer call
│ 3. Supervisor hears both sides but cannot speak
│ 4. DTMF *B can trigger barge-in
▼
[Supervisor SIP.js]
│
│ dialog.callType = "MONITORING"
│ dialog.state = "ACTIVE"
│ isSipCallMonitored = true
▼
[sip.service.ts]
│
│ createCIMMessage(intent: "CALL_LEG_STARTED", callType: "SILENT_MONITOR")
```
BARGE IN (Supervisor Joins as 3rd Party)
═══════════════════════════════════════════════════════════════════════════════
[Supervisor]
│
│ During silent monitor, presses DTMF *B (or clicks "Barge In")
▼
[sip.service.ts — bargeIn(dialogId)]
│
│ command = { action: "bargeIn", parameter: { dialogId } }
▼
[postMessages(command)]
▼
[webrtc-phone wrapper]
│
│ Custom SIP action
▼
[FreeSWITCH — barge.lua]
│
│ 1. Converts eavesdrop to full conference
│ 2. Supervisor can now speak
│ 3. All 3 parties in conference
▼
[All participants]
│
│ dialog.callType = "BARGE_CONFERENCE"
│ dialog.participants = [Agent, Customer, Supervisor]
```
---
5. Consult Transfer & Conference — FreeSWITCH Deep Dive
5.1 consult_conf.lua — The Heart of Consult Logic
The consult_conf.lua script is triggered by DTMF (A for transfer, C for conference) during a consult call. It manipulates FreeSWITCH channels directly.
Key Steps:
-
Channel Discovery: Queries FreeSWITCH's internal SQLite database (
channelstable) to find UUIDs: -
Conference Member Limit: Maximum 4 unique members per conference. If exceeded, sends
LIMIT_REACHEDerror. -
Recording Management:
-
On CONSULT_TRANSFER: stops recording for A1, starts new recording for A2
-
On CONSULT_CONFERENCE: starts new recordings for all conference members
-
Filename format:
{dialogId}:{agent}:{customer}:{startTime}.wav -
DTMF Bindings (set during consult initiation):
Variables Passed via SIP Headers:
|
Variable |
Set By |
Used By |
Purpose |
|---|---|---|---|
|
|
Wrapper |
consult_conf.lua |
Identifies customer |
|
|
Wrapper |
consult_conf.lua |
Dialog ID correlation |
|
|
vcApi.lua |
consult_conf.lua |
Service identifier |
|
|
customTransfer.lua |
consult_conf.lua |
EXTERNAL-CONSULT flag |
|
|
consult_conf.lua |
consult_conf.lua |
Conference room name |
5.2 Channel UUID Naming Convention in consult_conf.lua
|
Variable |
Meaning |
Example |
|---|---|---|
|
|
A1's channel in the A1-A2 consult call |
|
|
|
A1's channel in the A1-C1 customer call |
|
|
|
A1's other channel (same as a1c1uuid or separate) |
|
|
|
A2's channel in the A1-A2 consult call |
|
|
|
Customer's channel |
|
---
6. Manual Outbound Call — Complete Flow
6.1 Step-by-Step Flow
STEP 1: AGENT OPENS DIALOG
═══════════════════════════════════════════════════════════════════════════════
[Unified Agent]
│
│ Agent clicks "Call" button in toolbar or customer card
▼
[ManualOutboundCallComponent]
│
│ Opens dialog with phone input field
│ defaultPrefixOutbound = tenant-configured prefix (e.g., "+1")
▼
STEP 2: AGENT ENTERS NUMBER
═══════════════════════════════════════════════════════════════════════════════
[ManualOutboundCallComponent — formatePhoneNumber()]
│
│ Input processing:
│ • "00" prefix → converted to "+"
│ • Leading zeros removed, default prefix applied
│ • Example: "0018005550199" → "+18005550199"
│ • Example: "5551234" → "+15551234" (with +1 prefix)
▼
STEP 3: CUSTOMER LOOKUP
═══════════════════════════════════════════════════════════════════════════════
[ManualOutboundCallComponent — getCustomerByVoiceIdentifier()]
│
│ GET /customer-by-channel/CX_VOICE/{number}
│
│ If customer found:
│ → sipService.makeCallOnSip(customer, number)
│ If not found:
│ → Error snackbar: "Customer not found"
▼
STEP 4: MAKE CALL INITIATED
═══════════════════════════════════════════════════════════════════════════════
[sip.service.ts — makeCallOnSip(customer, number)]
│
│ 1. isOBCallRequested = true
│ 2. makeCXVoiceMrdNotReady(customer)
│ → socket.emit("changeAgentState", { action: "agentMRDState", state: "NOT_READY", mrdId })
│ → Agent state becomes NOT_READY (prevents new inbound calls)
│
│ 3. Wait 700ms for MRD state change
│
│ 4. Check voice MRD state:
│ IF state == "NOT_READY":
│ → getDefaultOutBoundChannel() // See Step 5
│ ELSE:
│ → Error: "OB-Call-Request" (MRD didn't change)
│ → resetMrdStateForManualOB()
▼
STEP 5: GET DEFAULT OUTBOUND CHANNEL
═══════════════════════════════════════════════════════════════════════════════
[sip.service.ts — getDefaultOutBoundChannel()]
│
│ GET /default-outbound-channel/{voiceChannelId}
│ (voiceChannelId = CX_VOICE channel type ID)
│
│ Response: { serviceIdentifier: "18005550199", ... }
│
│ This serviceIdentifier is the tenant's configured outbound DID,
│ NOT the number being dialed. It tells CCM "this outbound call
│ belongs to the Sales outbound channel" for routing/reporting.
▼
STEP 6: SEND SIP COMMAND
═══════════════════════════════════════════════════════════════════════════════
[sip.service.ts]
│
│ command = {
│ action: "makeOBCall",
│ parameter: {
│ callType: "audio",
│ calledNumber: number,
│ Destination_Number: serviceIdentifier // from Step 5
│ }
│ }
│
│ data = { action: "makeOBCall", command }
│ getDefaultOutBoundChannel(..., data) // passes command through
▼
[postMessages(command)]
▼
[webrtc-phone wrapper]
│
│ 1. Creates new SIP Inviter: new Inviter(ua, URI.parse(calledNumber))
│ 2. Sets custom headers: X-Destination-Number = serviceIdentifier
│ 3. inviter.invite()
▼
[FreeSWITCH]
│
│ 1. Receives SIP INVITE from agent
│ 2. Matches outbound dialplan
│ 3. Originates call through configured gateway
│ 4. Gateway calls the PSTN number
│ 5. When customer answers: bridges agent and customer
▼
STEP 7: CALL STATE EVENTS
═══════════════════════════════════════════════════════════════════════════════
[SIP.js → Angular events]
│
│ INITIATING: dialog created, ringing customer
│ → handleOutboundDialingEvent()
│ → identifyCustomer(number, "OUTBOUND")
│
│ ACTIVE: customer answered
│ → handleOutboundDialingEvent()
│ → handleCallActiveEvent()
│ → createCIMMessage(intent: "CALL_LEG_STARTED", callType: "OUTBOUND")
│ → ccmChannelSessionApi()
│
│ DROPPED: call ended
│ → handleCallDroppedEvent()
│ → createCIMMessage(intent: "CALL_LEG_ENDED")
▼
STEP 8: CONVERSATION CREATED IN CCM
═══════════════════════════════════════════════════════════════════════════════
[CCM receives CALL_LEG_STARTED CIM]
│
│ {
│ id: "uuid-v4",
│ header: {
│ channelData: {
│ channelCustomerIdentifier: "+18005551234",
│ serviceIdentifier: "18005550199",
│ additionalAttributes: [ / call variables / ]
│ },
│ customer: { / customer object / },
│ timestamp: 1705315425000,
│ intent: "CALL_LEG_STARTED",
│ sender: { id: "agent-id", senderName: "Bob", type: "AGENT" },
│ conversationId: "existing-conv-id-or-null"
│ },
│ body: {
│ type: "VOICE",
│ reasonCode: "OUTBOUND",
│ leg: "dialogId:1005:+18005551234:1705315420000",
│ callId: "dialogId",
│ dialog: { / full dialog object / },
│ state: null
│ }
│ }
│
│ If conversationId is null:
│ → CCM creates NEW conversation
│ → Associates with customer
│ → Creates VOICE_CALL activity
│ If conversationId exists:
│ → Appends to existing conversation
```
6.2 Service Identifier for Manual Outbound
Critical Point: The service identifier for a manual outbound call comes from the default outbound channel configuration, not the dialed number.
Dialed Number: +1-555-123-4567 (the actual phone number)
Service Identifier: 1-800-555-0199 (tenant's outbound DID / channel)
The service identifier is used by CCM to:
- Identify which queue/skill group this outbound call belongs to
- Apply routing rules (outbound campaigns, agent skills)
- Report on outbound channel performance
- Apply recording policies
API: GET /conversation-manager/default-outbound-channel/{channelTypeId}
- channelTypeId = ID of "CX_VOICE" channel type for the tenant
- Returns: { id, name, serviceIdentifier, channelType, ... }
---
7. CIM Message Structure (Agent Desk → CCM)
7.1 The createCIMMessage Function
// From sip.service.ts — simplified
function createCIMMessage(
messageType: string, // "VOICE"
channelCustomerIdentifier,// customer phone number
serviceIdentifier: string,// DNIS / outbound channel DID
intent: string, // "CALL_LEG_STARTED" | "CALL_LEG_ENDED" | "CALL_HOLD" | "CALL_RESUME"
customer: any, // customer object from API
leg: string, // "dialogId:extension:customerNumber:alertingTime"
dialog: any, // full SIP dialog object
reasonCode: string, // "INBOUND" | "OUTBOUND" | "CONSULT" | "CONFERENCE_ENDED" etc.
timestamp: number, // epoch milliseconds
state: any, // task state (optional)
taskConversationId: string// task's conversation ID (optional)
) {
const conversationId = getCurrentconversationIdORConversation("id") ?? taskConversationId;
return {
id: uuid.v4(),
header: {
channelData: {
channelCustomerIdentifier,
serviceIdentifier,
additionalAttributes: getCallVariablesList(dialog.callVariables?.CallVariable)
},
customer,
language: {},
timestamp,
securityInfo: {},
stamps: [],
intent,
entities: {},
sender: {
id: agent.id,
senderName: agent.username,
type: "AGENT",
additionalDetail: {}
},
conversationId
},
body: {
type: messageType,
markdownText: null,
reasonCode,
leg,
callId: dialog.id,
dialog,
state
}
};
}
```
7.2 Conversation ID Resolution
getCurrentconversationIdORConversation(type: "id" | "conversation") {
const conversations = socketService.conversations;
for (const conversation of conversations) {
if (conversation.activeChannelSessions) {
const voiceSession = conversation.activeChannelSessions.find(
s => s.channel.channelType.name.toLowerCase() === "cx_voice"
);
if (voiceSession) {
if (type === "id") return voiceSession.conversationId;
if (type === "conversation") return conversation;
}
}
}
}
How it works:
1. The Unified Agent maintains a WebSocket connection to CCM
2. All active conversations are in socketService.conversations
3. For voice calls, each conversation has activeChannelSessions
4. The agent scans for a session where channelType.name === "CX_VOICE"
5. Returns the conversationId from that session
6. This ensures the voice activity is appended to the correct conversation
Edge case — No existing conversation:
- If no active voice session exists (e.g., first inbound call, or manual outbound):
- conversationId is null in the CIM
- CCM creates a new conversation automatically
- The new conversation ID is returned in the API response
7.3 Leg Format
leg = "{dialogId}:{agentExtension}:{customerNumber}:{alertingTime}"
Example:
dialogId = "abc-123-def-456"
agentExtension = "1005"
customerNumber = "+18005550199"
alertingTime = "1705315420000" (epoch ms)
leg = "abc-123-def-456:1005:+18005550199:1705315420000"
```
The leg string is used by CCM and the recording system to correlate call legs with recordings.
7.4 Complete CIM Examples
CALL_LEG_STARTED (Inbound):
```json
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"header": {
"channelData": {
"channelCustomerIdentifier": "+18005550199",
"serviceIdentifier": "8001",
"additionalAttributes": [
{ "key": "ANI", "type": "String2000", "value": "+18005550199" },
{ "key": "DNIS", "type": "String2000", "value": "8001" }
]
},
"customer": { "_id": "cust-123", "firstName": "Alice" },
"timestamp": 1705315425000,
"intent": "CALL_LEG_STARTED",
"sender": { "id": "agent-5001", "senderName": "Bob", "type": "AGENT" },
"conversationId": "conv-xyz-789"
},
"body": {
"type": "VOICE",
"reasonCode": "INBOUND",
"leg": "dlg-abc:1005:+18005550199:1705315420000",
"callId": "dlg-abc",
"dialog": { / full Finesse/SIP dialog / }
}
}
```
CALL_LEG_ENDED (Outbound):
```json
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"header": {
"channelData": {
"channelCustomerIdentifier": "+15551234567",
"serviceIdentifier": "18005550199",
"additionalAttributes": [
{ "key": "conversationId", "type": "String2000", "value": "conv-new-123" }
]
},
"customer": { "_id": "cust-456", "firstName": "Charlie" },
"timestamp": 1705315725000,
"intent": "CALL_LEG_ENDED",
"sender": { "id": "agent-5001", "senderName": "Bob", "type": "AGENT" },
"conversationId": "conv-new-123"
},
"body": {
"type": "VOICE",
"reasonCode": "OUTBOUND",
"leg": "dlg-def:1005:+15551234567:1705315425000",
"callId": "dlg-def",
"dialog": { / dialog with callEndReason / },
"endingReason": "NORMAL_CLEARING"
}
}
```
CALL_HOLD:
```json
{
"header": { "intent": "CALL_HOLD" },
"body": { "reasonCode": "INBOUND", "leg": "..." }
}
```
CALL_RESUME:
```json
{
"header": { "intent": "CALL_RESUME" },
"body": { "reasonCode": "INBOUND", "leg": "..." }
}
```
---
8. FreeSWITCH Dialplan & Script Triggers from Agent Actions
8.1 Dialplan Patterns
<!-- Key dialplan entries for agent actions -->
<!-- Agent Extension -->
<extension name="agent_extension">
<condition field="destination_number" expression="^([0-9]{4})$">
<action application="set" data="originate_timeout=30"/>
<action application="bridge" data="user/$1@${domain_name}"/>
</condition>
</extension>
<!-- Queue Transfer (prefix 99887766) -->
<extension name="queue_transfer">
<condition field="destination_number" expression="^99887766-(.*)$">
<action application="lua" data="vcApi.lua directTransfer"/>
</condition>
</extension>
<!-- External Transfer (prefix 99887755) -->
<extension name="external_transfer">
<condition field="destination_number" expression="^99887755-(.*)$">
<action application="lua" data="customTransfer.lua"/>
</condition>
</extension>
<!-- Ad-hoc Conference -->
<extension name="conference">
<condition field="destination_number" expression="^99998888$">
<action application="conference" data="${conference_name}@default"/>
</condition>
</extension>
```
8.2 Agent Action → FreeSWITCH Script Mapping
|
Agent Action |
Wrapper Command |
Dialplan Match |
Lua Script |
Key ESL/API Calls |
|---|---|---|---|---|
|
Accept call |
|
Agent extension |
— |
|
|
End call |
|
Any |
|
|
|
Hold |
|
Any |
— |
|
|
Blind transfer (agent) |
|
Agent ext |
— |
|
|
Blind transfer (queue) |
|
|
|
|
|
Consult (agent) |
|
Agent ext |
— |
|
|
Consult (queue) |
|
|
|
|
|
Consult transfer |
|
DTMF |
|
|
|
Consult conference |
|
DTMF |
|
|
|
External transfer |
|
|
|
|
|
External consult |
|
|
|
|
|
Silent monitor |
|
Agent ext |
|
|
|
Barge in |
|
Agent ext |
|
|
|
DTMF |
|
Any |
— |
|
8.3 RONA on Transfer/Consult
When a blind transfer or consult target doesn't answer:
[FreeSWITCH]
│
│ originate_timeout = 30 seconds (configurable)
│ Agent 1006 doesn't answer within 30s
▼
[ originate_failed_cause = "NO_ANSWER" ]
│
▼
[vcApi.lua — RONA branch]
│
│ 1. Converts NO_ANSWER → "RONA"
│ 2. Checks voicemail_dn (if set, transfer to voicemail)
│ 3. If named transfer: ends call, sends END_CHAT
│ 4. If queue transfer: cancels agent, re-requests agent from same queue
│ → POST /cancel-agent to Voice Connector
│ → POST /request-agent to Voice Connector (requeue)
│ 5. Plays "error_try_later.wav"
▼
[Original agent]
│
│ dialog.callEndReason = "RONA"
│ createCIMMessage(intent: "CALL_LEG_ENDED", reasonCode: "RONA")
---
9. Campaign / Outbound Dialer Integration
9.1 Architecture
[Campaign Manager / Admin UI]
│
│ Uploads contact list, configures campaign
▼
[Outbound Dialer (Spring Boot)]
│
│ • Stores contacts in PostgreSQL
│ • Schedules dialing (cron job)
│ • Respects maxConcurrentCalls, callsPerSecond
▼
[DialerService — retrieveContacts()]
│
│ For each tenant:
│ 1. Get pending contacts
│ 2. Check gateway status: sofia status gateway {gatewayId}
│ 3. Build originate command
▼
[FreeSWITCH — originate command]
│
│ Command format:
│ {sip_h_X-Call-Id={id}, session_in_hangup_hook=true,
│ domain_name={tenant}, record_session=true,
│ sip_h_X-Destination-Number={serviceIdentifier},
│ call_direction=outbound, origination_uuid={id}}
│ sofia/gateway/{gatewayId}/{customerNumber}
│ {ivrScript} XML {tenant}
▼
[Customer answers]
│
▼
[Voice Connector — /request-agent]
│
│ Sends ASSIGN_RESOURCE_REQUESTED to CCM
│ Direction = "OUTBOUND"
▼
[CCM → Routing Engine]
│
│ Finds available agent
▼
[AGENT_RESERVED]
│
│ Voice Connector → Dialer API: POST /agent
│ { agentExtension, uuid, queueName }
▼
[DialerService — dialAgent()]
│
│ Builds originate command to connect agent to customer
│ { ... sip_h_X-agentExtension={ext} ... }
│ sofia/gateway/{gatewayId}/{customerNumber} agent XML {tenant}
▼
[FreeSWITCH]
│
│ Bridges agent and customer
9.2 Campaign Types
|
Type |
Flow |
Use Case |
|---|---|---|
|
IVR-based |
Dialer → Customer → IVR → Voicemail/Opt-out → End |
Automated surveys, payment reminders |
|
Agent-based |
Dialer → Customer → Agent available → Connect |
Sales calls, support callbacks |
|
Agent-based with AMD |
Dialer → Customer → AMD detects human → Agent |
Skip answering machines |
9.3 AMD (Answering Machine Detection)
[FreeSWITCH — agent_amd dialplan]
│
│ Executes: amd (Answer Machine Detection)
│ Parameters: initial_silence, greeting, after_greeting_silence
▼
[AMD Result]
│
│ HUMAN → Continue, connect to agent
│ MACHINE_START → Wait for beep, then play message OR hang up
│ MACHINE_END → Play message
│ NOTSURE → Treat as human
▼
[DialerService — amdResultHandler()]
│
│ If MACHINE: may end call, log result
│ If HUMAN: proceed with agent connection
9.4 Dialer → Voice Connector → CCM Flow
[Customer answers campaign call]
│
▼
[FreeSWITCH — outboundIvr.lua or agent_amd]
│
│ outboundIvr.lua:
│ • Plays prompts
│ • Collects DTMF
│ • May transfer to agent
│
│ agent_amd:
│ • AMD detection
│ • If human: request agent
▼
[Voice Connector — InboundController /request-agent]
│
│ CallDetails:
│ callingNumber: customerNumber
│ callUid: uuid
│ direction: "OUTBOUND"
│ serviceIdentifier: campaign DID
│ queue: campaignQueue
▼
[VcService — requestAgent()]
│
│ Creates CIM: ASSIGN_RESOURCE_REQUESTED
│ body.additionalDetail.callId = {uuid}
│ body.additionalDetail.direction = "OUTBOUND"
│ body.additionalDetail.resource = { type: "NAME", value: queue }
▼
[CCM]
│
│ Creates task, queues it
▼
[CCM → Routing Engine]
│
│ AGENT_RESERVED or NO_AGENT_AVAILABLE
▼
[Voice Connector — OutboundController /ccm-msg/receive/cim-messages]
│
│ If AGENT_RESERVED:
│ → VcService.routeAgent() → handleAgentForOutbound()
│ → POST to Dialer API: /agent
│ { agentExtension, uuid, queueName }
│
│ If NO_AGENT_AVAILABLE:
│ → VcService.routeAgent() → handleAgentForOutbound()
│ → If no agent: sendEndChat()
---
10. Summary: Agent Desk → Voice Stack Mapping
|
What Agent Does |
Angular Code |
Wrapper Command |
FreeSWITCH Result |
CCM Activity |
Recording Impact |
|---|---|---|---|---|---|
|
Answers call |
|
|
Bridge customer→agent |
|
Starts |
|
Hangs up |
|
|
BYE both legs |
|
Stops |
|
Holds call |
|
|
re-INVITE hold |
|
Continues |
|
Resumes call |
|
|
re-INVITE resume |
|
Continues |
|
Mutes |
|
|
Local audio mute |
— |
— |
|
Unmutes |
|
|
Local audio unmute |
— |
— |
|
Blind transfer agent |
|
|
REFER to ext |
|
New file for target |
|
Blind transfer queue |
|
|
REFER to queue |
Same as above |
Same as above |
|
Consult agent |
|
|
New INVITE |
|
New file for consult |
|
Consult queue |
|
|
New INVITE to queue |
Same as above |
Same as above |
|
Complete consult transfer |
|
|
DTMF *A → bridge |
|
New file for A2 |
|
Consult conference |
|
|
DTMF *C → conf |
|
Multiple files |
|
External transfer |
|
|
Transfer to PSTN |
|
Same file |
|
External consult |
|
|
Consult PSTN |
|
New file |
|
Manual outbound |
|
|
Originate |
|
Starts |
|
Silent monitor |
|
|
Eavesdrop |
|
Same file |
|
Barge in |
|
|
Join conference |
|
Same file |
|
DTMF |
|
|
SIP INFO |
— |
— |
---
11. Complete End-to-End Inbound Call Flow
This section traces a voice call from the moment it enters the network until the Agent Desk presents it to an agent. Two paths exist depending on whether the deployment uses native CX Voice (EFCX FreeSWITCH) or Cisco integration (CUCM/UCCE/UCCX).
11.1 Native CX Voice Inbound Path
PSTN Caller
│
▼
SBC / SIP Trunk ──► FreeSWITCH (INVITE to destination_number)
│ │
│ ▼
│ Dialplan matches destination_number
│ │
│ ▼
│ vcApi.lua executed (inbound IVR branch)
│ • session:answer()
│ • request_agent(serviceIdentifier, "INBOUND", queue, ...)
│ │
│ ▼
│ HTTP POST ──► Voice Connector /request-agent
│ │
│ ▼
│ VcService.requestAgent()
│ • Creates CIM Message: intent = ASSIGN_RESOURCE_REQUESTED
│ • body contains: callId, direction=INBOUND, mode=QUEUE,
│ metadata{uuid, eslHost}, resource{queueType, queueValue}
│ │
│ ▼
│ HTTP POST ──► CCM /message/receive
│ │
│ ▼
│ CCM MessageProcessor.processMessage()
│ • No existing ChannelSession → createChannelSession()
│ • handleAssignResourceRequested()
│ • Creates ChannelSession (callId = callSipId)
│ • Publishes ChannelSessionStartedEvent
│ • Publishes FindAgentEvent to conversation topic
│ │
│ ▼
│ ActiveMQ Topic ──► Routing Engine (MRE)
│ │
│ ▼
│ MRE TaskRouter / AssignAgentService
│ • Creates Task (TaskState = PENDING → QUEUED → RESERVED)
│ • Selects agent based on queue / precision queue rules
│ • Sends AGENT_RESERVED notification
│ │
│ ▼
│ ActiveMQ ──► CCM OutboundController
│ • NotificationType.AGENT_RESERVED
│ • Contains: agentExtension, taskMedia, eslHost, uuid
│ │
│ ▼
│ VcService.routeAgent()
│ • Direction = INBOUND
│ • handleAgentForInbound()
│ │
│ ▼
│ ESL Connection to FreeSWITCH
│ • uuid_setvar_multi: sip_h_X-queueType, sip_h_X-queue
│ • uuid_transfer {uuid} {agentExtension} XML {domain}
│ │
│ ▼
│ FreeSWITCH bridges call to agent's registered SIP endpoint
│ (SIP.js / wrapper receives INVITE)
│ │
│ ▼
│ Agent Desk Wrapper (webrtc-phone)
│ • Receives SIP INVITE
│ • Sends postMessage to Angular: dialogState = "confirmed"
│ │
│ ▼
│ sip.service.ts handleCallActiveEvent()
│ • Creates CALL_LEG_STARTED CIM message
│ • leg = "{dialogId}:{agentExt}:{customerNumber}:{alertingTime}"
│ • Sends POST to CCM /message/receive
│ │
│ ▼
│ CCM MessageProcessor → publishCallLegStartedEvent()
│ • Publishes CALL_LEG_STARTED to conversation topic
│ │
│ ▼
│ ConversationManager CallLegStartedEvent.handle()
│ • ActionsUtility.assignAgentAction(agentId, session, direction, true)
│ • Assigns agent to conversation, updates task state
│ │
│ ▼
│ Agent Desk shows ACTIVE call toolbar
│ • Timer starts, recording begins
11.2 Cisco-Integrated Inbound Path
PSTN Caller
│
▼
CUBE (Cisco Unified Border Element)
│
▼
CUCM (CallManager)
│
▼
Cisco Agent Desktop (Finesse) OR
CVP (Customer Voice Portal) → ICM Scripting
│
▼
If routed to EFCX agent:
Cisco CUCM → SIP Trunk → FreeSWITCH
(same FreeSWITCH path as 23.10.1 from here)
│
▼
EFCX Voice Connector receives call via SIP header mapping
• sip_h_X-CALL-ID carries Cisco Call ID
• sip_h_X-Destination-Number maps to EFCX channel
│
▼
Remainder of flow identical to 23.10.1
11.3 WebRTC Inbound Path
For WebRTC calls (browser-to-browser or browser-to-PSTN via gateway):
Customer Browser (WebRTC)
│
▼
FreeSWITCH mod_verto / SIP over WSS
│
▼
Dialplan: vcApi.lua 'webrtc' branch
• serviceIdentifier = sip_h_X-Destination-Number
• customerNumber = sip_h_X-Customer-Number
• direction = "INBOUND"
│
▼
request_agent() → Voice Connector → CCM → MRE
│
▼
Agent Desk receives call via SIP.js over WSS
11.4 Key Data Transformations Along the Path
|
Stage |
Key Action |
Identifier Used |
|---|---|---|
|
FreeSWITCH dialplan |
|
Called number (service identifier) |
|
vcApi.lua |
|
|
|
Voice Connector |
|
|
|
CCM |
|
ChannelSession.id = |
|
MRE |
Task creation |
Task metadata: |
|
AGENT_RESERVED |
VcService.routeAgent() |
|
|
Agent Desk |
|
|
---
12. Complete End-to-End Outbound & Manual Call Flow
12.1 Agent-Initiated Outbound (Manual Dial)
Agent Desk (Angular)
│
▼
Agent clicks "Dial" button, enters customer number
│
▼
sip.service.ts makeOutboundCall()
│
▼
HTTP GET ──► CCM API: /default-outbound-channel/{channelId}
│ (Retrieves default outbound channel for the tenant)
│
▼
Receive serviceIdentifier + channelCustomerIdentifier
│
▼
POST message to Wrapper (webrtc-phone)
│ command: "call" with destinationNumber, domain, etc.
│
▼
Wrapper (SIP.js UserAgent)
│
▼
SIP INVITE ──► FreeSWITCH
│ Via: SIP/2.0 UDP/WS (or TCP/TLS depending on transport)
│
▼
FreeSWITCH Dialplan
• Matches outbound profile
• Routes to configured SIP gateway
│
▼
SIP Gateway ──► PSTN / Carrier
│
▼
Customer phone rings
Parallel CIM Flow (Agent Desk → CCM):
Agent Desk (sip.service.ts)
│
▼
handleCallActiveEvent() for outbound
│
▼
createCIMMessage()
• intent = "CALL_LEG_STARTED"
• body type = "VOICE"
• reasonCode = "OUTBOUND"
• leg = "{dialogId}:{agentExt}:{customerNumber}:{alertingTime}"
│
▼
HTTP POST ──► CCM /message/receive
│
▼
CCM MessageProcessor
• No existing ChannelSession for outbound
• handleCallLegStartedIntent() → creates NEW ChannelSession
• ChannelSession.id = voiceMessage.callId
• Direction = OUTBOUND
• Publishes ChannelSessionStartedEvent
• Publishes CALL_LEG_STARTED event
│
▼
ConversationManager
• CallLegStartedEvent.handle() → ActionsUtility.assignAgentAction()
• Agent is assigned to their own outbound conversation
│
▼
CM also publishes AGENT_OUTBOUND event
• Used by CM to track that agent initiated outbound call
12.2 Campaign Outbound (Dialer-Initiated)
Outbound Dialer (Java service)
│
▼
Fetch contact from campaign list
│
▼
DialerService.originateCall()
│
▼
ESL: "originate" command to FreeSWITCH
│
▼
FreeSWITCH calls customer via gateway
│
▼
Customer answers
│
▼
FreeSWITCH executes dialplan → request_agent()
│ Direction = "OUTBOUND"
│
▼
Voice Connector → CCM → MRE (same as inbound from here)
│
▼
Agent reserved → call transferred to agent
12.3 Outbound Call CIM Message Structure
When the Agent Desk sends CALL_LEG_STARTED for an outbound call, the CIM message structure is:
{
"id": "uuid-v4",
"header": {
"channelData": {
"channelCustomerIdentifier": "+1234567890",
"serviceIdentifier": "cx-voice-channel-01"
},
"timestamp": 1715264400000,
"intent": "CALL_LEG_STARTED",
"customer": { "_id": "...", "firstName": "..." },
"sender": {
"id": "agent-uuid",
"senderName": "Agent Name",
"type": "AGENT"
}
},
"body": {
"type": "VOICE",
"callId": "dialog-id-from-sipjs",
"leg": "dialogId:agentExt:customerNumber:alertingTime",
"reasonCode": "OUTBOUND"
}
}
Important: For outbound calls, the serviceIdentifier comes from the tenant's default outbound channel, NOT the dialed number. This ensures proper queue routing, channel configuration, and reporting attribution.
---
13. How CCM Processes Voice Events
The Customer Channel Manager (CCM) is the first service to receive every CIM message. Its MessageProcessor class acts as the router that dispatches voice intents to the appropriate handlers.
13.1 MessageProcessor — The Entry Point
public ProcessMessageResponseDTO processMessage(@Valid CimMessage cimMessage)
Flow:
1. Look up existing ChannelSession in Redis using channelCustomerIdentifier + serviceIdentifier
2. If found → route to publishMessageOnCustomerTopic() (existing conversation)
3. If NOT found → check cimMessage.header.intent and dispatch to handler
13.2 Intent Routing Table
|
Intent |
Handler Method |
Action |
|---|---|---|
|
|
|
Creates ChannelSession, publishes CALL_ALERTING event |
|
|
|
Creates ChannelSession, publishes CALL_LEG_STARTED event |
|
|
|
Publishes CALL_LEG_ENDED, checks session termination |
|
|
|
Publishes CALL_HOLD event to conversation topic |
|
|
|
Publishes CALL_RESUME event to conversation topic |
|
|
|
Creates outbound ChannelSession, publishes AGENT_OUTBOUND |
|
|
|
Creates ChannelSession, publishes FindAgentEvent |
|
|
|
Publishes CancelResourceRequestedEvent |
|
|
|
Removes session from Redis, publishes ChannelSessionEnded |
13.3 Redis ChannelSession Cache Structure
CCM stores ChannelSessions in Redis with two keys:
Key 1: {tenantId}:channelSession:{channelCustomerIdentifier}:{serviceIdentifier}
→ Value: Full ChannelSession JSON
Key 2: {tenantId}:channelSessionKey:{channelSessionId}
→ Value: "channelSession:{channelCustomerIdentifier}:{serviceIdentifier}"
```
This dual-key design allows lookup by either:
- Customer + Channel (for new messages from FreeSWITCH)
- ChannelSessionId (for messages from Agent Desk referencing a specific session)
13.4 ConversationManager Event Handlers
When CCM publishes events to the conversation topic, the ConversationManager (CM) processes them:
CallLegStartedEvent
ActionMessage assignAgentAction = ActionsUtility.assignAgentAction(agentId, session, direction, true);
actionsHandler.handle(assignAgentAction, ...);
return RoomEventResponse.POST_CONTROLLER_INTENT;
}
```
What happens:
- Extracts agent ID from sender
- Extracts direction (INBOUND, OUTBOUND, CONSULT, etc.) from reasonCode
- Creates ASSIGN_AGENT action message
- ActionsHandler processes it → assigns agent as participant in conversation
- If this is the first agent, conversation state changes to ACTIVE
ChannelSessionEndedEvent
// 2. Check if customer is still present
if (!customerNotPresent(conversation)) {
// Customer still in conversation (e.g., transfer case)
setAgentSlaDurationIfNoVoiceSessionExists(mrdId, conversation);
return POST_CONTROLLER_INTENT;
}
// 3. Customer left — stop hold timer, update state
stopConversationHoldTimerIfRunning(conversation);
conversationCache.updateState(conversation, CUSTOMER_LEFT);
jmsPublisher.publishTopicStateChange(conversation, session);
// 4. Check if agents remain
int agentsInConversation = getAgentParticipants(conversation).size();
if (agentsInConversation > 0 && isWrapUpEnabled) {
conversationCache.updateState(conversation, WRAP_UP);
jmsPublisher.publishTopicStateChange(conversation, session);
}
// 5. No agents left — end conversation entirely
if (agentsInConversation == 0) {
ActionMessage endConversation = ActionsUtility.endConversationAction();
actionsHandler.handle(endConversation, ...);
return RoomEventResponse.RESTART_TRACKER;
}
}
```
Critical logic for transfers:
- When agent transfers and leaves, removeParticipant() removes them
- If customer is still present (on the call with new agent), conversation continues
- If no voice sessions remain but non-voice sessions exist, CM restores non-voice SLA timers
- Only when both customer and all agents have left does the conversation end
TaskStateChangedEvent
// SKIP for voice MRDs — voice SLA is managed differently
if (activeMedia.getMrdId().equals(CX_VOICE_MRD_ID) ||
activeMedia.getMrdId().equals(CISCO_CC_MRD_ID)) {
return POST_CONTROLLER_INTENT;
}
// For non-voice MRDs, set agent SLA from queue configuration
if (PULL mode) → getAgentSlaDuration from PullModeList
if (PUSH mode) → getAgentSlaDuration from PrecisionQueue
}
```
Key point: Voice MRDs bypass standard SLA timer logic because voice calls have real-time session-based lifecycle management.
13.5 VoiceActivityUtil — Leg Correlation Engine
After a conversation ends, ActivityServiceImpl processes all events to create a VOICE_ACTIVITY record. The VoiceActivityUtil class is the correlation engine:
public static CimEvent create(List<CimEvent> events, ChannelSession channelSession) {
// 1. Filter only voice-related events
List<CimEvent> voiceEvents = filterVoiceEvents(events);
// Includes: CALL_ALERTING, CALL_LEG_STARTED, CALL_LEG_ENDED,
// IVR_AGGREGATED_ACTIVITY, CALL_HOLD, CALL_RESUME, MEDIA_RECORDING
// 2. Create CallLeg objects by matching STARTED with ENDED
List<CallLeg> callLegs = createLegs(voiceEvents);
// 3. Calculate hold time per leg
double holdTime = calculateHoldTime(voiceEvents);
// 4. Extract recording URLs
List<MediaUrl> mediaUrls = getMediaUrls(voiceEvents);
// 5. Build VoiceActivity
VoiceActivity voiceActivity = new VoiceActivity(
startTime, endTime, duration, null, mediaUrls, wrapUps, holdTime, callLegs
);
return new CimEvent(voiceActivity, VOICE_ACTIVITY, ACTIVITY, ...);
}
```
Leg Matching Algorithm
static List<CallLeg> createLegs(List<CimEvent> voiceEvents) {
// Categorize events
List<CimMessage> startedAndAggregated = new ArrayList<>();
Map<String, Queue<CimMessage>> endedByLegId = new HashMap<>();
for (CimEvent event : voiceEvents) {
if (eventName is CALL_ALERTING / CALL_LEG_STARTED / IVR_AGGREGATED_ACTIVITY) {
startedAndAggregated.add(payload);
} else if (eventName is CALL_LEG_ENDED) {
String legId = extractLegId(payload);
endedByLegId.computeIfAbsent(legId, k -> new LinkedList<>()).add(payload);
}
}
// Match each started event with corresponding ended event
for (CimMessage startPayload : startedAndAggregated) {
String legId = extractLegId(startPayload);
Queue<CimMessage> endEvents = endedByLegId.get(legId);
CimMessage endPayload = endEvents.poll(); // FIFO matching
CallLeg leg = createCallLeg(startPayload, endPayload, legId);
callLegs.add(leg);
}
}
```
Leg ID format: {dialogId}:{agentExtension}:{customerNumber}:{alertingTime}
Hold Time Calculation
static Map<String, Double> calculateHoldTimePerLeg(List<CimEvent> voiceEvents) {
Map<String, Double> legHoldTimes = new HashMap<>();
Map<String, Timestamp> holdStartTimes = new HashMap<>();
for (CimEvent event : voiceEvents) {
String legId = extractLegIdFromPayload(payload);
switch (event.getName()) {
case CALL_HOLD:
holdStartTimes.put(legId, payload.getHeader().getTimestamp());
break;
case CALL_RESUME:
case CALL_LEG_ENDED:
if (holdStartTimes.containsKey(legId)) {
Timestamp start = holdStartTimes.remove(legId);
double duration = (endTime - startTime) / 1000.0;
legHoldTimes.merge(legId, duration, Double::sum);
}
break;
}
}
}
```
Excluded reason codes from hold time: CONSULT, SILENT_MONITOR, BARGE_IN
Duration Calculation (Excluding Special Legs)
static long getCallDuration(List<CallLeg> callLegs) {
long duration = 0L;
for (CallLeg leg : callLegs) {
if (leg.getDuration() > 0
&& !leg.getStartDirection().equals("CONSULT")
&& !leg.getStartDirection().equals("SILENT_MONITOR")
&& !leg.getStartDirection().equals("BARGE_IN")) {
duration += leg.getDuration();
}
}
return duration;
}
Important: Consult, silent monitor, and barge-in legs are tracked for completeness but do not count toward the total call duration.
---
14. Complete Consult / Transfer / Conference Backend Flow
This section explains how the backend knows about consult, transfer, and conference operations — which is fundamentally different from simple inbound/outbound calls because these operations involve multiple call legs and dynamic conversation membership changes.
14.1 The Core Problem
Unlike inbound calls (where FreeSWITCH sends CALL_ALERTING to CCM), consult/transfer/conference are initiated by the agent. FreeSWITCH does not automatically send CIM messages for these operations. Instead:
-
The Agent Desk sends
CALL_LEG_STARTED/CALL_LEG_ENDEDmessages -
FreeSWITCH scripts (
consult_conf.lua,customTransfer.lua) send custom events -
CM correlates these events to maintain conversation state
14.2 Consult Flow — Step by Step
Phase 1: Agent Initiates Consult
Agent A1 (talking to Customer C1)
│
▼
Clicks "Consult" button in Agent Desk
│
▼
sip.service.ts
• Validates: customer call exists, no existing consult call
• Sends postMessage to Wrapper: command = "consultCall"
│
▼
Wrapper → SIP.js sends INVITE to destination (A2 or external number)
│
▼
FreeSWITCH receives INVITE, creates new dialog (A1-A2)
│
▼
A2 answers
│
▼
Agent Desk (A1) receives "confirmed" dialog state
│
▼
sip.service.ts handleActiveConsultCallEvent()
• Creates CALL_LEG_STARTED CIM message
• reasonCode = "CONSULT"
• leg = "{consultDialogId}:{A1ext}:{customerNumber}:{alertingTime}"
│
▼
HTTP POST ──► CCM /message/receive
│
▼
CCM: existing ChannelSession found (from original call)
• Publishes CALL_LEG_STARTED to conversation topic
│
▼
CM CallLegStartedEvent.handle()
• assignAgentAction(agentId=A1, session, direction="CONSULT", true)
• A1 is already in conversation — no state change
At this point:
- A1 has TWO active dialogs: A1-C1 (customer) and A1-A2 (consult)
- CM knows about the consult leg via CALL_LEG_STARTED
- The consult leg is marked with reasonCode = "CONSULT"
Phase 2: Consult Transfer (A1 transfers C1 to A2)
Agent A1 presses Transfer (or DTMF *A)
│
▼
Wrapper → SIP.js sends BYE to A2 or signals transfer
│
▼
FreeSWITCH executes consult_conf.lua CONSULT_TRANSFER
│
▼
consult_conf.lua:
1. Finds A1C1 call UUID and A1A2 call UUID from channels DB
2. Sets hangup_after_bridge=false, park_after_bridge=true on A1 channels
3. Bridges C1 ↔ A2: uuid_bridge(c1c1uuid, a2a2uuid)
4. Kills A1 channels with hangup code "ATTENDED_TRANSFER"
5. Updates A2 channel variables:
• sip_h_X-CALL-ID = c1DialogId
• sip_h_X-Destination-Number = c1DestNum
6. Sends CONFERENCE/TRANSFER events to remaining members
│
▼
A1's Agent Desk receives call ended event
│
▼
sip.service.ts handleCallDroppedEvent()
• Creates CALL_LEG_ENDED for A1-C1 leg
• reasonCode = "ATTENDED_TRANSFER"
│
▼
HTTP POST ──► CCM /message/receive
│
▼
CCM MessageProcessor.handleCallLegEndedIntent()
• Publishes CALL_LEG_ENDED
• Checks: isValidCustomerChannelSessionEndedCase("ATTENDED_TRANSFER")?
→ NO (ATTENDED_TRANSFER is not in that set)
• ChannelSession remains active
Wait — what about A2?
A2 receives the transferred call as a new SIP INVITE. A2's Agent Desk:
1. Receives the INVITE via SIP.js
2. Shows incoming call notification
3. If A2 is already subscribed to the conversation (from the original task), they see it as an active call
4. A2's sip.service sends CALL_LEG_STARTED with the original conversation's dialogId
Actually, looking more carefully at the code: for a consult transfer, A2 was already reserved for the consult call. When the transfer happens, A2's channel is bridged directly to C1. A2's Agent Desk may send a new CALL_LEG_STARTED or may treat it as a continuation.
Phase 3: Consult Conference (A1 adds A2 to C1 call)
Agent A1 presses Conference (or DTMF *C)
│
▼
FreeSWITCH executes consult_conf.lua CONSULT_CONFERENCE
│
▼
consult_conf.lua:
1. Checks if A1C1 is already a conference
2. If NOT a conference:
• Creates conference name: {a1c1uuid}-conf
• Transfers A1A2 call into conference (both legs)
• Transfers A1C1 call into conference
• Kills excess A1 channel from A1A2 with "PRE_EMPTED"
3. If ALREADY a conference:
• Checks member count ≤ 3 (limit is 4 unique members)
• Transfers A1A2 into existing conference
• Kills excess A1 channel
4. Updates all members with CONFERENCE event
5. Starts new recording for A2
│
▼
All agents receive conference update via MessageUtil.sendMessage()
• Event: CUSTOM SMS::SEND_MESSAGE
• Received by Agent Desk via SIP.js event listener
│
▼
Agent Desk updates UI showing conference members
14.3 How CCM Handles Secondary Call Leg Endings
When a call leg ends, CCM's MessageProcessor.handleCallLegEndedIntent() uses reason codes to determine if the channel session should also end:
// Secondary ending reason codes — session does NOT end
private boolean isValidVoiceSecondaryEndingReasonCode(String reasonCode) {
return Set.of(
CONSULT_ENDED, // Agent ended consult
CONSULT_TRANSFER, // Consult was transferred
SILENT_MONITOR, // Supervisor stopped monitoring
CONFERENCE_ENDED, // Conference ended (agent left)
CONFERENCE_CUSTOMER_LEFT // Customer left conference
).contains(reasonCode);
}
// Primary ending reason codes — session ENDS
private boolean isValidCustomerChannelSessionEndedCase(String reasonCode) {
return Set.of(
EXTERNAL_CONSULT_TRANSFER, // External transfer completed
EXTERNAL_DIRECT_TRANSFER, // Direct external transfer
EXTERNAL_CONSULT_CONFERENCE, // External conference ended
DIALOG_ENDED, // Normal call end
ORIGINATOR_CANCEL // Caller hung up
).contains(reasonCode);
}
```
Key distinction:
- CONSULT_ENDED → Only the consult leg ends; customer session continues
- EXTERNAL_CONSULT_TRANSFER → The entire session ends (customer was transferred out)
- CONFERENCE_ENDED → One agent left conference; if customer still present, session continues
- DIALOG_ENDED → Normal termination, session ends
14.4 Conference Hangup Logic (cx_hangup.lua)
When any conference member hangs up, cx_hangup.lua is triggered with the conference name:
-- 1. Get remaining conference members
local conf_json = json.decode(api:executeString("conference " .. conf .. " json_list"))
local conf_members = conf_json[1]["members"]
-- 2. If only 2 members left, merge them back into a 2-person call
if (tonumber(conf_count) == 2) then
-- Unset conference variables
uuid_setvar(member1, "api_hangup_hook")
uuid_setvar(member1, "conference_name")
uuid_setvar(member2, "api_hangup_hook")
uuid_setvar(member2, "conference_name")
-- Bridge remaining two members
uuid_bridge(member1, member2)
end
-- 3. If only 1 member left, check if it's external consult
if (tonumber(conf_count) == 1) then
handleLastMemeberConferenceHangup()
-- If external consult exists, drop it to prevent stranded calls
end
```
Critical behavior: When a conference drops from 3 members to 2, FreeSWITCH automatically bridges the remaining two into a regular call. This is why CONFERENCE_ENDED doesn't end the channel session — the call continues as a normal 2-party call.
14.5 Transfer Types and Their Backend Impact
|
Transfer Type |
FreeSWITCH Action |
CIM Events Sent |
Session Impact |
|---|---|---|---|
|
Blind Transfer (Agent→Queue) |
|
CALL_LEG_ENDED (original agent), new CALL_LEG_STARTED (new agent) |
Original agent removed; new agent added |
|
Attended Transfer (Consult→Transfer) |
|
CALL_LEG_ENDED for A1, A2 continues |
A1 removed; A2 becomes primary |
|
Direct Transfer (to another agent) |
Similar to blind but to named agent |
Same as blind |
Same as blind |
|
External Transfer (to PSTN) |
|
CALL_LEG_ENDED with EXTERNAL_DIRECT_TRANSFER |
Session ends for all agents |
|
Consult Conference |
|
CONFERENCE events to all members |
All agents remain in conversation |
14.6 How CM Knows About Consulted Agent (A2)
This is a common question: "When A1 consults A2, how does CM know A2 is in the conversation?"
The answer depends on the type of consult:
Queue-based Consult (A2 from queue)
Named Agent Consult (A2 by extension)
Key insight: Even for named consults, the Agent Desk sends the CALL_LEG_STARTED CIM message, which is what adds A2 to the conversation in CM. Without this message, CM would not know A2 is involved.
14.7 Complete Event Sequence — Consult → Transfer
|
Step |
Source |
Event / CIM Intent |
Reason Code |
CM Action |
|---|---|---|---|---|
|
1 |
Voice Connector |
|
|
ChannelSession created |
|
2 |
Agent Desk (A1) |
|
|
A1 assigned to conversation |
|
3 |
Agent Desk (A1) |
|
|
A1's consult leg tracked |
|
4 |
Agent Desk (A2) |
|
|
A2 assigned to conversation |
|
5 |
FreeSWITCH |
|
— |
UI updates |
|
6 |
consult_conf.lua |
Bridges C1↔A2 |
— |
— |
|
7 |
Agent Desk (A1) |
|
|
A1 leg ended, A1 remains participant? |
|
8 |
consult_conf.lua |
Kills A1 channels |
|
— |
|
9 |
Agent Desk (A2) |
|
|
A2 becomes primary |
|
10 |
Customer hangs up |
|
|
ChannelSession ended |
Note: After transfer, A1 may still be a ConversationParticipant in CM until explicitly removed or until the conversation ends. The ChannelSession for the voice channel ends when the customer hangs up.
14.8 Recording During Consult/Transfer/Conference
-- consult_conf.lua starts new recording for new conference members
function startNewRecording(uuid, agent, startTime, record_path)
local recording_filename = c1DialogId .. ":" .. agent .. ":" .. c1 .. ":" .. startTime .. ".wav"
uuid_setvar(uuid, "recording_filename", recording_filename)
uuid_broadcast(uuid, "record_session::'" .. record_path .. "/" .. recording_filename .. "' aleg")
end
Recording behavior:
- Each leg gets its own recording file with naming convention: {dialogId}:{agent}:{customer}:{timestamp}.wav
- When consult conference is created, new recordings start for the newly added agent
- When conference drops to 2 members, recording continues on the bridged call
- All recording URLs are collected in the final VOICE_ACTIVITY event via MediaUrl objects
14.9 Summary: How CCM "Knows" About Each Operation
|
Operation |
How CCM Knows |
Key Mechanism |
|---|---|---|
|
Inbound call arrives |
Voice Connector sends |
FreeSWITCH → vcApi.lua → HTTP → CCM |
|
Agent answers |
Agent Desk sends |
SIP.js event → Angular → HTTP → CCM |
|
Agent holds |
Agent Desk sends |
Button click → HTTP → CCM |
|
Agent resumes |
Agent Desk sends |
Button click → HTTP → CCM |
|
Agent consults |
Agent Desk sends |
SIP.js confirmed → HTTP → CCM |
|
Consult transfer |
FreeSWITCH |
DTMF/Lua bridging + HTTP → CCM |
|
Consult conference |
FreeSWITCH |
DTMF/Lua conference + SMS events |
|
Agent leaves conference |
|
Hangup → HTTP → CCM |
|
Customer hangs up |
|
Hangup → HTTP → CCM |
|
External transfer |
|
Transfer completion → HTTP → CCM |
|
Silent monitor |
Agent Desk sends |
Supervisor joins → HTTP → CCM |
|
Barge-in |
Agent Desk sends |
Supervisor joins → HTTP → CCM |
The golden rule: Every voice operation that CM needs to track must result in either:
1. A CIM message (CALL_LEG_STARTED, CALL_LEG_ENDED, CALL_HOLD, CALL_RESUME) from the Agent Desk, OR
2. A CIM message (CALL_ALERTING, ASSIGN_RESOURCE_REQUESTED) from the Voice Connector / FreeSWITCH scripts
FreeSWITCH internal bridging operations alone do NOT update CM state — the CIM messages are the authoritative source of truth.