Skip to content

feat(user-state): handle browser voice connection loss (CAI-8629) - #756

Open
cmullenx wants to merge 7 commits into
webex:nextfrom
cmullenx:chrmulle/webrtcErrors
Open

cmullenx wants to merge 7 commits into
webex:nextfrom
cmullenx:chrmulle/webrtcErrors

Conversation

@cmullenx

@cmullenx cmullenx commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

COMPLETES # https://jira-eng-sjc12.cisco.com/jira/browse/CAI-8629

This pull request addresses

showing the user an error message when mobius connection drops and preventing user state changes to achieve parity with agent desktop

by making the following changes

  • adds dialog for when mobius connection drops
  • prevents changes to user state when mobius connection ot ready

Vidcast: https://app.vidcast.io/share/40b94562-07d1-4abc-aa12-c382443df773

< DESCRIBE YOUR CHANGES >

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

  • The testing is done with the amplify link
    < ENUMERATE TESTS PERFORMED, WHETHER MANUAL OR AUTOMATED >

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 - Codex
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

Checklist before merging

  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the testing document
  • I have tested the functionality with amplify link

Make sure to have followed the contributing guidelines before submitting.

@cmullenx
cmullenx requested a review from a team as a code owner November 5, 2026 12:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T16:31:33.725894Z c018359 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cmullenx cmullenx added the validated Indicates that the PR is ready for actions label Nov 5, 2026
@aws-amplify-us-east-2

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-756.d1b38q61t1z947.amplifyapp.com

@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: 2f601f221f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

