-
Notifications
You must be signed in to change notification settings - Fork 395
fix(consult): changes for EPDN consult transfer #4948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: task-refactor
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,12 @@ import {METHODS, TRANSCRIPT_EVENT_MAP} from './constants'; | |
| import {CC_EVENTS, WrapupData} from '../config/types'; | ||
| import {ConfigFlags, LoginOption, AIAssistantEventType, AIAssistantEventName} from '../../types'; | ||
| import LoggerProxy from '../../logger-proxy'; | ||
| import {getIsConferenceInProgress, isSecondaryEpDnAgent, shouldAutoAnswerTask} from './TaskUtils'; | ||
| import { | ||
| getIsConferenceInProgress, | ||
| isSecondaryEpDnAgent, | ||
| isSecondaryEpDnConsultedRecipient, | ||
| shouldAutoAnswerTask, | ||
| } from './TaskUtils'; | ||
| import TaskFactory from './TaskFactory'; | ||
| import WebRTC from './voice/WebRTC'; | ||
| import {TaskEvent, type TaskEventPayload} from './state-machine'; | ||
|
|
@@ -426,6 +431,16 @@ export default class TaskManager extends EventEmitter { | |
|
|
||
| const wasConsultedTask = Boolean(task?.data?.isConsulted); | ||
| const computeWrapUpRequired = () => { | ||
| const pending = message.data.agentsPendingWrapUp; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inline note: this transfer-specific wrap-up derivation prioritizes |
||
| if (Array.isArray(pending) && pending.length > 0 && this.agentId) { | ||
| return pending.includes(this.agentId); | ||
| } | ||
| if ( | ||
| this.agentId && | ||
| message.data.interaction?.participants?.[this.agentId]?.isWrapUp === true | ||
| ) { | ||
| return true; | ||
| } | ||
| if (message.data.wrapUpRequired !== undefined) { | ||
| return message.data.wrapUpRequired; | ||
| } | ||
|
|
@@ -478,6 +493,18 @@ export default class TaskManager extends EventEmitter { | |
| * Note: Task-level state transitions and event emissions are handled by | ||
| * the task state machine via sendStateMachineEvent() | ||
| */ | ||
| /** | ||
| * Resolves `isConsulted` when building or updating task data. | ||
| * EP-DN secondary shape alone must not mark the new primary owner as consulted after transfer. | ||
| */ | ||
| private resolveIsConsultedTaskFlag(payload: TaskData): boolean { | ||
| if (payload.isConsulted === true) return true; | ||
| // Explicit false from the server must win (e.g. consult transfer recipient); do not re-infer. | ||
| if (payload.isConsulted === false) return false; | ||
|
|
||
| return isSecondaryEpDnConsultedRecipient(payload.interaction, this.agentId); | ||
| } | ||
|
|
||
| private handleTaskLifecycleEvent(context: EventContext): TaskEventActions { | ||
| const {eventType} = context; | ||
|
|
||
|
|
@@ -502,8 +529,6 @@ export default class TaskManager extends EventEmitter { | |
| */ | ||
| private handleContactReserved(context: EventContext): TaskEventActions { | ||
| const {payload} = context; | ||
| const isConsultedTask = | ||
| payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction); | ||
| const shouldAutoAnswer = shouldAutoAnswerTask( | ||
| payload, | ||
| this.agentId, | ||
|
|
@@ -513,7 +538,7 @@ export default class TaskManager extends EventEmitter { | |
|
|
||
| const taskData: TaskData = { | ||
| ...payload, | ||
| isConsulted: isConsultedTask, | ||
| isConsulted: this.resolveIsConsultedTaskFlag(payload), | ||
| isAutoAnswering: shouldAutoAnswer, | ||
| }; | ||
|
|
||
|
|
@@ -541,8 +566,6 @@ export default class TaskManager extends EventEmitter { | |
| const {payload} = context; | ||
|
|
||
| if (!task) { | ||
| const isConsultedTask = | ||
| payload.isConsulted === true || isSecondaryEpDnAgent(payload.interaction); | ||
| const shouldAutoAnswer = shouldAutoAnswerTask( | ||
| payload, | ||
| this.agentId, | ||
|
|
@@ -551,7 +574,7 @@ export default class TaskManager extends EventEmitter { | |
| ); | ||
| const taskData: TaskData = { | ||
| ...payload, | ||
| isConsulted: isConsultedTask, | ||
| isConsulted: this.resolveIsConsultedTaskFlag(payload), | ||
| wrapUpRequired: payload.interaction?.participants?.[this.agentId]?.isWrapUp || false, | ||
| isConferenceInProgress: getIsConferenceInProgress(payload), | ||
| isAutoAnswering: shouldAutoAnswer, | ||
|
|
@@ -581,14 +604,20 @@ export default class TaskManager extends EventEmitter { | |
| const isConsultingFlow = | ||
| snapshot?.value === 'CONSULTING' || taskData.interaction?.state === 'consulting'; | ||
|
|
||
| const normalizedPayload: TaskData = { | ||
| ...taskData, | ||
| isConsulted: this.resolveIsConsultedTaskFlag(taskData), | ||
| }; | ||
|
Comment on lines
+607
to
+610
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
|
|
||
| const updateTaskData = isConsultingFlow | ||
| ? { | ||
| ...taskData, | ||
| destAgentId: taskData.destAgentId ?? snapshot?.context?.consultDestinationAgentId ?? null, | ||
| ...normalizedPayload, | ||
| destAgentId: | ||
| normalizedPayload.destAgentId ?? snapshot?.context?.consultDestinationAgentId ?? null, | ||
| destinationType: | ||
| taskData.destinationType ?? snapshot?.context?.consultDestinationType ?? null, | ||
| normalizedPayload.destinationType ?? snapshot?.context?.consultDestinationType ?? null, | ||
| } | ||
| : taskData; | ||
| : normalizedPayload; | ||
|
|
||
| task.updateTaskData(updateTaskData); | ||
| this.taskCollection[taskData.interactionId] = task; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,6 +97,20 @@ export const getIsConsultInProgressForConferenceControls = ( | |
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * Determines whether consult is in progress for generic control visibility. | ||
| * Uses explicit task flag first and falls back to consult media detection. | ||
| */ | ||
| export const getIsConsultInProgress = (taskData: TaskData | null | undefined): boolean => { | ||
| if (!taskData) return false; | ||
| if (typeof taskData.isConsultInProgress === 'boolean') return taskData.isConsultInProgress; | ||
|
|
||
| const media = taskData.interaction?.media; | ||
| if (!media) return false; | ||
|
|
||
| return Object.values(media).some((m: any) => m?.mType === 'consult'); | ||
| }; | ||
|
|
||
| export const getIsConsultedAgentForControls = ( | ||
| taskData: TaskData | null, | ||
| context: TaskContext, | ||
|
|
@@ -236,6 +250,26 @@ export const isSecondaryEpDnAgent = (interaction: Interaction): boolean => { | |
| return interaction.mediaType === 'telephony' && isSecondaryAgent(interaction); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inline note: owner-aware consulted detection is critical after consult transfer. Secondary EP-DN shape can still be present, but if |
||
| }; | ||
|
|
||
| /** | ||
| * True when we should infer `isConsulted` from EP-DN secondary interaction shape alone. | ||
| * After a consult transfer, metadata can still match {@link isSecondaryEpDnAgent} while the | ||
| * receiving agent is the new primary owner — in that case `interaction.owner` matches self and | ||
| * we must not mark the task as consulted. | ||
| */ | ||
| export const isSecondaryEpDnConsultedRecipient = ( | ||
| interaction: Interaction | undefined, | ||
| selfAgentId: string | undefined | ||
| ): boolean => { | ||
| if (!interaction || !selfAgentId || !isSecondaryEpDnAgent(interaction)) { | ||
| return false; | ||
| } | ||
| if (interaction.owner && interaction.owner === selfAgentId) { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| }; | ||
|
|
||
| /** | ||
| * Checks if auto-answer is enabled for the agent participant | ||
| * @param interaction - The interaction object | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,12 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { | |
| [TaskEvent.HYDRATE]: { | ||
| actions: ['updateTaskData', 'emitTaskHydrate'], | ||
| }, | ||
| // Wrapped-up can arrive while machine still reflects CONNECTED/CONSULTING due event ordering. | ||
| // Accept it globally so completion cleanup always runs and task is removed from collection. | ||
| [TaskEvent.WRAPUP_COMPLETE]: { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inline note: this root-level |
||
| target: `.${TaskState.COMPLETED}`, | ||
| actions: ['updateTaskData'], | ||
| }, | ||
| }, | ||
| states: { | ||
| [TaskState.IDLE]: { | ||
|
|
@@ -261,7 +267,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { | |
| // AgentConsultTransferred / AgentVTeamTransferred / AgentBlindTransferred | ||
| [TaskEvent.TRANSFER_SUCCESS]: [ | ||
| { | ||
| guard: guards.shouldWrapUpOrIsInitiator, | ||
| guard: guards.shouldWrapUp, | ||
| target: TaskState.WRAPPING_UP, | ||
| actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskWrapup'], | ||
| }, | ||
|
|
@@ -341,7 +347,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { | |
| // AgentConsultTransferred / AgentVTeamTransferred / AgentBlindTransferred | ||
| [TaskEvent.TRANSFER_SUCCESS]: [ | ||
| { | ||
| guard: guards.shouldWrapUpOrIsInitiator, | ||
| guard: guards.shouldWrapUp, | ||
| target: TaskState.WRAPPING_UP, | ||
| actions: ['updateTaskData', 'markEnded', 'emitTaskWrapup'], | ||
| }, | ||
|
|
@@ -496,7 +502,7 @@ export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) { | |
|
|
||
| [TaskEvent.TRANSFER_SUCCESS]: [ | ||
| { | ||
| guard: guards.shouldWrapUpOrIsInitiator, | ||
| guard: guards.shouldWrapUp, | ||
| target: TaskState.WRAPPING_UP, | ||
| actions: ['updateTaskData', 'markEnded', 'clearConsultState', 'emitTaskWrapup'], | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline note: local
WRAPUP_COMPLETEdispatch on wrapup API success is a reliability guard. It prevents stale wrapup dropdown/task cards when websocketAGENT_WRAPPEDUPis delayed or dropped.