Skip to content

feat(contact-center): wxcc-6026 wxapp answer, usersub publish, mercury mute sync - #5167

Merged
mkesavan13 merged 20 commits into
webex:nextfrom
akulakum:feature/WXCC-6026-wxapp-answer-SDK
Aug 20, 2026
Merged

mkesavan13 merged 20 commits into
webex:nextfrom
akulakum:feature/WXCC-6026-wxapp-answer-SDK

Conversation

@akulakum

@akulakum akulakum commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

COMPLETES WXCC-6026

This pull request addresses

Third-party CRM embeds using the Contact Center SDK need Answer on Webex (“Better together”): agents answer, decline, mute, and send DTMF from the embed while Webex App (thick client) handles telephony on the same machine and user.

Without this change, the SDK had no wxApp telephony surface, no cross-client toast suppression, and no bidirectional mute sync with Webex App.

by making the following changes

Unified wxApp task API (Voice tasks — rsarika review)

Hosts and widgets use the standard ITask contract only; SDK Voice routes wxApp telephony internally when enableWxBetterTogether is active:

await task.accept();
await task.decline();
await task.toggleMute({ muted: intendedMuteState, lineOwnerId? });
await task.transmitDtmf({ dtmf: digit, lineOwnerId? });
  • No public *OnWebex methods on IVoice (acceptOnWebex, rejectOnWebex, toggleMuteOnWebex, transmitDtmfOnWebex removed). Greenfield feature — no backward-compat aliases.
  • Orchestration in module helpers (wxAppVoiceMethods.ts): runWxAppAccept, runWxAppReject, runWxAppToggleMute, runWxAppTransmitDtmf wired via Voice.createWxAppLifecycle() — not exposed on the task prototype (avoids DevTools leaking executeWxApp* helpers).
  • Add internal helpers: WebexCallingUtils, AnswerCallOnWebexService.
  • Gate wxApp UI via task.uiControls.main.* when backend sends wxApp participant + device details on the offer.
  • InteractionUIControls.keypad promoted to a required field.
  • Inbound vs outdial: decline() on inbound wxApp offers uses telephony reject; outdial pre-accept decline uses CC routing (cancelTask). Track outdial offer state via wxAppAnswerPending on uiControlConfig.
  • uiControls accept guard: disable inbound Accept while wxAppAcceptInFlight / pending wxApp answer to prevent double-accept.

Cross-client toast suppression (usersub — P0)

  • Add WebexCrossClientService publishing POST usersub/api/v1/publish with cross-client-state and answer-calls-on-wxcc (appName: wxcc, ttl: 900, refresh ~14 min while ON).
  • Phase 1 (MMT) init-only: hosts set enableWxBetterTogether: true in webexConfig.cc before webex.init() / cc.register(). To change after init, re-init the SDK with updated config.
  • Public read API: cc.isWxBetterTogetherEnabled().
  • setManageWebexCallingInWxcc is private (@internal) — not part of the Phase 1 host contract. Production lifecycle uses ensureWxAppPostStationLogin() and teardownWxAppLocalState() (usersub + Mercury on station login / silent relogin / logout). Runtime toggle deferred to Phase 2.
  • Publish false on sign-out / deregister; reset enableWxBetterTogether on logout so stale flags do not carry into relogin.
  • Post-login init: ensureWxAppPostStationLogin() runs after station login and silent relogin (Extension / Agent DN only). When init flag is ON, auto-publishes usersub, subscribes Mercury, and backfills mute from call details. Skips Browser (WebRTC) login.
  • Guards: rollback config/task state if usersub publish or Mercury init fails.
  • SDK sample app: init-only checkbox — no runtime setter calls after login.

Bidirectional mute sync (Mercury + telephony GET)

  • Add WxAppTelephonyMercurySync for event:telephony_calls.muted / .unmuted.
  • Emit TASK_WXAPP_MUTE_STATE_UPDATED so embed UI stays in sync when the agent mutes/unmutes in Webex App.
  • Embed → Webex mute uses telephony REST with explicit intended mute state via unified task.toggleMute({ muted }).
  • syncWxAppMuteFromCallDetails backfill: re-seed mute from GET telephony/calls/{callId} on post-login, and after accept; coalesce in-flight syncs; retry when callId is not yet available; pass lineOwnerId when present.
  • Skip guard: no GET sync for terminated tasks or pre-accept wxApp offers (wxAppAnswerPending false); expected 400 / “Call not found” / 101002 responses are not logged as errors.

wxApp outdial QA fixes

  • ContactEnded mapping: post-accept agent-terminated outdial normalizes to OUTBOUND_FAILED / AGENT_ENDS (wrapup + TASK_OUTDIAL_FAILED parity with legacy AgentOutboundFailed). Pre-accept decline stays CONTACT_ENDED → TERMINATED (no wrapup), even when backend sends misleading interaction.state: wrapUp.
  • uiControls: distinguish wxApp inbound vs outdial offers; expose mute/keypad on the main leg only when wxApp is engaged (isWxAppEngagedForControls); hide mute during wrapup.

Codex review fixes

  • Outbound failure: handle wxApp OUTBOUND_FAILED from HELD, CONSULTING, and CONFERENCING states in TaskStateMachine.
  • Mercury init rollback: ensureWxAppMercuryAndSubscribe() rethrows after cleanup on partial failure; release CC-owned device/Mercury resources.
  • Wrap-up guard: reject wxApp call actions during wrap-up — getWebexCallingCallId() and isWxAppEngagedForControls() exclude WRAPPING_UP / terminated states.

Codex review fixes (follow-up)

  • Transitional outbound failure: handle wxApp OUTBOUND_FAILED in HOLD_INITIATING, RESUME_INITIATING, CONSULT_INITIATING, and CONF_INITIATING transitional states (same wrapup/terminate transitions as stable states).
  • Serialize no-arg mute toggles: chain concurrent wxApp toggleMute() calls via wxAppMuteToggleInFlight in Voice.ts so third-party/sample no-arg toggles read updated mute state; widgets already pass { muted }.
  • Usersub refresh retry: bounded retry (3 attempts, 30s delay) when scheduled usersub refresh publish fails; respects refreshGeneration on teardown.
  • SDK sample app: wxApp mute uses explicit toggleMute({ muted: nextMuted }) best practice.

Breaking change (Phase 1)

  • webex.cc.setManageWebexCallingInWxcc() removed from the public API (now private / @internal). Hosts must use enableWxBetterTogether at init; re-init to change mid-session.
  • Note: TypeScript private is compile-time only — the method may still appear on the runtime object in DevTools; it is not part of the supported public contract.

Other

  • Update contact-center sample app (docs/samples/contact-center) — init-only wxApp toggle; unified task methods.
  • Update CONTRACTS.md, WXCC-6026-wxapp-answer-flow.md, and task spec for init-only public API.
  • Unit tests for new services, unified Voice routing, runWxApp* orchestration, TaskManager (outdial ContactEnded mapping, mute backfill), uiControls, cc post-login init / guards, private setManageWebexCallingInWxcc impl, and Codex review fixes.

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

Automated (unit):

  • WebexCrossClientService — usersub publish body (appName: wxcc, ttl 900, true/false); bounded refresh retry on publish failure; no retry after teardown
  • WxAppTelephonyMercurySync — Mercury muted/unmuted filtering and callback
  • AnswerCallOnWebexService, WebexCallingUtils, wxAppVoiceMethods (inbound vs outdial offer detection, accept/toggleMute internals)
  • runWxAppAccept, runWxAppReject, runWxAppToggleMute, runWxAppTransmitDtmf — lifecycle wiring and error propagation
  • Private setManageWebexCallingInWxcc — config update, usersub publish, Mercury subscribe/unsubscribe, rollback on publish failure (tests via internal access)
  • cc.ensureWxAppPostStationLogin — auto usersub + Mercury + mute backfill when init flag ON; skip Browser login
  • uiControlsComputer — wxApp offer/engaged accept/decline/mute visibility; accept disabled during wxApp accept in-flight; main-leg mute/keypad; wrapup mute hidden
  • Voice — unified API routing (accept / decline / toggleMute / transmitDtmf → wxApp runners when flag active), mute sync skip/retry/coalesce, serialized no-arg mute toggles, call-not-found silent handling, wxAppAnswerPending
  • TaskManager — outdial ContactEnded → OUTBOUND_FAILED vs pre-accept TERMINATED; mute backfill helpers
  • TaskStateMachine — wxApp outdial wrapup / termination paths; outbound failure from HELD/CONSULTING/CONFERENCING and transitional states (HOLD/RESUME/CONSULT/CONF initiating)
  • Mercury partial-init rollback in ensureWxAppMercuryAndSubscribe
  • Wrap-up guard in getWebexCallingCallId / isWxAppEngagedForControls

Manual (end-to-end):

  • Init with enableWxBetterTogether: true → Extension/DN station login → usersub + Mercury + mute backfill without runtime toggle
  • Inbound wxApp offer → Accept / Decline from embed via unified task.accept() / task.decline() (telephony REST under the hood)
  • wxApp outdial → pre-accept Decline terminates without wrapup; post-accept agent end enters wrapup with outdial-failed popup
  • Mute/unmute embed → Webex App via task.toggleMute(); mute/unmute Webex App → embed (Mercury sync); mute icon refresh after reload
  • DTMF from embed via task.transmitDtmf() when keypad control is enabled
  • Sign-out / deinit → usersub publishes answer-calls-on-wxcc: false; config reset on logout
  • DevTools: task instance does not expose executeWxApp* orchestration methods

The GAI Coding Policy And Copyright Annotation Best Practices

  • GAI was not used (or, no additional notation is required)
  • Code was generated entirely by GAI
  • GAI was used to create a draft that was subsequently customized or modified
  • Coder created a draft manually that was non-substantively modified by GAI (e.g., refactoring was performed by GAI on manually written code)
  • Tool used for AI assistance (GitHub Copilot / Other - specify)
    • Github Copilot
    • Other - Cursor
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

I certified that

  • I have read and followed contributing guidelines
  • I discussed changes with code owners prior to submitting this pull request
  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the documentation accordingly

Make sure to have followed the contributing guidelines before submitting.

@akulakum
akulakum requested a review from a team as a code owner August 13, 2026 07:00

@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: ba7574076f

ℹ️ 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".

}
);

await this.ensureWxAppMercuryAndSubscribe();

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 Publish the enabled state during station login

When enableAnswerOnWebex is supplied in the initial SDK configuration—the path used by the updated sample—a successful station login only starts the Mercury subscription here. The only usersub publication of true is inside the runtime setter, so callers that configure the feature before initialization never publish answer-calls-on-wxcc and Webex App continues showing its competing call notification.

Useful? React with 👍 / 👎.

Comment on lines +99 to +103
if (mercury && this.boundMuteHandler) {
mercury.off(TELEPHONY_CALL_MUTED);
}
if (mercury && this.boundUnmuteHandler) {
mercury.off(TELEPHONY_CALL_UNMUTED);

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 Pass the stored handlers when unsubscribing

When this feature is disabled, station logout runs, or subscribe() refreshes its registration, these off(event) calls remove every Mercury listener for the event rather than only this service's callbacks. Since Mercury is shared by SDK plugins and host code, another consumer of the telephony mute events will silently stop receiving them; pass boundMuteHandler and boundUnmuteHandler back to off.

AGENTS.md reference: packages/@webex/contact-center/AGENTS.md:L75-L76

Useful? React with 👍 / 👎.

Comment on lines +50 to +52
const refreshTime = ttl * 1000 - EXPIRATION_OFFSET_MS;
this.refreshTimer = setTimeout(() => {
this.setManageWebexCallingInWxcc(true, {userId, ttl, appName: this.appName}).catch(

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 Invalidate an in-flight refresh during teardown

If disable/logout occurs after this timer fires but while its setManageWebexCallingInWxcc(true) request is still pending, teardown() sets the state false but the stale request can subsequently set it back to true and schedule another refresh. The logged-out instance can therefore resume publishing the enabled state indefinitely; use a generation/cancellation check before applying a refresh result or scheduling its successor.

Useful? React with 👍 / 👎.

Comment thread docs/samples/contact-center/app.js Outdated
Comment on lines +3228 to +3232
// WXCC-6026: wxApp mute toggle handler
async function toggleWxAppMute() {
if (!currentTask || !isAnswerOnWebexEnabled) return;
try {
await currentTask.toggleMuteOnWebex();

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 Wire the wxApp mute handler into the sample UI

For an enabled wxApp Voice task, the SDK now exposes the mute control, but this newly added handler has no callers: both the existing mute button and task-control action map still invoke muteUnmute(), which calls the unsupported currentTask.toggleMute() on a non-WebRTC Voice. Consequently the sample's visible wxApp mute button fails instead of invoking toggleMuteOnWebex().

Useful? React with 👍 / 👎.

* Updates config, publishes usersub state, and refreshes uiControls on active tasks.
* @public
*/
public async setManageWebexCallingInWxcc(enabled: boolean): Promise<void> {

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 canonical contracts for the new public API

This public runtime toggle is added alongside a new configuration flag, task methods, event, and UI-control field, but the commit does not update the owning module specification, ai-docs/CONTRACTS.md, or .sdd/manifest.json. That leaves the package's authoritative contract metadata inconsistent with the exported behavior and violates the package requirement to update those artifacts in the same change.

AGENTS.md reference: packages/@webex/contact-center/AGENTS.md:L52-L53

Useful? React with 👍 / 👎.

@@ -0,0 +1,113 @@
import {expect} from '@jest/globals';

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 Use the mandated test helpers in the new suites

The new test suites use Jest expect, spies, and jest.fn() throughout instead of the repository-required Sinon mocks/stubs and assert from @webex/test-helper-chai; call assertions also omit the prescribed assert.calledOnceWithExactly. Convert these additions to the package's mandated test helpers so they follow the repository's test contract.

AGENTS.md reference: AGENTS.md:L43-L47

Useful? React with 👍 / 👎.

Comment on lines +31 to +32
it('returns undefined when agentId is undefined', () => {
expect(getWebexCallingDeviceDetailsForAgent(undefined, participantsById)).toBeUndefined();

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 Parameterize the repeated device-detail cases

This describe block adds more than three same-shaped validation cases separately—for undefined or empty agent IDs, null or array participants, and missing device fields—despite the repository rule requiring parameterized tests once there are more than three similar cases. Consolidate these inputs into a table-driven test to avoid duplicated setup and assertions.

AGENTS.md reference: AGENTS.md:L49-L50

Useful? React with 👍 / 👎.

@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 13, 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: c85bbce8e9

ℹ️ 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 +140 to +142
protected onTaskAssigned(): void {
this.syncWxAppMuteFromCallDetails().catch(() => undefined);
}

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 Seed mute state when hydrating an active task

When an already-connected wxApp task is reconstructed from AgentContact after a reconnect or hydration, TaskManager sends HYDRATE, not ASSIGN, so this hook never runs and wxAppMuted remains its default false. If the actual call is muted, the next no-argument toggleMuteOnWebex() therefore sends another mute request rather than unmuting; invoke the call-details sync for hydrated engaged tasks as well.

Useful? React with 👍 / 👎.

Comment on lines +147 to +149
Object.values(interaction.participants).some(
(participant: {deviceType?: string}) => participant?.deviceType === 'wxApp'
)

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 Require the current agent to be the wxApp participant

When another participant is using wxApp but the current agent is using a different device type, this repo-wide some() check combines that other participant with the current agent's generic device details and marks the task as a wxApp offer/engaged call. The resulting accept, mute, and keypad controls are enabled even though getCallingDeviceDetails() requires the current agent's own participant to have deviceType === 'wxApp', so the exposed actions subsequently reject; derive this predicate from the current agent's participant/device details instead.

Useful? React with 👍 / 👎.

Comment on lines +194 to +196
export function mapWxAppVoiceError(error: unknown, method: string, module: string): never {
const {error: detailedError} = getErrorDetails(error, method, module);
throw detailedError;

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 structured REST failures through the voice wrapper

When a telephony request rejects with backend details, AnswerCallOnWebexService.telephonyRequest() has already passed it through getErrorDetails() and thrown a plain Error containing the backend reason. Calling getErrorDetails() again here sees no .details and replaces that useful reason with Error while performing <method> for all four public wxApp actions, preventing callers from distinguishing actionable REST failures; avoid remapping an error that the service has already normalized.

Useful? React with 👍 / 👎.

Comment on lines +1516 to +1520
} catch (error) {
LoggerProxy.error(`Failed to publish answer-calls-on-wxcc cross-client state: ${error}`, {
module: CC_FILE,
method: METHODS.SET_MANAGE_WEBEX_CALLING_IN_WXCC,
});

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 Propagate failures from the runtime usersub publish

When the usersub request fails during the public runtime toggle, this catch logs and swallows the rejection, so setManageWebexCallingInWxcc(true) resolves after enabling task controls even though Webex App toast suppression was not published. Because the failed service call also never schedules its refresh timer, the state remains unpublished indefinitely and the caller has no signal to retry; propagate the failure for the runtime setter or roll back the local enablement.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 14, 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: 73694e0e23

ℹ️ 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 +1133 to +1135
await this.publishAnswerOnWebexCrossClientState(false);
this.webexCrossClientService.teardown();
this.wxAppTelephonyMercurySync.unsubscribe();

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 Always finish teardown when the usersub publish fails

When the agent logout has already succeeded but this usersub request rejects, control jumps to the catch before webexCrossClientService.teardown() and wxAppTelephonyMercurySync.unsubscribe(). The caller is told logout failed even though the agent is logged out remotely, while the existing refresh timer and Mercury listener remain active and can continue publishing the enabled state; run the local teardown unconditionally while surfacing the publish failure separately.

Useful? React with 👍 / 👎.

Comment on lines +136 to +139
await this.publishCrossClientState([userId], ttl, composition, 'setManageWebexCallingInWxcc');

if (operationGeneration !== this.refreshGeneration) {
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 Compensate for stale enabled-state publishes

If disable, logout, or teardown occurs while an enable publish is in flight, this generation check runs only after the remote request has completed, so it suppresses local state changes but cannot undo the already-sent answer-calls-on-wxcc: true composition. A late request can therefore leave Webex App notifications suppressed for the 900-second TTL after the feature was disabled; fresh evidence beyond the prior teardown concern is that the current cancellation check remains after await publishCrossClientState() without issuing a compensating false publish.

Useful? React with 👍 / 👎.

Comment on lines +1436 to +1440
private assertWxAppStationLoginSupportedForEnable(): void {
if (this.agentConfig?.deviceType === LoginOption.BROWSER) {
throw new Error(
'setManageWebexCallingInWxcc is not supported for BROWSER (Desktop) login. Use EXTENSION or AGENT_DN.'
);

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 Validate the current station login type

For a normal station login, the success path calls webCallingService.setLoginOption() but never writes the selected login option to agentConfig.deviceType (that assignment only occurs in the silent-relogin handleDeviceType() path). This check can consequently read an absent or stale Extension value after the agent logs in with BROWSER and allow an explicitly unsupported enable operation, publishing toast suppression even though wxApp controls cannot operate on the browser participant; validate against the actual station-login selection or persist it on success.

Useful? React with 👍 / 👎.

Comment thread docs/samples/contact-center/app.js Outdated
Comment on lines +3245 to +3247
// WXCC-6026: wxApp DTMF transmit handler
async function transmitWxAppDtmf(digit) {
if (!currentTask || !isAnswerOnWebexEnabled) 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 Connect the wxApp DTMF handler to a sample control

This newly added function has no callers: the existing keypad invokes pressKey(), which only appends to the outdial number, while the task-control renderer omits keypad from both actionOrder and actionMap. Thus an engaged wxApp task can expose uiControls.keypad as enabled, but the sample provides no action that calls transmitDtmfOnWebex(), leaving the demonstrated DTMF feature unusable.

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: b7675e2897

ℹ️ 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 +203 to +205
this.wxAppMuteSyncInFlight = syncPromise.finally(() => {
if (this.wxAppMuteSyncInFlight === syncPromise) {
this.wxAppMuteSyncInFlight = undefined;

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 the completed mute-sync promise

After the first call-details sync settles, this comparison can never succeed because wxAppMuteSyncInFlight holds the promise returned by .finally(), not syncPromise. Every later sync therefore returns the permanently cached result without issuing another GET, so re-enabling the feature or attempting a reconnect backfill cannot recover mute changes missed by Mercury; compare against the wrapped promise or otherwise clear the field when that operation completes.

Useful? React with 👍 / 👎.

Comment thread docs/samples/contact-center/app.js Outdated
Comment on lines +3233 to +3234
if (isAnswerOnWebexEnabled && currentTask.isWebexAppCallingOffer && currentTask.isWebexAppCallingOffer()) {
await currentTask.rejectOnWebex();

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 Route wxApp outdial declines through cancelTask

For a wxApp OUTDIAL offer, isWebexAppCallingOffer() is true and the new UI controls explicitly enable decline, but this branch calls rejectOnWebex(), whose inbound-only guard rejects that task. The sample consequently logs an error and leaves the outgoing offer active; distinguish inbound from outdial here and invoke currentTask.decline() for the outdial cancellation path.

Useful? React with 👍 / 👎.

Comment on lines +133 to +136
await deps.answerCallOnWebexService.answerCall({
callId: details.deviceCallId,
endpointId: details.deviceId,
lineOwnerId: options?.lineOwnerId,

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 Default shared-line requests to the task's owner

When the wxApp participant carries a base64 lineOwnerId for a shared-line call, zero-argument calls such as the sample's acceptOnWebex() send undefined here, and the other action wrappers do the same; the call-details path instead forwards the encoded participant value unchanged. Thus the newly added decodedLineOwnerId() helper is never used and shared-line answer/reject/mute/DTMF requests lack the owner identifier expected by the telephony API. Derive and decode the participant owner when the caller does not explicitly override it.

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: c66156e217

ℹ️ 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".

// AgentOutboundFailed. Normalize to OUTBOUND_FAILED so state machine
// enters WRAPPING_UP and emits TASK_OUTDIAL_FAILED (AGENT_ENDS).
// Pre-accept cancelTask decline stays CONTACT_ENDED → TERMINATED (no wrapup).
if (TaskManager.isAgentTerminatedOutdialWrapup(payload, wrapUpRequired, task)) {

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 Restrict the failure remap to wxApp outdial tasks

For any ordinary Browser or extension OUTDIAL call that the agent ends, a ContactEnded payload with terminatingParty: 'Agent' and pending wrap-up now satisfies this predicate even when the task is not a wxApp call. The event is consequently remapped to OUTBOUND_FAILED, causing consumers to receive task:outdialFailed with AGENT_ENDS and potentially show a failure popup after a successfully connected call; require the task's current-agent participant/config to identify a wxApp-managed call before applying this special mapping.

Useful? React with 👍 / 👎.

Comment on lines +492 to +493
if (isWxAppInboundOffer) return VISIBLE_ENABLED;
if (isWxAppOutdialOffer) return VISIBLE_ENABLED;

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 Disable decline while a wxApp answer is pending

After acceptOnWebex() sets wxAppAnswerPending, the task remains OFFERED until the backend assignment arrives, but both inbound and outdial branches still expose an enabled decline control. A user can therefore click decline during the answer request and issue a telephony reject or cancelTask concurrently with the in-flight answer, leaving the Webex call and Contact Center task with conflicting outcomes; return a disabled decline control while wxAppAnswerPending is true.

Useful? React with 👍 / 👎.

return false;
}

if (!state || state === TaskState.OFFERED || state === TaskState.IDLE) {

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 terminal states from wxApp engagement

When a wxApp task reaches TERMINATED or COMPLETED while its participant details are still present, this predicate continues to classify it as engaged because it excludes only IDLE and OFFERED. The terminal state update then publishes enabled mute and keypad controls, and the matching call-ID helper likewise lets retained task objects send telephony actions against the ended call; limit engagement to the active call states rather than treating every non-offer state as active.

Useful? React with 👍 / 👎.

@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 18, 2026
@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 19, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 19, 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: 7ab610eecf

ℹ️ 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".

? {isVisible: true, isEnabled: isWebrtc && !isOutdial}
: DISABLED,
accept: (() => {
if (isWxAppInboundOffer) return VISIBLE_ENABLED;

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 Disable inbound accept while an answer is pending

For an inbound wxApp offer, this branch always keeps Accept enabled even after acceptOnWebex() sets wxAppAcceptInFlight and wxAppAnswerPending. Until the backend assignment changes the task from OFFERED, a double-click can therefore send multiple concurrent /answer requests; apply the same pending/in-flight check already used for outdial offers.

Useful? React with 👍 / 👎.

Comment on lines +693 to +695
await this.publishAnswerOnWebexCrossClientState(false);
this.webexCrossClientService.teardown();
this.wxAppTelephonyMercurySync.unsubscribe();

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 Always tear down wxApp state during deregistration

When deregistering while answer-call suppression is active, a usersub failure at this await exits before webexCrossClientService.teardown(), Mercury unsubscription, and the runtime flag reset. The SDK has already removed its other listeners and sockets, but the refresh timer and mute listeners can remain active and keep publishing suppression after deregistration; run the local cleanup unconditionally while surfacing the publish failure separately.

Useful? React with 👍 / 👎.

Comment on lines +345 to +347
const isOutdial = this.data?.interaction?.outboundType === 'OUTDIAL';
if (!this.enableAnswerOnWebex || !isOutdial) {
super.unsupportedMethodError(METHODS.REJECT);

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 Restrict outdial decline to an offered wxApp task

Whenever the global flag is enabled, this allows decline() for every outdial Voice task regardless of its current state or participant device type. A caller can consequently invoke it on a normal extension outdial or an already connected wxApp outdial and send cancelTask, although this API was added only for the offered wxApp cancellation path; require isWebexAppCallingOffer() before issuing the request.

Useful? React with 👍 / 👎.

@rsarika

rsarika commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

As per discussion, Can we do below implementation:

Have a simplistic API's

expose a public method in cc.ts to enable answer on webex (which we are already doing)

now based on this in Voice class we should updayte ui controls accordingly to show or hide accept decline buttons.

now based on this flag we call answer API(webex api) etc on Voice class accept method.

in this way the consumer(sample app or widgets) dont need to worry about all these flags and conditions, they just need to enable the feature using cc.enableAnwerOnWebex.....

public async decline(): Promise<TaskResponse> {
super.unsupportedMethodError(METHODS.REJECT);
const isOutdial = this.data?.interaction?.outboundType === 'OUTDIAL';
if (!this.enableAnswerOnWebex || !isOutdial) {

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.

can you verify this in webrtc flow, as per this condition normal decline might break

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.

Verified — normal WebRTC decline is unaffected.

WebRTC overrides decline() and calls webCallingService.declineCall() directly; it never enters Voice.decline() wxApp routing.

The wxApp outdial cancelTask path in Voice.decline() is gated by isWebexAppCallingOffer(), which requires TaskState.OFFERED plus the current agent's wxApp participant. Connected or non-wxApp outdial tasks therefore hit unsupportedMethodError, not cancelTask.

Unit coverage: WebRTC.ts spec (decline() calls declineCall) and Voice.ts unified API routing tests for wxApp inbound/outdial gates.

}

export async function acceptOnWebex(
deps: WxAppVoiceDeps,

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.

what is deps and WxAppVoiceDeps can we have a full form of this?

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.

WxAppVoiceDependencies is the full name (dependencies injected into wxApp voice helpers). WxAppVoiceDeps remains as a deprecated alias for backward compatibility in internal imports.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 19, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 19, 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: 612eb2fda0

ℹ️ 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".

* not Browser/Desktop (WebRTC) login.
* @private
*/
private assertWxAppStationLoginSupportedForEnable(): void {

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 Reject enablement until a supported station is selected

The fresh gap in the station-type validation is the pre-station-login case: after Webex device authentication but before stationLogin(), getCurrentStationLoginOption() is undefined, so this negative check allows the public setter to publish answer-calls-on-wxcc: true and start its refresh timer. If the agent subsequently selects BROWSER, ensureWxAppPostStationLogin() returns immediately without disabling or publishing false, leaving Webex App notifications suppressed indefinitely for an unsupported login mode; require the option to be explicitly EXTENSION or AGENT_DN before enabling.

Useful? React with 👍 / 👎.

Comment on lines +1551 to +1552
if (mercury && !mercury.connected) {
await mercury.connect();

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 Release Mercury resources opened for wxApp sync

When an Extension/AGENT_DN profile has WebRTC disabled or does not offer BROWSER login, this path can register the device and connect Mercury solely for wxApp mute sync, but deregister() disconnects/unregisters only under its existing webRtcEnabled && loginVoiceOptions.includes(BROWSER) guard. Unsubscribing the two mute handlers does not close that feature-created socket or device registration, so deregistering such an SDK instance leaves host resources active; track ownership and release resources opened here during teardown.

Useful? React with 👍 / 👎.

this.syncWxAppMuteFromCallDetails().catch(() => undefined);
}

protected onTaskHydrated(): void {

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 answer-pending state across offered-task hydration

When an AgentContact HYDRATE arrives after wxApp answer succeeds but before ASSIGN moves the task out of OFFERED, this hook unconditionally clears wxAppAnswerPending. That immediately re-enables Accept and also removes the marker TaskManager.isAgentTerminatedOutdialWrapup() uses to distinguish an answered outdial from a pre-accept cancellation, so a reconnect/update during this window can permit another answer request or misclassify the subsequent ContactEnded; clear the flag only after assignment or when hydrated data proves the offer is no longer pending.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 19, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 19, 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: 1e0b7d720c

ℹ️ 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
cc: {
allowMultiLogin: isMultiLoginEnabled,
disableWebRTCRegistration: isWebRTCRegistrationDisabled,
enableAnswerOnWebex: false,

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 Pass the persisted answer flag into SDK initialization

When the checkbox is persisted as enabled and the page reloads into the silent-relogin flow, this hardcoded false causes the SDK to publish the disabled cross-client state. The sample only calls the runtime setter from agent:stationLoginSuccess, while silent relogin emits agent:reloginSuccess, whose handler does not enable the feature, so Answer on Webex remains disabled despite the checked setting. Fresh evidence beyond the earlier initialization concern is that this revision explicitly ignores isAnswerOnWebexEnabled when generating the SDK configuration.

Useful? React with 👍 / 👎.

Comment thread docs/samples/contact-center/app.js Outdated
Comment on lines +3337 to +3338
const currentlyMuted = muteElm.innerText === 'Unmute';
const intendedMuted = !currentlyMuted;

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 Derive the mute action from the SDK mute state

When Mercury or the call-details backfill changes the wxApp mute state, the SDK emits task:wxapp-mute-state-updated, but the sample never handles that event, so the button text used here can be stale. For example, after muting in Webex App while the button still says “Mute,” clicking it sends {muted: true} again instead of unmuting; listen for the new event/update the label, or let the SDK's no-argument toggle derive the target from getWxAppMuted().

Useful? React with 👍 / 👎.

| `contact-center.user-preference` | Services/Config | `UserPreference`, `cc.userPreference`, `UserPreferenceData`, request/response types | `getUserPreference(params?)`, `createUserPreference(data)`, `updateUserPreference(userId, data)`, `deleteUserPreference(userId)` | semver public | `src/services/ai-docs/services-spec.md` | `src/index.ts`, `src/cc.ts`, `src/services/UserPreference.ts`, `src/services/config/types.ts` |
| `contact-center.preview-campaign` | Contact Center/Task | `acceptPreviewContact`, `skipPreviewContact`, `removePreviewContact` | `(payload: PreviewContactPayload) => Promise<TaskResponse>` | semver public | `ai-docs/contact-center-spec.md`, `src/services/task/ai-docs/task-spec.md` | `src/cc.ts`, `src/services/task/dialer.ts`, `src/services/task/types.ts` |
| `contact-center.state-controls` | Task state machine | `getDefaultUIControls`, task state/control types | function is exported directly from `uiControlsComputer.ts` by package root | semver public | `src/services/task/state-machine/ai-docs/task-state-machine-spec.md` | `src/index.ts`, `src/services/task/state-machine/uiControlsComputer.ts` |
| `contact-center.wxapp-answer` | Contact Center / Task | `enableAnswerOnWebex`, `setManageWebexCallingInWxcc`, `getWxAppMuted`, `syncWxAppMuteFromCallDetails`, unified task telephony (`ITask.accept`, `decline`, `toggleMute({ muted?, lineOwnerId? })`, `transmitDtmf({ dtmf, lineOwnerId? })`) | init flag ON → usersub `true` + Mercury on supported station login **and silent relogin**; init flag OFF → force usersub `false` on supported station login **and silent relogin** (clears stale suppression after refresh); runtime toggle via setter; `setManageWebexCallingInWxcc(true)` rejects `BROWSER` login; mute backfill via GET call details + `getWxAppMuted()` on hydrate/refresh; SDK `Voice` routes wxApp telephony when flag active | semver public | `src/services/task/ai-docs/task-spec.md`, `ai-docs/contact-center-spec.md`, `ai-docs/WXCC-6026-wxapp-answer-flow.md` | `src/cc.ts`, `src/services/task/voice/Voice.ts`, `src/services/task/voice/wxAppVoiceMethods.ts` |

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 Point the wxApp contract at an existing specification

The new contract row links to ai-docs/WXCC-6026-wxapp-answer-flow.md, but that file does not exist anywhere in the package, and the manifest-designated owning ai-docs/contact-center-spec.md was not updated by this commit. Fresh evidence beyond the previous contract comment is that the attempted catalog remediation itself introduces a dead detail link, leaving the public configuration and runtime method without an owning canonical specification; update that spec and reference an existing document.

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

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 20, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 20, 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: a37df0fc44

ℹ️ 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".

this.webCallingService.deregisterWebCallingLine();
}

await this.teardownWxAppLocalState();

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 init flag across profile updates

When enableWxBetterTogether is initialized as true, updateAgentProfile() calls stationLogout() and then stationLogin() on the same SDK instance. This teardown resets $config.enableWxBetterTogether to false, so the subsequent login force-publishes false and no longer initializes wxApp answering; switching from BROWSER to EXTENSION—the documented profile-update example—therefore silently loses the feature. Preserve the init setting across this internal logout/login sequence while still clearing runtime resources for a genuine logout.

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

Useful? React with 👍 / 👎.

method: METHODS.SET_MANAGE_WEBEX_CALLING_IN_WXCC,
}
);
this.scheduleForcedFalsePublishRetry();

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 Schedule rollback retries after disabling the flag

If the usersub true publish succeeds, Mercury initialization fails, and the compensating false publish also fails, this retry request is made while enableWxBetterTogether is still true. scheduleForcedFalsePublishRetry() consequently returns immediately because of its enabled-state guard, and the flag is reset only after this method returns, leaving the already-published suppression active until its TTL expires with no retry. Reset the flag before scheduling or allow rollback retries independently of the enabled guard.

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

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 20, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 20, 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: 1dc4aec77f

ℹ️ 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 +1744 to +1745
} catch (error) {
publishError = error;

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 Retry failed teardown publishes

When stationLogout() calls this helper without rethrowPublishError, a transient usersub failure is only recorded before webexCrossClientService.teardown() clears the active-state marker and refresh timers. No forced-false retry is scheduled, so the last successful answer-calls-on-wxcc: true composition can continue suppressing Webex App call notifications for up to its 900-second TTL after the agent has logged out; schedule the existing bounded false-publish retry for this teardown path.

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

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 20, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 20, 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: 6a941e29a3

ℹ️ 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".

// Clear any cached agent configuration
this.agentConfig = null;

await this.teardownWxAppLocalState({rethrowPublishError: true, clearInitFlag: true});

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 Publish the disabled state before unregistering the device

When deregister() is called directly for a WebRTC-capable profile while Better Together is active, the preceding branch successfully calls device.unregister() before reaching this teardown; the device implementation then clears its state (packages/@webex/internal-plugin-device/src/device.js:721), including the URL that WebexCrossClientService.setManageWebexCallingInWxcc() requires. The false publish therefore fails, and its retries cannot recover without a device URL, leaving Webex App notifications suppressed until the prior 900-second composition expires. Move this teardown ahead of the generic device unregister or preserve the device identity needed for the publish.

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

Useful? React with 👍 / 👎.

Comment on lines +1623 to +1626
await this.publishAnswerOnWebexCrossClientState(false, {force: true});
this.webexCrossClientService.teardown();
this.wxAppTelephonyMercurySync.unsubscribe();
await this.releaseWxAppMercuryResources();

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 Cancel stale compensating retries after a new login

If the false publish during stationLogout() fails, an unconditional compensating retry is scheduled while the init flag remains enabled. When the agent logs back in within the 30-second retry delay, the successful true publish does not clear this ContactCenter-level timer, so this callback later publishes false and tears down Mercury for the newly active session. Cancel or generation-guard compensating retries when a subsequent enable/login succeeds.

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

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 20, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 20, 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: e261e6f6ee

ℹ️ 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 +1552 to +1554
await this.publishAnswerOnWebexCrossClientState(true);
publishedEnable = true;
await this.ensureWxAppMercuryAndSubscribe();

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 device before publishing the enabled state

When an EXTENSION/AGENT_DN profile has not already connected Mercury—for example, webRtcEnabled is false or BROWSER login is unavailable—the WDM device can still be unregistered here. publishAnswerOnWebexCrossClientState(true) then either returns without publishing because device.userId is absent or fails because device.url is absent; the registration in ensureWxAppMercuryAndSubscribe() happens only afterward. Register/connect first and then publish, otherwise fresh extension-only sessions never suppress Webex App notifications.

Useful? React with 👍 / 👎.

this.services.rtdWebSocketManager.off('message', this.handleRTDWebsocketMessage);
this.services.connectionService.off('connectionLost', this.handleConnectionLost);

await this.teardownWxAppLocalState({rethrowPublishError: true, clearInitFlag: true});

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 Continue deregistration after a usersub publish failure

When the false usersub publish fails, this awaited helper performs the new local wxApp cleanup but then rethrows, so deregister() skips the Mercury/device cleanup, both WebSocket close() calls, and agentConfig reset below. Fresh evidence beyond the earlier teardown report is that wxApp-local cleanup now runs, but the broader SDK deregistration still aborts and leaves backend sockets active with their message handlers already removed; retain the publish error and throw it only after the remaining cleanup finishes.

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

Useful? React with 👍 / 👎.

Comment on lines +193 to +197
[TaskEvent.TASK_WRAPUP]: [
{
guard: guards.shouldWrapUp,
target: TaskState.WRAPPING_UP,
actions: ['updateTaskData', 'markEnded', 'emitTaskWrapup'],

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 Document the new OFFERED wrap-up branch

This changes AGENT_WRAPUP while OFFERED to enter WRAPPING_UP whenever shouldWrapUp succeeds, and the mapper always supplies wrapUpRequired: true, but the owning canonical spec still states OFFERED TASK_WRAPUP unconditionally reaches TERMINATED at task-state-machine-spec.md lines 862-866 and again in the event table at line 1749. Update those state descriptions and diagrams so validators and future lifecycle changes use the implemented branch.

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

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 20, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 20, 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: 96088a9453

ℹ️ 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".

publishedEnable = true;
this.taskManager.syncWxAppMuteFromCallDetailsForAllTasks();
} else {
await this.publishAnswerOnWebexCrossClientState(false, {force: true});

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 Register the device before the forced disabled publish

When a page reload starts with the flag disabled on an EXTENSION/AGENT_DN profile that did not connect Mercury during register() (for example, WebRTC is disabled or BROWSER login is unavailable), the WDM device is still unregistered. This forced publish does not register it: publishAnswerOnWebexCrossClientState() silently returns when device.userId is absent, or WebexCrossClientService rejects when the device URL is absent, and retries encounter the same state. Consequently, a prior session's true composition is not cleared and Webex App notifications remain suppressed until its 900-second TTL expires; establish the device identity before attempting this forced false publish.

Useful? React with 👍 / 👎.

Comment on lines +1834 to +1835
if (!enable && !options?.force && !this.webexCrossClientService.isAnswerCallsStateActive()) {
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 Do not skip teardown while an enable publish is pending

If stationLogout() or deregister() overlaps an enable publish—for example, when a reconnect-triggered silent relogin races with logout—answerCallsState is still false until that request resolves, so this guard skips the compensating false publish. Teardown then changes the generation, but the outstanding true request can still be applied by usersub; the generation check only discards its local completion, leaving call-notification suppression active for the full TTL after teardown. Track pending enable operations or serialize the publish sequence so teardown always follows an in-flight true with false.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 20, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 20, 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: 7877419fab

ℹ️ 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".

})) as IHttpResponse;

return (response.body ?? {}) as CallDetailsResponse;
} catch (error) {

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 Suppress expected call-detail failures before logging

When the post-accept mute backfill races call creation and Hydra returns the expected 400/“Call not found”/101002 response, this service logs the rejection as an error before Voice.syncWxAppMuteFromCallDetails() can classify and suppress it. The canonical task spec explicitly says these expected responses are not logged, but the existing Voice test mocks this service and therefore misses the lower-layer log; classify expected call-not-found responses here before calling LoggerProxy.error.

AGENTS.md reference: packages/@webex/contact-center/AGENTS.md:L46-L53

Useful? React with 👍 / 👎.

Comment on lines +399 to +400
if (this.wxAppMuteToggleInFlight) {
await this.wxAppMuteToggleInFlight;

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 Continue queued mute operations after an earlier failure

When two toggleMute() calls overlap and the first telephony request rejects, the second call rethrows that first rejection at this await and exits without issuing its own requested mute/unmute operation. This is especially visible for rapid no-argument retries: the second toggle never gets a chance to recompute its target from the unchanged mute state. Wait for settlement while swallowing the predecessor's error only within the queue, leaving the first caller's promise rejected independently.

Useful? React with 👍 / 👎.

this.webCallingService.deregisterWebCallingLine();
}

await this.teardownWxAppLocalState();

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 the Better Together flag on genuine station logout

When an application performs a genuine stationLogout() and later logs in again on the same SDK instance, this call tears down usersub and Mercury but does not pass clearInitFlag, so $config.enableWxBetterTogether and all task-manager flags remain enabled and the next station login republishes true. This contradicts the canonical teardown contract that logout resets runtime config; preserve the flag only for the internal updateAgentProfile() logout/login sequence while clearing it for the public logout path.

AGENTS.md reference: packages/@webex/contact-center/AGENTS.md:L46-L53

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 20, 2026
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 20, 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: 77a11a880a

ℹ️ 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 +496 to +498
if (wxAppAnswerPending && isWxAppInboundOffer) return VISIBLE_DISABLED;
if (isWxAppInboundOffer) return VISIBLE_ENABLED;
if (isWxAppOutdialOffer) return VISIBLE_ENABLED;

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 outdial decline while answer assignment is pending

After a wxApp outdial answer request succeeds, runWxAppAccept() leaves wxAppAnswerPending true until ASSIGN arrives, but this branch immediately re-enables Decline once the REST request is no longer in flight. Clicking it during that window calls cancelTask() and clears the pending marker, canceling an already-answered outdial and causing the ensuing terminal event to be treated as a pre-accept cancellation. Keep Decline disabled while wxAppAnswerPending is true for outdial offers as well, and guard the public decline() path similarly.

Useful? React with 👍 / 👎.

rethrowPublishError?: boolean;
clearInitFlag?: boolean;
}): Promise<{publishError?: unknown}> {
this.clearWxAppFalsePublishRetryTimer();

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 pending forced-false retries during teardown

When the forced false publish at a disabled EXTENSION/DN login fails, ensureWxAppPostStationLogin() schedules a retry, but a station logout or deregistration within the 30-second delay cancels it here. Because the feature flag and service active marker are both false, the teardown's subsequent publish is skipped and no replacement retry is scheduled, so a prior session's true composition can continue suppressing Webex App notifications until its TTL expires. Do not clear the pending cleanup retry unless teardown confirms a false publish or schedules an equivalent retry.

Useful? React with 👍 / 👎.

@mkesavan13 mkesavan13 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.

Just a few nitpicks. Rest looks great. Please go ahead and merge.

Thanks a lot @akulakum, @rsarika and @vivekv1504 for working on this one 💪🏻

Comment on lines +175 to +177
if (options) {
// parameter intentionally unused — WebRTC toggles from local stream state
}

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.

nitpick: Do we really need this block?

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.

nitpick: Could have named wxAppVoiceUtils instead of methods

Comment on lines +159 to +161
if (options) {
// parameter intentionally unused
}

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.

nitpick: Do we really need this and wherever similar blocks are empty?

Comment on lines +283 to +311
/**
* Update wxApp thick-client answer flag at runtime (Voice overrides).
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public setEnableWxBetterTogether(_enabled: boolean): void {
// no-op for non-voice tasks
}

/**
* Apply wxApp mute state from external sync (Voice overrides).
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public applyWxAppMuteStateFromSync(_incomingCallId: string, _muted: boolean): void {
// no-op for non-voice tasks
}

/**
* Hook for post-assign wxApp sync (Voice overrides).
*/
protected onTaskAssigned(): void {
// no-op by default
}

/**
* Hook for post-hydrate wxApp sync (Voice overrides).
*/
protected onTaskHydrated(): void {
// no-op by default
}

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.

Once again, isn't it enough we declare it in types but not write an empty one here to override in Voice?

@mkesavan13
mkesavan13 merged commit 2adc996 into webex:next Aug 20, 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