@webex/contact-center 3.12.0-llmrefactor.13 → 3.12.0-llmrefactor.15

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 (68) hide show
  1. package/.sdd/manifest.json +3 -2
  2. package/ai-docs/CONTRACTS.md +7 -1
  3. package/ai-docs/contact-center-spec.md +7 -7
  4. package/ai-docs/features/contact-owner-change-refactor.md +78 -0
  5. package/dist/cc.js +39 -7
  6. package/dist/cc.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +6 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +1 -0
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/AnswerCallOnWebexService.js +4 -4
  12. package/dist/services/AnswerCallOnWebexService.js.map +1 -1
  13. package/dist/services/WebexCrossClientService.js +5 -2
  14. package/dist/services/WebexCrossClientService.js.map +1 -1
  15. package/dist/services/WxAppTelephonyMercurySync.js +6 -3
  16. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
  17. package/dist/services/config/types.js.map +1 -1
  18. package/dist/services/task/TaskFactory.js +1 -0
  19. package/dist/services/task/TaskFactory.js.map +1 -1
  20. package/dist/services/task/TaskManager.js +50 -58
  21. package/dist/services/task/TaskManager.js.map +1 -1
  22. package/dist/services/task/types.js.map +1 -1
  23. package/dist/services/task/voice/Voice.js +63 -44
  24. package/dist/services/task/voice/Voice.js.map +1 -1
  25. package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
  26. package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
  27. package/dist/services/task/voice/wxAppVoiceMethods.js +52 -11
  28. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  29. package/dist/services/wxAppDiagnosticLogging.js +50 -13
  30. package/dist/services/wxAppDiagnosticLogging.js.map +1 -1
  31. package/dist/types/metrics/constants.d.ts +1 -0
  32. package/dist/types/services/config/types.d.ts +2 -0
  33. package/dist/types/services/task/types.d.ts +2 -0
  34. package/dist/types/services/task/voice/Voice.d.ts +15 -2
  35. package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
  36. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +19 -6
  37. package/dist/types/services/wxAppDiagnosticLogging.d.ts +12 -0
  38. package/dist/types.js.map +1 -1
  39. package/dist/webex.js +1 -1
  40. package/package.json +9 -9
  41. package/src/cc.ts +43 -7
  42. package/src/metrics/ai-docs/metrics-spec.md +44 -2
  43. package/src/metrics/behavioral-events.ts +6 -0
  44. package/src/metrics/constants.ts +1 -0
  45. package/src/services/AnswerCallOnWebexService.ts +4 -4
  46. package/src/services/WebexCrossClientService.ts +7 -2
  47. package/src/services/WxAppTelephonyMercurySync.ts +3 -3
  48. package/src/services/config/types.ts +2 -0
  49. package/src/services/task/TaskFactory.ts +1 -0
  50. package/src/services/task/TaskManager.ts +85 -87
  51. package/src/services/task/ai-docs/task-spec.md +7 -3
  52. package/src/services/task/types.ts +16 -0
  53. package/src/services/task/voice/Voice.ts +74 -52
  54. package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
  55. package/src/services/task/voice/wxAppVoiceMethods.ts +83 -24
  56. package/src/services/wxAppDiagnosticLogging.ts +135 -23
  57. package/src/types.ts +2 -0
  58. package/test/unit/spec/cc.ts +201 -19
  59. package/test/unit/spec/services/WebexCrossClientService.ts +45 -3
  60. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +1 -1
  61. package/test/unit/spec/services/task/TaskManager.ts +214 -3
  62. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
  63. package/test/unit/spec/services/task/voice/Voice.ts +10 -51
  64. package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
  65. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +121 -5
  66. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +44 -8
  67. package/umd/contact-center.min.js +2 -2
  68. package/umd/contact-center.min.js.map +1 -1
@@ -246,6 +246,7 @@ All event names are defined in `METRIC_EVENT_NAMES` (`constants.ts`). Events fol
246
246
  | `WXAPP_SESSION_SKIPPED` | `'WxApp Session Skipped'` | Webex Together session skipped (`wxcc_sdk.user.webex_together_session_init.ignore`) |
247
247
  | `WXAPP_USERSUB_PUBLISH_SUCCESS` / `FAILED` | `'WxApp Usersub Publish ...'` | Cross-client usersub publish (`wxcc_sdk.user.webex_together_usersub_publish.complete\|fail`). Preflight failures (missing `userId` or device URL before HTTP publish) emit `WXAPP_USERSUB_PUBLISH_FAILED` with `skipReason` (`user_id_unavailable` / `device_url_unavailable`) and no `timeEvent` timer. Stale-generation discards (teardown or concurrent disable while HTTP publish is in flight) call `cancelTimedEvent` without emitting success/fail only when the completing publish still owns the active timer (`usersubPublishMetricsId` guard — a newer overlapping publish must not have its timer cleared). |
