@webex/contact-center 3.12.0-llmrefactor.8 → 3.12.0-llmrefactor.9

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 (56) hide show
  1. package/.sdd/manifest.json +3 -2
  2. package/ai-docs/CONTRACTS.md +2 -1
  3. package/dist/cc.js +108 -6
  4. package/dist/cc.js.map +1 -1
  5. package/dist/metrics/MetricsManager.js +16 -0
  6. package/dist/metrics/MetricsManager.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +91 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +16 -0
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/AnswerCallOnWebexService.js +34 -6
  12. package/dist/services/AnswerCallOnWebexService.js.map +1 -1
  13. package/dist/services/WebexCrossClientService.js +54 -3
  14. package/dist/services/WebexCrossClientService.js.map +1 -1
  15. package/dist/services/WxAppTelephonyMercurySync.js +21 -6
  16. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
  17. package/dist/services/task/voice/Voice.js +81 -14
  18. package/dist/services/task/voice/Voice.js.map +1 -1
  19. package/dist/services/task/voice/wxAppVoiceMethods.js +154 -1
  20. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  21. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  22. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  23. package/dist/types/cc.d.ts +1 -0
  24. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  25. package/dist/types/metrics/constants.d.ts +15 -0
  26. package/dist/types/services/AnswerCallOnWebexService.d.ts +1 -0
  27. package/dist/types/services/WebexCrossClientService.d.ts +4 -0
  28. package/dist/types/services/task/voice/Voice.d.ts +5 -1
  29. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +3 -0
  30. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  31. package/dist/webex.js +1 -1
  32. package/package.json +8 -8
  33. package/src/cc.ts +155 -7
  34. package/src/metrics/MetricsManager.ts +16 -0
  35. package/src/metrics/ai-docs/metrics-spec.md +71 -9
  36. package/src/metrics/behavioral-events.ts +92 -0
  37. package/src/metrics/constants.ts +18 -0
  38. package/src/services/AnswerCallOnWebexService.ts +41 -8
  39. package/src/services/WebexCrossClientService.ts +85 -4
  40. package/src/services/WxAppTelephonyMercurySync.ts +31 -4
  41. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  42. package/src/services/task/ai-docs/task-spec.md +2 -0
  43. package/src/services/task/voice/Voice.ts +115 -26
  44. package/src/services/task/voice/wxAppVoiceMethods.ts +211 -2
  45. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  46. package/test/unit/spec/cc.ts +251 -22
  47. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  48. package/test/unit/spec/metrics/behavioral-events.ts +42 -0
  49. package/test/unit/spec/services/AnswerCallOnWebexService.ts +41 -0
  50. package/test/unit/spec/services/WebexCrossClientService.ts +207 -1
  51. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +23 -0
  52. package/test/unit/spec/services/task/voice/Voice.ts +380 -0
  53. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +145 -1
  54. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  55. package/umd/contact-center.min.js +2 -2
  56. package/umd/contact-center.min.js.map +1 -1
@@ -12,19 +12,30 @@ export type WxAppTelephonyError = Error & {
12
12
  status?: number | string;
13
13
  };
14
14
 
15
- const markWxAppTelephonyError = (error: Error, source: unknown): WxAppTelephonyError => {
16
- const marked = error as WxAppTelephonyError;
17
- marked.isWxAppTelephonyError = true;
18
- const sourceObj = source as {
19
- details?: {trackingId?: string; status?: number | string};
15
+ const extractWxAppTrackingId = (source: unknown): string | undefined => {
16
+ const sourceObj = source as {details?: {trackingId?: string}; trackingId?: string};
17
+
18
+ return sourceObj?.details?.trackingId || sourceObj?.trackingId;
19
+ };
20
+
21
+ const extractWxAppHttpStatus = (source: unknown): number | string | undefined => {
22
+ const obj = source as {
20
23
  statusCode?: number;
21
24
  status?: number | string;
25
+ details?: {status?: number | string};
22
26
  };
23
- const trackingId = sourceObj?.details?.trackingId;
27
+
28
+ return obj?.statusCode ?? obj?.status ?? obj?.details?.status;
29
+ };
30
+
31
+ const markWxAppTelephonyError = (error: Error, source: unknown): WxAppTelephonyError => {
32
+ const marked = error as WxAppTelephonyError;
33
+ marked.isWxAppTelephonyError = true;
34
+ const trackingId = extractWxAppTrackingId(source);
24
35
  if (trackingId) {
25
36
  marked.trackingId = trackingId;
26
37
  }
27
- const status = sourceObj?.statusCode ?? sourceObj?.details?.status ?? sourceObj?.status;
38
+ const status = extractWxAppHttpStatus(source);
28
39
  if (status !== undefined) {
29
40
  marked.status = status;
30
41
  }
@@ -73,6 +84,10 @@ export default class AnswerCallOnWebexService {
73
84
  return `${hydraUrl.replace(/\/$/, '')}/telephony/calls`;
74
85
  }
75
86
 
87
+ private extractTrackingId(source: unknown): string | undefined {
88
+ return extractWxAppTrackingId(source);
89
+ }
90
+
76
91
  private async telephonyRequest(
77
92
  resource: string,
78
93
  method: HTTP_METHODS,
@@ -88,16 +103,29 @@ export default class AnswerCallOnWebexService {
88
103
  data: {uri, bodyKeys: Object.keys(body)},
89
104
  });
90
105
 
91
- return (await this.webex.request({
106
+ const response = (await this.webex.request({
92
107
  uri,
93
108
  method,
94
109
  body,
95
110
  addAuthHeader: true,
96
111
  })) as IHttpResponse;
112
+
113
+ LoggerProxy.info(`AnswerCallOnWebexService.${logMethod} success`, {
114
+ module: ANSWER_CALL_ON_WEBEX_FILE,
115
+ method: logMethod,
116
+ data: {statusCode: response.statusCode},
117
+ });
118
+
119
+ return response;
97
120
  } catch (error) {
121
+ const trackingId = this.extractTrackingId(error);
98
122
  LoggerProxy.error(`AnswerCallOnWebexService.${logMethod} failed: ${error}`, {
99
123
  module: ANSWER_CALL_ON_WEBEX_FILE,
100
124
  method: logMethod,
125
+ data: {
126
+ trackingId,
127
+ status: extractWxAppHttpStatus(error),
128
+ },
101
129
  });
102
130
  const {error: detailedError} = getErrorDetails(error, logMethod, CC_FILE);
103
131
  throw markWxAppTelephonyError(detailedError, error);
@@ -191,9 +219,14 @@ export default class AnswerCallOnWebexService {
191
219
  throw markWxAppTelephonyError(err, error);
192
220
  }
193
221
 
222
+ const trackingId = this.extractTrackingId(error);
194
223
  LoggerProxy.error(`AnswerCallOnWebexService.getCallDetails failed: ${error}`, {
195
224
  module: ANSWER_CALL_ON_WEBEX_FILE,
196
225
  method: METHODS.GET_CALL_DETAILS_ON_WEBEX,
226
+ data: {
227
+ trackingId,
228
+ status: extractWxAppHttpStatus(error),
229
+ },
197
230
  });
198
231
  const {error: detailedError} = getErrorDetails(
199
232
  error,
@@ -1,6 +1,8 @@
1
1
  import LoggerProxy from '../logger-proxy';
2
2
  import {METHODS} from '../constants';
3
3
  import {WebexSDK} from '../types';
4
+ import MetricsManager from '../metrics/MetricsManager';
5
+ import {METRIC_EVENT_NAMES} from '../metrics/constants';
4
6
 
5
7
  const WEBEX_CROSS_CLIENT_FILE = 'WebexCrossClientService';
6
8
  const DEFAULT_CROSS_CLIENT_STATE_TTL = 900;
@@ -23,6 +25,8 @@ export default class WebexCrossClientService {
23
25
  private appName = DEFAULT_APP_NAME;
24
26
  /** Incremented on teardown/disable to ignore stale refresh timer callbacks. */
25
27
  private refreshGeneration = 0;
28
+ /** Monotonic id for usersub publish metrics timers; stale publishes cancel only their own timer. */
29
+ private usersubPublishMetricsId = 0;
26
30
 
27
31
  constructor(webex: WebexSDK) {
28
32
  this.webex = webex;
@@ -144,21 +148,31 @@ export default class WebexCrossClientService {
144
148
 
145
149
  public async setManageWebexCallingInWxcc(
146
150
  enable: boolean,
147
- options?: {userId?: string; ttl?: number; appName?: string}
151
+ options?: {userId?: string; ttl?: number; appName?: string; trackPublishMetrics?: boolean}
148
152
  ): Promise<void> {
149
153
  const operationGeneration = this.refreshGeneration;
150
154
  const userId = options?.userId ?? this.webex.internal.device?.userId;
151
155
  const ttl = options?.ttl ?? DEFAULT_CROSS_CLIENT_STATE_TTL;
152
156
  const appName = options?.appName ?? DEFAULT_APP_NAME;
157
+ const trackPublishMetrics = options?.trackPublishMetrics ?? false;
153
158
  this.appName = appName;
154
159
 
155
160
  if (!userId) {
156
- throw new Error('User ID is unavailable for cross-client publish');
161
+ const error = 'User ID is unavailable for cross-client publish';
162
+ this.trackUsersubPreflightFailure(enable, 'user_id_unavailable', error, trackPublishMetrics);
163
+ throw new Error(error);
157
164
  }
158
165
 
159
166
  const deviceUrl = this.getDeviceUrl();
160
167
  if (!deviceUrl) {
161
- throw new Error('Device URL is unavailable for cross-client publish');
168
+ const error = 'Device URL is unavailable for cross-client publish';
169
+ this.trackUsersubPreflightFailure(
170
+ enable,
171
+ 'device_url_unavailable',
172
+ error,
173
+ trackPublishMetrics
174
+ );
175
+ throw new Error(error);
162
176
  }
163
177
 
164
178
  const composition = {
@@ -173,9 +187,47 @@ export default class WebexCrossClientService {
173
187
  ],
174
188
  };
175
189
 
176
- await this.publishCrossClientState([userId], ttl, composition, 'setManageWebexCallingInWxcc');
190
+ const metricsManager = MetricsManager.getInstance();
191
+ const usersubPublishEventKeys = [
192
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
193
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
194
+ ];
195
+
196
+ let publishMetricsId = 0;
197
+ if (trackPublishMetrics) {
198
+ this.usersubPublishMetricsId += 1;
199
+ publishMetricsId = this.usersubPublishMetricsId;
200
+ metricsManager.timeEvent(usersubPublishEventKeys);
201
+ }
202
+
203
+ try {
204
+ await this.publishCrossClientState([userId], ttl, composition, 'setManageWebexCallingInWxcc');
205
+ } catch (error) {
206
+ if (trackPublishMetrics) {
207
+ if (operationGeneration !== this.refreshGeneration) {
208
+ if (publishMetricsId === this.usersubPublishMetricsId) {
209
+ metricsManager.cancelTimedEvent(usersubPublishEventKeys);
210
+ }
211
+ } else {
212
+ metricsManager.trackEvent(
213
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
214
+ {
215
+ enableWxBetterTogether: enable,
216
+ error: error instanceof Error ? error.toString() : String(error),
217
+ },
218
+ ['operational', 'behavioral']
219
+ );
220
+ }
221
+ }
222
+
223
+ throw error;
224
+ }
177
225
 
178
226
  if (operationGeneration !== this.refreshGeneration) {
227
+ if (trackPublishMetrics && publishMetricsId === this.usersubPublishMetricsId) {
228
+ metricsManager.cancelTimedEvent(usersubPublishEventKeys);
229
+ }
230
+
179
231
  return;
180
232
  }
181
233
 
@@ -188,6 +240,14 @@ export default class WebexCrossClientService {
188
240
  this.clearRefreshTimer();
189
241
  }
190
242
 
243
+ if (trackPublishMetrics) {
244
+ metricsManager.trackEvent(
245
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
246
+ {enableWxBetterTogether: enable},
247
+ ['operational', 'behavioral']
248
+ );
249
+ }
250
+
191
251
  LoggerProxy.info(`Cross-client answer-calls-on-wxcc set to ${enable}`, {
192
252
  module: WEBEX_CROSS_CLIENT_FILE,
193
253
  method: METHODS.SET_MANAGE_WEBEX_CALLING_IN_WXCC,
@@ -199,6 +259,27 @@ export default class WebexCrossClientService {
199
259
  });
200
260
  }
201
261
 
262
+ private trackUsersubPreflightFailure(
263
+ enable: boolean,
264
+ skipReason: 'user_id_unavailable' | 'device_url_unavailable',
265
+ error: string,
266
+ trackPublishMetrics: boolean
267
+ ): void {
268
+ if (!trackPublishMetrics) {
269
+ return;
270
+ }
271
+
272
+ MetricsManager.getInstance().trackEvent(
273
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
274
+ {
275
+ enableWxBetterTogether: enable,
276
+ skipReason,
277
+ error,
278
+ },
279
+ ['operational', 'behavioral']
280
+ );
281
+ }
282
+
202
283
  public teardown(): void {
203
284
  this.refreshGeneration += 1;
204
285
  this.clearRefreshTimer();
@@ -1,6 +1,9 @@
1
1
  import LoggerProxy from '../logger-proxy';
2
2
  import {METHODS} from '../constants';
3
3
  import {WebexSDK} from '../types';
4
+ import MetricsManager from '../metrics/MetricsManager';
5
+ import {METRIC_EVENT_NAMES} from '../metrics/constants';
6
+ import {callIdSuffix, logWxAppMercuryMuteSync} from './wxAppDiagnosticLogging';
4
7
 
5
8
  const WXAPP_TELEPHONY_MERCURY_SYNC_FILE = 'WxAppTelephonyMercurySync';
6
9
 
@@ -42,6 +45,11 @@ export default class WxAppTelephonyMercurySync {
42
45
  private handleMuteStateEvent = (agentId: string, event: UpdatedCallMuteStateEvent): void => {
43
46
  const {actorId, callId, muted} = event?.data ?? {};
44
47
  if (!actorId || !callId || muted === undefined) {
48
+ logWxAppMercuryMuteSync({
49
+ phase: 'dropped',
50
+ dropReason: 'missing_mercury_event_fields',
51
+ });
52
+
45
53
  return;
46
54
  }
47
55
 
@@ -49,13 +57,20 @@ export default class WxAppTelephonyMercurySync {
49
57
  const decodedCallId = this.decodeBase64(callId);
50
58
 
51
59
  if (!decodedActorId.endsWith(agentId)) {
60
+ logWxAppMercuryMuteSync({
61
+ phase: 'dropped',
62
+ muted,
63
+ callIdSuffix: callIdSuffix(decodedCallId),
64
+ dropReason: 'actor_id_mismatch',
65
+ });
66
+
52
67
  return;
53
68
  }
54
69
 
55
- LoggerProxy.info('WxApp telephony mute state sync received', {
56
- module: WXAPP_TELEPHONY_MERCURY_SYNC_FILE,
57
- method: METHODS.SYNC_WXAPP_MUTE_FROM_MERCURY,
58
- data: {muted},
70
+ logWxAppMercuryMuteSync({
71
+ phase: 'received',
72
+ muted,
73
+ callIdSuffix: callIdSuffix(decodedCallId),
59
74
  });
60
75
 
61
76
  this.muteChangeHandler?.(decodedCallId, muted);
@@ -71,6 +86,12 @@ export default class WxAppTelephonyMercurySync {
71
86
  method: METHODS.SYNC_WXAPP_MUTE_FROM_MERCURY,
72
87
  });
73
88
 
89
+ MetricsManager.getInstance().trackEvent(
90
+ METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED,
91
+ {error: 'Mercury is unavailable for wxApp mute sync'},
92
+ ['operational', 'behavioral']
93
+ );
94
+
74
95
  return;
75
96
  }
76
97
 
@@ -87,6 +108,12 @@ export default class WxAppTelephonyMercurySync {
87
108
  mercury.on(TELEPHONY_CALL_MUTED, this.boundMuteHandler);
88
109
  mercury.on(TELEPHONY_CALL_UNMUTED, this.boundUnmuteHandler);
89
110
 
111
+ MetricsManager.getInstance().trackEvent(
112
+ METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_SUCCESS,
113
+ {},
114
+ ['operational', 'behavioral']
115
+ );
116
+
90
117
  LoggerProxy.info('Subscribed to wxApp telephony Mercury mute sync', {
91
118
  module: WXAPP_TELEPHONY_MERCURY_SYNC_FILE,
92
119
  method: METHODS.SYNC_WXAPP_MUTE_FROM_MERCURY,
@@ -87,6 +87,7 @@ Key nested WebSocket binds use the actual outer/data constants, for example `CC_
87
87
  | AGENT-R-003 | Buddy-agent lookup must preserve its typed request and correlated response contract. | Consult/transfer selection depends on backend-filtered availability. | `src/services/agent/index.ts` | `test/unit/spec/services/agent/index.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
88
88
  | AGENT-R-004 | The agent factory exposes `reload`, while ContactCenter alone decides when automated relogin is permitted. | Transport recovery lacks the profile/policy context required to mutate an agent session safely. | `src/cc.ts` | `test/unit/spec/cc.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
89
89
  | AGENT-R-005 | Device/profile update must be documented as `ContactCenter.updateAgentProfile`, not as an agent-factory method. | Calling a non-existent `routingAgent.deviceUpdate` would fail at runtime. | `src/cc.ts` | `test/unit/spec/cc.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
90
+ | AGENT-R-006 | BROWSER station login must send `dialNumber` and `deviceId` as `webrtc-{agentId}` on `POST /v1/agents/login`. EXTENSION and AGENT_DN continue to send the caller-supplied dial number for both fields. | QRM stores `agent.dn` from `dialNumber`. Legacy VPOP RTMS only treats a DN as WebRTC when it starts with `webrtc-`; a bare UUID gets a steering prefix and the agent invite fails with SIP 484. | `src/cc.ts` | `test/unit/spec/cc.ts` | Public `stationLogin({loginOption: 'BROWSER'})` input is unchanged; only the backend login payload is prefixed. | PRESENT |
90
91
 
91
92
  ## Design Overview
92
93
  `routingAgent` is a pure AQM factory. It receives an initialized AqmReqs instance and returns five request functions: `reload`, `logout`, `stationLogin`, `stateChange`, and `buddyAgents`. Each request config declares the endpoint/payload plus exact `CC_EVENTS` notification binds.
@@ -619,6 +619,8 @@ This keeps transcript and suggestion delivery aligned on the same per-task event
619
619
 
620
620
  **wxApp offer UI (`uiControlsComputer`):** `wxAppAcceptInFlight` disables accept/decline during the accept REST call. `wxAppAnswerPending` additionally disables accept and decline for **inbound** offers until ASSIGN; wxApp **outdial** keeps decline enabled during the post-accept "Calling…" phase so `cancelTask` remains available.
621
621
 
622
+ **wxApp decline observability:** Inbound wxApp offers decline via telephony `rejectCall` (`runWxAppReject` → `WXAPP_TASK_DECLINE_*`). wxApp outdial cancellations use CC routing `cancelTask` (`runWxAppOutdialDecline` → additive `WXAPP_TASK_DECLINE_*` plus existing `TASK_DECLINE_*`).
623
+
622
624
  **wxApp mute backfill guard (`Voice.syncWxAppMuteFromCallDetails`):** Skips telephony `GET /calls/{callId}` when the interaction is terminated or the task is a pre-accept wxApp OFFERED offer (`wxAppAnswerPending` false). Post-accept OFFERED and engaged CONNECTED sync still run. `isWxAppEngagedForControls` and `getWebexCallingCallId` exclude `TERMINATED` and `COMPLETED`. Expected 400 / "Call not found" / `101002` responses are not logged as errors.
623
625
 
624
626
  For BROWSER login, TaskManager integrates with WebCalling:
@@ -41,12 +41,20 @@ import {
41
41
  isWebexAppInboundCallingOffer as wxIsWebexAppInboundCallingOffer,
42
42
  mapWxAppVoiceError,
43
43
  runWxAppAccept,
44
+ runWxAppOutdialDecline,
44
45
  runWxAppReject,
45
46
  runWxAppToggleMute,
46
47
  runWxAppTransmitDtmf,
47
48
  WxAppVoiceDependencies,
48
49
  WxAppVoiceLifecycle,
49
50
  } from './wxAppVoiceMethods';
51
+ import {
52
+ deriveWxAppAcceptReason,
53
+ logWxAppMercuryMuteSync,
54
+ logWxAppOfferDecision,
55
+ callIdSuffix,
56
+ WxAppAcceptReason,
57
+ } from '../../wxAppDiagnosticLogging';
50
58
 
51
59
  export default class Voice extends Task implements IVoice {
52
60
  private static readonly WXAPP_MUTE_SYNC_RETRY_DELAY_MS = 50;
@@ -58,7 +66,9 @@ export default class Voice extends Task implements IVoice {
58
66
  private wxAppAnswerPending = false;
59
67
  private wxAppAcceptInFlight = false;
60
68
  private wxAppMuteSyncInFlight?: Promise<boolean | undefined>;
61
- private wxAppMuteToggleInFlight?: Promise<void>;
69
+ private wxAppMuteToggleTail: Promise<void> = Promise.resolve();
70
+ private wxAppDtmfTail: Promise<void> = Promise.resolve();
71
+ private lastLoggedWxAppAcceptReason?: WxAppAcceptReason;
62
72
 
63
73
  constructor(
64
74
  contact: ReturnType<typeof routingContact>,
@@ -95,6 +105,7 @@ export default class Voice extends Task implements IVoice {
95
105
  enableWxBetterTogether: this.enableWxBetterTogether,
96
106
  answerCallOnWebexService: this.answerCallOnWebexService,
97
107
  agentId: this.agentId,
108
+ metricsManager: this.metricsManager,
98
109
  getTaskData: () => this.data,
99
110
  getTaskState: () => this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined,
100
111
  getWxAppMuted: () => this.wxAppMuted,
@@ -135,17 +146,98 @@ export default class Voice extends Task implements IVoice {
135
146
  this.updateUiControls(true);
136
147
  }
137
148
 
149
+ protected updateUiControls(forceEmit = false): void {
150
+ super.updateUiControls(forceEmit);
151
+ this.logWxAppOfferDecisionIfNeeded();
152
+ }
153
+
154
+ private logWxAppOfferDecisionIfNeeded(): void {
155
+ if (!this.enableWxBetterTogether) {
156
+ return;
157
+ }
158
+
159
+ const state = this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined;
160
+ if (state !== TaskState.OFFERED) {
161
+ return;
162
+ }
163
+
164
+ const accept = this.currentUiControls?.main?.accept;
165
+ if (!accept?.isVisible) {
166
+ return;
167
+ }
168
+
169
+ const deps = this.getWxAppVoiceDependencies();
170
+ const isOutdial = this.data?.interaction?.outboundType === 'OUTDIAL';
171
+ const isWxAppInboundOffer = this.isWebexAppInboundCallingOffer();
172
+ const isWxAppOutdialOffer = this.isWebexAppCallingOffer() && isOutdial;
173
+ const deviceDetails = getCallingDeviceDetails(deps);
174
+ const acceptReason = deriveWxAppAcceptReason({
175
+ isWxAppInboundOffer,
176
+ isWxAppOutdialOffer,
177
+ isWebrtc: this.uiControlConfig.voiceVariant === VOICE_VARIANT.WEBRTC,
178
+ isOutdial,
179
+ wxAppAcceptInFlight: this.wxAppAcceptInFlight,
180
+ wxAppAnswerPending: this.wxAppAnswerPending,
181
+ enableWxBetterTogether: this.enableWxBetterTogether,
182
+ hasDeviceCallId: Boolean(deviceDetails?.deviceCallId),
183
+ });
184
+
185
+ if (acceptReason === this.lastLoggedWxAppAcceptReason) {
186
+ return;
187
+ }
188
+
189
+ this.lastLoggedWxAppAcceptReason = acceptReason;
190
+
191
+ logWxAppOfferDecision({
192
+ interactionId: this.data.interactionId,
193
+ acceptVisible: accept.isVisible,
194
+ acceptEnabled: accept.isEnabled,
195
+ acceptReason,
196
+ wxAppParticipantDeviceType: deviceDetails?.deviceType,
197
+ hasDeviceCallId: Boolean(deviceDetails?.deviceCallId),
198
+ });
199
+ }
200
+
138
201
  public applyWxAppMuteStateFromSync(incomingCallId: string, muted: boolean): void {
139
202
  if (!this.enableWxBetterTogether) {
140
203
  return;
141
204
  }
142
205
 
143
206
  const activeCallId = this.getWebexCallingCallId();
144
- if (!activeCallId || !incomingCallId.endsWith(activeCallId) || this.wxAppMuted === muted) {
207
+ const incomingSuffix = callIdSuffix(incomingCallId);
208
+ const activeSuffix = callIdSuffix(activeCallId);
209
+
210
+ if (!activeCallId || !incomingCallId.endsWith(activeCallId)) {
211
+ logWxAppMercuryMuteSync({
212
+ phase: 'dropped',
213
+ muted,
214
+ callIdSuffix: incomingSuffix,
215
+ interactionId: this.data.interactionId,
216
+ dropReason: !activeCallId ? 'no_active_wxApp_call' : 'call_id_mismatch',
217
+ });
218
+
219
+ return;
220
+ }
221
+
222
+ if (this.wxAppMuted === muted) {
223
+ logWxAppMercuryMuteSync({
224
+ phase: 'dropped',
225
+ muted,
226
+ callIdSuffix: incomingSuffix,
227
+ interactionId: this.data.interactionId,
228
+ dropReason: 'mute_state_unchanged',
229
+ });
230
+
145
231
  return;
146
232
  }
147
233
 
148
234
  this.wxAppMuted = muted;
235
+ logWxAppMercuryMuteSync({
236
+ phase: 'applied',
237
+ muted,
238
+ callIdSuffix: activeSuffix,
239
+ interactionId: this.data.interactionId,
240
+ });
149
241
  this.emit(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted});
150
242
  }
151
243
 
@@ -354,7 +446,9 @@ export default class Voice extends Task implements IVoice {
354
446
  ]);
355
447
 
356
448
  this.setWxAppAnswerPending(false);
357
- const response = await this.contact.cancelTask({interactionId});
449
+ const response = await runWxAppOutdialDecline(this.getWxAppVoiceDependencies(), () =>
450
+ this.contact.cancelTask({interactionId})
451
+ );
358
452
 
359
453
  this.metricsManager.trackEvent(
360
454
  METRIC_EVENT_NAMES.TASK_DECLINE_SUCCESS,
@@ -388,25 +482,13 @@ export default class Voice extends Task implements IVoice {
388
482
  */
389
483
  public async toggleMute(options?: TaskToggleMuteOptions): Promise<void> {
390
484
  if (this.enableWxBetterTogether && this.getWebexCallingCallId()) {
391
- if (this.wxAppMuteToggleInFlight) {
392
- await this.wxAppMuteToggleInFlight.catch(() => undefined);
393
- }
394
-
395
- const togglePromise = runWxAppToggleMute(
396
- this.getWxAppVoiceDependencies(),
397
- this.createWxAppLifecycle(),
398
- options
399
- );
400
-
401
- this.wxAppMuteToggleInFlight = togglePromise;
402
- togglePromise
403
- .finally(() => {
404
- if (this.wxAppMuteToggleInFlight === togglePromise) {
405
- this.wxAppMuteToggleInFlight = undefined;
406
- }
407
- })
408
- .catch(() => undefined);
485
+ const togglePromise = this.wxAppMuteToggleTail
486
+ .catch(() => undefined)
487
+ .then(() =>
488
+ runWxAppToggleMute(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle(), options)
489
+ );
409
490
 
491
+ this.wxAppMuteToggleTail = togglePromise.catch(() => undefined);
410
492
  await togglePromise;
411
493
 
412
494
  return;
@@ -420,11 +502,18 @@ export default class Voice extends Task implements IVoice {
420
502
  */
421
503
  public async transmitDtmf(options: TaskTransmitDtmfOptions): Promise<void> {
422
504
  if (this.enableWxBetterTogether && this.getWebexCallingCallId()) {
423
- await runWxAppTransmitDtmf(
424
- this.getWxAppVoiceDependencies(),
425
- this.createWxAppLifecycle(),
426
- options
427
- );
505
+ const dtmfPromise = this.wxAppDtmfTail
506
+ .catch(() => undefined)
507
+ .then(() =>
508
+ runWxAppTransmitDtmf(
509
+ this.getWxAppVoiceDependencies(),
510
+ this.createWxAppLifecycle(),
511
+ options
512
+ )
513
+ );
514
+
515
+ this.wxAppDtmfTail = dtmfPromise.catch(() => undefined);
516
+ await dtmfPromise;
428
517
 
429
518
  return;
430
519
  }