@webex/contact-center 3.12.0-next.16 → 3.12.0-next.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/.sdd/manifest.json +887 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +62 -0
  5. package/ai-docs/GETTING_STARTED.md +168 -0
  6. package/ai-docs/GLOSSARY.md +43 -0
  7. package/ai-docs/README.md +138 -0
  8. package/ai-docs/REVIEW_CHECKLIST.md +41 -0
  9. package/ai-docs/RULES.md +444 -0
  10. package/ai-docs/SECURITY.md +52 -0
  11. package/ai-docs/SERVICE_STATE.md +52 -0
  12. package/ai-docs/SPEC_INDEX.md +65 -0
  13. package/ai-docs/adr/0001-spec-source-policy.md +55 -0
  14. package/ai-docs/adr/README.md +8 -0
  15. package/ai-docs/adr/_adr-template.md +31 -0
  16. package/ai-docs/contact-center-spec.md +385 -0
  17. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  18. package/ai-docs/features/contact-owner-change-refactor.md +78 -0
  19. package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
  20. package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
  21. package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
  22. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  23. package/ai-docs/patterns/testing-patterns.md +480 -0
  24. package/ai-docs/patterns/typescript-patterns.md +365 -0
  25. package/ai-docs/templates/README.md +102 -0
  26. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  27. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  28. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  29. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  30. package/ai-docs/templates/new-method/00-master.md +80 -0
  31. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  32. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  33. package/ai-docs/templates/new-method/03-tests.md +201 -0
  34. package/ai-docs/templates/new-method/04-validation.md +141 -0
  35. package/ai-docs/templates/new-service/00-master.md +109 -0
  36. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  37. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  38. package/ai-docs/templates/new-service/03-integration.md +178 -0
  39. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  40. package/ai-docs/templates/new-service/05-validation.md +145 -0
  41. package/dist/cc.js +888 -49
  42. package/dist/cc.js.map +1 -1
  43. package/dist/config.js +13 -0
  44. package/dist/config.js.map +1 -1
  45. package/dist/constants.js +38 -3
  46. package/dist/constants.js.map +1 -1
  47. package/dist/index.js +54 -17
  48. package/dist/index.js.map +1 -1
  49. package/dist/metrics/MetricsManager.js +16 -0
  50. package/dist/metrics/MetricsManager.js.map +1 -1
  51. package/dist/metrics/behavioral-events.js +185 -0
  52. package/dist/metrics/behavioral-events.js.map +1 -1
  53. package/dist/metrics/constants.js +54 -4
  54. package/dist/metrics/constants.js.map +1 -1
  55. package/dist/services/AddressBook.js +18 -15
  56. package/dist/services/AddressBook.js.map +1 -1
  57. package/dist/services/AnswerCallOnWebexService.js +202 -0
  58. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  59. package/dist/services/ApiAiAssistant.js +369 -14
  60. package/dist/services/ApiAiAssistant.js.map +1 -1
  61. package/dist/services/EntryPoint.js +46 -68
  62. package/dist/services/EntryPoint.js.map +1 -1
  63. package/dist/services/Queue.js +27 -22
  64. package/dist/services/Queue.js.map +1 -1
  65. package/dist/services/UserPreference.js +427 -0
  66. package/dist/services/UserPreference.js.map +1 -0
  67. package/dist/services/WebCallingService.js +10 -2
  68. package/dist/services/WebCallingService.js.map +1 -1
  69. package/dist/services/WebexCrossClientService.js +225 -0
  70. package/dist/services/WebexCrossClientService.js.map +1 -0
  71. package/dist/services/WxAppTelephonyMercurySync.js +111 -0
  72. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  73. package/dist/services/config/Util.js +48 -48
  74. package/dist/services/config/Util.js.map +1 -1
  75. package/dist/services/config/constants.js +56 -78
  76. package/dist/services/config/constants.js.map +1 -1
  77. package/dist/services/config/index.js +100 -226
  78. package/dist/services/config/index.js.map +1 -1
  79. package/dist/services/config/types.js +93 -24
  80. package/dist/services/config/types.js.map +1 -1
  81. package/dist/services/constants.js +11 -1
  82. package/dist/services/constants.js.map +1 -1
  83. package/dist/services/core/Err.js.map +1 -1
  84. package/dist/services/core/Utils.js +131 -32
  85. package/dist/services/core/Utils.js.map +1 -1
  86. package/dist/services/core/WebexRequest.js +6 -2
  87. package/dist/services/core/WebexRequest.js.map +1 -1
  88. package/dist/services/core/aqm-reqs.js +17 -11
  89. package/dist/services/core/aqm-reqs.js.map +1 -1
  90. package/dist/services/core/constants.js +9 -1
  91. package/dist/services/core/constants.js.map +1 -1
  92. package/dist/services/core/types.js.map +1 -1
  93. package/dist/services/core/websocket/WebSocketManager.js +19 -2
  94. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  95. package/dist/services/core/websocket/types.js.map +1 -1
  96. package/dist/services/index.js +1 -1
  97. package/dist/services/index.js.map +1 -1
  98. package/dist/services/task/Task.js +955 -0
  99. package/dist/services/task/Task.js.map +1 -0
  100. package/dist/services/task/TaskFactory.js +50 -0
  101. package/dist/services/task/TaskFactory.js.map +1 -0
  102. package/dist/services/task/TaskManager.js +1272 -605
  103. package/dist/services/task/TaskManager.js.map +1 -1
  104. package/dist/services/task/TaskUtils.js +221 -27
  105. package/dist/services/task/TaskUtils.js.map +1 -1
  106. package/dist/services/task/WebexCallingUtils.js +70 -0
  107. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  108. package/dist/services/task/constants.js +19 -2
  109. package/dist/services/task/constants.js.map +1 -1
  110. package/dist/services/task/contact.js +29 -0
  111. package/dist/services/task/contact.js.map +1 -1
  112. package/dist/services/task/digital/Digital.js +78 -0
  113. package/dist/services/task/digital/Digital.js.map +1 -0
  114. package/dist/services/task/state-machine/TaskStateMachine.js +982 -0
  115. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  116. package/dist/services/task/state-machine/actions.js +572 -0
  117. package/dist/services/task/state-machine/actions.js.map +1 -0
  118. package/dist/services/task/state-machine/constants.js +161 -0
  119. package/dist/services/task/state-machine/constants.js.map +1 -0
  120. package/dist/services/task/state-machine/guards.js +379 -0
  121. package/dist/services/task/state-machine/guards.js.map +1 -0
  122. package/dist/services/task/state-machine/index.js +53 -0
  123. package/dist/services/task/state-machine/index.js.map +1 -0
  124. package/dist/services/task/state-machine/types.js +54 -0
  125. package/dist/services/task/state-machine/types.js.map +1 -0
  126. package/dist/services/task/state-machine/uiControlsComputer.js +703 -0
  127. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  128. package/dist/services/task/taskDataNormalizer.js +99 -0
  129. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  130. package/dist/services/task/types.js +215 -5
  131. package/dist/services/task/types.js.map +1 -1
  132. package/dist/services/task/voice/Voice.js +1504 -0
  133. package/dist/services/task/voice/Voice.js.map +1 -0
  134. package/dist/services/task/voice/WebRTC.js +152 -0
  135. package/dist/services/task/voice/WebRTC.js.map +1 -0
  136. package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
  137. package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
  138. package/dist/services/task/voice/wxAppVoiceMethods.js +395 -0
  139. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  140. package/dist/services/wxAppDiagnosticLogging.js +159 -0
  141. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  142. package/dist/services/wxAppTelephonyUtils.js +19 -0
  143. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  144. package/dist/types/cc.d.ts +152 -9
  145. package/dist/types/config.d.ts +13 -0
  146. package/dist/types/constants.d.ts +36 -1
  147. package/dist/types/index.d.ts +54 -20
  148. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  149. package/dist/types/metrics/constants.d.ts +48 -1
  150. package/dist/types/services/AddressBook.d.ts +2 -1
  151. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  152. package/dist/types/services/ApiAiAssistant.d.ts +67 -5
  153. package/dist/types/services/EntryPoint.d.ts +7 -5
  154. package/dist/types/services/Queue.d.ts +5 -3
  155. package/dist/types/services/UserPreference.d.ts +118 -0
  156. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  157. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  158. package/dist/types/services/config/Util.d.ts +4 -8
  159. package/dist/types/services/config/constants.d.ts +49 -76
  160. package/dist/types/services/config/index.d.ts +16 -64
  161. package/dist/types/services/config/types.d.ts +240 -114
  162. package/dist/types/services/constants.d.ts +9 -0
  163. package/dist/types/services/core/Err.d.ts +2 -0
  164. package/dist/types/services/core/Utils.d.ts +43 -13
  165. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  166. package/dist/types/services/core/constants.d.ts +7 -0
  167. package/dist/types/services/core/types.d.ts +2 -0
  168. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  169. package/dist/types/services/core/websocket/types.d.ts +1 -1
  170. package/dist/types/services/index.d.ts +1 -1
  171. package/dist/types/services/task/Task.d.ts +186 -0
  172. package/dist/types/services/task/TaskFactory.d.ts +14 -0
  173. package/dist/types/services/task/TaskUtils.d.ts +54 -2
  174. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  175. package/dist/types/services/task/constants.d.ts +17 -0
  176. package/dist/types/services/task/contact.d.ts +4 -0
  177. package/dist/types/services/task/digital/Digital.d.ts +23 -0
  178. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1442 -0
  179. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  180. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  181. package/dist/types/services/task/state-machine/guards.d.ts +103 -0
  182. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  183. package/dist/types/services/task/state-machine/types.d.ts +277 -0
  184. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  185. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  186. package/dist/types/services/task/types.d.ts +738 -66
  187. package/dist/types/services/task/voice/Voice.d.ts +241 -0
  188. package/dist/types/services/task/voice/WebRTC.d.ts +54 -0
  189. package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
  190. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +68 -0
  191. package/dist/types/services/wxAppDiagnosticLogging.d.ts +63 -0
  192. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  193. package/dist/types/types.d.ts +213 -11
  194. package/dist/types/utils/PageCache.d.ts +20 -3
  195. package/dist/types/webex.d.ts +1 -0
  196. package/dist/types.js +168 -6
  197. package/dist/types.js.map +1 -1
  198. package/dist/utils/PageCache.js +19 -5
  199. package/dist/utils/PageCache.js.map +1 -1
  200. package/dist/webex.js +22 -2
  201. package/dist/webex.js.map +1 -1
  202. package/package.json +16 -12
  203. package/src/cc.ts +1137 -67
  204. package/src/config.ts +13 -0
  205. package/src/constants.ts +39 -1
  206. package/src/index.ts +62 -15
  207. package/src/metrics/MetricsManager.ts +16 -0
  208. package/src/metrics/ai-docs/AGENTS.md +350 -0
  209. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  210. package/src/metrics/ai-docs/metrics-spec.md +971 -0
  211. package/src/metrics/behavioral-events.ts +190 -0
  212. package/src/metrics/constants.ts +59 -4
  213. package/src/services/AddressBook.ts +17 -6
  214. package/src/services/AnswerCallOnWebexService.ts +239 -0
  215. package/src/services/ApiAiAssistant.ts +494 -15
  216. package/src/services/EntryPoint.ts +59 -60
  217. package/src/services/Queue.ts +29 -12
  218. package/src/services/UserPreference.ts +509 -0
  219. package/src/services/WebCallingService.ts +16 -2
  220. package/src/services/WebexCrossClientService.ts +298 -0
  221. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  222. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  223. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  224. package/src/services/agent/ai-docs/agent-spec.md +505 -0
  225. package/src/services/ai-docs/AGENTS.md +386 -0
  226. package/src/services/ai-docs/services-spec.md +512 -0
  227. package/src/services/config/Util.ts +54 -84
  228. package/src/services/config/ai-docs/AGENTS.md +250 -0
  229. package/src/services/config/ai-docs/ARCHITECTURE.md +413 -0
  230. package/src/services/config/ai-docs/config-spec.md +669 -0
  231. package/src/services/config/constants.ts +63 -85
  232. package/src/services/config/index.ts +116 -277
  233. package/src/services/config/types.ts +229 -125
  234. package/src/services/constants.ts +10 -0
  235. package/src/services/core/Err.ts +1 -0
  236. package/src/services/core/Utils.ts +151 -37
  237. package/src/services/core/WebexRequest.ts +3 -1
  238. package/src/services/core/ai-docs/AGENTS.md +381 -0
  239. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  240. package/src/services/core/ai-docs/core-spec.md +797 -0
  241. package/src/services/core/aqm-reqs.ts +18 -12
  242. package/src/services/core/constants.ts +8 -0
  243. package/src/services/core/types.ts +33 -1
  244. package/src/services/core/websocket/WebSocketManager.ts +28 -2
  245. package/src/services/core/websocket/types.ts +1 -1
  246. package/src/services/index.ts +1 -1
  247. package/src/services/task/Task.ts +1211 -0
  248. package/src/services/task/TaskFactory.ts +82 -0
  249. package/src/services/task/TaskManager.ts +1585 -724
  250. package/src/services/task/TaskUtils.ts +316 -25
  251. package/src/services/task/WebexCallingUtils.ts +136 -0
  252. package/src/services/task/ai-docs/AGENTS.md +457 -0
  253. package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
  254. package/src/services/task/ai-docs/task-spec.md +1501 -0
  255. package/src/services/task/constants.ts +18 -0
  256. package/src/services/task/contact.ts +30 -0
  257. package/src/services/task/digital/Digital.ts +102 -0
  258. package/src/services/task/state-machine/TaskStateMachine.ts +1320 -0
  259. package/src/services/task/state-machine/actions.ts +741 -0
  260. package/src/services/task/state-machine/ai-docs/AGENTS.md +460 -0
  261. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1156 -0
  262. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2247 -0
  263. package/src/services/task/state-machine/constants.ts +172 -0
  264. package/src/services/task/state-machine/guards.ts +442 -0
  265. package/src/services/task/state-machine/index.ts +28 -0
  266. package/src/services/task/state-machine/types.ts +258 -0
  267. package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
  268. package/src/services/task/taskDataNormalizer.ts +137 -0
  269. package/src/services/task/types.ts +918 -70
  270. package/src/services/task/voice/Voice.ts +1878 -0
  271. package/src/services/task/voice/WebRTC.ts +194 -0
  272. package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
  273. package/src/services/task/voice/wxAppVoiceMethods.ts +575 -0
  274. package/src/services/wxAppDiagnosticLogging.ts +286 -0
  275. package/src/services/wxAppTelephonyUtils.ts +14 -0
  276. package/src/types.ts +272 -13
  277. package/src/utils/AGENTS.md +289 -0
  278. package/src/utils/PageCache.ts +38 -5
  279. package/src/utils/ai-docs/utils-spec.md +391 -0
  280. package/src/webex.js +10 -0
  281. package/test/unit/fixtures/aiSummaryTestUtils.ts +40 -0
  282. package/test/unit/spec/cc.ts +3553 -107
  283. package/test/unit/spec/index.ts +885 -0
  284. package/test/unit/spec/logger-proxy.ts +70 -0
  285. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  286. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  287. package/test/unit/spec/services/AddressBook.ts +37 -6
  288. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  289. package/test/unit/spec/services/ApiAiAssistant.ts +442 -23
  290. package/test/unit/spec/services/EntryPoint.ts +87 -40
  291. package/test/unit/spec/services/Queue.ts +123 -12
  292. package/test/unit/spec/services/UserPreference.ts +401 -0
  293. package/test/unit/spec/services/WebCallingService.ts +45 -1
  294. package/test/unit/spec/services/WebexCrossClientService.ts +509 -0
  295. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  296. package/test/unit/spec/services/config/Util.ts +501 -0
  297. package/test/unit/spec/services/config/index.ts +317 -547
  298. package/test/unit/spec/services/core/Utils.ts +447 -8
  299. package/test/unit/spec/services/core/WebexRequest.ts +8 -7
  300. package/test/unit/spec/services/core/aqm-reqs.ts +142 -1
  301. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +129 -53
  302. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  303. package/test/unit/spec/services/task/Task.ts +2723 -0
  304. package/test/unit/spec/services/task/TaskFactory.ts +116 -0
  305. package/test/unit/spec/services/task/TaskManager.ts +4734 -1769
  306. package/test/unit/spec/services/task/TaskUtils.ts +307 -0
  307. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  308. package/test/unit/spec/services/task/contact.ts +33 -0
  309. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  310. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +4060 -0
  311. package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
  312. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  313. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
  314. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  315. package/test/unit/spec/services/task/voice/Voice.ts +2355 -0
  316. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  317. package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
  318. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +719 -0
  319. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +179 -0
  320. package/test/unit/spec/wellnessSampleUtils.ts +288 -0
  321. package/typedoc.md +1 -1
  322. package/umd/contact-center.min.js +2 -2
  323. package/umd/contact-center.min.js.map +1 -1
  324. package/dist/services/task/index.js +0 -1530
  325. package/dist/services/task/index.js.map +0 -1
  326. package/dist/types/services/task/index.d.ts +0 -650
  327. package/src/services/task/index.ts +0 -1806
  328. package/test/unit/spec/services/task/index.ts +0 -2205
