@webex/contact-center 3.12.0-next.13 → 3.12.0-next.130

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