@webex/contact-center 3.12.0-next.121 → 3.12.0-next.123

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.
@@ -310,7 +310,7 @@
310
310
  "canonical_spec": "src/services/task/ai-docs/task-spec.md",
311
311
  "contracts": {
312
312
  "provides": [
313
- "Task/Voice/WebRTC/Digital lifecycle APIs; TaskManager lifecycle correlation, narrowly scoped backend-authoritative ContactOwnerChanged recovery, and task-event publication, including routing owner-changing ContactUpdated through the existing task:hydrate event; task events/types; ordered action-specific consult/transfer destination controls; call-control and dialer operations including acceptPreviewContact, skipPreviewContact, and removePreviewContact"
313
+ "Task/Voice/WebRTC/Digital lifecycle APIs; TaskManager lifecycle correlation, narrowly scoped backend-authoritative ContactOwnerChanged recovery, and task-event publication, including routing owner-changing ContactUpdated through the existing task:hydrate event; TaskManager/Voice stamp wrapUpRequired on wrap-up ingress so task:wrapup and profile-gated auto wrap-up can start; task events/types; ordered action-specific consult/transfer destination controls; call-control and dialer operations including acceptPreviewContact, skipPreviewContact, and removePreviewContact"
314
314
  ],
315
315
  "requires": [
316
316
  "Contact/dialer AqmReqs; WebSocket managers; @webex/calling; MetricsManager; XState task-state module"
@@ -23,7 +23,7 @@
23
23
  | Contract ID | Owner module | Event / topic | Direction | Payload schema link | Delivery guarantees | Compatibility / deprecation | Defined at |
24
24
  |---|---|---|---|---|---|---|---|
25
25
  | `agent.events` | Agent | `agent:*` | publish to application | agent spec/types | realtime; backend ordering/correlation | additive constants | `src/services/agent/types.ts` |
26
- | `task.events` | Task | `task:*`, including existing `task:hydrate` | consume/map backend task lifecycle events, then publish to application/task | task spec `TASK-R-010` / types | `ContactOwnerChanged` and owner-changing `ContactUpdated` route through `CONTACT_OWNER_CHANGED`; a complete promoted-current-Agent owner change may narrowly recover a missing task; consumers receive one `task:hydrate` and no synthetic `task:incoming` | compatible semantic extension of an existing event; no new event constant; backend `interaction.owner` remains authoritative | `src/services/task/TaskManager.ts`, `src/services/task/Task.ts`, `src/services/task/types.ts` |
26
+ | `task.events` | Task | `task:*`, including existing `task:hydrate` and `task:wrapup` | consume/map backend task lifecycle events, then publish to application/task | task spec `TASK-R-010` / `TASK-R-011` / types | `ContactOwnerChanged` and owner-changing `ContactUpdated` route through `CONTACT_OWNER_CHANGED`; a complete promoted-current-Agent owner change may narrowly recover a missing task; consumers receive one `task:hydrate` and no synthetic `task:incoming`. TaskManager stamps `wrapUpRequired` once at ingress (`AGENT_WRAPUP`, conference-end/leave/transfer when this agent should wrap; HTTP `exitConference` on Voice uses the same empty-pending self-exit fallback as leave — true unless this agent is a consulted non-owner — and pending-list precedence uses the correlated response only) so consumers may receive `task:wrapup` and `task.autoWrapup` on those events. `PARTICIPANT_LEAVE` into `WRAPPING_UP` does not transition-emit; the `WRAPPING_UP` entry still emits `task:wrapup`. A nonempty `agentsPendingWrapUp` wins over transfer initiator signals (`consultingAgentId` / `transferConferenceRequested`); a named other `participantId` is never self-leave; leave wrap-up is not stamped when the actor ignores `PARTICIPANT_LEAVE` (`CONNECTED`, `HOLD_INITIATING`, `CONF_INITIATING`); leave-self with empty/absent pending stamps true unless this agent is a consulted non-owner; remaining conference agents stay unstamped. Profile-gated auto wrap-up still requires `autoWrapup !== false`, interval, and a default reason | compatible semantic extension of existing events; no new event constant; backend `interaction.owner` remains authoritative | `src/services/task/TaskManager.ts`, `src/services/task/Task.ts`, `src/services/task/voice/Voice.ts`, `src/services/task/types.ts` |
27
27
  | `task.wxapp-mute.events` | Task | `task:wxapp-mute-state-updated` | publish to application/task | task spec/types | event-driven | additive | `src/services/task/types.ts`, `src/services/task/voice/Voice.ts` |
28
28
  | `cc.events` | Config/Core | backend CC_EVENTS | consume/map | config/core specs | remote WebSocket delivery; AQM correlation where configured | backend contract | `src/services/config/types.ts` |
29
29
  | `rtd.events` | Task | realtime transcription/suggestion | consume then publish per task | task spec | websocket best-effort according to remote service | additive payloads | `src/services/task/TaskManager.ts` |
@@ -23,6 +23,8 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
23
23
  const CC_EVENT_SET = new Set(Object.values(_types2.CC_EVENTS));
24
24
  const MAIN_INTERACTION_CORRELATED_EVENTS = new Set([_types2.CC_EVENTS.AGENT_CONSULT_ENDED, _types2.CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE]);
25
25
  const isCcEvent = value => CC_EVENT_SET.has(value);
26
+ const WRAP_UP_STAMP_EVENTS = new Set([_types2.CC_EVENTS.AGENT_WRAPUP, _types2.CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, _types2.CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, _types2.CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED]);
27
+ const PARTICIPANT_LEAVE_WRAP_STATES = new Set([_stateMachine.TaskState.HELD, _stateMachine.TaskState.RESUME_INITIATING, _stateMachine.TaskState.CONSULTING, _stateMachine.TaskState.CONSULT_INITIATING, _stateMachine.TaskState.CONFERENCING]);
26
28
 
27
29
  /** @internal */
28
30
  class TaskManager extends _events.default {
@@ -140,6 +142,138 @@ class TaskManager extends _events.default {
140
142
  this.webCallingService.off(_calling.LINE_EVENTS.INCOMING_CALL, this.handleIncomingWebCall);
141
143
  }
142
144
 
145
+ /**
146
+ * True when this agent left on the current payload.
147
+ * A named other participantId is never self-leave (even if the participants
148
+ * map is partial). Missing-map fallback is EP-DN only: participantId absent
149
+ * and self omitted now, with previous-task evidence when a prior map exists.
150
+ */
151
+ static didSelfLeaveConference(payload, agentId, task) {
152
+ if (!agentId) {
153
+ return false;
154
+ }
155
+ if (payload.participantId === agentId) {
156
+ return true;
157
+ }
158
+ if (payload.participantId && payload.participantId !== agentId) {
159
+ return false;
160
+ }
161
+ const participants = payload.interaction?.participants;
162
+ if (!participants || agentId in participants) {
163
+ return false;
164
+ }
165
+ const previousParticipants = task?.data?.interaction?.participants;
166
+ if (previousParticipants && !(agentId in previousParticipants)) {
167
+ return false;
168
+ }
169
+ return true;
170
+ }
171
+
172
+ /**
173
+ * Consulted non-owner: Voice exitConference / shouldWrapUpForThisAgent stays
174
+ * false unless an explicit wrap-up signal includes this agent.
175
+ */
176
+ static isConsultedNonOwner(payload, agentId, task) {
177
+ const isConsulted = payload.isConsulted ?? task?.data?.isConsulted;
178
+ if (isConsulted !== true) {
179
+ return false;
180
+ }
181
+ const owner = payload.interaction?.owner ?? task?.data?.interaction?.owner;
182
+ return Boolean(agentId && owner && owner !== agentId);
183
+ }
184
+ static hasNonemptyPendingWrapUp(payload) {
185
+ return Array.isArray(payload.agentsPendingWrapUp) && payload.agentsPendingWrapUp.length > 0;
186
+ }
187
+
188
+ /**
189
+ * Transfer initiator fallback: widgets set transferConferenceRequested on the
190
+ * actor; desktop success payloads may include consultingAgentId. Only used
191
+ * when agentsPendingWrapUp is empty or absent.
192
+ */
193
+ static isSelfConferenceTransferInitiator(payload, agentId, task) {
194
+ const snapshotContext = task?.stateMachineService?.getSnapshot?.()?.context;
195
+ if (snapshotContext?.transferConferenceRequested === true) {
196
+ return true;
197
+ }
198
+ return Boolean(agentId && payload.consultingAgentId === agentId);
199
+ }
200
+
201
+ /**
202
+ * Explicit wrap-up signals only (no owner / isConsulted fallback).
203
+ * Used for events that remaining conference agents also receive.
204
+ * nonempty agentsPendingWrapUp is authoritative, then wrapUpRequired / isWrapUp.
205
+ */
206
+ static wrapUpRequiredFromExplicitSignals(payload, agentId) {
207
+ const pending = payload.agentsPendingWrapUp;
208
+ if (Array.isArray(pending) && pending.length > 0) {
209
+ return Boolean(agentId && pending.includes(agentId));
210
+ }
211
+ if (payload.wrapUpRequired === true) {
212
+ return true;
213
+ }
214
+ if (!agentId) {
215
+ return false;
216
+ }
217
+ return payload.interaction?.participants?.[agentId]?.isWrapUp === true;
218
+ }
219
+
220
+ /**
221
+ * Stamp wrapUpRequired once at ingress (prepareEventContext) so updateTaskData
222
+ * and the mapper see the same flag. Remaining agents stay false.
223
+ */
224
+ static stampWrapUpRequiredForEvent(eventType, payload, agentId, task) {
225
+ if (!WRAP_UP_STAMP_EVENTS.has(eventType)) {
226
+ return payload;
227
+ }
228
+ if (eventType === _types2.CC_EVENTS.AGENT_WRAPUP) {
229
+ return {
230
+ ...payload,
231
+ wrapUpRequired: true
232
+ };
233
+ }
234
+ if (eventType === _types2.CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED) {
235
+ return {
236
+ ...payload,
237
+ wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId)
238
+ };
239
+ }
240
+ if (eventType === _types2.CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
241
+ const actorState = task?.stateMachineService?.getSnapshot?.()?.value;
242
+ if (actorState && !PARTICIPANT_LEAVE_WRAP_STATES.has(actorState)) {
243
+ return payload;
244
+ }
245
+ const left = TaskManager.didSelfLeaveConference(payload, agentId, task);
246
+ if (!left) {
247
+ return {
248
+ ...payload,
249
+ wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId)
250
+ };
251
+ }
252
+ if (TaskManager.hasNonemptyPendingWrapUp(payload)) {
253
+ return {
254
+ ...payload,
255
+ wrapUpRequired: Boolean(agentId && payload.agentsPendingWrapUp?.includes(agentId))
256
+ };
257
+ }
258
+ return {
259
+ ...payload,
260
+ wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId) || !TaskManager.isConsultedNonOwner(payload, agentId, task)
261
+ };
262
+ }
263
+
264
+ // AGENT_CONFERENCE_TRANSFERRED — pending list wins; initiator only if empty/absent
265
+ if (TaskManager.hasNonemptyPendingWrapUp(payload)) {
266
+ return {
267
+ ...payload,
268
+ wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId)
269
+ };
270
+ }
271
+ return {
272
+ ...payload,
273
+ wrapUpRequired: TaskManager.wrapUpRequiredFromExplicitSignals(payload, agentId) || TaskManager.isSelfConferenceTransferInitiator(payload, agentId, task)
274
+ };
275
+ }
276
+
143
277
  /**
144
278
  * Map WebSocket CC_EVENTS to state machine TaskEvent
145
279
  * @param ccEvent - The CC_EVENT type from WebSocket
@@ -594,6 +728,9 @@ class TaskManager extends _events.default {
594
728
  if (task && eventType === _types2.CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
595
729
  adjustedPayload = TaskManager.preserveConfirmedOwner(task, adjustedPayload);
596
730
  }
731
+ if (WRAP_UP_STAMP_EVENTS.has(eventType)) {
732
+ adjustedPayload = TaskManager.stampWrapUpRequiredForEvent(eventType, adjustedPayload, this.agentId, task);
733
+ }
597
734
  const stateMachineEvent = TaskManager.mapEventToTaskStateMachineEvent(eventType, adjustedPayload, this.agentId, task, this.configFlags?.enableWxBetterTogether);
598
735
  _loggerProxy.default.info(`Handling task event ${eventType}`, {
599
736
  module: _constants.TASK_MANAGER_FILE,