@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/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?: boolean;
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. Treated as `true` when omitted. 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()`. |
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
- **An absent value means `true`**, as `readyOnLogin` does, because an agent who has signed in and
2148
- gone ready is telling the deployment they are working. Requiring a press before every contact is
2149
- the exception a provisioning file asks for, not the state it falls into when a flag is missing.
2150
-
2151
- Nothing is given away by that default. `acceptance` is the provider's own control and outranks
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`, unique within the provider. Omni scopes it with the provider ID. |
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 instead of `true` when the provider wants Omni to render real
2854
- choices. `true` remains valid and means "offer the control with nothing published".
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", destination: leadId }`, straight from `in-progress`: **no `completing` window**, the agent is idle at once | Continues alone, and ends as any call does |
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`, `decline` and `reject` in `pending`,
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). With the audio open on a softphone,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",