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

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