@xema/omni-protocol 0.1.100 → 0.1.102

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
@@ -22,7 +22,7 @@ are used precisely throughout and mean nothing looser here.
22
22
  | **Agent application** | The software the agent uses, such as Omni, whether web or desktop. It presents provider tasks and controls and manages local audio where supported. Existing API names use `Host` and `host` for this role. |
23
23
  | **Provider** | One independently connected external system: a voice platform, a chat platform, a mail platform. |
24
24
  | **Adapter** | The package implementing this contract for one provider. One adapter is one provider, so the words are often interchangeable; *provider* names the system, *adapter* the code speaking for it. |
25
- | **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; transfer destinations may include an agent when the provider publishes that directory item. |
25
+ | **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; a conference directory may include an agent when the provider publishes that directory item. |
26
26
  | **Lead** | An agent whose login declares `capabilities.lead`. The flag alone turns on the team feature in the agent application: the lead sees their members with their tasks and stats, and acts on them with the authority the flag carries. While the feature is on, the provider publishes a `TeamMembers` object to them and to nobody else: **the login is the permission**. |
27
27
  | **Local policy** | Rules configured by the agent application about this agent, outside the provider protocol and never sent to a provider. It gates whether an offer may be rejected, whether the agent goes ready on login, and whether tasks are auto-accepted. Where a capability and local policy disagree, the stricter wins. |
28
28
  | **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. The protocol never describes the IVR or the queue; it carries the call's history and details to whichever agent holds it now. |
@@ -40,9 +40,9 @@ are used precisely throughout and mean nothing looser here.
40
40
  | **Event** | One completed transaction reported after a snapshot established the baseline. |
41
41
  | **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
42
42
  | **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, team member list. |
43
- | **Dial** | One outbound call the agent application asks a provider to place — from the idle dialpad, a cold or warm transfer, a conference add, or a connect-back. Identified by the agent application's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
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
44
  | **Listen** | A lead's channel in a member's call, unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. It is a team act naming the member, never a task on the lead's desk; nothing of it reaches the member's task. A lead who wants the call takes it over. See **Listening to a call**. |
45
- | **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in from the moment their dial is placed. |
45
+ | **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone conferenced in from the moment their dial is placed. |
46
46
 
47
47
  These fields describe state within their containing objects. Both authentication and break
48
48
  state use `status`; name the object when discussing them to avoid ambiguity.
@@ -313,7 +313,6 @@ type RecordingState =
313
313
  type RecordingCommand = {
314
314
  type: "recording";
315
315
  source: RecordingSource;
316
- requestId: string;
317
316
  observationId: string;
318
317
  } & (
319
318
  | { action: "start"; recordingId?: never }
@@ -564,8 +563,7 @@ type TaskCapabilities<C extends Channel = Channel> =
564
563
  endCall?: Lockable<true>;
565
564
  terminateCall?: Lockable<true>;
566
565
  connectBack?: Lockable<true>;
567
- coldTransfer?: Lockable<DestinationDirectory>;
568
- warmTransfer?: Lockable<DestinationDirectory>;
566
+ schedule?: Lockable<true>;
569
567
  leadAssist?: Lockable<true>;
570
568
  conference?: Lockable<DestinationDirectory>;
571
569
  recording?: Lockable<TaskRecordingPolicy>;
@@ -675,7 +673,7 @@ type TaskCompletion =
675
673
  | { completionMode: "agent-command"; wrapAllowance?: DurationSeconds }
676
674
  | { completionMode: "provider-automatic"; wrapAllowance: DurationSeconds };
677
675
 
678
- type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
676
+ type OnCallRole = "party" | "agent" | "conferenced";
679
677
 
680
678
  type OnCallStage = "ringing" | "joined";
681
679
 
@@ -683,7 +681,7 @@ type OnCall = { since: IsoTimestamp; held?: true } & (
683
681
  | { role: "party"; dialId?: never; stage?: never }
684
682
  | { role: "party"; stage: OnCallStage; dialId?: DialId }
685
683
  | { role: "agent"; userId: UserId }
686
- | { role: "consulted" | "conferenced"; destinationId: string; stage: OnCallStage; dialId?: DialId; label?: string }
684
+ | { role: "conferenced"; destinationId: string; stage: OnCallStage; dialId?: DialId; label?: string }
687
685
  );
688
686
 
689
687
  type TaskLeadAssist = { note?: string; since: IsoTimestamp } & (
@@ -725,7 +723,7 @@ type Task<C extends Channel = Channel> = {
725
723
  party?: Contact;
726
724
  phase: TaskPhase;
727
725
  acceptance?: AcceptanceMode;
728
- previewEndsAt?: IsoTimestamp;
726
+ previewEndsInSeconds?: DurationSeconds;
729
727
  atDeadline?: PreviewDeadline;
730
728
  reference?: string;
731
729
  attributes?: TaskAttribute[];
@@ -745,7 +743,6 @@ type AcceptanceMode =
745
743
 
746
744
  type TaskOutcome =
747
745
  | { type: "completed"; by: "agent" | "provider" }
748
- | { type: "transferred"; destinationId?: string }
749
746
  | { type: "taken-over"; leadId: UserId }
750
747
  | { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
751
748
  | { type: "expired"; phase: "pending" | "confirmed" | "preview" }
@@ -765,10 +762,10 @@ const TASK_COMMAND_NAMES = {
765
762
  "end-call",
766
763
  "terminate-call",
767
764
  "connect-back",
768
- "transfer",
769
765
  "lead-assist",
770
766
  "conference",
771
767
  "recording",
768
+ "schedule",
772
769
  "complete",
773
770
  ],
774
771
  chat: ["accept", "decline", "pause", "resume", "complete"],
@@ -789,10 +786,7 @@ type VoiceTaskCommand =
789
786
  | { type: "end-call" }
790
787
  | { type: "terminate-call" }
791
788
  | { type: "connect-back"; dialId: DialId }
792
- | { type: "transfer"; action: "cold"; dialId: DialId; destinationId: string }
793
- | { type: "transfer"; action: "warm"; dialId: DialId; destinationId: string }
794
- | { type: "transfer"; action: "complete" }
795
- | { type: "transfer"; action: "cancel" }
789
+ | { type: "schedule"; at: IsoTimestamp; note?: string }
796
790
  | { type: "lead-assist"; action: "request"; note?: string }
797
791
  | { type: "lead-assist"; action: "cancel" }
798
792
  | { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
@@ -933,7 +927,7 @@ type BreakRequest = {
933
927
  };
934
928
 
935
929
  type ForcedBreak = {
936
- by: UserId;
930
+ by: UserId | "provider";
937
931
  expectedDurationMs?: number;
938
932
  };
939
933
 
@@ -977,10 +971,11 @@ type TeamMember = {
977
971
  break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
978
972
  tasks?: MemberTask[];
979
973
  listening?: MemberListening;
974
+ shift?: MemberShift;
975
+ request?: MemberRequest;
980
976
  };
981
977
 
982
- type LeadRequest = {
983
- memberId: UserId;
978
+ type MemberRequest = {
984
979
  assignmentId: AssignmentId;
985
980
  note?: string;
986
981
  since: IsoTimestamp;
@@ -1006,13 +1001,30 @@ type MemberTask<C extends Channel = Channel> = {
1006
1001
  : { onCall?: never; leadAssist?: never; takenOver?: never; audio?: never });
1007
1002
 
1008
1003
  type MemberListening = {
1004
+ assignmentId: AssignmentId;
1009
1005
  mode: ListeningMode;
1010
1006
  since: IsoTimestamp;
1011
1007
  };
1012
1008
 
1009
+ type ShiftEventKind = "signed-in" | "signed-out" | "break-started" | "break-ended";
1010
+
1011
+ type ShiftEvent = {
1012
+ at: IsoTimestamp;
1013
+ kind: ShiftEventKind;
1014
+ };
1015
+
1016
+ type MemberShift = {
1017
+ signedInAt: IsoTimestamp;
1018
+ signedOutAt?: IsoTimestamp;
1019
+ talkSeconds?: DurationSeconds;
1020
+ holdSeconds?: DurationSeconds;
1021
+ breakSeconds?: DurationSeconds;
1022
+ tasksHandled?: number;
1023
+ events?: ShiftEvent[];
1024
+ };
1025
+
1013
1026
  type TeamMembers = {
1014
1027
  members: TeamMember[];
1015
- requests: LeadRequest[];
1016
1028
  policies?: TeamPolicies;
1017
1029
  };
1018
1030
 
@@ -1103,19 +1115,20 @@ type ProviderEvent =
1103
1115
  | {
1104
1116
  type: "task-offered";
1105
1117
  task: Task;
1106
- assignmentExpiresAt?: IsoTimestamp;
1118
+ expiresInSeconds?: DurationSeconds;
1107
1119
  }
1108
1120
  | { type: "task-updated"; task: Task }
1109
1121
  | { type: "task-audio-started"; assignmentId: AssignmentId }
1110
1122
  | { type: "task-audio-ended"; assignmentId: AssignmentId }
1111
- | { type: "team-audio-started"; memberId: UserId; assignmentId: AssignmentId }
1112
- | { type: "team-audio-ended"; memberId: UserId; assignmentId: AssignmentId }
1113
1123
  | { type: "task-ended"; assignmentId: AssignmentId; outcome: TaskOutcome }
1114
1124
  | { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; assignmentId?: AssignmentId; destinationId?: string; reason?: string }
1115
1125
  | { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
1116
1126
  | { type: "queue-summary"; summary: QueueSummary }
1117
1127
  | { type: "diagnostic"; expected: string; observed: string; assignmentId?: AssignmentId }
1118
1128
  | { type: "team-updated"; team: TeamMembers }
1129
+ | { type: "team-member-updated"; member: TeamMember }
1130
+ | { type: "team-member-removed"; memberId: UserId }
1131
+ | { type: "team-policies-updated"; policies: TeamPolicies }
1119
1132
  | { type: "contacts-updated"; contacts: Contact[] }
1120
1133
  | { type: "calendar-updated"; calendar: ScheduledActivity[] };
1121
1134
 
@@ -1240,6 +1253,7 @@ const OMNI_FAILURE_CODES = [
1240
1253
  "omni.rate-limited",
1241
1254
  "omni.unavailable",
1242
1255
  "omni.break-already-committed",
1256
+ "omni.recording-unsettled",
1243
1257
  ] as const;
1244
1258
  type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
1245
1259
  ```
@@ -1411,8 +1425,8 @@ only where the corresponding capability is declared; an absent capability means
1411
1425
  there is no separate permission flag.
1412
1426
 
1413
1427
  The commands every task has are authorized by other fields the provider declared — a task that was
1414
- offered can be accepted, one in `preview` can be called, one whose `completionMode` is
1415
- `agent-command` can be completed. Nothing is issuable that the provider did not publish; only
1428
+ offered can be accepted, one in `preview` can be called, one with a wrap can be completed.
1429
+ Nothing is issuable that the provider did not publish; only
1416
1430
  which field says so varies. See **Which commands need a capability**.
1417
1431
 
1418
1432
  ### 5. Snapshots establish state; events report transactions
@@ -1606,11 +1620,11 @@ compile time.
1606
1620
  | `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
1607
1621
  | `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**. |
1608
1622
  | `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**. |
1609
- | `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 transfer, conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
1623
+ | `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**. |
1610
1624
  | `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; agent application polling is independently opt-in. |
1611
1625
  | `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
1612
1626
  | `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**. |
1613
- | `completionSettleMs` | Required. How long after an applied `complete` the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1627
+ | `completionSettleMs` | Required. How long after an applied `complete` the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1614
1628
 
1615
1629
  ### Authentication methods
1616
1630
 
@@ -2145,7 +2159,7 @@ surface in one place, and what obliges an adapter to implement each one.
2145
2159
  | `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**. |
2146
2160
  | `setCapacity(capacity)` | Always. Nothing may be assigned until a capacity is stated, so there is no connection that does not receive it. |
2147
2161
  | `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
2148
- | `getUserDetails(ids)` | The adapter publishes any `UserId`: on `ForcedBreak.by`, 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. |
2162
+ | `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. |
2149
2163
  | `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
2150
2164
  | `requestBreak(request)` | The login declares `capabilities.breaks`. |
2151
2165
  | `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
@@ -2200,8 +2214,8 @@ Turns `UserId` values into something an agent can read.
2200
2214
  getUserDetails(ids: UserId[]): Promise<User[]>
2201
2215
  ```
2202
2216
 
2203
- Required of any adapter that publishes a `UserId` — on `ForcedBreak.by`, a team member list, or
2204
- `history[].by`. Publishing an identifier Omni cannot resolve puts a name on screen
2217
+ Required of any adapter that publishes a `UserId` — on `ForcedBreak.by` where a person forced it, a
2218
+ team member list, or `history[].by`. Publishing an identifier Omni cannot resolve puts a name on screen
2205
2219
  that reads as a database key.
2206
2220
 
2207
2221
  - **Omit an id you cannot resolve; do not invent a name for it.** A missing entry says *I do not
@@ -2305,7 +2319,7 @@ declare const task: Task;
2305
2319
  const assignment = {
2306
2320
  type: "task-offered",
2307
2321
  task: { ...task, phase: "pending", acceptance: "consent" },
2308
- assignmentExpiresAt: "2026-08-25T10:41:07.000Z",
2322
+ expiresInSeconds: 30,
2309
2323
  } satisfies Extract<ProviderEvent, { type: "task-offered" }>;
2310
2324
  ```
2311
2325
 
@@ -2323,21 +2337,30 @@ on a desk phone — and is not in question per call. See **How the agent hears t
2323
2337
 
2324
2338
  Automatic acceptance still begins with `task-offered`.
2325
2339
 
2326
- `assignmentExpiresAt` is the deadline after which the offer lapses. Where present, Omni counts
2327
- down and stops offering **Accept** once it passes; the provider ends the lapsed offer with
2328
- `task-ended` and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.**
2329
- A provider that reports only elapsed ring time after the fact cannot say when an offer is due
2330
- to end, and a computed value would have Omni withdraw **Accept** from a task still pending.
2331
-
2332
- A preview's deadline is not on the offer. It travels on the task, as `previewEndsAt` with
2340
+ `expiresInSeconds` is how long the offer stands, in whole seconds from this event. Where present,
2341
+ Omni counts down from receipt and stops offering **Accept** once it runs out; the provider ends
2342
+ the lapsed offer with `task-ended` and an `expired` outcome naming `pending`, since nobody
2343
+ cancelled it. **Omit it unless the provider can observe it.** A provider that reports only elapsed
2344
+ ring time after the fact cannot say how long an offer has left, and a computed value would have
2345
+ Omni withdraw **Accept** from a task still pending.
2346
+
2347
+ **A deadline is seconds left, never an instant.** The agent application and the provider keep
2348
+ different clocks, and a countdown that compares one against the other is a countdown that runs
2349
+ fast or slow by their skew. Seconds from the publication that carries them are the provider's own
2350
+ arithmetic, counted down on the desk from the moment they arrive, and the transport delay is the
2351
+ only error. Restated on every publication that carries them, so a snapshot after a reconnect
2352
+ starts the countdown afresh from what is actually left (`event.taskOffered.expiresInSeconds`,
2353
+ `task.preview.previewEndsInSeconds`: a whole number, zero or more).
2354
+
2355
+ A preview's deadline is not on the offer. It travels on the task, as `previewEndsInSeconds` with
2333
2356
  `atDeadline`, so a snapshot carries it too -- see **Preview: the agent presses Call**.
2334
2357
 
2335
2358
  A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome, `by:
2336
2359
  "provider"`. **`cancelled` says who called the work off**, as `completed` says who completed it:
2337
2360
  `agent` for a decline, `provider` for a withdrawal or a re-route, `party` for a caller who abandoned
2338
2361
  the ring. One word for the three left a supervisor's record unable to tell them apart, so `by` is
2339
- required and closed (`event.taskEnded.outcome.cancelled.by`). An offer nobody acted on before
2340
- `assignmentExpiresAt` is not cancelled by anyone: it lapses, and ends `expired` naming `pending`.
2362
+ required and closed (`event.taskEnded.outcome.cancelled.by`). An offer nobody acted on while
2363
+ `expiresInSeconds` ran is not cancelled by anyone: it lapses, and ends `expired` naming `pending`.
2341
2364
 
2342
2365
  ### Tasks already in progress
2343
2366
 
@@ -2373,16 +2396,16 @@ time. Runtime conformance checks also require the task channel to match its prov
2373
2396
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
2374
2397
  | `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. |
2375
2398
  | `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**. |
2376
- | `previewEndsAt` | Voice only, in `preview`: the preparation target instant. Absent, the agent has as long as they need without a preparation countdown. Always with `atDeadline`. See **Preview: the agent presses Call**. |
2377
- | `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `provider-dials` makes the provider initiate dialing, `host-dials` makes the agent application issue Call, `waits` keeps the task in preview awaiting the agent. |
2399
+ | `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**. |
2400
+ | `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. |
2378
2401
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
2379
- | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
2380
- | `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. |
2402
+ | `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`). |
2403
+ | `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. `0` is no wrap at all, so a task with it publishes no `outcomes` (`task.outcomes.wrapAllowance`). |
2381
2404
  | `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. |
2382
2405
  | `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**. |
2383
- | `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer -- carrying a `stage` while being dialled again on the same task, a connect-back with the agent application's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where an agent application placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the agent application supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
2406
+ | `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**. |
2384
2407
  | `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**. |
2385
- | `takenOver` | Voice only, on a task that reached this agent by a lead's take-over: which member the call was taken from, and when. It is an ordinary assignment with the call's history, offered whatever break the lead is on and counted against no capacity. See **Lead assist**. |
2408
+ | `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**. |
2386
2409
 
2387
2410
  `TaskAttribute` entries carry typed detail alongside the task:
2388
2411
 
@@ -2418,12 +2441,10 @@ The canonical task transitions are:
2418
2441
  | `preview` | The call goes out and nobody answers | `completing` |
2419
2442
  | `preview` | The preparation target passes with `atDeadline: "waits"` | Remains `preview`, waiting for the agent to press Call |
2420
2443
  | `pending` | Provider withdraws the assignment, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
2421
- | `pending` | The offer lapses at `assignmentExpiresAt` | Removed by `task-ended` with `expired` outcome naming `pending` |
2444
+ | `pending` | The offer's `expiresInSeconds` runs out | Removed by `task-ended` with `expired` outcome naming `pending` |
2422
2445
  | No task | Snapshot reports work already underway | `in-progress` |
2423
2446
  | `in-progress` | Provider or agent pauses the task | `paused` |
2424
- | `in-progress` | Agent starts a warm transfer (`transfer` `warm`); the customer is parked | `paused` |
2425
2447
  | `paused` | Provider or agent resumes the task | `in-progress` |
2426
- | `paused` | Agent cancels a warm transfer (`transfer` `cancel`) | `in-progress` |
2427
2448
  | `in-progress` or `paused` | Contact interaction ends and follow-up work remains | `completing` |
2428
2449
  | `completing` | Agent connects back to the party (`connect-back`) | `in-progress` |
2429
2450
  | Any phase | Provider emits `task-ended` | Removed |
@@ -2468,9 +2489,9 @@ const previewed = {
2468
2489
  capabilities: {},
2469
2490
  phase: "preview",
2470
2491
  party: { name: "Maya Rao", number: "+919876543210" },
2471
- previewEndsAt: "2026-08-25T10:40:37.000Z",
2492
+ previewEndsInSeconds: 120,
2472
2493
  atDeadline: "provider-dials",
2473
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "party" | "previewEndsAt" | "atDeadline">;
2494
+ } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "party" | "previewEndsInSeconds" | "atDeadline">;
2474
2495
 
2475
2496
  // The agent presses Call. It is a dial like any other.
2476
2497
  const pressed: TaskCommand<"voice"> = { type: "dial", dialId: "dial-7f2" };
@@ -2488,8 +2509,9 @@ is work, not a cancellation. A `preview` task therefore needs a manifest that sa
2488
2509
  (`task.preview.dialOutcomes.required`).
2489
2510
 
2490
2511
  **Preparation and trigger ownership are per task.** Unlimited preparation omits both
2491
- `previewEndsAt` and `atDeadline`: only an agent pressing Call starts dialing. Fixed preparation
2492
- carries the provider's authoritative preparation-end instant and exactly one trigger owner:
2512
+ `previewEndsInSeconds` and `atDeadline`: only an agent pressing Call starts dialing. Fixed
2513
+ preparation carries the seconds the provider says are left, counted down on the desk from the
2514
+ publication that carried them and restated on every later one, and exactly one trigger owner:
2493
2515
 
2494
2516
  | Declaration | At preparation end |
2495
2517
  | --- | --- |
@@ -2534,14 +2556,14 @@ Submission acceptance is never an answered outcome or permission to publish `in-
2534
2556
  Unsupported source correlation/outcome/audio evidence remains an integration blocker.
2535
2557
 
2536
2558
  **Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
2537
- spreading the `preview` one carries `previewEndsAt` and `atDeadline` with it, and the agent application refuses
2559
+ spreading the `preview` one carries `previewEndsInSeconds` and `atDeadline` with it, and the agent application refuses
2538
2560
  the update (`task.preview.deadline.unexpected`): the desk keeps the task as it last stood, and the
2539
2561
  provider is told through `refused` exactly which rule, so the state that looked right on its side
2540
2562
  is named on its side. The task past preview has no deadline to wait for, so it carries neither
2541
2563
  field.
2542
2564
 
2543
- **Provider execution and agent application controls are distinct.** Answer, hold/resume, transfer (including
2544
- IVR routing), conference and interaction completion are executed by the provider
2565
+ **Provider execution and agent application controls are distinct.** Answer, hold/resume, conference,
2566
+ lead assist and interaction completion are executed by the provider
2545
2567
  through the adapter. The task's applicable capability, phase, completion mode and command-specific
2546
2568
  prerequisites decide which controls are offered. Caller disconnect also executes at the provider. Microphone mute is agent application-owned, never a provider
2547
2569
  TaskCommand or task capability. The separately declared agent application recording facility keeps its existing
@@ -2569,7 +2591,7 @@ become a caller-disconnect operation.
2569
2591
  **A task is never its audio.** A voice task represents the agent’s interaction: the call is offered when it is
2570
2592
  routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
2571
2593
  the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
2572
- consult, a conference leg joining or leaving, a transfer, a connect-back — the audio follows
2594
+ conference leg joining or leaving, a lead joining, a take-over, a connect-back — the audio follows
2573
2595
  separately, arriving on `task-audio-started`, attaching through `openAudio` and ending with
2574
2596
  `task-audio-ended`. Omni does not ring,
2575
2597
  bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
@@ -2593,6 +2615,19 @@ and `assertAudioFollowsTheTask` holds any sequence.
2593
2615
  the task open until Omni sends the channel's `complete` command. With `provider-automatic`, the
2594
2616
  provider may complete the task without receiving that command.
2595
2617
 
2618
+ **The agent may finish early under either mode.** `complete` is issuable wherever there is a wrap
2619
+ to cut short: under `agent-command` it is the end, and under `provider-automatic` it says the agent
2620
+ is done before the allowance ran out, and the provider is free to end the task at once. Only a task
2621
+ with no wrap at all -- `wrapAllowance: 0` -- has nothing to complete, and the command is refused
2622
+ there (`command.complete.wrapAllowance`). An ending the agent asked for says so, `completed` with
2623
+ `by: "agent"`, whichever mode the task was under.
2624
+
2625
+ **A required outcome is the agent's to give.** A task whose `outcomes` says `required: true` waits
2626
+ for a code the agent chooses, so it completes on the agent's command: `provider-automatic` cannot
2627
+ wait for it, and the pair is refused (`task.outcomes.required.mode`). Optional outcomes may ride on
2628
+ a task the provider completes itself; the agent gives one if in time. And a code is given in wrap,
2629
+ so a task with `wrapAllowance: 0` publishes no `outcomes` at all (`task.outcomes.wrapAllowance`).
2630
+
2596
2631
  `wrapAllowance` is independent of that decision. It is fixed, and when it starts depends on
2597
2632
  whether the channel carries real-time audio:
2598
2633
 
@@ -2609,8 +2644,9 @@ is optional: a conversation with nothing to wrap moves from `in-progress` to `ta
2609
2644
  `completing` is never published. Under `provider-automatic` with a non-zero `wrapAllowance` it is
2610
2645
  required, because the clock has to start somewhere: a chat or email task the provider completes
2611
2646
  from `in-progress` with an allowance to run gave the agent none of it, and the stream refuses the
2612
- ending (`stream.taskEnded.unwrapped`). Under `agent-command` the agent's `complete` is the end,
2613
- from `in-progress` or from `completing` alike.
2647
+ ending (`stream.taskEnded.unwrapped`); an ending the agent's own `complete` brought forward says
2648
+ `by: "agent"` and is not that. Under `agent-command` the agent's `complete` is the end, from
2649
+ `in-progress` or from `completing` alike.
2614
2650
 
2615
2651
  ```ts
2616
2652
  const emailCompletion = {
@@ -2622,8 +2658,9 @@ const emailCompletion = {
2622
2658
  In this example, the agent has two minutes after sending the email to add notes, select a
2623
2659
  outcome, and complete the task.
2624
2660
 
2625
- `0` means completion may happen immediately. With `provider-automatic`, the provider may complete
2626
- without waiting for a command; with `agent-command`, it still waits for `complete`.
2661
+ `0` means there is no wrap: under `provider-automatic` the provider completes the task at the
2662
+ end of the interaction, and under `agent-command` it completes on the interaction's end without
2663
+ waiting for `complete`, which has nothing to cut short and is refused.
2627
2664
 
2628
2665
  There is no value meaning "unlimited", because a number that is not a duration would be read as
2629
2666
  one. A provider that imposes no deadline says so by **omitting** `wrapAllowance`, which
@@ -2663,7 +2700,7 @@ deadline.
2663
2700
 
2664
2701
  **The room shows the party being dialled.** From the moment any agent application dial places the party -- a
2665
2702
  dialpad call, a preview Call, a connect-back -- the `party` entry carries `stage: "ringing"` and
2666
- the agent application's `dialId`, exactly as a consulted or conferenced entry does from its dial; `joined` on
2703
+ the agent application's `dialId`, exactly as a conferenced entry does from its dial; `joined` on
2667
2704
  the publication that follows the answered `dial-outcome` and never before it
2668
2705
  (`stream.taskUpdated.stage`); and no stage from the next publication on, so `joined` is transient
2669
2706
  and a later copy still carrying it is stale (`stream.taskUpdated.stage.lingering`). An agent application shows a
@@ -2725,16 +2762,22 @@ Migration from the earlier spellings:
2725
2762
  | HANDLING_STEPS_WITH_A_PERSON, HANDLING_STEPS_THAT_DIAL, and their INTERACTION spellings | `HISTORY_STEPS_WITH_A_PERSON`, `HISTORY_STEPS_THAT_DIAL` |
2726
2763
  | handlingStepExpectsAPerson, handlingStepDials, and their interaction spellings | `historyStepExpectsAPerson`, `historyStepDials` |
2727
2764
  | handleSeconds | `interactionSeconds` |
2728
- | Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `assignmentExpiresAt` |
2765
+ | Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `expiresInSeconds` |
2729
2766
  | Task.id, TaskId, and `taskId` on every event, command, report and lead request | gone: a task is named by `assignmentId` alone, and `assignmentKey(providerId, assignmentId)` scopes it |
2730
2767
  | 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`) |
2731
2768
  | Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
2732
2769
  | the call command, atDeadline calls and host-calls | `dial`, `"provider-dials"`, `"host-dials"` |
2733
- | 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`, `team-audio-started`, `team-audio-ended`, `audio` on the task, `TaskAudioState`, `openAudio`, `OpenAudioRequest`, `OpenAudioResult`, `CallAudio` on the result's `audio` |
2770
+ | 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 |
2734
2771
  | capabilitySource values ungoverned and undetermined | `nobody`, `not-yet-read` (`capabilitySource.notYetRead`) |
2735
2772
  | Snapshot.scheduledActivities, and scheduledActivities on calendar-updated | `calendar` in both |
2736
2773
  | availability reserved | `elsewhere` |
2737
2774
  | recording destinationId, HostRecording.destinationIds | `storageId`, `storageIds` (`recording.storage`, `recording.host.storage`) |
2775
+ | 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 |
2776
+ | assignmentExpiresAt on task-offered, previewEndsAt on the task | `expiresInSeconds`, `previewEndsInSeconds`: seconds left from the publication, never an instant (`event.taskOffered.expiresInSeconds`, `task.preview.previewEndsInSeconds`) |
2777
+ | TeamMembers.requests, LeadRequest | `TeamMember.request` (`MemberRequest`): the ask rides on the member (`team.member.request.*`) |
2778
+ | 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`) |
2779
+ | RecordingCommand.requestId | gone: no request identity; a partial effect is a settled `failed` under `omni.recording-unsettled` |
2780
+ | command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only with no wrap |
2738
2781
 
2739
2782
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
2740
2783
  History and report rule names use `history` and `historyReport`; assignment rules use
@@ -2847,7 +2890,7 @@ observed. No other step has it (`task.history.mutedBy`, `.mutedBy.unexpected`).
2847
2890
  **A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
2848
2891
  each end a dial, so the entry carries the `destinationId` it went to and, where an agent application placed it, the
2849
2892
  `dialId` the agent application minted; no other step dialled, and either field on one is refused
2850
- (`task.history.dialId.unexpected`). This is how a dial made before a transfer is placeable
2893
+ (`task.history.dialId.unexpected`). This is how a dial made before a take-over is placeable
2851
2894
  by whoever holds the task now: a `dial-outcome` naming a `dialId` the agent application never minted is looked
2852
2895
  up in the record, not discarded.
2853
2896
 
@@ -2860,7 +2903,7 @@ Four rules a provider has to keep:
2860
2903
  - **Omit `seconds` while it is unknown. Never send `0`.** A leg still talking is not a zero-second
2861
2904
  conversation, and on live data that is the ordinary case rather than an edge. A zero is rejected.
2862
2905
  - **`by` is a bare `UserId`, and not necessarily an agent.** A lead or a manager takes part
2863
- during an interaction too — a transfer accepted, a call conferenced in — so the field names whoever it was,
2906
+ during an interaction too — a call taken over, a call conferenced in — so the field names whoever it was,
2864
2907
  the same way `ForcedBreak.by` does. It comes from this provider's own directory, the same
2865
2908
  namespace as `AuthenticationState.identity.id` and the team member list, so entries pair
2866
2909
  within a provider and never across one.
@@ -2931,12 +2974,13 @@ can be muted by the agent application, and answers `recorded` with the canonical
2931
2974
  `.mutedBy.unexpected`). On a desk phone the microphone is the phone's:
2932
2975
  the agent application mutes nothing and records nothing.
2933
2976
 
2934
- **The provider's confirmed end is decisive.** The agent application keeps local state for interaction and
2935
- reporting, but follows the provider's authoritative state. When the provider publishes the end
2936
- of the current mute leg, the agent application ends that local mute and releases its agent application-controlled mute
2937
- on the matching task's audio. It does not wait for its own timer, reopen the leg, replace the
2938
- provider timestamp, or overwrite the provider's final duration with a later agent application report.
2939
- A later agent mute is a new leg, never a reopening of the ended one.
2977
+ **The record is a record.** The microphone is the agent application's, and nothing in the
2978
+ history moves it: a `muted` entry the provider publishes, or closes with a final `seconds`, is the
2979
+ provider's account of a leg the agent application reported, and the mute itself ends only when the
2980
+ agent, or the station, ends it. The agent application keeps its own mute state, reports each leg
2981
+ as it begins and ends, and takes the provider's published entry as the closed account of that leg:
2982
+ it does not reopen the leg, replace the provider's timestamp, or overwrite the provider's final
2983
+ duration with a later report. A later agent mute is a new leg, never a reopening of the ended one.
2940
2984
 
2941
2985
  Correlate the provider-published `muted` entry using the provider-selected `at` acknowledged
2942
2986
  for the current report key; a final `seconds` closes that leg under the history contract.
@@ -3194,10 +3238,9 @@ See **Which commands need a capability**.
3194
3238
  | `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
3195
3239
  | `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
3196
3240
  | `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**. |
3197
- | `terminateCall` | Primary button: Terminate call | The provider ends the caller channel. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
3241
+ | `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**. |
3198
3242
  | `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**. |
3199
- | `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
3200
- | `warmTransfer` | Secondary menu item: Warm transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Warm transfer**. |
3243
+ | `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**. |
3201
3244
  | `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**. |
3202
3245
  | `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**. |
3203
3246
  | `recording` | Overflow menu item: Recording | Per-task provider and agent application policies expose only their permitted recording actions; each has independent state and routing. |
@@ -3205,10 +3248,10 @@ See **Which commands need a capability**.
3205
3248
 
3206
3249
  ### Publishing codes and destinations
3207
3250
 
3208
- Four capabilities accept an object when the provider wants Omni to render real choices. For
3209
- `outcomes` alone, `true` remains valid and means "offer the control with nothing published";
3210
- the three directory controls -- `coldTransfer`, `warmTransfer`, `conference` -- carry their
3211
- directory or are refused, since once nothing is typed the directory is the control.
3251
+ Two capabilities accept an object when the provider wants Omni to render real choices. For
3252
+ `outcomes`, `true` remains valid and means "offer the control with nothing published"; the one
3253
+ directory control, `conference`, carries its directory or is refused, since once nothing is typed
3254
+ the directory is the control.
3212
3255
 
3213
3256
  #### `outcomes`
3214
3257
 
@@ -3254,11 +3297,11 @@ capabilities: {
3254
3297
  With `outcomes: true` Omni shows a Complete control and sends `complete` with no code, because
3255
3298
  the provider published none.
3256
3299
 
3257
- #### `coldTransfer`, `warmTransfer` and `conference`
3300
+ #### `conference`
3258
3301
 
3259
3302
  ```ts
3260
3303
  capabilities: {
3261
- coldTransfer: {
3304
+ conference: {
3262
3305
  destinations: [
3263
3306
  { id: "tier2", label: "Tier 2 support" },
3264
3307
  { id: "main-menu", label: "Back to the main menu" },
@@ -3270,7 +3313,7 @@ capabilities: {
3270
3313
  | Field | Contract |
3271
3314
  | --- | --- |
3272
3315
  | `destinations` | The items the control offers, at least one, with unique `id` values. Each is a button or a menu item on the desk. |
3273
- | `id` | What Omni sends as `destinationId` on a `transfer` or `conference` command, and what the provider executes. |
3316
+ | `id` | What Omni sends as `destinationId` on a `conference` command, and what the provider executes. |
3274
3317
  | `label` | What the agent reads. |
3275
3318
 
3276
3319
  **The protocol does not say what an item does.** A queue, an IVR menu, an outside line, a
@@ -3282,7 +3325,7 @@ reason (`task.destinations.shape`): once nothing is typed, the directory is the
3282
3325
 
3283
3326
  **The provider chooses the supported destinations.** A published item may route to an IVR,
3284
3327
  queue, named agent or another configured destination. No destination kind is inferred from its
3285
- label. The agent application offers only the directory attached to this task's specific transfer capability,
3328
+ label. The agent application offers only the directory attached to this task's `conference` capability,
3286
3329
  and sends the exact destination ID; it does not invent an agent picker or arbitrary dial target.
3287
3330
  The provider rechecks eligibility and executes the routing. Returning to an IVR or queue does not
3288
3331
  end the caller journey; a later assignment to the same agent is a new interaction.
@@ -3294,18 +3337,19 @@ provider. Two of them end the agent's part; they differ in what happens to the c
3294
3337
 
3295
3338
  ```ts
3296
3339
  { type: "end-call" } // the agent's channel and the channels the agent added end; the caller continues
3297
- { type: "terminate-call" } // the caller channel ends
3340
+ { type: "terminate-call" } // every channel on the call ends
3298
3341
  { type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
3299
3342
  { type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
3300
3343
  ```
3301
3344
 
3302
3345
  **`end-call` ends my part.** The agent's channel and every channel the agent added -- a
3303
- conferenced colleague, a consulted destination -- end, and the caller continues on the path the
3346
+ conferenced colleague, a lead who joined -- end, and the caller continues on the path the
3304
3347
  provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
3305
3348
 
3306
- **`terminate-call` ends the caller channel.** The call is over for the caller. Gated by
3307
- `terminateCall`, a separate permission, since ending a customer's call is a different thing from
3308
- putting my own side down.
3349
+ **`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
3350
+ the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
3351
+ line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
3352
+ different thing from putting my own side down.
3309
3353
 
3310
3354
  Both are commands to the provider through `execute`, and nothing happens on the desk until the
3311
3355
  provider reports it: the agent's audio ends on `task-audio-ended`, the task moves to `completing`,
@@ -3321,53 +3365,33 @@ when it acts.
3321
3365
  for the rest; it is gated by `conference`, since it only means something with a third person on
3322
3366
  the line. Removing the party leaves the agent with the colleague, a warm hand-over in reverse. A
3323
3367
  remove that would leave the agent alone is not a remove but an `end-call`, and a provider answers
3324
- it `failed`. The consulted destination of a warm transfer is not removed this way: `transfer`
3325
- `cancel` is its own step, which returns the agent to the parked customer, and a remove never
3326
- promises that.
3327
-
3328
- #### Warm transfer
3368
+ it `failed`.
3329
3369
 
3330
- A warm transfer parks the customer, calls the destination so the agent can speak to it first, and
3331
- then either hands the customer over or returns to them. Cold and warm are the words agents use;
3332
- telephony says blind and consult. It is its own capability, distinct from `coldTransfer` (a
3333
- hand-over with nobody spoken to first) and from `conference` (everybody on one call): a queue may
3334
- offer any of the three without the others, and each is declared on its own.
3370
+ **There is no transfer.** The agent hands the call to nobody, cold or warm: help arrives by
3371
+ **Lead assist**, where a lead joins the call or takes it over, and a call that changes hands does
3372
+ so on the platform's own routing -- an IVR return, a queue, a lead's take-over -- with the record
3373
+ saying so in a `transferred` step and the `transfers` total. Nothing the agent commands moves the
3374
+ caller to somebody else.
3335
3375
 
3336
- ```ts
3337
- // 1. Warm. A dial: the provider parks the customer and calls the destination, answers
3338
- // `dialling`, and the task reports `paused` with the destination `consulted` on `onCall`
3339
- // while the call to it stands.
3340
- { type: "transfer", action: "warm", dialId: "dial-7f2", destinationId: "tier2" }
3376
+ #### Scheduling a follow-up
3341
3377
 
3342
- // 2a. Hand the customer to the consulted destination and leave.
3343
- { type: "transfer", action: "complete" }
3378
+ An agent on a call promises to call back on Thursday, or in wrap writes up that the customer wants
3379
+ a callback once the refund lands. The follow-up goes on the calendar, and the platform owns the
3380
+ calendar: `schedule` asks the provider to put it there.
3344
3381
 
3345
- // 2b. Or drop the destination and return to the customer.
3346
- { type: "transfer", action: "cancel" }
3382
+ ```ts
3383
+ // From the call, or from its wrap: the time, and a note where the agent wrote one.
3384
+ { type: "schedule", at: "2026-08-28T10:00:00Z", note: "Call back about the refund" }
3347
3385
  ```
3348
3386
 
3349
- `warm` is gated by the `warmTransfer` capability and names a directory item exactly as a cold
3350
- transfer does, from the same kind of directory. It is a dial, so it carries the agent application's `dialId`
3351
- and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
3352
- the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
3353
- is what makes `complete` and `cancel` issuable -- they name no destination because there is exactly
3354
- one they could mean, which is why a task carries at most one `consulted` entry
3355
- (`task.onCall.consulted.single`). A consultation that could be started but not finished would
3356
- strand the customer and the destination both, which is why all three are commands and a provider
3357
- that offers `warmTransfer` implements all three.
3358
-
3359
- `applied` on `complete` says the provider is bridging the customer to the destination and
3360
- dropping the agent's leg. What follows is what follows any call the agent leaves: the agent's
3361
- audio ends and the provider reports `task-audio-ended`, the task moves to `completing`, any wrap
3362
- allowance runs, and the task ends `completed` as any call does. A warm complete is not a completion:
3363
- the agent has a wrap to do, so nothing is owed within `completionSettleMs`. `transferred` names a
3364
- cold transfer alone, where the agent had no wrap. `applied` on `cancel` says the destination is
3365
- dropped; the task returns to `in-progress` with the `consulted` entry gone. Omni waits for the
3366
- provider's report of both, as it does for every command.
3367
-
3368
- A destination that does not answer is a dial that ended: the provider reports its `dial-outcome`,
3369
- returns the task to `in-progress` with no `consulted` entry, and the agent is back with the
3370
- customer.
3387
+ `schedule` is gated by the `schedule` capability and issuable in `in-progress`, `paused` or
3388
+ `completing` (`command.phase.interaction`): a follow-up is promised on the call and written up in
3389
+ wrap alike, and the task names the party it is for. `applied` says the provider has taken it; the
3390
+ activity itself arrives on `calendar-updated`, which is the provider's word that it stands, as
3391
+ every calendar entry does. The command carries a time and a note and nothing else
3392
+ (`command.schedule.at`, `command.schedule.note`, `command.field`): what the platform makes of a
3393
+ follow-up -- a campaign record, a reminder, a scheduled dial -- is its own, and the agent sees it
3394
+ on the calendar.
3371
3395
 
3372
3396
  ### Chat capabilities
3373
3397
 
@@ -3444,7 +3468,7 @@ inside Omni.
3444
3468
 
3445
3469
  ## Every dial has an outcome
3446
3470
 
3447
- Five commands place a call: `dial()` from the idle dialpad, the `dial` command from preview, a cold or warm `transfer`, a
3471
+ Four commands place a call: `dial()` from the idle dialpad, the `dial` command from preview, a
3448
3472
  `conference` `add`, and `connect-back`. Each is accepted or refused at once, and each then ends later
3449
3473
  and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
3450
3474
  late in a call routinely outlives the call. Nothing in between is reported: the wire says
@@ -3466,13 +3490,13 @@ expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
3466
3490
  // 3. On the outcome, however late, against the task it belonged to.
3467
3491
  const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", assignmentId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
3468
3492
 
3469
- // 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
3493
+ // 4. In the record, so a dial made before a take-over is placeable by whoever holds the task now.
3470
3494
  const step: TaskHistoryStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
3471
3495
  ```
3472
3496
 
3473
3497
  The identity is a correlation handle between agent application and provider and **is never shown to the
3474
3498
  agent**. An agent application holding several tasks places an outcome by the `dialId` it minted; one it did not
3475
- mint -- a dial made by the previous agent before a transfer -- it finds on the task's `onCall` or in
3499
+ mint -- a dial made by the previous agent before a take-over -- it finds on the task's `onCall` or in
3476
3500
  its `history`, which is why the steps that dial carry it. The `dialId` is not a retry key:
3477
3501
  Omni never repeats a dial, and a command still carries no key for that purpose.
3478
3502
 
@@ -3505,7 +3529,7 @@ call it belonged to and never against the next one.
3505
3529
  The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which an agent application calls when it
3506
3530
  places one; an entry on a task's `onCall`; and a step in its `history`. Since a dialled entry
3507
3531
  is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- a
3508
- cold transfer, a connect-back, an idle dialpad call -- or whose entry has already left the room; an
3532
+ connect-back, an idle dialpad call -- or whose entry has already left the room; an
3509
3533
  outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
3510
3534
 
3511
3535
  **`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
@@ -3523,8 +3547,8 @@ provider does report -- `busy`, `no-answer`, `unreachable`, `rejected` or `cance
3523
3547
  (`manifest.dialOutcomes.unexplained.alone`): a provider that reports causes may honestly say the
3524
3548
  switch gave none, while one that reports none would be sending the generic failure this set exists
3525
3549
  to refuse. A provider that dials at all declares it: an idle
3526
- dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `connectBack`,
3527
- `coldTransfer`, `warmTransfer` or `conference` under a manifest without it is refused
3550
+ dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `connectBack`
3551
+ or `conference` under a manifest without it is refused
3528
3552
  (`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
3529
3553
  error there.
3530
3554
 
@@ -3542,10 +3566,10 @@ const onCall: OnCall[] = [
3542
3566
  ```
3543
3567
 
3544
3568
  `party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
3545
- neither carries a destination. `consulted` and `conferenced` were, so both carry the
3569
+ neither carries a destination. A `conferenced` colleague was, so the entry carries the
3546
3570
  `destinationId` of the directory item and, where an agent application's dial brought them, its `dialId`; a person
3547
3571
  the platform added itself carries none. `held: true` is presence as claim. A snapshot establishes state, so an agent application that attaches
3548
- after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
3572
+ after a take-over reads the room from here rather than inferring it from a sequence of outcomes it
3549
3573
  never saw.
3550
3574
 
3551
3575
  **`onCall` describes this agent's current interaction.** When the caller disconnects but the agent
@@ -3556,7 +3580,7 @@ agents' channels ended. A published room receives a final empty view for this in
3556
3580
  task may remain open for wrap. Completion is a separate task action, not the caller's disconnect.
3557
3581
 
3558
3582
  **A field that describes the present is cleared by the transition that ends it.** `onCall`,
3559
- `previewEndsAt` and `atDeadline` are three instances of one shape, and there will be more: each
3583
+ `previewEndsInSeconds` and `atDeadline` are three instances of one shape, and there will be more: each
3560
3584
  describes a state that is true now, and each is carried past the moment it stops being true by the
3561
3585
  most natural implementation there is, building the next task by spreading the last one. The
3562
3586
  provider's own state looks right, the claim on the wire is stale, and only the agent application sees it. An
@@ -3567,11 +3591,10 @@ describing the room live was never followed by one saying it had emptied, and ev
3567
3591
  derived anything from it was wrong in a way that looked like its own bug.
3568
3592
 
3569
3593
  **A dialled entry is listed from the moment the dial is placed, not from the answer.** The entry is
3570
- what makes `transfer` `cancel` issuable and what `conference` `remove` names, and a destination the
3571
- agent cannot call off while it rings is a parked customer with no way back -- which is the very case
3572
- `cancelled` exists for. Consult and conference behave alike here: the `consulted` or `conferenced`
3573
- entry appears with its `dialId` when the dial is placed, and a `conference` `remove` naming a
3574
- still-ringing `destinationId` calls that dial off, with `cancelled` as its outcome.
3594
+ what `conference` `remove` names, and a destination the agent cannot call off while it rings is a
3595
+ customer kept waiting with no way back -- which is the very case `cancelled` exists for. The
3596
+ `conferenced` entry appears with its `dialId` when the dial is placed, and a `conference` `remove`
3597
+ naming a still-ringing `destinationId` calls that dial off, with `cancelled` as its outcome.
3575
3598
 
3576
3599
  **The entry says where it stands.** A dialled entry carries `stage`: `ringing` until its dial is
3577
3600
  answered, `joined` after. The `dial-outcome` is the transition and the stage is the state, the same
@@ -3584,9 +3607,8 @@ whose move the stream cannot hold to an outcome: a clean stream proves nothing a
3584
3607
  than left to whoever placed the dial because that knowledge lives in one process: after a reload,
3585
3608
  on a second session, or on a supervisor's screen the snapshot is the only source, and a room that
3586
3609
  cannot tell ringing from joined says it contains someone who may never arrive. Nobody ringing is
3587
- held (`task.onCall.held.ringing`). A consult destination that never answers is a dial that
3588
- ended: the provider returns the task to `in-progress` with no `consulted` entry, as **Consult
3589
- transfer** states, so the customer is never left parked by a conforming provider.
3610
+ held (`task.onCall.held.ringing`). A colleague who never answers is a dial that ended: the
3611
+ provider restates the room without the entry, and the agent is back with the customer.
3590
3612
 
3591
3613
  The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
3592
3614
  they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
@@ -3746,7 +3768,16 @@ Without that evidence, show the duration without inventing a start or return tim
3746
3768
 
3747
3769
  A break applies to the **agent**, not to one provider. When a provider forces one, Omni immediately
3748
3770
  requests a break on every other connected provider, or they would keep routing work to somebody who
3749
- is not there. Providers should expect that follow-on request.
3771
+ is not there. On those providers it is the agent's own request, made through `requestBreak` like
3772
+ any other, and the provider grants or denies it as its policy says. **The agent application
3773
+ guarantees the agent cannot cancel those requests, or end the breaks they become,** while the
3774
+ forced break stands: the agent did not ask for them, and a break one provider imposed is not one
3775
+ the agent may lift on another. When the forcing provider ends the forced break, the agent
3776
+ application ends the follow-on breaks it made. Providers should expect that follow-on request.
3777
+
3778
+ `ForcedBreak.by` says who forced it: a lead, by user id, or `provider` where the platform itself
3779
+ did -- on its own rule, a schedule, a compliance hold. The agent sees who, and `getUserDetails()`
3780
+ is owed for a person, never for `provider` (`break.forced.by`).
3750
3781
 
3751
3782
  ## Capacity and break actions
3752
3783
 
@@ -4114,15 +4145,16 @@ with the authority the flag carries. Nothing the lead does here is a task on the
4114
4145
  who also takes calls is an agent like any other on the agent side, and a lead who does not want
4115
4146
  the agent role for now places themself on a break of a working kind and keeps the team feature on.
4116
4147
 
4117
- `Snapshot.team` carries a `TeamMembers` object, replaced whole by `team-updated`, and the provider
4118
- keeps it current: when a member is offered a call, answers, holds, mutes or is transferred, the
4119
- list is republished, so the lead's screen moves when the member's does. The agent application asks
4120
- for nothing and computes nothing.
4148
+ The team reaches the lead whole once, on `team-updated` after the lead switches the feature on
4149
+ and on every snapshot while it is on, and from then on one member at a time: when a member is
4150
+ offered a call, answers, holds, mutes or is taken over, the provider publishes that member whole
4151
+ on `team-member-updated`, so the lead's screen moves when the member's does and a team of two
4152
+ hundred is never resent because one of them held. The agent application asks for nothing and
4153
+ computes nothing. See **`team-updated`** and its companions.
4121
4154
 
4122
4155
  | Field | Contract |
4123
4156
  | --- | --- |
4124
4157
  | `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. |
4125
- | `requests` | The members currently asking a lead to join their call, each naming the member, the assignment and the note. Always carried, `[]` when nobody is asking. See **Lead assist**. |
4126
4158
  | `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**. |
4127
4159
 
4128
4160
  | `TeamMember` field | Contract |
@@ -4131,8 +4163,10 @@ for nothing and computes nothing.
4131
4163
  | `availability` | Required. What the member is doing now. |
4132
4164
  | `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
4133
4165
  | `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
4134
- | `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, trimmed by the provider. The assignment, title, task type, phase, party, room, audio and the full history are what the lead reads; the workspace — controls, their source, browsers, completion terms — is the member's and travels only if the provider sends it. `[]` when the member holds none; omitted only where the provider cannot see them. |
4135
- | `listening` | Present while this lead's channel is in this member's call — listening, coaching or joined — with the `mode` they are heard in and since when. One member's call at a time (`team.listening.single`). |
4166
+ | `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, trimmed by the provider. The assignment, title, task type, phase, party, room and audio are what the lead reads; the task's history is carried in the same shape as on the member's desk, as much of it as the provider chooses to send; the workspace — controls, their source, browsers, completion terms — is the member's and travels only if the provider sends it. `[]` when the member holds none; omitted only where the provider cannot see them. |
4167
+ | `listening` | Present while this lead's channel is in this member's call — listening, coaching or joined — naming which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. Its appearance is what the lead's audio opens on. One member's call at a time (`team.listening.single`). |
4168
+ | `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**. |
4169
+ | `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. Omitted where the provider cannot say. |
4136
4170
 
4137
4171
  Each availability value means one thing:
4138
4172
 
@@ -4144,11 +4178,16 @@ Each availability value means one thing:
4144
4178
  | `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**. |
4145
4179
  | `signed-out` | Known to this team but not signed in to this provider. |
4146
4180
 
4147
- **Always publish the complete team member list, never a change to it.** Team presence typically reaches an
4148
- adapter over a best-effort channel with no ordering and no delivery guarantee, so a stream of deltas
4149
- cannot be trusted to reconstruct the truth. The adapter reconciles against its own authoritative
4150
- read and publishes the result. That includes the members' tasks: a hold on a member's desk is a
4151
- republished list on the lead's.
4181
+ **Publish the whole team once, then each member whole.** Team presence typically reaches an
4182
+ adapter over a best-effort channel with no ordering and no delivery guarantee, so the adapter
4183
+ reconciles against its own authoritative read before it publishes anything. What it publishes is
4184
+ the whole team on `team-updated` -- after the switch, and on every snapshot -- and after that one
4185
+ member at a time on `team-member-updated`, carrying the member whole: their availability, their
4186
+ tasks, their listening, their shift, their request, as they now stand. Applying the same member
4187
+ twice changes nothing, so a redelivery is harmless, and a member the team never carried is added
4188
+ by it, as a colleague signing in is. `team-member-removed` takes one off. Never a field of a
4189
+ member, never a task of theirs on its own: a hold on a member's desk is that member republished
4190
+ whole on the lead's, and nothing else on the lead's screen moves.
4152
4191
 
4153
4192
  **Omit `since` rather than inventing one.** Omni renders it as a duration, so a timestamp
4154
4193
  synthesised from the adapter's own clock at seed time reads as "on task for 0 seconds" for
@@ -4157,23 +4196,32 @@ knows when the state actually began. It times the current `availability`, so it
4157
4196
  that value does.
4158
4197
 
4159
4198
  **The login is the permission.** A team member list goes to a login that declares
4160
- `capabilities.lead`, on every snapshot while the team feature is on, and to nobody else. Omni never
4161
- decides who leads a team: the provider said so at sign-in, and the team member list agrees with it
4162
- — present, `members: []` included, for a lead; absent for everybody else, which is the correct
4163
- rendering for an agent who leads nobody (`team.required`, `team.unentitled`). What the lead may do
4164
- with it comes with the flag: there is no per-action permission on the login or on the list.
4199
+ `capabilities.lead`, once the team feature is on, and to nobody else. Omni never decides who
4200
+ leads a team: the provider said so at sign-in, and the team member list agrees with it — owed,
4201
+ `members: []` included, once a lead has switched the feature on; absent for everybody else, which
4202
+ is the correct rendering for an agent who leads nobody (`team.required`, `team.unentitled`). What
4203
+ the lead may do with it comes with the flag: there is no per-action permission on the login or on
4204
+ the list.
4165
4205
 
4166
4206
  **The lead switches the feature on and off, and the provider is told.** A lead may work as an
4167
4207
  agent alone, as agent and lead, or as lead alone, and moves between them on the fly. The
4168
4208
  agent application says which with `{ type: "lead-features", enabled }`: while it is on, the
4169
4209
  provider sends team events scoped to the lead's team and the lead may act; while it is off, the
4170
- lead is an ordinary agent, no team member list is owed or expected (`team.unexpected`,
4210
+ lead is an ordinary agent, nothing of the team is owed or expected (`team.unexpected`,
4171
4211
  `event.team.features`), and no lead act is possible, since the lead learns their members'
4172
- assignments only from the list. It is on from sign-in for a lead until the lead turns it off.
4173
-
4174
- **The team member list never carries the agent it is published to — not in `members`, and not in
4175
- `requests`.** A lead does not report to themself: their own break request and their own ask for a
4176
- lead go up to whoever leads them and appear on *that* person's team member list, while the requester sees
4212
+ assignments only from the list.
4213
+
4214
+ **The provider assumes nothing.** The switch is not remembered across connections: the agent
4215
+ application sends `lead-features` on every connect, after it has stated capacity, and until it has
4216
+ the provider treats the feature as off. The connect snapshot therefore carries no team, whatever
4217
+ the login declares; the whole team arrives on `team-updated` once the switch is on, and every
4218
+ snapshot after that carries it. A conformance run switches the feature on for a lead and holds the
4219
+ provider to that first `team-updated` (`team.required`); a team on the connect snapshot is the
4220
+ provider assuming (`team.unexpected`).
4221
+
4222
+ **The team member list never carries the agent it is published to.** A lead does not report to
4223
+ themself: their own break request and their own ask for a lead go up to whoever leads them and
4224
+ appear on *that* person's team member list, as a member with a `request`, while the requester sees
4177
4225
  only their own `BreakState` and their task's `leadAssist` move. An adapter whose platform lists the lead
4178
4226
  among their own members filters the signed-in identity out before publishing. **Being a lead is a
4179
4227
  role the provider knows, never inferred from who is listed:** it is declared at sign-in, a lead
@@ -4229,8 +4277,8 @@ flow, in order:
4229
4277
  execute({ assignmentId: "alloc-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
4230
4278
  // task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
4231
4279
 
4232
- // 2. Every lead with the team feature on sees the request on their team member list.
4233
- // team-updated: requests: [{ memberId: "A-1", assignmentId: "alloc-42", note, since }]
4280
+ // 2. Every lead with the team feature on sees the request on the member.
4281
+ // team-member-updated: member: { id: "A-1", ..., request: { assignmentId: "alloc-42", note, since } }
4234
4282
 
4235
4283
  // 3. A lead joins, or declines.
4236
4284
  executeTeam({ command: { type: "join", memberId: "A-1", assignmentId: "alloc-42" } })
@@ -4239,8 +4287,8 @@ executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" }
4239
4287
 
4240
4288
  **On `join` the provider bridges the lead's channel into the call.** The member's task moves to
4241
4289
  `leadAssist: { stage: "joined", leadId }`, the member on the team member list carries
4242
- `listening: { mode: "join-call", since }`, and the lead's audio arrives as **Listening to a call**
4243
- describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
4290
+ `listening: { assignmentId, mode: "join-call", since }`, and the lead's audio opens on it as
4291
+ **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
4244
4292
  work assigned to them.
4245
4293
 
4246
4294
  **A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
@@ -4261,15 +4309,17 @@ listening unasked -- and one act at the end of either. What follows is the same
4261
4309
 
4262
4310
  | | The member's task | The lead's desk |
4263
4311
  | --- | --- | --- |
4264
- | 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`). | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`. |
4312
+ | 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 `listening` 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. |
4265
4313
  | 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. |
4266
4314
 
4267
- **The taken-over call is offered whatever break the lead is on.** A lead working as lead alone is
4268
- on a break of a working kind, and the provider assigns the call regardless: it is the one task a
4269
- break in effect may hold (`break.on-break.tasks` allows it, and nothing else). It is the lead's own
4270
- act, so it counts against no capacity, like the agent's own dial; the only conditions are that the
4271
- lead is not already on another call and their voice channel is free. A lead with the team feature
4272
- off cannot take over, since they have no member's assignment to name.
4315
+ **The taken-over call is offered whatever break the lead is on, and counted like any other.** A
4316
+ lead working as lead alone is on a break of a working kind, and the provider assigns the call
4317
+ regardless: it is the one task a break in effect may hold (`break.on-break.tasks` allows it, and
4318
+ nothing else). It is a call landing on the lead's desk, so it counts against their capacity as
4319
+ every call does, with no exemption: a lead at capacity cannot take over, and the provider answers
4320
+ `failed`. The other conditions are that the lead is not already on another call and their voice
4321
+ channel is free. A lead with the team feature off cannot take over, since they have no member's
4322
+ assignment to name.
4273
4323
 
4274
4324
  ```ts
4275
4325
  const leadAssistCapable = {
@@ -4303,34 +4353,47 @@ history. The three modes determine who hears the lead:
4303
4353
  | `coach` | The agent alone. The customer hears nothing. |
4304
4354
  | `join-call` | Everyone on the call. |
4305
4355
 
4306
- All three come with the `lead` flag; a centre reserves none of them per lead. The flow:
4356
+ All three come with the `lead` flag; a centre reserves none of them per lead.
4307
4357
 
4308
- ```ts
4309
- // 1. The lead picks a member from the team member list and starts silent.
4310
- executeTeam({ command: { type: "listen", memberId: "A-1" } })
4311
- // team-updated: the member carries listening: { mode: "listen", since }
4312
- // team-audio-started: { memberId: "A-1", assignmentId: "alloc-42" } -- the lead's audio attaches
4358
+ **There is no team audio.** Audio is the agent's or the lead's, and the lead's audio while
4359
+ listening follows the provider's published state exactly as a task's does: `listening` on the
4360
+ member is the provider's word that the lead's channel is in the call. When it appears, the
4361
+ application opens `openAudio({ assignmentId })` on the lead's connection for the call it names,
4362
+ and plays the `CallAudio` returned; when it goes, the application closes it. Nothing else says
4363
+ when the lead's audio is up, and a reload reopens it from the snapshot's team member list as a task
4364
+ carried with `audio: "started"` is reopened.
4313
4365
 
4314
- // 2. The lead changes how they are heard; the provider restates the member with the new mode.
4366
+ The flow, end to end:
4367
+
4368
+ ```ts
4369
+ // 1. The lead, with the feature on, sees a member on-task with their tasks, and picks a call.
4370
+ // 2. The lead's channel joins it in silence. Conditions: the lead's voice channel is free --
4371
+ // not on a call, not listening elsewhere. Capacity is not involved: nothing is assigned.
4372
+ executeTeam({ command: { type: "listen", memberId: "A-1", assignmentId: "alloc-42" } })
4373
+ // 3. The provider publishes the member whole on team-member-updated; the member carries
4374
+ // listening: { assignmentId: "alloc-42", mode: "listen", since }
4375
+ // -- the application opens openAudio({ assignmentId: "alloc-42" }) on the lead's connection.
4376
+ // 4. The lead changes how they are heard; the member is published again with the new listening.mode. The audio stays open.
4315
4377
  executeTeam({ command: { type: "coach", memberId: "A-1" } })
4316
4378
  executeTeam({ command: { type: "join-call", memberId: "A-1" } })
4317
-
4318
- // 3. The lead leaves, or takes the call.
4379
+ // 5. The lead leaves: the provider publishes the member without listening; the application closes the audio.
4319
4380
  executeTeam({ command: { type: "leave", memberId: "A-1" } })
4381
+ // 6. The member's call ends: the same as leave, from the provider's side.
4382
+ // 7. The lead takes the call: the member loses listening no later than the offer, the application
4383
+ // closes the listening audio, and the lead's own call arrives as an ordinary assignment marked
4384
+ // takenOver, counted against capacity, with its audio following task-audio-started as any call's does.
4385
+ // At capacity the take-over is refused and the lead stays listening.
4320
4386
  executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
4387
+ // 8. A join on the member's request is the same flow entered at step 3 with mode: "join-call",
4388
+ // and the member's task shows leadAssist: joined.
4389
+ // 9. A reload of the lead's application: the switch is sent again, the team-updated that follows
4390
+ // still carries listening on the member, so the audio is reopened.
4321
4391
  ```
4322
4392
 
4323
- **The lead's audio arrives through team audio.** While the lead's channel is in a member's call
4324
- -- listening, coaching or joined on request -- the lead's connection receives `team-audio-started`
4325
- naming the member and the member's assignment, and on a softphone the agent application opens
4326
- `openAudio` on that assignment as it does for a task; `team-audio-ended` closes it. Voice only, and
4327
- to a login that declares `lead` (`event.teamAudio.*`). The member's own audio events are the
4328
- member's and never reach the lead.
4329
-
4330
- **`listening` on the member is the state**, restated on every change of mode, and a lead is on
4331
- one member's call at a time (`team.listening.single`). The member's call ending ends the lead's
4332
- listening with it: the provider republishes the member without `listening` and sends
4333
- `team-audio-ended`.
4393
+ **`listening` on the member is the state**, restated on every change of mode, naming the call so
4394
+ the application knows which audio to open (`team.member.listening.assignmentId`, and one the
4395
+ member's tasks carry, `team.member.listening.assignment`), and a lead is on one member's call at a
4396
+ time (`team.listening.single`).
4334
4397
 
4335
4398
  **A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
4336
4399
  channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
@@ -4389,7 +4452,7 @@ mode stays with the login rather than being inferred from a snapshot or a device
4389
4452
 
4390
4453
  Nor does the audio ever stand in for the task: a task's presence and
4391
4454
  phase follow the provider's reports about the work, and the audio — attaching, moving through a
4392
- hold, a consult, a conference or a transfer, and ending — is transient beside it. See **A task is
4455
+ hold, a conference, a lead joining or a take-over, and ending — is transient beside it. See **A task is
4393
4456
  never its audio** under **Task assignment lifecycle**.
4394
4457
 
4395
4458
  ### The agent application reports, the adapter decides
@@ -4593,7 +4656,7 @@ capability's `notes` setting. A task publishing no codes still receives `complet
4593
4656
  ### Where a command executes
4594
4657
 
4595
4658
  Every command reaches the provider through `execute`, with no branch at the call site, and every
4596
- command asks the provider to **perform** something: `hold`, `transfer`, `conference`, `end-call`,
4659
+ command asks the provider to **perform** something: `hold`, `conference`, `schedule`, `end-call`,
4597
4660
  Provider-targeted `recording` commands and the rest act on the platform's own call leg, its bridge, or its record of the
4598
4661
  task. Nothing has happened until the provider applies them, and `failed` means nothing happened.
4599
4662
 
@@ -4663,7 +4726,7 @@ as by a click. What a call-control headset sends, and what becomes of it:
4663
4726
  | --- | --- | --- | --- |
4664
4727
  | 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"` |
4665
4728
  | 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 |
4666
- | 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 with no dial pad on a transfer, and are ignored | Nothing new |
4729
+ | 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 |
4667
4730
  | 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 |
4668
4731
  | 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 |
4669
4732
  | 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 |
@@ -4689,22 +4752,21 @@ declared:
4689
4752
  | --- | --- |
4690
4753
  | `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
4691
4754
  | `end-call` | The `endCall` capability: ends my part, the caller continues. |
4692
- | `terminate-call` | The `terminateCall` capability: ends the caller channel. |
4755
+ | `terminate-call` | The `terminateCall` capability: ends the whole call, every channel on it. |
4693
4756
  | `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`. |
4694
4757
  | `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
4695
4758
  | `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`. |
4696
- | `complete` | `completionMode: "agent-command"`. 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`). |
4759
+ | `complete` | A wrap to cut short: any task but 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`). |
4697
4760
  | `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. |
4698
- | `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
4699
- | `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
4761
+ | `schedule` | The `schedule` capability, in `in-progress`, `paused` or `completing`: a follow-up is promised on the call and written up in wrap alike. |
4700
4762
  | `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
4701
- | `transfer` with a destination, `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
4763
+ | `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`). |
4702
4764
  | `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`). |
4703
4765
  | Everything else | Its own named capability. |
4704
4766
 
4705
4767
  **A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
4706
- `hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `transfer` and `conference` action, and
4707
- every `lead-assist` action. These controls act within this agent's current interaction.
4768
+ `hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `conference` action, and
4769
+ every `lead-assist` action; `schedule` alone reaches into `completing`. These controls act within this agent's current interaction.
4708
4770
  In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
4709
4771
  an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
4710
4772
  of this interaction does not establish that the caller or bridge ended. The capability stays
@@ -4808,7 +4870,7 @@ The current exceptions and their reasons are:
4808
4870
 
4809
4871
  | Case | Exception and reason |
4810
4872
  | --- | --- |
4811
- | Agent application display of provider deadlines (`assignmentExpiresAt`, `previewEndsAt`, wrap deadline) | A countdown may use an explicitly bounded provider-clock estimate when a trusted direct clock is unavailable, because the deadline belongs to another clock domain. This estimates the display only; it does not establish that the provider acted. Without usable time, show timing uncertainty. |
4873
+ | Agent application display of provider deadlines (`expiresInSeconds`, `previewEndsInSeconds`, wrap deadline) | Each is seconds from the publication that carried it, counted down on the desk from receipt: the provider's own arithmetic, so no clock is compared and the transport delay is the only error. The countdown estimates the display only; it does not establish that the provider acted. Without usable time, show timing uncertainty. |
4812
4874
  | Agent application-triggered preview deadline | A bounded clock estimate with monotonic aging may schedule the agent application's Call command because the provider owns the deadline but the agent application owns the trigger. Do not trigger before the deadline is known to have passed; invalidate on clock discontinuity and reconcile when uncertain. |
4813
4875
  | Recording evidence expiry | A bounded observer-domain clock estimate with monotonic aging may assess freshness because observation and expiry belong to the recorder's clock. If time cannot be trusted, recording state is unknown; never renew evidence from receipt or replay. |
4814
4876
  | Unknown recording state | `observedAt` and `validUntil` are absent because there is no confirmed observation. The containing provider event still has its own `occurredAt`; that publication is not a recorder observation. |
@@ -5053,9 +5115,15 @@ multi-provider break**.
5053
5115
  Offers a task to Omni without a separate offer acknowledgement. An offer does not accept
5054
5116
  the task: when its phase is `pending`, Omni applies `autoAcceptTasks` and the task's
5055
5117
  `acceptance`. `task-offered` must not introduce a task as `in-progress`; only a reconnect or
5056
- resync snapshot may report work already in progress. The provider should include the task in later
5118
+ resync snapshot may report work already in progress. The provider includes the task in later
5057
5119
  snapshots until it ends.
5058
5120
 
5121
+ **Every offer is owed an ending.** An assignment the provider introduced is an assignment it ends,
5122
+ with `task-ended` and an outcome, whatever became of the call: answered and completed, declined,
5123
+ withdrawn, abandoned in the ring, lapsed, taken over. An offer that is simply never mentioned
5124
+ again leaves the desk holding a task nobody will close, and the conformance drive names it
5125
+ (`stream.taskOffered.unended`).
5126
+
5059
5127
  ### `task-updated`
5060
5128
 
5061
5129
  Replaces the current representation of one provider-local task. It is a full task value, not a
@@ -5099,7 +5167,7 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
5099
5167
  A `taken-over` outcome arrives at the member's own completion, after the wrap that follows a
5100
5168
  take-over as it follows an `end-call` -- see **Lead assist**.
5101
5169
 
5102
- A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
5170
+ A successful `complete` command does not clear the task. Omni waits for `task-ended`,
5103
5171
  and not for ever: `applied` to a `complete` says the
5104
5172
  provider has completed the task, and its `task-ended` follows within the
5105
5173
  manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
@@ -5165,10 +5233,31 @@ each connected provider.
5165
5233
 
5166
5234
  ### `team-updated`
5167
5235
 
5168
- Replaces this provider's complete `TeamMembers`. It is emitted only for an agent the provider
5169
- publishes a team member list to, and it carries the whole team every time — never a change to it, for the
5170
- reason set out under **Team leads**. A lead's snapshot always carries the team member list; it goes only when
5171
- a republished `authenticated` no longer declares `capabilities.lead`, or the lead turns the team feature off (`event.team.features`).
5236
+ The whole `TeamMembers`, replacing what the lead had. It is emitted once, after the lead switches
5237
+ the team feature on, and to nobody the provider does not publish a team to; every snapshot while
5238
+ the feature is on carries the same whole team. The changes that follow arrive one member at a
5239
+ time, below; the whole team is never resent for one of them. Nothing of the team goes to a login
5240
+ that no longer declares `capabilities.lead` on a republished `authenticated`, or to a lead who
5241
+ turned the feature off (`event.team.features`).
5242
+
5243
+ ### `team-member-updated`
5244
+
5245
+ One member, whole, as they now stand: their availability, their tasks, their listening, their
5246
+ shift, their request. It replaces the member of that `id` on the lead's list, or adds them where
5247
+ the list did not carry them, so applying it twice changes nothing. It comes only after the whole
5248
+ team has (`stream.team.baseline`), and it is what a member's hold, answer, mute or take-over
5249
+ publishes to the lead.
5250
+
5251
+ ### `team-member-removed`
5252
+
5253
+ One member taken off the lead's list, by `memberId`: somebody who left the team, not somebody who
5254
+ signed out, which is an availability the member is published with. It names a member the list
5255
+ carried (`stream.teamMember.unknown`).
5256
+
5257
+ ### `team-policies-updated`
5258
+
5259
+ The team's policies, whole, replacing what the lead had -- as `TeamMembers.policies` carried them.
5260
+ Emitted where the provider offers policy control, after the whole team has (`stream.team.baseline`).
5172
5261
 
5173
5262
  ### `contacts-updated`
5174
5263
 
@@ -5229,7 +5318,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
5229
5318
  | `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
5230
5319
  | `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the drive, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
5231
5320
  | `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
5232
- | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). An agent application validates before sending and an adapter before acting. |
5321
+ | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a conference's item, a schedule's time, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a lead requested, somebody else still on the call (`command.conference.remove.alone`). An agent application validates before sending and an adapter before acting. |
5233
5322
  | `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
5234
5323
  | `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
5235
5324
 
@@ -5240,12 +5329,11 @@ found at such as `snapshot.tasks[0].browsers[1].url`, and a `message`.
5240
5329
  Some rules need to know who is reading. `validateTeamMembers`, `validateSnapshot`, and
5241
5330
  `validateEventEnvelope` take an optional final `{ self, capabilities }` — the signed-in agent's
5242
5331
  `AuthenticationState.identity.id` and their login's `capabilities`. Given `self`, a team member list that
5243
- carries that agent reports `team.member.self` or `team.request.self`. Given `capabilities`, a lead's
5244
- snapshot without a team member list reports `team.required`, a team member list published to a login that does not lead
5245
- reports `team.unentitled`, and a `team-updated` to such a login reports `event.team.capability`; given
5246
- `leadFeatures: false`, a team member list reaching a lead who turned the feature off reports
5247
- `team.unexpected` or `event.team.features`. Without them those rules are not checked, because they cannot be.
5248
- `exerciseAdapter` always passes both.
5332
+ carries that agent reports `team.member.self`, on a `team-member-updated` and a `team-member-removed` as on the list. Given `capabilities`, a team member list published to a login that does not lead
5333
+ reports `team.unentitled`, and a team event to such a login reports `event.team.capability`; given
5334
+ `leadFeatures: true`, a snapshot without a team reports `team.required`, and given `leadFeatures: false`,
5335
+ anything of the team reaching the lead reports `team.unexpected` or `event.team.features`. Without them those rules are not checked, because they cannot be.
5336
+ `exerciseAdapter` passes all three, holding the switch off until it has sent it.
5249
5337
 
5250
5338
  `assertNoViolations(violations)` throws `ProtocolConformanceError` — which carries the full
5251
5339
  `violations` array — when the list is non-empty.
@@ -5306,7 +5394,7 @@ A capability granted by a later login requires its methods just as one declared
5306
5394
 
5307
5395
  `result.notExercised` lists what the run never reached — one subject per family of rules: each
5308
5396
  optional part of a task (`task.browsers`, `task.history`, `task.leadAssist`, …), the break's
5309
- `reasons` and `forced`, the team member list's `members` and `requests`, each declared contribution, and
5397
+ `reasons` and `forced`, the team member list's `members` and a member's `request`, each declared contribution, and
5310
5398
  each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
5311
5399
  Nothing there is a violation: an adapter with no team has nothing to exercise. But a fixture with
5312
5400
  no tasks exercises no task rule, and a pass over it reads as coverage it is not.
@@ -5482,18 +5570,22 @@ effect. A recorder that cannot confirm completion must not offer Cancel; it can
5482
5570
  Cancel never means cancelling an in-flight start request or deleting arbitrary past recordings.
5483
5571
  Stop can be applied only after finalization/retention succeeds; Cancel only after both cessation
5484
5572
  and completion of this recording's audio succeed under the recorder's storage contract.
5485
- Partial success (capture stopped but storage outcome unknown) cannot return failed with a claim
5486
- of no effect. It rejects with unknown outcome, reports the failure visibly and publishes whatever
5487
- current capture state is actually known. Retention is not a promise of sample-perfect audio.
5573
+ Partial success -- capture stopped but the storage outcome unknown, a start the recorder cannot
5574
+ vouch for either way -- is a settled `failed`, under the code `omni.recording-unsettled`: the
5575
+ command did not do what it promised, and the state the provider publishes with it is whatever
5576
+ capture state is actually known. It is never `applied`, and never an unsettled promise the agent
5577
+ application would be left to resync, since the provider knows exactly what it could not settle.
5578
+ There is no request identity on a recording command: the outcome is the state, and what the
5579
+ agent sees is the state. Retention is not a promise of sample-perfect audio.
5488
5580
 
5489
5581
  Provider commands go exclusively to `Connection.execute`; agent application commands go exclusively to
5490
- `HostRecording.execute`. Both include the assignment, request and observation identities; all
5582
+ `HostRecording.execute`. Both include the assignment and observation identities; all
5491
5583
  non-start commands identify the particular recording. IDs are opaque and scoped by provider login,
5492
5584
  task and recorder owner. They are never inferred from filenames or current agent identity. A
5493
5585
  recording ID survives pause/resume and reassignment only where the same recorder confirms continuity;
5494
5586
  commands always name the current assignment. A later start gets a different ID. Reconnect does not
5495
5587
  create a new recording or fresh evidence. After lost continuity, use unknown until reconciled.
5496
- There is no automatic restart, transfer to another recorder, or stop on task hold/disconnect.
5588
+ There is no automatic restart, hand-over to another recorder, or stop on task hold/disconnect.
5497
5589
  Task removal does not prove recording stopped: outstanding agent application recorders remain tracked by the
5498
5590
  agent application until its executor reconciles/finishes them, with visible unresolved cleanup failures.
5499
5591