@xema/omni-protocol 0.1.34 → 0.1.35

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 CHANGED
@@ -77,11 +77,13 @@ export interface DialCapability {
77
77
  /**
78
78
  * How a dial ended, as the switch distinguishes it. `answered` is the one success; the rest are
79
79
  * the ways a destination was not reached -- `cancelled` because the dialler called it off before
80
- * anyone answered, not because the destination failed to. A manifest declares which of these its
81
- * platform can tell apart (`dialOutcomes`), and a `dial-outcome` carries only a declared one.
80
+ * anyone answered, not because the destination failed to, and `unexplained` because the switch
81
+ * dropped the call and gave no cause, a statement about the switch rather than a shrug from the
82
+ * adapter. A manifest declares which of these its platform can tell apart (`dialOutcomes`), and a
83
+ * `dial-outcome` carries only a declared one.
82
84
  */
83
- export type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected" | "cancelled";
84
- export declare const DIAL_OUTCOMES: readonly ["answered", "busy", "no-answer", "unreachable", "rejected", "cancelled"];
85
+ export type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected" | "cancelled" | "unexplained";
86
+ export declare const DIAL_OUTCOMES: readonly ["answered", "busy", "no-answer", "unreachable", "rejected", "cancelled", "unexplained"];
85
87
  /** Every idle capability a provider may declare. Only voice may `dial`; the channel arm says so. */
86
88
  export declare const IDLE_CAPABILITIES: readonly ["dial", "personalBrowser", "calendar", "contacts"];
87
89
  export type IdleCapability = (typeof IDLE_CAPABILITIES)[number];
@@ -576,11 +578,13 @@ export type TaskCompletion = {
576
578
  export type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
577
579
  export declare const ON_CALL_ROLES: readonly ["party", "agent", "consulted", "conferenced"];
578
580
  /**
579
- * Somebody on the call right now, as the provider states it: who, since when, and whether they
580
- * are held aside. `party` is the customer; `agent` a person, by user id; `consulted` and
581
- * `conferenced` someone a dial brought in, carrying the dial when a host placed it and the address
582
- * either way. `label` names a destination -- a person, a queue -- not a phrase. At most one
583
- * `party` and one `consulted`: the consult commands name neither because there is exactly one.
581
+ * Somebody on the call, or being brought onto it, as the provider states it: who, since when, and
582
+ * whether they are held aside. `party` is the customer; `agent` a person, by user id; `consulted`
583
+ * and `conferenced` someone a dial is bringing in -- listed from the moment the dial is placed, so
584
+ * the agent can call it off while it rings -- carrying the dial when a host placed it and the
585
+ * address either way. Whether they answered is the `dial-outcome`'s to say. `label` names a
586
+ * destination -- a person, a queue -- not a phrase. At most one `party` and one `consulted`: the
587
+ * consult commands name neither because there is exactly one.
584
588
  */
585
589
  export type OnCall = {
586
590
  since: IsoTimestamp;
@@ -804,7 +808,7 @@ export type VoiceTaskCommand = {
804
808
  dialId: DialId;
805
809
  destination: string;
806
810
  }
807
- /** Drop the `conferenced` entry on `Task.onCall` with this destination. */
811
+ /** Drop the `conferenced` entry on `Task.onCall` with this destination; while it still rings, this calls the dial off. */
808
812
  | {
809
813
  type: "conference";
810
814
  action: "remove";
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export function negotiateProtocolVersion(adapterVersions, hostVersions = OMNI_SU
15
15
  const shared = adapterVersions.filter(version => host.has(version));
16
16
  return shared.length === 0 ? undefined : Math.max(...shared);
17
17
  }
18
- export const DIAL_OUTCOMES = ["answered", "busy", "no-answer", "unreachable", "rejected", "cancelled"];
18
+ export const DIAL_OUTCOMES = ["answered", "busy", "no-answer", "unreachable", "rejected", "cancelled", "unexplained"];
19
19
  /** Every idle capability a provider may declare. Only voice may `dial`; the channel arm says so. */
20
20
  export const IDLE_CAPABILITIES = ["dial", "personalBrowser", "calendar", "contacts"];
21
21
  /** What Omni calls each idle capability. */
@@ -67,7 +67,9 @@ const ACCESS_APPLIES_TO = membersOf({
67
67
  "initial-url": true, "all-navigation": true,
68
68
  });
69
69
  const DIAL_DESTINATION_POLICIES = membersOf({ "contacts-only": true, "any-number": true });
70
- const DIAL_OUTCOMES = membersOf({ answered: true, busy: true, "no-answer": true, unreachable: true, rejected: true, cancelled: true });
70
+ const DIAL_OUTCOMES = membersOf({ answered: true, busy: true, "no-answer": true, unreachable: true, rejected: true, cancelled: true, unexplained: true });
71
+ /** The outcomes that name a cause. `unexplained` is declarable only beside one of these. */
72
+ const EXPLAINED_FAILURES = ["busy", "no-answer", "unreachable", "rejected", "cancelled"];
71
73
  const ON_CALL_ROLES = membersOf({ party: true, agent: true, consulted: true, conferenced: true });
72
74
  /** The task capabilities under which a command dials, and so need the manifest to say how a dial ends. */
73
75
  const DIALLING_CAPABILITIES = ["callback", "blindTransfer", "consultTransfer", "conference"];
@@ -297,6 +299,11 @@ function validateDialOutcomes(manifest, path, into) {
297
299
  });
298
300
  into.require(declared.includes("answered"), "manifest.dialOutcomes.answered", path, "a dial that reached its destination is stated, never inferred: dialOutcomes includes answered");
299
301
  into.require(declared.some((outcome) => outcome !== "answered"), "manifest.dialOutcomes.failure", path, "a provider that can only say answered can never say a destination was not reached: declare at least one other outcome");
302
+ // "The switch gave no cause" is honest only from a provider that reports the cause when there is
303
+ // one; declared alone it is the generic failure every unsure provider would send.
304
+ if (declared.includes("unexplained")) {
305
+ into.require(declared.some((outcome) => EXPLAINED_FAILURES.includes(outcome)), "manifest.dialOutcomes.unexplained.alone", path, "unexplained says the switch gave no cause, which is a claim only a provider that reports causes can make: declare at least one of busy, no-answer, unreachable, rejected, cancelled beside it");
306
+ }
300
307
  }
301
308
  /** Whether a manifest says how a dial ends, and so may publish tasks that dial. `undefined` where there is no manifest to ask. */
302
309
  function manifestDials(manifest) {
package/guide.md CHANGED
@@ -36,7 +36,7 @@ are used precisely throughout and mean nothing looser here.
36
36
  | **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
37
37
  | **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
38
38
  | **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. |
39
+ | **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in from the moment their dial is placed. |
40
40
 
41
41
  Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
42
42
  one thing, so a bare "status" in conversation is always the authentication session's, and a
@@ -165,7 +165,7 @@ type DialDestinations = "contacts-only" | "any-number";
165
165
 
166
166
  type DialCapability = { destinations: DialDestinations };
167
167
 
168
- type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected" | "cancelled";
168
+ type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected" | "cancelled" | "unexplained";
169
169
 
170
170
  type IdleCapabilities<C extends Channel = Channel> = {
171
171
  personalBrowser?: PersonalBrowserCapability;
@@ -1399,6 +1399,10 @@ export default defineAdapter({
1399
1399
  });
1400
1400
  ```
1401
1401
 
1402
+ `dialOutcomes` is a compile error off voice, as `dial` is. An adapter written once over
1403
+ `C extends Channel` therefore sets it the way it already sets `idleCapabilities`, with
1404
+ `as Pick<Manifest<C>, "dialOutcomes">`; an adapter written for one channel needs no cast.
1405
+
1402
1406
  ### `Adapter.manifest`
1403
1407
 
1404
1408
  Static metadata that Omni can inspect before connecting.
@@ -2129,7 +2133,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2129
2133
  | `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. |
2130
2134
  | `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. |
2131
2135
  | `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**. |
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**. |
2136
+ | `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 `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**. |
2133
2137
  | `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**. |
2134
2138
  | `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**. |
2135
2139
 
@@ -2558,7 +2562,7 @@ const taskCapabilities = {
2558
2562
  | `blindTransfer` | Secondary menu item: Blind transfer | Omni may transfer the caller directly to a destination. |
2559
2563
  | `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**. |
2560
2564
  | `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**. |
2561
- | `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and drop a `conferenced` entry from it. |
2565
+ | `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and drop a `conferenced` entry from it -- one still ringing included, which calls the dial off. |
2562
2566
  | `recording` | Overflow menu item: Recording | Omni may expose start, pause, resume, and stop recording controls. |
2563
2567
  | `dispositions` | Primary button: Complete | Omni may request task disposal with a provider disposition and notes. |
2564
2568
 
@@ -2779,12 +2783,14 @@ nothing, is refused (`result.status`). `validateResult(result, method, path, dia
2779
2783
 
2780
2784
  **The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
2781
2785
  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`, `cancelled`. `answered` is stated
2783
- rather than read off somebody appearing on the call, because a host that infers success cannot tell
2784
- "reached" from "still ringing". `cancelled` is the dialler calling the dial off before anyone
2785
- answered -- the agent hanging up while a consult still rings -- which is not a `no-answer` the
2786
- destination never gave. `reason` is the switch's own words, optional, and shown to the agent
2787
- attributed to the switch rather than to Omni.
2786
+ set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `unexplained`.
2787
+ `answered` is stated rather than read off somebody appearing on the call, because a host that infers
2788
+ success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
2789
+ before anyone answered -- the agent hanging up while a consult still rings -- which is not a
2790
+ `no-answer` the destination never gave. `unexplained` is the switch dropping the call and giving no
2791
+ cause: a statement about the switch, sent instead of the nearest cause it did not give. `reason` is
2792
+ the switch's own words, optional, and shown to the agent attributed to the switch rather than to
2793
+ Omni.
2788
2794
 
2789
2795
  **`taskId` is the task the dial was placed on**, resolved from the dial's own identity and never from
2790
2796
  whatever task the agent is looking at when the outcome arrives. That task may already have ended,
@@ -2792,6 +2798,12 @@ and the harness places the outcome all the same (`TaskStream`, `stream.dialOutco
2792
2798
  `stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached, against the
2793
2799
  call it belonged to and never against the next one.
2794
2800
 
2801
+ The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which a host calls when it
2802
+ places one; an entry on a task's `onCall`; and a step in its `handlingHistory`. Since a dialled entry
2803
+ is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- a
2804
+ blind transfer, a callback, an idle dialpad call -- or whose entry has already left the room; an
2805
+ outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
2806
+
2795
2807
  **`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
2796
2808
  claims, and both are true at once when a leg answers and never joins. A desk shows `answered` as the
2797
2809
  dial's conclusion and says "on the call" only when the person appears on `Task.onCall`; the obvious
@@ -2802,7 +2814,11 @@ provider that cannot tell busy from unreachable must not pick one. The manifest
2802
2814
  `dialOutcomes`, and a `dial-outcome` carries only a declared member
2803
2815
  (`event.dialOutcome.undeclared`). The list includes `answered`, or the provider could never state a
2804
2816
  success (`manifest.dialOutcomes.answered`), and at least one other member, or it could never state
2805
- a failure (`manifest.dialOutcomes.failure`). A provider that dials at all declares it: an idle
2817
+ a failure (`manifest.dialOutcomes.failure`). `unexplained` is declarable only beside a cause the
2818
+ provider does report -- `busy`, `no-answer`, `unreachable`, `rejected` or `cancelled`
2819
+ (`manifest.dialOutcomes.unexplained.alone`): a provider that reports causes may honestly say the
2820
+ switch gave none, while one that reports none would be sending the generic failure this set exists
2821
+ to refuse. A provider that dials at all declares it: an idle
2806
2822
  dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `callback`,
2807
2823
  `blindTransfer`, `consultTransfer` or `conference` under a manifest without it is refused
2808
2824
  (`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
@@ -2825,7 +2841,17 @@ neither carries a destination. `consulted` and `conferenced` were, so both carry
2825
2841
  and, where a host's dial brought them, its `dialId`; a person the platform added itself carries
2826
2842
  none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
2827
2843
  after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
2828
- never saw. A `conference` `remove` names the `destination` of the `conferenced` entry to drop.
2844
+ never saw.
2845
+
2846
+ **A dialled entry is listed from the moment the dial is placed, not from the answer.** The entry is
2847
+ what makes `transfer` `cancel` issuable and what `conference` `remove` names, and a destination the
2848
+ agent cannot call off while it rings is a parked customer with no way back -- which is the very case
2849
+ `cancelled` exists for. Consult and conference behave alike here: the `consulted` or `conferenced`
2850
+ entry appears with its `dialId` when the dial is placed, whether it has answered is the
2851
+ `dial-outcome`'s to say, and a `conference` `remove` naming a still-ringing `destination` calls that
2852
+ dial off, with `cancelled` as its outcome. A consult destination that never answers is a dial that
2853
+ ended: the provider returns the task to `in-progress` with no `consulted` entry, as **Consult
2854
+ transfer** states, so the customer is never left parked by a conforming provider.
2829
2855
 
2830
2856
  The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
2831
2857
  they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",