@xema/omni-protocol 0.1.25 → 0.1.27

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,8 +359,8 @@ 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
366
  taskCount: number;
@@ -393,7 +393,7 @@ type ScheduledActivity = {
393
393
  title: string;
394
394
  startsAt: IsoTimestamp;
395
395
  endsAt?: IsoTimestamp;
396
- contact?: Contact;
396
+ party?: Contact;
397
397
  attributes?: Attribute[];
398
398
  };
399
399
 
@@ -411,7 +411,7 @@ type DialResult =
411
411
  ```ts
412
412
  type DispositionCode = { id: string; label: string; group?: string };
413
413
 
414
- type DispositionPolicy = {
414
+ type DispositionRules = {
415
415
  required?: boolean;
416
416
  notes?: "required" | "optional" | "hidden";
417
417
  codes?: DispositionCode[];
@@ -432,7 +432,7 @@ type DestinationDirectory = {
432
432
  type CustomCapability = {
433
433
  id: string;
434
434
  ui: {
435
- kind: "button" | "toggle" | "menu-item";
435
+ control: "button" | "toggle" | "menu-item";
436
436
  label: string;
437
437
  placement: "primary" | "secondary" | "overflow";
438
438
  render?: "inline" | "page";
@@ -442,7 +442,7 @@ type CustomCapability = {
442
442
 
443
443
  type SharedTaskCapabilities = {
444
444
  browsers?: true;
445
- dispositions?: true | DispositionPolicy;
445
+ dispositions?: true | DispositionRules;
446
446
  custom?: CustomCapability[];
447
447
  };
448
448
 
@@ -482,18 +482,22 @@ const BROWSER_ISOLATION_SCHEMES = {
482
482
  type BrowserIsolationScheme =
483
483
  (typeof BROWSER_ISOLATION_SCHEMES)[keyof typeof BROWSER_ISOLATION_SCHEMES];
484
484
 
485
- type TaskBrowserBase = {
485
+ type Browser = {
486
486
  id: string;
487
487
  name: string;
488
- purpose: string;
489
488
  url: string;
490
489
  };
491
490
 
492
- type TaskBrowser = TaskBrowserBase & (
493
- | { reuse: false; isolationScheme?: never }
494
- | { reuse: true; isolationScheme: BrowserIsolationScheme }
491
+ type TaskBrowser = Browser & {
492
+ purpose: string;
493
+ urlVisibility?: UrlVisibility;
494
+ } & (
495
+ | { sharedSession: false; isolationScheme?: never }
496
+ | { sharedSession: true; isolationScheme: BrowserIsolationScheme }
495
497
  );
496
498
 
499
+ type PersonalBrowser = Browser;
500
+
497
501
  type BrowserSessionKeyInput = {
498
502
  providerId: string;
499
503
  taskId: TaskId;
@@ -503,7 +507,7 @@ type BrowserSessionKeyInput = {
503
507
  ```
504
508
 
505
509
  That union is what makes a reusing browser with no scheme fail to compile rather than inherit a
506
- default — see **Choosing a reuse scheme**.
510
+ default — see **Choosing an isolation scheme**.
507
511
 
508
512
  ### Task
509
513
 
@@ -525,7 +529,7 @@ type TaskAttributeBase = {
525
529
 
526
530
  type TaskAttribute = TaskAttributeBase & (
527
531
  | { type: "text"; value: string }
528
- | { type: "contact"; contact: Contact }
532
+ | { type: "contact"; party: Contact }
529
533
  | { type: "timestamp"; at: IsoTimestamp }
530
534
  );
531
535
 
@@ -547,8 +551,8 @@ type TaskHandlingStep = {
547
551
  };
548
552
 
549
553
  type TaskCompletion =
550
- | { completionMode: "agent-command"; completionAllowance?: DurationSeconds }
551
- | { completionMode: "provider-automatic"; completionAllowance: DurationSeconds };
554
+ | { completionMode: "agent-command"; wrapAllowance?: DurationSeconds }
555
+ | { completionMode: "provider-automatic"; wrapAllowance: DurationSeconds };
552
556
 
553
557
  type TaskConsultation = {
554
558
  destination: string;
@@ -557,7 +561,7 @@ type TaskConsultation = {
557
561
  };
558
562
 
559
563
  type TaskLead = {
560
- status: "requested" | "joined";
564
+ stage: "requested" | "joined";
561
565
  leadId?: UserId;
562
566
  note?: string;
563
567
  since: IsoTimestamp;
@@ -569,21 +573,21 @@ type TaskAssisting = {
569
573
  since: IsoTimestamp;
570
574
  };
571
575
 
572
- type Tier = string;
576
+ type Level = string;
573
577
 
574
- type TierDeclaration = {
575
- id: Tier;
578
+ type LevelDeclaration = {
579
+ id: Level;
576
580
  label: string;
577
581
  };
578
582
 
579
583
  type Locked = {
580
- lockedBy: Tier;
584
+ lockedBy: Level;
581
585
  reason?: string;
582
586
  };
583
587
 
584
588
  type Lockable<T> = T | Locked;
585
589
 
586
- type TaskMediaState = "ready" | "ended";
590
+ type TaskMediaState = "started" | "ended";
587
591
 
588
592
  type Task<C extends Channel = Channel> = {
589
593
  id: TaskId;
@@ -592,7 +596,7 @@ type Task<C extends Channel = Channel> = {
592
596
  taskType: string;
593
597
  capabilities: TaskCapabilities<C>;
594
598
  browsers: TaskBrowser[];
595
- contact?: Contact;
599
+ party?: Contact;
596
600
  phase: TaskPhase;
597
601
  reference?: string;
598
602
  attributes?: TaskAttribute[];
@@ -705,33 +709,32 @@ what the queue allows: on for everyone, off for everyone, or left to the person.
705
709
  your team is a **policy**; what you do to yourself is a **preference**. A person belongs to one
706
710
  team and many queues, so a policy applies across every queue the person works.
707
711
 
708
- **The provider resolves; the protocol carries the result and who decided.** The structure's tiers
709
- 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
710
714
  the person settles the value for everyone below it, and where nothing enforces the most specific
711
- tier that says anything wins. The protocol names a tier by the id the manifest declares for it and
712
- never describes the chain between them: which tiers a person passes through is the structure's to
713
- 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`,
714
718
  `team`, `person`, each with the label a desk shows. A structure that differs states its whole
715
- ladder in `Manifest.orgTiers`, `person` included: what the list carries is in force, and what it
716
- leaves out does not exist — a structure with no site tier declares `org`, `team`, `person`, and
717
- `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
718
722
  at: a label with no policy behind it decides nothing. A manifest that declares none has exactly
719
- the four. `lockedBy` is any tier in force except `person`, who never locks their own value;
720
- `setBy` is any tier in force, or `provisioning`, the protocol's word for "no tier has said
721
- anything and the provider's own configuration supplied the value" the provider speaking, never
722
- 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
723
726
  declared labels and needs no others, and validates every republished `authenticated` state
724
727
  against them, not only the sign-in. What the wire carries is the resolution:
725
728
 
726
- - **`lockedBy`** — a tier above the person made this value theirs to keep. A person never locks
727
- their own value, and the queue is not a tier: what the queue does not allow at all is absent.
728
- - **`setBy`** — who stated the value as it stands: a tier, the `person` themself, or
729
- `provisioning` where no tier has said anything. Provenance, not a lock: a value that came from a
730
- 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.
731
734
 
732
735
  **On a task, a control the queue could allow may stand locked in its place.** `Task.capabilities`
733
736
  is the effective set. What the queue does not allow is absent and nothing is shown. What the queue
734
- 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
735
738
  value would be — `mute: { lockedBy: "team", reason: "Nobody on this team mutes" }` — and Omni
736
739
  renders that control disabled, saying who decided, so an agent who cannot press Mute knows whether
737
740
  to ask their lead or their site. `lockedBy` is the discriminant: a value that carries it is the
@@ -747,20 +750,20 @@ controls — is content, and is never locked.
747
750
  with `on`, `off`, or `agent`, for any task control, `dial`, or a skill — and only `hold`, `mute`
748
751
  and skills may be `agent`; callback and new call are the team's, on or off, within what the queue
749
752
  allows. The roster carries `policies` for such a login: every policy as it stands, who set it, and
750
- `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
751
754
  change — `executeTeamPolicy` on it answers `failed` with `omni.capability-not-enabled`.
752
755
 
753
756
  **What the team left to the person is the person's, and the provider keeps it.** The login's
754
757
  `capabilities.preferences` lists every preference the person may hold — `hold`, `mute`, a skill —
755
758
  with where it stands and who set it: `setBy: "team"` while they inherit the team's default,
756
- `"person"` once they have set their own, `"provisioning"` where no tier has said anything. Nothing
757
- 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
758
761
  `lockedBy`. `setPreference` is the person's act — `{ id, enabled }` to set their own, `{ id,
759
762
  inherit: true }` to give it up and inherit again — answered `applied` and republished as a new
760
763
  `authenticated` state when something changed, a state and not a flicker, as every republish of
761
764
  `authenticated` is; and it is durable: the person's across sessions. A lead may also set a
762
765
  person's preference from their own screen, which arrives the same way. A preference is keyed by
763
- 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
764
767
  `Task.capabilities`, set for the team in `policies`, left to the person in `preferences`. The
765
768
  command `mute` acts on one call; the preference `mute` says whether the person wants the control
766
769
  at all, and a host renders it in its settings, never as the button on a call.
@@ -794,7 +797,7 @@ type ImposedBreak =
794
797
 
795
798
  type BreakState = {
796
799
  approval: BreakApproval;
797
- accepting: boolean;
800
+ mayAsk: boolean;
798
801
  refusedReason?: string;
799
802
  decisionReason?: string;
800
803
  retryAfterMs?: number;
@@ -914,7 +917,7 @@ type OpenMediaRequest = {
914
917
  ```ts
915
918
  type SummaryMetric = { id: string; label: string; value: string };
916
919
 
917
- type ProviderSummary = {
920
+ type QueueSummary = {
918
921
  title: string;
919
922
  subtitle?: string;
920
923
  waitingCount: number;
@@ -922,12 +925,12 @@ type ProviderSummary = {
922
925
  metrics?: SummaryMetric[];
923
926
  };
924
927
 
925
- type ConnectionRecovery = "reconnect" | "reauthenticate";
928
+ type TransportRecovery = "reconnect" | "reauthenticate";
926
929
 
927
930
  type ProviderEvent =
928
931
  | { type: "snapshot"; reason: "reconnected" | "provider-requested"; snapshot: Snapshot }
929
- | { type: "provider-status"; status: "connecting" | "active"; message?: string }
930
- | { 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 }
931
934
  | { type: "break-state"; break: BreakState }
932
935
  | {
933
936
  type: "task-offered";
@@ -937,18 +940,18 @@ type ProviderEvent =
937
940
  preparationEndsAt?: IsoTimestamp;
938
941
  }
939
942
  | { type: "task-updated"; task: Task }
940
- | { type: "task-media-ready"; taskId: TaskId }
943
+ | { type: "task-media-started"; taskId: TaskId }
941
944
  | { type: "task-media-ended"; taskId: TaskId }
942
945
  | { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
943
946
  | { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
944
- | { type: "provider-summary"; summary: ProviderSummary }
947
+ | { type: "queue-summary"; summary: QueueSummary }
945
948
  | { type: "team-updated"; team: TeamRoster }
946
949
  | { type: "contacts-updated"; contacts: Contact[] }
947
950
  | { type: "calendar-updated"; scheduledActivities: ScheduledActivity[] };
948
951
 
949
952
  type ProviderEventEnvelope = {
950
953
  id: string;
951
- sessionId: string;
954
+ loginId: string;
952
955
  occurredAt: IsoTimestamp;
953
956
  event: ProviderEvent;
954
957
  };
@@ -998,12 +1001,12 @@ const ALLOWED_BROWSER_URL_SCHEMES = ["http:", "https:"] as const;
998
1001
  const IDLE_CAPABILITIES = ["dial", "personalBrowser", "calendar", "contacts"] as const;
999
1002
  type IdleCapability = (typeof IDLE_CAPABILITIES)[number];
1000
1003
 
1001
- const DEFAULT_TIERS = [
1004
+ const DEFAULT_LEVELS = [
1002
1005
  { id: "org", label: "Your organisation" },
1003
1006
  { id: "site", label: "Your site" },
1004
1007
  { id: "team", label: "Your team" },
1005
1008
  { id: "person", label: "You" },
1006
- ] as const satisfies readonly TierDeclaration[];
1009
+ ] as const satisfies readonly LevelDeclaration[];
1007
1010
 
1008
1011
  const IDLE_CAPABILITY_UI = {
1009
1012
  dial: "Dialpad",
@@ -1247,7 +1250,7 @@ make a repeat safe. Omni does not retry; if the agent acts again it is a new com
1247
1250
 
1248
1251
  On a persistent ordered transport there is only one way a command goes unsettled: the connection
1249
1252
  went away underneath it. **An adapter that cannot settle a command has lost its transport, and
1250
- says so** — `provider-status` `connecting`, reconnect, snapshot — whichever channel the command
1253
+ says so** — `transport-status` `connecting`, reconnect, snapshot — whichever channel the command
1251
1254
  actually travelled on. An unsettled promise is therefore always followed by a snapshot, and that
1252
1255
  snapshot is the answer; Omni waits for it rather than calling `snapshot()` itself. While the
1253
1256
  transport is up, a result says the provider accepted the command, and the event that follows —
@@ -1256,7 +1259,7 @@ transport is up, a result says the provider accepted the command, and the event
1256
1259
  **Classify on the rejection, never on a connection status published separately from it.** The
1257
1260
  status is a report about the wire and races the rejection; the rejection is the event. A rejection
1258
1261
  is the provider's answer only when it carries the provider's answer — a failure the provider
1259
- 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.
1260
1263
 
1261
1264
  A command therefore carries no key. The provider names its own records — a task, a lead request, a
1262
1265
  member — and Omni refers to them by those names; **Omni never asks a provider to remember a name
@@ -1346,7 +1349,7 @@ export default defineAdapter({
1346
1349
  supportedProtocolVersions: [OMNI_PROTOCOL_VERSION],
1347
1350
  authenticationMethods: ["browser-sso"],
1348
1351
  idleCapabilities: {
1349
- dial: { destinationPolicy: "any-number" },
1352
+ dial: { destinations: "any-number" },
1350
1353
  },
1351
1354
  },
1352
1355
  createAuthenticationSession: context => createAcmeAuthentication(context),
@@ -1371,7 +1374,7 @@ compile time.
1371
1374
  | `idleCapabilities` | Declares actions Omni may offer while the agent has no active task, such as voice dialing. Task controls do not belong here. |
1372
1375
  | `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
1373
1376
  | `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
1374
- | `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**. |
1375
1378
 
1376
1379
  ### Authentication methods
1377
1380
 
@@ -1425,11 +1428,11 @@ a destination policy:
1425
1428
 
1426
1429
  ```ts
1427
1430
  idleCapabilities: {
1428
- dial: { destinationPolicy: "any-number" }
1431
+ dial: { destinations: "any-number" }
1429
1432
  }
1430
1433
  ```
1431
1434
 
1432
- `destinationPolicy` is required and accepts one `DialDestinationPolicy`:
1435
+ `destinations` is required and accepts one `DialDestinations`:
1433
1436
 
1434
1437
  | Value | Contract |
1435
1438
  | --- | --- |
@@ -1523,7 +1526,7 @@ changes.
1523
1526
  | `title` | Required agent-facing activity title. |
1524
1527
  | `startsAt` | Required RFC-3339 start time with an explicit timezone. |
1525
1528
  | `endsAt` | Optional RFC-3339 end time with an explicit timezone. |
1526
- | `contact` | Optional related `Contact`. |
1529
+ | `party` | The person the activity reaches — a callback's customer — as a `Contact`. Optional. |
1527
1530
  | `attributes` | Optional ordered `Attribute` entries. Keys must be non-empty. |
1528
1531
 
1529
1532
  **There is no `type` field**, for the reason there is none on `Contact`: an open category is
@@ -1624,7 +1627,7 @@ Creates the provider-scoped authentication session.
1624
1627
  | `AuthenticationContext` field | Contract |
1625
1628
  | --- | --- |
1626
1629
  | `protocolVersion` | The negotiated version, fixed for this login. |
1627
- | `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. |
1628
1631
  | `secrets` | Omni-provided `SecretStore`, scoped to this provider's manifest id. |
1629
1632
  | `signal` | Optional cancellation signal. |
1630
1633
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
@@ -1658,11 +1661,11 @@ login fix, rendered apart from a provider Omni cannot reach. Commands meanwhile
1658
1661
  `omni.not-authenticated`.
1659
1662
 
1660
1663
  **Re-authentication restores the login; it does not replace it.** It runs on the session Omni
1661
- 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
1662
1665
  new challenge — and when the state returns to `authenticated` the connection and everything on it
1663
1666
  carry on: Omni does not call `connect()` again, since a second connection would be a second
1664
1667
  session for one agent. What "signing in again replaces the login" describes is a new
1665
- `AuthenticationSession` under a new `sessionId`, after `signed-out`.
1668
+ `AuthenticationSession` under a new `loginId`, after `signed-out`.
1666
1669
 
1667
1670
  ### What the login may do
1668
1671
 
@@ -1813,14 +1816,14 @@ Creates one live provider connection for the signed-in agent.
1813
1816
  - The returned connection owns reconnect until Omni calls `disconnect()` or aborts `context.signal`.
1814
1817
  - May be called again on the same login after that: once per `Connection`, not once per login.
1815
1818
  Omni disposes a connection whose `error` named `recovery: "reconnect"` with `disconnect()` and
1816
- calls `connect()` afresh — see **`provider-status`**.
1819
+ calls `connect()` afresh — see **`transport-status`**.
1817
1820
 
1818
1821
  ### `ConnectContext`
1819
1822
 
1820
1823
  | Field | Contract |
1821
1824
  | --- | --- |
1822
1825
  | `protocolVersion` | Version negotiated before authentication. Fixed for this login. |
1823
- | `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. |
1824
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. |
1825
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**. |
1826
1829
  | `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
@@ -1845,9 +1848,9 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
1845
1848
 
1846
1849
  | Field | Contract |
1847
1850
  | --- | --- |
1848
- | `status` | Current `ConnectionStatus` — whether this provider's transport can serve the session. Defined under **`provider-status`**. |
1849
- | `sessionId` | Identity of this login session. It must match the connection context. |
1850
- | `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. |
1851
1854
  | `tasks` | Complete set of tasks currently offered to or owned by this agent. |
1852
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. |
1853
1856
  | `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
@@ -2068,12 +2071,12 @@ time. Runtime conformance checks also require the task channel to match its prov
2068
2071
  | `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
2069
2072
  | `capabilities` | Controls and workspace features available for this specific task. |
2070
2073
  | `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
2071
- | `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. |
2074
+ | `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. |
2072
2075
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, or `completing`. |
2073
- | `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`**. |
2074
2077
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
2075
2078
  | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
2076
- | `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. |
2077
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. |
2078
2081
  | `handlingHistory` | Optional ordered handling history for this currently open task. It is live task data, not a permanent archive. |
2079
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**. |
@@ -2088,7 +2091,7 @@ const attributes: TaskAttribute[] = [
2088
2091
  key: "related-contact",
2089
2092
  label: "Related contact",
2090
2093
  type: "contact",
2091
- contact: { name: "Asha Rao", number: "+919876543210" },
2094
+ party: { name: "Asha Rao", number: "+919876543210" },
2092
2095
  },
2093
2096
  {
2094
2097
  key: "answered",
@@ -2130,7 +2133,7 @@ command.
2130
2133
  routed to the agent and accepted as `acceptanceMode` dictates, and its presence and phase follow
2131
2134
  the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
2132
2135
  consult, a conference leg joining or leaving, a transfer, a callback — the media follows
2133
- 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
2134
2137
  `task-media-ended`. Omni does not ring,
2135
2138
  bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
2136
2139
  are the adapter's and the platform's, transient, and decide neither when a task exists nor what
@@ -2142,7 +2145,7 @@ allowance starts on it and the callback control appears on it — and Omni follo
2142
2145
  follows any other. What Omni never does is derive a task's state from its own media session: a
2143
2146
  stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
2144
2147
  endpoint re-registering change nothing about the task until the provider says so. Structurally:
2145
- `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
2146
2149
  only where it arrived, what follows the media ending is `completing` or `task-ended`, and every
2147
2150
  task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
2148
2151
  and `assertMediaFollowsTheTask` holds any sequence.
@@ -2153,10 +2156,10 @@ and `assertMediaFollowsTheTask` holds any sequence.
2153
2156
  the task open until Omni sends the channel's `complete` command. With `provider-automatic`, the
2154
2157
  provider may complete the task without receiving that command.
2155
2158
 
2156
- `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
2157
2160
  whether the channel carries real-time media:
2158
2161
 
2159
- | Channel | Completion allowance starts at |
2162
+ | Channel | Wrap allowance starts at |
2160
2163
  | --- | --- |
2161
2164
  | Voice and any channel with real-time media | The `task-media-ended` event |
2162
2165
  | Chat | When the conversation ends and the task enters `completing` |
@@ -2166,8 +2169,8 @@ whether the channel carries real-time media:
2166
2169
  ```ts
2167
2170
  const emailCompletion = {
2168
2171
  completionMode: "agent-command",
2169
- completionAllowance: 120,
2170
- } satisfies Pick<Task<"email">, "completionMode" | "completionAllowance">;
2172
+ wrapAllowance: 120,
2173
+ } satisfies Pick<Task<"email">, "completionMode" | "wrapAllowance">;
2171
2174
  ```
2172
2175
 
2173
2176
  In this example, the agent has two minutes after sending the email to add notes, select a
@@ -2177,7 +2180,7 @@ disposition, and complete the task.
2177
2180
  without waiting for a command; with `agent-command`, it still waits for `complete`.
2178
2181
 
2179
2182
  There is no value meaning "unlimited", because a number that is not a duration would be read as
2180
- 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
2181
2184
  only `agent-command` permits: the provider will not complete the task itself, so there is nothing
2182
2185
  for a deadline to trigger, and Omni counts nothing down. **Omitted and empty are different
2183
2186
  claims** applies -- omitted says there is no deadline to see, where `0` says the deadline is now.
@@ -2186,7 +2189,7 @@ Under `provider-automatic` the field is required, because the provider is going
2186
2189
  ```ts
2187
2190
  const untimedWrap = {
2188
2191
  completionMode: "agent-command",
2189
- } satisfies Pick<Task<"voice">, "completionMode" | "completionAllowance">;
2192
+ } satisfies Pick<Task<"voice">, "completionMode" | "wrapAllowance">;
2190
2193
  ```
2191
2194
 
2192
2195
  Here the customer has hung up, `task-media-ended` has been sent on time, the task is `completing`,
@@ -2202,7 +2205,7 @@ Omni issues `{ type: "callback" }`; it is issuable only in `completing`, and onl
2202
2205
  capability is declared. The provider knows who the party is; the command carries no destination.
2203
2206
 
2204
2207
  On `applied` the provider is placing the call and the task returns to `in-progress`: the agent is
2205
- 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
2206
2209
  reported as any call is -- `paused`, `in-progress`, and when its media ends, `task-media-ended`
2207
2210
  again, which starts a fresh allowance from that instant. A party who does not answer is a call
2208
2211
  whose media ended: the task returns to `completing` through the same event and the clock starts
@@ -2210,7 +2213,7 @@ again from there. At no point is an agent dialling against a deadline.
2210
2213
 
2211
2214
  **The control exists only while there is a window to use it in.** Under `agent-command` the task
2212
2215
  stays `completing` until the agent completes it, so the window is open for as long as they need.
2213
- 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
2214
2217
  is none: the provider disposes the task at provider end, and Omni does not offer Call back, whatever
2215
2218
  the task declares. A capability names a control that can be used; on a task with no `completing`
2216
2219
  window it cannot, and declaring it there changes nothing.
@@ -2221,8 +2224,8 @@ const callbackCapable = {
2221
2224
  capabilities: { hold: true, callback: true, dispositions: true },
2222
2225
  phase: "completing",
2223
2226
  completionMode: "provider-automatic",
2224
- completionAllowance: 30,
2225
- } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "completionAllowance">;
2227
+ wrapAllowance: 30,
2228
+ } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "wrapAllowance">;
2226
2229
  ```
2227
2230
 
2228
2231
  With ten seconds of the thirty left, the agent presses Call back: `execute({ command: { type:
@@ -2233,8 +2236,8 @@ from that instant.
2233
2236
  ```ts
2234
2237
  const immediateProviderCompletion = {
2235
2238
  completionMode: "provider-automatic",
2236
- completionAllowance: 0,
2237
- } satisfies Pick<Task, "completionMode" | "completionAllowance">;
2239
+ wrapAllowance: 0,
2240
+ } satisfies Pick<Task, "completionMode" | "wrapAllowance">;
2238
2241
  ```
2239
2242
 
2240
2243
  ### How a task has been handled
@@ -2309,6 +2312,13 @@ capabilities: { browsers: true }
2309
2312
 
2310
2313
  Tasks without browser definitions omit the capability and provide an empty `browsers` array.
2311
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
+
2312
2322
  #### `TaskBrowser` and isolation
2313
2323
 
2314
2324
  Each `TaskBrowser` defines one named browser in the task workspace.
@@ -2319,13 +2329,14 @@ Each `TaskBrowser` defines one named browser in the task workspace.
2319
2329
  | `name` | Agent-facing tab label, unique within the task, and an input to schemes containing `TAB_NAME`. |
2320
2330
  | `purpose` | Human-readable explanation of the browser's role. |
2321
2331
  | `url` | Initial URL. Must use `http:` or `https:`; see below. Later navigation comes from Chromium. |
2322
- | `reuse` | Required. `false` creates a task-specific browser session. |
2323
- | `isolationScheme` | **Required when `reuse` is `true`**, and rejected when it is `false`. There is no default: see below. |
2332
+ | `sharedSession` | Required. `false` creates a task-specific browser session. |
2333
+ | `isolationScheme` | **Required when `sharedSession` 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. |
2324
2335
 
2325
- ##### Choosing a reuse scheme
2336
+ ##### Choosing an isolation scheme
2326
2337
 
2327
2338
  Every scheme is supported and the provider picks the one its deployment needs. There is no
2328
- default, and a `reuse: true` browser that declares none is invalid — the type will not compile
2339
+ default, and a `sharedSession: true` browser that declares none is invalid — the type will not compile
2329
2340
  it and `validateSnapshot` reports `task.browser.isolationScheme.required`.
2330
2341
 
2331
2342
  That is deliberate. Sharing a signed-in session decides **who else may see those credentials**,
@@ -2347,7 +2358,7 @@ page rather than following a disallowed URL, and `isAllowedBrowserUrl()` is the
2347
2358
 
2348
2359
  ##### Reuse and isolation
2349
2360
 
2350
- With `reuse: true`, definitions producing the same isolation key share one **storage profile**:
2361
+ With `sharedSession: true`, definitions producing the same isolation key share one **storage profile**:
2351
2362
  cookies, local storage, session storage, permissions, and cached credentials. Different keys are
2352
2363
  isolated from one another.
2353
2364
 
@@ -2371,7 +2382,7 @@ browsers: [
2371
2382
  name: "CRM",
2372
2383
  purpose: "Contact record",
2373
2384
  url: "https://crm.example.com/contact/42",
2374
- reuse: true,
2385
+ sharedSession: true,
2375
2386
  isolationScheme: BROWSER_ISOLATION_SCHEMES.PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME,
2376
2387
  }
2377
2388
  ]
@@ -2519,7 +2530,7 @@ commands and a provider that offers `consultTransfer` implements all three.
2519
2530
 
2520
2531
  `applied` on `complete` says the provider is bridging the customer to the destination and
2521
2532
  dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
2522
- 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
2523
2534
  `transferred` outcome naming the destination. `applied` on `cancel` says the destination is
2524
2535
  dropped; the task returns to `in-progress` with `consultation` gone. Omni waits for the
2525
2536
  provider's report of both, as it does for every command.
@@ -2550,13 +2561,13 @@ Every task may publish additional provider-specific controls in `capabilities.cu
2550
2561
  capabilities: {
2551
2562
  hold: true,
2552
2563
  custom: [
2553
- { id: "request-supervisor", ui: { kind: "button", label: "Request supervisor", placement: "secondary" } },
2554
- { 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" } },
2555
2566
  ],
2556
2567
  }
2557
2568
  ```
2558
2569
 
2559
- 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`,
2560
2571
  or `menu-item`; `ui.placement` is `primary`, `secondary`, or `overflow`. `ui.render` says where the
2561
2572
  control's work appears: `inline`, in the workspace beside the task, or `page`, as a page of its own
2562
2573
  — a tab in the same work area as the task's browsers, beside them, and alone on a task that has
@@ -2589,7 +2600,7 @@ Starts one outbound call from the idle dialpad. It is present only when the voic
2589
2600
  declares `dial`.
2590
2601
 
2591
2602
  - `destination` is the original number selected or entered by the agent.
2592
- - 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
2593
2604
  number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
2594
2605
  - `dialled` confirms that outbound call creation completed.
2595
2606
  - `failed` contains a `ProtocolFailure` and confirms no call was placed.
@@ -2607,8 +2618,8 @@ nothing while none are being accepted — so they are not published separately.
2607
2618
  | Field | Contract |
2608
2619
  | --- | --- |
2609
2620
  | `approval` | Where the agent's current request stands. See the states below. |
2610
- | `accepting` | Whether the agent may ask at all. Distinct from `approval`. |
2611
- | `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. |
2612
2623
  | `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
2613
2624
  | `retryAfterMs` | How long until the agent may retry, when the provider can say. |
2614
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. |
@@ -2635,7 +2646,7 @@ A provider reports `starting-after-task` only after Omni commits a `granted` req
2635
2646
  work is still active. Omni does not send the request again, because asking again would not move
2636
2647
  it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
2637
2648
 
2638
- `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
2639
2650
  refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
2640
2651
  something a busy hour can cancel, and Omni keeps offering those while the rest are withdrawn.
2641
2652
 
@@ -2973,7 +2984,7 @@ A lead who also takes calls sees their team on the idle dashboard. `Snapshot.tea
2973
2984
  | --- | --- |
2974
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. |
2975
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**. |
2976
- | `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**. |
2977
2988
 
2978
2989
  | `TeamMember` field | Contract |
2979
2990
  | --- | --- |
@@ -3034,7 +3045,7 @@ never an identifier from another provider, and Omni does not translate between t
3034
3045
  from `describeUsers()`.
3035
3046
 
3036
3047
  `suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
3037
- 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
3038
3049
  agents so they see it before asking. A `place` must likewise reach that member as an `imposed` break
3039
3050
  on their own `BreakState`, or they are stopped from working with no way to see why.
3040
3051
 
@@ -3071,7 +3082,7 @@ executeTeamConsult({ command: { type: "decline", requestId: "req-7", reason: "In
3071
3082
  **On `join` the provider bridges three parties and the lead is on a task of their own**, on the
3072
3083
  same task id, arriving on the lead's connection as `task-offered` with `require-automatic-acceptance`
3073
3084
  -- the way a call an agent placed themselves arrives -- and carrying `assisting`. The agent's task
3074
- 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
3075
3086
  trigger it; but from then on it is an outstanding task the provider counts against the lead's
3076
3087
  stated ceiling like any other, nothing more is allocated to the lead while it stands, and a
3077
3088
  provider whose lead is already at the ceiling answers the join `failed`.
@@ -3108,7 +3119,7 @@ const consultLeadCapable = {
3108
3119
  channel: "voice",
3109
3120
  capabilities: { hold: true, consultLead: true, dispositions: true },
3110
3121
  phase: "in-progress",
3111
- 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" },
3112
3123
  } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "lead">;
3113
3124
  ```
3114
3125
 
@@ -3184,7 +3195,6 @@ what failed, and reports. It never decides for the adapter what a missing microp
3184
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. |
3185
3196
  | `audio` | Present on a voice connection, absent where there is no audio. |
3186
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. |
3187
- | `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`. |
3188
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. |
3189
3199
 
3190
3200
  Omni republishes the report whenever it changes — a permission granted late, a headset unplugged,
@@ -3258,8 +3268,8 @@ carries as `audio.input.localAudio`, and absent while that input is `unavailable
3258
3268
  bridges audio without a host-side input may ignore it; one that needs it and finds it absent
3259
3269
  answers `unavailable` with a failure Omni shows the agent.
3260
3270
 
3261
- **When to ask is the provider's word, not Omni's guess.** Omni opens media on `task-media-ready`,
3262
- 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
3263
3273
  when the task ends. Between those words, nothing Omni's own senses report — a stream that drops, a
3264
3274
  track that ends — moves the task or its audio.
3265
3275
 
@@ -3336,7 +3346,7 @@ Applies a `TaskCommandRequest` to one provider-local task.
3336
3346
  - `applied` confirms the side effect completed. `failed` confirms it did **not**, with a typed
3337
3347
  `ProtocolFailure`; a provider that will not and one that cannot report the same shape, and `code`
3338
3348
  says which.
3339
- - 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`.
3340
3350
  Neither Omni nor the adapter queues it.
3341
3351
  - **A command that asks for a state answers `applied` when that state holds, whoever brought it
3342
3352
  about; a command that acts answers `failed` when it cannot act.** Declining a lead request
@@ -3374,7 +3384,7 @@ react rather than only display the message:
3374
3384
  | `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
3375
3385
  | `omni.destination-not-permitted` | The dial or transfer destination violates the provider's policy. |
3376
3386
  | `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
3377
- | `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`. |
3378
3388
  | `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
3379
3389
 
3380
3390
  They are published as `OMNI_FAILURE_CODES`.
@@ -3386,14 +3396,14 @@ They are published as `OMNI_FAILURE_CODES`.
3386
3396
  | Field | Contract |
3387
3397
  | --- | --- |
3388
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. |
3389
- | `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. |
3390
3400
  | `occurredAt` | Valid RFC-3339 timestamp with an explicit timezone, representing provider observation time. |
3391
3401
  | `event` | Typed `ProviderEvent` payload. |
3392
3402
 
3393
3403
  #### Provider instants are read against a provider clock
3394
3404
 
3395
3405
  Every deadline in this contract is a provider instant that Omni counts down: `allocationExpiresAt`,
3396
- `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
3397
3407
  one is stated.
3398
3408
  Comparing those against the host clock is wrong by whatever the two machines disagree by, and the
3399
3409
  damaging direction is early — **Accept** withdrawn from an offer still ringing, a wrap timer
@@ -3427,8 +3437,8 @@ Omni apply state the snapshot already superseded.
3427
3437
 
3428
3438
  #### Liveness
3429
3439
 
3430
- `provider-status` is the only signal Omni has that a transport died. An adapter must emit
3431
- `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
3432
3442
  leaving a stale `active` in place while it retries internally; Omni cannot distinguish a quiet
3433
3443
  healthy provider from a dead one.
3434
3444
 
@@ -3448,9 +3458,9 @@ snapshot. It carries what the login's capabilities call for — a roster for a l
3448
3458
  snapshot — and nothing they do not; a capability is withdrawn by a republished `authenticated`,
3449
3459
  never by an omission from a snapshot.
3450
3460
 
3451
- ### `provider-status`
3461
+ ### `transport-status`
3452
3462
 
3453
- 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
3454
3464
  safe for the agent to see.
3455
3465
 
3456
3466
  | Value | Contract |
@@ -3464,7 +3474,7 @@ died; the host knows how to run a login. `recovery` joins the two:
3464
3474
 
3465
3475
  - **`reconnect`** — the login is good and this connection is not: a backend restart, a session the
3466
3476
  platform no longer recognises. Omni calls `disconnect()` on the dead connection and then
3467
- `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
3468
3478
  re-establishes state exactly as a reconnect snapshot does. `connect()` is once per
3469
3479
  `Connection`, not once per login.
3470
3480
  - **`reauthenticate`** — the session under the login died: a token rejected, a remote logout. Omni
@@ -3476,7 +3486,7 @@ has to decide when to stop. Omni owns giving up: after however long it chooses t
3476
3486
  call `disconnect()` and either `connect()` afresh or surface the failure — so neither side waits
3477
3487
  for the other to blink.
3478
3488
 
3479
- **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,
3480
3490
  whether they are on a break, or how much work they can take: capacity travels on `setCapacity`,
3481
3491
  availability on `BreakState`. Nor does it carry authentication — a session that expired reports
3482
3492
  `expired` on `AuthenticationState` and fails actions with `omni.not-authenticated`, while
@@ -3491,7 +3501,7 @@ come — see **Liveness**.
3491
3501
 
3492
3502
  Replaces this provider's complete `break` object. Its `approval` uses the canonical
3493
3503
  `not-requested`, `awaiting-decision`, `granted`, `starting-after-task` and
3494
- `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,
3495
3505
  reasons, retry details, and any imposed break.
3496
3506
 
3497
3507
  Each state is also held to the one before it. A commit's states, `starting-after-task` and
@@ -3524,17 +3534,17 @@ snapshots until it ends.
3524
3534
  Replaces the current representation of one provider-local task. It is a full task value, not a
3525
3535
  partial patch.
3526
3536
 
3527
- ### `task-media-ready`
3537
+ ### `task-media-started`
3528
3538
 
3529
3539
  The provider's word that the task's audio should now attach. Omni calls `openMedia` on it — and on
3530
- 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
3531
3541
  earlier event brought — and renders the call as live from that word, never from its own senses. It
3532
- names a task whose work has begun, and it alternates with `task-media-ended`: media that was never
3533
- 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
3534
3544
  breach, not a state a desk fills in from its own devices.
3535
3545
 
3536
3546
  The event is the transition and the task's `media` field is the state. A `task-updated` re-states
3537
- the media its task already holds — republishing `ready` on a hold is a statement, not a second
3547
+ the media its task already holds — republishing `started` on a hold is a statement, not a second
3538
3548
  arrival — but it does not move it: an update that itself flips the field is refused
3539
3549
  (`stream.taskUpdated.media`), and the pairing at the moment audio arrives is the phase change
3540
3550
  without the field, then the event. Releasing `ended` is the one move an update may make, since
@@ -3543,8 +3553,8 @@ wrapped audio has nothing left to end.
3543
3553
  ### `task-media-ended`
3544
3554
 
3545
3555
  Signals that a task's real-time media ended. For voice and similar channels, this starts the fixed
3546
- completion timer. It does not remove the task, and it ends only audio that `task-media-ready` — or
3547
- 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.
3548
3558
 
3549
3559
  ### `task-ended`
3550
3560
 
@@ -3570,7 +3580,7 @@ Publishes an agent-facing message. `text` is always required and is the accessib
3570
3580
  Optional HTML is sanitized by Omni. `announcedAt` and optional `expiresAt` are RFC-3339 times with
3571
3581
  explicit timezones.
3572
3582
 
3573
- ### `provider-summary`
3583
+ ### `queue-summary`
3574
3584
 
3575
3585
  Publishes the provider's current dashboard contribution. Omni combines only the latest summary from
3576
3586
  each connected provider.
@@ -3632,7 +3642,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
3632
3642
  | Function | Validates |
3633
3643
  | --- | --- |
3634
3644
  | `validateManifest(manifest)` | Identity, protocol-version interoperability, authentication methods, and idle-capability shapes. |
3635
- | `validateTask(task, { channel })` | Identity, channel agreement, phase, completion allowance, capability shapes, custom controls, and browsers. |
3645
+ | `validateTask(task, { channel })` | Identity, channel agreement, phase, wrap allowance, capability shapes, custom controls, and browsers. |
3636
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. |
3637
3647
  | `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
3638
3648
  | `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
@@ -3727,12 +3737,12 @@ cannot be established from TypeScript structure alone.
3727
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. |
3728
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. |
3729
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. |
3730
- | `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. |
3731
3741
  | `assertBreakParticipants(candidates, participants)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
3732
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. |
3733
3743
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
3734
3744
  | `assertWrapTimeout(task, mediaEndedAt, deadline, toleranceMs?)` | The wrap deadline equals media end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |
3735
- | `assertBrowserIsolationAndReuse(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
3745
+ | `assertBrowserSessionIsolation(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
3736
3746
  | `assertNoBrowserSessionKeyCollisions(scenarios)` | No two distinct scenarios derive the same session key. Feed it adversarial names. |
3737
3747
 
3738
3748
  Adapters should run the relevant scenarios against deterministic test state before publishing.