@xema/omni-protocol 0.1.72 → 0.1.73

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
@@ -174,6 +174,13 @@ export interface Manifest<C extends Channel = Channel> {
174
174
  * when it ended, and a running report is refused: what a provider never asked for never crosses.
175
175
  */
176
176
  runningStepReports?: true;
177
+ /**
178
+ * How long after an applied disposal -- `complete`, `transfer` `complete`, `lead-assist`
179
+ * `take-over` -- the provider's `task-ended` is owed, in milliseconds. `applied` says the provider
180
+ * has disposed of the task; the ending follows within this, or the host resyncs and shows the
181
+ * task as unsettled. Stated per provider, since platforms settle at different speeds.
182
+ */
183
+ disposalSettleMs: number;
177
184
  }
178
185
  export interface SecretStore {
179
186
  get(key: string): Promise<string | undefined>;
package/dist/testing.js CHANGED
@@ -1217,7 +1217,7 @@ async function driveOneCall(drive) {
1217
1217
  const refuse = (rule, path, message) => found.push({ rule, path, message });
1218
1218
  const isTask = (value) => isRecord(value) && typeof value.id === "string";
1219
1219
  // Wait for an envelope that satisfies `matches`, looking first at what already arrived.
1220
- const waitFor = (what, matches, from) => new Promise(resolve => {
1220
+ const waitFor = (what, matches, from, within = { ms: drive.timeoutMs, onExpiry: () => refuse("drive.timeout", "drive", `the provider owed ${what} within ${drive.timeoutMs}ms and it never arrived`) }) => new Promise(resolve => {
1221
1221
  for (let index = from; index < drive.events.length; index += 1) {
1222
1222
  const hit = matches(drive.events[index]);
1223
1223
  if (hit !== undefined) {
@@ -1227,9 +1227,9 @@ async function driveOneCall(drive) {
1227
1227
  }
1228
1228
  const timer = setTimeout(() => {
1229
1229
  drive.waiters.delete(waiter);
1230
- refuse("drive.timeout", "drive", `the provider owed ${what} within ${drive.timeoutMs}ms and it never arrived`);
1230
+ within.onExpiry();
1231
1231
  resolve(undefined);
1232
- }, drive.timeoutMs);
1232
+ }, within.ms);
1233
1233
  const waiter = (envelope) => {
1234
1234
  const hit = matches(envelope);
1235
1235
  if (hit === undefined)
@@ -1368,10 +1368,10 @@ async function driveOneCall(drive) {
1368
1368
  return undefined;
1369
1369
  }, cursor).then(hit => { if (hit)
1370
1370
  cursor = hit.at; return hit; });
1371
- const ended = () => waitFor("a task-ended for the driven task", envelope => {
1371
+ const ended = (within) => waitFor("a task-ended for the driven task", envelope => {
1372
1372
  const event = envelope.event;
1373
1373
  return event.type === "task-ended" && event.taskId === taskId ? event : undefined;
1374
- }, cursor);
1374
+ }, cursor, within);
1375
1375
  // Every command the drive sends is validated against the task as published, and its answer for its method.
1376
1376
  ruleEvaluated("drive.timeout");
1377
1377
  const send = async (command, dialId) => {
@@ -1564,8 +1564,30 @@ async function driveOneCall(drive) {
1564
1564
  if (isRecord(dispositions) && dispositions.required === true && Array.isArray(dispositions.codes) && isRecord(dispositions.codes[0])) {
1565
1565
  command.disposition = dispositions.codes[0].id;
1566
1566
  }
1567
- if (await send(command) !== undefined && await ended() !== undefined)
1568
- await storeReleased();
1567
+ if (await send(command) !== undefined) {
1568
+ // applied says the provider has disposed of the task, and its ending follows within the bound
1569
+ // the manifest stated. Past it the host resyncs: a snapshot still carrying the task is a task
1570
+ // held open by a provider that said it was done, and the desk shows it as unsettled.
1571
+ ruleEvaluated("drive.disposal.unsettled");
1572
+ const settle = Number(drive.manifest.disposalSettleMs);
1573
+ let unsettled = false;
1574
+ const end = await ended({ ms: settle, onExpiry: () => { unsettled = true; } });
1575
+ if (end !== undefined)
1576
+ await storeReleased();
1577
+ else if (unsettled) {
1578
+ let resync;
1579
+ try {
1580
+ resync = await drive.connection.snapshot();
1581
+ }
1582
+ catch (error) {
1583
+ refuse("drive.command.rejected", "drive.disposal", `snapshot() after an unsettled disposal rejected: ${String(error)}`);
1584
+ }
1585
+ const still = isRecord(resync) && Array.isArray(resync.tasks) && resync.tasks.some(t => isRecord(t) && t.id === taskId && t.allocationId === allocationOf());
1586
+ refuse("drive.disposal.unsettled", "drive.disposal", still
1587
+ ? `complete was applied and ${settle}ms later the task-ended has not come and a snapshot still carries ${taskId}: applied says the provider disposed of the task, and it has not`
1588
+ : `complete was applied and ${settle}ms later the task-ended has not come; a snapshot no longer carries ${taskId}, so the ending was owed and never sent`);
1589
+ }
1590
+ }
1569
1591
  }
1570
1592
  else if (latestTask().phase === "completing") {
1571
1593
  if (await ended() !== undefined)
@@ -422,6 +422,7 @@ export function validateManifest(manifest, path = "manifest") {
422
422
  if (manifest.runningStepReports !== undefined) {
423
423
  into.require(manifest.runningStepReports === true, "manifest.runningStepReports", `${path}.runningStepReports`, "runningStepReports is declared by presence, as true; a provider that takes begin and end only omits it");
424
424
  }
425
+ into.require(typeof manifest.disposalSettleMs === "number" && Number.isInteger(manifest.disposalSettleMs) && manifest.disposalSettleMs > 0, "manifest.disposalSettleMs", `${path}.disposalSettleMs`, "disposalSettleMs is how long after an applied disposal the task-ended is owed, a positive whole number of milliseconds, stated by every provider");
425
426
  if (manifest.orgLevels !== undefined) {
426
427
  if (!Array.isArray(manifest.orgLevels)) {
427
428
  into.add("manifest.orgLevels.shape", `${path}.orgLevels`, "orgLevels must be an array when present");
package/guide.md CHANGED
@@ -193,6 +193,7 @@ type Manifest<C extends Channel = Channel> = {
193
193
  dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
194
194
  phones?: C extends "voice" ? Phone[] : never;
195
195
  runningStepReports?: true;
196
+ disposalSettleMs: number;
196
197
  };
197
198
  ```
198
199
 
@@ -1471,6 +1472,7 @@ export default defineAdapter({
1471
1472
  channel: "voice",
1472
1473
  supportedProtocolVersions: [OMNI_PROTOCOL_VERSION],
1473
1474
  authenticationMethods: ["browser-sso"],
1475
+ disposalSettleMs: 5000,
1474
1476
  idleCapabilities: {
1475
1477
  dial: { destinations: "any-number" },
1476
1478
  },
@@ -1506,6 +1508,7 @@ compile time.
1506
1508
  | `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the host) and/or `deskPhone` (a handset the platform rings; the host shows the call and opens nothing). The host picks one per login. See **How the agent hears the call**. |
1507
1509
  | `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**. |
1508
1510
  | `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**. |
1511
+ | `disposalSettleMs` | Required. How long after an applied disposal -- `complete`, `transfer` `complete`, `lead-assist` `take-over` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.disposalSettleMs`). Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1509
1512
 
1510
1513
  ### Authentication methods
1511
1514
 
@@ -4492,8 +4495,16 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
4492
4495
  A `left` outcome ends the task for this agent alone: the call continues without them, as it does
4493
4496
  when a lead who joined it leaves -- see **Lead assist**.
4494
4497
 
4495
- A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`.
4496
- The `task-media-ended` event and the `completing` phase are likewise non-terminal. A replacement
4498
+ A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
4499
+ and not for ever: `applied` to a disposal -- `complete`, `transfer` `complete`, `lead-assist`
4500
+ `take-over` -- says the provider has disposed of the task, and its `task-ended` follows within the
4501
+ manifest's `disposalSettleMs`. A provider never answers `applied` for a disposal it has not yet
4502
+ performed. Past the bound the host calls `snapshot()`: a snapshot still carrying the task is a task
4503
+ held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
4504
+ the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
4505
+ task, since the ending was owed and lost. The drive holds a provider to the same bound
4506
+ (`drive.disposal.unsettled`). The `task-media-ended` event and the `completing` phase are likewise
4507
+ non-terminal. A replacement
4497
4508
  snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
4498
4509
  same envelope ID is harmless, and a `task-ended` naming an allocation that has already ended is
4499
4510
  recognised as the late event it is, never applied to the life now open under the same id.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.72",
3
+ "version": "0.1.73",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",