@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.4

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 (187) hide show
  1. package/.sdd/manifest.json +5 -4
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +6 -2
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +440 -10
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/behavioral-events.js +13 -0
  15. package/dist/metrics/behavioral-events.js.map +1 -1
  16. package/dist/metrics/constants.js +2 -0
  17. package/dist/metrics/constants.js.map +1 -1
  18. package/dist/services/AddressBook.js +18 -15
  19. package/dist/services/AddressBook.js.map +1 -1
  20. package/dist/services/AnswerCallOnWebexService.js +174 -0
  21. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  22. package/dist/services/EntryPoint.js +46 -68
  23. package/dist/services/EntryPoint.js.map +1 -1
  24. package/dist/services/Queue.js +27 -22
  25. package/dist/services/Queue.js.map +1 -1
  26. package/dist/services/WebexCrossClientService.js +171 -0
  27. package/dist/services/WebexCrossClientService.js.map +1 -0
  28. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  29. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  30. package/dist/services/config/Util.js +3 -0
  31. package/dist/services/config/Util.js.map +1 -1
  32. package/dist/services/config/constants.js +10 -6
  33. package/dist/services/config/constants.js.map +1 -1
  34. package/dist/services/config/types.js +4 -0
  35. package/dist/services/config/types.js.map +1 -1
  36. package/dist/services/core/Err.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +6 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +28 -14
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/task/Task.js +73 -7
  43. package/dist/services/task/Task.js.map +1 -1
  44. package/dist/services/task/TaskFactory.js +8 -4
  45. package/dist/services/task/TaskFactory.js.map +1 -1
  46. package/dist/services/task/TaskManager.js +353 -21
  47. package/dist/services/task/TaskManager.js.map +1 -1
  48. package/dist/services/task/WebexCallingUtils.js +70 -0
  49. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  50. package/dist/services/task/constants.js +4 -1
  51. package/dist/services/task/constants.js.map +1 -1
  52. package/dist/services/task/contact.js +29 -0
  53. package/dist/services/task/contact.js.map +1 -1
  54. package/dist/services/task/digital/Digital.js +3 -2
  55. package/dist/services/task/digital/Digital.js.map +1 -1
  56. package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
  57. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  58. package/dist/services/task/state-machine/actions.js +6 -1
  59. package/dist/services/task/state-machine/actions.js.map +1 -1
  60. package/dist/services/task/state-machine/guards.js +38 -11
  61. package/dist/services/task/state-machine/guards.js.map +1 -1
  62. package/dist/services/task/state-machine/types.js.map +1 -1
  63. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  64. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  65. package/dist/services/task/types.js +16 -1
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/services/task/voice/Voice.js +350 -14
  68. package/dist/services/task/voice/Voice.js.map +1 -1
  69. package/dist/services/task/voice/WebRTC.js +4 -1
  70. package/dist/services/task/voice/WebRTC.js.map +1 -1
  71. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  72. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  73. package/dist/services/wxAppTelephonyUtils.js +19 -0
  74. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  75. package/dist/types/cc.d.ts +77 -4
  76. package/dist/types/config.d.ts +7 -0
  77. package/dist/types/constants.d.ts +8 -0
  78. package/dist/types/index.d.ts +3 -1
  79. package/dist/types/metrics/constants.d.ts +2 -0
  80. package/dist/types/services/AddressBook.d.ts +2 -1
  81. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  82. package/dist/types/services/EntryPoint.d.ts +7 -5
  83. package/dist/types/services/Queue.d.ts +5 -3
  84. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  85. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  86. package/dist/types/services/config/constants.d.ts +8 -5
  87. package/dist/types/services/config/types.d.ts +17 -4
  88. package/dist/types/services/core/Err.d.ts +2 -0
  89. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  90. package/dist/types/services/core/types.d.ts +2 -0
  91. package/dist/types/services/task/Task.d.ts +20 -2
  92. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  93. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  94. package/dist/types/services/task/constants.d.ts +3 -0
  95. package/dist/types/services/task/contact.d.ts +4 -0
  96. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  97. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
  98. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  99. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  100. package/dist/types/services/task/types.d.ts +81 -4
  101. package/dist/types/services/task/voice/Voice.d.ts +48 -9
  102. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  103. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  104. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  105. package/dist/types/types.d.ts +14 -8
  106. package/dist/types/utils/PageCache.d.ts +20 -3
  107. package/dist/types.js +7 -2
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/PageCache.js +19 -5
  110. package/dist/utils/PageCache.js.map +1 -1
  111. package/dist/webex.js +1 -1
  112. package/package.json +9 -9
  113. package/src/cc.ts +506 -9
  114. package/src/config.ts +7 -0
  115. package/src/constants.ts +8 -0
  116. package/src/index.ts +2 -0
  117. package/src/metrics/ai-docs/metrics-spec.md +9 -3
  118. package/src/metrics/behavioral-events.ts +14 -0
  119. package/src/metrics/constants.ts +2 -0
  120. package/src/services/AddressBook.ts +17 -6
  121. package/src/services/AnswerCallOnWebexService.ts +206 -0
  122. package/src/services/EntryPoint.ts +59 -60
  123. package/src/services/Queue.ts +29 -12
  124. package/src/services/WebexCrossClientService.ts +212 -0
  125. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  126. package/src/services/ai-docs/AGENTS.md +10 -10
  127. package/src/services/ai-docs/services-spec.md +6 -1
  128. package/src/services/config/Util.ts +3 -0
  129. package/src/services/config/ai-docs/AGENTS.md +1 -1
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  131. package/src/services/config/ai-docs/config-spec.md +6 -0
  132. package/src/services/config/constants.ts +10 -6
  133. package/src/services/config/types.ts +16 -4
  134. package/src/services/core/Err.ts +1 -0
  135. package/src/services/core/WebexRequest.ts +3 -1
  136. package/src/services/core/ai-docs/core-spec.md +5 -1
  137. package/src/services/core/aqm-reqs.ts +30 -15
  138. package/src/services/core/types.ts +2 -0
  139. package/src/services/task/Task.ts +79 -8
  140. package/src/services/task/TaskFactory.ts +8 -3
  141. package/src/services/task/TaskManager.ts +522 -16
  142. package/src/services/task/WebexCallingUtils.ts +136 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  144. package/src/services/task/ai-docs/task-spec.md +152 -2
  145. package/src/services/task/constants.ts +3 -0
  146. package/src/services/task/contact.ts +30 -0
  147. package/src/services/task/digital/Digital.ts +4 -2
  148. package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
  149. package/src/services/task/state-machine/actions.ts +5 -2
  150. package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
  151. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
  152. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +56 -24
  153. package/src/services/task/state-machine/guards.ts +64 -11
  154. package/src/services/task/state-machine/types.ts +16 -1
  155. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  156. package/src/services/task/types.ts +114 -4
  157. package/src/services/task/voice/Voice.ts +462 -12
  158. package/src/services/task/voice/WebRTC.ts +5 -1
  159. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  160. package/src/services/wxAppTelephonyUtils.ts +14 -0
  161. package/src/types.ts +33 -9
  162. package/src/utils/AGENTS.md +21 -10
  163. package/src/utils/PageCache.ts +38 -5
  164. package/src/utils/ai-docs/utils-spec.md +21 -11
  165. package/test/unit/spec/cc.ts +1274 -0
  166. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  167. package/test/unit/spec/services/AddressBook.ts +37 -6
  168. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  169. package/test/unit/spec/services/EntryPoint.ts +87 -40
  170. package/test/unit/spec/services/Queue.ts +123 -12
  171. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  172. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  173. package/test/unit/spec/services/config/Util.ts +85 -0
  174. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  175. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  176. package/test/unit/spec/services/task/Task.ts +200 -0
  177. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  178. package/test/unit/spec/services/task/TaskManager.ts +1294 -1
  179. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  180. package/test/unit/spec/services/task/contact.ts +33 -0
  181. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
  182. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  183. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  184. package/test/unit/spec/services/task/voice/Voice.ts +874 -0
  185. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  186. package/umd/contact-center.min.js +2 -2
  187. package/umd/contact-center.min.js.map +1 -1
