fix(task-refactor): consult flow bug bash fixes#4962
Conversation
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
| }; | ||
|
|
||
| const isActiveConsultState = (taskData: TaskData | undefined, selfAgentId?: string): boolean => { | ||
| if (taskData?.interaction?.state === 'consulting') return true; |
There was a problem hiding this comment.
are we not using constants for 'consulting' and 'post_call' etc. if not consider doing it
There was a problem hiding this comment.
Good catch! Added INTERACTION_STATE and CONSULT_STATE constants in constants.ts and updated all usages in actions.ts and guards.ts to use them instead of raw strings.
|
|
||
| return ( | ||
| taskData?.interaction?.isTerminated === true && | ||
| shouldWrapUpForThisAgent(context, taskData) |
There was a problem hiding this comment.
Here using shouldWrapUpForThisAgent should suffice right why do we need extra conditions here ? We already have matches present for cases where initiator has to go back to HELD after end consult and consulted agent goes to terminated. We do not need this elaborated guard
There was a problem hiding this comment.
shouldWrapUpForThisAgent alone would match even when the consult ends normally (where the initiator should return to HELD or CONFERENCING). The isTerminated check distinguishes between: (1) normal consult end — Agent 2 hangs up, main call still active → initiator goes to HELD, and (2) interaction terminated — customer left during consult → initiator should wrap up. Without isTerminated, this guard fires prematurely during a normal consult end and swallows transitions meant for the subsequent guards below it.
| [TaskEvent.CONSULT_END]: { | ||
| actions: ['updateTaskData', 'clearConsultState'], | ||
| }, | ||
| [TaskEvent.CONSULT_END]: [ |
There was a problem hiding this comment.
Which case this new transitions would be handling ?
There was a problem hiding this comment.
This handles: Agent 1 merges EP_DN consult → enters CONFERENCING → clicks End Call → backend sends AgentConsultEnded with isTerminated: true. Previously, CONSULT_END in CONFERENCING only cleared consult state without transitioning, so the agent stayed stuck in CONFERENCING instead of moving to WRAPPING_UP. The guard checks isTerminated && shouldWrapUpForThisAgent to only transition when the call has actually ended.
| actions: ['handleConferenceFailed', 'emitTaskConferenceFailed'], | ||
| }, | ||
| // AgentConsultEnded while conference is initiating (end call before conference completes) | ||
| [TaskEvent.CONSULT_END]: [ |
There was a problem hiding this comment.
I would like to understand these 2 cases as well, Could you please elaborate these for me
There was a problem hiding this comment.
These are defense-in-depth handlers for race conditions. After Agent 1 clicks merge, the state machine enters CONF_INITIATING while awaiting CONFERENCE_START. If the call ends or consult ends before the conference confirmation arrives (e.g., network delay, Agent 1 immediately clicks End Call), these events would be dropped since CONF_INITIATING previously only handled CONFERENCE_START and CONFERENCE_FAILED.
Two cases:
- isTerminated === true → interaction has ended, transition to WRAPPING_UP
- Not terminated → consult ended normally, transition back to CONNECTED
| const isActiveConsultState = (taskData: TaskData | undefined, selfAgentId?: string): boolean => { | ||
| if (taskData?.interaction?.state === INTERACTION_STATE.CONSULTING) return true; | ||
| if (taskData?.interaction?.state === INTERACTION_STATE.POST_CALL && selfAgentId) { | ||
| const selfParticipant = taskData.interaction?.participants?.[selfAgentId] as any; |
There was a problem hiding this comment.
Using any should be avoided as much as possible
There was a problem hiding this comment.
Fixed. Removed as any cast on selfParticipant (direct property access from participants map) and used explicit type assertion (media as {mType?: string})?.mType for media objects instead of as any.
| }; | ||
|
|
||
| const selfAgentId = context.uiControlConfig.agentId ?? taskData?.agentId; | ||
| const consultingActive = isActiveConsultState(taskData, selfAgentId); |
There was a problem hiding this comment.
Isn't there a simpler way to figure out if there is an active consult ? State machine state itself would be consulting for active consult. We are using isActiveConsult only in one place and it looks like unncessary complicated logic to just figure out if there is an active consult.
There was a problem hiding this comment.
This function is used specifically during hydration (page refresh) in deriveTaskDataUpdates. At that point, the state machine state hasn't been restored yet — we're inspecting raw TaskData from the backend to determine which context fields to populate. We can't rely on the state machine's current state because it's being rebuilt from scratch. The post_call case is needed because when the customer leaves during a consult, interaction.state becomes 'post_call' but the consult between agents is still active. Without this check, consult context fields wouldn't be populated on refresh, and the UI would lose the consulting section.
| const consultDestinationType = | ||
| 'destinationType' in event ? event.destinationType ?? null : null; | ||
| const consultDestinationAgentId = 'destAgentId' in event ? event.destAgentId ?? null : null; | ||
| const consultDestinationAgentId = |
There was a problem hiding this comment.
Why was this change required ?
There was a problem hiding this comment.
For EP_DN consults, event.destAgentId may not be present in the event payload, but event.destination contains the Entry Point UUID needed for conference API calls. This change ensures we store the EP_DN UUID in consultDestinationAgentId so that Voice.consultConference() can resolve the correct destAgentId when initiating a conference, preventing the 400 Bad Request error.
|
|
||
| if (taskData.destAgentId) { | ||
| updates.consultDestinationAgentId = taskData.destAgentId; | ||
| const isEpDnWithStoredId = |
There was a problem hiding this comment.
Please explain this change
There was a problem hiding this comment.
During hydration, taskData.destAgentId can contain the EP_DN phone number (e.g., +13159998059) instead of the Entry Point UUID. If the context already has the correct UUID stored from the live consult flow (consultDestinationType === 'entryPoint' and consultDestinationAgentId exists), we skip overwriting it with the phone number. Without this, after a page refresh, the conference API would send the phone number instead of the UUID, causing a 400 Bad Request.
| } | ||
|
|
||
| // Customer left during consult: interaction state is "post_call" but consult | ||
| // between agents is still active. Detect via agent's consultState + consult media. |
There was a problem hiding this comment.
How was this guard impacting the flow and this change was needed here ? and this looks very similar to what we are doing in isActiveConsult as well
There was a problem hiding this comment.
The isInteractionConsulting guard determines which state the machine hydrates into on page refresh. Without this post_call check, when the customer leaves during an active consult, the guard returns false (since interaction.state is 'post_call', not 'consulting'), and the state machine hydrates to CONNECTED instead of CONSULTING — causing the agent to lose the entire consulting section and controls. The similarity with isActiveConsultState is by design: the guard decides the target state (CONSULTING), while isActiveConsultState in actions decides whether to populate consult context fields. Both need the same detection logic at different layers. We can extract a shared utility if preferred.
| isConsulted: true, | ||
| }, | ||
| 'customer-1': {id: 'customer-1', pType: 'CUSTOMER', hasLeft: false}, | ||
| 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false}, |
There was a problem hiding this comment.
Did we not need any other unit test changes ?
There was a problem hiding this comment.
Added 11 new unit tests covering: (1) conference controls — pending vs real conference (transfer/recording/consult/exitConference behavior based on participant count), (2) hold visibility in conference, (3) post-call consult controls when customer left, (4) CONF_INITIATING state transitions (CONSULT_END, CONTACT_ENDED, TASK_WRAPUP), (5) CONFERENCING → WRAPPING_UP on CONSULT_END with isTerminated, and (6) isInteractionConsulting guard for EP_DN and post_call hydration scenarios.
| // derivedDestAgentId is most reliable as it resolves epId for EP_DN | ||
| // and agent ID for regular agents from live interaction data | ||
| destAgentId: | ||
| derivedDestAgentId || |
There was a problem hiding this comment.
No, it's not always present — it's derived from calculateDestAgentId() which inspects live interaction participants. It returns empty string if no consulted agent or EP_DN participant is found. That's why we have a fallback chain: derivedDestAgentId || context.consultDestinationAgentId || this.data.destAgentId. The derivedDestAgentId is the most reliable source because it resolves the correct EP_ID UUID for EP_DN from the interaction's participant epId field, but we fall back to context/taskData if the interaction data isn't available yet.
…ActiveConsultInPostCall utility
…ouldWrapUpForThisAgent
|
COMPLETES #https://jira-eng-sjc12.cisco.com/jira/browse/CAI-7897
This pull request addresses
Issues in the contact center SDK task-refactor state machine related to the consult flow lifecycle and 3-party conference controls. The uiControlsComputer, state machine guards, actions, task manager, and voice service did not correctly handle: (1) post-call lifecycle when the customer leaves a 3-way consult interaction, (2) conference UI controls during and after merge, (3) EP_DN conference API payload resolution, and (4) pending vs real conference state distinction.
by making the following changes
1. Fix extra controls visible after customer ends call (uiControlsComputer.ts)
customerPresentis false during an active consult,transfer,conference,mergeToConference, andswitchcontrols now returnVISIBLE_DISABLEDon the consult leg andDISABLED(hidden) on the main leg.recordingcontrol on the main leg returnsDISABLEDwhenhasParallelConsultLeg && !customerPresent.2. Emit post-call activity event to Widgets (TaskManager.ts)
CC_EVENTS.PARTICIPANT_POST_CALL_ACTIVITYis received, the task manager now explicitly emitsTASK_EVENTS.TASK_POST_CALL_ACTIVITYon the task object so Widgets can render the "Post Call" timer label.removeTaskFromCollectioninhandleContactMergedEventto preventcancelAutoWrapupTimerTypeError.3. Fix hydration failure during post-call state (guards.ts, actions.ts)
isInteractionConsultingguard to return true wheninteraction.state === 'post_call'if self-agent'sconsultState === 'consulting'and consult media exists.isActiveConsultStatehelper to correctly gate hydration of consult context fields during post-call refresh.4. Fix incorrect wrap-up transition and conference state actions (TaskStateMachine.ts)
consultInitiator === true,interaction.isTerminated === true, andshouldWrapUpForThisAgentis true, transitions directly fromCONSULTING → WRAPPING_UP.updateTaskData/syncTaskDataFromEventactions toCONFERENCE_STARTandUNHOLD_SUCCESStransitions to ensure task data is current when entering conference.clearConsultStateaction toCONF_INITIATING → CONFERENCINGtransition to reset stale consult context.5. Fix conference Exit Conference button for pending state (uiControlsComputer.ts)
exitConferencereturnsDISABLEDwhenparticipantCount <= 1(pending conference where only initiating agent is present).6. Fix EP_DN conference 400 Bad Request (Voice.ts)
consultConference()to usederivedDestAgentId/derivedDestType(computed from live interaction data viacalculateDestAgentId/calculateDestType) as highest priority, ensuring EP_ID UUID is used instead of phone number.7. Fix conference controls for pending vs real conference (uiControlsComputer.ts)
participantCountto distinguish pending conference (only self agent in mainCall) vs real multi-agent conference:8. Fix missing wrapup screen after end call from pending conference (TaskManager.ts, TaskStateMachine.ts)
AgentConsultConferencingto theCONFERENCE_STARTevent mapping in TaskManager so the state machine correctly transitions fromCONF_INITIATINGtoCONFERENCINGwhen Agent 1 merges before Agent 2 accepts.CONSULT_END,CONTACT_ENDED, andTASK_WRAPUPhandlers to theCONF_INITIATINGstate for defense-in-depth, ensuring wrapup transitions are never dropped.CONFERENCINGstate'sCONSULT_ENDhandler to transition toWRAPPING_UPwhenisTerminated === true.Change Type
The following scenarios were tested
< ENUMERATE TESTS PERFORMED, WHETHER MANUAL OR AUTOMATED >
The GAI Coding Policy And Copyright Annotation Best Practices
I certified that
Make sure to have followed the contributing guidelines before submitting.