@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.
- package/.sdd/manifest.json +3 -2
- package/ai-docs/CONTRACTS.md +3 -2
- package/dist/cc.js +108 -6
- package/dist/cc.js.map +1 -1
- package/dist/metrics/MetricsManager.js +16 -0
- package/dist/metrics/MetricsManager.js.map +1 -1
- package/dist/metrics/behavioral-events.js +91 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +16 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +34 -6
- package/dist/services/AnswerCallOnWebexService.js.map +1 -1
- package/dist/services/WebexCrossClientService.js +54 -3
- package/dist/services/WebexCrossClientService.js.map +1 -1
- package/dist/services/WxAppTelephonyMercurySync.js +21 -6
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +0 -17
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +7 -37
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/voice/Voice.js +81 -14
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/wxAppVoiceMethods.js +154 -1
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
- package/dist/services/wxAppDiagnosticLogging.js +122 -0
- package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
- package/dist/types/cc.d.ts +1 -0
- package/dist/types/metrics/MetricsManager.d.ts +6 -0
- package/dist/types/metrics/constants.d.ts +15 -0
- package/dist/types/services/AnswerCallOnWebexService.d.ts +1 -0
- package/dist/types/services/WebexCrossClientService.d.ts +4 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +0 -42
- package/dist/types/services/task/state-machine/guards.d.ts +4 -4
- package/dist/types/services/task/voice/Voice.d.ts +5 -1
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +3 -0
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
- package/dist/webex.js +1 -1
- package/package.json +8 -8
- package/src/cc.ts +155 -7
- package/src/metrics/MetricsManager.ts +16 -0
- package/src/metrics/ai-docs/metrics-spec.md +71 -9
- package/src/metrics/behavioral-events.ts +92 -0
- package/src/metrics/constants.ts +18 -0
- package/src/services/AnswerCallOnWebexService.ts +41 -8
- package/src/services/WebexCrossClientService.ts +85 -4
- package/src/services/WxAppTelephonyMercurySync.ts +31 -4
- package/src/services/agent/ai-docs/agent-spec.md +1 -0
- package/src/services/task/ai-docs/task-spec.md +17 -16
- package/src/services/task/state-machine/TaskStateMachine.ts +0 -20
- package/src/services/task/state-machine/ai-docs/AGENTS.md +2 -4
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +8 -9
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +5 -7
- package/src/services/task/state-machine/guards.ts +7 -63
- package/src/services/task/voice/Voice.ts +115 -26
- package/src/services/task/voice/wxAppVoiceMethods.ts +211 -2
- package/src/services/wxAppDiagnosticLogging.ts +174 -0
- package/test/unit/spec/cc.ts +251 -22
- package/test/unit/spec/metrics/MetricsManager.ts +25 -0
- package/test/unit/spec/metrics/behavioral-events.ts +42 -0
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +41 -0
- package/test/unit/spec/services/WebexCrossClientService.ts +207 -1
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +23 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +72 -46
- package/test/unit/spec/services/task/state-machine/guards.ts +6 -6
- package/test/unit/spec/services/task/voice/Voice.ts +380 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +145 -1
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
- package/umd/contact-center.min.js +2 -2
- 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]: {
|
package/src/metrics/constants.ts
CHANGED
|
@@ -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
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
906
|
-
|
|
907
|
-
`
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
`
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
//
|
|
278
|
-
//
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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`
|
|
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` |
|
|
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 |
|