@xema/omni-protocol 0.1.113 → 0.1.114

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
@@ -1106,7 +1106,10 @@ export type VoiceTaskCommand = {
1106
1106
  } | {
1107
1107
  type: "resume";
1108
1108
  }
1109
- /** End the agent's channel and every channel the agent added; the caller continues on the provider's path. Gated by `endCall`. */
1109
+ /**
1110
+ * End the agent's channel and every channel the agent added; the caller continues on the provider's
1111
+ * path. Gated by `endCall`, and never from hold: a `paused` task is resumed first (`command.endCall.held`).
1112
+ */
1110
1113
  | {
1111
1114
  type: "end-call";
1112
1115
  }
package/dist/testing.js CHANGED
@@ -1959,11 +1959,33 @@ async function testOneCall(call) {
1959
1959
  return found;
1960
1960
  }
1961
1961
  }
1962
+ // Ending my part from hold would leave the caller in the hold with nobody coming back to them: a
1963
+ // host holds it back (command.endCall.held), and an adapter that receives it anyway must refuse
1964
+ // it -- so the test sends it past the validator and expects failed.
1965
+ const endCallRefusedOnHold = async () => {
1966
+ ruleTested("test.command.rejected", "test.command.held");
1967
+ let answer;
1968
+ try {
1969
+ answer = await call.connection.execute({ assignmentId: taskId, command: { type: "end-call" } });
1970
+ }
1971
+ catch (error) {
1972
+ refuse("test.command.rejected", "test.command.end-call", `execute rejected rather than answered: ${String(error)}`);
1973
+ return;
1974
+ }
1975
+ if (isRecord(answer) && answer.status !== "failed") {
1976
+ refuse("test.command.held", "test.command.end-call", "the provider ended the agent's part with the caller on hold: end-call waits for resume, and the adapter is the second gate");
1977
+ // Applied, the task leaves paused; the run reads where the adapter put it rather than resuming a call it ended.
1978
+ await updated(t => t.phase !== "paused", "the task after an end-call applied from hold");
1979
+ }
1980
+ };
1962
1981
  // 4. Hold and resume, where offered.
1963
1982
  if (latestTask().phase === "in-progress" && offers("hold")) {
1964
1983
  if (await send({ type: call.channel === "chat" ? "pause" : "hold" }) !== undefined) {
1965
1984
  if (await updated(t => t.phase === "paused", "the task pausing after hold") !== undefined) {
1966
- if (await send({ type: "resume" }) !== undefined) {
1985
+ if (call.channel === "voice" && offers("endCall"))
1986
+ await endCallRefusedOnHold();
1987
+ // An adapter that ended the agent's part from hold has nothing left to resume; the run goes on from where it put the task.
1988
+ if (latestTask().phase === "paused" && await send({ type: "resume" }) !== undefined) {
1967
1989
  await updated(t => t.phase === "in-progress", "the task resuming after resume");
1968
1990
  }
1969
1991
  }
@@ -2758,6 +2758,8 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2758
2758
  case "end-call":
2759
2759
  offered("endCall");
2760
2760
  interaction();
2761
+ // Ending my part with the caller on hold leaves them in the hold, with nobody coming back to them.
2762
+ into.require(task.phase !== "paused", "command.endCall.held", path, "the caller is on hold: resume before ending your part");
2761
2763
  break;
2762
2764
  case "terminate-call":
2763
2765
  offered("terminateCall");
package/guide/agent.md CHANGED
@@ -384,6 +384,7 @@ Migration from the earlier spellings:
384
384
  | the phone unspoken | `PhoneState` -- the device, its mute, its channels -- on `Snapshot.phone`, `phone-updated` and `TeamMember.phone`, from a manifest declaring `phoneStatus` (`snapshot.phone.required`, `.unexpected`, `phone.*`, `phone.channel.*`, `event.phone.capability`) |
385
385
  | the agent's readiness for the next call read into setCapacity | the agent's own queue: `capabilities.nextCall`, `requestNextCall`/`cancelNextCall`/`releaseLinedUp`, `Snapshot.nextCall` and `linedUp`, the `next-call` and `lined-up` events (`snapshot.nextCall.idle`, `nextCall.*`, `linedUp.*`) |
386
386
  | a harness that exercised an adapter, drove a call and evaluated rules | the harness reads as a test: `testAdapter`, `withCall`, `timeoutMs`, `notTested`, `rulesTested`, rules `test.*` (`test.timeout`, `test.offer.expired`, `test.preview.deadline`, ...) |
387
+ | an `end-call` while the caller was on hold | refused: a `paused` task is resumed first (`command.endCall.held`), the desk shows End call disabled on hold, the provider answers `failed`, and the test names an adapter that ends from hold (`test.command.held`); `terminate-call` is not gated, it ends the caller's channel too |
387
388
 
388
389
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
389
390
  History and report rule names use `history` and `historyReport`; assignment rules use
@@ -1005,7 +1006,7 @@ declared:
1005
1006
  | Command | What makes it available |
1006
1007
  | --- | --- |
1007
1008
  | `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
1008
- | `end-call` | The `endCall` capability: ends my part, the caller continues. |
1009
+ | `end-call` | The `endCall` capability: ends my part, the caller continues. Never from hold: a `paused` task is resumed first (`command.endCall.held`), or the caller is left in the hold. |
1009
1010
  | `terminate-call` | The `terminateCall` capability: ends the whole call, every channel on it. |
1010
1011
  | `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`. |
1011
1012
  | `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
@@ -1020,7 +1021,8 @@ declared:
1020
1021
 
1021
1022
  **A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
1022
1023
  `hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `conference` action, and
1023
- every `lead-assist` action; `schedule` alone reaches into `completing`. These controls act within this agent's current interaction.
1024
+ every `lead-assist` action; `schedule` alone reaches into `completing`, and `end-call` alone stops
1025
+ short of `paused` (`command.endCall.held`), since ending my part from hold leaves the caller in it. These controls act within this agent's current interaction.
1024
1026
  In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
1025
1027
  an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
1026
1028
  of this interaction does not establish that the caller or bridge ended. The capability stays
package/guide/voice.md CHANGED
@@ -258,6 +258,13 @@ provider. Two of them end the agent's part; they differ in what happens to the c
258
258
  conferenced colleague, a lead who joined -- end, and the caller continues on the path the
259
259
  provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
260
260
 
261
+ **Never from hold.** An `end-call` while the task is `paused` would leave the caller in the hold
262
+ with nobody coming back to them, and the provider's path for a caller who continues never begins.
263
+ So the caller is taken off hold first: the desk holds the command back while the task is `paused`
264
+ (`command.endCall.held`) and shows End call disabled on hold, and a provider that receives it
265
+ anyway answers `failed`. Resume, then end. `terminate-call` needs no such gate: it ends the
266
+ caller's channel with the rest, held or not, and nobody is left waiting.
267
+
261
268
  **`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
262
269
  the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
263
270
  line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
package/guide.md CHANGED
@@ -2902,7 +2902,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
2902
2902
  | `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
2903
2903
  | `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the test, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
2904
2904
  | `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
2905
- | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a conference's item, a schedule's time, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a lead requested, somebody else still on the call (`command.conference.remove.alone`). An agent application validates before sending and an adapter before acting. |
2905
+ | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a conference's item, a schedule's time, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a lead requested, somebody else still on the call (`command.conference.remove.alone`), the caller off hold before the agent's part ends (`command.endCall.held`). An agent application validates before sending and an adapter before acting. |
2906
2906
  | `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
2907
2907
  | `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
2908
2908
 
@@ -3050,7 +3050,9 @@ interaction phases with `hold` still declared -- in
3050
3050
  `confirmed`, where the provider publishes it, and in `completing` once this agent's interaction has ended -- the
3051
3051
  test sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
3052
3052
  is the second gate on a control on the contact, and one that applies it outside this task's
3053
- current interaction is named (`test.command.interaction`). The test cannot put a task into a phase the provider
3053
+ current interaction is named (`test.command.interaction`). With the task on hold, the test sends `end-call` past the
3054
+ validator the same way and expects `failed`, naming an adapter that ends the agent's part with the caller in the
3055
+ hold (`test.command.held`). The test cannot put a task into a phase the provider
3054
3056
  never publishes, so a provider that goes straight from `pending` to `in-progress` is checked in
3055
3057
  `completing` alone. A task the agent completes is
3056
3058
  completed from wherever it stands once the test has nothing left to do on it -- from `completing`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.113",
3
+ "version": "0.1.114",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",