@xema/omni-protocol 0.1.106 → 0.1.108

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
@@ -11,6 +11,23 @@ their contract type or use `satisfies` so the relationship between the example a
11
11
  visible and compiler-checkable. Use another language only when the artifact itself is not
12
12
  TypeScript.
13
13
 
14
+ ## The guide in files
15
+
16
+ This file holds what every reader needs and what the others rely on: the terms, the shapes, the
17
+ contract rules, how an adapter is declared, signed in, connected and heard from, and how a
18
+ conformance run holds it to all of that. The rest is by role and by channel, one file each, and
19
+ every cross-reference names the file it points into.
20
+
21
+ | File | |
22
+ | --- | --- |
23
+ | `guide/agent.md` | **The agent's work.** What an agent does on any channel: the assignment from offer to wrap, the task and its record, the controls a task offers, the commands the desk sends. |
24
+ | `guide/voice.md` | **Voice.** What is true of a call and of nothing else: preview, connecting back, ending a call, conference, every dial and its outcome, recording. |
25
+ | `guide/phone.md` | **The phone.** How the agent hears the call: softphone and desk phone, the station and its headset, opening the audio, the microphone and the record. |
26
+ | `guide/chat.md` | **Chat.** What is true of a chat and of nothing else. |
27
+ | `guide/email.md` | **Email.** What is true of an email and of nothing else. |
28
+ | `guide/breaks.md` | **Breaks.** Asking for a break, a break forced on the agent, capacity, and one break across several providers. |
29
+ | `guide/lead.md` | **Team leads.** The lead's own surface: the team member list, the lead's commands, lead assist, listening to a call. |
30
+
14
31
  ## Terms
15
32
 
16
33
  **Define a vocabulary; do not merely list it** applies to this document's own prose. These words
@@ -28,10 +45,10 @@ are used precisely throughout and mean nothing looser here.
28
45
  | **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
46
  | **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. |
30
47
  | **Interaction** | The agent's time on the call, from answer until their audio ends: the `in-progress` and `paused` phases. An assignment produces at most one interaction, and a call that returns produces a new assignment and so a new interaction. |
31
- | **Task** | The desk's record of one assignment, from offer through wrap: the workspace, controls and completion work the agent has for it, described by the provider and held by the desk. A task is identified by the assignment it is the record of. `pending`, `confirmed` and `preview` are assigned and not yet interacting; `in-progress` and `paused` are the interaction; `completing` is the assignment outliving its interaction. A voice task is one assignment, never the caller's whole call. |
48
+ | **Task** in `guide/agent.md` | The desk's record of one assignment, from offer through wrap: the workspace, controls and completion work the agent has for it, described by the provider and held by the desk. A task is identified by the assignment it is the record of. `pending`, `confirmed` and `preview` are assigned and not yet interacting; `in-progress` and `paused` are the interaction; `completing` is the assignment outliving its interaction. A voice task is one assignment, never the caller's whole call. |
32
49
  | **Channel** | The kind of work a provider carries: `voice`, `chat`, or `email`. Fixed per provider by its manifest. |
33
50
  | **Task type** | The provider's own name for a category of work — a queue, a mailbox folder, a chat source. Free-form, and finer-grained than a channel. |
34
- | **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*; the provider performs it, and the agent application only offers it — see **Where a command executes**. |
51
+ | **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*; the provider performs it, and the agent application only offers it — see **Where a command executes** in `guide/agent.md`. |
35
52
  | **Login** | One authenticated sign-in to one provider, identified by `loginId`. A transport reconnect keeps it; signing in again replaces it, and nothing tied to the old `loginId` survives. |
36
53
  | **Transport** | The adapter's connection to its platform: a WebSocket or SignalR connection, required to be persistent and ordered. Which one and how it reconnects are the adapter's business; losing it does not end a login. |
37
54
  | **Connection** | The `Connection` object Omni holds for one login: the methods it can call and the events it receives. |
@@ -40,8 +57,8 @@ are used precisely throughout and mean nothing looser here.
40
57
  | **Event** | One completed transaction reported after a snapshot established the baseline. |
41
58
  | **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
59
  | **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
- | **Dial** | One outbound call the agent application asks a provider to place — from the idle dialpad, a preview's Call, 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'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; `listen` and `coach` leave no trace on the member's task, while a lead heard by everyone is on its `onCall` as an `agent`. Every lead sees every lead on the call. A lead who wants the call takes it over. See **Listening to a call**. |
60
+ | **Dial** | One outbound call the agent application asks a provider to place — from the idle dialpad, a preview's Call, 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** in `guide/voice.md`. |
61
+ | **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; `listen` and `coach` leave no trace on the member's task, while a lead heard by everyone is on its `onCall` as an `agent`. Every lead sees every lead on the call. A lead who wants the call takes it over. See **Listening to a call** in `guide/lead.md`. |
45
62
  | **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 conferenced in from the moment their dial is placed. |
46
63
 
47
64
  These fields describe state within their containing objects. Both authentication and break
@@ -182,6 +199,24 @@ type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected
182
199
 
183
200
  type Phone = "softphone" | "deskPhone";
184
201
 
