@webex/contact-center 3.12.0-llmrefactor.7 → 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 (69) hide show
  1. package/.sdd/manifest.json +3 -2
  2. package/ai-docs/CONTRACTS.md +3 -2
  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/state-machine/TaskStateMachine.js +0 -17
  18. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  19. package/dist/services/task/state-machine/guards.js +7 -37
  20. package/dist/services/task/state-machine/guards.js.map +1 -1
  21. package/dist/services/task/voice/Voice.js +81 -14
  22. package/dist/services/task/voice/Voice.js.map +1 -1
  23. package/dist/services/task/voice/wxAppVoiceMethods.js +154 -1
  24. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  25. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  26. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  27. package/dist/types/cc.d.ts +1 -0
  28. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  29. package/dist/types/metrics/constants.d.ts +15 -0
  30. package/dist/types/services/AnswerCallOnWebexService.d.ts +1 -0
  31. package/dist/types/services/WebexCrossClientService.d.ts +4 -0
  32. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +0 -42
  33. package/dist/types/services/task/state-machine/guards.d.ts +4 -4
  34. package/dist/types/services/task/voice/Voice.d.ts +5 -1
  35. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +3 -0
  36. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  37. package/dist/webex.js +1 -1
  38. package/package.json +8 -8
  39. package/src/cc.ts +155 -7
  40. package/src/metrics/MetricsManager.ts +16 -0
  41. package/src/metrics/ai-docs/metrics-spec.md +71 -9
  42. package/src/metrics/behavioral-events.ts +92 -0
  43. package/src/metrics/constants.ts +18 -0
  44. package/src/services/AnswerCallOnWebexService.ts +41 -8
  45. package/src/services/WebexCrossClientService.ts +85 -4
  46. package/src/services/WxAppTelephonyMercurySync.ts +31 -4
  47. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  48. package/src/services/task/ai-docs/task-spec.md +17 -16
  49. package/src/services/task/state-machine/TaskStateMachine.ts +0 -20
  50. package/src/services/task/state-machine/ai-docs/AGENTS.md +2 -4
  51. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +8 -9
  52. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +5 -7
  53. package/src/services/task/state-machine/guards.ts +7 -63
  54. package/src/services/task/voice/Voice.ts +115 -26
  55. package/src/services/task/voice/wxAppVoiceMethods.ts +211 -2
  56. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  57. package/test/unit/spec/cc.ts +251 -22
  58. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  59. package/test/unit/spec/metrics/behavioral-events.ts +42 -0
  60. package/test/unit/spec/services/AnswerCallOnWebexService.ts +41 -0
  61. package/test/unit/spec/services/WebexCrossClientService.ts +207 -1
  62. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +23 -0
  63. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +72 -46
  64. package/test/unit/spec/services/task/state-machine/guards.ts +6 -6
  65. package/test/unit/spec/services/task/voice/Voice.ts +380 -0
  66. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +145 -1
  67. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  68. package/umd/contact-center.min.js +2 -2
  69. package/umd/contact-center.min.js.map +1 -1
@@ -313,6 +313,98 @@ const eventTaxonomyMap: Record<string, BehavioralEventTaxonomy> = {
313
313
  target: 'task_outdial',
314
314
  verb: 'fail',
315
315
  },
316
+ [METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_SUCCESS]: {
317
+ product,
318
+ agent: 'user',
319
+ target: 'task_mute_webex_together',
320
+ verb: 'complete',
321
+ },
322
+ [METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_FAILED]: {
323
+ product,
324
+ agent: 'user',
325
+ target: 'task_mute_webex_together',
326
+ verb: 'fail',
327
+ },
328
+ [METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_SUCCESS]: {
329
+ product,
330
+ agent: 'user',
331
+ target: 'task_dtmf_webex_together',
332
+ verb: 'complete',
333
+ },
334
+ [METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_FAILED]: {
335
+ product,
336
+ agent: 'user',
337
+ target: 'task_dtmf_webex_together',
338
+ verb: 'fail',
339
+ },
340
+
341
+ // Webex Together (wxApp)
342
+ [METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS]: {
343
+ product,
344
+ agent: 'user',
345
+ target: 'task_accept_webex_together',
346
+ verb: 'complete',
347
+ },
348
+ [METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_FAILED]: {
349
+ product,
350
+ agent: 'user',
351
+ target: 'task_accept_webex_together',
352
+ verb: 'fail',
353
+ },
354
+ [METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS]: {
355
+ product,
356
+ agent: 'user',
357
+ target: 'task_reject_webex_together',
358
+ verb: 'complete',
359
+ },
360
+ [METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED]: {
361
+ product,
362
+ agent: 'user',
363
+ target: 'task_reject_webex_together',
364
+ verb: 'fail',
365
+ },
366
+ [METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS]: {
367
+ product,
368
+ agent: 'user',
369
+ target: 'webex_together_session_init',
370
+ verb: 'complete',
371
+ },
372
+ [METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED]: {
373
+ product,
374
+ agent: 'user',
375
+ target: 'webex_together_session_init',
376
+ verb: 'fail',
377
+ },
378
+ [METRIC_EVENT_NAMES.WXAPP_SESSION_SKIPPED]: {
379
+ product,
380
+ agent: 'user',
381
+ target: 'webex_together_session_init',
382
+ verb: 'ignore',
383
+ },
384
+ [METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS]: {
385
+ product,
386
+ agent: 'user',
387
+ target: 'webex_together_usersub_publish',
388
+ verb: 'complete',
389
+ },
390
+ [METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED]: {
391
+ product,
392
+ agent: 'user',
393
+ target: 'webex_together_usersub_publish',
394
+ verb: 'fail',
395
+ },
396
+ [METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_SUCCESS]: {
397
+ product,
398
+ agent: 'user',
399
+ target: 'webex_together_mercury_subscribe',
400
+ verb: 'complete',
401
+ },
402
+ [METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED]: {
403
+ product,
404
+ agent: 'user',
405
+ target: 'webex_together_mercury_subscribe',
406
+ verb: 'fail',
407
+ },
316
408
 
317
409
  // Conference Tasks
318
410
  [METRIC_EVENT_NAMES.TASK_CONFERENCE_START_SUCCESS]: {
@@ -139,6 +139,24 @@ export const METRIC_EVENT_NAMES = {
139
139
  TASK_OUTDIAL_SUCCESS: 'Task Outdial Success',
140
140
  TASK_OUTDIAL_FAILED: 'Task Outdial Failed',
141
141
 
142
+ WXAPP_TASK_MUTE_SUCCESS: 'WxApp Task Mute Success',
143
+ WXAPP_TASK_MUTE_FAILED: 'WxApp Task Mute Failed',
144
+ WXAPP_TASK_DTMF_SUCCESS: 'WxApp Task Dtmf Success',
145
+ WXAPP_TASK_DTMF_FAILED: 'WxApp Task Dtmf Failed',
146
+
147
+ // WxApp Better Together
148
+ WXAPP_TASK_ACCEPT_SUCCESS: 'WxApp Task Accept Success',
149
+ WXAPP_TASK_ACCEPT_FAILED: 'WxApp Task Accept Failed',
150
+ WXAPP_TASK_DECLINE_SUCCESS: 'WxApp Task Decline Success',
151
+ WXAPP_TASK_DECLINE_FAILED: 'WxApp Task Decline Failed',
152
+ WXAPP_SESSION_INIT_SUCCESS: 'WxApp Session Init Success',
153
+ WXAPP_SESSION_INIT_FAILED: 'WxApp Session Init Failed',
154
+ WXAPP_SESSION_SKIPPED: 'WxApp Session Skipped',
155
+ WXAPP_USERSUB_PUBLISH_SUCCESS: 'WxApp Usersub Publish Success',
156
+ WXAPP_USERSUB_PUBLISH_FAILED: 'WxApp Usersub Publish Failed',
157
+ WXAPP_MERCURY_SUBSCRIBE_SUCCESS: 'WxApp Mercury Subscribe Success',
158
+ WXAPP_MERCURY_SUBSCRIBE_FAILED: 'WxApp Mercury Subscribe Failed',
159
+
142
160
  UPLOAD_LOGS_SUCCESS: 'Upload Logs Success',
143
161
  UPLOAD_LOGS_FAILED: 'Upload Logs Failed',
144
162
  WEBSOCKET_DEREGISTER_SUCCESS: 'Websocket Deregister Success',
@@ -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:
@@ -902,22 +904,21 @@ event so consumers can rebind to the surviving task object.
902
904
 
903
905
  Dropping an Agent while that Agent is consulting continues through the existing
904
906
  `PARTICIPANT_LEAVE` transition, which clears consult state and emits the normal
905
- participant-left and task-end lifecycle. This self-departure check applies in
906
- every active call-control state and uses an explicit self participant ID,
907
- `hasLeft`, or removal of a previously active Agent from the participant map. A
908
- participant-left event naming somebody else never infers self-departure from a
909
- partial media roster. A consulted Agent receiving
910
- `AgentConsultEnded` continues through `CONSULT_END`; current-agent departure is
911
- evaluated before consult-initiator recovery. For the from-conference nested-consult
912
- ordering race only, the guard also compares `mainCall` membership (located by
913
- `mType`) when the Agent remains active in the participant map and is still present
914
- on the consult leg. Missing, contradictory, or ordinary CONNECTED/HELD media snapshots
915
- are non-terminal. Accepted consultees emit
916
- `task:consultEnd` and `task:end`, while an unaccepted OFFERED consultee retains
917
- the existing consult-end-only signal. A surviving consult initiator still
918
- returns to the main-call state selected by the existing guards. Starting a
919
- consult preserves the prior task snapshot so this membership comparison remains
920
- available while the consult is initiating.
907
+ participant-left and task-end lifecycle. `PARTICIPANT_LEAVE` is handled in
908
+ `HELD`, `RESUME_INITIATING`, `CONSULTING`, `CONSULT_INITIATING`, and
909
+ `CONFERENCING`; `CONNECTED`, `HOLD_INITIATING`, and `CONF_INITIATING` ignore the
910
+ event. The current Agent is treated as departed only when the event names that
911
+ Agent or the updated participants map omits that Agent (EP-DN removal). Remaining
912
+ in the map with `hasLeft`, or disappearing only from `mainCall` media, is
913
+ non-terminal. A consulted Agent receiving `AgentConsultEnded` continues through
914
+ `CONSULT_END`; current-agent departure is evaluated before consult-initiator
915
+ recovery and does not use mainCall membership as self-departure evidence.
916
+ Accepted consultees emit `task:consultEnd` and `task:end`, while an unaccepted
917
+ OFFERED consultee retains the existing consult-end-only signal. A surviving
918
+ consult initiator still returns to the main-call state selected by the existing
919
+ guards. The state-machine transition table lives in
920
+ `src/services/task/state-machine/ai-docs/task-state-machine-spec.md`
921
+ `TASK_STATE_MACHINE-R-008`.
921
922
 
922
923
  Primary-Agent promotion remains backend-authoritative and follows the two-event
923
924
  desktop contract. `ContactOwnerChanged` updates the promoted Agent; TaskManager
@@ -416,14 +416,6 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
416
416
  actions: ['updateTaskData', 'clearConsultState', 'emitTaskConsultEnd'],
417
417
  },
418
418
  ],
419
- // Routing lifecycle events can arrive out of order in child-task/EP-DN flows. The
420
- // local actor may still be CONNECTED when ParticipantLeftConference reports that this
421
- // agent left the main interaction. Handle that self-departure race; for another
422
- // participant, the action-only fallback updates data while preserving CONNECTED.
423
- [TaskEvent.PARTICIPANT_LEAVE]: [
424
- ...currentAgentParticipantLeaveTransitions(),
425
- {actions: ['updateTaskData', 'handleParticipantLeft', 'emitTaskParticipantLeft']},
426
- ],
427
419
  // AgentContactEnded Event
428
420
  [TaskEvent.CONTACT_ENDED]: [
429
421
  {
@@ -486,13 +478,6 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
486
478
  target: TaskState.CONNECTED,
487
479
  actions: ['updateTaskData'],
488
480
  },
489
- // Another agent can remove this agent while the local hold request is awaiting its
490
- // routing acknowledgement. Process that backend lifecycle event independently of
491
- // whether Drop was initiated from this task; another-participant event preserves state.
492
- [TaskEvent.PARTICIPANT_LEAVE]: [
493
- ...currentAgentParticipantLeaveTransitions(),
494
- {actions: ['updateTaskData', 'handleParticipantLeft', 'emitTaskParticipantLeft']},
495
- ],
496
481
  [TaskEvent.OUTBOUND_FAILED]: [
497
482
  {
498
483
  guard: guards.shouldWrapUp,
@@ -626,7 +611,6 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
626
611
  [TaskEvent.UNHOLD_FAILED]: {
627
612
  target: TaskState.HELD,
628
613
  },
629
- // The same remote-removal race can occur while an unhold request is in flight.
630
614
  [TaskEvent.PARTICIPANT_LEAVE]: [
631
615
  ...currentAgentParticipantLeaveTransitions(),
632
616
  {actions: ['updateTaskData', 'handleParticipantLeft', 'emitTaskParticipantLeft']},
@@ -1020,10 +1004,6 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
1020
1004
  actions: ['updateTaskData', 'clearConsultState'],
1021
1005
  },
1022
1006
  ],
1023
- [TaskEvent.PARTICIPANT_LEAVE]: [
1024
- ...currentAgentParticipantLeaveTransitions(),
1025
- {actions: ['updateTaskData', 'handleParticipantLeft', 'emitTaskParticipantLeft']},
1026
- ],
1027
1007
  [TaskEvent.OUTBOUND_FAILED]: [
1028
1008
  {
1029
1009
  guard: guards.shouldWrapUp,
@@ -274,10 +274,8 @@ didCurrentAgentLeaveMainInteraction(context, event) {
274
274
  const participantIdFromEvent = 'participantId' in event ? event.participantId : undefined;
275
275
  const participantId = participantIdFromEvent ?? event.taskData?.participantId;
276
276
  if (Boolean(participantId) && participantId === selfAgentId) return true;
277
- // Explicit hasLeft or removal of a previously active self from the participant map is terminal.
278
- // PARTICIPANT_LEAVE naming another participant does not infer self departure from media.
279
- // Only a from-conference CONSULT_END may compare mainCall membership, and only when self
280
- // remains active in the participant map and on the consult leg. Partial ordinary calls are false.
277
+ // Self missing from the updated participants map is terminal (EP-DN removal).
278
+ // Remaining in the map with hasLeft, or disappearing only from mainCall media, is not.
281
279
  }
282
280
  ```
283
281
 
@@ -476,13 +476,12 @@ It is instantiated by `Task` and receives mapped backend/user events through `se
476
476
  - Guard: default
477
477
  - Actions: `updateTaskData`, `handleParticipantLeft`, `emitTaskParticipantLeft`
478
478
 
479
- `didCurrentAgentLeaveMainInteraction` treats an explicit self participant ID,
480
- self `hasLeft`, or removal from a previously active participant map as terminal.
481
- A `PARTICIPANT_LEAVE` event naming another participant does not infer departure
482
- from media membership. The previous-versus-updated `mainCall` comparison is
483
- restricted to from-conference `CONSULT_END` nested-consult cleanup when self
484
- remains active in the participant map and on the consult leg; partial ordinary CONNECTED/HELD
485
- snapshots remain non-terminal.
479
+ `didCurrentAgentLeaveMainInteraction` treats an explicit self participant ID
480
+ or absence from the updated participants map as terminal (EP-DN removal).
481
+ Remaining in the map with `hasLeft`, or disappearing only from `mainCall` media,
482
+ is not treated as departure. `PARTICIPANT_LEAVE` is handled in `HELD`,
483
+ `RESUME_INITIATING`, `CONSULTING`, `CONSULT_INITIATING`, and `CONFERENCING`;
484
+ `CONNECTED`, `HOLD_INITIATING`, and `CONF_INITIATING` ignore the event.
486
485
 
487
486
  - `CONFERENCE_END` -> `WRAPPING_UP`
488
487
  - Guard: `guards.shouldWrapUp`
@@ -758,7 +757,7 @@ Complete mapping from backend CC_EVENTS to internal TaskEvent types.
758
757
  | `PARTICIPANT_JOINED_CONFERENCE` | `CONFERENCE_START` | `CONSULTING` / `CONF_INITIATING` / `CONFERENCING` | `CONFERENCING` / same | Conference participant joined |
759
758
  | `AGENT_CONSULT_CONFERENCE_FAILED` | `CONFERENCE_FAILED` | `CONF_INITIATING` | `CONSULTING` | Merge fail fallback |
760
759
  | `AGENT_CONSULT_CONFERENCE_ENDED` | `CONFERENCE_END` | `CONFERENCING` | `WRAPPING_UP` / `CONNECTED` / `TERMINATED` | Guard-driven |
761
- | `PARTICIPANT_LEFT_CONFERENCE` | `PARTICIPANT_LEAVE` | `CONFERENCING` | `WRAPPING_UP` / `TERMINATED` / `CONNECTED` / same | Ownership + downgrade guards |
760
+ | `PARTICIPANT_LEFT_CONFERENCE` | `PARTICIPANT_LEAVE` | `HELD` / `RESUME_INITIATING` / `CONSULTING` / `CONSULT_INITIATING` / `CONFERENCING` | `WRAPPING_UP` / `TERMINATED` / `CONNECTED` / same | Named self or omitted from participant map; `CONNECTED` / `HOLD_INITIATING` / `CONF_INITIATING` ignore |
762
761
  | `AGENT_CONFERENCE_TRANSFERRED` | `TRANSFER_CONFERENCE_SUCCESS` | `CONSULTING` / `CONFERENCING` | `WRAPPING_UP` / `CONFERENCING` / `TERMINATED` / same | Initiator/receiver dependent |
763
762
 
764
763
  ### Explicitly not mapped to state machine
@@ -818,7 +817,7 @@ Complete mapping from backend CC_EVENTS to internal TaskEvent types.
818
817
  | API `task.consultConference()` | `MERGE_TO_CONFERENCE` | CONSULTING → CONF_INITIATING | Starts merge flow |
819
818
  | `AgentConsultConferenced` | `CONFERENCE_START` | CONSULTING/CONF_INITIATING → CONFERENCING | `handleConferenceStarted` path |
820
819
  | `ParticipantJoinedConference` | `CONFERENCE_START` | CONFERENCING → CONFERENCING | Refresh + emit conference started |
821
- | `ParticipantLeftConference` | `PARTICIPANT_LEAVE` | Any active call-control state → WRAPPING_UP / TERMINATED / CONNECTED / stay | Uses `didCurrentAgentLeaveMainInteraction`, `shouldWrapUp`, `shouldDowngradeConferenceToConnected` |
820
+ | `ParticipantLeftConference` | `PARTICIPANT_LEAVE` | HELD / RESUME_INITIATING / CONSULTING / CONSULT_INITIATING / CONFERENCING → WRAPPING_UP / TERMINATED / CONNECTED / stay; CONNECTED / HOLD_INITIATING / CONF_INITIATING ignore | Uses `didCurrentAgentLeaveMainInteraction` (named self or omitted from participant map), `shouldWrapUp`, `shouldDowngradeConferenceToConnected` |
822
821
  | `AgentConsultConferenceEnded` | `CONFERENCE_END` | CONFERENCING → WRAPPING_UP / CONNECTED / TERMINATED | Guard-based branch |
823
822
  | `AgentConsultConferenceFailed` | `CONFERENCE_FAILED` | CONF_INITIATING → CONSULTING | Merge failed fallback |
824
823
  | `AgentConferenceTransferred` | `TRANSFER_CONFERENCE_SUCCESS` | CONSULTING/CONFERENCING branch logic | Initiator/receiver dependent |