@xema/omni-protocol 0.1.24 → 0.1.26

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
@@ -27,7 +27,7 @@ are used precisely throughout and mean nothing looser here.
27
27
  | **Channel** | The kind of work a provider carries: `voice`, `chat`, or `email`. Fixed per provider by its manifest. |
28
28
  | **Task type** | The provider's own name for a category of work — a queue, a mailbox folder, a chat source. Free-form, and finer-grained than a channel. |
29
29
  | **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*, and nothing about who carries it out — that is fixed per command, see **Where a command executes**. |
30
- | **Login** | One authenticated sign-in to one provider, identified by `sessionId`. A transport reconnect keeps it; signing in again replaces it, and nothing tied to the old `sessionId` survives. |
30
+ | **Login** | One authenticated sign-in to one provider, identified by `loginId`. A transport reconnect keeps it; signing in again replaces it, and nothing tied to the old `loginId` survives. |
31
31
  | **Transport** | The adapter's connection to its platform: a WebSocket or SignalR connection, required to be persistent and ordered. Which one and how it reconnects are the adapter's business; losing it does not end a login. |
32
32
  | **Connection** | The `Connection` object Omni holds for one login: the methods it can call and the events it receives. |
33
33
  | **Concurrent capacity** | How many tasks this provider may have allocated to the agent at once — an absolute ceiling, stated as `AgentCapacity.count` and standing until Omni restates it. The provider counts its own outstanding tasks against it. |
@@ -36,14 +36,15 @@ are used precisely throughout and mean nothing looser here.
36
36
  | **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
37
37
  | **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
38
38
 
39
- Four words describe *what state a thing is in*, and they are not interchangeable. `status` is the
40
- one used twice, for two unrelated things — which is why a bare "status" in conversation is always
41
- worth pinning down:
39
+ Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
40
+ one thing, so a bare "status" in conversation is always the authentication session's, and a
41
+ transport, a task, a break and a call each have a word of their own:
42
42
 
43
43
  | Word | Belongs to | Values |
44
44
  | --- | --- | --- |
45
45
  | `phase` | A task | `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, `completing` |
46
- | `status` | A connection | `connecting`, `active`, `error` |
46
+ | `media` | A task's audio | `started`, `ended` |
47
+ | `transport` | A connection | `connecting`, `active`, `error` |
47
48
  | `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
48
49
  | `approval` | A break request | `not-requested`, `awaiting-decision`, `granted`, `starting-after-task`, `in-effect` |
49
50
  | `availability` | A roster member | `ready`, `on-task`, `on-break`, `signed-out` |
@@ -146,20 +147,20 @@ type Attribute = { key: string; value: string };
146
147
  ```ts
147
148
  type AuthenticationMethod = "browser-sso" | "credentials";
148
149
 
149
- type BrowserAccessPolicy = {
150
+ type BrowserAccess = {
150
151
  mode: "allow-all" | "block-all";
151
152
  allowList?: string[];
152
153
  blockList?: string[];
153
154
  };
154
155
 
155
156
  type PersonalBrowserCapability = {
156
- access: BrowserAccessPolicy;
157
+ access: BrowserAccess;
157
158
  accessPolicyScope?: "initial-url" | "all-navigation";
158
159
  };
159
160
 
160
- type DialDestinationPolicy = "contacts-only" | "any-number";
161
+ type DialDestinations = "contacts-only" | "any-number";
161
162
 
162
- type DialCapability = { destinationPolicy: DialDestinationPolicy };
163
+ type DialCapability = { destinations: DialDestinations };
163
164
 
164
165
  type IdleCapabilities<C extends Channel = Channel> = {
165
166
  personalBrowser?: PersonalBrowserCapability;
@@ -176,7 +177,7 @@ type Manifest<C extends Channel = Channel> = {
176
177
  idleCapabilities?: IdleCapabilities<C>;
177
178
  phaseLabels?: TaskPhaseLabels;
178
179
  taskTypePresentation?: Record<string, TaskTypePresentation>;
179
- orgTiers?: TierDeclaration[];
180
+ orgLevels?: LevelDeclaration[];
180
181
  };
181
182
  ```
182
183
 
@@ -215,7 +216,7 @@ type SecretStore = {
215
216
 
216
217
  type AuthenticationContext = {
217
218
  protocolVersion: number;
218
- sessionId: string;
219
+ loginId: string;
219
220
  secrets: SecretStore;
220
221
  signal?: AbortSignal;
221
222
  log?: (entry: unknown) => void;
@@ -227,7 +228,7 @@ type TeamCapabilities = {
227
228
  policyControl?: true;
228
229
  };
229
230
 
230
- type SessionCapabilities = {
231
+ type UserCapabilities = {
231
232
  breaks?: true;
232
233
  preferences?: AgentPreference[];
233
234
  team?: TeamCapabilities;
@@ -236,8 +237,8 @@ type SessionCapabilities = {
236
237
  type AuthenticationState =
237
238
  | { status: "signed-out" }
238
239
  | { status: "authenticating" }
239
- | { status: "authenticated"; identity: User; capabilities: SessionCapabilities; expiresAt?: IsoTimestamp }
240
- | { status: "refreshing"; identity: User; capabilities: SessionCapabilities }
240
+ | { status: "authenticated"; identity: User; capabilities: UserCapabilities; expiresAt?: IsoTimestamp }
241
+ | { status: "refreshing"; identity: User; capabilities: UserCapabilities }
241
242
  | { status: "expired"; identity?: User; failure?: AuthenticationFailure };
242
243
 
243
244
  type AuthenticationFailure = {
@@ -262,7 +263,6 @@ type HostAudioOutput =
262
263
  type UrlVisibility = "full" | "domain" | "hidden";
263
264
 
264
265
  type HostReport = {
265
- browsers: { urlVisibility: UrlVisibility };
266
266
  online: boolean;
267
267
  audio?: {
268
268
  input: HostAudioInput;
@@ -277,14 +277,14 @@ type Host = {
277
277
 
278
278
  type ConnectContext = {
279
279
  protocolVersion: number;
280
- sessionId: string;
280
+ loginId: string;
281
281
  autoAcceptTasks?: boolean;
282
282
  host: Host;
283
283
  signal?: AbortSignal;
284
284
  log?: (entry: unknown) => void;
285
285
  };
286
286
 
287
- type ConnectionStatus = "connecting" | "active" | "error";
287
+ type TransportStatus = "connecting" | "active" | "error";
288
288
 
289
289
  type CredentialField = {
290
290
  name: string;
@@ -311,7 +311,7 @@ type CompleteAuthenticationRequest =
311
311
  | { flowId: string; method: "credentials"; values: Readonly<Record<string, string>> };
312
312
 
313
313
  type CompleteAuthenticationResult =
314
- | { status: "authenticated"; identity: User; capabilities: SessionCapabilities; expiresAt?: IsoTimestamp }
314
+ | { status: "authenticated"; identity: User; capabilities: UserCapabilities; expiresAt?: IsoTimestamp }
315
315
  | { status: "rejected"; failure: AuthenticationFailure };
316
316
 
317
317
  type AuthenticationActionResult =
@@ -336,11 +336,11 @@ type AuthenticationSession = {
336
336
  ```ts
337
337
  type PreferenceId = "hold" | "mute" | `skill:${string}`;
338
338
 
339
- type SetBy = Tier | "provisioning";
339
+ type SetBy = Level | "provider";
340
340
 
341
341
  type Resolved = {
342
342
  setBy: SetBy;
343
- lockedBy?: Tier;
343
+ lockedBy?: Level;
344
344
  reason?: string;
345
345
  };
346
346
 
@@ -359,10 +359,11 @@ type PreferenceResult =
359
359
  | { status: "failed"; failure: ProtocolFailure };
360
360
 
361
361
  type Snapshot = {
362
- status: ConnectionStatus;
363
- sessionId: string;
362
+ transport: TransportStatus;
363
+ loginId: string;
364
364
  break: BreakState;
365
365
  tasks: Task[];
366
+ taskCount: number;
366
367
  contacts?: Contact[];
367
368
  scheduledActivities?: ScheduledActivity[];
368
369
  team?: TeamRoster;
@@ -410,7 +411,7 @@ type DialResult =
410
411
  ```ts
411
412
  type DispositionCode = { id: string; label: string; group?: string };
412
413
 
413
- type DispositionPolicy = {
414
+ type DispositionRules = {
414
415
  required?: boolean;
415
416
  notes?: "required" | "optional" | "hidden";
416
417
  codes?: DispositionCode[];
@@ -431,7 +432,7 @@ type DestinationDirectory = {
431
432
  type CustomCapability = {
432
433
  id: string;
433
434
  ui: {
434
- kind: "button" | "toggle" | "menu-item";
435
+ control: "button" | "toggle" | "menu-item";
435
436
  label: string;
436
437
  placement: "primary" | "secondary" | "overflow";
437
438
  render?: "inline" | "page";
@@ -441,7 +442,7 @@ type CustomCapability = {
441
442
 
442
443
  type SharedTaskCapabilities = {
443
444
  browsers?: true;
444
- dispositions?: true | DispositionPolicy;
445
+ dispositions?: true | DispositionRules;
445
446
  custom?: CustomCapability[];
446
447
  };
447
448
 
@@ -481,18 +482,22 @@ const BROWSER_ISOLATION_SCHEMES = {
481
482
  type BrowserIsolationScheme =
482
483
  (typeof BROWSER_ISOLATION_SCHEMES)[keyof typeof BROWSER_ISOLATION_SCHEMES];
483
484
 
484
- type TaskBrowserBase = {
485
+ type Browser = {
485
486
  id: string;
486
487
  name: string;
487
- purpose: string;
488
488
  url: string;
489
489
  };
490
490
 
491
- type TaskBrowser = TaskBrowserBase & (
491
+ type TaskBrowser = Browser & {
492
+ purpose: string;
493
+ urlVisibility?: UrlVisibility;
494
+ } & (
492
495
  | { reuse: false; isolationScheme?: never }
493
496
  | { reuse: true; isolationScheme: BrowserIsolationScheme }
494
497
  );
495
498
 
499
+ type PersonalBrowser = Browser;
500
+
496
501
  type BrowserSessionKeyInput = {
497
502
  providerId: string;
498
503
  taskId: TaskId;
@@ -546,8 +551,8 @@ type TaskHandlingStep = {
546
551
  };
547
552
 
548
553
  type TaskCompletion =
549
- | { completionMode: "agent-command"; completionAllowance?: DurationSeconds }
550
- | { completionMode: "provider-automatic"; completionAllowance: DurationSeconds };
554
+ | { completionMode: "agent-command"; wrapAllowance?: DurationSeconds }
555
+ | { completionMode: "provider-automatic"; wrapAllowance: DurationSeconds };
551
556
 
552
557
  type TaskConsultation = {
553
558
  destination: string;
@@ -556,7 +561,7 @@ type TaskConsultation = {
556
561
  };
557
562
 
558
563
  type TaskLead = {
559
- status: "requested" | "joined";
564
+ stage: "requested" | "joined";
560
565
  leadId?: UserId;
561
566
  note?: string;
562
567
  since: IsoTimestamp;
@@ -568,21 +573,21 @@ type TaskAssisting = {
568
573
  since: IsoTimestamp;
569
574
  };
570
575
 
571
- type Tier = string;
576
+ type Level = string;
572
577
 
573
- type TierDeclaration = {
574
- id: Tier;
578
+ type LevelDeclaration = {
579
+ id: Level;
575
580
  label: string;
576
581
  };
577
582
 
578
583
  type Locked = {
579
- lockedBy: Tier;
584
+ lockedBy: Level;
580
585
  reason?: string;
581
586
  };
582
587
 
583
588
  type Lockable<T> = T | Locked;
584
589
 
585
- type TaskMediaState = "ready" | "ended";
590
+ type TaskMediaState = "started" | "ended";
586
591
 
587
592
  type Task<C extends Channel = Channel> = {
588
593
  id: TaskId;
@@ -704,33 +709,32 @@ what the queue allows: on for everyone, off for everyone, or left to the person.
704
709
  your team is a **policy**; what you do to yourself is a **preference**. A person belongs to one
705
710
  team and many queues, so a policy applies across every queue the person works.
706
711
 
707
- **The provider resolves; the protocol carries the result and who decided.** The structure's tiers
708
- are the provider's ladder: each tier states only what it sets, an enforcing policy at a tier above
712
+ **The provider resolves; the protocol carries the result and who decided.** The structure's levels
713
+ are the provider's ladder: each level states only what it sets, an enforcing policy at a level above
709
714
  the person settles the value for everyone below it, and where nothing enforces the most specific
710
- tier that says anything wins. The protocol names a tier by the id the manifest declares for it and
711
- never describes the chain between them: which tiers a person passes through is the structure's to
712
- know. A typical organisation has four, and they are the defaults — `DEFAULT_TIERS`: `org`, `site`,
715
+ level that says anything wins. The protocol names a level by the id the manifest declares for it and
716
+ never describes the chain between them: which levels a person passes through is the structure's to
717
+ know. A typical organisation has four, and they are the defaults — `DEFAULT_LEVELS`: `org`, `site`,
713
718
  `team`, `person`, each with the label a desk shows. A structure that differs states its whole
714
- ladder in `Manifest.orgTiers`, `person` included: what the list carries is in force, and what it
715
- leaves out does not exist — a structure with no site tier declares `org`, `team`, `person`, and
716
- `site` is refused on its wire. A declared tier is one the provider's own store actually resolves
719
+ ladder in `Manifest.orgLevels`, `person` included: what the list carries is in force, and what it
720
+ leaves out does not exist — a structure with no site level declares `org`, `team`, `person`, and
721
+ `site` is refused on its wire. A declared level is one the provider's own store actually resolves
717
722
  at: a label with no policy behind it decides nothing. A manifest that declares none has exactly
718
- the four. `lockedBy` is any tier in force except `person`, who never locks their own value;
719
- `setBy` is any tier in force, or `provisioning`, the protocol's word for "no tier has said
720
- anything and the provider's own configuration supplied the value" the provider speaking, never
721
- Omni's provisioning file, which does not reach the wire. A host renders "who decided" from the
723
+ the four. `lockedBy` is any level in force except `person`, who never locks their own value;
724
+ `setBy` is any level in force, or `provider`, the protocol's word for "no level has said
725
+ anything and the provider's own configuration supplied the value". A host renders "who decided" from the
722
726
  declared labels and needs no others, and validates every republished `authenticated` state
723
727
  against them, not only the sign-in. What the wire carries is the resolution:
724
728
 
725
- - **`lockedBy`** — a tier above the person made this value theirs to keep. A person never locks
726
- their own value, and the queue is not a tier: what the queue does not allow at all is absent.
727
- - **`setBy`** — who stated the value as it stands: a tier, the `person` themself, or
728
- `provisioning` where no tier has said anything. Provenance, not a lock: a value that came from a
729
- broad tier as a default is still the person's to change.
729
+ - **`lockedBy`** — a level above the person made this value theirs to keep. A person never locks
730
+ their own value, and the queue is not a level: what the queue does not allow at all is absent.
731
+ - **`setBy`** — who stated the value as it stands: a level, the `person` themself, or
732
+ `provider` where no level has said anything. Provenance, not a lock: a value that came from a
733
+ broad level as a default is still the person's to change.
730
734
 
731
735
  **On a task, a control the queue could allow may stand locked in its place.** `Task.capabilities`
732
736
  is the effective set. What the queue does not allow is absent and nothing is shown. What the queue
733
- allows and a tier above the person locked is present as `{ lockedBy, reason? }` where the control's
737
+ allows and a level above the person locked is present as `{ lockedBy, reason? }` where the control's
734
738
  value would be — `mute: { lockedBy: "team", reason: "Nobody on this team mutes" }` — and Omni
735
739
  renders that control disabled, saying who decided, so an agent who cannot press Mute knows whether
736
740
  to ask their lead or their site. `lockedBy` is the discriminant: a value that carries it is the
@@ -746,20 +750,20 @@ controls — is content, and is never locked.
746
750
  with `on`, `off`, or `agent`, for any task control, `dial`, or a skill — and only `hold`, `mute`
747
751
  and skills may be `agent`; callback and new call are the team's, on or off, within what the queue
748
752
  allows. The roster carries `policies` for such a login: every policy as it stands, who set it, and
749
- `lockedBy` where a tier above the team made it theirs to keep, which the lead sees and cannot
753
+ `lockedBy` where a level above the team made it theirs to keep, which the lead sees and cannot
750
754
  change — `executeTeamPolicy` on it answers `failed` with `omni.capability-not-enabled`.
751
755
 
752
756
  **What the team left to the person is the person's, and the provider keeps it.** The login's
753
757
  `capabilities.preferences` lists every preference the person may hold — `hold`, `mute`, a skill —
754
758
  with where it stands and who set it: `setBy: "team"` while they inherit the team's default,
755
- `"person"` once they have set their own, `"provisioning"` where no tier has said anything. Nothing
756
- is hidden for want of a row, and a preference a tier above has since locked is listed with
759
+ `"person"` once they have set their own, `"provider"` where no level has said anything. Nothing
760
+ is hidden for want of a row, and a preference a level above has since locked is listed with
757
761
  `lockedBy`. `setPreference` is the person's act — `{ id, enabled }` to set their own, `{ id,
758
762
  inherit: true }` to give it up and inherit again — answered `applied` and republished as a new
759
763
  `authenticated` state when something changed, a state and not a flicker, as every republish of
760
764
  `authenticated` is; and it is durable: the person's across sessions. A lead may also set a
761
765
  person's preference from their own screen, which arrives the same way. A preference is keyed by
762
- the capability's own name because it is the same capability at another tier: effective in
766
+ the capability's own name because it is the same capability at another level: effective in
763
767
  `Task.capabilities`, set for the team in `policies`, left to the person in `preferences`. The
764
768
  command `mute` acts on one call; the preference `mute` says whether the person wants the control
765
769
  at all, and a host renders it in its settings, never as the button on a call.
@@ -793,7 +797,7 @@ type ImposedBreak =
793
797
 
794
798
  type BreakState = {
795
799
  approval: BreakApproval;
796
- accepting: boolean;
800
+ mayAsk: boolean;
797
801
  refusedReason?: string;
798
802
  decisionReason?: string;
799
803
  retryAfterMs?: number;
@@ -921,12 +925,12 @@ type ProviderSummary = {
921
925
  metrics?: SummaryMetric[];
922
926
  };
923
927
 
924
- type ConnectionRecovery = "reconnect" | "reauthenticate";
928
+ type TransportRecovery = "reconnect" | "reauthenticate";
925
929
 
926
930
  type ProviderEvent =
927
931
  | { type: "snapshot"; reason: "reconnected" | "provider-requested"; snapshot: Snapshot }
928
- | { type: "provider-status"; status: "connecting" | "active"; message?: string }
929
- | { type: "provider-status"; status: "error"; recovery: ConnectionRecovery; message?: string }
932
+ | { type: "transport-status"; status: "connecting" | "active"; message?: string }
933
+ | { type: "transport-status"; status: "error"; recovery: TransportRecovery; message?: string }
930
934
  | { type: "break-state"; break: BreakState }
931
935
  | {
932
936
  type: "task-offered";
@@ -936,7 +940,7 @@ type ProviderEvent =
936
940
  preparationEndsAt?: IsoTimestamp;
937
941
  }
938
942
  | { type: "task-updated"; task: Task }
939
- | { type: "task-media-ready"; taskId: TaskId }
943
+ | { type: "task-media-started"; taskId: TaskId }
940
944
  | { type: "task-media-ended"; taskId: TaskId }
941
945
  | { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
942
946
  | { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
@@ -947,7 +951,7 @@ type ProviderEvent =
947
951
 
948
952
  type ProviderEventEnvelope = {
949
953
  id: string;
950
- sessionId: string;
954
+ loginId: string;
951
955
  occurredAt: IsoTimestamp;
952
956
  event: ProviderEvent;
953
957
  };
@@ -997,12 +1001,12 @@ const ALLOWED_BROWSER_URL_SCHEMES = ["http:", "https:"] as const;
997
1001
  const IDLE_CAPABILITIES = ["dial", "personalBrowser", "calendar", "contacts"] as const;
998
1002
  type IdleCapability = (typeof IDLE_CAPABILITIES)[number];
999
1003
 
1000
- const DEFAULT_TIERS = [
1004
+ const DEFAULT_LEVELS = [
1001
1005
  { id: "org", label: "Your organisation" },
1002
1006
  { id: "site", label: "Your site" },
1003
1007
  { id: "team", label: "Your team" },
1004
1008
  { id: "person", label: "You" },
1005
- ] as const satisfies readonly TierDeclaration[];
1009
+ ] as const satisfies readonly LevelDeclaration[];
1006
1010
 
1007
1011
  const IDLE_CAPABILITY_UI = {
1008
1012
  dial: "Dialpad",
@@ -1223,6 +1227,17 @@ which field says so varies. See **Which commands need a capability**.
1223
1227
 
1224
1228
  Snapshots establish and replace provider state when an agent signs in, reconnects, or resynchronises.
1225
1229
 
1230
+ **A snapshot is the provider's answer, and an adapter that got no answer publishes nothing.** A
1231
+ state read that answers unknown — a session not yet associated, a backend mid-failover — is not an
1232
+ empty state: the adapter keeps what it holds, stays `connecting`, and publishes a snapshot only
1233
+ once the provider has actually answered for this login, exactly as `connect()` may not resolve
1234
+ before it can provide a meaningful one. And emptiness is stated, never inferred: every snapshot
1235
+ carries `taskCount`, the provider's own count reconciled against `tasks.length`, so a snapshot
1236
+ with no work says `taskCount: 0` in so many words and a blank or half-built state — which lacks
1237
+ the count — can never pass as a confirmed empty. Absence of knowledge is never evidence of
1238
+ absence, and every place "empty" is allowed to carry both meanings will eventually clear
1239
+ somebody's live call.
1240
+
1226
1241
  Events report completed transactions after that baseline. Nothing is missed while the connection
1227
1242
  holds; when it drops, the reconnect snapshot re-establishes the baseline before any further event
1228
1243
  is applied.
@@ -1235,7 +1250,7 @@ make a repeat safe. Omni does not retry; if the agent acts again it is a new com
1235
1250
 
1236
1251
  On a persistent ordered transport there is only one way a command goes unsettled: the connection
1237
1252
  went away underneath it. **An adapter that cannot settle a command has lost its transport, and
1238
- says so** — `provider-status` `connecting`, reconnect, snapshot — whichever channel the command
1253
+ says so** — `transport-status` `connecting`, reconnect, snapshot — whichever channel the command
1239
1254
  actually travelled on. An unsettled promise is therefore always followed by a snapshot, and that
1240
1255
  snapshot is the answer; Omni waits for it rather than calling `snapshot()` itself. While the
1241
1256
  transport is up, a result says the provider accepted the command, and the event that follows —
@@ -1244,7 +1259,7 @@ transport is up, a result says the provider accepted the command, and the event
1244
1259
  **Classify on the rejection, never on a connection status published separately from it.** The
1245
1260
  status is a report about the wire and races the rejection; the rejection is the event. A rejection
1246
1261
  is the provider's answer only when it carries the provider's answer — a failure the provider
1247
- named. Every other rejection is transport loss, whatever the last `provider-status` said.
1262
+ named. Every other rejection is transport loss, whatever the last `transport-status` said.
1248
1263
 
1249
1264
  A command therefore carries no key. The provider names its own records — a task, a lead request, a
1250
1265
  member — and Omni refers to them by those names; **Omni never asks a provider to remember a name
@@ -1334,7 +1349,7 @@ export default defineAdapter({
1334
1349
  supportedProtocolVersions: [OMNI_PROTOCOL_VERSION],
1335
1350
  authenticationMethods: ["browser-sso"],
1336
1351
  idleCapabilities: {
1337
- dial: { destinationPolicy: "any-number" },
1352
+ dial: { destinations: "any-number" },
1338
1353
  },
1339
1354
  },
1340
1355
  createAuthenticationSession: context => createAcmeAuthentication(context),
@@ -1359,7 +1374,7 @@ compile time.
1359
1374
  | `idleCapabilities` | Declares actions Omni may offer while the agent has no active task, such as voice dialing. Task controls do not belong here. |
1360
1375
  | `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
1361
1376
  | `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
1362
- | `orgTiers` | The organisation's whole ladder as the provider calls it, each tier 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_TIERS`. See **Who decides what an agent may do**. |
1377
+ | `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**. |
1363
1378
 
1364
1379
  ### Authentication methods
1365
1380
 
@@ -1413,11 +1428,11 @@ a destination policy:
1413
1428
 
1414
1429
  ```ts
1415
1430
  idleCapabilities: {
1416
- dial: { destinationPolicy: "any-number" }
1431
+ dial: { destinations: "any-number" }
1417
1432
  }
1418
1433
  ```
1419
1434
 
1420
- `destinationPolicy` is required and accepts one `DialDestinationPolicy`:
1435
+ `destinations` is required and accepts one `DialDestinations`:
1421
1436
 
1422
1437
  | Value | Contract |
1423
1438
  | --- | --- |
@@ -1612,7 +1627,7 @@ Creates the provider-scoped authentication session.
1612
1627
  | `AuthenticationContext` field | Contract |
1613
1628
  | --- | --- |
1614
1629
  | `protocolVersion` | The negotiated version, fixed for this login. |
1615
- | `sessionId` | Omni-generated identity for this login. The same value Omni later passes as `ConnectContext.sessionId`, and how an adapter ties a connection back to the session that authenticated it. |
1630
+ | `loginId` | Omni-generated identity for this login. The same value Omni later passes as `ConnectContext.loginId`, and how an adapter ties a connection back to the session that authenticated it. |
1616
1631
  | `secrets` | Omni-provided `SecretStore`, scoped to this provider's manifest id. |
1617
1632
  | `signal` | Optional cancellation signal. |
1618
1633
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
@@ -1646,11 +1661,11 @@ login fix, rendered apart from a provider Omni cannot reach. Commands meanwhile
1646
1661
  `omni.not-authenticated`.
1647
1662
 
1648
1663
  **Re-authentication restores the login; it does not replace it.** It runs on the session Omni
1649
- kept, under the same `sessionId` — the old `flowId` died with the expiry, so the adapter issues a
1664
+ kept, under the same `loginId` — the old `flowId` died with the expiry, so the adapter issues a
1650
1665
  new challenge — and when the state returns to `authenticated` the connection and everything on it
1651
1666
  carry on: Omni does not call `connect()` again, since a second connection would be a second
1652
1667
  session for one agent. What "signing in again replaces the login" describes is a new
1653
- `AuthenticationSession` under a new `sessionId`, after `signed-out`.
1668
+ `AuthenticationSession` under a new `loginId`, after `signed-out`.
1654
1669
 
1655
1670
  ### What the login may do
1656
1671
 
@@ -1801,14 +1816,14 @@ Creates one live provider connection for the signed-in agent.
1801
1816
  - The returned connection owns reconnect until Omni calls `disconnect()` or aborts `context.signal`.
1802
1817
  - May be called again on the same login after that: once per `Connection`, not once per login.
1803
1818
  Omni disposes a connection whose `error` named `recovery: "reconnect"` with `disconnect()` and
1804
- calls `connect()` afresh — see **`provider-status`**.
1819
+ calls `connect()` afresh — see **`transport-status`**.
1805
1820
 
1806
1821
  ### `ConnectContext`
1807
1822
 
1808
1823
  | Field | Contract |
1809
1824
  | --- | --- |
1810
1825
  | `protocolVersion` | Version negotiated before authentication. Fixed for this login. |
1811
- | `sessionId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.sessionId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
1826
+ | `loginId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.loginId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
1812
1827
  | `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login. Treated as `true` when omitted. When `true`, `task-offered` carries an `acceptanceMode`; when `false`, every task requires agent acceptance. |
1813
1828
  | `host` | The host's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The host reports, the adapter decides**. |
1814
1829
  | `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
@@ -1833,10 +1848,11 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
1833
1848
 
1834
1849
  | Field | Contract |
1835
1850
  | --- | --- |
1836
- | `status` | Current `ConnectionStatus` — whether this provider's transport can serve the session. Defined under **`provider-status`**. |
1837
- | `sessionId` | Identity of this login session. It must match the connection context. |
1838
- | `break` | Complete break state, including approval, accepting state, reasons, retry details, and any imposed break. |
1851
+ | `transport` | Current `TransportStatus` — whether this provider's transport can serve the login. Defined under **`transport-status`**. |
1852
+ | `loginId` | Identity of this login. It must match the connection context. |
1853
+ | `break` | Complete break state, including approval, whether the agent may ask, reasons, retry details, and any imposed break. |
1839
1854
  | `tasks` | Complete set of tasks currently offered to or owned by this agent. |
1855
+ | `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. |
1840
1856
  | `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
1841
1857
  | `scheduledActivities` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
1842
1858
  | `team` | Required `TeamRoster` when the login declares `capabilities.team`, `[]` when nobody is in it. Forbidden otherwise — the login is the permission. |
@@ -2057,10 +2073,10 @@ time. Runtime conformance checks also require the task channel to match its prov
2057
2073
  | `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
2058
2074
  | `contact` | Optional `Contact` for the person or entity on this task. Often a name and one address; a withheld caller ID may leave nothing to send at all. |
2059
2075
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, or `completing`. |
2060
- | `media` | Voice only. The task's real-time audio as the provider holds it: `ready` while audio should be attached, `ended` once it ended, omitted while none should be. The provider's word — see **`task-media-ready`**. |
2076
+ | `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`**. |
2061
2077
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
2062
2078
  | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
2063
- | `completionAllowance` | 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. |
2079
+ | `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. |
2064
2080
  | `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
2065
2081
  | `handlingHistory` | Optional ordered handling history for this currently open task. It is live task data, not a permanent archive. |
2066
2082
  | `consultation` | Voice only. Present while the agent is consulting a transfer destination: who is being consulted, and since when where the provider records it. Its presence is what makes `transfer` `complete` and `cancel` issuable. `label` is a name for the destination -- a person, a queue -- not a phrase; the host supplies the verb. See **Consult transfer**. |
@@ -2117,7 +2133,7 @@ command.
2117
2133
  routed to the agent and accepted as `acceptanceMode` dictates, and its presence and phase follow
2118
2134
  the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
2119
2135
  consult, a conference leg joining or leaving, a transfer, a callback — the media follows
2120
- separately, arriving on `task-media-ready`, attaching through `openMedia` and ending with
2136
+ separately, arriving on `task-media-started`, attaching through `openMedia` and ending with
2121
2137
  `task-media-ended`. Omni does not ring,
2122
2138
  bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
2123
2139
  are the adapter's and the platform's, transient, and decide neither when a task exists nor what
@@ -2129,7 +2145,7 @@ allowance starts on it and the callback control appears on it — and Omni follo
2129
2145
  follows any other. What Omni never does is derive a task's state from its own media session: a
2130
2146
  stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
2131
2147
  endpoint re-registering change nothing about the task until the provider says so. Structurally:
2132
- `task-media-ready` and `task-media-ended` alternate on a task whose work has begun, media ends
2148
+ `task-media-started` and `task-media-ended` alternate on a task whose work has begun, media ends
2133
2149
  only where it arrived, what follows the media ending is `completing` or `task-ended`, and every
2134
2150
  task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
2135
2151
  and `assertMediaFollowsTheTask` holds any sequence.
@@ -2140,10 +2156,10 @@ and `assertMediaFollowsTheTask` holds any sequence.
2140
2156
  the task open until Omni sends the channel's `complete` command. With `provider-automatic`, the
2141
2157
  provider may complete the task without receiving that command.
2142
2158
 
2143
- `completionAllowance` is independent of that decision. It is fixed, and when it starts depends on
2159
+ `wrapAllowance` is independent of that decision. It is fixed, and when it starts depends on
2144
2160
  whether the channel carries real-time media:
2145
2161
 
2146
- | Channel | Completion allowance starts at |
2162
+ | Channel | Wrap allowance starts at |
2147
2163
  | --- | --- |
2148
2164
  | Voice and any channel with real-time media | The `task-media-ended` event |
2149
2165
  | Chat | When the conversation ends and the task enters `completing` |
@@ -2153,8 +2169,8 @@ whether the channel carries real-time media:
2153
2169
  ```ts
2154
2170
  const emailCompletion = {
2155
2171
  completionMode: "agent-command",
2156
- completionAllowance: 120,
2157
- } satisfies Pick<Task<"email">, "completionMode" | "completionAllowance">;
2172
+ wrapAllowance: 120,
2173
+ } satisfies Pick<Task<"email">, "completionMode" | "wrapAllowance">;
2158
2174
  ```
2159
2175
 
2160
2176
  In this example, the agent has two minutes after sending the email to add notes, select a
@@ -2164,7 +2180,7 @@ disposition, and complete the task.
2164
2180
  without waiting for a command; with `agent-command`, it still waits for `complete`.
2165
2181
 
2166
2182
  There is no value meaning "unlimited", because a number that is not a duration would be read as
2167
- one. A provider that imposes no deadline says so by **omitting** `completionAllowance`, which
2183
+ one. A provider that imposes no deadline says so by **omitting** `wrapAllowance`, which
2168
2184
  only `agent-command` permits: the provider will not complete the task itself, so there is nothing
2169
2185
  for a deadline to trigger, and Omni counts nothing down. **Omitted and empty are different
2170
2186
  claims** applies -- omitted says there is no deadline to see, where `0` says the deadline is now.
@@ -2173,7 +2189,7 @@ Under `provider-automatic` the field is required, because the provider is going
2173
2189
  ```ts
2174
2190
  const untimedWrap = {
2175
2191
  completionMode: "agent-command",
2176
- } satisfies Pick<Task<"voice">, "completionMode" | "completionAllowance">;
2192
+ } satisfies Pick<Task<"voice">, "completionMode" | "wrapAllowance">;
2177
2193
  ```
2178
2194
 
2179
2195
  Here the customer has hung up, `task-media-ended` has been sent on time, the task is `completing`,
@@ -2189,7 +2205,7 @@ Omni issues `{ type: "callback" }`; it is issuable only in `completing`, and onl
2189
2205
  capability is declared. The provider knows who the party is; the command carries no destination.
2190
2206
 
2191
2207
  On `applied` the provider is placing the call and the task returns to `in-progress`: the agent is
2192
- working again, and the completion allowance is **discarded, not paused**. From there the call is
2208
+ working again, and the wrap allowance is **discarded, not paused**. From there the call is
2193
2209
  reported as any call is -- `paused`, `in-progress`, and when its media ends, `task-media-ended`
2194
2210
  again, which starts a fresh allowance from that instant. A party who does not answer is a call
2195
2211
  whose media ended: the task returns to `completing` through the same event and the clock starts
@@ -2197,7 +2213,7 @@ again from there. At no point is an agent dialling against a deadline.
2197
2213
 
2198
2214
  **The control exists only while there is a window to use it in.** Under `agent-command` the task
2199
2215
  stays `completing` until the agent completes it, so the window is open for as long as they need.
2200
- Under `provider-automatic` the window is the allowance -- and with `completionAllowance: 0` there
2216
+ Under `provider-automatic` the window is the allowance -- and with `wrapAllowance: 0` there
2201
2217
  is none: the provider disposes the task at provider end, and Omni does not offer Call back, whatever
2202
2218
  the task declares. A capability names a control that can be used; on a task with no `completing`
2203
2219
  window it cannot, and declaring it there changes nothing.
@@ -2208,8 +2224,8 @@ const callbackCapable = {
2208
2224
  capabilities: { hold: true, callback: true, dispositions: true },
2209
2225
  phase: "completing",
2210
2226
  completionMode: "provider-automatic",
2211
- completionAllowance: 30,
2212
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "completionAllowance">;
2227
+ wrapAllowance: 30,
2228
+ } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "wrapAllowance">;
2213
2229
  ```
2214
2230
 
2215
2231
  With ten seconds of the thirty left, the agent presses Call back: `execute({ command: { type:
@@ -2220,8 +2236,8 @@ from that instant.
2220
2236
  ```ts
2221
2237
  const immediateProviderCompletion = {
2222
2238
  completionMode: "provider-automatic",
2223
- completionAllowance: 0,
2224
- } satisfies Pick<Task, "completionMode" | "completionAllowance">;
2239
+ wrapAllowance: 0,
2240
+ } satisfies Pick<Task, "completionMode" | "wrapAllowance">;
2225
2241
  ```
2226
2242
 
2227
2243
  ### How a task has been handled
@@ -2296,6 +2312,13 @@ capabilities: { browsers: true }
2296
2312
 
2297
2313
  Tasks without browser definitions omit the capability and provide an empty `browsers` array.
2298
2314
 
2315
+ **A browser is one tab, and each workspace keeps its own.** `Browser` is what a tab is — an id, a
2316
+ name, a URL. The task workspace shows the task's `TaskBrowser` entries, one tab each, fixed at the
2317
+ task's definition: their count and their details, `urlVisibility` included, arrive with the task,
2318
+ and nothing adds a tab to a task later. The personal workspace is the agent's: as many
2319
+ `PersonalBrowser` tabs as they open, never on the wire, and no provider says anything about what
2320
+ they may see there.
2321
+
2299
2322
  #### `TaskBrowser` and isolation
2300
2323
 
2301
2324
  Each `TaskBrowser` defines one named browser in the task workspace.
@@ -2308,6 +2331,7 @@ Each `TaskBrowser` defines one named browser in the task workspace.
2308
2331
  | `url` | Initial URL. Must use `http:` or `https:`; see below. Later navigation comes from Chromium. |
2309
2332
  | `reuse` | Required. `false` creates a task-specific browser session. |
2310
2333
  | `isolationScheme` | **Required when `reuse` is `true`**, and rejected when it is `false`. There is no default: see below. |
2334
+ | `urlVisibility` | What the agent sees of this tab's URL in Omni's chrome: `hidden`, `domain`, or `full`. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read — a caller's number, a CRM token. Per browser, on the provider's word; Omni honours it tab by tab. |
2311
2335
 
2312
2336
  ##### Choosing a reuse scheme
2313
2337
 
@@ -2506,7 +2530,7 @@ commands and a provider that offers `consultTransfer` implements all three.
2506
2530
 
2507
2531
  `applied` on `complete` says the provider is bridging the customer to the destination and
2508
2532
  dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
2509
- the provider reports `task-media-ended`, any completion allowance runs, and the task ends with a
2533
+ the provider reports `task-media-ended`, any wrap allowance runs, and the task ends with a
2510
2534
  `transferred` outcome naming the destination. `applied` on `cancel` says the destination is
2511
2535
  dropped; the task returns to `in-progress` with `consultation` gone. Omni waits for the
2512
2536
  provider's report of both, as it does for every command.
@@ -2537,13 +2561,13 @@ Every task may publish additional provider-specific controls in `capabilities.cu
2537
2561
  capabilities: {
2538
2562
  hold: true,
2539
2563
  custom: [
2540
- { id: "request-supervisor", ui: { kind: "button", label: "Request supervisor", placement: "secondary" } },
2541
- { id: "mark-vip", ui: { kind: "toggle", label: "Mark as VIP", placement: "overflow" } },
2564
+ { id: "request-supervisor", ui: { control: "button", label: "Request supervisor", placement: "secondary" } },
2565
+ { id: "mark-vip", ui: { control: "toggle", label: "Mark as VIP", placement: "overflow" } },
2542
2566
  ],
2543
2567
  }
2544
2568
  ```
2545
2569
 
2546
- Custom capability IDs must be non-empty and unique within the task. `ui.kind` is `button`, `toggle`,
2570
+ Custom capability IDs must be non-empty and unique within the task. `ui.control` is `button`, `toggle`,
2547
2571
  or `menu-item`; `ui.placement` is `primary`, `secondary`, or `overflow`. `ui.render` says where the
2548
2572
  control's work appears: `inline`, in the workspace beside the task, or `page`, as a page of its own
2549
2573
  — a tab in the same work area as the task's browsers, beside them, and alone on a task that has
@@ -2576,7 +2600,7 @@ Starts one outbound call from the idle dialpad. It is present only when the voic
2576
2600
  declares `dial`.
2577
2601
 
2578
2602
  - `destination` is the original number selected or entered by the agent.
2579
- - The provider holds `destination` to its declared `destinationPolicy`: under `contacts-only`, a
2603
+ - The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
2580
2604
  number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
2581
2605
  - `dialled` confirms that outbound call creation completed.
2582
2606
  - `failed` contains a `ProtocolFailure` and confirms no call was placed.
@@ -2594,8 +2618,8 @@ nothing while none are being accepted — so they are not published separately.
2594
2618
  | Field | Contract |
2595
2619
  | --- | --- |
2596
2620
  | `approval` | Where the agent's current request stands. See the states below. |
2597
- | `accepting` | Whether the agent may ask at all. Distinct from `approval`. |
2598
- | `refusedReason` | Display-ready reason shown when `accepting` is false — a standing gate that applies to everyone. |
2621
+ | `mayAsk` | Whether the agent may ask at all. Distinct from `approval`. |
2622
+ | `refusedReason` | Display-ready reason shown when `mayAsk` is false — a standing gate that applies to everyone. |
2599
2623
  | `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
2600
2624
  | `retryAfterMs` | How long until the agent may retry, when the provider can say. |
2601
2625
  | `reasons` | Not-ready codes this provider offers. Omitted when it defines none; an empty list is refused, being a second spelling of the same fact. |
@@ -2622,7 +2646,7 @@ A provider reports `starting-after-task` only after Omni commits a `granted` req
2622
2646
  work is still active. Omni does not send the request again, because asking again would not move
2623
2647
  it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
2624
2648
 
2625
- `accepting: false` is what lets Omni withdraw the control rather than let an agent ask and be
2649
+ `mayAsk: false` is what lets Omni withdraw the control rather than let an agent ask and be
2626
2650
  refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
2627
2651
  something a busy hour can cancel, and Omni keeps offering those while the rest are withdrawn.
2628
2652
 
@@ -2960,7 +2984,7 @@ A lead who also takes calls sees their team on the idle dashboard. `Snapshot.tea
2960
2984
  | --- | --- |
2961
2985
  | `members` | Every member of this lead's team, whatever their state. `[]` says the lead has a team with nobody in it; omitting the roster says something else entirely — see **The login is the permission** below. |
2962
2986
  | `requests` | The members currently asking this lead to join a call, each with the task and the note. Required when the login declares `team.consultControl`, `[]` when nobody is asking; omitted when it does not. See **Consulting a lead**. |
2963
- | `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a tier above the team made it theirs to keep. Required when the login declares `team.policyControl`; omitted when it does not. See **Who decides what an agent may do**. |
2987
+ | `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a level above the team made it theirs to keep. Required when the login declares `team.policyControl`; omitted when it does not. See **Who decides what an agent may do**. |
2964
2988
 
2965
2989
  | `TeamMember` field | Contract |
2966
2990
  | --- | --- |
@@ -3021,7 +3045,7 @@ never an identifier from another provider, and Omni does not translate between t
3021
3045
  from `describeUsers()`.
3022
3046
 
3023
3047
  `suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
3024
- approve them. A provider that suspends breaks must also publish `accepting: false` to the team's
3048
+ approve them. A provider that suspends breaks must also publish `mayAsk: false` to the team's
3025
3049
  agents so they see it before asking. A `place` must likewise reach that member as an `imposed` break
3026
3050
  on their own `BreakState`, or they are stopped from working with no way to see why.
3027
3051
 
@@ -3058,7 +3082,7 @@ executeTeamConsult({ command: { type: "decline", requestId: "req-7", reason: "In
3058
3082
  **On `join` the provider bridges three parties and the lead is on a task of their own**, on the
3059
3083
  same task id, arriving on the lead's connection as `task-offered` with `require-automatic-acceptance`
3060
3084
  -- the way a call an agent placed themselves arrives -- and carrying `assisting`. The agent's task
3061
- moves to `lead: { status: "joined", leadId }`. A join is the lead's own act, so capacity does not
3085
+ moves to `lead: { stage: "joined", leadId }`. A join is the lead's own act, so capacity does not
3062
3086
  trigger it; but from then on it is an outstanding task the provider counts against the lead's
3063
3087
  stated ceiling like any other, nothing more is allocated to the lead while it stands, and a
3064
3088
  provider whose lead is already at the ceiling answers the join `failed`.
@@ -3095,7 +3119,7 @@ const consultLeadCapable = {
3095
3119
  channel: "voice",
3096
3120
  capabilities: { hold: true, consultLead: true, dispositions: true },
3097
3121
  phase: "in-progress",
3098
- lead: { status: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
3122
+ lead: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
3099
3123
  } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "lead">;
3100
3124
  ```
3101
3125
 
@@ -3171,7 +3195,6 @@ what failed, and reports. It never decides for the adapter what a missing microp
3171
3195
  | `online` | Whether the host has a network interface up. Not a claim that anything is reachable — the adapter knows whether it can reach its own platform far better than the host does — so `false` is a reason not to go ready and `true` is not a reason to. |
3172
3196
  | `audio` | Present on a voice connection, absent where there is no audio. |
3173
3197
  | `audio.input` | `ready` with `localAudio` — the microphone as captured, the same stream `openMedia` receives — and `flowing`, false while the hardware or OS says no audio moves through it (a headset's own mute switch, which Omni's Mute control never touches). `unavailable` with `reason`, since each wants a different fix from the agent: `no-device`; `denied`; `not-asked`, which a host that asks at connect never publishes; `in-use`, a device present and permitted that another application holds — on an agent desktop the commonest of all; `lost`, a capture that ended. A host decides the reason from the devices before the error name: a browser can report a permission error on a machine with no microphone at all, and "grant permission" is the wrong instruction for an agent who needs to plug one in. `failure` carries the words Omni showed them. |
3174
- | `browsers.urlVisibility` | What Omni's own chrome shows of a task browser's URL: `hidden`, `domain`, or `full`. A statement about the chrome, not about what the page renders or a screenshot captures; task browsers only — the personal browser is the agent's own and nothing a provider sends appears in it. A provider reads it before deciding what URL it is willing to send: one that carries confidential data goes out under `hidden` and not under `full`. |
3175
3198
  | `audio.output` | `ready`, or `unavailable` with `reason` — `no-device`, or `lost` for one removed — and `failure`: an agent who cannot hear is as unable to take a call as one who cannot speak. |
3176
3199
 
3177
3200
  Omni republishes the report whenever it changes — a permission granted late, a headset unplugged,
@@ -3245,8 +3268,8 @@ carries as `audio.input.localAudio`, and absent while that input is `unavailable
3245
3268
  bridges audio without a host-side input may ignore it; one that needs it and finds it absent
3246
3269
  answers `unavailable` with a failure Omni shows the agent.
3247
3270
 
3248
- **When to ask is the provider's word, not Omni's guess.** Omni opens media on `task-media-ready`,
3249
- and on a task arriving with `media: "ready"` on a snapshot; it closes on `task-media-ended` and
3271
+ **When to ask is the provider's word, not Omni's guess.** Omni opens media on `task-media-started`,
3272
+ and on a task arriving with `media: "started"` on a snapshot; it closes on `task-media-ended` and
3250
3273
  when the task ends. Between those words, nothing Omni's own senses report — a stream that drops, a
3251
3274
  track that ends — moves the task or its audio.
3252
3275
 
@@ -3323,7 +3346,7 @@ Applies a `TaskCommandRequest` to one provider-local task.
3323
3346
  - `applied` confirms the side effect completed. `failed` confirms it did **not**, with a typed
3324
3347
  `ProtocolFailure`; a provider that will not and one that cannot report the same shape, and `code`
3325
3348
  says which.
3326
- - A command sent while `provider-status` is not `active` answers `failed` with `omni.unavailable`.
3349
+ - A command sent while `transport-status` is not `active` answers `failed` with `omni.unavailable`.
3327
3350
  Neither Omni nor the adapter queues it.
3328
3351
  - **A command that asks for a state answers `applied` when that state holds, whoever brought it
3329
3352
  about; a command that acts answers `failed` when it cannot act.** Declining a lead request
@@ -3361,7 +3384,7 @@ react rather than only display the message:
3361
3384
  | `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
3362
3385
  | `omni.destination-not-permitted` | The dial or transfer destination violates the provider's policy. |
3363
3386
  | `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
3364
- | `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `provider-status` is not `active`. |
3387
+ | `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
3365
3388
  | `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
3366
3389
 
3367
3390
  They are published as `OMNI_FAILURE_CODES`.
@@ -3373,14 +3396,14 @@ They are published as `OMNI_FAILURE_CODES`.
3373
3396
  | Field | Contract |
3374
3397
  | --- | --- |
3375
3398
  | `id` | Required identifier for this event, unique within the login. Omni does not act on it; it exists so a host log line and an adapter log line can be matched when something has to be traced. |
3376
- | `sessionId` | Login session that produced the event. Omni rejects any other value, which only reaches it if an adapter kept an old connection emitting after a re-login. |
3399
+ | `loginId` | Login session that produced the event. Omni rejects any other value, which only reaches it if an adapter kept an old connection emitting after a re-login. |
3377
3400
  | `occurredAt` | Valid RFC-3339 timestamp with an explicit timezone, representing provider observation time. |
3378
3401
  | `event` | Typed `ProviderEvent` payload. |
3379
3402
 
3380
3403
  #### Provider instants are read against a provider clock
3381
3404
 
3382
3405
  Every deadline in this contract is a provider instant that Omni counts down: `allocationExpiresAt`,
3383
- `preparationEndsAt`, and the wrap deadline of `task-media-ended` plus `completionAllowance` where
3406
+ `preparationEndsAt`, and the wrap deadline of `task-media-ended` plus `wrapAllowance` where
3384
3407
  one is stated.
3385
3408
  Comparing those against the host clock is wrong by whatever the two machines disagree by, and the
3386
3409
  damaging direction is early — **Accept** withdrawn from an offer still ringing, a wrap timer
@@ -3402,7 +3425,10 @@ silently lose a message, so while the connection is up Omni has seen everything
3402
3425
  Loss has exactly one shape: the connection went away. The adapter reports `connecting` or `error`,
3403
3426
  reconnects, and emits a `snapshot` event carrying complete state. That snapshot is the repair —
3404
3427
  whatever was missed while the connection was down is in it, and Omni replaces its provider view
3405
- rather than reasoning about what it did not receive.
3428
+ rather than reasoning about what it did not receive. A repair is an answer like any other: a
3429
+ platform that has not yet answered for this login after a reconnect — a state read served empty by
3430
+ a backend that does not know the session yet — yields no snapshot, and the adapter stays
3431
+ `connecting` holding what it holds. See **Snapshots establish state; events report transactions**.
3406
3432
 
3407
3433
  A snapshot must account for **everything the adapter has emitted before it resolves**, not merely
3408
3434
  everything emitted when it was requested. Omni discards events buffered during the read on that
@@ -3411,8 +3437,8 @@ Omni apply state the snapshot already superseded.
3411
3437
 
3412
3438
  #### Liveness
3413
3439
 
3414
- `provider-status` is the only signal Omni has that a transport died. An adapter must emit
3415
- `provider-status` with `connecting` or `error` as soon as it loses its transport, rather than
3440
+ `transport-status` is the only signal Omni has that a transport died. An adapter must emit
3441
+ `transport-status` with `connecting` or `error` as soon as it loses its transport, rather than
3416
3442
  leaving a stale `active` in place while it retries internally; Omni cannot distinguish a quiet
3417
3443
  healthy provider from a dead one.
3418
3444
 
@@ -3432,9 +3458,9 @@ snapshot. It carries what the login's capabilities call for — a roster for a l
3432
3458
  snapshot — and nothing they do not; a capability is withdrawn by a republished `authenticated`,
3433
3459
  never by an omission from a snapshot.
3434
3460
 
3435
- ### `provider-status`
3461
+ ### `transport-status`
3436
3462
 
3437
- Updates `ConnectionStatus`, and carries an optional `message` that may explain an error but must be
3463
+ Updates `TransportStatus`, and carries an optional `message` that may explain an error but must be
3438
3464
  safe for the agent to see.
3439
3465
 
3440
3466
  | Value | Contract |
@@ -3448,7 +3474,7 @@ died; the host knows how to run a login. `recovery` joins the two:
3448
3474
 
3449
3475
  - **`reconnect`** — the login is good and this connection is not: a backend restart, a session the
3450
3476
  platform no longer recognises. Omni calls `disconnect()` on the dead connection and then
3451
- `connect()` again on the same login — same `sessionId` — and the fresh connect snapshot
3477
+ `connect()` again on the same login — same `loginId` — and the fresh connect snapshot
3452
3478
  re-establishes state exactly as a reconnect snapshot does. `connect()` is once per
3453
3479
  `Connection`, not once per login.
3454
3480
  - **`reauthenticate`** — the session under the login died: a token rejected, a remote logout. Omni
@@ -3460,7 +3486,7 @@ has to decide when to stop. Omni owns giving up: after however long it chooses t
3460
3486
  call `disconnect()` and either `connect()` afresh or surface the failure — so neither side waits
3461
3487
  for the other to blink.
3462
3488
 
3463
- **Status is about the transport, nothing else.** It does not say whether the agent is available,
3489
+ **`transport` is about the transport, nothing else.** It does not say whether the agent is available,
3464
3490
  whether they are on a break, or how much work they can take: capacity travels on `setCapacity`,
3465
3491
  availability on `BreakState`. Nor does it carry authentication — a session that expired reports
3466
3492
  `expired` on `AuthenticationState` and fails actions with `omni.not-authenticated`, while
@@ -3475,7 +3501,7 @@ come — see **Liveness**.
3475
3501
 
3476
3502
  Replaces this provider's complete `break` object. Its `approval` uses the canonical
3477
3503
  `not-requested`, `awaiting-decision`, `granted`, `starting-after-task` and
3478
- `in-effect` states defined under Breaks; the event also carries the corresponding accepting state,
3504
+ `in-effect` states defined under Breaks; the event also carries the corresponding may-ask state,
3479
3505
  reasons, retry details, and any imposed break.
3480
3506
 
3481
3507
  Each state is also held to the one before it. A commit's states, `starting-after-task` and
@@ -3508,20 +3534,27 @@ snapshots until it ends.
3508
3534
  Replaces the current representation of one provider-local task. It is a full task value, not a
3509
3535
  partial patch.
3510
3536
 
3511
- ### `task-media-ready`
3537
+ ### `task-media-started`
3512
3538
 
3513
3539
  The provider's word that the task's audio should now attach. Omni calls `openMedia` on it — and on
3514
- a task carried with `media: "ready"`, which is how a reconnect snapshot reattaches audio an
3540
+ a task carried with `media: "started"`, which is how a reconnect snapshot reattaches audio an
3515
3541
  earlier event brought — and renders the call as live from that word, never from its own senses. It
3516
- names a task whose work has begun, and it alternates with `task-media-ended`: media that was never
3517
- made ready cannot end, so a live call whose provider says nothing about its audio is a provider in
3542
+ names a task whose work has begun, and it alternates with `task-media-ended`: media that never
3543
+ started cannot end, so a live call whose provider says nothing about its audio is a provider in
3518
3544
  breach, not a state a desk fills in from its own devices.
3519
3545
 
3546
+ The event is the transition and the task's `media` field is the state. A `task-updated` re-states
3547
+ the media its task already holds — republishing `started` on a hold is a statement, not a second
3548
+ arrival — but it does not move it: an update that itself flips the field is refused
3549
+ (`stream.taskUpdated.media`), and the pairing at the moment audio arrives is the phase change
3550
+ without the field, then the event. Releasing `ended` is the one move an update may make, since
3551
+ wrapped audio has nothing left to end.
3552
+
3520
3553
  ### `task-media-ended`
3521
3554
 
3522
3555
  Signals that a task's real-time media ended. For voice and similar channels, this starts the fixed
3523
- completion timer. It does not remove the task, and it ends only audio that `task-media-ready` — or
3524
- a task carried with `media: "ready"` — attached.
3556
+ completion timer. It does not remove the task, and it ends only audio that `task-media-started` — or
3557
+ a task carried with `media: "started"` — attached.
3525
3558
 
3526
3559
  ### `task-ended`
3527
3560
 
@@ -3609,8 +3642,8 @@ same exported checks are used by Omni and adapter tests so their interpretations
3609
3642
  | Function | Validates |
3610
3643
  | --- | --- |
3611
3644
  | `validateManifest(manifest)` | Identity, protocol-version interoperability, authentication methods, and idle-capability shapes. |
3612
- | `validateTask(task, { channel })` | Identity, channel agreement, phase, completion allowance, capability shapes, custom controls, and browsers. |
3613
- | `validateSnapshot(snapshot, manifest)` | Status, break state, break reasons, team roster, and every task, contact, and activity, including idle-capability gating both ways: a contribution the manifest never declared is refused, and one it declares is required, `[]` included. |
3645
+ | `validateTask(task, { channel })` | Identity, channel agreement, phase, wrap allowance, capability shapes, custom controls, and browsers. |
3646
+ | `validateSnapshot(snapshot, manifest)` | Status, break state, break reasons, team roster, the stated `taskCount` reconciled against the tasks carried, and every task, contact, and activity, including idle-capability gating both ways: a contribution the manifest never declared is refused, and one it declares is required, `[]` included. |
3614
3647
  | `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
3615
3648
  | `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
3616
3649
  | `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
@@ -3704,7 +3737,7 @@ cannot be established from TypeScript structure alone.
3704
3737
  | `stillHost(report?)` | A host that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a voice adapter. |
3705
3738
  | `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for a host that wants the same rules at its boundary: `seed(snapshot)`, then `apply(envelope)` returns the violations. |
3706
3739
  | `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a placed break arriving in effect with `imposed`. The harness applies the same rules after the connect snapshot. |
3707
- | `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-ready` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
3740
+ | `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-started` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
3708
3741
  | `assertBreakParticipants(candidates, participants)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
3709
3742
  | `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `in-effect` only once nothing is outstanding — never beside a task, never later than the step that has none. |
3710
3743
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |