@xema/omni-protocol 0.1.32 → 0.1.33

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
@@ -35,6 +35,8 @@ are used precisely throughout and mean nothing looser here.
35
35
  | **Event** | One completed transaction reported after a snapshot established the baseline. |
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
+ | **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a transfer, a conference add, or a callback. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
39
+ | **On the call** | Who a voice task's audio joins right now, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in. |
38
40
 
39
41
  Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
40
42
  one thing, so a bare "status" in conversation is always the authentication session's, and a
@@ -94,6 +96,7 @@ the semantic name in every contract field rather than repeating the primitive ty
94
96
  type IsoTimestamp = string;
95
97
  type UserId = string;
96
98
  type TaskId = string;
99
+ type DialId = string;
97
100
  type DurationSeconds = number;
98
101
  ```
99
102
 
@@ -162,6 +165,8 @@ type DialDestinations = "contacts-only" | "any-number";
162
165
 
163
166
  type DialCapability = { destinations: DialDestinations };
164
167
 
168
+ type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected";
169
+
165
170
  type IdleCapabilities<C extends Channel = Channel> = {
166
171
  personalBrowser?: PersonalBrowserCapability;
167
172
  calendar?: true;
@@ -178,6 +183,7 @@ type Manifest<C extends Channel = Channel> = {
178
183
  phaseLabels?: TaskPhaseLabels;
179
184
  taskTypePresentation?: Record<string, TaskTypePresentation>;
180
185
  orgLevels?: LevelDeclaration[];
186
+ dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
181
187
  runningStepReports?: true;
182
188
  };
183
189
  ```
@@ -414,11 +420,12 @@ type ScheduledActivity = {
414
420
  };
415
421
 
416
422
  type DialRequest = {
423
+ dialId: DialId;
417
424
  destination: string;
418
425
  };
419
426
 
420
427
  type DialResult =
421
- | { status: "dialled" }
428
+ | { status: "dialling"; dialId: DialId }
422
429
  | { status: "failed"; failure: ProtocolFailure };
423
430
  ```
424
431
 
@@ -570,6 +577,8 @@ type TaskHandlingHistory = {
570
577
  type TaskHandlingStep = {
571
578
  step: HandlingStep;
572
579
  at: IsoTimestamp;
580
+ dialId?: DialId;
581
+ destination?: string;
573
582
  seconds?: DurationSeconds;
574
583
  by?: UserId;
575
584
  };
@@ -578,11 +587,13 @@ type TaskCompletion =
578
587
  | { completionMode: "agent-command"; wrapAllowance?: DurationSeconds }
579
588
  | { completionMode: "provider-automatic"; wrapAllowance: DurationSeconds };
580
589
 
581
- type TaskConsultation = {
582
- destination: string;
583
- label?: string;
584
- since?: IsoTimestamp;
585
- };
590
+ type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
591
+
592
+ type OnCall = { since: IsoTimestamp; held?: true } & (
593
+ | { role: "party" }
594
+ | { role: "agent"; userId: UserId }
595
+ | { role: "consulted" | "conferenced"; destination: string; dialId?: DialId; label?: string }
596
+ );
586
597
 
587
598
  type TaskLead = {
588
599
  stage: "requested" | "joined";
@@ -628,8 +639,8 @@ type Task<C extends Channel = Channel> = {
628
639
  handlingHistory?: TaskHandlingHistory;
629
640
  } & TaskCompletion & (
630
641
  C extends "voice"
631
- ? { consultation?: TaskConsultation; lead?: TaskLead; assisting?: TaskAssisting; media?: TaskMediaState }
632
- : { consultation?: never; lead?: never; assisting?: never; media?: never }
642
+ ? { onCall?: OnCall[]; lead?: TaskLead; assisting?: TaskAssisting; media?: TaskMediaState }
643
+ : { onCall?: never; lead?: never; assisting?: never; media?: never }
633
644
  );
634
645
 
635
646
  type AcceptanceMode =
@@ -682,16 +693,17 @@ type VoiceTaskCommand =
682
693
  | { type: "hold" }
683
694
  | { type: "resume" }
684
695
  | { type: "disconnect" }
685
- | { type: "callback" }
686
- | { type: "transfer"; destination: string; action?: never }
687
- | { type: "transfer"; action: "consult"; destination: string }
696
+ | { type: "callback"; dialId: DialId }
697
+ | { type: "transfer"; dialId: DialId; destination: string; action?: never }
698
+ | { type: "transfer"; action: "consult"; dialId: DialId; destination: string }
688
699
  | { type: "transfer"; action: "complete" }
689
700
  | { type: "transfer"; action: "cancel" }
690
701
  | { type: "lead"; action: "request"; note?: string }
691
702
  | { type: "lead"; action: "cancel" }
692
703
  | { type: "lead"; action: "take-over" }
693
704
  | { type: "lead"; action: "leave" }
694
- | { type: "conference"; participant: string; action: "add" | "remove" }
705
+ | { type: "conference"; action: "add"; dialId: DialId; destination: string }
706
+ | { type: "conference"; action: "remove"; destination: string }
695
707
  | { type: "recording"; action: "start" | "pause" | "resume" | "stop" }
696
708
  | ({ type: "complete" } & DispositionPayload);
697
709
 
@@ -720,6 +732,7 @@ type TaskCommandRequest<C extends Channel = Channel> = {
720
732
 
721
733
  type TaskCommandResult =
722
734
  | { status: "applied" }
735
+ | { status: "dialling"; dialId: DialId }
723
736
  | { status: "failed"; failure: ProtocolFailure };
724
737
  ```
725
738
 
@@ -967,6 +980,7 @@ type ProviderEvent =
967
980
  | { type: "task-media-started"; taskId: TaskId }
968
981
  | { type: "task-media-ended"; taskId: TaskId }
969
982
  | { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
983
+ | { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId; destination?: string; reason?: string }
970
984
  | { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
971
985
  | { type: "queue-summary"; summary: QueueSummary }
972
986
  | { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
@@ -1296,11 +1310,12 @@ already committed stops an agent who is already stopped. A command whose second
1296
1310
  cost — a dial places a second call — is not made safe by anyone, which is why it is never repeated
1297
1311
  without a person deciding to.
1298
1312
 
1299
- An agent dials. The provider places the call and the answer is lost. Omni shows the dial as
1300
- unknown, not failed. Within a moment the provider offers the resulting call through `task-offered`
1301
- and the agent is on it; had nothing been placed, nothing arrives and the agent dials again. What
1302
- Omni must not do is dial again on the agent's behalf — the one outcome worse than a lost answer is
1303
- two phones ringing at the customer.
1313
+ An agent dials. The provider places the call and the `dialling` answer is lost. Omni shows the dial
1314
+ as unknown, not failed. Within a moment the provider offers the resulting call through
1315
+ `task-offered` and reports the dial's `dial-outcome` under the `dialId` Omni sent, and the agent is
1316
+ on it; had nothing been placed, nothing arrives and the agent dials again. What Omni must not do is
1317
+ dial again on the agent's behalf — the one outcome worse than a lost answer is two phones ringing
1318
+ at the customer.
1304
1319
 
1305
1320
  An agent presses Hold while the provider is reconnecting. Nothing is queued at either end: the
1306
1321
  adapter answers `failed` with `omni.unavailable`, Omni shows the refusal, and the agent presses
@@ -1377,6 +1392,7 @@ export default defineAdapter({
1377
1392
  idleCapabilities: {
1378
1393
  dial: { destinations: "any-number" },
1379
1394
  },
1395
+ dialOutcomes: ["answered", "no-answer"],
1380
1396
  },
1381
1397
  createAuthenticationSession: context => createAcmeAuthentication(context),
1382
1398
  connect: context => createConnection(context),
@@ -1401,6 +1417,7 @@ compile time.
1401
1417
  | `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
1402
1418
  | `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
1403
1419
  | `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**. |
1420
+ | `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that transfer, conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
1404
1421
  | `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
1405
1422
 
1406
1423
  ### Authentication methods
@@ -1471,9 +1488,9 @@ aggregated from every provider, but a `contacts-only` dialpad offers only the en
1471
1488
  contributed. Any other reading lets a second provider put destinations into a directory the first
1472
1489
  restricted precisely so it could control what was dialled.
1473
1490
 
1474
- A declared dial capability requires `Connection.dial()`. Omni sends the original
1475
- destination and whether it came from a `contact` or `manual` entry. With `contacts-only`, Omni must
1476
- never send `source: "manual"`.
1491
+ A declared dial capability requires `Connection.dial()` and a `dialOutcomes` declaration, since a
1492
+ dialpad dials. Omni sends the destination as the agent selected or typed it, under a `dialId` of
1493
+ its own minting, and the provider holds it to the declared `destinations`.
1477
1494
 
1478
1495
  #### Personal Browser (`personalBrowser`)
1479
1496
 
@@ -1898,7 +1915,7 @@ surface in one place, and what obliges an adapter to implement each one.
1898
1915
  | `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
1899
1916
  | `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
1900
1917
  | `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. |
1901
- | `dial(request)` | The manifest declares `idleCapabilities.dial`. |
1918
+ | `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
1902
1919
  | `requestBreak(request)` | The login declares `capabilities.breaks`. |
1903
1920
  | `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
1904
1921
  | `cancelBreak()` | The login declares `capabilities.breaks`. |
@@ -2112,7 +2129,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2112
2129
  | `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. |
2113
2130
  | `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. |
2114
2131
  | `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**. |
2115
- | `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**. |
2132
+ | `onCall` | Voice only. Who is on the call right now, 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 brought in -- with the `destination` dialled, the `dialId` where a host placed it, and `held: true` on anyone 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**. |
2116
2133
  | `lead` | 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 **Consulting a lead**. |
2117
2134
  | `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` `take-over` and `leave` issuable. See **Consulting a lead**. |
2118
2135
 
@@ -2315,9 +2332,16 @@ duration is the `held` entry's `seconds`, and a desk that restarts its counter o
2315
2332
  counting the wrong thing.
2316
2333
 
2317
2334
  `muted` is there because Omni performs the mute rather than the provider — see **Where a command
2318
- executes** — so without a step the one participant that keeps the task's record would have no
2335
+ executes** — so without a step the provider, which alone keeps the task's record, would have no
2319
2336
  account of a period the agent could not be heard.
2320
2337
 
2338
+ **A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
2339
+ each end a dial, so the entry carries the `destination` it went to and, where a host placed it, the
2340
+ `dialId` the host minted; no other step dialled, and either field on one is refused
2341
+ (`task.handlingHistory.dialId.unexpected`). This is how a dial made before a transfer is placeable
2342
+ by whoever holds the task now: a `dial-outcome` naming a `dialId` the host never minted is looked
2343
+ up in the record, not discarded.
2344
+
2321
2345
  Four rules a provider has to keep:
2322
2346
 
2323
2347
  - **Report `seconds`; never expect Omni to derive it.** Omni does not subtract one timestamp from
@@ -2391,6 +2415,12 @@ did ask for them forwards upstream, and how often, is its own business. The step
2391
2415
  `handlingHistory` when the *provider* publishes it: Omni never writes the record itself.
2392
2416
  `recordStep` is required of a connection whose tasks declare `mute`, and answers `recorded`.
2393
2417
 
2418
+ **A host-performed leg still open when the task's media ends, or the task ends, is ended by the
2419
+ host at that instant** — `ended: true`, `seconds` to the end, the same `at` — since a provider
2420
+ left to close it would be guessing at a host-performed duration. Both ends are read off one clock,
2421
+ the host's, and a leg shorter than a second is reported as the second it was, never as the nought
2422
+ the record refuses.
2423
+
2394
2424
  ### Browser capability
2395
2425
 
2396
2426
  `browsers` is available to voice, chat, and email tasks. When declared, Omni renders the task's
@@ -2528,7 +2558,7 @@ const taskCapabilities = {
2528
2558
  | `blindTransfer` | Secondary menu item: Blind transfer | Omni may transfer the caller directly to a destination. |
2529
2559
  | `consultTransfer` | Secondary menu item: Consult transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Consult transfer**. |
2530
2560
  | `consultLead` | Secondary menu item: Consult lead | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.lead`. See **Consulting a lead**. |
2531
- | `conference` | Secondary button: Conference | Omni may add or remove participants from the active call. |
2561
+ | `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and drop a `conferenced` entry from it. |
2532
2562
  | `recording` | Overflow menu item: Recording | Omni may expose start, pause, resume, and stop recording controls. |
2533
2563
  | `dispositions` | Primary button: Complete | Omni may request task disposal with a provider disposition and notes. |
2534
2564
 
@@ -2577,7 +2607,7 @@ capabilities: {
2577
2607
  | Field | Contract |
2578
2608
  | --- | --- |
2579
2609
  | `destinations` | Directory Omni renders, with unique `id` values. |
2580
- | `address` | The value Omni sends as `TaskCommand.transfer.destination` or `conference.participant`. |
2610
+ | `address` | The value Omni sends as `destination` on a `transfer` or a `conference` command. |
2581
2611
  | `kind` | Where the contact is going. See below. |
2582
2612
  | `allowManualEntry` | Whether the agent may type a destination that is not in the directory. A directory with no destinations must allow manual entry, or the control has nothing to offer. |
2583
2613
 
@@ -2601,9 +2631,10 @@ from `blindTransfer` (a hand-over with nobody consulted) and from `conference` (
2601
2631
  call): a queue may offer any of the three without the others, and each is declared on its own.
2602
2632
 
2603
2633
  ```ts
2604
- // 1. Consult. The provider parks the customer and calls the destination; the task reports
2605
- // `paused` and carries `consultation` while the call to the destination stands.
2606
- { type: "transfer", action: "consult", destination: "+14155550111" }
2634
+ // 1. Consult. A dial: the provider parks the customer and calls the destination, answers
2635
+ // `dialling`, and the task reports `paused` with the destination `consulted` on `onCall`
2636
+ // while the call to it stands.
2637
+ { type: "transfer", action: "consult", dialId: "dial-7f2", destination: "+14155550111" }
2607
2638
 
2608
2639
  // 2a. Hand the customer to the consulted destination and leave.
2609
2640
  { type: "transfer", action: "complete" }
@@ -2613,21 +2644,25 @@ call): a queue may offer any of the three without the others, and each is declar
2613
2644
  ```
2614
2645
 
2615
2646
  `consult` is gated by the `consultTransfer` capability and takes a destination exactly as a blind
2616
- transfer does, from the same kind of directory. While the consultation stands the task carries
2617
- `consultation`, and that presence is what makes `complete` and `cancel` issuable -- they name no
2618
- destination because there is exactly one they could mean. A consultation that could be started
2619
- but not finished would strand the customer and the destination both, which is why all three are
2620
- commands and a provider that offers `consultTransfer` implements all three.
2647
+ transfer does, from the same kind of directory. It is a dial, so it carries the host's `dialId`
2648
+ and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
2649
+ the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
2650
+ is what makes `complete` and `cancel` issuable -- they name no destination because there is exactly
2651
+ one they could mean, which is why a task carries at most one `consulted` entry
2652
+ (`task.onCall.consulted.single`). A consultation that could be started but not finished would
2653
+ strand the customer and the destination both, which is why all three are commands and a provider
2654
+ that offers `consultTransfer` implements all three.
2621
2655
 
2622
2656
  `applied` on `complete` says the provider is bridging the customer to the destination and
2623
2657
  dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
2624
2658
  the provider reports `task-media-ended`, any wrap allowance runs, and the task ends with a
2625
2659
  `transferred` outcome naming the destination. `applied` on `cancel` says the destination is
2626
- dropped; the task returns to `in-progress` with `consultation` gone. Omni waits for the
2660
+ dropped; the task returns to `in-progress` with the `consulted` entry gone. Omni waits for the
2627
2661
  provider's report of both, as it does for every command.
2628
2662
 
2629
- A destination that does not answer is a consultation that ended: the provider clears
2630
- `consultation`, returns the task to `in-progress`, and the agent is back with the customer.
2663
+ A destination that does not answer is a dial that ended: the provider reports its `dial-outcome`,
2664
+ returns the task to `in-progress` with no `consulted` entry, and the agent is back with the
2665
+ customer.
2631
2666
 
2632
2667
  ### Chat capabilities
2633
2668
 
@@ -2690,14 +2725,101 @@ Custom capabilities must not redefine the meaning of a standard channel capabili
2690
2725
  Starts one outbound call from the idle dialpad. It is present only when the voice provider
2691
2726
  declares `dial`.
2692
2727
 
2728
+ - `dialId` is the host's identity for this dial, minted before the request leaves.
2693
2729
  - `destination` is the original number selected or entered by the agent.
2694
2730
  - The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
2695
2731
  number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
2696
- - `dialled` confirms that outbound call creation completed.
2732
+ - `dialling` says the request was accepted and the call is being placed, and restates the
2733
+ `dialId`. It says nothing about whether anyone will answer.
2697
2734
  - `failed` contains a `ProtocolFailure` and confirms no call was placed.
2698
2735
 
2699
- The resulting call is offered through the normal `task-offered` event. A successful dial result
2700
- does not manufacture a task inside Omni.
2736
+ The resulting call is offered through the normal `task-offered` event, and how the dial ended
2737
+ arrives on `dial-outcome` under the same `dialId`. A `dialling` result does not manufacture a task
2738
+ inside Omni.
2739
+
2740
+ ## Every dial has an outcome
2741
+
2742
+ Four commands place a call: `dial` from the idle dialpad, a blind or consult `transfer`, a
2743
+ `conference` `add`, and `callback`. Each is accepted or refused at once, and each then ends later
2744
+ and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
2745
+ late in a call routinely outlives the call. Nothing in between is reported: the wire says
2746
+ `dialling`, then says how it ended, once.
2747
+
2748
+ **The host mints the identity.** Every command that dials carries a `dialId` the host made, unique
2749
+ across the installation, and it travels four ways so no leg is a lookup:
2750
+
2751
+ ```ts
2752
+ declare const connection: Connection<"voice">;
2753
+ declare const taskId: TaskId;
2754
+
2755
+ // 1. Out, minted by the host.
2756
+ const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2", destination: "+14155550111" } });
2757
+
2758
+ // 2. Back on the result, restated, so the host compares and confirms.
2759
+ expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
2760
+
2761
+ // 3. On the outcome, however late, against the task it belonged to.
2762
+ const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, destination: "+14155550111", outcome: "no-answer", reason: "No route to destination" };
2763
+
2764
+ // 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
2765
+ const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destination: "+14155550111" };
2766
+ ```
2767
+
2768
+ The identity is a correlation handle between host and provider and **is never shown to the
2769
+ agent**. A host holding several tasks places an outcome by the `dialId` it minted; one it did not
2770
+ mint -- a dial made by the previous agent before a transfer -- it finds on the task's `onCall` or in
2771
+ its `handlingHistory`, which is why the steps that dial carry it. The `dialId` is not a retry key:
2772
+ Omni never repeats a dial, and a command still carries no key for that purpose.
2773
+
2774
+ **`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
2775
+ `{ status: "dialling", dialId }`, restating the host's identity (`result.dialId`,
2776
+ `result.dialId.mismatch`); `applied` from such a command, or `dialling` from one that dialled
2777
+ nothing, is refused (`result.status`). `validateResult(result, method, path, dialId)` takes the
2778
+ `dialId` the host sent for that reason.
2779
+
2780
+ **The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
2781
+ there was one, the destination where the provider knows it, and how the dial ended -- from a closed
2782
+ set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`. `answered` is stated rather than
2783
+ read off somebody appearing on the call, because a host that infers success cannot tell "reached"
2784
+ from "still ringing". `reason` is the switch's own words, optional, and shown to the agent
2785
+ attributed to the switch rather than to Omni. The task named may already have ended, and the
2786
+ harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
2787
+ `stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached.
2788
+
2789
+ **A provider says which outcomes it can tell apart.** No switch distinguishes all five, and a
2790
+ provider that cannot tell busy from unreachable must not pick one. The manifest declares
2791
+ `dialOutcomes`, and a `dial-outcome` carries only a declared member
2792
+ (`event.dialOutcome.undeclared`). The list includes `answered`, or the provider could never state a
2793
+ success (`manifest.dialOutcomes.answered`), and at least one other member, or it could never state
2794
+ a failure (`manifest.dialOutcomes.failure`). A provider that dials at all declares it: an idle
2795
+ dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `callback`,
2796
+ `blindTransfer`, `consultTransfer` or `conference` under a manifest without it is refused
2797
+ (`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
2798
+ error there.
2799
+
2800
+ **Who is on the call is stated on the task.** `Task.onCall` lists everyone the task's audio joins
2801
+ right now, replaced whole with the task like every other field, and present when the provider
2802
+ knows the room:
2803
+
2804
+ ```ts
2805
+ const onCall: OnCall[] = [
2806
+ { role: "party", since: "2026-08-21T09:12:00Z" },
2807
+ { role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
2808
+ { role: "conferenced", destination: "+14155550111", dialId: "dial-7f2", held: true, since: "2026-08-21T09:15:30Z" },
2809
+ ];
2810
+ ```
2811
+
2812
+ `party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
2813
+ neither carries a destination. `consulted` and `conferenced` were, so both carry the `destination`
2814
+ and, where a host's dial brought them, its `dialId`; a person the platform added itself carries
2815
+ none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
2816
+ after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
2817
+ never saw. A `conference` `remove` names the `destination` of the `conferenced` entry to drop.
2818
+
2819
+ The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
2820
+ they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
2821
+ belonged to -- with no retry, since a wrong number is not worth redialling and the decision is the
2822
+ agent's.
2701
2823
 
2702
2824
  ## Breaks
2703
2825
 
@@ -2948,36 +3070,36 @@ Omni offers the aggregate Break control only when every provider currently holdi
2948
3070
  declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
2949
3071
  knowingly permit partial availability.
2950
3072
 
2951
- Omni coordinates one attempt as follows:
3073
+ Omni coordinates one break attempt as follows:
2952
3074
 
2953
- 1. Freeze the participant set to every connected provider from which the agent can currently
2954
- receive work. A provider joining during the attempt is given no capacity until it finishes.
2955
- A provider whose authentication is `expired` is not one the agent can receive work from and is
2956
- not a participant: nothing is asked of it, the break proceeds without it, and when the login
2957
- is restored it is reconciled from its snapshot as a set-aside provider is, with no capacity
2958
- until then. `refreshing` keeps a provider in — its identity and capabilities remain available
2959
- and work continues. `assertBreakParticipants` holds a host to this set.
3075
+ 1. Freeze the providers the attempt asks: every connected provider from which the agent can
3076
+ currently receive work. A provider joining during the attempt is given no capacity until it
3077
+ finishes. A provider whose authentication is `expired` is not one the agent can receive work
3078
+ from and is not asked: nothing is asked of it, the break proceeds without it, and when the
3079
+ login is restored it is reconciled from its snapshot as a set-aside provider is, with no
3080
+ capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
3081
+ available and work continues. `assertBreakAttemptProviders` holds a host to this set.
2960
3082
  2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
2961
- 3. Send one `requestBreak` to every participant. A provider reports `awaiting-decision` or
3083
+ 3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-decision` or
2962
3084
  `granted`; neither state stops work. A denial transitions directly to `not-requested` and
2963
3085
  causes Omni to take the cancel path.
2964
- 4. If every participant reports `granted`, durably choose commit, enter `committing-break`,
2965
- and send `commitBreak()` to every participant. A provider then stops offering new work
3086
+ 4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
3087
+ and send `commitBreak()` to each of them. A provider then stops offering new work
2966
3088
  and reports `starting-after-task` or `in-effect`. The **commit bound** — ten seconds from the
2967
- decision, tunable per deployment — decides who is kept: a participant that has not applied the
3089
+ decision, tunable per deployment — decides who is kept: a provider that has not applied the
2968
3090
  commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
2969
- begins without it. `in-effect` decides `on-break`: Omni enters it once every kept participant
2970
- reports `in-effect`. A kept participant reporting `starting-after-task` has applied the commit
3091
+ begins without it. `in-effect` decides `on-break`: Omni enters it once every kept provider
3092
+ reports `in-effect`. A kept provider reporting `starting-after-task` has applied the commit
2971
3093
  and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
2972
3094
  settled and beginning when the task ends, and offers no cancel, because the commit is durable.
2973
- 5. If any participant fails or denies the request, cannot be reconciled within the bounded
3095
+ 5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
2974
3096
  decision timeout, or the agent cancels before commit, durably choose cancel and enter
2975
- `cancelling-break`. Send `cancelBreak()` to every participant still reporting
3097
+ `cancelling-break`. Send `cancelBreak()` to every provider still reporting
2976
3098
  `awaiting-decision` or `granted`. Work continues during cancellation because no stop was
2977
- committed. Return to `working` only after no participant retains either state.
3099
+ committed. Return to `working` only after no provider retains either state.
2978
3100
 
2979
- Commit and cancel are mutually exclusive decisions for one attempt. Once Omni chooses commit it
2980
- never rolls that attempt back: it is reconciled by snapshot until every participant is stopped.
3101
+ Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
3102
+ it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
2981
3103
  A provider that reports `granted` must therefore preserve the request across reconnects and must
2982
3104
  honour a later commit or cancel. This
2983
3105
  durable promise prevents a provider from failing the commit after another provider has already
@@ -2986,7 +3108,7 @@ stopped the agent.
2986
3108
  #### Why the commit phase is bounded and the decision is not
2987
3109
 
2988
3110
  Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
2989
- durable and cannot be rolled back, so a participant that crashes, has its authentication revoked,
3111
+ durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
2990
3112
  or is uninstalled between granting and committing would hold Omni in `committing-break` with no
2991
3113
  exit: the providers that did commit have already stopped the agent, and the agent is neither
2992
3114
  working nor on a break.
@@ -2996,7 +3118,7 @@ on one platform while another keeps routing work to them — and **a provider Om
2996
3118
  routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
2997
3119
  for it costs the agent their break.
2998
3120
 
2999
- Setting a participant aside is not a rollback and not a cancel. The commit stands and the
3121
+ Setting a provider aside is not a rollback and not a cancel. The commit stands and the
3000
3122
  obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
3001
3123
  snapshot before anything else, and the snapshot decides:
3002
3124
 
@@ -3011,7 +3133,7 @@ snapshot before anything else, and the snapshot decides:
3011
3133
  The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
3012
3134
  no capacity until it is reconciled.
3013
3135
 
3014
- **If the agent has already ended the break elsewhere, the attempt is over** and the returning
3136
+ **If the agent has already ended the break elsewhere, the break attempt is over** and the returning
3015
3137
  provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
3016
3138
  would stop an agent who is working again; `starting-after-task` or `in-effect` gets `endBreak()`.
3017
3139
  Never rolling back is about a break that is still on, not one the agent has finished.
@@ -3020,7 +3142,7 @@ Omni may tell the agent which platforms the break has not yet reached, as it alr
3020
3142
  break cannot be paired across every provider.
3021
3143
 
3022
3144
  The decision phase needs no such bound, because nothing has stopped. Work continues throughout
3023
- `requesting-break`, so a participant that never answers costs the agent a wait rather than their
3145
+ `requesting-break`, so a provider that never answers costs the agent a wait rather than their
3024
3146
  availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
3025
3147
  because no stop was ever committed.
3026
3148
 
@@ -3436,7 +3558,7 @@ declared:
3436
3558
  | `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. |
3437
3559
  | `callback` | The `callback` 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. |
3438
3560
  | `transfer` with `action: "consult"` | The `consultTransfer` capability. Blind `transfer` is gated by `blindTransfer`; the two are declared and offered separately. |
3439
- | `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- `Task.consultation` present. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
3561
+ | `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`. |
3440
3562
  | `lead` with `action: "request"` or `"cancel"` | The `consultLead` capability. `cancel` needs a request standing -- `Task.lead` with status `requested`. |
3441
3563
  | `lead` 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`. |
3442
3564
  | Everything else | Its own named capability. |
@@ -3627,11 +3749,11 @@ from `in-effect` or `starting-after-task` to a grant or a request, or from `gran
3627
3749
  `exerciseAdapter` holds the stream to that from the connect snapshot on;
3628
3750
  `assertBreakFollowsItsRequests` holds any sequence.
3629
3751
 
3630
- For a multi-provider attempt, "every provider" is the participant set frozen when the attempt
3631
- entered `requesting-break`. Omni commits only after every participant reports `granted` —
3752
+ For a multi-provider break attempt, "every provider" is the set of providers frozen when the
3753
+ attempt entered `requesting-break`. Omni commits only after every asked provider reports `granted` —
3632
3754
  that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
3633
- `on-break` once every kept participant reports `in-effect`; the commit bound decides who is kept,
3634
- setting aside a participant that has not applied the commit rather than holding a break that has
3755
+ `on-break` once every kept provider reports `in-effect`; the commit bound decides who is kept,
3756
+ setting aside a provider that has not applied the commit rather than holding a break that has
3635
3757
  already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
3636
3758
  multi-provider break**.
3637
3759
 
@@ -3688,6 +3810,15 @@ The `task-media-ended` event and the `completing` phase are likewise non-termina
3688
3810
  snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
3689
3811
  same envelope ID is harmless.
3690
3812
 
3813
+ ### `dial-outcome`
3814
+
3815
+ How a dial the host placed ended, once, either way, named by the `dialId` the host sent: `answered`,
3816
+ or one of the declared ways of not reaching the destination, with the switch's `reason` where it
3817
+ gave one. `taskId` names the task the dial was on, where there was one, and that task may already
3818
+ have ended. Omni shows it to the agent against the call it belonged to and does nothing else: no
3819
+ audio moves, since the provider bridges the leg on its side, and no dial is repeated. See **Every
3820
+ dial has an outcome**.
3821
+
3691
3822
  ### `announcement`
3692
3823
 
3693
3824
  Publishes an agent-facing message. `text` is always required and is the accessible fallback.
@@ -3874,7 +4005,7 @@ cannot be established from TypeScript structure alone.
3874
4005
  | `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. |
3875
4006
  | `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. |
3876
4007
  | `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. |
3877
- | `assertBreakParticipants(candidates, participants)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
4008
+ | `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
3878
4009
  | `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. |
3879
4010
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
3880
4011
  | `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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",