@webex/contact-center 3.12.0-next.15 → 3.12.0-next.151

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