@xema/omni-protocol 0.1.81 → 0.1.83

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
@@ -58,7 +58,7 @@ transport, a task, a break and a call each have a word of their own:
58
58
 
59
59
  ### `OMNI_PROTOCOL_VERSION`
60
60
 
61
- The exact protocol version implemented by this package. The current value is `2`. Version 2 replaces the broad recording flag and untargeted recording command; version 1 is not negotiated by this package. Hosts and adapters must explicitly migrate together or retain their own version-1 implementation.
61
+ The exact protocol version implemented by this package. The current value remains `1` during pre-release development. Recording contract changes do not introduce a new protocol version. Hosts and adapters adopt the current declarations together; this does not provide compatibility mapping for older recording shapes.
62
62
 
63
63
  ### `Manifest.supportedProtocolVersions`
64
64
 
@@ -189,6 +189,8 @@ type Manifest<C extends Channel = Channel> = {
189
189
  supportedProtocolVersions: number[];
190
190
  authenticationMethods: AuthenticationMethod[];
191
191
  idleCapabilities?: IdleCapabilities<C>;
192
+ timeCheck?: true;
193
+ timestampAuthority?: "provider";
192
194
  phaseLabels?: TaskPhaseLabels;
193
195
  taskTypePresentation?: Record<string, TaskTypePresentation>;
194
196
  orgLevels?: LevelDeclaration[];
@@ -292,13 +294,12 @@ type UrlVisibility = "full" | "domain" | "hidden";
292
294
  type RecordingSource = "provider" | "host";
293
295
  type RecordingAction = "start" | "pause" | "resume" | "stop" | "cancel";
294
296
  const RECORDING_ACTIONS = ["start", "pause", "resume", "stop", "cancel"] as const satisfies readonly RecordingAction[];
295
- type RecordingCancelEffect = "retain" | "discard";
296
297
  interface RecordingActions {
297
298
  start?: true;
298
299
  pause?: true;
299
300
  resume?: true;
300
301
  stop?: true;
301
- cancel?: { effect: RecordingCancelEffect };
302
+ cancel?: true;
302
303
  }
303
304
  interface TaskRecordingPolicy {
304
305
  provider?: RecordingActions;
@@ -316,9 +317,8 @@ type RecordingCommand = {
316
317
  requestId: string;
317
318
  observationId: string;
318
319
  } & (
319
- | { action: "start"; recordingId?: never; cancelEffect?: never }
320
- | { action: "pause" | "resume" | "stop"; recordingId: string; cancelEffect?: never }
321
- | { action: "cancel"; recordingId: string; cancelEffect: RecordingCancelEffect }
320
+ | { action: "start"; recordingId?: never }
321
+ | { action: "pause" | "resume" | "stop" | "cancel"; recordingId: string }
322
322
  );
323
323
  interface HostRecordingReport {
324
324
  taskId: TaskId;
@@ -326,8 +326,8 @@ interface HostRecordingReport {
326
326
  state: RecordingState;
327
327
  }
328
328
  interface HostRecording {
329
+ announcesToCaller?: true;
329
330
  actions: RecordingAction[];
330
- cancelEffects: RecordingCancelEffect[];
331
331
  destinationIds: string[];
332
332
  execute(request: HostRecordingRequest): Promise<RecordingCommandResult>;
333
333
  }
@@ -352,7 +352,15 @@ type HostGuarantees = {
352
352
  personConsent?: true;
353
353
  };
354
354
 
355
+ type ProviderTimeScope = { providerId: string; loginId: string };
356
+ type ProviderTimeEstimate = ProviderTimeScope & {
357
+ at: IsoTimestamp;
358
+ clockId: string;
359
+ uncertaintyMs?: number;
360
+ };
361
+
355
362
  type Host = {
363
+ estimateProviderTime?(scope: ProviderTimeScope): ProviderTimeEstimate | undefined;
356
364
  recording?: HostRecording;
357
365
  guarantees: HostGuarantees;
358
366
  mute?: HostMute;
@@ -460,7 +468,7 @@ type HandlingReport = { taskId: TaskId; allocationId: AllocationId; at: IsoTimes
460
468
  );
461
469
 
462
470
  type HandlingReportResult =
463
- | { status: "recorded" }
471
+ | { status: "recorded"; at: IsoTimestamp }
464
472
  | { status: "failed"; failure: ProtocolFailure };
465
473
 
466
474
  type PreferenceResult =
@@ -741,7 +749,7 @@ type Task<C extends Channel = Channel> = {
741
749
  : { recording?: never; onCall?: never; leadAssist?: never; assisting?: never; monitoring?: never; media?: never }
742
750
  );
743
751
 
744
- type PreviewDeadline = "calls" | "expires";
752
+ type PreviewDeadline = "calls" | "host-calls" | "waits";
745
753
 
746
754
  type AcceptanceMode =
747
755
  | "no-preference"
@@ -1130,7 +1138,22 @@ type Refusal = {
1130
1138
  violations: ProtocolViolation[];
1131
1139
  };
1132
1140
 
1141
+ type ProviderTimeCheckPolicy = {
1142
+ intervalMs: number;
1143
+ timeoutMs: number;
1144
+ maxRoundTripMs: number;
1145
+ maxSampleAgeMs: number;
1146
+ };
1147
+ type ProviderTimeCheckRequest = { requestId: string };
1148
+ type ProviderTimeCheckResult = {
1149
+ requestId: string;
1150
+ loginId: string;
1151
+ clockId: string;
1152
+ providerTime: IsoTimestamp;
1153
+ };
1154
+
1133
1155
  type Connection<C extends Channel = Channel> = {
1156
+ checkTime?(request: ProviderTimeCheckRequest): Promise<ProviderTimeCheckResult>;
1134
1157
  snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
1135
1158
  subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
1136
1159
  refused(report: Refusal): void;
@@ -1585,6 +1608,8 @@ compile time.
1585
1608
  | `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**. |
1586
1609
  | `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the host) and/or `deskPhone` (a handset the platform rings; the host shows the call and opens nothing). The host picks one per login. See **How the agent hears the call**. |
1587
1610
  | `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**. |
1611
+ | `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; host polling is independently opt-in. |
1612
+ | `timestampAuthority` | Optional `"provider"`: provider timestamps are final; host instants are advisory. Omission makes no trust promise. |
1588
1613
  | `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
1589
1614
  | `disposalSettleMs` | Required. How long after an applied disposal -- `complete`, or a lead's `take-over` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.disposalSettleMs`). A warm transfer's `complete` is not a disposal: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1590
1615
 
@@ -2120,6 +2145,7 @@ surface in one place, and what obliges an adapter to implement each one.
2120
2145
  | Method | Implement it when |
2121
2146
  | --- | --- |
2122
2147
  | `snapshot()` | Always. |
2148
+ | `checkTime(request)` | The manifest declares `timeCheck: true`. Read-only clock check; no task command or accuracy guarantee. |
2123
2149
  | `subscribe(listener)` | Always. |
2124
2150
  | `disconnect()` | Always. |
2125
2151
  | `refused(report)` | Always. The host 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 host does with what it refuses**. |
@@ -2355,10 +2381,10 @@ time. Runtime conformance checks also require the task channel to match its prov
2355
2381
  | `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
2356
2382
  | `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
2357
2383
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
2358
- | `media` | 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-media-started`**. Media names a task whose work has begun, or whose party the host is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it; with the party ringing by a host dial, ring-back is audio and media starts on `dialling`. |
2384
+ | `media` | 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-media-started`**. Media names a task whose work has begun, or whose party the host is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it; with the party ringing by a host dial or provider-triggered preview dial, actual ring-back media may precede answer. |
2359
2385
  | `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**. |
2360
- | `previewEndsAt` | Voice only, in `preview`: when the system stops waiting for the agent to press Call. Absent, the agent has as long as they need. Always with `atDeadline`. See **Preview: the agent presses Call**. |
2361
- | `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` places the call itself, `expires` takes the record back and the task ends `expired`. |
2386
+ | `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**. |
2387
+ | `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` makes the provider initiate dialing, `host-calls` makes the host issue Call, `waits` keeps the task in preview awaiting the agent. |
2362
2388
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
2363
2389
  | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
2364
2390
  | `wrapAllowance` | Fixed time allowed to complete the task after primary handling ends. For real-time media, it begins after `task-media-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. |
@@ -2401,7 +2427,7 @@ The canonical task transitions are:
2401
2427
  | `confirmed` | Work begins | `in-progress` |
2402
2428
  | `preview` | Agent presses Call (`call`) and the customer answers, or the deadline `calls` and they answer | `in-progress` |
2403
2429
  | `preview` | The call goes out and nobody answers | `completing` |
2404
- | `preview` | The deadline `expires` | Removed by `task-ended` with `expired` outcome |
2430
+ | `preview` | The preparation target passes with `atDeadline: "waits"` | Remains `preview`, waiting for the agent to press Call |
2405
2431
  | `pending` | Provider withdraws the allocation, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
2406
2432
  | `pending` | The offer lapses at `allocationExpiresAt` | Removed by `task-ended` with `expired` outcome naming `pending` |
2407
2433
  | No task | Snapshot reports work already underway | `in-progress` |
@@ -2439,8 +2465,9 @@ command.
2439
2465
  #### Preview: the agent presses Call
2440
2466
 
2441
2467
  An outbound campaign that lets the agent see who they are about to call is a **preview**: the
2442
- record arrives as a task, the task sits in `preview` with the party on it, and no call has gone
2443
- out. The agent reads and presses Call. That is the whole phase, and it exists on voice alone --
2468
+ record arrives as a task and sits in `preview` with the party on it before a call goes
2469
+ out. The agent prepares; manual or declared automatic initiation may then begin dialing. It
2470
+ remains preview while ringing until an evidenced answer or non-answer outcome. The agent may press Call. That is the whole phase, and it exists on voice alone --
2444
2471
  chat and email have no call to place, and their reading is ordinary work in `in-progress`.
2445
2472
 
2446
2473
  ```ts
@@ -2468,14 +2495,51 @@ no-answer as the disposition it is -- in a campaign that is the commonest outcom
2468
2495
  is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
2469
2496
  (`task.preview.dialOutcomes.required`).
2470
2497
 
2471
- **The deadline says what the system will do.** A provider that will not wait forever states
2472
- `previewEndsAt`, and with it `atDeadline`: `calls` means the system places the call itself when the
2473
- time runs out, which is progressive dialling; `expires` means the record is taken back and the task
2474
- ends with an `expired` outcome naming `preview`. The two travel together
2475
- (`task.preview.atDeadline.required`, `task.preview.previewEndsAt.required`), only while the task is in
2476
- `preview` (`task.preview.deadline.unexpected`), and Omni's countdown reads "calling in 12s" or
2477
- "expires in 12s" from the provider's word. With neither, the agent has as long as they need.
2478
- Reaching the instant is not itself a transition: the provider reports what it did, as an event.
2498
+ **Preparation and trigger ownership are per task.** Unlimited preparation omits both
2499
+ `previewEndsAt` and `atDeadline`: only an agent pressing Call starts dialing. Fixed preparation
2500
+ carries the provider's authoritative preparation-end instant and exactly one trigger owner:
2501
+
2502
+ | Declaration | At preparation end |
2503
+ | --- | --- |
2504
+ | `atDeadline: "calls"` | The provider initiates dialing. The host never sends a timer-triggered Call. |
2505
+ | `atDeadline: "host-calls"` | The host sends the ordinary `call` command with a fresh host `dialId`. The provider does not independently auto-dial this task. |
2506
+ | `atDeadline: "waits"` | Neither side auto-dials. The task remains in preview until the agent presses Call. |
2507
+
2508
+ The agent may press Call early in either fixed-preparation dialing mode, or at any time in
2509
+ `waits` mode. With `waits`, the countdown is a preparation target, not an expiry: when it reaches
2510
+ zero the UI says "Waiting for agent" and keeps Call available. There is no automatic task-ended,
2511
+ disposal, dialing or phase change. No repeating timer action occurs on later snapshots. This differs
2512
+ from unlimited preparation only by displaying a preparation target. The former `expires` deadline
2513
+ value is not supported; elapsed preparation must not withdraw the task. For `calls` and `host-calls`, the deadline ends
2514
+ preparation and triggers initiation; it does not promise ringing, playable audio or customer
2515
+ answer at that exact instant. Scheduling/dispatch delay must remain visible, and a failed or
2516
+ prevented initiation must be reported rather than leaving a silently expired countdown.
2517
+ A source eligibility threshold that may never trigger an attempt is not this promise.
2518
+
2519
+ The two deadline fields travel together and only in preview. The host uses trusted provider-domain
2520
+ time; clock estimation is allowed only under the explicit host-triggered preview exception below.
2521
+ Without usable provider-domain time
2522
+ it cannot safely auto-trigger and must expose the uncertainty and reconcile. No zero-duration,
2523
+ receipt-time or local-default deadline is inferred. Neither elapsed time nor submission changes
2524
+ the task phase: subsequent authoritative events state ringing, answer, media and completion.
2525
+
2526
+ A host must serialize manual clicks and its timer under the exact provider/login/task/allocation
2527
+ scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
2528
+ allocation and whether an attempt already exists before dispatch. A new snapshot or policy update
2529
+ cancels an obsolete timer; login loss, disconnect, task end and allocation replacement fence its
2530
+ callback. Reconnect past a deadline first reconciles current task and attempt state; it must not
2531
+ blindly submit again. An uncertain prior submission remains unresolved, without automatic retry.
2532
+ The provider atomically arbitrates any residual manual/timer race and does not accept a second
2533
+ command as ownership of an already-running independent attempt.
2534
+
2535
+ Host-triggered automatic dialing is an ordinary host dial: its result is `dialling` with the same
2536
+ host `dialId`, followed by exactly one correlated terminal `dial-outcome`. A provider-triggered
2537
+ dial has no invented host ID or host dial outcome. It reports source-evidenced party ringing,
2538
+ actual media and customer answer separately. In preview with `atDeadline: "calls"`, a ringing
2539
+ party without a host ID may carry actual pre-answer media; the deadline alone permits no media.
2540
+ Media is still introduced/ended by the corresponding events and must not imply customer answer.
2541
+ Submission acceptance is never an answered outcome or permission to publish `in-progress` early.
2542
+ Unsupported source correlation/outcome/media evidence remains an integration blocker.
2479
2543
 
2480
2544
  **Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
2481
2545
  spreading the `preview` one carries `previewEndsAt` and `atDeadline` with it, and the host refuses
@@ -2484,6 +2548,33 @@ provider is told through `refused` exactly which rule, so the state that looked
2484
2548
  is named on its side. The task past preview has no deadline to wait for, so it carries neither
2485
2549
  field.
2486
2550
 
2551
+ **Provider execution and host controls are distinct.** Answer, hold/resume, transfer (including
2552
+ IVR routing), conference and handling completion are executed by the provider
2553
+ through the adapter. The task's applicable capability, phase, completion mode and command-specific
2554
+ prerequisites decide which controls are offered. Caller disconnect also executes at the provider. Microphone mute is host-owned, never a provider
2555
+ TaskCommand or task capability. The separately declared host recording facility keeps its existing
2556
+ recording contract; this distinction does not remove it or move provider call operations to the host.
2557
+
2558
+ A caller journey may revisit the same agent while an earlier handling still wraps. Public task
2559
+ IDs must remain distinct for concurrently open handlings. For source-owned handling IDs allocated
2560
+ before offer and never reused per assignment, both public task ID and allocation ID may equal that
2561
+ handling ID. Restore them unchanged on reconnect. Commands, media, endings and handling reports
2562
+ retain the exact pair; never retarget an old command to the newest handling of a journey.
2563
+ `validateTaskCommandRequest` checks the request's pair against the supplied published task and
2564
+ then its command prerequisites. The caller must select that task within the correct provider/login
2565
+ and recheck at the provider; the validator cannot prove source freshness or authorization.
2566
+ `task-ended` retires only that handling's resources. Another handling, caller channel or IVR/queue
2567
+ portion must not be cleared because this task ended. Snapshot counts include all still-owned wrap
2568
+ and active handlings; task disposal is not caller hangup.
2569
+
2570
+ **Voice describes the agent's handling within the wider call.** The task defines that agent's
2571
+ workspace, tools, permissions, media participation and completion work. The caller's channel may
2572
+ continue through IVR, queues, other agents, holds or conferences while this handling ends or wraps.
2573
+ Disposing this task ends this handling responsibility; it is not evidence that the caller's channel
2574
+ or journey ended. Task media describes this agent's attachment, not the lifetime of every party's
2575
+ connection. Commands retain their explicit targets and effects; task disposal must not silently
2576
+ become a caller-disconnect operation.
2577
+
2487
2578
  **A task is never its audio.** A voice task is the allocation: the call is offered when it is
2488
2579
  routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
2489
2580
  the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
@@ -2690,12 +2781,27 @@ platform cannot hold a leg the host reported keeps it in the login's `store` for
2690
2781
  task, so a reload of the host restates the same record; the entry is keyed by `step` and `at`, so
2691
2782
  a running hold restated with its final `seconds` is the same entry.
2692
2783
 
2693
- **The record is ordered by the instants stated in it, whoever stamped them.** The host stamps the
2694
- legs it performs from the same clock it reports everything else with, and the provider writes a
2695
- host leg into the record with the host's `at`, in its place among the others by that instant, and
2696
- answers `recorded`: it never refuses a leg, or the task, for a timestamp it did not write. A host
2697
- whose clock runs ahead of the platform's puts its own mute before the platform's answer, and the
2698
- record shows what was stated; that is the host's clock to fix, not the provider's record to edit. **Handle time is anchored, not restarted.** It runs from the
2784
+ **The provider owns the timestamps in its record.** Host timestamps are advisory. The provider
2785
+ may retain a host instant it accepts or assign its own observation/receipt instant; it need not
2786
+ copy the host clock. A provider declaring `timestampAuthority: "provider"` uses its own timestamps
2787
+ for final records. Receipt time is an observation boundary, not a claim about when the physical
2788
+ host action happened. Records are ordered by their published provider-selected instants, with no
2789
+ rewriting of an already-published entry when a later host report arrives.
2790
+
2791
+ The incoming host `step` and `at`, scoped to the task and its current life, identify one reported
2792
+ leg for correlation only. The provider retains a binding from that key to its chosen history
2793
+ `at`. Every successful `recordStep` returns `{ status: "recorded", at }` with that same canonical
2794
+ history instant, even for subsequent duration/end reports. The host keeps sending the original
2795
+ report key, never the returned history timestamp. A changed host timestamp is not a correction
2796
+ to the same report: it names a different leg. Bindings survive reconnect/reload for the retained
2797
+ task lifetime. The host renders provider-published history unchanged. It never compares the
2798
+ provider instant back to its own clock, substitutes its own timestamp, or adjusts later messages
2799
+ to match an earlier host estimate. The acknowledgment identifies the provider record for
2800
+ correlation/conformance only; it is not a host-side timestamp reconciliation instruction. Conflicting reuse is refused visibly, not paired by arrival time or nearest instant.
2801
+ If two distinct entries would collide under the history's `(step, at)` key, do not invent a time
2802
+ or combine them: report the representation conflict. No ambiguous event is silently accepted.
2803
+
2804
+ **Handle time is anchored, not restarted.** It runs from the
2699
2805
  `answered` step's `at` — from the task's first `in-progress` where the provider reports no
2700
2806
  history — until the task's media ends, and a hold neither pauses nor resets it: the hold's own
2701
2807
  duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
@@ -2778,8 +2884,9 @@ void connection.recordStep?.({ taskId, allocationId, step: "muted", at, mutedBy:
2778
2884
  void connection.recordStep?.({ taskId, allocationId, step: "muted", at, mutedBy: "host", seconds: 42, ended: true }); // the moment they unmute
2779
2885
  ```
2780
2886
 
2781
- The entry is keyed by `step` and `at`, so every report about one leg names the same instant. The
2782
- host is the authority for the legs it performs, so it may say how long so far — `seconds` is
2887
+ Every report about one leg repeats its original `step` and host `at` as a correlation key;
2888
+ the published history uses the provider-selected `at` returned by `recordStep`. The host reports
2889
+ the action it performed and its measured duration, not an authoritative provider timestamp. It may say how long so far — `seconds` is
2783
2890
  elapsed while the leg runs and final once it has ended — and **the end is stated, never
2784
2891
  inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
2785
2892
  provider never asked for never crosses.** The running report is sent only to a provider whose
@@ -2789,16 +2896,33 @@ a running one it was never asked for (`handlingReport.running.unexpected`). What
2789
2896
  did ask for them forwards upstream, and how often, is its own business. The step appears in
2790
2897
  `handlingHistory` when the *provider* publishes it: Omni never writes the record itself.
2791
2898
  `recordStep` is required of every softphone login's connection, since every call on a softphone
2792
- can be muted by the host, and answers `recorded`. A `muted` report says whose the silence was,
2899
+ can be muted by the host, and answers `recorded` with the canonical history `at`. A `muted` report says whose the silence was,
2793
2900
  `mutedBy: "host"` or `"station"`, and no other report has the word (`handlingReport.mutedBy`,
2794
2901
  `.mutedBy.unexpected`). On a desk phone the microphone is the phone's:
2795
2902
  the host mutes nothing and records nothing.
2796
2903
 
2797
- **A host-performed leg still open when the task's media ends, or the task ends, is ended by the
2798
- host at that instant** — `ended: true`, `seconds` to the end, the same `at` — since a provider
2799
- left to close it would be guessing at a host-performed duration. Both ends are read off one clock,
2800
- the host's, and a leg shorter than a second is reported as the second it was, never as the nought
2801
- the record refuses.
2904
+ **The provider's confirmed end is decisive.** The host keeps local state for interaction and
2905
+ reporting, but follows the provider's authoritative state. When the provider publishes the end
2906
+ of the current mute leg, the host ends that local mute and releases its host-controlled mute
2907
+ on the matching task's media. It does not wait for its own timer, reopen the leg, replace the
2908
+ provider timestamp, or overwrite the provider's final duration with a later host report.
2909
+ A later agent mute is a new leg, never a reopening of the ended one.
2910
+
2911
+ Correlate the provider-published `muted` entry using the provider-selected `at` acknowledged
2912
+ for the current report key; a final `seconds` closes that leg under the history contract.
2913
+ This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
2914
+ task, or a stale connection cannot end a newer local mute. If publication precedes the acknowledgment,
2915
+ retain the current provider view and apply the matching closure once correlation is available;
2916
+ never guess a match from arrival order or nearest timestamp. Failure to release a host-controlled
2917
+ mute is reported visibly; local device reports still describe the actual device state.
2918
+
2919
+ At a provider-confirmed task/media end, the host also stops the associated local mute and reports
2920
+ its observed ending where the report is still accepted. A host closing report repeats its original
2921
+ key and may include its measured duration, but cannot reverse an already confirmed provider end.
2922
+ The provider acknowledges a known closed leg without rewriting its final record; after the task
2923
+ has been disposed, it may refuse the report as task-not-found. The host reads that response rather
2924
+ than retrying or recreating the task. An observed host end before a provider closure is still
2925
+ reported normally; the provider decides the final timestamp and publishes the record.
2802
2926
 
2803
2927
  ### Browser capability
2804
2928
 
@@ -3039,7 +3163,7 @@ See **Which commands need a capability**.
3039
3163
  | --- | --- | --- |
3040
3164
  | `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when provisioning also permits declining. |
3041
3165
  | `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
3042
- | `endCall` | Primary button: End call | Omni may end the whole call: everyone leaves, the media ends, and the task stays for its wrap-up. See **Ending a call, and removing one person from it**. |
3166
+ | `endCall` | Primary button: End call | The provider ends the caller connection and all owned/inherited agent-added channels; wrap/disposal remain separate. See **Ending a call, and removing one person from it**. |
3043
3167
  | `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 disposes at provider end. See **Connecting back during completion**. |
3044
3168
  | `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
3045
3169
  | `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**. |
@@ -3105,12 +3229,12 @@ kind travels, nothing is typed, and a control with an empty directory has nothin
3105
3229
  refused (`task.destinations.offer`). A control declared as bare `true` is refused for the same
3106
3230
  reason (`task.destinations.shape`): once nothing is typed, the directory is the control.
3107
3231
 
3108
- **An agent never transfers to a named agent**, warm or cold. A contact that needs another pair of
3109
- hands goes back to a queue or a menu the queue configured, and who takes it next is the queue's
3110
- decision: routing by skill, availability and fairness is what the queue is for, and a hand-off to a
3111
- chosen colleague bypasses all three. The desk shows no agent picker and no dial pad on a transfer.
3112
- The one way a named person ends up with an agent's call is a lead taking it over through lead
3113
- assist, which is the lead's act, not a transfer the agent chose.
3232
+ **The provider chooses the supported destinations.** A published item may route to an IVR,
3233
+ queue, named agent or another configured destination. No destination kind is inferred from its
3234
+ label. The host offers only the directory attached to this task's specific transfer capability,
3235
+ and sends the exact destination ID; it does not invent an agent picker or arbitrary dial target.
3236
+ The provider rechecks eligibility and executes the routing. Returning to an IVR or queue does not
3237
+ end the caller journey; a later assignment to the same agent is a new handling.
3114
3238
 
3115
3239
  #### Ending a call, and removing one person from it
3116
3240
 
@@ -3118,18 +3242,44 @@ A call has everyone on `Task.onCall`, and two commands take people off it, both
3118
3242
  provider.
3119
3243
 
3120
3244
  ```ts
3121
- { type: "end-call" } // everyone leaves; the media ends; the task stays for its wrap-up
3245
+ { type: "end-call" } // provider ends caller and owned/inherited agent-added channels; wrap/disposal are separate
3122
3246
  { type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
3123
3247
  { type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
3124
3248
  ```
3125
3249
 
3126
- `end-call` ends the whole conversation for everyone on it, and is gated by `endCall`. The task does
3127
- not end with it: `task-media-ended` follows, any wrap allowance runs, and the agent dispositions
3128
- the call. `conference` `remove` takes one person off, named as the room names them, and the call
3129
- goes on for the rest; it is gated by `conference`, since it only means something with a third
3130
- person on the line. Removing the party leaves the agent with the colleague, a warm hand-over in
3131
- reverse. A remove that would leave the agent alone is not a remove but an `end-call`, and a
3132
- provider answers it `failed`. The consulted destination of a warm transfer is not removed this way:
3250
+ The host sends the ordinary task-scoped `execute` request with `command: { type: "end-call" }`.
3251
+ The provider knows the caller and agent-added channel bindings; no channel list or owner details
3252
+ are sent by the host. The task's `endCall` capability is permission, not an executor selection.
3253
+ `validateTaskCommandRequest` checks the exact handling/allocation and delegates the command's
3254
+ policy/phase checks. The provider rechecks current ownership atomically; no client validator can
3255
+ prove that its view is fresh. The host does not implement agent end-call; low-level removal of individual channels is not
3256
+ this agent action. There is no host end-call execution API or fallback route.
3257
+
3258
+ `end-call` is gated by `endCall` on the current handling. It ends the caller connection and
3259
+ all agent-added channels owned by that handling, including channels originally added by previous
3260
+ agents and inherited through transfer or takeover. Ownership is not inferred from who originally
3261
+ dialed, the current UI selection, or a person's label. The provider maintains that authority
3262
+ and checks the exact handling/allocation when acting. Agent-added channels are not left behind
3263
+ merely because another agent added them.
3264
+
3265
+ On a successful transfer or takeover, ownership of those channels passes to the receiving
3266
+ handling (lead or another agent), not just ownership of the caller leg. The outgoing handling may
3267
+ still have its allowed wrap work, but cannot disconnect channels it no longer owns. A pending or
3268
+ unknown handover is not a confirmed ownership change: retain source reservations and reconcile.
3269
+ The provider must serialize handover and end-call so a delayed outgoing command cannot tear down
3270
+ the recipient's call. No public channel-owner field or guessed identity is introduced here;
3271
+ the source must provide authoritative ownership and command fencing before declaring permission.
3272
+
3273
+ The resulting end of this agent's media is published as `task-media-ended`. Any wrap remains
3274
+ until its own completion/disposal; `end-call` is not `task-ended`, and `complete` is not an implicit
3275
+ caller/channel disconnect. Other agents' independent tasks end or wrap only on their own source
3276
+ transitions. Neither disposal nor transfer itself ends the continuing caller journey.
3277
+
3278
+ `conference` `remove` remains a person-specific provider operation, gated by `conference`.
3279
+ It does not implicitly dispose any other handling. Its person/consultation prerequisites
3280
+ remain separate from the permission to disconnect the caller. A remove that would leave the
3281
+ agent alone is not a remove but an `end-call`, and the provider answers it `failed`.
3282
+ The consulted destination of a warm transfer is not removed this way:
3133
3283
  `transfer` `cancel` is its own step, which returns the agent to the parked customer, and a remove
3134
3284
  never promises that.
3135
3285
 
@@ -3357,14 +3507,12 @@ the platform added itself carries none. `held: true` is presence as claim. A sna
3357
3507
  after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
3358
3508
  never saw.
3359
3509
 
3360
- **`onCall` is who is on the call now, and a task outlives its call.** Wrap-up is not an ending:
3361
- the task is alive, the agent is working, and nothing arrives to say the call ended until they
3362
- dispose. So the room must not be the last word about a call that has ended. Once the call is over
3363
- -- the task `completing`, or its `media` ended -- `onCall` is empty, or absent only where the
3364
- provider never publishes the room at all, and a task carrying people on a call that has ended is
3365
- refused (`task.onCall.ended`). Empty says nobody is on the call; absent says the provider does not
3366
- say who is, which is a different claim, so a provider that publishes the room publishes one last
3367
- change: the empty room.
3510
+ **`onCall` describes this agent's current handling.** When the caller disconnects but the agent
3511
+ and added channels remain connected, the remaining room is still valid. When this handling's
3512
+ media ends or the task enters `completing`, clear its `onCall` view; absence is used only by a
3513
+ provider that never publishes the room. This does not assert that the caller, bridge or other
3514
+ agents' channels ended. A published room receives a final empty view for this handling, and the
3515
+ task may remain open for wrap. Disposal is a separate task action, not the caller's disconnect.
3368
3516
 
3369
3517
  **A field that describes the present is cleared by the transition that ends it.** `onCall`,
3370
3518
  `previewEndsAt` and `atDeadline` are three instances of one shape, and there will be more: each
@@ -3554,6 +3702,40 @@ against it (`stream.taskOffered.overCapacity`).
3554
3702
  idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
3555
3703
  it: the ceiling binds allocation, not the agent's own hand.
3556
3704
 
3705
+ ### Runtime break prerequisite checks
3706
+
3707
+ Use `validateBreakCommand(method, request, state, context)` from the validation entry point
3708
+ before any agent break dispatch. The context supplies current authentication and transport;
3709
+ all four methods require the live login's break capability and active transport. Pass the
3710
+ request object only to the request method, and undefined to the other three.
3711
+
3712
+ | Method | Required current state |
3713
+ | --- | --- |
3714
+ | `requestBreak` | `not-requested`; selected current reason code when codes exist; `mayAsk` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
3715
+ | `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `mayAsk` does not revoke the grant. |
3716
+ | `cancelBreak` | `awaiting-decision` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
3717
+ | `endBreak` | `in-effect` or `starting-after-task` during reconciliation; an agent cannot end an imposed break. |
3718
+
3719
+ Use `validateTeamBreakCommand(request, context)` for lead decisions, placement, release and
3720
+ policy commands. It requires the live lead capability and active transport, a current target
3721
+ roster for member commands, and the target's complete break state for placement/release.
3722
+ Approve/deny requires an awaiting decision; placement uses the target's reason codes; release
3723
+ requires an imposed committed break. The context's target state must belong to the named member;
3724
+ that association and backend authorization are provider responsibilities.
3725
+
3726
+ Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
3727
+ as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
3728
+ checks. Pending and completing tasks still count as work. Only the existing monitoring exception
3729
+ allows tasks beside a break in effect. Validate full task shapes and the envelope separately.
3730
+
3731
+ These validators report violations without dispatching, rewriting state or proving freshness.
3732
+ Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
3733
+ Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
3734
+ is not a grant, and no method response overwrites a newer state emission. Rejected promises
3735
+ remain unknown and recover by snapshot, not automatic retry. The host's frozen provider set,
3736
+ durable mutually exclusive commit/cancel decision and reconciliation obligations above remain
3737
+ required; per-provider validation does not implement the multi-provider coordinator.
3738
+
3557
3739
  ### `requestBreak(request)`
3558
3740
 
3559
3741
  Requests permission to stop the agent later; it does not itself stop work. The provider continues
@@ -4275,7 +4457,7 @@ an affordance, never the enforcement: the host asks, and the provider does or de
4275
4457
  answers `failed` for a command whose capability it did not publish, and that is the provider
4276
4458
  honouring its own declaration, not the host deciding policy -- a host that refuses a command on
4277
4459
  its own reading of a queue's flag has decided something that was never its to decide. What a
4278
- command means on the platform is the provider's to work out: `end-call` ends the conversation, and
4460
+ command means on the platform is the provider's to work out: `end-call` disconnects the caller channel, and
4279
4461
  which legs on which bridge that touches is a fact about the switch, never a choice the host makes.
4280
4462
  The one thing the host performs physically is the microphone, and that is not a command at all.
4281
4463
 
@@ -4377,12 +4559,13 @@ declared:
4377
4559
 
4378
4560
  **A control on the contact belongs to the handling phases**, `in-progress` and `paused`:
4379
4561
  `hold`, `resume` and `pause`, `end-call`, every `transfer` and `conference` action, and
4380
- every `lead-assist` action. Each acts on the call or the conversation, and only while there is one.
4381
- Before `in-progress` nothing has been placed or opened; in `completing` the handling has ended -- a
4382
- call with nobody on it, a conversation closed -- and a wrap-up that still shows Transfer shows it
4383
- for nothing. The capability stays declared, because it is a property of the task and the task is
4384
- still open; the phase says there is nothing to use it on. Omni shows none of these controls outside
4385
- the two phases, and `validateTaskCommand` refuses each of them there (`command.phase.handling`).
4562
+ every `lead-assist` action. These controls act within this agent's current handling.
4563
+ In `completing`, that handling has ended and its wrap work remains. The caller may still be in
4564
+ an IVR, queue or another agent's handling, and other channels may remain connected. Completion
4565
+ of this handling does not establish that the caller or bridge ended. The capability stays
4566
+ declared while the task remains open; the phase prevents this task from controlling a handling
4567
+ it no longer owns. Omni shows these controls only in the two handling phases, and
4568
+ `validateTaskCommand` refuses them outside those phases (`command.phase.handling`).
4386
4569
  The commands with a phase of their own -- `answer`, `accept` and `decline` in `pending`,
4387
4570
  `call` in `preview`, `connect-back` in `completing`, `complete` in any -- are not among them.
4388
4571
 
@@ -4462,22 +4645,102 @@ They are published as `OMNI_FAILURE_CODES`.
4462
4645
  | `occurredAt` | Valid RFC-3339 timestamp with an explicit timezone, representing provider observation time. |
4463
4646
  | `event` | Typed `ProviderEvent` payload. |
4464
4647
 
4465
- #### Provider instants are read against a provider clock
4648
+ #### Timestamp standard and explicit exceptions
4649
+
4650
+ Every provider event envelope carries `occurredAt` as an ISO timestamp in the contract's RFC-3339
4651
+ form, with `Z` or an explicit numeric timezone offset. Preserve the original instant. Payloads
4652
+ carry their source event/observation instants wherever available and declared by their type;
4653
+ publication and receipt time must not be substituted for a missing source instant. A required
4654
+ instant that the source cannot establish makes that evidence unavailable, not permission to
4655
+ invent a timestamp or silently omit a required field.
4466
4656
 
4467
- Every deadline in this contract is a provider instant that Omni counts down: `allocationExpiresAt`,
4468
- `previewEndsAt`, and the wrap deadline of `task-media-ended` plus `wrapAllowance` where
4469
- one is stated.
4470
- Comparing those against the host clock is wrong by whatever the two machines disagree by, and the
4471
- damaging direction is early — **Accept** withdrawn from an offer still ringing, a wrap timer
4472
- expiring before the agent has finished.
4657
+ Clock estimation is an exception, not the standard interpretation of events. An ISO timestamp
4658
+ identifies an instant; it does not prove that the host and provider clocks are synchronized.
4659
+ Use an explicitly trusted clock in the relevant domain. Do not infer a general clock offset
4660
+ from event arrivals, shift source timestamps, or use an estimate to order events or create history.
4473
4661
 
4474
- `occurredAt` is what fixes it. Omni notes the host time at which each envelope arrives, keeps the
4475
- running offset against the `occurredAt` inside it, and translates provider instants through that
4476
- offset before counting down. What remains is network delay, which biases every deadline later —
4477
- the direction that costs a second rather than an action.
4662
+ The current exceptions and their reasons are:
4478
4663
 
4479
- **Report `seconds`; never expect Omni to derive it** is the same hazard from the provider's side:
4480
- neither party recomputes a duration across a clock it does not own.
4664
+ | Case | Exception and reason |
4665
+ | --- | --- |
4666
+ | Host display of provider deadlines (`allocationExpiresAt`, `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. |
4667
+ | Host-triggered preview deadline | A bounded clock estimate with monotonic aging may schedule the host's Call command because the provider owns the deadline but the host owns the trigger. Do not trigger before the deadline is known to have passed; invalidate on clock discontinuity and reconcile when uncertain. |
4668
+ | 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. |
4669
+ | 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. |
4670
+ | Direct snapshot reads and method requests/results | These are reads/operations, not event envelopes, and their current types have no general event timestamp. A snapshot event still carries `occurredAt`; embedded source instants remain unchanged. Do not treat a method result or read completion time as an occurrence boundary. |
4671
+ | Host-provided provider-clock estimate | Best-effort ISO time may be supplied by the host for optional comparisons; it is not a source observation or accuracy guarantee. The reason is that the host does not own the provider clock. |
4672
+ | Provider receipt timestamps | The provider may use its own receipt instant for final records of receipt/processing because host timestamps are untrusted advisory input. Receipt must not be presented as an earlier action or capture boundary. |
4673
+ | Optional source instants and deadlines | Omit only where the declared type permits absence and the source has no evidence or the policy has no deadline (for example unlimited preview). Do not replace absence with host time. |
4674
+
4675
+ Every additional exception must be listed with its scope and reason before adoption. Estimates
4676
+ must specify their uncertainty and validity; elapsed time should use monotonic aging, and a clock
4677
+ jump invalidates the estimate. The optional check below samples provider time without changing event timestamps. Report
4678
+ source-measured durations where required; timestamp subtraction is not a substitute.
4679
+
4680
+ #### Optional periodic provider time checks and host estimates
4681
+
4682
+ A provider may declare `Manifest.timeCheck: true` and implement `Connection.checkTime(request)`.
4683
+ Omission means unsupported. The host separately opts in with `ProviderTimeCheckPolicy`; these
4684
+ host-local settings are not task policy or a guarantee. All four durations are explicit positive
4685
+ safe integer milliseconds, with `maxRoundTripMs <= timeoutMs <= intervalMs`. No default interval
4686
+ is assumed. Validate settings with `validateProviderTimeCheckPolicy`.
4687
+
4688
+ While the connection is active, the host checks immediately on opt-in and then at `intervalMs`
4689
+ using a monotonic scheduler. Only one check may be outstanding. Missed ticks are skipped, never
4690
+ replayed in a burst. Each request has a fresh ID. The provider samples its own event/deadline
4691
+ clock after receiving the request and before returning `providerTime` as an ISO/RFC3339 instant,
4692
+ with the request ID, authenticated login ID and `clockId`. No cached sample or adapter-local time
4693
+ may masquerade as provider time. A provider unable to sample that domain must not declare support.
4694
+ Clock authority replacement or a discontinuity changes `clockId`.
4695
+
4696
+ Validate requests and responses with `validateProviderTimeCheckRequest` and
4697
+ `validateProviderTimeCheckResult`. The host also checks the outstanding request, provider/connection
4698
+ and login, monotonic elapsed time and cancellation after the await: a late successful response is
4699
+ still rejected. Reject samples at or beyond `timeoutMs`, or above `maxRoundTripMs`. Cancel local
4700
+ tracking and invalidate estimates on disconnect, logout, policy disable, clock change or clock
4701
+ jump; late results cannot restore them. Resume with a fresh check after reconnection. Expire a
4702
+ sample at `maxSampleAgeMs` using monotonic elapsed time. Failure is visible through the host's
4703
+ existing diagnostic path, with the estimate unavailable; the next scheduled check can recover.
4704
+ Checking failure alone does not assert call/recording state or require a healthy connection to close.
4705
+ This read-only check is not a task command. Protocol supplies declarations and validators; the
4706
+ host implements the scheduler and the provider implements the clock read.
4707
+
4708
+ The host may also expose `Host.estimateProviderTime({ providerId, loginId })`. It returns an ISO
4709
+ `at` in that provider's clock domain, the same explicit scope and `clockId`, or `undefined` when
4710
+ unavailable. Optional `uncertaintyMs` is an estimate, not a guaranteed bound. The method is optional
4711
+ at the host level and is deliberately absent from `HostGuarantees`. A provider declaring time
4712
+ checks does not require the host to expose estimates, and an estimate need not originate from
4713
+ this check if the host has another explicitly configured source for that provider clock.
4714
+
4715
+ To estimate the difference, the host brackets the request with local send/receive instants and
4716
+ measures elapsed time monotonically. For provider sample P and local bracketing instants H0/H1,
4717
+ the offset under stable clocks lies between P-H1 and P-H0, before clock error and timestamp
4718
+ precision are considered. A midpoint is only an estimate; one-way delays need not be symmetric.
4719
+ Never treat round-trip uncertainty as proof of the source clock's accuracy. Advance a retained
4720
+ sample with monotonic elapsed time, and return unavailable after its configured validity ends.
4721
+ `validateProviderTimeEstimate` checks shape and scope, not actual accuracy or freshness.
4722
+
4723
+ A provider may declare `Manifest.timestampAuthority: "provider"`: its own timestamps are
4724
+ used for final records, and host-supplied timestamps are advisory. This declaration is independent
4725
+ of time-check support and the optional host estimate; neither changes who owns the final record.
4726
+ Omission makes no promise that host timestamps will be trusted. No host-authoritative default
4727
+ is inferred. Successful `recordStep` responses identify the provider-selected history instant;
4728
+ see **The host records what it performs** for correlation and retained bindings.
4729
+
4730
+ A provider need not trust or adopt any host-supplied timestamp. It may timestamp an incoming
4731
+ message using its own clock at receipt and use that for its own processing/accounting. The reason
4732
+ for this exception is that host clocks and host estimates are not authoritative at the provider.
4733
+ Receipt time must be described as receipt/observation time, not relabeled as the original host
4734
+ action, recorder capture boundary, or proof an operation applied. An existing field with a
4735
+ specific occurrence meaning still requires that evidence; this option does not silently redefine
4736
+ it. Host-only instants remain advisory input; the provider owns its authoritative publication.
4737
+
4738
+ This host estimate is another explicit timestamp exception: it helps optional displays and
4739
+ provider-clock comparisons when the host lacks that clock directly. It is not a source occurrence,
4740
+ may not replace event/history timestamps, and alone cannot authorize deadline actions or prove
4741
+ recording freshness. Uses requiring a trustworthy bound still need independently established
4742
+ clock accuracy/drift assumptions. It neither synchronizes the operating-system clock nor changes
4743
+ ISO timestamps already published by the provider.
4481
4744
 
4482
4745
  #### Nothing is lost until the connection drops
4483
4746
 
@@ -4590,6 +4853,48 @@ from `in-effect` or `starting-after-task` to a grant or a request, or from `gran
4590
4853
  `exerciseAdapter` holds the stream to that from the connect snapshot on;
4591
4854
  `assertBreakFollowsItsRequests` holds any sequence.
4592
4855
 
4856
+ Adapters and hosts can call `validateBreakTransition(before, after)` from the package’s validation entry point
4857
+ with two complete break objects before applying an event. It checks shape and the transition
4858
+ rules above; it does not mutate either state or apply the event. Report a violation visibly,
4859
+ retain the last accepted state without claiming it remains current, and reconcile from the
4860
+ source. Continue to validate the full envelope/login and task/break consistency separately.
4861
+ The conformance `BreakStream` validates its baseline, retains the last accepted state on a
4862
+ rejected delta, and sets `needsRecovery`. Transport loss also requires reseeding; an `active` transport
4863
+ notification alone does not restore a break baseline. Until a fresh validated snapshot is seeded, further
4864
+ break deltas are refused (`stream.breakState.baseline`); the first delta is never an implicit
4865
+ snapshot. `seed(snapshot)` returns violations, which callers must check. This is a conformance
4866
+ tracker, not a complete production reducer: snapshot freshness, login/connection fencing and
4867
+ full task consistency remain separately required.
4868
+
4869
+ **Both sides must enforce the order.** The host validates immediately before dispatch; the
4870
+ provider rechecks current permission and state atomically before acting. The provider validates
4871
+ the resulting break/task state before publication and serializes those publications. The host
4872
+ validates the envelope, transition and full task consistency before replacing local state. A
4873
+ request result does not optimistically advance break state, and a delayed command cannot apply
4874
+ to a later attempt merely because its approval happens to look compatible. Keep at most one
4875
+ unresolved operation per provider/attempt; recovery must resolve uncertainty before another
4876
+ operation is dispatched. The source must fence delayed operations against its own attempt state.
4877
+
4878
+ Normal order is `not-requested` → `awaiting-decision` → `granted` →
4879
+ `starting-after-task` → `in-effect` → `not-requested`. Auto-approval may go directly to
4880
+ `granted`; a commit with no outstanding work may go directly to `in-effect`. Denial/cancel
4881
+ returns a precommit request to `not-requested`; an authorized end/release returns a committed
4882
+ break there. Same-state restatements are allowed. An evidenced imposed break is the explicit
4883
+ exception to requesting/granting, and must carry its imposed actor/state. Neither skipped
4884
+ publication nor a host-local guess creates another exception. A later normal attempt starts
4885
+ from `not-requested`, never by regressing an active break into a request.
4886
+
4887
+ A fresh authoritative snapshot establishes a new baseline; do not run this event-transition
4888
+ check across it. A snapshot may legitimately establish a later request or an already active
4889
+ break after reconnect. Its freshness must be established by the source/adapter's ordered
4890
+ snapshot/publication boundary and fencing of obsolete callbacks and reads. A delayed old
4891
+ snapshot is not detectable from break approval alone. Neither envelope time nor event ID
4892
+ supplies a break revision. Do not suppress all requests after an active break: a later attempt
4893
+ passes through `not-requested`, and then may request again. Do not replay earlier ProviderEvents
4894
+ after recovery. Raw backend break handlers must preserve source order too; this helper does
4895
+ not implement or assume invocation of Protocol's two-phase request/commit coordinator.
4896
+
4897
+
4593
4898
  For a multi-provider break attempt, "every provider" is the set of providers frozen when the
4594
4899
  attempt entered `requesting-break`. Omni commits only after every asked provider reports `granted` —
4595
4900
  that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
@@ -4768,6 +5073,10 @@ same exported checks are used by Omni and adapter tests so their interpretations
4768
5073
  | `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
4769
5074
  | `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
4770
5075
  | `validateHostGuarantees(guarantees)` | What a host promises: only the guarantees this contract names, each declared by presence and never `false`. The harness validates the guarantees of whatever host a test hands the adapter. |
5076
+ | `validateProviderTimeCheckPolicy(policy)` | Explicit optional polling settings, positive safe-integer durations and round-trip/timeout/interval ordering. |
5077
+ | `validateProviderTimeCheckRequest(request)` | Fresh-request shape; the host enforces actual uniqueness and outstanding-request lifetime. |
5078
+ | `validateProviderTimeCheckResult(result, request, loginId)` | ISO timestamp, clock identity, exact request/login correlation; timing and source accuracy remain runtime checks. |
5079
+ | `validateProviderTimeEstimate(estimate, scope)` | Optional host estimate shape and provider/login scope; no accuracy guarantee. |
4771
5080
  | `validateHandlingReport(report, path?, manifest?)` | What the host reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
4772
5081
  | `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
4773
5082
  | `validateHostMute(mute, softphone)` | What the host's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the host holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
@@ -4882,7 +5191,7 @@ as each case was considered, so a test that needs a rule to have run asserts it
4882
5191
  inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
4883
5192
  gap and not a pass. With the audio open on a softphone,
4884
5193
  the drive mutes it for one second and reports the leg through `recordStep`, begun and then ended,
4885
- expecting each report `recorded` (`drive.recordStep.failed`, `.rejected`); then it mutes again and
5194
+ expecting each report `recorded` with the provider-selected history `at` (`drive.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
4886
5195
  ends the call muted, as agents do, so the leg is open when the media ends, the provider closes it
4887
5196
  in the completing publication or the open entry is refused (`task.handlingHistory.muted.open`),
4888
5197
  and the drive's closing report after the media ended is expected `recorded` and to change nothing:
@@ -4892,7 +5201,7 @@ Where the provider restates the task's record afterwards, each leg is in it or t
4892
5201
  does, the drive reloads the host as a reload happens: the first client is unsubscribed,
4893
5202
  disconnected and its session closed (`drive.reload.handover`), and only then is a second adapter
4894
5203
  built and connected for the same login with the same context and the same `store`; its snapshot
4895
- must carry the task with that leg and the host's word, and the run goes on with the second as its
5204
+ must carry the task with that leg at its acknowledged provider timestamp and the reported actor, and the run goes on with the second as its
4896
5205
  connection to the end. A platform that holds the record hands it back, and an adapter that composed
4897
5206
  the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
4898
5207
  `.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
@@ -4915,10 +5224,10 @@ another id (`drive.store.retained`); an adapter whose keys never named the task
4915
5224
  unevaluated, and the result says so rather than passing it: a task's keys go
4916
5225
  with the task, or the next offer of the same id inherits them. Outside the
4917
5226
  handling phases with `hold` still declared -- in
4918
- `confirmed`, where the provider publishes it, and in `completing` once the call has ended -- the
5227
+ `confirmed`, where the provider publishes it, and in `completing` once this agent's handling has ended -- the
4919
5228
  drive sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
4920
- is the second gate on a control on the contact, and one that applies it where there is nothing to
4921
- hold is named (`drive.command.handling`). The drive cannot put a task into a phase the provider
5229
+ is the second gate on a control on the contact, and one that applies it outside this task's
5230
+ current handling is named (`drive.command.handling`). The drive cannot put a task into a phase the provider
4922
5231
  never publishes, so a provider that goes straight from `pending` to `in-progress` is checked in
4923
5232
  `completing` alone. A task the agent completes is
4924
5233
  completed from wherever it stands once the drive has nothing left to do on it -- from `completing`
@@ -4992,7 +5301,7 @@ capabilities and what its work is **called** through `phaseLabels` and `taskType
4992
5301
  any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
4993
5302
  override the agent's theme and font preferences.
4994
5303
 
4995
- ## Independent task recording (protocol 2)
5304
+ ## Independent task recording
4996
5305
 
4997
5306
  Recording is voice-only. A task can arrive already recording, including while pending, and offer
4998
5307
  no recording controls. The provider publishes only its own current state on
@@ -5019,14 +5328,14 @@ This package declares that contract; it supplies no recorder, media mixing, stor
5019
5328
  | pause | active | paused, preserving recording identity and captured audio |
5020
5329
  | resume | paused | active with the same recording identity |
5021
5330
  | stop | active or paused | inactive; finish and retain captured audio |
5022
- | cancel | active or paused | inactive; abandon with the explicitly declared captured-audio disposition |
5023
-
5024
- Cancel must declare retain or discard in task policy and echo that exact effect in the command.
5025
- Retain abandons further capture while preserving captured audio; discard additionally requires
5026
- confirmed disposal of that recording's audio under the recorder's storage contract. Neither
5027
- means cancelling an in-flight start request or deleting arbitrary past recordings. A recorder
5028
- unable to guarantee the offered effect must not advertise it. Stop can be applied only after
5029
- finalization/retention succeeds; cancel-discard only after cessation and disposition succeed.
5331
+ | cancel | active or paused | inactive; abandon and discard captured audio |
5332
+
5333
+ Stop finishes the recording and retains captured audio. Cancel abandons the recording and discards
5334
+ its captured audio; the task policy offers it with `cancel: true`. There is no configurable cancel
5335
+ effect. A recorder that cannot confirm disposal must not offer Cancel; it can offer Stop instead.
5336
+ Cancel never means cancelling an in-flight start request or deleting arbitrary past recordings.
5337
+ Stop can be applied only after finalization/retention succeeds; Cancel only after both cessation
5338
+ and disposal of this recording's audio succeed under the recorder's storage contract.
5030
5339
  Partial success (capture stopped but storage outcome unknown) cannot return failed with a claim
5031
5340
  of no effect. It rejects with unknown outcome, reports the failure visibly and publishes whatever
5032
5341
  current capture state is actually known. Retention is not a promise of sample-perfect audio.
@@ -5052,7 +5361,8 @@ observation instant and an exclusive expiry. These times describe current eviden
5052
5361
  capture boundaries. A trusted observer-domain current time must satisfy observedAt <= now < validUntil.
5053
5362
  Use `effectiveRecordingState` with that explicitly trusted time; an unavailable clock yields unknown.
5054
5363
  Receipt, replay and task publication never extend freshness. Clock discontinuity invalidates evidence;
5055
- consumers must invalidate their clock estimate and use monotonic aging so clock rollback cannot
5364
+ consumers using the explicit recording-expiry clock-estimation exception must invalidate that
5365
+ estimate and use monotonic aging so clock rollback cannot
5056
5366
  revive expired evidence. The executor compares observation identity and recording identity against
5057
5367
  its latest state atomically before I/O. An intervening observation or allocation change refuses the
5058
5368
  stale command; it never acts on a replacement recorder. Providers lacking trustworthy current-state
@@ -5075,8 +5385,9 @@ updates remain full current task/host views, not replayed provider events. These
5075
5385
  create no handling-history entries and infer no actors, durations or historical capture boundaries.
5076
5386
 
5077
5387
  Migration is explicit: replace the old true recording capability with per-path action policies,
5078
- replace untargeted commands with scoped commands, and declare protocol 2 only after implementing
5079
- these semantics. Existing version-1 adapters are refused by negotiation rather than silently mapped.
5388
+ replace untargeted commands with scoped commands, and retain protocol version 1. During pre-release
5389
+ development, hosts and adapters must align their recording contracts; negotiation does not detect
5390
+ differences between package revisions that share that protocol number. No legacy-shape fallback is provided.
5080
5391
  This change does not publish a package or enable recording in any existing host/provider by itself.
5081
5392
 
5082
5393
  `validateRecordingOutcome` checks confirming observations against recording-specific applied/failed
@@ -5088,3 +5399,48 @@ keep that binding through pause/resume/stop/cancel; a later policy cannot redire
5088
5399
  The executor rejects a mismatched destination rather than moving or discarding another binding.
5089
5400
  Action permission does not authorize unattended invocation: host controls require the agent's explicit
5090
5401
  act, and provider authorization remains enforced at its authenticated command boundary.
5402
+
5403
+
5404
+ Recording dispatch must receive the same known task-validation context as the published task.
5405
+ Pass organisation levels and other known restrictions through the optional fourth argument of
5406
+ `validateTaskCommand`, and through `taskContext` in `validateRecordingRequest`. This keeps a valid
5407
+ custom organisation lock from being rejected against the default ladder, and prevents known
5408
+ capability restrictions from disappearing at dispatch. The standalone `validateRecordingCommandState`
5409
+ requires an affirmative permission; false, malformed permission declarations and unknown actions grant nothing.
5410
+
5411
+
5412
+ ### Host recording announcements to the caller
5413
+
5414
+ `HostRecording.announcesToCaller`, when true, is a guarantee that the host delivers audible recording
5415
+ status messages to the remote party over the call's outgoing audio. It belongs inside the optional
5416
+ host recording capability: without that capability there is no such guarantee. Omission makes no
5417
+ promise; false is invalid. It does not belong in the provider manifest, the task's recording policy
5418
+ or the general `HostGuarantees` object. A provider can inspect this host guarantee when deciding
5419
+ whether to permit host recording on a task.
5420
+
5421
+ The guarantee applies only to host-owned recordings. Providers implement announcements for their
5422
+ own recordings at their end; provider state updates must not cause the host to announce those
5423
+ recordings. Both recorders can operate independently on the same call.
5424
+
5425
+ On a confirmed host start, the remote party hears that recording started. On a confirmed stop or
5426
+ cancel, they hear that recording stopped. If pause/resume is supported, say paused/resumed so a
5427
+ pause is not presented as a finished recording. Announcements follow actual capture transitions,
5428
+ not button clicks, accepted requests or task-policy changes. When attaching to an already-active
5429
+ host recording, announce that recording is active, without inventing its original start time.
5430
+ Repeated observations of the same state do not repeat announcements. Unknown or expired evidence
5431
+ must never produce a stopped announcement.
5432
+
5433
+ A local sound, UI indicator, screen-reader message to the agent, or sound leaking through the
5434
+ microphone does not fulfil this guarantee. The host must deliver the message directly in the audio
5435
+ sent to the remote party, even when the agent microphone is muted. It may also notify the agent.
5436
+ Only declare the guarantee when the host can provide that outgoing audio path. An announcement
5437
+ that cannot be delivered, including after the remote party has disconnected, is a visible failure;
5438
+ never claim delivery or replay the notice into a different call.
5439
+
5440
+ For a command under this guarantee, applied requires both the recording action and its announcement
5441
+ to succeed. If capture changes but announcement delivery fails, publish the actual recording state,
5442
+ report the announcement failure visibly and reject with unknown command outcome. Do not claim no
5443
+ effect, automatically repeat the recording action or silently switch to an agent-only notice.
5444
+
5445
+ `validateHostRecording` checks the declaration, including its true-or-absent guarantee. Actual audio
5446
+ delivery remains the host implementation's responsibility and must be exercised in its own tests.