@webex/contact-center 3.12.0-llmrefactor.12 → 3.12.0-llmrefactor.14

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 (107) hide show
  1. package/.sdd/manifest.json +6 -5
  2. package/ai-docs/CONTRACTS.md +8 -0
  3. package/ai-docs/SERVICE_STATE.md +4 -0
  4. package/ai-docs/contact-center-spec.md +26 -3
  5. package/ai-docs/features/contact-owner-change-refactor.md +78 -0
  6. package/dist/cc.js +202 -9
  7. package/dist/cc.js.map +1 -1
  8. package/dist/constants.js +5 -1
  9. package/dist/constants.js.map +1 -1
  10. package/dist/index.js +25 -10
  11. package/dist/index.js.map +1 -1
  12. package/dist/metrics/behavioral-events.js +6 -0
  13. package/dist/metrics/behavioral-events.js.map +1 -1
  14. package/dist/metrics/constants.js +6 -0
  15. package/dist/metrics/constants.js.map +1 -1
  16. package/dist/services/AnswerCallOnWebexService.js +4 -4
  17. package/dist/services/AnswerCallOnWebexService.js.map +1 -1
  18. package/dist/services/ApiAiAssistant.js +116 -1
  19. package/dist/services/ApiAiAssistant.js.map +1 -1
  20. package/dist/services/WebexCrossClientService.js +5 -2
  21. package/dist/services/WebexCrossClientService.js.map +1 -1
  22. package/dist/services/WxAppTelephonyMercurySync.js +6 -3
  23. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
  24. package/dist/services/config/Util.js +5 -1
  25. package/dist/services/config/Util.js.map +1 -1
  26. package/dist/services/config/constants.js +10 -1
  27. package/dist/services/config/constants.js.map +1 -1
  28. package/dist/services/config/index.js +51 -0
  29. package/dist/services/config/index.js.map +1 -1
  30. package/dist/services/config/types.js +4 -3
  31. package/dist/services/config/types.js.map +1 -1
  32. package/dist/services/core/Utils.js +5 -1
  33. package/dist/services/core/Utils.js.map +1 -1
  34. package/dist/services/task/TaskFactory.js +1 -0
  35. package/dist/services/task/TaskFactory.js.map +1 -1
  36. package/dist/services/task/TaskManager.js +50 -58
  37. package/dist/services/task/TaskManager.js.map +1 -1
  38. package/dist/services/task/types.js.map +1 -1
  39. package/dist/services/task/voice/Voice.js +63 -44
  40. package/dist/services/task/voice/Voice.js.map +1 -1
  41. package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
  42. package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
  43. package/dist/services/task/voice/wxAppVoiceMethods.js +52 -11
  44. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  45. package/dist/services/wxAppDiagnosticLogging.js +50 -13
  46. package/dist/services/wxAppDiagnosticLogging.js.map +1 -1
  47. package/dist/types/cc.d.ts +21 -1
  48. package/dist/types/constants.d.ts +4 -0
  49. package/dist/types/index.d.ts +11 -1
  50. package/dist/types/metrics/constants.d.ts +6 -0
  51. package/dist/types/services/ApiAiAssistant.d.ts +35 -2
  52. package/dist/types/services/config/constants.d.ts +5 -0
  53. package/dist/types/services/config/types.d.ts +15 -2
  54. package/dist/types/services/core/Utils.d.ts +2 -0
  55. package/dist/types/services/task/types.d.ts +2 -0
  56. package/dist/types/services/task/voice/Voice.d.ts +15 -2
  57. package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
  58. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +19 -6
  59. package/dist/types/services/wxAppDiagnosticLogging.d.ts +12 -0
  60. package/dist/types/types.d.ts +56 -1
  61. package/dist/types.js +39 -2
  62. package/dist/types.js.map +1 -1
  63. package/dist/webex.js +9 -1
  64. package/dist/webex.js.map +1 -1
  65. package/package.json +9 -9
  66. package/src/cc.ts +294 -25
  67. package/src/constants.ts +4 -0
  68. package/src/index.ts +11 -0
  69. package/src/metrics/ai-docs/metrics-spec.md +61 -20
  70. package/src/metrics/behavioral-events.ts +6 -0
  71. package/src/metrics/constants.ts +6 -0
  72. package/src/services/AnswerCallOnWebexService.ts +4 -4
  73. package/src/services/ApiAiAssistant.ts +153 -1
  74. package/src/services/WebexCrossClientService.ts +7 -2
  75. package/src/services/WxAppTelephonyMercurySync.ts +3 -3
  76. package/src/services/ai-docs/services-spec.md +11 -3
  77. package/src/services/config/Util.ts +6 -0
  78. package/src/services/config/ai-docs/config-spec.md +14 -3
  79. package/src/services/config/constants.ts +12 -0
  80. package/src/services/config/index.ts +65 -0
  81. package/src/services/config/types.ts +13 -2
  82. package/src/services/core/Utils.ts +4 -0
  83. package/src/services/task/TaskFactory.ts +1 -0
  84. package/src/services/task/TaskManager.ts +85 -87
  85. package/src/services/task/ai-docs/task-spec.md +7 -3
  86. package/src/services/task/types.ts +16 -0
  87. package/src/services/task/voice/Voice.ts +74 -52
  88. package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
  89. package/src/services/task/voice/wxAppVoiceMethods.ts +83 -24
  90. package/src/services/wxAppDiagnosticLogging.ts +135 -23
  91. package/src/types.ts +65 -0
  92. package/src/webex.js +8 -0
  93. package/test/unit/spec/cc.ts +469 -19
  94. package/test/unit/spec/services/ApiAiAssistant.ts +134 -2
  95. package/test/unit/spec/services/WebexCrossClientService.ts +45 -3
  96. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +1 -1
  97. package/test/unit/spec/services/config/Util.ts +44 -0
  98. package/test/unit/spec/services/config/index.ts +68 -0
  99. package/test/unit/spec/services/task/TaskManager.ts +214 -3
  100. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
  101. package/test/unit/spec/services/task/voice/Voice.ts +10 -51
  102. package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
  103. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +121 -5
  104. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +44 -8
  105. package/test/unit/spec/wellnessSampleUtils.ts +288 -0
  106. package/umd/contact-center.min.js +2 -2
  107. package/umd/contact-center.min.js.map +1 -1
@@ -11,8 +11,8 @@
11
11
  | Doc kind | Module spec |
12
12
  | Coverage score | Partial (manifest-authoritative); 15/15 required document fields present |
13
13
  | Generated from | `module-spec` @ SDLC template library `0.2.1` |
14
- | generated_by / approved_by / updated_at | Codex generator / developer-approved follow-up review remediation / 2026-07-21 |
15
- | Validation status | Follow-up validation passed (independent Claude fallback, 2026-07-21); coverage remains Partial |
14
+ | generated_by / approved_by / updated_at | Codex generator / developer-approved Agent Wellness Break delta / 2026-09-02 |
15
+ | Validation status | Agent Wellness Break v0.4 delta independently validated by claude-code on 2026-09-07; coverage remains Partial until the remaining baseline promotion criteria are satisfied |
16
16
 
17
17
  ## Evidence Rules
18
18
  Every requirement cites stable source and test file paths. Code/tests are the behavioral referee; routed source text supplies explicit intent and rationale. Missing or contradictory evidence blocks promotion.
@@ -100,6 +100,8 @@ services/config/
100
100
  | Organization masking | `OrgSettings.maskSensitiveData` | `src/services/config/types.ts`, `src/services/config/Util.ts` |
101
101
  | Auxiliary-code list URL | includes `desktopProfileFilter=true` | `src/services/config/constants.ts` |
102
102
  | Outdial ANI entries | public wrapper through ContactCenter | `src/services/config/index.ts`, `src/cc.ts` |
103
+ | `Profile.isWellnessBreakEnabled` | required effective flag derived from backend AI feature configuration and organization license quantity | `src/services/config/Util.ts`, `src/services/config/types.ts` |
104
+ | `getWellbeingBreakIdleCode(orgId)` | paginated exact lookup for the active system idle code named `WellbeingBreak`, exposed and registration-cached by ContactCenter | `src/services/config/index.ts`, `src/services/config/constants.ts`, `src/cc.ts` |
103
105
 
104
106
  The service has no `TeamList.channelMap` contract. Exact package exports are indexed in root `CONTRACTS.md`.
105
107
 
@@ -116,6 +118,8 @@ The service has no `TeamList.channelMap` contract. Exact package exports are ind
116
118
  | CONFIG-R-003 | Paginate teams and auxiliary codes until completion and include `desktopProfileFilter=true` for auxiliary-code requests. | A partial or unfiltered set yields invalid profile/team/auxiliary choices. | `src/services/config/constants.ts` | `test/unit/spec/services/config/index.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
117
119
  | CONFIG-R-004 | Expose current response/field names: `MultimediaProfileResponse`, `OrgSettings.maskSensitiveData`, and real TeamList fields only. | Type-name drift causes invalid consumer code and incorrect privacy behavior. | `src/services/config/types.ts` | `test/unit/spec/services/config/index.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
118
120
  | CONFIG-R-005 | Reject the entire profile aggregation when any required dependent request fails. | Consumers must never receive an internally inconsistent partial Profile. | `src/services/config/index.ts` | `test/unit/spec/services/config/index.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
121
+ | CONFIG-R-006 | Always populate `Profile.isWellnessBreakEnabled`; set it true only when `agentWellbeing.enable` is true, `wellnessBreakReminders` is `ENABLED`, and `OrgSettings.aiAssistantQuantity` is greater than zero. Missing or null inputs fail closed. | Hosts need one license-and-server-rollout-aware signal rather than reconstructing partial configuration. | `src/services/config/Util.ts`, `src/services/config/types.ts` | `test/unit/spec/services/config/Util.ts` | The supported SDK contracts expose no separate rollout response field; backend delivery of enabled `agentWellbeing` configuration is the rollout decision. | PRESENT |
122
+ | CONFIG-R-007 | Retrieve the wellness idle code from `/v2/auxiliary-code` using `workType=IDLE_CODE`, `customFilter=isSystemCode==true`, and `desktopProfileFilter=false`; follow pagination and accept only the exact active system code name `WellbeingBreak`. | The dedicated state must be independent of desktop-profile filtering and must never fall back to a similarly named or non-system code. | `src/services/config/index.ts`, `src/services/config/constants.ts` | `test/unit/spec/services/config/index.ts` | None. | PRESENT |
119
123
 
120
124
  ## Design Overview
121
125
  Config separates its stable consumption boundary from collaborators so ownership and failure behavior stay explicit. Profile creation is all-or-nothing across dependent API calls so consumers never receive internally inconsistent partial configuration.
@@ -162,6 +166,7 @@ The AgentProfile is defined as the [`Profile`](../types.ts) type. This is not an
162
166
  | `accessQueue` | `'ALL' \| 'SPECIFIC' \| 'NONE'` | Collaboration tab queue access scope from Desktop Profile |
163
167
  | `accessEntryPoint` | `'ALL' \| 'SPECIFIC' \| 'NONE'` | Collaboration tab entry-point access scope from Desktop Profile |
164
168
  | `accessBuddyTeam` | `'ALL' \| 'SPECIFIC' \| 'NONE'` | Collaboration tab buddy-team access scope from Desktop Profile |
169
+ | `isWellnessBreakEnabled` | boolean | Effective wellness gate derived from backend enablement/reminder configuration and positive AI Assistant license quantity |
165
170
 
166
171
  `parseAgentConfigs()` maps the three `access*` fields from `DesktopProfileResponse` onto the public `Profile` returned by `cc.register()`.
167
172
 
@@ -264,7 +269,7 @@ The service fetches data from multiple APIs with these response structures:
264
269
  | `getTenantData` | `TenantData` | `outdialEnabled`, `forceDefaultDn`, `privacyShieldVisible`, `timeoutDesktopInactivityEnabled` | Tenant-level feature flags |
265
270
  | `getOrgInfo` | `OrgInfo` | `tenantId`, `timezone` | Organization metadata |
266
271
  | `getAllAuxCodes` | `AuxCode[]` | `id`, `name`, `workTypeCode`, `active`, `isSystemCode`, `defaultCode` | Auxiliary codes for idle/wrap-up states |
267
- | `getOrganizationSetting` | `OrgSettings` | `webRtcEnabled`, `maskSensitiveData`, `campaignManagerEnabled` | Organization-level feature flags |
272
+ | `getOrganizationSetting` | `OrgSettings` | `webRtcEnabled`, `maskSensitiveData`, `campaignManagerEnabled`, `aiAssistantQuantity` | Organization-level feature flags and AI Assistant license quantity |
268
273
  | `getDialPlanData` | `DialPlanEntity[]` | `id`, `name`, `regularExpression`, `prefix`, `strippedChars` | Dial plan rules for outbound calling |
269
274
  | `getURLMapping` | `URLMapping[]` | `name`, `url` | External service URL mappings |
270
275
  | `getSiteInfo` | `SiteInfo` | Site-specific configuration | Site details |
@@ -449,11 +454,14 @@ classDiagram
449
454
  - **UC-2 Paginated teams/aux codes:** follow backend page metadata to completion; auxiliary-code requests include `desktopProfileFilter=true`. Evidence: `src/services/config/index.ts`, `test/unit/spec/services/config/index.ts`.
450
455
  - **UC-3 Dial plan and URL mapping:** request dial-plan data only when the desktop profile enables it while always including URL mapping in profile aggregation. Evidence: `src/services/config/index.ts`, `test/unit/spec/services/config/index.ts`.
451
456
  - **UC-4 Outdial ANI retrieval:** return the organization-scoped ANI list through authenticated WebexRequest and propagate failures without a partial substitute. Evidence: `src/services/config/index.ts`, `test/unit/spec/services/config/index.ts`.
457
+ - **UC-5 Wellness configuration:** produce one fail-closed effective profile flag and retrieve the exact system wellness idle code across pages. Evidence: `src/services/config/Util.ts`, `src/services/config/index.ts`, `test/unit/spec/services/config/Util.ts`, `test/unit/spec/services/config/index.ts`.
452
458
 
453
459
  ## Business Rules & Invariants
454
460
  - `getAgentConfig` rejects when any required dependent request rejects; it never returns a partial Profile.
455
461
  - `Profile.aiFeature` is derived from the AI-feature response, and sensitive-data masking uses the real `maskSensitiveData` field.
456
462
  - Team data has no `channelMap` contract; multimedia profile responses use `MultimediaProfileResponse`.
463
+ - `isWellnessBreakEnabled` is always boolean and defaults false when any gate is absent, disabled, or unlicensed.
464
+ - The system wellness code lookup is separate from profile auxiliary-code aggregation and uses `desktopProfileFilter=false`.
457
465
 
458
466
  ## Concurrency & Reactive Flow
459
467
  - The initial user lookup supplies identifiers for a ten-promise `Promise.all`; pagination loops await pages in order and stop from returned metadata.
@@ -465,6 +473,7 @@ All Config operations are authenticated REST calls through WebexRequest. Importa
465
473
  |---|---|
466
474
  | AI feature flags | `organization/{orgId}/v2/ai-feature?page=0&pageSize=100` |
467
475
  | Auxiliary codes | pagination/filter/attributes plus `desktopProfileFilter=true` |
476
+ | Wellness system idle code | `organization/{orgId}/v2/auxiliary-code?page={page}&pageSize={pageSize}&workType=IDLE_CODE&customFilter=isSystemCode==true&desktopProfileFilter=false` |
468
477
  | Multimedia profile | organization-scoped multimedia-profile resource |
469
478
  | Dial plan | organization-scoped dial-plan resource when desktop profile enables it |
470
479
 
@@ -653,6 +662,8 @@ public async getUserUsingCI(orgId: string, agentId: string): Promise<AgentRespon
653
662
  | `CONFIG-R-003` | `test/unit/spec/services/config/index.ts` | None. |
654
663
  | `CONFIG-R-004` | `test/unit/spec/services/config/index.ts` | None. |
655
664
  | `CONFIG-R-005` | `test/unit/spec/services/config/index.ts` | Keep an explicit rejection assertion for each required dependency category. |
665
+ | `CONFIG-R-006` | `test/unit/spec/services/config/Util.ts` | None. |
666
+ | `CONFIG-R-007` | `test/unit/spec/services/config/index.ts` | None. |
656
667
 
657
668
  ## Traceability
658
669
  - Repo architecture: `../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../ai-docs/SPEC_INDEX.md`
@@ -34,6 +34,12 @@ export const AGENT_STATE_AVAILABLE_ID = '0';
34
34
  */
35
35
  export const AGENT_STATE_AVAILABLE = 'Available';
36
36
 
37
+ /** System idle-code name reserved for Agent Wellness Break. */
38
+ export const WELLBEING_BREAK_IDLE_CODE = 'WellbeingBreak';
39
+
40
+ /** Backend sentinel indicating wellness-break reminders are enabled. @internal */
41
+ export const WELLNESS_BREAK_REMINDERS_ENABLED = 'ENABLED';
42
+
37
43
  /**
38
44
  * Description for the 'Available' agent state.
39
45
  * @type {string}
@@ -75,6 +81,7 @@ export const METHODS = {
75
81
  GET_URL_MAPPING: 'getURLMapping',
76
82
  GET_DIAL_PLAN_DATA: 'getDialPlanData',
77
83
  GET_AI_FEATURE_FLAGS: 'getAIFeatureFlags',
84
+ GET_WELLBEING_BREAK_IDLE_CODE: 'getWellbeingBreakIdleCode',
78
85
  GET_QUEUES: 'getQueues',
79
86
 
80
87
  // Util methods
@@ -175,6 +182,11 @@ export const endPointMap = {
175
182
  filter && filter.length > 0 ? `&filter=id=in=(${filter})` : ''
176
183
  }&attributes=${attributes}&desktopProfileFilter=true`,
177
184
 
185
+ /** Lists system idle codes without applying the agent Desktop Profile filter. */
186
+ systemIdleCodes: (orgId: string, page: number, pageSize: number) =>
187
+ `organization/${orgId}/v2/auxiliary-code?page=${page}&pageSize=${pageSize}` +
188
+ '&workType=IDLE_CODE&customFilter=isSystemCode==true&desktopProfileFilter=false',
189
+
178
190
  /**
179
191
  * Gets the endpoint for organization info.
180
192
  * @param orgId - Organization ID.
@@ -23,6 +23,7 @@ import {
23
23
  OutdialAniEntriesResponse,
24
24
  OutdialAniParams,
25
25
  AIFeatureFlagsResponse,
26
+ Entity,
26
27
  } from './types';
27
28
  import WebexRequest from '../core/WebexRequest';
28
29
  import {WCC_API_GATEWAY} from '../constants';
@@ -34,6 +35,7 @@ import {
34
35
  DEFAULT_PAGE_SIZE,
35
36
  endPointMap,
36
37
  METHODS,
38
+ WELLBEING_BREAK_IDLE_CODE,
37
39
  } from './constants';
38
40
 
39
41
  /**
@@ -470,6 +472,69 @@ export default class AgentConfigService {
470
472
  }
471
473
  }
472
474
 
475
+ /**
476
+ * Retrieves the exact system-managed WellbeingBreak idle code.
477
+ * This query intentionally bypasses Desktop Profile filtering so the system code does not
478
+ * need to appear in the normal agent-selectable idle-code list.
479
+ * @param orgId - Organization identifier
480
+ * @returns The normalized WellbeingBreak entity
481
+ * @throws When the request fails or the exact system code is unavailable
482
+ * @internal
483
+ */
484
+ public async getWellbeingBreakIdleCode(orgId: string): Promise<Entity> {
485
+ LoggerProxy.info('Fetching WellbeingBreak system idle code', {
486
+ module: CONFIG_FILE_NAME,
487
+ method: METHODS.GET_WELLBEING_BREAK_IDLE_CODE,
488
+ });
489
+
490
+ try {
491
+ let page = DEFAULT_PAGE;
492
+ let totalPages = 1;
493
+
494
+ do {
495
+ // Pagination is intentionally sequential so the lookup can stop as soon as the code is found.
496
+ // eslint-disable-next-line no-await-in-loop
497
+ const response = await this.webexReq.request({
498
+ service: WCC_API_GATEWAY,
499
+ resource: endPointMap.systemIdleCodes(orgId, page, DEFAULT_PAGE_SIZE),
500
+ method: HTTP_METHODS.GET,
501
+ });
502
+
503
+ if (response.statusCode !== 200) {
504
+ throw new Error(`API call failed with ${response.statusCode}`);
505
+ }
506
+
507
+ const result = response.body as ListAuxCodesResponse;
508
+ const wellbeingCode = result.data?.find(
509
+ (code) =>
510
+ code.active === true &&
511
+ code.isSystemCode === true &&
512
+ code.name === WELLBEING_BREAK_IDLE_CODE
513
+ );
514
+
515
+ if (wellbeingCode) {
516
+ return {
517
+ id: wellbeingCode.id,
518
+ name: wellbeingCode.name,
519
+ isSystem: true,
520
+ isDefault: wellbeingCode.defaultCode,
521
+ };
522
+ }
523
+
524
+ totalPages = result.meta?.totalPages ?? 1;
525
+ page += 1;
526
+ } while (page < totalPages);
527
+
528
+ throw new Error('WELLBEING_BREAK_IDLE_CODE_NOT_FOUND');
529
+ } catch (error) {
530
+ LoggerProxy.error(`getWellbeingBreakIdleCode API call failed with ${error}`, {
531
+ module: CONFIG_FILE_NAME,
532
+ method: METHODS.GET_WELLBEING_BREAK_IDLE_CODE,
533
+ });
534
+ throw error;
535
+ }
536
+ }
537
+
473
538
  /**
474
539
  * Fetches the site data for the given orgId and siteId.
475
540
  * @ignore
@@ -150,8 +150,7 @@ export const CC_TASK_EVENTS = {
150
150
  /**
151
151
  * Events emitted on Contact Center agent operations
152
152
  * @enum {string}
153
- * @private
154
- * @ignore
153
+ * @public
155
154
  */
156
155
  export const CC_AGENT_EVENTS = {
157
156
  /** Welcome event when agent connects to websocket/backend */
@@ -190,6 +189,8 @@ export const CC_AGENT_EVENTS = {
190
189
  AGENT_BUDDY_AGENTS_RETRIEVE_FAILED: 'BuddyAgentsRetrieveFailed',
191
190
  /** Event emitted when contact is reserved for agent */
192
191
  AGENT_CONTACT_RESERVED: 'AgentContactReserved',
192
+ /** Agent-level Agent Wellness Break notification */
193
+ WELLNESS_BREAK: 'WellnessBreak',
193
194
  } as const;
194
195
 
195
196
  /**
@@ -729,6 +730,8 @@ export type OrgSettings = {
729
730
  maskSensitiveData: boolean;
730
731
  /** Whether campaign manager features are enabled */
731
732
  campaignManagerEnabled: boolean;
733
+ /** Number of AI Assistant licenses assigned to the organization */
734
+ aiAssistantQuantity?: number | null;
732
735
  };
733
736
 
734
737
  /**
@@ -1237,6 +1240,8 @@ export type Profile = {
1237
1240
  maskSensitiveData?: boolean;
1238
1241
  /** Whether agent is currently logged in */
1239
1242
  isAgentLoggedIn?: boolean;
1243
+ /** Active station session after a successful registered relogin. */
1244
+ agentSessionId?: string;
1240
1245
  /** Last auxiliary code ID used for state change */
1241
1246
  lastStateAuxCodeId?: string;
1242
1247
  /** Timestamp of last state change */
@@ -1245,6 +1250,12 @@ export type Profile = {
1245
1250
  lastIdleCodeChangeTimestamp?: number;
1246
1251
  /** AI feature flags resolved from organization config */
1247
1252
  aiFeature?: AIFeatureFlags;
1253
+ /**
1254
+ * Whether Agent Wellness Break is effectively enabled for this profile.
1255
+ * This combines the organization wellbeing configuration with AI Assistant licensing;
1256
+ * Server-side rollout remains enforced by the backend configuration and notification delivery.
1257
+ */
1258
+ isWellnessBreakEnabled?: boolean;
1248
1259
  };
1249
1260
 
1250
1261
  /**
@@ -16,6 +16,10 @@ import {
16
16
  import {PARTICIPANT_TYPES, STATE_CONSULT} from './constants';
17
17
  import {DialPlan} from '../config/types';
18
18
 
19
+ /** Returns whether a value is a non-null object with string keys. */
20
+ export const isRecord = (value: unknown): value is Record<string, unknown> =>
21
+ typeof value === 'object' && value !== null;
22
+
19
23
  /**
20
24
  * Extracts common error details from a Webex request payload.
21
25
  *
@@ -31,6 +31,7 @@ export default class TaskFactory {
31
31
  isRecordingEnabled: recordingEnabled,
32
32
  enableWxBetterTogether: configFlags.enableWxBetterTogether ?? false,
33
33
  answerCallOnWebexService,
34
+ getUsersubPublished: configFlags.getWxAppUsersubPublished,
34
35
  consultTransferConfig: consultTransfer,
35
36
  };
36
37
  switch (mediaType) {
@@ -778,6 +778,7 @@ export default class TaskManager extends EventEmitter {
778
778
  const interactionId = message.data.interactionId;
779
779
  const reservationInteractionId = message.data.reservationInteractionId;
780
780
  let task = this.taskCollection[interactionId];
781
+ let ownerChangeResolvedByRelatedInteraction = false;
781
782
 
782
783
  // When a campaign preview contact is accepted, the assigned event may arrive
783
784
  // with a new interactionId while the task is stored under the original
@@ -792,7 +793,66 @@ export default class TaskManager extends EventEmitter {
792
793
  }
793
794
 
794
795
  if (!task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
795
- task = this.findTaskForContactOwnerChange(message.data);
796
+ const incomingShowsCurrentAgentPromotion = Boolean(
797
+ this.agentId &&
798
+ message.data.interaction?.owner === this.agentId &&
799
+ TaskManager.isParticipantActiveOnMainInteraction(message.data, this.agentId)
800
+ );
801
+
802
+ task = this.findUniqueTaskByRelatedInteraction(
803
+ message.data,
804
+ (candidate) => {
805
+ const currentAgentId = this.agentId || candidate.data?.agentId;
806
+
807
+ return Boolean(
808
+ currentAgentId &&
809
+ (TaskManager.isParticipantActiveOnMainInteraction(candidate.data, currentAgentId) ||
810
+ incomingShowsCurrentAgentPromotion)
811
+ );
812
+ },
813
+ {includeCollectionKeys: true, includeMainCallMediaKeys: true}
814
+ );
815
+ ownerChangeResolvedByRelatedInteraction = Boolean(task);
816
+ }
817
+
818
+ let ownerChangeMainInteractionId: string | undefined;
819
+ if (task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
820
+ const taskMainInteractionId = task.data?.interaction?.mainInteractionId;
821
+ const payloadMainInteractionId = message.data.interaction?.mainInteractionId;
822
+ const hasConflictingMainInteractionIds = Boolean(
823
+ taskMainInteractionId &&
824
+ payloadMainInteractionId &&
825
+ taskMainInteractionId !== payloadMainInteractionId
826
+ );
827
+ // A promoted child task can retain stale embedded IDs. Prefer the
828
+ // authoritative incoming main-call identity before local snapshot IDs.
829
+ const stableMainInteractionId =
830
+ payloadMainInteractionId ||
831
+ TaskManager.getMainCallMediaId(message.data.interaction) ||
832
+ TaskManager.getMainCallMediaId(task.data?.interaction) ||
833
+ taskMainInteractionId ||
834
+ message.data.interaction?.interactionId ||
835
+ task.data?.interaction?.interactionId ||
836
+ task.data?.interactionId;
837
+ const taskAtStableKey = stableMainInteractionId
838
+ ? this.taskCollection[stableMainInteractionId]
839
+ : undefined;
840
+
841
+ if (
842
+ !stableMainInteractionId ||
843
+ hasConflictingMainInteractionIds ||
844
+ (taskAtStableKey && taskAtStableKey !== task)
845
+ ) {
846
+ LoggerProxy.warn('Unable to safely correlate owner change to the main interaction task', {
847
+ module: TASK_MANAGER_FILE,
848
+ method: 'prepareEventContext',
849
+ interactionId:
850
+ payloadMainInteractionId || taskMainInteractionId || message.data.interactionId,
851
+ });
852
+ task = undefined;
853
+ } else if (ownerChangeResolvedByRelatedInteraction) {
854
+ ownerChangeMainInteractionId = stableMainInteractionId;
855
+ }
796
856
  }
797
857
 
798
858
  if (!task && MAIN_INTERACTION_CORRELATED_EVENTS.has(eventType)) {
@@ -821,8 +881,11 @@ export default class TaskManager extends EventEmitter {
821
881
  }
822
882
  : message.data;
823
883
 
824
- if (task && eventType === CC_EVENTS.CONTACT_OWNER_CHANGED) {
825
- adjustedPayload = TaskManager.preserveMainTaskIdentity(task, adjustedPayload);
884
+ if (task && ownerChangeMainInteractionId) {
885
+ adjustedPayload = {
886
+ ...adjustedPayload,
887
+ interactionId: ownerChangeMainInteractionId,
888
+ };
826
889
  }
827
890
 
828
891
  if (task && eventType === CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE) {
@@ -860,53 +923,6 @@ export default class TaskManager extends EventEmitter {
860
923
  };
861
924
  }
862
925
 
863
- /**
864
- * ContactOwnerChanged can identify the promoted agent's child interaction while
865
- * the surviving task is stored under the main interaction. Accept one related
866
- * task when its existing snapshot or the authoritative incoming promotion proves
867
- * current-agent main-call membership, while excluding non-promoted and ambiguous matches.
868
- */
869
- private findTaskForContactOwnerChange(payload: WebSocketPayload): ITask | undefined {
870
- const relatedCandidates = this.findRelatedTasks(payload, {
871
- includeCollectionKeys: true,
872
- includeMainCallMediaKeys: true,
873
- });
874
- const incomingShowsCurrentAgentPromotion =
875
- this.isCurrentAgentPromotedOnIncomingMainCall(payload);
876
- const eligibleCandidates = relatedCandidates.filter(
877
- (candidate) =>
878
- this.isCurrentAgentActiveOnMainInteraction(candidate.data) ||
879
- incomingShowsCurrentAgentPromotion
880
- );
881
-
882
- if (eligibleCandidates.length === 1) {
883
- return eligibleCandidates[0];
884
- }
885
-
886
- if (eligibleCandidates.length > 1) {
887
- LoggerProxy.warn('Unable to correlate task event to a unique main interaction task', {
888
- module: TASK_MANAGER_FILE,
889
- method: 'findTaskForContactOwnerChange',
890
- interactionId:
891
- payload.interaction?.mainInteractionId ||
892
- payload.interaction?.parentInteractionId ||
893
- payload.interaction?.callProcessingDetails?.parentInteractionId ||
894
- payload.interactionId,
895
- });
896
-
897
- return undefined;
898
- }
899
-
900
- return undefined;
901
- }
902
-
903
- private isCurrentAgentActiveOnMainInteraction(taskData: TaskData | undefined): boolean {
904
- const currentAgentId = this.agentId || taskData?.agentId;
905
- if (!currentAgentId) return false;
906
-
907
- return TaskManager.isParticipantActiveOnMainInteraction(taskData, currentAgentId);
908
- }
909
-
910
926
  private static isParticipantActiveOnMainInteraction(
911
927
  taskData: TaskData | WebSocketPayload | undefined,
912
928
  participantId: string
@@ -921,14 +937,6 @@ export default class TaskManager extends EventEmitter {
921
937
  );
922
938
  }
923
939
 
924
- private isCurrentAgentPromotedOnIncomingMainCall(payload: WebSocketPayload): boolean {
925
- return Boolean(
926
- this.agentId &&
927
- payload.interaction?.owner === this.agentId &&
928
- TaskManager.isParticipantActiveOnMainInteraction(payload, this.agentId)
929
- );
930
- }
931
-
932
940
  private static getMainCallMediaId(
933
941
  interaction: TaskData['interaction'] | undefined
934
942
  ): string | undefined {
@@ -960,31 +968,6 @@ export default class TaskManager extends EventEmitter {
960
968
  );
961
969
  }
962
970
 
963
- /** Keep a related owner-change event from re-keying the surviving main task to a child ID. */
964
- private static preserveMainTaskIdentity(
965
- task: ITask,
966
- payload: WebSocketPayload
967
- ): WebSocketPayload {
968
- const currentMainInteractionId = task.data?.interaction?.mainInteractionId;
969
- const payloadMainInteractionId = payload.interaction?.mainInteractionId;
970
- const currentMainMediaId = TaskManager.getMainCallMediaId(task.data?.interaction);
971
- const payloadMainMediaId = TaskManager.getMainCallMediaId(payload.interaction);
972
- const stableInteractionId =
973
- currentMainInteractionId ||
974
- payloadMainInteractionId ||
975
- currentMainMediaId ||
976
- payloadMainMediaId ||
977
- payload.interaction?.interactionId ||
978
- task.data?.interaction?.interactionId ||
979
- task.data?.interactionId;
980
-
981
- if (!stableInteractionId || stableInteractionId === payload.interactionId) {
982
- return payload;
983
- }
984
-
985
- return {...payload, interactionId: stableInteractionId};
986
- }
987
-
988
971
  /**
989
972
  * ParticipantLeftConference may arrive after an owner update while still carrying
990
973
  * the departed owner. Keep the confirmed owner only when the new roster proves that
@@ -1024,9 +1007,10 @@ export default class TaskManager extends EventEmitter {
1024
1007
  */
1025
1008
  private findUniqueTaskByRelatedInteraction(
1026
1009
  payload: WebSocketPayload,
1027
- candidateFilter: (task: ITask) => boolean = () => true
1010
+ candidateFilter: (task: ITask) => boolean = () => true,
1011
+ options: {includeCollectionKeys?: boolean; includeMainCallMediaKeys?: boolean} = {}
1028
1012
  ): ITask | undefined {
1029
- const candidates = this.findRelatedTasks(payload).filter(candidateFilter);
1013
+ const candidates = this.findRelatedTasks(payload, options).filter(candidateFilter);
1030
1014
 
1031
1015
  if (candidates.length === 1) {
1032
1016
  return candidates[0];
@@ -1060,7 +1044,9 @@ export default class TaskManager extends EventEmitter {
1060
1044
  payload.interaction?.parentInteractionId,
1061
1045
  payload.interaction?.callProcessingDetails?.parentInteractionId,
1062
1046
  ...(includeMainCallMediaKeys ? [TaskManager.getMainCallMediaId(payload.interaction)] : []),
1063
- ].filter((interactionId): interactionId is string => Boolean(interactionId))
1047
+ ].filter((relatedInteractionId): relatedInteractionId is string =>
1048
+ Boolean(relatedInteractionId)
1049
+ )
1064
1050
  );
1065
1051
  if (payloadInteractionIds.size === 0) return [];
1066
1052
 
@@ -1082,7 +1068,8 @@ export default class TaskManager extends EventEmitter {
1082
1068
  ];
1083
1069
 
1084
1070
  return candidateInteractionIds.some(
1085
- (interactionId) => Boolean(interactionId) && payloadInteractionIds.has(interactionId)
1071
+ (relatedInteractionId) =>
1072
+ Boolean(relatedInteractionId) && payloadInteractionIds.has(relatedInteractionId)
1086
1073
  );
1087
1074
  })
1088
1075
  .map(([, candidate]) => candidate)
@@ -1457,6 +1444,7 @@ export default class TaskManager extends EventEmitter {
1457
1444
  }
1458
1445
 
1459
1446
  private removeTaskFromCollection(task: ITask) {
1447
+ task.disposeWxAppOfferObservability?.();
1460
1448
  if (typeof task.cancelAutoWrapupTimer === 'function') {
1461
1449
  task.cancelAutoWrapupTimer();
1462
1450
  }
@@ -1655,6 +1643,16 @@ export default class TaskManager extends EventEmitter {
1655
1643
  });
1656
1644
  }
1657
1645
 
1646
+ /**
1647
+ * Re-run OFFERED wxApp offer diagnostics for collected voice tasks.
1648
+ * Used after usersub publish so pre-publish offers can schedule mismatch grace.
1649
+ */
1650
+ public refreshWxAppOfferObservabilityForAllTasks(): void {
1651
+ Object.values(this.taskCollection).forEach((task) => {
1652
+ task.refreshWxAppOfferObservability?.();
1653
+ });
1654
+ }
1655
+
1658
1656
  public static getTaskManager(
1659
1657
  apiAIAssistant: ApiAIAssistant,
1660
1658
  contact: ReturnType<typeof routingContact>,
@@ -618,6 +618,8 @@ This keeps transcript and suggestion delivery aligned on the same per-task event
618
618
 
619
619
  **wxApp consumer contract (WXCC-6026):** Hosts enable `enableWxBetterTogether` at init (Phase 1 init-only; re-init to change), bind UI to `task.uiControls` (including optional `main.keypad`), and call **`task.accept()`**, **`task.decline()`**, **`task.toggleMute({ muted? })`**, **`task.transmitDtmf({ dtmf })`**. SDK routes wxApp telephony internally on `Voice`. Shared-line `lineOwnerId` defaults from the wxApp agent participant when omitted.
620
620
 
621
+ **wxApp OFFERED diagnostics:** `Voice.updateUiControls` delegates offer-decision logging and deferred participant-mismatch WARN/metric to `voice/wxAppOfferObservability.ts` (500 ms grace; observability only). After usersub publish, `TaskManager.refreshWxAppOfferObservabilityForAllTasks()` re-invokes the observer without emitting UI controls. `removeTaskFromCollection` calls `disposeWxAppOfferObservability()` so a pending grace timer cannot emit after the task is dropped (including `CONTACT_MERGED` child removal). Dispose is terminal: the observer sets a disposed guard (timer cancel plus no-op `handleUiControlsUpdate`) so a later `updateUiControls` / refresh cannot schedule a new grace timer.
622
+
621
623
  **wxApp offer UI (`uiControlsComputer`):** `wxAppAcceptInFlight` disables accept/decline during the accept REST call. `wxAppAnswerPending` additionally disables accept and decline for **inbound** offers until ASSIGN; wxApp **outdial** keeps decline enabled during the post-accept "Calling…" phase so `cancelTask` remains available.
622
624
 
623
625
  **wxApp decline observability:** Inbound wxApp offers decline via telephony `rejectCall` (`runWxAppReject` → `WXAPP_TASK_DECLINE_*`). wxApp outdial cancellations use CC routing `cancelTask` (`runWxAppOutdialDecline` → additive `WXAPP_TASK_DECLINE_*` plus existing `TASK_DECLINE_*`).
@@ -924,15 +926,17 @@ guards. The state-machine transition table lives in
924
926
  Primary-Agent promotion remains backend-authoritative and follows the two-event
925
927
  desktop contract. `ContactOwnerChanged` updates the promoted Agent; TaskManager
926
928
  prefers an exact task, then one unique related task resolved through nested
927
- main/parent identifiers or the `mainCall` media-map identity. A related
928
- candidate is eligible when its current
929
+ main/parent identifiers, the task collection key, or the `mainCall` media-map
930
+ identity. A related candidate is eligible when its current
929
931
  snapshot contains the current Agent's participant entry with `hasLeft !== true`
930
932
  and membership on the `mType: mainCall` leg. The incoming snapshot can provide
931
933
  that evidence only when it also names the current Agent as `interaction.owner`;
932
934
  this permits an authoritative promotion payload to repair a stale child-keyed
933
935
  snapshot while still excluding consult-only tasks. The update keeps the
934
936
  surviving main interaction identity even when the notification names a
935
- promoted-Agent child interaction.
937
+ promoted-Agent child interaction. Conflicting main identifiers, ambiguous
938
+ matches, and a canonical key occupied by a distinct task are ignored rather
939
+ than replacing an existing task.
936
940
 
937
941
  If no related task exists, TaskManager recovers only the promoted current Agent
938
942
  from a non-terminal telephony `ContactOwnerChanged` payload that provides the same
@@ -1759,6 +1759,20 @@ export interface ITask extends IEventEmitter {
1759
1759
  */
1760
1760
  syncWxAppMuteFromCallDetails?(): Promise<boolean | undefined>;
1761
1761
 
1762
+ /**
1763
+ * Re-run OFFERED wxApp offer diagnostics without emitting UI controls.
1764
+ * Voice tasks implement this; other channel types omit it.
1765
+ * @internal
1766
+ */
1767
+ refreshWxAppOfferObservability?(): void;
1768
+
1769
+ /**
1770
+ * Cancel pending wxApp offer observability timers (participant-mismatch grace).
1771
+ * Voice tasks implement this; other channel types omit it.
1772
+ * @internal
1773
+ */
1774
+ disposeWxAppOfferObservability?(): void;
1775
+
1762
1776
  /**
1763
1777
  * State machine instance for managing task state transitions and derived properties.
1764
1778
  * The state machine handles:
@@ -2070,6 +2084,8 @@ export type VoiceUIControlOptions = {
2070
2084
  enableWxBetterTogether?: boolean;
2071
2085
  consultTransferConfig?: ConsultTransferDestinationConfig;
2072
2086
  answerCallOnWebexService?: import('../AnswerCallOnWebexService').default;
2087
+ /** Read-only observability: whether usersub answer-calls-on-wxcc is active this session. */
2088
+ getUsersubPublished?: () => boolean;
2073
2089
  };
2074
2090
 
2075
2091
  /**