@@ -0,0 +1,1878 @@
1
+ import {CC_FILE, METHODS} from '../../../constants';
2
+ import {
3
+ buildConsultConferenceParamData,
4
+ calculateDestAgentId,
5
+ calculateDestType,
6
+ getErrorDetails,
7
+ } from '../../core/Utils';
8
+ import routingContact from '../contact';
9
+ import {
10
+ ConsultPayload,
11
+ ConsultEndPayload,
12
+ ResumeRecordingPayload,
13
+ TaskData,
14
+ TaskResponse,
15
+ DropConferenceParticipantPayload,
16
+ IVoice,
17
+ VoiceUIControlOptions,
18
+ TaskToggleMuteOptions,
19
+ TaskTransmitDtmfOptions,
20
+ TransferPayLoad,
21
+ ConsultTransferPayLoad,
22
+ consultConferencePayloadData,
23
+ CONSULT_TRANSFER_DESTINATION_TYPE,
24
+ TASK_EVENTS,
25
+ VOICE_VARIANT,
26
+ } from '../types';
27
+ import Task from '../Task';
28
+ import LoggerProxy from '../../../logger-proxy';
29
+ import MetricsManager from '../../../metrics/MetricsManager';
30
+ import {METRIC_EVENT_NAMES} from '../../../metrics/constants';
31
+ import {TaskState, TaskEvent, TaskActionArgs, type TaskContext} from '../state-machine';
32
+ import {shouldWrapUpForThisAgent} from '../state-machine/guards';
33
+ import type {WrapupData} from '../../config/types';
34
+ import type ApiAIAssistant from '../../ApiAiAssistant';
35
+ import {getConsultMediaResourceId, getIsConferenceInProgress} from '../TaskUtils';
36
+ import AnswerCallOnWebexService from '../../AnswerCallOnWebexService';
37
+ import {isWxAppCallNotFoundError} from '../../wxAppTelephonyUtils';
38
+ import {
39
+ getCallingDeviceDetails,
40
+ getWebexCallingCallId,
41
+ resolveWxAppLineOwnerId,
42
+ isWebexAppCallingOffer as wxIsWebexAppCallingOffer,
43
+ isWebexAppInboundCallingOffer as wxIsWebexAppInboundCallingOffer,
44
+ mapWxAppVoiceError,
45
+ runWxAppAccept,
46
+ runWxAppOutdialDecline,
47
+ runWxAppReject,
48
+ runWxAppToggleMute,
49
+ runWxAppTransmitDtmf,
50
+ WxAppVoiceDependencies,
51
+ WxAppVoiceLifecycle,
52
+ } from './wxAppVoiceMethods';
53
+ import {
54
+ logWxAppMercuryMuteSync,
55
+ callIdSuffix,
56
+ type WxAppAcceptReason,
57
+ } from '../../wxAppDiagnosticLogging';
58
+ import {WxAppOfferObservability, WxAppOfferObservabilityContext} from './wxAppOfferObservability';
59
+
60
+ const hasNonemptyPendingWrapUp = (taskData?: TaskData): boolean =>
61
+ Array.isArray(taskData?.agentsPendingWrapUp) && taskData.agentsPendingWrapUp.length > 0;
62
+
63
+ /**
64
+ * Same consulted-non-owner rule as TaskManager leave stamp. Used only for the
65
+ * HTTP exitConference self-exit fallback (shouldWrapUpForThisAgent stays unchanged).
66
+ */
67
+ const isConsultedNonOwner = (taskData: TaskData, fallback?: TaskData): boolean => {
68
+ const isConsulted = taskData.isConsulted ?? fallback?.isConsulted;
69
+ if (isConsulted !== true) {
70
+ return false;
71
+ }
72
+ const owner = taskData.interaction?.owner ?? fallback?.interaction?.owner;
73
+ const selfAgentId = taskData.agentId ?? fallback?.agentId;
74
+
75
+ return Boolean(selfAgentId && owner && owner !== selfAgentId);
76
+ };
77
+
78
+ export default class Voice extends Task implements IVoice {
79
+ private static readonly WXAPP_MUTE_SYNC_RETRY_DELAY_MS = 50;
80
+ private static readonly WXAPP_MUTE_SYNC_MAX_RETRIES = 3;
81
+
82
+ private answerCallOnWebexService?: AnswerCallOnWebexService;
83
+ private enableWxBetterTogether = false;
84
+ private wxAppMuted = false;
85
+ private wxAppAnswerPending = false;
86
+ private wxAppAcceptInFlight = false;
87
+ private wxAppMuteSyncInFlight?: Promise<boolean | undefined>;
88
+ private wxAppMuteToggleTail: Promise<void> = Promise.resolve();
89
+ private wxAppDtmfTail: Promise<void> = Promise.resolve();
90
+ /** Lazy — `super()` may call `updateUiControls` before field initializers run. */
91
+ private wxAppOfferObservability: WxAppOfferObservability | null = null;
92
+ private getUsersubPublished?: () => boolean;
93
+
94
+ constructor(
95
+ contact: ReturnType<typeof routingContact>,
96
+ data: TaskData,
97
+ callOptions?: VoiceUIControlOptions,
98
+ wrapupData?: WrapupData,
99
+ agentId?: string,
100
+ agentName?: string,
101
+ apiAIAssistant?: ApiAIAssistant
102
+ ) {
103
+ const resolvedOptions = {
104
+ isEndTaskEnabled: callOptions?.isEndTaskEnabled ?? true,
105
+ isEndConsultEnabled: callOptions?.isEndConsultEnabled ?? true,
106
+ voiceVariant: callOptions?.voiceVariant ?? VOICE_VARIANT.PSTN,
107
+ isRecordingEnabled: callOptions?.isRecordingEnabled ?? true,
108
+ enableWxBetterTogether: callOptions?.enableWxBetterTogether ?? false,
109
+ };
110
+
111
+ super(
112
+ contact,
113
+ data,
114
+ {
115
+ ...resolvedOptions,
116
+ consultTransferConfig: callOptions?.consultTransferConfig,
117
+ },
118
+ wrapupData,
119
+ agentId,
120
+ agentName,
121
+ apiAIAssistant
122
+ );
123
+
124
+ this.enableWxBetterTogether = resolvedOptions.enableWxBetterTogether;
125
+ this.answerCallOnWebexService = callOptions?.answerCallOnWebexService;
126
+ this.getUsersubPublished = callOptions?.getUsersubPublished;
127
+ }
128
+
129
+ private getWxAppVoiceDependencies(): WxAppVoiceDependencies {
130
+ return {
131
+ enableWxBetterTogether: this.enableWxBetterTogether,
132
+ answerCallOnWebexService: this.answerCallOnWebexService,
133
+ agentId: this.agentId,
134
+ metricsManager: this.metricsManager,
135
+ getTaskData: () => this.data,
136
+ getTaskState: () => this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined,
137
+ getWxAppMuted: () => this.wxAppMuted,
138
+ setWxAppMuted: (muted: boolean) => {
139
+ this.wxAppMuted = muted;
140
+ },
141
+ getUsersubPublished: this.getUsersubPublished,
142
+ };
143
+ }
144
+
145
+ /** @deprecated Use {@link Voice.getWxAppVoiceDependencies} */
146
+ private getWxAppVoiceDeps(): WxAppVoiceDependencies {
147
+ return this.getWxAppVoiceDependencies();
148
+ }
149
+
150
+ public isWebexAppCallingOffer(): boolean {
151
+ return wxIsWebexAppCallingOffer(this.getWxAppVoiceDeps());
152
+ }
153
+
154
+ public isWebexAppInboundCallingOffer(): boolean {
155
+ return wxIsWebexAppInboundCallingOffer(this.getWxAppVoiceDeps());
156
+ }
157
+
158
+ private setWxAppAnswerPending(pending: boolean): void {
159
+ this.wxAppAnswerPending = pending;
160
+ this.uiControlConfig = {...this.uiControlConfig, wxAppAnswerPending: pending};
161
+ this.updateUiControls(true);
162
+ }
163
+
164
+ private setWxAppAcceptInFlight(inFlight: boolean): void {
165
+ this.wxAppAcceptInFlight = inFlight;
166
+ this.uiControlConfig = {...this.uiControlConfig, wxAppAcceptInFlight: inFlight};
167
+ this.updateUiControls(true);
168
+ }
169
+
170
+ public setEnableWxBetterTogether(enabled: boolean): void {
171
+ this.enableWxBetterTogether = enabled;
172
+ this.uiControlConfig = {...this.uiControlConfig, enableWxBetterTogether: enabled};
173
+ this.updateUiControls(true);
174
+ }
175
+
176
+ private getOrCreateWxAppOfferObservability(): WxAppOfferObservability {
177
+ if (!this.wxAppOfferObservability) {
178
+ this.wxAppOfferObservability = new WxAppOfferObservability();
179
+ }
180
+
181
+ return this.wxAppOfferObservability;
182
+ }
183
+
184
+ protected updateUiControls(forceEmit = false): void {
185
+ super.updateUiControls(forceEmit);
186
+ this.getOrCreateWxAppOfferObservability().handleUiControlsUpdate(
187
+ this.getWxAppOfferObservabilityContext()
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Re-run OFFERED wxApp diagnostics without emitting UI controls.
193
+ * Used after usersub publish so a pre-publish offer can schedule mismatch grace.
194
+ */
195
+ public refreshWxAppOfferObservability(): void {
196
+ this.getOrCreateWxAppOfferObservability().handleUiControlsUpdate(
197
+ this.getWxAppOfferObservabilityContext()
198
+ );
199
+ }
200
+
201
+ /**
202
+ * Cancel pending participant-mismatch grace timer. Observability only.
203
+ */
204
+ public disposeWxAppOfferObservability(): void {
205
+ this.wxAppOfferObservability?.dispose();
206
+ }
207
+
208
+ private getWxAppOfferObservabilityContext(): WxAppOfferObservabilityContext {
209
+ return {
210
+ getInteractionId: () => this.data.interactionId,
211
+ getTaskState: () => this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined,
212
+ getAcceptControl: () => this.currentUiControls?.main?.accept,
213
+ getVoiceVariant: () => this.uiControlConfig.voiceVariant ?? VOICE_VARIANT.PSTN,
214
+ isOutdial: () => this.data?.interaction?.outboundType === 'OUTDIAL',
215
+ isWxAppInboundOffer: () => this.isWebexAppInboundCallingOffer(),
216
+ isWxAppCallingOffer: () => this.isWebexAppCallingOffer(),
217
+ getWxAppAcceptInFlight: () => this.wxAppAcceptInFlight,
218
+ getWxAppAnswerPending: () => this.wxAppAnswerPending,
219
+ getEnableWxBetterTogether: () => this.enableWxBetterTogether,
220
+ getUsersubPublished: () => this.getUsersubPublished?.() ?? false,
221
+ getWxAppVoiceDependencies: () => this.getWxAppVoiceDependencies(),
222
+ getMetricsManager: () => this.metricsManager,
223
+ };
224
+ }
225
+
226
+ public applyWxAppMuteStateFromSync(incomingCallId: string, muted: boolean): void {
227
+ if (!this.enableWxBetterTogether) {
228
+ return;
229
+ }
230
+
231
+ const activeCallId = this.getWebexCallingCallId();
232
+ const incomingSuffix = callIdSuffix(incomingCallId);
233
+ const activeSuffix = callIdSuffix(activeCallId);
234
+
235
+ if (!activeCallId || !incomingCallId.endsWith(activeCallId)) {
236
+ logWxAppMercuryMuteSync({
237
+ phase: 'dropped',
238
+ muted,
239
+ callIdSuffix: incomingSuffix,
240
+ interactionId: this.data.interactionId,
241
+ dropReason: !activeCallId ? 'no_active_wxApp_call' : 'call_id_mismatch',
242
+ });
243
+
244
+ return;
245
+ }
246
+
247
+ if (this.wxAppMuted === muted) {
248
+ logWxAppMercuryMuteSync({
249
+ phase: 'dropped',
250
+ muted,
251
+ callIdSuffix: incomingSuffix,
252
+ interactionId: this.data.interactionId,
253
+ dropReason: 'mute_state_unchanged',
254
+ });
255
+
256
+ return;
257
+ }
258
+
259
+ this.wxAppMuted = muted;
260
+ logWxAppMercuryMuteSync({
261
+ phase: 'applied',
262
+ muted,
263
+ callIdSuffix: activeSuffix,
264
+ interactionId: this.data.interactionId,
265
+ });
266
+ this.emit(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted});
267
+ }
268
+
269
+ public getWxAppMuted(): boolean {
270
+ return this.wxAppMuted;
271
+ }
272
+
273
+ private static delay(ms: number): Promise<void> {
274
+ return new Promise((resolve) => {
275
+ setTimeout(resolve, ms);
276
+ });
277
+ }
278
+
279
+ private canRetryWxAppMuteSync(): boolean {
280
+ if (!this.enableWxBetterTogether) {
281
+ return false;
282
+ }
283
+
284
+ return (
285
+ Boolean(getCallingDeviceDetails(this.getWxAppVoiceDeps())?.deviceCallId) &&
286
+ !this.getWebexCallingCallId()
287
+ );
288
+ }
289
+
290
+ private shouldSkipWxAppMuteSync(): boolean {
291
+ if (this.data?.interaction?.isTerminated) {
292
+ return true;
293
+ }
294
+
295
+ return this.isWebexAppCallingOffer() && !this.uiControlConfig?.wxAppAnswerPending;
296
+ }
297
+
298
+ private applyWxAppMuteFromBackfill(callId: string, muted: boolean): void {
299
+ if (!this.enableWxBetterTogether || this.wxAppMuted === muted) {
300
+ return;
301
+ }
302
+
303
+ const activeCallId = this.getWebexCallingCallId();
304
+ if (activeCallId && !callId.endsWith(activeCallId) && !activeCallId.endsWith(callId)) {
305
+ return;
306
+ }
307
+
308
+ this.wxAppMuted = muted;
309
+ this.emit(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted});
310
+ }
311
+
312
+ public async syncWxAppMuteFromCallDetails(retryAttempt = 0): Promise<boolean | undefined> {
313
+ if (!this.enableWxBetterTogether || !this.answerCallOnWebexService) {
314
+ return undefined;
315
+ }
316
+
317
+ if (retryAttempt === 0 && this.shouldSkipWxAppMuteSync()) {
318
+ return undefined;
319
+ }
320
+
321
+ if (retryAttempt === 0 && this.wxAppMuteSyncInFlight) {
322
+ return this.wxAppMuteSyncInFlight;
323
+ }
324
+
325
+ const syncPromise = this.performWxAppMuteSync(retryAttempt);
326
+
327
+ if (retryAttempt === 0) {
328
+ this.wxAppMuteSyncInFlight = syncPromise;
329
+ syncPromise
330
+ .finally(() => {
331
+ if (this.wxAppMuteSyncInFlight === syncPromise) {
332
+ this.wxAppMuteSyncInFlight = undefined;
333
+ }
334
+ })
335
+ .catch(() => undefined);
336
+
337
+ return syncPromise;
338
+ }
339
+
340
+ return syncPromise;
341
+ }
342
+
343
+ private async performWxAppMuteSync(retryAttempt: number): Promise<boolean | undefined> {
344
+ if (this.shouldSkipWxAppMuteSync()) {
345
+ return undefined;
346
+ }
347
+
348
+ let callId = this.getWebexCallingCallId();
349
+ if (!callId) {
350
+ if (retryAttempt < Voice.WXAPP_MUTE_SYNC_MAX_RETRIES && this.canRetryWxAppMuteSync()) {
351
+ await Voice.delay(Voice.WXAPP_MUTE_SYNC_RETRY_DELAY_MS);
352
+
353
+ return this.performWxAppMuteSync(retryAttempt + 1);
354
+ }
355
+
356
+ callId = getCallingDeviceDetails(this.getWxAppVoiceDeps())?.deviceCallId ?? null;
357
+ if (!callId) {
358
+ return undefined;
359
+ }
360
+ }
361
+
362
+ try {
363
+ const lineOwnerId = resolveWxAppLineOwnerId(this.getWxAppVoiceDeps());
364
+ const details = await this.answerCallOnWebexService.getCallDetails({callId, lineOwnerId});
365
+ if (details.muted !== undefined) {
366
+ this.applyWxAppMuteStateFromSync(callId, details.muted);
367
+ if (this.wxAppMuted !== details.muted) {
368
+ this.applyWxAppMuteFromBackfill(callId, details.muted);
369
+ }
370
+
371
+ return details.muted;
372
+ }
373
+
374
+ return this.wxAppMuted;
375
+ } catch (error) {
376
+ if (!isWxAppCallNotFoundError(error)) {
377
+ LoggerProxy.error(`Failed to sync wxApp mute from call details: ${error}`, {
378
+ module: CC_FILE,
379
+ method: METHODS.GET_CALL_DETAILS_ON_WEBEX,
380
+ interactionId: this.data?.interactionId,
381
+ });
382
+ }
383
+
384
+ return undefined;
385
+ }
386
+ }
387
+
388
+ protected onTaskAssigned(): void {
389
+ this.setWxAppAnswerPending(false);
390
+ queueMicrotask(() => {
391
+ this.syncWxAppMuteFromCallDetails().catch(() => undefined);
392
+ });
393
+ }
394
+
395
+ protected onTaskHydrated(): void {
396
+ const state = this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined;
397
+ const answerStillPending =
398
+ state === TaskState.OFFERED &&
399
+ (this.uiControlConfig?.wxAppAcceptInFlight || this.uiControlConfig?.wxAppAnswerPending);
400
+
401
+ if (!answerStillPending) {
402
+ this.setWxAppAnswerPending(false);
403
+ }
404
+
405
+ this.syncWxAppMuteFromCallDetails().catch(() => undefined);
406
+ }
407
+
408
+ public getCallingDeviceDetails() {
409
+ return getCallingDeviceDetails(this.getWxAppVoiceDeps());
410
+ }
411
+
412
+ public getWebexCallingCallId(): string | null {
413
+ return getWebexCallingCallId(this.getWxAppVoiceDeps());
414
+ }
415
+
416
+ private createWxAppLifecycle(): WxAppVoiceLifecycle {
417
+ return {
418
+ setWxAppAcceptInFlight: (inFlight) => this.setWxAppAcceptInFlight(inFlight),
419
+ setWxAppAnswerPending: (pending) => this.setWxAppAnswerPending(pending),
420
+ resetWxAppMuted: () => {
421
+ this.wxAppMuted = false;
422
+ },
423
+ syncWxAppMuteFromCallDetails: () => this.syncWxAppMuteFromCallDetails(),
424
+ mapWxAppVoiceError: (error, method) => mapWxAppVoiceError(error, method, CC_FILE),
425
+ };
426
+ }
427
+
428
+ private getStateMachineSnapshot() {
429
+ return this.stateMachineService?.getSnapshot?.();
430
+ }
431
+
432
+ private getWxAppAcceptReasonForMetrics(): WxAppAcceptReason {
433
+ if (this.wxAppAcceptInFlight) {
434
+ return 'wxApp_accept_in_flight';
435
+ }
436
+
437
+ if (this.wxAppAnswerPending) {
438
+ return 'wxApp_answer_pending';
439
+ }
440
+
441
+ return 'wxApp_offer_ready';
442
+ }
443
+
444
+ /**
445
+ * Accepts the task. Routes to wxApp telephony answer when `enableWxBetterTogether` and wxApp offer.
446
+ */
447
+ public async accept(): Promise<TaskResponse> {
448
+ if (this.enableWxBetterTogether && this.isWebexAppCallingOffer()) {
449
+ await runWxAppAccept(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle(), {
450
+ acceptReason: this.getWxAppAcceptReasonForMetrics(),
451
+ });
452
+
453
+ return Promise.resolve();
454
+ }
455
+
456
+ return super.unsupportedMethodError(METHODS.ACCEPT);
457
+ }
458
+
459
+ /**
460
+ * Declines the task. Routes wxApp inbound to telephony reject; wxApp outdial offer to cancelTask.
461
+ */
462
+ public async decline(): Promise<TaskResponse> {
463
+ if (this.enableWxBetterTogether && this.isWebexAppInboundCallingOffer()) {
464
+ await runWxAppReject(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle(), {
465
+ acceptReason: this.getWxAppAcceptReasonForMetrics(),
466
+ });
467
+
468
+ return Promise.resolve();
469
+ }
470
+
471
+ const isOutdial = this.data?.interaction?.outboundType === 'OUTDIAL';
472
+ if (!this.enableWxBetterTogether || !isOutdial || !this.isWebexAppCallingOffer()) {
473
+ return super.unsupportedMethodError(METHODS.REJECT);
474
+ }
475
+
476
+ const interactionId = this.data.interactionId;
477
+ LoggerProxy.log(`Declining wxApp outdial task for taskId:${interactionId}`, {
478
+ module: CC_FILE,
479
+ method: METHODS.REJECT,
480
+ interactionId,
481
+ });
482
+
483
+ try {
484
+ this.metricsManager.timeEvent([
485
+ METRIC_EVENT_NAMES.TASK_DECLINE_SUCCESS,
486
+ METRIC_EVENT_NAMES.TASK_DECLINE_FAILED,
487
+ ]);
488
+
489
+ const declineAcceptReason = this.wxAppAnswerPending
490
+ ? 'wxApp_answer_pending'
491
+ : 'wxApp_offer_ready';
492
+ this.setWxAppAnswerPending(false);
493
+ const response = await runWxAppOutdialDecline(
494
+ this.getWxAppVoiceDependencies(),
495
+ () => this.contact.cancelTask({interactionId}),
496
+ {acceptReason: declineAcceptReason}
497
+ );
498
+
499
+ this.metricsManager.trackEvent(
500
+ METRIC_EVENT_NAMES.TASK_DECLINE_SUCCESS,
501
+ {
502
+ taskId: interactionId,
503
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
504
+ },
505
+ ['operational', 'behavioral']
506
+ );
507
+
508
+ return response;
509
+ } catch (error) {
510
+ this.metricsManager.trackEvent(
511
+ METRIC_EVENT_NAMES.TASK_DECLINE_FAILED,
512
+ {
513
+ taskId: interactionId,
514
+ error: error.toString(),
515
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(
516
+ (error as any).details || {}
517
+ ),
518
+ },
519
+ ['operational', 'behavioral']
520
+ );
521
+ const {error: detailedError} = getErrorDetails(error, METHODS.REJECT, CC_FILE);
522
+ throw detailedError;
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Toggles mute for wxApp engaged telephony when `enableWxBetterTogether` is active.
528
+ */
529
+ public async toggleMute(options?: TaskToggleMuteOptions): Promise<void> {
530
+ if (this.enableWxBetterTogether && this.getWebexCallingCallId()) {
531
+ const togglePromise = this.wxAppMuteToggleTail
532
+ .catch(() => undefined)
533
+ .then(() =>
534
+ runWxAppToggleMute(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle(), options)
535
+ );
536
+
537
+ this.wxAppMuteToggleTail = togglePromise.catch(() => undefined);
538
+ await togglePromise;
539
+
540
+ return;
541
+ }
542
+
543
+ super.unsupportedMethodError(METHODS.TOGGLE_MUTE);
544
+ }
545
+
546
+ /**
547
+ * Sends in-call DTMF for wxApp engaged telephony when `enableWxBetterTogether` is active.
548
+ */
549
+ public async transmitDtmf(options: TaskTransmitDtmfOptions): Promise<void> {
550
+ if (this.enableWxBetterTogether && this.getWebexCallingCallId()) {
551
+ const dtmfPromise = this.wxAppDtmfTail
552
+ .catch(() => undefined)
553
+ .then(() =>
554
+ runWxAppTransmitDtmf(
555
+ this.getWxAppVoiceDependencies(),
556
+ this.createWxAppLifecycle(),
557
+ options
558
+ )
559
+ );
560
+
561
+ this.wxAppDtmfTail = dtmfPromise.catch(() => undefined);
562
+ await dtmfPromise;
563
+
564
+ return;
565
+ }
566
+
567
+ super.unsupportedMethodError(METHODS.TRANSMIT_DTMF);
568
+ }
569
+
570
+ /**
571
+ * This is used to hold the task.
572
+ * @returns Promise<TaskResponse>
573
+ * @throws Error
574
+ * @example
575
+ * ```typescript
576
+ * task.hold().then(()=>{}).catch(()=>{})
577
+ * ```
578
+ * */
579
+ public async hold(): Promise<TaskResponse> {
580
+ return this.holdResume();
581
+ }
582
+
583
+ /**
584
+ * This is used to resume the task.
585
+ * @returns Promise<TaskResponse>
586
+ * @throws Error
587
+ * @example
588
+ * ```typescript
589
+ * task.resume().then(()=>{}).catch(()=>{})
590
+ * ```
591
+ * */
592
+ public async resume(): Promise<TaskResponse> {
593
+ return this.holdResume();
594
+ }
595
+
596
+ /**
597
+ * This is used to hold or resume the task.
598
+ * @param isHeld: boolean - true to hold the task, false to resume it
599
+ * @returns Promise<TaskResponse>
600
+ * @throws Error
601
+ * @example
602
+ * ```typescript
603
+ * task.holdResume(isHeld: true).then(()=>{}).catch(()=>{})
604
+ * ```
605
+ * */
606
+ public async holdResume(): Promise<TaskResponse> {
607
+ /*
608
+ Determine if the task is being held or resumed based on the media resource state
609
+ If the media resource is not found, default to resuming the task
610
+ */
611
+ const snapshot = this.getStateMachineSnapshot();
612
+ const snapshotState = snapshot?.value as TaskState | undefined;
613
+ const mainInteractionId = this.data.interaction?.mainInteractionId || this.data.interactionId;
614
+ const mainMediaResource =
615
+ this.data.interaction?.media?.[mainInteractionId]?.mediaResourceId ||
616
+ this.data.mediaResourceId;
617
+ const mediaHoldState =
618
+ this.data.interaction?.media?.[mainInteractionId]?.isHold ??
619
+ this.data.interaction.media?.[mainMediaResource]?.isHold;
620
+ let shouldHold = !(mediaHoldState ?? false);
621
+ if (snapshotState === TaskState.HELD || snapshotState === TaskState.RESUME_INITIATING) {
622
+ shouldHold = false;
623
+ } else if (snapshotState === TaskState.CONNECTED && mediaHoldState !== true) {
624
+ // CONNECTED with isHold:true can happen after consult RONA/decline when event ordering
625
+ // leaves the machine in CONNECTED while main media is still on hold — resume on first click.
626
+ shouldHold = true;
627
+ }
628
+
629
+ // Validate operation is allowed in current state
630
+ const state = snapshot;
631
+ if (state) {
632
+ const currentState = state.value as TaskState;
633
+ if (shouldHold) {
634
+ if (!state.matches(TaskState.CONNECTED)) {
635
+ const error = new Error(`Cannot hold call in current state: ${currentState}`);
636
+ LoggerProxy.error('Hold operation not allowed', {
637
+ module: CC_FILE,
638
+ method: METHODS.HOLD_RESUME,
639
+ interactionId: this.data.interactionId,
640
+ });
641
+ throw error;
642
+ }
643
+ } else if (
644
+ !state.matches(TaskState.HELD) &&
645
+ !(state.matches(TaskState.CONFERENCING) && mediaHoldState === true) &&
646
+ !(state.matches(TaskState.CONNECTED) && mediaHoldState === true)
647
+ ) {
648
+ const error = new Error(`Cannot resume call in current state: ${currentState}`);
649
+ LoggerProxy.error('Resume operation not allowed', {
650
+ module: CC_FILE,
651
+ method: METHODS.HOLD_RESUME,
652
+ interactionId: this.data.interactionId,
653
+ });
654
+ throw error;
655
+ }
656
+ }
657
+
658
+ // Send initiating event to transition to intermediate state
659
+ if (this.stateMachineService) {
660
+ const initiatingEvent = shouldHold ? TaskEvent.HOLD_INITIATED : TaskEvent.UNHOLD_INITIATED;
661
+ this.stateMachineService.send({
662
+ type: initiatingEvent,
663
+ mediaResourceId: mainMediaResource,
664
+ });
665
+ }
666
+
667
+ LoggerProxy.info(`${shouldHold ? 'Holding' : 'Resuming'} task`, {
668
+ module: CC_FILE,
669
+ method: METHODS.HOLD_RESUME,
670
+ interactionId: this.data.interactionId,
671
+ });
672
+ const [successEvt, failedEvt] = shouldHold
673
+ ? [METRIC_EVENT_NAMES.TASK_HOLD_SUCCESS, METRIC_EVENT_NAMES.TASK_HOLD_FAILED]
674
+ : [METRIC_EVENT_NAMES.TASK_RESUME_SUCCESS, METRIC_EVENT_NAMES.TASK_RESUME_FAILED];
675
+
676
+ this.metricsManager.timeEvent([successEvt, failedEvt]);
677
+
678
+ try {
679
+ let response: TaskResponse;
680
+ if (shouldHold) {
681
+ response = await this.contact.hold({
682
+ interactionId: this.data.interactionId,
683
+ data: {mediaResourceId: mainMediaResource},
684
+ });
685
+
686
+ // Send success event to complete the transition
687
+ if (this.stateMachineService) {
688
+ this.stateMachineService.send({
689
+ type: TaskEvent.HOLD_SUCCESS,
690
+ mediaResourceId: mainMediaResource,
691
+ });
692
+ }
693
+
694
+ this.metricsManager.trackEvent(
695
+ successEvt,
696
+ {
697
+ taskId: this.data.interactionId,
698
+ mediaResourceId: mainMediaResource,
699
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
700
+ },
701
+ ['operational', 'behavioral']
702
+ );
703
+ LoggerProxy.log(`Task placed on hold successfully`, {
704
+ module: CC_FILE,
705
+ method: METHODS.HOLD_RESUME,
706
+ trackingId: response.trackingId,
707
+ interactionId: this.data.interactionId,
708
+ });
709
+ } else {
710
+ response = await this.contact.unHold({
711
+ interactionId: this.data.interactionId,
712
+ data: {mediaResourceId: mainMediaResource},
713
+ });
714
+
715
+ // Send success event to complete the transition
716
+ if (this.stateMachineService) {
717
+ this.stateMachineService.send({
718
+ type: TaskEvent.UNHOLD_SUCCESS,
719
+ mediaResourceId: mainMediaResource,
720
+ });
721
+ }
722
+
723
+ this.metricsManager.trackEvent(
724
+ successEvt,
725
+ {
726
+ taskId: this.data.interactionId,
727
+ mainInteractionId,
728
+ mediaResourceId: mainMediaResource,
729
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
730
+ },
731
+ ['operational', 'behavioral']
732
+ );
733
+ LoggerProxy.log(`Task resumed successfully`, {
734
+ module: CC_FILE,
735
+ method: METHODS.HOLD_RESUME,
736
+ trackingId: response.trackingId,
737
+ interactionId: this.data.interactionId,
738
+ });
739
+ }
740
+
741
+ return response;
742
+ } catch (error) {
743
+ const failureEvent = shouldHold ? TaskEvent.HOLD_FAILED : TaskEvent.UNHOLD_FAILED;
744
+ this.stateMachineService.send({
745
+ type: failureEvent,
746
+ reason: error.toString(),
747
+ mediaResourceId: this.data.mediaResourceId,
748
+ });
749
+
750
+ const {error: detailedError} = getErrorDetails(error, 'holdResume', CC_FILE);
751
+ this.metricsManager.trackEvent(
752
+ failedEvt,
753
+ shouldHold
754
+ ? {
755
+ taskId: this.data.interactionId,
756
+ mediaResourceId: this.data.mediaResourceId,
757
+ error: error.toString(),
758
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
759
+ }
760
+ : {
761
+ taskId: this.data.interactionId,
762
+ error: error.toString(),
763
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
764
+ },
765
+ ['operational', 'behavioral']
766
+ );
767
+ throw detailedError;
768
+ }
769
+ }
770
+
771
+ /**
772
+ * This is used to pause the call recording
773
+ * @returns Promise<TaskResponse>
774
+ * @throws Error
775
+ * @example
776
+ * ```typescript
777
+ * task.pauseRecording().then(()=>{}).catch(()=>{});
778
+ * ```
779
+ */
780
+ public async pauseRecording(): Promise<TaskResponse> {
781
+ // Validate recording is active
782
+ const state = this.getStateMachineSnapshot();
783
+ if (state) {
784
+ const {recordingControlsAvailable, recordingInProgress} = state.context as {
785
+ recordingControlsAvailable?: boolean;
786
+ recordingInProgress?: boolean;
787
+ };
788
+ const recordingActive = Boolean(recordingControlsAvailable && recordingInProgress);
789
+ if (!recordingActive) {
790
+ const error = new Error('Recording is not active or already paused');
791
+ LoggerProxy.error('Pause recording operation not allowed', {
792
+ module: CC_FILE,
793
+ method: 'pauseRecording',
794
+ interactionId: this.data.interactionId,
795
+ });
796
+ throw error;
797
+ }
798
+ }
799
+
800
+ try {
801
+ LoggerProxy.info(`Pausing recording`, {
802
+ module: CC_FILE,
803
+ method: 'pauseRecording',
804
+ interactionId: this.data.interactionId,
805
+ });
806
+ this.metricsManager.timeEvent([
807
+ METRIC_EVENT_NAMES.TASK_PAUSE_RECORDING_SUCCESS,
808
+ METRIC_EVENT_NAMES.TASK_PAUSE_RECORDING_FAILED,
809
+ ]);
810
+ const result = await this.contact.pauseRecording({interactionId: this.data.interactionId});
811
+ this.metricsManager.trackEvent(
812
+ METRIC_EVENT_NAMES.TASK_PAUSE_RECORDING_SUCCESS,
813
+ {
814
+ taskId: this.data.interactionId,
815
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
816
+ },
817
+ ['operational', 'behavioral', 'business']
818
+ );
819
+ LoggerProxy.log(`Recording paused successfully`, {
820
+ module: CC_FILE,
821
+ method: 'pauseRecording',
822
+ trackingId: result.trackingId,
823
+ interactionId: this.data.interactionId,
824
+ });
825
+
826
+ return result;
827
+ } catch (error) {
828
+ const {error: detailedError} = getErrorDetails(error, 'pauseRecording', CC_FILE);
829
+ this.metricsManager.trackEvent(
830
+ METRIC_EVENT_NAMES.TASK_PAUSE_RECORDING_FAILED,
831
+ {
832
+ taskId: this.data.interactionId,
833
+ error: error.toString(),
834
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
835
+ },
836
+ ['operational', 'behavioral', 'business']
837
+ );
838
+ throw detailedError;
839
+ }
840
+ }
841
+
842
+ /**
843
+ * This is used to pause the call recording
844
+ * @param resumeRecordingPayload
845
+ * @returns Promise<TaskResponse>
846
+ * @throws Error
847
+ * @example
848
+ * ```typescript
849
+ * task.resumeRecording(resumeRecordingPayload).then(()=>{}).catch(()=>{});
850
+ * ```
851
+ */
852
+ public async resumeRecording(
853
+ resumeRecordingPayload?: ResumeRecordingPayload
854
+ ): Promise<TaskResponse> {
855
+ // Validate recording is paused
856
+ const state = this.getStateMachineSnapshot();
857
+ if (state) {
858
+ const {recordingControlsAvailable, recordingInProgress} = state.context as {
859
+ recordingControlsAvailable?: boolean;
860
+ recordingInProgress?: boolean;
861
+ };
862
+ const recordingPaused = Boolean(recordingControlsAvailable && !recordingInProgress);
863
+ if (!recordingPaused) {
864
+ const error = new Error('Recording is not paused');
865
+ LoggerProxy.error('Resume recording operation not allowed', {
866
+ module: CC_FILE,
867
+ method: 'resumeRecording',
868
+ interactionId: this.data.interactionId,
869
+ });
870
+ throw error;
871
+ }
872
+ }
873
+
874
+ try {
875
+ LoggerProxy.info(`Resuming recording`, {
876
+ module: CC_FILE,
877
+ method: 'resumeRecording',
878
+ interactionId: this.data.interactionId,
879
+ });
880
+ this.metricsManager.timeEvent([
881
+ METRIC_EVENT_NAMES.TASK_RESUME_RECORDING_SUCCESS,
882
+ METRIC_EVENT_NAMES.TASK_RESUME_RECORDING_FAILED,
883
+ ]);
884
+ resumeRecordingPayload ??= {autoResumed: false};
885
+
886
+ const result = await this.contact.resumeRecording({
887
+ interactionId: this.data.interactionId,
888
+ data: resumeRecordingPayload,
889
+ });
890
+ this.metricsManager.trackEvent(
891
+ METRIC_EVENT_NAMES.TASK_RESUME_RECORDING_SUCCESS,
892
+ {
893
+ taskId: this.data.interactionId,
894
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
895
+ },
896
+ ['operational', 'behavioral', 'business']
897
+ );
898
+ LoggerProxy.log(`Recording resumed successfully`, {
899
+ module: CC_FILE,
900
+ method: 'resumeRecording',
901
+ trackingId: result.trackingId,
902
+ interactionId: this.data.interactionId,
903
+ });
904
+
905
+ return result;
906
+ } catch (error) {
907
+ const {error: detailedError} = getErrorDetails(error, 'resumeRecording', CC_FILE);
908
+ this.metricsManager.trackEvent(
909
+ METRIC_EVENT_NAMES.TASK_RESUME_RECORDING_FAILED,
910
+ {
911
+ taskId: this.data.interactionId,
912
+ error: error.toString(),
913
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
914
+ },
915
+ ['operational', 'behavioral', 'business']
916
+ );
917
+ throw detailedError;
918
+ }
919
+ }
920
+
921
+ /**
922
+ * This is used to consult the task
923
+ * @param consultPayload
924
+ * @returns Promise<TaskResponse>
925
+ * @throws Error
926
+ * @example
927
+ * ```typescript
928
+ * const consultPayload = {
929
+ * destination: 'myBuddyAgentId',
930
+ * destinationType: DESTINATION_TYPE.AGENT,
931
+ * }
932
+ * task.consult(consultPayload).then(()=>{}).catch(()=>{});
933
+ * ```
934
+ * */
935
+ public async consult(consultPayload?: ConsultPayload): Promise<TaskResponse> {
936
+ // Validate consult is allowed
937
+ const state = this.getStateMachineSnapshot();
938
+ const canConsult =
939
+ state &&
940
+ (state.matches(TaskState.CONNECTED) ||
941
+ state.matches(TaskState.HELD) ||
942
+ state.matches(TaskState.CONFERENCING));
943
+
944
+ if (!canConsult) {
945
+ const currentState = state?.value as TaskState;
946
+ const error = new Error(`Cannot initiate consult in ${currentState} state`);
947
+ LoggerProxy.error('Consult operation not allowed', {
948
+ module: CC_FILE,
949
+ method: 'consult',
950
+ interactionId: this.data.interactionId,
951
+ });
952
+ throw error;
953
+ }
954
+
955
+ // Send initiating event to transition to CONSULT_INITIATING state
956
+ if (this.stateMachineService) {
957
+ this.stateMachineService.send({
958
+ type: TaskEvent.CONSULT,
959
+ destination: consultPayload.to,
960
+ destinationType: consultPayload.destinationType,
961
+ });
962
+ }
963
+
964
+ const requestInteractionId =
965
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
966
+
967
+ try {
968
+ LoggerProxy.info(`Starting consult`, {
969
+ module: CC_FILE,
970
+ method: 'consult',
971
+ interactionId: requestInteractionId,
972
+ });
973
+ this.metricsManager.timeEvent([
974
+ METRIC_EVENT_NAMES.TASK_CONSULT_START_SUCCESS,
975
+ METRIC_EVENT_NAMES.TASK_CONSULT_START_FAILED,
976
+ ]);
977
+ const result = await this.contact.consult({
978
+ interactionId: requestInteractionId,
979
+ data: consultPayload,
980
+ });
981
+
982
+ // Send success event to transition to CONSULTING state
983
+ if (this.stateMachineService) {
984
+ this.stateMachineService.send({
985
+ type: TaskEvent.CONSULT_SUCCESS,
986
+ taskData: result.data,
987
+ });
988
+ }
989
+
990
+ this.metricsManager.trackEvent(
991
+ METRIC_EVENT_NAMES.TASK_CONSULT_START_SUCCESS,
992
+ {
993
+ taskId: this.data.interactionId,
994
+ requestInteractionId,
995
+ destination: consultPayload.to,
996
+ destinationType: consultPayload.destinationType,
997
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
998
+ },
999
+ ['operational', 'behavioral', 'business']
1000
+ );
1001
+ LoggerProxy.log(`Consult successfully initiated to ${consultPayload.to}`, {
1002
+ module: CC_FILE,
1003
+ method: 'consult',
1004
+ trackingId: result.trackingId,
1005
+ interactionId: requestInteractionId,
1006
+ });
1007
+
1008
+ return result;
1009
+ } catch (error) {
1010
+ this.stateMachineService.send({
1011
+ type: TaskEvent.CONSULT_FAILED,
1012
+ reason: error.toString(),
1013
+ });
1014
+
1015
+ const {error: detailedError} = getErrorDetails(error, 'consult', CC_FILE);
1016
+ this.metricsManager.trackEvent(
1017
+ METRIC_EVENT_NAMES.TASK_CONSULT_START_FAILED,
1018
+ {
1019
+ taskId: this.data.interactionId,
1020
+ requestInteractionId,
1021
+ destination: consultPayload.to,
1022
+ destinationType: consultPayload.destinationType,
1023
+ error: error.toString(),
1024
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
1025
+ },
1026
+ ['operational', 'behavioral', 'business']
1027
+ );
1028
+ throw detailedError;
1029
+ }
1030
+ }
1031
+
1032
+ /**
1033
+ * This is used to end the consult session on the task.
1034
+ * @param consultEndPayload - Payload indicating consult end flags and identifiers
1035
+ * @returns Promise<TaskResponse>
1036
+ * @throws Error
1037
+ * @example
1038
+ * ```typescript
1039
+ * task.endConsult({
1040
+ * isConsult: true,
1041
+ * queueId: 'myQueueId',
1042
+ * taskId: 'taskId',
1043
+ * });
1044
+ * ```
1045
+ */
1046
+ public async endConsult(consultEndPayload?: ConsultEndPayload): Promise<TaskResponse> {
1047
+ const requestInteractionId =
1048
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
1049
+
1050
+ try {
1051
+ LoggerProxy.info(`Ending consult`, {
1052
+ module: CC_FILE,
1053
+ method: 'endConsult',
1054
+ interactionId: requestInteractionId,
1055
+ });
1056
+ this.metricsManager.timeEvent([
1057
+ METRIC_EVENT_NAMES.TASK_CONSULT_END_SUCCESS,
1058
+ METRIC_EVENT_NAMES.TASK_CONSULT_END_FAILED,
1059
+ ]);
1060
+ const result = await this.contact.consultEnd({
1061
+ interactionId: requestInteractionId,
1062
+ data: consultEndPayload,
1063
+ });
1064
+ this.metricsManager.trackEvent(
1065
+ METRIC_EVENT_NAMES.TASK_CONSULT_END_SUCCESS,
1066
+ {
1067
+ taskId: this.data.interactionId,
1068
+ requestInteractionId,
1069
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
1070
+ },
1071
+ ['operational', 'behavioral', 'business']
1072
+ );
1073
+ LoggerProxy.log(`Consult ended successfully`, {
1074
+ module: CC_FILE,
1075
+ method: 'endConsult',
1076
+ trackingId: result.trackingId,
1077
+ interactionId: this.data.interactionId,
1078
+ });
1079
+
1080
+ return result;
1081
+ } catch (error) {
1082
+ const {error: detailedError} = getErrorDetails(error, 'endConsult', CC_FILE);
1083
+ this.metricsManager.trackEvent(
1084
+ METRIC_EVENT_NAMES.TASK_CONSULT_END_FAILED,
1085
+ {
1086
+ taskId: this.data.interactionId,
1087
+ requestInteractionId,
1088
+ error: error.toString(),
1089
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
1090
+ },
1091
+ ['operational', 'behavioral', 'business']
1092
+ );
1093
+ throw detailedError;
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * This is used to transfer the task.
1099
+ * @param payload - Transfer payload
1100
+ * @returns Promise<TaskResponse>
1101
+ * @throws Error
1102
+ * @example
1103
+ * ```typescript
1104
+ * task.transfer({
1105
+ * to: 'destinationId',
1106
+ * destinationType: DESTINATION_TYPE.AGENT,
1107
+ * consult: true, // Optional, if true will perform a consult transfer else blind transfer
1108
+ * });
1109
+ * ```
1110
+ */
1111
+ public async transfer(payload: TransferPayLoad): Promise<TaskResponse> {
1112
+ try {
1113
+ LoggerProxy.info(`Transferring task to ${payload.to}`, {
1114
+ module: CC_FILE,
1115
+ method: METHODS.TRANSFER_CALL,
1116
+ interactionId: this.data.interactionId,
1117
+ });
1118
+ this.metricsManager.timeEvent([
1119
+ METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
1120
+ METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
1121
+ ]);
1122
+
1123
+ // consult transfer path
1124
+ if (this.data.interaction.state === 'consulting') {
1125
+ const normalizedDestinationType =
1126
+ payload.destinationType === 'Agent' || payload.destinationType === 'Queue'
1127
+ ? (payload.destinationType.toLowerCase() as ConsultTransferPayLoad['destinationType'])
1128
+ : payload.destinationType;
1129
+ let consultPayload: ConsultTransferPayLoad = {
1130
+ to: payload.to,
1131
+ destinationType: normalizedDestinationType,
1132
+ };
1133
+
1134
+ if (normalizedDestinationType === CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE) {
1135
+ const consultContext = this.getStateMachineSnapshot()?.context;
1136
+ const destAgent = consultContext?.consultDestinationAgentId || this.data.destAgentId;
1137
+ if (!destAgent) {
1138
+ throw new Error('No agent has accepted this queue consult yet');
1139
+ }
1140
+ consultPayload = {
1141
+ to: destAgent,
1142
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
1143
+ };
1144
+ }
1145
+
1146
+ const result = await this.contact.consultTransfer({
1147
+ interactionId: this.data.interactionId,
1148
+ data: consultPayload,
1149
+ });
1150
+ this.metricsManager.trackEvent(
1151
+ METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
1152
+ {
1153
+ taskId: this.data.interactionId,
1154
+ destination: consultPayload.to,
1155
+ destinationType: consultPayload.destinationType,
1156
+ isConsultTransfer: true,
1157
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(result),
1158
+ },
1159
+ ['operational', 'behavioral', 'business']
1160
+ );
1161
+ LoggerProxy.log(`Consult transfer completed successfully to ${consultPayload.to}`, {
1162
+ module: CC_FILE,
1163
+ method: METHODS.TRANSFER_CALL,
1164
+ trackingId: result.trackingId,
1165
+ interactionId: this.data.interactionId,
1166
+ });
1167
+
1168
+ return result;
1169
+ }
1170
+
1171
+ // standard blind transfer
1172
+ return await super.transfer({
1173
+ to: payload.to,
1174
+ destinationType: payload.destinationType,
1175
+ });
1176
+ } catch (err) {
1177
+ const {error: detailedError} = getErrorDetails(err, METHODS.TRANSFER_CALL, CC_FILE);
1178
+ this.metricsManager.trackEvent(
1179
+ METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
1180
+ {
1181
+ taskId: this.data.interactionId,
1182
+ destination: payload.to,
1183
+ destinationType: payload.destinationType,
1184
+ isConsultTransfer: this.data.interaction.state === 'consulting',
1185
+ error: err.toString(),
1186
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(err.details || {}),
1187
+ },
1188
+ ['operational', 'behavioral', 'business']
1189
+ );
1190
+ throw detailedError;
1191
+ }
1192
+ }
1193
+
1194
+ /**
1195
+ * Start a consult conference, merging main and consult calls.
1196
+ */
1197
+ public async consultConference(): Promise<TaskResponse> {
1198
+ const derivedDestAgentId =
1199
+ this.data.interaction && this.data.agentId
1200
+ ? calculateDestAgentId(this.data.interaction, this.data.agentId)
1201
+ : '';
1202
+ const derivedDestType =
1203
+ this.data.interaction && this.data.agentId
1204
+ ? calculateDestType(this.data.interaction, this.data.agentId)
1205
+ : '';
1206
+
1207
+ // derivedDestType is most reliable as it inspects live interaction participants
1208
+ const resolvedDestinationType =
1209
+ derivedDestType ||
1210
+ this.getStateMachineSnapshot()?.context?.consultDestinationType ||
1211
+ this.data.destinationType ||
1212
+ 'agent';
1213
+
1214
+ const consultationData: consultConferencePayloadData = {
1215
+ agentId: this.data.agentId,
1216
+ destinationType: resolvedDestinationType,
1217
+ // derivedDestAgentId is most reliable as it resolves epId for EP_DN
1218
+ // and agent ID for regular agents from live interaction data
1219
+ destAgentId:
1220
+ derivedDestAgentId ||
1221
+ this.getStateMachineSnapshot()?.context?.consultDestinationAgentId ||
1222
+ this.data.destAgentId,
1223
+ };
1224
+
1225
+ // Send state machine event to transition to CONF_INITIATING
1226
+ if (this.stateMachineService) {
1227
+ this.stateMachineService.send({
1228
+ type: TaskEvent.MERGE_TO_CONFERENCE,
1229
+ });
1230
+ }
1231
+
1232
+ try {
1233
+ if (!consultationData.destAgentId) {
1234
+ throw new Error('Unable to determine consult destination for conference');
1235
+ }
1236
+
1237
+ LoggerProxy.info(`Initiating consult conference to ${consultationData.destAgentId}`, {
1238
+ module: CC_FILE,
1239
+ method: METHODS.CONSULT_CONFERENCE,
1240
+ interactionId: this.data.interactionId,
1241
+ });
1242
+
1243
+ const paramsDataForConferenceV2 = buildConsultConferenceParamData(
1244
+ consultationData,
1245
+ this.data.interactionId
1246
+ );
1247
+
1248
+ const response = await this.contact.consultConference({
1249
+ interactionId: paramsDataForConferenceV2.interactionId,
1250
+ data: paramsDataForConferenceV2.data,
1251
+ });
1252
+
1253
+ // Send success event to transition to CONFERENCING
1254
+ if (this.stateMachineService) {
1255
+ this.stateMachineService.send({
1256
+ type: TaskEvent.CONFERENCE_START,
1257
+ });
1258
+ }
1259
+
1260
+ this.metricsManager.trackEvent(
1261
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_START_SUCCESS,
1262
+ {
1263
+ taskId: this.data.interactionId,
1264
+ destination: paramsDataForConferenceV2.data.to,
1265
+ destinationType: paramsDataForConferenceV2.data.destinationType,
1266
+ agentId: paramsDataForConferenceV2.data.agentId,
1267
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1268
+ },
1269
+ ['operational', 'behavioral', 'business']
1270
+ );
1271
+
1272
+ LoggerProxy.log(`Consult conference started successfully`, {
1273
+ module: CC_FILE,
1274
+ method: METHODS.CONSULT_CONFERENCE,
1275
+ interactionId: this.data.interactionId,
1276
+ });
1277
+
1278
+ return response;
1279
+ } catch (error) {
1280
+ // Send failure event to revert state
1281
+ if (this.stateMachineService) {
1282
+ this.stateMachineService.send({
1283
+ type: TaskEvent.CONFERENCE_FAILED,
1284
+ reason: error.toString(),
1285
+ });
1286
+ }
1287
+
1288
+ const {error: detailedError} = getErrorDetails(error, METHODS.CONSULT_CONFERENCE, CC_FILE);
1289
+
1290
+ const failedParamsData = buildConsultConferenceParamData(
1291
+ consultationData,
1292
+ this.data.interactionId
1293
+ );
1294
+
1295
+ this.metricsManager.trackEvent(
1296
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_START_FAILED,
1297
+ {
1298
+ taskId: this.data.interactionId,
1299
+ destination: failedParamsData.data.to,
1300
+ destinationType: failedParamsData.data.destinationType,
1301
+ agentId: failedParamsData.data.agentId,
1302
+ error: error.toString(),
1303
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
1304
+ },
1305
+ ['operational', 'behavioral', 'business']
1306
+ );
1307
+
1308
+ LoggerProxy.error(`Failed to start consult conference`, {
1309
+ module: CC_FILE,
1310
+ method: METHODS.CONSULT_CONFERENCE,
1311
+ interactionId: this.data.interactionId,
1312
+ });
1313
+
1314
+ throw detailedError;
1315
+ }
1316
+ }
1317
+
1318
+ /**
1319
+ * Removes another participant from an active conference.
1320
+ * Completion is correlated with ParticipantLeftConference by the AQM request layer.
1321
+ * @param payload - Participant identifier to remove
1322
+ * @returns Promise<TaskResponse>
1323
+ */
1324
+ public async dropConferenceParticipant(
1325
+ payload: DropConferenceParticipantPayload
1326
+ ): Promise<TaskResponse> {
1327
+ // The TypeScript contract requires payload, but the published SDK is also callable from
1328
+ // untyped JavaScript. Keep an explicit runtime boundary check before direct property access.
1329
+ if (!payload) {
1330
+ throw new Error('participantId must be a non-empty string');
1331
+ }
1332
+
1333
+ const {participantId} = payload;
1334
+
1335
+ if (typeof participantId !== 'string' || participantId.trim().length === 0) {
1336
+ throw new Error('participantId must be a non-empty string');
1337
+ }
1338
+
1339
+ const requestInteractionId =
1340
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
1341
+ // taskId identifies the SDK task instance, which can remain child-interaction keyed after
1342
+ // consult/EP-DN flows. requestInteractionId identifies the main interaction sent to AQM.
1343
+
1344
+ LoggerProxy.info('Dropping conference participant', {
1345
+ module: CC_FILE,
1346
+ method: METHODS.DROP_CONFERENCE_PARTICIPANT,
1347
+ interactionId: requestInteractionId,
1348
+ });
1349
+
1350
+ try {
1351
+ this.metricsManager.timeEvent([
1352
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS,
1353
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
1354
+ ]);
1355
+
1356
+ const response = await this.contact.dropConferenceParticipant({
1357
+ interactionId: requestInteractionId,
1358
+ participantId,
1359
+ });
1360
+
1361
+ this.metricsManager.trackEvent(
1362
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS,
1363
+ {
1364
+ taskId: this.data.interactionId,
1365
+ requestInteractionId,
1366
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1367
+ agentId: this.agentId,
1368
+ },
1369
+ ['operational', 'behavioral', 'business']
1370
+ );
1371
+
1372
+ LoggerProxy.log('Successfully dropped conference participant', {
1373
+ module: CC_FILE,
1374
+ method: METHODS.DROP_CONFERENCE_PARTICIPANT,
1375
+ trackingId: response?.trackingId,
1376
+ interactionId: requestInteractionId,
1377
+ });
1378
+
1379
+ return response;
1380
+ } catch (error) {
1381
+ const failureTrackingFields = MetricsManager.getCommonTrackingFieldForAQMResponseFailed(
1382
+ error?.details || {}
1383
+ );
1384
+
1385
+ this.metricsManager.trackEvent(
1386
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
1387
+ {
1388
+ taskId: this.data.interactionId,
1389
+ requestInteractionId,
1390
+ agentId: this.agentId,
1391
+ trackingId: failureTrackingFields.trackingId,
1392
+ notifTrackingId: failureTrackingFields.notifTrackingId,
1393
+ orgId: failureTrackingFields.orgId,
1394
+ failureType: failureTrackingFields.failureType,
1395
+ reasonCode: failureTrackingFields.reasonCode,
1396
+ },
1397
+ ['operational', 'behavioral', 'business']
1398
+ );
1399
+
1400
+ LoggerProxy.error('Failed to drop conference participant', {
1401
+ module: CC_FILE,
1402
+ method: METHODS.DROP_CONFERENCE_PARTICIPANT,
1403
+ trackingId: failureTrackingFields.trackingId,
1404
+ interactionId: requestInteractionId,
1405
+ });
1406
+
1407
+ throw error;
1408
+ }
1409
+ }
1410
+
1411
+ /**
1412
+ * Exit from a conference call.
1413
+ * Per conference-spec.md:
1414
+ * - Primary agent exits to wrapup
1415
+ * - Non-primary agent exits to available/connected
1416
+ * - Other participants continue the call
1417
+ *
1418
+ * @returns Promise<TaskResponse>
1419
+ * @throws Error if not in conference or exit fails
1420
+ * @example
1421
+ * ```typescript
1422
+ * task.exitConference().then(() => {}).catch(() => {});
1423
+ * ```
1424
+ */
1425
+ public async exitConference(): Promise<TaskResponse> {
1426
+ // Validate we're in conference state OR conference is in progress per task data
1427
+ // This handles cases where:
1428
+ // 1. State machine is in CONFERENCING state
1429
+ // 2. State machine is in CONNECTED but conference is active (e.g., ownership transferred)
1430
+ const state = this.getStateMachineSnapshot();
1431
+ const isConferencingState = state?.matches(TaskState.CONFERENCING);
1432
+
1433
+ const isConferenceInProgressFromData = this.data ? getIsConferenceInProgress(this.data) : false;
1434
+
1435
+ if (!state || (!isConferencingState && !isConferenceInProgressFromData)) {
1436
+ const currentState = state?.value as TaskState;
1437
+ const error = new Error(`Cannot exit conference in ${currentState} state`);
1438
+ LoggerProxy.error('Exit conference operation not allowed', {
1439
+ module: CC_FILE,
1440
+ method: 'exitConference',
1441
+ interactionId: this.data.interactionId,
1442
+ });
1443
+ throw error;
1444
+ }
1445
+
1446
+ // Send state machine event
1447
+ if (this.stateMachineService) {
1448
+ this.stateMachineService.send({
1449
+ type: TaskEvent.EXIT_CONFERENCE,
1450
+ agentId: this.data.agentId,
1451
+ });
1452
+ }
1453
+
1454
+ const requestInteractionId =
1455
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
1456
+
1457
+ try {
1458
+ LoggerProxy.info(`Exiting conference`, {
1459
+ module: CC_FILE,
1460
+ method: 'exitConference',
1461
+ interactionId: requestInteractionId,
1462
+ });
1463
+
1464
+ this.metricsManager.timeEvent([
1465
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_EXIT_SUCCESS,
1466
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_EXIT_FAILED,
1467
+ ]);
1468
+
1469
+ const response = await this.contact.exitConference({
1470
+ interactionId: requestInteractionId,
1471
+ });
1472
+
1473
+ // Send success event to transition state
1474
+ if (this.stateMachineService) {
1475
+ const responseTaskData = (response?.data ?? {}) as TaskData;
1476
+ const snapshot = this.stateMachineService.getSnapshot();
1477
+ const wrapUpContext = (snapshot?.context ?? {
1478
+ uiControlConfig: this.uiControlConfig,
1479
+ taskData: this.data,
1480
+ }) as TaskContext;
1481
+ const mergedTaskData = {
1482
+ ...this.data,
1483
+ ...responseTaskData,
1484
+ };
1485
+ // Pending-list precedence is response-only; do not keep a cached snapshot list.
1486
+ if (!Object.prototype.hasOwnProperty.call(responseTaskData, 'agentsPendingWrapUp')) {
1487
+ delete mergedTaskData.agentsPendingWrapUp;
1488
+ }
1489
+ let wrapUpRequired = shouldWrapUpForThisAgent(wrapUpContext, mergedTaskData);
1490
+ // Confirmed self-exit: empty/absent response pending matches TaskManager leave stamp.
1491
+ if (!wrapUpRequired && !hasNonemptyPendingWrapUp(responseTaskData)) {
1492
+ wrapUpRequired = !isConsultedNonOwner(mergedTaskData, this.data);
1493
+ }
1494
+
1495
+ this.stateMachineService.send({
1496
+ type: TaskEvent.EXIT_CONFERENCE_SUCCESS,
1497
+ taskData: {
1498
+ ...responseTaskData,
1499
+ wrapUpRequired,
1500
+ },
1501
+ });
1502
+ }
1503
+
1504
+ this.metricsManager.trackEvent(
1505
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_EXIT_SUCCESS,
1506
+ {
1507
+ taskId: this.data.interactionId,
1508
+ requestInteractionId,
1509
+ agentId: this.data.agentId,
1510
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1511
+ },
1512
+ ['operational', 'behavioral', 'business']
1513
+ );
1514
+
1515
+ LoggerProxy.log(`Successfully exited conference`, {
1516
+ module: CC_FILE,
1517
+ method: 'exitConference',
1518
+ trackingId: response.trackingId,
1519
+ interactionId: requestInteractionId,
1520
+ });
1521
+
1522
+ return response;
1523
+ } catch (error) {
1524
+ // Send failure event
1525
+ if (this.stateMachineService) {
1526
+ this.stateMachineService.send({
1527
+ type: TaskEvent.EXIT_CONFERENCE_FAILED,
1528
+ reason: error.toString(),
1529
+ });
1530
+ }
1531
+
1532
+ const {error: detailedError} = getErrorDetails(error, 'exitConference', CC_FILE);
1533
+ this.metricsManager.trackEvent(
1534
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_EXIT_FAILED,
1535
+ {
1536
+ taskId: this.data.interactionId,
1537
+ requestInteractionId,
1538
+ agentId: this.data.agentId,
1539
+ error: error.toString(),
1540
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
1541
+ },
1542
+ ['operational', 'behavioral', 'business']
1543
+ );
1544
+
1545
+ LoggerProxy.error(`Failed to exit conference`, {
1546
+ module: CC_FILE,
1547
+ method: 'exitConference',
1548
+ interactionId: requestInteractionId,
1549
+ });
1550
+
1551
+ throw detailedError;
1552
+ }
1553
+ }
1554
+
1555
+ /**
1556
+ * Transfer the conference to another participant.
1557
+ * Per conference-spec.md: Only primary agent can transfer conference.
1558
+ * After transfer, the transferring agent exits to wrapup.
1559
+ *
1560
+ * @returns Promise<TaskResponse>
1561
+ * @throws Error if not in conference or transfer fails
1562
+ * @example
1563
+ * ```typescript
1564
+ * task.transferConference().then(() => {}).catch(() => {});
1565
+ * ```
1566
+ */
1567
+ public async transferConference(): Promise<TaskResponse> {
1568
+ // Validate we're in conference or consulting state
1569
+ // CONSULTING is allowed because agent can transfer conference while consulting
1570
+ // (transfers ownership to the consulted agent)
1571
+ const state = this.getStateMachineSnapshot();
1572
+ const isValidState =
1573
+ state && (state.matches(TaskState.CONFERENCING) || state.matches(TaskState.CONSULTING));
1574
+ if (!isValidState) {
1575
+ const currentState = state?.value as TaskState;
1576
+ const error = new Error(`Cannot transfer conference in ${currentState} state`);
1577
+ LoggerProxy.error('Transfer conference operation not allowed', {
1578
+ module: CC_FILE,
1579
+ method: 'transferConference',
1580
+ interactionId: this.data.interactionId,
1581
+ });
1582
+ throw error;
1583
+ }
1584
+
1585
+ // Send state machine event
1586
+ if (this.stateMachineService) {
1587
+ this.stateMachineService.send({
1588
+ type: TaskEvent.TRANSFER_CONFERENCE,
1589
+ agentId: this.data.agentId,
1590
+ });
1591
+ }
1592
+
1593
+ const requestInteractionId =
1594
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
1595
+
1596
+ try {
1597
+ LoggerProxy.info(`Transferring conference`, {
1598
+ module: CC_FILE,
1599
+ method: 'transferConference',
1600
+ interactionId: requestInteractionId,
1601
+ });
1602
+
1603
+ this.metricsManager.timeEvent([
1604
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_TRANSFER_SUCCESS,
1605
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_TRANSFER_FAILED,
1606
+ ]);
1607
+
1608
+ const response = await this.contact.conferenceTransfer({
1609
+ interactionId: requestInteractionId,
1610
+ });
1611
+
1612
+ // Send success event to transition state
1613
+ if (this.stateMachineService) {
1614
+ this.stateMachineService.send({
1615
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
1616
+ taskData: response.data,
1617
+ });
1618
+ }
1619
+
1620
+ this.metricsManager.trackEvent(
1621
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_TRANSFER_SUCCESS,
1622
+ {
1623
+ taskId: this.data.interactionId,
1624
+ requestInteractionId,
1625
+ agentId: this.data.agentId,
1626
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1627
+ },
1628
+ ['operational', 'behavioral', 'business']
1629
+ );
1630
+
1631
+ LoggerProxy.log(`Successfully transferred conference`, {
1632
+ module: CC_FILE,
1633
+ method: 'transferConference',
1634
+ trackingId: response.trackingId,
1635
+ interactionId: requestInteractionId,
1636
+ });
1637
+
1638
+ return response;
1639
+ } catch (error) {
1640
+ // Send failure event
1641
+ if (this.stateMachineService) {
1642
+ this.stateMachineService.send({
1643
+ type: TaskEvent.TRANSFER_CONFERENCE_FAILED,
1644
+ reason: error.toString(),
1645
+ });
1646
+ }
1647
+
1648
+ const {error: detailedError} = getErrorDetails(error, 'transferConference', CC_FILE);
1649
+ this.metricsManager.trackEvent(
1650
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_TRANSFER_FAILED,
1651
+ {
1652
+ taskId: this.data.interactionId,
1653
+ requestInteractionId,
1654
+ agentId: this.data.agentId,
1655
+ error: error.toString(),
1656
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
1657
+ },
1658
+ ['operational', 'behavioral', 'business']
1659
+ );
1660
+
1661
+ LoggerProxy.error(`Failed to transfer conference`, {
1662
+ module: CC_FILE,
1663
+ method: 'transferConference',
1664
+ interactionId: requestInteractionId,
1665
+ });
1666
+
1667
+ throw detailedError;
1668
+ }
1669
+ }
1670
+
1671
+ /**
1672
+ * Toggle between consult call and main call during consulting.
1673
+ * If on consult leg (consultCallHeld = false), switches to main call by holding consult.
1674
+ * If on main call (consultCallHeld = true), switches to consult by resuming consult.
1675
+ *
1676
+ * @returns Promise<TaskResponse>
1677
+ * @throws Error if not in CONSULTING state or no consult media resource
1678
+ * @example
1679
+ * ```typescript
1680
+ * await task.switchCall();
1681
+ * ```
1682
+ */
1683
+ public async switchCall(): Promise<TaskResponse> {
1684
+ // Validate we're in CONSULTING state
1685
+ const state = this.getStateMachineSnapshot();
1686
+ if (!state?.matches(TaskState.CONSULTING)) {
1687
+ const currentState = state?.value as TaskState;
1688
+ const error = new Error(`Cannot switch call in ${currentState} state`);
1689
+ LoggerProxy.error('Switch call operation not allowed', {
1690
+ module: CC_FILE,
1691
+ method: 'switchCall',
1692
+ interactionId: this.data.interactionId,
1693
+ });
1694
+ throw error;
1695
+ }
1696
+
1697
+ // Validate we have a consult media resource
1698
+ const consultMediaResourceId = getConsultMediaResourceId(
1699
+ this.data.interaction,
1700
+ this.data.consultMediaResourceId,
1701
+ this.data.agentId
1702
+ );
1703
+ if (!consultMediaResourceId) {
1704
+ const error = new Error('No consult media resource available');
1705
+ LoggerProxy.error('Switch call failed - no consult media resource', {
1706
+ module: CC_FILE,
1707
+ method: 'switchCall',
1708
+ interactionId: this.data.interactionId,
1709
+ });
1710
+ throw error;
1711
+ }
1712
+
1713
+ const context = state.context;
1714
+ const isOnConsultLeg = !context.consultCallHeld;
1715
+
1716
+ // Determine direction and send appropriate state machine event
1717
+ const targetEvent = isOnConsultLeg
1718
+ ? TaskEvent.SWITCH_TO_MAIN_CALL
1719
+ : TaskEvent.SWITCH_TO_CONSULT;
1720
+ const revertEvent = isOnConsultLeg
1721
+ ? TaskEvent.SWITCH_TO_CONSULT
1722
+ : TaskEvent.SWITCH_TO_MAIN_CALL;
1723
+
1724
+ if (this.stateMachineService) {
1725
+ this.stateMachineService.send({type: targetEvent});
1726
+ }
1727
+
1728
+ this.metricsManager.timeEvent([
1729
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_SUCCESS,
1730
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_FAILED,
1731
+ ]);
1732
+
1733
+ try {
1734
+ if (isOnConsultLeg) {
1735
+ const response = await this.contact.unHold({
1736
+ interactionId: this.data.interactionId,
1737
+ data: {mediaResourceId: this.data.mediaResourceId},
1738
+ });
1739
+
1740
+ this.metricsManager.trackEvent(
1741
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_SUCCESS,
1742
+ {
1743
+ taskId: this.data.interactionId,
1744
+ direction: 'toMainCall',
1745
+ mediaResourceId: consultMediaResourceId,
1746
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1747
+ },
1748
+ ['operational', 'behavioral']
1749
+ );
1750
+
1751
+ LoggerProxy.log(`Switched to main call successfully`, {
1752
+ module: CC_FILE,
1753
+ method: 'switchCall',
1754
+ trackingId: response.trackingId,
1755
+ interactionId: this.data.interactionId,
1756
+ });
1757
+
1758
+ return response;
1759
+ }
1760
+
1761
+ const response = await this.contact.hold({
1762
+ interactionId: this.data.interactionId,
1763
+ data: {mediaResourceId: this.data.mediaResourceId},
1764
+ });
1765
+
1766
+ this.metricsManager.trackEvent(
1767
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_SUCCESS,
1768
+ {
1769
+ taskId: this.data.interactionId,
1770
+ direction: 'toConsultCall',
1771
+ mediaResourceId: consultMediaResourceId,
1772
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1773
+ },
1774
+ ['operational', 'behavioral']
1775
+ );
1776
+
1777
+ LoggerProxy.log(`Switched to consult call successfully`, {
1778
+ module: CC_FILE,
1779
+ method: 'switchCall',
1780
+ trackingId: response.trackingId,
1781
+ interactionId: this.data.interactionId,
1782
+ });
1783
+
1784
+ return response;
1785
+ } catch (error) {
1786
+ if (this.stateMachineService) {
1787
+ this.stateMachineService.send({type: revertEvent});
1788
+ }
1789
+
1790
+ this.metricsManager.trackEvent(
1791
+ METRIC_EVENT_NAMES.TASK_SWITCH_CALL_FAILED,
1792
+ {
1793
+ taskId: this.data.interactionId,
1794
+ direction: isOnConsultLeg ? 'toMainCall' : 'toConsultCall',
1795
+ mediaResourceId: consultMediaResourceId,
1796
+ error: error.toString(),
1797
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details || {}),
1798
+ },
1799
+ ['operational', 'behavioral']
1800
+ );
1801
+
1802
+ const {error: detailedError} = getErrorDetails(error, 'switchCall', CC_FILE);
1803
+ LoggerProxy.error(`Failed to switch call`, {
1804
+ module: CC_FILE,
1805
+ method: 'switchCall',
1806
+ interactionId: this.data.interactionId,
1807
+ });
1808
+ throw detailedError;
1809
+ }
1810
+ }
1811
+
1812
+ protected override getChannelSpecificActionOverrides() {
1813
+ const baseOverrides = super.getChannelSpecificActionOverrides();
1814
+
1815
+ return {
1816
+ ...baseOverrides,
1817
+ emitTaskHold: this.createEmitSelfAction(TASK_EVENTS.TASK_HOLD, {updateTaskData: true}),
1818
+ emitTaskResume: this.createEmitSelfAction(TASK_EVENTS.TASK_RESUME, {updateTaskData: true}),
1819
+ emitTaskRecordingStarted: this.createEmitSelfAction(TASK_EVENTS.TASK_RECORDING_STARTED, {
1820
+ updateTaskData: true,
1821
+ }),
1822
+ emitTaskRecordingPaused: this.createEmitSelfAction(TASK_EVENTS.TASK_RECORDING_PAUSED, {
1823
+ updateTaskData: true,
1824
+ }),
1825
+ emitTaskRecordingPauseFailed: this.createEmitSelfAction(
1826
+ TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED,
1827
+ {updateTaskData: true}
1828
+ ),
1829
+ emitTaskRecordingResumed: this.createEmitSelfAction(TASK_EVENTS.TASK_RECORDING_RESUMED, {
1830
+ updateTaskData: true,
1831
+ }),
1832
+ emitTaskRecordingResumeFailed: this.createEmitSelfAction(
1833
+ TASK_EVENTS.TASK_RECORDING_RESUME_FAILED,
1834
+ {updateTaskData: true}
1835
+ ),
1836
+ // Conference event emitters
1837
+ emitTaskParticipantJoined: this.createEmitSelfAction(TASK_EVENTS.TASK_PARTICIPANT_JOINED, {
1838
+ updateTaskData: true,
1839
+ }),
1840
+ emitTaskParticipantLeft: this.createEmitSelfAction(TASK_EVENTS.TASK_PARTICIPANT_LEFT, {
1841
+ updateTaskData: true,
1842
+ }),
1843
+ emitTaskConferenceStarted: this.createEmitSelfAction(TASK_EVENTS.TASK_CONFERENCE_STARTED, {
1844
+ updateTaskData: true,
1845
+ }),
1846
+ emitTaskConferenceEnded: this.createEmitSelfAction(TASK_EVENTS.TASK_CONFERENCE_ENDED, {
1847
+ updateTaskData: true,
1848
+ }),
1849
+ emitTaskConferenceFailed: this.createEmitSelfAction(TASK_EVENTS.TASK_CONFERENCE_FAILED, {
1850
+ updateTaskData: true,
1851
+ }),
1852
+ emitTaskExitConference: this.createEmitSelfAction(TASK_EVENTS.TASK_EXIT_CONFERENCE, {
1853
+ updateTaskData: false,
1854
+ }),
1855
+ emitTaskTransferConference: this.createEmitSelfAction(TASK_EVENTS.TASK_TRANSFER_CONFERENCE, {
1856
+ updateTaskData: false,
1857
+ }),
1858
+ emitTaskSwitchCall: this.createEmitSelfAction(TASK_EVENTS.TASK_SWITCH_CALL, {
1859
+ updateTaskData: false,
1860
+ }),
1861
+ emitTaskTransferConferenceFailed: this.createEmitSelfAction(
1862
+ TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED,
1863
+ {updateTaskData: true}
1864
+ ),
1865
+ emitTaskOutdialFailed: ({event}: TaskActionArgs) => {
1866
+ const taskData = (event as {taskData?: TaskData})?.taskData;
1867
+ if (taskData) {
1868
+ this.updateTaskData(taskData);
1869
+ }
1870
+ if (taskData?.suppressOutdialFailedPopup) {
1871
+ return;
1872
+ }
1873
+ const reason = (event as {reason?: string})?.reason || 'Outdial failed';
1874
+ this.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, reason);
1875
+ },
1876
+ };
1877
+ }
1878
+ }