@xema/omni-protocol 0.1.66 → 0.1.68
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/dist/index.d.ts +36 -3
- package/dist/index.js +2 -2
- package/dist/testing.d.ts +20 -1
- package/dist/testing.js +233 -22
- package/dist/validation.d.ts +16 -2
- package/dist/validation.js +149 -22
- package/guide.md +103 -36
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -97,6 +97,7 @@ the semantic name in every contract field rather than repeating the primitive ty
|
|
|
97
97
|
type IsoTimestamp = string;
|
|
98
98
|
type UserId = string;
|
|
99
99
|
type TaskId = string;
|
|
100
|
+
type AllocationId = string;
|
|
100
101
|
type DialId = string;
|
|
101
102
|
type DurationSeconds = number;
|
|
102
103
|
```
|
|
@@ -314,7 +315,7 @@ type LoginStore = {
|
|
|
314
315
|
type ConnectContext = {
|
|
315
316
|
protocolVersion: number;
|
|
316
317
|
loginId: string;
|
|
317
|
-
autoAcceptTasks
|
|
318
|
+
autoAcceptTasks: boolean;
|
|
318
319
|
timeZone: TimeZone;
|
|
319
320
|
store: LoginStore;
|
|
320
321
|
phone?: Phone;
|
|
@@ -396,7 +397,7 @@ type SetPreferenceRequest =
|
|
|
396
397
|
| { id: PreferenceId; enabled: boolean }
|
|
397
398
|
| { id: PreferenceId; inherit: true };
|
|
398
399
|
|
|
399
|
-
type HandlingReport = { taskId: TaskId; at: IsoTimestamp } & (
|
|
400
|
+
type HandlingReport = { taskId: TaskId; allocationId: AllocationId; at: IsoTimestamp } & (
|
|
400
401
|
| { step: "muted"; mutedBy: MutedBy }
|
|
401
402
|
| { step: Exclude<HandlingStep, "muted">; mutedBy?: never }
|
|
402
403
|
) & (
|
|
@@ -552,6 +553,7 @@ type PersonalBrowser = Browser;
|
|
|
552
553
|
type BrowserSessionKeyInput = {
|
|
553
554
|
providerId: string;
|
|
554
555
|
taskId: TaskId;
|
|
556
|
+
allocationId: AllocationId;
|
|
555
557
|
taskType: string;
|
|
556
558
|
browser: TaskBrowser;
|
|
557
559
|
};
|
|
@@ -621,7 +623,8 @@ type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
|
|
|
621
623
|
type OnCallStage = "ringing" | "joined";
|
|
622
624
|
|
|
623
625
|
type OnCall = { since: IsoTimestamp; held?: true } & (
|
|
624
|
-
| { role: "party" }
|
|
626
|
+
| { role: "party"; dialId?: never; stage?: never }
|
|
627
|
+
| { role: "party"; stage: OnCallStage; dialId?: DialId }
|
|
625
628
|
| { role: "agent"; userId: UserId }
|
|
626
629
|
| { role: "consulted" | "conferenced"; destinationId: string; stage: OnCallStage; dialId?: DialId; label?: string }
|
|
627
630
|
);
|
|
@@ -642,6 +645,7 @@ type TaskAssisting = {
|
|
|
642
645
|
type TaskMonitoring = {
|
|
643
646
|
memberId: UserId;
|
|
644
647
|
taskId: TaskId;
|
|
648
|
+
allocationId: AllocationId;
|
|
645
649
|
mode: MonitorMode;
|
|
646
650
|
since: IsoTimestamp;
|
|
647
651
|
};
|
|
@@ -666,6 +670,7 @@ type CapabilitySource = "queue" | "ungoverned" | "undetermined";
|
|
|
666
670
|
|
|
667
671
|
type Task<C extends Channel = Channel> = {
|
|
668
672
|
id: TaskId;
|
|
673
|
+
allocationId: AllocationId;
|
|
669
674
|
title: string;
|
|
670
675
|
channel: C;
|
|
671
676
|
taskType: string;
|
|
@@ -771,6 +776,7 @@ type TaskCommand<C extends Channel = Channel> =
|
|
|
771
776
|
|
|
772
777
|
type TaskCommandRequest<C extends Channel = Channel> = {
|
|
773
778
|
taskId: TaskId;
|
|
779
|
+
allocationId: AllocationId;
|
|
774
780
|
command: TaskCommand<C>;
|
|
775
781
|
};
|
|
776
782
|
|
|
@@ -923,6 +929,7 @@ type LeadRequest = {
|
|
|
923
929
|
id: string;
|
|
924
930
|
memberId: UserId;
|
|
925
931
|
taskId: TaskId;
|
|
932
|
+
allocationId: AllocationId;
|
|
926
933
|
note?: string;
|
|
927
934
|
since: IsoTimestamp;
|
|
928
935
|
};
|
|
@@ -1002,6 +1009,7 @@ type OpenMediaResult =
|
|
|
1002
1009
|
|
|
1003
1010
|
type OpenMediaRequest = {
|
|
1004
1011
|
taskId: TaskId;
|
|
1012
|
+
allocationId: AllocationId;
|
|
1005
1013
|
localAudio?: MediaStream;
|
|
1006
1014
|
};
|
|
1007
1015
|
```
|
|
@@ -1032,13 +1040,13 @@ type ProviderEvent =
|
|
|
1032
1040
|
allocationExpiresAt?: IsoTimestamp;
|
|
1033
1041
|
}
|
|
1034
1042
|
| { type: "task-updated"; task: Task }
|
|
1035
|
-
| { type: "task-media-started"; taskId: TaskId }
|
|
1036
|
-
| { type: "task-media-ended"; taskId: TaskId }
|
|
1037
|
-
| { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
|
|
1038
|
-
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId; destinationId?: string; reason?: string }
|
|
1043
|
+
| { type: "task-media-started"; taskId: TaskId; allocationId: AllocationId }
|
|
1044
|
+
| { type: "task-media-ended"; taskId: TaskId; allocationId: AllocationId }
|
|
1045
|
+
| { type: "task-ended"; taskId: TaskId; allocationId: AllocationId; outcome: TaskOutcome }
|
|
1046
|
+
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId; allocationId?: AllocationId; destinationId?: string; reason?: string }
|
|
1039
1047
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
1040
1048
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
1041
|
-
| { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
|
|
1049
|
+
| { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId; allocationId?: AllocationId }
|
|
1042
1050
|
| { type: "team-updated"; team: TeamRoster }
|
|
1043
1051
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
1044
1052
|
| { type: "calendar-updated"; scheduledActivities: ScheduledActivity[] };
|
|
@@ -1943,7 +1951,7 @@ Creates one live provider connection for the signed-in agent.
|
|
|
1943
1951
|
| --- | --- |
|
|
1944
1952
|
| `protocolVersion` | Version negotiated before authentication. Fixed for this login. |
|
|
1945
1953
|
| `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. |
|
|
1946
|
-
| `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login
|
|
1954
|
+
| `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login, stated by the host on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
1947
1955
|
| `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
|
|
1948
1956
|
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's host states what its Mute does, and a desk-phone or conversation login's host cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the host's**. |
|
|
1949
1957
|
| `store` | The login's operational store, kept by the host for the life of the login, across a reload of the host, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the handling legs a host reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store a host may clear aggressively. **A task's keys carry the task id and go with the task**: a key written about a task names the task's id in the key, and is deleted before the task's end is published, because a platform retires a task id minutes after closing it and a requeue takes seconds, so a key that outlives its task is inherited by the next offer of the same id -- a record with legs the host never reported against it. A login-scoped key carries no task id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the host's, not the adapter's: a host reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.task-not-found` and writes nothing -- the host ends its own open legs at the task's end, so such a report is the host's error to see, and a host reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
@@ -2144,12 +2152,11 @@ provider's requirement, stated on a wire where Omni was willing to accept for th
|
|
|
2144
2152
|
no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
|
|
2145
2153
|
press is Omni's doing, not the provider's.
|
|
2146
2154
|
|
|
2147
|
-
**
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
it: `consent` puts the decision back in the agent's hands for any task where it
|
|
2155
|
+
**The value is stated, never assumed.** `autoAcceptTasks` is required of every connection: a
|
|
2156
|
+
host says which policy the deployment provisioned, and a validator that is not told checks
|
|
2157
|
+
neither rule rather than guess the permissive one (`task.acceptance.required` and
|
|
2158
|
+
`.unexpected` fire only against a stated value). `acceptance` is the provider's own control and
|
|
2159
|
+
outranks the policy: `consent` puts the decision back in the agent's hands for any task where it
|
|
2153
2160
|
belongs, whatever the host was configured with.
|
|
2154
2161
|
|
|
2155
2162
|
An automatically accepted task still arrives through `task-offered`.
|
|
@@ -2221,7 +2228,8 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2221
2228
|
|
|
2222
2229
|
| Field | Contract |
|
|
2223
2230
|
| --- | --- |
|
|
2224
|
-
| `id` | Required `TaskId`,
|
|
2231
|
+
| `id` | Required `TaskId`, the platform's own identity for the task. Unique among the tasks open at once; a platform retires an id minutes after closing it, so the same id comes back for another customer. Omni scopes it with the provider ID. |
|
|
2232
|
+
| `allocationId` | Required `AllocationId`: this life of the task, minted once per offer and never reused for the life of the login, whatever the id does. Every event, command and report that names a task names its allocation too, so a late dial outcome or a late handling report for the first customer never lands on the next under the same id. The stream refuses an offer reusing one (`stream.taskOffered.allocation`), an update restating another life of the id (`stream.taskUpdated.allocation`), and a media or ending event naming a life that has ended or that nobody has seen (`stream.allocation.ended`, `.unknown`); a `dial-outcome` may name an ended life, since a dial placed late routinely outlives its call, and the host routes it there. See **A task's life on the wire**. |
|
|
2225
2233
|
| `title` | Agent-facing task title. |
|
|
2226
2234
|
| `channel` | Channel handling this task. It must equal the source provider's manifest channel. |
|
|
2227
2235
|
| `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
|
|
@@ -2230,7 +2238,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2230
2238
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2231
2239
|
| `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. |
|
|
2232
2240
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2233
|
-
| `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`**. |
|
|
2241
|
+
| `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. Media names a task whose work has begun: on a `pending`, `confirmed` or `preview` task it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it. |
|
|
2234
2242
|
| `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
|
|
2235
2243
|
| `previewEndsAt` | Voice only, in `preview`: when the system stops waiting for the agent to press Call. Absent, the agent has as long as they need. Always with `atDeadline`. See **Preview: the agent presses Call**. |
|
|
2236
2244
|
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` places the call itself, `expires` takes the record back and the task ends `expired`. |
|
|
@@ -2239,7 +2247,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2239
2247
|
| `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. |
|
|
2240
2248
|
| `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. |
|
|
2241
2249
|
| `handlingHistory` | The call record: `steps` — the ordered handling history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `handleSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **How a task has been handled**. |
|
|
2242
|
-
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where a host placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2250
|
+
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer -- carrying a `stage` while being dialled again on the same task, a connect-back with the host's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where a host placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2243
2251
|
| `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
|
|
2244
2252
|
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead-assist` `take-over` and `leave` issuable. See **Lead assist**. |
|
|
2245
2253
|
| `monitoring` | Voice only, on the lead's own task while they listen to a member's call: whose call, which call, and the `mode` they are heard in, restated on every change. Never on the member's task, and never together with `assisting`. See **Monitoring a call**. |
|
|
@@ -2287,6 +2295,24 @@ The canonical task transitions are:
|
|
|
2287
2295
|
| `completing` | Agent connects back to the party (`connect-back`) | `in-progress` |
|
|
2288
2296
|
| Any phase | Provider emits `task-ended` | Removed |
|
|
2289
2297
|
|
|
2298
|
+
#### A task's life on the wire
|
|
2299
|
+
|
|
2300
|
+
A task id is the platform's, and platforms reuse them: a closed id is retired minutes later, a
|
|
2301
|
+
requeue takes seconds, and on one platform a call was offered twice, thirteen seconds apart, under
|
|
2302
|
+
one id, through a close and a re-offer. Everything that names a task after the fact -- a dial
|
|
2303
|
+
outcome, a media event, an ending, a handling report, a command -- would land on whichever life of
|
|
2304
|
+
the id is open when it arrives. So every offer mints an `allocationId`, unique for the life of the
|
|
2305
|
+
login, and every one of those names it beside the `taskId`. The host keys its state on the pair, a
|
|
2306
|
+
late event finds the life it belongs to or is refused, and a task-scoped browser session
|
|
2307
|
+
(`PROVIDER_NAME__TASK_ID__TAB_NAME`) lives one allocation, never the next customer's cookies under
|
|
2308
|
+
a reused id. A dial the host placed was already safe, since its outcome is placed by the host's own
|
|
2309
|
+
`dialId`; where the allocation earns its place is everything the host does not mint --
|
|
2310
|
+
`task-media-started`, `task-media-ended`, `task-ended`, a `recordStep` naming a task -- any of which
|
|
2311
|
+
would otherwise find the new task under the old id and act on it. The allocation is part of the task
|
|
2312
|
+
the adapter keeps, not a field held in memory beside it: a rebuilt adapter comes back with the same
|
|
2313
|
+
allocation on the same task, or every late event it was minted to catch mismatches
|
|
2314
|
+
(`drive.reload.allocation`).
|
|
2315
|
+
|
|
2290
2316
|
Allocation, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
2291
2317
|
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
2292
2318
|
subsequent transitions to `preview` or `in-progress`; Omni does not infer them from the acceptance
|
|
@@ -2422,6 +2448,16 @@ whose outcome says so and a call whose media ended: the task returns to `complet
|
|
|
2422
2448
|
same event and the clock starts again from there. At no point is an agent dialling against a
|
|
2423
2449
|
deadline.
|
|
2424
2450
|
|
|
2451
|
+
**The room shows the party being dialled again.** From the moment the connect-back is placed the
|
|
2452
|
+
`party` entry carries `stage: "ringing"` and the host's `dialId`, exactly as a consulted or
|
|
2453
|
+
conferenced entry does from its dial, and `joined` on the answered `dial-outcome` and never before
|
|
2454
|
+
it (`stream.taskUpdated.stage`); a host shows a call being placed, not a party it asserts is on a
|
|
2455
|
+
call that is still ringing. A callback the platform places itself on the same task -- the first
|
|
2456
|
+
call over, the platform dialling the party again without a command -- shows the same thing with no
|
|
2457
|
+
host `dialId`, as any platform-placed dial does. A party with no stage is on the call, and a dial
|
|
2458
|
+
with no stage is half a claim (`task.onCall.party.dial`). The stage is also what lets the stream
|
|
2459
|
+
tell a task coming back from one going backwards (`stream.taskUpdated.phase`).
|
|
2460
|
+
|
|
2425
2461
|
**The control exists only while there is a window to use it in.** Under `agent-command` the task
|
|
2426
2462
|
stays `completing` until the agent completes it, so the window is open for as long as they need.
|
|
2427
2463
|
Under `provider-automatic` the window is the allowance -- and with `wrapAllowance: 0` there
|
|
@@ -2500,7 +2536,10 @@ entries, one per join. Order is enforced: an entry earlier than the one before i
|
|
|
2500
2536
|
total the provider adds: *time to offer*, from the call's arrival to the agent's screen lighting
|
|
2501
2537
|
up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
|
|
2502
2538
|
`queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
|
|
2503
|
-
that interval.
|
|
2539
|
+
that interval. **The second is the record's grain.** A duration is a positive whole number of
|
|
2540
|
+
seconds, and nought is refused because it claims no time; a leg that happened is stated at the
|
|
2541
|
+
grain: rounded to the nearest second, and a leg that rounds to nought is stated as `1`, the error
|
|
2542
|
+
being under the contract's resolution, where dropping the entry would say the leg never happened.
|
|
2504
2543
|
|
|
2505
2544
|
**A record once read is not unread.** Every restatement of a task's record -- on `task-updated`,
|
|
2506
2545
|
on a resync snapshot -- carries every entry the host has already read, by step and instant, and
|
|
@@ -2590,11 +2629,12 @@ writes it into its record as it writes every other leg:
|
|
|
2590
2629
|
```ts
|
|
2591
2630
|
declare const connection: Connection<"voice">;
|
|
2592
2631
|
declare const taskId: TaskId;
|
|
2632
|
+
declare const allocationId: AllocationId;
|
|
2593
2633
|
declare const at: IsoTimestamp;
|
|
2594
2634
|
|
|
2595
|
-
void connection.recordStep?.({ taskId, step: "muted", at, mutedBy: "host" }); // the moment the agent mutes
|
|
2596
|
-
void connection.recordStep?.({ taskId, step: "muted", at, mutedBy: "host", seconds: 15 }); // still running, if the host chooses to say
|
|
2597
|
-
void connection.recordStep?.({ taskId, step: "muted", at, mutedBy: "host", seconds: 42, ended: true }); // the moment they unmute
|
|
2635
|
+
void connection.recordStep?.({ taskId, allocationId, step: "muted", at, mutedBy: "host" }); // the moment the agent mutes
|
|
2636
|
+
void connection.recordStep?.({ taskId, allocationId, step: "muted", at, mutedBy: "host", seconds: 15 }); // still running, if the host chooses to say
|
|
2637
|
+
void connection.recordStep?.({ taskId, allocationId, step: "muted", at, mutedBy: "host", seconds: 42, ended: true }); // the moment they unmute
|
|
2598
2638
|
```
|
|
2599
2639
|
|
|
2600
2640
|
The entry is keyed by `step` and `at`, so every report about one leg names the same instant. The
|
|
@@ -2711,7 +2751,7 @@ The supported `BrowserIsolationScheme` values, declared under **Shapes**, key as
|
|
|
2711
2751
|
|
|
2712
2752
|
| Enum member | Example session key |
|
|
2713
2753
|
| --- | --- |
|
|
2714
|
-
| `PROVIDER_NAME__TASK_ID__TAB_NAME` | `mailflow.EMAIL-829102.CRM` |
|
|
2754
|
+
| `PROVIDER_NAME__TASK_ID__TAB_NAME` | `mailflow.EMAIL-829102%2Ea1.CRM` -- the task's allocation, so one life of the id, never the next customer's cookies |
|
|
2715
2755
|
| `TAB_NAME` | `CRM` |
|
|
2716
2756
|
| `PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME` | `mailflow.Support.CRM` |
|
|
2717
2757
|
| `PROVIDER_NAME__TAB_NAME` | `mailflow.CRM` |
|
|
@@ -2795,7 +2835,17 @@ Terms once read stay read: a re-read that fails mid-task is not a new fact about
|
|
|
2795
2835
|
last statement stands and the failure is a `diagnostic`, and a task that was published under
|
|
2796
2836
|
`queue` or `ungoverned` never returns to `undetermined`, on an update (`stream.taskUpdated.capabilitySource`)
|
|
2797
2837
|
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
2798
|
-
entry the same two ways (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`)
|
|
2838
|
+
entry the same two ways (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`);
|
|
2839
|
+
and a task does not go backwards (`stream.taskUpdated.phase`): the stream sees publications, not
|
|
2840
|
+
transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
|
|
2841
|
+
with `confirmed` between them, and what is refused is a phase unreachable from the last one read by
|
|
2842
|
+
the transition table -- back to `pending`, back to `confirmed` or `preview` once work began, out of
|
|
2843
|
+
`completing` except by the party being dialled again, a connect-back or a platform's callback,
|
|
2844
|
+
which the update itself shows: the party on the call carrying a `stage`. A completing task
|
|
2845
|
+
republished as `in-progress` from a stale copy carries no such stage, and that is the ending the
|
|
2846
|
+
agent never saw. Audio arrives only on a task at work: `task-media-started` on a `completing` task
|
|
2847
|
+
is refused as it is on a pending one (`stream.taskMediaStarted.beforeWork`), since a connect-back
|
|
2848
|
+
returns the task to `in-progress` before any media.
|
|
2799
2849
|
|
|
2800
2850
|
What the agent is told differs by source, and only one source tells them anything. Under `queue`
|
|
2801
2851
|
and `ungoverned` the agent sees controls and nothing about where they came from: both are facts,
|
|
@@ -2850,8 +2900,10 @@ See **Which commands need a capability**.
|
|
|
2850
2900
|
|
|
2851
2901
|
### Publishing codes and destinations
|
|
2852
2902
|
|
|
2853
|
-
Four capabilities accept an object
|
|
2854
|
-
|
|
2903
|
+
Four capabilities accept an object when the provider wants Omni to render real choices. For
|
|
2904
|
+
`dispositions` alone, `true` remains valid and means "offer the control with nothing published";
|
|
2905
|
+
the three directory controls -- `coldTransfer`, `warmTransfer`, `conference` -- carry their
|
|
2906
|
+
directory or are refused, since once nothing is typed the directory is the control.
|
|
2855
2907
|
|
|
2856
2908
|
#### `dispositions`
|
|
2857
2909
|
|
|
@@ -3060,15 +3112,16 @@ across the installation, and it travels four ways so no leg is a lookup:
|
|
|
3060
3112
|
```ts
|
|
3061
3113
|
declare const connection: Connection<"voice">;
|
|
3062
3114
|
declare const taskId: TaskId;
|
|
3115
|
+
declare const allocationId: AllocationId;
|
|
3063
3116
|
|
|
3064
3117
|
// 1. Out, minted by the host.
|
|
3065
|
-
const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
|
|
3118
|
+
const result = await connection.execute({ taskId, allocationId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
|
|
3066
3119
|
|
|
3067
3120
|
// 2. Back on the result, restated, so the host compares and confirms.
|
|
3068
3121
|
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
3069
3122
|
|
|
3070
3123
|
// 3. On the outcome, however late, against the task it belonged to.
|
|
3071
|
-
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
3124
|
+
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, allocationId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
3072
3125
|
|
|
3073
3126
|
// 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
|
|
3074
3127
|
const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
|
|
@@ -3698,7 +3751,7 @@ choice that is no command at all:
|
|
|
3698
3751
|
|
|
3699
3752
|
| The lead | The agent's task | The lead's task |
|
|
3700
3753
|
| --- | --- | --- |
|
|
3701
|
-
| `{ type: "lead-assist", action: "take-over" }` | `task-ended` with `{ type: "transferred",
|
|
3754
|
+
| `{ type: "lead-assist", action: "take-over" }` | `task-ended` with `{ type: "transferred", destinationId: leadId }`, straight from `in-progress`: **no `completing` window**, the agent is idle at once | Continues alone, and ends as any call does |
|
|
3702
3755
|
| `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
3703
3756
|
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own disposition | The same, independently: **both have the disposal window** |
|
|
3704
3757
|
|
|
@@ -3744,7 +3797,7 @@ executeTeamMonitor({ command: { type: "monitor", memberId: "A-1" } })
|
|
|
3744
3797
|
|
|
3745
3798
|
// 2. The lead's own task arrives -- task-offered with `automatic`, as a joined call does -- and
|
|
3746
3799
|
// carries `monitoring`; the task id is the lead's, the member's call is named inside it.
|
|
3747
|
-
// monitoring: { memberId: "A-1", taskId: "call-42", mode: "monitor", since }
|
|
3800
|
+
// monitoring: { memberId: "A-1", taskId: "call-42", allocationId: "alloc-42", mode: "monitor", since }
|
|
3748
3801
|
|
|
3749
3802
|
// 3. The lead changes how they are heard; the provider restates the task with the new mode.
|
|
3750
3803
|
executeTeamMonitor({ command: { type: "whisper" } })
|
|
@@ -3781,7 +3834,7 @@ const monitoringLead = {
|
|
|
3781
3834
|
channel: "voice",
|
|
3782
3835
|
capabilities: {},
|
|
3783
3836
|
phase: "in-progress",
|
|
3784
|
-
monitoring: { memberId: "A-1", taskId: "call-42", mode: "whisper", since: "2026-08-21T09:04:00Z" },
|
|
3837
|
+
monitoring: { memberId: "A-1", taskId: "call-42", allocationId: "alloc-42", mode: "whisper", since: "2026-08-21T09:04:00Z" },
|
|
3785
3838
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "monitoring">;
|
|
3786
3839
|
```
|
|
3787
3840
|
|
|
@@ -4134,12 +4187,14 @@ declared:
|
|
|
4134
4187
|
| `conference` with `action: "remove"` | The `conference` capability, and somebody else on the call: a remove that would leave the agent alone is `end-call`, and a provider answers it `failed`. |
|
|
4135
4188
|
| `decline` | The `decline` capability on any channel, **and** Omni provisioning permitting it. One word for refusing an offer, whatever the channel. |
|
|
4136
4189
|
| `call` | The `preview` phase. A record put in front of an agent is there to be called, so the phase is the gate and there is no capability. It is a dial, with a `dialId` and a `dial-outcome`. |
|
|
4137
|
-
| `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. |
|
|
4190
|
+
| `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. What travels is what the capability published: a code from its list where it has one (`command.complete.disposition.unknown`), a code at all where it requires one (`.disposition.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no dispositions (`.disposition.unexpected`). |
|
|
4138
4191
|
| `connect-back` | The `connectBack` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
|
|
4139
4192
|
| `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
|
|
4140
4193
|
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
|
|
4141
4194
|
| `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
|
|
4142
4195
|
| `lead-assist` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
4196
|
+
| `transfer` with a destination, `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
|
|
4197
|
+
| `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying every field the control's `prompt` asked for (`command.custom.prompt`). |
|
|
4143
4198
|
| Everything else | Its own named capability. |
|
|
4144
4199
|
|
|
4145
4200
|
**A control on the contact belongs to the handling phases**, `in-progress` and `paused`:
|
|
@@ -4150,7 +4205,7 @@ call with nobody on it, a conversation closed -- and a wrap-up that still shows
|
|
|
4150
4205
|
for nothing. The capability stays declared, because it is a property of the task and the task is
|
|
4151
4206
|
still open; the phase says there is nothing to use it on. Omni shows none of these controls outside
|
|
4152
4207
|
the two phases, and `validateTaskCommand` refuses each of them there (`command.phase.handling`).
|
|
4153
|
-
The commands with a phase of their own -- `answer`, `accept
|
|
4208
|
+
The commands with a phase of their own -- `answer`, `accept` and `decline` in `pending`,
|
|
4154
4209
|
`call` in `preview`, `connect-back` in `completing`, `complete` in any -- are not among them.
|
|
4155
4210
|
|
|
4156
4211
|
`validateTaskCommand(command, task)` holds a command to this table at runtime, both ways: the
|
|
@@ -4411,7 +4466,8 @@ when a lead who joined it leaves -- see **Lead assist**.
|
|
|
4411
4466
|
A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`.
|
|
4412
4467
|
The `task-media-ended` event and the `completing` phase are likewise non-terminal. A replacement
|
|
4413
4468
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|
|
4414
|
-
same envelope ID is harmless
|
|
4469
|
+
same envelope ID is harmless, and a `task-ended` naming an allocation that has already ended is
|
|
4470
|
+
recognised as the late event it is, never applied to the life now open under the same id.
|
|
4415
4471
|
|
|
4416
4472
|
### `dial-outcome`
|
|
4417
4473
|
|
|
@@ -4448,7 +4504,7 @@ fails loudly rather than passing with a note. A task published under `capability
|
|
|
4448
4504
|
| --- | --- |
|
|
4449
4505
|
| `expected` | Required. The rule that was broken, as a sentence a person can read. |
|
|
4450
4506
|
| `observed` | Required. What the platform answered instead. |
|
|
4451
|
-
| `taskId` | Optional. The task concerned, where there is one. |
|
|
4507
|
+
| `taskId` | Optional. The task concerned, where there is one, with its `allocationId` beside it: a task is named with its life, and an allocation never alone. |
|
|
4452
4508
|
|
|
4453
4509
|
### `queue-summary`
|
|
4454
4510
|
|
|
@@ -4594,7 +4650,18 @@ complete it with a disposition where the agent completes -- and holds every step
|
|
|
4594
4650
|
host holds a provider to. Each command is validated against the task as published
|
|
4595
4651
|
(`drive.command.*`), each answer for its method, a refusal of a control the task offered is a
|
|
4596
4652
|
violation (`drive.command.failed`), and an event the provider owes and never sends is one too
|
|
4597
|
-
(`drive.timeout`, after `driveTimeoutMs`, 5000 by default).
|
|
4653
|
+
(`drive.timeout`, after `driveTimeoutMs`, 5000 by default). Around the drive the exercise holds
|
|
4654
|
+
the run to what a host holds a provider to between commands: an offer before the host stated
|
|
4655
|
+
capacity, or beyond the count with nothing dialled on it, is named
|
|
4656
|
+
(`stream.taskOffered.beforeCapacity`, `.overCapacity`); every user the snapshot names is looked up
|
|
4657
|
+
through `describeUsers` and the answer held to the shape, nobody unasked, nobody described as
|
|
4658
|
+
nothing (`describeUsers.user.*`, `describeUsers.unasked`, `connection.describeUsers.empty`); and
|
|
4659
|
+
the second adapter a `rebuild` gives comes up signed in as the same login from the secrets alone
|
|
4660
|
+
before it reads anything (`drive.reload.login`). The result also says which rules the
|
|
4661
|
+
run evaluated, pass or fail, in `rulesEvaluated`: the validators' as each was applied, the stream's
|
|
4662
|
+
as each case was considered, so a test that needs a rule to have run asserts it there rather than
|
|
4663
|
+
inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
|
|
4664
|
+
gap and not a pass. With the audio open on a softphone,
|
|
4598
4665
|
the drive mutes it for one second and reports the leg through `recordStep`, begun and then ended,
|
|
4599
4666
|
expecting each report `recorded` (`drive.recordStep.failed`, `.rejected`); and where the provider
|
|
4600
4667
|
restates the task's record afterwards, the leg is in it or the hole is named
|