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

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 +5 -1
  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 +176 -20
  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 +1 -1
  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 +254 -15
  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 +117 -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 +46 -22
  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 +653 -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
@@ -27,6 +27,8 @@ import {
27
27
  shouldAutoAnswerTask,
28
28
  } from './TaskUtils';
29
29
  import TaskFactory from './TaskFactory';
30
+ import AnswerCallOnWebexService from '../AnswerCallOnWebexService';
31
+ import {getWebexCallingDeviceDetailsForAgent} from './WebexCallingUtils';
30
32
  import WebRTC from './voice/WebRTC';
31
33
  import {TaskEvent, type TaskEventPayload} from './state-machine';
32
34
  import {normalizeTaskData} from './taskDataNormalizer';
@@ -34,6 +36,11 @@ import {ApiAIAssistant} from '../ApiAiAssistant';
34
36
 
35
37
  const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
36
38
 
39
+ const MAIN_INTERACTION_CORRELATED_EVENTS = new Set<CC_EVENTS>([
40
+ CC_EVENTS.AGENT_CONSULT_ENDED,
41
+ CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
42
+ ]);
43
+
37
44
  const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
38
45
 
39
46
  /** @internal */
@@ -55,6 +62,7 @@ export default class TaskManager extends EventEmitter {
55
62
  private wrapupData: WrapupData;
56
63
  private agentId: string;
57
64
  private webRtcEnabled: boolean;
65
+ private answerCallOnWebexService?: AnswerCallOnWebexService;
58
66
  private apiAIAssistant?: ApiAIAssistant;
59
67
  /**
60
68
  * @param contact - Routing Contact layer. Talks to AQMReq layer to convert events to promises
@@ -148,6 +156,10 @@ export default class TaskManager extends EventEmitter {
148
156
  this.webRtcEnabled = webRtcEnabled;
149
157
  }
150
158
 
159
+ public setAnswerCallOnWebexService(answerCallOnWebexService: AnswerCallOnWebexService) {
160
+ this.answerCallOnWebexService = answerCallOnWebexService;
161
+ }
162
+
151
163
  private handleIncomingWebCall = (call: ICall) => {
152
164
  const currentTask = Object.values(this.taskCollection).find(
153
165
  (task) =>
@@ -187,12 +199,15 @@ export default class TaskManager extends EventEmitter {
187
199
  * @param ccEvent - The CC_EVENT type from WebSocket
188
200
  * @param payload - The event payload
189
201
  * @param agentId - Optional agent ID for state detection (needed for HYDRATE)
202
+ * @param task - Optional task instance for state-aware CONTACT_ENDED mapping
190
203
  * @returns TaskEventPayload for state machine or null if no mapping
191
204
  */
192
205
  private static mapEventToTaskStateMachineEvent(
193
206
  ccEvent: CC_EVENTS,
194
207
  payload: WebSocketPayload,
195
- agentId?: string
208
+ agentId?: string,
209
+ task?: ITask,
210
+ enableWxBetterTogether?: boolean
196
211
  ): TaskEventPayload | null {
197
212
  const mediaResourceId =
198
213
  payload.mediaResourceId ||
@@ -299,16 +314,39 @@ export default class TaskManager extends EventEmitter {
299
314
  case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
300
315
  return {type: TaskEvent.TRANSFER_FAILED, taskData: payload};
301
316
 
302
- case CC_EVENTS.CONTACT_ENDED:
317
+ case CC_EVENTS.CONTACT_ENDED: {
318
+ const wrapUpRequired = isCampaignPreviewTask(payload)
319
+ ? false
320
+ : payload.agentsPendingWrapUp?.includes(agentId || '') || false;
321
+
322
+ // Post-accept wxApp outdial agent-end delivers ContactEnded instead of
323
+ // AgentOutboundFailed. Normalize to OUTBOUND_FAILED so state machine
324
+ // enters WRAPPING_UP and emits TASK_OUTDIAL_FAILED (AGENT_ENDS).
325
+ // Pre-accept cancelTask decline stays CONTACT_ENDED → TERMINATED (no wrapup).
326
+ if (
327
+ TaskManager.isAgentTerminatedOutdialWrapup(
328
+ payload,
329
+ wrapUpRequired,
330
+ task,
331
+ agentId,
332
+ enableWxBetterTogether
333
+ )
334
+ ) {
335
+ return {
336
+ type: TaskEvent.OUTBOUND_FAILED,
337
+ reason: 'AGENT_ENDS',
338
+ taskData: {...payload, wrapUpRequired: true},
339
+ };
340
+ }
341
+
303
342
  return {
304
343
  type: TaskEvent.CONTACT_ENDED,
305
344
  taskData: {
306
345
  ...payload,
307
- wrapUpRequired: isCampaignPreviewTask(payload)
308
- ? false
309
- : payload.agentsPendingWrapUp?.includes(agentId || '') || false,
346
+ wrapUpRequired,
310
347
  },
311
348
  };
349
+ }
312
350
 
313
351
  case CC_EVENTS.AGENT_INVITE_FAILED:
314
352
  return {type: TaskEvent.INVITE_FAILED, reason: payload.reason};
@@ -319,8 +357,23 @@ export default class TaskManager extends EventEmitter {
319
357
  case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
320
358
  return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
321
359
 
322
- case CC_EVENTS.AGENT_OUTBOUND_FAILED:
323
- return {type: TaskEvent.OUTBOUND_FAILED, taskData: payload, reason: payload.reason};
360
+ case CC_EVENTS.AGENT_OUTBOUND_FAILED: {
361
+ const isAgentTerminatedOutdial =
362
+ payload.interaction?.outboundType === 'OUTDIAL' &&
363
+ (payload.terminatingParty === 'Agent' || payload.reason === 'AGENT_ENDS');
364
+
365
+ const suppressOutdialFailedPopup =
366
+ isAgentTerminatedOutdial &&
367
+ !TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether);
368
+
369
+ return {
370
+ type: TaskEvent.OUTBOUND_FAILED,
371
+ taskData: suppressOutdialFailedPopup
372
+ ? {...payload, suppressOutdialFailedPopup: true}
373
+ : payload,
374
+ reason: payload.reason,
375
+ };
376
+ }
324
377
 
325
378
  case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
326
379
  return {type: TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED, taskData: payload};
@@ -371,6 +424,68 @@ export default class TaskManager extends EventEmitter {
371
424
  }
372
425
  }
373
426
 
427
+ /**
428
+ * True when the current agent's task is a wxApp-managed outdial (thick-client telephony).
429
+ */
430
+ private static isWxAppManagedOutdialTask(
431
+ agentId: string | undefined,
432
+ payload: WebSocketPayload,
433
+ task: ITask | undefined,
434
+ enableWxBetterTogether?: boolean
435
+ ): boolean {
436
+ const wxAppEnabled =
437
+ (task as {uiControlConfig?: {enableWxBetterTogether?: boolean}})?.uiControlConfig
438
+ ?.enableWxBetterTogether ??
439
+ enableWxBetterTogether ??
440
+ false;
441
+
442
+ if (!wxAppEnabled) {
443
+ return false;
444
+ }
445
+
446
+ const effectiveAgentId = agentId ?? task?.data?.agentId;
447
+ const participants = payload.interaction?.participants ?? task?.data?.interaction?.participants;
448
+ const details = getWebexCallingDeviceDetailsForAgent(effectiveAgentId, participants);
449
+
450
+ return details?.deviceType === 'wxApp';
451
+ }
452
+
453
+ /**
454
+ * True when ContactEnded represents an agent-terminated wxApp outdial that should
455
+ * enter wrapup with an outdial-failed popup (legacy AgentOutboundFailed parity).
456
+ */
457
+ private static isAgentTerminatedOutdialWrapup(
458
+ payload: WebSocketPayload,
459
+ wrapUpRequired: boolean,
460
+ task?: ITask,
461
+ agentId?: string,
462
+ enableWxBetterTogether?: boolean
463
+ ): boolean {
464
+ const isOutdial = payload.interaction?.outboundType === 'OUTDIAL';
465
+ const agentTerminated = payload.terminatingParty === 'Agent';
466
+
467
+ if (!isOutdial || !agentTerminated) {
468
+ return false;
469
+ }
470
+
471
+ if (!TaskManager.isWxAppManagedOutdialTask(agentId, payload, task, enableWxBetterTogether)) {
472
+ return false;
473
+ }
474
+
475
+ const wxAppAnswerPending =
476
+ (task as {uiControlConfig?: {wxAppAnswerPending?: boolean}})?.uiControlConfig
477
+ ?.wxAppAnswerPending ?? false;
478
+
479
+ // Pre-accept decline: decline() clears wxAppAnswerPending; backend may still send state wrapUp.
480
+ if (!wxAppAnswerPending && !wrapUpRequired) {
481
+ return false;
482
+ }
483
+
484
+ const interactionWrapUp = payload.interaction?.state === 'wrapUp';
485
+
486
+ return wrapUpRequired || interactionWrapUp || wxAppAnswerPending;
487
+ }
488
+
374
489
  /**
375
490
  * Register WebSocket message listeners for task events
376
491
  *
@@ -482,6 +597,10 @@ export default class TaskManager extends EventEmitter {
482
597
  }
483
598
  }
484
599
 
600
+ if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
601
+ task = this.findUniqueTaskByRelatedInteraction(message.data);
602
+ }
603
+
485
604
  const wasConsultedTask = Boolean(task?.data?.isConsulted);
486
605
  const computeWrapUpRequired = () => {
487
606
  if (message.data.wrapUpRequired !== undefined) {
@@ -507,7 +626,9 @@ export default class TaskManager extends EventEmitter {
507
626
  const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
508
627
  eventType,
509
628
  adjustedPayload,
510
- this.agentId
629
+ this.agentId,
630
+ task,
631
+ this.configFlags?.enableWxBetterTogether
511
632
  );
512
633
 
513
634
  LoggerProxy.info(`Handling task event ${eventType}`, {
@@ -524,6 +645,62 @@ export default class TaskManager extends EventEmitter {
524
645
  };
525
646
  }
526
647
 
648
+ /**
649
+ * Resolve lifecycle events that are emitted for the main interaction while the
650
+ * local task can still be indexed by a child consult interaction. Exact task
651
+ * keys are handled before this fallback. Multiple aliases of the same task are
652
+ * treated as one candidate; genuinely ambiguous matches are intentionally ignored.
653
+ */
654
+ private findUniqueTaskByRelatedInteraction(payload: WebSocketPayload): ITask | undefined {
655
+ const payloadInteractionIds = new Set(
656
+ [
657
+ payload.interactionId,
658
+ payload.interaction?.interactionId,
659
+ payload.interaction?.mainInteractionId,
660
+ payload.interaction?.parentInteractionId,
661
+ payload.interaction?.callProcessingDetails?.parentInteractionId,
662
+ ].filter((interactionId): interactionId is string => Boolean(interactionId))
663
+ );
664
+ if (payloadInteractionIds.size === 0) return undefined;
665
+
666
+ const candidates = [
667
+ ...new Set(
668
+ Object.values(this.taskCollection).filter((candidate) => {
669
+ const taskInteraction = candidate?.data?.interaction;
670
+ const candidateInteractionIds = [
671
+ candidate?.data?.interactionId,
672
+ taskInteraction?.interactionId,
673
+ taskInteraction?.mainInteractionId,
674
+ taskInteraction?.parentInteractionId,
675
+ taskInteraction?.callProcessingDetails?.parentInteractionId,
676
+ ];
677
+
678
+ return candidateInteractionIds.some(
679
+ (interactionId) => Boolean(interactionId) && payloadInteractionIds.has(interactionId)
680
+ );
681
+ })
682
+ ),
683
+ ];
684
+
685
+ if (candidates.length === 1) {
686
+ return candidates[0];
687
+ }
688
+
689
+ if (candidates.length > 1) {
690
+ LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
691
+ module: TASK_MANAGER_FILE,
692
+ method: 'findUniqueTaskByRelatedInteraction',
693
+ interactionId:
694
+ payload.interaction?.mainInteractionId ||
695
+ payload.interaction?.parentInteractionId ||
696
+ payload.interaction?.callProcessingDetails?.parentInteractionId ||
697
+ payload.interactionId,
698
+ });
699
+ }
700
+
701
+ return undefined;
702
+ }
703
+
527
704
  /**
528
705
  * Handle task lifecycle events and determine required actions
529
706
  *
@@ -647,7 +824,8 @@ export default class TaskManager extends EventEmitter {
647
824
  },
648
825
  this.configFlags,
649
826
  this.wrapupData,
650
- this.agentId
827
+ this.agentId,
828
+ this.answerCallOnWebexService
651
829
  );
652
830
  this.setupTaskListeners(task);
653
831
  this.taskCollection[payload.interactionId] = task;
@@ -685,7 +863,8 @@ export default class TaskManager extends EventEmitter {
685
863
  taskData,
686
864
  this.configFlags,
687
865
  this.wrapupData,
688
- this.agentId
866
+ this.agentId,
867
+ this.answerCallOnWebexService
689
868
  );
690
869
 
691
870
  this.setupTaskListeners(task);
@@ -725,7 +904,8 @@ export default class TaskManager extends EventEmitter {
725
904
  taskData,
726
905
  this.configFlags,
727
906
  this.wrapupData,
728
- this.agentId
907
+ this.agentId,
908
+ this.answerCallOnWebexService
729
909
  );
730
910
  this.setupTaskListeners(task);
731
911
  this.taskCollection[payload.interactionId] = task;
@@ -753,6 +933,14 @@ export default class TaskManager extends EventEmitter {
753
933
  : taskData;
754
934
 
755
935
  task.updateTaskData(updateTaskData);
936
+
937
+ // A main-interaction lifecycle event can re-key a child consult task. Remove
938
+ // any aliases first so consumers never observe duplicate task entries.
939
+ Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
940
+ if (candidate === task && taskId !== taskData.interactionId) {
941
+ delete this.taskCollection[taskId];
942
+ }
943
+ });
756
944
  this.taskCollection[taskData.interactionId] = task;
757
945
 
758
946
  return task;
@@ -798,8 +986,10 @@ export default class TaskManager extends EventEmitter {
798
986
  task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
799
987
  this.handleTaskCleanup(t);
800
988
  if (options?.removeFromCollection) {
801
- const interactionId = t?.data?.interactionId;
802
- if (interactionId && this.taskCollection[interactionId]) {
989
+ const taskIsStillCollected = Object.values(this.taskCollection).some(
990
+ (candidate) => candidate === t
991
+ );
992
+ if (taskIsStillCollected) {
803
993
  this.removeTaskFromCollection(t);
804
994
  }
805
995
  }
@@ -811,7 +1001,11 @@ export default class TaskManager extends EventEmitter {
811
1001
  task.cancelAutoWrapupTimer();
812
1002
  }
813
1003
  if (task?.data?.interactionId) {
814
- delete this.taskCollection[task.data.interactionId];
1004
+ Object.entries(this.taskCollection).forEach(([taskId, candidate]) => {
1005
+ if (candidate === task) {
1006
+ delete this.taskCollection[taskId];
1007
+ }
1008
+ });
815
1009
  LoggerProxy.info(`Task removed from collection`, {
816
1010
  module: TASK_MANAGER_FILE,
817
1011
  method: METHODS.REMOVE_TASK_FROM_COLLECTION,
@@ -864,7 +1058,8 @@ export default class TaskManager extends EventEmitter {
864
1058
  taskData,
865
1059
  this.configFlags,
866
1060
  this.wrapupData,
867
- this.agentId
1061
+ this.agentId,
1062
+ this.answerCallOnWebexService
868
1063
  );
869
1064
  this.taskCollection[payload.interactionId] = task;
870
1065
 
@@ -956,6 +1151,50 @@ export default class TaskManager extends EventEmitter {
956
1151
  return {...this.taskCollection};
957
1152
  }
958
1153
 
1154
+ /**
1155
+ * Propagate runtime enableWxBetterTogether changes to config flags and active tasks.
1156
+ */
1157
+ public applyEnableWxBetterTogether(enabled: boolean): void {
1158
+ if (this.configFlags) {
1159
+ this.configFlags = {...this.configFlags, enableWxBetterTogether: enabled};
1160
+ }
1161
+
1162
+ Object.values(this.taskCollection).forEach((task) => {
1163
+ task.setEnableWxBetterTogether(enabled);
1164
+ if (enabled) {
1165
+ TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
1166
+ }
1167
+ });
1168
+ }
1169
+
1170
+ /**
1171
+ * Propagate wxApp mute state from Mercury sync to active voice tasks.
1172
+ */
1173
+ public applyWxAppMuteStateFromSync(callId: string, muted: boolean): void {
1174
+ Object.values(this.taskCollection).forEach((task) => {
1175
+ task.applyWxAppMuteStateFromSync(callId, muted);
1176
+ });
1177
+ }
1178
+
1179
+ /**
1180
+ * Re-seeds wxApp mute state from GET telephony/calls/{callId} for one task.
1181
+ */
1182
+ public static syncWxAppMuteFromCallDetailsForTask(task: ITask): void {
1183
+ const sync = task.syncWxAppMuteFromCallDetails?.bind(task);
1184
+ if (typeof sync === 'function') {
1185
+ sync().catch(() => undefined);
1186
+ }
1187
+ }
1188
+
1189
+ /**
1190
+ * Re-seeds wxApp mute for all engaged voice tasks (reload / runtime enable backfill).
1191
+ */
1192
+ public syncWxAppMuteFromCallDetailsForAllTasks(): void {
1193
+ Object.values(this.taskCollection).forEach((task) => {
1194
+ TaskManager.syncWxAppMuteFromCallDetailsForTask(task);
1195
+ });
1196
+ }
1197
+
959
1198
  public static getTaskManager(
960
1199
  apiAIAssistant: ApiAIAssistant,
961
1200
  contact: ReturnType<typeof routingContact>,
@@ -0,0 +1,136 @@
1
+ import {TaskState} from './state-machine/constants';
2
+
3
+ export type WebexCallingDeviceDetails = {
4
+ deviceType: string;
5
+ deviceId: string;
6
+ deviceCallId: string;
7
+ };
8
+
9
+ type WebexCallingParticipant = {
10
+ id?: unknown;
11
+ deviceType?: unknown;
12
+ deviceCallId?: unknown;
13
+ deviceId?: unknown;
14
+ };
15
+
16
+ const normalizeId = (id: unknown): string | undefined => {
17
+ if (typeof id !== 'string') {
18
+ return undefined;
19
+ }
20
+
21
+ const normalizedId = id.trim();
22
+
23
+ return normalizedId === '' ? undefined : normalizedId;
24
+ };
25
+
26
+ const isParticipantsById = (participants: unknown): participants is Record<string, unknown> =>
27
+ Boolean(participants) &&
28
+ typeof participants === 'object' &&
29
+ !Array.isArray(participants) &&
30
+ !(participants instanceof Map);
31
+
32
+ const hasWebexCallingDeviceDetails = (
33
+ participant: unknown
34
+ ): participant is WebexCallingParticipant &
35
+ Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'> => {
36
+ const participantDetails = participant as WebexCallingParticipant | undefined;
37
+
38
+ return (
39
+ typeof participantDetails?.deviceCallId === 'string' &&
40
+ participantDetails.deviceCallId.trim() !== '' &&
41
+ typeof participantDetails?.deviceId === 'string' &&
42
+ participantDetails.deviceId.trim() !== ''
43
+ );
44
+ };
45
+
46
+ const isWebexCallingParticipantForAgent = (
47
+ participant: unknown,
48
+ agentId: string
49
+ ): participant is WebexCallingParticipant &
50
+ Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'> => {
51
+ const participantDetails = participant as WebexCallingParticipant | undefined;
52
+
53
+ return (
54
+ normalizeId(participantDetails?.id) === agentId && hasWebexCallingDeviceDetails(participant)
55
+ );
56
+ };
57
+
58
+ const getWebexCallingDeviceDetails = (
59
+ participant: WebexCallingParticipant &
60
+ Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'>
61
+ ): WebexCallingDeviceDetails => ({
62
+ deviceType: typeof participant.deviceType === 'string' ? participant.deviceType : '',
63
+ deviceId: participant.deviceId,
64
+ deviceCallId: participant.deviceCallId,
65
+ });
66
+
67
+ export function getWebexCallingDeviceDetailsForAgent(
68
+ agentId: string | undefined,
69
+ participants: unknown
70
+ ): WebexCallingDeviceDetails | undefined {
71
+ const normalizedAgentId = normalizeId(agentId);
72
+ if (!normalizedAgentId || !isParticipantsById(participants)) {
73
+ return undefined;
74
+ }
75
+
76
+ const agentParticipant = participants[normalizedAgentId];
77
+ const matchingAgentParticipant = hasWebexCallingDeviceDetails(agentParticipant)
78
+ ? agentParticipant
79
+ : Object.values(participants).find((participant) =>
80
+ isWebexCallingParticipantForAgent(participant, normalizedAgentId)
81
+ );
82
+
83
+ if (!matchingAgentParticipant || !hasWebexCallingDeviceDetails(matchingAgentParticipant)) {
84
+ return undefined;
85
+ }
86
+
87
+ return getWebexCallingDeviceDetails(matchingAgentParticipant);
88
+ }
89
+
90
+ export function isWebexCallingCallForAgent(
91
+ agentId: string | undefined,
92
+ participants: unknown
93
+ ): boolean {
94
+ return Boolean(getWebexCallingDeviceDetailsForAgent(agentId, participants));
95
+ }
96
+
97
+ /** Matches wxAppVoiceMethods.getWebexCallingCallId engaged semantics for uiControls. */
98
+ export function isWxAppEngagedForControls(
99
+ enableWxBetterTogether: boolean,
100
+ agentId: string | undefined,
101
+ participants: unknown,
102
+ state: TaskState
103
+ ): boolean {
104
+ if (!enableWxBetterTogether) {
105
+ return false;
106
+ }
107
+
108
+ if (
109
+ !state ||
110
+ state === TaskState.OFFERED ||
111
+ state === TaskState.IDLE ||
112
+ state === TaskState.WRAPPING_UP ||
113
+ state === TaskState.TERMINATED ||
114
+ state === TaskState.COMPLETED
115
+ ) {
116
+ return false;
117
+ }
118
+
119
+ const details = getWebexCallingDeviceDetailsForAgent(agentId, participants);
120
+
121
+ return details?.deviceType === 'wxApp';
122
+ }
123
+
124
+ export function decodedLineOwnerId(lineOwnerId?: string): string | undefined {
125
+ if (!lineOwnerId) {
126
+ return undefined;
127
+ }
128
+
129
+ try {
130
+ const decoded = atob(lineOwnerId);
131
+
132
+ return decoded.split('/').pop();
133
+ } catch {
134
+ return undefined;
135
+ }
136
+ }
@@ -35,6 +35,7 @@
35
35
  - `state?: SnapshotFrom<TaskStateMachine>`
36
36
  - `autoWrapup?: AutoWrapup`
37
37
  - `uiControls: TaskUIControls` (getter)
38
+ - includes ordered, action-specific `consultTransferDestinations` computed from Task profile configuration and live interaction data
38
39
 
39
40
  **Methods**
40
41