@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 +7 -0
- package/dist/testing.js +29 -7
- package/dist/validation.js +1 -0
- package/guide.md +13 -2
- package/package.json +1 -1
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
|
-
|
|
1230
|
+
within.onExpiry();
|
|
1231
1231
|
resolve(undefined);
|
|
1232
|
-
},
|
|
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
|
|
1568
|
-
|
|
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)
|
package/dist/validation.js
CHANGED
|
@@ -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
|
-
|
|
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.
|