@xema/omni-protocol 0.1.97 → 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
@@ -114,7 +114,7 @@ type DurationSeconds = number;
114
114
  | --- | --- | --- |
115
115
  | `IsoTimestamp` | `string` | An RFC-3339 timestamp with `Z` or an explicit numeric offset. Timezone-less values are invalid. It must pass the shared runtime validator. A JavaScript `Date` never crosses the protocol boundary. |
116
116
  | `UserId` | `string` | A non-empty, opaque, stable identifier for a person, **issued by the provider** and drawn from the same directory as `AuthenticationState.identity.id`. It names agents and managers alike; the role is established by where the value appears, not by its type. Compare it exactly and only within one provider; do not parse it or infer meaning from its format. |
117
- | `AssignmentId` | `string` | A non-empty, opaque identifier for one assignment, **issued by the provider**: unique within the provider and never reused while the provider is still speaking about it. Whether it is the platform's own handle passed through or one the adapter minted is the adapter's business. Omni scopes it with the provider ID -- see `taskKey()`. |
117
+ | `AssignmentId` | `string` | A non-empty, opaque identifier for one assignment, **issued by the provider**: unique within the provider and never reused while the provider is still speaking about it. Whether it is the platform's own handle passed through or one the adapter minted is the adapter's business. Omni scopes it with the provider ID -- see `assignmentKey()`. |
118
118
  | `DurationSeconds` | `number` | A non-negative integer duration measured in seconds. |
119
119
 
120
120
  ### There is no Omni-wide user identity
@@ -127,7 +127,7 @@ So a `UserId` means nothing outside the provider that issued it. Provider A's
127
127
  `history[].by` and provider B's team member list `memberId` are unrelated strings that will
128
128
  eventually collide, and one person on several providers has several identities that nothing here
129
129
  pairs. Scope every user identifier with its provider ID before storing or comparing it, exactly as
130
- `taskKey()` already does for tasks — see `userKey()` under **Utilities**.
130
+ `assignmentKey()` already does for tasks — see `userKey()` under **Utilities**.
131
131
 
132
132
  ## Shapes
133
133
 
@@ -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>;
@@ -587,7 +581,7 @@ The channel arms are why `Task<"email">` rejects `hold` at compile time rather t
587
581
 
588
582
  ```ts
589
583
  const BROWSER_ISOLATION_SCHEMES = {
590
- PROVIDER_NAME__TASK_ID__TAB_NAME: "ProviderName.TaskId.TabName",
584
+ PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME: "ProviderName.AssignmentId.TabName",
591
585
  TAB_NAME: "TabName",
592
586
  PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME: "ProviderName.TaskTypeName.TabName",
593
587
  PROVIDER_NAME__TAB_NAME: "ProviderName.TabName",
@@ -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> = {
@@ -1235,7 +1234,7 @@ const HISTORY_STEPS_WITH_A_PERSON = [
1235
1234
  const OMNI_FAILURE_CODES = [
1236
1235
  "omni.not-authenticated",
1237
1236
  "omni.capability-not-enabled",
1238
- "omni.task-not-found",
1237
+ "omni.assignment-not-found",
1239
1238
  "omni.destination-not-permitted",
1240
1239
  "omni.phone-not-permitted",
1241
1240
  "omni.rate-limited",
@@ -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.
@@ -2073,7 +2067,7 @@ Creates one live provider connection for the signed-in agent.
2073
2067
  | `autoAcceptTasks` | Agent local policy policy relayed to the provider at login, stated by the agent application on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
2074
2068
  | `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
2075
2069
  | `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a desk-phone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's**. |
2076
- | `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.task-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
2070
+ | `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
2077
2071
  | `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides**. |
2078
2072
  | `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
2079
2073
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
@@ -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
 
@@ -2448,7 +2438,7 @@ ending, a history report, a command -- would land on whichever assignment is ope
2448
2438
  So the assignment id is unique within the provider and never reused: where the platform's handle
2449
2439
  never comes back, the adapter passes it through; where it does, the adapter mints the assignment id
2450
2440
  and keeps the mapping, and the desk never learns which. A late event finds the assignment it
2451
- belongs to or is refused, and a task-scoped browser session (`PROVIDER_NAME__TASK_ID__TAB_NAME`)
2441
+ belongs to or is refused, and a task-scoped browser session (`PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME`)
2452
2442
  lives one assignment, never the next customer's cookies. A dial the agent application placed was
2453
2443
  already safe, since its outcome is placed by the agent application's own `dialId`; where the
2454
2444
  assignment earns its place is everything the agent application does not mint --
@@ -2736,7 +2726,8 @@ Migration from the earlier spellings:
2736
2726
  | handlingStepExpectsAPerson, handlingStepDials, and their interaction spellings | `historyStepExpectsAPerson`, `historyStepDials` |
2737
2727
  | handleSeconds | `interactionSeconds` |
2738
2728
  | Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `assignmentExpiresAt` |
2739
- | Task.id, TaskId, and `taskId` on every event, command, report and lead request | gone: a task is named by `assignmentId` alone, and `taskKey(providerId, assignmentId)` scopes it |
2729
+ | Task.id, TaskId, and `taskId` on every event, command, report and lead request | gone: a task is named by `assignmentId` alone, and `assignmentKey(providerId, assignmentId)` scopes it |
2730
+ | taskKey, omni.task-not-found, PROVIDER_NAME__TASK_ID__TAB_NAME (ProviderName.TaskId.TabName) | `assignmentKey`, `omni.assignment-not-found`, `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` (`ProviderName.AssignmentId.TabName`) |
2740
2731
  | Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
2741
2732
 
2742
2733
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
@@ -2953,7 +2944,7 @@ At a provider-confirmed task/media end, the agent application also stops the ass
2953
2944
  its observed ending where the report is still accepted. An agent application closing report repeats its original
2954
2945
  key and may include its measured duration, but cannot reverse an already confirmed provider end.
2955
2946
  The provider acknowledges a known closed leg without rewriting its final record; after the task
2956
- has been completed, it may refuse the report as task-not-found. The agent application reads that response rather
2947
+ has been completed, it may refuse the report as assignment-not-found. The agent application reads that response rather
2957
2948
  than retrying or recreating the task. An observed agent application end before a provider closure is still
2958
2949
  reported normally; the provider decides the final timestamp and publishes the record.
2959
2950
 
@@ -3049,7 +3040,7 @@ The supported `BrowserIsolationScheme` values, declared under **Shapes**, key as
3049
3040
 
3050
3041
  | Enum member | Example session key |
3051
3042
  | --- | --- |
3052
- | `PROVIDER_NAME__TASK_ID__TAB_NAME` | `mailflow.EMAIL-829102%2Ea1.CRM` -- the task's assignment id, so one assignment, never the next customer's cookies; the scheme's name is a stable wire value and keeps its spelling |
3043
+ | `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` | `mailflow.EMAIL-829102%2Ea1.CRM` -- the task's assignment id, so one assignment, never the next customer's cookies |
3053
3044
  | `TAB_NAME` | `CRM` |
3054
3045
  | `PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME` | `mailflow.Support.CRM` |
3055
3046
  | `PROVIDER_NAME__TAB_NAME` | `mailflow.CRM` |
@@ -3196,7 +3187,8 @@ See **Which commands need a capability**.
3196
3187
  | --- | --- | --- |
3197
3188
  | `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
3198
3189
  | `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
3199
- | `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**. |
3200
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**. |
3201
3193
  | `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
3202
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**. |
@@ -3291,50 +3283,41 @@ end the caller journey; a later assignment to the same agent is a new interactio
3291
3283
 
3292
3284
  #### Ending a call, and removing one person from it
3293
3285
 
3294
- A call has everyone on `Task.onCall`, and two commands take people off it, both performed by the
3295
- 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.
3296
3288
 
3297
3289
  ```ts
3298
- { 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
3299
3292
  { type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
3300
3293
  { type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
3301
3294
  ```
3302
3295
 
3303
- The agent application sends the ordinary task-scoped `execute` request with `command: { type: "end-call" }`.
3304
- The provider knows the caller and agent-added channel bindings; no channel list or owner details
3305
- are sent by the agent application. The task's `endCall` capability is permission, not an executor selection.
3306
- `validateTaskCommandRequest` checks the exact interaction/assignment and delegates the command's
3307
- policy/phase checks. The provider rechecks current ownership atomically; no client validator can
3308
- prove that its view is fresh. The agent application does not implement agent end-call; low-level removal of individual channels is not
3309
- this agent action. There is no agent application end-call execution API or fallback route.
3310
-
3311
- `end-call` is gated by `endCall` on the current interaction. It ends the caller connection and
3312
- all agent-added channels owned by that interaction, including channels originally added by previous
3313
- agents and inherited through transfer or takeover. Ownership is not inferred from who originally
3314
- dialed, the current UI selection, or a person's label. The provider maintains that authority
3315
- and checks the exact interaction/assignment when acting. Agent-added channels are not left behind
3316
- merely because another agent added them.
3317
-
3318
- On a successful transfer or takeover, ownership of those channels passes to the receiving
3319
- interaction (lead or another agent), not just ownership of the caller leg. The outgoing interaction may
3320
- still have its allowed wrap work, but cannot disconnect channels it no longer owns. A pending or
3321
- unknown handover is not a confirmed ownership change: retain source reservations and reconcile.
3322
- The provider must serialize handover and end-call so a delayed outgoing command cannot tear down
3323
- the recipient's call. No public channel-owner field or guessed identity is introduced here;
3324
- the source must provide authoritative ownership and command fencing before declaring permission.
3325
-
3326
- The resulting end of this agent's media is published as `task-media-ended`. Any wrap remains
3327
- until its own completion; `end-call` is not `task-ended`, and `complete` is not an implicit
3328
- caller/channel disconnect. Other agents' independent tasks end or wrap only on their own source
3329
- transitions. Neither completion nor transfer itself ends the continuing caller journey.
3330
-
3331
- `conference` `remove` remains a person-specific provider operation, gated by `conference`.
3332
- It does not implicitly complete any other interaction. Its person/consultation prerequisites
3333
- remain separate from the permission to disconnect the caller. A remove that would leave the
3334
- agent alone is not a remove but an `end-call`, and the provider answers it `failed`.
3335
- The consulted destination of a warm transfer is not removed this way:
3336
- `transfer` `cancel` is its own step, which returns the agent to the parked customer, and a remove
3337
- 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.
3338
3321
 
3339
3322
  #### Warm transfer
3340
3323
 
@@ -3631,7 +3614,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
3631
3614
  | `awaiting-approval` | A person has to decide. The agent is waiting on somebody. |
3632
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. |
3633
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`. |
3634
- | `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**. |
3635
3618
 
3636
3619
  A denial is a decision, not a standing approval state. The provider transitions the request directly
3637
3620
  to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
@@ -3842,12 +3825,15 @@ request object only to the request method, and undefined to the other three.
3842
3825
  | `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
3843
3826
  | `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
3844
3827
 
3845
- Use `validateTeamBreakCommand(request, context)` for lead decisions, forcing a break, ending a forced break and
3846
- policy commands. It requires the live lead capability and active transport, a current target
3847
- team member list for member commands, and the target's complete break state for forcing/ending a break.
3848
- Approve/deny requires an awaiting decision; forcing a break uses the target's reason codes; ending a forced break
3849
- requires a forced committed break. The context's target state must belong to the named member;
3850
- 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.
3851
3837
 
3852
3838
  Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
3853
3839
  as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
@@ -4115,22 +4101,23 @@ so there is nothing on it to resume.
4115
4101
 
4116
4102
  ## Team leads
4117
4103
 
4118
- A lead who also takes calls sees their team on the idle dashboard. `Snapshot.team` carries a
4119
- `TeamMembers` object, replaced whole by `team-updated`.
4120
-
4121
- Migration: TeamRoster is now `TeamMembers`, and validateTeamRoster is now
4122
- `validateTeamMembers`. Update type imports and validator calls; no legacy export aliases exist.
4123
- The object still contains `members` and the permitted `requests` and `policies`; it is not a
4124
- bare array. `Snapshot.team`, the `team-updated` event and its `team` payload, login permissions,
4125
- and `team.*` validation rules keep their names and semantics. An empty team has `members: []`;
4126
- 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.
4127
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.
4128
4115
 
4129
4116
  | Field | Contract |
4130
4117
  | --- | --- |
4131
- | `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. |
4132
- | `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**. |
4133
- | `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**. |
4134
4121
 
4135
4122
  | `TeamMember` field | Contract |
4136
4123
  | --- | --- |
@@ -4138,6 +4125,8 @@ an absent `team` still means the login is not entitled to that contribution.
4138
4125
  | `availability` | Required. What the member is doing now. |
4139
4126
  | `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
4140
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`). |
4141
4130
 
4142
4131
  Each availability value means one thing:
4143
4132
 
@@ -4152,7 +4141,8 @@ Each availability value means one thing:
4152
4141
  **Always publish the complete team member list, never a change to it.** Team presence typically reaches an
4153
4142
  adapter over a best-effort channel with no ordering and no delivery guarantee, so a stream of deltas
4154
4143
  cannot be trusted to reconstruct the truth. The adapter reconciles against its own authoritative
4155
- 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.
4156
4146
 
4157
4147
  **Omit `since` rather than inventing one.** Omni renders it as a duration, so a timestamp
4158
4148
  synthesised from the adapter's own clock at seed time reads as "on task for 0 seconds" for
@@ -4160,11 +4150,20 @@ everybody — worse than showing nothing, because it looks like data. Send it on
4160
4150
  knows when the state actually began. It times the current `availability`, so it moves every time
4161
4151
  that value does.
4162
4152
 
4163
- **The login is the permission.** A team member list goes to a login that declares `capabilities.team`, on
4164
- every snapshot, and to nobody else. Omni never decides who leads a team: the provider said so at
4165
- sign-in, and the team member list agrees with it — present, `[]` included, for a lead; absent for everybody
4166
- else, which is the correct rendering for an agent who leads nobody. What the lead may do with the
4167
- 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.
4168
4167
 
4169
4168
  **The team member list never carries the agent it is published to — not in `members`, and not in
4170
4169
  `requests`.** A lead does not report to themself: their own break request and their own ask for a
@@ -4172,24 +4171,36 @@ lead go up to whoever leads them and appear on *that* person's team member list,
4172
4171
  only their own `BreakState` and their task's `leadAssist` move. An adapter whose platform lists the lead
4173
4172
  among their own members filters the signed-in identity out before publishing. **Being a lead is a
4174
4173
  role the provider knows, never inferred from who is listed:** it is declared at sign-in, a lead
4175
- 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
4176
4175
  member count can tell those two apart.
4177
4176
 
4178
4177
  ### Lead commands
4179
4178
 
4180
- One method, `executeTeamBreak`, taking a discriminated command exactly as `execute` takes a
4181
- `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.
4182
4183
 
4183
4184
  | Command | Effect |
4184
4185
  | --- | --- |
4185
- | `{ 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`. |
4186
4188
  | `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
4187
- | `{ 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. |
4188
- | `{ 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**. |
4189
4199
 
4190
4200
  `memberId` is this provider's own identifier for the member, as published on its team member list. It is
4191
4201
  never an identifier from another provider, and Omni does not translate between them; names come
4192
- from `getUserDetails()`.
4202
+ from `getUserDetails()`. `validateTeamCommand` holds each command to the team member list as it
4203
+ stands -- see **Runtime break prerequisite checks**.
4193
4204
 
4194
4205
  `requests-suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
4195
4206
  approve them. A provider that suspends break requests must also publish `canRequestBreak: false` to the team's
@@ -4203,71 +4214,56 @@ never expressed here.
4203
4214
 
4204
4215
  An agent on a call may ask a lead to join it -- a dispute that needs approval, a customer who
4205
4216
  asks for a manager, a moment the agent wants a second pair of ears. A call centre calls this
4206
- 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
4207
- team, and a second lead method beside `executeTeamBreak`:
4208
-
4209
- ```ts
4210
- executeTeamLeadAssist({ command: TeamLeadAssistCommand }): Promise<TeamCommandResult>
4211
- ```
4212
-
4213
- Required when the login declares `capabilities.team.leadAssistControl`, and gated by it exactly as
4214
- `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:
4215
4220
 
4216
4221
  ```ts
4217
4222
  // 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
4218
4223
  execute({ assignmentId: "alloc-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
4219
4224
  // task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
4220
4225
 
4221
- // 2. Every lead entitled to it sees the request on their team member list.
4222
- // 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 }]
4223
4228
 
4224
4229
  // 3. A lead joins, or declines.
4225
- executeTeamLeadAssist({ command: { type: "join", requestId: "req-7" } })
4226
- 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" } })
4227
4232
  ```
4228
4233
 
4229
- **On `join` the provider bridges three parties and the lead is on a task of their own**, an
4230
- assignment of its own, arriving on the lead's connection as `task-offered` with `automatic`
4231
- -- the way a call an agent placed themselves arrives -- and carrying `assisting`, which names the
4232
- member and the member's assignment. The agent's task
4233
- moves to `leadAssist: { stage: "joined", leadId }`. A join is the lead's own act, so capacity does not
4234
- trigger it; but from then on it is an outstanding task the provider counts against the lead's
4235
- stated ceiling like any other, nothing more is assigned to the lead while it stands, and a
4236
- provider whose lead is already at the ceiling answers the join `failed`.
4237
-
4238
- **A lead assists one call at a time.** A `join` from a lead already on a call -- their own or one
4239
- they joined -- is answered `failed`, whatever their ceiling; the request stands for another lead,
4240
- or until it is withdrawn or declined.
4241
-
4242
- **A lead on a break does not join.** A break is a reported state in which the agent is not working,
4243
- and a join is work. Omni offers Join to a lead only while their own `BreakState.status` is neither
4244
- `starting-after-task` nor `on-break` -- a committed break waiting for the lead's current work to
4245
- finish is not given more -- and a provider answers a `join` from a lead on such a break `failed`.
4246
- 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`.
4247
4244
 
4248
4245
  **On `decline`, or a request the agent withdraws with `{ type: "lead-assist", action: "cancel" }`, the
4249
4246
  provider clears `leadAssist` from the agent's task** and drops the request from every team member list. Nothing
4250
4247
  else changes; the agent is still on the call.
4251
4248
 
4252
- **Take over call** uses `{ type: "lead-assist", action: "take-over-call" }`.
4253
- The lead assumes the agent’s interaction; the caller’s wider journey continues.
4254
- Migration: replace the former take-over action literal with `take-over-call` in hosts and
4255
- providers together. The former spelling is rejected, with no compatibility alias.
4256
- The `taken-over` task outcome and existing completion behavior remain unchanged.
4257
- 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.
4258
4251
 
4259
- The lead then has two commands on their copy, gated by `assisting` being present, and a third
4260
- 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:
4261
4255
 
4262
- | The lead | The agent's task | The lead's task |
4256
+ | | The member's task | The lead's desk |
4263
4257
  | --- | --- | --- |
4264
- | `{ 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 |
4265
- | `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-media-ended`, then `task-ended` with `{ type: "left" }` -- the call goes on without them |
4266
- | 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. |
4267
4260
 
4268
- `left` is the one outcome that ends a task without ending the call: this agent left a call that
4269
- continues without them. It reads as neither a completion nor a cancellation, because it is
4270
- 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.
4271
4267
 
4272
4268
  ```ts
4273
4269
  const leadAssistCapable = {
@@ -4276,6 +4272,13 @@ const leadAssistCapable = {
4276
4272
  phase: "in-progress",
4277
4273
  leadAssist: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
4278
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">;
4279
4282
  ```
4280
4283
 
4281
4284
  Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
@@ -4283,44 +4286,10 @@ Lead and member alike are `UserId`s of this provider, so an adapter publishing t
4283
4286
 
4284
4287
  ### Listening to a call
4285
4288
 
4286
- **Listen** lets the lead hear the call without being heard. The modes are `listen`, `coach`
4287
- (agent-only lead audio), and `join-call` (lead audio heard by everyone).
4288
-
4289
- | Former API | Current API |
4290
- | --- | --- |
4291
- | MonitorMode / MONITOR_MODES | `ListeningMode` / `LISTENING_MODES` |
4292
- | TeamMonitorCommand / TeamMonitorCommandRequest | `TeamListenCommand` / `TeamListenCommandRequest` |
4293
- | executeTeamMonitor | `executeTeamListen` |
4294
- | team.monitorControl | `team.listeningControl` |
4295
- | TaskMonitoring / task.monitoring | `TaskListening` / the task’s `listening` field |
4296
- | MONITORING_BREAK_KINDS / breakKindAllowsMonitoring | `LISTENING_BREAK_KINDS` / `breakKindAllowsListening` |
4297
- | monitor action and mode | `listen` action and mode |
4298
-
4299
- Update command dispatch, allowed modes, task snapshots, imports and diagnostics together.
4300
- The former names are not aliases. Old task and permission fields are rejected even alongside
4301
- new fields. Validation rule segments now use `listening`, `listeningControl`, and `listen`.
4302
- The existing login permissions, single-call limits, allowed break kinds and audio effects
4303
- remain unchanged. Listening includes its coach/join-call modes and does not imply takeover.
4304
-
4305
-
4306
- **Coach** lets the lead speak privately to the agent; the caller cannot hear the lead.
4307
- The command and listening mode are `coach`. Migration: replace the former whisper literal
4308
- in `TeamListenCommand`, `ListeningMode`, `team.listeningControl`, and the task’s `listening.mode`.
4309
- `LISTENING_MODES` exports `coach`; update command producers, permissions and retained snapshots
4310
- together. No legacy alias is accepted. The lead must already be listening and permitted to
4311
- coach. This mode does not join the audible caller conversation or take over the task.
4312
-
4313
- The user-facing action is **Join call**. The command and listening mode are `join-call`.
4314
- Migration: replace the former barge literal in `TeamListenCommand`, `ListeningMode`,
4315
- `team.listeningControl`, and the task’s `listening.mode`. `LISTENING_MODES` exports the new literal;
4316
- no legacy alias is accepted. Update commands, capability declarations and retained snapshots
4317
- together. The lead must already be listening and permitted to use this mode. Everyone on the
4318
- call hears the lead; this does not transfer ownership or invoke lead-assist takeover.
4319
-
4320
4289
  A lead may listen to a member's call without being asked -- to coach, to check quality, to step in
4321
- when it goes wrong. This is not lead assist: nobody requested it, the member's task says nothing
4322
- about it, and there is no take-over in it. A lead who wants the call itself uses lead assist. The
4323
- 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:
4324
4293
 
4325
4294
  | Mode | Who hears the lead |
4326
4295
  | --- | --- |
@@ -4328,61 +4297,44 @@ three modes determine who hears the lead:
4328
4297
  | `coach` | The agent alone. The customer hears nothing. |
4329
4298
  | `join-call` | Everyone on the call. |
4330
4299
 
4331
- The login says which modes this lead has, as a list on `team.listeningControl`, and it always
4332
- includes `listen`, because coach and join-call begin from one
4333
- (`authentication.capability.team.listeningControl.listen`). A centre that lets every lead listen but
4334
- reserves joining the call declares `["listen", "coach"]`, and Omni offers no Join call to that lead. The
4335
- declaration requires `executeTeamListen`, gated exactly as `executeTeamLeadAssist` is by
4336
- `team.leadAssistControl`.
4300
+ All three come with the `lead` flag; a centre reserves none of them per lead. The flow:
4337
4301
 
4338
4302
  ```ts
4339
4303
  // 1. The lead picks a member from the team member list and starts silent.
4340
- 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
4341
4307
 
4342
- // 2. The lead's own task arrives -- task-offered with `automatic`, as a joined call does -- and
4343
- // carries `listening`; the assignment is the lead's own, the member's is named inside it.
4344
- // 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" } })
4345
4311
 
4346
- // 3. The lead changes how they are heard; the provider restates the task with the new mode.
4347
- executeTeamListen({ command: { type: "coach" } })
4348
- executeTeamListen({ command: { type: "join-call" } })
4349
-
4350
- // 4. The lead leaves; their task ends with `{ type: "left" }`. The member's call goes on.
4351
- 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" } })
4352
4315
  ```
4353
4316
 
4354
- **The lead's task is what gives them audio**: Omni opens media on it as on any voice task, and the
4355
- provider bridges the lead's leg into the member's call in the mode stated. `listening.mode` is
4356
- the state, restated on every change, and a `coach` or `join-call` the login's list does not include
4357
- is answered `failed`. **A lead listens to one call at a time** (`snapshot.listening.single`), and
4358
- a task carries lead assistance or listening, never both (`task.listening.assisting`).
4359
- The `join-call` mode still belongs to listening; it does not create `assisting`. The member's
4360
- call ending ends the lead's task as it ends the member's, with `left`, since the lead was never
4361
- working on it.
4362
-
4363
- **A lead listens only while holding no work of their own.** A listening task is the lead's only
4364
- task, and a snapshot carrying one beside any other is refused (`snapshot.listening.alone`); Omni
4365
- offers Listen to a lead with no task. That includes a lead on a break -- unlike joining, which a
4366
- break forbids -- but only a break that is work of another sort: `coaching`, `administrative` or
4367
- `training`, the kinds in `LISTENING_BREAK_KINDS`, and never a meal or a rest. A break in effect
4368
- otherwise holds no task; the listening task is its one exception, and a snapshot carrying one
4369
- during a break whose active reason is of any other kind, or of no stated kind, is refused
4370
- (`snapshot.listening.break`). The kind decides, not the reason's name: a centre that wants leads
4371
- 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.
4372
4333
 
4373
4334
  **Nothing reaches the member.** The member's task carries no trace of a listening lead in any
4374
4335
  mode: not on `onCall`, not in the record. Whether coaching is announced to the agent is the
4375
4336
  platform's business and travels on the audio, not on this wire.
4376
4337
 
4377
- ```ts
4378
- const listeningLead = {
4379
- channel: "voice",
4380
- capabilities: {},
4381
- phase: "in-progress",
4382
- listening: { memberId: "A-1", assignmentId: "alloc-42", mode: "coach", since: "2026-08-21T09:04:00Z" },
4383
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "listening">;
4384
- ```
4385
-
4386
4338
  ### A member waiting for a break
4387
4339
 
4388
4340
  A member who has asked for a break **keeps working** until Omni commits it, so asking is not an
@@ -4410,7 +4362,7 @@ whole and replaced whole, and a provider that cannot say omits it.
4410
4362
 
4411
4363
  **An agent is not waiting on one person.** Authority is held by several, everyone who holds it
4412
4364
  sees the request on their own console, and **any one of them settles it**. Omni offers the
4413
- 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
4414
4366
  says who may decide — and does not try to work out whose turn it is.
4415
4367
 
4416
4368
  A request needing *more than one* approval is not something this contract describes. There is
@@ -4618,7 +4570,7 @@ platform is the adapter's business.
4618
4570
  ## Task commands
4619
4571
 
4620
4572
  Command names follow the channel's operational vocabulary, and each channel's command is a union
4621
- 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
4622
4574
  unions are declared under **Shapes**.
4623
4575
 
4624
4576
  `assignmentId` is not repeated on the command. It travels on the `TaskCommandRequest` around it.
@@ -4730,7 +4682,8 @@ declared:
4730
4682
  | Command | What makes it available |
4731
4683
  | --- | --- |
4732
4684
  | `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
4733
- | `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. |
4734
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`. |
4735
4688
  | `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
4736
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`. |
@@ -4739,13 +4692,12 @@ declared:
4739
4692
  | `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
4740
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`. |
4741
4694
  | `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
4742
- | `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`. |
4743
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`). |
4744
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`). |
4745
4697
  | Everything else | Its own named capability. |
4746
4698
 
4747
4699
  **A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
4748
- `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
4749
4701
  every `lead-assist` action. These controls act within this agent's current interaction.
4750
4702
  In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
4751
4703
  an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
@@ -4812,7 +4764,7 @@ react rather than only display the message:
4812
4764
  | --- | --- |
4813
4765
  | `omni.not-authenticated` | The provider session is no longer usable. The adapter has published `expired` at or before this answer — the state is what Omni surfaces reauthentication from; the code says why this action failed, and is never the only signal. |
4814
4766
  | `omni.capability-not-enabled` | The action targets a capability this task, manifest, or login did not declare — including a lead command from a login whose `capabilities` no longer carry it. |
4815
- | `omni.task-not-found` | The assignment named is not one the provider holds, typically after the task already ended. |
4767
+ | `omni.assignment-not-found` | The assignment named is not one the provider holds, typically after the task already ended. |
4816
4768
  | `omni.phone-not-permitted` | The agent application declared a `phone` the platform does not permit for this agent -- a softphone for an agent configured for a desk phone, or the reverse. The login is refused at authentication, and the provider never reconfigures the agent to make the declaration true. See **How the agent hears the call**. |
4817
4769
  | `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
4818
4770
  | `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
@@ -5138,11 +5090,11 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
5138
5090
  - releases task-scoped resources; and
5139
5091
  - selects another task or returns to the idle workspace.
5140
5092
 
5141
- A `left` outcome ends the task for this agent alone: the call continues without them, as it does
5142
- 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**.
5143
5095
 
5144
5096
  A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
5145
- 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
5146
5098
  provider has completed the task, and its `task-ended` follows within the
5147
5099
  manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
5148
5100
  performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
@@ -5210,7 +5162,7 @@ each connected provider.
5210
5162
  Replaces this provider's complete `TeamMembers`. It is emitted only for an agent the provider
5211
5163
  publishes a team member list to, and it carries the whole team every time — never a change to it, for the
5212
5164
  reason set out under **Team leads**. A lead's snapshot always carries the team member list; it goes only when
5213
- 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`).
5214
5166
 
5215
5167
  ### `contacts-updated`
5216
5168
 
@@ -5224,7 +5176,7 @@ declares the `calendar` idle capability.
5224
5176
 
5225
5177
  ## Utilities
5226
5178
 
5227
- ### `taskKey(providerId, assignmentId)`
5179
+ ### `assignmentKey(providerId, assignmentId)`
5228
5180
 
5229
5181
  Returns a collision-safe global task key by encoding and joining the provider id and the
5230
5182
  assignment id. Use this key in Omni state; an assignment id is unique within its provider and
@@ -5284,9 +5236,9 @@ Some rules need to know who is reading. `validateTeamMembers`, `validateSnapshot
5284
5236
  `AuthenticationState.identity.id` and their login's `capabilities`. Given `self`, a team member list that
5285
5237
  carries that agent reports `team.member.self` or `team.request.self`. Given `capabilities`, a lead's
5286
5238
  snapshot without a team member list reports `team.required`, a team member list published to a login that does not lead
5287
- reports `team.unentitled`, `requests` on a team member list whose login lacks `team.leadAssistControl` reports
5288
- `team.requests.capability`, and a team member list without them on a login that declares it reports
5289
- `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.
5290
5242
  `exerciseAdapter` always passes both.
5291
5243
 
5292
5244
  `assertNoViolations(violations)` throws `ProtocolConformanceError` — which carries the full
@@ -5458,7 +5410,7 @@ cannot be established from TypeScript structure alone.
5458
5410
 
5459
5411
  | Helper | Contract checked |
5460
5412
  | --- | --- |
5461
- | `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. |
5462
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. |
5463
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. |
5464
5416
  | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |