Skip to content

feat(contact-center): centralize consult transfer list policy (CAI-8354) - #5179

Merged
rsarika merged 17 commits into
webex:nextfrom
rsarika:fix/cai-8354-expose-desktop-profile-collaboration-flags
Aug 23, 2026
Merged

feat(contact-center): centralize consult transfer list policy (CAI-8354)#5179
rsarika merged 17 commits into
webex:nextfrom
rsarika:fix/cai-8354-expose-desktop-profile-collaboration-flags

Conversation

@rsarika

@rsarika rsarika commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

COMPLETES CAI-8354

This pull request addresses

Contact Center consumers need consistent consult and transfer destination eligibility, visibility, and backend ordering through the existing SDK surfaces.

Changes

  • Keep getBuddyAgents, getQueues, and getEntryPoints as the public list methods.
  • Apply action-aware buddy-agent defaults while preserving explicit existing options.
  • Apply queue eligibility, profile-view, ordering, and cache-safety defaults through getQueues.
  • Map profile-scoped dial-number rows through getEntryPoints and expose the dialled number as EntryPointRecord.number.
  • Preserve backend response order and pagination metadata.
  • Expose ordered Consult and Transfer destination arrays on root Task UI controls for all task types.
  • Route supported entry-point transfers through the existing vteam transfer path.
  • Update the Contact Center sample, focused unit coverage, and coordinated feature specification.

Demo

Watch the Vidcast

Impact

Consumers can use the existing list methods and read one root Task control field without recreating reusable destination decisions. Existing explicit list options remain available for consumers that need supported overrides.

Validation

  • Contact Center unit tests: 38 suites and 1,024 tests passed.
  • Contact Center build passed.
  • Style checks completed with 0 errors; 40 existing warnings remain.
  • Diff-scoped specification drift check passed.

Change Type

  • Bug fix
  • SDK behavioral defaults
  • Documentation update

GAI Coding Policy

  • GAI was used to create a draft that was subsequently customized or modified.

@rsarika
rsarika marked this pull request as ready for review August 19, 2026 11:36
@rsarika
rsarika requested a review from a team as a code owner August 19, 2026 11:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78d9b379db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2245 to +2247
public async getConsultTransferQueues(
params: ConsultTransferQueueSearchParams = {}
): Promise<ContactServiceQueuesResponse> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register the new public APIs in the contract catalogs

This adds public façade methods and exported request types, but neither ai-docs/CONTRACTS.md nor .sdd/manifest.json contains the new consult/transfer APIs, so the package's authoritative contract and routing metadata will omit this public surface during conformance checks. Add the corresponding catalog and manifest entries in this change.

AGENTS.md reference: packages/@webex/contact-center/AGENTS.md:L64-L70

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61b3ada8cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +100 to +105
/** Desktop-profile filtering changes the returned result set */
desktopProfileFilter?: boolean;
/** Provisioning-view filtering changes the returned result set */
provisioningView?: boolean;
/** Single-object response mode changes the response shape */
singleObjectResponse?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the scoped PageCache contract guide

These new cache-validation fields and bypass semantics are absent from src/utils/AGENTS.md, which still documents only search, filter, attributes, and sortBy as CacheValidationParams and canUseCache() inputs. Update that scoped contract so maintainers do not follow stale cache-safety guidance; its prescribed workflow explicitly requires contract changes to update the utils guide and pass a drift check.

AGENTS.md reference: packages/@webex/contact-center/src/utils/AGENTS.md:L247-L253

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e2ec8310a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

destinations.push(CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE);
}
if (isVoice) destinations.push(CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER);
if (canUseEntryPoints) destinations.push(CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude entry points from blind-transfer destinations

When a voice task has entry-point access, this shared builder appends entryPoint to both the Consult and Transfer arrays. The existing destination contract explicitly marks DESTINATION_TYPE.ENTRYPOINT as supported only for consult operations (src/services/task/types.ts:36-37), while Task.transfer() sends every non-queue destination through blindTransfer() (src/services/task/Task.ts:675-684). A consumer following the new transfer controls can therefore offer an unsupported entry-point transfer that is rejected by the backend; build the two arrays separately so entry points are included only for Consult.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9aa9dc7af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +109 to +112
filter: 'queueType==INBOUND;channelType==TELEPHONY;active==true',
sortBy: 'name',
sortOrder: 'asc',
...params,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve queue defaults when optional fields are undefined

When callers build this optional parameter object with fields whose values are undefined (for example, getQueues({filter: selectedFilter, sortBy: selectedSort}) before either selection exists), ...params overwrites the default filter and sort values. fetchQueues() then emits neither the inbound-active-telephony filter nor sort=name,ASC, returning a broader and unordered queue list even though the caller supplied no effective override. Normalize undefined fields before merging, while retaining an explicit empty filter if that is the supported way to disable the default.

Useful? React with 👍 / 👎.

Comment on lines +97 to +100
filter: DEFAULT_ENTRY_POINT_FILTER,
sortBy: 'name',
sortOrder: 'asc',
...params,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the entry-point filter when its override is undefined

If an options object contains filter: undefined, which is common when forwarding an optional typed filter, this spread replaces DEFAULT_ENTRY_POINT_FILTER with undefined. The request consequently omits the active inbound telephony restriction and can return destinations outside the documented default policy; only a defined caller filter should override this default.

Useful? React with 👍 / 👎.

@rsarika rsarika added the validated If the pull request is validated for automation. label Aug 20, 2026
@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 21, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd0664183e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/samples/contact-center/app.js Outdated

function applyTaskDestinationTypes(dropdown, action) {
const destinations = currentTask?.uiControls?.consultTransferDestinations?.[action];
if (!Array.isArray(destinations) || destinations.length === 0) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear destination choices when policy returns none

When a task legitimately exposes an empty destination array—for example, a digital task whose profile denies both buddy-team and queue access—this early return leaves the static HTML options (queue, agent, dialNumber, and entryPoint) in the dropdown. The sample can then submit a destination category that the SDK explicitly omitted, defeating the centralized policy; distinguish a missing policy field used for backward compatibility from an explicitly empty array, and clear or disable the dropdown for the latter.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54560d7e30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/samples/contact-center/app.js Outdated
Comment on lines +1154 to +1156
applyTaskDestinationTypes(document.querySelector('#transfer-destination-type'), 'transfer');
transferOptions.style.display = 'block';
onTransferTypeSelectionChanged();
await onTransferTypeSelectionChanged();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop transfer setup when the destination policy is empty

When a connected digital task denies both buddy-team and queue access, consultTransferDestinations.transfer is explicitly empty. applyTaskDestinationTypes() clears and disables the type selector, but this unconditional call then handles its empty value as the fallback case and creates an enabled free-text destination input; initiateTransfer() subsequently accepts that text and submits a transfer with destinationType: ''. Fresh evidence in the current revision is this post-clear handler call, so return before building transfer inputs when the policy array is empty.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08acad6fc0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const isSearchRequest = !!(search || filter || attributes || sortBy);
const hasCustomSort = sortBy !== 'name' || sortOrder !== 'asc';
const isSearchRequest = !!(search || filter || attributes || hasCustomSort);
const effectiveSortBy = sortBy === 'name' ? DEFAULT_ENTRY_POINT_SORT_FIELD : sortBy;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Translate the public entry-point ID sort field

When an existing caller uses getEntryPoints({sortBy: 'id'}), this forwards id to the new dial-number endpoint, where it identifies the dial-number mapping, while the returned EntryPointRecord.id is populated from entryPointId. The response is therefore ordered by an internal identifier that is discarded rather than by the public ID requested by the caller, regressing the previous /v2/entry-point behavior. Map the public id sort field to entryPointId, as is already done for name.

AGENTS.md reference: packages/@webex/contact-center/AGENTS.md:L81-L85

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14fe9c5ca0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

dropdown.innerHTML = '';
dropdown.disabled = destinations.length === 0;

if (destinations.length === 0) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable submission when no destinations are allowed

When the task policy explicitly returns an empty array, this branch disables only the destination-type selector; the initial or previously rendered destination input and the Initiate button remain enabled. Although the current revision now avoids rebuilding the input, initiateConsult() and initiateTransfer() can still read that editable/stale input and submit a task operation with destinationType: '', bypassing the empty policy. Clear or disable the destination controls and submission action before returning.

Useful? React with 👍 / 👎.

@rsarika rsarika added the validated If the pull request is validated for automation. label Aug 21, 2026

@rarajes2 rarajes2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please attach a vidcast to showcase the feature.

Comment on lines -267 to +268
* const url = endPointMap.entryPointList('org123', 'page=0&pageSize=10');
* @ignore
*/
entryPointList: (orgId: string, queryParams: string) =>
`/organization/${orgId}/v2/entry-point?${queryParams}`,
entryPointDialNumberList: (orgId: string, queryParams: string) =>
`/organization/${orgId}/v3/dial-number?${queryParams}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not need the v2 at all ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v3 dial-number endpoint is intentional for this flow because it supports desktopProfileFilter, returns the entry-point mapping and dialled number, and matches the list required by the consumer. The previous v2 entry-point endpoint has no remaining call site. If we later need raw entry-point entities, that should be handled as a separate use case.

Comment on lines +241 to +243
accessQueue: agentProfileData.accessQueue as 'ALL' | 'SPECIFIC' | 'NONE',
accessEntryPoint: agentProfileData.accessEntryPoint as 'ALL' | 'SPECIFIC' | 'NONE',
accessBuddyTeam: agentProfileData.accessBuddyTeam as 'ALL' | 'SPECIFIC' | 'NONE',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have created types for these, we should use them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The collaboration-access type should be defined once and reused by DesktopProfileResponse, Profile, the task destination configuration, and this mapping instead of repeating the literal union. addressed

resource,
method,
body,
...(headers ? {headers} : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check once if the spread is working as expected

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. When headers is provided, the spread adds {headers} to the request; when it is undefined, no headers property is added. The WebexRequest unit test verifies forwarding the header object, and the EntryPoint tests verify the required headers reach webex.request()

transfer: [],
};

if (destinationConfig.accessBuddyTeam?.toUpperCase() !== 'NONE') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using the hardcoded strings here, we should use the created constants. Applicable everywhere

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I’ll use a shared collaboration-access constant for NONE and the corresponding direction constants instead of spelling these values inline.

* Desktop Profile access level for a consult/transfer destination category.
* @internal
*/
export type ConsultTransferCollaborationAccess = 'ALL' | 'SPECIFIC' | 'NONE';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is repeated in this file as well - packages/@webex/contact-center/src/services/config/types.ts

Please search through the codebase and centralize it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This is the same access domain used by the profile and task configuration. I’ll centralize the constant/type in the configuration types and reuse it from the task state-machine types.

Comment on lines +30 to +34
attributes
?.split(',')
.map((attribute) => attribute.trim())
.filter(Boolean)
.forEach((attribute) => mergedAttributes.add(attribute));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q. How does the .filter(Boolean) work ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After splitting and trimming the attributes, .filter(Boolean) removes empty strings. For example, "id, ,name," becomes ["id", "name"] before the values are added to the Set.

* The media type channel to filter buddy agents. Defaults to telephony when omitted.
*/
mediaType: 'telephony' | 'chat' | 'social' | 'email';
mediaType?: 'telephony' | 'chat' | 'social' | 'email';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not a required field anymore ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct—it is intentionally optional now. getBuddyAgents() defaults it to telephony, allowing calls such as getBuddyAgents({action: 'Consult'}). Digital consumers can still pass chat, email, or social explicitly. The default behavior is covered by a unit test.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 22, 2026
…4-expose-desktop-profile-collaboration-flags

# Conflicts:
#	packages/@webex/contact-center/ai-docs/CONTRACTS.md
#	packages/@webex/contact-center/src/services/task/ai-docs/task-spec.md
@rsarika rsarika added the validated If the pull request is validated for automation. label Aug 22, 2026
@rsarika
rsarika requested a review from rarajes2 August 22, 2026 06:30
@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03d6aae36e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +33 to +37
const mapDialNumberEntryPoint = (item: DialNumberEntryPointRecord): EntryPointRecord => ({
id: item.entryPointId,
name: item.entryPointName,
...(item.dialledNumber ? {number: item.dialledNumber} : {}),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve requested entry-point attributes

When a caller uses the retained attributes option—for example, requesting description or another compatible dial-number field—the request includes that field, but this mapper unconditionally discards every property except the remapped ID, name, and number. Consequently, getEntryPoints({attributes: ...}) cannot return the attributes it explicitly requested, including fields still represented by EntryPointRecord; preserve compatible requested properties while overriding only the SDK-owned mapped fields.

Useful? React with 👍 / 👎.

@rsarika rsarika added the validated If the pull request is validated for automation. label Aug 22, 2026

@rarajes2 rarajes2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@rsarika
rsarika enabled auto-merge (squash) August 23, 2026 05:34
@rsarika
rsarika merged commit baca5b2 into webex:next Aug 23, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

validated If the pull request is validated for automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants