@webex/contact-center 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.10

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 (198) hide show
  1. package/.sdd/manifest.json +7 -5
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +8 -3
  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 +544 -12
  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/MetricsManager.js +16 -0
  15. package/dist/metrics/MetricsManager.js.map +1 -1
  16. package/dist/metrics/behavioral-events.js +104 -0
  17. package/dist/metrics/behavioral-events.js.map +1 -1
  18. package/dist/metrics/constants.js +18 -0
  19. package/dist/metrics/constants.js.map +1 -1
  20. package/dist/services/AddressBook.js +18 -15
  21. package/dist/services/AddressBook.js.map +1 -1
  22. package/dist/services/AnswerCallOnWebexService.js +202 -0
  23. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  24. package/dist/services/EntryPoint.js +46 -68
  25. package/dist/services/EntryPoint.js.map +1 -1
  26. package/dist/services/Queue.js +27 -22
  27. package/dist/services/Queue.js.map +1 -1
  28. package/dist/services/WebexCrossClientService.js +222 -0
  29. package/dist/services/WebexCrossClientService.js.map +1 -0
  30. package/dist/services/WxAppTelephonyMercurySync.js +108 -0
  31. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  32. package/dist/services/config/Util.js +3 -0
  33. package/dist/services/config/Util.js.map +1 -1
  34. package/dist/services/config/constants.js +10 -6
  35. package/dist/services/config/constants.js.map +1 -1
  36. package/dist/services/config/types.js +4 -0
  37. package/dist/services/config/types.js.map +1 -1
  38. package/dist/services/core/Err.js.map +1 -1
  39. package/dist/services/core/WebexRequest.js +6 -2
  40. package/dist/services/core/WebexRequest.js.map +1 -1
  41. package/dist/services/core/aqm-reqs.js +28 -14
  42. package/dist/services/core/aqm-reqs.js.map +1 -1
  43. package/dist/services/core/types.js.map +1 -1
  44. package/dist/services/task/Task.js +73 -7
  45. package/dist/services/task/Task.js.map +1 -1
  46. package/dist/services/task/TaskFactory.js +8 -4
  47. package/dist/services/task/TaskFactory.js.map +1 -1
  48. package/dist/services/task/TaskManager.js +490 -21
  49. package/dist/services/task/TaskManager.js.map +1 -1
  50. package/dist/services/task/WebexCallingUtils.js +70 -0
  51. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  52. package/dist/services/task/constants.js +4 -1
  53. package/dist/services/task/constants.js.map +1 -1
  54. package/dist/services/task/contact.js +29 -0
  55. package/dist/services/task/contact.js.map +1 -1
  56. package/dist/services/task/digital/Digital.js +3 -2
  57. package/dist/services/task/digital/Digital.js.map +1 -1
  58. package/dist/services/task/state-machine/TaskStateMachine.js +162 -53
  59. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  60. package/dist/services/task/state-machine/actions.js +6 -1
  61. package/dist/services/task/state-machine/actions.js.map +1 -1
  62. package/dist/services/task/state-machine/guards.js +7 -10
  63. package/dist/services/task/state-machine/guards.js.map +1 -1
  64. package/dist/services/task/state-machine/types.js.map +1 -1
  65. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  66. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  67. package/dist/services/task/types.js +16 -1
  68. package/dist/services/task/types.js.map +1 -1
  69. package/dist/services/task/voice/Voice.js +456 -15
  70. package/dist/services/task/voice/Voice.js.map +1 -1
  71. package/dist/services/task/voice/WebRTC.js +4 -1
  72. package/dist/services/task/voice/WebRTC.js.map +1 -1
  73. package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
  74. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  75. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  76. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  77. package/dist/services/wxAppTelephonyUtils.js +19 -0
  78. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  79. package/dist/types/cc.d.ts +78 -4
  80. package/dist/types/config.d.ts +7 -0
  81. package/dist/types/constants.d.ts +8 -0
  82. package/dist/types/index.d.ts +3 -1
  83. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  84. package/dist/types/metrics/constants.d.ts +17 -0
  85. package/dist/types/services/AddressBook.d.ts +2 -1
  86. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  87. package/dist/types/services/EntryPoint.d.ts +7 -5
  88. package/dist/types/services/Queue.d.ts +5 -3
  89. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  90. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  91. package/dist/types/services/config/constants.d.ts +8 -5
  92. package/dist/types/services/config/types.d.ts +17 -4
  93. package/dist/types/services/core/Err.d.ts +2 -0
  94. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  95. package/dist/types/services/core/types.d.ts +2 -0
  96. package/dist/types/services/task/Task.d.ts +20 -2
  97. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  98. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  99. package/dist/types/services/task/constants.d.ts +3 -0
  100. package/dist/types/services/task/contact.d.ts +4 -0
  101. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  102. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +304 -56
  103. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  104. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  105. package/dist/types/services/task/types.d.ts +81 -4
  106. package/dist/types/services/task/voice/Voice.d.ts +52 -9
  107. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  108. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
  109. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  110. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  111. package/dist/types/types.d.ts +14 -8
  112. package/dist/types/utils/PageCache.d.ts +20 -3
  113. package/dist/types.js +7 -2
  114. package/dist/types.js.map +1 -1
  115. package/dist/utils/PageCache.js +19 -5
  116. package/dist/utils/PageCache.js.map +1 -1
  117. package/dist/webex.js +1 -1
  118. package/package.json +8 -8
  119. package/src/cc.ts +657 -12
  120. package/src/config.ts +7 -0
  121. package/src/constants.ts +8 -0
  122. package/src/index.ts +2 -0
  123. package/src/metrics/MetricsManager.ts +16 -0
  124. package/src/metrics/ai-docs/metrics-spec.md +78 -10
  125. package/src/metrics/behavioral-events.ts +106 -0
  126. package/src/metrics/constants.ts +20 -0
  127. package/src/services/AddressBook.ts +17 -6
  128. package/src/services/AnswerCallOnWebexService.ts +239 -0
  129. package/src/services/EntryPoint.ts +59 -60
  130. package/src/services/Queue.ts +29 -12
  131. package/src/services/WebexCrossClientService.ts +293 -0
  132. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  133. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  134. package/src/services/ai-docs/AGENTS.md +10 -10
  135. package/src/services/ai-docs/services-spec.md +6 -1
  136. package/src/services/config/Util.ts +3 -0
  137. package/src/services/config/ai-docs/AGENTS.md +1 -1
  138. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  139. package/src/services/config/ai-docs/config-spec.md +6 -0
  140. package/src/services/config/constants.ts +10 -6
  141. package/src/services/config/types.ts +16 -4
  142. package/src/services/core/Err.ts +1 -0
  143. package/src/services/core/WebexRequest.ts +3 -1
  144. package/src/services/core/ai-docs/core-spec.md +5 -1
  145. package/src/services/core/aqm-reqs.ts +30 -15
  146. package/src/services/core/types.ts +2 -0
  147. package/src/services/task/Task.ts +79 -8
  148. package/src/services/task/TaskFactory.ts +8 -3
  149. package/src/services/task/TaskManager.ts +716 -17
  150. package/src/services/task/WebexCallingUtils.ts +136 -0
  151. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  152. package/src/services/task/ai-docs/task-spec.md +156 -3
  153. package/src/services/task/constants.ts +3 -0
  154. package/src/services/task/contact.ts +30 -0
  155. package/src/services/task/digital/Digital.ts +4 -2
  156. package/src/services/task/state-machine/TaskStateMachine.ts +235 -81
  157. package/src/services/task/state-machine/actions.ts +5 -2
  158. package/src/services/task/state-machine/ai-docs/AGENTS.md +6 -4
  159. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +35 -16
  160. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +103 -33
  161. package/src/services/task/state-machine/guards.ts +7 -10
  162. package/src/services/task/state-machine/types.ts +16 -1
  163. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  164. package/src/services/task/types.ts +114 -4
  165. package/src/services/task/voice/Voice.ts +595 -14
  166. package/src/services/task/voice/WebRTC.ts +5 -1
  167. package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
  168. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  169. package/src/services/wxAppTelephonyUtils.ts +14 -0
  170. package/src/types.ts +33 -9
  171. package/src/utils/AGENTS.md +21 -10
  172. package/src/utils/PageCache.ts +38 -5
  173. package/src/utils/ai-docs/utils-spec.md +21 -11
  174. package/test/unit/spec/cc.ts +1522 -19
  175. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  176. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  177. package/test/unit/spec/services/AddressBook.ts +37 -6
  178. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  179. package/test/unit/spec/services/EntryPoint.ts +87 -40
  180. package/test/unit/spec/services/Queue.ts +123 -12
  181. package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
  182. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  183. package/test/unit/spec/services/config/Util.ts +85 -0
  184. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  185. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  186. package/test/unit/spec/services/task/Task.ts +274 -6
  187. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  188. package/test/unit/spec/services/task/TaskManager.ts +1717 -9
  189. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  190. package/test/unit/spec/services/task/contact.ts +33 -0
  191. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1485 -109
  192. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  193. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  194. package/test/unit/spec/services/task/voice/Voice.ts +1412 -0
  195. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
  196. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  197. package/umd/contact-center.min.js +2 -2
  198. package/umd/contact-center.min.js.map +1 -1
@@ -12,8 +12,11 @@ import {
12
12
  ResumeRecordingPayload,
13
13
  TaskData,
14
14
  TaskResponse,
15
+ DropConferenceParticipantPayload,
15
16
  IVoice,
16
17
  VoiceUIControlOptions,
18
+ TaskToggleMuteOptions,
19
+ TaskTransmitDtmfOptions,
17
20
  TransferPayLoad,
18
21
  ConsultTransferPayLoad,
19
22
  consultConferencePayloadData,
@@ -25,11 +28,67 @@ import Task from '../Task';
25
28
  import LoggerProxy from '../../../logger-proxy';
26
29
  import MetricsManager from '../../../metrics/MetricsManager';
27
30
  import {METRIC_EVENT_NAMES} from '../../../metrics/constants';
28
- import {TaskState, TaskEvent, TaskActionArgs} from '../state-machine';
31
+ import {TaskState, TaskEvent, TaskActionArgs, type TaskContext} from '../state-machine';
32
+ import {shouldWrapUpForThisAgent} from '../state-machine/guards';
29
33
  import {WrapupData} from '../../config/types';
30
34
  import {getConsultMediaResourceId, getIsConferenceInProgress} from '../TaskUtils';
35
+ import AnswerCallOnWebexService from '../../AnswerCallOnWebexService';
36
+ import {isWxAppCallNotFoundError} from '../../wxAppTelephonyUtils';
37
+ import {
38
+ getCallingDeviceDetails,
39
+ getWebexCallingCallId,
40
+ resolveWxAppLineOwnerId,
41
+ isWebexAppCallingOffer as wxIsWebexAppCallingOffer,
42
+ isWebexAppInboundCallingOffer as wxIsWebexAppInboundCallingOffer,
43
+ mapWxAppVoiceError,
44
+ runWxAppAccept,
45
+ runWxAppOutdialDecline,
46
+ runWxAppReject,
47
+ runWxAppToggleMute,
48
+ runWxAppTransmitDtmf,
49
+ WxAppVoiceDependencies,
50
+ WxAppVoiceLifecycle,
51
+ } from './wxAppVoiceMethods';
52
+ import {
53
+ deriveWxAppAcceptReason,
54
+ logWxAppMercuryMuteSync,
55
+ logWxAppOfferDecision,
56
+ callIdSuffix,
57
+ WxAppAcceptReason,
58
+ } from '../../wxAppDiagnosticLogging';
59
+
60
+ const hasNonemptyPendingWrapUp = (taskData?: TaskData): boolean =>
61
+ Array.isArray(taskData?.agentsPendingWrapUp) && taskData.agentsPendingWrapUp.length > 0;
62
+
63
+ /**
64
+ * Same consulted-non-owner rule as TaskManager leave stamp. Used only for the
65
+ * HTTP exitConference self-exit fallback (shouldWrapUpForThisAgent stays unchanged).
66
+ */
67
+ const isConsultedNonOwner = (taskData: TaskData, fallback?: TaskData): boolean => {
68
+ const isConsulted = taskData.isConsulted ?? fallback?.isConsulted;
69
+ if (isConsulted !== true) {
70
+ return false;
71
+ }
72
+ const owner = taskData.interaction?.owner ?? fallback?.interaction?.owner;
73
+ const selfAgentId = taskData.agentId ?? fallback?.agentId;
74
+
75
+ return Boolean(selfAgentId && owner && owner !== selfAgentId);
76
+ };
31
77
 
32
78
  export default class Voice extends Task implements IVoice {
79
+ private static readonly WXAPP_MUTE_SYNC_RETRY_DELAY_MS = 50;
80
+ private static readonly WXAPP_MUTE_SYNC_MAX_RETRIES = 3;
81
+
82
+ private answerCallOnWebexService?: AnswerCallOnWebexService;
83
+ private enableWxBetterTogether = false;
84
+ private wxAppMuted = false;
85
+ private wxAppAnswerPending = false;
86
+ private wxAppAcceptInFlight = false;
87
+ private wxAppMuteSyncInFlight?: Promise<boolean | undefined>;
88
+ private wxAppMuteToggleTail: Promise<void> = Promise.resolve();
89
+ private wxAppDtmfTail: Promise<void> = Promise.resolve();
90
+ private lastLoggedWxAppAcceptReason?: WxAppAcceptReason;
91
+
33
92
  constructor(
34
93
  contact: ReturnType<typeof routingContact>,
35
94
  data: TaskData,
@@ -42,6 +101,7 @@ export default class Voice extends Task implements IVoice {
42
101
  isEndConsultEnabled: callOptions?.isEndConsultEnabled ?? true,
43
102
  voiceVariant: callOptions?.voiceVariant ?? VOICE_VARIANT.PSTN,
44
103
  isRecordingEnabled: callOptions?.isRecordingEnabled ?? true,
104
+ enableWxBetterTogether: callOptions?.enableWxBetterTogether ?? false,
45
105
  };
46
106
 
47
107
  super(
@@ -49,10 +109,314 @@ export default class Voice extends Task implements IVoice {
49
109
  data,
50
110
  {
51
111
  ...resolvedOptions,
112
+ consultTransferConfig: callOptions?.consultTransferConfig,
52
113
  },
53
114
  wrapupData,
54
115
  agentId
55
116
  );
117
+
118
+ this.enableWxBetterTogether = resolvedOptions.enableWxBetterTogether;
119
+ this.answerCallOnWebexService = callOptions?.answerCallOnWebexService;
120
+ }
121
+
122
+ private getWxAppVoiceDependencies(): WxAppVoiceDependencies {
123
+ return {
124
+ enableWxBetterTogether: this.enableWxBetterTogether,
125
+ answerCallOnWebexService: this.answerCallOnWebexService,
126
+ agentId: this.agentId,
127
+ metricsManager: this.metricsManager,
128
+ getTaskData: () => this.data,
129
+ getTaskState: () => this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined,
130
+ getWxAppMuted: () => this.wxAppMuted,
131
+ setWxAppMuted: (muted: boolean) => {
132
+ this.wxAppMuted = muted;
133
+ },
134
+ };
135
+ }
136
+
137
+ /** @deprecated Use {@link Voice.getWxAppVoiceDependencies} */
138
+ private getWxAppVoiceDeps(): WxAppVoiceDependencies {
139
+ return this.getWxAppVoiceDependencies();
140
+ }
141
+
142
+ public isWebexAppCallingOffer(): boolean {
143
+ return wxIsWebexAppCallingOffer(this.getWxAppVoiceDeps());
144
+ }
145
+
146
+ public isWebexAppInboundCallingOffer(): boolean {
147
+ return wxIsWebexAppInboundCallingOffer(this.getWxAppVoiceDeps());
148
+ }
149
+
150
+ private setWxAppAnswerPending(pending: boolean): void {
151
+ this.wxAppAnswerPending = pending;
152
+ this.uiControlConfig = {...this.uiControlConfig, wxAppAnswerPending: pending};
153
+ this.updateUiControls(true);
154
+ }
155
+
156
+ private setWxAppAcceptInFlight(inFlight: boolean): void {
157
+ this.wxAppAcceptInFlight = inFlight;
158
+ this.uiControlConfig = {...this.uiControlConfig, wxAppAcceptInFlight: inFlight};
159
+ this.updateUiControls(true);
160
+ }
161
+
162
+ public setEnableWxBetterTogether(enabled: boolean): void {
163
+ this.enableWxBetterTogether = enabled;
164
+ this.uiControlConfig = {...this.uiControlConfig, enableWxBetterTogether: enabled};
165
+ this.updateUiControls(true);
166
+ }
167
+
168
+ protected updateUiControls(forceEmit = false): void {
169
+ super.updateUiControls(forceEmit);
170
+ this.logWxAppOfferDecisionIfNeeded();
171
+ }
172
+
173
+ private logWxAppOfferDecisionIfNeeded(): void {
174
+ if (!this.enableWxBetterTogether) {
175
+ return;
176
+ }
177
+
178
+ const state = this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined;
179
+ if (state !== TaskState.OFFERED) {
180
+ return;
181
+ }
182
+
183
+ const accept = this.currentUiControls?.main?.accept;
184
+ if (!accept?.isVisible) {
185
+ return;
186
+ }
187
+
188
+ const deps = this.getWxAppVoiceDependencies();
189
+ const isOutdial = this.data?.interaction?.outboundType === 'OUTDIAL';
190
+ const isWxAppInboundOffer = this.isWebexAppInboundCallingOffer();
191
+ const isWxAppOutdialOffer = this.isWebexAppCallingOffer() && isOutdial;
192
+ const deviceDetails = getCallingDeviceDetails(deps);
193
+ const acceptReason = deriveWxAppAcceptReason({
194
+ isWxAppInboundOffer,
195
+ isWxAppOutdialOffer,
196
+ isWebrtc: this.uiControlConfig.voiceVariant === VOICE_VARIANT.WEBRTC,
197
+ isOutdial,
198
+ wxAppAcceptInFlight: this.wxAppAcceptInFlight,
199
+ wxAppAnswerPending: this.wxAppAnswerPending,
200
+ enableWxBetterTogether: this.enableWxBetterTogether,
201
+ hasDeviceCallId: Boolean(deviceDetails?.deviceCallId),
202
+ });
203
+
204
+ if (acceptReason === this.lastLoggedWxAppAcceptReason) {
205
+ return;
206
+ }
207
+
208
+ this.lastLoggedWxAppAcceptReason = acceptReason;
209
+
210
+ logWxAppOfferDecision({
211
+ interactionId: this.data.interactionId,
212
+ acceptVisible: accept.isVisible,
213
+ acceptEnabled: accept.isEnabled,
214
+ acceptReason,
215
+ wxAppParticipantDeviceType: deviceDetails?.deviceType,
216
+ hasDeviceCallId: Boolean(deviceDetails?.deviceCallId),
217
+ });
218
+ }
219
+
220
+ public applyWxAppMuteStateFromSync(incomingCallId: string, muted: boolean): void {
221
+ if (!this.enableWxBetterTogether) {
222
+ return;
223
+ }
224
+
225
+ const activeCallId = this.getWebexCallingCallId();
226
+ const incomingSuffix = callIdSuffix(incomingCallId);
227
+ const activeSuffix = callIdSuffix(activeCallId);
228
+
229
+ if (!activeCallId || !incomingCallId.endsWith(activeCallId)) {
230
+ logWxAppMercuryMuteSync({
231
+ phase: 'dropped',
232
+ muted,
233
+ callIdSuffix: incomingSuffix,
234
+ interactionId: this.data.interactionId,
235
+ dropReason: !activeCallId ? 'no_active_wxApp_call' : 'call_id_mismatch',
236
+ });
237
+
238
+ return;
239
+ }
240
+
241
+ if (this.wxAppMuted === muted) {
242
+ logWxAppMercuryMuteSync({
243
+ phase: 'dropped',
244
+ muted,
245
+ callIdSuffix: incomingSuffix,
246
+ interactionId: this.data.interactionId,
247
+ dropReason: 'mute_state_unchanged',
248
+ });
249
+
250
+ return;
251
+ }
252
+
253
+ this.wxAppMuted = muted;
254
+ logWxAppMercuryMuteSync({
255
+ phase: 'applied',
256
+ muted,
257
+ callIdSuffix: activeSuffix,
258
+ interactionId: this.data.interactionId,
259
+ });
260
+ this.emit(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted});
261
+ }
262
+
263
+ public getWxAppMuted(): boolean {
264
+ return this.wxAppMuted;
265
+ }
266
+
267
+ private static delay(ms: number): Promise<void> {
268
+ return new Promise((resolve) => {
269
+ setTimeout(resolve, ms);
270
+ });
271
+ }
272
+
273
+ private canRetryWxAppMuteSync(): boolean {
274
+ if (!this.enableWxBetterTogether) {
275
+ return false;
276
+ }
277
+
278
+ return (
279
+ Boolean(getCallingDeviceDetails(this.getWxAppVoiceDeps())?.deviceCallId) &&
280
+ !this.getWebexCallingCallId()
281
+ );
282
+ }
283
+
284
+ private shouldSkipWxAppMuteSync(): boolean {
285
+ if (this.data?.interaction?.isTerminated) {
286
+ return true;
287
+ }
288
+
289
+ return this.isWebexAppCallingOffer() && !this.uiControlConfig?.wxAppAnswerPending;
290
+ }
291
+
292
+ private applyWxAppMuteFromBackfill(callId: string, muted: boolean): void {
293
+ if (!this.enableWxBetterTogether || this.wxAppMuted === muted) {
294
+ return;
295
+ }
296
+
297
+ const activeCallId = this.getWebexCallingCallId();
298
+ if (activeCallId && !callId.endsWith(activeCallId) && !activeCallId.endsWith(callId)) {
299
+ return;
300
+ }
301
+
302
+ this.wxAppMuted = muted;
303
+ this.emit(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted});
304
+ }
305
+
306
+ public async syncWxAppMuteFromCallDetails(retryAttempt = 0): Promise<boolean | undefined> {
307
+ if (!this.enableWxBetterTogether || !this.answerCallOnWebexService) {
308
+ return undefined;
309
+ }
310
+
311
+ if (retryAttempt === 0 && this.shouldSkipWxAppMuteSync()) {
312
+ return undefined;
313
+ }
314
+
315
+ if (retryAttempt === 0 && this.wxAppMuteSyncInFlight) {
316
+ return this.wxAppMuteSyncInFlight;
317
+ }
318
+
319
+ const syncPromise = this.performWxAppMuteSync(retryAttempt);
320
+
321
+ if (retryAttempt === 0) {
322
+ this.wxAppMuteSyncInFlight = syncPromise;
323
+ syncPromise
324
+ .finally(() => {
325
+ if (this.wxAppMuteSyncInFlight === syncPromise) {
326
+ this.wxAppMuteSyncInFlight = undefined;
327
+ }
328
+ })
329
+ .catch(() => undefined);
330
+
331
+ return syncPromise;
332
+ }
333
+
334
+ return syncPromise;
335
+ }
336
+
337
+ private async performWxAppMuteSync(retryAttempt: number): Promise<boolean | undefined> {
338
+ if (this.shouldSkipWxAppMuteSync()) {
339
+ return undefined;
340
+ }
341
+
342
+ let callId = this.getWebexCallingCallId();
343
+ if (!callId) {
344
+ if (retryAttempt < Voice.WXAPP_MUTE_SYNC_MAX_RETRIES && this.canRetryWxAppMuteSync()) {
345
+ await Voice.delay(Voice.WXAPP_MUTE_SYNC_RETRY_DELAY_MS);
346
+
347
+ return this.performWxAppMuteSync(retryAttempt + 1);
348
+ }
349
+
350
+ callId = getCallingDeviceDetails(this.getWxAppVoiceDeps())?.deviceCallId ?? null;
351
+ if (!callId) {
352
+ return undefined;
353
+ }
354
+ }
355
+
356
+ try {
357
+ const lineOwnerId = resolveWxAppLineOwnerId(this.getWxAppVoiceDeps());
358
+ const details = await this.answerCallOnWebexService.getCallDetails({callId, lineOwnerId});
359
+ if (details.muted !== undefined) {
360
+ this.applyWxAppMuteStateFromSync(callId, details.muted);
361
+ if (this.wxAppMuted !== details.muted) {
362
+ this.applyWxAppMuteFromBackfill(callId, details.muted);
363
+ }
364
+
365
+ return details.muted;
366
+ }
367
+
368
+ return this.wxAppMuted;
369
+ } catch (error) {
370
+ if (!isWxAppCallNotFoundError(error)) {
371
+ LoggerProxy.error(`Failed to sync wxApp mute from call details: ${error}`, {
372
+ module: CC_FILE,
373
+ method: METHODS.GET_CALL_DETAILS_ON_WEBEX,
374
+ interactionId: this.data?.interactionId,
375
+ });
376
+ }
377
+
378
+ return undefined;
379
+ }
380
+ }
381
+
382
+ protected onTaskAssigned(): void {
383
+ this.setWxAppAnswerPending(false);
384
+ queueMicrotask(() => {
385
+ this.syncWxAppMuteFromCallDetails().catch(() => undefined);
386
+ });
387
+ }
388
+
389
+ protected onTaskHydrated(): void {
390
+ const state = this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined;
391
+ const answerStillPending =
392
+ state === TaskState.OFFERED &&
393
+ (this.uiControlConfig?.wxAppAcceptInFlight || this.uiControlConfig?.wxAppAnswerPending);
394
+
395
+ if (!answerStillPending) {
396
+ this.setWxAppAnswerPending(false);
397
+ }
398
+
399
+ this.syncWxAppMuteFromCallDetails().catch(() => undefined);
400
+ }
401
+
402
+ public getCallingDeviceDetails() {
403
+ return getCallingDeviceDetails(this.getWxAppVoiceDeps());
404
+ }
405
+
406
+ public getWebexCallingCallId(): string | null {
407
+ return getWebexCallingCallId(this.getWxAppVoiceDeps());
408
+ }
409
+
410
+ private createWxAppLifecycle(): WxAppVoiceLifecycle {
411
+ return {
412
+ setWxAppAcceptInFlight: (inFlight) => this.setWxAppAcceptInFlight(inFlight),
413
+ setWxAppAnswerPending: (pending) => this.setWxAppAnswerPending(pending),
414
+ resetWxAppMuted: () => {
415
+ this.wxAppMuted = false;
416
+ },
417
+ syncWxAppMuteFromCallDetails: () => this.syncWxAppMuteFromCallDetails(),
418
+ mapWxAppVoiceError: (error, method) => mapWxAppVoiceError(error, method, CC_FILE),
419
+ };
56
420
  }
57
421
 
58
422
  private getStateMachineSnapshot() {
@@ -60,23 +424,120 @@ export default class Voice extends Task implements IVoice {
60
424
  }
61
425
 
62
426
  /**
63
- * This method is used to accept the task.
64
- * It is expected to be overridden by child classes.
65
- * @returns Promise<TaskResponse>
66
- * @throws Error
427
+ * Accepts the task. Routes to wxApp telephony answer when `enableWxBetterTogether` and wxApp offer.
67
428
  */
68
429
  public async accept(): Promise<TaskResponse> {
69
- super.unsupportedMethodError(METHODS.ACCEPT);
430
+ if (this.enableWxBetterTogether && this.isWebexAppCallingOffer()) {
431
+ await runWxAppAccept(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle());
432
+
433
+ return Promise.resolve();
434
+ }
435
+
436
+ return super.unsupportedMethodError(METHODS.ACCEPT);
70
437
  }
71
438
 
72
439
  /**
73
- * This method is used to decline the task.
74
- * It is expected to be overridden by child classes.
75
- * @returns Promise<TaskResponse>
76
- * @throws Error
440
+ * Declines the task. Routes wxApp inbound to telephony reject; wxApp outdial offer to cancelTask.
77
441
  */
78
442
  public async decline(): Promise<TaskResponse> {
79
- super.unsupportedMethodError(METHODS.REJECT);
443
+ if (this.enableWxBetterTogether && this.isWebexAppInboundCallingOffer()) {
444
+ await runWxAppReject(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle());
445
+
446
+ return Promise.resolve();
447
+ }
448
+
449
+ const isOutdial = this.data?.interaction?.outboundType === 'OUTDIAL';
450
+ if (!this.enableWxBetterTogether || !isOutdial || !this.isWebexAppCallingOffer()) {
451
+ return super.unsupportedMethodError(METHODS.REJECT);
452
+ }
453
+
454
+ const interactionId = this.data.interactionId;
455
+ LoggerProxy.log(`Declining wxApp outdial task for taskId:${interactionId}`, {
456
+ module: CC_FILE,
457
+ method: METHODS.REJECT,
458
+ interactionId,
459
+ });
460
+
461
+ try {
462
+ this.metricsManager.timeEvent([
463
+ METRIC_EVENT_NAMES.TASK_DECLINE_SUCCESS,
464
+ METRIC_EVENT_NAMES.TASK_DECLINE_FAILED,
465
+ ]);
466
+
467
+ this.setWxAppAnswerPending(false);
468
+ const response = await runWxAppOutdialDecline(this.getWxAppVoiceDependencies(), () =>
469
+ this.contact.cancelTask({interactionId})
470
+ );
471
+
472
+ this.metricsManager.trackEvent(
473
+ METRIC_EVENT_NAMES.TASK_DECLINE_SUCCESS,
474
+ {
475
+ taskId: interactionId,
476
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
477
+ },
478
+ ['operational', 'behavioral']
479
+ );
480
+
481
+ return response;
482
+ } catch (error) {
483
+ this.metricsManager.trackEvent(
484
+ METRIC_EVENT_NAMES.TASK_DECLINE_FAILED,
485
+ {
486
+ taskId: interactionId,
487
+ error: error.toString(),
488
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(
489
+ (error as any).details || {}
490
+ ),
491
+ },
492
+ ['operational', 'behavioral']
493
+ );
494
+ const {error: detailedError} = getErrorDetails(error, METHODS.REJECT, CC_FILE);
495
+ throw detailedError;
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Toggles mute for wxApp engaged telephony when `enableWxBetterTogether` is active.
501
+ */
502
+ public async toggleMute(options?: TaskToggleMuteOptions): Promise<void> {
503
+ if (this.enableWxBetterTogether && this.getWebexCallingCallId()) {
504
+ const togglePromise = this.wxAppMuteToggleTail
505
+ .catch(() => undefined)
506
+ .then(() =>
507
+ runWxAppToggleMute(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle(), options)
508
+ );
509
+
510
+ this.wxAppMuteToggleTail = togglePromise.catch(() => undefined);
511
+ await togglePromise;
512
+
513
+ return;
514
+ }
515
+
516
+ super.unsupportedMethodError(METHODS.TOGGLE_MUTE);
517
+ }
518
+
519
+ /**
520
+ * Sends in-call DTMF for wxApp engaged telephony when `enableWxBetterTogether` is active.
521
+ */
522
+ public async transmitDtmf(options: TaskTransmitDtmfOptions): Promise<void> {
523
+ if (this.enableWxBetterTogether && this.getWebexCallingCallId()) {
524
+ const dtmfPromise = this.wxAppDtmfTail
525
+ .catch(() => undefined)
526
+ .then(() =>
527
+ runWxAppTransmitDtmf(
528
+ this.getWxAppVoiceDependencies(),
529
+ this.createWxAppLifecycle(),
530
+ options
531
+ )
532
+ );
533
+
534
+ this.wxAppDtmfTail = dtmfPromise.catch(() => undefined);
535
+ await dtmfPromise;
536
+
537
+ return;
538
+ }
539
+
540
+ super.unsupportedMethodError(METHODS.TRANSMIT_DTMF);
80
541
  }
81
542
 
82
543
  /**
@@ -827,6 +1288,99 @@ export default class Voice extends Task implements IVoice {
827
1288
  }
828
1289
  }
829
1290
 
1291
+ /**
1292
+ * Removes another participant from an active conference.
1293
+ * Completion is correlated with ParticipantLeftConference by the AQM request layer.
1294
+ * @param payload - Participant identifier to remove
1295
+ * @returns Promise<TaskResponse>
1296
+ */
1297
+ public async dropConferenceParticipant(
1298
+ payload: DropConferenceParticipantPayload
1299
+ ): Promise<TaskResponse> {
1300
+ // The TypeScript contract requires payload, but the published SDK is also callable from
1301
+ // untyped JavaScript. Keep an explicit runtime boundary check before direct property access.
1302
+ if (!payload) {
1303
+ throw new Error('participantId must be a non-empty string');
1304
+ }
1305
+
1306
+ const {participantId} = payload;
1307
+
1308
+ if (typeof participantId !== 'string' || participantId.trim().length === 0) {
1309
+ throw new Error('participantId must be a non-empty string');
1310
+ }
1311
+
1312
+ const requestInteractionId =
1313
+ this.data.interaction?.mainInteractionId || this.data.interactionId;
1314
+ // taskId identifies the SDK task instance, which can remain child-interaction keyed after
1315
+ // consult/EP-DN flows. requestInteractionId identifies the main interaction sent to AQM.
1316
+
1317
+ LoggerProxy.info('Dropping conference participant', {
1318
+ module: CC_FILE,
1319
+ method: METHODS.DROP_CONFERENCE_PARTICIPANT,
1320
+ interactionId: requestInteractionId,
1321
+ });
1322
+
1323
+ try {
1324
+ this.metricsManager.timeEvent([
1325
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS,
1326
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
1327
+ ]);
1328
+
1329
+ const response = await this.contact.dropConferenceParticipant({
1330
+ interactionId: requestInteractionId,
1331
+ participantId,
1332
+ });
1333
+
1334
+ this.metricsManager.trackEvent(
1335
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS,
1336
+ {
1337
+ taskId: this.data.interactionId,
1338
+ requestInteractionId,
1339
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(response),
1340
+ agentId: this.agentId,
1341
+ },
1342
+ ['operational', 'behavioral', 'business']
1343
+ );
1344
+
1345
+ LoggerProxy.log('Successfully dropped conference participant', {
1346
+ module: CC_FILE,
1347
+ method: METHODS.DROP_CONFERENCE_PARTICIPANT,
1348
+ trackingId: response?.trackingId,
1349
+ interactionId: requestInteractionId,
1350
+ });
1351
+
1352
+ return response;
1353
+ } catch (error) {
1354
+ const failureTrackingFields = MetricsManager.getCommonTrackingFieldForAQMResponseFailed(
1355
+ error?.details || {}
1356
+ );
1357
+
1358
+ this.metricsManager.trackEvent(
1359
+ METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
1360
+ {
1361
+ taskId: this.data.interactionId,
1362
+ requestInteractionId,
1363
+ agentId: this.agentId,
1364
+ trackingId: failureTrackingFields.trackingId,
1365
+ notifTrackingId: failureTrackingFields.notifTrackingId,
1366
+ orgId: failureTrackingFields.orgId,
1367
+ failureType: failureTrackingFields.failureType,
1368
+ reasonCode: failureTrackingFields.reasonCode,
1369
+ },
1370
+ ['operational', 'behavioral', 'business']
1371
+ );
1372
+
1373
+ LoggerProxy.error('Failed to drop conference participant', {
1374
+ module: CC_FILE,
1375
+ method: METHODS.DROP_CONFERENCE_PARTICIPANT,
1376
+ trackingId: failureTrackingFields.trackingId,
1377
+ interactionId: requestInteractionId,
1378
+ });
1379
+
1380
+ throw error;
1381
+ }
1382
+ }
1383
+
830
1384
  /**
831
1385
  * Exit from a conference call.
832
1386
  * Per conference-spec.md:
@@ -891,9 +1445,32 @@ export default class Voice extends Task implements IVoice {
891
1445
 
892
1446
  // Send success event to transition state
893
1447
  if (this.stateMachineService) {
1448
+ const responseTaskData = (response?.data ?? {}) as TaskData;
1449
+ const snapshot = this.stateMachineService.getSnapshot();
1450
+ const wrapUpContext = (snapshot?.context ?? {
1451
+ uiControlConfig: this.uiControlConfig,
1452
+ taskData: this.data,
1453
+ }) as TaskContext;
1454
+ const mergedTaskData = {
1455
+ ...this.data,
1456
+ ...responseTaskData,
1457
+ };
1458
+ // Pending-list precedence is response-only; do not keep a cached snapshot list.
1459
+ if (!Object.prototype.hasOwnProperty.call(responseTaskData, 'agentsPendingWrapUp')) {
1460
+ delete mergedTaskData.agentsPendingWrapUp;
1461
+ }
1462
+ let wrapUpRequired = shouldWrapUpForThisAgent(wrapUpContext, mergedTaskData);
1463
+ // Confirmed self-exit: empty/absent response pending matches TaskManager leave stamp.
1464
+ if (!wrapUpRequired && !hasNonemptyPendingWrapUp(responseTaskData)) {
1465
+ wrapUpRequired = !isConsultedNonOwner(mergedTaskData, this.data);
1466
+ }
1467
+
894
1468
  this.stateMachineService.send({
895
1469
  type: TaskEvent.EXIT_CONFERENCE_SUCCESS,
896
- taskData: response.data,
1470
+ taskData: {
1471
+ ...responseTaskData,
1472
+ wrapUpRequired,
1473
+ },
897
1474
  });
898
1475
  }
899
1476
 
@@ -1259,8 +1836,12 @@ export default class Voice extends Task implements IVoice {
1259
1836
  {updateTaskData: true}
1260
1837
  ),
1261
1838
  emitTaskOutdialFailed: ({event}: TaskActionArgs) => {
1262
- if (event && 'taskData' in event && event.taskData) {
1263
- this.updateTaskData(event.taskData as TaskData);
1839
+ const taskData = (event as {taskData?: TaskData})?.taskData;
1840
+ if (taskData) {
1841
+ this.updateTaskData(taskData);
1842
+ }
1843
+ if (taskData?.suppressOutdialFailedPopup) {
1844
+ return;
1264
1845
  }
1265
1846
  const reason = (event as {reason?: string})?.reason || 'Outdial failed';
1266
1847
  this.emit(TASK_EVENTS.TASK_OUTDIAL_FAILED, reason);
@@ -9,6 +9,7 @@ import {
9
9
  IWebRTC,
10
10
  VoiceUIControlOptions,
11
11
  VOICE_VARIANT,
12
+ TaskToggleMuteOptions,
12
13
  } from '../types';
13
14
  import Voice from './Voice';
14
15
  import WebCallingService from '../../WebCallingService';
@@ -170,7 +171,10 @@ export default class WebRTC extends Voice implements IWebRTC {
170
171
  * task.toggleMute().then(()=>{}).catch(()=>{})
171
172
  * ```
172
173
  */
173
- public async toggleMute() {
174
+ public async toggleMute(options?: TaskToggleMuteOptions) {
175
+ if (options) {
176
+ // parameter intentionally unused — WebRTC toggles from local stream state
177
+ }
174
178
  LoggerProxy.log(`Toggling mute WebRTC task for taskId:${this.data.interactionId}`, {
175
179
  module: 'WebRTC',
176
180
  method: 'toggleMute',