@xema/omni-protocol 0.1.98 → 0.1.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/guide.md CHANGED
@@ -23,7 +23,7 @@ are used precisely throughout and mean nothing looser here.
23
23
  | **Provider** | One independently connected external system: a voice platform, a chat platform, a mail platform. |
24
24
  | **Adapter** | The package implementing this contract for one provider. One adapter is one provider, so the words are often interchangeable; *provider* names the system, *adapter* the code speaking for it. |
25
25
  | **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; transfer destinations may include an agent when the provider publishes that directory item. |
26
- | **Lead** | An agent whose login declares `capabilities.team`. The provider publishes a `TeamMembers` object to them and to nobody else: **the login is the permission**. |
26
+ | **Lead** | An agent whose login declares `capabilities.lead`. The flag alone turns on the team feature in the agent application: the lead sees their members with their tasks and stats, and acts on them with the authority the flag carries. While the feature is on, the provider publishes a `TeamMembers` object to them and to nobody else: **the login is the permission**. |
27
27
  | **Local policy** | Rules configured by the agent application about this agent, outside the provider protocol and never sent to a provider. It gates whether an offer may be rejected, whether the agent goes ready on login, and whether tasks are auto-accepted. Where a capability and local policy disagree, the stricter wins. |
28
28
  | **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. The protocol never describes the IVR or the queue; it carries the call's history and details to whichever agent holds it now. |
29
29
  | **Assignment** | The call routed to this agent, from the offer until the task ends. It is the one thing that crosses: the provider assigns, names the assignment with `assignmentId`, and that is the only name the desk ever uses back. A call that comes back is a new assignment on the same call, and a declined or lapsed offer is an assignment that became nothing more. What the platform calls its own record, and whether it reuses that name, is the adapter's business and unknown to the desk. |
@@ -41,7 +41,7 @@ are used precisely throughout and mean nothing looser here.
41
41
  | **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
42
42
  | **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, team member list. |
43
43
  | **Dial** | One outbound call the agent application asks a provider to place — from the idle dialpad, a cold or warm transfer, a conference add, or a connect-back. Identified by the agent application's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
44
- | **Listen** | A lead listening to a member's call unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. Nothing of it reaches the member's task, and there is no take-over in it. See **Listening to a call**. |
44
+ | **Listen** | A lead's channel in a member's call, unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. It is a team act naming the member, never a task on the lead's desk; nothing of it reaches the member's task. A lead who wants the call takes it over. See **Listening to a call**. |
45
45
  | **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in from the moment their dial is placed. |
46
46
 
47
47
  These fields describe state within their containing objects. Both authentication and break
@@ -252,17 +252,10 @@ type AuthenticationContext = {
252
252
 
253
253
  type ListeningMode = "listen" | "coach" | "join-call";
254
254
 
255
- type TeamCapabilities = {
256
- breakControl?: true;
257
- leadAssistControl?: true;
258
- listeningControl?: ListeningMode[];
259
- policyControl?: true;
260
- };
261
-
262
255
  type UserCapabilities = {
263
256
  breaks?: true;
264
257
  preferences?: AgentPreference[];
265
- team?: TeamCapabilities;
258
+ lead?: true;
266
259
  };
267
260
 
268
261
  type AuthenticationState =
@@ -569,6 +562,7 @@ type TaskCapabilities<C extends Channel = Channel> =
569
562
  decline?: Lockable<true>;
570
563
  hold?: Lockable<true>;
571
564
  endCall?: Lockable<true>;
565
+ terminateCall?: Lockable<true>;
572
566
  connectBack?: Lockable<true>;
573
567
  coldTransfer?: Lockable<DestinationDirectory>;
574
568
  warmTransfer?: Lockable<DestinationDirectory>;
@@ -697,17 +691,8 @@ type TaskLeadAssist = { note?: string; since: IsoTimestamp } & (
697
691
  | { stage: "joined"; leadId: UserId }
698
692
  );
699
693
 
700
- type TaskAssisting = {
694
+ type TaskTakenOver = {
701
695
  memberId: UserId;
702
- assignmentId: AssignmentId;
703
- note?: string;
704
- since: IsoTimestamp;
705
- };
706
-
707
- type TaskListening = {
708
- memberId: UserId;
709
- assignmentId: AssignmentId;
710
- mode: ListeningMode;
711
696
  since: IsoTimestamp;
712
697
  };
713
698
 
@@ -747,8 +732,8 @@ type Task<C extends Channel = Channel> = {
747
732
  history?: TaskHistory;
748
733
  } & TaskCompletion & (
749
734
  C extends "voice"
750
- ? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting; listening?: TaskListening; media?: TaskMediaState }
751
- : { recording?: never; onCall?: never; leadAssist?: never; assisting?: never; listening?: never; media?: never }
735
+ ? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist; takenOver?: TaskTakenOver; media?: TaskMediaState }
736
+ : { recording?: never; onCall?: never; leadAssist?: never; takenOver?: never; media?: never }
752
737
  );
753
738
 
754
739
  type PreviewDeadline = "calls" | "host-calls" | "waits";
@@ -764,7 +749,6 @@ type TaskOutcome =
764
749
  | { type: "taken-over"; leadId: UserId }
765
750
  | { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
766
751
  | { type: "expired"; phase: "pending" | "confirmed" | "preview" }
767
- | { type: "left" }
768
752
  | { type: "failed"; failure: ProtocolFailure };
769
753
  ```
770
754
 
@@ -779,6 +763,7 @@ const TASK_COMMAND_NAMES = {
779
763
  "hold",
780
764
  "resume",
781
765
  "end-call",
766
+ "terminate-call",
782
767
  "connect-back",
783
768
  "transfer",
784
769
  "lead-assist",
@@ -802,6 +787,7 @@ type VoiceTaskCommand =
802
787
  | { type: "hold" }
803
788
  | { type: "resume" }
804
789
  | { type: "end-call" }
790
+ | { type: "terminate-call" }
805
791
  | { type: "connect-back"; dialId: DialId }
806
792
  | { type: "transfer"; action: "cold"; dialId: DialId; destinationId: string }
807
793
  | { type: "transfer"; action: "warm"; dialId: DialId; destinationId: string }
@@ -809,8 +795,6 @@ type VoiceTaskCommand =
809
795
  | { type: "transfer"; action: "cancel" }
810
796
  | { type: "lead-assist"; action: "request"; note?: string }
811
797
  | { type: "lead-assist"; action: "cancel" }
812
- | { type: "lead-assist"; action: "take-over-call" }
813
- | { type: "lead-assist"; action: "leave" }
814
798
  | { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
815
799
  | { type: "conference"; action: "remove"; destinationId: string; party?: never }
816
800
  | { type: "conference"; action: "remove"; party: true; destinationId?: never }
@@ -901,12 +885,12 @@ pass. An agent application never has the value and never asks. A name is not loc
901
885
  controls — is content, and is never locked.
902
886
 
903
887
  **A lead sets the team's policy from their team member list.** A login that declares
904
- `capabilities.team.policyControl` may `executeTeamPolicy({ type: "set", capability, setting })`
888
+ `capabilities.lead` may `executeTeam({ command: { type: "set-policy", capability, setting } })`, where the provider offers policy control and says so by publishing `policies`,
905
889
  with `on`, `off`, or `person`, for any task control, `dial`, or a skill — and only `hold`
906
890
  and skills may be `person`; connect back and new call are the team's, on or off, within what the queue
907
891
  allows. The team member list carries `policies` for such a login: every policy as it stands, who set it, and
908
892
  `lockedBy` where a level above the team made it theirs to keep, which the lead sees and cannot
909
- change — `executeTeamPolicy` on it answers `failed` with `omni.capability-not-enabled`.
893
+ change — `set-policy` on it answers `failed` with `omni.capability-not-enabled`.
910
894
 
911
895
  **What the team left to the person is the person's, and the provider keeps it.** The login's
912
896
  `capabilities.preferences` lists every preference the person may hold — `hold`, a skill —
@@ -991,19 +975,44 @@ type TeamMember = {
991
975
  availability: TeamMemberAvailability;
992
976
  since?: IsoTimestamp;
993
977
  break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
978
+ tasks?: MemberTask[];
979
+ listening?: MemberListening;
994
980
  };
995
981
 
996
982
  type LeadRequest = {
997
- id: string;
998
983
  memberId: UserId;
999
984
  assignmentId: AssignmentId;
1000
985
  note?: string;
1001
986
  since: IsoTimestamp;
1002
987
  };
1003
988
 
989
+ type MemberTask<C extends Channel = Channel> = {
990
+ assignmentId: AssignmentId;
991
+ title: string;
992
+ channel: C;
993
+ taskType: string;
994
+ phase: TaskPhase;
995
+ party?: Contact;
996
+ reference?: string;
997
+ attributes?: TaskAttribute[];
998
+ history?: TaskHistory;
999
+ capabilities?: TaskCapabilities<C>;
1000
+ capabilitySource?: CapabilitySource;
1001
+ browsers?: TaskBrowser[];
1002
+ completionMode?: CompletionMode;
1003
+ wrapAllowance?: DurationSeconds;
1004
+ } & (C extends "voice"
1005
+ ? { onCall?: OnCall[]; leadAssist?: TaskLeadAssist; takenOver?: TaskTakenOver; media?: TaskMediaState }
1006
+ : { onCall?: never; leadAssist?: never; takenOver?: never; media?: never });
1007
+
1008
+ type MemberListening = {
1009
+ mode: ListeningMode;
1010
+ since: IsoTimestamp;
1011
+ };
1012
+
1004
1013
  type TeamMembers = {
1005
1014
  members: TeamMember[];
1006
- requests?: LeadRequest[];
1015
+ requests: LeadRequest[];
1007
1016
  policies?: TeamPolicies;
1008
1017
  };
1009
1018
 
@@ -1022,15 +1031,18 @@ type TeamPolicy = Resolved & {
1022
1031
 
1023
1032
  type TeamPolicies = Partial<Record<PolicyKey, TeamPolicy>>;
1024
1033
 
1025
- type TeamPolicyCommand = { type: "set"; capability: PolicyKey; setting: TeamPolicySetting };
1034
+ type TeamPolicyCommand = { type: "set-policy"; capability: PolicyKey; setting: TeamPolicySetting };
1026
1035
 
1027
- type TeamPolicyCommandRequest = {
1028
- command: TeamPolicyCommand;
1029
- };
1036
+ type TeamFeatureCommand = { type: "lead-features"; enabled: boolean };
1030
1037
 
1031
- type TeamLeadAssistCommand =
1032
- | { type: "join"; requestId: string }
1033
- | { type: "decline"; requestId: string; reason?: string };
1038
+ type TeamCallCommand =
1039
+ | { type: "join"; memberId: UserId; assignmentId?: AssignmentId }
1040
+ | { type: "decline"; memberId: UserId; assignmentId?: AssignmentId; reason?: string }
1041
+ | { type: "listen"; memberId: UserId; assignmentId?: AssignmentId }
1042
+ | { type: "coach"; memberId: UserId; assignmentId?: AssignmentId }
1043
+ | { type: "join-call"; memberId: UserId; assignmentId?: AssignmentId }
1044
+ | { type: "leave"; memberId: UserId; assignmentId?: AssignmentId }
1045
+ | { type: "take-over-call"; memberId: UserId; assignmentId?: AssignmentId };
1034
1046
 
1035
1047
  type TeamBreakCommand =
1036
1048
  | { type: "decide-break-request"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
@@ -1038,22 +1050,10 @@ type TeamBreakCommand =
1038
1050
  | { type: "force-break"; memberId: UserId; reasonId?: string; reason?: string; expectedDurationMs?: number }
1039
1051
  | { type: "end-forced-break"; memberId: UserId };
1040
1052
 
1041
- type TeamBreakCommandRequest = {
1042
- command: TeamBreakCommand;
1043
- };
1053
+ type TeamCommand = TeamFeatureCommand | TeamBreakCommand | TeamCallCommand | TeamPolicyCommand;
1044
1054
 
1045
- type TeamLeadAssistCommandRequest = {
1046
- command: TeamLeadAssistCommand;
1047
- };
1048
-
1049
- type TeamListenCommand =
1050
- | { type: "listen"; memberId: UserId }
1051
- | { type: "coach" }
1052
- | { type: "join-call" }
1053
- | { type: "leave" };
1054
-
1055
- type TeamListenCommandRequest = {
1056
- command: TeamListenCommand;
1055
+ type TeamCommandRequest = {
1056
+ command: TeamCommand;
1057
1057
  };
1058
1058
 
1059
1059
  type TeamCommandResult =
@@ -1108,6 +1108,8 @@ type ProviderEvent =
1108
1108
  | { type: "task-updated"; task: Task }
1109
1109
  | { type: "task-media-started"; assignmentId: AssignmentId }
1110
1110
  | { type: "task-media-ended"; assignmentId: AssignmentId }
1111
+ | { type: "team-media-started"; memberId: UserId; assignmentId: AssignmentId }
1112
+ | { type: "team-media-ended"; memberId: UserId; assignmentId: AssignmentId }
1111
1113
  | { type: "task-ended"; assignmentId: AssignmentId; outcome: TaskOutcome }
1112
1114
  | { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; assignmentId?: AssignmentId; destinationId?: string; reason?: string }
1113
1115
  | { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
@@ -1169,13 +1171,10 @@ type Connection<C extends Channel = Channel> = {
1169
1171
  cancelBreak?(): Promise<BreakCancelResult>;
1170
1172
  endBreak?(): Promise<BreakEndResult>;
1171
1173
 
1172
- executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
1173
- executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
1174
- executeTeamListen?(request: TeamListenCommandRequest): Promise<TeamCommandResult>;
1174
+ executeTeam?(request: TeamCommandRequest): Promise<TeamCommandResult>;
1175
1175
  openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
1176
1176
  setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
1177
1177
  recordStep?(report: HistoryReport): Promise<HistoryReportResult>;
1178
- executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
1179
1178
  };
1180
1179
 
1181
1180
  type Adapter<C extends Channel = Channel> = {
@@ -1611,7 +1610,7 @@ compile time.
1611
1610
  | `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; agent application polling is independently opt-in. |
1612
1611
  | `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
1613
1612
  | `runningStepReports` | The provider takes running reports of an agent application-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the agent application sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The agent application records what it performs**. |
1614
- | `completionSettleMs` | Required. How long after an applied completion -- `complete`, or a lead's `take-over-call` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1613
+ | `completionSettleMs` | Required. How long after an applied `complete` the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1615
1614
 
1616
1615
  ### Authentication methods
1617
1616
 
@@ -1912,20 +1911,15 @@ replaces the login" describes is a new `AuthenticationSession` under a new `logi
1912
1911
  ### What the login may do
1913
1912
 
1914
1913
  `capabilities` declares provider actions available to this login rather than to one task: whether
1915
- the agent may ask for a break, and whether they lead a team — and if so, whether they decide its
1916
- breaks and whether they may join a member's call. It is declared by presence, like every capability
1917
- in this contract, and it travels with the identity because it is part of who the agent is on this
1918
- provider: the provider knows the roles, and says so at sign-in rather than leaving Omni to infer
1919
- them from what arrives later.
1914
+ the agent may ask for a break, and whether they lead a team. It is declared by presence, like every
1915
+ capability in this contract, and it travels with the identity because it is part of who the agent
1916
+ is on this provider: the provider knows the roles, and says so at sign-in rather than leaving Omni
1917
+ to infer them from what arrives later.
1920
1918
 
1921
1919
  | Field | Contract |
1922
1920
  | --- | --- |
1923
1921
  | `breaks` | This login may request a break. Requires the four break methods on the connection. |
1924
- | `team` | This login leads a team. The provider publishes a `TeamMembers` object to it on every snapshot — `members: []` when nobody is in it — and to nobody else. |
1925
- | `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — force-break, end-forced-break, decide-break-request, set-break-policy — as far as the provider supports; a command it lacks answers `omni.capability-not-enabled`. Omni asks for a decision only against a member whose `break` is `awaiting-approval`, so a provider that grants on request is never asked to decide. Requires `executeTeamBreak`. |
1926
- | `team.leadAssistControl` | This lead may join a member's call on request. Requires `executeTeamLeadAssist`. |
1927
- | `team.listeningControl` | This lead may listen to a member's call unasked, in the listed modes and no others: `listen`, `coach`, `join-call`. The list always includes `listen`, since the other two begin from one. Requires `executeTeamListen`. See **Listening to a call**. |
1928
- | `team.policyControl` | This lead sets the team's policy per capability — on, off, or the person's — within what the queue allows. Requires `executeTeamPolicy`; the team member list carries `policies`. |
1922
+ | `lead` | This login leads a team. The flag alone turns on the team feature in the agent application, and every lead act comes with it; there is no per-action permission beside it. While the lead has the feature on, the provider publishes a `TeamMembers` object to them on every snapshot — `members: []` when nobody is in it — and to nobody else. Requires `executeTeam`. See **Team leads**. |
1929
1923
  | `preferences` | What the team left to this person, with where each stands and who set it. Omitted when nothing was. Requires `setPreference`. See **Who decides what an agent may do**. |
1930
1924
 
1931
1925
  A session action is available only when both the capability and Omni local policy permit it.
@@ -2134,7 +2128,7 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
2134
2128
  | `taskCount` | The provider's own count of those tasks, stated rather than inferred, and it must equal `tasks.length`. A snapshot with no work says `taskCount: 0` in so many words — a blank or unanswered state lacks the count and cannot pass as a confirmed empty. |
2135
2129
  | `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
2136
2130
  | `scheduledActivities` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
2137
- | `team` | Required `TeamMembers` when the login declares `capabilities.team`, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission. |
2131
+ | `team` | Required `TeamMembers` when the login declares `capabilities.lead` and the lead has the team feature on, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission, and a lead who turned the feature off gets nothing of the team. |
2138
2132
 
2139
2133
  ## Live connection
2140
2134
 
@@ -2157,12 +2151,9 @@ surface in one place, and what obliges an adapter to implement each one.
2157
2151
  | `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
2158
2152
  | `cancelBreak()` | The login declares `capabilities.breaks`. |
2159
2153
  | `endBreak()` | The login declares `capabilities.breaks`. |
2160
- | `executeTeamBreak(command)` | The login declares `capabilities.team.breakControl`. |
2161
- | `executeTeamLeadAssist(command)` | The login declares `capabilities.team.leadAssistControl`. |
2162
- | `executeTeamListen(command)` | The login declares `capabilities.team.listeningControl`. |
2154
+ | `executeTeam(request)` | The login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. See **Lead commands**. |
2163
2155
  | `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
2164
2156
  | `recordStep(report)` | The manifest lists `softphone` among its `phones`. On a softphone the agent application mutes its own microphone on any call, and the provider's record has to have somewhere to take that leg; a desk phone's microphone is the phone's. See **The agent application records what it performs**. |
2165
- | `executeTeamPolicy(command)` | The login declares `capabilities.team.policyControl`. |
2166
2157
  | `openMedia(request)` | The manifest lists `softphone` among its `phones`. On a softphone the call's audio lands in Omni, so the adapter has to open it; a platform of desk phones alone never does. |
2167
2158
 
2168
2159
  **The four break methods stand or fall together.** Declaring `capabilities.breaks` at login and then
@@ -2295,7 +2286,7 @@ An automatically accepted task still arrives through `task-offered`.
2295
2286
 
2296
2287
  **Work the agent originated is accepted by the command that created it.** A task born of the
2297
2288
  agent's own act — a dialpad call or a connect-back, recognisable by the agent application's `dialId` on
2298
- `onCall`; a lead's join, carrying `assisting`; a lead's listen, carrying `listening` — arrives
2289
+ `onCall`; a call a lead took over, carrying `takenOver` — arrives
2299
2290
  through `task-offered` with `acceptance: "automatic"` whatever `autoAcceptTasks` says, and the
2300
2291
  validator holds it there under either local policy and under none (`task.acceptance.originated`):
2301
2292
  the desk shows no **Accept** for a call the agent placed. The local policy governs work the queue
@@ -2391,8 +2382,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2391
2382
  | `history` | The call record: `steps` — the ordered interaction history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `interactionSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **Interaction history**. |
2392
2383
  | `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer -- carrying a `stage` while being dialled again on the same task, a connect-back with the agent application's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where an agent application placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the agent application supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
2393
2384
  | `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
2394
- | `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, the member's `assignmentId`, and their note. The lead's task has an assignment of its own, so the member's is named, or the copy no longer says which call it joined. Its presence is what makes `lead-assist` `take-over-call` and `leave` issuable. See **Lead assist**. |
2395
- | `listening` | Voice only, on the lead's own task while they listen to a member's call: whose call, which call, and the `mode` they are heard in, restated on every change. Never on the member's task, and never together with `assisting`. See **Listening to a call**. |
2385
+ | `takenOver` | Voice only, on a task that reached this agent by a lead's take-over: which member the call was taken from, and when. It is an ordinary assignment with the call's history, offered whatever break the lead is on and counted against no capacity. See **Lead assist**. |
2396
2386
 
2397
2387
  `TaskAttribute` entries carry typed detail alongside the task:
2398
2388
 
@@ -3197,7 +3187,8 @@ See **Which commands need a capability**.
3197
3187
  | --- | --- | --- |
3198
3188
  | `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
3199
3189
  | `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
3200
- | `endCall` | Primary button: End call | The provider ends the caller connection and all owned/inherited agent-added channels; wrap/completion remain separate. See **Ending a call, and removing one person from it**. |
3190
+ | `endCall` | Primary button: End call | The provider ends the agent's channel and every channel the agent added; the caller continues on the provider's path. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
3191
+ | `terminateCall` | Primary button: Terminate call | The provider ends the caller channel. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
3201
3192
  | `connectBack` | Completing-task button: Connect back | Omni may have the provider connect the agent back to the task's party while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance completes the task at provider end. See **Connecting back during completion**. |
3202
3193
  | `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
3203
3194
  | `warmTransfer` | Secondary menu item: Warm transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Warm transfer**. |
@@ -3292,50 +3283,41 @@ end the caller journey; a later assignment to the same agent is a new interactio
3292
3283
 
3293
3284
  #### Ending a call, and removing one person from it
3294
3285
 
3295
- A call has everyone on `Task.onCall`, and two commands take people off it, both performed by the
3296
- provider.
3286
+ A call has everyone on `Task.onCall`, and three commands take people off it, all performed by the
3287
+ provider. Two of them end the agent's part; they differ in what happens to the caller.
3297
3288
 
3298
3289
  ```ts
3299
- { type: "end-call" } // provider ends caller and owned/inherited agent-added channels; wrap/completion are separate
3290
+ { type: "end-call" } // the agent's channel and the channels the agent added end; the caller continues
3291
+ { type: "terminate-call" } // the caller channel ends
3300
3292
  { type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
3301
3293
  { type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
3302
3294
  ```
3303
3295
 
3304
- The agent application sends the ordinary task-scoped `execute` request with `command: { type: "end-call" }`.
3305
- The provider knows the caller and agent-added channel bindings; no channel list or owner details
3306
- are sent by the agent application. The task's `endCall` capability is permission, not an executor selection.
3307
- `validateTaskCommandRequest` checks the exact interaction/assignment and delegates the command's
3308
- policy/phase checks. The provider rechecks current ownership atomically; no client validator can
3309
- prove that its view is fresh. The agent application does not implement agent end-call; low-level removal of individual channels is not
3310
- this agent action. There is no agent application end-call execution API or fallback route.
3311
-
3312
- `end-call` is gated by `endCall` on the current interaction. It ends the caller connection and
3313
- all agent-added channels owned by that interaction, including channels originally added by previous
3314
- agents and inherited through transfer or takeover. Ownership is not inferred from who originally
3315
- dialed, the current UI selection, or a person's label. The provider maintains that authority
3316
- and checks the exact interaction/assignment when acting. Agent-added channels are not left behind
3317
- merely because another agent added them.
3318
-
3319
- On a successful transfer or takeover, ownership of those channels passes to the receiving
3320
- interaction (lead or another agent), not just ownership of the caller leg. The outgoing interaction may
3321
- still have its allowed wrap work, but cannot disconnect channels it no longer owns. A pending or
3322
- unknown handover is not a confirmed ownership change: retain source reservations and reconcile.
3323
- The provider must serialize handover and end-call so a delayed outgoing command cannot tear down
3324
- the recipient's call. No public channel-owner field or guessed identity is introduced here;
3325
- the source must provide authoritative ownership and command fencing before declaring permission.
3326
-
3327
- The resulting end of this agent's media is published as `task-media-ended`. Any wrap remains
3328
- until its own completion; `end-call` is not `task-ended`, and `complete` is not an implicit
3329
- caller/channel disconnect. Other agents' independent tasks end or wrap only on their own source
3330
- transitions. Neither completion nor transfer itself ends the continuing caller journey.
3331
-
3332
- `conference` `remove` remains a person-specific provider operation, gated by `conference`.
3333
- It does not implicitly complete any other interaction. Its person/consultation prerequisites
3334
- remain separate from the permission to disconnect the caller. A remove that would leave the
3335
- agent alone is not a remove but an `end-call`, and the provider answers it `failed`.
3336
- The consulted destination of a warm transfer is not removed this way:
3337
- `transfer` `cancel` is its own step, which returns the agent to the parked customer, and a remove
3338
- never promises that.
3296
+ **`end-call` ends my part.** The agent's channel and every channel the agent added -- a
3297
+ conferenced colleague, a consulted destination -- end, and the caller continues on the path the
3298
+ provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
3299
+
3300
+ **`terminate-call` ends the caller channel.** The call is over for the caller. Gated by
3301
+ `terminateCall`, a separate permission, since ending a customer's call is a different thing from
3302
+ putting my own side down.
3303
+
3304
+ Both are commands to the provider through `execute`, and nothing happens on the desk until the
3305
+ provider reports it: the agent's media ends on `task-media-ended`, the task moves to `completing`,
3306
+ any wrap allowance runs, and the agent completes. Neither is `task-ended`, and `complete` is
3307
+ neither of them. Both capabilities are the queue's to grant and a level's to lock, and both may
3308
+ change while the task is open: the provider republishes the task at the moment a permission
3309
+ changes, and a command that arrives after its capability was withdrawn is refused, as **Task
3310
+ capabilities** sets out. The provider knows which channels are the agent's and which were added;
3311
+ the agent application sends the command and no channel list, and the provider rechecks ownership
3312
+ when it acts.
3313
+
3314
+ `conference` `remove` takes one person off, named as the room names them, and the call goes on
3315
+ for the rest; it is gated by `conference`, since it only means something with a third person on
3316
+ the line. Removing the party leaves the agent with the colleague, a warm hand-over in reverse. A
3317
+ remove that would leave the agent alone is not a remove but an `end-call`, and a provider answers
3318
+ it `failed`. The consulted destination of a warm transfer is not removed this way: `transfer`
3319
+ `cancel` is its own step, which returns the agent to the parked customer, and a remove never
3320
+ promises that.
3339
3321
 
3340
3322
  #### Warm transfer
3341
3323
 
@@ -3632,7 +3614,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
3632
3614
  | `awaiting-approval` | A person has to decide. The agent is waiting on somebody. |
3633
3615
  | `granted` | A person decided yes. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
3634
3616
  | `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. It waits on a task, so beside no task it is refused (`break.starting-after-task.tasks`): a committed break with nothing outstanding is `on-break`. |
3635
- | `on-break` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `on-break` beside a task is refused as `break.on-break.tasks`. The one exception is a lead's listening task during a `coaching`, `administrative` or `training` break -- see **Listening to a call**. |
3617
+ | `on-break` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `on-break` beside a task is refused as `break.on-break.tasks`. The one exception is a call a lead took over, which the provider assigns whatever break the lead is on -- see **Lead assist**. |
3636
3618
 
3637
3619
  A denial is a decision, not a standing approval state. The provider transitions the request directly
3638
3620
  to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
@@ -3843,12 +3825,15 @@ request object only to the request method, and undefined to the other three.
3843
3825
  | `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
3844
3826
  | `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
3845
3827
 
3846
- Use `validateTeamBreakCommand(request, context)` for lead decisions, forcing a break, ending a forced break and
3847
- policy commands. It requires the live lead capability and active transport, a current target
3848
- team member list for member commands, and the target's complete break state for forcing/ending a break.
3849
- Approve/deny requires an awaiting decision; forcing a break uses the target's reason codes; ending a forced break
3850
- requires a forced committed break. The context's target state must belong to the named member;
3851
- that association and backend authorization are provider responsibilities.
3828
+ Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
3829
+ decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
3830
+ the live `lead` flag and active transport, a current team member list for any command naming a
3831
+ member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
3832
+ awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
3833
+ forced committed break; join and decline need the member's request on the list; coach, join-call
3834
+ and leave need the lead already on that member's call; an assignment named must be one the list
3835
+ shows the member holding. The context's target state must belong to the named member; that
3836
+ association and backend authorization are provider responsibilities.
3852
3837
 
3853
3838
  Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
3854
3839
  as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
@@ -4116,22 +4101,23 @@ so there is nothing on it to resume.
4116
4101
 
4117
4102
  ## Team leads
4118
4103
 
4119
- A lead who also takes calls sees their team on the idle dashboard. `Snapshot.team` carries a
4120
- `TeamMembers` object, replaced whole by `team-updated`.
4121
-
4122
- Migration: TeamRoster is now `TeamMembers`, and validateTeamRoster is now
4123
- `validateTeamMembers`. Update type imports and validator calls; no legacy export aliases exist.
4124
- The object still contains `members` and the permitted `requests` and `policies`; it is not a
4125
- bare array. `Snapshot.team`, the `team-updated` event and its `team` payload, login permissions,
4126
- and `team.*` validation rules keep their names and semantics. An empty team has `members: []`;
4127
- an absent `team` still means the login is not entitled to that contribution.
4104
+ The team feature is the lead's own surface, beside the agent's and never mixed with it. The
4105
+ login's `lead` flag turns it on in the agent application; the lead retrieves their members and
4106
+ sees, for each, their assignments as they stand and the full history of each, and acts on them
4107
+ with the authority the flag carries. Nothing the lead does here is a task on their desk. A lead
4108
+ who also takes calls is an agent like any other on the agent side, and a lead who does not want
4109
+ the agent role for now places themself on a break of a working kind and keeps the team feature on.
4128
4110
 
4111
+ `Snapshot.team` carries a `TeamMembers` object, replaced whole by `team-updated`, and the provider
4112
+ keeps it current: when a member is offered a call, answers, holds, mutes or is transferred, the
4113
+ list is republished, so the lead's screen moves when the member's does. The agent application asks
4114
+ for nothing and computes nothing.
4129
4115
 
4130
4116
  | Field | Contract |
4131
4117
  | --- | --- |
4132
- | `members` | Every member of this lead's team, whatever their state. `[]` says the lead has a team with nobody in it; omitting the team member list says something else entirely — see **The login is the permission** below. |
4133
- | `requests` | The members currently asking this lead to join a call, each with the task and the note. Required when the login declares `team.leadAssistControl`, `[]` when nobody is asking; omitted when it does not. See **Lead assist**. |
4134
- | `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a level above the team made it theirs to keep. Required when the login declares `team.policyControl`; omitted when it does not. See **Who decides what an agent may do**. |
4118
+ | `members` | Every member of this lead's team, whatever their state, each with their open tasks. `[]` says the lead has a team with nobody in it; omitting the team member list says something else entirely — see **The login is the permission** below. |
4119
+ | `requests` | The members currently asking a lead to join their call, each naming the member, the assignment and the note. Always carried, `[]` when nobody is asking. See **Lead assist**. |
4120
+ | `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a level above the team made it theirs to keep. Present where the provider offers policy control; absent where it does not. See **Who decides what an agent may do**. |
4135
4121
 
4136
4122
  | `TeamMember` field | Contract |
4137
4123
  | --- | --- |
@@ -4139,6 +4125,8 @@ an absent `team` still means the login is not entitled to that contribution.
4139
4125
  | `availability` | Required. What the member is doing now. |
4140
4126
  | `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
4141
4127
  | `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
4128
+ | `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, trimmed by the provider. The assignment, title, task type, phase, party, room, media and the full history are what the lead reads; the workspace — controls, their source, browsers, completion terms — is the member's and travels only if the provider sends it. `[]` when the member holds none; omitted only where the provider cannot see them. |
4129
+ | `listening` | Present while this lead's channel is in this member's call — listening, coaching or joined — with the `mode` they are heard in and since when. One member's call at a time (`team.listening.single`). |
4142
4130
 
4143
4131
  Each availability value means one thing:
4144
4132
 
@@ -4153,7 +4141,8 @@ Each availability value means one thing:
4153
4141
  **Always publish the complete team member list, never a change to it.** Team presence typically reaches an
4154
4142
  adapter over a best-effort channel with no ordering and no delivery guarantee, so a stream of deltas
4155
4143
  cannot be trusted to reconstruct the truth. The adapter reconciles against its own authoritative
4156
- read and publishes the result.
4144
+ read and publishes the result. That includes the members' tasks: a hold on a member's desk is a
4145
+ republished list on the lead's.
4157
4146
 
4158
4147
  **Omit `since` rather than inventing one.** Omni renders it as a duration, so a timestamp
4159
4148
  synthesised from the adapter's own clock at seed time reads as "on task for 0 seconds" for
@@ -4161,11 +4150,20 @@ everybody — worse than showing nothing, because it looks like data. Send it on
4161
4150
  knows when the state actually began. It times the current `availability`, so it moves every time
4162
4151
  that value does.
4163
4152
 
4164
- **The login is the permission.** A team member list goes to a login that declares `capabilities.team`, on
4165
- every snapshot, and to nobody else. Omni never decides who leads a team: the provider said so at
4166
- sign-in, and the team member list agrees with it — present, `[]` included, for a lead; absent for everybody
4167
- else, which is the correct rendering for an agent who leads nobody. What the lead may do with the
4168
- team member list is on the login too, `team.breakControl` and `team.leadAssistControl`, never on the team member list.
4153
+ **The login is the permission.** A team member list goes to a login that declares
4154
+ `capabilities.lead`, on every snapshot while the team feature is on, and to nobody else. Omni never
4155
+ decides who leads a team: the provider said so at sign-in, and the team member list agrees with it
4156
+ — present, `members: []` included, for a lead; absent for everybody else, which is the correct
4157
+ rendering for an agent who leads nobody (`team.required`, `team.unentitled`). What the lead may do
4158
+ with it comes with the flag: there is no per-action permission on the login or on the list.
4159
+
4160
+ **The lead switches the feature on and off, and the provider is told.** A lead may work as an
4161
+ agent alone, as agent and lead, or as lead alone, and moves between them on the fly. The
4162
+ agent application says which with `{ type: "lead-features", enabled }`: while it is on, the
4163
+ provider sends team events scoped to the lead's team and the lead may act; while it is off, the
4164
+ lead is an ordinary agent, no team member list is owed or expected (`team.unexpected`,
4165
+ `event.team.features`), and no lead act is possible, since the lead learns their members'
4166
+ assignments only from the list. It is on from sign-in for a lead until the lead turns it off.
4169
4167
 
4170
4168
  **The team member list never carries the agent it is published to — not in `members`, and not in
4171
4169
  `requests`.** A lead does not report to themself: their own break request and their own ask for a
@@ -4173,24 +4171,36 @@ lead go up to whoever leads them and appear on *that* person's team member list,
4173
4171
  only their own `BreakState` and their task's `leadAssist` move. An adapter whose platform lists the lead
4174
4172
  among their own members filters the signed-in identity out before publishing. **Being a lead is a
4175
4173
  role the provider knows, never inferred from who is listed:** it is declared at sign-in, a lead
4176
- with nobody in their team publishes `[]`, an agent with no such role publishes nothing, and no
4174
+ with nobody in their team publishes `members: []`, an agent with no such role publishes nothing, and no
4177
4175
  member count can tell those two apart.
4178
4176
 
4179
4177
  ### Lead commands
4180
4178
 
4181
- One method, `executeTeamBreak`, taking a discriminated command exactly as `execute` takes a
4182
- `TaskCommand`:
4179
+ One method, `executeTeam`, taking a discriminated command exactly as `execute` takes a
4180
+ `TaskCommand`. Every act on a member's call names the member, `memberId`, and may name the
4181
+ assignment, `assignmentId`, where the lead has it; the provider resolves the member's current
4182
+ assignment from the member alone.
4183
4183
 
4184
4184
  | Command | Effect |
4185
4185
  | --- | --- |
4186
- | `{ type: "decide-break-request", memberId: UserId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
4186
+ | `{ type: "lead-features", enabled }` | The team feature on or off. See **The lead switches the feature on and off** above. |
4187
+ | `{ type: "decide-break-request", memberId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
4187
4188
  | `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
4188
- | `{ type: "force-break", memberId: UserId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory and is published on the resulting forced break. |
4189
- | `{ type: "end-forced-break", memberId: UserId }` | Lifts the forced-break restriction, whoever forced it, by clearing `BreakState.forced`. The committed break continues; only the agent resumes work. |
4189
+ | `{ type: "force-break", memberId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory and is published on the resulting forced break. |
4190
+ | `{ type: "end-forced-break", memberId }` | Lifts the forced-break restriction, whoever forced it, by clearing `BreakState.forced`. The committed break continues; only the agent resumes work. |
4191
+ | `{ type: "join", memberId, assignmentId? }` | Answers the member's request: the provider bridges the lead's channel into the call. See **Lead assist**. |
4192
+ | `{ type: "decline", memberId, assignmentId?, reason? }` | Refuses the member's request. |
4193
+ | `{ type: "listen", memberId, assignmentId? }` | The lead's channel joins the member's call unasked, in silence. See **Listening to a call**. |
4194
+ | `{ type: "coach", memberId, assignmentId? }` | The lead is heard by the member alone. Needs the lead on that member's call. |
4195
+ | `{ type: "join-call", memberId, assignmentId? }` | The lead is heard by everyone on the call. Needs the lead on that member's call. |
4196
+ | `{ type: "leave", memberId, assignmentId? }` | The lead's channel leaves the member's call, however it got there. The member's call goes on. |
4197
+ | `{ type: "take-over-call", memberId, assignmentId? }` | The provider moves the call to the lead as its new agent, from a joined call or a listened one alike. See **Lead assist**. |
4198
+ | `{ type: "set-policy", capability, setting }` | Sets the team's policy for one capability: `on`, `off`, or `person`. See **Who decides what an agent may do**. |
4190
4199
 
4191
4200
  `memberId` is this provider's own identifier for the member, as published on its team member list. It is
4192
4201
  never an identifier from another provider, and Omni does not translate between them; names come
4193
- from `getUserDetails()`.
4202
+ from `getUserDetails()`. `validateTeamCommand` holds each command to the team member list as it
4203
+ stands -- see **Runtime break prerequisite checks**.
4194
4204
 
4195
4205
  `requests-suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
4196
4206
  approve them. A provider that suspends break requests must also publish `canRequestBreak: false` to the team's
@@ -4204,71 +4214,56 @@ never expressed here.
4204
4214
 
4205
4215
  An agent on a call may ask a lead to join it -- a dispute that needs approval, a customer who
4206
4216
  asks for a manager, a moment the agent wants a second pair of ears. A call centre calls this
4207
- assistance or escalation, and the name says who assists: the capability is `leadAssist` on the task; the lead's side is the team member list, which is already the lead's view of the
4208
- team, and a second lead method beside `executeTeamBreak`:
4209
-
4210
- ```ts
4211
- executeTeamLeadAssist({ command: TeamLeadAssistCommand }): Promise<TeamCommandResult>
4212
- ```
4213
-
4214
- Required when the login declares `capabilities.team.leadAssistControl`, and gated by it exactly as
4215
- `executeTeamBreak` is by `team.breakControl`. The flow, in order:
4217
+ assistance or escalation, and the name says who assists: the capability is `leadAssist` on the
4218
+ task; the lead's side is the team member list, which is already the lead's view of the team. The
4219
+ flow, in order:
4216
4220
 
4217
4221
  ```ts
4218
4222
  // 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
4219
4223
  execute({ assignmentId: "alloc-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
4220
4224
  // task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
4221
4225
 
4222
- // 2. Every lead entitled to it sees the request on their team member list.
4223
- // team-updated: requests: [{ id: "req-7", memberId: "A-1", assignmentId: "alloc-42", note, since }]
4226
+ // 2. Every lead with the team feature on sees the request on their team member list.
4227
+ // team-updated: requests: [{ memberId: "A-1", assignmentId: "alloc-42", note, since }]
4224
4228
 
4225
4229
  // 3. A lead joins, or declines.
4226
- executeTeamLeadAssist({ command: { type: "join", requestId: "req-7" } })
4227
- executeTeamLeadAssist({ command: { type: "decline", requestId: "req-7", reason: "In a call" } })
4230
+ executeTeam({ command: { type: "join", memberId: "A-1", assignmentId: "alloc-42" } })
4231
+ executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" } })
4228
4232
  ```
4229
4233
 
4230
- **On `join` the provider bridges three parties and the lead is on a task of their own**, an
4231
- assignment of its own, arriving on the lead's connection as `task-offered` with `automatic`
4232
- -- the way a call an agent placed themselves arrives -- and carrying `assisting`, which names the
4233
- member and the member's assignment. The agent's task
4234
- moves to `leadAssist: { stage: "joined", leadId }`. A join is the lead's own act, so capacity does not
4235
- trigger it; but from then on it is an outstanding task the provider counts against the lead's
4236
- stated ceiling like any other, nothing more is assigned to the lead while it stands, and a
4237
- provider whose lead is already at the ceiling answers the join `failed`.
4238
-
4239
- **A lead assists one call at a time.** A `join` from a lead already on a call -- their own or one
4240
- they joined -- is answered `failed`, whatever their ceiling; the request stands for another lead,
4241
- or until it is withdrawn or declined.
4242
-
4243
- **A lead on a break does not join.** A break is a reported state in which the agent is not working,
4244
- and a join is work. Omni offers Join to a lead only while their own `BreakState.status` is neither
4245
- `starting-after-task` nor `on-break` -- a committed break waiting for the lead's current work to
4246
- finish is not given more -- and a provider answers a `join` from a lead on such a break `failed`.
4247
- The request stands for another lead, as it does when this one is already on a call.
4234
+ **On `join` the provider bridges the lead's channel into the call.** The member's task moves to
4235
+ `leadAssist: { stage: "joined", leadId }`, the member on the team member list carries
4236
+ `listening: { mode: "join-call", since }`, and the lead's audio arrives as **Listening to a call**
4237
+ describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
4238
+ work assigned to them.
4239
+
4240
+ **A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
4241
+ or a member's -- is answered `failed`; the request stands for another lead, or until it is
4242
+ withdrawn or declined. A join is work, so Omni offers Join only to a lead whose voice channel is
4243
+ free, and a provider answers a `join` from one whose channel is not `failed`.
4248
4244
 
4249
4245
  **On `decline`, or a request the agent withdraws with `{ type: "lead-assist", action: "cancel" }`, the
4250
4246
  provider clears `leadAssist` from the agent's task** and drops the request from every team member list. Nothing
4251
4247
  else changes; the agent is still on the call.
4252
4248
 
4253
- **Take over call** uses `{ type: "lead-assist", action: "take-over-call" }`.
4254
- The lead assumes the agent’s interaction; the caller’s wider journey continues.
4255
- Migration: replace the former take-over action literal with `take-over-call` in hosts and
4256
- providers together. The former spelling is rejected, with no compatibility alias.
4257
- The `taken-over` task outcome and existing completion behavior remain unchanged.
4258
- This action is distinct from `join-call`, which lets everyone hear a lead listening to a call.
4249
+ **On `leave`, the lead's channel drops and the call goes on**: the member's task loses its
4250
+ `leadAssist`, the member loses `listening`, and the lead is free. Nothing ended for the member.
4259
4251
 
4260
- The lead then has two commands on their copy, gated by `assisting` being present, and a third
4261
- choice that is no command at all:
4252
+ **On `take-over-call`, the provider moves the call from the member to the lead**, and the lead is
4253
+ its new agent. There are two paths to it -- the agent asked and the lead joined, or the lead was
4254
+ listening unasked -- and one act at the end of either. What follows is the same on both:
4262
4255
 
4263
- | The lead | The agent's task | The lead's task |
4256
+ | | The member's task | The lead's desk |
4264
4257
  | --- | --- | --- |
4265
- | `{ type: "lead-assist", action: "take-over-call" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
4266
- | `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-media-ended`, then `task-ended` with `{ type: "left" }` -- the call goes on without them |
4267
- | Stays until the customer hangs up | `task-media-ended`, `completing`, its own outcome | The same, independently: **both have the completion window** |
4258
+ | Take-over | `task-media-ended`, then `completing`: the member's interaction is over and their wrap runs, exactly as after `end-call`. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`). | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`. |
4259
+ | The member completes | `task-ended` with `{ type: "taken-over", leadId }`, at the member's own completion. The lead is named by user id, since a lead is not a directory item. | The lead works the call as any agent would, and ends it as any call ends. |
4268
4260
 
4269
- `left` is the one outcome that ends a task without ending the call: this agent left a call that
4270
- continues without them. It reads as neither a completion nor a cancellation, because it is
4271
- neither.
4261
+ **The taken-over call is offered whatever break the lead is on.** A lead working as lead alone is
4262
+ on a break of a working kind, and the provider assigns the call regardless: it is the one task a
4263
+ break in effect may hold (`break.on-break.tasks` allows it, and nothing else). It is the lead's own
4264
+ act, so it counts against no capacity, like the agent's own dial; the only conditions are that the
4265
+ lead is not already on another call and their voice channel is free. A lead with the team feature
4266
+ off cannot take over, since they have no member's assignment to name.
4272
4267
 
4273
4268
  ```ts
4274
4269
  const leadAssistCapable = {
@@ -4277,6 +4272,13 @@ const leadAssistCapable = {
4277
4272
  phase: "in-progress",
4278
4273
  leadAssist: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
4279
4274
  } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "leadAssist">;
4275
+
4276
+ const takenOver = {
4277
+ channel: "voice",
4278
+ capabilities: { hold: true, endCall: true, outcomes: true },
4279
+ phase: "in-progress",
4280
+ takenOver: { memberId: "A-1", since: "2026-08-21T09:06:00Z" },
4281
+ } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "takenOver">;
4280
4282
  ```
4281
4283
 
4282
4284
  Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
@@ -4284,44 +4286,10 @@ Lead and member alike are `UserId`s of this provider, so an adapter publishing t
4284
4286
 
4285
4287
  ### Listening to a call
4286
4288
 
4287
- **Listen** lets the lead hear the call without being heard. The modes are `listen`, `coach`
4288
- (agent-only lead audio), and `join-call` (lead audio heard by everyone).
4289
-
4290
- | Former API | Current API |
4291
- | --- | --- |
4292
- | MonitorMode / MONITOR_MODES | `ListeningMode` / `LISTENING_MODES` |
4293
- | TeamMonitorCommand / TeamMonitorCommandRequest | `TeamListenCommand` / `TeamListenCommandRequest` |
4294
- | executeTeamMonitor | `executeTeamListen` |
4295
- | team.monitorControl | `team.listeningControl` |
4296
- | TaskMonitoring / task.monitoring | `TaskListening` / the task’s `listening` field |
4297
- | MONITORING_BREAK_KINDS / breakKindAllowsMonitoring | `LISTENING_BREAK_KINDS` / `breakKindAllowsListening` |
4298
- | monitor action and mode | `listen` action and mode |
4299
-
4300
- Update command dispatch, allowed modes, task snapshots, imports and diagnostics together.
4301
- The former names are not aliases. Old task and permission fields are rejected even alongside
4302
- new fields. Validation rule segments now use `listening`, `listeningControl`, and `listen`.
4303
- The existing login permissions, single-call limits, allowed break kinds and audio effects
4304
- remain unchanged. Listening includes its coach/join-call modes and does not imply takeover.
4305
-
4306
-
4307
- **Coach** lets the lead speak privately to the agent; the caller cannot hear the lead.
4308
- The command and listening mode are `coach`. Migration: replace the former whisper literal
4309
- in `TeamListenCommand`, `ListeningMode`, `team.listeningControl`, and the task’s `listening.mode`.
4310
- `LISTENING_MODES` exports `coach`; update command producers, permissions and retained snapshots
4311
- together. No legacy alias is accepted. The lead must already be listening and permitted to
4312
- coach. This mode does not join the audible caller conversation or take over the task.
4313
-
4314
- The user-facing action is **Join call**. The command and listening mode are `join-call`.
4315
- Migration: replace the former barge literal in `TeamListenCommand`, `ListeningMode`,
4316
- `team.listeningControl`, and the task’s `listening.mode`. `LISTENING_MODES` exports the new literal;
4317
- no legacy alias is accepted. Update commands, capability declarations and retained snapshots
4318
- together. The lead must already be listening and permitted to use this mode. Everyone on the
4319
- call hears the lead; this does not transfer ownership or invoke lead-assist takeover.
4320
-
4321
4289
  A lead may listen to a member's call without being asked -- to coach, to check quality, to step in
4322
- when it goes wrong. This is not lead assist: nobody requested it, the member's task says nothing
4323
- about it, and there is no take-over in it. A lead who wants the call itself uses lead assist. The
4324
- three modes determine who hears the lead:
4290
+ when it goes wrong. It is a team act naming the member, and no task on the lead's desk carries it:
4291
+ the lead's view of the call is the member's task on the team member list, with its room and its
4292
+ history. The three modes determine who hears the lead:
4325
4293
 
4326
4294
  | Mode | Who hears the lead |
4327
4295
  | --- | --- |
@@ -4329,61 +4297,44 @@ three modes determine who hears the lead:
4329
4297
  | `coach` | The agent alone. The customer hears nothing. |
4330
4298
  | `join-call` | Everyone on the call. |
4331
4299
 
4332
- The login says which modes this lead has, as a list on `team.listeningControl`, and it always
4333
- includes `listen`, because coach and join-call begin from one
4334
- (`authentication.capability.team.listeningControl.listen`). A centre that lets every lead listen but
4335
- reserves joining the call declares `["listen", "coach"]`, and Omni offers no Join call to that lead. The
4336
- declaration requires `executeTeamListen`, gated exactly as `executeTeamLeadAssist` is by
4337
- `team.leadAssistControl`.
4300
+ All three come with the `lead` flag; a centre reserves none of them per lead. The flow:
4338
4301
 
4339
4302
  ```ts
4340
4303
  // 1. The lead picks a member from the team member list and starts silent.
4341
- executeTeamListen({ command: { type: "listen", memberId: "A-1" } })
4304
+ executeTeam({ command: { type: "listen", memberId: "A-1" } })
4305
+ // team-updated: the member carries listening: { mode: "listen", since }
4306
+ // team-media-started: { memberId: "A-1", assignmentId: "alloc-42" } -- the lead's audio attaches
4342
4307
 
4343
- // 2. The lead's own task arrives -- task-offered with `automatic`, as a joined call does -- and
4344
- // carries `listening`; the assignment is the lead's own, the member's is named inside it.
4345
- // listening: { memberId: "A-1", assignmentId: "alloc-42", mode: "listen", since }
4308
+ // 2. The lead changes how they are heard; the provider restates the member with the new mode.
4309
+ executeTeam({ command: { type: "coach", memberId: "A-1" } })
4310
+ executeTeam({ command: { type: "join-call", memberId: "A-1" } })
4346
4311
 
4347
- // 3. The lead changes how they are heard; the provider restates the task with the new mode.
4348
- executeTeamListen({ command: { type: "coach" } })
4349
- executeTeamListen({ command: { type: "join-call" } })
4350
-
4351
- // 4. The lead leaves; their task ends with `{ type: "left" }`. The member's call goes on.
4352
- executeTeamListen({ command: { type: "leave" } })
4312
+ // 3. The lead leaves, or takes the call.
4313
+ executeTeam({ command: { type: "leave", memberId: "A-1" } })
4314
+ executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
4353
4315
  ```
4354
4316
 
4355
- **The lead's task is what gives them audio**: Omni opens media on it as on any voice task, and the
4356
- provider bridges the lead's leg into the member's call in the mode stated. `listening.mode` is
4357
- the state, restated on every change, and a `coach` or `join-call` the login's list does not include
4358
- is answered `failed`. **A lead listens to one call at a time** (`snapshot.listening.single`), and
4359
- a task carries lead assistance or listening, never both (`task.listening.assisting`).
4360
- The `join-call` mode still belongs to listening; it does not create `assisting`. The member's
4361
- call ending ends the lead's task as it ends the member's, with `left`, since the lead was never
4362
- working on it.
4363
-
4364
- **A lead listens only while holding no work of their own.** A listening task is the lead's only
4365
- task, and a snapshot carrying one beside any other is refused (`snapshot.listening.alone`); Omni
4366
- offers Listen to a lead with no task. That includes a lead on a break -- unlike joining, which a
4367
- break forbids -- but only a break that is work of another sort: `coaching`, `administrative` or
4368
- `training`, the kinds in `LISTENING_BREAK_KINDS`, and never a meal or a rest. A break in effect
4369
- otherwise holds no task; the listening task is its one exception, and a snapshot carrying one
4370
- during a break whose active reason is of any other kind, or of no stated kind, is refused
4371
- (`snapshot.listening.break`). The kind decides, not the reason's name: a centre that wants leads
4372
- listening during a break declares that break `coaching`, `administrative` or `training`.
4317
+ **The lead's audio arrives through team media.** While the lead's channel is in a member's call
4318
+ -- listening, coaching or joined on request -- the lead's connection receives `team-media-started`
4319
+ naming the member and the member's assignment, and on a softphone the agent application opens
4320
+ `openMedia` on that assignment as it does for a task; `team-media-ended` closes it. Voice only, and
4321
+ to a login that declares `lead` (`event.teamMedia.*`). The member's own media events are the
4322
+ member's and never reach the lead.
4323
+
4324
+ **`listening` on the member is the state**, restated on every change of mode, and a lead is on
4325
+ one member's call at a time (`team.listening.single`). The member's call ending ends the lead's
4326
+ listening with it: the provider republishes the member without `listening` and sends
4327
+ `team-media-ended`.
4328
+
4329
+ **A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
4330
+ channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
4331
+ includes a lead on a break: a lead working as lead alone is on a break of a working kind, and
4332
+ listens through it.
4373
4333
 
4374
4334
  **Nothing reaches the member.** The member's task carries no trace of a listening lead in any
4375
4335
  mode: not on `onCall`, not in the record. Whether coaching is announced to the agent is the
4376
4336
  platform's business and travels on the audio, not on this wire.
4377
4337
 
4378
- ```ts
4379
- const listeningLead = {
4380
- channel: "voice",
4381
- capabilities: {},
4382
- phase: "in-progress",
4383
- listening: { memberId: "A-1", assignmentId: "alloc-42", mode: "coach", since: "2026-08-21T09:04:00Z" },
4384
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "listening">;
4385
- ```
4386
-
4387
4338
  ### A member waiting for a break
4388
4339
 
4389
4340
  A member who has asked for a break **keeps working** until Omni commits it, so asking is not an
@@ -4411,7 +4362,7 @@ whole and replaced whole, and a provider that cannot say omits it.
4411
4362
 
4412
4363
  **An agent is not waiting on one person.** Authority is held by several, everyone who holds it
4413
4364
  sees the request on their own console, and **any one of them settles it**. Omni offers the
4414
- decision to every login that declares `team.breakControl` — which is how the provider already
4365
+ decision to every login that declares `lead` and has the team feature on — which is how the provider already
4415
4366
  says who may decide — and does not try to work out whose turn it is.
4416
4367
 
4417
4368
  A request needing *more than one* approval is not something this contract describes. There is
@@ -4619,7 +4570,7 @@ platform is the adapter's business.
4619
4570
  ## Task commands
4620
4571
 
4621
4572
  Command names follow the channel's operational vocabulary, and each channel's command is a union
4622
- discriminated by `type` — the same discriminant `executeTeamBreak` and `custom` already use. The
4573
+ discriminated by `type` — the same discriminant `executeTeam` and `custom` already use. The
4623
4574
  unions are declared under **Shapes**.
4624
4575
 
4625
4576
  `assignmentId` is not repeated on the command. It travels on the `TaskCommandRequest` around it.
@@ -4731,7 +4682,8 @@ declared:
4731
4682
  | Command | What makes it available |
4732
4683
  | --- | --- |
4733
4684
  | `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
4734
- | `end-call` | The `endCall` capability. |
4685
+ | `end-call` | The `endCall` capability: ends my part, the caller continues. |
4686
+ | `terminate-call` | The `terminateCall` capability: ends the caller channel. |
4735
4687
  | `conference` with `action: "remove"` | The `conference` capability, and somebody else on the call: a remove that would leave the agent alone is `end-call`, and a provider answers it `failed`. |
4736
4688
  | `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
4737
4689
  | `call` | The `preview` phase. A record put in front of an agent is there to be called, so the phase is the gate and there is no capability. It is a dial, with a `dialId` and a `dial-outcome`. |
@@ -4740,13 +4692,12 @@ declared:
4740
4692
  | `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
4741
4693
  | `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
4742
4694
  | `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
4743
- | `lead-assist` with `action: "take-over-call"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
4744
4695
  | `transfer` with a destination, `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
4745
4696
  | `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying a non-empty string for each `required` prompt field and strings for any optional fields supplied (`command.custom.prompt`). A toggle carries its target `on` boolean (`command.custom.on`). |
4746
4697
  | Everything else | Its own named capability. |
4747
4698
 
4748
4699
  **A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
4749
- `hold`, `resume` and `pause`, `end-call`, every `transfer` and `conference` action, and
4700
+ `hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `transfer` and `conference` action, and
4750
4701
  every `lead-assist` action. These controls act within this agent's current interaction.
4751
4702
  In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
4752
4703
  an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
@@ -5139,11 +5090,11 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
5139
5090
  - releases task-scoped resources; and
5140
5091
  - selects another task or returns to the idle workspace.
5141
5092
 
5142
- A `left` outcome ends the task for this agent alone: the call continues without them, as it does
5143
- when a lead who joined it leaves -- see **Lead assist**.
5093
+ A `taken-over` outcome arrives at the member's own completion, after the wrap that follows a
5094
+ take-over as it follows an `end-call` -- see **Lead assist**.
5144
5095
 
5145
5096
  A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
5146
- and not for ever: `applied` to a completion -- `complete`, or a lead's `take-over-call` -- says the
5097
+ and not for ever: `applied` to a `complete` says the
5147
5098
  provider has completed the task, and its `task-ended` follows within the
5148
5099
  manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
5149
5100
  performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
@@ -5211,7 +5162,7 @@ each connected provider.
5211
5162
  Replaces this provider's complete `TeamMembers`. It is emitted only for an agent the provider
5212
5163
  publishes a team member list to, and it carries the whole team every time — never a change to it, for the
5213
5164
  reason set out under **Team leads**. A lead's snapshot always carries the team member list; it goes only when
5214
- a republished `authenticated` no longer declares `capabilities.team`.
5165
+ a republished `authenticated` no longer declares `capabilities.lead`, or the lead turns the team feature off (`event.team.features`).
5215
5166
 
5216
5167
  ### `contacts-updated`
5217
5168
 
@@ -5285,9 +5236,9 @@ Some rules need to know who is reading. `validateTeamMembers`, `validateSnapshot
5285
5236
  `AuthenticationState.identity.id` and their login's `capabilities`. Given `self`, a team member list that
5286
5237
  carries that agent reports `team.member.self` or `team.request.self`. Given `capabilities`, a lead's
5287
5238
  snapshot without a team member list reports `team.required`, a team member list published to a login that does not lead
5288
- reports `team.unentitled`, `requests` on a team member list whose login lacks `team.leadAssistControl` reports
5289
- `team.requests.capability`, and a team member list without them on a login that declares it reports
5290
- `team.requests.required`. Without them those rules are not checked, because they cannot be.
5239
+ reports `team.unentitled`, and a `team-updated` to such a login reports `event.team.capability`; given
5240
+ `leadFeatures: false`, a team member list reaching a lead who turned the feature off reports
5241
+ `team.unexpected` or `event.team.features`. Without them those rules are not checked, because they cannot be.
5291
5242
  `exerciseAdapter` always passes both.
5292
5243
 
5293
5244
  `assertNoViolations(violations)` throws `ProtocolConformanceError` — which carries the full
@@ -5459,7 +5410,7 @@ cannot be established from TypeScript structure alone.
5459
5410
 
5460
5411
  | Helper | Contract checked |
5461
5412
  | --- | --- |
5462
- | `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no team member list for a login that no longer leads, no requests for one that may no longer join. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
5413
+ | `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no team member list for a login that no longer leads. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
5463
5414
  | `assertTaskCapabilityWithdrawal(tasks, manifest, command)` | A capability withdrawn by a republish of the task is gone from the task: every task in the sequence is validated, all carry the offer's id, at least one capability the offer declared is absent at the end (a locked control is present, not withdrawn), and `command` is clean against the first task and refused against the last for want of a withdrawn capability and nothing else. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's answer. |
5464
5415
  | `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. The same assertion serves a command the provider never supported under a capability it declares. |
5465
5416
  | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |