@webex/contact-center 3.11.0 → 3.12.0-auth-prejoin-fetch.1

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 (325) hide show
  1. package/.sdd/manifest.json +883 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +50 -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 +48 -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 +359 -0
  17. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  18. package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
  19. package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
  20. package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
  21. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  22. package/ai-docs/patterns/testing-patterns.md +480 -0
  23. package/ai-docs/patterns/typescript-patterns.md +365 -0
  24. package/ai-docs/templates/README.md +102 -0
  25. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  26. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  27. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  28. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  29. package/ai-docs/templates/new-method/00-master.md +80 -0
  30. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  31. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  32. package/ai-docs/templates/new-method/03-tests.md +201 -0
  33. package/ai-docs/templates/new-method/04-validation.md +141 -0
  34. package/ai-docs/templates/new-service/00-master.md +109 -0
  35. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  36. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  37. package/ai-docs/templates/new-service/03-integration.md +178 -0
  38. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  39. package/ai-docs/templates/new-service/05-validation.md +145 -0
  40. package/dist/cc.js +818 -59
  41. package/dist/cc.js.map +1 -1
  42. package/dist/config.js +13 -0
  43. package/dist/config.js.map +1 -1
  44. package/dist/constants.js +31 -3
  45. package/dist/constants.js.map +1 -1
  46. package/dist/index.js +27 -5
  47. package/dist/index.js.map +1 -1
  48. package/dist/metrics/behavioral-events.js +127 -0
  49. package/dist/metrics/behavioral-events.js.map +1 -1
  50. package/dist/metrics/constants.js +34 -3
  51. package/dist/metrics/constants.js.map +1 -1
  52. package/dist/services/AddressBook.js +18 -15
  53. package/dist/services/AddressBook.js.map +1 -1
  54. package/dist/services/AnswerCallOnWebexService.js +174 -0
  55. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  56. package/dist/services/ApiAiAssistant.js +318 -0
  57. package/dist/services/ApiAiAssistant.js.map +1 -0
  58. package/dist/services/EntryPoint.js +46 -68
  59. package/dist/services/EntryPoint.js.map +1 -1
  60. package/dist/services/Queue.js +27 -22
  61. package/dist/services/Queue.js.map +1 -1
  62. package/dist/services/UserPreference.js +427 -0
  63. package/dist/services/UserPreference.js.map +1 -0
  64. package/dist/services/WebexCrossClientService.js +171 -0
  65. package/dist/services/WebexCrossClientService.js.map +1 -0
  66. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  67. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  68. package/dist/services/agent/types.js.map +1 -1
  69. package/dist/services/config/Util.js +11 -4
  70. package/dist/services/config/Util.js.map +1 -1
  71. package/dist/services/config/constants.js +45 -8
  72. package/dist/services/config/constants.js.map +1 -1
  73. package/dist/services/config/index.js +41 -2
  74. package/dist/services/config/index.js.map +1 -1
  75. package/dist/services/config/types.js +70 -8
  76. package/dist/services/config/types.js.map +1 -1
  77. package/dist/services/constants.js +27 -1
  78. package/dist/services/constants.js.map +1 -1
  79. package/dist/services/core/Err.js.map +1 -1
  80. package/dist/services/core/Utils.js +122 -25
  81. package/dist/services/core/Utils.js.map +1 -1
  82. package/dist/services/core/WebexRequest.js +6 -2
  83. package/dist/services/core/WebexRequest.js.map +1 -1
  84. package/dist/services/core/aqm-reqs.js +119 -30
  85. package/dist/services/core/aqm-reqs.js.map +1 -1
  86. package/dist/services/core/types.js.map +1 -1
  87. package/dist/services/core/websocket/WebSocketManager.js +22 -6
  88. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  89. package/dist/services/core/websocket/connection-service.js +3 -1
  90. package/dist/services/core/websocket/connection-service.js.map +1 -1
  91. package/dist/services/core/websocket/types.js.map +1 -1
  92. package/dist/services/index.js +6 -0
  93. package/dist/services/index.js.map +1 -1
  94. package/dist/services/task/Task.js +754 -0
  95. package/dist/services/task/Task.js.map +1 -0
  96. package/dist/services/task/TaskFactory.js +49 -0
  97. package/dist/services/task/TaskFactory.js.map +1 -0
  98. package/dist/services/task/TaskManager.js +1073 -447
  99. package/dist/services/task/TaskManager.js.map +1 -1
  100. package/dist/services/task/TaskUtils.js +220 -23
  101. package/dist/services/task/TaskUtils.js.map +1 -1
  102. package/dist/services/task/WebexCallingUtils.js +70 -0
  103. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  104. package/dist/services/task/constants.js +26 -2
  105. package/dist/services/task/constants.js.map +1 -1
  106. package/dist/services/task/contact.js +29 -0
  107. package/dist/services/task/contact.js.map +1 -1
  108. package/dist/services/task/dialer.js +129 -0
  109. package/dist/services/task/dialer.js.map +1 -1
  110. package/dist/services/task/digital/Digital.js +78 -0
  111. package/dist/services/task/digital/Digital.js.map +1 -0
  112. package/dist/services/task/state-machine/TaskStateMachine.js +971 -0
  113. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  114. package/dist/services/task/state-machine/actions.js +572 -0
  115. package/dist/services/task/state-machine/actions.js.map +1 -0
  116. package/dist/services/task/state-machine/constants.js +161 -0
  117. package/dist/services/task/state-machine/constants.js.map +1 -0
  118. package/dist/services/task/state-machine/guards.js +409 -0
  119. package/dist/services/task/state-machine/guards.js.map +1 -0
  120. package/dist/services/task/state-machine/index.js +53 -0
  121. package/dist/services/task/state-machine/index.js.map +1 -0
  122. package/dist/services/task/state-machine/types.js +54 -0
  123. package/dist/services/task/state-machine/types.js.map +1 -0
  124. package/dist/services/task/state-machine/uiControlsComputer.js +703 -0
  125. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  126. package/dist/services/task/taskDataNormalizer.js +99 -0
  127. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  128. package/dist/services/task/types.js +243 -5
  129. package/dist/services/task/types.js.map +1 -1
  130. package/dist/services/task/voice/Voice.js +1380 -0
  131. package/dist/services/task/voice/Voice.js.map +1 -0
  132. package/dist/services/task/voice/WebRTC.js +152 -0
  133. package/dist/services/task/voice/WebRTC.js.map +1 -0
  134. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  135. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  136. package/dist/services/wxAppTelephonyUtils.js +19 -0
  137. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  138. package/dist/types/cc.d.ts +967 -0
  139. package/dist/types/config.d.ts +79 -0
  140. package/dist/types/constants.d.ts +74 -0
  141. package/dist/types/index.d.ts +201 -0
  142. package/dist/types/logger-proxy.d.ts +71 -0
  143. package/dist/types/metrics/MetricsManager.d.ts +223 -0
  144. package/dist/types/metrics/behavioral-events.d.ts +29 -0
  145. package/dist/types/metrics/constants.d.ts +183 -0
  146. package/dist/types/services/AddressBook.d.ts +75 -0
  147. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  148. package/dist/types/services/ApiAiAssistant.d.ts +49 -0
  149. package/dist/types/services/EntryPoint.d.ts +69 -0
  150. package/dist/types/services/Queue.d.ts +78 -0
  151. package/dist/types/services/UserPreference.d.ts +118 -0
  152. package/dist/types/services/WebCallingService.d.ts +1 -0
  153. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  154. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  155. package/dist/types/services/agent/index.d.ts +46 -0
  156. package/dist/types/services/agent/types.d.ts +413 -0
  157. package/dist/types/services/config/Util.d.ts +20 -0
  158. package/dist/types/services/config/constants.d.ts +273 -0
  159. package/dist/types/services/config/index.d.ts +177 -0
  160. package/dist/types/services/config/types.d.ts +1381 -0
  161. package/dist/types/services/constants.d.ts +110 -0
  162. package/dist/types/services/core/Err.d.ts +127 -0
  163. package/dist/types/services/core/GlobalTypes.d.ts +58 -0
  164. package/dist/types/services/core/Utils.d.ts +121 -0
  165. package/dist/types/services/core/WebexRequest.d.ts +23 -0
  166. package/dist/types/services/core/aqm-reqs.d.ts +65 -0
  167. package/dist/types/services/core/constants.d.ts +99 -0
  168. package/dist/types/services/core/types.d.ts +49 -0
  169. package/dist/types/services/core/websocket/WebSocketManager.d.ts +36 -0
  170. package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
  171. package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
  172. package/dist/types/services/core/websocket/types.d.ts +37 -0
  173. package/dist/types/services/index.d.ts +54 -0
  174. package/dist/types/services/task/AutoWrapup.d.ts +40 -0
  175. package/dist/types/services/task/Task.d.ts +175 -0
  176. package/dist/types/services/task/TaskFactory.d.ts +13 -0
  177. package/dist/types/services/task/TaskManager.d.ts +1 -0
  178. package/dist/types/services/task/TaskUtils.d.ts +138 -0
  179. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  180. package/dist/types/services/task/constants.d.ts +94 -0
  181. package/dist/types/services/task/contact.d.ts +73 -0
  182. package/dist/types/services/task/dialer.d.ts +73 -0
  183. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  184. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1398 -0
  185. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  186. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  187. package/dist/types/services/task/state-machine/guards.d.ts +103 -0
  188. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  189. package/dist/types/services/task/state-machine/types.d.ts +277 -0
  190. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  191. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  192. package/dist/types/services/task/types.d.ts +1933 -0
  193. package/dist/types/services/task/voice/Voice.d.ts +223 -0
  194. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  195. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  196. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  197. package/dist/types/types.d.ts +784 -0
  198. package/dist/types/utils/PageCache.d.ts +190 -0
  199. package/dist/types/webex-config.d.ts +53 -0
  200. package/dist/types/webex.d.ts +8 -0
  201. package/dist/types.js +137 -3
  202. package/dist/types.js.map +1 -1
  203. package/dist/utils/PageCache.js +19 -5
  204. package/dist/utils/PageCache.js.map +1 -1
  205. package/dist/webex.js +14 -2
  206. package/dist/webex.js.map +1 -1
  207. package/package.json +16 -12
  208. package/src/cc.ts +983 -60
  209. package/src/config.ts +13 -0
  210. package/src/constants.ts +29 -1
  211. package/src/index.ts +26 -5
  212. package/src/metrics/ai-docs/AGENTS.md +350 -0
  213. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  214. package/src/metrics/ai-docs/metrics-spec.md +860 -0
  215. package/src/metrics/behavioral-events.ts +134 -0
  216. package/src/metrics/constants.ts +39 -3
  217. package/src/services/AddressBook.ts +17 -6
  218. package/src/services/AnswerCallOnWebexService.ts +206 -0
  219. package/src/services/ApiAiAssistant.ts +412 -0
  220. package/src/services/EntryPoint.ts +59 -60
  221. package/src/services/Queue.ts +29 -12
  222. package/src/services/UserPreference.ts +509 -0
  223. package/src/services/WebexCrossClientService.ts +212 -0
  224. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  225. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  226. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  227. package/src/services/agent/ai-docs/agent-spec.md +504 -0
  228. package/src/services/agent/types.ts +1 -1
  229. package/src/services/ai-docs/AGENTS.md +386 -0
  230. package/src/services/ai-docs/services-spec.md +497 -0
  231. package/src/services/config/Util.ts +13 -2
  232. package/src/services/config/ai-docs/AGENTS.md +255 -0
  233. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  234. package/src/services/config/ai-docs/config-spec.md +675 -0
  235. package/src/services/config/constants.ts +47 -7
  236. package/src/services/config/index.ts +45 -1
  237. package/src/services/config/types.ts +253 -11
  238. package/src/services/constants.ts +29 -0
  239. package/src/services/core/Err.ts +4 -0
  240. package/src/services/core/Utils.ts +143 -30
  241. package/src/services/core/WebexRequest.ts +3 -1
  242. package/src/services/core/ai-docs/AGENTS.md +381 -0
  243. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  244. package/src/services/core/ai-docs/core-spec.md +787 -0
  245. package/src/services/core/aqm-reqs.ts +125 -32
  246. package/src/services/core/types.ts +2 -0
  247. package/src/services/core/websocket/WebSocketManager.ts +23 -6
  248. package/src/services/core/websocket/connection-service.ts +5 -1
  249. package/src/services/core/websocket/types.ts +1 -1
  250. package/src/services/index.ts +4 -0
  251. package/src/services/task/Task.ts +908 -0
  252. package/src/services/task/TaskFactory.ts +60 -0
  253. package/src/services/task/TaskManager.ts +1291 -513
  254. package/src/services/task/TaskUtils.ts +314 -24
  255. package/src/services/task/WebexCallingUtils.ts +136 -0
  256. package/src/services/task/ai-docs/AGENTS.md +457 -0
  257. package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
  258. package/src/services/task/ai-docs/task-spec.md +1469 -0
  259. package/src/services/task/constants.ts +26 -0
  260. package/src/services/task/contact.ts +30 -0
  261. package/src/services/task/dialer.ts +136 -1
  262. package/src/services/task/digital/Digital.ts +97 -0
  263. package/src/services/task/state-machine/TaskStateMachine.ts +1313 -0
  264. package/src/services/task/state-machine/actions.ts +741 -0
  265. package/src/services/task/state-machine/ai-docs/AGENTS.md +462 -0
  266. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1146 -0
  267. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2209 -0
  268. package/src/services/task/state-machine/constants.ts +172 -0
  269. package/src/services/task/state-machine/guards.ts +498 -0
  270. package/src/services/task/state-machine/index.ts +28 -0
  271. package/src/services/task/state-machine/types.ts +258 -0
  272. package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
  273. package/src/services/task/taskDataNormalizer.ts +137 -0
  274. package/src/services/task/types.ts +843 -70
  275. package/src/services/task/voice/Voice.ts +1720 -0
  276. package/src/services/task/voice/WebRTC.ts +191 -0
  277. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  278. package/src/services/wxAppTelephonyUtils.ts +14 -0
  279. package/src/types.ts +238 -11
  280. package/src/utils/AGENTS.md +289 -0
  281. package/src/utils/PageCache.ts +38 -5
  282. package/src/utils/ai-docs/utils-spec.md +391 -0
  283. package/src/webex.js +2 -0
  284. package/test/unit/spec/cc.ts +1856 -122
  285. package/test/unit/spec/logger-proxy.ts +70 -0
  286. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  287. package/test/unit/spec/services/AddressBook.ts +37 -6
  288. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  289. package/test/unit/spec/services/ApiAiAssistant.ts +273 -0
  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 +7 -1
  294. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  295. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  296. package/test/unit/spec/services/config/Util.ts +85 -0
  297. package/test/unit/spec/services/config/index.ts +85 -29
  298. package/test/unit/spec/services/core/Utils.ts +481 -2
  299. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  300. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  301. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +137 -41
  302. package/test/unit/spec/services/core/websocket/connection-service.ts +3 -1
  303. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  304. package/test/unit/spec/services/task/Task.ts +677 -0
  305. package/test/unit/spec/services/task/TaskFactory.ts +99 -0
  306. package/test/unit/spec/services/task/TaskManager.ts +2209 -918
  307. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  308. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  309. package/test/unit/spec/services/task/contact.ts +33 -0
  310. package/test/unit/spec/services/task/dialer.ts +372 -96
  311. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  312. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +3433 -0
  313. package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
  314. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  315. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
  316. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  317. package/test/unit/spec/services/task/voice/Voice.ts +1523 -0
  318. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  319. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  320. package/umd/contact-center.min.js +2 -2
  321. package/umd/contact-center.min.js.map +1 -1
  322. package/dist/services/task/index.js +0 -1525
  323. package/dist/services/task/index.js.map +0 -1
  324. package/src/services/task/index.ts +0 -1801
  325. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -3,23 +3,46 @@ import {ICall, LINE_EVENTS} from '@webex/calling';
3
3
  import {WebSocketManager} from '../core/websocket/WebSocketManager';
4
4
  import routingContact from './contact';
5
5
  import WebCallingService from '../WebCallingService';
6
- import {ITask, MEDIA_CHANNEL, TASK_EVENTS, TaskData, TaskId} from './types';
6
+ import {
7
+ MEDIA_CHANNEL,
8
+ TASK_EVENTS,
9
+ TaskData,
10
+ TaskId,
11
+ ITask,
12
+ WebSocketPayload,
13
+ WebSocketMessage,
14
+ TaskEventActions,
15
+ EventContext,
16
+ } from './types';
7
17
  import {TASK_MANAGER_FILE} from '../../constants';
8
- import {METHODS} from './constants';
9
- import {CC_EVENTS, CC_TASK_EVENTS, WrapupData} from '../config/types';
10
- import {LoginOption} from '../../types';
18
+ import {METHODS, TRANSCRIPT_EVENT_MAP} from './constants';
19
+ import {CC_EVENTS, WrapupData} from '../config/types';
20
+ import {ConfigFlags, LoginOption, AIAssistantEventType, AIAssistantEventName} from '../../types';
11
21
  import LoggerProxy from '../../logger-proxy';
12
- import Task from '.';
13
- import MetricsManager from '../../metrics/MetricsManager';
14
- import {METRIC_EVENT_NAMES} from '../../metrics/constants';
15
22
  import {
16
- checkParticipantNotInInteraction,
17
23
  getIsConferenceInProgress,
18
- isParticipantInMainInteraction,
19
- isPrimary,
24
+ isCampaignPreviewTask,
25
+ isCampaignPreviewReservation,
20
26
  isSecondaryEpDnAgent,
21
27
  shouldAutoAnswerTask,
22
28
  } from './TaskUtils';
29
+ import TaskFactory from './TaskFactory';
30
+ import AnswerCallOnWebexService from '../AnswerCallOnWebexService';
31
+ import {getWebexCallingDeviceDetailsForAgent} from './WebexCallingUtils';
32
+ import WebRTC from './voice/WebRTC';
33
+ import {TaskEvent, type TaskEventPayload} from './state-machine';
34
+ import {MEDIA_TYPE_MAIN_CALL} from './state-machine/constants';
35
+ import {normalizeTaskData} from './taskDataNormalizer';
36
+ import {ApiAIAssistant} from '../ApiAiAssistant';
37
+
38
+ const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
39
+
40
+ const MAIN_INTERACTION_CORRELATED_EVENTS = new Set<CC_EVENTS>([
41
+ CC_EVENTS.AGENT_CONSULT_ENDED,
42
+ CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
43
+ ]);
44
+
45
+ const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
23
46
 
24
47
  /** @internal */
25
48
  export default class TaskManager extends EventEmitter {
@@ -33,35 +56,90 @@ export default class TaskManager extends EventEmitter {
33
56
  private taskCollection: Record<TaskId, ITask>;
34
57
  private webCallingService: WebCallingService;
35
58
  private webSocketManager: WebSocketManager;
36
- private metricsManager: MetricsManager;
37
- private static taskManager;
59
+ private rtdWebSocketManager: WebSocketManager;
60
+ // eslint-disable-next-line no-use-before-define
61
+ private static taskManager: TaskManager;
62
+ private configFlags?: ConfigFlags;
38
63
  private wrapupData: WrapupData;
39
64
  private agentId: string;
40
65
  private webRtcEnabled: boolean;
66
+ private answerCallOnWebexService?: AnswerCallOnWebexService;
67
+ private apiAIAssistant?: ApiAIAssistant;
41
68
  /**
42
69
  * @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
43
70
  * @param webCallingService - Webrtc Service Layer
44
71
  * @param webSocketManager - Websocket Manager to maintain websocket connection and keepalives
45
72
  */
46
73
  constructor(
74
+ apiAIAssistant: ApiAIAssistant,
47
75
  contact: ReturnType<typeof routingContact>,
48
76
  webCallingService: WebCallingService,
49
- webSocketManager: WebSocketManager
77
+ webSocketManager: WebSocketManager,
78
+ rtdWebSocketManager: WebSocketManager
50
79
  ) {
51
80
  super();
81
+ this.apiAIAssistant = apiAIAssistant;
52
82
  this.contact = contact;
53
- this.taskCollection = {};
54
83
  this.webCallingService = webCallingService;
55
84
  this.webSocketManager = webSocketManager;
56
- this.metricsManager = MetricsManager.getInstance();
85
+ this.rtdWebSocketManager = rtdWebSocketManager;
86
+ this.taskCollection = {};
87
+ this.webRtcEnabled = false;
88
+
57
89
  this.registerTaskListeners();
58
90
  this.registerIncomingCallEvent();
59
91
  }
60
92
 
93
+ public handleRealtimeWebsocketEvent(event: string) {
94
+ try {
95
+ const payload = JSON.parse(event);
96
+
97
+ const interactionId = payload?.data?.data?.conversationId;
98
+ if (!interactionId) return;
99
+
100
+ const task = this.taskCollection[interactionId];
101
+ if (!task) {
102
+ LoggerProxy.info(`Realtime transcription task not found`, {
103
+ module: TASK_MANAGER_FILE,
104
+ method: METHODS.HANDLE_REAL_TIME_WEBSOCKET_EVENT,
105
+ interactionId,
106
+ });
107
+
108
+ return;
109
+ }
110
+
111
+ switch (payload.type) {
112
+ case CC_EVENTS.REAL_TIME_TRANSCRIPTION:
113
+ case CC_EVENTS.SUGGESTED_RESPONSE:
114
+ task.emit(payload.type, payload.data);
115
+ break;
116
+ }
117
+ } catch (error) {
118
+ LoggerProxy.error('Failed to parse RTD WebSocket message', {
119
+ module: TASK_MANAGER_FILE,
120
+ method: METHODS.HANDLE_REAL_TIME_WEBSOCKET_EVENT,
121
+ error,
122
+ });
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Set config flags for task creation
128
+ */
129
+ public setConfigFlags(configFlags: ConfigFlags) {
130
+ this.configFlags = configFlags;
131
+ }
132
+
133
+ /**
134
+ * Set wrapup configuration data
135
+ */
61
136
  public setWrapupData(wrapupData: WrapupData) {
62
137
  this.wrapupData = wrapupData;
63
138
  }
64
139
 
140
+ /**
141
+ * Set agent ID for task operations
142
+ */
65
143
  public setAgentId(agentId: string) {
66
144
  this.agentId = agentId;
67
145
  }
@@ -79,9 +157,14 @@ export default class TaskManager extends EventEmitter {
79
157
  this.webRtcEnabled = webRtcEnabled;
80
158
  }
81
159
 
160
+ public setAnswerCallOnWebexService(answerCallOnWebexService: AnswerCallOnWebexService) {
161
+ this.answerCallOnWebexService = answerCallOnWebexService;
162
+ }
163
+
82
164
  private handleIncomingWebCall = (call: ICall) => {
83
165
  const currentTask = Object.values(this.taskCollection).find(
84
- (task) => task.data.interaction.mediaType === 'telephony'
166
+ (task) =>
167
+ task.data.interaction.mediaType === 'telephony' && !isCampaignPreviewReservation(task)
85
168
  );
86
169
 
87
170
  if (currentTask) {
@@ -91,7 +174,15 @@ export default class TaskManager extends EventEmitter {
91
174
  method: METHODS.HANDLE_INCOMING_WEB_CALL,
92
175
  interactionId: currentTask.data.interactionId,
93
176
  });
94
- this.emit(TASK_EVENTS.TASK_INCOMING, currentTask);
177
+
178
+ // Send TASK_INCOMING to state machine - it will emit on the task object
179
+ const eventPayload = TaskManager.mapEventToTaskStateMachineEvent(
180
+ CC_EVENTS.AGENT_CONTACT_RESERVED,
181
+ currentTask.data
182
+ );
183
+ if (eventPayload && currentTask) {
184
+ currentTask.sendStateMachineEvent(eventPayload);
185
+ }
95
186
  }
96
187
  this.call = call;
97
188
  };
@@ -104,473 +195,1084 @@ export default class TaskManager extends EventEmitter {
104
195
  this.webCallingService.off(LINE_EVENTS.INCOMING_CALL, this.handleIncomingWebCall);
105
196
  }
106
197
 
107
- private registerTaskListeners() {
108
- this.webSocketManager.on('message', (event) => {
109
- const payload = JSON.parse(event);
110
- // Re-emit the task events to the task object
111
- let task: ITask;
112
- if (payload.data?.type) {
113
- if (Object.values(CC_TASK_EVENTS).includes(payload.data.type)) {
114
- task = this.taskCollection[payload.data.interactionId];
198
+ /**
199
+ * Map WebSocket CC_EVENTS to state machine TaskEvent
200
+ * @param ccEvent - The CC_EVENT type from WebSocket
201
+ * @param payload - The event payload
202
+ * @param agentId - Optional agent ID for state detection (needed for HYDRATE)
203
+ * @param task - Optional task instance for state-aware CONTACT_ENDED mapping
204
+ * @returns TaskEventPayload for state machine or null if no mapping
205
+ */
206
+ private static mapEventToTaskStateMachineEvent(
207
+ ccEvent: CC_EVENTS,
208
+ payload: WebSocketPayload,
209
+ agentId?: string,
210
+ task?: ITask,
211
+ enableWxBetterTogether?: boolean
212
+ ): TaskEventPayload | null {
213
+ const mediaResourceId =
214
+ payload.mediaResourceId ||
215
+ payload.interaction?.media?.[payload.interactionId]?.mediaResourceId;
216
+
217
+ switch (ccEvent) {
218
+ // CC -> TaskEvent mappings (see TaskStateMachine comment for quick reference)
219
+ case CC_EVENTS.AGENT_CONTACT_RESERVED: // AgentContactReserved -> TASK_INCOMING
220
+ if (isCampaignPreviewTask(payload)) {
221
+ return {
222
+ type: TaskEvent.TASK_INCOMING,
223
+ taskData: payload,
224
+ isCampaignReservationAccept: true,
225
+ };
115
226
  }
116
- LoggerProxy.info(`Handling task event ${payload.data?.type}`, {
117
- module: TASK_MANAGER_FILE,
118
- method: METHODS.REGISTER_TASK_LISTENERS,
119
- interactionId: payload.data?.interactionId,
120
- });
121
- switch (payload.data.type) {
122
- case CC_EVENTS.AGENT_CONTACT:
123
- // Case1 : Task is already present in taskCollection
124
- if (this.taskCollection[payload.data.interactionId]) {
125
- LoggerProxy.log(`Got AGENT_CONTACT: Task already exists in collection`, {
126
- module: TASK_MANAGER_FILE,
127
- method: METHODS.REGISTER_TASK_LISTENERS,
128
- interactionId: payload.data.interactionId,
129
- });
130
- break;
131
- } else if (!this.taskCollection[payload.data.interactionId]) {
132
- // Case2 : Task is not present in taskCollection
133
- LoggerProxy.log(`Got AGENT_CONTACT : Creating new task in taskManager`, {
134
- module: TASK_MANAGER_FILE,
135
- method: METHODS.REGISTER_TASK_LISTENERS,
136
- interactionId: payload.data.interactionId,
137
- });
138
-
139
- // Check if auto-answer should happen for this task
140
- const shouldAutoAnswer = shouldAutoAnswerTask(
141
- payload.data,
142
- this.agentId,
143
- this.webCallingService.loginOption,
144
- this.webRtcEnabled
145
- );
146
-
147
- task = new Task(
148
- this.contact,
149
- this.webCallingService,
150
- {
151
- ...payload.data,
152
- wrapUpRequired:
153
- payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
154
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
155
- isAutoAnswering: shouldAutoAnswer, // Set flag before emitting
156
- },
157
- this.wrapupData,
158
- this.agentId
159
- );
160
- this.taskCollection[payload.data.interactionId] = task;
161
- // Condition 1: The state is=new i.e it is a incoming task
162
- if (payload.data.interaction.state === 'new') {
163
- LoggerProxy.log(
164
- `Got AGENT_CONTACT for a task with state=new, sending TASK_INCOMING event`,
165
- {
166
- module: TASK_MANAGER_FILE,
167
- method: METHODS.REGISTER_TASK_LISTENERS,
168
- interactionId: payload.data.interactionId,
169
- }
170
- );
171
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
172
- } else {
173
- // Condition 2: The state is anything else i.e the task was connected
174
- LoggerProxy.log(
175
- `Got AGENT_CONTACT for a task with state=${payload.data.interaction.state}, sending TASK_HYDRATE event`,
176
- {
177
- module: TASK_MANAGER_FILE,
178
- method: METHODS.REGISTER_TASK_LISTENERS,
179
- interactionId: payload.data.interactionId,
180
- }
181
- );
182
- this.emit(TASK_EVENTS.TASK_HYDRATE, task);
183
- }
184
- }
185
- break;
186
-
187
- case CC_EVENTS.AGENT_CONTACT_RESERVED: {
188
- // Check if auto-answer should happen for this task
189
- const shouldAutoAnswerReserved = shouldAutoAnswerTask(
190
- payload.data,
191
- this.agentId,
192
- this.webCallingService.loginOption,
193
- this.webRtcEnabled
194
- );
195
227
 
196
- task = new Task(
197
- this.contact,
198
- this.webCallingService,
199
- {
200
- ...payload.data,
201
- isConsulted: false,
202
- isAutoAnswering: shouldAutoAnswerReserved, // Set flag before emitting
203
- },
204
- this.wrapupData,
205
- this.agentId
206
- );
207
- this.taskCollection[payload.data.interactionId] = task;
208
- if (
209
- this.webCallingService.loginOption !== LoginOption.BROWSER ||
210
- task.data.interaction.mediaType !== MEDIA_CHANNEL.TELEPHONY // for digital channels
211
- ) {
212
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
213
- } else if (this.call) {
214
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
215
- }
216
- break;
217
- }
218
- case CC_EVENTS.AGENT_OFFER_CONTACT:
219
- // We don't have to emit any event here since this will be result of promise.
220
- task = this.updateTaskData(task, payload.data);
221
- LoggerProxy.log(`Agent offer contact received for task`, {
222
- module: TASK_MANAGER_FILE,
223
- method: METHODS.REGISTER_TASK_LISTENERS,
224
- interactionId: payload.data?.interactionId,
225
- });
226
- this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, task);
227
-
228
- // Handle auto-answer for offer contact
229
- this.handleAutoAnswer(task);
230
- break;
231
- case CC_EVENTS.AGENT_OUTBOUND_FAILED:
232
- if (task) {
233
- task = this.updateTaskData(task, payload.data);
234
- this.metricsManager.trackEvent(
235
- METRIC_EVENT_NAMES.TASK_OUTDIAL_FAILED,
236
- {
237
- ...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
238
- taskId: payload.data.interactionId,
239
- reason: payload.data.reasonCode || payload.data.reason,
240
- },
241
- ['behavioral', 'operational']
242
- );
243
- LoggerProxy.log(`Agent outbound failed for task`, {
244
- module: TASK_MANAGER_FILE,
245
- method: METHODS.REGISTER_TASK_LISTENERS,
246
- interactionId: payload.data.interactionId,
247
- });
248
- task.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, payload.data.reason ?? 'UNKNOWN_REASON');
249
- }
250
- break;
251
- case CC_EVENTS.AGENT_CONTACT_ASSIGNED:
252
- task = this.updateTaskData(task, payload.data);
253
- task.emit(TASK_EVENTS.TASK_ASSIGNED, task);
254
- break;
255
- case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
256
- task = this.updateTaskData(task, {
257
- ...payload.data,
258
- wrapUpRequired: true,
259
- });
260
- task.emit(TASK_EVENTS.TASK_END, task);
261
- break;
262
- case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
263
- case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
264
- case CC_EVENTS.AGENT_INVITE_FAILED: {
265
- task = this.updateTaskData(task, payload.data);
266
-
267
- const eventTypeToMetricMap: Record<string, keyof typeof METRIC_EVENT_NAMES> = {
268
- [CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED]: 'AGENT_CONTACT_ASSIGN_FAILED',
269
- [CC_EVENTS.AGENT_INVITE_FAILED]: 'AGENT_INVITE_FAILED',
270
- };
271
- const metricEventName: keyof typeof METRIC_EVENT_NAMES =
272
- eventTypeToMetricMap[payload.data.type] || 'AGENT_RONA';
273
-
274
- this.metricsManager.trackEvent(
275
- METRIC_EVENT_NAMES[metricEventName],
276
- {
277
- ...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
278
- taskId: payload.data.interactionId,
279
- reason: payload.data.reason,
280
- },
281
- ['behavioral', 'operational']
282
- );
283
- this.handleTaskCleanup(task);
284
- task.emit(TASK_EVENTS.TASK_REJECT, payload.data.reason);
285
- break;
286
- }
287
- case CC_EVENTS.CONTACT_ENDED:
288
- // Update task data
289
- if (task) {
290
- task = this.updateTaskData(task, {
291
- ...payload.data,
292
- wrapUpRequired:
293
- payload.data.interaction.state !== 'new' &&
294
- !isSecondaryEpDnAgent(payload.data.interaction),
295
- });
296
-
297
- // Handle cleanup based on whether task should be deleted
298
- this.handleTaskCleanup(task);
299
-
300
- task?.emit(TASK_EVENTS.TASK_END, task);
301
- }
302
- break;
303
- case CC_EVENTS.CONTACT_MERGED:
304
- task = this.handleContactMerged(task, payload.data);
305
- break;
306
- case CC_EVENTS.AGENT_CONTACT_HELD:
307
- // As soon as the main interaction is held, we need to emit TASK_HOLD
308
- task = this.updateTaskData(task, payload.data);
309
- task.emit(TASK_EVENTS.TASK_HOLD, task);
310
- break;
311
- case CC_EVENTS.AGENT_CONTACT_UNHELD:
312
- // As soon as the main interaction is unheld, we need to emit TASK_RESUME
313
- task = this.updateTaskData(task, payload.data);
314
- task.emit(TASK_EVENTS.TASK_RESUME, task);
315
- break;
316
- case CC_EVENTS.AGENT_VTEAM_TRANSFERRED:
317
- task = this.updateTaskData(task, {
318
- ...payload.data,
319
- wrapUpRequired: true,
320
- });
321
- task.emit(TASK_EVENTS.TASK_END, task);
322
- break;
323
- case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
324
- task = this.updateTaskData(task, payload.data);
325
- task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED, task);
326
- break;
327
- case CC_EVENTS.AGENT_CONSULT_CREATED:
328
- // Received when self agent initiates a consult
329
- task = this.updateTaskData(task, {
330
- ...payload.data,
331
- isConsulted: false, // This ensures that the task consult status is always reset
332
- });
333
- task.emit(TASK_EVENTS.TASK_CONSULT_CREATED, task);
334
- break;
335
- case CC_EVENTS.AGENT_OFFER_CONSULT:
336
- // Received when other agent sends us a consult offer
337
- task = this.updateTaskData(task, {
338
- ...payload.data,
339
- isConsulted: true, // This ensures that the task is marked as us being requested for a consult
340
- });
341
- task.emit(TASK_EVENTS.TASK_OFFER_CONSULT, task);
342
-
343
- // Handle auto-answer for consult offer
344
- this.handleAutoAnswer(task);
345
- break;
346
- case CC_EVENTS.AGENT_CONSULTING:
347
- // Received when agent is in an active consult state
348
- // TODO: Check if we can use backend consult state instead of isConsulted
349
- task = this.updateTaskData(task, payload.data);
350
- if (task.data.isConsulted) {
351
- // Fire only if you are the agent who received the consult request
352
- task.emit(TASK_EVENTS.TASK_CONSULT_ACCEPTED, task);
353
- } else {
354
- // Fire only if you are the agent who initiated the consult
355
- task.emit(TASK_EVENTS.TASK_CONSULTING, task);
356
- }
357
- break;
358
- case CC_EVENTS.AGENT_CONSULT_FAILED:
359
- // This can only be received by the agent who initiated the consult.
360
- // We need not emit any event here since this will be result of promise
361
- task = this.updateTaskData(task, payload.data);
362
- break;
363
- case CC_EVENTS.AGENT_CONSULT_ENDED:
364
- task = this.updateTaskData(task, payload.data);
365
- if (task.data.isConsulted) {
366
- // This will be the end state of the task as soon as we end the consult in case of
367
- // us being offered a consult
368
- this.removeTaskFromCollection(task);
369
- }
370
- task.emit(TASK_EVENTS.TASK_CONSULT_END, task);
371
- break;
372
- case CC_EVENTS.AGENT_CTQ_CANCELLED:
373
- // This event is received when the consult using queue is cancelled using API
374
- task = this.updateTaskData(task, payload.data);
375
- task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, task);
376
- break;
377
- case CC_EVENTS.AGENT_WRAPUP:
378
- task = this.updateTaskData(task, {...payload.data, wrapUpRequired: true});
379
- task.emit(TASK_EVENTS.TASK_END, task);
380
- break;
381
- case CC_EVENTS.AGENT_WRAPPEDUP:
382
- task.cancelAutoWrapupTimer();
383
- this.removeTaskFromCollection(task);
384
- task.emit(TASK_EVENTS.TASK_WRAPPEDUP, task);
385
- break;
386
- case CC_EVENTS.CONTACT_RECORDING_PAUSED:
387
- task = this.updateTaskData(task, payload.data);
388
- task.emit(TASK_EVENTS.TASK_RECORDING_PAUSED, task);
389
- break;
390
- case CC_EVENTS.CONTACT_RECORDING_PAUSE_FAILED:
391
- task = this.updateTaskData(task, payload.data);
392
- task.emit(TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED, task);
393
- break;
394
- case CC_EVENTS.CONTACT_RECORDING_RESUMED:
395
- task = this.updateTaskData(task, payload.data);
396
- task.emit(TASK_EVENTS.TASK_RECORDING_RESUMED, task);
397
- break;
398
- case CC_EVENTS.CONTACT_RECORDING_RESUME_FAILED:
399
- task = this.updateTaskData(task, payload.data);
400
- task.emit(TASK_EVENTS.TASK_RECORDING_RESUME_FAILED, task);
401
- break;
402
- case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
403
- // Conference is being established - update task state and emit establishing event
404
- task = this.updateTaskData(task, payload.data);
405
- task.emit(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, task);
406
- break;
407
- case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
408
- // Conference started successfully - update task state and emit event
409
- task = this.updateTaskData(task, payload.data);
410
- task.emit(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
411
- break;
412
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
413
- // Conference failed - update task state and emit failure event
414
- task = this.updateTaskData(task, payload.data);
415
- task.emit(TASK_EVENTS.TASK_CONFERENCE_FAILED, task);
416
- break;
417
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
418
- // Conference ended - update task state and emit event
419
- task = this.updateTaskData(task, payload.data);
420
- if (
421
- !task ||
422
- isPrimary(task, this.agentId) ||
423
- isParticipantInMainInteraction(task, this.agentId)
424
- ) {
425
- LoggerProxy.log('Primary or main interaction participant leaving conference');
426
- } else {
427
- this.removeTaskFromCollection(task);
428
- }
429
- task.emit(TASK_EVENTS.TASK_CONFERENCE_ENDED, task);
430
- break;
431
- case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE: {
432
- task = this.updateTaskData(task, {
433
- ...payload.data,
434
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
435
- });
436
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
437
- break;
438
- }
439
- case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE: {
440
- // Conference ended - update task state and emit event
441
-
442
- task = this.updateTaskData(task, {
443
- ...payload.data,
444
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
445
- });
446
- if (checkParticipantNotInInteraction(task, this.agentId)) {
447
- if (
448
- isParticipantInMainInteraction(task, this.agentId) ||
449
- isPrimary(task, this.agentId)
450
- ) {
451
- LoggerProxy.log('Primary or main interaction participant leaving conference');
452
- } else {
453
- this.removeTaskFromCollection(task);
454
- }
455
- }
456
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
457
- break;
458
- }
459
- case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED:
460
- // Conference exit failed - update task state and emit failure event
461
- task = this.updateTaskData(task, payload.data);
462
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, task);
463
- break;
464
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_END_FAILED:
465
- // Conference end failed - update task state with error details and emit failure event
466
- task = this.updateTaskData(task, payload.data);
467
- task.emit(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, task);
468
- break;
469
- case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
470
- // Conference was transferred - update task state and emit transfer success event
471
- // Note: Backend should provide hasLeft and wrapUpRequired status
472
- task = this.updateTaskData(task, payload.data);
473
- task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, task);
474
- break;
475
- case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
476
- // Conference transfer failed - update task state with error details and emit failure event
477
- task = this.updateTaskData(task, payload.data);
478
- task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, task);
479
- break;
480
- case CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY:
481
- // Post-call activity for participant - update task state with activity details
482
- task = this.updateTaskData(task, payload.data);
483
- task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
484
- break;
485
- default:
486
- break;
228
+ return {type: TaskEvent.TASK_INCOMING, taskData: payload};
229
+
230
+ case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: // -> TASK_INCOMING (campaign branch via guard)
231
+ return {
232
+ type: TaskEvent.TASK_INCOMING,
233
+ taskData: payload,
234
+ isCampaignReservationAccept: true,
235
+ };
236
+
237
+ case CC_EVENTS.AGENT_OFFER_CONTACT: // AgentOfferContact -> TASK_OFFERED
238
+ return {type: TaskEvent.TASK_OFFERED, taskData: payload};
239
+
240
+ case CC_EVENTS.AGENT_CONTACT: // AgentContact -> HYDRATE
241
+ // Include agentId for state detection (e.g., checking isWrapUp in participant data)
242
+ return {type: TaskEvent.HYDRATE, taskData: payload, agentId};
243
+
244
+ case CC_EVENTS.CONTACT_UPDATED:
245
+ if (
246
+ task &&
247
+ typeof payload.interaction?.owner === 'string' &&
248
+ payload.interaction.owner.trim().length > 0 &&
249
+ payload.interaction.owner !== task.data?.interaction?.owner
250
+ ) {
251
+ return {type: TaskEvent.CONTACT_OWNER_CHANGED, taskData: payload};
487
252
  }
488
- if (task) {
489
- task.emit(payload.data.type, payload.data);
253
+
254
+ return {type: TaskEvent.CONTACT_UPDATED, taskData: payload};
255
+ case CC_EVENTS.CONTACT_OWNER_CHANGED:
256
+ return {type: TaskEvent.CONTACT_OWNER_CHANGED, taskData: payload};
257
+
258
+ case CC_EVENTS.AGENT_OFFER_CONSULT: // AgentOfferConsult -> OFFER_CONSULT
259
+ return {
260
+ type: TaskEvent.OFFER_CONSULT,
261
+ taskData: {...payload, isConsulted: true},
262
+ };
263
+
264
+ case CC_EVENTS.AGENT_CONTACT_ASSIGNED: // AgentContactAssigned -> ASSIGN
265
+ return {type: TaskEvent.ASSIGN, taskData: payload};
266
+
267
+ case CC_EVENTS.AGENT_CONTACT_HELD:
268
+ return {
269
+ type: TaskEvent.HOLD_SUCCESS,
270
+ mediaResourceId: mediaResourceId || '',
271
+ taskData: payload,
272
+ };
273
+
274
+ case CC_EVENTS.AGENT_CONTACT_UNHELD:
275
+ return {
276
+ type: TaskEvent.UNHOLD_SUCCESS,
277
+ mediaResourceId: mediaResourceId || '',
278
+ taskData: payload,
279
+ };
280
+
281
+ case CC_EVENTS.AGENT_CONSULT_CREATED:
282
+ return {
283
+ type: TaskEvent.CONSULT_CREATED,
284
+ taskData: {...payload, isConsulted: false},
285
+ };
286
+
287
+ case CC_EVENTS.AGENT_CONSULTING: // AgentConsulting -> CONSULTING_ACTIVE
288
+ // use context to figure out if it's the initiator or receiver using consultInitiator from context
289
+ return {
290
+ type: TaskEvent.CONSULTING_ACTIVE,
291
+ consultDestinationAgentJoined: true,
292
+ taskData: payload,
293
+ };
294
+
295
+ case CC_EVENTS.AGENT_CONSULT_ENDED: // AgentConsultEnded -> CONSULT_END
296
+ return {type: TaskEvent.CONSULT_END, taskData: payload};
297
+
298
+ case CC_EVENTS.AGENT_CONSULT_FAILED:
299
+ case CC_EVENTS.AGENT_CTQ_FAILED:
300
+ return {type: TaskEvent.CONSULT_FAILED, reason: payload.reason, taskData: payload};
301
+
302
+ case CC_EVENTS.AGENT_CTQ_CANCELLED:
303
+ return {type: TaskEvent.CTQ_CANCEL, taskData: payload};
304
+
305
+ case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
306
+ return {type: TaskEvent.CTQ_CANCEL_FAILED, taskData: payload};
307
+
308
+ case CC_EVENTS.AGENT_BLIND_TRANSFERRED: // AgentBlindTransferred -> TRANSFER_SUCCESS
309
+ case CC_EVENTS.AGENT_CONSULT_TRANSFERRED: // AgentConsultTransferred -> TRANSFER_SUCCESS
310
+ case CC_EVENTS.AGENT_VTEAM_TRANSFERRED: // AgentVTeamTransferred -> TRANSFER_SUCCESS
311
+ return {
312
+ type: TaskEvent.TRANSFER_SUCCESS,
313
+ taskData: payload,
314
+ };
315
+
316
+ case CC_EVENTS.AGENT_WRAPUP:
317
+ return {type: TaskEvent.TASK_WRAPUP, taskData: {...payload, wrapUpRequired: true}};
318
+ case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
319
+ return null; // Add WRAPUP if needed
320
+
321
+ case CC_EVENTS.AGENT_BLIND_TRANSFER_FAILED:
322
+ case CC_EVENTS.AGENT_VTEAM_TRANSFER_FAILED:
323
+ case CC_EVENTS.AGENT_CONSULT_TRANSFER_FAILED:
324
+ case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
325
+ return {type: TaskEvent.TRANSFER_FAILED, taskData: payload};
326
+
327
+ case CC_EVENTS.CONTACT_ENDED: {
328
+ const wrapUpRequired = isCampaignPreviewTask(payload)
329
+ ? false
330
+ : payload.agentsPendingWrapUp?.includes(agentId || '') || false;
331
+
332
+ // Post-accept wxApp outdial agent-end delivers ContactEnded instead of
333
+ // AgentOutboundFailed. Normalize to OUTBOUND_FAILED so state machine
334
+ // enters WRAPPING_UP and emits TASK_OUTDIAL_FAILED (AGENT_ENDS).
335
+ // Pre-accept cancelTask decline stays CONTACT_ENDED → TERMINATED (no wrapup).
336
+ if (
337
+ TaskManager.isAgentTerminatedOutdialWrapup(
338
+ payload,
339
+ wrapUpRequired,
340
+ task,
341
+ agentId,
342
+ enableWxBetterTogether
343
+ )
344
+ ) {
345
+ return {
346
+ type: TaskEvent.OUTBOUND_FAILED,
347
+ reason: 'AGENT_ENDS',
348
+ taskData: {...payload, wrapUpRequired: true},
349
+ };
490
350
  }
351
+
352
+ return {
353
+ type: TaskEvent.CONTACT_ENDED,
354
+ taskData: {
355
+ ...payload,
356
+ wrapUpRequired,
357
+ },
358
+ };
491
359
  }
492
- });
360
+
361
+ case CC_EVENTS.AGENT_INVITE_FAILED:
362
+ return {type: TaskEvent.INVITE_FAILED, reason: payload.reason};
363
+
364
+ case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
365
+ return {type: TaskEvent.ASSIGN_FAILED, reason: payload.reason};
366
+
367
+ case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
368
+ return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
369
+
370
+ case CC_EVENTS.AGENT_OUTBOUND_FAILED: {
371
+ const isAgentTerminatedOutdial =
372
+ payload.interaction?.outboundType === 'OUTDIAL' &&
373
+ (payload.terminatingParty === 'Agent' || payload.reason === 'AGENT_ENDS');
374
+
375
+ const suppressOutdialFailedPopup =
376
+ isAgentTerminatedOutdial &&
377
+ !TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether);
378
+
379
+ return {
380
+ type: TaskEvent.OUTBOUND_FAILED,
381
+ taskData: suppressOutdialFailedPopup
382
+ ? {...payload, suppressOutdialFailedPopup: true}
383
+ : payload,
384
+ reason: payload.reason,
385
+ };
386
+ }
387
+
388
+ case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
389
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED, taskData: payload};
390
+
391
+ case CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED:
392
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_SKIP_FAILED, taskData: payload};
393
+
394
+ case CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED:
395
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_REMOVE_FAILED, taskData: payload};
396
+
397
+ case CC_EVENTS.CONTACT_RECORDING_STARTED:
398
+ return {type: TaskEvent.RECORDING_STARTED, taskData: payload};
399
+
400
+ case CC_EVENTS.CONTACT_RECORDING_PAUSED:
401
+ return {type: TaskEvent.PAUSE_RECORDING, taskData: payload};
402
+
403
+ case CC_EVENTS.CONTACT_RECORDING_RESUMED:
404
+ return {type: TaskEvent.RESUME_RECORDING, taskData: payload};
405
+
406
+ case CC_EVENTS.AGENT_WRAPPEDUP:
407
+ return {type: TaskEvent.WRAPUP_COMPLETE, taskData: payload};
408
+
409
+ // Conference events - these trigger state machine transition to CONFERENCING
410
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
411
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
412
+ case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE:
413
+ return {type: TaskEvent.CONFERENCE_START, taskData: payload};
414
+
415
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
416
+ return {type: TaskEvent.CONFERENCE_FAILED, reason: payload.reason, taskData: payload};
417
+
418
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
419
+ return {type: TaskEvent.CONFERENCE_END, taskData: payload};
420
+
421
+ case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE:
422
+ return {
423
+ type: TaskEvent.PARTICIPANT_LEAVE,
424
+ taskData: payload,
425
+ participantId: payload?.participantId,
426
+ };
427
+
428
+ case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
429
+ return {type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: payload};
430
+
431
+ default:
432
+ // Not all events need state machine mapping
433
+ return null;
434
+ }
493
435
  }
494
436
 
495
- private updateTaskData(task: ITask, taskData: TaskData): ITask {
496
- if (!task) {
497
- return undefined;
437
+ /**
438
+ * True when the current agent's task is a wxApp-managed outdial (thick-client telephony).
439
+ */
440
+ private static isWxAppManagedOutdialTask(
441
+ agentId: string | undefined,
442
+ payload: WebSocketPayload,
443
+ task: ITask | undefined,
444
+ enableWxBetterTogether?: boolean
445
+ ): boolean {
446
+ const wxAppEnabled =
447
+ (task as {uiControlConfig?: {enableWxBetterTogether?: boolean}})?.uiControlConfig
448
+ ?.enableWxBetterTogether ??
449
+ enableWxBetterTogether ??
450
+ false;
451
+
452
+ if (!wxAppEnabled) {
453
+ return false;
498
454
  }
499
455
 
500
- if (!taskData?.interactionId) {
501
- LoggerProxy.warn('Received task update with missing interactionId', {
502
- module: TASK_MANAGER_FILE,
503
- method: METHODS.UPDATE_TASK_DATA,
504
- });
456
+ const effectiveAgentId = agentId ?? task?.data?.agentId;
457
+ const participants = payload.interaction?.participants ?? task?.data?.interaction?.participants;
458
+ const details = getWebexCallingDeviceDetailsForAgent(effectiveAgentId, participants);
459
+
460
+ return details?.deviceType === 'wxApp';
461
+ }
462
+
463
+ /**
464
+ * True when ContactEnded represents an agent-terminated wxApp outdial that should
465
+ * enter wrapup with an outdial-failed popup (legacy AgentOutboundFailed parity).
466
+ */
467
+ private static isAgentTerminatedOutdialWrapup(
468
+ payload: WebSocketPayload,
469
+ wrapUpRequired: boolean,
470
+ task?: ITask,
471
+ agentId?: string,
472
+ enableWxBetterTogether?: boolean
473
+ ): boolean {
474
+ const isOutdial = payload.interaction?.outboundType === 'OUTDIAL';
475
+ const agentTerminated = payload.terminatingParty === 'Agent';
476
+
477
+ if (!isOutdial || !agentTerminated) {
478
+ return false;
479
+ }
480
+
481
+ if (!TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether)) {
482
+ return false;
505
483
  }
506
484
 
485
+ const wxAppAnswerPending =
486
+ (task as {uiControlConfig?: {wxAppAnswerPending?: boolean}})?.uiControlConfig
487
+ ?.wxAppAnswerPending ?? false;
488
+
489
+ // Pre-accept decline: decline() clears wxAppAnswerPending; backend may still send state wrapUp.
490
+ if (!wxAppAnswerPending && !wrapUpRequired) {
491
+ return false;
492
+ }
493
+
494
+ const interactionWrapUp = payload.interaction?.state === 'wrapUp';
495
+
496
+ return wrapUpRequired || interactionWrapUp || wxAppAnswerPending;
497
+ }
498
+
499
+ /**
500
+ * Register WebSocket message listeners for task events
501
+ *
502
+ * Main entry point that orchestrates event processing through a clear pipeline:
503
+ * 1. Parse and validate incoming WebSocket messages
504
+ * 2. Prepare event context with task and state machine mappings
505
+ * 3. Handle task lifecycle (creation, updates, collection management)
506
+ * 4. Send events to state machine (task-level transitions/emissions)
507
+ * 5. Cleanup is triggered via task events emitted by the state machine
508
+ *
509
+ * This architecture separates concerns:
510
+ * - TaskManager: Manages task collection lifecycle and operational concerns
511
+ * - State Machine: Manages individual task state and event emissions
512
+ */
513
+ private registerTaskListeners() {
514
+ this.webSocketManager.on('message', (event) => {
515
+ // Step 1: Parse and validate the message
516
+ const message = TaskManager.parseWebSocketMessage(event);
517
+ if (!message) return;
518
+
519
+ // Step 2: Prepare event context
520
+ const eventContext = this.prepareEventContext(message);
521
+ if (!eventContext) return;
522
+
523
+ const actions = this.handleTaskLifecycleEvent(eventContext);
524
+
525
+ const {task} = actions;
526
+ if (!task) return;
527
+
528
+ const {payload, stateMachineEvent} = eventContext;
529
+
530
+ // Always keep task.data updated (even for mapped events) so consumers relying
531
+ // on TaskManager-managed task instances see the latest payload.
532
+ if (payload) {
533
+ this.updateTaskData(task, payload);
534
+ }
535
+
536
+ // Send event to state machine - this will trigger all TASK_EVENTS emissions
537
+ // including TASK_INCOMING which is now handled via the state machine callbacks
538
+ if (stateMachineEvent) {
539
+ task.sendStateMachineEvent(stateMachineEvent);
540
+ }
541
+
542
+ // Emit TASK_POST_CALL_ACTIVITY for ParticipantPostCallActivity events so
543
+ // consumers (Widgets) can detect the interaction state change to post_call.
544
+ if (eventContext.eventType === CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY) {
545
+ task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
546
+ }
547
+
548
+ // Send transcript start/stop events for relevant CC events
549
+ this.requestRealTimeTranscripts(eventContext.eventType, payload.interactionId);
550
+ });
551
+ }
552
+
553
+ /**
554
+ * Parse and validate WebSocket message
555
+ * @returns Parsed message or null if invalid/keepalive
556
+ */
557
+ private static parseWebSocketMessage(event: string): WebSocketMessage | null {
507
558
  try {
508
- const currentTask = task.updateTaskData(taskData);
509
- this.taskCollection[taskData.interactionId] = currentTask;
559
+ const payload = JSON.parse(event) as WebSocketMessage;
560
+
561
+ // Filter out keepalive messages
562
+ if (payload?.keepalive === 'true' || payload?.keepalive === true) {
563
+ return null;
564
+ }
565
+
566
+ // Normalize task data if present
567
+ if (payload?.data?.interaction) {
568
+ payload.data = normalizeTaskData(payload.data);
569
+ }
510
570
 
511
- return currentTask;
571
+ return payload;
512
572
  } catch (error) {
513
- LoggerProxy.error(`Failed to update task`, {
573
+ LoggerProxy.error('Failed to parse WebSocket message', {
514
574
  module: TASK_MANAGER_FILE,
515
- method: METHODS.UPDATE_TASK_DATA,
516
- interactionId: taskData.interactionId,
575
+ method: 'parseWebSocketMessage',
576
+ error,
517
577
  });
518
578
 
519
- return task;
579
+ return null;
520
580
  }
521
581
  }
522
582
 
523
583
  /**
524
- * Handles CONTACT_MERGED event logic
525
- * @param task - The task to process
526
- * @param taskData - The task data from the event payload
527
- * @returns Updated or newly created task
528
- * @private
584
+ * Prepare context for event processing
585
+ * @returns Event context or null if event type is invalid
529
586
  */
530
- private handleContactMerged(task: ITask, taskData: TaskData): ITask {
531
- if (taskData.childInteractionId) {
532
- // remove the child task from collection
533
- this.removeTaskFromCollection(this.taskCollection[taskData.childInteractionId]);
587
+ private prepareEventContext(message: WebSocketMessage): EventContext | null {
588
+ const eventType = message.data?.type || message.type;
589
+
590
+ if (!eventType || !isCcEvent(eventType)) {
591
+ return null;
534
592
  }
535
593
 
536
- if (this.taskCollection[taskData.interactionId]) {
537
- LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
594
+ const interactionId = message.data.interactionId;
595
+ const reservationInteractionId = message.data.reservationInteractionId;
596
+ let task = this.taskCollection[interactionId];
597
+
598
+ // When a campaign preview contact is accepted, the assigned event may arrive
599
+ // with a new interactionId while the task is stored under the original
600
+ // reservationInteractionId. Fall back to that key so the task is found.
601
+ if (!task && reservationInteractionId) {
602
+ task = this.taskCollection[reservationInteractionId];
603
+ if (task) {
604
+ // Re-key the task under the new interaction ID and remove the old entry
605
+ delete this.taskCollection[reservationInteractionId];
606
+ this.taskCollection[interactionId] = task;
607
+ }
608
+ }
609
+
610
+ if (!task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
611
+ task = this.findTaskForContactOwnerChange(message.data);
612
+ }
613
+
614
+ if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
615
+ task = this.findUniqueTaskByRelatedInteraction(message.data);
616
+ }
617
+
618
+ const wasConsultedTask = Boolean(task?.data?.isConsulted);
619
+ const computeWrapUpRequired = () => {
620
+ if (message.data.wrapUpRequired !== undefined) {
621
+ return message.data.wrapUpRequired;
622
+ }
623
+ if (message.data.isConsulted !== undefined) {
624
+ return !message.data.isConsulted;
625
+ }
626
+
627
+ return !wasConsultedTask;
628
+ };
629
+
630
+ let adjustedPayload: WebSocketPayload =
631
+ eventType === CC_EVENTS.AGENT_CONSULT_TRANSFERRED ||
632
+ eventType === CC_EVENTS.AGENT_BLIND_TRANSFERRED ||
633
+ eventType === CC_EVENTS.AGENT_VTEAM_TRANSFERRED
634
+ ? {
635
+ ...message.data,
636
+ wrapUpRequired: computeWrapUpRequired(),
637
+ }
638
+ : message.data;
639
+
640
+ if (task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
641
+ adjustedPayload = TaskManager.preserveMainTaskIdentity(task, adjustedPayload);
642
+ }
643
+
644
+ if (task && eventType === CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
645
+ adjustedPayload = TaskManager.preserveConfirmedOwner(task, adjustedPayload);
646
+ }
647
+
648
+ const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
649
+ eventType,
650
+ adjustedPayload,
651
+ this.agentId,
652
+ task,
653
+ this.configFlags?.enableWxBetterTogether
654
+ );
655
+
656
+ LoggerProxy.info(`Handling task event ${eventType}`, {
657
+ module: TASK_MANAGER_FILE,
658
+ method: 'prepareEventContext',
659
+ interactionId,
660
+ });
661
+
662
+ return {
663
+ eventType,
664
+ payload: adjustedPayload,
665
+ task,
666
+ stateMachineEvent,
667
+ };
668
+ }
669
+
670
+ /**
671
+ * ContactOwnerChanged can identify the promoted agent's child interaction while
672
+ * the surviving task is stored under the main interaction. Accept one related
673
+ * task when its existing snapshot or the authoritative incoming promotion proves
674
+ * current-agent main-call membership, while excluding non-promoted and ambiguous matches.
675
+ */
676
+ private findTaskForContactOwnerChange(payload: WebSocketPayload): ITask | undefined {
677
+ const relatedCandidates = this.findRelatedTasks(payload, {
678
+ includeCollectionKeys: true,
679
+ includeMainCallMediaKeys: true,
680
+ });
681
+ const incomingShowsCurrentAgentPromotion =
682
+ this.isCurrentAgentPromotedOnIncomingMainCall(payload);
683
+ const eligibleCandidates = relatedCandidates.filter(
684
+ (candidate) =>
685
+ this.isCurrentAgentActiveOnMainInteraction(candidate.data) ||
686
+ incomingShowsCurrentAgentPromotion
687
+ );
688
+
689
+ if (eligibleCandidates.length === 1) {
690
+ return eligibleCandidates[0];
691
+ }
692
+
693
+ if (eligibleCandidates.length > 1) {
694
+ LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
538
695
  module: TASK_MANAGER_FILE,
539
- method: METHODS.REGISTER_TASK_LISTENERS,
540
- interactionId: taskData.interactionId,
696
+ method: 'findTaskForContactOwnerChange',
697
+ interactionId:
698
+ payload.interaction?.mainInteractionId ||
699
+ payload.interaction?.parentInteractionId ||
700
+ payload.interaction?.callProcessingDetails?.parentInteractionId ||
701
+ payload.interactionId,
541
702
  });
542
- // update the task data
543
- task = this.updateTaskData(task, taskData);
544
- } else {
545
- // Case2 : Task is not present in taskCollection
546
- LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
703
+
704
+ return undefined;
705
+ }
706
+
707
+ return undefined;
708
+ }
709
+
710
+ private isCurrentAgentActiveOnMainInteraction(taskData: TaskData | undefined): boolean {
711
+ const currentAgentId = this.agentId || taskData?.agentId;
712
+ if (!currentAgentId) return false;
713
+
714
+ return TaskManager.isParticipantActiveOnMainInteraction(taskData, currentAgentId);
715
+ }
716
+
717
+ private static isParticipantActiveOnMainInteraction(
718
+ taskData: TaskData | WebSocketPayload | undefined,
719
+ participantId: string
720
+ ): boolean {
721
+ const interaction = taskData?.interaction;
722
+ const participant = interaction?.participants?.[participantId];
723
+ if (!participant || participant.hasLeft === true) return false;
724
+
725
+ return Object.values(interaction?.media ?? {}).some(
726
+ (media) =>
727
+ media?.mType === MEDIA_TYPE_MAIN_CALL && media.participants?.includes(participantId)
728
+ );
729
+ }
730
+
731
+ private isCurrentAgentPromotedOnIncomingMainCall(payload: WebSocketPayload): boolean {
732
+ return Boolean(
733
+ this.agentId &&
734
+ payload.interaction?.owner === this.agentId &&
735
+ TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId)
736
+ );
737
+ }
738
+
739
+ private static getMainCallMediaId(
740
+ interaction: TaskData['interaction'] | undefined
741
+ ): string | undefined {
742
+ return Object.entries(interaction?.media ?? {}).find(
743
+ ([, media]) => media?.mType === MEDIA_TYPE_MAIN_CALL
744
+ )?.[0];
745
+ }
746
+
747
+ private static getStableMainInteractionId(payload: WebSocketPayload): string | undefined {
748
+ return (
749
+ payload.interaction?.mainInteractionId ||
750
+ TaskManager.getMainCallMediaId(payload.interaction) ||
751
+ payload.interaction?.interactionId ||
752
+ payload.interactionId
753
+ );
754
+ }
755
+
756
+ private canRecoverTaskFromContactOwnerChanged(payload: WebSocketPayload): boolean {
757
+ const interaction = payload.interaction;
758
+ const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
759
+
760
+ return Boolean(
761
+ this.agentId &&
762
+ stableInteractionId &&
763
+ interaction?.mediaType === MEDIA_CHANNEL.TELEPHONY &&
764
+ interaction.isTerminated !== true &&
765
+ interaction.owner === this.agentId &&
766
+ TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId)
767
+ );
768
+ }
769
+
770
+ /** Keep a related owner-change event from re-keying the surviving main task to a child ID. */
771
+ private static preserveMainTaskIdentity(
772
+ task: ITask,
773
+ payload: WebSocketPayload
774
+ ): WebSocketPayload {
775
+ const currentMainInteractionId = task.data?.interaction?.mainInteractionId;
776
+ const payloadMainInteractionId = payload.interaction?.mainInteractionId;
777
+ const currentMainMediaId = TaskManager.getMainCallMediaId(task.data?.interaction);
778
+ const payloadMainMediaId = TaskManager.getMainCallMediaId(payload.interaction);
779
+ const stableInteractionId =
780
+ currentMainInteractionId ||
781
+ payloadMainInteractionId ||
782
+ currentMainMediaId ||
783
+ payloadMainMediaId ||
784
+ payload.interaction?.interactionId ||
785
+ task.data?.interaction?.interactionId ||
786
+ task.data?.interactionId;
787
+
788
+ if (!stableInteractionId || stableInteractionId === payload.interactionId) {
789
+ return payload;
790
+ }
791
+
792
+ return {...payload, interactionId: stableInteractionId};
793
+ }
794
+
795
+ /**
796
+ * ParticipantLeftConference may arrive after an owner update while still carrying
797
+ * the departed owner. Keep the confirmed owner only when the new roster proves that
798
+ * owner is active on mainCall and identifies the incoming owner as departed.
799
+ */
800
+ private static preserveConfirmedOwner(task: ITask, payload: WebSocketPayload): WebSocketPayload {
801
+ const confirmedOwner = task.data?.interaction?.owner;
802
+ const incomingOwner = payload.interaction?.owner;
803
+ if (!confirmedOwner || !incomingOwner || confirmedOwner === incomingOwner) {
804
+ return payload;
805
+ }
806
+
807
+ const confirmedOwnerIsActive = TaskManager.isParticipantActiveOnMainInteraction(
808
+ payload,
809
+ confirmedOwner
810
+ );
811
+ const incomingOwnerIsExplicitlyDeparted =
812
+ payload.participantId === incomingOwner ||
813
+ payload.interaction?.participants?.[incomingOwner]?.hasLeft === true;
814
+
815
+ if (!confirmedOwnerIsActive || !incomingOwnerIsExplicitlyDeparted) {
816
+ return payload;
817
+ }
818
+
819
+ return {
820
+ ...payload,
821
+ owner: confirmedOwner,
822
+ interaction: {...payload.interaction, owner: confirmedOwner},
823
+ };
824
+ }
825
+
826
+ /**
827
+ * Resolve lifecycle events that are emitted for the main interaction while the
828
+ * local task can still be indexed by a child consult interaction. Exact task
829
+ * keys are handled before this fallback. Multiple aliases of the same task are
830
+ * treated as one candidate; genuinely ambiguous matches are intentionally ignored.
831
+ */
832
+ private findUniqueTaskByRelatedInteraction(
833
+ payload: WebSocketPayload,
834
+ candidateFilter: (task: ITask) => boolean = () => true
835
+ ): ITask | undefined {
836
+ const candidates = this.findRelatedTasks(payload).filter(candidateFilter);
837
+
838
+ if (candidates.length === 1) {
839
+ return candidates[0];
840
+ }
841
+
842
+ if (candidates.length > 1) {
843
+ LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
844
+ module: TASK_MANAGER_FILE,
845
+ method: 'findUniqueTaskByRelatedInteraction',
846
+ interactionId:
847
+ payload.interaction?.mainInteractionId ||
848
+ payload.interaction?.parentInteractionId ||
849
+ payload.interaction?.callProcessingDetails?.parentInteractionId ||
850
+ payload.interactionId,
851
+ });
852
+ }
853
+
854
+ return undefined;
855
+ }
856
+
857
+ private findRelatedTasks(
858
+ payload: WebSocketPayload,
859
+ options: {includeCollectionKeys?: boolean; includeMainCallMediaKeys?: boolean} = {}
860
+ ): ITask[] {
861
+ const {includeCollectionKeys = false, includeMainCallMediaKeys = false} = options;
862
+ const payloadInteractionIds = new Set(
863
+ [
864
+ payload.interactionId,
865
+ payload.interaction?.interactionId,
866
+ payload.interaction?.mainInteractionId,
867
+ payload.interaction?.parentInteractionId,
868
+ payload.interaction?.callProcessingDetails?.parentInteractionId,
869
+ ...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(payload.interaction)] : []),
870
+ ].filter((interactionId): interactionId is string => Boolean(interactionId))
871
+ );
872
+ if (payloadInteractionIds.size === 0) return [];
873
+
874
+ return [
875
+ ...new Set(
876
+ Object.entries(this.taskCollection)
877
+ .filter(([taskId, candidate]) => {
878
+ const taskInteraction = candidate?.data?.interaction;
879
+ const candidateInteractionIds = [
880
+ ...(includeCollectionKeys ? [taskId] : []),
881
+ candidate?.data?.interactionId,
882
+ taskInteraction?.interactionId,
883
+ taskInteraction?.mainInteractionId,
884
+ taskInteraction?.parentInteractionId,
885
+ taskInteraction?.callProcessingDetails?.parentInteractionId,
886
+ ...(includeMainCallMediaKeys
887
+ ? [TaskManager.getMainCallMediaId(taskInteraction)]
888
+ : []),
889
+ ];
890
+
891
+ return candidateInteractionIds.some(
892
+ (interactionId) => Boolean(interactionId) && payloadInteractionIds.has(interactionId)
893
+ );
894
+ })
895
+ .map(([, candidate]) => candidate)
896
+ ),
897
+ ];
898
+ }
899
+
900
+ /**
901
+ * Handle task lifecycle events and determine required actions
902
+ *
903
+ * Delegates to specific event handlers based on event type. Each handler
904
+ * is responsible for TaskManager-level concerns:
905
+ * - Task creation and collection management
906
+ * - Metrics tracking
907
+ * - Resource cleanup decisions
908
+ *
909
+ * Note: Task-level state transitions and event emissions are handled by
910
+ * the task state machine via sendStateMachineEvent()
911
+ */
912
+ private handleTaskLifecycleEvent(context: EventContext): TaskEventActions {
913
+ const {eventType} = context;
914
+
915
+ switch (eventType) {
916
+ case CC_EVENTS.AGENT_CONTACT_RESERVED:
917
+ return this.handleContactReserved(context);
918
+
919
+ case CC_EVENTS.AGENT_CONTACT:
920
+ return this.handleAgentContact(context);
921
+
922
+ case CC_EVENTS.CONTACT_MERGED:
923
+ return this.handleContactMergedEvent(context);
924
+
925
+ case CC_EVENTS.CONTACT_OWNER_CHANGED:
926
+ return this.handleContactOwnerChanged(context);
927
+
928
+ case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
929
+ return this.handleCampaignPreviewReservation(context);
930
+
931
+ case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
932
+ return this.handleCampaignContactUpdated(context);
933
+
934
+ default:
935
+ return {task: context.task};
936
+ }
937
+ }
938
+
939
+ /**
940
+ * Recover the promoted agent's active voice task when local task state was lost.
941
+ * Existing related tasks are never replaced: an unresolved or ambiguous relation
942
+ * is safer to ignore than to create a duplicate task.
943
+ */
944
+ private handleContactOwnerChanged(context: EventContext): TaskEventActions {
945
+ if (context.task) {
946
+ return {task: context.task};
947
+ }
948
+
949
+ const {payload} = context;
950
+ if (
951
+ this.findRelatedTasks(payload, {
952
+ includeCollectionKeys: true,
953
+ includeMainCallMediaKeys: true,
954
+ }).length > 0 ||
955
+ !this.canRecoverTaskFromContactOwnerChanged(payload)
956
+ ) {
957
+ return {};
958
+ }
959
+
960
+ const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
961
+ if (!stableInteractionId || this.taskCollection[stableInteractionId]) {
962
+ return {};
963
+ }
964
+
965
+ const normalizedPayload: WebSocketPayload =
966
+ payload.interactionId === stableInteractionId
967
+ ? payload
968
+ : {...payload, interactionId: stableInteractionId};
969
+ const taskData: TaskData = {
970
+ ...normalizedPayload,
971
+ owner: normalizedPayload.interaction.owner,
972
+ wrapUpRequired: normalizedPayload.interaction.participants?.[this.agentId]?.isWrapUp || false,
973
+ isConferenceInProgress: getIsConferenceInProgress(normalizedPayload),
974
+ isConsulted: false,
975
+ isAutoAnswering: false,
976
+ };
977
+ const task = TaskFactory.createTask(
978
+ this.contact,
979
+ this.webCallingService,
980
+ taskData,
981
+ this.configFlags,
982
+ this.wrapupData,
983
+ this.agentId,
984
+ this.answerCallOnWebexService
985
+ );
986
+
987
+ this.taskCollection[stableInteractionId] = task;
988
+
989
+ // Restore the actor before installing listeners so the internal hydrate does
990
+ // not leak as a second public hydrate or as an incoming-task notification.
991
+ task.sendStateMachineEvent({
992
+ type: TaskEvent.HYDRATE,
993
+ taskData,
994
+ agentId: this.agentId,
995
+ } as TaskEventPayload);
996
+
997
+ this.setupTaskListeners(task);
998
+ context.payload = taskData;
999
+ context.stateMachineEvent = {
1000
+ type: TaskEvent.CONTACT_OWNER_CHANGED,
1001
+ taskData,
1002
+ };
1003
+
1004
+ return {task};
1005
+ }
1006
+
1007
+ private handleCampaignContactUpdated(context: EventContext) {
1008
+ const {payload} = context;
1009
+ let {task} = context;
1010
+ // CampaignContactUpdated is a non-terminal event (e.g., next contact after skip/remove).
1011
+ // Update the task data and emit an event so consumers can react to the updated contact.
1012
+ // Do NOT remove the task or emit TASK_END — cleanup is handled by CONTACT_ENDED.
1013
+ if (task) {
1014
+ // Carry forward campaign preview fields from existing task data since the updated
1015
+ // contact payload may not include them, and reconcileData would delete them.
1016
+ const existingCpd = task.data?.interaction?.callProcessingDetails;
1017
+ const updatedData: TaskData = {...payload};
1018
+
1019
+ if (existingCpd) {
1020
+ const campaignFields = {
1021
+ ...(existingCpd.campaignPreviewAutoAction && {
1022
+ campaignPreviewAutoAction: existingCpd.campaignPreviewAutoAction,
1023
+ }),
1024
+ ...(existingCpd.campaignPreviewOfferTimeout && {
1025
+ campaignPreviewOfferTimeout: existingCpd.campaignPreviewOfferTimeout,
1026
+ }),
1027
+ ...(existingCpd.campaignPreviewSkipDisabled && {
1028
+ campaignPreviewSkipDisabled: existingCpd.campaignPreviewSkipDisabled,
1029
+ }),
1030
+ ...(existingCpd.campaignPreviewRemoveDisabled && {
1031
+ campaignPreviewRemoveDisabled: existingCpd.campaignPreviewRemoveDisabled,
1032
+ }),
1033
+ };
1034
+
1035
+ if (!updatedData.interaction) {
1036
+ updatedData.interaction = {} as typeof updatedData.interaction;
1037
+ }
1038
+
1039
+ updatedData.interaction = {
1040
+ ...updatedData.interaction,
1041
+ callProcessingDetails: {
1042
+ ...campaignFields,
1043
+ ...(updatedData.interaction.callProcessingDetails || {}),
1044
+ } as typeof existingCpd,
1045
+ };
1046
+ }
1047
+
1048
+ LoggerProxy.log('Campaign contact updated - carrying forward preview fields', {
547
1049
  module: TASK_MANAGER_FILE,
548
1050
  method: METHODS.REGISTER_TASK_LISTENERS,
549
- interactionId: taskData.interactionId,
1051
+ interactionId: payload.interactionId,
1052
+ data: {
1053
+ hasCpd: !!updatedData.interaction?.callProcessingDetails,
1054
+ autoAction: updatedData.interaction?.callProcessingDetails?.campaignPreviewAutoAction,
1055
+ skipDisabled: updatedData.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled,
1056
+ removeDisabled:
1057
+ updatedData.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled,
1058
+ },
550
1059
  });
551
1060
 
552
- task = new Task(
1061
+ task = this.updateTaskData(task, updatedData);
1062
+ task.emit(TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED, task);
1063
+ }
1064
+
1065
+ return {task};
1066
+ }
1067
+
1068
+ /**
1069
+ * Creates or updates a task for campaign preview reservation.
1070
+ * TASK_CAMPAIGN_PREVIEW_RESERVATION is emitted by the state machine (campaign TASK_INCOMING branch).
1071
+ */
1072
+ private handleCampaignPreviewReservation(context: EventContext): TaskEventActions {
1073
+ const {payload} = context;
1074
+ let {task} = context;
1075
+
1076
+ LoggerProxy.log('Campaign preview reservation received', {
1077
+ module: TASK_MANAGER_FILE,
1078
+ method: METHODS.REGISTER_TASK_LISTENERS,
1079
+ interactionId: payload.interactionId,
1080
+ });
1081
+
1082
+ if (!task) {
1083
+ task = TaskFactory.createTask(
553
1084
  this.contact,
554
1085
  this.webCallingService,
555
1086
  {
556
- ...taskData,
557
- wrapUpRequired: taskData.interaction?.participants?.[this.agentId]?.isWrapUp || false,
558
- isConferenceInProgress: getIsConferenceInProgress(taskData),
1087
+ ...payload,
1088
+ wrapUpRequired: false,
1089
+ isConferenceInProgress: false,
1090
+ isAutoAnswering: false,
559
1091
  },
1092
+ this.configFlags,
1093
+ this.wrapupData,
1094
+ this.agentId,
1095
+ this.answerCallOnWebexService
1096
+ );
1097
+ this.setupTaskListeners(task);
1098
+ this.taskCollection[payload.interactionId] = task;
1099
+ } else {
1100
+ task = this.updateTaskData(task, payload);
1101
+ }
1102
+
1103
+ return {task};
1104
+ }
1105
+
1106
+ /**
1107
+ * Handle AGENT_CONTACT_RESERVED event
1108
+ * Creates a new task; state machine event is sent during processing
1109
+ */
1110
+ private handleContactReserved(context: EventContext): TaskEventActions {
1111
+ const {payload} = context;
1112
+ const isConsultedTask =
1113
+ payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
1114
+ const shouldAutoAnswer = shouldAutoAnswerTask(
1115
+ payload,
1116
+ this.agentId,
1117
+ this.webCallingService.loginOption,
1118
+ this.webRtcEnabled
1119
+ );
1120
+
1121
+ const taskData: TaskData = {
1122
+ ...payload,
1123
+ isConsulted: isConsultedTask,
1124
+ isAutoAnswering: shouldAutoAnswer,
1125
+ };
1126
+
1127
+ const task = TaskFactory.createTask(
1128
+ this.contact,
1129
+ this.webCallingService,
1130
+ taskData,
1131
+ this.configFlags,
1132
+ this.wrapupData,
1133
+ this.agentId,
1134
+ this.answerCallOnWebexService
1135
+ );
1136
+
1137
+ this.setupTaskListeners(task);
1138
+ this.taskCollection[payload.interactionId] = task;
1139
+
1140
+ return {task};
1141
+ }
1142
+
1143
+ /**
1144
+ * Handle AGENT_CONTACT event
1145
+ * Re-creates task if missing (multi-session scenario)
1146
+ */
1147
+ private handleAgentContact(context: EventContext): TaskEventActions {
1148
+ let {task} = context;
1149
+ const {payload} = context;
1150
+
1151
+ if (!task) {
1152
+ const isConsultedTask =
1153
+ payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
1154
+ const shouldAutoAnswer = shouldAutoAnswerTask(
1155
+ payload,
1156
+ this.agentId,
1157
+ this.webCallingService.loginOption,
1158
+ this.webRtcEnabled
1159
+ );
1160
+ const taskData: TaskData = {
1161
+ ...payload,
1162
+ isConsulted: isConsultedTask,
1163
+ wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
1164
+ isConferenceInProgress: getIsConferenceInProgress(payload),
1165
+ isAutoAnswering: shouldAutoAnswer,
1166
+ };
1167
+
1168
+ task = TaskFactory.createTask(
1169
+ this.contact,
1170
+ this.webCallingService,
1171
+ taskData,
1172
+ this.configFlags,
560
1173
  this.wrapupData,
561
- this.agentId
1174
+ this.agentId,
1175
+ this.answerCallOnWebexService
562
1176
  );
563
- this.taskCollection[taskData.interactionId] = task;
1177
+ this.setupTaskListeners(task);
1178
+ this.taskCollection[payload.interactionId] = task;
1179
+ }
1180
+
1181
+ return {task};
1182
+ }
1183
+
1184
+ private updateTaskData(task: ITask, taskData: TaskData): ITask {
1185
+ if (!task) {
1186
+ throw new Error('Task not found for update');
564
1187
  }
565
1188
 
566
- this.emit(TASK_EVENTS.TASK_MERGED, task);
1189
+ const snapshot = task.stateMachineService?.getSnapshot?.();
1190
+ const isConsultingFlow =
1191
+ snapshot?.value === 'CONSULTING' || taskData.interaction?.state === 'consulting';
1192
+
1193
+ const updateTaskData = isConsultingFlow
1194
+ ? {
1195
+ ...taskData,
1196
+ destAgentId: taskData.destAgentId ?? snapshot?.context?.consultDestinationAgentId ?? null,
1197
+ destinationType:
1198
+ taskData.destinationType ?? snapshot?.context?.consultDestinationType ?? null,
1199
+ }
1200
+ : taskData;
1201
+
1202
+ task.updateTaskData(updateTaskData);
1203
+
1204
+ // A main-interaction lifecycle event can re-key a child consult task. Remove
1205
+ // any aliases first so consumers never observe duplicate task entries.
1206
+ Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
1207
+ if (candidate === task && taskId !== taskData.interactionId) {
1208
+ delete this.taskCollection[taskId];
1209
+ }
1210
+ });
1211
+ this.taskCollection[taskData.interactionId] = task;
567
1212
 
568
1213
  return task;
569
1214
  }
570
1215
 
1216
+ /**
1217
+ * Setup listeners for task events that need to be bubbled up to TaskManager
1218
+ * This replaces the previous callback injection pattern
1219
+ */
1220
+ private setupTaskListeners(task: ITask): void {
1221
+ // Listen for TASK_INCOMING and re-emit so webex.cc can notify consumers
1222
+ task.on(TASK_EVENTS.TASK_INCOMING, (t: ITask) => {
1223
+ LoggerProxy.log(`Task incoming event received`, {
1224
+ module: TASK_MANAGER_FILE,
1225
+ method: METHODS.REGISTER_TASK_LISTENERS,
1226
+ interactionId: t.data?.interactionId,
1227
+ });
1228
+
1229
+ this.emit(TASK_EVENTS.TASK_INCOMING, t);
1230
+ });
1231
+
1232
+ task.on(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, (t: ITask) => {
1233
+ LoggerProxy.log(`Campaign preview reservation event received`, {
1234
+ module: TASK_MANAGER_FILE,
1235
+ method: METHODS.REGISTER_TASK_LISTENERS,
1236
+ interactionId: t.data?.interactionId,
1237
+ });
1238
+
1239
+ this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, t);
1240
+ });
1241
+
1242
+ // Listen for TASK_HYDRATE on the task and re-emit on TaskManager
1243
+ task.on(TASK_EVENTS.TASK_HYDRATE, (t: ITask) => {
1244
+ // Task data is already updated by the task itself before emitting
1245
+ this.emit(TASK_EVENTS.TASK_HYDRATE, t);
1246
+ });
1247
+
1248
+ task.on(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, (t: ITask) => {
1249
+ this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, t);
1250
+ });
1251
+
1252
+ // Listen for internal cleanup signal emitted by the state machine
1253
+ task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
1254
+ this.handleTaskCleanup(t);
1255
+ if (options?.removeFromCollection) {
1256
+ const taskIsStillCollected = Object.values(this.taskCollection).some(
1257
+ (candidate) => candidate === t
1258
+ );
1259
+ if (taskIsStillCollected) {
1260
+ this.removeTaskFromCollection(t);
1261
+ }
1262
+ }
1263
+ });
1264
+ }
1265
+
571
1266
  private removeTaskFromCollection(task: ITask) {
1267
+ if (typeof task.cancelAutoWrapupTimer === 'function') {
1268
+ task.cancelAutoWrapupTimer();
1269
+ }
572
1270
  if (task?.data?.interactionId) {
573
- delete this.taskCollection[task.data.interactionId];
1271
+ Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
1272
+ if (candidate === task) {
1273
+ delete this.taskCollection[taskId];
1274
+ }
1275
+ });
574
1276
  LoggerProxy.info(`Task removed from collection`, {
575
1277
  module: TASK_MANAGER_FILE,
576
1278
  method: METHODS.REMOVE_TASK_FROM_COLLECTION,
@@ -580,69 +1282,73 @@ export default class TaskManager extends EventEmitter {
580
1282
  }
581
1283
 
582
1284
  /**
583
- * Handles auto-answer logic for incoming tasks
584
- * Automatically accepts tasks when isAutoAnswering flag is set
585
- * The flag is set during task creation based on:
586
- * 1. WebRTC calls with auto-answer enabled in agent profile
587
- * 2. Agent-initiated WebRTC outdial calls
588
- * 3. Agent-initiated digital outbound (Email/SMS) without previous transfers
589
- *
590
- * @param task - The task to auto-answer
1285
+ * Handles CONTACT_MERGED event logic
1286
+ * @param task - The task to process
1287
+ * @param taskData - The task data from the event payload
1288
+ * @returns Updated or newly created task
591
1289
  * @private
592
1290
  */
593
- private async handleAutoAnswer(task: ITask): Promise<void> {
594
- if (!task || !task.data || !task.data.isAutoAnswering) {
595
- return;
596
- }
1291
+ private handleContactMergedEvent(context: EventContext): TaskEventActions {
1292
+ const {payload} = context;
1293
+ let task = context.task;
597
1294
 
598
- LoggerProxy.info(`Auto-answering task`, {
599
- module: TASK_MANAGER_FILE,
600
- method: 'handleAutoAnswer',
601
- interactionId: task.data.interactionId,
602
- });
1295
+ if (payload.childInteractionId && this.taskCollection[payload.childInteractionId]) {
1296
+ this.removeTaskFromCollection(this.taskCollection[payload.childInteractionId]);
1297
+ }
603
1298
 
604
- try {
605
- await task.accept();
606
- LoggerProxy.info(`Task auto-answered successfully`, {
1299
+ if (task) {
1300
+ LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
607
1301
  module: TASK_MANAGER_FILE,
608
- method: 'handleAutoAnswer',
609
- interactionId: task.data.interactionId,
1302
+ method: METHODS.REGISTER_TASK_LISTENERS,
1303
+ interactionId: payload.interactionId,
610
1304
  });
611
-
612
- // Track successful auto-answer
613
- this.metricsManager.trackEvent(
614
- METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS,
615
- {
616
- taskId: task.data.interactionId,
617
- mediaType: task.data.interaction.mediaType,
618
- isAutoAnswered: true,
619
- },
620
- ['behavioral', 'operational']
621
- );
622
- // Emit task:autoAnswered event for widgets/UI to react
623
- task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
624
- } catch (error) {
625
- // Reset isAutoAnswering flag on failure
626
- task.updateTaskData({...task.data, isAutoAnswering: false});
627
- LoggerProxy.error(`Failed to auto-answer task`, {
1305
+ // update the task data
1306
+ this.updateTaskData(task, payload);
1307
+ } else {
1308
+ // Case2 : Task is not present in taskCollection
1309
+ LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
628
1310
  module: TASK_MANAGER_FILE,
629
- method: 'handleAutoAnswer',
630
- interactionId: task.data.interactionId,
631
- error,
1311
+ method: METHODS.REGISTER_TASK_LISTENERS,
1312
+ interactionId: payload.interactionId,
632
1313
  });
633
1314
 
634
- // Track auto-answer failure
635
- this.metricsManager.trackEvent(
636
- METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_FAILED,
637
- {
638
- taskId: task.data.interactionId,
639
- mediaType: task.data.interaction.mediaType,
640
- error: error?.message || 'Unknown error',
641
- isAutoAnswered: false,
642
- },
643
- ['behavioral', 'operational']
1315
+ const taskData: TaskData = {
1316
+ ...payload,
1317
+ wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
1318
+ isConferenceInProgress: getIsConferenceInProgress(payload),
1319
+ isConsulted: false,
1320
+ };
1321
+
1322
+ task = TaskFactory.createTask(
1323
+ this.contact,
1324
+ this.webCallingService,
1325
+ taskData,
1326
+ this.configFlags,
1327
+ this.wrapupData,
1328
+ this.agentId,
1329
+ this.answerCallOnWebexService
644
1330
  );
1331
+ this.taskCollection[payload.interactionId] = task;
1332
+
1333
+ // Transition the new task out of IDLE immediately so UI controls are
1334
+ // computed before TASK_MERGED is emitted. This handles the race where
1335
+ // AgentContactAssigned arrives before ContactMerged and gets dropped.
1336
+ // Send HYDRATE before setupTaskListeners so the emitTaskHydrate action
1337
+ // doesn't bubble up to the Widget (avoids duplicate listener registration).
1338
+ task.sendStateMachineEvent({
1339
+ type: TaskEvent.HYDRATE,
1340
+ taskData,
1341
+ agentId: this.agentId,
1342
+ } as TaskEventPayload);
1343
+
1344
+ this.setupTaskListeners(task);
645
1345
  }
1346
+
1347
+ if (task) {
1348
+ this.emit(TASK_EVENTS.TASK_MERGED, task);
1349
+ }
1350
+
1351
+ return {task};
646
1352
  }
647
1353
 
648
1354
  /**
@@ -654,7 +1360,8 @@ export default class TaskManager extends EventEmitter {
654
1360
  // Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
655
1361
  if (
656
1362
  this.webCallingService.loginOption === LoginOption.BROWSER &&
657
- task.data.interaction.mediaType === 'telephony'
1363
+ task.data.interaction.mediaType === MEDIA_CHANNEL.TELEPHONY &&
1364
+ task instanceof WebRTC
658
1365
  ) {
659
1366
  task.unregisterWebCallListeners();
660
1367
  this.webCallingService.cleanUpCall();
@@ -662,45 +1369,116 @@ export default class TaskManager extends EventEmitter {
662
1369
 
663
1370
  const isOutdial = task.data.interaction.outboundType === 'OUTDIAL';
664
1371
  const isNew = task.data.interaction.state === 'new';
665
- const needsWrapUp = task.data.agentsPendingWrapUp?.length > 0;
1372
+ const needsWrapUp = task.data.agentsPendingWrapUp?.includes(this.agentId) ?? false;
666
1373
 
667
1374
  // For OUTDIAL: only remove if NOT terminated (user-declined, no wrap-up follows)
668
- // If terminated, keep task for wrap-up flow (CONTACT_ENDED → AGENT_WRAPUP)
669
1375
  // For non-OUTDIAL: remove if state is 'new'
670
1376
  // Always remove if secondary EpDn agent
671
- if ((isNew && !(isOutdial && needsWrapUp)) || isSecondaryEpDnAgent(task.data.interaction)) {
1377
+ if (
1378
+ (isNew && !(isOutdial && needsWrapUp)) ||
1379
+ isSecondaryEpDnAgent(task.data.interaction) ||
1380
+ (!needsWrapUp && isOutdial) // For outdial tasks, needs wrap-up is false and state is "WRAPUP". We need to just remove the task.
1381
+ ) {
672
1382
  this.removeTaskFromCollection(task);
673
1383
  }
674
1384
  }
675
1385
 
676
1386
  /**
677
- * @param taskId - Unique identifier for each task
1387
+ * Sends transcript start/stop event based on the CC event type.
1388
+ * Fire-and-forget; errors are logged but do not interrupt event processing.
678
1389
  */
679
- public getTask = (taskId: string) => {
1390
+ private requestRealTimeTranscripts(eventType: string, interactionId: string): void {
1391
+ const action = TRANSCRIPT_EVENT_MAP[eventType];
1392
+ if (!action || !this.apiAIAssistant) return;
1393
+ if (this.configFlags?.aiFeature?.realtimeTranscripts?.enable !== true) return;
1394
+
1395
+ this.apiAIAssistant
1396
+ .sendEvent(
1397
+ this.agentId,
1398
+ interactionId,
1399
+ AIAssistantEventType.CUSTOM_EVENT,
1400
+ AIAssistantEventName.GET_TRANSCRIPTS,
1401
+ {action}
1402
+ )
1403
+ .catch((error) => {
1404
+ LoggerProxy.error(`Failed to send transcript ${action} event`, {
1405
+ module: TASK_MANAGER_FILE,
1406
+ method: METHODS.REQUEST_REAL_TIME_TRANSCRIPTS,
1407
+ interactionId,
1408
+ error,
1409
+ });
1410
+ });
1411
+ }
1412
+
1413
+ public getTask(taskId: TaskId): ITask {
680
1414
  return this.taskCollection[taskId];
681
- };
1415
+ }
1416
+
1417
+ public getAllTasks(): Record<TaskId, ITask> {
1418
+ return {...this.taskCollection};
1419
+ }
682
1420
 
683
1421
  /**
684
- * @param taskId - Unique identifier for each task
1422
+ * Propagate runtime enableWxBetterTogether changes to config flags and active tasks.
685
1423
  */
686
- public getAllTasks = (): Record<TaskId, ITask> => {
687
- return this.taskCollection;
688
- };
1424
+ public applyEnableWxBetterTogether(enabled: boolean): void {
1425
+ if (this.configFlags) {
1426
+ this.configFlags = {...this.configFlags, enableWxBetterTogether: enabled};
1427
+ }
1428
+
1429
+ Object.values(this.taskCollection).forEach((task) => {
1430
+ task.setEnableWxBetterTogether(enabled);
1431
+ if (enabled) {
1432
+ TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
1433
+ }
1434
+ });
1435
+ }
689
1436
 
690
1437
  /**
691
- * @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
692
- * @param webCallingService - Webrtc Service Layer
693
- * @param webSocketManager - Websocket Manager to maintain websocket connection and keepalives
1438
+ * Propagate wxApp mute state from Mercury sync to active voice tasks.
694
1439
  */
695
- public static getTaskManager = (
1440
+ public applyWxAppMuteStateFromSync(callId: string, muted: boolean): void {
1441
+ Object.values(this.taskCollection).forEach((task) => {
1442
+ task.applyWxAppMuteStateFromSync(callId, muted);
1443
+ });
1444
+ }
1445
+
1446
+ /**
1447
+ * Re-seeds wxApp mute state from GET telephony/calls/{callId} for one task.
1448
+ */
1449
+ public static syncWxAppMuteFromCallDetailsForTask(task: ITask): void {
1450
+ const sync = task.syncWxAppMuteFromCallDetails?.bind(task);
1451
+ if (typeof sync === 'function') {
1452
+ sync().catch(() => undefined);
1453
+ }
1454
+ }
1455
+
1456
+ /**
1457
+ * Re-seeds wxApp mute for all engaged voice tasks (reload / runtime enable backfill).
1458
+ */
1459
+ public syncWxAppMuteFromCallDetailsForAllTasks(): void {
1460
+ Object.values(this.taskCollection).forEach((task) => {
1461
+ TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
1462
+ });
1463
+ }
1464
+
1465
+ public static getTaskManager(
1466
+ apiAIAssistant: ApiAIAssistant,
696
1467
  contact: ReturnType<typeof routingContact>,
697
1468
  webCallingService: WebCallingService,
698
- webSocketManager: WebSocketManager
699
- ): TaskManager => {
700
- if (!this.taskManager) {
701
- this.taskManager = new TaskManager(contact, webCallingService, webSocketManager);
1469
+ webSocketManager: WebSocketManager,
1470
+ rtdWebSocketManager?: WebSocketManager
1471
+ ): TaskManager {
1472
+ if (!TaskManager.taskManager) {
1473
+ TaskManager.taskManager = new TaskManager(
1474
+ apiAIAssistant,
1475
+ contact,
1476
+ webCallingService,
1477
+ webSocketManager,
1478
+ rtdWebSocketManager
1479
+ );
702
1480
  }
703
1481
 
704
- return this.taskManager;
705
- };
1482
+ return TaskManager.taskManager;
1483
+ }
706
1484
  }