@webex/contact-center 3.12.0-llmrefactor.3 → 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.
@@ -309,7 +309,7 @@
309
309
  "canonical_spec": "src/services/task/ai-docs/task-spec.md",
310
310
  "contracts": {
311
311
  "provides": [
312
- "Task/Voice/WebRTC/Digital lifecycle APIs; TaskManager; task events/types; ordered action-specific consult/transfer destination controls; call-control and dialer operations including acceptPreviewContact, skipPreviewContact, and removePreviewContact"
312
+ "Task/Voice/WebRTC/Digital lifecycle APIs; TaskManager lifecycle correlation, narrowly scoped backend-authoritative ContactOwnerChanged recovery, and task-event publication, including routing owner-changing ContactUpdated through the existing task:hydrate event; task events/types; ordered action-specific consult/transfer destination controls; call-control and dialer operations including acceptPreviewContact, skipPreviewContact, and removePreviewContact"
313
313
  ],
314
314
  "requires": [
315
315
  "Contact/dialer AqmReqs; WebSocket managers; @webex/calling; MetricsManager; XState task-state module"
@@ -22,7 +22,7 @@
22
22
  | Contract ID | Owner module | Event / topic | Direction | Payload schema link | Delivery guarantees | Compatibility / deprecation | Defined at |
23
23
  |---|---|---|---|---|---|---|---|
24
24
  | `agent.events` | Agent | `agent:*` | publish to application | agent spec/types | realtime; backend ordering/correlation | additive constants | `src/services/agent/types.ts` |
25
- | `task.events` | Task | `task:*` | publish to application/task | task spec/types | event-driven; state guarded | additive constants | `src/services/task/types.ts` |
25
+ | `task.events` | Task | `task:*`, including existing `task:hydrate` | consume/map backend task lifecycle events, then publish to application/task | task spec `TASK-R-010` / types | `ContactOwnerChanged` and owner-changing `ContactUpdated` route through `CONTACT_OWNER_CHANGED`; a complete promoted-current-Agent owner change may narrowly recover a missing task; consumers receive one `task:hydrate` and no synthetic `task:incoming` | compatible semantic extension of an existing event; no new event constant; backend `interaction.owner` remains authoritative | `src/services/task/TaskManager.ts`, `src/services/task/Task.ts`, `src/services/task/types.ts` |
26
26
  | `task.wxapp-mute.events` | Task | `task:wxapp-mute-state-updated` | publish to application/task | task spec/types | event-driven | additive | `src/services/task/types.ts`, `src/services/task/voice/Voice.ts` |
27
27
  | `cc.events` | Config/Core | backend CC_EVENTS | consume/map | config/core specs | remote WebSocket delivery; AQM correlation where configured | backend contract | `src/services/config/types.ts` |
28
28
  | `rtd.events` | Task | realtime transcription/suggestion | consume then publish per task | task spec | websocket best-effort according to remote service | additive payloads | `src/services/task/TaskManager.ts` |
@@ -17,6 +17,7 @@ var _TaskFactory = _interopRequireDefault(require("./TaskFactory"));
17
17
  var _WebexCallingUtils = require("./WebexCallingUtils");
18
18
  var _WebRTC = _interopRequireDefault(require("./voice/WebRTC"));
19
19
  var _stateMachine = require("./state-machine");
20
+ var _constants3 = require("./state-machine/constants");
20
21
  var _taskDataNormalizer = require("./taskDataNormalizer");
21
22
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
22
23
  const CC_EVENT_SET = new Set(Object.values(_types2.CC_EVENTS));
@@ -186,6 +187,12 @@ class TaskManager extends _events.default {
186
187
  agentId
187
188
  };
188
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
+ }
189
196
  return {
190
197
  type: _stateMachine.TaskEvent.CONTACT_UPDATED,
191
198
  taskData: payload
@@ -561,6 +568,9 @@ class TaskManager extends _events.default {
561
568
  this.taskCollection[interactionId] = task;
562
569
  }
563
570
  }
571
+ if (!task && eventType === _types2.CC_EVENTS.CONTACT_OWNER_CHANGED) {
572
+ task = this.findTaskForContactOwnerChange(message.data);
573
+ }
564
574
  if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
565
575
  task = this.findUniqueTaskByRelatedInteraction(message.data);
566
576
  }
@@ -574,10 +584,16 @@ class TaskManager extends _events.default {
574
584
  }
575
585
  return !wasConsultedTask;
576
586
  };
577
- 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 ? {
578
588
  ...message.data,
579
589
  wrapUpRequired: computeWrapUpRequired()
580
590
  } : message.data;
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
+ }
581
597
  const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(eventType, adjustedPayload, this.agentId, task, this.configFlags?.enableWxBetterTogether);
582
598
  _loggerProxy.default.info(`Handling task event ${eventType}`, {
583
599
  module: _constants.TASK_MANAGER_FILE,
@@ -592,20 +608,108 @@ class TaskManager extends _events.default {
592
608
  };
593
609
  }
594
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
+
595
705
  /**
596
706
  * Resolve lifecycle events that are emitted for the main interaction while the
597
707
  * local task can still be indexed by a child consult interaction. Exact task
598
708
  * keys are handled before this fallback. Multiple aliases of the same task are
599
709
  * treated as one candidate; genuinely ambiguous matches are intentionally ignored.
600
710
  */
601
- findUniqueTaskByRelatedInteraction(payload) {
602
- const payloadInteractionIds = new Set([payload.interactionId, payload.interaction?.interactionId, payload.interaction?.mainInteractionId, payload.interaction?.parentInteractionId, payload.interaction?.callProcessingDetails?.parentInteractionId].filter(interactionId => Boolean(interactionId)));
603
- if (payloadInteractionIds.size === 0) return undefined;
604
- const candidates = [...new Set(Object.values(this.taskCollection).filter(candidate => {
605
- const taskInteraction = candidate?.data?.interaction;
606
- const candidateInteractionIds = [candidate?.data?.interactionId, taskInteraction?.interactionId, taskInteraction?.mainInteractionId, taskInteraction?.parentInteractionId, taskInteraction?.callProcessingDetails?.parentInteractionId];
607
- return candidateInteractionIds.some(interactionId => Boolean(interactionId) && payloadInteractionIds.has(interactionId));
608
- }))];
711
+ findUniqueTaskByRelatedInteraction(payload, candidateFilter = () => true) {
712
+ const candidates = this.findRelatedTasks(payload).filter(candidateFilter);
609
713
  if (candidates.length === 1) {
610
714
  return candidates[0];
611
715
  }
@@ -618,6 +722,19 @@ class TaskManager extends _events.default {
618
722
  }
619
723
  return undefined;
620
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
+ }
621
738
 
622
739
  /**
623
740
  * Handle task lifecycle events and determine required actions
@@ -642,6 +759,8 @@ class TaskManager extends _events.default {
642
759
  return this.handleAgentContact(context);
643
760
  case _types2.CC_EVENTS.CONTACT_MERGED:
644
761
  return this.handleContactMergedEvent(context);
762
+ case _types2.CC_EVENTS.CONTACT_OWNER_CHANGED:
763
+ return this.handleContactOwnerChanged(context);
645
764
  case _types2.CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
646
765
  return this.handleCampaignPreviewReservation(context);
647
766
  case _types2.CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
@@ -652,6 +771,63 @@ class TaskManager extends _events.default {
652
771
  };
653
772
  }
654
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
+ }
655
831
  handleCampaignContactUpdated(context) {
656
832
  const {
657
833
  payload