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

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 +5 -1
  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 +176 -20
  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 +1 -1
  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 +254 -15
  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 +117 -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 +46 -22
  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 +653 -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
@@ -74,10 +74,11 @@ See root `CONTRACTS.md` for the package-level state-control export.
74
74
  | TASK_STATE_MACHINE-R-001 | Map typed Task events through the XState graph and preserve guards/actions for offer, assignment, consult, conference, transfer, wrapup, termination, and hydration. | A deterministic event vocabulary isolates lifecycle policy from transport payloads. | `src/services/task/state-machine/TaskStateMachine.ts` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
75
75
  | TASK_STATE_MACHINE-R-002 | Keep `handleConferenceFailed`, `handleSwitchToMainCall`, and `handleSwitchToConsult` wired where the graph invokes them; retain `forceConsultInitiator` as defined-but-currently-unwired. | Incorrect absence/wiring claims cause maintainers to duplicate or remove real actions. | `src/services/task/state-machine/actions.ts` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
76
76
  | TASK_STATE_MACHINE-R-003 | Treat `syncTaskDataFromEvent` as a Task-supplied machine implementation, not a default action in `actions.ts`. | The reusable graph declares the action name while Task owns integration-specific data synchronization. | `src/services/task/Task.ts` | `test/unit/spec/services/task/Task.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
77
- | TASK_STATE_MACHINE-R-004 | Compute UI controls through the real private voice/digital helper names and preserve the public `getDefaultUIControls` shape. | Applications depend on stable control state while implementation helpers remain private. | `src/services/task/state-machine/uiControlsComputer.ts` | `test/unit/spec/services/task/state-machine/uiControlsComputer.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
77
+ | TASK_STATE_MACHINE-R-004 | Compute per-leg action controls and ordered `consultTransferDestinations.consult`/`.transfer` arrays through `uiControlsComputer`, preserving the public `getDefaultUIControls` shape with empty destination arrays. Voice queue rules use Consult profile enablement or Transfer direction plus `callProcessingDetails.outdialTransferToQueueEnabled`; digital supports agent/queue only; profile `NONE` removes the matching category. | Applications depend on one deterministic Task control surface, the default order (`agent`, `queue`, `dialNumber`, `entryPoint`), and correct action/media/profile gating. | `src/services/task/state-machine/uiControlsComputer.ts` | `test/unit/spec/services/task/state-machine/uiControlsComputer.ts` | Consumers may further hide allowed categories but must not infer or enable omitted categories. | PRESENT |
78
78
  | TASK_STATE_MACHINE-R-005 | Keep authentication and credentials outside the state-machine layer; it receives typed Task data/events and never invokes authenticated transport. | Pure transition logic remains reusable and cannot leak or mutate host authentication state. | `src/services/task/state-machine/TaskStateMachine.ts`, `src/services/task/state-machine/types.ts` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | None; security/auth applicability is explicitly N/A. | PRESENT |
79
79
  | TASK_STATE_MACHINE-R-006 | Treat `UIControlConfig` values as Task-supplied capability configuration, not rollout flags evaluated or owned by the state machine. | Rollout and profile policy must be resolved before actor construction so transitions remain deterministic. | `src/services/task/state-machine/types.ts`, `src/services/task/Task.ts` | `test/unit/spec/services/task/Task.ts` | None; rollout ownership is explicit. | PRESENT |
80
80
  | TASK_STATE_MACHINE-R-007 | Keep logging and metrics in Task/TaskManager integration; the state-machine implementation has no LoggerProxy or MetricsManager dependency. | Separating observability side effects from guards/actions preserves deterministic transition tests. | `src/services/task/state-machine/TaskStateMachine.ts`, `src/services/task/Task.ts` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/Task.ts` | None; observability ownership is explicit. | PRESENT |
81
+ | TASK_STATE_MACHINE-R-008 | `CONTACT_OWNER_CHANGED` must synchronize task/context data and emit `task:hydrate` without transitioning state; participant Drop must rely on the existing `ParticipantLeftConference` mapping rather than adding an initiating state event. From every active call-control state, `PARTICIPANT_LEAVE` terminates or wraps the current Agent only when the event names that Agent, marks that Agent `hasLeft`, or removes a previously active Agent from the participant map; an event naming another participant cannot infer self-departure from a partial media roster. `CONSULT_END` evaluates the same explicit evidence before initiator recovery and additionally supports the narrow from-conference nested-consult race where a previously main-leg Agent is absent from the updated `mainCall` but remains active in the participant map and present on the consult leg. Removed accepted Agents emit `task:consultEnd` plus `task:end`, surviving initiators recover to their main call, and an unaccepted OFFERED consultee emits only `task:consultEnd`. Missing, partial, contradictory, or ordinary CONNECTED/HELD media membership is non-terminal, and starting Consult must preserve the prior task snapshot used by the guard. | Owner-sensitive consumers must rerender promptly while participant and consult removal remain backend-authoritative, state-independent, and compatible with existing incoming-task callback behavior. | `src/services/task/state-machine/TaskStateMachine.ts`, `src/services/task/state-machine/actions.ts`, `src/services/task/state-machine/guards.ts`, `src/services/task/TaskManager.ts` | `test/unit/spec/services/task/Task.ts`, `test/unit/spec/services/task/TaskManager.ts`, `test/unit/spec/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/state-machine/guards.ts` | Backend ownership-successor selection and event delivery are outside this module. | PRESENT |
81
82
 
82
83
  ## Design Overview
83
84
  TaskManager maps Contact Center notifications to `TaskEvent` values. Each Task sends those events to its XState actor built by `createTaskStateMachine()`. The configuration applies guards and named actions, updates `TaskContext`, and computes UI controls. Task supplies the integration-specific `syncTaskDataFromEvent` implementation through machine options; it is not a default action in `actions.ts`.
@@ -507,14 +508,18 @@ shouldWrapUpOrIsInitiator(context, event) {
507
508
  return Boolean(event.taskData?.wrapUpRequired || context.consultInitiator);
508
509
  }
509
510
 
510
- // Check whether the leaving participant is the current agent
511
- didCurrentAgentLeaveConference(context, event) {
511
+ // Check whether the current agent left the main interaction
512
+ didCurrentAgentLeaveMainInteraction(context, event) {
512
513
  const selfAgentId = getSelfAgentId(context, event.taskData);
513
514
  if (!selfAgentId) return false;
514
515
 
515
516
  const participantIdFromEvent = 'participantId' in event ? event.participantId : undefined;
516
517
  const participantId = participantIdFromEvent ?? event.taskData?.participantId;
517
- return Boolean(participantId) && participantId === selfAgentId;
518
+ if (Boolean(participantId) && participantId === selfAgentId) return true;
519
+ // Explicit hasLeft or removal of a previously active self from the participant map is terminal.
520
+ // PARTICIPANT_LEAVE naming another participant does not infer self departure from media.
521
+ // Only a from-conference CONSULT_END may compare mainCall membership, and only when self
522
+ // remains active in the participant map and on the consult leg. Partial ordinary calls are false.
518
523
  }
519
524
 
520
525
  // True when this agent initiated the conference transfer (widgets or desktop).
@@ -859,9 +864,15 @@ It is instantiated by `Task` and receives mapped backend/user events through `se
859
864
 
860
865
  - Actions: `updateTaskData`, `setConsultAgentJoined`, `emitTaskConsultAccepted`, `emitTaskConsulting`
861
866
 
867
+ - `TASK_WRAPUP` -> `WRAPPING_UP`
868
+
869
+ - Guard: `guards.shouldWrapUp`
870
+
871
+ - Actions: `updateTaskData`, `markEnded`, `emitTaskWrapup`
872
+
862
873
  - `TASK_WRAPUP` -> `TERMINATED`
863
874
 
864
- - Guard: none
875
+ - Guard: default (when wrap-up is not required)
865
876
 
866
877
  - Actions: `updateTaskData`, `markEnded`, `emitTaskEnd`
867
878
 
@@ -1311,19 +1322,19 @@ It is instantiated by `Task` and receives mapped backend/user events through `se
1311
1322
 
1312
1323
  - `PARTICIPANT_LEAVE` -> `WRAPPING_UP`
1313
1324
 
1314
- - Guard: `guards.didCurrentAgentLeaveConference && guards.shouldWrapUp`
1325
+ - Guard: `guards.didCurrentAgentLeaveMainInteraction && guards.shouldWrapUp`
1315
1326
 
1316
1327
  - Actions: `updateTaskData`, `handleParticipantLeft`, `markEnded`, `clearConsultState`, `emitTaskParticipantLeft`, `emitTaskWrapup`
1317
1328
 
1318
1329
  - `PARTICIPANT_LEAVE` -> `TERMINATED`
1319
1330
 
1320
- - Guard: `guards.didCurrentAgentLeaveConference`
1331
+ - Guard: `guards.didCurrentAgentLeaveMainInteraction`
1321
1332
 
1322
1333
  - Actions: `updateTaskData`, `handleParticipantLeft`, `markEnded`, `clearConsultState`, `emitTaskParticipantLeft`, `emitTaskEnd`
1323
1334
 
1324
1335
  - `PARTICIPANT_LEAVE` -> `CONNECTED`
1325
1336
 
1326
- - Guard: `!guards.didCurrentAgentLeaveConference && guards.shouldDowngradeConferenceToConnected`
1337
+ - Guard: `guards.shouldDowngradeConferenceToConnected` (after current-agent departure guards)
1327
1338
 
1328
1339
  - Actions: `updateTaskData`, `handleParticipantLeft`, `clearConsultState`, `emitTaskParticipantLeft`, `emitTaskConferenceEnded`
1329
1340
 
@@ -1491,13 +1502,13 @@ It is instantiated by `Task` and receives mapped backend/user events through `se
1491
1502
 
1492
1503
  - `PARTICIPANT_LEAVE` -> `WRAPPING_UP`
1493
1504
 
1494
- - Guard: `guards.didCurrentAgentLeaveConference && guards.shouldWrapUp`
1505
+ - Guard: `guards.didCurrentAgentLeaveMainInteraction && guards.shouldWrapUp`
1495
1506
 
1496
1507
  - Actions: `updateTaskData`, `handleParticipantLeft`, `markEnded`, `clearConsultState`, `emitTaskParticipantLeft`, `emitTaskWrapup`
1497
1508
 
1498
1509
  - `PARTICIPANT_LEAVE` -> `TERMINATED`
1499
1510
 
1500
- - Guard: `guards.didCurrentAgentLeaveConference`
1511
+ - Guard: `guards.didCurrentAgentLeaveMainInteraction`
1501
1512
 
1502
1513
  - Actions: `updateTaskData`, `handleParticipantLeft`, `markEnded`, `clearConsultState`, `emitTaskParticipantLeft`, `emitTaskEnd`
1503
1514
 
@@ -1513,6 +1524,14 @@ It is instantiated by `Task` and receives mapped backend/user events through `se
1513
1524
 
1514
1525
  - Actions: `updateTaskData`, `handleParticipantLeft`, `emitTaskParticipantLeft`
1515
1526
 
1527
+ Customer hangup and agent-initiated customer Drop share this backend event.
1528
+ When two or more active conference participants survive on the main leg, the
1529
+ customer is removed from the authoritative snapshot while the task remains
1530
+ `CONFERENCING`; no `task:conferenceEnded` event is emitted. When another
1531
+ agent/endpoint leaves, the same rule preserves the remaining conference, and
1532
+ the guarded `CONNECTED` branch is used only after the conference participant
1533
+ count falls below two while the customer remains.
1534
+
1516
1535
  - `CONFERENCE_END` -> `WRAPPING_UP`
1517
1536
 
1518
1537
  - Guard: `guards.shouldWrapUp`
@@ -1561,7 +1580,7 @@ It is instantiated by `Task` and receives mapped backend/user events through `se
1561
1580
 
1562
1581
  **Description**: Final wrapped-up terminal state.
1563
1582
 
1564
- **Entry Actions**:
1583
+ **Entry Actions** (order matters — `emitTaskWrappedup` before `cleanupResources` so subscribers receive intact task data):
1565
1584
 
1566
1585
  - `emitTaskWrappedup`
1567
1586
 
@@ -1731,7 +1750,7 @@ Complete mapping from backend CC_EVENTS to internal TaskEvent types.
1731
1750
  | `AGENT_OFFER_CONTACT` | `TASK_OFFERED` | `OFFERED` | `OFFERED` | Offer payload refresh |
1732
1751
  | `AGENT_CONTACT` | `HYDRATE` | `IDLE` | `WRAPPING_UP` / `CONSULTING` / `HELD` / `CONNECTED` / `CONFERENCING` / `IDLE` | Guard-based restore |
1733
1752
  | `CONTACT_UPDATED` | `CONTACT_UPDATED` | any | same | Context sync |
1734
- | `CONTACT_OWNER_CHANGED` | `CONTACT_OWNER_CHANGED` | any | same | Context sync |
1753
+ | `CONTACT_OWNER_CHANGED` | `CONTACT_OWNER_CHANGED` | any | same | Context/data sync + `task:hydrate` |
1735
1754
  | `AGENT_OFFER_CONSULT` | `OFFER_CONSULT` | `OFFERED` | `OFFERED` | Receiver-side consult offer |
1736
1755
  | `AGENT_CONTACT_ASSIGNED` | `ASSIGN` | `OFFERED` / `CONNECTED` / `CONSULTING` | `CONNECTED` | Assign/reassign |
1737
1756
  | `AGENT_CONTACT_HELD` | `HOLD_SUCCESS` | `HOLD_INITIATING` | `HELD` | Includes `mediaResourceId` |
@@ -1746,16 +1765,16 @@ Complete mapping from backend CC_EVENTS to internal TaskEvent types.
1746
1765
  | `AGENT_BLIND_TRANSFERRED` | `TRANSFER_SUCCESS` | `CONNECTED` / `HELD` / `CONSULTING` | `WRAPPING_UP` / `CONNECTED` | `shouldWrapUpOrIsInitiator` |
1747
1766
  | `AGENT_CONSULT_TRANSFERRED` | `TRANSFER_SUCCESS` | `CONNECTED` / `HELD` / `CONSULTING` | `WRAPPING_UP` / `CONNECTED` | Same path |
1748
1767
  | `AGENT_VTEAM_TRANSFERRED` | `TRANSFER_SUCCESS` | `CONNECTED` / `HELD` / `CONSULTING` | `WRAPPING_UP` / `CONNECTED` | Same path |
1749
- | `AGENT_WRAPUP` | `TASK_WRAPUP` | `OFFERED` / `CONNECTED` / `HELD` / `CONSULTING` | `TERMINATED` / `WRAPPING_UP` | `OFFERED` terminates; others wrap |
1768
+ | `AGENT_WRAPUP` | `TASK_WRAPUP` | `OFFERED` / `CONNECTED` / `HELD` / `CONSULTING` | `WRAPPING_UP` / `TERMINATED` | `shouldWrapUp` selects wrap-up; default terminates (wxApp outdial pre-accept agent end may wrap while still `OFFERED`) |
1750
1769
  | `AGENT_BLIND_TRANSFER_FAILED` | `TRANSFER_FAILED` | `CONNECTED` / `HELD` / `CONSULTING` | same | Context update |
1751
1770
  | `AGENT_VTEAM_TRANSFER_FAILED` | `TRANSFER_FAILED` | `CONNECTED` / `HELD` / `CONSULTING` | same | Context update |
1752
1771
  | `AGENT_CONSULT_TRANSFER_FAILED` | `TRANSFER_FAILED` | `CONNECTED` / `HELD` / `CONSULTING` | same | Context update |
1753
1772
  | `AGENT_CONFERENCE_TRANSFER_FAILED` | `TRANSFER_FAILED` | `CONNECTED` / `HELD` / `CONSULTING` | same | Context update |
1754
- | `CONTACT_ENDED` | `CONTACT_ENDED` | `CONNECTED` / `HELD` / `CONSULTING` / `CONFERENCING` | `CONFERENCING` / `WRAPPING_UP` / `TERMINATED` / same | Guard-driven branch |
1773
+ | `CONTACT_ENDED` | `CONTACT_ENDED` or `OUTBOUND_FAILED` | `OFFERED` / `CONNECTED` / `HELD` / `CONSULTING` / `CONFERENCING` | `WRAPPING_UP` / `TERMINATED` / etc. | wxApp outdial pre-accept decline → `CONTACT_ENDED` → `TERMINATED` (no wrapup) when `wxAppAnswerPending` is false and `agentsPendingWrapUp` is empty (backend may still send `interaction.state: wrapUp`). Post-accept agent end remaps to `OUTBOUND_FAILED` (`AGENT_ENDS`) when `wxAppAnswerPending` or wrapup pending — including while SM is still `OFFERED` before `ASSIGN`. |
1755
1774
  | `AGENT_INVITE_FAILED` | `INVITE_FAILED` | `OFFERED` | `TERMINATED` | Reject path |
1756
1775
  | `AGENT_CONTACT_ASSIGN_FAILED` | `ASSIGN_FAILED` | `OFFERED` | `TERMINATED` | Reject path |
1757
1776
  | `AGENT_CONTACT_OFFER_RONA` | `RONA` | `OFFERED` | `TERMINATED` | Timeout path |
1758
- | `AGENT_OUTBOUND_FAILED` | `OUTBOUND_FAILED` | `OFFERED` | `TERMINATED` | Outbound failure |
1777
+ | `AGENT_OUTBOUND_FAILED` | `OUTBOUND_FAILED` | `IDLE` / `OFFERED` / `CONNECTED` / `HOLD_INITIATING` / `HELD` / `RESUME_INITIATING` / `CONSULT_INITIATING` / `CONSULTING` / `CONF_INITIATING` / `CONFERENCING` | `WRAPPING_UP` or `TERMINATED` | Outbound failure; wrapup when `shouldWrapUp`. Non-wxApp agent-terminated `AGENT_ENDS` sets `suppressOutdialFailedPopup` (wrapup without outdial-failed modal). `IDLE` handles race before `TASK_INCOMING`. |
1759
1778
  | `CONTACT_RECORDING_STARTED` | `RECORDING_STARTED` | any | same | Recording state update |
1760
1779
  | `CONTACT_RECORDING_PAUSED` | `PAUSE_RECORDING` | `CONNECTED` | same | Recording state update |
1761
1780
  | `CONTACT_RECORDING_RESUMED` | `RESUME_RECORDING` | `CONNECTED` | same | Recording state update |
@@ -1776,7 +1795,7 @@ Complete mapping from backend CC_EVENTS to internal TaskEvent types.
1776
1795
  | `AgentContact` | `HYDRATE` | Various | State restoration |
1777
1796
  | `AgentContactAssigned` | `ASSIGN` | OFFERED → CONNECTED (also CONNECTED/CONSULTING refresh paths) | Task accepted/reassigned |
1778
1797
  | `ContactUpdated` | `CONTACT_UPDATED` | No change | Data update only |
1779
- | `ContactOwnerChanged` | `CONTACT_OWNER_CHANGED` | No change | Owner update only |
1798
+ | `ContactOwnerChanged` | `CONTACT_OWNER_CHANGED` | No change | Owner update plus `task:hydrate` emission |
1780
1799
  | `ContactEnded` | `CONTACT_ENDED` | Guard-based branch | CONFERENCING / WRAPPING_UP / TERMINATED / stay |
1781
1800
  | `AgentContactUnassigned` | None | N/A | Handled by other events |
1782
1801
 
@@ -1810,9 +1829,10 @@ Complete mapping from backend CC_EVENTS to internal TaskEvent types.
1810
1829
  | Backend Event / API | TaskEvent | State Transition | Context Update |
1811
1830
  |---|---|---|---|
1812
1831
  | API `task.consultConference()` | `MERGE_TO_CONFERENCE` | CONSULTING → CONF_INITIATING | Starts merge flow |
1832
+ | API `task.dropConferenceParticipant()` | None | No direct state transition | AQM waits for backend `ParticipantLeftConference`; that event follows the existing `PARTICIPANT_LEAVE` path |
1813
1833
  | `AgentConsultConferenced` | `CONFERENCE_START` | CONSULTING/CONF_INITIATING → CONFERENCING | `handleConferenceStarted` path |
1814
1834
  | `ParticipantJoinedConference` | `CONFERENCE_START` | CONFERENCING → CONFERENCING | Refresh + emit conference started |
1815
- | `ParticipantLeftConference` | `PARTICIPANT_LEAVE` | CONSULTING / CONFERENCING → WRAPPING_UP / TERMINATED / CONNECTED / stay | Uses `didCurrentAgentLeaveConference`, `shouldWrapUp`, `shouldDowngradeConferenceToConnected` |
1835
+ | `ParticipantLeftConference` | `PARTICIPANT_LEAVE` | Any active call-control state → WRAPPING_UP / TERMINATED / CONNECTED / stay | Uses `didCurrentAgentLeaveMainInteraction`, `shouldWrapUp`, `shouldDowngradeConferenceToConnected` |
1816
1836
  | `AgentConsultConferenceEnded` | `CONFERENCE_END` | CONFERENCING → WRAPPING_UP / CONNECTED / TERMINATED | Guard-based branch |
1817
1837
  | `AgentConsultConferenceFailed` | `CONFERENCE_FAILED` | CONF_INITIATING → CONSULTING | Merge failed fallback |
1818
1838
  | `AgentConferenceTransferred` | `TRANSFER_CONFERENCE_SUCCESS` | CONSULTING/CONFERENCING branch logic | Initiator/receiver dependent |
@@ -1847,6 +1867,7 @@ stateDiagram-v2
1847
1867
  OFFERED --> TERMINATED: AGENT_CONTACT_ASSIGN_FAILED (CC Event) -> ASSIGN_FAILED (State Machine Event)
1848
1868
  OFFERED --> TERMINATED: AGENT_INVITE_FAILED (CC Event) -> INVITE_FAILED (State Machine Event)
1849
1869
  OFFERED --> TERMINATED: AGENT_OUTBOUND_FAILED (CC Event) -> OUTBOUND_FAILED (State Machine Event)
1870
+ OFFERED --> WRAPPING_UP: AGENT_WRAPUP (CC Event) -> TASK_WRAPUP (State Machine Event) [shouldWrapUp]
1850
1871
  OFFERED --> TERMINATED: AGENT_WRAPUP (CC Event) -> TASK_WRAPUP (State Machine Event)
1851
1872
 
1852
1873
  %% CONNECTED
@@ -1907,8 +1928,8 @@ stateDiagram-v2
1907
1928
 
1908
1929
  %% CONFERENCING
1909
1930
  CONFERENCING --> CONSULT_INITIATING: CONSULT (State Machine Event)
1910
- CONFERENCING --> WRAPPING_UP: PARTICIPANT_LEAVE (CC Event) -> PARTICIPANT_LEAVE (State Machine Event) [didCurrentAgentLeaveConference && shouldWrapUp]
1911
- CONFERENCING --> TERMINATED: PARTICIPANT_LEAVE (CC Event) -> PARTICIPANT_LEAVE (State Machine Event) [didCurrentAgentLeaveConference]
1931
+ CONFERENCING --> WRAPPING_UP: PARTICIPANT_LEAVE (CC Event) -> PARTICIPANT_LEAVE (State Machine Event) [didCurrentAgentLeaveMainInteraction && shouldWrapUp]
1932
+ CONFERENCING --> TERMINATED: PARTICIPANT_LEAVE (CC Event) -> PARTICIPANT_LEAVE (State Machine Event) [didCurrentAgentLeaveMainInteraction]
1912
1933
  CONFERENCING --> CONNECTED: PARTICIPANT_LEAVE (CC Event) -> PARTICIPANT_LEAVE (State Machine Event) [shouldDowngradeConferenceToConnected]
1913
1934
  CONFERENCING --> WRAPPING_UP: CONFERENCE_END (CC Event) -> CONFERENCE_END (State Machine Event) [shouldWrapUp]
1914
1935
  CONFERENCING --> CONNECTED: CONFERENCE_END (CC Event) -> CONFERENCE_END (State Machine Event) [customerInCall]
@@ -1930,7 +1951,9 @@ stateDiagram-v2
1930
1951
  OFFERED --> OFFERED: AGENT_CONTACT_OFFER -> TASK_OFFERED
1931
1952
  OFFERED --> OFFERED: AGENT_CONSULT_OFFER -> OFFER_CONSULT
1932
1953
  OFFERED --> CONNECTED: AGENT_CONTACT_ASSIGNED -> ASSIGN
1933
- OFFERED --> TERMINATED: AGENT_CONTACT_OFFER_RONA/AGENT_CONTACT_ASSIGN_FAILED/AGENT_INVITE_FAILED/AGENT_OUTBOUND_FAILED/AGENT_WRAPUP -> RONA/ASSIGN_FAILED/INVITE_FAILED/OUTBOUND_FAILED/TASK_WRAPUP
1954
+ OFFERED --> TERMINATED: AGENT_CONTACT_OFFER_RONA/AGENT_CONTACT_ASSIGN_FAILED/AGENT_INVITE_FAILED/AGENT_OUTBOUND_FAILED -> RONA/ASSIGN_FAILED/INVITE_FAILED/OUTBOUND_FAILED
1955
+ OFFERED --> WRAPPING_UP: AGENT_WRAPUP -> TASK_WRAPUP [shouldWrapUp]
1956
+ OFFERED --> TERMINATED: AGENT_WRAPUP -> TASK_WRAPUP
1934
1957
 
1935
1958
  CONNECTED --> HOLD_INITIATING: task.hold() -> HOLD_INITIATED
1936
1959
  CONNECTED --> CONSULT_INITIATING: task.consult() -> CONSULT
@@ -2012,8 +2035,8 @@ stateDiagram-v2
2012
2035
  CONFERENCING --> CONFERENCING: AGENT_CONFERENCE_TRANSFERRED -> TRANSFER_CONFERENCE_SUCCESS
2013
2036
  CONFERENCING --> CONFERENCING: AGENT_CONFERENCE_TRANSFER_FAILED-> TRANSFER_CONFERENCE_FAILED
2014
2037
 
2015
- CONFERENCING --> WRAPPING_UP: PARTICIPANT_LEFT_CONFERENCE -> PARTICIPANT_LEAVE [didCurrentAgentLeaveConference && shouldWrapUp]
2016
- CONFERENCING --> TERMINATED: PARTICIPANT_LEFT_CONFERENCE -> PARTICIPANT_LEAVE [didCurrentAgentLeaveConference]
2038
+ CONFERENCING --> WRAPPING_UP: PARTICIPANT_LEFT_CONFERENCE -> PARTICIPANT_LEAVE [didCurrentAgentLeaveMainInteraction && shouldWrapUp]
2039
+ CONFERENCING --> TERMINATED: PARTICIPANT_LEFT_CONFERENCE -> PARTICIPANT_LEAVE [didCurrentAgentLeaveMainInteraction]
2017
2040
  CONFERENCING --> CONNECTED: PARTICIPANT_LEFT_CONFERENCE -> PARTICIPANT_LEAVE [shouldDowngradeConferenceToConnected]
2018
2041
  CONFERENCING --> WRAPPING_UP: AGENT_CONSULT_CONFERENCE_ENDED -> CONFERENCE_END [shouldWrapUp]
2019
2042
  CONFERENCING --> CONNECTED: AGENT_CONSULT_CONFERENCE_ENDED -> CONFERENCE_END [customerInCall]
@@ -2139,6 +2162,7 @@ Use `test/unit/spec/services/task/state-machine/TaskStateMachine.ts`, `guards.ts
2139
2162
  | `TASK_STATE_MACHINE-R-005` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | Transport/auth absence is also verified from imports. |
2140
2163
  | `TASK_STATE_MACHINE-R-006` | `test/unit/spec/services/task/Task.ts` | None. |
2141
2164
  | `TASK_STATE_MACHINE-R-007` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/Task.ts` | Observability absence is also verified from imports. |
2165
+ | `TASK_STATE_MACHINE-R-008` | `test/unit/spec/services/task/Task.ts`, `test/unit/spec/services/task/TaskManager.ts` | None. |
2142
2166
 
2143
2167
  ## Traceability
2144
2168
  - Repo architecture: `../../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../../ai-docs/SPEC_INDEX.md`
@@ -362,13 +362,14 @@ export const guards = {
362
362
  },
363
363
 
364
364
  /**
365
- * True if PARTICIPANT_LEAVE indicates that *this* agent left the conference.
365
+ * True when an updated lifecycle event shows that this agent left the main interaction.
366
366
  *
367
- * Important: PARTICIPANT_LEAVE is broadcast to all agents in the conference.
368
- * Only the agent whose id matches the leaving participant should transition to
369
- * TERMINATED / WRAPPING_UP based on wrapup rules.
367
+ * Explicit self-participant evidence is authoritative. Main-call membership is used only
368
+ * for the from-conference nested-consult race where CONSULT_END still represents the agent as
369
+ * active on the consult leg after removing them from the main leg. Partial or ordinary call
370
+ * snapshots are not treated as a departure.
370
371
  */
371
- didCurrentAgentLeaveConference: ({context, event}: GuardParams): boolean => {
372
+ didCurrentAgentLeaveMainInteraction: ({context, event}: GuardParams): boolean => {
372
373
  const taskData = getTaskDataFromEvent(event);
373
374
  const selfAgentId = getSelfAgentId(context, taskData);
374
375
  if (!selfAgentId) return false;
@@ -383,12 +384,64 @@ export const guards = {
383
384
  return true;
384
385
  }
385
386
 
386
- // For EP-DN agents the backend removes the leaving participant entirely
387
- // from the participants map (rather than setting hasLeft). If this task
388
- // is in CONFERENCING (implied by the guard being evaluated here) but the
389
- // agent is absent from the updated participants, they have left.
390
- const participants = taskData?.interaction?.participants;
391
- if (participants && !(selfAgentId in participants)) {
387
+ // Routing represents self-departure either by naming the participant or by retaining the
388
+ // participant entry with hasLeft set. Agent Desktop treats both representations as terminal.
389
+ const currentParticipant = taskData?.interaction?.participants?.[selfAgentId];
390
+ if (currentParticipant?.hasLeft === true) {
391
+ return true;
392
+ }
393
+
394
+ // A participant-left event that explicitly identifies somebody else must not use a
395
+ // potentially partial participant or media roster to infer that self also departed.
396
+ if (
397
+ event?.type === TaskEvent.PARTICIPANT_LEAVE &&
398
+ Boolean(participantId) &&
399
+ participantId !== selfAgentId
400
+ ) {
401
+ return false;
402
+ }
403
+
404
+ const previousParticipants = context.taskData?.interaction?.participants;
405
+ const updatedParticipants = taskData?.interaction?.participants;
406
+ const wasPreviouslyActive = Boolean(
407
+ previousParticipants?.[selfAgentId] && previousParticipants[selfAgentId].hasLeft !== true
408
+ );
409
+ // Some EP-DN lifecycle payloads remove the participant entry instead of setting hasLeft.
410
+ // Requiring a previously active entry prevents an incomplete initial snapshot from ending a task.
411
+ if (updatedParticipants && wasPreviouslyActive && !(selfAgentId in updatedParticipants)) {
412
+ return true;
413
+ }
414
+
415
+ const previousMainMedia = Object.values(context.taskData?.interaction?.media ?? {}).filter(
416
+ (media) => media?.mType === 'mainCall'
417
+ );
418
+ const updatedMainMedia = Object.values(taskData?.interaction?.media ?? {}).filter(
419
+ (media) => media?.mType === 'mainCall'
420
+ );
421
+ const wasPreviouslyInMainCall = previousMainMedia.some((media) =>
422
+ media.participants?.includes(selfAgentId)
423
+ );
424
+ const remainsInMainCall = updatedMainMedia.some((media) =>
425
+ media.participants?.includes(selfAgentId)
426
+ );
427
+ // A hasLeft participant returned above, so presence here means the updated map keeps self active.
428
+ const remainsActiveInParticipantMap = Boolean(currentParticipant);
429
+ const remainsOnConsultLeg = Object.values(taskData?.interaction?.media ?? {}).some(
430
+ (media) => media?.mType === MEDIA_TYPE_CONSULT && media.participants?.includes(selfAgentId)
431
+ );
432
+
433
+ // Agent Desktop uses mainCall membership for AgentConsultEnded cleanup. Restrict that fallback
434
+ // to a consult that originated from conference and require corroborating active-map and
435
+ // consult-leg membership so ordinary partial CONNECTED/HELD payloads cannot terminate a task.
436
+ if (
437
+ event?.type === TaskEvent.CONSULT_END &&
438
+ context.consultFromConference === true &&
439
+ updatedMainMedia.length > 0 &&
440
+ wasPreviouslyInMainCall &&
441
+ !remainsInMainCall &&
442
+ remainsActiveInParticipantMap &&
443
+ remainsOnConsultLeg
444
+ ) {
392
445
  return true;
393
446
  }
394
447
 
@@ -6,7 +6,14 @@
6
6
  */
7
7
 
8
8
  import type {AnyStateNodeConfig, ActionFunctionMap, EventObject, ActionArgs} from 'xstate';
9
- import {DestinationType, TaskChannelType, TaskData, TaskUIControls, VoiceVariant} from '../types';
9
+ import {
10
+ ConsultTransferDestinationConfig,
11
+ DestinationType,
12
+ TaskChannelType,
13
+ TaskData,
14
+ TaskUIControls,
15
+ VoiceVariant,
16
+ } from '../types';
10
17
  import {TaskEvent, TaskState} from './constants';
11
18
 
12
19
  /**
@@ -43,6 +50,14 @@ export interface UIControlConfig {
43
50
  isRecordingEnabled: boolean;
44
51
  /** Current agent ID for ownership checks (transfer conference) */
45
52
  agentId?: string;
53
+ /** Agent-profile policy for ordered consult/transfer destinations */
54
+ consultTransferConfig?: ConsultTransferDestinationConfig;
55
+ /** Whether wxApp thick-client answer is enabled (WXCC-6026) */
56
+ enableWxBetterTogether?: boolean;
57
+ /** True while wxApp telephony answer is in flight (outdial Calling… label) */
58
+ wxAppAnswerPending?: boolean;
59
+ /** True only during the wxApp accept REST call (decline disabled for inbound + outdial) */
60
+ wxAppAcceptInFlight?: boolean;
46
61
  }
47
62
 
48
63
  /**