248
248
  | `WXAPP_MERCURY_SUBSCRIBE_SUCCESS` / `FAILED` | `'WxApp Mercury Subscribe ...'` | Telephony Mercury mute-sync subscribe (`wxcc_sdk.user.webex_together_mercury_subscribe.complete\|fail`) |
249
+ | `WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING` | `'WxApp Offer Participant Fields Missing'` | Read-only observability when usersub is active but inbound OFFERED participant **still** lacks valid wxApp fields after a **500 ms grace period** (orchestrated in `voice/wxAppOfferObservability.ts`; avoids false alarms from transient WS ordering). After usersub publish, `TaskManager.refreshWxAppOfferObservabilityForAllTasks()` re-runs the observer so a pre-publish OFFERED task can still schedule grace. Task removal calls `disposeWxAppOfferObservability()` so a pending timer cannot emit after `CONTACT_MERGED` / collection drop. Emitted with matching `[CC wxApp] participant fields mismatch` WARN (`wxcc_sdk.user.webex_together_offer_participant_fields.fail`) |
249
250
  | `TASK_CONFERENCE_START_SUCCESS` / `FAILED` | `'Task Conference Start ...'` | Conference start result |
250
251
  | `TASK_CONFERENCE_END_SUCCESS` / `FAILED` | `'Task Conference End ...'` | Conference end result |
251
252
  | `TASK_CONFERENCE_TRANSFER_SUCCESS` / `FAILED` | `'Task Conference Transfer ...'` | Conference transfer result |
@@ -301,6 +302,7 @@ All event names are defined in `constants.ts` as `METRIC_EVENT_NAMES`. Events fo
301
302
  | WxApp Session Init | `WXAPP_SESSION_INIT_SUCCESS` | `WXAPP_SESSION_INIT_FAILED` |
302
303
  | WxApp Usersub Publish | `WXAPP_USERSUB_PUBLISH_SUCCESS` | `WXAPP_USERSUB_PUBLISH_FAILED` |
303
304
  | WxApp Mercury Subscribe | `WXAPP_MERCURY_SUBSCRIBE_SUCCESS` | `WXAPP_MERCURY_SUBSCRIBE_FAILED` |
305
+ | WxApp Offer Participant Fields | — | `WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING` |
304
306
  | Upload Logs | `UPLOAD_LOGS_SUCCESS` | `UPLOAD_LOGS_FAILED` |
305
307
  | WebSocket Deregister | `WEBSOCKET_DEREGISTER_SUCCESS` | `WEBSOCKET_DEREGISTER_FAIL` |
306
308
  | Device Type Update | `AGENT_DEVICE_TYPE_UPDATE_SUCCESS` | `AGENT_DEVICE_TYPE_UPDATE_FAILED` |
@@ -322,9 +324,48 @@ Webex Together events (`WXAPP_*` constants) use `*_webex_together` compound targ
322
324
  | Usersub publish | `webex_together_usersub_publish` | `wxcc_sdk.user.webex_together_usersub_publish.complete` | `wxcc_sdk.user.webex_together_usersub_publish.fail` | — |
323
325
  | Mercury subscribe | `webex_together_mercury_subscribe` | `wxcc_sdk.user.webex_together_mercury_subscribe.complete` | `wxcc_sdk.user.webex_together_mercury_subscribe.fail` | — |
324
326
  | Session init | `webex_together_session_init` | `wxcc_sdk.user.webex_together_session_init.complete` | `wxcc_sdk.user.webex_together_session_init.fail` | `wxcc_sdk.user.webex_together_session_init.ignore` |
327
+ | Offer participant fields | `webex_together_offer_participant_fields` | — | `wxcc_sdk.user.webex_together_offer_participant_fields.fail` | — |
325
328
 
326
329
  Operational and business wire names are unchanged (`WXCC_SDK_WXAPP_*`).
327
330
 
331
+ ### WxApp metric payload fields (operational / behavioral tags)
332
+
333
+ | Event | Payload fields (booleans/enums; no raw IDs) |
334
+ |---|---|
335
+ | `WXAPP_SESSION_INIT_SUCCESS` | `loginOption`, `enableWxBetterTogether`, `usersubPublished`, `mercurySubscribed`, `telephonyTaskType` |
336
+ | `WXAPP_SESSION_INIT_FAILED` | above + `skipReason` (`usersub_not_published`, `mercury_not_subscribed`, `mercury_subscribe_failed`, `publish_failed`) + optional `error`. `usersubPublished` reflects **retained session state** (`isAnswerCallsStateActive()` / `answerCallsState`), including on catch-path failures — not gated on the current init attempt's `publishedEnable`. |
337
+ | `WXAPP_USERSUB_PUBLISH_SUCCESS` | `enableWxBetterTogether`, `usersubPublished` |
338
+ | `WXAPP_USERSUB_PUBLISH_FAILED` | `enableWxBetterTogether`, `usersubPublished`, optional `skipReason`, `error`. `usersubPublished` reflects **retained session state** (`answerCallsState`), not the requested enable value — a failed disable/refresh while usersub remains active reports `true`. |
339
+ | `WXAPP_MERCURY_SUBSCRIBE_SUCCESS` | `mercurySubscribed: true` |
340
+ | `WXAPP_MERCURY_SUBSCRIBE_FAILED` | `mercurySubscribed: false`, `error` |
341
+ | `WXAPP_TASK_ACCEPT_*` / `DECLINE_*` | `taskId`, `hasDeviceCallId`, `hasDeviceId`, optional `acceptReason`; failures add `trackingId` when available. `acceptReason` is captured **before** the telephony REST call (not hard-coded `wxApp_offer_ready`) so success and failure both retain `wxApp_answer_pending` / `wxApp_accept_in_flight` when those flags were already set. Outdial-cancel AQM failures (`error.details`) include the same participant context fields via `getWxAppTelephonyMetricContext` merged with AQM correlation fields. Outdial cancel during post-accept pending phase may report `acceptReason: wxApp_answer_pending`. |
342
+ | `WXAPP_TASK_MUTE_*` / `DTMF_*` | `taskId`, `hasDeviceCallId`, `hasDeviceId`; failures add `trackingId` when available |
343
+ | `WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING` | `taskId`, `usersubPublished`, `hasDeviceCallId`, `hasDeviceId`, `acceptReason` |
344
+
345
+ ### Customer console log triage (MMT / support)
346
+
347
+ Customers often embed with `logger.level: 'log'`. wxApp diagnostics use **`LoggerProxy.log`** / **`warn`** / **`error`** with prefix **`[CC wxApp]`** and grep-friendly `key=value` suffixes on the message string (fields also appear in structured `data`).
348
+
349
+ | Search in browser console | Meaning |
350
+ |---|---|
351
+ | `[CC wxApp]` | All wxApp diagnostic lines (primary search) |
352
+ | `WXCC_SDK_WXAPP` | Operational metric wire names (name only in console; payloads go to backend) |
353
+ | `session readiness` | usersub + Mercury state after station login |
354
+ | `offer decision` | Why Accept is visible/enabled; includes `acceptReason`, `hasDeviceCallId`, `usersubPublished` |
355
+ | `participant fields mismatch` | usersub active but WS participant **still** missing wxApp triple after ~500 ms (transient gaps on first OFFERED tick are normal — look for `wxApp_offer_ready` within 1 s) |
356
+ | `telephony accept failed` | REST answer failure; includes `trackingId` when available |
357
+
358
+ Do **not** search `WXAPP` alone — it matches metric wire names but misses `[CC wxApp]` diagnostic lines.
359
+
360
+ Example lines at `logger.level: 'log'`:
361
+
362
+ ```text
363
+ PLUGIN_CC - [LOG]: ... [CC wxApp] session readiness usersubPublished=true mercurySubscribed=true loginOption=EXTENSION ...
364
+ PLUGIN_CC - [LOG]: ... [CC wxApp] offer decision acceptReason=wxApp_offer_ready hasDeviceCallId=true usersubPublished=true interactionId=...
365
+ PLUGIN_CC - [WARN]: ... [CC wxApp] participant fields mismatch usersubPublished=true hasDeviceCallId=false acceptReason=extension_non_wxApp_offer interactionId=...
366
+ PLUGIN_CC - [LOG]: ... operational-events -> @submitEvent. Submit event: WXCC_SDK_WXAPP_TASK_ACCEPT_SUCCESS
367
+ ```
368
+
328
369
  Special events (no success/failure pair):
329
370
 
330
371
  - `AGENT_RONA` — has behavioral taxonomy (`service.agent_rona.set`)
@@ -337,11 +378,11 @@ Special events (no success/failure pair):
337
378
 
338
379
  - `WXAPP_SESSION_SKIPPED` — has behavioral taxonomy (`wxcc_sdk.user.webex_together_session_init.ignore`)
339
380
 
340
- Of the 116 defined metric names, 96 have behavioral taxonomy and 20 do not. The Agent Wellness Break names are intentionally operational-only and therefore have no `eventTaxonomyMap` entries.
381
+ Of the 113 defined metric names, 97 have behavioral taxonomy and 16 do not. Events **without** an `eventTaxonomyMap` entry are the original eight `AI_ASSISTANT_*` names, the three Agent Wellness Break `AI_ASSISTANT_WELLNESS_*` names, the two `WELLBEING_BREAK_IDLE_CODE_FETCH_*` names, plus `WEBSOCKET_DEREGISTER_SUCCESS`, `WEBSOCKET_DEREGISTER_FAIL`, and `WEBSOCKET_EVENT_RECEIVED`. The Agent Wellness Break names are intentionally operational-only and therefore have no `eventTaxonomyMap` entries.
341
382
 
342
383
  ### Complete METRIC_EVENT_NAMES catalog
343
384
 
344
- This table contains all 116 names from `src/metrics/constants.ts`; taxonomy presence is checked against `src/metrics/behavioral-events.ts`: 96 mapped and 20 unmapped.
385
+ This table contains all 113 names from `src/metrics/constants.ts`; taxonomy presence is checked against `src/metrics/behavioral-events.ts`: 97 mapped and 16 unmapped.
345
386
 
346
387
  | Constant | Emitted name | Behavioral taxonomy? |
347
388
  |---|---|---|
@@ -415,6 +456,7 @@ This table contains all 116 names from `src/metrics/constants.ts`; taxonomy pres
415
456
  | `WXAPP_USERSUB_PUBLISH_FAILED` | `WxApp Usersub Publish Failed` | yes |
416
457
  | `WXAPP_MERCURY_SUBSCRIBE_SUCCESS` | `WxApp Mercury Subscribe Success` | yes |
417
458
  | `WXAPP_MERCURY_SUBSCRIBE_FAILED` | `WxApp Mercury Subscribe Failed` | yes |
459
+ | `WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING` | `WxApp Offer Participant Fields Missing` | yes |
418
460
  | `UPLOAD_LOGS_SUCCESS` | `Upload Logs Success` | yes |
419
461
  | `UPLOAD_LOGS_FAILED` | `Upload Logs Failed` | yes |
420
462
  | `WEBSOCKET_DEREGISTER_SUCCESS` | `Websocket Deregister Success` | no |
@@ -405,6 +405,12 @@ const eventTaxonomyMap: Record<string, BehavioralEventTaxonomy> = {
405
405
  target: 'webex_together_mercury_subscribe',
406
406
  verb: 'fail',
407
407
  },
408
+ [METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING]: {
409
+ product,
410
+ agent: 'user',
411
+ target: 'webex_together_offer_participant_fields',
412
+ verb: 'fail',
413
+ },
408
414
 
409
415
  // Conference Tasks
410
416
  [METRIC_EVENT_NAMES.TASK_CONFERENCE_START_SUCCESS]: {
@@ -156,6 +156,7 @@ export const METRIC_EVENT_NAMES = {
156
156
  WXAPP_USERSUB_PUBLISH_FAILED: 'WxApp Usersub Publish Failed',
157
157
  WXAPP_MERCURY_SUBSCRIBE_SUCCESS: 'WxApp Mercury Subscribe Success',
158
158
  WXAPP_MERCURY_SUBSCRIBE_FAILED: 'WxApp Mercury Subscribe Failed',
159
+ WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING: 'WxApp Offer Participant Fields Missing',
159
160
 
160
161
  UPLOAD_LOGS_SUCCESS: 'Upload Logs Success',
161
162
  UPLOAD_LOGS_FAILED: 'Upload Logs Failed',
@@ -97,7 +97,7 @@ export default class AnswerCallOnWebexService {
97
97
  try {
98
98
  const uri = `${this.getTelephonyBaseUrl()}${resource}`;
99
99
 
100
- LoggerProxy.info(`AnswerCallOnWebexService.${logMethod} request`, {
100
+ LoggerProxy.log(`AnswerCallOnWebexService.${logMethod} request`, {
101
101
  module: ANSWER_CALL_ON_WEBEX_FILE,
102
102
  method: logMethod,
103
103
  data: {uri, bodyKeys: Object.keys(body)},
@@ -110,7 +110,7 @@ export default class AnswerCallOnWebexService {
110
110
  addAuthHeader: true,
111
111
  })) as IHttpResponse;
112
112
 
113
- LoggerProxy.info(`AnswerCallOnWebexService.${logMethod} success`, {
113
+ LoggerProxy.log(`AnswerCallOnWebexService.${logMethod} success`, {
114
114
  module: ANSWER_CALL_ON_WEBEX_FILE,
115
115
  method: logMethod,
116
116
  data: {statusCode: response.statusCode},
@@ -197,10 +197,10 @@ export default class AnswerCallOnWebexService {
197
197
  const uri = `${this.getTelephonyBaseUrl()}/${callId}${lineOwnerIdQuery}`;
198
198
 
199
199
  try {
200
- LoggerProxy.info('AnswerCallOnWebexService.getCallDetails request', {
200
+ LoggerProxy.log('AnswerCallOnWebexService.getCallDetails request', {
201
201
  module: ANSWER_CALL_ON_WEBEX_FILE,
202
202
  method: METHODS.GET_CALL_DETAILS_ON_WEBEX,
203
- data: {callId},
203
+ data: {callIdSuffix: callId.length <= 8 ? callId : callId.slice(-8)},
204
204
  });
205
205
 
206
206
  const response = (await this.webex.request({
@@ -213,6 +213,7 @@ export default class WebexCrossClientService {
213
213
  METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
214
214
  {
215
215
  enableWxBetterTogether: enable,
216
+ usersubPublished: this.answerCallsState,
216
217
  error: error instanceof Error ? error.toString() : String(error),
217
218
  },
218
219
  ['operational', 'behavioral']
@@ -243,12 +244,15 @@ export default class WebexCrossClientService {
243
244
  if (trackPublishMetrics) {
244
245
  metricsManager.trackEvent(
245
246
  METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
246
- {enableWxBetterTogether: enable},
247
+ {
248
+ enableWxBetterTogether: enable,
249
+ usersubPublished: enable,
250
+ },
247
251
  ['operational', 'behavioral']
248
252
  );
249
253
  }
250
254
 
251
- LoggerProxy.info(`Cross-client answer-calls-on-wxcc set to ${enable}`, {
255
+ LoggerProxy.log(`Cross-client answer-calls-on-wxcc set to ${enable}`, {
252
256
  module: WEBEX_CROSS_CLIENT_FILE,
253
257
  method: METHODS.SET_MANAGE_WEBEX_CALLING_IN_WXCC,
254
258
  data: {
@@ -273,6 +277,7 @@ export default class WebexCrossClientService {
273
277
  METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
274
278
  {
275
279
  enableWxBetterTogether: enable,
280
+ usersubPublished: this.answerCallsState,
276
281
  skipReason,
277
282
  error,
278
283
  },
@@ -88,7 +88,7 @@ export default class WxAppTelephonyMercurySync {
88
88
 
89
89
  MetricsManager.getInstance().trackEvent(
90
90
  METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED,
91
- {error: 'Mercury is unavailable for wxApp mute sync'},
91
+ {error: 'Mercury is unavailable for wxApp mute sync', mercurySubscribed: false},
92
92
  ['operational', 'behavioral']
93
93
  );
94
94
 
@@ -110,11 +110,11 @@ export default class WxAppTelephonyMercurySync {
110
110
 
111
111
  MetricsManager.getInstance().trackEvent(
112
112
  METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_SUCCESS,
113
- {},
113
+ {mercurySubscribed: true},
114
114
  ['operational', 'behavioral']
115
115
  );
116
116
 
117
- LoggerProxy.info('Subscribed to wxApp telephony Mercury mute sync', {
117
+ LoggerProxy.log('Subscribed to wxApp telephony Mercury mute sync', {
118
118
  module: WXAPP_TELEPHONY_MERCURY_SYNC_FILE,
119
119
  method: METHODS.SYNC_WXAPP_MUTE_FROM_MERCURY,
120
120
  });
@@ -1240,6 +1240,8 @@ export type Profile = {
1240
1240
  maskSensitiveData?: boolean;
1241
1241
  /** Whether agent is currently logged in */
1242
1242
  isAgentLoggedIn?: boolean;
1243
+ /** Active station session after a successful registered relogin. */
1244
+ agentSessionId?: string;
1243
1245
  /** Last auxiliary code ID used for state change */
1244
1246
  lastStateAuxCodeId?: string;
1245
1247
  /** Timestamp of last state change */
@@ -31,6 +31,7 @@ export default class TaskFactory {
31
31
  isRecordingEnabled: recordingEnabled,
32
32
  enableWxBetterTogether: configFlags.enableWxBetterTogether ?? false,
33
33
  answerCallOnWebexService,
34
+ getUsersubPublished: configFlags.getWxAppUsersubPublished,
34
35
  consultTransferConfig: consultTransfer,
35
36
  };
36
37
  switch (mediaType) {
@@ -778,6 +778,7 @@ export default class TaskManager extends EventEmitter {
778
778
  const interactionId = message.data.interactionId;
779
779
  const reservationInteractionId = message.data.reservationInteractionId;
780
780
  let task = this.taskCollection[interactionId];
781
+ let ownerChangeResolvedByRelatedInteraction = false;
781
782
 
782
783
  // When a campaign preview contact is accepted, the assigned event may arrive
783
784
  // with a new interactionId while the task is stored under the original
@@ -792,7 +793,66 @@ export default class TaskManager extends EventEmitter {
792
793
  }
793
794
 
794
795
  if (!task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
795
- task = this.findTaskForContactOwnerChange(message.data);
796
+ const incomingShowsCurrentAgentPromotion = Boolean(
797
+ this.agentId &&
798
+ message.data.interaction?.owner === this.agentId &&
799
+ TaskManager.isParticipantActiveOnMainInteraction(message.data, this.agentId)
800
+ );
801
+
802
+ task = this.findUniqueTaskByRelatedInteraction(
803
+ message.data,
804
+ (candidate) => {
805
+ const currentAgentId = this.agentId || candidate.data?.agentId;
806
+
807
+ return Boolean(
808
+ currentAgentId &&
809
+ (TaskManager.isParticipantActiveOnMainInteraction(candidate.data, currentAgentId) ||
810
+ incomingShowsCurrentAgentPromotion)
811
+ );
812
+ },
813
+ {includeCollectionKeys: true, includeMainCallMediaKeys: true}
814
+ );
815
+ ownerChangeResolvedByRelatedInteraction = Boolean(task);
816
+ }
817
+
818
+ let ownerChangeMainInteractionId: string | undefined;
819
+ if (task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
820
+ const taskMainInteractionId = task.data?.interaction?.mainInteractionId;
821
+ const payloadMainInteractionId = message.data.interaction?.mainInteractionId;
822
+ const hasConflictingMainInteractionIds = Boolean(
823
+ taskMainInteractionId &&
824
+ payloadMainInteractionId &&
825
+ taskMainInteractionId !== payloadMainInteractionId
826
+ );
827
+ // A promoted child task can retain stale embedded IDs. Prefer the
828
+ // authoritative incoming main-call identity before local snapshot IDs.
829
+ const stableMainInteractionId =
830
+ payloadMainInteractionId ||
831
+ TaskManager.getMainCallMediaId(message.data.interaction) ||
832
+ TaskManager.getMainCallMediaId(task.data?.interaction) ||
833
+ taskMainInteractionId ||
834
+ message.data.interaction?.interactionId ||
835
+ task.data?.interaction?.interactionId ||
836
+ task.data?.interactionId;
837
+ const taskAtStableKey = stableMainInteractionId
838
+ ? this.taskCollection[stableMainInteractionId]
839
+ : undefined;
840
+
841
+ if (
842
+ !stableMainInteractionId ||
843
+ hasConflictingMainInteractionIds ||
844
+ (taskAtStableKey && taskAtStableKey !== task)
845
+ ) {
846
+ LoggerProxy.warn('Unable to safely correlate owner change to the main interaction task', {
847
+ module: TASK_MANAGER_FILE,
848
+ method: 'prepareEventContext',
849
+ interactionId:
850
+ payloadMainInteractionId || taskMainInteractionId || message.data.interactionId,
851
+ });
852
+ task = undefined;
853
+ } else if (ownerChangeResolvedByRelatedInteraction) {
854
+ ownerChangeMainInteractionId = stableMainInteractionId;
855
+ }
796
856
  }
797
857
 
798
858
  if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
@@ -821,8 +881,11 @@ export default class TaskManager extends EventEmitter {
821
881
  }
822
882
  : message.data;
823
883
 
824
- if (task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
825
- adjustedPayload = TaskManager.preserveMainTaskIdentity(task, adjustedPayload);
884
+ if (task && ownerChangeMainInteractionId) {
885
+ adjustedPayload = {
886
+ ...adjustedPayload,
887
+ interactionId: ownerChangeMainInteractionId,
888
+ };
826
889
  }
827
890
 
828
891
  if (task && eventType === CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
@@ -860,53 +923,6 @@ export default class TaskManager extends EventEmitter {
860
923
  };
861
924
  }
862
925
 
863
- /**
864
- * ContactOwnerChanged can identify the promoted agent's child interaction while
865
- * the surviving task is stored under the main interaction. Accept one related
866
- * task when its existing snapshot or the authoritative incoming promotion proves
867
- * current-agent main-call membership, while excluding non-promoted and ambiguous matches.
868
- */
869
- private findTaskForContactOwnerChange(payload: WebSocketPayload): ITask | undefined {
870
- const relatedCandidates = this.findRelatedTasks(payload, {
871
- includeCollectionKeys: true,
872
- includeMainCallMediaKeys: true,
873
- });
874
- const incomingShowsCurrentAgentPromotion =
875
- this.isCurrentAgentPromotedOnIncomingMainCall(payload);
876
- const eligibleCandidates = relatedCandidates.filter(
877
- (candidate) =>
878
- this.isCurrentAgentActiveOnMainInteraction(candidate.data) ||
879
- incomingShowsCurrentAgentPromotion
880
- );
881
-
882
- if (eligibleCandidates.length === 1) {
883
- return eligibleCandidates[0];
884
- }
885
-
886
- if (eligibleCandidates.length > 1) {
887
- LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
888
- module: TASK_MANAGER_FILE,
889
- method: 'findTaskForContactOwnerChange',
890
- interactionId:
891
- payload.interaction?.mainInteractionId ||
892
- payload.interaction?.parentInteractionId ||
893
- payload.interaction?.callProcessingDetails?.parentInteractionId ||
894
- payload.interactionId,
895
- });
896
-
897
- return undefined;
898
- }
899
-
900
- return undefined;
901
- }
902
-
903
- private isCurrentAgentActiveOnMainInteraction(taskData: TaskData | undefined): boolean {
904
- const currentAgentId = this.agentId || taskData?.agentId;
905
- if (!currentAgentId) return false;
906
-
907
- return TaskManager.isParticipantActiveOnMainInteraction(taskData, currentAgentId);
908
- }
909
-
910
926
  private static isParticipantActiveOnMainInteraction(
911
927
  taskData: TaskData | WebSocketPayload | undefined,
912
928
  participantId: string
@@ -921,14 +937,6 @@ export default class TaskManager extends EventEmitter {
921
937
  );
922
938
  }
923
939
 
924
- private isCurrentAgentPromotedOnIncomingMainCall(payload: WebSocketPayload): boolean {
925
- return Boolean(
926
- this.agentId &&
927
- payload.interaction?.owner === this.agentId &&
928
- TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId)
929
- );
930
- }
931
-
932
940
  private static getMainCallMediaId(
933
941
  interaction: TaskData['interaction'] | undefined
934
942
  ): string | undefined {
@@ -960,31 +968,6 @@ export default class TaskManager extends EventEmitter {
960
968
  );
961
969
  }
962
970
 
963
- /** Keep a related owner-change event from re-keying the surviving main task to a child ID. */
964
- private static preserveMainTaskIdentity(
965
- task: ITask,
966
- payload: WebSocketPayload
967
- ): WebSocketPayload {
968
- const currentMainInteractionId = task.data?.interaction?.mainInteractionId;
969
- const payloadMainInteractionId = payload.interaction?.mainInteractionId;
970
- const currentMainMediaId = TaskManager.getMainCallMediaId(task.data?.interaction);
971
- const payloadMainMediaId = TaskManager.getMainCallMediaId(payload.interaction);
972
- const stableInteractionId =
973
- currentMainInteractionId ||
974
- payloadMainInteractionId ||
975
- currentMainMediaId ||
976
- payloadMainMediaId ||
977
- payload.interaction?.interactionId ||
978
- task.data?.interaction?.interactionId ||
979
- task.data?.interactionId;
980
-
981
- if (!stableInteractionId || stableInteractionId === payload.interactionId) {
982
- return payload;
983
- }
984
-
985
- return {...payload, interactionId: stableInteractionId};
986
- }
987
-
988
971
  /**
989
972
  * ParticipantLeftConference may arrive after an owner update while still carrying
990
973
  * the departed owner. Keep the confirmed owner only when the new roster proves that
@@ -1024,9 +1007,10 @@ export default class TaskManager extends EventEmitter {
1024
1007
  */
1025
1008
  private findUniqueTaskByRelatedInteraction(
1026
1009
  payload: WebSocketPayload,
1027
- candidateFilter: (task: ITask) => boolean = () => true
1010
+ candidateFilter: (task: ITask) => boolean = () => true,
1011
+ options: {includeCollectionKeys?: boolean; includeMainCallMediaKeys?: boolean} = {}
1028
1012
  ): ITask | undefined {
1029
- const candidates = this.findRelatedTasks(payload).filter(candidateFilter);
1013
+ const candidates = this.findRelatedTasks(payload, options).filter(candidateFilter);
1030
1014
 
1031
1015
  if (candidates.length === 1) {
1032
1016
  return candidates[0];
@@ -1060,7 +1044,9 @@ export default class TaskManager extends EventEmitter {
1060
1044
  payload.interaction?.parentInteractionId,
1061
1045
  payload.interaction?.callProcessingDetails?.parentInteractionId,
1062
1046
  ...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(payload.interaction)] : []),
1063
- ].filter((interactionId): interactionId is string => Boolean(interactionId))
1047
+ ].filter((relatedInteractionId): relatedInteractionId is string =>
1048
+ Boolean(relatedInteractionId)
1049
+ )
1064
1050
  );
1065
1051
  if (payloadInteractionIds.size === 0) return [];
1066
1052
 
@@ -1082,7 +1068,8 @@ export default class TaskManager extends EventEmitter {
1082
1068
  ];
1083
1069
 
1084
1070
  return candidateInteractionIds.some(
1085
- (interactionId) => Boolean(interactionId) && payloadInteractionIds.has(interactionId)
1071
+ (relatedInteractionId) =>
1072
+ Boolean(relatedInteractionId) && payloadInteractionIds.has(relatedInteractionId)
1086
1073
  );
1087
1074
  })
1088
1075
  .map(([, candidate]) => candidate)
@@ -1457,6 +1444,7 @@ export default class TaskManager extends EventEmitter {
1457
1444
  }
1458
1445
 
1459
1446
  private removeTaskFromCollection(task: ITask) {
1447
+ task.disposeWxAppOfferObservability?.();
1460
1448
  if (typeof task.cancelAutoWrapupTimer === 'function') {
1461
1449
  task.cancelAutoWrapupTimer();
1462
1450
  }
@@ -1655,6 +1643,16 @@ export default class TaskManager extends EventEmitter {
1655
1643
  });
1656
1644
  }
1657
1645
 
1646
+ /**
1647
+ * Re-run OFFERED wxApp offer diagnostics for collected voice tasks.
1648
+ * Used after usersub publish so pre-publish offers can schedule mismatch grace.
1649
+ */
1650
+ public refreshWxAppOfferObservabilityForAllTasks(): void {
1651
+ Object.values(this.taskCollection).forEach((task) => {
1652
+ task.refreshWxAppOfferObservability?.();
1653
+ });
1654
+ }
1655
+
1658
1656
  public static getTaskManager(
1659
1657
  apiAIAssistant: ApiAIAssistant,
1660
1658
  contact: ReturnType<typeof routingContact>,
@@ -618,6 +618,8 @@ This keeps transcript and suggestion delivery aligned on the same per-task event
618
618
 
619
619
  **wxApp consumer contract (WXCC-6026):** Hosts enable `enableWxBetterTogether` at init (Phase 1 init-only; re-init to change), bind UI to `task.uiControls` (including optional `main.keypad`), and call **`task.accept()`**, **`task.decline()`**, **`task.toggleMute({ muted? })`**, **`task.transmitDtmf({ dtmf })`**. SDK routes wxApp telephony internally on `Voice`. Shared-line `lineOwnerId` defaults from the wxApp agent participant when omitted.
620
620
 
621
+ **wxApp OFFERED diagnostics:** `Voice.updateUiControls` delegates offer-decision logging and deferred participant-mismatch WARN/metric to `voice/wxAppOfferObservability.ts` (500 ms grace; observability only). After usersub publish, `TaskManager.refreshWxAppOfferObservabilityForAllTasks()` re-invokes the observer without emitting UI controls. `removeTaskFromCollection` calls `disposeWxAppOfferObservability()` so a pending grace timer cannot emit after the task is dropped (including `CONTACT_MERGED` child removal). Dispose is terminal: the observer sets a disposed guard (timer cancel plus no-op `handleUiControlsUpdate`) so a later `updateUiControls` / refresh cannot schedule a new grace timer.
622
+
621
623
  **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.
622
624
 
623
625
  **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_*`).
@@ -924,15 +926,17 @@ guards. The state-machine transition table lives in
924
926
  Primary-Agent promotion remains backend-authoritative and follows the two-event
925
927
  desktop contract. `ContactOwnerChanged` updates the promoted Agent; TaskManager
926
928
  prefers an exact task, then one unique related task resolved through nested
927
- main/parent identifiers or the `mainCall` media-map identity. A related
928
- candidate is eligible when its current
929
+ main/parent identifiers, the task collection key, or the `mainCall` media-map
930
+ identity. A related candidate is eligible when its current
929
931
  snapshot contains the current Agent's participant entry with `hasLeft !== true`
930
932
  and membership on the `mType: mainCall` leg. The incoming snapshot can provide
931
933
  that evidence only when it also names the current Agent as `interaction.owner`;
932
934
  this permits an authoritative promotion payload to repair a stale child-keyed
933
935
  snapshot while still excluding consult-only tasks. The update keeps the
934
936
  surviving main interaction identity even when the notification names a
935
- promoted-Agent child interaction.
937
+ promoted-Agent child interaction. Conflicting main identifiers, ambiguous
938
+ matches, and a canonical key occupied by a distinct task are ignored rather
939
+ than replacing an existing task.
936
940
 
937
941
  If no related task exists, TaskManager recovers only the promoted current Agent
938
942
  from a non-terminal telephony `ContactOwnerChanged` payload that provides the same
@@ -1759,6 +1759,20 @@ export interface ITask extends IEventEmitter {
1759
1759
  */
1760
1760
  syncWxAppMuteFromCallDetails?(): Promise<boolean | undefined>;
1761
1761
 
1762
+ /**
1763
+ * Re-run OFFERED wxApp offer diagnostics without emitting UI controls.
1764
+ * Voice tasks implement this; other channel types omit it.
1765
+ * @internal
1766
+ */
1767
+ refreshWxAppOfferObservability?(): void;
1768
+
1769
+ /**
1770
+ * Cancel pending wxApp offer observability timers (participant-mismatch grace).
1771
+ * Voice tasks implement this; other channel types omit it.
1772
+ * @internal
1773
+ */
1774
+ disposeWxAppOfferObservability?(): void;
1775
+
1762
1776
  /**
1763
1777
  * State machine instance for managing task state transitions and derived properties.
1764
1778
  * The state machine handles:
@@ -2070,6 +2084,8 @@ export type VoiceUIControlOptions = {
2070
2084
  enableWxBetterTogether?: boolean;
2071
2085
  consultTransferConfig?: ConsultTransferDestinationConfig;
2072
2086
  answerCallOnWebexService?: import('../AnswerCallOnWebexService').default;
2087
+ /** Read-only observability: whether usersub answer-calls-on-wxcc is active this session. */
2088
+ getUsersubPublished?: () => boolean;
2073
2089
  };
2074
2090
 
2075
2091
  /**