@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/README.md +1 -1
- package/dist/index.d.ts +78 -72
- package/dist/index.js +7 -7
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +27 -27
- package/dist/validation.d.ts +10 -10
- package/dist/validation.js +97 -100
- package/guide.md +160 -150
- package/package.json +1 -1
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 `
|
|
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
|
-
|
|
40
|
-
one
|
|
41
|
-
|
|
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
|
-
| `
|
|
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
|
|
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:
|
|
157
|
+
access: BrowserAccess;
|
|
157
158
|
accessPolicyScope?: "initial-url" | "all-navigation";
|
|
158
159
|
};
|
|
159
160
|
|
|
160
|
-
type
|
|
161
|
+
type DialDestinations = "contacts-only" | "any-number";
|
|
161
162
|
|
|
162
|
-
type DialCapability = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
240
|
-
| { status: "refreshing"; identity: User; capabilities:
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
339
|
+
type SetBy = Level | "provider";
|
|
340
340
|
|
|
341
341
|
type Resolved = {
|
|
342
342
|
setBy: SetBy;
|
|
343
|
-
lockedBy?:
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 |
|
|
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
|
|
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 =
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
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";
|
|
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";
|
|
551
|
-
| { completionMode: "provider-automatic";
|
|
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
|
-
|
|
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
|
|
576
|
+
type Level = string;
|
|
573
577
|
|
|
574
|
-
type
|
|
575
|
-
id:
|
|
578
|
+
type LevelDeclaration = {
|
|
579
|
+
id: Level;
|
|
576
580
|
label: string;
|
|
577
581
|
};
|
|
578
582
|
|
|
579
583
|
type Locked = {
|
|
580
|
-
lockedBy:
|
|
584
|
+
lockedBy: Level;
|
|
581
585
|
reason?: string;
|
|
582
586
|
};
|
|
583
587
|
|
|
584
588
|
type Lockable<T> = T | Locked;
|
|
585
589
|
|
|
586
|
-
type TaskMediaState = "
|
|
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
|
-
|
|
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
|
|
709
|
-
are the provider's ladder: each
|
|
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
|
-
|
|
712
|
-
never describes the chain between them: which
|
|
713
|
-
know. A typical organisation has four, and they are the defaults — `
|
|
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.
|
|
716
|
-
leaves out does not exist — a structure with no site
|
|
717
|
-
`site` is refused on its wire. A declared
|
|
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
|
|
720
|
-
`setBy` is any
|
|
721
|
-
anything and the provider's own configuration supplied the value"
|
|
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
|
|
727
|
-
their own value, and the queue is not a
|
|
728
|
-
- **`setBy`** — who stated the value as it stands: a
|
|
729
|
-
`
|
|
730
|
-
broad
|
|
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
|
|
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
|
|
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, `"
|
|
757
|
-
is hidden for want of a row, and a preference a
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
928
|
+
type TransportRecovery = "reconnect" | "reauthenticate";
|
|
926
929
|
|
|
927
930
|
type ProviderEvent =
|
|
928
931
|
| { type: "snapshot"; reason: "reconnected" | "provider-requested"; snapshot: Snapshot }
|
|
929
|
-
| { type: "
|
|
930
|
-
| { type: "
|
|
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-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
|
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** — `
|
|
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 `
|
|
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: {
|
|
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
|
-
| `
|
|
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: {
|
|
1431
|
+
dial: { destinations: "any-number" }
|
|
1429
1432
|
}
|
|
1430
1433
|
```
|
|
1431
1434
|
|
|
1432
|
-
`
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
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 `
|
|
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 `
|
|
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 **`
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
1849
|
-
| `
|
|
1850
|
-
| `break` | Complete break state, including approval,
|
|
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
|
-
| `
|
|
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: `
|
|
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
|
-
| `
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
`
|
|
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 |
|
|
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
|
-
|
|
2170
|
-
} satisfies Pick<Task<"email">, "completionMode" | "
|
|
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** `
|
|
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" | "
|
|
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
|
|
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 `
|
|
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
|
-
|
|
2225
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "
|
|
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
|
-
|
|
2237
|
-
} satisfies Pick<Task, "completionMode" | "
|
|
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
|
-
| `
|
|
2323
|
-
| `isolationScheme` | **Required when `
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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: {
|
|
2554
|
-
{ id: "mark-vip", ui: {
|
|
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.
|
|
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 `
|
|
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
|
-
| `
|
|
2611
|
-
| `refusedReason` | Display-ready reason shown when `
|
|
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
|
-
`
|
|
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
|
|
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 `
|
|
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: {
|
|
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: {
|
|
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-
|
|
3262
|
-
and on a task arriving with `media: "
|
|
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 `
|
|
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 `
|
|
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
|
-
| `
|
|
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 `
|
|
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
|
-
`
|
|
3431
|
-
`
|
|
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
|
-
### `
|
|
3461
|
+
### `transport-status`
|
|
3452
3462
|
|
|
3453
|
-
Updates `
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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-
|
|
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: "
|
|
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
|
|
3533
|
-
|
|
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 `
|
|
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-
|
|
3547
|
-
a task carried with `media: "
|
|
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
|
-
### `
|
|
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,
|
|
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-
|
|
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
|
-
| `
|
|
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.
|