202
+ type PhoneStatus = "ready" | "unregistered" | "do-not-disturb" | "off-hook";
203
+
204
+ type PhoneChannelState = "ringing" | "active" | "held";
205
+
206
+ type PhoneChannel = {
207
+ state: PhoneChannelState;
208
+ since: IsoTimestamp;
209
+ assignmentId?: AssignmentId;
210
+ };
211
+
212
+ type PhoneState = {
213
+ phone: Phone;
214
+ status: PhoneStatus;
215
+ muted?: true;
216
+ since?: IsoTimestamp;
217
+ channels: PhoneChannel[];
218
+ };
219
+
185
220
  type IdleCapabilities<C extends Channel = Channel> = {
186
221
  personalBrowser?: PersonalBrowserCapability;
187
222
  calendar?: true;
@@ -202,6 +237,7 @@ type Manifest<C extends Channel = Channel> = {
202
237
  orgLevels?: LevelDeclaration[];
203
238
  dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
204
239
  phones?: C extends "voice" ? Phone[] : never;
240
+ phoneStatus?: C extends "voice" ? true : never;
205
241
  runningStepReports?: true;
206
242
  settleMs: number;
207
243
  };
@@ -481,6 +517,7 @@ type Snapshot = {
481
517
  contacts?: Contact[];
482
518
  calendar?: ScheduledActivity[];
483
519
  shift?: Shift;
520
+ phone?: PhoneState;
484
521
  team?: TeamMembers;
485
522
  };
486
523
 
@@ -917,7 +954,7 @@ the capability's own name because it is the same capability at another level: ef
917
954
  command `hold` acts on one call; the preference `hold` says whether the person wants the control
918
955
  at all, and an agent application renders it in its settings, never as the button on a call. Mute is in none of
919
956
  these ladders: the microphone is the agent application's, and nobody on the provider's side allows, locks or
920
- keeps a choice about it. See **The station is the agent application's**.
957
+ keeps a choice about it. See **The station is the agent application's** in `guide/phone.md`.
921
958
 
922
959
  ## Breaks
923
960
 
@@ -989,6 +1026,7 @@ type TeamMember = {
989
1026
  listening?: MemberListening[];
990
1027
  shift?: Shift;
991
1028
  request?: MemberRequest;
1029
+ phone?: PhoneState;
992
1030
  };
993
1031
 
994
1032
  type MemberRequest = {
@@ -1145,7 +1183,8 @@ type ProviderEvent =
1145
1183
  | { type: "team-policies-updated"; policies: TeamPolicies }
1146
1184
  | { type: "contacts-updated"; contacts: Contact[] }
1147
1185
  | { type: "calendar-updated"; calendar: ScheduledActivity[] }
1148
- | { type: "shift-updated"; shift: Shift };
1186
+ | { type: "shift-updated"; shift: Shift }
1187
+ | { type: "phone-updated"; phone: PhoneState };
1149
1188
 
1150
1189
  type ProviderEventEnvelope = {
1151
1190
  id: string;
@@ -1444,7 +1483,7 @@ The commands every task has are authorized by other fields the provider declared
1444
1483
  offered can be accepted, one in `preview` can be called, any task under `agent-command` and one
1445
1484
  with a wrap under `provider-automatic` can be completed.
1446
1485
  Nothing is issuable that the provider did not publish; only
1447
- which field says so varies. See **Which commands need a capability**.
1486
+ which field says so varies. See **Which commands need a capability** in `guide/agent.md`.
1448
1487
 
1449
1488
  ### 5. Snapshots establish state; events report transactions
1450
1489
 
@@ -1636,11 +1675,12 @@ compile time.
1636
1675
  | `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
1637
1676
  | `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
1638
1677
  | `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label a desk shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
1639
- | `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `deskPhone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call**. |
1640
- | `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
1678
+ | `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `deskPhone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call** in `guide/phone.md`. |
1679
+ | `phoneStatus` | Voice only, declared by presence as `true`: the platform sees the phone itself -- its registration, its do-not-disturb, its hook, its calls -- and publishes it as `Snapshot.phone` and `phone-updated`. A platform that cannot see the phone omits it, and then publishes no phone (`manifest.phoneStatus`). See **The phone's own view** in `guide/phone.md`. |
1680
+ | `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome** in `guide/voice.md`. |
1641
1681
  | `timeCheck` | `true`: implements `checkTime` and states `providerTime` on every snapshot. Required of a provider that publishes an instant the desk renders as a running duration -- a member's `since`, a `listening[].since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts from the provider's clock (`manifest.timeCheck.required`); agent application polling is independently opt-in. See **Every screen counts from the provider's clock**. |
1642
1682
  | `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
1643
- | `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**. |
1683
+ | `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** in `guide/agent.md`. |
1644
1684
  | `settleMs` | Required. How long after `applied` the wire shows what the provider did: the `task-ended` after a `complete`, the `calendar-updated` after a `schedule`. A positive whole number of milliseconds (`manifest.settleMs`). Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1645
1685
 
1646
1686
  ### Authentication methods
@@ -1773,7 +1813,7 @@ in the agent's view would show that it had happened.
1773
1813
  `calendar` is available to voice, chat, and email providers. It contributes a calendar to the idle
1774
1814
  dashboard for callbacks and other scheduled activities associated with that provider. Calendar is
1775
1815
  the provider's: Omni displays activities and, where a task offers `schedule`, asks the provider to
1776
- create one -- see **Scheduling a follow-up** -- but cannot reschedule, cancel, or complete them.
1816
+ create one -- see **Scheduling a follow-up** in `guide/agent.md` -- but cannot reschedule, cancel, or complete them.
1777
1817
 
1778
1818
  ```ts
1779
1819
  idleCapabilities: { calendar: true }
@@ -1896,7 +1936,7 @@ Creates the provider-scoped authentication session.
1896
1936
  | `protocolVersion` | The negotiated version, fixed for this login. |
1897
1937
  | `loginId` | Omni-generated identity for this login. The same value Omni later passes as `ConnectContext.loginId`, and how an adapter ties a connection back to the session that authenticated it. |
1898
1938
  | `timeZone` | The zone the agent's day is reckoned in, as an IANA name from the agent application's clock, stated before any identity exists so the first `authenticated` state already carries it. The same value Omni later passes as `ConnectContext.timeZone`. See **The agent's day**. |
1899
- | `phone` | How this login hears its calls, chosen by the agent application from the manifest's `phones`: required for a voice provider, absent for any other. The same value Omni later passes as `ConnectContext.phone`. See **How the agent hears the call**. |
1939
+ | `phone` | How this login hears its calls, chosen by the agent application from the manifest's `phones`: required for a voice provider, absent for any other. The same value Omni later passes as `ConnectContext.phone`. See **How the agent hears the call** in `guide/phone.md`. |
1900
1940
  | `secrets` | Omni-provided `SecretStore`, scoped to this provider's manifest id. |
1901
1941
  | `signal` | Optional cancellation signal. |
1902
1942
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
@@ -1950,7 +1990,7 @@ to infer them from what arrives later.
1950
1990
  | Field | Contract |
1951
1991
  | --- | --- |
1952
1992
  | `breaks` | This login may request a break. Requires the four break methods on the connection. |
1953
- | `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**. |
1993
+ | `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** in `guide/lead.md`. |
1954
1994
  | `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**. |
1955
1995
 
1956
1996
  A session action is available only when both the capability and Omni local policy permit it.
@@ -2103,9 +2143,9 @@ Creates one live provider connection for the signed-in agent.
2103
2143
  | `loginId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.loginId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
2104
2144
  | `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()`. |
2105
2145
  | `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**. |
2106
- | `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**. |
2146
+ | `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** in `guide/phone.md`. |
2107
2147
  | `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. |
2108
- | `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**. |
2148
+ | `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** in `guide/phone.md`. |
2109
2149
  | `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
2110
2150
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
2111
2151
 
@@ -2166,13 +2206,14 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
2166
2206
  | `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. |
2167
2207
  | `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
2168
2208
  | `calendar` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
2209
+ | `phone` | The phone as the platform sees it -- the device, whether it can take a call, its own mute where observed -- from a provider whose manifest declares `phoneStatus`; required there (`snapshot.phone.required`) and forbidden otherwise (`snapshot.phone.unexpected`). Replaced whole by `phone-updated`. See **The phone's own view** in `guide/phone.md`. |
2169
2210
  | `shift` | The agent's own day so far, as the provider counts it -- the same `Shift` their lead sees on the team member list, the same numbers from the same count. Replaced whole by `shift-updated`. Omitted only where the provider cannot say. See **The agent's day is on the wire**. |
2170
2211
  | `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. |
2171
2212
 
2172
2213
  ## Live connection
2173
2214
 
2174
2215
  `Connection` is what `connect()` returns. Its methods are documented in the sections that follow
2175
- and under **Breaks**, **Team leads**, **Real-time audio** and **Task commands**; this is the whole
2216
+ and under **Breaks** in `guide/breaks.md`, **Team leads** in `guide/lead.md`, **Real-time audio** in `guide/phone.md` and **Task commands** in `guide/agent.md`; this is the whole
2176
2217
  surface in one place, and what obliges an adapter to implement each one.
2177
2218
 
2178
2219
  | Method | Implement it when |
@@ -2183,21 +2224,21 @@ surface in one place, and what obliges an adapter to implement each one.
2183
2224
  | `disconnect()` | Always. |
2184
2225
  | `refused(report)` | Always. The agent application tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the agent application does with what it refuses**. |
2185
2226
  | `setCapacity(capacity)` | Always. Nothing may be assigned until a capacity is stated, so there is no connection that does not receive it. |
2186
- | `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
2227
+ | `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability** in `guide/agent.md`. |
2187
2228
  | `getUserDetails(ids)` | The adapter publishes any `UserId`: on `ForcedBreak.by` where a person forced it, a team member list, or `history[].by`. Each `User` carries its `timeZone`; a person whose zone the provider cannot name is omitted from the answer, as any unresolvable id is. |
2188
2229
  | `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
2189
2230
  | `requestBreak(request)` | The login declares `capabilities.breaks`. |
2190
2231
  | `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
2191
2232
  | `cancelBreak()` | The login declares `capabilities.breaks`. |
2192
2233
  | `endBreak()` | The login declares `capabilities.breaks`. |
2193
- | `executeTeam(request)` | The login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. See **Lead commands**. |
2234
+ | `executeTeam(request)` | The login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. See **Lead commands** in `guide/lead.md`. |
2194
2235
  | `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
2195
- | `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**. |
2236
+ | `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** in `guide/agent.md`. |
2196
2237
  | `openAudio(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. |
2197
2238
 
2198
2239
  **The four break methods stand or fall together.** Declaring `capabilities.breaks` at login and then
2199
2240
  implementing `requestBreak` without `commitBreak` leaves an agent granted a break that can never
2200
- start, and the two-phase coordination in **Coordinating a multi-provider break** has no way to
2241
+ start, and the two-phase coordination in **Coordinating a multi-provider break** in `guide/breaks.md` has no way to
2201
2242
  report that: `granted` is a promise to honour a later commit.
2202
2243
 
2203
2244
  ### `Connection.snapshot()`
@@ -2267,2699 +2308,6 @@ Stops the connection and releases adapter-owned resources.
2267
2308
  - Must remove event handlers and release audio resources owned by the adapter.
2268
2309
  - Does not imply that active tasks were completed or removed.
2269
2310
 
2270
- ## Task assignment lifecycle
2271
-
2272
- The task-assignment lifecycle has five ordered stages:
2273
-
2274
- **1. The agent signs in.** Nothing on the wire says ready or not-ready: the agent is not ready
2275
- until the agent application has stated a capacity, and ready once it has. A provider assigns nothing before a
2276
- capacity is stated, and needs no other word for it.
2277
-
2278
- **2. The agent becomes ready.** The agent application states the capacity when its local policy says: at once,
2279
- or when the agent presses Ready. A successful connection, a healthy provider, or the absence of a
2280
- break does not imply readiness. After that, an agent stops taking work on this provider only by a
2281
- break under `capabilities.breaks`, or by the agent application restating `count: 0` -- which is how
2282
- a forced break on another provider reaches this one; a login that declares no breaks has no
2283
- not-ready of its own, and `count: 0` is the agent application's division of capacity, not the
2284
- agent's choice.
2285
-
2286
- **3. Omni states their concurrent capacity.** Only now does Omni ask providers for work, saying how
2287
- much the agent can take at once. It is a standing declaration rather than a poll: Omni restates the
2288
- capacity every time it changes, in either direction, and does not ask again while that value holds.
2289
- Silence keeps the last one in force, so a provider that waits to be asked a second time will never
2290
- deliver again once it has gone quiet. Hold the capacity and deliver when work arrives.
2291
-
2292
- **4. A provider offers a task.** The provider emits `task-offered` within the stated capacity, and
2293
- the task is `pending`: an offer introduces work nobody has accepted, so it is the only phase an
2294
- offer introduces (`event.taskOffered.phase`). `confirmed`, `preview` and the rest are reached from
2295
- it, or carried on a snapshot.
2296
-
2297
- **5. Omni decides how the task is accepted.** When `autoAcceptTasks` is `false`, every task requires
2298
- agent acceptance. When it is `true`, the pending task's `acceptance` states the provider's
2299
- intent.
2300
-
2301
- ### Acceptance modes
2302
-
2303
- During login, Omni sends the agent's `autoAcceptTasks` value to the provider. When it is `true`, the
2304
- provider states `acceptance` on each pending task — on the task rather than the offer, so a
2305
- reconnect snapshot says it too and an offer the agent application never received is not accepted on the
2306
- person's behalf for want of a word:
2307
-
2308
- | Directive | Contract |
2309
- | --- | --- |
2310
- | `no-preference` | The provider leaves acceptance to Omni; with `autoAcceptTasks: true`, Omni accepts automatically. |
2311
- | `consent` | The provider requires the person's explicit consent: Omni presents **Accept** and waits, whatever its own policy would have done. An agent application that declares `guarantees.personConsent` promises exactly this; a provider checks it before offering work only a person may take. |
2312
- | `automatic` | Omni accepts immediately without agent interaction. |
2313
-
2314
- When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task the
2315
- queue routes requires agent acceptance. The two are never confused on the wire: `consent` is always the
2316
- provider's requirement, stated on a wire where Omni was willing to accept for the agent; Omni's own
2317
- no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
2318
- press is Omni's doing, not the provider's.
2319
-
2320
- **The value is stated, never assumed.** `autoAcceptTasks` is required of every connection: a
2321
- agent application says which policy the deployment provisioned, and a validator that is not told checks
2322
- neither rule rather than guess the permissive one (`task.acceptance.required` and
2323
- `.unexpected` fire only against a stated value). `acceptance` is the provider's own control and
2324
- outranks the policy: `consent` puts the decision back in the agent's hands for any task where it
2325
- belongs, whatever the agent application was configured with.
2326
-
2327
- An automatically accepted task still arrives through `task-offered`.
2328
-
2329
- **Work the agent originated is accepted by the command that created it.** A task born of the
2330
- agent's own act — a dialpad call or a connect-back, recognisable by the agent application's `dialId` on
2331
- `onCall`; a call a lead took over, carrying `takenOver` — arrives
2332
- through `task-offered` with `acceptance: "automatic"` whatever `autoAcceptTasks` says, and the
2333
- validator holds it there under either local policy and under none (`task.acceptance.originated`):
2334
- the desk shows no **Accept** for a call the agent placed. The local policy governs work the queue
2335
- routes to the agent, and nothing else.
2336
-
2337
- ### Pending
2338
-
2339
- A task in the `pending` phase has been **offered to the agent and not yet accepted**. Omni applies
2340
- `autoAcceptTasks` and the task's `acceptance` to decide whether acceptance is
2341
- automatic or requires the agent. A provider that requires automatic acceptance still emits
2342
- `task-offered`; it does not introduce new work as `in-progress`.
2343
-
2344
- ```ts
2345
- declare const task: Task;
2346
-
2347
- const assignment = {
2348
- type: "task-offered",
2349
- task: { ...task, phase: "pending", acceptance: "consent", expiresInSeconds: 30 },
2350
- } satisfies Extract<ProviderEvent, { type: "task-offered" }>;
2351
- ```
2352
-
2353
- The rule the phase exists to express: **nothing is acquired on the agent's behalf while a task
2354
- is pending.** An agent application that carries audio must not open the microphone until the task is
2355
- accepted. Omni does not open the task's browsers either — a task that rings out costs nothing.
2356
-
2357
- When consent is required, Omni offers the agent an **Accept** control. The call is the
2358
- medium the task arrives on, not a separate decision.
2359
-
2360
- **Once a task is accepted, the call that comes with it is answered.** Omni has no discretion
2361
- there and the provider is not consulted twice: one decision about the work, and the medium
2362
- follows it. Where the audio lands was settled at sign-in — in Omni on a softphone, on the handset
2363
- on a desk phone — and is not in question per call. See **How the agent hears the call**.
2364
-
2365
- Automatic acceptance still begins with `task-offered`.
2366
-
2367
- `expiresInSeconds` is how long the offer has left, in whole seconds from the publication that
2368
- carries it. It rides on the pending task, as `acceptance` does, so a reconnect snapshot restates
2369
- it from what is actually left; it belongs to `pending` alone and is gone once the task is accepted
2370
- (`task.pending.expiresInSeconds`, `.unexpected`). Where present, Omni counts down from receipt and
2371
- stops offering **Accept** once it runs out; the provider ends the lapsed offer with `task-ended`
2372
- and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.** A provider that reports only elapsed
2373
- ring time after the fact cannot say how long an offer has left, and a computed value would have
2374
- Omni withdraw **Accept** from a task still pending.
2375
-
2376
- **A deadline is seconds left, never an instant.** The agent application and the provider keep
2377
- different clocks, and a countdown that compares one against the other is a countdown that runs
2378
- fast or slow by their skew. Seconds from the publication that carries them are the provider's own
2379
- arithmetic, counted down on the desk from the moment they arrive, and the transport delay is the
2380
- only error. Restated on every publication that carries them, so a snapshot after a reconnect
2381
- starts the countdown afresh from what is actually left (`task.pending.expiresInSeconds`,
2382
- `task.preview.previewEndsInSeconds`: a whole number, zero or more).
2383
-
2384
- A preview's deadline is not on the offer. It travels on the task, as `previewEndsInSeconds` with
2385
- `atDeadline`, so a snapshot carries it too -- see **Preview: the agent presses Call**.
2386
-
2387
- A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome, `by:
2388
- "provider"`. **`cancelled` says who called the work off**, as `completed` says who completed it:
2389
- `agent` for a decline, `provider` for a withdrawal or a re-route, `party` for a caller who abandoned
2390
- the ring. One word for the three left a supervisor's record unable to tell them apart, so `by` is
2391
- required and closed (`event.taskEnded.outcome.cancelled.by`). An offer nobody acted on while
2392
- `expiresInSeconds` ran is not cancelled by anyone: it lapses, and ends `expired` naming `pending`.
2393
-
2394
- ### Tasks already in progress
2395
-
2396
- The provider does not introduce new work as already `in-progress`.
2397
-
2398
- A task appears already `in-progress` only in a snapshot taken after a reconnect or a resync,
2399
- reporting work that began earlier in this login and never stopped. A fresh login has none to
2400
- report: nothing has been assigned yet, and work the agent was working elsewhere is not carried
2401
- into a new session.
2402
-
2403
- ## Tasks
2404
-
2405
- ### `Task`
2406
-
2407
- `Task` is the provider-owned description of one task presented to Omni.
2408
-
2409
- `task-offered` introduces a new task and does not imply acceptance.
2410
-
2411
- `Task<C>` is channel-discriminated. For example, `Task<"email">` accepts
2412
- `browsers` and `outcomes`, but rejects voice-only controls such as `hold` and `endCall` at compile
2413
- time. Runtime conformance checks also require the task channel to match its provider manifest.
2414
-
2415
- | Field | Contract |
2416
- | --- | --- |
2417
- | `assignmentId` | Required `AssignmentId`: the assignment this task is the record of, and its one identity. Issued by the provider, unique within the provider, never reused. Every event, command and report that names a task names it by this, so a late dial outcome or a late history report for one customer never lands on the next. The stream refuses an assignment introduced twice (`stream.taskOffered.duplicate`) and an audio or ending event naming one that has ended or that nobody has seen (`stream.assignment.ended`, `.unknown`); a `dial-outcome` may name an ended assignment, since a dial placed late routinely outlives its call, and the agent application routes it there. See **A task's life on the wire**. |
2418
- | `title` | Agent-facing task title. |
2419
- | `channel` | Channel used for this task. It must equal the source provider's manifest channel. |
2420
- | `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
2421
- | `capabilities` | Controls and workspace features available for this specific task. |
2422
- | `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `nobody` when nothing handed the work over -- an agent's own outbound -- and `not-yet-read` when a queue was named and its terms could not be read, in which case `capabilities` is what the provider will honour, not what the platform permits. An agent application shows `not-yet-read` where the agent works. See **Task capabilities**. |
2423
- | `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
2424
- | `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
2425
- | `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
2426
- | `audio` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-audio-started`**. Audio names a task whose work has begun, or whose party the agent application is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.audio.beforeWork`), on a snapshot as on the event, since an agent application opens the microphone on it; with the party ringing by an agent application dial or provider-triggered preview dial, actual ring-back audio may precede answer. |
2427
- | `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
2428
- | `expiresInSeconds` | In `pending` only: how long the offer has left, in whole seconds from this publication, restated on every publication that carries it. Absent, the offer stands until the provider says otherwise. See **Pending**. |
2429
- | `previewEndsInSeconds` | Voice only, in `preview`: how long the preparation has left, in whole seconds from this publication, restated on every publication that carries it. Absent, the agent has as long as they need without a preparation countdown. Always with `atDeadline`. See **Preview: the agent presses Call**. |
2430
- | `atDeadline` | Voice only, in `preview`, with `previewEndsInSeconds`: what the system does when it runs out -- `provider-dials` makes the provider initiate dialing, `host-dials` makes the agent application issue Call, `waits` keeps the task in preview awaiting the agent. |
2431
- | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
2432
- | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one, and takes the agent's `complete` as finishing early. A required outcome is the agent's to give, so it needs `agent-command` (`task.outcomes.required.mode`). |
2433
- | `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time audio, it begins after `task-audio-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down; stated, it is the expected wrap, shown and never acted on. `0` under `provider-automatic` is no wrap at all, so such a task publishes no `outcomes` (`task.outcomes.wrapAllowance`). |
2434
- | `wrapEndsInSeconds` | In `completing`, wherever `wrapAllowance` is stated: how much of the wrap is left, in whole seconds from this publication, restated on every publication that carries it (`task.completing.wrapEndsInSeconds`, `.required`, `.unexpected`). A reloaded desk and a lead's screen count down the same number. See **Completion timing**. |
2435
- | `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
2436
- | `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**. |
2437
- | `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, `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. `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**. |
2438
- | `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**. |
2439
- | `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 their capacity like any other call. See **Lead assist**. |
2440
-
2441
- `TaskAttribute` entries carry typed detail alongside the task:
2442
-
2443
- ```ts
2444
- const attributes: TaskAttribute[] = [
2445
- {
2446
- key: "related-contact",
2447
- label: "Related contact",
2448
- type: "contact",
2449
- party: { name: "Asha Rao", number: "+919876543210" },
2450
- },
2451
- {
2452
- key: "answered",
2453
- label: "Answered",
2454
- type: "timestamp",
2455
- at: "2026-08-27T09:30:00.000Z",
2456
- },
2457
- ];
2458
- ```
2459
-
2460
- `key` is the stable machine identifier and must be unique within the task's `attributes` array.
2461
- `label` is the optional agent-facing name.
2462
-
2463
- The canonical task transitions are:
2464
-
2465
- | From | Decision or event | To |
2466
- | --- | --- | --- |
2467
- | No task | Provider assigns a task | `pending` |
2468
- | `pending` | Task is accepted | `confirmed` |
2469
- | `confirmed` | The customer's record is put in front of the agent before any call goes out (voice) | `preview` |
2470
- | `confirmed` | Work begins | `in-progress` |
2471
- | `preview` | Agent presses Call (`dial`) and the customer answers, or the deadline `provider-dials` and they answer | `in-progress` |
2472
- | `preview` | The call goes out and nobody answers | `completing` |
2473
- | `preview` | The preparation target passes with `atDeadline: "waits"` | Remains `preview`, waiting for the agent to press Call |
2474
- | `pending` | Provider withdraws the assignment, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
2475
- | `pending` | The offer's `expiresInSeconds` runs out | Removed by `task-ended` with `expired` outcome naming `pending` |
2476
- | No task | Snapshot reports work already underway | `in-progress` |
2477
- | `in-progress` | Provider or agent pauses the task | `paused` |
2478
- | `paused` | Provider or agent resumes the task | `in-progress` |
2479
- | `in-progress` or `paused` | Contact interaction ends and follow-up work remains | `completing` |
2480
- | `completing` | Agent connects back to the party (`connect-back`) | `in-progress` |
2481
- | Any phase | Provider emits `task-ended` | Removed |
2482
-
2483
- #### A task's life on the wire
2484
-
2485
- A task has one name, the assignment it is the record of, and the provider issues it. Platforms
2486
- reuse their own handles: a closed one is retired minutes later, a requeue takes seconds, and on
2487
- one platform a call was offered twice, thirteen seconds apart, under one handle, through a close
2488
- and a re-offer. Everything that names a task after the fact -- a dial outcome, an audio event, an
2489
- ending, a history report, a command -- would land on whichever assignment is open when it arrives.
2490
- So the assignment id is unique within the provider and never reused: where the platform's handle
2491
- never comes back, the adapter passes it through; where it does, the adapter mints the assignment id
2492
- and keeps the mapping, and the desk never learns which. A late event finds the assignment it
2493
- belongs to or is refused, and a task-scoped browser session (`PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME`)
2494
- lives one assignment, never the next customer's cookies. A dial the agent application placed was
2495
- already safe, since its outcome is placed by the agent application's own `dialId`; where the
2496
- assignment earns its place is everything the agent application does not mint --
2497
- `task-audio-started`, `task-audio-ended`, `task-ended`, a `recordStep` naming a task -- any of which
2498
- would otherwise find the next customer under a reused handle and act on them. A minted id is part
2499
- of the task the adapter keeps, not a field held in memory beside it: it lives in the login's
2500
- `store` with the task, or is derived from a platform handle that itself never reuses, so a rebuilt
2501
- adapter comes back with the same assignment on the same task, or every late event it was minted to
2502
- catch mismatches and the second adapter's snapshot no longer carries the task (`drive.reload.snapshot`).
2503
-
2504
- Assignment, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
2505
- task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
2506
- subsequent transitions to `preview` or `in-progress`; Omni does not infer them from the acceptance
2507
- command.
2508
-
2509
- #### Preview: the agent presses Call
2510
-
2511
- An outbound campaign that lets the agent see who they are about to call is a **preview**: the
2512
- record arrives as a task and sits in `preview` with the party on it before a call goes
2513
- out. The agent prepares; manual or declared automatic initiation may then begin dialing. It
2514
- remains preview while ringing until an evidenced answer or non-answer outcome. The agent may press Call. That is the whole phase, and it exists on voice alone --
2515
- chat and email have no call to place, and their reading is ordinary work in `in-progress`.
2516
-
2517
- ```ts
2518
- const previewed = {
2519
- channel: "voice",
2520
- capabilities: {},
2521
- phase: "preview",
2522
- party: { name: "Maya Rao", number: "+919876543210" },
2523
- previewEndsInSeconds: 120,
2524
- atDeadline: "provider-dials",
2525
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "party" | "previewEndsInSeconds" | "atDeadline">;
2526
-
2527
- // The agent presses Call. It is a dial like any other.
2528
- const pressed: TaskCommand<"voice"> = { type: "dial", dialId: "dial-7f2" };
2529
- ```
2530
-
2531
- **Call is a dial.** It carries the agent application's `dialId`, is answered `dialling`, and ends in exactly one
2532
- `dial-outcome`; the phase is its gate and there is no capability, since a record put in front of an
2533
- agent is there to be called. Its audio starts on `dialling`, as every agent application-placed dial's does --
2534
- ring-back is audio the agent hears -- so `task-audio-started` arrives on the `preview` task with
2535
- its party ringing. On `answered` the task is `in-progress`; on any other outcome the audio ends,
2536
- `task-audio-ended` starts the wrap clock as on every voice task, and the task goes to
2537
- `completing`, so the agent records the
2538
- no-answer as the outcome it is -- in a campaign that is the commonest outcome there is, and it
2539
- is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
2540
- (`task.preview.dialOutcomes.required`).
2541
-
2542
- **Preparation and trigger ownership are per task.** Unlimited preparation omits both
2543
- `previewEndsInSeconds` and `atDeadline`: only an agent pressing Call starts dialing. Fixed
2544
- preparation carries the seconds the provider says are left, counted down on the desk from the
2545
- publication that carried them and restated on every later one, and exactly one trigger owner:
2546
-
2547
- | Declaration | At preparation end |
2548
- | --- | --- |
2549
- | `atDeadline: "provider-dials"` | The provider initiates dialing. The agent application never sends a timer-triggered Call. |
2550
- | `atDeadline: "host-dials"` | The agent application sends the ordinary `dial` command with a fresh agent application `dialId`. The provider does not independently auto-dial this task. |
2551
- | `atDeadline: "waits"` | Neither side auto-dials. The task remains in preview until the agent presses Call. |
2552
-
2553
- The agent may press Call early in either fixed-preparation dialing mode, or at any time in
2554
- `waits` mode. With `waits`, the countdown is a preparation target, not an expiry: when it reaches
2555
- zero the UI says "Waiting for agent" and keeps Call available. There is no automatic task-ended,
2556
- completion, dialing or phase change. No repeating timer action occurs on later snapshots. This differs
2557
- from unlimited preparation only by displaying a preparation target. The former `expires` deadline
2558
- value is not supported; elapsed preparation must not withdraw the task. For `provider-dials` and `host-dials`, the deadline ends
2559
- preparation and triggers initiation; it does not promise ringing, playable audio or customer
2560
- answer at that exact instant. Scheduling/dispatch delay must remain visible, and a failed or
2561
- prevented initiation must be reported rather than leaving a silently expired countdown.
2562
- A source eligibility threshold that may never trigger an attempt is not this promise.
2563
-
2564
- The two deadline fields travel together and only in preview. The countdown is the seconds the
2565
- publication carried, counted from its receipt: the agent application infers no deadline where none
2566
- was stated and reads no clock against another. Neither elapsed time nor submission changes the
2567
- task phase: subsequent authoritative events state ringing, answer, audio and completion.
2568
-
2569
- An agent application must serialize manual clicks and its timer under the exact provider/login/task/assignment
2570
- scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
2571
- assignment and whether an attempt already exists before dispatch. A new snapshot or policy update
2572
- cancels an obsolete timer; login loss, disconnect, task end and assignment replacement fence its
2573
- callback. Reconnect past a deadline first reconciles current task and attempt state; it must not
2574
- blindly submit again. An uncertain prior submission remains unresolved, without automatic retry.
2575
- The provider atomically arbitrates any residual manual/timer race and does not accept a second
2576
- command as ownership of an already-running independent attempt.
2577
-
2578
- Agent application-triggered automatic dialing is an ordinary agent application dial: its result is `dialling` with the same
2579
- agent application `dialId`, followed by exactly one correlated terminal `dial-outcome`. A provider-triggered
2580
- dial has no invented agent application ID or agent application dial outcome. It reports source-evidenced party ringing,
2581
- actual audio and customer answer separately. In preview with `atDeadline: "provider-dials"`, a ringing
2582
- party without an agent application ID may carry actual pre-answer audio; the deadline alone permits no audio.
2583
- Audio is still introduced/ended by the corresponding events and must not imply customer answer.
2584
- Submission acceptance is never an answered outcome or permission to publish `in-progress` early.
2585
- Unsupported source correlation/outcome/audio evidence remains an integration blocker.
2586
-
2587
- **Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
2588
- spreading the `preview` one carries `previewEndsInSeconds` and `atDeadline` with it, and the agent application refuses
2589
- the update (`task.preview.deadline.unexpected`): the desk keeps the task as it last stood, and the
2590
- provider is told through `refused` exactly which rule, so the state that looked right on its side
2591
- is named on its side. The task past preview has no deadline to wait for, so it carries neither
2592
- field.
2593
-
2594
- **Provider execution and agent application controls are distinct.** Answer, hold/resume, conference,
2595
- lead assist and interaction completion are executed by the provider
2596
- through the adapter. The task's applicable capability, phase, completion mode and command-specific
2597
- prerequisites decide which controls are offered. Caller disconnect also executes at the provider. Microphone mute is agent application-owned, never a provider
2598
- TaskCommand or task capability. The separately declared agent application recording facility keeps its existing
2599
- recording contract; this distinction does not remove it or move provider call operations to the agent application.
2600
-
2601
- A caller journey may revisit the same agent while an earlier interaction still wraps. Each is its
2602
- own assignment, and two open at once carry two assignment ids. Restore them unchanged on
2603
- reconnect. Commands, audio, endings and history reports name the exact assignment; never retarget
2604
- an old command to the newest interaction of a journey. `validateTaskCommandRequest` checks the
2605
- request's assignment against the supplied published task and then its command prerequisites. The
2606
- caller must select that task within the correct provider/login and recheck at the provider; the
2607
- validator cannot prove source freshness or authorization.
2608
- `task-ended` retires only that interaction's resources. Another interaction, caller channel or IVR/queue
2609
- portion must not be cleared because this task ended. Snapshot counts include all still-owned wrap
2610
- and active interactions; task completion is not caller hangup.
2611
-
2612
- **Voice describes the agent's interaction within the wider call.** The task defines that agent's
2613
- workspace, tools, permissions, audio participation and completion work. The caller's channel may
2614
- continue through IVR, queues, other agents, holds or conferences while this interaction ends or wraps.
2615
- Completing this task ends this interaction responsibility; it is not evidence that the caller's channel
2616
- or journey ended. Task audio describes this agent's attachment, not the lifetime of every party's
2617
- connection. Commands retain their explicit targets and effects; task completion must not silently
2618
- become a caller-disconnect operation.
2619
-
2620
- **A task is never its audio.** A voice task represents the agent’s interaction: the call is offered when it is
2621
- routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
2622
- the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
2623
- conference leg joining or leaving, a lead joining, a take-over, a connect-back — the audio follows
2624
- separately, arriving on `task-audio-started`, attaching through `openAudio` and ending with
2625
- `task-audio-ended`. Omni does not ring,
2626
- bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
2627
- are the adapter's and the platform's, transient, and decide neither when a task exists nor what
2628
- phase it is in.
2629
-
2630
- The line runs between the provider's word and Omni's own senses. `task-audio-ended` is the
2631
- provider's report that primary interaction ended — a fact about the work, which is why the completion
2632
- allowance starts on it and the Connect back control appears on it — and Omni follows that report as it
2633
- follows any other. What Omni never does is derive a task's state from its own audio session: a
2634
- stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
2635
- endpoint re-registering change nothing about the task until the provider says so. Structurally:
2636
- `task-audio-started` and `task-audio-ended` alternate on a task whose work has begun, audio ends
2637
- only where it arrived, what follows the audio ending is `completing` or `task-ended`, and every
2638
- task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
2639
- and `assertAudioFollowsTheTask` holds any sequence.
2640
-
2641
- #### Completion timing
2642
-
2643
- `completionMode` determines how completion is triggered. With `agent-command`, the provider keeps
2644
- the task open until Omni sends the channel's `complete` command. With `provider-automatic`, the
2645
- provider may complete the task without receiving that command.
2646
-
2647
- **The agent may finish early under either mode.** `complete` is issuable wherever there is a wrap
2648
- to cut short: under `agent-command` it is the end, and under `provider-automatic` it says the agent
2649
- is done before the allowance ran out, and the provider is free to end the task at once. Only a task
2650
- with no wrap at all -- `provider-automatic` with `wrapAllowance: 0` -- has nothing to complete, and
2651
- the command is refused there (`command.complete.wrapAllowance`). An ending the agent asked for says
2652
- so, `completed` with `by: "agent"`, whichever mode the task was under.
2653
-
2654
- **A required outcome is the agent's to give.** A task whose `outcomes` says `required: true` waits
2655
- for a code the agent chooses, so it completes on the agent's command: `provider-automatic` cannot
2656
- wait for it, and the pair is refused (`task.outcomes.required.mode`). Optional outcomes may ride on
2657
- a task the provider completes itself; the agent gives one if in time. And a code is given in wrap,
2658
- so a `provider-automatic` task with `wrapAllowance: 0` publishes no `outcomes` at all
2659
- (`task.outcomes.wrapAllowance`).
2660
-
2661
- **The allowance means one thing per mode.** Under `provider-automatic` it is what the provider
2662
- acts on: the task ends when it runs out. Under `agent-command` it is the expected wrap, stated so
2663
- the agent can see it: the desk counts it down and, past zero, shows how far over they are, and
2664
- nothing acts on it -- the task waits for `complete` however long that takes, with its outcomes
2665
- collected whenever the agent gives them. A provider that would end the task at a time is
2666
- `provider-automatic`.
2667
-
2668
- **What is left travels with the task.** The allowance is the length; `wrapEndsInSeconds` is how
2669
- much of it remains, stated on the `completing` task and restated on every publication that
2670
- carries it, as `previewEndsInSeconds` is on a preview. A desk that reloads mid-wrap finds the
2671
- number on the snapshot; a lead's screen finds it on the member; neither starts a clock of its own,
2672
- and both count down from receipt. Under `provider-automatic` the provider ends the task when it
2673
- reaches zero; under `agent-command` it is the expected wrap left, `0` once overrun, and nothing
2674
- acts on it. Off voice, where `completing` is the provider's word that interaction ended, the field
2675
- arrives with that word, so the clock starts nowhere else.
2676
-
2677
- `wrapAllowance` is fixed, and when it starts depends on whether the channel carries real-time
2678
- audio:
2679
-
2680
- | Channel | Wrap allowance starts at |
2681
- | --- | --- |
2682
- | Voice and any channel with real-time audio | The `task-audio-ended` event |
2683
- | Chat | The `task-updated` that moves the task to `completing`: the provider's word that the conversation is closed on its side; what is left arrives on that publication as `wrapEndsInSeconds` and is counted down from receipt |
2684
- | Email | The `task-updated` that moves the task to `completing`: the provider's word that the platform has accepted the outgoing message; what is left arrives on that publication as `wrapEndsInSeconds` and is counted down from receipt |
2685
- | Other non-audio channels | The `task-updated` that moves the task to `completing` |
2686
-
2687
- **Off voice, `completing` is the provider's word that interaction ended.** There is no audio event to
2688
- carry it, so the phase does; an agent application starts the wrap clock at that publication and nowhere else. It
2689
- is optional: a conversation with nothing to wrap moves from `in-progress` to `task-ended` and
2690
- `completing` is never published. Under `provider-automatic` with a non-zero `wrapAllowance` it is
2691
- required, because the clock has to start somewhere: a chat or email task the provider completes
2692
- from `in-progress` with an allowance to run gave the agent none of it, and the stream refuses the
2693
- ending (`stream.taskEnded.unwrapped`); an ending the agent's own `complete` brought forward says
2694
- `by: "agent"` and is not that. Under `agent-command` the agent's `complete` is the end, from
2695
- `in-progress` or from `completing` alike.
2696
-
2697
- ```ts
2698
- const emailCompletion = {
2699
- completionMode: "agent-command",
2700
- wrapAllowance: 120,
2701
- } satisfies Pick<Task<"email">, "completionMode" | "wrapAllowance">;
2702
- ```
2703
-
2704
- In this example, the agent has two minutes after sending the email to add notes, select a
2705
- outcome, and complete the task.
2706
-
2707
- `0` means no wrap under `provider-automatic`: the provider completes the task at the end of the
2708
- interaction, and `complete` has nothing to cut short. Under `agent-command` it is an expected wrap
2709
- of nothing -- the desk shows the overrun from the start -- and the task still waits for `complete`.
2710
-
2711
- There is no value meaning "unlimited", because a number that is not a duration would be read as
2712
- one. A provider that imposes no deadline says so by **omitting** `wrapAllowance`, which
2713
- only `agent-command` permits: the provider will not complete the task itself, so there is nothing
2714
- for a deadline to trigger, and Omni counts nothing down. **Omitted and empty are different
2715
- claims** applies -- omitted says there is no deadline to see, where `0` says the deadline is now.
2716
- Under `provider-automatic` the field is required, because the provider is going to act on it.
2717
-
2718
- ```ts
2719
- const untimedWrap = {
2720
- completionMode: "agent-command",
2721
- } satisfies Pick<Task<"voice">, "completionMode" | "wrapAllowance">;
2722
- ```
2723
-
2724
- Here the customer has hung up, `task-audio-ended` has been sent on time, the task is `completing`,
2725
- and the agent takes as long as the work needs. Moving the task to `completing` late to avoid a
2726
- deadline is not an alternative on an audio channel: the clock starts at a real event, and delaying
2727
- that event would falsify the phase and everything timed from it.
2728
-
2729
- #### Connecting back during completion
2730
-
2731
- A task that declares `connectBack` lets the agent connect back to the party while the task is
2732
- `completing` -- to finish what the call left unfinished, on the same task rather than a new one,
2733
- whoever placed the call in the first place. Most such calls came in, so this is not a redial: the
2734
- agent rings a party who rang them. Omni issues `{ type: "connect-back", dialId }`; it is issuable
2735
- only in `completing`, and only where the capability is declared. The provider knows who the party
2736
- is; the command carries no destination, and it is a dial like any other, so it carries the agent application's
2737
- `dialId`, is answered `dialling`, and ends in one `dial-outcome`.
2738
-
2739
- On `dialling` the provider is placing the call and the task returns to `in-progress`: the agent is
2740
- working again, and the wrap allowance is **discarded, not paused**. From there the call is
2741
- reported as any call is -- `paused`, `in-progress`, and when its audio ends, `task-audio-ended`
2742
- again, which starts a fresh allowance from that instant. A party who does not answer is a dial
2743
- whose outcome says so and a call whose audio ended: the task returns to `completing` through the
2744
- same event and the clock starts again from there. At no point is an agent dialling against a
2745
- deadline.
2746
-
2747
- **The room shows the party being dialled.** From the moment any agent application dial places the party -- a
2748
- dialpad call, a preview Call, a connect-back -- the `party` entry carries `stage: "ringing"` and
2749
- the agent application's `dialId`, exactly as a conferenced entry does from its dial; `joined` on
2750
- the publication that follows the answered `dial-outcome` and never before it
2751
- (`stream.taskUpdated.stage`); and no stage from the next publication on, so `joined` is transient
2752
- and a later copy still carrying it is stale (`stream.taskUpdated.stage.lingering`). An agent application shows a
2753
- call being placed, not a party it asserts is on a call that is still ringing. A callback the platform places itself on the same task -- the first
2754
- call over, the platform dialling the party again without a command -- shows the same thing with no
2755
- agent application `dialId`, as any platform-placed dial does. A party with no stage is on the call, and a dial
2756
- with no stage is half a claim (`task.onCall.party.dial`). The stage is also what lets the stream
2757
- tell a task coming back from one going backwards (`stream.taskUpdated.phase`).
2758
-
2759
- **The control exists only while there is a window to use it in.** Under `agent-command` the task
2760
- stays `completing` until the agent completes it, so the window is open for as long as they need.
2761
- Under `provider-automatic` the window is the allowance -- and with `wrapAllowance: 0` there
2762
- is none: the provider completes the task at provider end, and Omni does not offer Connect back,
2763
- whatever the task declares. A capability names a control that can be used; on a task with no `completing`
2764
- window it cannot, and declaring it there changes nothing.
2765
-
2766
- ```ts
2767
- const connectBackCapable = {
2768
- channel: "voice",
2769
- capabilities: { hold: true, connectBack: true, outcomes: true },
2770
- phase: "completing",
2771
- completionMode: "provider-automatic",
2772
- wrapAllowance: 30,
2773
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "wrapAllowance">;
2774
- ```
2775
-
2776
- With ten seconds of the thirty left, the agent presses Connect back: `execute({ command: { type:
2777
- "connect-back", dialId } })` returns `dialling`, the task is `in-progress`, and the thirty seconds
2778
- are gone.
2779
- The second call ends: `task-audio-ended`, the task is `completing`, and a new thirty seconds runs
2780
- from that instant.
2781
-
2782
- ```ts
2783
- const immediateProviderCompletion = {
2784
- completionMode: "provider-automatic",
2785
- wrapAllowance: 0,
2786
- } satisfies Pick<Task, "completionMode" | "wrapAllowance">;
2787
- ```
2788
-
2789
- ### History
2790
-
2791
- The record is the call's, not the task's: most of what it holds happened before this agent, and
2792
- `queued` and `offered` are not interactions at all, so the field is `history` and its types are
2793
- `TaskHistory`, `TaskHistoryStep`, `HistoryStep`, `HistoryReport` and `HistoryReportResult`.
2794
- *Interaction* keeps the one meaning **Terms** gives it, the agent's time on the call, which is why
2795
- the total of earlier agents' time is `interactionSeconds` and the phase rules are
2796
- `command.phase.interaction`. Hosts and providers adopt these names together; no legacy aliases
2797
- are provided. The manifest's `settleMs` bounds final task completion, not entry into
2798
- the `completing` wrap-up phase, and bounds an applied `schedule` the same way. The `recordStep` method and `complete` command keep their names.
2799
-
2800
- Migration from the earlier spellings:
2801
-
2802
- | Former API | Current API |
2803
- | --- | --- |
2804
- | Task.handlingHistory, Task.interactionHistory | `Task.history` |
2805
- | TaskHandlingHistory, TaskInteractionHistory | `TaskHistory` |
2806
- | TaskHandlingStep, HandlingStep, TaskInteractionStep, InteractionStep | `TaskHistoryStep`, `HistoryStep` |
2807
- | HandlingReport, HandlingReportResult, validateHandlingReport, and their Interaction spellings | `HistoryReport`, `HistoryReportResult`, `validateHistoryReport` |
2808
- | HANDLING_STEPS_WITH_A_PERSON, HANDLING_STEPS_THAT_DIAL, and their INTERACTION spellings | `HISTORY_STEPS_WITH_A_PERSON`, `HISTORY_STEPS_THAT_DIAL` |
2809
- | handlingStepExpectsAPerson, handlingStepDials, and their interaction spellings | `historyStepExpectsAPerson`, `historyStepDials` |
2810
- | handleSeconds | `interactionSeconds` |
2811
- | Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `expiresInSeconds` |
2812
- | 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 |
2813
- | 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`) |
2814
- | Manifest.disposalSettleMs, Manifest.completionSettleMs | `Manifest.settleMs` (`manifest.settleMs`, `manifest.settleMs.renamed`): the bound after `applied` for `complete` and `schedule` alike |
2815
- | the call command, atDeadline calls and host-calls | `dial`, `"provider-dials"`, `"host-dials"` |
2816
- | media: task-media-started/-ended, team-media-*, Task.media, TaskMediaState, openMedia, OpenMediaRequest/Result, VoiceMediaSession and its session field | audio: `task-audio-started`, `task-audio-ended`, `audio` on the task, `TaskAudioState`, `openAudio`, `OpenAudioRequest`, `OpenAudioResult`, `CallAudio` on the result's `audio`; there is no team audio, the lead's follows `listening` on the member |
2817
- | capabilitySource values ungoverned and undetermined | `nobody`, `not-yet-read` (`capabilitySource.notYetRead`) |
2818
- | Snapshot.scheduledActivities, and scheduledActivities on calendar-updated | `calendar` in both |
2819
- | availability reserved | `elsewhere` |
2820
- | recording destinationId, HostRecording.destinationIds | `storageId`, `storageIds` (`recording.storage`, `recording.host.storage`) |
2821
- | coldTransfer, warmTransfer, the transfer command and its four actions, the consulted role, the transferred outcome | gone: help arrives by **Lead assist**; the `transferred` history step and the `transfers` total stay, since the platform's own routing still changes hands |
2822
- | assignmentExpiresAt on task-offered, previewEndsAt on the task | `expiresInSeconds`, `previewEndsInSeconds`: seconds left from the publication, never an instant (`event.taskOffered.expiresInSeconds`, `task.preview.previewEndsInSeconds`) |
2823
- | TeamMembers.requests, LeadRequest | `TeamMember.request` (`MemberRequest`): the ask rides on the member (`team.member.request.*`) |
2824
- | team-updated on every change | `team-updated` whole once after the switch and on snapshots; `team-member-updated`, `team-member-removed`, `team-policies-updated` after (`stream.team.baseline`, `stream.teamMember.unknown`) |
2825
- | RecordingCommand.requestId | gone: no request identity; a partial effect is a settled `failed` under `omni.recording-unsettled` |
2826
- | command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only on `provider-automatic` with no wrap |
2827
- | expiresInSeconds on task-offered | `Task.expiresInSeconds`, in `pending` (`task.pending.expiresInSeconds`, `.unexpected`; `event.taskOffered.expiresInSeconds.unexpected`) |
2828
- | the expired outcome naming preview | gone: nothing expires a preview; withdrawn, it is `cancelled` by the provider |
2829
- | schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`drive.schedule.unsettled`) |
2830
- | a forced break requesting breaks on the other providers | `setCapacity({ count: 0 })` on every other usable provider (`assertForcedBreakStopsTheRest`) |
2831
- | end-forced-break on a break the platform imposed | refused: `team.command.endForcedBreak.provider`, `omni.break-forced-by-provider` |
2832
- | ForcedBreak.expectedDurationMs | `expectedEndsInSeconds`: seconds left, restated (`break.forced.expectedEndsInSeconds`, `.expectedDurationMs.renamed`); the command keeps `expectedDurationMs` |
2833
- | MemberShift; the day on the lead's list alone | `Shift`, on `Snapshot.shift` and `shift-updated` for the agent, on `TeamMember.shift` for the lead |
2834
- | TeamMember.listening as this lead's one entry | `MemberListening[]`, every lead's, each with `leadId` (`team.member.listening.leadId`, `.unique`, `.empty`, `.entry`, `.heard`) |
2835
- | a member's history trimmed on the lead's list | the record whole, as the member's desk holds it |
2836
- | durations counted from the screen's clock | the provider's: `timeCheck` required of a provider publishing a running instant (`manifest.timeCheck.required`), `Snapshot.providerTime` with it (`snapshot.providerTime`, `.unexpected`) |
2837
- | a wrap the desk timed from an event's receipt | `Task.wrapEndsInSeconds` in `completing`, restated (`task.completing.wrapEndsInSeconds`, `.required`, `.unexpected`) |
2838
- | MemberTask.capabilities, capabilitySource, browsers | gone: the workspace never travels to a lead (`team.member.task.workspace`); the record and the completion terms do |
2839
- | a member republished on a hold, a mute, a take-over | on every publication of the member's task to the member |
2840
- | two clients on one login | the later wins; the first is ended with `recovery: "displaced"` |
2841
-
2842
- Update producers, consumers, saved task snapshots, and validation-rule assertions together.
2843
- History and report rule names use `history` and `historyReport`; assignment rules use
2844
- `assignment` (`task.assignmentId`, `stream.taskOffered.duplicate`, `stream.assignment.ended`);
2845
- phase and conformance rules use `interaction` and final-task rules use `completion`. The former
2846
- fields are refused even when the new field is also supplied (`task.history.renamed`,
2847
- `task.assignmentId.renamed` for a task's `id` or `allocationId`, `event.assignmentId.renamed`,
2848
- `command.request.assignmentId.renamed` and `historyReport.assignmentId.renamed` for a `taskId`).
2849
-
2850
-
2851
- `Task.history` is the call record: the steps that brought the task to the agent, oldest
2852
- first, and what they add up to before this agent:
2853
-
2854
- ```ts
2855
- history: {
2856
- steps: [
2857
- { step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
2858
- { step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
2859
- { step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
2860
- { step: "transferred", at: "2026-08-21T01:04:53Z", by: "a-17" },
2861
- { step: "answered", at: "2026-08-21T01:05:02Z", by: "a-23" },
2862
- ],
2863
- interactionSeconds: 312, // handled by others before this agent
2864
- holdSeconds: 35, // holds others put the caller on
2865
- queueSeconds: 41, // waiting before anyone answered
2866
- transfers: 1, // hands the call has changed
2867
- }
2868
- ```
2869
-
2870
- **This is not an archive.** It is live data about a task that is still open: it travels with the task
2871
- to whoever holds it next and ends when the task does. Nothing stores it, nothing queries it, and
2872
- there is no archive behind it. A provider that keeps a record of *completed* contacts is describing
2873
- something else, which will arrive under its own name and must not be folded in here.
2874
-
2875
- It rides in the snapshot and is replaced whole like everything else there.
2876
-
2877
- Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
2878
- `unanswered`, and each is defined on `TaskHistoryStep`.
2879
-
2880
- **The record is one entry per occurrence, oldest first.** Each hold is its own `held` entry — `at`
2881
- when it began, `seconds` once it ended and omitted while it runs — and a second hold is a second
2882
- entry after the first, never a revision of it. A leg that has ended states its duration, and the
2883
- task says whether a leg can still be running: a hold runs only while the task is `paused`, a mute
2884
- only while its audio is up, so a `held` entry without `seconds` on a task that is not paused, or a
2885
- `muted` one on a call that is over, is a leg nobody closed and reads exactly like a leg running now
2886
- (`task.history.held.open`, `.muted.open`). Whoever performs the leg closes it — the
2887
- provider whose platform parks the caller, the agent application whose microphone it is — by restating the entry
2888
- with its duration, and **an entry that cannot be closed is not written**: open for ever is a
2889
- plausible nought one level down from a total. **A leg still open when the audio ends is closed by
2890
- the provider**, at that instant, in the same publication that says the audio ended: the provider
2891
- is the one that knows the instant, and the agent application can only learn of it afterwards. Agents end calls
2892
- muted, so the agent application's leg is routinely open at audio end; the provider closes it with the duration
2893
- from the leg's `at` to the audio's end, and the agent application's own closing report for that leg, which
2894
- follows what it hears, is answered `recorded` and changes nothing, the entry standing as the
2895
- provider closed it. One publisher of the closed leg, and no order between them to get right. There is no resumed step; a resume is the end of a
2896
- hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming it twice. The same goes for every step: two mutes are two
2897
- `muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
2898
- entries, one per join. Order is enforced: an entry earlier than the one before it is refused
2899
- (`task.history.order`). **Intervals between steps are the agent application's to subtract**, never a
2900
- total the provider adds: *time to offer*, from the call's arrival to the agent's screen lighting
2901
- up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
2902
- `queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
2903
- that interval. **The second is the record's grain.** A duration is a positive whole number of
2904
- seconds, and nought is refused because it claims no time; a leg that happened is stated at the
2905
- grain: rounded to the nearest second, and a leg that rounds to nought is stated as `1`, the error
2906
- being under the contract's resolution, where dropping the entry would say the leg never happened.
2907
-
2908
- **A record once read is not unread.** Every restatement of a task's record -- on `task-updated`,
2909
- on a resync snapshot -- carries every entry the agent application has already read, by step and instant, and
2910
- may add to them; it never has fewer, and never drops the record while the task is open. A fact
2911
- stated to the agent application and then withdrawn without an event is a record contradicting itself, exactly
2912
- as a task that lost the terms it had read would be, and the stream refuses it the same way
2913
- (`stream.taskUpdated.history`, `stream.snapshot.history`). A provider whose
2914
- platform cannot hold a leg the agent application reported keeps it in the login's `store` for the life of the
2915
- task, so a reload of the agent application restates the same record; the entry is keyed by `step` and `at`, so
2916
- a running hold restated with its final `seconds` is the same entry.
2917
-
2918
- **The provider owns the timestamps in its record.** Agent application timestamps are advisory. The provider
2919
- may retain an agent application instant it accepts or assign its own observation/receipt instant; it need not
2920
- copy the agent application clock. A provider declaring `timestampAuthority: "provider"` uses its own timestamps
2921
- for final records. Receipt time is an observation boundary, not a claim about when the physical
2922
- agent application action happened. Records are ordered by their published provider-selected instants, with no
2923
- rewriting of an already-published entry when a later agent application report arrives.
2924
-
2925
- The incoming agent application `step` and `at`, scoped to the task and its current life, identify one reported
2926
- leg for correlation only. The provider retains a binding from that key to its chosen history
2927
- `at`. Every successful `recordStep` returns `{ status: "recorded", at }` with that same canonical
2928
- history instant, even for subsequent duration/end reports. The agent application keeps sending the original
2929
- report key, never the returned history timestamp. A changed agent application timestamp is not a correction
2930
- to the same report: it names a different leg. Bindings survive reconnect/reload for the retained
2931
- task lifetime. The agent application renders provider-published history unchanged. It never compares the
2932
- provider instant back to its own clock, substitutes its own timestamp, or adjusts later messages
2933
- to match an earlier agent application estimate. The acknowledgment identifies the provider record for
2934
- correlation/conformance only; it is not an application-side timestamp reconciliation instruction. Conflicting reuse is refused visibly, not paired by arrival time or nearest instant.
2935
- If two distinct entries would collide under the history's `(step, at)` key, do not invent a time
2936
- or combine them: report the representation conflict. No ambiguous event is silently accepted.
2937
-
2938
- **Interaction time is anchored, not restarted.** It runs from the
2939
- `answered` step's `at` — from the task's first `in-progress` where the provider reports no
2940
- history — until the task's audio ends, and a hold neither pauses nor resets it: the hold's own
2941
- duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
2942
- counting the wrong thing.
2943
-
2944
- `muted` is there because the mute is the agent application's and never the provider's — see **The station is
2945
- the agent application's** — so without a step the provider, which alone keeps the task's record, would have no
2946
- account of a period the agent could not be heard. Each `muted` entry carries `mutedBy`, as the agent application
2947
- reported it: `host` for the agent's own Mute, `station` for a headset or system mute the agent application
2948
- observed. No other step has it (`task.history.mutedBy`, `.mutedBy.unexpected`).
2949
-
2950
- **A step that dialled says which dial and where.** `conferenced` and `unanswered` each end a dial,
2951
- and `transferred` a hand-over on the platform's own routing, so the entry carries the `destinationId`
2952
- it went to and, where an agent application placed the dial, the `dialId` the agent application minted; no other step dialled, and either field on one is refused
2953
- (`task.history.dialId.unexpected`). This is how a dial made before a take-over is placeable
2954
- by whoever holds the task now: a `dial-outcome` naming a `dialId` the agent application never minted is looked
2955
- up in the record, not discarded.
2956
-
2957
- Four rules a provider has to keep:
2958
-
2959
- - **Report `seconds`; never expect Omni to derive it.** Omni does not subtract one timestamp from
2960
- the next. An entry can be written while its leg is still running, so the arithmetic has no second
2961
- operand, and a provider holding the authoritative number should not have it recomputed from
2962
- instants that may be rounded or clock-skewed. And once stated, `seconds` is what every screen
2963
- shows for that leg: a count a screen kept while the leg ran is replaced by it, never kept beside it.
2964
- - **Omit `seconds` while it is unknown. Never send `0`.** A leg still talking is not a zero-second
2965
- conversation, and on live data that is the ordinary case rather than an edge. A zero is rejected.
2966
- - **`by` is a bare `UserId`, and not necessarily an agent.** A lead or a manager takes part
2967
- during an interaction too — a call taken over, a call conferenced in — so the field names whoever it was,
2968
- from the same directory `ForcedBreak.by` names a lead from. It comes from this provider's own directory, the same
2969
- namespace as `AuthenticationState.identity.id` and the team member list, so entries pair
2970
- within a provider and never across one.
2971
- - **A task carries no names.** Omni resolves what to display with `getUserDetails()`. Two people
2972
- called Arun on one site is ordinary, and anything pairing entries on a display name pairs them
2973
- wrongly; carrying the name here would also copy it into every task and leave it to go stale.
2974
-
2975
- **An absent `by` means different things on different steps, and both are legitimate.** On
2976
- `queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
2977
- `HISTORY_STEPS_WITH_A_PERSON` and `historyStepExpectsAPerson()` — so an absent `by` there says
2978
- *this was handled and the provider cannot say by whom*. The one step that is never unattributed is
2979
- `muted`: the agent application has exactly one agent and the provider knows who, so `by` is required there
2980
- (`task.history.muted.by`).
2981
-
2982
- That case is ordinary rather than theoretical: a leg answered on a shared phone, a manager's
2983
- handset, or a device the provider cannot resolve to a person. **Report the step without `by`
2984
- rather than dropping it.** A list missing a real handler looks complete and is wrong, which is
2985
- worse than one saying plainly it could not attribute a leg — and far better than publishing
2986
- nothing because a single leg could not be named.
2987
-
2988
- An agent application must render the two differently. Showing an unattributed `answered` the same way as
2989
- `queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
2990
- *"not recorded"* in the place the name would go.
2991
-
2992
- Omit `history` entirely when the provider cannot observe the steps. Empty `steps` is a
2993
- different claim — it says the task has had none.
2994
-
2995
- **What the record adds up to is stated, not summed.** A call that has changed hands arrives
2996
- carrying what others already used of it, and the agent reads that before they say hello: the
2997
- totals are the provider's own — never a desk summing instants that may be rounded or skewed, for
2998
- the same reason `seconds` is reported and not derived — and each is present when the provider
2999
- knows it and absent when it does not, never a plausible nought. A fresh call from the queue has
3000
- `steps` with no prior `answered` and no totals to state, and a desk shows nothing rather than
3001
- zeros. The record is restated with the task on every `task-updated` and snapshot, so an entry
3002
- that arrives late corrects the sums. These four are what every platform reports; more will be
3003
- added here as a need is shown, not invented ahead of one.
3004
-
3005
- #### The agent application records what it performs
3006
-
3007
- `muted` is the one leg the agent application performs rather than the provider, and a record kept by the
3008
- provider would have a hole exactly there. So the agent application reports it, through `recordStep`, and the provider
3009
- writes it into its record as it writes every other leg:
3010
-
3011
- ```ts
3012
- declare const connection: Connection<"voice">;
3013
- declare const assignmentId: AssignmentId;
3014
- declare const at: IsoTimestamp;
3015
-
3016
- void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host" }); // the moment the agent mutes
3017
- void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host", seconds: 15 }); // still running, if the agent application chooses to say
3018
- void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host", seconds: 42, ended: true }); // the moment they unmute
3019
- ```
3020
-
3021
- Every report about one leg repeats its original `step` and agent application `at` as a correlation key;
3022
- the published history uses the provider-selected `at` returned by `recordStep`. The agent application reports
3023
- the action it performed and its measured duration, not an authoritative provider timestamp. It may say how long so far — `seconds` is
3024
- elapsed while the leg runs and final once it has ended — and **the end is stated, never
3025
- inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
3026
- provider never asked for never crosses.** The running report is sent only to a provider whose
3027
- manifest declares `runningStepReports`; every other provider receives exactly two reports per
3028
- leg, when it began and when it ended, and `validateHistoryReport(report, path, manifest)` refuses
3029
- a running one it was never asked for (`historyReport.running.unexpected`). What a provider that
3030
- did ask for them forwards upstream, and how often, is its own business. The step appears in
3031
- `history` when the *provider* publishes it: Omni never writes the record itself.
3032
- `recordStep` is required of every softphone login's connection, since every call on a softphone
3033
- can be muted by the agent application, and answers `recorded` with the canonical history `at`. A `muted` report says whose the silence was,
3034
- `mutedBy: "host"` or `"station"`, and no other report has the word (`historyReport.mutedBy`,
3035
- `.mutedBy.unexpected`). On a desk phone the microphone is the phone's:
3036
- the agent application mutes nothing and records nothing.
3037
-
3038
- **The record is a record.** The microphone is the agent application's, and nothing in the
3039
- history moves it: a `muted` entry the provider publishes, or closes with a final `seconds`, is the
3040
- provider's account of a leg the agent application reported, and the mute itself ends only when the
3041
- agent, or the station, ends it. The agent application keeps its own mute state, reports each leg
3042
- as it begins and ends, and takes the provider's published entry as the closed account of that leg:
3043
- it does not reopen the leg, replace the provider's timestamp, or overwrite the provider's final
3044
- duration with a later report. A later agent mute is a new leg, never a reopening of the ended one.
3045
-
3046
- Correlate the provider-published `muted` entry using the provider-selected `at` acknowledged
3047
- for the current report key; a final `seconds` closes that leg under the history contract.
3048
- This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
3049
- task, or a stale connection cannot end a newer local mute. If publication precedes the acknowledgment,
3050
- retain the current provider view and apply the matching closure once correlation is available;
3051
- never guess a match from arrival order or nearest timestamp. Failure to release an agent application-controlled
3052
- mute is reported visibly; local device reports still describe the actual device state.
3053
-
3054
- At a provider-confirmed task/audio end, the agent application also stops the associated local mute and reports
3055
- its observed ending where the report is still accepted. An agent application closing report repeats its original
3056
- key and may include its measured duration, but cannot reverse an already confirmed provider end.
3057
- The provider acknowledges a known closed leg without rewriting its final record; after the task
3058
- has been completed, it may refuse the report as assignment-not-found. The agent application reads that response rather
3059
- than retrying or recreating the task. An observed agent application end before a provider closure is still
3060
- reported normally; the provider decides the final timestamp and publishes the record.
3061
-
3062
- ### Browser capability
3063
-
3064
- `browsers` is available to voice, chat, and email tasks. When declared, Omni renders the task's
3065
- `TaskBrowser` entries as named browsers in the task workspace. A task that supplies one or more
3066
- browser definitions must declare:
3067
-
3068
- ```ts
3069
- capabilities: { browsers: true }
3070
- ```
3071
-
3072
- Tasks without browser definitions omit the capability and provide an empty `browsers` array.
3073
-
3074
- **A browser is one tab, and each workspace keeps its own.** `Browser` is what a tab is — an id, a
3075
- name, a URL. The task workspace shows the task's `TaskBrowser` entries, one tab each, fixed at the
3076
- task's definition: their count and their details, `urlVisibility` included, arrive with the task,
3077
- and nothing adds a tab to a task later. The personal workspace is the agent's: as many
3078
- `PersonalBrowser` tabs as they open, never on the wire, and no provider says anything about what
3079
- they may see there.
3080
-
3081
- #### `TaskBrowser` and isolation
3082
-
3083
- Each `TaskBrowser` defines one named browser in the task workspace.
3084
-
3085
- | Field | Contract |
3086
- | --- | --- |
3087
- | `id` | Stable internal selection and update identity within the task. |
3088
- | `name` | Agent-facing tab label, unique within the task, and an input to schemes containing `TAB_NAME`. |
3089
- | `purpose` | Human-readable explanation of the browser's role. |
3090
- | `url` | Initial URL. Must use `http:` or `https:`; see below. Later navigation comes from Chromium. |
3091
- | `sharedSession` | Required. `false` creates a task-specific browser session. |
3092
- | `isolationScheme` | **Required when `sharedSession` is `true`**, and rejected when it is `false`. There is no default: see below. |
3093
- | `urlVisibility` | What the agent sees of this tab's URL in Omni's chrome: `hidden`, `domain`, or `full`. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read — a caller's number, a CRM token. Per browser, on the provider's word; an agent application that declares `guarantees.browserUrlVisibility` honours it tab by tab, and a provider checks that guarantee before it sends such a URL at all. |
3094
-
3095
- ##### Choosing an isolation scheme
3096
-
3097
- Every scheme is supported and the provider picks the one its deployment needs. There is no
3098
- default, and a `sharedSession: true` browser that declares none is invalid — the type will not compile
3099
- it and `validateSnapshot` reports `task.browser.isolationScheme.required`.
3100
-
3101
- That is deliberate. Sharing a signed-in session decides **who else may see those credentials**,
3102
- and it is not a decision to inherit from whichever value happened to be the default. `TAB_NAME`
3103
- keys on the tab label alone, so two providers that each publish a browser named "CRM" share one
3104
- signed-in session — legitimate where a deployment wants exactly that, and a silent credential
3105
- leak where it does not. It remains available; it has to be asked for.
3106
-
3107
- `browserSessionKey` fails closed: given a reusing browser with no scheme it returns `undefined`
3108
- and the browser is isolated. The safe reading of an invalid declaration is "do not share", never
3109
- "share with everyone named the same".
3110
-
3111
- ##### Permitted URL schemes
3112
-
3113
- `TaskBrowser.url` is provider-supplied and is loaded inside Omni's managed browser, so it is
3114
- restricted to the schemes in `ALLOWED_BROWSER_URL_SCHEMES` — currently `http:` and `https:`.
3115
- `file:`, `chrome:`, `javascript:`, and every other scheme are rejected. Omni substitutes a blank
3116
- page rather than following a disallowed URL, and `isAllowedBrowserUrl()` is the shared predicate.
3117
-
3118
- ##### Reuse and isolation
3119
-
3120
- With `sharedSession: true`, definitions producing the same isolation key share one **storage profile**:
3121
- cookies, local storage, session storage, permissions, and cached credentials. Different keys are
3122
- isolated from one another.
3123
-
3124
- Sharing a profile is not sharing a window. Two browsers in the same task keep their own tab, their
3125
- own visible label, and their own navigation state and history even when their keys match — a
3126
- scheme that omits `TAB_NAME`, such as `PROVIDER_NAME__TASK_TYPE_NAME`, deliberately places every
3127
- named browser of that task type in one signed-in profile without merging them into one page.
3128
-
3129
- `browserSessionKey()` derives the key. Every part is escaped before the `.` separator is applied,
3130
- because `encodeURIComponent` leaves `.` untouched and a raw join would let one value forge
3131
- another key: provider `Acme.Voice` with task type `Support` would otherwise produce the same key
3132
- as provider `Acme` with task type `Voice.Support`, silently placing two providers in one cookie
3133
- jar. Hosts that must flatten the key further — for a native window label or a partition name with
3134
- a restricted charset — must keep the mapping injective, for example by appending a fingerprint of
3135
- the exact key, since lowercasing or replacing punctuation reintroduces exactly this collision.
3136
-
3137
- ```ts
3138
- browsers: [
3139
- {
3140
- id: "crm",
3141
- name: "CRM",
3142
- purpose: "Contact record",
3143
- url: "https://crm.example.com/contact/42",
3144
- sharedSession: true,
3145
- isolationScheme: BROWSER_ISOLATION_SCHEMES.PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME,
3146
- }
3147
- ]
3148
- ```
3149
-
3150
- The supported `BrowserIsolationScheme` values, declared under **Shapes**, key as follows:
3151
-
3152
- | Enum member | Example session key |
3153
- | --- | --- |
3154
- | `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 |
3155
- | `TAB_NAME` | `CRM` |
3156
- | `PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME` | `mailflow.Support.CRM` |
3157
- | `PROVIDER_NAME__TAB_NAME` | `mailflow.CRM` |
3158
- | `PROVIDER_NAME__TASK_TYPE_NAME` | `mailflow.Support` |
3159
- | `TASK_TYPE_NAME__TAB_NAME` | `Support.CRM` |
3160
-
3161
- `TASK_TYPE_NAME` refers to the mandatory `Task.taskType`. The isolation scheme never changes
3162
- the browser tab label. Serialized values are stable protocol values and must not be renamed or
3163
- reused.
3164
-
3165
- **`PROVIDER_NAME` is `manifest.id`, never `manifest.displayName`.** Only the id is required unique
3166
- across an installation; two providers may legitimately share a display label, and keying a cookie
3167
- jar on one would put them in the same signed-in session. The id is also stable across launches,
3168
- where a display name may be re-worded — and a changed key silently signs the agent out of every
3169
- browser that used it.
3170
-
3171
- ### Task capabilities
3172
-
3173
- Task capabilities belong to each `Task`. If `hold` is omitted on one task, Omni
3174
- must not show or issue hold for that task even if another task from the same provider supports it.
3175
-
3176
- **A capability is a property of the task, and of nothing the task names.** A provider derives it
3177
- from no other source -- not the queue the task came from, not the login, not its own configuration
3178
- -- because each of those answers a question about this task from somewhere that does not know
3179
- about this task, and a client with two sources and a rule for choosing between them is the shape
3180
- that produces two consumers disagreeing about one fact. The task is the one source, and the
3181
- provider puts on the task what the platform permits for it -- and says, in `capabilitySource`, who
3182
- chose those terms. `queue` says somebody configured them. `nobody` says nothing handed the work
3183
- over: an agent's own outbound call has no queue behind it, and the provider states what it permits
3184
- for such a call. Neither changes where an agent application reads the capabilities from, which is the task; the
3185
- source is one more published fact about them, and the one that lets an agent application tell a fact from a fault
3186
- (see below).
3187
-
3188
- **Completing the task is not a capability, and no capability set withholds it.** `complete` is
3189
- governed by `completionMode` alone: under `agent-command` it is always available, whatever the set
3190
- says, and the `outcomes` capability decides only whether a code travels with it. Likewise
3191
- Answer on a pending task and Call on a preview are the phase's controls, not the set's. The
3192
- capability set governs what the agent may do *with* the task; completing it is never on the
3193
- list.
3194
-
3195
- **A permission that changes while the task is open is republished on the task at the moment it
3196
- changes.** An agent on a billing dispute may refund up to their own limit and no further. The
3197
- customer wants more, the agent asks for a lead, and the moment the lead joins the call the task is
3198
- republished with the Refund control the agent could not have a minute ago; when the lead leaves, it
3199
- is republished without it. Nothing about the agent changed -- who is on the call did -- and the
3200
- task said so both times, at the moment it became true. A call moves queues the same way: a caller
3201
- identified as a priority customer is transferred to the priority queue, and the task arrives under
3202
- that queue's terms -- a longer wrap allowance, a discount control the general queue never offered --
3203
- restated on the task at the hand-over, not inferred from the queue it left. A capability stated
3204
- once at offer or answer and never corrected is a fact with a shelf life and no expiry, and an agent application
3205
- draws a control the provider will now refuse -- or withholds one the agent now has. So the provider republishes
3206
- the task, with its capabilities as they now stand, and an agent application treats the last statement as current
3207
- rather than re-deriving anything. This is the same shape as a room left full after the call ends
3208
- -- a field describing the present, carried past the moment it stopped being true -- carried past
3209
- this time not by a spread but by nobody sending the correction. A provider whose platform can
3210
- change a permission mid-task and does not republish is in breach, however conformant its offer was.
3211
- A capability set can shrink as well as grow, and an agent application that has only ever seen it grow meets a
3212
- control that was there a moment ago and is gone; a command that arrives after its capability was
3213
- withdrawn is refused with a reason, never acted on. The asymmetry decides which direction a
3214
- provider gets right first: a control gained late is a nicety, a control withdrawn and not
3215
- republished is a button that fails when pressed. `assertTaskCapabilityWithdrawal` is the test for that
3216
- direction: the task as offered and as republished, and one command that was issuable under the
3217
- first and is refused under the last for want of the capability withdrawn -- and nothing else.
3218
-
3219
- **An empty capability set is a statement, not a shrug.** `capabilities: {}` under `queue` or
3220
- `nobody` says the platform permits nothing capability-gated on this task, and an agent application draws
3221
- nothing beyond what the phase and `completionMode` require. A provider that has not yet learned
3222
- what the platform permits -- a queue's configuration that has not reached it -- knows nothing of
3223
- the kind, and must not publish the task as if it did, neither as `{}` nor as every control it has.
3224
- It publishes the task under `capabilitySource: "not-yet-read"`, with the capabilities it will
3225
- honour until it knows, and an agent application shows that where the agent works, beside the controls it draws
3226
- from them: "no queue governs this call" is a fact, "the configuration has not arrived" is a fault,
3227
- and the set alone cannot tell them apart, so the provider says which. What a provider honours
3228
- under not-yet-read terms is its own call -- a floor that would rather an agent briefly hold a
3229
- control the platform might not have granted than lose hold or hang-up mid-call over a slow
3230
- configuration read publishes those -- and the protocol chooses no default set for it. The fault is
3231
- also reported as a `diagnostic` naming the task, one per occurrence, so an operator counts it. When
3232
- the terms arrive, the task is republished under `queue` with the set as it now stands: the same
3233
- republish as any other permission that changed while the task was open. The move goes one way.
3234
- Terms once read stay read: a re-read that fails mid-task is not a new fact about the task, so the
3235
- last statement stands and the failure is a `diagnostic`, and a task that was published under
3236
- `queue` or `nobody` never returns to `not-yet-read`, on an update (`stream.taskUpdated.capabilitySource`)
3237
- or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
3238
- entry the same two ways (`stream.taskUpdated.history`, `stream.snapshot.history`);
3239
- a snapshot carrying a task still at work does not forget the audio the stream held up, since audio
3240
- ends on `task-audio-ended` and the call moves on (`stream.snapshot.audio`); a voice task ends after
3241
- its audio ends, never around it, whatever the outcome (`stream.taskEnded.audioOpen`); and a task does not go
3242
- backwards, on an update or on a resync (`stream.taskUpdated.phase`, `stream.snapshot.phase`): the
3243
- stream sees publications, not
3244
- transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
3245
- with `confirmed` between them, and what is refused is a phase unreachable from the last one read by
3246
- the transition table -- back to `pending`, back to `confirmed` or `preview` once work began, out of
3247
- `completing` except by the party being dialled again, a connect-back or a platform's callback,
3248
- which the update itself shows: the party ringing, or joined by a dial whose answered outcome the
3249
- stream saw in this life. A completing task republished as `in-progress` from a stale copy carries
3250
- no such stage, and that is the ending the agent never saw. Audio arrives only on a task at work: `task-audio-started` on a `completing` task
3251
- is refused as it is on a pending one (`stream.taskAudioStarted.beforeWork`), since a connect-back
3252
- returns the task to `in-progress` before any audio. And a task completes after its audio ends,
3253
- never around it: an update moving a task to `completing` while the stream holds its audio as
3254
- started is refused (`stream.taskUpdated.audioOpen`), whoever caused the ending, and a task stating
3255
- `completing` with `audio: "started"` contradicts itself on any snapshot or update
3256
- (`task.audio.completing`). The consequence the rule exists for is concrete: the customer's audio
3257
- keeps playing through the agent's wrap-up.
3258
-
3259
- What the agent is told differs by source, and only one source tells them anything. Under `queue`
3260
- and `nobody` the agent sees controls and nothing about where they came from: both are facts,
3261
- and an agent working a call has no use for the name of the rule behind its buttons. Under
3262
- `not-yet-read` the agent is told, beside the controls, that these are what the provider will honour
3263
- until the queue's terms arrive, and that the controls may change when they do -- a statement about
3264
- the buttons in front of them now, not about the provider, because that is what changes when the
3265
- republish lands. It stays for as long as the set is provisional, beside the controls it qualifies;
3266
- a message that shows and clears has said nothing about the buttons still on the screen. The two
3267
- words are close in English and far apart on the desk: `nobody` is silence, `not-yet-read` is a
3268
- standing notice.
3269
-
3270
- One consequence for whoever builds the agent application: because a conformance run fails on `not-yet-read`, a
3271
- conformant adapter never shows an agent application `not-yet-read` under test, and a clean `exerciseAdapter`
3272
- result says nothing about how the agent application renders it. That rendering is tested against a fixture --
3273
- a task published under `not-yet-read` shows the notice, the same task under `queue` shows none --
3274
- and never against an adapter, even in principle. `exerciseAdapter` treats a
3275
- task published under `not-yet-read` as a violation (`capabilitySource.notYetRead`), as it treats
3276
- a diagnostic: a conformance run against a platform that cannot say what it permits fails loudly
3277
- rather than passing with a note.
3278
-
3279
- ```ts
3280
- const taskCapabilities = {
3281
- channel: "voice",
3282
- capabilities: {
3283
- hold: true,
3284
- outcomes: true,
3285
- },
3286
- browsers: [],
3287
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "browsers">;
3288
- ```
3289
-
3290
- A capability says the control may be offered; the task's phase says whether there is anything to use
3291
- it on. Every control below that acts on the call or the conversation -- everything but `decline`,
3292
- `connectBack`, `schedule` and `outcomes` -- is offered only while the task is `in-progress` or `paused`.
3293
- See **Which commands need a capability**.
3294
-
3295
- ### Voice capabilities
3296
-
3297
- | Capability | Omni UI | Contract |
3298
- | --- | --- | --- |
3299
- | `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
3300
- | `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
3301
- | `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**. |
3302
- | `terminateCall` | Primary button: Terminate call | The provider ends the whole call: every channel on it, the agent's, the caller's, any colleague's and anyone else's. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
3303
- | `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**. |
3304
- | `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the call or from its wrap: a time, and a note. See **Scheduling a follow-up**. |
3305
- | `leadAssist` | Secondary menu item: Lead assist | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.leadAssist`. See **Lead assist**. |
3306
- | `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and remove one person from it -- a conferenced entry, one still ringing included, which calls the dial off, or the party, leaving the agent with the colleague. See **Ending a call, and removing one person from it**. |
3307
- | `recording` | Overflow menu item: Recording | Per-task provider and agent application policies expose only their permitted recording actions; each has independent state and routing. |
3308
- | `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
3309
-
3310
- ### Publishing codes and destinations
3311
-
3312
- Two capabilities accept an object when the provider wants Omni to render real choices. For
3313
- `outcomes`, `true` remains valid and means "offer the control with nothing published"; the one
3314
- directory control, `conference`, carries its directory or is refused, since once nothing is typed
3315
- the directory is the control.
3316
-
3317
- #### `outcomes`
3318
-
3319
- An outcome describes what happened; completion finishes the task. Outcome codes are selected
3320
- from the task's own published list. They are distinct from `DialOutcome`, which reports the
3321
- result of a dial attempt.
3322
-
3323
- Migration requires providers and hosts to adopt the renamed types and fields together:
3324
-
3325
- | Former API | Current API |
3326
- | --- | --- |
3327
- | DispositionCode | `OutcomeCode` |
3328
- | DispositionRules | `OutcomeRules` |
3329
- | DispositionPayload | `OutcomePayload` |
3330
- | Task.capabilities.dispositions | `Task.capabilities.outcomes` |
3331
- | complete.disposition | `complete.outcome` |
3332
-
3333
- Validation and conformance diagnostics use `task.outcomes`, `task.outcome`, and
3334
- `command.complete.outcome` in place of the former names. Update retained task snapshots and
3335
- command producers as well as imports. Old capability and command fields are rejected, including
3336
- payloads containing both names; there are no compatibility aliases. Code IDs, notes, required
3337
- selection rules, and completion behavior are unchanged.
3338
-
3339
- ```ts
3340
- capabilities: {
3341
- outcomes: {
3342
- required: true,
3343
- notes: "optional",
3344
- codes: [
3345
- { id: "resolved", label: "Resolved" },
3346
- { id: "callback", label: "Callback needed", group: "Follow-up" },
3347
- ],
3348
- },
3349
- }
3350
- ```
3351
-
3352
- | Field | Contract |
3353
- | --- | --- |
3354
- | `required` | When `true`, Omni must collect a code before issuing `complete`. A required policy must publish at least one code. |
3355
- | `notes` | `required`, `optional`, or `none`; controls the free-text field beside the code. |
3356
- | `codes` | Codes Omni offers. `id` values are non-empty and unique; Omni sends the chosen `id` as `TaskCommand.complete.outcome`. |
3357
-
3358
- With `outcomes: true` Omni shows a Complete control and sends `complete` with no code, because
3359
- the provider published none.
3360
-
3361
- #### `conference`
3362
-
3363
- ```ts
3364
- capabilities: {
3365
- conference: {
3366
- destinations: [
3367
- { id: "tier2", label: "Tier 2 support" },
3368
- { id: "main-menu", label: "Back to the main menu" },
3369
- ],
3370
- },
3371
- }
3372
- ```
3373
-
3374
- | Field | Contract |
3375
- | --- | --- |
3376
- | `destinations` | The items the control offers, at least one, with unique `id` values. Each is a button or a menu item on the desk. |
3377
- | `id` | What Omni sends as `destinationId` on a `conference` command, and what the provider executes. |
3378
- | `label` | What the agent reads. |
3379
-
3380
- **The protocol does not say what an item does.** A queue, an IVR menu, an outside line, a
3381
- skill group -- that is the queue's configuration and the provider's business. The agent sees a
3382
- list of labels, picks one, and Omni sends its `id`; the provider executes whatever the item is. No
3383
- kind travels, nothing is typed, and a control with an empty directory has nothing to offer and is
3384
- refused (`task.destinations.offer`). A control declared as bare `true` is refused for the same
3385
- reason (`task.destinations.shape`): once nothing is typed, the directory is the control.
3386
-
3387
- **The provider chooses the supported destinations.** A published item may route to an IVR,
3388
- queue, named agent or another configured destination. No destination kind is inferred from its
3389
- label. The agent application offers only the directory attached to this task's `conference` capability,
3390
- and sends the exact destination ID; it does not invent an agent picker or arbitrary dial target.
3391
- The provider rechecks eligibility and executes the routing. Returning to an IVR or queue does not
3392
- end the caller journey; a later assignment to the same agent is a new interaction.
3393
-
3394
- #### Ending a call, and removing one person from it
3395
-
3396
- A call has everyone on `Task.onCall`, and three commands take people off it, all performed by the
3397
- provider. Two of them end the agent's part; they differ in what happens to the caller.
3398
-
3399
- ```ts
3400
- { type: "end-call" } // the agent's channel and the channels the agent added end; the caller continues
3401
- { type: "terminate-call" } // every channel on the call ends
3402
- { type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
3403
- { type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
3404
- ```
3405
-
3406
- **`end-call` ends my part.** The agent's channel and every channel the agent added -- a
3407
- conferenced colleague, a lead who joined -- end, and the caller continues on the path the
3408
- provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
3409
-
3410
- **`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
3411
- the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
3412
- line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
3413
- different thing from putting my own side down.
3414
-
3415
- Both are commands to the provider through `execute`, and nothing happens on the desk until the
3416
- provider reports it: the agent's audio ends on `task-audio-ended`, the task moves to `completing`,
3417
- any wrap allowance runs, and the agent completes. Neither is `task-ended`, and `complete` is
3418
- neither of them. Both capabilities are the queue's to grant and a level's to lock, and both may
3419
- change while the task is open: the provider republishes the task at the moment a permission
3420
- changes, and a command that arrives after its capability was withdrawn is refused, as **Task
3421
- capabilities** sets out. The provider knows which channels are the agent's and which were added;
3422
- the agent application sends the command and no channel list, and the provider rechecks ownership
3423
- when it acts.
3424
-
3425
- `conference` `remove` takes one person off, named as the room names them, and the call goes on
3426
- for the rest; it is gated by `conference`, since it only means something with a third person on
3427
- the line. Removing the party leaves the agent with the colleague, a warm hand-over in reverse. A
3428
- remove that would leave the agent alone is not a remove but an `end-call`, and a provider answers
3429
- it `failed`.
3430
-
3431
- **There is no transfer.** The agent hands the call to nobody, cold or warm: help arrives by
3432
- **Lead assist**, where a lead joins the call or takes it over, and a call that changes hands does
3433
- so on the platform's own routing -- an IVR return, a queue, a lead's take-over -- with the record
3434
- saying so in a `transferred` step and the `transfers` total. Nothing the agent commands moves the
3435
- caller to somebody else.
3436
-
3437
- #### Scheduling a follow-up
3438
-
3439
- An agent on a call promises to call back on Thursday, or in wrap writes up that the customer wants
3440
- a callback once the refund lands. The follow-up goes on the calendar, and the platform owns the
3441
- calendar: `schedule` asks the provider to put it there. A chat or an email agent promises a
3442
- callback as often, so the control is on every channel.
3443
-
3444
- ```ts
3445
- // From the call or the conversation, or from its wrap: the time, and a note where the agent wrote one.
3446
- { type: "schedule", at: "2026-08-28T10:00:00Z", note: "Call back about the refund" }
3447
- ```
3448
-
3449
- `schedule` is gated by the `schedule` capability and issuable in `in-progress`, `paused` or
3450
- `completing` (`command.phase.interaction`): a follow-up is promised on the call and written up in
3451
- wrap alike, and the task names the party it is for. It lands on the calendar, so a task may offer
3452
- it only under a manifest that declares the `calendar` idle capability
3453
- (`task.capability.calendar.required`), as a control that dials needs `dialOutcomes`; a locked
3454
- `schedule` is still a declared one. The command carries a time and a note and nothing else
3455
- (`command.schedule.at`, `command.schedule.note`, `command.field`): what the platform makes of a
3456
- follow-up -- a campaign record, a reminder, a scheduled dial -- is its own, and the agent sees it
3457
- on the calendar.
3458
-
3459
- **`applied` says the follow-up is on the calendar**, and the `calendar-updated` that shows it
3460
- follows within the manifest's `settleMs`, as a `task-ended` follows an applied `complete`. Past
3461
- the bound the agent application calls `snapshot()`: a calendar carrying an activity at that instant
3462
- clears the wait; one without it shows the follow-up as unsettled -- "Scheduled... the provider has
3463
- not confirmed" -- naming the command. The drive schedules one follow-up where the task offers it,
3464
- at a time it takes from the provider's own publication, and holds the provider to the same bound
3465
- (`drive.schedule.unsettled`).
3466
-
3467
- ### Chat capabilities
3468
-
3469
- | Capability | Omni UI | Contract |
3470
- | --- | --- | --- |
3471
- | `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when local policy also permits declining. |
3472
- | `hold` | Primary toggle: Hold | Omni may pause and resume the agent’s interaction in the chat. |
3473
- | `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the conversation or from its wrap. See **Scheduling a follow-up**. |
3474
- | `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
3475
-
3476
- ### Email capabilities
3477
-
3478
- | Capability | Omni UI | Contract |
3479
- | --- | --- | --- |
3480
- | `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when local policy also permits declining. |
3481
- | `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the message or from its wrap. See **Scheduling a follow-up**. |
3482
- | `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
3483
-
3484
- ### Custom capabilities
3485
-
3486
- Every task may publish additional provider-specific controls in `capabilities.custom`:
3487
-
3488
- ```ts
3489
- capabilities: {
3490
- hold: true,
3491
- custom: [
3492
- { id: "request-supervisor", ui: { control: "button", label: "Request supervisor", placement: "secondary" } },
3493
- { id: "mark-vip", ui: { control: "toggle", label: "Mark as VIP", placement: "overflow" } },
3494
- ],
3495
- }
3496
- ```
3497
-
3498
- Custom capability IDs must be non-empty and unique within the task. `ui.control` is `button`, `toggle`,
3499
- or `menu-item`; `ui.placement` is `primary`, `secondary`, or `overflow`. `ui.render` says where the
3500
- control's work appears: `inline`, in the workspace beside the task, or `page`, as a page of its own
3501
- — a tab in the same work area as the task's browsers, beside them, and alone on a task that has
3502
- none; stated on every control, never defaulted (`task.custom.ui.render`). `prompt.fields` are what the agent supplies before the action runs — a
3503
- destination number, a reference — as `CredentialField`s Omni renders as a form; the values travel
3504
- on the custom command under the fields' names, as strings, and Omni sends the command only once
3505
- every `required` field has a value. The provider validates what arrives as it validates any
3506
- command; a form is a declaration, not a contract for what the agent typed. Omni renders the
3507
- control and invokes it with the shared custom task command:
3508
-
3509
- ```ts
3510
- {
3511
- type: "custom",
3512
- name: "request-supervisor",
3513
- }
3514
- ```
3515
-
3516
- `name` is the `id` of the custom capability the agent used. There is no `assignmentId` here: like every
3517
- other command it travels on the `TaskCommandRequest` around it. A `toggle` carries the state it
3518
- wants — `{ type: "custom", name: "mark-vip", on: true }` — never a flip, for the reason under
3519
- **Task commands**.
3520
-
3521
- Custom capabilities must not redefine the meaning of a standard channel capability.
3522
-
3523
- ## Idle actions
3524
-
3525
- ### `dial(request)`
3526
-
3527
- Starts one outbound call from the idle dialpad. It is present only when the voice provider
3528
- declares `dial`.
3529
-
3530
- - `dialId` is the agent application's identity for this dial, minted before the request leaves.
3531
- - `destination` is the original number selected or entered by the agent.
3532
- - The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
3533
- number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
3534
- - `dialling` says the request was accepted and the call is being placed, and restates the
3535
- `dialId`. It says nothing about whether anyone will answer.
3536
- - `failed` contains a `ProtocolFailure` and confirms no call was placed.
3537
-
3538
- The resulting call is offered through the normal `task-offered` event, and how the dial ended
3539
- arrives on `dial-outcome` under the same `dialId`. A `dialling` result does not manufacture a task
3540
- inside Omni.
3541
-
3542
- ## Every dial has an outcome
3543
-
3544
- Four commands place a call: `dial()` from the idle dialpad, the `dial` command from preview, a
3545
- `conference` `add`, and `connect-back`. Each is accepted or refused at once, and each then ends later
3546
- and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
3547
- late in a call routinely outlives the call. Nothing in between is reported: the wire says
3548
- `dialling`, then says how it ended, once.
3549
-
3550
- **The agent application mints the identity.** Every command that dials carries a `dialId` the agent application made, unique
3551
- across the installation, and it travels four ways so no leg is a lookup:
3552
-
3553
- ```ts
3554
- declare const connection: Connection<"voice">;
3555
- declare const assignmentId: AssignmentId;
3556
-
3557
- // 1. Out, minted by the agent application.
3558
- const result = await connection.execute({ assignmentId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
3559
-
3560
- // 2. Back on the result, restated, so the agent application compares and confirms.
3561
- expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
3562
-
3563
- // 3. On the outcome, however late, against the task it belonged to.
3564
- const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", assignmentId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
3565
-
3566
- // 4. In the record, so a dial made before a take-over is placeable by whoever holds the task now.
3567
- const step: TaskHistoryStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
3568
- ```
3569
-
3570
- The identity is a correlation handle between agent application and provider and **is never shown to the
3571
- agent**. An agent application holding several tasks places an outcome by the `dialId` it minted; one it did not
3572
- mint -- a dial made by the previous agent before a take-over -- it finds on the task's `onCall` or in
3573
- its `history`, which is why the steps that dial carry it. The `dialId` is not a retry key:
3574
- Omni never repeats a dial, and a command still carries no key for that purpose.
3575
-
3576
- **`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
3577
- `{ status: "dialling", dialId }`, restating the agent application's identity (`result.dialId`,
3578
- `result.dialId.mismatch`); `applied` from such a command, or `dialling` from one that dialled
3579
- nothing, is refused (`result.status`). `validateResult(result, method, path, dialId)` takes the
3580
- `dialId` the agent application sent for that reason.
3581
-
3582
- **The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
3583
- there was one, the directory item it went to where there was one, and how the dial ended -- from a closed
3584
- set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `unexplained`.
3585
- `answered` is stated rather than read off somebody appearing on the call, because an agent application that infers
3586
- success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
3587
- before anyone answered -- the agent hanging up while a conferenced colleague still rings -- which is not a
3588
- `no-answer` the destination never gave. `unexplained` is the switch dropping the call with no cause among
3589
- those named here: a statement about the switch, sent instead of the nearest cause it did not give.
3590
- `reason` is the switch's own words, optional, and shown to the agent attributed to the switch rather
3591
- than to Omni. The two are different things and may travel together: a switch can say "could not
3592
- create dialog" in words and still name no cause a code would carry, so `unexplained` with a
3593
- `reason` is a dial the switch described but did not classify, and a desk renders the words without
3594
- inventing the class.
3595
-
3596
- **`assignmentId` is the task the dial was placed on**, resolved from the dial's own identity and never from
3597
- whatever task the agent is looking at when the outcome arrives. That task may already have ended,
3598
- and the harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
3599
- `stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached, against the
3600
- call it belonged to and never against the next one.
3601
-
3602
- The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which an agent application calls when it
3603
- places one; an entry on a task's `onCall`; and a step in its `history`. Since a dialled entry
3604
- is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- an
3605
- idle dialpad call, which has no task yet -- or whose entry has already left the room; an
3606
- outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
3607
-
3608
- **`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
3609
- claims, and both are true at once when a leg answers and never joins. A desk shows `answered` as the
3610
- dial's conclusion and says "on the call" only when the person appears on `Task.onCall`; the obvious
3611
- reading of `answered` -- that they are on the line -- is the one it does not make.
3612
-
3613
- **A provider says which outcomes it can tell apart.** No switch distinguishes all five, and a
3614
- provider that cannot tell busy from unreachable must not pick one. The manifest declares
3615
- `dialOutcomes`, and a `dial-outcome` carries only a declared member
3616
- (`event.dialOutcome.undeclared`). The list includes `answered`, or the provider could never state a
3617
- success (`manifest.dialOutcomes.answered`), and at least one other member, or it could never state
3618
- a failure (`manifest.dialOutcomes.failure`). `unexplained` is declarable only beside a cause the
3619
- provider does report -- `busy`, `no-answer`, `unreachable`, `rejected` or `cancelled`
3620
- (`manifest.dialOutcomes.unexplained.alone`): a provider that reports causes may honestly say the
3621
- switch gave none, while one that reports none would be sending the generic failure this set exists
3622
- to refuse. A provider that dials at all declares it: an idle
3623
- dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `connectBack`
3624
- or `conference` under a manifest without it is refused
3625
- (`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
3626
- error there.
3627
-
3628
- **Who is on the call is stated on the task.** `Task.onCall` lists everyone the task's audio joins
3629
- right now, replaced whole with the task like every other field, and present when the provider
3630
- knows the room:
3631
-
3632
- ```ts
3633
- const onCall: OnCall[] = [
3634
- { role: "party", since: "2026-08-21T09:12:00Z" },
3635
- { role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
3636
- { role: "conferenced", destinationId: "tier2", dialId: "dial-7f2", stage: "joined", held: true, since: "2026-08-21T09:15:30Z" },
3637
- { role: "conferenced", destinationId: "main-menu", dialId: "dial-3c9", stage: "ringing", since: "2026-08-21T09:16:02Z" },
3638
- ];
3639
- ```
3640
-
3641
- `party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
3642
- neither carries a destination. A `conferenced` colleague was, so the entry carries the
3643
- `destinationId` of the directory item and, where an agent application's dial brought them, its `dialId`; a person
3644
- the platform added itself carries none. `held: true` is presence as claim. A snapshot establishes state, so an agent application that attaches
3645
- after a take-over reads the room from here rather than inferring it from a sequence of outcomes it
3646
- never saw.
3647
-
3648
- **`onCall` describes this agent's current interaction.** When the caller disconnects but the agent
3649
- and added channels remain connected, the remaining room is still valid. When this interaction's
3650
- audio ends or the task enters `completing`, clear its `onCall` view; absence is used only by a
3651
- provider that never publishes the room. This does not assert that the caller, bridge or other
3652
- agents' channels ended. A published room receives a final empty view for this interaction, and the
3653
- task may remain open for wrap. Completion is a separate task action, not the caller's disconnect.
3654
-
3655
- **A field that describes the present is cleared by the transition that ends it.** `onCall`,
3656
- `previewEndsInSeconds` and `atDeadline` are three instances of one shape, and there will be more: each
3657
- describes a state that is true now, and each is carried past the moment it stops being true by the
3658
- most natural implementation there is, building the next task by spreading the last one. The
3659
- provider's own state looks right, the claim on the wire is stale, and only the agent application sees it. An
3660
- adapter that builds each transition from what is true after it, rather than from what was true
3661
- before, needs none of these rules named. Two agents were
3662
- once shown on calls for the better part of an hour while callers queued, because the last message
3663
- describing the room live was never followed by one saying it had emptied, and every consumer that
3664
- derived anything from it was wrong in a way that looked like its own bug.
3665
-
3666
- **A dialled entry is listed from the moment the dial is placed, not from the answer.** The entry is
3667
- what `conference` `remove` names, and a destination the agent cannot call off while it rings is a
3668
- customer kept waiting with no way back -- which is the very case `cancelled` exists for. The
3669
- `conferenced` entry appears with its `dialId` when the dial is placed, and a `conference` `remove`
3670
- naming a still-ringing `destinationId` calls that dial off, with `cancelled` as its outcome.
3671
-
3672
- **The entry says where it stands.** A dialled entry carries `stage`: `ringing` until its dial is
3673
- answered, `joined` after. The `dial-outcome` is the transition and the stage is the state, the same
3674
- pairing as `task-audio-started` and `audio`: an `answered` outcome and the task restated with the
3675
- entry `joined` say one thing twice, and any other outcome removes the entry. The outcome comes
3676
- first: a `task-updated` that itself moves an entry from `ringing` to `joined` before an `answered`
3677
- outcome for its dial is refused (`stream.taskUpdated.stage`), as an update that moves `audio` is.
3678
- The rule keys on the entry's `dialId`, so an entry the platform added itself, with none, is one
3679
- whose move the stream cannot hold to an outcome: a clean stream proves nothing about ordering there. It is stated rather
3680
- than left to whoever placed the dial because that knowledge lives in one process: after a reload,
3681
- on a second session, or on a supervisor's screen the snapshot is the only source, and a room that
3682
- cannot tell ringing from joined says it contains someone who may never arrive. Nobody ringing is
3683
- held (`task.onCall.held.ringing`). A colleague who never answers is a dial that ended: the
3684
- provider restates the room without the entry, and the agent is back with the customer.
3685
-
3686
- The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
3687
- they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
3688
- belonged to -- with no retry, since a wrong number is not worth redialling and the decision is the
3689
- agent's.
3690
-
3691
- ## Breaks
3692
-
3693
- Everything about an agent's breaks on one provider arrives as one object, `Snapshot.break`,
3694
- replaced whole by a single `break-state` event. These facts are only meaningful together — an
3695
- approval says nothing without knowing whether the agent chose the break, and a list of reasons says
3696
- nothing while none are being accepted — so they are not published separately.
3697
-
3698
- | Field | Contract |
3699
- | --- | --- |
3700
- | `status` | Where the agent’s break lifecycle stands. See the states below. |
3701
- | `canRequestBreak` | Whether the agent may ask at all. Distinct from `status`. |
3702
- | `requestUnavailableReason` | Display-ready reason shown when `canRequestBreak` is false — a standing gate that applies to everyone. |
3703
- | `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
3704
- | `retryRequestAfterMs` | Milliseconds until the agent may retry a break request, when the provider can say. |
3705
- | `reasons` | Not-ready codes this provider offers. Omitted when it defines none; an empty list is refused, being a second spelling of the same fact. |
3706
- | `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break. Required on a break `on-break` or `starting-after-task` where the provider publishes `reasons`, a forced break included (`break.activeReasonId.required`): a break with a kind the provider cannot name is a break whose rules nobody can apply. |
3707
- | `forced` | Set when the break was forced on the agent rather than requested. |
3708
-
3709
- A request can be waiting for two unrelated things, and they are separate values because
3710
- rendering one as the other tells an agent to wait for somebody who is never coming:
3711
-
3712
- | `status` | Meaning |
3713
- | --- | --- |
3714
- | `not-requested` | No request outstanding. |
3715
- | `awaiting-approval` | A person has to decide. The agent is waiting on somebody. |
3716
- | `granted` | The request was granted, by a person or by an `automatically-approved` policy. 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. |
3717
- | `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`. |
3718
- | `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**. |
3719
-
3720
- A denial is a decision, not a standing approval state. The provider transitions the request directly
3721
- to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
3722
- answer and ask again when they want to. `decisionReason` may carry the words attached to that
3723
- decision, but `status` does not remain denied.
3724
-
3725
- A provider reports `starting-after-task` only after Omni commits a `granted` request while
3726
- work is still active. Omni does not send the request again, because asking again would not move
3727
- it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
3728
-
3729
- `canRequestBreak: false` is what lets Omni withdraw the control rather than let an agent ask and be
3730
- refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
3731
- something a busy hour can cancel, and Omni keeps offering those while the rest are withdrawn.
3732
-
3733
- ### Forced breaks
3734
-
3735
- Migration: the former awaiting-decision value is now `awaiting-approval` in break status
3736
- and the team member’s pending break state. The old value is rejected without an alias;
3737
- the request can still be granted or denied, and ordering rules are unchanged.
3738
-
3739
- Migration: the former BreakApproval type is now `BreakStatus`, matching `BreakState.status`.
3740
- The former type is not exported as an alias; its values and behavior are unchanged.
3741
-
3742
- Migration: `BreakState.status` replaces the former approval field. It covers the full break
3743
- lifecycle. The old field is rejected even alongside status; related diagnostics now use status.
3744
- The `BreakStatus` union and its values, `reasons`, `activeReasonId`, and the separate testing
3745
- helper BreakOnTaskStep.approval are unchanged. Hosts and providers must update together.
3746
-
3747
- Migration: the break-state field formerly named retryAfterMs is now `BreakState.retryRequestAfterMs`,
3748
- with diagnostic `break.retryRequestAfterMs`. It remains an optional non-negative finite number
3749
- of milliseconds; the former break field is rejected even alongside the new field.
3750
- The separate authentication and general failure retryAfterMs fields are unchanged.
3751
-
3752
- Migration: the former in-effect break approval value is now `on-break`. It means the
3753
- agent is on the break, not merely granted permission. The old value is rejected without an
3754
- alias. The task prerequisite, the take-over exception and ordering rules are unchanged;
3755
- the task-conflict diagnostic is `break.on-break.tasks`. Team member availability already uses
3756
- `on-break` and is unchanged.
3757
-
3758
- Migration: the former refusedReason field is now `BreakState.requestUnavailableReason`.
3759
- It explains why requests are unavailable when `canRequestBreak` is false; `decisionReason`
3760
- continues to explain a particular approval or denial. The old field is rejected even alongside
3761
- the new one, and related diagnostics use requestUnavailableReason.
3762
-
3763
- Migration: the former mayAsk field is now `BreakState.canRequestBreak`. The old field is
3764
- rejected even beside the new field. Validation diagnostics use canRequestBreak in place of
3765
- mayAsk. Request eligibility remains distinct from approval; existing alwaysAvailable reason
3766
- exceptions are unchanged. Hosts and providers must update together.
3767
-
3768
- Migration: ImposedBreak is now `ForcedBreak`, and the former imposed field is now
3769
- `BreakState.forced`. Diagnostic and harness coverage names use `break.forced`. The former agent-end prohibition
3770
- has been removed: forced breaks also permit explicit agent resumption. Update hosts and providers together. The old field
3771
- is rejected, including when both spellings are sent; no compatibility alias is provided.
3772
- The break policy value formerly named auto-approve is now `automatically-approved`.
3773
- Requests are approved automatically; approval does not skip commitment or the prerequisites
3774
- for starting a break. The former value is rejected without an alias.
3775
- The break policy value formerly named suspended is now `requests-suspended`. The former
3776
- value is rejected without an alias. It suspends new requests, not breaks already underway.
3777
- The break policy value formerly named ask is now `approval-required`: requests require
3778
- approval. The former value is rejected without an alias; automatically-approved and requests-suspended retain
3779
- their existing behavior.
3780
- The team break command formerly named policy is now `set-break-policy`. The former
3781
- command is rejected without an alias; its policy field is unchanged. Hosts and providers must adopt the new name together.
3782
- The team break command formerly named decide is now `decide-break-request`. The former
3783
- spelling is rejected without an alias; the granted/denied decisions and pending-request
3784
- prerequisite are unchanged.
3785
- The team break command formerly named place is now `force-break`. Hosts and providers must
3786
- adopt the new command together; place and the interim force spelling are rejected without aliases.
3787
- The team break command formerly named release is now `end-forced-break`, and its diagnostic is
3788
- `team.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
3789
- The agent’s `endBreak` now applies to forced breaks as well as requested breaks.
3790
-
3791
- `ForcedBreak` names who forced the break and may say how long it is expected to run yet, as
3792
- `expectedEndsInSeconds`: whole seconds from the publication that carries it, restated on every
3793
- publication, so every screen -- the agent's, reloaded or not, and every lead's -- counts down the
3794
- same number from receipt (`break.forced.expectedEndsInSeconds`). The duration the lead asked for
3795
- travels on the `force-break` command alone; the state says what is left, and drops the field once
3796
- it has run out. The agent must explicitly resume when ready. The retired endsAutomatically and
3797
- endsAt fields are rejected, including when an expectation is also provided. Who forced it is
3798
- `by`, as **A forced break is the provider's act** below sets out.
3799
-
3800
- Omni resolves the name to show with `getUserDetails()`, so a provider sends the identifier and never
3801
- a display name.
3802
-
3803
- **A forced break travels with `on-break` or `starting-after-task`, and nothing else.** It is a
3804
- break in progress or about to be; beside `granted` or `awaiting-approval` the agent application would read a
3805
- request the agent never made and commit it (`break.forced.status`). Where the provider publishes
3806
- `reasons`, the lead's `force-break` named one, and the member's state carries it as `activeReasonId`.
3807
-
3808
- For example:
3809
-
3810
- ```ts
3811
- forced: {
3812
- by: "manager-1042",
3813
- expectedEndsInSeconds: 600
3814
- }
3815
- ```
3816
-
3817
- The agent application keeps **Resume** available for an agent on a forced break. The agent explicitly chooses
3818
- Resume; the agent application sends `endBreak()` and follows the provider-confirmed state before resuming
3819
- work. The expectation is whole seconds still to run, restated on every publication; omission
3820
- means none was stated, or it has run out. Neither its reaching zero nor an overdue indication
3821
- authorizes the agent application or provider to end the break, restore availability, or route
3822
- work to the agent.
3823
-
3824
- **A forced break is the provider's act.** A lead's `force-break` is a command to the provider,
3825
- which forces the break on the member; `by` names the lead who asked it to. A break the platform
3826
- imposed on its own -- a schedule, a compliance hold -- says `by: "provider"`, and is the platform's
3827
- to lift: a lead's `end-forced-break` on it is refused before it is sent
3828
- (`team.command.endForcedBreak.provider`), and a provider that receives one answers `failed` with
3829
- `omni.break-forced-by-provider`. The desk offers End forced break on a member only where
3830
- `forced.by` names a lead.
3831
-
3832
- **A lead lifting the restriction does not resume the agent.** On an applied
3833
- `end-forced-break`, the provider clears `BreakState.forced` while preserving the current
3834
- `on-break` or `starting-after-task` status and `activeReasonId`. This command cannot publish
3835
- `not-requested`, restore readiness, or start routing work. Ordinary progress from
3836
- `starting-after-task` to `on-break` still happens when work finishes. The agent application follows the
3837
- published state and waits for the agent's explicit Resume, even after reconnect. Historical
3838
- attribution is not rewritten by clearing the current forced marker.
3839
-
3840
- The agent may explicitly resume either before or after the lead lifts the restriction. A lead
3841
- command, timer, late snapshot or reconnect is never a substitute for that choice. Providers
3842
- must correlate resumption to the authenticated agent's action; a state-only ordering validator
3843
- cannot establish the cause of a transition. The usual multi-provider end/reconciliation flow
3844
- still applies, and work resumes only on confirmed provider state.
3845
-
3846
- An agent application counts `expectedEndsInSeconds` down from receipt, on every publication that
3847
- carries it, and shows nothing of it where the provider sends none. It invents no start and no
3848
- return time, and nothing acts on the number reaching zero.
3849
-
3850
- A break applies to the **agent**, not to one provider. When a provider forces one, Omni stops the
3851
- agent everywhere else at once, and not by asking: it states `setCapacity({ count: 0 })` on every
3852
- other usable provider holding capacity, which is taken and never refused, and which their team
3853
- lists show as `elsewhere`. Nobody on those providers can deny it, and the agent cannot cancel it,
3854
- because there is nothing to cancel: a break request on them would be the agent's own to withdraw
3855
- and a lead's to refuse, and a forced break is neither. When the forcing provider ends the forced
3856
- break and the agent resumes there, the agent application restates its capacity on the rest. A
3857
- provider whose login is not usable is left alone, as a break attempt leaves it;
3858
- `assertForcedBreakStopsTheRest` holds an agent application to this set. **The provider keeps its
3859
- shift totals true.** A member held elsewhere is one the provider sees -- `elsewhere` is a state it
3860
- publishes -- and the day's `shift` is its own account of the day, so the time the agent was
3861
- stopped for a break forced on another provider is the provider's to count, as a break of a kind
3862
- its platform has, or introduces, for exactly this. A `breakSeconds` that leaves it out is a total
3863
- the provider knew to be short, and **Never report a value you cannot observe** cuts the other way
3864
- here: the provider observed it.
3865
-
3866
- `ForcedBreak.by` says who forced it: a lead, by user id, or `provider` where the platform itself
3867
- did -- on its own rule, a schedule, a compliance hold. The agent sees who, and `getUserDetails()`
3868
- is owed for a person, never for `provider` (`break.forced.by`).
3869
-
3870
- ## Capacity and break actions
3871
-
3872
- **Each method answers in its own words.** A result is read by a person far more often than it is
3873
- branched on by code — in a log, a support ticket, a conformance failure — so it says what happened
3874
- rather than that something happened. `failed` is shared, because failing is the same act
3875
- everywhere; success is not.
3876
-
3877
- | Method | Succeeded |
3878
- | --- | --- |
3879
- | `setCapacity` | `applied` |
3880
- | `recordStep` | `recorded` |
3881
- | `requestBreak` | `requested` |
3882
- | `commitBreak` | `committed` |
3883
- | `cancelBreak` | `cancelled` |
3884
- | `endBreak` | `ended` |
3885
-
3886
- `failed` carries a typed `ProtocolFailure` and means the provider did not take the action, whether
3887
- it would not or could not.
3888
-
3889
- **Succeeding is not the outcome.** `requested` says the provider holds the request, not that a
3890
- break was granted; `ended` says the provider has the instruction, not that the agent is working
3891
- again.
3892
- Every break method reports its real result through `break-state`; `setCapacity` reports none at
3893
- all, because capacity is a statement rather than a request.
3894
-
3895
- `execute` keeps `applied` rather than a verb per command, because the command
3896
- is in the request: `execute({ command: { type: "hold" } })` returning `applied` already says the
3897
- hold applied. A `held` result would repeat the discriminant that travelled with it.
3898
-
3899
- ### `setCapacity(capacity)`
3900
-
3901
- States how many tasks this provider may have assigned to the agent **at once**.
3902
-
3903
- `count` is an absolute ceiling, not an increment, a whole number of zero or more
3904
- (`capacity.count`). An agent's capacity is a property of the agent, not of the moment: it is
3905
- stated when the agent is set up and restated only when it genuinely changes, which is a
3906
- local policy change rather than a task starting or ending -- with one exception below.
3907
-
3908
- **The provider counts its own outstanding tasks against it.** Assign while you hold fewer than
3909
- `count` tasks for this agent, and stop when you hold that many; when one of yours ends you have
3910
- room again and need no new signal to know it. Omni does not re-state capacity as tasks come and
3911
- go, and a provider that waits for it will stall.
3912
-
3913
- Your own tasks are the only ones you count. What the agent holds at other providers is not your
3914
- concern — Omni set `count` knowing it, and this is how: the agent is one person on several
3915
- providers, and the agent application divides their capacity among them rather than telling each the whole. A
3916
- provider that has none of it for now is told **`count: 0`, agent application-stopped**: assign nothing, show
3917
- the member as `elsewhere` on the team member list -- signed in here, capacity held by the agent application for elsewhere,
3918
- a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
3919
- count as any other when the agent application has capacity for this provider again. Zero is the one restatement
3920
- that follows work rather than local policy.
3921
-
3922
- **A capacity is taken, never refused.** `setCapacity` answers `applied` and nothing else: a
3923
- statement has no failure to report, and a `failed` arm gave four agent application-provider pairings four
3924
- readings of which ceiling stood after it. A provider that cannot carry the count assigns within
3925
- what it can and says so on a `diagnostic`, so the gap is visible and nothing is inferred from a
3926
- refusal.
3927
-
3928
- Capacity supersedes rather than accumulates: the latest value is the ceiling, and a decrease is as
3929
- ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
3930
- it was ever told, or returns early on a small one -- cannot be told to take less work, and an agent application
3931
- taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
3932
- both ways after the drive, two then one then nought, and an offer after a lower count is caught
3933
- against it (`stream.taskOffered.overCapacity`).
3934
-
3935
- **Capacity gates what the provider assigns, not what the agent starts.** A call placed from the
3936
- idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
3937
- it: the ceiling binds assignment, not the agent's own hand.
3938
-
3939
- ### Runtime break prerequisite checks
3940
-
3941
- Use `validateBreakCommand(method, request, state, context)` from the validation entry point
3942
- before any agent break dispatch. The context supplies current authentication and transport;
3943
- all four methods require the live login's break capability and active transport. Pass the
3944
- request object only to the request method, and undefined to the other three.
3945
-
3946
- | Method | Required current state |
3947
- | --- | --- |
3948
- | `requestBreak` | `not-requested`; selected current reason code when codes exist; `canRequestBreak` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
3949
- | `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
3950
- | `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
3951
- | `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
3952
-
3953
- Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
3954
- decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
3955
- the live `lead` flag and active transport, a current team member list for any command naming a
3956
- member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
3957
- awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
3958
- forced committed break; join and decline need the member's request on the list; coach, join-call
3959
- and leave need the lead already on that member's call; an assignment named must be one the list
3960
- shows the member holding. The context's target state must belong to the named member; that
3961
- association and backend authorization are provider responsibilities.
3962
-
3963
- Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
3964
- as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
3965
- checks. Pending and completing tasks still count as work. Only a call a lead took over
3966
- (`takenOver`) sits beside a break in effect. Validate full task shapes and the envelope separately.
3967
-
3968
- These validators report violations without dispatching, rewriting state or proving freshness.
3969
- Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
3970
- Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
3971
- is not a grant, and no method response overwrites a newer state emission. Rejected promises
3972
- remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
3973
- durable mutually exclusive commit/cancel decision and reconciliation obligations under
3974
- **Coordinating a multi-provider break** remain
3975
- required; per-provider validation does not implement the multi-provider coordinator.
3976
-
3977
- ### `requestBreak(request)`
3978
-
3979
- **Explanatory text is optional.** An agent may omit `BreakRequest.reason` for any break type.
3980
- A lead may likewise omit reason text from `force-break`. Do not require a text explanation
3981
- just because a break is agent-requested or forced. If supplied, text must be nonempty; omit
3982
- it rather than sending an empty string. A selected `reasonId` identifies a published break
3983
- choice and is separate from this optional text.
3984
-
3985
- With published choices, `{ reasonId: "lunch" }` is a valid request without explanation;
3986
- without published choices, `{}` is valid. A lead can similarly send a `force-break` command
3987
- with its member ID and, where required, the selected reason ID, without reason text.
3988
-
3989
- Requests permission to stop the agent later; it does not itself stop work. The provider continues
3990
- offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
3991
- provider reports `not-requested` directly, with `decisionReason` when one was supplied.
3992
-
3993
- **An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
3994
- workspace as it does on the idle dashboard. Asked on a task, the break is decided and committed like
3995
- any other and begins when the work ends: that is `starting-after-task`, and
3996
- `assertBreakBeginsAfterTask` is the scenario that holds a provider to it.
3997
-
3998
- #### Break reasons
3999
-
4000
- A provider that defines not-ready reason codes publishes them on `Snapshot.break.reasons`,
4001
- and Omni returns the agent's choice as `BreakRequest.reasonId`:
4002
-
4003
- ```ts
4004
- break: {
4005
- reasons: [
4006
- { id: "lunch", label: "Lunch" },
4007
- { id: "training", label: "Training", group: "Scheduled" },
4008
- ]
4009
- }
4010
- ```
4011
-
4012
- Reason ids are non-empty and unique within the provider. They live on the snapshot rather than the
4013
- manifest because a provider may change the codes it offers during a shift; a `snapshot` event
4014
- replaces the list. `BreakRequest.reason` remains available for free text and is never a substitute
4015
- for `reasonId` when the provider publishes codes. A provider that defines no codes omits the
4016
- field.
4017
-
4018
- #### One break across several providers
4019
-
4020
- An agent connected to several providers takes **one** break, not one per platform. Omni gathers
4021
- what every connected provider offers, shows the distinct breaks, and on a choice sends one request
4022
- per provider: the same `reason`, and each provider's own `reasonId`.
4023
-
4024
- To do that, Omni has to know when two providers mean the same break. Say so with `kind`:
4025
-
4026
- ```ts
4027
- break: {
4028
- reasons: [
4029
- { id: "lunch", label: "Lunch", kind: "meal" },
4030
- { id: "rest", label: "Mandatory rest period", kind: "rest", alwaysAvailable: true },
4031
- ]
4032
- }
4033
- ```
4034
-
4035
- A provider decides which breaks it offers and what it calls them. `BREAK_KINDS` is the list Omni
4036
- supports mapping them onto, and every member means something specific:
4037
-
4038
- | Kind | What it means |
4039
- | --- | --- |
4040
- | `short-break` | A brief rest between contacts — the comfort break a shift plan allows for. |
4041
- | `meal` | A meal: lunch, dinner, whatever the shift calls it. |
4042
- | `rest` | A rest period the agent is entitled to and a busy hour cannot cancel. Usually the reason also marked `alwaysAvailable`, though the two are separate: this says what the break *is*, that says whether policy can withdraw it. |
4043
- | `training` | Learning something the agent is expected to know afterwards: a course, e-learning, a product walkthrough. However it is delivered, and whoever attends. |
4044
- | `coaching` | Reviewing this agent's own work with somebody accountable for it: a call listened back, quality feedback, a one-to-one about their interactions. Even where the outcome is that they learn something. |
4045
- | `meeting` | A scheduled gathering that is neither — a team huddle, a project call, a town hall. The agent attends and contributes; nobody is assessing their work and there is nothing they must know by the end. |
4046
- | `administrative` | Paperwork and follow-up not attached to a particular contact. |
4047
- | `technical` | Equipment or system trouble stopping the agent taking work: a dead headset, a phone that never registered, a tool that will not load. The one member that is not an activity — it says why the agent *cannot* work rather than what they are doing, which makes it the right home for a not-ready state raised about the agent's equipment. |
4048
- | `personal` | Personal time the deployment does not classify further. |
4049
- | `other` | None of the above. Matches nothing, including another provider's `other`. |
4050
-
4051
- **The definitions are the point, not decoration.** Ten undefined strings would be the label
4052
- problem one level up: two providers could both declare `technical`, one meaning a dead headset
4053
- and the other scheduled maintenance, match on it, and nothing could tell the difference.
4054
-
4055
- `training`, `coaching` and `meeting` can all fit one session, so take them in that order of
4056
- specificity: about this agent's own work is `coaching`, else something they must know afterwards is
4057
- `training`, else `meeting`.
4058
-
4059
- **A break somebody forced on the agent is not automatically one of these.** Where the agent was
4060
- stopped rather than choosing to stop — see **Forced breaks** — set `BreakState.forced` and prefer
4061
- omitting `kind` to reaching for `other`. None of the ten describes "something was done to this
4062
- agent", and `other` claims a classification that was never made.
4063
-
4064
- Omni matches in this order:
4065
-
4066
- 1. **`kind`**, where both providers declare one. This wins over the label, so `{ id: "MEAL",
4067
- label: "Meal", kind: "meal" }` lines up with `{ id: "lunch", label: "Lunch", kind: "meal" }`
4068
- and nobody has to word their codes the same way.
4069
- 2. **The label**, folded for case and surrounding spacing, where a kind is missing. `"other"`
4070
- counts as missing: two providers saying `"other"` have only said their break is not on the
4071
- list, which is no evidence they mean the same thing.
4072
-
4073
- Nothing else is matched. A break Omni cannot pair stays on its own, the agent is told how many
4074
- platforms their break will actually reach, and they can pair the odd one by hand — Omni remembers
4075
- that for next time.
4076
-
4077
- Three rules for a provider:
4078
-
4079
- - **Declare `kind` where you can, and leave it out where you cannot.** Leaving it out costs a
4080
- little precision. Getting it wrong sends an agent to lunch on one platform and to training on
4081
- another, and nothing can detect that.
4082
- - **You may publish several reasons of one kind** — two meal slots, say. Omni will not choose
4083
- between them for the agent; saying they are the same kind is not saying they are interchangeable.
4084
- - **Do not read `reason` as your own label.** It is the agent's single choice, in whichever
4085
- provider's wording they picked it from, sent unchanged to every provider in the break.
4086
-
4087
- A provider that publishes no reason codes still receives the request, with `reason` set and
4088
- `reasonId` omitted.
4089
-
4090
- #### Coordinating a multi-provider break
4091
-
4092
- Sending the requests is a two-phase operation, not independent best effort. A provider first grants
4093
- permission for the agent to stop, then Omni either commits or cancels that permission.
4094
-
4095
- `granted` is provider-visible state. It means only that this provider has granted the active
4096
- request and is ready to stop the agent when Omni asks. It does not reveal that other providers exist
4097
- or why Omni has not committed yet. While reporting `granted`, the provider remains available,
4098
- continues to honour Omni's current capacity, and continues offering work normally.
4099
-
4100
- Omni separately tracks the aggregate agent application states `working`, `requesting-break`, `committing-break`,
4101
- `cancelling-break`, and `on-break`. While `requesting-break`, Omni shows which providers remain
4102
- outstanding and offers **Cancel break request**, but does not tell the agent that their break has
4103
- begun.
4104
-
4105
- Omni offers the aggregate Break control only when every provider currently holding capacity
4106
- declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
4107
- knowingly permit partial availability.
4108
-
4109
- Omni coordinates one break attempt as follows:
4110
-
4111
- 1. Freeze the providers the attempt asks: every connected provider from which the agent can
4112
- currently receive work. A provider joining during the attempt is given no capacity until it
4113
- finishes. A provider whose authentication is `expired` is not one the agent can receive work
4114
- from and is not asked: nothing is asked of it, the break proceeds without it, and when the
4115
- login is restored it is reconciled from its snapshot as a set-aside provider is, with no
4116
- capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
4117
- available and work continues. `assertBreakAttemptProviders` holds an agent application to this set.
4118
- 2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
4119
- 3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
4120
- `granted`; neither state stops work. A denial transitions directly to `not-requested` and
4121
- causes Omni to take the cancel path.
4122
- 4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
4123
- and send `commitBreak()` to each of them. A provider then stops offering new work
4124
- and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
4125
- decision, tunable per deployment — decides who is kept: a provider that has not applied the
4126
- commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
4127
- begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
4128
- reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
4129
- and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
4130
- settled and beginning when the task ends, and offers no cancel, because the commit is durable.
4131
- 5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
4132
- decision timeout, or the agent cancels before commit, durably choose cancel and enter
4133
- `cancelling-break`. Send `cancelBreak()` to every provider still reporting
4134
- `awaiting-approval` or `granted`. Work continues during cancellation because no stop was
4135
- committed. Return to `working` only after no provider retains either state.
4136
-
4137
- Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
4138
- it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
4139
- A provider that reports `granted` must therefore preserve the request across reconnects and must
4140
- honour a later commit or cancel. This
4141
- durable promise prevents a provider from failing the commit after another provider has already
4142
- stopped the agent.
4143
-
4144
- #### Why the commit phase is bounded and the decision is not
4145
-
4146
- Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
4147
- durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
4148
- or is uninstalled between granting and committing would hold Omni in `committing-break` with no
4149
- exit: the providers that did commit have already stopped the agent, and the agent is neither
4150
- working nor on a break.
4151
-
4152
- Unanimity is required for a reason that survives the bound. It exists so the agent is not stopped
4153
- on one platform while another keeps routing work to them — and **a provider Omni cannot reach is
4154
- routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
4155
- for it costs the agent their break.
4156
-
4157
- Setting a provider aside is not a rollback and not a cancel. The commit stands and the
4158
- obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
4159
- snapshot before anything else, and the snapshot decides:
4160
-
4161
- - `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
4162
- - still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
4163
- late behind it, the provider stops an agent who is already stopped; nothing happens, because a
4164
- commit is a state to be in, not an act to be done.
4165
- - `not-requested` — the grant did not survive. Omni makes a new request for that provider alone,
4166
- against an agent who is already on break elsewhere. **A new login is this case too**: the grant
4167
- belonged to the old session.
4168
-
4169
- The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
4170
- no capacity until it is reconciled.
4171
-
4172
- **If the agent has already ended the break elsewhere, the break attempt is over** and the returning
4173
- provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
4174
- would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
4175
- Never rolling back is about a break that is still on, not one the agent has finished.
4176
-
4177
- Omni may tell the agent which platforms the break has not yet reached, as it already does when a
4178
- break cannot be paired across every provider.
4179
-
4180
- The decision phase needs no such bound, because nothing has stopped. Work continues throughout
4181
- `requesting-break`, so a provider that never answers costs the agent a wait rather than their
4182
- availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
4183
- because no stop was ever committed.
4184
-
4185
- If cancel races with a late approval, the provider remains on the cancel path. If commit has already
4186
- won, cancel returns `omni.break-already-committed`, and Omni resumes commit recovery rather than
4187
- returning the agent to `working`.
4188
-
4189
- A forced break is not rolled back by this algorithm. If one appears during either the request or
4190
- cancellation, the attempt is abandoned: Omni cancels every grant still outstanding and states
4191
- `setCapacity({ count: 0 })` on every other usable provider, as **Forced breaks** sets out; no grant
4192
- is committed, because the agent has already stopped everywhere.
4193
-
4194
- This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
4195
- the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
4196
-
4197
- #### Reporting the break the agent is on
4198
-
4199
- `BreakState.activeReasonId` is the `BreakReason.id` the current break is on. Omni remembers what it
4200
- asked for, but only until the session ends — after a reload or reconnect, or where the provider put
4201
- the agent on the break itself, the provider is the only one who knows.
4202
-
4203
- Omit it when you cannot say, and when there is no break: reporting a reason alongside
4204
- `status: "not-requested"` describes a break that is not happening, and is rejected.
4205
-
4206
- ### `cancelBreak()`
4207
-
4208
- Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
4209
- Cancellation releases the request but does not restore work because work never stopped. If
4210
- commit already won, the provider returns `omni.break-already-committed`. The resulting state is
4211
- reported through `break-state`.
4212
-
4213
- ### `commitBreak()`
4214
-
4215
- Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
4216
- business reason. On commit the provider stops offering new work and reports
4217
- `starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
4218
- Committing a break that is already in effect changes nothing and answers `committed`.
4219
-
4220
- ### `endBreak()`
4221
-
4222
- Tells a provider that an agent already on a break wants to become available again. The provider
4223
- reports the resulting state through `break-state` and provider status events.
4224
-
4225
- It ends the break, which is the thing that started. Nothing about the connection was ever paused,
4226
- so there is nothing on it to resume.
4227
-
4228
- ## Team leads
4229
-
4230
- The team feature is the lead's own surface, beside the agent's and never mixed with it. The
4231
- login's `lead` flag turns it on in the agent application; the lead retrieves their members and
4232
- sees, for each, their assignments as they stand and the full history of each, and acts on them
4233
- with the authority the flag carries. Nothing the lead does here is a task on their desk. A lead
4234
- who also takes calls is an agent like any other on the agent side, and a lead who does not want
4235
- the agent role for now places themself on a break of a working kind and keeps the team feature on.
4236
-
4237
- The team reaches the lead whole once, on `team-updated` after the lead switches the feature on
4238
- and on every snapshot while it is on, and from then on one member at a time: when a member is
4239
- offered a call, answers, holds, mutes or is taken over, the provider publishes that member whole
4240
- on `team-member-updated`, so the lead's screen moves when the member's does and a team of two
4241
- hundred is never resent because one of them held. The agent application asks for nothing and
4242
- computes nothing. See **`team-updated`** and its companions.
4243
-
4244
- | Field | Contract |
4245
- | --- | --- |
4246
- | `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. |
4247
- | `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**. |
4248
-
4249
- | `TeamMember` field | Contract |
4250
- | --- | --- |
4251
- | `id` | Required `UserId`. A task carries no names and neither does a team member list: Omni resolves what to display with `getUserDetails()`. |
4252
- | `availability` | Required. What the member is doing now. |
4253
- | `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
4254
- | `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
4255
- | `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, less the workspace. The assignment, title, task type, phase, party, room, audio and completion terms -- `completionMode`, `wrapAllowance`, `wrapEndsInSeconds` -- are what the lead reads, and the task's record -- `history`, steps and totals -- travels whole, exactly as the member's desk holds it, so the lead's screen and the member's show the same record. The workspace -- `capabilities`, `capabilitySource`, `browsers` and their URLs -- is the member's desk's alone and never travels (`team.member.task.workspace`). The party's number and email are shown to leads or locked as the provider decides for leads, apart from what it decides for the member -- see **What the queue locks is locked on the whole task**. `[]` when the member holds none; omitted only where the provider cannot see them. |
4256
- | `listening` | Every lead on this member's call — listening, coaching or joined — published to every lead alike: each entry names the lead by `leadId`, which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. The entry naming the signed-in lead is what their own audio opens on. Absent while nobody is on the call, never `[]` (`team.member.listening.*`); a lead is on one member's call at a time (`team.listening.single`), and a lead who is heard is also in the task's `onCall` (`team.member.listening.heard`). |
4257
- | `request` | Present while the member is asking a lead to join one of their calls: which assignment, the note they wrote, since when. The call named is one their `tasks` carry (`team.member.request.assignment`). Absent when they are not asking. See **Lead assist**. |
4258
- | `shift` | The member's own history for the day, about the person rather than any one call: `signedInAt`, `signedOutAt` once it has happened, today's totals as the provider counts them — `talkSeconds`, `holdSeconds`, `breakSeconds`, `tasksHandled`, each present only when the provider knows it — and the day's sign-in, sign-out and break `events`, oldest first. The same `Shift` the member's own snapshot carries, the same numbers from the same count. Omitted where the provider cannot say. |
4259
-
4260
- Each availability value means one thing:
4261
-
4262
- | Value | Meaning |
4263
- | --- | --- |
4264
- | `ready` | Signed in, able to take work, none assigned. |
4265
- | `on-task` | Working on at least one task. It says nothing about how many, and nothing about whether more will fit. |
4266
- | `on-break` | Stopped and not taking work, whether they asked or somebody stopped them. The reason lives on their own `BreakState`, not here. |
4267
- | `elsewhere` | Signed in here, and the agent application holds this agent's capacity for another provider (`count: 0`, agent application-stopped): not receiving this provider's work, and not on a break. See **Capacity**. |
4268
- | `signed-out` | Known to this team but not signed in to this provider. |
4269
-
4270
- **Publish the whole team once, then each member whole.** Team presence typically reaches an
4271
- adapter over a best-effort channel with no ordering and no delivery guarantee, so the adapter
4272
- reconciles against its own authoritative read before it publishes anything. What it publishes is
4273
- the whole team on `team-updated` -- after the switch, and on every snapshot -- and after that one
4274
- member at a time on `team-member-updated`, carrying the member whole: their availability, their
4275
- tasks, their listening, their shift, their request, as they now stand. Applying the same member
4276
- twice changes nothing, so a redelivery is harmless, and a member the team never carried is added
4277
- by it, as a colleague signing in is. `team-member-removed` takes one off. Never a field of a
4278
- member, never a task of theirs on its own: a hold on a member's desk is that member republished
4279
- whole on the lead's, and nothing else on the lead's screen moves.
4280
-
4281
- **Whatever the member's desk hears of a task, every lead hears of the member.** The trigger is
4282
- not a list of changes but the publication itself: each `task-offered`, `task-updated`,
4283
- `task-audio-started`, `task-audio-ended` and `task-ended` the provider sends to the member is
4284
- matched by a `team-member-updated` to every lead with the feature on, carrying the member whole
4285
- with the task as it now stands, or without it once it has ended. A held leg closing with its
4286
- `seconds`, a room restated, a phase moving, the wrap left ticking on a restatement: each reaches
4287
- the lead's copy in the same round it reaches the member's, and the two copies of the record never
4288
- part for longer than the delay between them.
4289
-
4290
- **The agent's day is on the wire.** The lead sees `shift` totals as the provider counts them, and
4291
- the agent sees the same: `Snapshot.shift` and `shift-updated` carry the agent's own day in the same
4292
- shape, from the same count, so "calls today: 12" is one number on both screens. The desk computes
4293
- nothing of the day; `tasksHandled` moves when a task ends and `talkSeconds` when a leg closes, and
4294
- the active call's running time is the screen's own until then, as **Every screen counts from the
4295
- provider's clock** sets out.
4296
-
4297
- **Omit `since` rather than inventing one.** Omni renders it as a duration, so a timestamp
4298
- synthesised from the adapter's own clock at seed time reads as "on task for 0 seconds" for
4299
- everybody — worse than showing nothing, because it looks like data. Send it only when the provider
4300
- knows when the state actually began. It times the current `availability`, so it moves every time
4301
- that value does. And it is counted from the provider's clock, never the screen's, so a provider
4302
- that publishes it declares `timeCheck` -- see **Every screen counts from the provider's clock**.
4303
-
4304
- **The login is the permission.** A team member list goes to a login that declares
4305
- `capabilities.lead`, once the team feature is on, and to nobody else. Omni never decides who
4306
- leads a team: the provider said so at sign-in, and the team member list agrees with it — owed,
4307
- `members: []` included, once a lead has switched the feature on; absent for everybody else, which
4308
- is the correct rendering for an agent who leads nobody (`team.required`, `team.unentitled`). What
4309
- the lead may do with it comes with the flag: there is no per-action permission on the login or on
4310
- the list.
4311
-
4312
- **The lead switches the feature on and off, and the provider is told.** A lead may work as an
4313
- agent alone, as agent and lead, or as lead alone, and moves between them on the fly. The
4314
- agent application says which with `{ type: "lead-features", enabled }`: while it is on, the
4315
- provider sends team events scoped to the lead's team and the lead may act; while it is off, the
4316
- lead is an ordinary agent, nothing of the team is owed or expected (`team.unexpected`,
4317
- `event.team.features`), and no lead act is possible, since the lead learns their members'
4318
- assignments only from the list.
4319
-
4320
- **The provider assumes nothing.** The switch is not remembered across connections: the agent
4321
- application sends `lead-features` on every connect, after it has stated capacity, and until it has
4322
- the provider treats the feature as off. The connect snapshot therefore carries no team, whatever
4323
- the login declares; the whole team arrives on `team-updated` once the switch is on, and every
4324
- snapshot after that carries it. A conformance run switches the feature on for a lead and holds the
4325
- provider to that first `team-updated` (`team.required`); a team on the connect snapshot is the
4326
- provider assuming (`team.unexpected`).
4327
-
4328
- **The team member list never carries the agent it is published to.** A lead does not report to
4329
- themself: their own break request and their own ask for a lead go up to whoever leads them and
4330
- appear on *that* person's team member list, as a member with a `request`, while the requester sees
4331
- only their own `BreakState` and their task's `leadAssist` move. An adapter whose platform lists the lead
4332
- among their own members filters the signed-in identity out before publishing. **Being a lead is a
4333
- role the provider knows, never inferred from who is listed:** it is declared at sign-in, a lead
4334
- with nobody in their team publishes `members: []`, an agent with no such role publishes nothing, and no
4335
- member count can tell those two apart.
4336
-
4337
- ### Lead commands
4338
-
4339
- One method, `executeTeam`, taking a discriminated command exactly as `execute` takes a
4340
- `TaskCommand`. Every act on a member's call names the member, `memberId`, and may name the
4341
- assignment, `assignmentId`, where the lead has it; the provider resolves the member's current
4342
- assignment from the member alone.
4343
-
4344
- | Command | Effect |
4345
- | --- | --- |
4346
- | `{ type: "lead-features", enabled }` | The team feature on or off. See **The lead switches the feature on and off** above. |
4347
- | `{ 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`. |
4348
- | `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
4349
- | `{ 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; the resulting forced break publishes what is left of it as `expectedEndsInSeconds`, restated on every publication. |
4350
- | `{ type: "end-forced-break", memberId }` | Asks the provider to lift a forced-break restriction a lead asked for, by clearing `BreakState.forced`; one the platform imposed is its own to lift (`team.command.endForcedBreak.provider`, `omni.break-forced-by-provider`). The committed break continues; only the agent resumes work. |
4351
- | `{ type: "join", memberId, assignmentId? }` | Answers the member's request: the provider bridges the lead's channel into the call. See **Lead assist**. |
4352
- | `{ type: "decline", memberId, assignmentId?, reason? }` | Refuses the member's request. |
4353
- | `{ type: "listen", memberId, assignmentId? }` | The lead's channel joins the member's call unasked, in silence. See **Listening to a call**. |
4354
- | `{ type: "coach", memberId, assignmentId? }` | The lead is heard by the member alone. Needs the lead on that member's call. |
4355
- | `{ type: "join-call", memberId, assignmentId? }` | The lead is heard by everyone on the call. Needs the lead on that member's call. |
4356
- | `{ type: "leave", memberId, assignmentId? }` | The lead's channel leaves the member's call, however it got there. The member's call goes on. |
4357
- | `{ 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**. |
4358
- | `{ 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**. |
4359
-
4360
- `memberId` is this provider's own identifier for the member, as published on its team member list. It is
4361
- never an identifier from another provider, and Omni does not translate between them; names come
4362
- from `getUserDetails()`. `validateTeamCommand` holds each command to the team member list as it
4363
- stands -- see **Runtime break prerequisite checks**.
4364
-
4365
- `requests-suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
4366
- approve them. A provider that suspends break requests must also publish `canRequestBreak: false` to the team's
4367
- agents so they see it before asking. A `force-break` must likewise reach that member as a `forced` break
4368
- on their own `BreakState`, or they are stopped from working with no way to see why.
4369
-
4370
- What happens when no lead is online — auto-approving, for instance — is the provider's decision and is
4371
- never expressed here.
4372
-
4373
- ### Lead assist
4374
-
4375
- An agent on a call may ask a lead to join it -- a dispute that needs approval, a customer who
4376
- asks for a manager, a moment the agent wants a second pair of ears. A call centre calls this
4377
- assistance or escalation, and the name says who assists: the capability is `leadAssist` on the
4378
- task; the lead's side is the team member list, which is already the lead's view of the team. The
4379
- flow, in order:
4380
-
4381
- ```ts
4382
- // 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
4383
- execute({ assignmentId: "alloc-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
4384
- // task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
4385
-
4386
- // 2. Every lead with the team feature on sees the request on the member.
4387
- // team-member-updated: member: { id: "A-1", ..., request: { assignmentId: "alloc-42", note, since } }
4388
-
4389
- // 3. A lead joins, or declines.
4390
- executeTeam({ command: { type: "join", memberId: "A-1", assignmentId: "alloc-42" } })
4391
- executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" } })
4392
- ```
4393
-
4394
- **On `join` the provider bridges the lead's channel into the call.** The member's task moves to
4395
- `leadAssist: { stage: "joined", leadId }` and its `onCall` gains the lead as an `agent`, since a
4396
- lead who is heard is in the room; the member on the team member list carries
4397
- `listening: [{ leadId, assignmentId, mode: "join-call", since }]`, and the lead's audio opens on
4398
- it as **Listening to a call** describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
4399
- work assigned to them.
4400
-
4401
- **A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
4402
- or a member's -- is answered `failed`; the request stands for another lead, or until it is
4403
- withdrawn or declined. A join is work, so Omni offers Join only to a lead whose voice channel is
4404
- free, and a provider answers a `join` from one whose channel is not `failed`.
4405
-
4406
- **On `decline`, or a request the agent withdraws with `{ type: "lead-assist", action: "cancel" }`, the
4407
- provider clears `leadAssist` from the agent's task** and drops the request from every team member list. Nothing
4408
- else changes; the agent is still on the call.
4409
-
4410
- **On `leave`, the lead's channel drops and the call goes on**: the member's task loses its
4411
- `leadAssist`, the member loses this lead's entry in `listening`, and the lead is free. Nothing ended for the member.
4412
-
4413
- **On `take-over-call`, the provider moves the call from the member to the lead**, and the lead is
4414
- its new agent. There are two paths to it -- the agent asked and the lead joined, or the lead was
4415
- listening unasked -- and one act at the end of either. What follows is the same on both:
4416
-
4417
- | | The member's task | The lead's desk |
4418
- | --- | --- | --- |
4419
- | Take-over | `task-audio-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.audioOpen`). The member loses this lead's `listening` entry on the team member list no later than the offer, and the lead's listening audio closes. | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`, within the lead's capacity. |
4420
- | 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. |
4421
-
4422
- **The taken-over call is offered whatever break the lead is on, and counted like any other.** A
4423
- lead working as lead alone is on a break of a working kind, and the provider assigns the call
4424
- regardless: it is the one task a break in effect may hold (`break.on-break.tasks` allows it, and
4425
- nothing else). It is a call landing on the lead's desk, so it counts against their capacity as
4426
- every call does, with no exemption: a lead at capacity cannot take over, and the provider answers
4427
- `failed`. The other conditions are that the lead is not already on another call and their voice
4428
- channel is free. A lead with the team feature off cannot take over, since they have no member's
4429
- assignment to name.
4430
-
4431
- ```ts
4432
- const leadAssistCapable = {
4433
- channel: "voice",
4434
- capabilities: { hold: true, leadAssist: true, outcomes: true },
4435
- phase: "in-progress",
4436
- leadAssist: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
4437
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "leadAssist">;
4438
-
4439
- const takenOver = {
4440
- channel: "voice",
4441
- capabilities: { hold: true, endCall: true, outcomes: true },
4442
- phase: "in-progress",
4443
- takenOver: { memberId: "A-1", since: "2026-08-21T09:06:00Z" },
4444
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "takenOver">;
4445
- ```
4446
-
4447
- Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
4448
- `getUserDetails()`; names never travel on a task or a team member list.
4449
-
4450
- ### Listening to a call
4451
-
4452
- A lead may listen to a member's call without being asked -- to coach, to check quality, to step in
4453
- when it goes wrong. It is a team act naming the member, and no task on the lead's desk carries it:
4454
- the lead's view of the call is the member's task on the team member list, with its room and its
4455
- history. The three modes determine who hears the lead:
4456
-
4457
- | Mode | Who hears the lead |
4458
- | --- | --- |
4459
- | `listen` | Nobody. The lead hears both sides in silence. |
4460
- | `coach` | The agent alone. The customer hears nothing. |
4461
- | `join-call` | Everyone on the call. |
4462
-
4463
- All three come with the `lead` flag; a centre reserves none of them per lead.
4464
-
4465
- **There is no team audio.** Audio is the agent's or the lead's, and the lead's audio while
4466
- listening follows the provider's published state exactly as a task's does: the entry naming this
4467
- lead in `listening` on the member is the provider's word that the lead's channel is in the call.
4468
- When it appears, the application opens `openAudio({ assignmentId })` on the lead's connection for
4469
- the call it names, and plays the `CallAudio` returned; when it goes, the application closes it.
4470
- Another lead's entry opens nothing: it is shown, and that is all. Nothing else says
4471
- when the lead's audio is up, and a reload reopens it from the snapshot's team member list as a task
4472
- carried with `audio: "started"` is reopened.
4473
-
4474
- The flow, end to end:
4475
-
4476
- ```ts
4477
- // 1. The lead, with the feature on, sees a member on-task with their tasks, and picks a call.
4478
- // 2. The lead's channel joins it in silence. Conditions: the lead's voice channel is free --
4479
- // not on a call, not listening elsewhere. Capacity is not involved: nothing is assigned.
4480
- executeTeam({ command: { type: "listen", memberId: "A-1", assignmentId: "alloc-42" } })
4481
- // 3. The provider publishes the member whole on team-member-updated, to every lead; the member carries
4482
- // listening: [{ leadId: "L-9", assignmentId: "alloc-42", mode: "listen", since }]
4483
- // -- L-9's application opens openAudio({ assignmentId: "alloc-42" }) on its own connection; every
4484
- // other lead's shows L-9 on the call and opens nothing.
4485
- // 4. The lead changes how they are heard; the member is published again with the entry's new mode. The audio stays open.
4486
- // join-call is heard by everyone, so the member's task is also republished with L-9 on onCall as an agent.
4487
- executeTeam({ command: { type: "coach", memberId: "A-1" } })
4488
- executeTeam({ command: { type: "join-call", memberId: "A-1" } })
4489
- // 5. The lead leaves: the provider publishes the member without L-9's entry, and the task without L-9 in the room; the application closes the audio.
4490
- executeTeam({ command: { type: "leave", memberId: "A-1" } })
4491
- // 6. The member's call ends: the same as leave, from the provider's side.
4492
- // 7. The lead takes the call: the member loses this lead's listening entry no later than the offer, the application
4493
- // closes the listening audio, and the lead's own call arrives as an ordinary assignment marked
4494
- // takenOver, counted against capacity, with its audio following task-audio-started as any call's does.
4495
- // At capacity the take-over is refused and the lead stays listening.
4496
- executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
4497
- // 8. A join on the member's request is the same flow entered at step 3 with mode: "join-call",
4498
- // and the member's task shows leadAssist: joined.
4499
- // 9. A reload of the lead's application: the switch is sent again, the team-updated that follows
4500
- // still carries this lead's entry in listening on the member, so the audio is reopened.
4501
- ```
4502
-
4503
- **`listening` on the member is the state**, one entry per lead on the call, restated on every
4504
- change of mode, naming the lead (`team.member.listening.leadId`, once per member,
4505
- `team.member.listening.unique`) and the call, so the application knows which audio is its own to
4506
- open (`team.member.listening.assignmentId`, and one the member's tasks carry,
4507
- `team.member.listening.assignment`). Every lead sees every entry: a silent lead is hidden from
4508
- the agent, never from the other leads and managers watching the same member on other screens.
4509
- A lead is on one member's call at a time (`team.listening.single`).
4510
-
4511
- **A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
4512
- channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
4513
- includes a lead on a break: a lead working as lead alone is on a break of a working kind, and
4514
- listens through it.
4515
-
4516
- **A silent lead reaches nobody but the leads.** In `listen` and `coach` the member's task carries
4517
- no trace of the lead: not on `onCall`, not in the record. Whether coaching is announced to the
4518
- agent is the platform's business and travels on the audio, not on this wire. In `join-call`, and
4519
- on a `join` answering a request, the lead is heard by everyone and is in the room: the task's
4520
- `onCall` carries them as an `agent` by user id, on the member's desk and on every lead's copy of
4521
- the task alike, and `leave` takes the entry off as it takes the channel off
4522
- (`team.member.listening.heard`).
4523
-
4524
- ### A member waiting for a break
4525
-
4526
- A member who has asked for a break **keeps working** until Omni commits it, so asking is not an
4527
- availability of its own — it rides alongside one on `break`. That a request exists says nothing
4528
- about whether anybody has to act on it, and the difference is a lead's entire action list:
4529
-
4530
- | `break` | Means |
4531
- | --- | --- |
4532
- | `awaiting-approval` | Somebody has to decide. This is the lead's queue. |
4533
- | `granted` | Decided yes, but Omni has not told the provider to stop yet. Work continues and nobody needs to decide. |
4534
- | `starting-after-task` | Already granted; it begins when their current task ends. Nobody needs to act. |
4535
-
4536
- Those three are the only values that appear here. `not-requested` is absence — omit `break`
4537
- instead. `on-break` is `availability: "on-break"`, and a denial transitions to `not-requested`,
4538
- so neither survives to be reported. It is otherwise the same `BreakStatus` the member's own
4539
- break state uses, rather than a parallel vocabulary for the lead's view, so the two cannot drift
4540
- apart.
4541
-
4542
- Omni offers Approve and Deny only while a member is `awaiting-approval`, shows `granted` as agreed
4543
- but not started, and shows `starting-after-task` as settled.
4544
-
4545
- **Live status, not a record.** The provider derives it from what is true now — not stored, not
4546
- historical, carrying no decision made earlier. Like the team member list it belongs to, it is published
4547
- whole and replaced whole, and a provider that cannot say omits it.
4548
-
4549
- **An agent is not waiting on one person.** Authority is held by several, everyone who holds it
4550
- sees the request on their own console, and **any one of them settles it**. Omni offers the
4551
- decision to every login that declares `lead` and has the team feature on — which is how the provider already
4552
- says who may decide — and does not try to work out whose turn it is.
4553
-
4554
- A request needing *more than one* approval is not something this contract describes. There is
4555
- no partial state to report and no progress to display: a request is either still owed a
4556
- decision or it is not.
4557
-
4558
- ## Real-time audio
4559
-
4560
- Every voice provider has audio: `channel: "voice"` says audio exists. Where the agent hears it
4561
- depends on the `phone` the agent application selected at authentication and connect from the manifest's
4562
- `phones`. On a softphone, audio lands in Omni; on a desk phone, it lands on the handset and Omni
4563
- opens no audio. See **How the agent hears the call**. Audio transitions are voice-only;
4564
- chat and email publish none (`event.audio.channel`, `stream.taskAudio.channel`).
4565
-
4566
- The provider owns signalling and the platform's endpoint configuration. The agent application selects the
4567
- declared phone mode; it does not enumerate or reconfigure the platform's devices. The selected
4568
- mode stays with the login rather than being inferred from a snapshot or a device failure.
4569
-
4570
- Nor does the audio ever stand in for the task: a task's presence and
4571
- phase follow the provider's reports about the work, and the audio — attaching, moving through a
4572
- hold, a conference, a lead joining or a take-over, and ending — is transient beside it. See **A task is
4573
- never its audio** under **Task assignment lifecycle**.
4574
-
4575
- ### The agent application reports, the adapter decides
4576
-
4577
- The adapter runs inside Omni and sees nothing of the station for itself: the devices, the
4578
- permissions, the audio element and the network are the agent application's, and only the agent application can tell. So
4579
- the agent application reports, and the adapter asks. **An adapter consults `ConnectContext.host` before it
4580
- declares the agent ready to its platform, and again whenever the report changes**, and it decides
4581
- what any of it means and what to relay upstream — go not-ready, refuse calls, carry on because the
4582
- platform's audio lands elsewhere. Omni facilitates and does not take responsibility: it captures
4583
- the microphone once as the voice connection opens, so the permission prompt lands while the agent
4584
- is signing in rather than over a contact, prompts, retries on the agent's request, tells the agent
4585
- what failed, and reports. It never decides for the adapter what a missing microphone means.
4586
-
4587
- **The agent application also guarantees, and a promise the provider cannot see is not one it can rely on.**
4588
- Two of this contract's obligations fall on the agent application rather than the provider — honouring a task
4589
- browser's `urlVisibility`, and taking a `consent` offer only on the person's own press — and more
4590
- than one desk speaks this contract: a browser and a native application differ in what they can
4591
- reach, and a provider never branches on which it is talking to, only on what it has declared —
4592
- here, and in `host.mute` (see **The station is the agent application's**). So `ConnectContext.host.guarantees` says which promises the
4593
- connected agent application makes, declared once per connection, presence being the guarantee exactly as it is
4594
- the permission everywhere else:
4595
-
4596
- | Guarantee | Contract |
4597
- | --- | --- |
4598
- | `browserUrlVisibility` | Every task browser's `urlVisibility` is honoured in this agent application's chrome, tab by tab. A provider that would send a caller's number in a URL checks this first and tokenises where the promise is absent. |
4599
- | `personConsent` | A `consent` offer is accepted only by the person's own explicit act, never on their behalf. A provider whose work may only be taken by a human checks this first and does not offer it where the promise is absent. |
4600
-
4601
- A guarantee the agent application does not make is an absent key, never `false` — `validateHostGuarantees`
4602
- refuses a false one, as it refuses a name this contract does not list.
4603
-
4604
- | Field | Contract |
4605
- | --- | --- |
4606
- | `online` | Whether the agent application has a network interface up. Not a claim that anything is reachable — the adapter knows whether it can reach its own platform far better than the agent application does — so `false` is a reason not to go ready and `true` is not a reason to. |
4607
- | `audio` | Present on a voice connection, absent where there is no audio. |
4608
- | `audio.input` | `available` with `localAudio` — the microphone as captured, the same stream `openAudio` receives — and `flowing`, false while no audio moves through it, when `mutedBy` says who stopped it: `host`, the agent application's own Mute on an agent application that mutes the station, or `station`, a slider on the headset or the operating system, which the agent application observed and did not do. An adapter treats `host` as the agent's act on a call and `station` as a condition of the station. `unavailable` with `reason`, since each wants a different fix from the agent: `no-device`; `denied`; `not-asked`, which an agent application that asks at connect never publishes; `in-use`, a device present and permitted that another application holds — on an agent desktop the commonest of all; `lost`, a capture that ended. An agent application decides the reason from the devices before the error name: a browser can report a permission error on a machine with no microphone at all, and "grant permission" is the wrong instruction for an agent who needs to plug one in. `failure` carries the words Omni showed them. |
4609
- | `audio.output` | `available`, with `flowing` where the agent application can know whether audio reaches the speaker — a browser mostly cannot, and omits it; a native agent application reads the endpoint — false while it is silenced, when `mutedBy` says who did it, as on input. Or `unavailable` with `reason` — `no-device`, or `lost` for one removed — and `failure`: an agent who cannot hear is as unable to take a call as one who cannot speak. |
4610
-
4611
- Omni republishes the report whenever it changes — a permission granted late, a headset unplugged,
4612
- a network gone — and it publishes a state, not a flicker: a change that resolves within moments is
4613
- not reported, so an adapter may act on what it reads without debouncing it again. Nothing in the
4614
- report is a task fact or a capacity fact; it is what the station can do, and the adapter's platform
4615
- is the one that knows whether an agent without a microphone, or without a speaker, can work.
4616
-
4617
- The agent application's own obligations here — asking at connect, never publishing `not-asked` when it does,
4618
- publishing a state and not a flicker — are Omni's tests' to hold. `exerciseAdapter` holds the
4619
- other side: it validates the shape of whatever agent application a test hands the adapter, requires `audio` on
4620
- a softphone login and none elsewhere -- not on a desk phone, not off voice
4621
- (`context.host.audio.required` / `.unexpected`), holds `host.mute` to the same line (`host.mute.required`
4622
- / `.unexpected`, `host.mute` for a third word) -- and refuses a voice adapter that never asked the
4623
- agent application anything (`connection.host.consulted`), and one that subscribed to nothing will never hear of a
4624
- change (`connection.host.subscribed`): the obligation has two halves, and each is held.
4625
-
4626
- ### Capacity around setup
4627
-
4628
- Connecting is not the same as being able to take a call. A provider that treats a live connection
4629
- as reachability opens a window where it believes the agent is available and the agent is not yet
4630
- set up — the adapter's own registration incomplete, its credentials not yet renewed.
4631
-
4632
- Nothing closes that window, because nothing opens it: **Omni states no capacity until the
4633
- connection is established**, and **Work is pulled, never pushed** makes an assignment with none
4634
- stated a violation. Capacity does not wait on the microphone: whether an agent without agent application audio
4635
- can take calls is the platform's question, answered by the adapter from the agent application's report, not by
4636
- Omni withholding capacity for every platform alike.
4637
-
4638
- Capacity follows **automatically** once the connection is established; the agent does not press
4639
- anything to become available.
4640
-
4641
- | Situation | What Omni sends |
4642
- | --- | --- |
4643
- | Connecting | Nothing. No capacity has been stated, so nothing may be assigned. |
4644
- | Connected and idle | `setCapacity({ count: n })`, with the agent application's report already available to consult. |
4645
- | A task starts or ends | Nothing. The provider counts its own against the ceiling. |
4646
- | The agent's provisioned capacity changes | `setCapacity({ count: n })` |
4647
- | Agent asks for a break | `requestBreak`. Capacity is unchanged and work continues. |
4648
- | Omni commits a break | `commitBreak`. The break stops assignment, not the ceiling. |
4649
- | Agent returns from break | `endBreak` |
4650
-
4651
- **A break is not a capacity of zero, and neither is the reverse.** Capacity says how much of this
4652
- agent this provider may carry; a break says they are not working at all. A provider told
4653
- `count: 0` shows an agent whose capacity is elsewhere, `elsewhere`; a provider told a break shows
4654
- `on-break`. Collapsing the two would leave a provider unable to tell an agent working on another
4655
- provider from an agent who has gone to lunch, and only one of those needs a reason, a decision and
4656
- a return.
4657
-
4658
- ### How the agent hears the call
4659
-
4660
- A voice platform can put an agent on a **softphone**, where the call's audio lands in the agent application
4661
- and Omni owns the microphone and the playback, or on a **desk phone**, a handset on the platform's
4662
- switch that the platform rings, where the agent application owns no audio and only shows the call. Which of
4663
- these a platform can do is a fact about the platform, and which one a login is on is a choice the
4664
- agent application makes for that agent -- so the manifest declares and the agent application chooses.
4665
-
4666
- ```ts
4667
- // Manifest: what the platform can do
4668
- phones: ["softphone", "deskPhone"]
4669
- // The agent application, at authentication and again at connect: which one this login is on
4670
- { phone: "softphone" }
4671
- ```
4672
-
4673
- A voice manifest lists its `phones` and any other channel lists none (`manifest.phones.required`,
4674
- `manifest.phones.channel`). The agent application's `phone` is required on a voice login, absent on any other,
4675
- and one the manifest listed (`context.phone.required`, `.unexpected`, `.unsupported`). Everything
4676
- about audio then follows the phone rather than the channel: on a softphone the agent application reports its
4677
- audio, the adapter implements `openAudio`, and `task-audio-started` is the word to open it; on a
4678
- desk phone the agent application reports no audio, opens nothing, and `task-audio-started` still marks the
4679
- moment the call is live so the desk shows it, with the sound on the handset. A platform that lists
4680
- `deskPhone` alone never implements `openAudio`; one that lists `softphone` always does.
4681
-
4682
- **The mode comes from the agent application; the status comes from the provider.** The agent application knows which kind
4683
- of station the agent signed in at, because the person chose it, and nothing else can know that.
4684
- The provider knows whether that station can carry a call right now -- whether a handset is
4685
- registered -- which an agent application in a browser cannot see. So the agent application's `phone` selects the branch, and
4686
- the provider's own knowledge of the device decides readiness within it. A provider never overrides
4687
- the agent application's declaration from its device record: treating a desk-phone login as a softphone because
4688
- the handset is momentarily unregistered, and demanding a microphone of it, is the reading this
4689
- sentence exists to refuse. What the provider does when the handset is not registered is what it
4690
- does for any station that cannot take a call -- hold the agent not-ready and say why.
4691
-
4692
- **On a desk-phone login the agent application never calls `openAudio`.** There is no stream to hand over and
4693
- no audio to attach; an agent application that calls it anyway is in error, and an adapter that receives the call
4694
- answers `unavailable` with a non-retryable failure, since waiting changes nothing about a station
4695
- that is a telephone. The harness requires no `openAudio` of such an adapter and never calls it.
4696
-
4697
- **Which handset a desk-phone login rings is the platform's configuration for that agent**, and
4698
- this wire never asks the agent for it: an agent application declares `phone` and nothing more. What a platform
4699
- asks on surfaces of its own is its own decision.
4700
-
4701
- **A declared phone the platform does not permit for this agent is a login that cannot be
4702
- established.** The manifest says what the platform can do; the agent's record, kept by an
4703
- administrator, says what this agent is configured for, and the two can disagree with the agent application's
4704
- declaration -- a softphone declared for an agent whose record says desk phone. Honouring that
4705
- declaration on a switch that allows one registration per endpoint would evict the handset: the
4706
- agent's phone stops ringing and their calls land in a tab, and nobody is told. So the provider
4707
- refuses the login at authentication, with `omni.phone-not-permitted` and a message the agent application shows
4708
- -- "this agent is configured for a desk phone" -- and refusing is correct, not an override of the
4709
- agent application. It is a refusal, never a negotiation afterwards and never a quiet substitution, and it is
4710
- never retryable: trying again does not reconfigure the agent, and an agent application validating the answer
4711
- (`validateAuthenticationResult`) refuses a phone refusal marked otherwise
4712
- (`authentication.failure.phone.retryable`). And **a
4713
- provider never makes a declared phone true by changing the platform's configuration**: the record
4714
- is the administrator's, and a login is not a request to reconfigure an agent.
4715
-
4716
- ### Opening the audio
4717
-
4718
- `openAudio` hands Omni the remote audio for one task. Every adapter whose manifest lists
4719
- `softphone` implements it, because on a softphone the call's audio lands in Omni:
4720
-
4721
- ```ts
4722
- openAudio({ assignmentId, localAudio }): Promise<OpenAudioResult>
4723
- // { status: "opened", session } | { status: "unavailable", failure }
4724
- ```
4725
-
4726
- The adapter speaks whatever its platform speaks — SIP over WebSocket, a vendor SDK, plain
4727
- WebRTC — and **none of that appears in this contract**. Registration, signalling, credential
4728
- renewal and reconnect are the adapter's, exactly as its authentication and transport already
4729
- are. Omni owns what belongs to the agent application: the microphone, the output element, mute, and when a
4730
- session ends — owning the microphone meaning capturing it, prompting, retrying and reporting how
4731
- it stands, never deciding for the adapter what a missing one means (see **The agent application reports, the
4732
- adapter decides**).
4733
-
4734
- | Member | Contract |
4735
- | --- | --- |
4736
- | `remoteAudio` | `MediaStream` Omni plays. |
4737
- | `setMuted(muted)` | Mutes the agent's microphone on this session. |
4738
- | `close()` | Releases the session. Omni calls it when the task ends. |
4739
-
4740
- `localAudio` is the agent's microphone as Omni captured it, the same stream the agent application's report
4741
- carries as `audio.input.localAudio`, and absent while that input is `unavailable`. A provider that
4742
- bridges audio without an application-side input may ignore it; one that needs it and finds it absent
4743
- answers `unavailable` with a failure Omni shows the agent.
4744
-
4745
- **When to ask is the provider's word, not Omni's guess.** On a softphone login, Omni opens audio on `task-audio-started`,
4746
- and on a task arriving with `audio: "started"` on a snapshot; it closes on `task-audio-ended` and
4747
- when the task ends. Between those words, nothing Omni's own senses report — a stream that drops, a
4748
- track that ends — moves the task or its audio.
4749
-
4750
- **A task-scoped session does not oblige one call per task.** A platform holding a nailed-up
4751
- leg for a whole shift may return the same session for every task and release the underlying
4752
- path only when the connection closes. A platform placing a call per contact returns a new one
4753
- each time. Omni asks when it needs audio and closes when it is done; how that maps to the
4754
- platform is the adapter's business.
4755
-
4756
- ## Task commands
4757
-
4758
- Command names follow the channel's operational vocabulary, and each channel's command is a union
4759
- discriminated by `type` — the same discriminant `executeTeam` and `custom` already use. The
4760
- unions are declared under **Shapes**.
4761
-
4762
- `assignmentId` is not repeated on the command. It travels on the `TaskCommandRequest` around it.
4763
-
4764
- **A toggle carries the state it wants, not a flip.** Inverting whatever is found cannot converge
4765
- with a stale view: a flip against a state the provider has already changed turns something on and
4766
- then off again. A custom `toggle` control therefore carries its own boolean. `hold` and `resume`,
4767
- `pause` and `resume` need no flag, being pairs rather than toggles.
4768
-
4769
- **`complete` sends an outcome only where one was published.** `outcome` is a
4770
- `OutcomeCode.id` from the task's own `outcomes` capability, and `notes` obeys that
4771
- capability's `notes` setting. A task publishing no codes still receives `complete`, with neither.
4772
-
4773
- ### Where a command executes
4774
-
4775
- Every command reaches the provider through `execute`, with no branch at the call site, and every
4776
- command asks the provider to **perform** something: `hold`, `conference`, `schedule`, `end-call`,
4777
- Provider-targeted `recording` commands and the rest act on the platform's own call leg, its bridge, or its record of the
4778
- task. Nothing has happened until the provider applies them, and `failed` means nothing happened.
4779
-
4780
- **The provider performs every action; the agent application only offers it.** A control drawn on the agent application is
4781
- an affordance, never the enforcement: the agent application asks, and the provider does or declines. A provider
4782
- answers `failed` for a command whose capability it did not publish, and that is the provider
4783
- honouring its own declaration, not the agent application deciding policy -- an agent application that refuses a command on
4784
- its own reading of a queue's flag has decided something that was never its to decide. What a
4785
- command means on the platform is the provider's to work out: `end-call` disconnects the caller channel, and
4786
- which legs on which bridge that touches is a fact about the switch, never a choice the agent application makes.
4787
- The one thing the agent application performs physically is the microphone, and that is not a command at all.
4788
-
4789
- ### The station is the agent application's
4790
-
4791
- The microphone, the speaker, the headset and its buttons are the station's, and the station is the
4792
- agent application's. So mute is not a capability a provider declares, not a control a queue allows or a team
4793
- locks, not a preference the person keeps with the provider, and not a command: nothing about the
4794
- station crosses to the provider except two things -- the station's condition, in the agent application report,
4795
- and the record of when the agent could not be heard, through `recordStep`. An agent application offers Mute on
4796
- every voice task with audio open, under its own local policy, in `in-progress` and `paused`
4797
- alone, and performs it through `CallAudio.setMuted()`. A task carrying
4798
- `capabilities.mute` is refused (`task.capability.unknown`), and so is a `mute` command
4799
- (`command.type`), a `mute` preference (`preference.id`) and a `mute` policy (`team.policy.key`).
4800
-
4801
- **Two mutes, and the agent application states which its Mute performs.** A *stream* mute stops the audio the
4802
- agent application sends: the microphone keeps capturing, the party hears silence, nothing else on the machine
4803
- changes, and every softphone agent application can do it. A *station* mute silences the microphone itself at
4804
- the operating system: every application on the machine goes quiet, the system shows it, and a
4805
- headset that follows the system follows it. Only a native agent application can do it. `Host.mute` says which
4806
- this agent application does -- `stream` or `station` -- stated on a softphone login, where the agent application holds the
4807
- microphone, and absent on a desk phone and off voice (`host.mute.required` / `.unexpected`). A
4808
- browser says `stream`. A native agent application says whichever it does. Nothing on the wire says what kind of
4809
- application the agent application is; a provider reads only what the agent application declared.
4810
-
4811
- **A silenced device says who silenced it.** `audio.input.flowing: false` and
4812
- `audio.output.flowing: false` carry `mutedBy`: `host` when the agent application's own Mute did it -- which is
4813
- only ever true of an agent application whose mute is `station`, since a stream mute leaves the microphone
4814
- flowing -- and `station` when a slider on the headset or the operating system did, which the agent application
4815
- observed and did not do. Without the word, a station-muting agent application's own Mute would read as a fault
4816
- and an adapter would take the agent out of ready in the middle of a call. A browser reads a station
4817
- mute through the capture track's muted state, read-only, and cannot lift it; a native agent application reads
4818
- the endpoint and can clear an operating-system mute. A hardware slider is nobody's to clear.
4819
-
4820
- **The record carries the same word.** A `muted` interaction leg is a period the agent could not be
4821
- heard, and the record exists so that period is not a hole. The agent application reports every such period
4822
- through `recordStep` -- its own Mute, and a station mute it observed during a call -- with
4823
- `mutedBy` saying whose the silence was, and the provider writes the word into the entry
4824
- (`task.history.mutedBy`). The report names no agent because the agent application has exactly one, and
4825
- the provider knows who that is: it attributes the leg to the login's agent in `by`, a fact it
4826
- holds and not an inference, for a station mute as much as for the agent application's own. An unattributed
4827
- `muted` entry is refused (`task.history.muted.by`); a shared handset's unattributed hold
4828
- is a different claim, and stands. A supervisor reading the record then sees "the agent muted for
4829
- forty seconds" and "the agent's headset was muted for forty seconds" as the different things they
4830
- are. See **The agent application records what it performs**.
4831
-
4832
- **Mute has a lifecycle, and none of it is inferred.** A call that starts while the station is
4833
- already muted begins a `muted` leg the moment its audio starts, `mutedBy: "station"`. Audio that
4834
- ends while any leg is open ends the leg at that instant, as every agent application-performed leg ends. And the
4835
- agent application's own Mute starts off on every call, which is every `task-audio-started` -- a connect-back
4836
- opens a second call on the same task with no offer -- so an agent is never muted by the call before.
4837
-
4838
- **Every press on a headset is the agent's own press**, performed the agent application's way, and the lights
4839
- follow the agent application's state. The `personConsent` guarantee is honoured by a hook-switch press exactly
4840
- as by a click. What a call-control headset sends, and what becomes of it:
4841
-
4842
- | Input | Where it comes from | What the agent application does | What crosses to the provider |
4843
- | --- | --- | --- | --- |
4844
- | Mute button | The headset, on the HID Telephony usage page: a Phone Mute report on the press, a Mute LED the agent application writes back | A press of the agent application's Mute, performed the agent application's way; the LED follows the agent application's state, so button, light and control are one state with one owner | The `muted` leg, `mutedBy: "host"` |
4845
- | Hook switch | The headset, HID Telephony | A press of the agent application's Answer or End call, only where the task is in a phase that has one; the off-hook and ring lights follow the task | The existing `answer` and `end-call` commands |
4846
- | Flash, redial, speed dial | Older headsets and desk-phone style devices | Flash is Hold and Resume where the task offers `hold`; redial and speed dial map to nothing on a desk that never redials, and are ignored | Nothing new |
4847
- | Volume up and down | The headset's own amplifier, or the operating system through the consumer-control keys | Nothing: the device and the system handle it | Nothing, except that a speaker at nought is `audio.output.flowing: false` where the agent application can know it |
4848
- | Microphone gain and level | The operating system, the headset's own boost | A level meter, so the agent can see they are heard | Nothing: a level is a flicker, not a state |
4849
- | Hardware or operating-system mute | A slider on the headset, the system's input mute | Observes it, publishes `flowing: false, mutedBy: "station"`, records the leg during a call, and tells the agent which it is where it knows -- headset or system -- and what to do; an agent application whose mute is `station` clears a system mute itself, a browser can only say so | The report and the leg |
4850
- | Device changed, unplugged, permission revoked | The operating system | Re-capture, prompt, report | Already `audio.input` and `audio.output` with `lost`, `denied`, `no-device` |
4851
-
4852
- **What the agent application shows.** Beside Mute, a standing line while the station is muted, saying which and
4853
- what to do, with a button that clears a system mute only on an agent application whose mute is `station`. The
4854
- Mute control does not pretend: while the station is muted it shows the agent cannot be heard and
4855
- that pressing it will not change that, and a press still toggles the agent application's own mute, so the two
4856
- states stay separate and the agent is not unmuted by surprise when the slider moves back. Before a
4857
- call, the same line in the ready state, so an agent does not learn of it from a silent customer;
4858
- whether work is held back is the adapter's decision from the report, as it is for every station
4859
- fact. Where a provider's own platform holds a mute of its own -- a bridge that silences a leg --
4860
- that is a fact about the switch, reported as the provider sees fit, and never the agent application's Mute.
4861
-
4862
- ### Which commands need a capability
4863
-
4864
- **Presence is the permission** gates the controls a provider chooses to offer. Four commands are
4865
- not among them, because every task has them; each is authorized by a different field the provider
4866
- declared:
4867
-
4868
- | Command | What makes it available |
4869
- | --- | --- |
4870
- | `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
4871
- | `end-call` | The `endCall` capability: ends my part, the caller continues. |
4872
- | `terminate-call` | The `terminateCall` capability: ends the whole call, every channel on it. |
4873
- | `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`. |
4874
- | `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
4875
- | `dial` | 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`. |
4876
- | `complete` | A wrap to cut short: any task but a `provider-automatic` one with `wrapAllowance: 0`, under either completion mode (`command.complete.wrapAllowance`). The `outcomes` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. What travels is what the capability published: a code from its list where it has one (`command.complete.outcome.unknown`), a code at all where it requires one (`.outcome.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no outcomes (`.outcome.unexpected`). |
4877
- | `connect-back` | The `connectBack` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
4878
- | `schedule` | The `schedule` capability, on any channel, in `in-progress`, `paused` or `completing`: a follow-up is promised on the call and written up in wrap alike. The manifest declares `calendar` for it to land on. |
4879
- | `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with `stage: "requested"`. |
4880
- | `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`). |
4881
- | `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`). |
4882
- | Everything else | Its own named capability. |
4883
-
4884
- **A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
4885
- `hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `conference` action, and
4886
- every `lead-assist` action; `schedule` alone reaches into `completing`. These controls act within this agent's current interaction.
4887
- In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
4888
- an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
4889
- of this interaction does not establish that the caller or bridge ended. The capability stays
4890
- declared while the task remains open; the phase prevents this task from controlling an interaction
4891
- it no longer owns. Omni shows these controls only in the two interaction phases, and
4892
- `validateTaskCommand` refuses them outside those phases (`command.phase.interaction`).
4893
- The commands with a phase of their own -- `answer`, `accept` and `decline` in `pending`,
4894
- `dial` in `preview`, `connect-back` in `completing`, `complete` in any -- are not among them.
4895
-
4896
- `validateTaskCommand(command, task)` holds a command to this table at runtime, both ways: the
4897
- capability it needs, the phase it belongs to, and the state that has to stand. The task it wants
4898
- is the one the provider published, not an agent application's own mapping of it: an agent application that keeps only its
4899
- mapped shape has nothing honest to pass, and then checks shape alone, which is still worth doing
4900
- -- it names a command the wire never had -- but is not the table. Keeping the published task
4901
- beside the mapped one is what the full check costs an agent application; an adapter has it for free. The
4902
- validator holds a command only to a task that stands: a task handed in that is not one the wire
4903
- published is named (`command.task`) rather than checked against.
4904
-
4905
- Declining a pending offer ends it without accepting or completing it. The provider confirms the
4906
- end with `task-ended` and a `cancelled` outcome, `by: "agent"`.
4907
-
4908
- ### `execute(request)`
4909
-
4910
- Applies a `TaskCommandRequest` to one provider-local task.
4911
-
4912
- - Omni serializes commands per task and sends the next only after the previous settled or was
4913
- given up as unknown; it stops sending when `task-ended` arrives.
4914
- - `applied` confirms the side effect completed. `failed` confirms it did **not**, with a typed
4915
- `ProtocolFailure`; a provider that will not and one that cannot report the same shape, and `code`
4916
- says which.
4917
- - A command sent while `transport-status` is not `active` answers `failed` with `omni.unavailable`.
4918
- Neither Omni nor the adapter queues it.
4919
- - **A command that asks for a state answers `applied` when that state holds, whoever brought it
4920
- about; a command that acts answers `failed` when it cannot act.** Declining a lead request
4921
- already gone is `applied`; joining one already gone is `failed`, since nobody joined. A lead
4922
- deciding a member's break that another lead has already decided is `applied` when the decisions
4923
- agree and `failed`, saying so in `message`, when they differ. `commitBreak()` on a break already
4924
- in effect is `committed` for the same reason.
4925
- - **A result is untrusted for the same reason a snapshot is.** It comes from an adapter that may be
4926
- compiled against another version, and Omni shows the agent what it says. Omni validates it at
4927
- the boundary with `validateResult(result, "execute")` — a status the method does not answer, a
4928
- `failed` without its failure, a success carrying one, or an `omni.` code this contract lacks is
4929
- refused, and the command is treated as unsettled.
4930
- - **A settled result is a fact; an unsettled promise is not.** Transport uncertainty may reject the
4931
- promise with no result at all, and that means *unknown*, not *failed*, and a snapshot follows —
4932
- see **An unsettled result is unknown**. `failed` must never be returned for something the
4933
- provider is unsure of, because Omni will show the agent it did not happen.
4934
-
4935
- ### `ProtocolFailure`
4936
-
4937
- | Field | Contract |
4938
- | --- | --- |
4939
- | `code` | Required stable machine-readable value. See the reserved codes below. |
4940
- | `message` | Required, and safe for logs or agent display. |
4941
- | `retryable` | Whether repeating the action can succeed at all. |
4942
- | `retryAfterMs` | Optional minimum suggested delay before a retry. A suggestion, not a guarantee. |
4943
-
4944
- The `omni.` prefix is **reserved**. Adapters must not invent codes under it; every other value is
4945
- provider-private and Omni treats it as opaque. Using a reserved code where it applies lets Omni
4946
- react rather than only display the message:
4947
-
4948
- | Code | Meaning |
4949
- | --- | --- |
4950
- | `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. |
4951
- | `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. |
4952
- | `omni.assignment-not-found` | The assignment named is not one the provider holds, typically after the task already ended. |
4953
- | `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**. |
4954
- | `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
4955
- | `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
4956
- | `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
4957
- | `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
4958
- | `omni.recording-unsettled` | A recording command the provider could not settle either way: a settled `failed`, never a promise left unresolved. See **Independent task recording**. |
4959
- | `omni.break-forced-by-provider` | A lead asked to lift a break the platform imposed; the platform lifts its own. See **Forced breaks**. |
4960
-
4961
- They are published as `OMNI_FAILURE_CODES`.
4962
-
4963
2311
  ## Event delivery
4964
2312
 
4965
2313
  ### `ProviderEventEnvelope`
@@ -5075,7 +2423,7 @@ used for final records, and agent application-supplied timestamps are advisory.
5075
2423
  of time-check support and the optional agent application estimate; neither changes who owns the final record.
5076
2424
  Omission makes no promise that agent application timestamps will be trusted. No agent application-authoritative default
5077
2425
  is inferred. Successful `recordStep` responses identify the provider-selected history instant;
5078
- see **The agent application records what it performs** for correlation and retained bindings.
2426
+ see **The agent application records what it performs** in `guide/agent.md` for correlation and retained bindings.
5079
2427
 
5080
2428
  A provider need not trust or adopt any agent application-supplied timestamp. It may timestamp an incoming
5081
2429
  message using its own clock at receipt and use that for its own processing/accounting. The reason
@@ -5315,7 +2663,7 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
5315
2663
  - selects another task or returns to the idle workspace.
5316
2664
 
5317
2665
  A `taken-over` outcome arrives at the member's own completion, after the wrap that follows a
5318
- take-over as it follows an `end-call` -- see **Lead assist**.
2666
+ take-over as it follows an `end-call` -- see **Lead assist** in `guide/lead.md`.
5319
2667
 
5320
2668
  A successful `complete` command does not clear the task. Omni waits for `task-ended`,
5321
2669
  and not for ever: `applied` to a `complete` says the
@@ -5360,7 +2708,7 @@ provider does not batch. `exerciseAdapter` treats a diagnostic delivered during
5360
2708
  violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
5361
2709
  fails loudly rather than passing with a note. A task published under `capabilitySource:
5362
2710
  "not-yet-read"` is the same kind of fact, stated on the task instead, and fails a run the same way
5363
- (`capabilitySource.notYetRead`); see **Task capabilities**.
2711
+ (`capabilitySource.notYetRead`); see **Task capabilities** in `guide/agent.md`.
5364
2712
 
5365
2713
  | Field | Contract |
5366
2714
  | --- | --- |
@@ -5396,7 +2744,7 @@ One member, whole, as they now stand: their availability, their tasks, their lis
5396
2744
  shift, their request. It replaces the member of that `id` on the lead's list, or adds them where
5397
2745
  the list did not carry them, so applying it twice changes nothing. It comes only after the whole
5398
2746
  team has (`stream.team.baseline`), and it is what every publication of a member's task to the
5399
- member publishes to the lead, as **Team leads** sets out: never fewer.
2747
+ member publishes to the lead, as **Team leads** in `guide/lead.md` sets out: never fewer.
5400
2748
 
5401
2749
  ### `team-member-removed`
5402
2750
 
@@ -5419,6 +2767,14 @@ the `contacts` idle capability.
5419
2767
  Replaces this provider's complete scheduled-activity contribution. It is emitted only when the manifest
5420
2768
  declares the `calendar` idle capability.
5421
2769
 
2770
+ ### `phone-updated`
2771
+
2772
+ The phone as the platform now sees it, whole, replacing `Snapshot.phone`: a registration lost or
2773
+ back, do-not-disturb pressed or cleared, the handset lifted with no call or replaced, the desk
2774
+ phone's own mute. Emitted only by a provider whose manifest declares `phoneStatus`
2775
+ (`event.phone.capability`). The lead's member carries the same `PhoneState`, republished with the
2776
+ member. See **The phone's own view** in `guide/phone.md`.
2777
+
5422
2778
  ### `shift-updated`
5423
2779
 
5424
2780
  The agent's own day, whole, replacing `Snapshot.shift`: emitted whenever a total moves -- a task
@@ -5674,7 +3030,7 @@ cannot be established from TypeScript structure alone.
5674
3030
  | `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a forced break arriving in effect with `forced`. The harness applies the same rules after the connect snapshot. |
5675
3031
  | `assertAudioFollowsTheTask(envelopes, snapshot?)` | The audio follows the task and never decides it: every task is introduced once, `task-audio-started` and `task-audio-ended` alternate on work that has begun, audio ends only where it arrived, and what follows the audio ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no audio satisfies it by never testing it — pair it with the assertion that the audio end is present. |
5676
3032
  | `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
5677
- | `assertForcedBreakStopsTheRest(forcedOn, candidates, stopped)` | A forced break on one provider stops the agent everywhere else: capacity zero stated on every other usable provider holding capacity, nothing on the forcing one, nothing on a dead login. See **Forced breaks**. |
3033
+ | `assertForcedBreakStopsTheRest(forcedOn, candidates, stopped)` | A forced break on one provider stops the agent everywhere else: capacity zero stated on every other usable provider holding capacity, nothing on the forcing one, nothing on a dead login. See **Forced breaks** in `guide/breaks.md`. |
5678
3034
  | `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `on-break` only once nothing is outstanding — never beside a task, never later than the step that has none. |
5679
3035
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
5680
3036
  | `assertBrowserSessionIsolation(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
@@ -5693,151 +3049,3 @@ belongs in this contract is the boundary. A provider says what a control **is**
5693
3049
  capabilities and what its work is **called** through `phaseLabels` and `taskTypePresentation`; how
5694
3050
  any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
5695
3051
  override the agent's theme and font preferences.
5696
-
5697
- ## Independent task recording
5698
-
5699
- Recording is voice-only. A task can arrive already recording, including while pending, and offer
5700
- no recording controls. The provider publishes only its own current state on
5701
- the provider field of the task’s `recording` state. The agent application publishes its own full scoped view in `HostReport.recordings`.
5702
- A provider task update cannot replace agent application state. Neither a capability nor an accepted command
5703
- establishes recording. Missing state, lost recorder observation, transport loss and expired
5704
- observation mean unknown; they never mean stopped. Paused means a recording remains open without
5705
- capturing. Task hold, task pause and recording pause are independent.
5706
-
5707
- The per-task policy is `Task.capabilities.recording`, with independently optional provider and agent application
5708
- action sets. The outer capability may be locked. Absence grants no permission, and a state may
5709
- exist without any permission. A provider may offer only stop for a recording started automatically,
5710
- or withdraw a control on a later task update. There is no global recording mode and no automatic
5711
- start from a capability declaration. Agent application support is declared on `ConnectContext.host.recording`,
5712
- not the provider-owned manifest. The task names one of the storage places the agent application provisioned, by `storageId`;
5713
- an unknown storage place or an unsupported action is refused visibly, never redirected to provider
5714
- recording or a default upload location. Initial agent application support requires voice softphone audio and
5715
- capture of both local and remote audio; an agent application unable to capture either must refuse start/resume.
5716
- This package declares that contract; it supplies no recorder, audio mixing, storage or upload.
5717
-
5718
- | Action | Required current recording state | Confirmed outcome |
5719
- | --- | --- | --- |
5720
- | start | inactive | active with a new recording identity |
5721
- | pause | active | paused, preserving recording identity and captured audio |
5722
- | resume | paused | active with the same recording identity |
5723
- | stop | active or paused | inactive; finish and retain captured audio |
5724
- | cancel | active or paused | inactive; abandon and discard captured audio |
5725
-
5726
- Stop finishes the recording and retains captured audio. Cancel abandons the recording and discards
5727
- its captured audio; the task policy offers it with `cancel: true`. There is no configurable cancel
5728
- effect. A recorder that cannot confirm completion must not offer Cancel; it can offer Stop instead.
5729
- Cancel never means cancelling an in-flight start request or deleting arbitrary past recordings.
5730
- Stop can be applied only after finalization/retention succeeds; Cancel only after both cessation
5731
- and completion of this recording's audio succeed under the recorder's storage contract.
5732
- Partial success -- capture stopped but the storage outcome unknown, a start the recorder cannot
5733
- vouch for either way -- is a settled `failed`, under the code `omni.recording-unsettled`: the
5734
- command did not do what it promised, and the state the provider publishes with it is whatever
5735
- capture state is actually known. It is never `applied`, and never an unsettled promise the agent
5736
- application would be left to resync, since the provider knows exactly what it could not settle.
5737
- There is no request identity on a recording command: the outcome is the state, and what the
5738
- agent sees is the state. Retention is not a promise of sample-perfect audio.
5739
-
5740
- Provider commands go exclusively to `Connection.execute`; agent application commands go exclusively to
5741
- `HostRecording.execute`. Both include the assignment and observation identities; all
5742
- non-start commands identify the particular recording. IDs are opaque and scoped by provider login,
5743
- task and recorder owner. They are never inferred from filenames or current agent identity. A
5744
- recording ID survives pause/resume and reassignment only where the same recorder confirms continuity;
5745
- commands always name the current assignment. A later start gets a different ID. Reconnect does not
5746
- create a new recording or fresh evidence. After lost continuity, use unknown until reconciled.
5747
- There is no automatic restart, hand-over to another recorder, or stop on task hold/disconnect.
5748
- Task removal does not prove recording stopped: outstanding agent application recorders remain tracked by the
5749
- agent application until its executor reconciles/finishes them, with visible unresolved cleanup failures.
5750
-
5751
- Only start/resume require an in-progress or paused task with started audio. Pause/stop/cancel may
5752
- also finish an independently observed recorder while the task is completing. A pending task may
5753
- show active recording, but agent controls wait until interaction begins. The two recording paths may
5754
- both be active. A command to either path has no implied effect on the other.
5755
-
5756
- Each confirmed observation has a fresh opaque observation identity, a canonical UTC millisecond
5757
- observation instant and an exclusive expiry. These times describe current evidence, never historical
5758
- capture boundaries. A trusted observer-domain current time must satisfy observedAt <= now < validUntil.
5759
- Use `effectiveRecordingState` with that explicitly trusted time; an unavailable clock yields unknown.
5760
- Receipt, replay and task publication never extend freshness. Clock discontinuity invalidates evidence;
5761
- consumers using the explicit recording-expiry clock-estimation exception must invalidate that
5762
- estimate and use monotonic aging so clock rollback cannot
5763
- revive expired evidence. The executor compares observation identity and recording identity against
5764
- its latest state atomically before I/O. An intervening observation or assignment change refuses the
5765
- stale command; it never acts on a replacement recorder. Providers lacking trustworthy current-state
5766
- identity or observation time publish unknown and offer no issuable controls.
5767
-
5768
- Use `validateTask` and `validateTaskCommand` for published shape and static permissions.
5769
- Immediately before dispatch, additionally use `validateRecordingRequest` against the full current
5770
- task and explicit observer-domain time. For agent application commands also supply the current agent application declaration,
5771
- full agent application report and softphone context. The same checks must run at the executing boundary;
5772
- client validation alone is not authorization. The executor serializes operations on each recorder
5773
- and compares `observationId` and `recordingId` against its latest evidence before acting: a
5774
- command against superseded evidence is refused, and nothing is retried on the executor's own
5775
- account.
5776
-
5777
- Keep command progress in agent application UI separately from authoritative state. Applied means the requested
5778
- recording transition and any retain/discard effect actually completed; publish the confirming task or agent application report before
5779
- resolving applied. Failed means confirmed no effect. Rejected promise means outcome unknown:
5780
- show the failure, reconcile that path and do not automatically retry or pretend inactive. Authoritative
5781
- updates remain full current task/agent application views, not replayed provider events. These current-state fields
5782
- create no interaction-history entries and infer no actors, durations or historical capture boundaries.
5783
-
5784
- Migration is explicit: replace the old true recording capability with per-path action policies,
5785
- replace untargeted commands with scoped commands, and retain protocol version 1. During pre-release
5786
- development, hosts and adapters must align their recording contracts; negotiation does not detect
5787
- differences between package revisions that share that protocol number. No legacy-shape fallback is provided.
5788
- This change does not publish a package or enable recording in any existing agent application/provider by itself.
5789
-
5790
- `validateRecordingOutcome` checks confirming observations against recording-specific applied/failed
5791
- semantics, including pause/resume identity and rejection of a dialling result. It cannot prove audio
5792
- retention/completion or source truth from a status flag; those remain executor obligations.
5793
-
5794
- The storage place named on the task is bound when start actually creates a recording. Existing recordings
5795
- keep that binding through pause/resume/stop/cancel; a later policy cannot redirect their stored audio.
5796
- The executor rejects a mismatched storage place rather than moving or discarding another binding.
5797
- Action permission does not authorize unattended invocation: agent application controls require the agent's explicit
5798
- act, and provider authorization remains enforced at its authenticated command boundary.
5799
-
5800
-
5801
- Recording dispatch must receive the same known task-validation context as the published task.
5802
- Pass organisation levels and other known restrictions through the optional fourth argument of
5803
- `validateTaskCommand`, and through `taskContext` in `validateRecordingRequest`. This keeps a valid
5804
- custom organisation lock from being rejected against the default ladder, and prevents known
5805
- capability restrictions from disappearing at dispatch. The standalone `validateRecordingCommandState`
5806
- requires an affirmative permission; false, malformed permission declarations and unknown actions grant nothing.
5807
-
5808
-
5809
- ### Agent application recording announcements to the caller
5810
-
5811
- `HostRecording.announcesToCaller`, when true, is a guarantee that the agent application delivers audible recording
5812
- status messages to the remote party over the call's outgoing audio. It belongs inside the optional
5813
- agent application recording capability: without that capability there is no such guarantee. Omission makes no
5814
- promise; false is invalid. It does not belong in the provider manifest, the task's recording policy
5815
- or the general `HostGuarantees` object. A provider can inspect this agent application guarantee when deciding
5816
- whether to permit agent application recording on a task.
5817
-
5818
- The guarantee applies only to agent application-owned recordings. Providers implement announcements for their
5819
- own recordings at their end; provider state updates must not cause the agent application to announce those
5820
- recordings. Both recorders can operate independently on the same call.
5821
-
5822
- On a confirmed agent application start, the remote party hears that recording started. On a confirmed stop or
5823
- cancel, they hear that recording stopped. If pause/resume is supported, say paused/resumed so a
5824
- pause is not presented as a finished recording. Announcements follow actual capture transitions,
5825
- not button clicks, accepted requests or task-policy changes. When attaching to an already-active
5826
- agent application recording, announce that recording is active, without inventing its original start time.
5827
- Repeated observations of the same state do not repeat announcements. Unknown or expired evidence
5828
- must never produce a stopped announcement.
5829
-
5830
- A local sound, UI indicator, screen-reader message to the agent, or sound leaking through the
5831
- microphone does not fulfil this guarantee. The agent application must deliver the message directly in the audio
5832
- sent to the remote party, even when the agent microphone is muted. It may also notify the agent.
5833
- Only declare the guarantee when the agent application can provide that outgoing audio path. An announcement
5834
- that cannot be delivered, including after the remote party has disconnected, is a visible failure;
5835
- never claim delivery or replay the notice into a different call.
5836
-
5837
- For a command under this guarantee, applied requires both the recording action and its announcement
5838
- to succeed. If capture changes but announcement delivery fails, publish the actual recording state,
5839
- report the announcement failure visibly and answer `failed` under `omni.recording-unsettled`. Do not claim no
5840
- effect, automatically repeat the recording action or silently switch to an agent-only notice.
5841
-
5842
- `validateHostRecording` checks the declaration, including its true-or-absent guarantee. Actual audio
5843
- delivery remains the agent application implementation's responsibility and must be exercised in its own tests.