@webex/contact-center 3.12.0-next.72 → 3.12.0-next.74

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 (188) hide show
  1. package/AGENTS.md +438 -0
  2. package/ai-docs/README.md +131 -0
  3. package/ai-docs/RULES.md +455 -0
  4. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  5. package/ai-docs/patterns/testing-patterns.md +480 -0
  6. package/ai-docs/patterns/typescript-patterns.md +365 -0
  7. package/ai-docs/templates/README.md +102 -0
  8. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  9. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  10. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  11. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  12. package/ai-docs/templates/new-method/00-master.md +80 -0
  13. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  14. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  15. package/ai-docs/templates/new-method/03-tests.md +201 -0
  16. package/ai-docs/templates/new-method/04-validation.md +141 -0
  17. package/ai-docs/templates/new-service/00-master.md +109 -0
  18. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  19. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  20. package/ai-docs/templates/new-service/03-integration.md +178 -0
  21. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  22. package/ai-docs/templates/new-service/05-validation.md +145 -0
  23. package/dist/cc.js +53 -38
  24. package/dist/cc.js.map +1 -1
  25. package/dist/config.js +6 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/constants.js +14 -1
  28. package/dist/constants.js.map +1 -1
  29. package/dist/index.js +13 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/metrics/behavioral-events.js +26 -0
  32. package/dist/metrics/behavioral-events.js.map +1 -1
  33. package/dist/metrics/constants.js +9 -3
  34. package/dist/metrics/constants.js.map +1 -1
  35. package/dist/services/ApiAiAssistant.js +74 -3
  36. package/dist/services/ApiAiAssistant.js.map +1 -1
  37. package/dist/services/config/Util.js +2 -2
  38. package/dist/services/config/Util.js.map +1 -1
  39. package/dist/services/config/types.js +25 -9
  40. package/dist/services/config/types.js.map +1 -1
  41. package/dist/services/core/Utils.js +74 -27
  42. package/dist/services/core/Utils.js.map +1 -1
  43. package/dist/services/core/websocket/WebSocketManager.js +2 -1
  44. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  45. package/dist/services/core/websocket/types.js.map +1 -1
  46. package/dist/services/index.js +1 -1
  47. package/dist/services/index.js.map +1 -1
  48. package/dist/services/task/Task.js +688 -0
  49. package/dist/services/task/Task.js.map +1 -0
  50. package/dist/services/task/TaskFactory.js +45 -0
  51. package/dist/services/task/TaskFactory.js.map +1 -0
  52. package/dist/services/task/TaskManager.js +725 -612
  53. package/dist/services/task/TaskManager.js.map +1 -1
  54. package/dist/services/task/TaskUtils.js +162 -26
  55. package/dist/services/task/TaskUtils.js.map +1 -1
  56. package/dist/services/task/constants.js +6 -1
  57. package/dist/services/task/constants.js.map +1 -1
  58. package/dist/services/task/digital/Digital.js +77 -0
  59. package/dist/services/task/digital/Digital.js.map +1 -0
  60. package/dist/services/task/state-machine/TaskStateMachine.js +837 -0
  61. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  62. package/dist/services/task/state-machine/actions.js +543 -0
  63. package/dist/services/task/state-machine/actions.js.map +1 -0
  64. package/dist/services/task/state-machine/constants.js +161 -0
  65. package/dist/services/task/state-machine/constants.js.map +1 -0
  66. package/dist/services/task/state-machine/guards.js +340 -0
  67. package/dist/services/task/state-machine/guards.js.map +1 -0
  68. package/dist/services/task/state-machine/index.js +53 -0
  69. package/dist/services/task/state-machine/index.js.map +1 -0
  70. package/dist/services/task/state-machine/types.js +54 -0
  71. package/dist/services/task/state-machine/types.js.map +1 -0
  72. package/dist/services/task/state-machine/uiControlsComputer.js +553 -0
  73. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  74. package/dist/services/task/taskDataNormalizer.js +99 -0
  75. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  76. package/dist/services/task/types.js +168 -16
  77. package/dist/services/task/types.js.map +1 -1
  78. package/dist/services/task/voice/Voice.js +1042 -0
  79. package/dist/services/task/voice/Voice.js.map +1 -0
  80. package/dist/services/task/voice/WebRTC.js +149 -0
  81. package/dist/services/task/voice/WebRTC.js.map +1 -0
  82. package/dist/types/cc.d.ts +19 -18
  83. package/dist/types/config.d.ts +6 -0
  84. package/dist/types/constants.d.ts +14 -1
  85. package/dist/types/index.d.ts +11 -5
  86. package/dist/types/metrics/constants.d.ts +7 -1
  87. package/dist/types/services/ApiAiAssistant.d.ts +11 -3
  88. package/dist/types/services/config/types.d.ts +113 -9
  89. package/dist/types/services/core/Utils.d.ts +23 -10
  90. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  91. package/dist/types/services/core/websocket/types.d.ts +1 -1
  92. package/dist/types/services/index.d.ts +1 -1
  93. package/dist/types/services/task/Task.d.ts +157 -0
  94. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  95. package/dist/types/services/task/TaskUtils.d.ts +46 -2
  96. package/dist/types/services/task/constants.d.ts +5 -0
  97. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  98. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1144 -0
  99. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  100. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  101. package/dist/types/services/task/state-machine/guards.d.ts +90 -0
  102. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  103. package/dist/types/services/task/state-machine/types.d.ts +267 -0
  104. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  105. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  106. package/dist/types/services/task/types.d.ts +551 -78
  107. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  108. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  109. package/dist/types/types.d.ts +92 -0
  110. package/dist/types/webex.d.ts +1 -0
  111. package/dist/types.js +85 -0
  112. package/dist/types.js.map +1 -1
  113. package/dist/webex.js +14 -2
  114. package/dist/webex.js.map +1 -1
  115. package/package.json +15 -12
  116. package/src/cc.ts +63 -43
  117. package/src/config.ts +6 -0
  118. package/src/constants.ts +14 -1
  119. package/src/index.ts +14 -5
  120. package/src/metrics/ai-docs/AGENTS.md +348 -0
  121. package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
  122. package/src/metrics/behavioral-events.ts +28 -0
  123. package/src/metrics/constants.ts +9 -4
  124. package/src/services/ApiAiAssistant.ts +104 -3
  125. package/src/services/agent/ai-docs/AGENTS.md +238 -0
  126. package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
  127. package/src/services/ai-docs/AGENTS.md +384 -0
  128. package/src/services/config/Util.ts +2 -2
  129. package/src/services/config/ai-docs/AGENTS.md +253 -0
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
  131. package/src/services/config/types.ts +116 -10
  132. package/src/services/core/Utils.ts +85 -34
  133. package/src/services/core/ai-docs/AGENTS.md +379 -0
  134. package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
  135. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  136. package/src/services/core/websocket/types.ts +1 -1
  137. package/src/services/index.ts +1 -1
  138. package/src/services/task/Task.ts +837 -0
  139. package/src/services/task/TaskFactory.ts +55 -0
  140. package/src/services/task/TaskManager.ts +738 -697
  141. package/src/services/task/TaskUtils.ts +205 -25
  142. package/src/services/task/ai-docs/AGENTS.md +455 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +585 -0
  144. package/src/services/task/constants.ts +5 -0
  145. package/src/services/task/digital/Digital.ts +95 -0
  146. package/src/services/task/state-machine/TaskStateMachine.ts +1077 -0
  147. package/src/services/task/state-machine/actions.ts +685 -0
  148. package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
  149. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
  150. package/src/services/task/state-machine/constants.ts +172 -0
  151. package/src/services/task/state-machine/guards.ts +406 -0
  152. package/src/services/task/state-machine/index.ts +28 -0
  153. package/src/services/task/state-machine/types.ts +241 -0
  154. package/src/services/task/state-machine/uiControlsComputer.ts +867 -0
  155. package/src/services/task/taskDataNormalizer.ts +137 -0
  156. package/src/services/task/types.ts +654 -85
  157. package/src/services/task/voice/Voice.ts +1267 -0
  158. package/src/services/task/voice/WebRTC.ts +187 -0
  159. package/src/types.ts +112 -1
  160. package/src/utils/AGENTS.md +276 -0
  161. package/src/webex.js +2 -0
  162. package/test/unit/spec/cc.ts +133 -3
  163. package/test/unit/spec/logger-proxy.ts +70 -0
  164. package/test/unit/spec/services/ApiAiAssistant.ts +122 -17
  165. package/test/unit/spec/services/WebCallingService.ts +7 -1
  166. package/test/unit/spec/services/config/index.ts +27 -27
  167. package/test/unit/spec/services/core/Utils.ts +335 -1
  168. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  169. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  170. package/test/unit/spec/services/task/Task.ts +477 -0
  171. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  172. package/test/unit/spec/services/task/TaskManager.ts +821 -1936
  173. package/test/unit/spec/services/task/TaskUtils.ts +206 -0
  174. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  175. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1825 -0
  176. package/test/unit/spec/services/task/state-machine/guards.ts +479 -0
  177. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  178. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2020 -0
  179. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  180. package/test/unit/spec/services/task/voice/Voice.ts +631 -0
  181. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  182. package/umd/contact-center.min.js +2 -2
  183. package/umd/contact-center.min.js.map +1 -1
  184. package/dist/services/task/index.js +0 -1530
  185. package/dist/services/task/index.js.map +0 -1
  186. package/dist/types/services/task/index.d.ts +0 -650
  187. package/src/services/task/index.ts +0 -1806
  188. package/test/unit/spec/services/task/index.ts +0 -2205
@@ -3,25 +3,38 @@ import {ICall, LINE_EVENTS} from '@webex/calling';
3
3
  import {WebSocketManager} from '../core/websocket/WebSocketManager';
4
4
  import routingContact from './contact';
5
5
  import WebCallingService from '../WebCallingService';
6
- import {ITask, MEDIA_CHANNEL, TASK_EVENTS, TaskData, TaskId} from './types';
6
+ import {
7
+ MEDIA_CHANNEL,
8
+ TASK_EVENTS,
9
+ TaskData,
10
+ TaskId,
11
+ ITask,
12
+ WebSocketPayload,
13
+ WebSocketMessage,
14
+ TaskEventActions,
15
+ EventContext,
16
+ } from './types';
7
17
  import {TASK_MANAGER_FILE} from '../../constants';
8
18
  import {METHODS, TRANSCRIPT_EVENT_MAP} from './constants';
9
- import {CC_EVENTS, CC_TASK_EVENTS, WrapupData} from '../config/types';
10
- import {AIAssistantEventName, AIAssistantEventType, LoginOption} from '../../types';
19
+ import {CC_EVENTS, WrapupData} from '../config/types';
20
+ import {ConfigFlags, LoginOption, AIAssistantEventType, AIAssistantEventName} from '../../types';
11
21
  import LoggerProxy from '../../logger-proxy';
12
- import Task from '.';
13
- import MetricsManager from '../../metrics/MetricsManager';
14
- import {METRIC_EVENT_NAMES} from '../../metrics/constants';
15
22
  import {
16
- checkParticipantNotInInteraction,
17
23
  getIsConferenceInProgress,
24
+ isCampaignPreviewTask,
18
25
  isCampaignPreviewReservation,
19
- isParticipantInMainInteraction,
20
- isPrimary,
21
26
  isSecondaryEpDnAgent,
22
27
  shouldAutoAnswerTask,
23
28
  } from './TaskUtils';
24
- import ApiAIAssistant from '../ApiAiAssistant';
29
+ import TaskFactory from './TaskFactory';
30
+ import WebRTC from './voice/WebRTC';
31
+ import {TaskEvent, type TaskEventPayload} from './state-machine';
32
+ import {normalizeTaskData} from './taskDataNormalizer';
33
+ import {ApiAIAssistant} from '../ApiAiAssistant';
34
+
35
+ const CC_EVENT_SET = new Set<CC_EVENTS>(Object.values(CC_EVENTS) as CC_EVENTS[]);
36
+
37
+ const isCcEvent = (value: string): value is CC_EVENTS => CC_EVENT_SET.has(value as CC_EVENTS);
25
38
 
26
39
  /** @internal */
27
40
  export default class TaskManager extends EventEmitter {
@@ -35,8 +48,10 @@ export default class TaskManager extends EventEmitter {
35
48
  private taskCollection: Record<TaskId, ITask>;
36
49
  private webCallingService: WebCallingService;
37
50
  private webSocketManager: WebSocketManager;
38
- private metricsManager: MetricsManager;
39
- private static taskManager;
51
+ private rtdWebSocketManager: WebSocketManager;
52
+ // eslint-disable-next-line no-use-before-define
53
+ private static taskManager: TaskManager;
54
+ private configFlags?: ConfigFlags;
40
55
  private wrapupData: WrapupData;
41
56
  private agentId: string;
42
57
  private webRtcEnabled: boolean;
@@ -50,47 +65,28 @@ export default class TaskManager extends EventEmitter {
50
65
  apiAIAssistant: ApiAIAssistant,
51
66
  contact: ReturnType<typeof routingContact>,
52
67
  webCallingService: WebCallingService,
53
- webSocketManager: WebSocketManager
68
+ webSocketManager: WebSocketManager,
69
+ rtdWebSocketManager: WebSocketManager
54
70
  ) {
55
71
  super();
56
72
  this.apiAIAssistant = apiAIAssistant;
57
73
  this.contact = contact;
58
- this.taskCollection = {};
59
74
  this.webCallingService = webCallingService;
60
75
  this.webSocketManager = webSocketManager;
61
- this.metricsManager = MetricsManager.getInstance();
76
+ this.rtdWebSocketManager = rtdWebSocketManager;
77
+ this.taskCollection = {};
78
+ this.webRtcEnabled = false;
79
+
62
80
  this.registerTaskListeners();
63
81
  this.registerIncomingCallEvent();
64
82
  }
65
83
 
66
- public setWrapupData(wrapupData: WrapupData) {
67
- this.wrapupData = wrapupData;
68
- }
69
-
70
- public setAgentId(agentId: string) {
71
- this.agentId = agentId;
72
- }
73
-
74
- /**
75
- * Gets the current agent ID
76
- * @returns {string} The agent ID set for this task manager instance
77
- * @public
78
- */
79
- public getAgentId(): string {
80
- return this.agentId;
81
- }
82
-
83
- public setWebRtcEnabled(webRtcEnabled: boolean) {
84
- this.webRtcEnabled = webRtcEnabled;
85
- }
86
-
87
84
  public handleRealtimeWebsocketEvent(event: string) {
88
85
  try {
89
86
  const payload = JSON.parse(event);
90
87
 
91
- const eventType = payload?.type || payload?.data?.notifType;
92
88
  const interactionId = payload?.data?.data?.conversationId;
93
- if (!eventType || !interactionId) return;
89
+ if (!interactionId) return;
94
90
 
95
91
  const task = this.taskCollection[interactionId];
96
92
  if (!task) {
@@ -103,7 +99,12 @@ export default class TaskManager extends EventEmitter {
103
99
  return;
104
100
  }
105
101
 
106
- task.emit(eventType, payload.data);
102
+ switch (payload.type) {
103
+ case CC_EVENTS.REAL_TIME_TRANSCRIPTION:
104
+ case CC_EVENTS.SUGGESTED_RESPONSE:
105
+ task.emit(payload.type, payload.data);
106
+ break;
107
+ }
107
108
  } catch (error) {
108
109
  LoggerProxy.error('Failed to parse RTD WebSocket message', {
109
110
  module: TASK_MANAGER_FILE,
@@ -113,6 +114,40 @@ export default class TaskManager extends EventEmitter {
113
114
  }
114
115
  }
115
116
 
117
+ /**
118
+ * Set config flags for task creation
119
+ */
120
+ public setConfigFlags(configFlags: ConfigFlags) {
121
+ this.configFlags = configFlags;
122
+ }
123
+
124
+ /**
125
+ * Set wrapup configuration data
126
+ */
127
+ public setWrapupData(wrapupData: WrapupData) {
128
+ this.wrapupData = wrapupData;
129
+ }
130
+
131
+ /**
132
+ * Set agent ID for task operations
133
+ */
134
+ public setAgentId(agentId: string) {
135
+ this.agentId = agentId;
136
+ }
137
+
138
+ /**
139
+ * Gets the current agent ID
140
+ * @returns {string} The agent ID set for this task manager instance
141
+ * @public
142
+ */
143
+ public getAgentId(): string {
144
+ return this.agentId;
145
+ }
146
+
147
+ public setWebRtcEnabled(webRtcEnabled: boolean) {
148
+ this.webRtcEnabled = webRtcEnabled;
149
+ }
150
+
116
151
  private handleIncomingWebCall = (call: ICall) => {
117
152
  const currentTask = Object.values(this.taskCollection).find(
118
153
  (task) =>
@@ -126,7 +161,15 @@ export default class TaskManager extends EventEmitter {
126
161
  method: METHODS.HANDLE_INCOMING_WEB_CALL,
127
162
  interactionId: currentTask.data.interactionId,
128
163
  });
129
- this.emit(TASK_EVENTS.TASK_INCOMING, currentTask);
164
+
165
+ // Send TASK_INCOMING to state machine - it will emit on the task object
166
+ const eventPayload = TaskManager.mapEventToTaskStateMachineEvent(
167
+ CC_EVENTS.AGENT_CONTACT_RESERVED,
168
+ currentTask.data
169
+ );
170
+ if (eventPayload && currentTask) {
171
+ currentTask.sendStateMachineEvent(eventPayload);
172
+ }
130
173
  }
131
174
  this.call = call;
132
175
  };
@@ -139,635 +182,634 @@ export default class TaskManager extends EventEmitter {
139
182
  this.webCallingService.off(LINE_EVENTS.INCOMING_CALL, this.handleIncomingWebCall);
140
183
  }
141
184
 
185
+ /**
186
+ * Map WebSocket CC_EVENTS to state machine TaskEvent
187
+ * @param ccEvent - The CC_EVENT type from WebSocket
188
+ * @param payload - The event payload
189
+ * @param agentId - Optional agent ID for state detection (needed for HYDRATE)
190
+ * @returns TaskEventPayload for state machine or null if no mapping
191
+ */
192
+ private static mapEventToTaskStateMachineEvent(
193
+ ccEvent: CC_EVENTS,
194
+ payload: WebSocketPayload,
195
+ agentId?: string
196
+ ): TaskEventPayload | null {
197
+ const mediaResourceId =
198
+ payload.mediaResourceId ||
199
+ payload.interaction?.media?.[payload.interactionId]?.mediaResourceId;
200
+
201
+ switch (ccEvent) {
202
+ // CC -> TaskEvent mappings (see TaskStateMachine comment for quick reference)
203
+ case CC_EVENTS.AGENT_CONTACT_RESERVED: // AgentContactReserved -> TASK_INCOMING
204
+ if (isCampaignPreviewTask(payload)) {
205
+ return {
206
+ type: TaskEvent.TASK_INCOMING,
207
+ taskData: payload,
208
+ isCampaignReservationAccept: true,
209
+ };
210
+ }
211
+
212
+ return {type: TaskEvent.TASK_INCOMING, taskData: payload};
213
+
214
+ case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: // -> TASK_INCOMING (campaign branch via guard)
215
+ return {
216
+ type: TaskEvent.TASK_INCOMING,
217
+ taskData: payload,
218
+ isCampaignReservationAccept: true,
219
+ };
220
+
221
+ case CC_EVENTS.AGENT_OFFER_CONTACT: // AgentOfferContact -> TASK_OFFERED
222
+ return {type: TaskEvent.TASK_OFFERED, taskData: payload};
223
+
224
+ case CC_EVENTS.AGENT_CONTACT: // AgentContact -> HYDRATE
225
+ // Include agentId for state detection (e.g., checking isWrapUp in participant data)
226
+ return {type: TaskEvent.HYDRATE, taskData: payload, agentId};
227
+
228
+ case CC_EVENTS.CONTACT_UPDATED:
229
+ return {type: TaskEvent.CONTACT_UPDATED, taskData: payload};
230
+ case CC_EVENTS.CONTACT_OWNER_CHANGED:
231
+ return {type: TaskEvent.CONTACT_OWNER_CHANGED, taskData: payload};
232
+
233
+ case CC_EVENTS.AGENT_OFFER_CONSULT: // AgentOfferConsult -> OFFER_CONSULT
234
+ return {
235
+ type: TaskEvent.OFFER_CONSULT,
236
+ taskData: {...payload, isConsulted: true},
237
+ };
238
+
239
+ case CC_EVENTS.AGENT_CONTACT_ASSIGNED: // AgentContactAssigned -> ASSIGN
240
+ return {type: TaskEvent.ASSIGN, taskData: payload};
241
+
242
+ case CC_EVENTS.AGENT_CONTACT_HELD:
243
+ return {
244
+ type: TaskEvent.HOLD_SUCCESS,
245
+ mediaResourceId: mediaResourceId || '',
246
+ taskData: payload,
247
+ };
248
+
249
+ case CC_EVENTS.AGENT_CONTACT_UNHELD:
250
+ return {
251
+ type: TaskEvent.UNHOLD_SUCCESS,
252
+ mediaResourceId: mediaResourceId || '',
253
+ taskData: payload,
254
+ };
255
+
256
+ case CC_EVENTS.AGENT_CONSULT_CREATED:
257
+ return {
258
+ type: TaskEvent.CONSULT_CREATED,
259
+ taskData: {...payload, isConsulted: false},
260
+ };
261
+
262
+ case CC_EVENTS.AGENT_CONSULTING: // AgentConsulting -> CONSULTING_ACTIVE
263
+ // use context to figure out if it's the initiator or receiver using consultInitiator from context
264
+ return {
265
+ type: TaskEvent.CONSULTING_ACTIVE,
266
+ consultDestinationAgentJoined: true,
267
+ taskData: payload,
268
+ };
269
+
270
+ case CC_EVENTS.AGENT_CONSULT_ENDED: // AgentConsultEnded -> CONSULT_END
271
+ return {type: TaskEvent.CONSULT_END, taskData: payload};
272
+
273
+ case CC_EVENTS.AGENT_CONSULT_FAILED:
274
+ case CC_EVENTS.AGENT_CTQ_FAILED:
275
+ return {type: TaskEvent.CONSULT_FAILED, reason: payload.reason, taskData: payload};
276
+
277
+ case CC_EVENTS.AGENT_CTQ_CANCELLED:
278
+ return {type: TaskEvent.CTQ_CANCEL, taskData: payload};
279
+
280
+ case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
281
+ return {type: TaskEvent.CTQ_CANCEL_FAILED, taskData: payload};
282
+
283
+ case CC_EVENTS.AGENT_BLIND_TRANSFERRED: // AgentBlindTransferred -> TRANSFER_SUCCESS
284
+ case CC_EVENTS.AGENT_CONSULT_TRANSFERRED: // AgentConsultTransferred -> TRANSFER_SUCCESS
285
+ case CC_EVENTS.AGENT_VTEAM_TRANSFERRED: // AgentVTeamTransferred -> TRANSFER_SUCCESS
286
+ return {
287
+ type: TaskEvent.TRANSFER_SUCCESS,
288
+ taskData: payload,
289
+ };
290
+
291
+ case CC_EVENTS.AGENT_WRAPUP:
292
+ return {type: TaskEvent.TASK_WRAPUP, taskData: {...payload, wrapUpRequired: true}};
293
+ case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
294
+ return null; // Add WRAPUP if needed
295
+
296
+ case CC_EVENTS.AGENT_BLIND_TRANSFER_FAILED:
297
+ case CC_EVENTS.AGENT_VTEAM_TRANSFER_FAILED:
298
+ case CC_EVENTS.AGENT_CONSULT_TRANSFER_FAILED:
299
+ case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
300
+ return {type: TaskEvent.TRANSFER_FAILED, taskData: payload};
301
+
302
+ case CC_EVENTS.CONTACT_ENDED:
303
+ return {
304
+ type: TaskEvent.CONTACT_ENDED,
305
+ taskData: {
306
+ ...payload,
307
+ wrapUpRequired: isCampaignPreviewTask(payload)
308
+ ? false
309
+ : payload.agentsPendingWrapUp?.includes(agentId || '') || false,
310
+ },
311
+ };
312
+
313
+ case CC_EVENTS.AGENT_INVITE_FAILED:
314
+ return {type: TaskEvent.INVITE_FAILED, reason: payload.reason};
315
+
316
+ case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
317
+ return {type: TaskEvent.ASSIGN_FAILED, reason: payload.reason};
318
+
319
+ case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
320
+ return {type: TaskEvent.RONA, taskData: payload, reason: payload.reason};
321
+
322
+ case CC_EVENTS.AGENT_OUTBOUND_FAILED:
323
+ return {type: TaskEvent.OUTBOUND_FAILED, taskData: payload, reason: payload.reason};
324
+
325
+ case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
326
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_ACCEPT_FAILED, taskData: payload};
327
+
328
+ case CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED:
329
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_SKIP_FAILED, taskData: payload};
330
+
331
+ case CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED:
332
+ return {type: TaskEvent.CAMPAIGN_PREVIEW_REMOVE_FAILED, taskData: payload};
333
+
334
+ case CC_EVENTS.CONTACT_RECORDING_STARTED:
335
+ return {type: TaskEvent.RECORDING_STARTED, taskData: payload};
336
+
337
+ case CC_EVENTS.CONTACT_RECORDING_PAUSED:
338
+ return {type: TaskEvent.PAUSE_RECORDING, taskData: payload};
339
+
340
+ case CC_EVENTS.CONTACT_RECORDING_RESUMED:
341
+ return {type: TaskEvent.RESUME_RECORDING, taskData: payload};
342
+
343
+ case CC_EVENTS.AGENT_WRAPPEDUP:
344
+ return {type: TaskEvent.WRAPUP_COMPLETE, taskData: payload};
345
+
346
+ // Conference events - these trigger state machine transition to CONFERENCING
347
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
348
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
349
+ case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE:
350
+ return {type: TaskEvent.CONFERENCE_START, taskData: payload};
351
+
352
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
353
+ return {type: TaskEvent.CONFERENCE_FAILED, reason: payload.reason, taskData: payload};
354
+
355
+ case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
356
+ return {type: TaskEvent.CONFERENCE_END, taskData: payload};
357
+
358
+ case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE:
359
+ return {
360
+ type: TaskEvent.PARTICIPANT_LEAVE,
361
+ taskData: payload,
362
+ participantId: payload?.participantId,
363
+ };
364
+
365
+ case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
366
+ return {type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS, taskData: payload};
367
+
368
+ default:
369
+ // Not all events need state machine mapping
370
+ return null;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Register WebSocket message listeners for task events
376
+ *
377
+ * Main entry point that orchestrates event processing through a clear pipeline:
378
+ * 1. Parse and validate incoming WebSocket messages
379
+ * 2. Prepare event context with task and state machine mappings
380
+ * 3. Handle task lifecycle (creation, updates, collection management)
381
+ * 4. Send events to state machine (task-level transitions/emissions)
382
+ * 5. Cleanup is triggered via task events emitted by the state machine
383
+ *
384
+ * This architecture separates concerns:
385
+ * - TaskManager: Manages task collection lifecycle and operational concerns
386
+ * - State Machine: Manages individual task state and event emissions
387
+ */
142
388
  private registerTaskListeners() {
143
389
  this.webSocketManager.on('message', (event) => {
144
- const payload = JSON.parse(event);
145
- // Re-emit the task events to the task object
146
- let task: ITask;
147
- if (payload.data?.type || payload.type) {
148
- if (Object.values(CC_TASK_EVENTS).includes(payload.data.type || payload.type)) {
149
- task =
150
- this.taskCollection[payload.data?.interactionId] ||
151
- this.taskCollection[payload.data?.data?.conversationId];
152
- }
153
- LoggerProxy.info(`Handling task event ${payload.data?.type}`, {
154
- module: TASK_MANAGER_FILE,
155
- method: METHODS.REGISTER_TASK_LISTENERS,
156
- interactionId: payload.data?.interactionId,
157
- });
158
- switch (payload.data.type) {
159
- case CC_EVENTS.AGENT_CONTACT:
160
- // Case1 : Task is already present in taskCollection
161
- if (this.taskCollection[payload.data.interactionId]) {
162
- LoggerProxy.log(`Got AGENT_CONTACT: Task already exists in collection`, {
163
- module: TASK_MANAGER_FILE,
164
- method: METHODS.REGISTER_TASK_LISTENERS,
165
- interactionId: payload.data.interactionId,
166
- });
167
- break;
168
- } else if (!this.taskCollection[payload.data.interactionId]) {
169
- // Case2 : Task is not present in taskCollection
170
- LoggerProxy.log(`Got AGENT_CONTACT : Creating new task in taskManager`, {
171
- module: TASK_MANAGER_FILE,
172
- method: METHODS.REGISTER_TASK_LISTENERS,
173
- interactionId: payload.data.interactionId,
174
- });
175
-
176
- // Check if auto-answer should happen for this task
177
- const shouldAutoAnswer = shouldAutoAnswerTask(
178
- payload.data,
179
- this.agentId,
180
- this.webCallingService.loginOption,
181
- this.webRtcEnabled
182
- );
183
-
184
- task = new Task(
185
- this.contact,
186
- this.webCallingService,
187
- {
188
- ...payload.data,
189
- wrapUpRequired:
190
- payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
191
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
192
- isAutoAnswering: shouldAutoAnswer, // Set flag before emitting
193
- },
194
- this.wrapupData,
195
- this.agentId
196
- );
197
- this.taskCollection[payload.data.interactionId] = task;
198
- // Condition 1: The state is=new i.e it is a incoming task
199
- if (payload.data.interaction.state === 'new') {
200
- LoggerProxy.log(
201
- `Got AGENT_CONTACT for a task with state=new, sending TASK_INCOMING event`,
202
- {
203
- module: TASK_MANAGER_FILE,
204
- method: METHODS.REGISTER_TASK_LISTENERS,
205
- interactionId: payload.data.interactionId,
206
- }
207
- );
208
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
209
- } else {
210
- // Condition 2: The state is anything else i.e the task was connected
211
- LoggerProxy.log(
212
- `Got AGENT_CONTACT for a task with state=${payload.data.interaction.state}, sending TASK_HYDRATE event`,
213
- {
214
- module: TASK_MANAGER_FILE,
215
- method: METHODS.REGISTER_TASK_LISTENERS,
216
- interactionId: payload.data.interactionId,
217
- }
218
- );
219
- this.emit(TASK_EVENTS.TASK_HYDRATE, task);
220
- }
221
- }
222
- break;
223
-
224
- case CC_EVENTS.AGENT_CONTACT_RESERVED: {
225
- // Check if auto-answer should happen for this task
226
- const shouldAutoAnswerReserved = shouldAutoAnswerTask(
227
- payload.data,
228
- this.agentId,
229
- this.webCallingService.loginOption,
230
- this.webRtcEnabled
231
- );
232
-
233
- task = new Task(
234
- this.contact,
235
- this.webCallingService,
236
- {
237
- ...payload.data,
238
- isConsulted: false,
239
- isAutoAnswering: shouldAutoAnswerReserved, // Set flag before emitting
240
- },
241
- this.wrapupData,
242
- this.agentId
243
- );
244
- this.taskCollection[payload.data.interactionId] = task;
245
- if (
246
- this.webCallingService.loginOption !== LoginOption.BROWSER ||
247
- task.data.interaction.mediaType !== MEDIA_CHANNEL.TELEPHONY // for digital channels
248
- ) {
249
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
250
- } else if (this.call) {
251
- this.emit(TASK_EVENTS.TASK_INCOMING, task);
252
- }
253
- break;
254
- }
255
- case CC_EVENTS.AGENT_OFFER_CONTACT:
256
- // We don't have to emit any event here since this will be result of promise.
257
- task = this.updateTaskData(task, payload.data);
258
- LoggerProxy.log(`Agent offer contact received for task`, {
259
- module: TASK_MANAGER_FILE,
260
- method: METHODS.REGISTER_TASK_LISTENERS,
261
- interactionId: payload.data?.interactionId,
262
- });
263
- this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, task);
264
-
265
- // Handle auto-answer for offer contact
266
- this.handleAutoAnswer(task);
267
- break;
268
- case CC_EVENTS.AGENT_OUTBOUND_FAILED:
269
- if (task) {
270
- task = this.updateTaskData(task, payload.data);
271
- this.metricsManager.trackEvent(
272
- METRIC_EVENT_NAMES.TASK_OUTDIAL_FAILED,
273
- {
274
- ...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
275
- taskId: payload.data.interactionId,
276
- reason: payload.data.reasonCode || payload.data.reason,
277
- },
278
- ['behavioral', 'operational']
279
- );
280
- LoggerProxy.log(`Agent outbound failed for task`, {
281
- module: TASK_MANAGER_FILE,
282
- method: METHODS.REGISTER_TASK_LISTENERS,
283
- interactionId: payload.data.interactionId,
284
- });
285
- task.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, payload.data.reason ?? 'UNKNOWN_REASON');
286
- }
287
- break;
288
- case CC_EVENTS.AGENT_CONTACT_ASSIGNED:
289
- // When a campaign preview contact is accepted, the assigned event may arrive
290
- // with a new interactionId while the task is stored under the original
291
- // reservationInteractionId. Fall back to that key so the task is found.
292
- if (!task && payload.data.reservationInteractionId) {
293
- task = this.taskCollection[payload.data.reservationInteractionId];
294
- if (task) {
295
- // Re-key the task under the new interaction ID and remove the old entry
296
- delete this.taskCollection[payload.data.reservationInteractionId];
297
- this.taskCollection[payload.data.interactionId] = task;
298
- }
299
- }
300
- if (task) {
301
- task = this.updateTaskData(task, payload.data);
302
- task.emit(TASK_EVENTS.TASK_ASSIGNED, task);
303
- }
304
- // We emit the event to the task manager to send the task data to the instance without mobius registration in case of multi login with desktop login
305
- this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
306
- break;
307
- case CC_EVENTS.AGENT_CONTACT_UNASSIGNED:
308
- task = this.updateTaskData(task, {
309
- ...payload.data,
310
- wrapUpRequired: true,
311
- });
312
- task.emit(TASK_EVENTS.TASK_END, task);
313
- break;
314
- case CC_EVENTS.AGENT_CONTACT_OFFER_RONA:
315
- case CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED:
316
- case CC_EVENTS.AGENT_INVITE_FAILED: {
317
- LoggerProxy.info(`Task removal triggered by ${payload.data.type}`, {
318
- module: TASK_MANAGER_FILE,
319
- method: METHODS.REGISTER_TASK_LISTENERS,
320
- interactionId: payload.data.interactionId,
321
- });
322
- task = this.updateTaskData(task, payload.data);
323
-
324
- const eventTypeToMetricMap: Record<string, keyof typeof METRIC_EVENT_NAMES> = {
325
- [CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED]: 'AGENT_CONTACT_ASSIGN_FAILED',
326
- [CC_EVENTS.AGENT_INVITE_FAILED]: 'AGENT_INVITE_FAILED',
327
- };
328
- const metricEventName: keyof typeof METRIC_EVENT_NAMES =
329
- eventTypeToMetricMap[payload.data.type] || 'AGENT_RONA';
330
-
331
- this.metricsManager.trackEvent(
332
- METRIC_EVENT_NAMES[metricEventName],
333
- {
334
- ...MetricsManager.getCommonTrackingFieldForAQMResponse(payload.data),
335
- taskId: payload.data.interactionId,
336
- reason: payload.data.reason,
337
- },
338
- ['behavioral', 'operational']
339
- );
340
- this.handleTaskCleanup(task);
341
- task.emit(TASK_EVENTS.TASK_REJECT, payload.data.reason);
342
- break;
343
- }
344
- case CC_EVENTS.CONTACT_ENDED:
345
- // Update task data.
346
- if (task) {
347
- LoggerProxy.info(`Contact ended for interaction`, {
348
- module: TASK_MANAGER_FILE,
349
- method: METHODS.REGISTER_TASK_LISTENERS,
350
- interactionId: payload.data.interactionId,
351
- });
352
-
353
- // Campaign preview tasks should never trigger wrapup on ContactEnded —
354
- // they are terminal cleanup events. For all other tasks, derive
355
- // wrapUpRequired from agentsPendingWrapUp as before.
356
- const CAMPAIGN_OUTBOUND_TYPES = [
357
- 'STANDARD_PREVIEW_CAMPAIGN',
358
- 'DIRECT_PREVIEW_CAMPAIGN',
359
- ];
360
- const isCampaignPreview = CAMPAIGN_OUTBOUND_TYPES.includes(
361
- task.data?.interaction?.outboundType ?? ''
362
- );
363
- const wrapUpRequired = isCampaignPreview
364
- ? false
365
- : payload.data.agentsPendingWrapUp?.includes(this.agentId) || false;
366
-
367
- task = this.updateTaskData(task, {
368
- ...payload.data,
369
- wrapUpRequired,
370
- });
371
-
372
- // Handle cleanup based on whether task should be deleted
373
- this.handleTaskCleanup(task);
374
-
375
- task?.emit(TASK_EVENTS.TASK_END, task);
376
- }
377
- break;
378
- case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED: {
379
- // CampaignContactUpdated is a non-terminal event (e.g., next contact after skip/remove).
380
- // Update the task data and emit an event so consumers can react to the updated contact.
381
- // Do NOT remove the task or emit TASK_END — cleanup is handled by CONTACT_ENDED.
382
- if (task) {
383
- // Carry forward campaign preview fields from existing task data since the updated
384
- // contact payload may not include them, and reconcileData would delete them.
385
- const existingCpd = task.data?.interaction?.callProcessingDetails;
386
- const updatedData = {...payload.data};
387
-
388
- if (existingCpd) {
389
- const campaignFields = {
390
- ...(existingCpd.campaignPreviewAutoAction && {
391
- campaignPreviewAutoAction: existingCpd.campaignPreviewAutoAction,
392
- }),
393
- ...(existingCpd.campaignPreviewOfferTimeout && {
394
- campaignPreviewOfferTimeout: existingCpd.campaignPreviewOfferTimeout,
395
- }),
396
- ...(existingCpd.campaignPreviewSkipDisabled && {
397
- campaignPreviewSkipDisabled: existingCpd.campaignPreviewSkipDisabled,
398
- }),
399
- ...(existingCpd.campaignPreviewRemoveDisabled && {
400
- campaignPreviewRemoveDisabled: existingCpd.campaignPreviewRemoveDisabled,
401
- }),
402
- };
403
-
404
- if (!updatedData.interaction) {
405
- updatedData.interaction = {} as typeof updatedData.interaction;
406
- }
407
-
408
- updatedData.interaction = {
409
- ...updatedData.interaction,
410
- callProcessingDetails: {
411
- ...campaignFields,
412
- ...(updatedData.interaction.callProcessingDetails || {}),
413
- } as typeof existingCpd,
414
- };
415
- }
416
-
417
- LoggerProxy.log('Campaign contact updated - carrying forward preview fields', {
418
- module: TASK_MANAGER_FILE,
419
- method: METHODS.REGISTER_TASK_LISTENERS,
420
- interactionId: payload.data.interactionId,
421
- data: {
422
- hasCpd: !!updatedData.interaction?.callProcessingDetails,
423
- autoAction:
424
- updatedData.interaction?.callProcessingDetails?.campaignPreviewAutoAction,
425
- skipDisabled:
426
- updatedData.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled,
427
- removeDisabled:
428
- updatedData.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled,
429
- },
430
- });
431
-
432
- task = this.updateTaskData(task, updatedData);
433
- task.emit(TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED, task);
434
- }
435
- break;
436
- }
437
- case CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED:
438
- if (task) {
439
- // Failure payloads are sparse (no interaction field). Spread existing
440
- // task data first so reconcileData doesn't delete interaction/cpd.
441
- task = this.updateTaskData(task, {...task.data, ...payload.data});
442
- task.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED, task);
443
- }
444
- break;
445
- case CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED:
446
- if (task) {
447
- task = this.updateTaskData(task, {...task.data, ...payload.data});
448
- task.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_SKIP_FAILED, task);
449
- }
450
- break;
451
- case CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED:
452
- if (task) {
453
- task = this.updateTaskData(task, {...task.data, ...payload.data});
454
- task.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED, task);
455
- }
456
- break;
457
- case CC_EVENTS.CONTACT_MERGED:
458
- task = this.handleContactMerged(task, payload.data);
459
- break;
460
- case CC_EVENTS.AGENT_CONTACT_HELD:
461
- // As soon as the main interaction is held, we need to emit TASK_HOLD
462
- task = this.updateTaskData(task, payload.data);
463
- task.emit(TASK_EVENTS.TASK_HOLD, task);
464
- break;
465
- case CC_EVENTS.AGENT_CONTACT_UNHELD:
466
- // As soon as the main interaction is unheld, we need to emit TASK_RESUME
467
- task = this.updateTaskData(task, payload.data);
468
- task.emit(TASK_EVENTS.TASK_RESUME, task);
469
- break;
470
- case CC_EVENTS.AGENT_VTEAM_TRANSFERRED:
471
- task = this.updateTaskData(task, {
472
- ...payload.data,
473
- wrapUpRequired: true,
474
- });
475
- task.emit(TASK_EVENTS.TASK_END, task);
476
- break;
477
- case CC_EVENTS.AGENT_CTQ_CANCEL_FAILED:
478
- task = this.updateTaskData(task, payload.data);
479
- task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED, task);
480
- break;
481
- case CC_EVENTS.AGENT_CONSULT_CREATED:
482
- // Received when self agent initiates a consult
483
- task = this.updateTaskData(task, {
484
- ...payload.data,
485
- isConsulted: false, // This ensures that the task consult status is always reset
486
- });
487
- task.emit(TASK_EVENTS.TASK_CONSULT_CREATED, task);
488
- break;
489
- case CC_EVENTS.AGENT_OFFER_CONSULT:
490
- // Received when other agent sends us a consult offer
491
- task = this.updateTaskData(task, {
492
- ...payload.data,
493
- isConsulted: true, // This ensures that the task is marked as us being requested for a consult
494
- });
495
- task.emit(TASK_EVENTS.TASK_OFFER_CONSULT, task);
496
-
497
- // Handle auto-answer for consult offer
498
- this.handleAutoAnswer(task);
499
- break;
500
- case CC_EVENTS.AGENT_CONSULTING:
501
- // Received when agent is in an active consult state
502
- // TODO: Check if we can use backend consult state instead of isConsulted
503
- task = this.updateTaskData(task, payload.data);
504
- if (task.data.isConsulted) {
505
- // Fire only if you are the agent who received the consult request
506
- task.emit(TASK_EVENTS.TASK_CONSULT_ACCEPTED, task);
507
- } else {
508
- // Fire only if you are the agent who initiated the consult
509
- task.emit(TASK_EVENTS.TASK_CONSULTING, task);
510
- }
511
- // Also hydrate multi-login listeners when consulting state is received.
512
- this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
513
- break;
514
- case CC_EVENTS.AGENT_CONSULT_FAILED:
515
- // This can only be received by the agent who initiated the consult.
516
- // We need not emit any event here since this will be result of promise
517
- task = this.updateTaskData(task, payload.data);
518
- break;
519
- case CC_EVENTS.AGENT_CONSULT_ENDED:
520
- task = this.updateTaskData(task, payload.data);
521
- if (task.data.isConsulted) {
522
- // This will be the end state of the task as soon as we end the consult in case of
523
- // us being offered a consult
524
- this.removeTaskFromCollection(task);
525
- }
526
- task.emit(TASK_EVENTS.TASK_CONSULT_END, task);
527
- break;
528
- case CC_EVENTS.AGENT_CTQ_CANCELLED:
529
- // This event is received when the consult using queue is cancelled using API
530
- task = this.updateTaskData(task, payload.data);
531
- task.emit(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, task);
532
- break;
533
- case CC_EVENTS.AGENT_WRAPUP:
534
- task = this.updateTaskData(task, {...payload.data, wrapUpRequired: true});
535
- task.emit(TASK_EVENTS.TASK_END, task);
536
- break;
537
- case CC_EVENTS.AGENT_WRAPPEDUP:
538
- task.cancelAutoWrapupTimer();
539
- this.removeTaskFromCollection(task);
540
- task.emit(TASK_EVENTS.TASK_WRAPPEDUP, task);
541
- break;
542
- case CC_EVENTS.CONTACT_RECORDING_PAUSED:
543
- task = this.updateTaskData(task, payload.data);
544
- task.emit(TASK_EVENTS.TASK_RECORDING_PAUSED, task);
545
- break;
546
- case CC_EVENTS.CONTACT_RECORDING_PAUSE_FAILED:
547
- task = this.updateTaskData(task, payload.data);
548
- task.emit(TASK_EVENTS.TASK_RECORDING_PAUSE_FAILED, task);
549
- break;
550
- case CC_EVENTS.CONTACT_RECORDING_RESUMED:
551
- task = this.updateTaskData(task, payload.data);
552
- task.emit(TASK_EVENTS.TASK_RECORDING_RESUMED, task);
553
- break;
554
- case CC_EVENTS.CONTACT_RECORDING_RESUME_FAILED:
555
- task = this.updateTaskData(task, payload.data);
556
- task.emit(TASK_EVENTS.TASK_RECORDING_RESUME_FAILED, task);
557
- break;
558
- case CC_EVENTS.AGENT_CONSULT_CONFERENCING:
559
- // Conference is being established - update task state and emit establishing event
560
- task = this.updateTaskData(task, payload.data);
561
- task.emit(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, task);
562
- break;
563
- case CC_EVENTS.AGENT_CONSULT_CONFERENCED:
564
- // Conference started successfully - update task state and emit event
565
- task = this.updateTaskData(task, payload.data);
566
- task.emit(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
567
- break;
568
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED:
569
- // Conference failed - update task state and emit failure event
570
- task = this.updateTaskData(task, payload.data);
571
- task.emit(TASK_EVENTS.TASK_CONFERENCE_FAILED, task);
572
- break;
573
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED:
574
- // Conference ended - update task state and emit event
575
- task = this.updateTaskData(task, payload.data);
576
- if (
577
- !task ||
578
- isPrimary(task, this.agentId) ||
579
- isParticipantInMainInteraction(task, this.agentId)
580
- ) {
581
- LoggerProxy.log('Primary or main interaction participant leaving conference');
582
- } else {
583
- this.removeTaskFromCollection(task);
584
- }
585
- task.emit(TASK_EVENTS.TASK_CONFERENCE_ENDED, task);
586
- break;
587
- case CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE: {
588
- task = this.updateTaskData(task, {
589
- ...payload.data,
590
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
591
- });
592
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
593
- break;
594
- }
595
- case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE: {
596
- // Conference ended - update task state and emit event
597
-
598
- task = this.updateTaskData(task, {
599
- ...payload.data,
600
- isConferenceInProgress: getIsConferenceInProgress(payload.data),
601
- });
602
- if (checkParticipantNotInInteraction(task, this.agentId)) {
603
- if (
604
- isParticipantInMainInteraction(task, this.agentId) ||
605
- isPrimary(task, this.agentId)
606
- ) {
607
- LoggerProxy.log('Primary or main interaction participant leaving conference');
608
- } else {
609
- this.removeTaskFromCollection(task);
610
- }
611
- }
612
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
613
- break;
614
- }
615
- case CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED:
616
- // Conference exit failed - update task state and emit failure event
617
- task = this.updateTaskData(task, payload.data);
618
- task.emit(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, task);
619
- break;
620
- case CC_EVENTS.AGENT_CONSULT_CONFERENCE_END_FAILED:
621
- // Conference end failed - update task state with error details and emit failure event
622
- task = this.updateTaskData(task, payload.data);
623
- task.emit(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, task);
624
- break;
625
- case CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED:
626
- // Conference was transferred - update task state and emit transfer success event
627
- // Note: Backend should provide hasLeft and wrapUpRequired status
628
- task = this.updateTaskData(task, payload.data);
629
- task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, task);
630
- break;
631
- case CC_EVENTS.AGENT_CONFERENCE_TRANSFER_FAILED:
632
- // Conference transfer failed - update task state with error details and emit failure event
633
- task = this.updateTaskData(task, payload.data);
634
- task.emit(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, task);
635
- break;
636
- case CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY:
637
- // Post-call activity for participant - update task state with activity details
638
- task = this.updateTaskData(task, payload.data);
639
- task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
640
- break;
641
- case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION: {
642
- // Campaign preview contact offered to agent
643
- // Create a task in the collection so subsequent events (e.g. AGENT_CONTACT_ASSIGNED
644
- // after acceptPreviewContact) can find and update it.
645
- // Emit TASK_CAMPAIGN_PREVIEW_RESERVATION instead of TASK_INCOMING so the call
646
- // does not ring out to the customer before the agent explicitly accepts the preview contact.
647
- LoggerProxy.log('Campaign preview reservation received', {
648
- module: TASK_MANAGER_FILE,
649
- method: METHODS.REGISTER_TASK_LISTENERS,
650
- interactionId: payload.data.interactionId,
651
- });
652
-
653
- if (!this.taskCollection[payload.data.interactionId]) {
654
- task = new Task(
655
- this.contact,
656
- this.webCallingService,
657
- {
658
- ...payload.data,
659
- wrapUpRequired: false,
660
- isConferenceInProgress: false,
661
- isAutoAnswering: false,
662
- },
663
- this.wrapupData,
664
- this.agentId
665
- );
666
- this.taskCollection[payload.data.interactionId] = task;
667
- } else {
668
- task = this.updateTaskData(task, payload.data);
669
- }
670
-
671
- this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, task);
672
- break;
673
- }
390
+ // Step 1: Parse and validate the message
391
+ const message = TaskManager.parseWebSocketMessage(event);
392
+ if (!message) return;
674
393
 
675
- default:
676
- break;
677
- }
678
- if (task) {
679
- task.emit(payload.data.type, payload.data);
680
- }
394
+ // Step 2: Prepare event context
395
+ const eventContext = this.prepareEventContext(message);
396
+ if (!eventContext) return;
681
397
 
682
- const transcriptInteractionId =
683
- payload.data?.interactionId ||
684
- payload.data?.data?.conversationId ||
685
- task?.data?.interactionId;
398
+ const actions = this.handleTaskLifecycleEvent(eventContext);
686
399
 
687
- if (TRANSCRIPT_EVENT_MAP[payload.data.type] && transcriptInteractionId) {
688
- this.requestRealTimeTranscripts(payload.data.type, transcriptInteractionId);
689
- }
400
+ const {task} = actions;
401
+ if (!task) return;
402
+
403
+ const {payload, stateMachineEvent} = eventContext;
404
+
405
+ // Always keep task.data updated (even for mapped events) so consumers relying
406
+ // on TaskManager-managed task instances see the latest payload.
407
+ if (payload) {
408
+ this.updateTaskData(task, payload);
409
+ }
410
+
411
+ // Send event to state machine - this will trigger all TASK_EVENTS emissions
412
+ // including TASK_INCOMING which is now handled via the state machine callbacks
413
+ if (stateMachineEvent) {
414
+ task.sendStateMachineEvent(stateMachineEvent);
415
+ }
416
+
417
+ // Emit TASK_POST_CALL_ACTIVITY for ParticipantPostCallActivity events so
418
+ // consumers (Widgets) can detect the interaction state change to post_call.
419
+ if (eventContext.eventType === CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITY) {
420
+ task.emit(TASK_EVENTS.TASK_POST_CALL_ACTIVITY, task);
690
421
  }
422
+
423
+ // Send transcript start/stop events for relevant CC events
424
+ this.requestRealTimeTranscripts(eventContext.eventType, payload.interactionId);
691
425
  });
692
426
  }
693
427
 
694
- private updateTaskData(task: ITask, taskData: TaskData): ITask {
695
- if (!task) {
696
- return undefined;
697
- }
428
+ /**
429
+ * Parse and validate WebSocket message
430
+ * @returns Parsed message or null if invalid/keepalive
431
+ */
432
+ private static parseWebSocketMessage(event: string): WebSocketMessage | null {
433
+ try {
434
+ const payload = JSON.parse(event) as WebSocketMessage;
698
435
 
699
- if (!taskData?.interactionId) {
700
- LoggerProxy.warn('Received task update with missing interactionId', {
701
- module: TASK_MANAGER_FILE,
702
- method: METHODS.UPDATE_TASK_DATA,
703
- });
704
- }
436
+ // Filter out keepalive messages
437
+ if (payload?.keepalive === 'true' || payload?.keepalive === true) {
438
+ return null;
439
+ }
705
440
 
706
- try {
707
- const currentTask = task.updateTaskData(taskData);
708
- this.taskCollection[taskData.interactionId] = currentTask;
441
+ // Normalize task data if present
442
+ if (payload?.data?.interaction) {
443
+ payload.data = normalizeTaskData(payload.data);
444
+ }
709
445
 
710
- return currentTask;
446
+ return payload;
711
447
  } catch (error) {
712
- LoggerProxy.error(`Failed to update task`, {
448
+ LoggerProxy.error('Failed to parse WebSocket message', {
713
449
  module: TASK_MANAGER_FILE,
714
- method: METHODS.UPDATE_TASK_DATA,
715
- interactionId: taskData.interactionId,
450
+ method: 'parseWebSocketMessage',
451
+ error,
716
452
  });
717
453
 
718
- return task;
454
+ return null;
719
455
  }
720
456
  }
721
457
 
722
458
  /**
723
- * Handles CONTACT_MERGED event logic
724
- * @param task - The task to process
725
- * @param taskData - The task data from the event payload
726
- * @returns Updated or newly created task
727
- * @private
459
+ * Prepare context for event processing
460
+ * @returns Event context or null if event type is invalid
728
461
  */
729
- private handleContactMerged(task: ITask, taskData: TaskData): ITask {
730
- if (taskData.childInteractionId) {
731
- // remove the child task from collection
732
- this.removeTaskFromCollection(this.taskCollection[taskData.childInteractionId]);
462
+ private prepareEventContext(message: WebSocketMessage): EventContext | null {
463
+ const eventType = message.data?.type || message.type;
464
+
465
+ if (!eventType || !isCcEvent(eventType)) {
466
+ return null;
733
467
  }
734
468
 
735
- if (this.taskCollection[taskData.interactionId]) {
736
- LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
737
- module: TASK_MANAGER_FILE,
738
- method: METHODS.REGISTER_TASK_LISTENERS,
739
- interactionId: taskData.interactionId,
740
- });
741
- // update the task data
742
- task = this.updateTaskData(task, taskData);
743
- } else {
744
- // Case2 : Task is not present in taskCollection
745
- LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
469
+ const interactionId = message.data.interactionId;
470
+ const reservationInteractionId = message.data.reservationInteractionId;
471
+ let task = this.taskCollection[interactionId];
472
+
473
+ // When a campaign preview contact is accepted, the assigned event may arrive
474
+ // with a new interactionId while the task is stored under the original
475
+ // reservationInteractionId. Fall back to that key so the task is found.
476
+ if (!task && reservationInteractionId) {
477
+ task = this.taskCollection[reservationInteractionId];
478
+ if (task) {
479
+ // Re-key the task under the new interaction ID and remove the old entry
480
+ delete this.taskCollection[reservationInteractionId];
481
+ this.taskCollection[interactionId] = task;
482
+ }
483
+ }
484
+
485
+ const wasConsultedTask = Boolean(task?.data?.isConsulted);
486
+ const computeWrapUpRequired = () => {
487
+ if (message.data.wrapUpRequired !== undefined) {
488
+ return message.data.wrapUpRequired;
489
+ }
490
+ if (message.data.isConsulted !== undefined) {
491
+ return !message.data.isConsulted;
492
+ }
493
+
494
+ return !wasConsultedTask;
495
+ };
496
+
497
+ const adjustedPayload =
498
+ eventType === CC_EVENTS.AGENT_CONSULT_TRANSFERRED ||
499
+ eventType === CC_EVENTS.AGENT_BLIND_TRANSFERRED ||
500
+ eventType === CC_EVENTS.AGENT_VTEAM_TRANSFERRED
501
+ ? {
502
+ ...message.data,
503
+ wrapUpRequired: computeWrapUpRequired(),
504
+ }
505
+ : message.data;
506
+
507
+ const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(
508
+ eventType,
509
+ adjustedPayload,
510
+ this.agentId
511
+ );
512
+
513
+ LoggerProxy.info(`Handling task event ${eventType}`, {
514
+ module: TASK_MANAGER_FILE,
515
+ method: 'prepareEventContext',
516
+ interactionId,
517
+ });
518
+
519
+ return {
520
+ eventType,
521
+ payload: adjustedPayload,
522
+ task,
523
+ stateMachineEvent,
524
+ };
525
+ }
526
+
527
+ /**
528
+ * Handle task lifecycle events and determine required actions
529
+ *
530
+ * Delegates to specific event handlers based on event type. Each handler
531
+ * is responsible for TaskManager-level concerns:
532
+ * - Task creation and collection management
533
+ * - Metrics tracking
534
+ * - Resource cleanup decisions
535
+ *
536
+ * Note: Task-level state transitions and event emissions are handled by
537
+ * the task state machine via sendStateMachineEvent()
538
+ */
539
+ private handleTaskLifecycleEvent(context: EventContext): TaskEventActions {
540
+ const {eventType} = context;
541
+
542
+ switch (eventType) {
543
+ case CC_EVENTS.AGENT_CONTACT_RESERVED:
544
+ return this.handleContactReserved(context);
545
+
546
+ case CC_EVENTS.AGENT_CONTACT:
547
+ return this.handleAgentContact(context);
548
+
549
+ case CC_EVENTS.CONTACT_MERGED:
550
+ return this.handleContactMergedEvent(context);
551
+
552
+ case CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION:
553
+ return this.handleCampaignPreviewReservation(context);
554
+
555
+ case CC_EVENTS.CAMPAIGN_CONTACT_UPDATED:
556
+ return this.handleCampaignContactUpdated(context);
557
+
558
+ default:
559
+ return {task: context.task};
560
+ }
561
+ }
562
+
563
+ private handleCampaignContactUpdated(context: EventContext) {
564
+ const {payload} = context;
565
+ let {task} = context;
566
+ // CampaignContactUpdated is a non-terminal event (e.g., next contact after skip/remove).
567
+ // Update the task data and emit an event so consumers can react to the updated contact.
568
+ // Do NOT remove the task or emit TASK_END — cleanup is handled by CONTACT_ENDED.
569
+ if (task) {
570
+ // Carry forward campaign preview fields from existing task data since the updated
571
+ // contact payload may not include them, and reconcileData would delete them.
572
+ const existingCpd = task.data?.interaction?.callProcessingDetails;
573
+ const updatedData: TaskData = {...payload};
574
+
575
+ if (existingCpd) {
576
+ const campaignFields = {
577
+ ...(existingCpd.campaignPreviewAutoAction && {
578
+ campaignPreviewAutoAction: existingCpd.campaignPreviewAutoAction,
579
+ }),
580
+ ...(existingCpd.campaignPreviewOfferTimeout && {
581
+ campaignPreviewOfferTimeout: existingCpd.campaignPreviewOfferTimeout,
582
+ }),
583
+ ...(existingCpd.campaignPreviewSkipDisabled && {
584
+ campaignPreviewSkipDisabled: existingCpd.campaignPreviewSkipDisabled,
585
+ }),
586
+ ...(existingCpd.campaignPreviewRemoveDisabled && {
587
+ campaignPreviewRemoveDisabled: existingCpd.campaignPreviewRemoveDisabled,
588
+ }),
589
+ };
590
+
591
+ if (!updatedData.interaction) {
592
+ updatedData.interaction = {} as typeof updatedData.interaction;
593
+ }
594
+
595
+ updatedData.interaction = {
596
+ ...updatedData.interaction,
597
+ callProcessingDetails: {
598
+ ...campaignFields,
599
+ ...(updatedData.interaction.callProcessingDetails || {}),
600
+ } as typeof existingCpd,
601
+ };
602
+ }
603
+
604
+ LoggerProxy.log('Campaign contact updated - carrying forward preview fields', {
746
605
  module: TASK_MANAGER_FILE,
747
606
  method: METHODS.REGISTER_TASK_LISTENERS,
748
- interactionId: taskData.interactionId,
607
+ interactionId: payload.interactionId,
608
+ data: {
609
+ hasCpd: !!updatedData.interaction?.callProcessingDetails,
610
+ autoAction: updatedData.interaction?.callProcessingDetails?.campaignPreviewAutoAction,
611
+ skipDisabled: updatedData.interaction?.callProcessingDetails?.campaignPreviewSkipDisabled,
612
+ removeDisabled:
613
+ updatedData.interaction?.callProcessingDetails?.campaignPreviewRemoveDisabled,
614
+ },
749
615
  });
750
616
 
751
- task = new Task(
617
+ task = this.updateTaskData(task, updatedData);
618
+ task.emit(TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED, task);
619
+ }
620
+
621
+ return {task};
622
+ }
623
+
624
+ /**
625
+ * Creates or updates a task for campaign preview reservation.
626
+ * TASK_CAMPAIGN_PREVIEW_RESERVATION is emitted by the state machine (campaign TASK_INCOMING branch).
627
+ */
628
+ private handleCampaignPreviewReservation(context: EventContext): TaskEventActions {
629
+ const {payload} = context;
630
+ let {task} = context;
631
+
632
+ LoggerProxy.log('Campaign preview reservation received', {
633
+ module: TASK_MANAGER_FILE,
634
+ method: METHODS.REGISTER_TASK_LISTENERS,
635
+ interactionId: payload.interactionId,
636
+ });
637
+
638
+ if (!task) {
639
+ task = TaskFactory.createTask(
752
640
  this.contact,
753
641
  this.webCallingService,
754
642
  {
755
- ...taskData,
756
- wrapUpRequired: taskData.interaction?.participants?.[this.agentId]?.isWrapUp || false,
757
- isConferenceInProgress: getIsConferenceInProgress(taskData),
643
+ ...payload,
644
+ wrapUpRequired: false,
645
+ isConferenceInProgress: false,
646
+ isAutoAnswering: false,
758
647
  },
648
+ this.configFlags,
649
+ this.wrapupData,
650
+ this.agentId
651
+ );
652
+ this.setupTaskListeners(task);
653
+ this.taskCollection[payload.interactionId] = task;
654
+ } else {
655
+ task = this.updateTaskData(task, payload);
656
+ }
657
+
658
+ return {task};
659
+ }
660
+
661
+ /**
662
+ * Handle AGENT_CONTACT_RESERVED event
663
+ * Creates a new task; state machine event is sent during processing
664
+ */
665
+ private handleContactReserved(context: EventContext): TaskEventActions {
666
+ const {payload} = context;
667
+ const isConsultedTask =
668
+ payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
669
+ const shouldAutoAnswer = shouldAutoAnswerTask(
670
+ payload,
671
+ this.agentId,
672
+ this.webCallingService.loginOption,
673
+ this.webRtcEnabled
674
+ );
675
+
676
+ const taskData: TaskData = {
677
+ ...payload,
678
+ isConsulted: isConsultedTask,
679
+ isAutoAnswering: shouldAutoAnswer,
680
+ };
681
+
682
+ const task = TaskFactory.createTask(
683
+ this.contact,
684
+ this.webCallingService,
685
+ taskData,
686
+ this.configFlags,
687
+ this.wrapupData,
688
+ this.agentId
689
+ );
690
+
691
+ this.setupTaskListeners(task);
692
+ this.taskCollection[payload.interactionId] = task;
693
+
694
+ return {task};
695
+ }
696
+
697
+ /**
698
+ * Handle AGENT_CONTACT event
699
+ * Re-creates task if missing (multi-session scenario)
700
+ */
701
+ private handleAgentContact(context: EventContext): TaskEventActions {
702
+ let {task} = context;
703
+ const {payload} = context;
704
+
705
+ if (!task) {
706
+ const isConsultedTask =
707
+ payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction);
708
+ const shouldAutoAnswer = shouldAutoAnswerTask(
709
+ payload,
710
+ this.agentId,
711
+ this.webCallingService.loginOption,
712
+ this.webRtcEnabled
713
+ );
714
+ const taskData: TaskData = {
715
+ ...payload,
716
+ isConsulted: isConsultedTask,
717
+ wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
718
+ isConferenceInProgress: getIsConferenceInProgress(payload),
719
+ isAutoAnswering: shouldAutoAnswer,
720
+ };
721
+
722
+ task = TaskFactory.createTask(
723
+ this.contact,
724
+ this.webCallingService,
725
+ taskData,
726
+ this.configFlags,
759
727
  this.wrapupData,
760
728
  this.agentId
761
729
  );
762
- this.taskCollection[taskData.interactionId] = task;
730
+ this.setupTaskListeners(task);
731
+ this.taskCollection[payload.interactionId] = task;
732
+ }
733
+
734
+ return {task};
735
+ }
736
+
737
+ private updateTaskData(task: ITask, taskData: TaskData): ITask {
738
+ if (!task) {
739
+ throw new Error('Task not found for update');
763
740
  }
764
741
 
765
- this.emit(TASK_EVENTS.TASK_MERGED, task);
742
+ const snapshot = task.stateMachineService?.getSnapshot?.();
743
+ const isConsultingFlow =
744
+ snapshot?.value === 'CONSULTING' || taskData.interaction?.state === 'consulting';
745
+
746
+ const updateTaskData = isConsultingFlow
747
+ ? {
748
+ ...taskData,
749
+ destAgentId: taskData.destAgentId ?? snapshot?.context?.consultDestinationAgentId ?? null,
750
+ destinationType:
751
+ taskData.destinationType ?? snapshot?.context?.consultDestinationType ?? null,
752
+ }
753
+ : taskData;
754
+
755
+ task.updateTaskData(updateTaskData);
756
+ this.taskCollection[taskData.interactionId] = task;
766
757
 
767
758
  return task;
768
759
  }
769
760
 
761
+ /**
762
+ * Setup listeners for task events that need to be bubbled up to TaskManager
763
+ * This replaces the previous callback injection pattern
764
+ */
765
+ private setupTaskListeners(task: ITask): void {
766
+ // Listen for TASK_INCOMING and re-emit so webex.cc can notify consumers
767
+ task.on(TASK_EVENTS.TASK_INCOMING, (t: ITask) => {
768
+ LoggerProxy.log(`Task incoming event received`, {
769
+ module: TASK_MANAGER_FILE,
770
+ method: METHODS.REGISTER_TASK_LISTENERS,
771
+ interactionId: t.data?.interactionId,
772
+ });
773
+
774
+ this.emit(TASK_EVENTS.TASK_INCOMING, t);
775
+ });
776
+
777
+ task.on(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, (t: ITask) => {
778
+ LoggerProxy.log(`Campaign preview reservation event received`, {
779
+ module: TASK_MANAGER_FILE,
780
+ method: METHODS.REGISTER_TASK_LISTENERS,
781
+ interactionId: t.data?.interactionId,
782
+ });
783
+
784
+ this.emit(TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION, t);
785
+ });
786
+
787
+ // Listen for TASK_HYDRATE on the task and re-emit on TaskManager
788
+ task.on(TASK_EVENTS.TASK_HYDRATE, (t: ITask) => {
789
+ // Task data is already updated by the task itself before emitting
790
+ this.emit(TASK_EVENTS.TASK_HYDRATE, t);
791
+ });
792
+
793
+ task.on(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, (t: ITask) => {
794
+ this.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, t);
795
+ });
796
+
797
+ // Listen for internal cleanup signal emitted by the state machine
798
+ task.on(TASK_EVENTS.TASK_CLEANUP, (t: ITask, options?: {removeFromCollection?: boolean}) => {
799
+ this.handleTaskCleanup(t);
800
+ if (options?.removeFromCollection) {
801
+ const interactionId = t?.data?.interactionId;
802
+ if (interactionId && this.taskCollection[interactionId]) {
803
+ this.removeTaskFromCollection(t);
804
+ }
805
+ }
806
+ });
807
+ }
808
+
770
809
  private removeTaskFromCollection(task: ITask) {
810
+ if (typeof task.cancelAutoWrapupTimer === 'function') {
811
+ task.cancelAutoWrapupTimer();
812
+ }
771
813
  if (task?.data?.interactionId) {
772
814
  delete this.taskCollection[task.data.interactionId];
773
815
  LoggerProxy.info(`Task removed from collection`, {
@@ -779,69 +821,72 @@ export default class TaskManager extends EventEmitter {
779
821
  }
780
822
 
781
823
  /**
782
- * Handles auto-answer logic for incoming tasks
783
- * Automatically accepts tasks when isAutoAnswering flag is set
784
- * The flag is set during task creation based on:
785
- * 1. WebRTC calls with auto-answer enabled in agent profile
786
- * 2. Agent-initiated WebRTC outdial calls
787
- * 3. Agent-initiated digital outbound (Email/SMS) without previous transfers
788
- *
789
- * @param task - The task to auto-answer
824
+ * Handles CONTACT_MERGED event logic
825
+ * @param task - The task to process
826
+ * @param taskData - The task data from the event payload
827
+ * @returns Updated or newly created task
790
828
  * @private
791
829
  */
792
- private async handleAutoAnswer(task: ITask): Promise<void> {
793
- if (!task || !task.data || !task.data.isAutoAnswering) {
794
- return;
795
- }
830
+ private handleContactMergedEvent(context: EventContext): TaskEventActions {
831
+ const {payload} = context;
832
+ let task = context.task;
796
833
 
797
- LoggerProxy.info(`Auto-answering task`, {
798
- module: TASK_MANAGER_FILE,
799
- method: 'handleAutoAnswer',
800
- interactionId: task.data.interactionId,
801
- });
834
+ if (payload.childInteractionId && this.taskCollection[payload.childInteractionId]) {
835
+ this.removeTaskFromCollection(this.taskCollection[payload.childInteractionId]);
836
+ }
802
837
 
803
- try {
804
- await task.accept();
805
- LoggerProxy.info(`Task auto-answered successfully`, {
838
+ if (task) {
839
+ LoggerProxy.log(`Got CONTACT_MERGED: Task already exists in collection`, {
806
840
  module: TASK_MANAGER_FILE,
807
- method: 'handleAutoAnswer',
808
- interactionId: task.data.interactionId,
841
+ method: METHODS.REGISTER_TASK_LISTENERS,
842
+ interactionId: payload.interactionId,
809
843
  });
810
-
811
- // Track successful auto-answer
812
- this.metricsManager.trackEvent(
813
- METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_SUCCESS,
814
- {
815
- taskId: task.data.interactionId,
816
- mediaType: task.data.interaction.mediaType,
817
- isAutoAnswered: true,
818
- },
819
- ['behavioral', 'operational']
820
- );
821
- // Emit task:autoAnswered event for widgets/UI to react
822
- task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
823
- } catch (error) {
824
- // Reset isAutoAnswering flag on failure
825
- task.updateTaskData({...task.data, isAutoAnswering: false});
826
- LoggerProxy.error(`Failed to auto-answer task`, {
844
+ // update the task data
845
+ this.updateTaskData(task, payload);
846
+ } else {
847
+ // Case2 : Task is not present in taskCollection
848
+ LoggerProxy.log(`Got CONTACT_MERGED : Creating new task in taskManager`, {
827
849
  module: TASK_MANAGER_FILE,
828
- method: 'handleAutoAnswer',
829
- interactionId: task.data.interactionId,
830
- error,
850
+ method: METHODS.REGISTER_TASK_LISTENERS,
851
+ interactionId: payload.interactionId,
831
852
  });
832
853
 
833
- // Track auto-answer failure
834
- this.metricsManager.trackEvent(
835
- METRIC_EVENT_NAMES.TASK_AUTO_ANSWER_FAILED,
836
- {
837
- taskId: task.data.interactionId,
838
- mediaType: task.data.interaction.mediaType,
839
- error: error?.message || 'Unknown error',
840
- isAutoAnswered: false,
841
- },
842
- ['behavioral', 'operational']
854
+ const taskData: TaskData = {
855
+ ...payload,
856
+ wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false,
857
+ isConferenceInProgress: getIsConferenceInProgress(payload),
858
+ isConsulted: false,
859
+ };
860
+
861
+ task = TaskFactory.createTask(
862
+ this.contact,
863
+ this.webCallingService,
864
+ taskData,
865
+ this.configFlags,
866
+ this.wrapupData,
867
+ this.agentId
843
868
  );
869
+ this.taskCollection[payload.interactionId] = task;
870
+
871
+ // Transition the new task out of IDLE immediately so UI controls are
872
+ // computed before TASK_MERGED is emitted. This handles the race where
873
+ // AgentContactAssigned arrives before ContactMerged and gets dropped.
874
+ // Send HYDRATE before setupTaskListeners so the emitTaskHydrate action
875
+ // doesn't bubble up to the Widget (avoids duplicate listener registration).
876
+ task.sendStateMachineEvent({
877
+ type: TaskEvent.HYDRATE,
878
+ taskData,
879
+ agentId: this.agentId,
880
+ } as TaskEventPayload);
881
+
882
+ this.setupTaskListeners(task);
844
883
  }
884
+
885
+ if (task) {
886
+ this.emit(TASK_EVENTS.TASK_MERGED, task);
887
+ }
888
+
889
+ return {task};
845
890
  }
846
891
 
847
892
  /**
@@ -853,7 +898,8 @@ export default class TaskManager extends EventEmitter {
853
898
  // Clean up Desktop/WebRTC calling resources for browser-based telephony tasks
854
899
  if (
855
900
  this.webCallingService.loginOption === LoginOption.BROWSER &&
856
- task.data.interaction.mediaType === 'telephony'
901
+ task.data.interaction.mediaType === MEDIA_CHANNEL.TELEPHONY &&
902
+ task instanceof WebRTC
857
903
  ) {
858
904
  task.unregisterWebCallListeners();
859
905
  this.webCallingService.cleanUpCall();
@@ -881,12 +927,8 @@ export default class TaskManager extends EventEmitter {
881
927
  */
882
928
  private requestRealTimeTranscripts(eventType: string, interactionId: string): void {
883
929
  const action = TRANSCRIPT_EVENT_MAP[eventType];
884
- if (
885
- !action ||
886
- !this.apiAIAssistant ||
887
- this.apiAIAssistant.aiFeature?.realtimeTranscripts?.enable === false
888
- )
889
- return;
930
+ if (!action || !this.apiAIAssistant) return;
931
+ if (this.configFlags?.aiFeature?.realtimeTranscripts?.enable !== true) return;
890
932
 
891
933
  this.apiAIAssistant
892
934
  .sendEvent(
@@ -899,7 +941,7 @@ export default class TaskManager extends EventEmitter {
899
941
  .catch((error) => {
900
942
  LoggerProxy.error(`Failed to send transcript ${action} event`, {
901
943
  module: TASK_MANAGER_FILE,
902
- method: 'requestRealTimeTranscripts',
944
+ method: METHODS.REQUEST_REAL_TIME_TRANSCRIPTS,
903
945
  interactionId,
904
946
  error,
905
947
  });
@@ -910,28 +952,27 @@ export default class TaskManager extends EventEmitter {
910
952
  return this.taskCollection[taskId];
911
953
  }
912
954
 
913
- /**
914
- * @param taskId - Unique identifier for each task
915
- */
916
- public getAllTasks = (): Record<TaskId, ITask> => {
917
- return this.taskCollection;
918
- };
955
+ public getAllTasks(): Record<TaskId, ITask> {
956
+ return {...this.taskCollection};
957
+ }
919
958
 
920
959
  public static getTaskManager(
921
960
  apiAIAssistant: ApiAIAssistant,
922
961
  contact: ReturnType<typeof routingContact>,
923
962
  webCallingService: WebCallingService,
924
- webSocketManager: WebSocketManager
963
+ webSocketManager: WebSocketManager,
964
+ rtdWebSocketManager?: WebSocketManager
925
965
  ): TaskManager {
926
966
  if (!TaskManager.taskManager) {
927
967
  TaskManager.taskManager = new TaskManager(
928
968
  apiAIAssistant,
929
969
  contact,
930
970
  webCallingService,
931
- webSocketManager
971
+ webSocketManager,
972
+ rtdWebSocketManager
932
973
  );
933
974
  }
934
975
 
935
- return this.taskManager;
976
+ return TaskManager.taskManager;
936
977
  }
937
978
  }