@@ -0,0 +1,136 @@
1
+ import {TaskState} from './state-machine/constants';
2
+
3
+ export type WebexCallingDeviceDetails = {
4
+ deviceType: string;
5
+ deviceId: string;
6
+ deviceCallId: string;
7
+ };
8
+
9
+ type WebexCallingParticipant = {
10
+ id?: unknown;
11
+ deviceType?: unknown;
12
+ deviceCallId?: unknown;
13
+ deviceId?: unknown;
14
+ };
15
+
16
+ const normalizeId = (id: unknown): string | undefined => {
17
+ if (typeof id !== 'string') {
18
+ return undefined;
19
+ }
20
+
21
+ const normalizedId = id.trim();
22
+
23
+ return normalizedId === '' ? undefined : normalizedId;
24
+ };
25
+
26
+ const isParticipantsById = (participants: unknown): participants is Record<string, unknown> =>
27
+ Boolean(participants) &&
28
+ typeof participants === 'object' &&
29
+ !Array.isArray(participants) &&
30
+ !(participants instanceof Map);
31
+
32
+ const hasWebexCallingDeviceDetails = (
33
+ participant: unknown
34
+ ): participant is WebexCallingParticipant &
35
+ Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'> => {
36
+ const participantDetails = participant as WebexCallingParticipant | undefined;
37
+
38
+ return (
39
+ typeof participantDetails?.deviceCallId === 'string' &&
40
+ participantDetails.deviceCallId.trim() !== '' &&
41
+ typeof participantDetails?.deviceId === 'string' &&
42
+ participantDetails.deviceId.trim() !== ''
43
+ );
44
+ };
45
+
46
+ const isWebexCallingParticipantForAgent = (
47
+ participant: unknown,
48
+ agentId: string
49
+ ): participant is WebexCallingParticipant &
50
+ Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'> => {
51
+ const participantDetails = participant as WebexCallingParticipant | undefined;
52
+
53
+ return (
54
+ normalizeId(participantDetails?.id) === agentId && hasWebexCallingDeviceDetails(participant)
55
+ );
56
+ };
57
+
58
+ const getWebexCallingDeviceDetails = (
59
+ participant: WebexCallingParticipant &
60
+ Pick<WebexCallingDeviceDetails, 'deviceCallId' | 'deviceId'>
61
+ ): WebexCallingDeviceDetails => ({
62
+ deviceType: typeof participant.deviceType === 'string' ? participant.deviceType : '',
63
+ deviceId: participant.deviceId,
64
+ deviceCallId: participant.deviceCallId,
65
+ });
66
+
67
+ export function getWebexCallingDeviceDetailsForAgent(
68
+ agentId: string | undefined,
69
+ participants: unknown
70
+ ): WebexCallingDeviceDetails | undefined {
71
+ const normalizedAgentId = normalizeId(agentId);
72
+ if (!normalizedAgentId || !isParticipantsById(participants)) {
73
+ return undefined;
74
+ }
75
+
76
+ const agentParticipant = participants[normalizedAgentId];
77
+ const matchingAgentParticipant = hasWebexCallingDeviceDetails(agentParticipant)
78
+ ? agentParticipant
79
+ : Object.values(participants).find((participant) =>
80
+ isWebexCallingParticipantForAgent(participant, normalizedAgentId)
81
+ );
82
+
83
+ if (!matchingAgentParticipant || !hasWebexCallingDeviceDetails(matchingAgentParticipant)) {
84
+ return undefined;
85
+ }
86
+
87
+ return getWebexCallingDeviceDetails(matchingAgentParticipant);
88
+ }
89
+
90
+ export function isWebexCallingCallForAgent(
91
+ agentId: string | undefined,
92
+ participants: unknown
93
+ ): boolean {
94
+ return Boolean(getWebexCallingDeviceDetailsForAgent(agentId, participants));
95
+ }
96
+
97
+ /** Matches wxAppVoiceMethods.getWebexCallingCallId engaged semantics for uiControls. */
98
+ export function isWxAppEngagedForControls(
99
+ enableWxBetterTogether: boolean,
100
+ agentId: string | undefined,
101
+ participants: unknown,
102
+ state: TaskState
103
+ ): boolean {
104
+ if (!enableWxBetterTogether) {
105
+ return false;
106
+ }
107
+
108
+ if (
109
+ !state ||
110
+ state === TaskState.OFFERED ||
111
+ state === TaskState.IDLE ||
112
+ state === TaskState.WRAPPING_UP ||
113
+ state === TaskState.TERMINATED ||
114
+ state === TaskState.COMPLETED
115
+ ) {
116
+ return false;
117
+ }
118
+
119
+ const details = getWebexCallingDeviceDetailsForAgent(agentId, participants);
120
+
121
+ return details?.deviceType === 'wxApp';
122
+ }
123
+
124
+ export function decodedLineOwnerId(lineOwnerId?: string): string | undefined {
125
+ if (!lineOwnerId) {
126
+ return undefined;
127
+ }
128
+
129
+ try {
130
+ const decoded = atob(lineOwnerId);
131
+
132
+ return decoded.split('/').pop();
133
+ } catch {
134
+ return undefined;
135
+ }
136
+ }
@@ -35,6 +35,7 @@
35
35
  - `state?: SnapshotFrom<TaskStateMachine>`
36
36
  - `autoWrapup?: AutoWrapup`
37
37
  - `uiControls: TaskUIControls` (getter)
38
+ - includes ordered, action-specific `consultTransferDestinations` computed from Task profile configuration and live interaction data
38
39
 
39
40
  **Methods**
40
41
 
@@ -33,7 +33,7 @@ Manage task lifecycle including inbound/outbound calls, hold/resume, consult, tr
33
33
 
34
34
  - **Event Emission and Public APIs**: Task objects register listeners, update context, emit SDK events (e.g., `task:*`), and expose public methods that delegate to `contact.ts` for call control and to the state machine for transition validation.
35
35
 
36
- - **AQM Contact Operations**: `contact.ts` builds the AQM request surface for call control (accept, hold, consult, transfer, wrapup, end) and is the primary bridge from `Task`/`Voice`/`WebRTC`/`Digital` methods to WCC task APIs.
36
+ - **AQM Contact Operations**: `contact.ts` builds the AQM request surface for call control (accept, hold, consult, transfer, participant Drop, wrapup, end) and is the primary bridge from `Task`/`Voice`/`WebRTC`/`Digital` methods to WCC task APIs.
37
37
 
38
38
  - **Outbound and Preview-Campaign Dialing**: `dialer.ts` exposes `startOutdial` plus `acceptPreviewContact`, `skipPreviewContact`, and `removePreviewContact`; ContactCenter publishes these operations through typed `cc` methods.
39
39
 
@@ -141,6 +141,8 @@ services/task/
141
141
  |---|---|---|---|---|---|---|
142
142
  | `task.surface` | SDK / event / internal API | Exported Task/types/events plus application-facing task instances and call-control methods. | Stable module consumption boundary. | Additive changes by default; breaking package exports require a major-version transition. | `src/services/task/Task.ts` | `../../../../ai-docs/CONTRACTS.md` |
143
143
  | `task.preview-campaign` | SDK/AQM API | `acceptPreviewContact`, `skipPreviewContact`, `removePreviewContact`, and `PreviewContactPayload`. | Accept, skip, or remove a reserved campaign preview contact; each method returns `Promise<TaskResponse>`. | Additive semver-public methods; removals or signature changes are breaking. | `src/cc.ts`, `src/services/task/dialer.ts`, `src/services/task/types.ts` | `../../../../ai-docs/CONTRACTS.md` |
144
+ | `task.consult-transfer-controls` | SDK task controls | `TaskUIControls.consultTransferDestinations` with ordered `consult` and `transfer` arrays. | Surface default destination availability on every Task without an extra policy method. | Additive semver-public field/types; array order is meaningful and the first item is the default. | `src/services/task/types.ts`, `src/services/task/state-machine/uiControlsComputer.ts` | `../../../../ai-docs/CONTRACTS.md` |
145
+ | `task.conference-participant-drop` | SDK/AQM API | `task.dropConferenceParticipant(payload: DropConferenceParticipantPayload): Promise<TaskResponse>`. | Remove a supported target from a voice conference after correlated routing completion. | Additive semver-public method and payload; removals or signature changes are breaking. | `src/services/task/voice/Voice.ts`, `src/services/task/contact.ts`, `src/services/task/types.ts` | `../../../../ai-docs/CONTRACTS.md` |
144
146
 
145
147
  Compatibility notes:
146
148
  - Do not remove or reinterpret exported symbols/events without a documented consumer migration.
@@ -151,6 +153,8 @@ Compatibility notes:
151
153
 
152
154
  - `PreviewContactPayload` (`types.ts`) with `interactionId` and campaign-name `campaignId`
153
155
 
156
+ - `DropConferenceParticipantPayload` (`types.ts`) with a runtime-validated non-empty `participantId`
157
+
154
158
  - `ITask`, `IVoice`, `IWebRTC`, `IDigital` (`types.ts`)
155
159
 
156
160
  - `MEDIA_CHANNEL`, `TASK_CHANNEL_TYPE`, `VOICE_VARIANT` (`types.ts`)
@@ -346,6 +350,20 @@ End consultation without transfer.
346
350
 
347
351
  **Returns**: `Promise<TaskResponse>`
348
352
 
353
+ Remove another participant from a voice conference.
354
+
355
+ **Parameters**:
356
+
357
+ - `participantId` (string, required): backend participant target identifier; blank and non-string values reject before AQM registration or telemetry.
358
+
359
+ **Returns**: `Promise<TaskResponse>` after `ParticipantLeftConference` is received for the latest `interaction.mainInteractionId || task.data.interactionId`.
360
+
361
+ **Failure**: `ParticipantDropConferenceFailed`, HTTP rejection, or the default 20-second AQM timeout rejects the promise. The operation does not add a task state-machine event or optimistically change the roster.
362
+
363
+ ```typescript
364
+ await task.dropConferenceParticipant({participantId});
365
+ ```
366
+
349
367
  ### Complete TASK_EVENTS inventory
350
368
 
351
369
  The public `TASK_EVENTS` enum contains 49 members; every member is listed below from `src/services/task/types.ts`.
@@ -417,6 +435,9 @@ The public `TASK_EVENTS` enum contains 49 members; every member is listed below
417
435
  | TASK-R-005 | The contact dependency belongs to Task/TaskFactory-created tasks; dialer is an AqmReqs request factory without that constructor. | Misattributing constructor dependencies causes invalid instantiation examples. | `src/services/task/TaskFactory.ts` | `test/unit/spec/services/task/dialer.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
418
436
  | TASK-R-006 | Keep credentials and authentication outside Task; remote operations delegate through contact/dialer routing, AqmReqs, and Core/WebexRequest. | Task lifecycle objects should never duplicate host token handling or leak authentication state into interaction data. | `src/services/task/Task.ts`, `src/services/task/contact.ts`, `src/services/core/WebexRequest.ts` | `test/unit/spec/services/task/Task.ts`, `test/unit/spec/services/task/contact.ts` | None; authentication ownership is explicit. | PRESENT |
419
437
  | TASK-R-007 | Route enabled preview-campaign accept, skip, and remove operations through the dialer AQM factory using `PreviewContactPayload`, returning `Promise<TaskResponse>` from the public ContactCenter methods. Before routing skip/remove, reject the operation when the matching task's disable flag is `'true'`. | Preview reservations require typed payloads and correlated backend completion, while campaign controls must block prohibited skip/remove requests before transport begins. | `src/cc.ts`, `src/services/task/dialer.ts`, `src/services/task/types.ts` | `test/unit/spec/cc.ts`, `test/unit/spec/services/task/dialer.ts` | Public delegation and dialer requests are covered; the `campaignPreviewSkipDisabled` and `campaignPreviewRemoveDisabled` early-exit guards lack direct unit coverage. Independent review identified this gap on 2026-07-15. | PRESENT |
438
+ | TASK-R-008 | Voice `dropConferenceParticipant` must validate the public target, resolve the latest main interaction, POST an empty body to the encoded participant-drop route, and settle only from `ParticipantLeftConference`, `ParticipantDropConferenceFailed`, HTTP failure, or the existing AQM timeout. | Participant removal is backend-authoritative and must preserve event-driven roster/state synchronization without exposing participant identifiers through telemetry or logs. | `src/services/task/voice/Voice.ts`, `src/services/task/contact.ts`, `src/services/task/types.ts` | `test/unit/spec/services/task/voice/Voice.ts`, `test/unit/spec/services/task/contact.ts`, `test/unit/spec/services/core/aqm-reqs.ts` | Backend authorization and media removal are remote-service responsibilities. | PRESENT |
439
+ | TASK-R-009 | ContactCenter must inject Desktop Profile collaboration policy into TaskFactory, and every created voice/digital Task must expose ordered Consult and Transfer destination categories through `TaskUIControls.consultTransferDestinations`. | Task consumers need one already-computed policy surface and must not fetch profile flags or duplicate destination decisions. | `src/cc.ts`, `src/services/task/TaskFactory.ts`, `src/services/task/types.ts` | `test/unit/spec/cc.ts`, `test/unit/spec/services/task/TaskFactory.ts` | Host-specific UI options may only further hide an SDK-allowed category. | PRESENT |
440
+ | TASK-R-010 | TaskManager must propagate backend owner promotion without electing an owner locally. `ContactOwnerChanged` resolves an exact task first, then one unique related task whose existing snapshot proves that the current Agent is active on `mainCall`, or whose incoming snapshot both names that Agent as `interaction.owner` and proves the same active-main-leg membership. Active membership requires a participant entry with `hasLeft !== true`. If no task exists, recovery is limited to that incoming owner=current-Agent proof: create the normal Task under the stable main interaction ID, silently HYDRATE its actor before listener installation, then process the original owner-change event so consumers receive one `task:hydrate` and no `task:incoming`. An owner-changing `ContactUpdated` uses the existing owner-change hydrate path only for an existing task, and a late `ParticipantLeftConference` cannot replace a confirmed active owner with the departed participant. | Every surviving agent must observe the backend-selected primary Agent immediately, including stale child-keyed and narrowly recoverable desynchronization cases, without duplicating tasks or exposing an uninitialized actor. | `src/services/task/TaskManager.ts` | `test/unit/spec/services/task/TaskManager.ts`, `test/unit/spec/services/task/Task.ts` | Backend owner selection and delivery of a complete owner-bearing `ContactOwnerChanged` payload remain remote responsibilities; missing-task `ContactUpdated` remains update-only. | PRESENT |
420
441
 
421
442
  ## Design Overview
422
443
  Task separates its stable consumption boundary from collaborators so ownership and failure behavior stay explicit. A shared Task base preserves a stable API while media-specific subclasses and a separate state engine enforce capability differences.
@@ -494,6 +515,8 @@ Returns an object of AQM request methods wired to `TASK_API` and `TASK_MESSAGE_T
494
515
 
495
516
  - `consultConference`
496
517
 
518
+ - `dropConferenceParticipant`
519
+
497
520
  - `exitConference`
498
521
 
499
522
  - `conferenceTransfer`
@@ -532,6 +555,8 @@ Uses `contact` for:
532
555
 
533
556
  - `consultConference`, `exitConference`, `conferenceTransfer`
534
557
 
558
+ - `dropConferenceParticipant` (Voice only; base Task rejects as unsupported)
559
+
535
560
  Uses `contact.accept` in `accept()`.
536
561
 
537
562
  TaskManager maintains a map of active tasks:
@@ -588,6 +613,14 @@ this.webSocketManager.on('message', (event) => {
588
613
 
589
614
  This keeps transcript and suggestion delivery aligned on the same per-task event surface.
590
615
 
616
+ **wxApp outdial `ContactEnded` / `AgentOutboundFailed` mapping:** Applies only when the current agent is a **wxApp-managed** outdial (`enableWxBetterTogether` + agent participant `deviceType: wxApp`). Pre-accept decline maps to `CONTACT_ENDED` when `wxAppAnswerPending` is false and `agentsPendingWrapUp` is empty — even if the backend sends misleading `interaction.state: wrapUp`. Post-accept wxApp agent end remaps to `OUTBOUND_FAILED` / `AGENT_ENDS` when `wxAppAnswerPending`, `agentsPendingWrapUp`, or `interaction.state === 'wrapUp'` indicates wrapup. **Non-wxApp** agent-terminated `AgentOutboundFailed` (`AGENT_ENDS` / `terminatingParty: Agent`) stays on `OUTBOUND_FAILED` so the OFFERED-state `shouldWrapUp` path runs; `suppressOutdialFailedPopup` on `taskData` skips the outdial-failed modal only. Non-wxApp `ContactEnded` keeps the legacy path. Other failure reasons (`CUSTOMER_BUSY`, etc.) still use `OUTBOUND_FAILED` with the popup. See `TaskManager.isWxAppManagedOutdialTask`, `isAgentTerminatedOutdialWrapup`, and `task-state-machine-spec.md`.
617
+
618
+ **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.
619
+
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
+
622
+ **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
+
591
624
  For BROWSER login, TaskManager integrates with WebCalling:
592
625
 
593
626
  ```mermaid
@@ -689,6 +722,7 @@ isSecondaryEpDnAgent(interaction);
689
722
  | `TASK_WRAPUP_SUCCESS` | operational | Wrapup completed |
690
723
  | `TASK_TRANSFER_SUCCESS` | behavioral, business | Transfer completed |
691
724
  | `TASK_OUTDIAL_SUCCESS` | behavioral, business | Outdial completed |
725
+ | `TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS` | operational, behavioral, business | Participant Drop completed |
692
726
 
693
727
  ## Data Flow
694
728
  1. **WebSocket event arrives** → `TaskManager` maps CC event to `TaskEvent`.
@@ -727,6 +761,7 @@ Sequence coverage:
727
761
  | Incoming task creation | Incoming task | Unsupported media or incomplete event context prevents publication of a partial task. |
728
762
  | Voice hold/resume | Hold or resume | Invalid state rejects before transport; failure notification/HTTP error/timeout sends the matching failure event and throws. |
729
763
  | Consult/transfer/conference | Consult and transfer | Backend failure notifications drive explicit failure actions and preserve stable call context. |
764
+ | Participant Drop | Participant Drop | Validation rejects locally; AQM success/failure/HTTP/timeout paths settle and clean up without optimistic state changes. |
730
765
  | Wrapup/end | Wrapup or end | Validation and backend failures throw; backend events retain distinct WRAPPING_UP/COMPLETED/TERMINATED outcomes. |
731
766
  | WebRTC and digital accept | Channel-specific accept | WebRTC media/calling failure and Digital AQM failure follow different rejection paths. |
732
767
 
@@ -820,6 +855,118 @@ sequenceDiagram
820
855
  end
821
856
  ```
822
857
 
858
+ ### Participant Drop
859
+
860
+ ```mermaid
861
+ sequenceDiagram
862
+ participant App
863
+ participant Voice
864
+ participant Contact as routingContact
865
+ participant AQM as AqmReqs
866
+ participant Backend
867
+ participant WS as Primary WebSocket
868
+ participant TM as TaskManager/state machine
869
+ App->>Voice: dropConferenceParticipant({participantId})
870
+ alt participantId is invalid
871
+ Voice-->>App: reject before metrics, HTTP, or bind registration
872
+ else valid target
873
+ Voice->>Voice: select latest main interaction ID
874
+ Voice->>Contact: participant Drop request
875
+ Contact->>AQM: POST encoded path with empty body and sensitive-log redaction
876
+ AQM->>Backend: authenticated HTTP initiation
877
+ alt ParticipantLeftConference
878
+ Backend-->>WS: ParticipantLeftConference
879
+ WS-->>AQM: correlated success
880
+ AQM-->>Voice: TaskResponse
881
+ WS-->>TM: existing PARTICIPANT_LEAVE mapping updates roster/conference state
882
+ Voice-->>App: resolve
883
+ else ParticipantDropConferenceFailed, HTTP failure, or timeout
884
+ AQM-->>Voice: structured rejection
885
+ Voice-->>App: reject without participant data in logs/metrics
886
+ end
887
+ end
888
+ ```
889
+
890
+ `ParticipantLeftConference` is authoritative for both a customer hangup and an
891
+ agent-initiated customer Drop. The task removes only participants absent from
892
+ the latest interaction snapshot. TaskManager first uses the exact interaction
893
+ key, then the existing reservation fallback, and finally one unique task whose
894
+ `interaction.mainInteractionId`, interaction-level `parentInteractionId`, or
895
+ `callProcessingDetails.parentInteractionId` matches an identifier carried by the
896
+ lifecycle event. Object aliases are deduplicated and all aliases are removed
897
+ during terminal cleanup; ambiguous related-interaction matches are ignored
898
+ without logging participant data. Events carrying none of those correlation
899
+ identifiers are not routed. `ContactMerged` removes the EP-DN child task,
900
+ hydrates the main-interaction task, and publishes the existing `task:merged`
901
+ event so consumers can rebind to the surviving task object.
902
+
903
+ Dropping an Agent while that Agent is consulting continues through the existing
904
+ `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.
921
+
922
+ Primary-Agent promotion remains backend-authoritative and follows the two-event
923
+ desktop contract. `ContactOwnerChanged` updates the promoted Agent; TaskManager
924
+ prefers an exact task, then one unique related task resolved through nested
925
+ main/parent identifiers or the `mainCall` media-map identity. A related
926
+ candidate is eligible when its current
927
+ snapshot contains the current Agent's participant entry with `hasLeft !== true`
928
+ and membership on the `mType: mainCall` leg. The incoming snapshot can provide
929
+ that evidence only when it also names the current Agent as `interaction.owner`;
930
+ this permits an authoritative promotion payload to repair a stale child-keyed
931
+ snapshot while still excluding consult-only tasks. The update keeps the
932
+ surviving main interaction identity even when the notification names a
933
+ promoted-Agent child interaction.
934
+
935
+ If no related task exists, TaskManager recovers only the promoted current Agent
936
+ from a non-terminal telephony `ContactOwnerChanged` payload that provides the same
937
+ active-main-leg evidence. It creates the normal Task through TaskFactory under the
938
+ stable main interaction ID, sends an internal HYDRATE before installing external
939
+ listeners, and then processes the original owner-change event. Consumers
940
+ therefore receive one `task:hydrate`, never a synthetic `task:incoming`, and a
941
+ later canonical contact or merge event reuses the same task rather than creating
942
+ an alias. Missing, partial, departed, consult-only, non-promoted, and ambiguous
943
+ payloads are ignored for recovery.
944
+
945
+ For other surviving Agents, a `ContactUpdated` whose non-empty
946
+ `interaction.owner` differs from the current owner is delivered through the
947
+ existing `CONTACT_OWNER_CHANGED`/`task:hydrate` path; same-owner or owner-less
948
+ updates remain data-only, and a missing task is not created from `ContactUpdated`.
949
+ If a later `ParticipantLeftConference` still names the departed participant as
950
+ owner, an already confirmed owner is retained only when the new roster proves
951
+ that confirmed owner remains active on `mainCall` and explicitly names the
952
+ incoming owner as `participantId` or marks it `hasLeft`. Main-call omission alone
953
+ is not departure evidence. The SDK never chooses the successor locally.
954
+
955
+ In the Contact Center sample roster, non-owner and Supervisor targets remain
956
+ visible but non-actionable; the UI does not display a separate read-only label.
957
+ The roster is visible whenever the viewing agent remains active on the main leg
958
+ and at least one supported non-customer participant (Agent, EP-DN, or Supervisor)
959
+ is visible. Consequently, Customer-only calls use the original 1-to-1 UI, while
960
+ a single Agent remains visible after the Customer leaves. Dropping the last
961
+ non-customer participant while the Customer remains restores the 1-to-1 UI.
962
+ Terminated tasks and viewing-agent departure hide the roster immediately.
963
+
964
+ The current consult leg may additionally contribute a pending EP-DN row before
965
+ conference merge. The sample displays its `dn`, then its participant/media key
966
+ as a fallback, and deduplicates the row once it joins the main leg. The owner can
967
+ see a disabled Drop action until merge; other active main-call agents see the
968
+ same row without an action. Consult-only Agent targets remain excluded.
969
+
823
970
  ### Wrapup or end
824
971
 
825
972
  ```mermaid
@@ -1270,7 +1417,7 @@ await cc.stationLogin({ loginOption: 'BROWSER', ... });
1270
1417
  - A shared Task base preserves a stable API while media-specific subclasses and a separate state engine enforce capability differences.
1271
1418
 
1272
1419
  ## Test-Case Strategy (module)
1273
- Use `test/unit/spec/services/task/Task.ts`, `TaskFactory.ts`, `TaskManager.ts`, media-specific suites, contact/dialer suites, and state-machine suites. Cover concrete-versus-interface method signatures, every TASK_EVENTS group, unsupported media rejection, primary/RTD event ownership, injected state actions, preview-campaign accept/skip/remove payloads and failure paths, the disabled skip/remove pre-guards, and success/failure/timeout paths.
1420
+ Use `test/unit/spec/services/task/Task.ts`, `TaskFactory.ts`, `TaskManager.ts`, media-specific suites, contact/dialer suites, and state-machine suites. Cover concrete-versus-interface method signatures, every TASK_EVENTS group, unsupported media rejection, primary/RTD event ownership, injected state actions, preview-campaign accept/skip/remove payloads and failure paths, the disabled skip/remove pre-guards, Participant Drop validation/correlation/privacy, primary-owner promotion propagation, and success/failure/timeout paths.
1274
1421
 
1275
1422
  | Behavior / Requirement | Existing test evidence | Gap |
1276
1423
  |---|---|---|
@@ -1281,6 +1428,9 @@ Use `test/unit/spec/services/task/Task.ts`, `TaskFactory.ts`, `TaskManager.ts`,
1281
1428
  | `TASK-R-005` | `test/unit/spec/services/task/TaskFactory.ts`, `test/unit/spec/services/task/dialer.ts` | None. |
1282
1429
  | `TASK-R-006` | `test/unit/spec/services/task/contact.ts`, `test/unit/spec/services/core/WebexRequest.ts` | Authentication ownership is verified across routing/Core boundaries. |
1283
1430
  | `TASK-R-007` | `test/unit/spec/cc.ts`, `test/unit/spec/services/task/dialer.ts` | Add direct tests proving disabled skip/remove flags throw before dialer invocation; keep public signatures, metrics/error handling, and AQM request contracts synchronized. |
1431
+ | `TASK-R-008` | `test/unit/spec/services/task/Task.ts`, `test/unit/spec/services/task/voice/Voice.ts`, `test/unit/spec/services/task/contact.ts`, `test/unit/spec/services/core/aqm-reqs.ts`, `test/unit/spec/services/task/TaskManager.ts` | None. |
1432
+ | `TASK-R-009` | `test/unit/spec/cc.ts`, `test/unit/spec/services/task/TaskFactory.ts`, `test/unit/spec/services/task/state-machine/uiControlsComputer.ts` | None. |
1433
+ | `TASK-R-010` | `test/unit/spec/services/task/TaskManager.ts`, `test/unit/spec/services/task/Task.ts` | None. |
1284
1434
 
1285
1435
  ## Traceability
1286
1436
  - Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md`
@@ -14,6 +14,8 @@ export const CONSULT = '/consult';
14
14
  export const CONSULT_ACCEPT = '/consult/accept';
15
15
  export const CONSULT_END = '/consult/end';
16
16
  export const TRANSFER = '/transfer';
17
+ /** Backend destination value for direct entry-point transfers through the vteam endpoint. */
18
+ export const ENTRY_POINT_TRANSFER_DESTINATION_TYPE = 'entrypointDialNumber';
17
19
  export const CONSULT_TRANSFER = '/consult/transfer';
18
20
  export const PAUSE = '/record/pause';
19
21
  export const RESUME = '/record/resume';
@@ -73,6 +75,7 @@ export const METHODS = {
73
75
  TRANSFER: 'transfer',
74
76
  CONSULT_TRANSFER: 'consultTransfer',
75
77
  CONSULT_CONFERENCE: 'consultConference',
78
+ DROP_CONFERENCE_PARTICIPANT: 'dropConferenceParticipant',
76
79
  EXIT_CONFERENCE: 'exitConference',
77
80
  TRANSFER_CONFERENCE: 'transferConference',
78
81
  UPDATE_TASK_DATA: 'updateTaskData',
@@ -458,6 +458,36 @@ export default function routingContact(aqm: AqmReqs) {
458
458
  })
459
459
  ),
460
460
 
461
+ /*
462
+ * Drop another participant from an active conference
463
+ */
464
+ dropConferenceParticipant: aqm.req((p: {interactionId: string; participantId: string}) => ({
465
+ url: `${TASK_API}${p.interactionId}/conference/participants/${encodeURIComponent(
466
+ p.participantId
467
+ )}/drop`,
468
+ data: {},
469
+ host: WCC_API_GATEWAY,
470
+ err,
471
+ redactSensitiveLogs: true,
472
+ notifSuccess: {
473
+ bind: {
474
+ type: TASK_MESSAGE_TYPE,
475
+ data: {
476
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
477
+ interactionId: p.interactionId,
478
+ },
479
+ },
480
+ msg: {} as Contact.AgentContact,
481
+ },
482
+ notifFail: {
483
+ bind: {
484
+ type: TASK_MESSAGE_TYPE,
485
+ data: {type: CC_EVENTS.PARTICIPANT_DROP_CONFERENCE_FAILED},
486
+ },
487
+ errId: 'Service.aqm.task.dropConferenceParticipant',
488
+ },
489
+ })),
490
+
461
491
  /*
462
492
  * Exit conference
463
493
  */
@@ -1,6 +1,6 @@
1
1
  import {CC_FILE, METHODS} from '../../../constants';
2
2
  import {getErrorDetails} from '../../core/Utils';
3
- import {IDigital, TaskResponse, TaskData} from '../types';
3
+ import {ConsultTransferDestinationConfig, IDigital, TaskResponse, TaskData} from '../types';
4
4
  import {WrapupData} from '../../config/types';
5
5
  import Task from '../Task';
6
6
  import routingContact from '../contact';
@@ -13,7 +13,8 @@ export default class Digital extends Task implements IDigital {
13
13
  contact: ReturnType<typeof routingContact>,
14
14
  data: TaskData,
15
15
  wrapupData?: WrapupData,
16
- agentId?: string
16
+ agentId?: string,
17
+ consultTransferConfig?: ConsultTransferDestinationConfig
17
18
  ) {
18
19
  super(
19
20
  contact,
@@ -22,6 +23,7 @@ export default class Digital extends Task implements IDigital {
22
23
  isEndTaskEnabled: true,
23
24
  isEndConsultEnabled: false,
24
25
  isRecordingEnabled: false,
26
+ consultTransferConfig,
25
27
  },
26
28
  wrapupData,
27
29
  agentId