@xema/omni-protocol 0.1.35 → 0.1.36

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
@@ -576,15 +576,18 @@ export type TaskCompletion = {
576
576
  wrapAllowance: DurationSeconds;
577
577
  };
578
578
  export type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
579
+ /** Where a dialled entry stands: `ringing` until its dial is answered, `joined` after. */
580
+ export type OnCallStage = "ringing" | "joined";
579
581
  export declare const ON_CALL_ROLES: readonly ["party", "agent", "consulted", "conferenced"];
580
582
  /**
581
583
  * Somebody on the call, or being brought onto it, as the provider states it: who, since when, and
582
584
  * whether they are held aside. `party` is the customer; `agent` a person, by user id; `consulted`
583
585
  * 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.
586
+ * the agent can call it off while it rings -- carrying the dial when a host placed it, the address
587
+ * either way, and the `stage` it has reached: `ringing` until the dial is answered, `joined` after,
588
+ * stated so a snapshot says who is present without anyone having seen the outcome. Nobody ringing is
589
+ * held. `label` names a destination -- a person, a queue -- not a phrase. At most one `party` and
590
+ * one `consulted`: the consult commands name neither because there is exactly one.
588
591
  */
589
592
  export type OnCall = {
590
593
  since: IsoTimestamp;
@@ -597,6 +600,7 @@ export type OnCall = {
597
600
  } | {
598
601
  role: "consulted" | "conferenced";
599
602
  destination: string;
603
+ stage: OnCallStage;
600
604
  dialId?: DialId;
601
605
  label?: string;
602
606
  });
@@ -71,6 +71,7 @@ const DIAL_OUTCOMES = membersOf({ answered: true, busy: true, "no-answer": true,
71
71
  /** The outcomes that name a cause. `unexplained` is declarable only beside one of these. */
72
72
  const EXPLAINED_FAILURES = ["busy", "no-answer", "unreachable", "rejected", "cancelled"];
73
73
  const ON_CALL_ROLES = membersOf({ party: true, agent: true, consulted: true, conferenced: true });
74
+ const ON_CALL_STAGES = membersOf({ ringing: true, joined: true });
74
75
  /** The task capabilities under which a command dials, and so need the manifest to say how a dial ends. */
75
76
  const DIALLING_CAPABILITIES = ["callback", "blindTransfer", "consultTransfer", "conference"];
76
77
  const SNAPSHOT_REASONS = membersOf({
@@ -778,13 +779,17 @@ function validateOnCall(value, channel, path, into) {
778
779
  }
779
780
  if (role === "consulted" || role === "conferenced") {
780
781
  into.filled(entry.destination, "task.onCall.destination", `${at}.destination`, `a ${role} entry names the destination that was dialled`);
782
+ // Stated, so a snapshot says who is present without anyone having seen the dial's outcome.
783
+ if (into.oneOf(entry.stage, ON_CALL_STAGES, "task.onCall.stage", `${at}.stage`)) {
784
+ into.require(!(entry.stage === "ringing" && entry.held === true), "task.onCall.held.ringing", `${at}.held`, "nobody ringing is held; held belongs to somebody who has joined");
785
+ }
781
786
  if (entry.dialId !== undefined)
782
787
  into.filled(entry.dialId, "task.onCall.dialId", `${at}.dialId`, "dialId must not be empty when present");
783
788
  if (entry.label !== undefined)
784
789
  into.filled(entry.label, "task.onCall.label", `${at}.label`, "a label must not be empty when present");
785
790
  }
786
791
  else {
787
- for (const field of ["destination", "dialId", "label"]) {
792
+ for (const field of ["destination", "dialId", "label", "stage"]) {
788
793
  into.require(entry[field] === undefined, `task.onCall.${field}.unexpected`, `${at}.${field}`, `a ${role} was dialled from nowhere; ${field} belongs on consulted or conferenced`);
789
794
  }
790
795
  }
package/guide.md CHANGED
@@ -589,10 +589,12 @@ type TaskCompletion =
589
589
 
590
590
  type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
591
591
 
592
+ type OnCallStage = "ringing" | "joined";
593
+
592
594
  type OnCall = { since: IsoTimestamp; held?: true } & (
593
595
  | { role: "party" }
594
596
  | { role: "agent"; userId: UserId }
595
- | { role: "consulted" | "conferenced"; destination: string; dialId?: DialId; label?: string }
597
+ | { role: "consulted" | "conferenced"; destination: string; stage: OnCallStage; dialId?: DialId; label?: string }
596
598
  );
597
599
 
598
600
  type TaskLead = {
@@ -2133,7 +2135,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2133
2135
  | `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. |
2134
2136
  | `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. |
2135
2137
  | `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**. |
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**. |
2138
+ | `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, 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**. |
2137
2139
  | `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**. |
2138
2140
  | `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**. |
2139
2141
 
@@ -2787,10 +2789,13 @@ set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `u
2787
2789
  `answered` is stated rather than read off somebody appearing on the call, because a host that infers
2788
2790
  success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
2789
2791
  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.
2792
+ `no-answer` the destination never gave. `unexplained` is the switch dropping the call with no cause among
2793
+ those named here: a statement about the switch, sent instead of the nearest cause it did not give.
2794
+ `reason` is the switch's own words, optional, and shown to the agent attributed to the switch rather
2795
+ than to Omni. The two are different things and may travel together: a switch can say "could not
2796
+ create dialog" in words and still name no cause a code would carry, so `unexplained` with a
2797
+ `reason` is a dial the switch described but did not classify, and a desk renders the words without
2798
+ inventing the class.
2794
2799
 
2795
2800
  **`taskId` is the task the dial was placed on**, resolved from the dial's own identity and never from
2796
2801
  whatever task the agent is looking at when the outcome arrives. That task may already have ended,
@@ -2832,7 +2837,8 @@ knows the room:
2832
2837
  const onCall: OnCall[] = [
2833
2838
  { role: "party", since: "2026-08-21T09:12:00Z" },
2834
2839
  { role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
2835
- { role: "conferenced", destination: "+14155550111", dialId: "dial-7f2", held: true, since: "2026-08-21T09:15:30Z" },
2840
+ { role: "conferenced", destination: "+14155550111", dialId: "dial-7f2", stage: "joined", held: true, since: "2026-08-21T09:15:30Z" },
2841
+ { role: "conferenced", destination: "+14155550199", dialId: "dial-3c9", stage: "ringing", since: "2026-08-21T09:16:02Z" },
2836
2842
  ];
2837
2843
  ```
2838
2844
 
@@ -2847,9 +2853,17 @@ never saw.
2847
2853
  what makes `transfer` `cancel` issuable and what `conference` `remove` names, and a destination the
2848
2854
  agent cannot call off while it rings is a parked customer with no way back -- which is the very case
2849
2855
  `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
2856
+ entry appears with its `dialId` when the dial is placed, and a `conference` `remove` naming a
2857
+ still-ringing `destination` calls that dial off, with `cancelled` as its outcome.
2858
+
2859
+ **The entry says where it stands.** A dialled entry carries `stage`: `ringing` until its dial is
2860
+ answered, `joined` after. The `dial-outcome` is the transition and the stage is the state, the same
2861
+ pairing as `task-media-started` and `media`: an `answered` outcome and the task restated with the
2862
+ entry `joined` say one thing twice, and any other outcome removes the entry. It is stated rather
2863
+ than left to whoever placed the dial because that knowledge lives in one process: after a reload,
2864
+ on a second session, or on a supervisor's screen the snapshot is the only source, and a room that
2865
+ cannot tell ringing from joined says it contains someone who may never arrive. Nobody ringing is
2866
+ held (`task.onCall.held.ringing`). A consult destination that never answers is a dial that
2853
2867
  ended: the provider returns the task to `in-progress` with no `consulted` entry, as **Consult
2854
2868
  transfer** states, so the customer is never left parked by a conforming provider.
2855
2869
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",