- Guard voice snapshot initialization when cc is undefined or getVoiceConnectionState is not yet exposed
- Preserve forced Idle state transition during wellness-code lookup
- Synchronize recovery dialog to open only after Idle state transition successfully resolves
- Anchor select overlays in user-state-select-wrapper independently of alert message heights
@github-actions github-actions Bot removed the validated Indicates that the PR is ready for actions label Nov 10, 2026
@brain-frog brain-frog added the validated Indicates that the PR is ready for actions label Nov 10, 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: e066d4db9b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const defaultIdleCode = idleCodes.find((idleCode) => idleCode.isDefault);
if (defaultIdleCode && currentStateRef.current !== defaultIdleCode.id) {
pendingVoiceLossDialogRef.current = true;
setAgentStatus(defaultIdleCode.id);

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 Read the live wellness-lookup flag before forcing Idle

When a wellness-enabled session starts loadWellbeingBreakIdleCode() during the two-second loss window (for example, AGENT_RELOGIN_SUCCESS calls it in storeEventsWrapper.ts:2368-2372), this timeout still invokes the setAgentStatus closure from the earlier READY→UNAVAILABLE render. The fresh fix routes the forced change through setAgentStatus, but because the voice-loss effect neither reads a ref for nor is recreated from wellnessIdleCodeLookupPending, that closure sees the old false value and does not populate pendingUserSelectionRef; the current-state effect then advances prevStateRef while lookup is pending, so lookup completion never calls cc.setAgentState and the recovery dialog never opens. Record the forced transition using the latest lookup state.

Useful? React with 👍 / 👎.

Comment on lines +131 to +132
} else {
setVoiceConnectionLossDialogOpen(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 Avoid claiming Idle when no default code exists

If registration returns no idle code with isDefault: true (a shape already represented by test-fixtures/src/fixtures.ts:48), defaultIdleCode is undefined and this else opens the recovery modal without ever calling setAgentStatus; the user is therefore told that their availability changed to Idle while both the store and backend remain unchanged. The new success-gating covers rejected setAgentState calls, but not this no-default path, so distinguish it from the already-in-default case and avoid the false dialog copy.

Useful? React with 👍 / 👎.

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.

If there is no default idle code when the timeout expires, neither branch opens the recovery dialog. The selector remains disabled, but the agent has no way to access the Reconnect action. If the idle codes arrive later, the effect reruns with the previous voice status already UNAVAILABLE, so it does not restart the loss flow. Could we show recovery guidance without claiming that the agent changed to Idle, and handle a default code becoming available after the timeout?

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.

Fixed in a9f8d21 — When no default idle code exists at timeout, the recovery dialog now still opens with neutral recovery copy ("You're no longer connected to Desktop. Check your internet connection, then try reconnecting.") so the agent retains access to the Reconnect action without false Idle claims. If idleCodes arrives with a default code after the timeout has elapsed, the hook automatically requests the transition and updates the copy to reflect Idle once settled.

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

I'll review the rest of the changes during the day. Wanted to send out a quick review

buildDropdownItems,
} from './user-state.utils';
import {withMetrics} from '@webex/cc-ui-logging';
import {VOICE_CONNECTION_STATUS, VoiceLineStatus} from '@webex/cc-store';

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.

I see no evidence of VOICE_CONNECTION_STATUS or VoiceLineStatus being available in the entire cc-store codebase.

Search link for next branch:

Search link for cmullenx:chrmulle/webrtcErrors branch:

Did we miss checking in the entire store folder?

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

Local Agent Desktop parity review feedback.

<Button
slot="footer-button-primary"
type="button"
disabled={!onVoiceReconnect}

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.

When the host does not pass onVoiceReconnect, this button is disabled. Existing UserState embeds therefore show a Reconnect action the user cannot use; the sample’s regular UserState instance also omits this callback. Agent Desktop wires the action to registerWebCalling. Please provide a host integration example that supplies a real retry callback, or clearly document that reconnect requires host wiring before describing this as full parity.

@akulakum

Copy link
Copy Markdown
Contributor

@cmullenx, could you please address the outstanding Codex review comments? The findings are valid, especially the Wellness Break timeout and lookup edge cases, and the case where the dialog says the agent changed to Idle when no default idle code exists. Thanks.

…(CAI-8629)

- Only open recovery dialog when default idle code exists and transition settles (or was already in default idle)
- Suppress opening recovery dialog with false Idle claim when profile has no default idle code
- Wire onVoiceReconnect on sample UserState embed and document host retry contract in user-state spec
@github-actions github-actions Bot removed the validated Indicates that the PR is ready for actions label Nov 12, 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: 6899699ea8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

…ate default codes (CAI-8629)

- Bind recovery dialog specifically to the settled forced Idle request to prevent concurrent in-flight updates from falsely opening the modal
- Show recovery dialog with non-Idle copy when no default idle code exists so Reconnect action remains accessible
- Transition to default idle code if default code arrives after voice-loss timeout has elapsed
- Omit onVoiceReconnect from regular sample embed while preserving interactive preview

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +119 to +121
if (defaultIdleCode && currentStateRef.current !== defaultIdleCode.id) {
forcedVoiceLossIdleCodeRef.current = defaultIdleCode.id;
setAgentStatus(defaultIdleCode.id);

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 Prevent rejected forced-Idle requests from retrying on every render

Once the two-second loss timeout fires—for example, when CSP blocks the Blob worker—a rejected forced-Idle request rolls currentState back but leaves hasVoiceLossTimeoutElapsedRef set. StoreWrapper.idleCodes returns a fresh filtered array on every read (storeEventsWrapper.ts:177-180), so the rollback render re-enters this branch and immediately initiates another state-change request. Persistent rejection therefore creates an unbounded request/rollback loop without another connection event or user retry. Limit this latch to waiting for an initially missing default code and consume it once the forced transition is issued.

Useful? React with 👍 / 👎.

.catch((error) => {
if (forcedVoiceLossIdleCodeRef.current === currentState) {
forcedVoiceLossIdleCodeRef.current = null;
setVoiceConnectionLossDialogOpen(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 Keep reconnect accessible when the forced Idle update fails

If the forced Idle request rejects while voice remains unavailable, a host-provided onVoiceReconnect becomes inaccessible through the widget: this suppresses the dialog, and UserStateComponent renders its only Reconnect button inside that dialog. The selector remains disabled, while the warning and state-change error provide no retry action, so continued state-update failures also prevent the agent from requesting voice recovery. The component already supports truthful non-Idle dialog copy; keep that recovery UI, or an equivalent reconnect control, available independently of availability-update success.

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +146 to +147
forcedVoiceLossIdleCodeRef.current = defaultIdleCode.id;
setAgentStatus(defaultIdleCode.id);

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 Persist forced Idle transitions out of WellbeingBreak

If this timeout expires during WellbeingBreak with wellness lookup already complete—for example, in a host that blocks Blob workers—externallyManagedStateRef is still true. This store update changes isCurrentStateExternallyManaged to false, but the current-state effect's isCurrentStateExternallyManaged || externallyManagedStateRef.current branch consumes the transition as an SDK restoration, advancing prevStateRef without calling cc.setAgentState. The dropdown therefore moves to the default Idle code while the backend remains on the wellness code, and the success continuation that opens the recovery dialog never runs. Distinguish explicit forced voice-loss requests from SDK restoration so this transition is persisted rather than suppressed.

Useful? React with 👍 / 👎.

…low (CAI-8629)

- Read live wellnessIdleCodeLookupPendingRef before forcing Idle to prevent stale timeout closures from skipping pending state registration
- Persist forced Idle transitions out of WellbeingBreak rather than suppressing them as SDK wellness restorations
- Stabilize voice loss timeout against displayedIdleCodes and idleCodes array reference changes
- Prevent rejected forced-Idle requests from retrying on every render
- Keep recovery dialog accessible when forced Idle update fails

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +137 to +140
if (defaultIdleCode && currentStateRef.current !== defaultIdleCode.id) {
hasForcedIdleAttemptedRef.current = true;
forcedVoiceLossIdleCodeRef.current = defaultIdleCode.id;
setAgentStatus(defaultIdleCode.id);

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 Persist forced Idle when it matches the last confirmed state

If an agent starts in the default Idle code, selects Available, and that request remains pending beyond the two-second voice-loss timeout, this write returns currentState to prevStateRef.current. The current-state effect therefore skips updateAgentState, so no compensating Idle request is sent and no recovery-dialog continuation is installed. If the original Available request subsequently succeeds, the agent can remain Available while voice is unavailable. Queue the forced transition behind the in-flight change, or explicitly persist it even when its target matches the last confirmed state.

Useful? React with 👍 / 👎.

>
Simulate voice loss (2 seconds)
</Button>
<UserStateComponent

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 Isolate the readiness preview from live-widget selectors

When User State is enabled in the React sample, this mounts a second state control alongside the live <UserState>, duplicating the state-select and elapsed-time test IDs. playwright.config.ts:13-16 serves this app, while playwright/tests/user-state-test.spec.ts:41 and playwright/Utils/userStateUtils.ts:57-62 use unscoped single-element locators, which now encounter strict-mode violations; changeUserState also catches the ambiguity and silently skips the requested change. Make the preview opt-in or introduce instance-specific selectors and scope the affected helpers to the live widget.

Useful? React with 👍 / 👎.

) {
hasVoiceLossTimeoutElapsedRef.current = false;
hasForcedIdleAttemptedRef.current = false;
forcedVoiceLossIdleCodeRef.current = null;

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 deferred forced-Idle intent across voice recovery

When an already-known WellbeingBreak session starts another wellness-code lookup, such as during relogin, the loss timeout can select default Idle while persistence waits for that lookup. If voice becomes READY before lookup finishes, this reset clears the marker required by the new isForcedVoiceLossTransition bypass. Lookup completion then takes the externally managed restoration branch because externallyManagedStateRef is still true, advances prevStateRef, and never calls cc.setAgentState, leaving default Idle in the store while the backend retains the wellness code. Preserve the deferred marker until persistence is processed, or revert the optimistic state if recovery cancels it.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants