@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.4

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 (187) hide show
  1. package/.sdd/manifest.json +5 -4
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +6 -2
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +440 -10
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/behavioral-events.js +13 -0
  15. package/dist/metrics/behavioral-events.js.map +1 -1
  16. package/dist/metrics/constants.js +2 -0
  17. package/dist/metrics/constants.js.map +1 -1
  18. package/dist/services/AddressBook.js +18 -15
  19. package/dist/services/AddressBook.js.map +1 -1
  20. package/dist/services/AnswerCallOnWebexService.js +174 -0
  21. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  22. package/dist/services/EntryPoint.js +46 -68
  23. package/dist/services/EntryPoint.js.map +1 -1
  24. package/dist/services/Queue.js +27 -22
  25. package/dist/services/Queue.js.map +1 -1
  26. package/dist/services/WebexCrossClientService.js +171 -0
  27. package/dist/services/WebexCrossClientService.js.map +1 -0
  28. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  29. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  30. package/dist/services/config/Util.js +3 -0
  31. package/dist/services/config/Util.js.map +1 -1
  32. package/dist/services/config/constants.js +10 -6
  33. package/dist/services/config/constants.js.map +1 -1
  34. package/dist/services/config/types.js +4 -0
  35. package/dist/services/config/types.js.map +1 -1
  36. package/dist/services/core/Err.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +6 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +28 -14
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/task/Task.js +73 -7
  43. package/dist/services/task/Task.js.map +1 -1
  44. package/dist/services/task/TaskFactory.js +8 -4
  45. package/dist/services/task/TaskFactory.js.map +1 -1
  46. package/dist/services/task/TaskManager.js +353 -21
  47. package/dist/services/task/TaskManager.js.map +1 -1
  48. package/dist/services/task/WebexCallingUtils.js +70 -0
  49. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  50. package/dist/services/task/constants.js +4 -1
  51. package/dist/services/task/constants.js.map +1 -1
  52. package/dist/services/task/contact.js +29 -0
  53. package/dist/services/task/contact.js.map +1 -1
  54. package/dist/services/task/digital/Digital.js +3 -2
  55. package/dist/services/task/digital/Digital.js.map +1 -1
  56. package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
  57. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  58. package/dist/services/task/state-machine/actions.js +6 -1
  59. package/dist/services/task/state-machine/actions.js.map +1 -1
  60. package/dist/services/task/state-machine/guards.js +38 -11
  61. package/dist/services/task/state-machine/guards.js.map +1 -1
  62. package/dist/services/task/state-machine/types.js.map +1 -1
  63. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  64. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  65. package/dist/services/task/types.js +16 -1
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/services/task/voice/Voice.js +350 -14
  68. package/dist/services/task/voice/Voice.js.map +1 -1
  69. package/dist/services/task/voice/WebRTC.js +4 -1
  70. package/dist/services/task/voice/WebRTC.js.map +1 -1
  71. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  72. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  73. package/dist/services/wxAppTelephonyUtils.js +19 -0
  74. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  75. package/dist/types/cc.d.ts +77 -4
  76. package/dist/types/config.d.ts +7 -0
  77. package/dist/types/constants.d.ts +8 -0
  78. package/dist/types/index.d.ts +3 -1
  79. package/dist/types/metrics/constants.d.ts +2 -0
  80. package/dist/types/services/AddressBook.d.ts +2 -1
  81. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  82. package/dist/types/services/EntryPoint.d.ts +7 -5
  83. package/dist/types/services/Queue.d.ts +5 -3
  84. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  85. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  86. package/dist/types/services/config/constants.d.ts +8 -5
  87. package/dist/types/services/config/types.d.ts +17 -4
  88. package/dist/types/services/core/Err.d.ts +2 -0
  89. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  90. package/dist/types/services/core/types.d.ts +2 -0
  91. package/dist/types/services/task/Task.d.ts +20 -2
  92. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  93. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  94. package/dist/types/services/task/constants.d.ts +3 -0
  95. package/dist/types/services/task/contact.d.ts +4 -0
  96. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  97. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
  98. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  99. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  100. package/dist/types/services/task/types.d.ts +81 -4
  101. package/dist/types/services/task/voice/Voice.d.ts +48 -9
  102. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  103. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  104. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  105. package/dist/types/types.d.ts +14 -8
  106. package/dist/types/utils/PageCache.d.ts +20 -3
  107. package/dist/types.js +7 -2
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/PageCache.js +19 -5
  110. package/dist/utils/PageCache.js.map +1 -1
  111. package/dist/webex.js +1 -1
  112. package/package.json +9 -9
  113. package/src/cc.ts +506 -9
  114. package/src/config.ts +7 -0
  115. package/src/constants.ts +8 -0
  116. package/src/index.ts +2 -0
  117. package/src/metrics/ai-docs/metrics-spec.md +9 -3
  118. package/src/metrics/behavioral-events.ts +14 -0
  119. package/src/metrics/constants.ts +2 -0
  120. package/src/services/AddressBook.ts +17 -6
  121. package/src/services/AnswerCallOnWebexService.ts +206 -0
  122. package/src/services/EntryPoint.ts +59 -60
  123. package/src/services/Queue.ts +29 -12
  124. package/src/services/WebexCrossClientService.ts +212 -0
  125. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  126. package/src/services/ai-docs/AGENTS.md +10 -10
  127. package/src/services/ai-docs/services-spec.md +6 -1
  128. package/src/services/config/Util.ts +3 -0
  129. package/src/services/config/ai-docs/AGENTS.md +1 -1
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  131. package/src/services/config/ai-docs/config-spec.md +6 -0
  132. package/src/services/config/constants.ts +10 -6
  133. package/src/services/config/types.ts +16 -4
  134. package/src/services/core/Err.ts +1 -0
  135. package/src/services/core/WebexRequest.ts +3 -1
  136. package/src/services/core/ai-docs/core-spec.md +5 -1
  137. package/src/services/core/aqm-reqs.ts +30 -15
  138. package/src/services/core/types.ts +2 -0
  139. package/src/services/task/Task.ts +79 -8
  140. package/src/services/task/TaskFactory.ts +8 -3
  141. package/src/services/task/TaskManager.ts +522 -16
  142. package/src/services/task/WebexCallingUtils.ts +136 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  144. package/src/services/task/ai-docs/task-spec.md +152 -2
  145. package/src/services/task/constants.ts +3 -0
  146. package/src/services/task/contact.ts +30 -0
  147. package/src/services/task/digital/Digital.ts +4 -2
  148. package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
  149. package/src/services/task/state-machine/actions.ts +5 -2
  150. package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
  151. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
  152. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +56 -24
  153. package/src/services/task/state-machine/guards.ts +64 -11
  154. package/src/services/task/state-machine/types.ts +16 -1
  155. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  156. package/src/services/task/types.ts +114 -4
  157. package/src/services/task/voice/Voice.ts +462 -12
  158. package/src/services/task/voice/WebRTC.ts +5 -1
  159. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  160. package/src/services/wxAppTelephonyUtils.ts +14 -0
  161. package/src/types.ts +33 -9
  162. package/src/utils/AGENTS.md +21 -10
  163. package/src/utils/PageCache.ts +38 -5
  164. package/src/utils/ai-docs/utils-spec.md +21 -11
  165. package/test/unit/spec/cc.ts +1274 -0
  166. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  167. package/test/unit/spec/services/AddressBook.ts +37 -6
  168. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  169. package/test/unit/spec/services/EntryPoint.ts +87 -40
  170. package/test/unit/spec/services/Queue.ts +123 -12
  171. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  172. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  173. package/test/unit/spec/services/config/Util.ts +85 -0
  174. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  175. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  176. package/test/unit/spec/services/task/Task.ts +200 -0
  177. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  178. package/test/unit/spec/services/task/TaskManager.ts +1294 -1
  179. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  180. package/test/unit/spec/services/task/contact.ts +33 -0
  181. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
  182. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  183. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  184. package/test/unit/spec/services/task/voice/Voice.ts +874 -0
  185. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  186. package/umd/contact-center.min.js +2 -2
  187. package/umd/contact-center.min.js.map +1 -1
@@ -14,11 +14,14 @@ var _types3 = require("../../types");
14
14
  var _loggerProxy = _interopRequireDefault(require("../../logger-proxy"));
15
15
  var _TaskUtils = require("./TaskUtils");
16
16
  var _TaskFactory = _interopRequireDefault(require("./TaskFactory"));
17
+ var _WebexCallingUtils = require("./WebexCallingUtils");
17
18
  var _WebRTC = _interopRequireDefault(require("./voice/WebRTC"));
18
19
  var _stateMachine = require("./state-machine");
20
+ var _constants3 = require("./state-machine/constants");
19
21
  var _taskDataNormalizer = require("./taskDataNormalizer");
20
22
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
21
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]);
22
25
  const isCcEvent = value => CC_EVENT_SET.has(value);
23
26
 
24
27
  /** @internal */
@@ -109,6 +112,9 @@ class TaskManager extends _events.default {
109
112
  setWebRtcEnabled(webRtcEnabled) {
110
113
  this.webRtcEnabled = webRtcEnabled;
111
114
  }
115
+ setAnswerCallOnWebexService(answerCallOnWebexService) {
116
+ this.answerCallOnWebexService = answerCallOnWebexService;
117
+ }
112
118
  handleIncomingWebCall = call => {
113
119
  const currentTask = Object.values(this.taskCollection).find(task => task.data.interaction.mediaType === 'telephony' && !(0, _TaskUtils.isCampaignPreviewReservation)(task));
114
120
  if (currentTask) {
@@ -139,9 +145,10 @@ class TaskManager extends _events.default {
139
145
  * @param ccEvent - The CC_EVENT type from WebSocket
140
146
  * @param payload - The event payload
141
147
  * @param agentId - Optional agent ID for state detection (needed for HYDRATE)
148
+ * @param task - Optional task instance for state-aware CONTACT_ENDED mapping
142
149
  * @returns TaskEventPayload for state machine or null if no mapping
143
150
  */
144
- static mapEventToTaskStateMachineEvent(ccEvent, payload, agentId) {
151
+ static mapEventToTaskStateMachineEvent(ccEvent, payload, agentId, task, enableWxBetterTogether) {
145
152
  const mediaResourceId = payload.mediaResourceId || payload.interaction?.media?.[payload.interactionId]?.mediaResourceId;
146
153
  switch (ccEvent) {
147
154
  // CC -> TaskEvent mappings (see TaskStateMachine comment for quick reference)
@@ -180,6 +187,12 @@ class TaskManager extends _events.default {
180
187
  agentId
181
188
  };
182
189
  case _types2.CC_EVENTS.CONTACT_UPDATED:
190
+ if (task && typeof payload.interaction?.owner === 'string' && payload.interaction.owner.trim().length > 0 && payload.interaction.owner !== task.data?.interaction?.owner) {
191
+ return {
192
+ type: _stateMachine.TaskEvent.CONTACT_OWNER_CHANGED,
193
+ taskData: payload
194
+ };
195
+ }
183
196
  return {
184
197
  type: _stateMachine.TaskEvent.CONTACT_UPDATED,
185
198
  taskData: payload
@@ -284,13 +297,31 @@ class TaskManager extends _events.default {
284
297
  taskData: payload
285
298
  };
286
299
  case _types2.CC_EVENTS.CONTACT_ENDED:
287
- return {
288
- type: _stateMachine.TaskEvent.CONTACT_ENDED,
289
- taskData: {
290
- ...payload,
291
- wrapUpRequired: (0, _TaskUtils.isCampaignPreviewTask)(payload) ? false : payload.agentsPendingWrapUp?.includes(agentId || '') || false
300
+ {
301
+ const wrapUpRequired = (0, _TaskUtils.isCampaignPreviewTask)(payload) ? false : payload.agentsPendingWrapUp?.includes(agentId || '') || false;
302
+
303
+ // Post-accept wxApp outdial agent-end delivers ContactEnded instead of
304
+ // AgentOutboundFailed. Normalize to OUTBOUND_FAILED so state machine
305
+ // enters WRAPPING_UP and emits TASK_OUTDIAL_FAILED (AGENT_ENDS).
306
+ // Pre-accept cancelTask decline stays CONTACT_ENDED → TERMINATED (no wrapup).
307
+ if (TaskManager.isAgentTerminatedOutdialWrapup(payload, wrapUpRequired, task, agentId, enableWxBetterTogether)) {
308
+ return {
309
+ type: _stateMachine.TaskEvent.OUTBOUND_FAILED,
310
+ reason: 'AGENT_ENDS',
311
+ taskData: {
312
+ ...payload,
313
+ wrapUpRequired: true
314
+ }
315
+ };
292
316
  }
293
- };
317
+ return {
318
+ type: _stateMachine.TaskEvent.CONTACT_ENDED,
319
+ taskData: {
320
+ ...payload,
321
+ wrapUpRequired
322
+ }
323
+ };
324
+ }
294
325
  case _types2.CC_EVENTS.AGENT_INVITE_FAILED:
295
326
  return {
296
327
  type: _stateMachine.TaskEvent.INVITE_FAILED,
@@ -308,11 +339,18 @@ class TaskManager extends _events.default {
308
339
  reason: payload.reason
309
340
  };
310
341
  case _types2.CC_EVENTS.AGENT_OUTBOUND_FAILED:
311
- return {
312
- type: _stateMachine.TaskEvent.OUTBOUND_FAILED,
313
- taskData: payload,
314
- reason: payload.reason
315
- };
342
+ {
343
+ const isAgentTerminatedOutdial = payload.interaction?.outboundType === 'OUTDIAL' && (payload.terminatingParty === 'Agent' || payload.reason === 'AGENT_ENDS');
344
+ const suppressOutdialFailedPopup = isAgentTerminatedOutdial && !TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether);
345
+ return {
346
+ type: _stateMachine.TaskEvent.OUTBOUND_FAILED,
347
+ taskData: suppressOutdialFailedPopup ? {
348
+ ...payload,
349
+ suppressOutdialFailedPopup: true
350
+ } : payload,
351
+ reason: payload.reason
352
+ };
353
+ }
316
354
  case _types2.CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
317
355
  return {
318
356
  type: _stateMachine.TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED,
@@ -385,6 +423,43 @@ class TaskManager extends _events.default {
385
423
  }
386
424
  }
387
425
 
426
+ /**
427
+ * True when the current agent's task is a wxApp-managed outdial (thick-client telephony).
428
+ */
429
+ static isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether) {
430
+ const wxAppEnabled = task?.uiControlConfig?.enableWxBetterTogether ?? enableWxBetterTogether ?? false;
431
+ if (!wxAppEnabled) {
432
+ return false;
433
+ }
434
+ const effectiveAgentId = agentId ?? task?.data?.agentId;
435
+ const participants = payload.interaction?.participants ?? task?.data?.interaction?.participants;
436
+ const details = (0, _WebexCallingUtils.getWebexCallingDeviceDetailsForAgent)(effectiveAgentId, participants);
437
+ return details?.deviceType === 'wxApp';
438
+ }
439
+
440
+ /**
441
+ * True when ContactEnded represents an agent-terminated wxApp outdial that should
442
+ * enter wrapup with an outdial-failed popup (legacy AgentOutboundFailed parity).
443
+ */
444
+ static isAgentTerminatedOutdialWrapup(payload, wrapUpRequired, task, agentId, enableWxBetterTogether) {
445
+ const isOutdial = payload.interaction?.outboundType === 'OUTDIAL';
446
+ const agentTerminated = payload.terminatingParty === 'Agent';
447
+ if (!isOutdial || !agentTerminated) {
448
+ return false;
449
+ }
450
+ if (!TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether)) {
451
+ return false;
452
+ }
453
+ const wxAppAnswerPending = task?.uiControlConfig?.wxAppAnswerPending ?? false;
454
+
455
+ // Pre-accept decline: decline() clears wxAppAnswerPending; backend may still send state wrapUp.
456
+ if (!wxAppAnswerPending && !wrapUpRequired) {
457
+ return false;
458
+ }
459
+ const interactionWrapUp = payload.interaction?.state === 'wrapUp';
460
+ return wrapUpRequired || interactionWrapUp || wxAppAnswerPending;
461
+ }
462
+
388
463
  /**
389
464
  * Register WebSocket message listeners for task events
390
465
  *
@@ -493,6 +568,12 @@ class TaskManager extends _events.default {
493
568
  this.taskCollection[interactionId] = task;
494
569
  }
495
570
  }
571
+ if (!task && eventType === _types2.CC_EVENTS.CONTACT_OWNER_CHANGED) {
572
+ task = this.findTaskForContactOwnerChange(message.data);
573
+ }
574
+ if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
575
+ task = this.findUniqueTaskByRelatedInteraction(message.data);
576
+ }
496
577
  const wasConsultedTask = Boolean(task?.data?.isConsulted);
497
578
  const computeWrapUpRequired = () => {
498
579
  if (message.data.wrapUpRequired !== undefined) {
@@ -503,11 +584,17 @@ class TaskManager extends _events.default {
503
584
  }
504
585
  return !wasConsultedTask;
505
586
  };
506
- const adjustedPayload = eventType === _types2.CC_EVENTS.AGENT_CONSULT_TRANSFERRED || eventType === _types2.CC_EVENTS.AGENT_BLIND_TRANSFERRED || eventType === _types2.CC_EVENTS.AGENT_VTEAM_TRANSFERRED ? {
587
+ let adjustedPayload = eventType === _types2.CC_EVENTS.AGENT_CONSULT_TRANSFERRED || eventType === _types2.CC_EVENTS.AGENT_BLIND_TRANSFERRED || eventType === _types2.CC_EVENTS.AGENT_VTEAM_TRANSFERRED ? {
507
588
  ...message.data,
508
589
  wrapUpRequired: computeWrapUpRequired()
509
590
  } : message.data;
510
- const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(eventType, adjustedPayload, this.agentId);
591
+ if (task && eventType === _types2.CC_EVENTS.CONTACT_OWNER_CHANGED) {
592
+ adjustedPayload = TaskManager.preserveMainTaskIdentity(task, adjustedPayload);
593
+ }
594
+ if (task && eventType === _types2.CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
595
+ adjustedPayload = TaskManager.preserveConfirmedOwner(task, adjustedPayload);
596
+ }
597
+ const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(eventType, adjustedPayload, this.agentId, task, this.configFlags?.enableWxBetterTogether);
511
598
  _loggerProxy.default.info(`Handling task event ${eventType}`, {
512
599
  module: _constants.TASK_MANAGER_FILE,
513
600
  method: 'prepareEventContext',
@@ -521,6 +608,134 @@ class TaskManager extends _events.default {
521
608
  };
522
609
  }
523
610
 
611
+ /**
612
+ * ContactOwnerChanged can identify the promoted agent's child interaction while
613
+ * the surviving task is stored under the main interaction. Accept one related
614
+ * task when its existing snapshot or the authoritative incoming promotion proves
615
+ * current-agent main-call membership, while excluding non-promoted and ambiguous matches.
616
+ */
617
+ findTaskForContactOwnerChange(payload) {
618
+ const relatedCandidates = this.findRelatedTasks(payload, {
619
+ includeCollectionKeys: true,
620
+ includeMainCallMediaKeys: true
621
+ });
622
+ const incomingShowsCurrentAgentPromotion = this.isCurrentAgentPromotedOnIncomingMainCall(payload);
623
+ const eligibleCandidates = relatedCandidates.filter(candidate => this.isCurrentAgentActiveOnMainInteraction(candidate.data) || incomingShowsCurrentAgentPromotion);
624
+ if (eligibleCandidates.length === 1) {
625
+ return eligibleCandidates[0];
626
+ }
627
+ if (eligibleCandidates.length > 1) {
628
+ _loggerProxy.default.warn('Unable to correlate task event to a unique main interaction task', {
629
+ module: _constants.TASK_MANAGER_FILE,
630
+ method: 'findTaskForContactOwnerChange',
631
+ interactionId: payload.interaction?.mainInteractionId || payload.interaction?.parentInteractionId || payload.interaction?.callProcessingDetails?.parentInteractionId || payload.interactionId
632
+ });
633
+ return undefined;
634
+ }
635
+ return undefined;
636
+ }
637
+ isCurrentAgentActiveOnMainInteraction(taskData) {
638
+ const currentAgentId = this.agentId || taskData?.agentId;
639
+ if (!currentAgentId) return false;
640
+ return TaskManager.isParticipantActiveOnMainInteraction(taskData, currentAgentId);
641
+ }
642
+ static isParticipantActiveOnMainInteraction(taskData, participantId) {
643
+ const interaction = taskData?.interaction;
644
+ const participant = interaction?.participants?.[participantId];
645
+ if (!participant || participant.hasLeft === true) return false;
646
+ return Object.values(interaction?.media ?? {}).some(media => media?.mType === _constants3.MEDIA_TYPE_MAIN_CALL && media.participants?.includes(participantId));
647
+ }
648
+ isCurrentAgentPromotedOnIncomingMainCall(payload) {
649
+ return Boolean(this.agentId && payload.interaction?.owner === this.agentId && TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId));
650
+ }
651
+ static getMainCallMediaId(interaction) {
652
+ return Object.entries(interaction?.media ?? {}).find(([, media]) => media?.mType === _constants3.MEDIA_TYPE_MAIN_CALL)?.[0];
653
+ }
654
+ static getStableMainInteractionId(payload) {
655
+ return payload.interaction?.mainInteractionId || TaskManager.getMainCallMediaId(payload.interaction) || payload.interaction?.interactionId || payload.interactionId;
656
+ }
657
+ canRecoverTaskFromContactOwnerChanged(payload) {
658
+ const interaction = payload.interaction;
659
+ const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
660
+ return Boolean(this.agentId && stableInteractionId && interaction?.mediaType === _types.MEDIA_CHANNEL.TELEPHONY && interaction.isTerminated !== true && interaction.owner === this.agentId && TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId));
661
+ }
662
+
663
+ /** Keep a related owner-change event from re-keying the surviving main task to a child ID. */
664
+ static preserveMainTaskIdentity(task, payload) {
665
+ const currentMainInteractionId = task.data?.interaction?.mainInteractionId;
666
+ const payloadMainInteractionId = payload.interaction?.mainInteractionId;
667
+ const currentMainMediaId = TaskManager.getMainCallMediaId(task.data?.interaction);
668
+ const payloadMainMediaId = TaskManager.getMainCallMediaId(payload.interaction);
669
+ const stableInteractionId = currentMainInteractionId || payloadMainInteractionId || currentMainMediaId || payloadMainMediaId || payload.interaction?.interactionId || task.data?.interaction?.interactionId || task.data?.interactionId;
670
+ if (!stableInteractionId || stableInteractionId === payload.interactionId) {
671
+ return payload;
672
+ }
673
+ return {
674
+ ...payload,
675
+ interactionId: stableInteractionId
676
+ };
677
+ }
678
+
679
+ /**
680
+ * ParticipantLeftConference may arrive after an owner update while still carrying
681
+ * the departed owner. Keep the confirmed owner only when the new roster proves that
682
+ * owner is active on mainCall and identifies the incoming owner as departed.
683
+ */
684
+ static preserveConfirmedOwner(task, payload) {
685
+ const confirmedOwner = task.data?.interaction?.owner;
686
+ const incomingOwner = payload.interaction?.owner;
687
+ if (!confirmedOwner || !incomingOwner || confirmedOwner === incomingOwner) {
688
+ return payload;
689
+ }
690
+ const confirmedOwnerIsActive = TaskManager.isParticipantActiveOnMainInteraction(payload, confirmedOwner);
691
+ const incomingOwnerIsExplicitlyDeparted = payload.participantId === incomingOwner || payload.interaction?.participants?.[incomingOwner]?.hasLeft === true;
692
+ if (!confirmedOwnerIsActive || !incomingOwnerIsExplicitlyDeparted) {
693
+ return payload;
694
+ }
695
+ return {
696
+ ...payload,
697
+ owner: confirmedOwner,
698
+ interaction: {
699
+ ...payload.interaction,
700
+ owner: confirmedOwner
701
+ }
702
+ };
703
+ }
704
+
705
+ /**
706
+ * Resolve lifecycle events that are emitted for the main interaction while the
707
+ * local task can still be indexed by a child consult interaction. Exact task
708
+ * keys are handled before this fallback. Multiple aliases of the same task are
709
+ * treated as one candidate; genuinely ambiguous matches are intentionally ignored.
710
+ */
711
+ findUniqueTaskByRelatedInteraction(payload, candidateFilter = () => true) {
712
+ const candidates = this.findRelatedTasks(payload).filter(candidateFilter);
713
+ if (candidates.length === 1) {
714
+ return candidates[0];
715
+ }
716
+ if (candidates.length > 1) {
717
+ _loggerProxy.default.warn('Unable to correlate task event to a unique main interaction task', {
718
+ module: _constants.TASK_MANAGER_FILE,
719
+ method: 'findUniqueTaskByRelatedInteraction',
720
+ interactionId: payload.interaction?.mainInteractionId || payload.interaction?.parentInteractionId || payload.interaction?.callProcessingDetails?.parentInteractionId || payload.interactionId
721
+ });
722
+ }
723
+ return undefined;
724
+ }
725
+ findRelatedTasks(payload, options = {}) {
726
+ const {
727
+ includeCollectionKeys = false,
728
+ includeMainCallMediaKeys = false
729
+ } = options;
730
+ 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(interactionId => Boolean(interactionId)));
731
+ if (payloadInteractionIds.size === 0) return [];
732
+ return [...new Set(Object.entries(this.taskCollection).filter(([taskId, candidate]) => {
733
+ const taskInteraction = candidate?.data?.interaction;
734
+ const candidateInteractionIds = [...(includeCollectionKeys ? [taskId] : []), candidate?.data?.interactionId, taskInteraction?.interactionId, taskInteraction?.mainInteractionId, taskInteraction?.parentInteractionId, taskInteraction?.callProcessingDetails?.parentInteractionId, ...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(taskInteraction)] : [])];
735
+ return candidateInteractionIds.some(interactionId => Boolean(interactionId) && payloadInteractionIds.has(interactionId));
736
+ }).map(([, candidate]) => candidate))];
737
+ }
738
+
524
739
  /**
525
740
  * Handle task lifecycle events and determine required actions
526
741
  *
@@ -544,6 +759,8 @@ class TaskManager extends _events.default {
544
759
  return this.handleAgentContact(context);
545
760
  case _types2.CC_EVENTS.CONTACT_MERGED:
546
761
  return this.handleContactMergedEvent(context);
762
+ case _types2.CC_EVENTS.CONTACT_OWNER_CHANGED:
763
+ return this.handleContactOwnerChanged(context);
547
764
  case _types2.CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
548
765
  return this.handleCampaignPreviewReservation(context);
549
766
  case _types2.CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
@@ -554,6 +771,63 @@ class TaskManager extends _events.default {
554
771
  };
555
772
  }
556
773
  }
774
+
775
+ /**
776
+ * Recover the promoted agent's active voice task when local task state was lost.
777
+ * Existing related tasks are never replaced: an unresolved or ambiguous relation
778
+ * is safer to ignore than to create a duplicate task.
779
+ */
780
+ handleContactOwnerChanged(context) {
781
+ if (context.task) {
782
+ return {
783
+ task: context.task
784
+ };
785
+ }
786
+ const {
787
+ payload
788
+ } = context;
789
+ if (this.findRelatedTasks(payload, {
790
+ includeCollectionKeys: true,
791
+ includeMainCallMediaKeys: true
792
+ }).length > 0 || !this.canRecoverTaskFromContactOwnerChanged(payload)) {
793
+ return {};
794
+ }
795
+ const stableInteractionId = TaskManager.getStableMainInteractionId(payload);
796
+ if (!stableInteractionId || this.taskCollection[stableInteractionId]) {
797
+ return {};
798
+ }
799
+ const normalizedPayload = payload.interactionId === stableInteractionId ? payload : {
800
+ ...payload,
801
+ interactionId: stableInteractionId
802
+ };
803
+ const taskData = {
804
+ ...normalizedPayload,
805
+ owner: normalizedPayload.interaction.owner,
806
+ wrapUpRequired: normalizedPayload.interaction.participants?.[this.agentId]?.isWrapUp || false,
807
+ isConferenceInProgress: (0, _TaskUtils.getIsConferenceInProgress)(normalizedPayload),
808
+ isConsulted: false,
809
+ isAutoAnswering: false
810
+ };
811
+ const task = _TaskFactory.default.createTask(this.contact, this.webCallingService, taskData, this.configFlags, this.wrapupData, this.agentId, this.answerCallOnWebexService);
812
+ this.taskCollection[stableInteractionId] = task;
813
+
814
+ // Restore the actor before installing listeners so the internal hydrate does
815
+ // not leak as a second public hydrate or as an incoming-task notification.
816
+ task.sendStateMachineEvent({
817
+ type: _stateMachine.TaskEvent.HYDRATE,
818
+ taskData,
819
+ agentId: this.agentId
820
+ });
821
+ this.setupTaskListeners(task);
822
+ context.payload = taskData;
823
+ context.stateMachineEvent = {
824
+ type: _stateMachine.TaskEvent.CONTACT_OWNER_CHANGED,
825
+ taskData
826
+ };
827
+ return {
828
+ task
829
+ };
830
+ }
557
831
  handleCampaignContactUpdated(context) {
558
832
  const {
559
833
  payload
@@ -638,7 +912,7 @@ class TaskManager extends _events.default {
638
912
  wrapUpRequired: false,
639
913
  isConferenceInProgress: false,
640
914
  isAutoAnswering: false
641
- }, this.configFlags, this.wrapupData, this.agentId);
915
+ }, this.configFlags, this.wrapupData, this.agentId, this.answerCallOnWebexService);
642
916
  this.setupTaskListeners(task);
643
917
  this.taskCollection[payload.interactionId] = task;
644
918
  } else {
@@ -664,7 +938,7 @@ class TaskManager extends _events.default {
664
938
  isConsulted: isConsultedTask,
665
939
  isAutoAnswering: shouldAutoAnswer
666
940
  };
667
- const task = _TaskFactory.default.createTask(this.contact, this.webCallingService, taskData, this.configFlags, this.wrapupData, this.agentId);
941
+ const task = _TaskFactory.default.createTask(this.contact, this.webCallingService, taskData, this.configFlags, this.wrapupData, this.agentId, this.answerCallOnWebexService);
668
942
  this.setupTaskListeners(task);
669
943
  this.taskCollection[payload.interactionId] = task;
670
944
  return {
@@ -693,7 +967,7 @@ class TaskManager extends _events.default {
693
967
  isConferenceInProgress: (0, _TaskUtils.getIsConferenceInProgress)(payload),
694
968
  isAutoAnswering: shouldAutoAnswer
695
969
  };
696
- task = _TaskFactory.default.createTask(this.contact, this.webCallingService, taskData, this.configFlags, this.wrapupData, this.agentId);
970
+ task = _TaskFactory.default.createTask(this.contact, this.webCallingService, taskData, this.configFlags, this.wrapupData, this.agentId, this.answerCallOnWebexService);
697
971
  this.setupTaskListeners(task);
698
972
  this.taskCollection[payload.interactionId] = task;
699
973
  }
@@ -713,6 +987,14 @@ class TaskManager extends _events.default {
713
987
  destinationType: taskData.destinationType ?? snapshot?.context?.consultDestinationType ?? null
714
988
  } : taskData;
715
989
  task.updateTaskData(updateTaskData);
990
+
991
+ // A main-interaction lifecycle event can re-key a child consult task. Remove
992
+ // any aliases first so consumers never observe duplicate task entries.
993
+ Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
994
+ if (candidate === task && taskId !== taskData.interactionId) {
995
+ delete this.taskCollection[taskId];
996
+ }
997
+ });
716
998
  this.taskCollection[taskData.interactionId] = task;
717
999
  return task;
718
1000
  }
@@ -753,8 +1035,8 @@ class TaskManager extends _events.default {
753
1035
  task.on(_types.TASK_EVENTS.TASK_CLEANUP, (t, options) => {
754
1036
  this.handleTaskCleanup(t);
755
1037
  if (options?.removeFromCollection) {
756
- const interactionId = t?.data?.interactionId;
757
- if (interactionId && this.taskCollection[interactionId]) {
1038
+ const taskIsStillCollected = Object.values(this.taskCollection).some(candidate => candidate === t);
1039
+ if (taskIsStillCollected) {
758
1040
  this.removeTaskFromCollection(t);
759
1041
  }
760
1042
  }
@@ -765,7 +1047,11 @@ class TaskManager extends _events.default {
765
1047
  task.cancelAutoWrapupTimer();
766
1048
  }
767
1049
  if (task?.data?.interactionId) {
768
- delete this.taskCollection[task.data.interactionId];
1050
+ Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
1051
+ if (candidate === task) {
1052
+ delete this.taskCollection[taskId];
1053
+ }
1054
+ });
769
1055
  _loggerProxy.default.info(`Task removed from collection`, {
770
1056
  module: _constants.TASK_MANAGER_FILE,
771
1057
  method: _constants2.METHODS.REMOVE_TASK_FROM_COLLECTION,
@@ -810,7 +1096,7 @@ class TaskManager extends _events.default {
810
1096
  isConferenceInProgress: (0, _TaskUtils.getIsConferenceInProgress)(payload),
811
1097
  isConsulted: false
812
1098
  };
813
- task = _TaskFactory.default.createTask(this.contact, this.webCallingService, taskData, this.configFlags, this.wrapupData, this.agentId);
1099
+ task = _TaskFactory.default.createTask(this.contact, this.webCallingService, taskData, this.configFlags, this.wrapupData, this.agentId, this.answerCallOnWebexService);
814
1100
  this.taskCollection[payload.interactionId] = task;
815
1101
 
816
1102
  // Transition the new task out of IDLE immediately so UI controls are
@@ -884,6 +1170,52 @@ class TaskManager extends _events.default {
884
1170
  ...this.taskCollection
885
1171
  };
886
1172
  }
1173
+
1174
+ /**
1175
+ * Propagate runtime enableWxBetterTogether changes to config flags and active tasks.
1176
+ */
1177
+ applyEnableWxBetterTogether(enabled) {
1178
+ if (this.configFlags) {
1179
+ this.configFlags = {
1180
+ ...this.configFlags,
1181
+ enableWxBetterTogether: enabled
1182
+ };
1183
+ }
1184
+ Object.values(this.taskCollection).forEach(task => {
1185
+ task.setEnableWxBetterTogether(enabled);
1186
+ if (enabled) {
1187
+ TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
1188
+ }
1189
+ });
1190
+ }
1191
+
1192
+ /**
1193
+ * Propagate wxApp mute state from Mercury sync to active voice tasks.
1194
+ */
1195
+ applyWxAppMuteStateFromSync(callId, muted) {
1196
+ Object.values(this.taskCollection).forEach(task => {
1197
+ task.applyWxAppMuteStateFromSync(callId, muted);
1198
+ });
1199
+ }
1200
+
1201
+ /**
1202
+ * Re-seeds wxApp mute state from GET telephony/calls/{callId} for one task.
1203
+ */
1204
+ static syncWxAppMuteFromCallDetailsForTask(task) {
1205
+ const sync = task.syncWxAppMuteFromCallDetails?.bind(task);
1206
+ if (typeof sync === 'function') {
1207
+ sync().catch(() => undefined);
1208
+ }
1209
+ }
1210
+
1211
+ /**
1212
+ * Re-seeds wxApp mute for all engaged voice tasks (reload / runtime enable backfill).
1213
+ */
1214
+ syncWxAppMuteFromCallDetailsForAllTasks() {
1215
+ Object.values(this.taskCollection).forEach(task => {
1216
+ TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
1217
+ });
1218
+ }
887
1219
  static getTaskManager(apiAIAssistant, contact, webCallingService, webSocketManager, rtdWebSocketManager) {
888
1220
  if (!TaskManager.taskManager) {
889
1221
  TaskManager.taskManager = new TaskManager(apiAIAssistant, contact, webCallingService, webSocketManager, rtdWebSocketManager);