@xema/omni-protocol 0.1.102 → 0.1.104

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
@@ -203,7 +203,7 @@ type Manifest<C extends Channel = Channel> = {
203
203
  dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
204
204
  phones?: C extends "voice" ? Phone[] : never;
205
205
  runningStepReports?: true;
206
- completionSettleMs: number;
206
+ settleMs: number;
207
207
  };
208
208
  ```
209
209
 
@@ -474,11 +474,13 @@ type PreferenceResult =
474
474
  type Snapshot = {
475
475
  transport: TransportStatus;
476
476
  loginId: string;
477
+ providerTime?: IsoTimestamp;
477
478
  break: BreakState;
478
479
  tasks: Task[];
479
480
  taskCount: number;
480
481
  contacts?: Contact[];
481
482
  calendar?: ScheduledActivity[];
483
+ shift?: Shift;
482
484
  team?: TeamMembers;
483
485
  };
484
486
 
@@ -569,8 +571,8 @@ type TaskCapabilities<C extends Channel = Channel> =
569
571
  recording?: Lockable<TaskRecordingPolicy>;
570
572
  }
571
573
  : C extends "chat"
572
- ? SharedTaskCapabilities & { decline?: Lockable<true>; hold?: Lockable<true> }
573
- : SharedTaskCapabilities & { decline?: Lockable<true> };
574
+ ? SharedTaskCapabilities & { decline?: Lockable<true>; hold?: Lockable<true>; schedule?: Lockable<true> }
575
+ : SharedTaskCapabilities & { decline?: Lockable<true>; schedule?: Lockable<true> };
574
576
  ```
575
577
 
576
578
  The channel arms are why `Task<"email">` rejects `hold` at compile time rather than at runtime.
@@ -723,6 +725,7 @@ type Task<C extends Channel = Channel> = {
723
725
  party?: Contact;
724
726
  phase: TaskPhase;
725
727
  acceptance?: AcceptanceMode;
728
+ expiresInSeconds?: DurationSeconds;
726
729
  previewEndsInSeconds?: DurationSeconds;
727
730
  atDeadline?: PreviewDeadline;
728
731
  reference?: string;
@@ -745,7 +748,7 @@ type TaskOutcome =
745
748
  | { type: "completed"; by: "agent" | "provider" }
746
749
  | { type: "taken-over"; leadId: UserId }
747
750
  | { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
748
- | { type: "expired"; phase: "pending" | "confirmed" | "preview" }
751
+ | { type: "expired"; phase: "pending" | "confirmed" }
749
752
  | { type: "failed"; failure: ProtocolFailure };
750
753
  ```
751
754
 
@@ -768,8 +771,8 @@ const TASK_COMMAND_NAMES = {
768
771
  "schedule",
769
772
  "complete",
770
773
  ],
771
- chat: ["accept", "decline", "pause", "resume", "complete"],
772
- email: ["accept", "decline", "complete"],
774
+ chat: ["accept", "decline", "pause", "resume", "schedule", "complete"],
775
+ email: ["accept", "decline", "schedule", "complete"],
773
776
  } as const;
774
777
 
775
778
  type TaskCommandName<C extends keyof typeof TASK_COMMAND_NAMES> =
@@ -786,7 +789,7 @@ type VoiceTaskCommand =
786
789
  | { type: "end-call" }
787
790
  | { type: "terminate-call" }
788
791
  | { type: "connect-back"; dialId: DialId }
789
- | { type: "schedule"; at: IsoTimestamp; note?: string }
792
+ | ScheduleCommand
790
793
  | { type: "lead-assist"; action: "request"; note?: string }
791
794
  | { type: "lead-assist"; action: "cancel" }
792
795
  | { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
@@ -795,16 +798,20 @@ type VoiceTaskCommand =
795
798
  | (RecordingCommand & { source: "provider" })
796
799
  | ({ type: "complete" } & OutcomePayload);
797
800
 
801
+ type ScheduleCommand = { type: "schedule"; at: IsoTimestamp; note?: string };
802
+
798
803
  type ChatTaskCommand =
799
804
  | { type: "accept" }
800
805
  | { type: "decline" }
801
806
  | { type: "pause" }
802
807
  | { type: "resume" }
808
+ | ScheduleCommand
803
809
  | ({ type: "complete" } & OutcomePayload);
804
810
 
805
811
  type EmailTaskCommand =
806
812
  | { type: "accept" }
807
813
  | { type: "decline" }
814
+ | ScheduleCommand
808
815
  | ({ type: "complete" } & OutcomePayload);
809
816
 
810
817
  type CustomTaskCommand = { type: "custom"; name: string; [key: string]: unknown };
@@ -928,7 +935,7 @@ type BreakRequest = {
928
935
 
929
936
  type ForcedBreak = {
930
937
  by: UserId | "provider";
931
- expectedDurationMs?: number;
938
+ expectedEndsInSeconds?: DurationSeconds;
932
939
  };
933
940
 
934
941
  type BreakState = {
@@ -970,8 +977,8 @@ type TeamMember = {
970
977
  since?: IsoTimestamp;
971
978
  break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
972
979
  tasks?: MemberTask[];
973
- listening?: MemberListening;
974
- shift?: MemberShift;
980
+ listening?: MemberListening[];
981
+ shift?: Shift;
975
982
  request?: MemberRequest;
976
983
  };
977
984
 
@@ -1001,6 +1008,7 @@ type MemberTask<C extends Channel = Channel> = {
1001
1008
  : { onCall?: never; leadAssist?: never; takenOver?: never; audio?: never });
1002
1009
 
1003
1010
  type MemberListening = {
1011
+ leadId: UserId;
1004
1012
  assignmentId: AssignmentId;
1005
1013
  mode: ListeningMode;
1006
1014
  since: IsoTimestamp;
@@ -1013,7 +1021,7 @@ type ShiftEvent = {
1013
1021
  kind: ShiftEventKind;
1014
1022
  };
1015
1023
 
1016
- type MemberShift = {
1024
+ type Shift = {
1017
1025
  signedInAt: IsoTimestamp;
1018
1026
  signedOutAt?: IsoTimestamp;
1019
1027
  talkSeconds?: DurationSeconds;
@@ -1115,7 +1123,6 @@ type ProviderEvent =
1115
1123
  | {
1116
1124
  type: "task-offered";
1117
1125
  task: Task;
1118
- expiresInSeconds?: DurationSeconds;
1119
1126
  }
1120
1127
  | { type: "task-updated"; task: Task }
1121
1128
  | { type: "task-audio-started"; assignmentId: AssignmentId }
@@ -1130,7 +1137,8 @@ type ProviderEvent =
1130
1137
  | { type: "team-member-removed"; memberId: UserId }
1131
1138
  | { type: "team-policies-updated"; policies: TeamPolicies }
1132
1139
  | { type: "contacts-updated"; contacts: Contact[] }
1133
- | { type: "calendar-updated"; calendar: ScheduledActivity[] };
1140
+ | { type: "calendar-updated"; calendar: ScheduledActivity[] }
1141
+ | { type: "shift-updated"; shift: Shift };
1134
1142
 
1135
1143
  type ProviderEventEnvelope = {
1136
1144
  id: string;
@@ -1254,6 +1262,7 @@ const OMNI_FAILURE_CODES = [
1254
1262
  "omni.unavailable",
1255
1263
  "omni.break-already-committed",
1256
1264
  "omni.recording-unsettled",
1265
+ "omni.break-forced-by-provider",
1257
1266
  ] as const;
1258
1267
  type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
1259
1268
  ```
@@ -1586,7 +1595,7 @@ export default defineAdapter({
1586
1595
  channel: "voice",
1587
1596
  supportedProtocolVersions: [OMNI_PROTOCOL_VERSION],
1588
1597
  authenticationMethods: ["browser-sso"],
1589
- completionSettleMs: 5000,
1598
+ settleMs: 5000,
1590
1599
  idleCapabilities: {
1591
1600
  dial: { destinations: "any-number" },
1592
1601
  },
@@ -1621,10 +1630,10 @@ compile time.
1621
1630
  | `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**. |
1622
1631
  | `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**. |
1623
1632
  | `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**. |
1624
- | `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; agent application polling is independently opt-in. |
1633
+ | `timeCheck` | `true`: implements `checkTime` and states `providerTime` on every snapshot. Required of a provider that publishes an instant the desk renders as a running duration -- a member's `since`, a lead's `listening.since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts from the provider's clock (`manifest.timeCheck.required`); agent application polling is independently opt-in. See **Every screen counts from the provider's clock**. |
1625
1634
  | `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
1626
1635
  | `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**. |
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`**. |
1636
+ | `settleMs` | Required. How long after `applied` the wire shows what the provider did: the `task-ended` after a `complete`, the `calendar-updated` after a `schedule`. A positive whole number of milliseconds (`manifest.settleMs`). Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1628
1637
 
1629
1638
  ### Authentication methods
1630
1639
 
@@ -2137,11 +2146,13 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
2137
2146
  | --- | --- |
2138
2147
  | `transport` | Current `TransportStatus` — whether this provider's transport can serve the login. Defined under **`transport-status`**. |
2139
2148
  | `loginId` | Identity of this login. It must match the connection context. |
2149
+ | `providerTime` | The provider's own instant of this read. Required of a provider that declares `timeCheck` (`snapshot.providerTime`), forbidden of one that does not (`snapshot.providerTime.unexpected`): a clock sample the desk counts durations from, saving it a `checkTime`. |
2140
2150
  | `break` | Complete break state, including status, whether the agent may ask, reasons, retry details, and any forced break. |
2141
2151
  | `tasks` | Complete set of tasks currently offered to or owned by this agent. |
2142
2152
  | `taskCount` | The provider's own count of those tasks, stated rather than inferred, and it must equal `tasks.length`. A snapshot with no work says `taskCount: 0` in so many words — a blank or unanswered state lacks the count and cannot pass as a confirmed empty. |
2143
2153
  | `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
2144
2154
  | `calendar` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
2155
+ | `shift` | The agent's own day so far, as the provider counts it -- the same `Shift` their lead sees on the team member list, the same numbers from the same count. Replaced whole by `shift-updated`. Omitted only where the provider cannot say. See **The agent's day is on the wire**. |
2145
2156
  | `team` | Required `TeamMembers` when the login declares `capabilities.lead` and the lead has the team feature on, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission, and a lead who turned the feature off gets nothing of the team. |
2146
2157
 
2147
2158
  ## Live connection
@@ -2318,8 +2329,7 @@ declare const task: Task;
2318
2329
 
2319
2330
  const assignment = {
2320
2331
  type: "task-offered",
2321
- task: { ...task, phase: "pending", acceptance: "consent" },
2322
- expiresInSeconds: 30,
2332
+ task: { ...task, phase: "pending", acceptance: "consent", expiresInSeconds: 30 },
2323
2333
  } satisfies Extract<ProviderEvent, { type: "task-offered" }>;
2324
2334
  ```
2325
2335
 
@@ -2337,10 +2347,12 @@ on a desk phone — and is not in question per call. See **How the agent hears t
2337
2347
 
2338
2348
  Automatic acceptance still begins with `task-offered`.
2339
2349
 
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
2350
+ `expiresInSeconds` is how long the offer has left, in whole seconds from the publication that
2351
+ carries it. It rides on the pending task, as `acceptance` does, so a reconnect snapshot restates
2352
+ it from what is actually left; it belongs to `pending` alone and is gone once the task is accepted
2353
+ (`task.pending.expiresInSeconds`, `.unexpected`). Where present, Omni counts down from receipt and
2354
+ stops offering **Accept** once it runs out; the provider ends the lapsed offer with `task-ended`
2355
+ and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.** A provider that reports only elapsed
2344
2356
  ring time after the fact cannot say how long an offer has left, and a computed value would have
2345
2357
  Omni withdraw **Accept** from a task still pending.
2346
2358
 
@@ -2349,7 +2361,7 @@ different clocks, and a countdown that compares one against the other is a count
2349
2361
  fast or slow by their skew. Seconds from the publication that carries them are the provider's own
2350
2362
  arithmetic, counted down on the desk from the moment they arrive, and the transport delay is the
2351
2363
  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`,
2364
+ starts the countdown afresh from what is actually left (`task.pending.expiresInSeconds`,
2353
2365
  `task.preview.previewEndsInSeconds`: a whole number, zero or more).
2354
2366
 
2355
2367
  A preview's deadline is not on the offer. It travels on the task, as `previewEndsInSeconds` with
@@ -2396,6 +2408,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2396
2408
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
2397
2409
  | `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. |
2398
2410
  | `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**. |
2411
+ | `expiresInSeconds` | In `pending` only: how long the offer has left, in whole seconds from this publication, restated on every publication that carries it. Absent, the offer stands until the provider says otherwise. See **Pending**. |
2399
2412
  | `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
2413
  | `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. |
2401
2414
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
@@ -2618,18 +2631,26 @@ provider may complete the task without receiving that command.
2618
2631
  **The agent may finish early under either mode.** `complete` is issuable wherever there is a wrap
2619
2632
  to cut short: under `agent-command` it is the end, and under `provider-automatic` it says the agent
2620
2633
  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.
2634
+ with no wrap at all -- `provider-automatic` with `wrapAllowance: 0` -- has nothing to complete, and
2635
+ the command is refused there (`command.complete.wrapAllowance`). An ending the agent asked for says
2636
+ so, `completed` with `by: "agent"`, whichever mode the task was under.
2624
2637
 
2625
2638
  **A required outcome is the agent's to give.** A task whose `outcomes` says `required: true` waits
2626
2639
  for a code the agent chooses, so it completes on the agent's command: `provider-automatic` cannot
2627
2640
  wait for it, and the pair is refused (`task.outcomes.required.mode`). Optional outcomes may ride on
2628
2641
  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`).
2642
+ so a `provider-automatic` task with `wrapAllowance: 0` publishes no `outcomes` at all
2643
+ (`task.outcomes.wrapAllowance`).
2644
+
2645
+ **The allowance means one thing per mode.** Under `provider-automatic` it is what the provider
2646
+ acts on: the task ends when it runs out. Under `agent-command` it is the expected wrap, stated so
2647
+ the agent can see it: the desk counts it down and, past zero, shows how far over they are, and
2648
+ nothing acts on it -- the task waits for `complete` however long that takes, with its outcomes
2649
+ collected whenever the agent gives them. A provider that would end the task at a time is
2650
+ `provider-automatic`.
2630
2651
 
2631
- `wrapAllowance` is independent of that decision. It is fixed, and when it starts depends on
2632
- whether the channel carries real-time audio:
2652
+ `wrapAllowance` is fixed, and when it starts depends on whether the channel carries real-time
2653
+ audio:
2633
2654
 
2634
2655
  | Channel | Wrap allowance starts at |
2635
2656
  | --- | --- |
@@ -2658,9 +2679,9 @@ const emailCompletion = {
2658
2679
  In this example, the agent has two minutes after sending the email to add notes, select a
2659
2680
  outcome, and complete the task.
2660
2681
 
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.
2682
+ `0` means no wrap under `provider-automatic`: the provider completes the task at the end of the
2683
+ interaction, and `complete` has nothing to cut short. Under `agent-command` it is an expected wrap
2684
+ of nothing -- the desk shows the overrun from the start -- and the task still waits for `complete`.
2664
2685
 
2665
2686
  There is no value meaning "unlimited", because a number that is not a duration would be read as
2666
2687
  one. A provider that imposes no deadline says so by **omitting** `wrapAllowance`, which
@@ -2748,7 +2769,7 @@ The record is the call's, not the task's: most of what it holds happened before
2748
2769
  *Interaction* keeps the one meaning **Terms** gives it, the agent's time on the call, which is why
2749
2770
  the total of earlier agents' time is `interactionSeconds` and the phase rules are
2750
2771
  `command.phase.interaction`. Hosts and providers adopt these names together; no legacy aliases
2751
- are provided. The manifest's `completionSettleMs` bounds final task completion, not entry into
2772
+ are provided. The manifest's `settleMs` bounds final task completion, not entry into
2752
2773
  the `completing` wrap-up phase. The `recordStep` method and `complete` command keep their names.
2753
2774
 
2754
2775
  Migration from the earlier spellings:
@@ -2765,7 +2786,7 @@ Migration from the earlier spellings:
2765
2786
  | Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `expiresInSeconds` |
2766
2787
  | 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 |
2767
2788
  | 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`) |
2768
- | Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
2789
+ | Manifest.disposalSettleMs, Manifest.completionSettleMs | `Manifest.settleMs` (`manifest.settleMs`, `manifest.settleMs.renamed`): the bound after `applied` for `complete` and `schedule` alike |
2769
2790
  | the call command, atDeadline calls and host-calls | `dial`, `"provider-dials"`, `"host-dials"` |
2770
2791
  | 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 |
2771
2792
  | capabilitySource values ungoverned and undetermined | `nobody`, `not-yet-read` (`capabilitySource.notYetRead`) |
@@ -2777,7 +2798,17 @@ Migration from the earlier spellings:
2777
2798
  | TeamMembers.requests, LeadRequest | `TeamMember.request` (`MemberRequest`): the ask rides on the member (`team.member.request.*`) |
2778
2799
  | 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
2800
  | 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 |
2801
+ | command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only on `provider-automatic` with no wrap |
2802
+ | expiresInSeconds on task-offered | `Task.expiresInSeconds`, in `pending` (`task.pending.expiresInSeconds`, `.unexpected`; `event.taskOffered.expiresInSeconds.unexpected`) |
2803
+ | the expired outcome naming preview | gone: nothing expires a preview; withdrawn, it is `cancelled` by the provider |
2804
+ | schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`drive.schedule.unsettled`) |
2805
+ | a forced break requesting breaks on the other providers | `setCapacity({ count: 0 })` on every other usable provider (`assertForcedBreakStopsTheRest`) |
2806
+ | end-forced-break on a break the platform imposed | refused: `team.command.endForcedBreak.provider`, `omni.break-forced-by-provider` |
2807
+ | ForcedBreak.expectedDurationMs | `expectedEndsInSeconds`: seconds left, restated (`break.forced.expectedEndsInSeconds`, `.expectedDurationMs.renamed`); the command keeps `expectedDurationMs` |
2808
+ | MemberShift; the day on the lead's list alone | `Shift`, on `Snapshot.shift` and `shift-updated` for the agent, on `TeamMember.shift` for the lead |
2809
+ | TeamMember.listening as this lead's one entry | `MemberListening[]`, every lead's, each with `leadId` (`team.member.listening.leadId`, `.unique`, `.empty`, `.entry`, `.heard`) |
2810
+ | a member's history trimmed on the lead's list | the record whole, as the member's desk holds it |
2811
+ | durations counted from the screen's clock | the provider's: `timeCheck` required of a provider publishing a running instant (`manifest.timeCheck.required`), `Snapshot.providerTime` with it (`snapshot.providerTime`, `.unexpected`) |
2781
2812
 
2782
2813
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
2783
2814
  History and report rule names use `history` and `historyReport`; assignment rules use
@@ -3377,28 +3408,39 @@ caller to somebody else.
3377
3408
 
3378
3409
  An agent on a call promises to call back on Thursday, or in wrap writes up that the customer wants
3379
3410
  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.
3411
+ calendar: `schedule` asks the provider to put it there. A chat or an email agent promises a
3412
+ callback as often, so the control is on every channel.
3381
3413
 
3382
3414
  ```ts
3383
- // From the call, or from its wrap: the time, and a note where the agent wrote one.
3415
+ // From the call or the conversation, or from its wrap: the time, and a note where the agent wrote one.
3384
3416
  { type: "schedule", at: "2026-08-28T10:00:00Z", note: "Call back about the refund" }
3385
3417
  ```
3386
3418
 
3387
3419
  `schedule` is gated by the `schedule` capability and issuable in `in-progress`, `paused` or
3388
3420
  `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
3421
+ wrap alike, and the task names the party it is for. It lands on the calendar, so a task may offer
3422
+ it only under a manifest that declares the `calendar` idle capability
3423
+ (`task.capability.calendar.required`), as a control that dials needs `dialOutcomes`; a locked
3424
+ `schedule` is still a declared one. The command carries a time and a note and nothing else
3392
3425
  (`command.schedule.at`, `command.schedule.note`, `command.field`): what the platform makes of a
3393
3426
  follow-up -- a campaign record, a reminder, a scheduled dial -- is its own, and the agent sees it
3394
3427
  on the calendar.
3395
3428
 
3429
+ **`applied` says the follow-up is on the calendar**, and the `calendar-updated` that shows it
3430
+ follows within the manifest's `settleMs`, as a `task-ended` follows an applied `complete`. Past
3431
+ the bound the agent application calls `snapshot()`: a calendar carrying an activity at that instant
3432
+ clears the wait; one without it shows the follow-up as unsettled -- "Scheduled... the provider has
3433
+ not confirmed" -- naming the command. The drive schedules one follow-up where the task offers it,
3434
+ at a time it takes from the provider's own publication, and holds the provider to the same bound
3435
+ (`drive.schedule.unsettled`).
3436
+
3396
3437
  ### Chat capabilities
3397
3438
 
3398
3439
  | Capability | Omni UI | Contract |
3399
3440
  | --- | --- | --- |
3400
3441
  | `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when local policy also permits declining. |
3401
3442
  | `hold` | Primary toggle: Hold | Omni may pause and resume the agent’s interaction in the chat. |
3443
+ | `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the conversation or from its wrap. See **Scheduling a follow-up**. |
3402
3444
  | `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
3403
3445
 
3404
3446
  ### Email capabilities
@@ -3406,6 +3448,7 @@ on the calendar.
3406
3448
  | Capability | Omni UI | Contract |
3407
3449
  | --- | --- | --- |
3408
3450
  | `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when local policy also permits declining. |
3451
+ | `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the message or from its wrap. See **Scheduling a follow-up**. |
3409
3452
  | `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
3410
3453
 
3411
3454
  ### Custom capabilities
@@ -3715,14 +3758,14 @@ The team break command formerly named release is now `end-forced-break`, and its
3715
3758
  `team.break.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
3716
3759
  The agent’s `endBreak` now applies to forced breaks as well as requested breaks.
3717
3760
 
3718
- `ForcedBreak` names who forced the break and may include an advisory `expectedDurationMs`.
3719
- The agent must explicitly resume when ready. The retired endsAutomatically and endsAt fields
3720
- are rejected, including when an expected duration is also provided.
3721
-
3722
- **Every forced break has a person behind it.** A lead or a manager forced it; there is no such
3723
- thing as a break the platform forced on its own. Where a platform applies one automatically, it is
3724
- executing a preference somebody configured, and that person is the owner of the action — `by` names
3725
- them, not the machinery that carried it out.
3761
+ `ForcedBreak` names who forced the break and may say how long it is expected to run yet, as
3762
+ `expectedEndsInSeconds`: whole seconds from the publication that carries it, restated on every
3763
+ publication, so every screen -- the agent's, reloaded or not, and every lead's -- counts down the
3764
+ same number from receipt (`break.forced.expectedEndsInSeconds`). The duration the lead asked for
3765
+ travels on the `force-break` command alone; the state says what is left, and drops the field once
3766
+ it has run out. The agent must explicitly resume when ready. The retired endsAutomatically and
3767
+ endsAt fields are rejected, including when an expectation is also provided. Who forced it is
3768
+ `by`, as **A forced break is the provider's act** below sets out.
3726
3769
 
3727
3770
  Omni resolves the name to show with `getUserDetails()`, so a provider sends the identifier and never
3728
3771
  a display name.
@@ -3737,16 +3780,24 @@ For example:
3737
3780
  ```ts
3738
3781
  forced: {
3739
3782
  by: "manager-1042",
3740
- expectedDurationMs: 600000
3783
+ expectedEndsInSeconds: 600
3741
3784
  }
3742
3785
  ```
3743
3786
 
3744
3787
  The agent application keeps **Resume** available for an agent on a forced break. The agent explicitly chooses
3745
3788
  Resume; the agent application sends `endBreak()` and follows the provider-confirmed state before resuming
3746
- work. An expected duration is a positive finite number of milliseconds, measured from actual
3747
- entry into `on-break`, excluding any `starting-after-task` wait. Omission means no expected
3748
- duration was supplied. Neither duration expiry nor an overdue indication authorizes the agent application
3749
- or provider to end the break, restore availability, or route work to the agent.
3789
+ work. The expectation is whole seconds still to run, restated on every publication; omission
3790
+ means none was stated, or it has run out. Neither its reaching zero nor an overdue indication
3791
+ authorizes the agent application or provider to end the break, restore availability, or route
3792
+ work to the agent.
3793
+
3794
+ **A forced break is the provider's act.** A lead's `force-break` is a command to the provider,
3795
+ which forces the break on the member; `by` names the lead who asked it to. A break the platform
3796
+ imposed on its own -- a schedule, a compliance hold -- says `by: "provider"`, and is the platform's
3797
+ to lift: a lead's `end-forced-break` on it is refused before it is sent
3798
+ (`team.command.endForcedBreak.provider`), and a provider that receives one answers `failed` with
3799
+ `omni.break-forced-by-provider`. The desk offers End forced break on a member only where
3800
+ `forced.by` names a lead.
3750
3801
 
3751
3802
  **A lead lifting the restriction does not resume the agent.** On an applied
3752
3803
  `end-forced-break`, the provider clears `BreakState.forced` while preserving the current
@@ -3762,18 +3813,25 @@ must correlate resumption to the authenticated agent's action; a state-only orde
3762
3813
  cannot establish the cause of a transition. The usual multi-provider end/reconciliation flow
3763
3814
  still applies, and work resumes only on confirmed provider state.
3764
3815
 
3765
- An agent application may display the expected duration. A countdown requires an evidenced actual start;
3766
- a received snapshot, replay or reconnect is not a new start and cannot restart the duration.
3767
- Without that evidence, show the duration without inventing a start or return time.
3768
-
3769
- A break applies to the **agent**, not to one provider. When a provider forces one, Omni immediately
3770
- requests a break on every other connected provider, or they would keep routing work to somebody who
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.
3816
+ An agent application counts `expectedEndsInSeconds` down from receipt, on every publication that
3817
+ carries it, and shows nothing of it where the provider sends none. It invents no start and no
3818
+ return time, and nothing acts on the number reaching zero.
3819
+
3820
+ A break applies to the **agent**, not to one provider. When a provider forces one, Omni stops the
3821
+ agent everywhere else at once, and not by asking: it states `setCapacity({ count: 0 })` on every
3822
+ other usable provider holding capacity, which is taken and never refused, and which their team
3823
+ lists show as `elsewhere`. Nobody on those providers can deny it, and the agent cannot cancel it,
3824
+ because there is nothing to cancel: a break request on them would be the agent's own to withdraw
3825
+ and a lead's to refuse, and a forced break is neither. When the forcing provider ends the forced
3826
+ break and the agent resumes there, the agent application restates its capacity on the rest. A
3827
+ provider whose login is not usable is left alone, as a break attempt leaves it;
3828
+ `assertForcedBreakStopsTheRest` holds an agent application to this set. **The provider keeps its
3829
+ shift totals true.** A member held elsewhere is one the provider sees -- `elsewhere` is a state it
3830
+ publishes -- and the day's `shift` is its own account of the day, so the time the agent was
3831
+ stopped for a break forced on another provider is the provider's to count, as a break of a kind
3832
+ its platform has, or introduces, for exactly this. A `breakSeconds` that leaves it out is a total
3833
+ the provider knew to be short, and **Never report a value you cannot observe** cuts the other way
3834
+ here: the provider observed it.
3777
3835
 
3778
3836
  `ForcedBreak.by` says who forced it: a lead, by user id, or `provider` where the platform itself
3779
3837
  did -- on its own rule, a schedule, a compliance hold. The agent sees who, and `getUserDetails()`
@@ -4163,10 +4221,10 @@ computes nothing. See **`team-updated`** and its companions.
4163
4221
  | `availability` | Required. What the member is doing now. |
4164
4222
  | `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
4165
4223
  | `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
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`). |
4224
+ | `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, and the task's record -- `history`, steps and totals -- travels whole, exactly as the member's desk holds it, so the lead's screen and the member's show the same record; only 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. |
4225
+ | `listening` | Every lead on this member's call — listening, coaching or joined — published to every lead alike: each entry names the lead by `leadId`, which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. The entry naming the signed-in lead is what their own audio opens on. Absent while nobody is on the call, never `[]` (`team.member.listening.*`); a lead is on one member's call at a time (`team.listening.single`), and a lead who is heard is also in the task's `onCall` (`team.member.listening.heard`). |
4168
4226
  | `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. |
4227
+ | `shift` | The member's own history for the day, about the person rather than any one call: `signedInAt`, `signedOutAt` once it has happened, today's totals as the provider counts them — `talkSeconds`, `holdSeconds`, `breakSeconds`, `tasksHandled`, each present only when the provider knows it — and the day's sign-in, sign-out and break `events`, oldest first. The same `Shift` the member's own snapshot carries, the same numbers from the same count. Omitted where the provider cannot say. |
4170
4228
 
4171
4229
  Each availability value means one thing:
4172
4230
 
@@ -4189,11 +4247,19 @@ by it, as a colleague signing in is. `team-member-removed` takes one off. Never
4189
4247
  member, never a task of theirs on its own: a hold on a member's desk is that member republished
4190
4248
  whole on the lead's, and nothing else on the lead's screen moves.
4191
4249
 
4250
+ **The agent's day is on the wire.** The lead sees `shift` totals as the provider counts them, and
4251
+ the agent sees the same: `Snapshot.shift` and `shift-updated` carry the agent's own day in the same
4252
+ shape, from the same count, so "calls today: 12" is one number on both screens. The desk computes
4253
+ nothing of the day; `tasksHandled` moves when a task ends and `talkSeconds` when a leg closes, and
4254
+ the active call's running time is the screen's own until then, as **Every screen counts from the
4255
+ provider's clock** sets out.
4256
+
4192
4257
  **Omit `since` rather than inventing one.** Omni renders it as a duration, so a timestamp
4193
4258
  synthesised from the adapter's own clock at seed time reads as "on task for 0 seconds" for
4194
4259
  everybody — worse than showing nothing, because it looks like data. Send it only when the provider
4195
4260
  knows when the state actually began. It times the current `availability`, so it moves every time
4196
- that value does.
4261
+ that value does. And it is counted from the provider's clock, never the screen's, so a provider
4262
+ that publishes it declares `timeCheck` -- see **Every screen counts from the provider's clock**.
4197
4263
 
4198
4264
  **The login is the permission.** A team member list goes to a login that declares
4199
4265
  `capabilities.lead`, once the team feature is on, and to nobody else. Omni never decides who
@@ -4241,7 +4307,7 @@ assignment from the member alone.
4241
4307
  | `{ type: "decide-break-request", memberId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
4242
4308
  | `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
4243
4309
  | `{ type: "force-break", memberId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory and is published on the resulting forced break. |
4244
- | `{ type: "end-forced-break", memberId }` | Lifts the forced-break restriction, whoever forced it, by clearing `BreakState.forced`. The committed break continues; only the agent resumes work. |
4310
+ | `{ type: "end-forced-break", memberId }` | Asks the provider to lift a forced-break restriction a lead asked for, by clearing `BreakState.forced`; one the platform imposed is its own to lift (`team.command.endForcedBreak.provider`, `omni.break-forced-by-provider`). The committed break continues; only the agent resumes work. |
4245
4311
  | `{ type: "join", memberId, assignmentId? }` | Answers the member's request: the provider bridges the lead's channel into the call. See **Lead assist**. |
4246
4312
  | `{ type: "decline", memberId, assignmentId?, reason? }` | Refuses the member's request. |
4247
4313
  | `{ type: "listen", memberId, assignmentId? }` | The lead's channel joins the member's call unasked, in silence. See **Listening to a call**. |
@@ -4286,9 +4352,10 @@ executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" }
4286
4352
  ```
4287
4353
 
4288
4354
  **On `join` the provider bridges the lead's channel into the call.** The member's task moves to
4289
- `leadAssist: { stage: "joined", leadId }`, the member on the team member list carries
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
4355
+ `leadAssist: { stage: "joined", leadId }` and its `onCall` gains the lead as an `agent`, since a
4356
+ lead who is heard is in the room; the member on the team member list carries
4357
+ `listening: [{ leadId, assignmentId, mode: "join-call", since }]`, and the lead's audio opens on
4358
+ it as **Listening to a call** describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
4292
4359
  work assigned to them.
4293
4360
 
4294
4361
  **A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
@@ -4356,10 +4423,11 @@ history. The three modes determine who hears the lead:
4356
4423
  All three come with the `lead` flag; a centre reserves none of them per lead.
4357
4424
 
4358
4425
  **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
4426
+ listening follows the provider's published state exactly as a task's does: the entry naming this
4427
+ lead in `listening` on the member is the provider's word that the lead's channel is in the call.
4428
+ When it appears, the application opens `openAudio({ assignmentId })` on the lead's connection for
4429
+ the call it names, and plays the `CallAudio` returned; when it goes, the application closes it.
4430
+ Another lead's entry opens nothing: it is shown, and that is all. Nothing else says
4363
4431
  when the lead's audio is up, and a reload reopens it from the snapshot's team member list as a task
4364
4432
  carried with `audio: "started"` is reopened.
4365
4433
 
@@ -4370,13 +4438,15 @@ The flow, end to end:
4370
4438
  // 2. The lead's channel joins it in silence. Conditions: the lead's voice channel is free --
4371
4439
  // not on a call, not listening elsewhere. Capacity is not involved: nothing is assigned.
4372
4440
  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.
4441
+ // 3. The provider publishes the member whole on team-member-updated, to every lead; the member carries
4442
+ // listening: [{ leadId: "L-9", assignmentId: "alloc-42", mode: "listen", since }]
4443
+ // -- L-9's application opens openAudio({ assignmentId: "alloc-42" }) on its own connection; every
4444
+ // other lead's shows L-9 on the call and opens nothing.
4445
+ // 4. The lead changes how they are heard; the member is published again with the entry's new mode. The audio stays open.
4446
+ // join-call is heard by everyone, so the member's task is also republished with L-9 on onCall as an agent.
4377
4447
  executeTeam({ command: { type: "coach", memberId: "A-1" } })
4378
4448
  executeTeam({ command: { type: "join-call", memberId: "A-1" } })
4379
- // 5. The lead leaves: the provider publishes the member without listening; the application closes the audio.
4449
+ // 5. The lead leaves: the provider publishes the member without L-9's entry, and the task without L-9 in the room; the application closes the audio.
4380
4450
  executeTeam({ command: { type: "leave", memberId: "A-1" } })
4381
4451
  // 6. The member's call ends: the same as leave, from the provider's side.
4382
4452
  // 7. The lead takes the call: the member loses listening no later than the offer, the application
@@ -4387,22 +4457,29 @@ executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
4387
4457
  // 8. A join on the member's request is the same flow entered at step 3 with mode: "join-call",
4388
4458
  // and the member's task shows leadAssist: joined.
4389
4459
  // 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.
4460
+ // still carries this lead's entry in listening on the member, so the audio is reopened.
4391
4461
  ```
4392
4462
 
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`).
4463
+ **`listening` on the member is the state**, one entry per lead on the call, restated on every
4464
+ change of mode, naming the lead (`team.member.listening.leadId`, once per member,
4465
+ `team.member.listening.unique`) and the call, so the application knows which audio is its own to
4466
+ open (`team.member.listening.assignmentId`, and one the member's tasks carry,
4467
+ `team.member.listening.assignment`). Every lead sees every entry: a silent lead is hidden from
4468
+ the agent, never from the other leads and managers watching the same member on other screens.
4469
+ A lead is on one member's call at a time (`team.listening.single`).
4397
4470
 
4398
4471
  **A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
4399
4472
  channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
4400
4473
  includes a lead on a break: a lead working as lead alone is on a break of a working kind, and
4401
4474
  listens through it.
4402
4475
 
4403
- **Nothing reaches the member.** The member's task carries no trace of a listening lead in any
4404
- mode: not on `onCall`, not in the record. Whether coaching is announced to the agent is the
4405
- platform's business and travels on the audio, not on this wire.
4476
+ **A silent lead reaches nobody but the leads.** In `listen` and `coach` the member's task carries
4477
+ no trace of the lead: not on `onCall`, not in the record. Whether coaching is announced to the
4478
+ agent is the platform's business and travels on the audio, not on this wire. In `join-call`, and
4479
+ on a `join` answering a request, the lead is heard by everyone and is in the room: the task's
4480
+ `onCall` carries them as an `agent` by user id, on the member's desk and on every lead's copy of
4481
+ the task alike, and `leave` takes the entry off as it takes the channel off
4482
+ (`team.member.listening.heard`).
4406
4483
 
4407
4484
  ### A member waiting for a break
4408
4485
 
@@ -4756,9 +4833,9 @@ declared:
4756
4833
  | `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`. |
4757
4834
  | `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
4758
4835
  | `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`. |
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`). |
4836
+ | `complete` | A wrap to cut short: any task but a `provider-automatic` one with `wrapAllowance: 0`, under either completion mode (`command.complete.wrapAllowance`). The `outcomes` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. What travels is what the capability published: a code from its list where it has one (`command.complete.outcome.unknown`), a code at all where it requires one (`.outcome.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no outcomes (`.outcome.unexpected`). |
4760
4837
  | `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. |
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. |
4838
+ | `schedule` | The `schedule` capability, on any channel, in `in-progress`, `paused` or `completing`: a follow-up is promised on the call and written up in wrap alike. The manifest declares `calendar` for it to land on. |
4762
4839
  | `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
4763
4840
  | `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`). |
4764
4841
  | `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`). |
@@ -4884,6 +4961,24 @@ must specify their uncertainty and validity; elapsed time should use monotonic a
4884
4961
  jump invalidates the estimate. The optional check below samples provider time without changing event timestamps. Report
4885
4962
  source-measured durations where required; timestamp subtraction is not a substitute.
4886
4963
 
4964
+ #### Every screen counts from the provider's clock
4965
+
4966
+ Two screens watching the same agent -- their own desk, their lead's, a manager's on another floor
4967
+ -- must show the same numbers. Delay between them is fine; arithmetic that differs is not. So
4968
+ nothing the desk shows is its own count, with one exception: the running duration of the active
4969
+ task or call, which each screen times from the moment it saw the call begin, and which two screens
4970
+ may therefore disagree on, since their clocks differ. Everything else comes from the wire.
4971
+
4972
+ Durations off the active call -- how long a member has been `on-break` from their `since`, how
4973
+ long a lead has been listening, how long since `signedInAt` -- are counted from the provider's
4974
+ clock, never the screen's. The desk keeps the latest provider instant it has received -- a
4975
+ snapshot's `providerTime`, an envelope's `occurredAt`, a `checkTime` result -- with the monotonic
4976
+ elapsed time since receipt, and renders every such duration from that; a sample older than
4977
+ `maxSampleAgeMs` is refreshed with `checkTime`. Without a usable sample the desk shows the instant
4978
+ and no count. A provider that publishes any of these instants therefore declares `timeCheck`
4979
+ (`manifest.timeCheck.required`) and states `providerTime` on every snapshot
4980
+ (`snapshot.providerTime`). Two screens then differ by their delay and nothing else.
4981
+
4887
4982
  #### Optional periodic provider time checks and agent application estimates
4888
4983
 
4889
4984
  A provider may declare `Manifest.timeCheck: true` and implement `Connection.checkTime(request)`.
@@ -5170,7 +5265,7 @@ take-over as it follows an `end-call` -- see **Lead assist**.
5170
5265
  A successful `complete` command does not clear the task. Omni waits for `task-ended`,
5171
5266
  and not for ever: `applied` to a `complete` says the
5172
5267
  provider has completed the task, and its `task-ended` follows within the
5173
- manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
5268
+ manifest's `settleMs`. A provider never answers `applied` for a completion it has not yet
5174
5269
  performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
5175
5270
  held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
5176
5271
  the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
@@ -5269,6 +5364,13 @@ the `contacts` idle capability.
5269
5364
  Replaces this provider's complete scheduled-activity contribution. It is emitted only when the manifest
5270
5365
  declares the `calendar` idle capability.
5271
5366
 
5367
+ ### `shift-updated`
5368
+
5369
+ The agent's own day, whole, replacing `Snapshot.shift`: emitted whenever a total moves -- a task
5370
+ ended, a break ended, a sign-out -- and never to say nothing changed. It carries the same `Shift`
5371
+ the agent's lead sees for them on the team member list, so the two screens show one day. See
5372
+ **The agent's day is on the wire**.
5373
+
5272
5374
  ## Utilities
5273
5375
 
5274
5376
  ### `assignmentKey(providerId, assignmentId)`
@@ -5333,7 +5435,9 @@ carries that agent reports `team.member.self`, on a `team-member-updated` and a
5333
5435
  reports `team.unentitled`, and a team event to such a login reports `event.team.capability`; given
5334
5436
  `leadFeatures: true`, a snapshot without a team reports `team.required`, and given `leadFeatures: false`,
5335
5437
  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.
5438
+ `exerciseAdapter` passes all three, holding the switch off until it has sent it. Given the manifest, a
5439
+ snapshot or event that carries an instant the desk renders as a running duration under a manifest
5440
+ without `timeCheck` reports `manifest.timeCheck.required`.
5337
5441
 
5338
5442
  `assertNoViolations(violations)` throws `ProtocolConformanceError` — which carries the full
5339
5443
  `violations` array — when the list is non-empty.
@@ -5515,6 +5619,7 @@ cannot be established from TypeScript structure alone.
5515
5619
  | `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a forced break arriving in effect with `forced`. The harness applies the same rules after the connect snapshot. |
5516
5620
  | `assertAudioFollowsTheTask(envelopes, snapshot?)` | The audio follows the task and never decides it: every task is introduced once, `task-audio-started` and `task-audio-ended` alternate on work that has begun, audio ends only where it arrived, and what follows the audio ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no audio satisfies it by never testing it — pair it with the assertion that the audio end is present. |
5517
5621
  | `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
5622
+ | `assertForcedBreakStopsTheRest(forcedOn, candidates, stopped)` | A forced break on one provider stops the agent everywhere else: capacity zero stated on every other usable provider holding capacity, nothing on the forcing one, nothing on a dead login. See **Forced breaks**. |
5518
5623
  | `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `on-break` only once nothing is outstanding — never beside a task, never later than the step that has none. |
5519
5624
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
5520
5625
  | `assertWrapTimeout(task, audioEndedAt, deadline, toleranceMs?)` | The wrap deadline equals audio end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |