@xema/omni-protocol 0.1.64 → 0.1.66
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/testing.js +15 -0
- package/dist/validation.js +13 -2
- package/guide.md +10 -2
- package/package.json +1 -1
package/dist/testing.js
CHANGED
|
@@ -1026,14 +1026,26 @@ async function driveOneCall(drive) {
|
|
|
1026
1026
|
// A task's keys are gone with the task. A task-scoped key carries the task id, so the watched
|
|
1027
1027
|
// store says which keys were the task's; one still held after task-ended is a key about to be
|
|
1028
1028
|
// inherited by the next offer of the same id, since a platform retires an id minutes after closing it.
|
|
1029
|
+
let retainedAtEnd;
|
|
1029
1030
|
const storeReleased = async () => {
|
|
1030
1031
|
await Promise.resolve();
|
|
1031
1032
|
await Promise.resolve();
|
|
1032
1033
|
const retained = [...drive.held].filter(key => key.includes(taskId));
|
|
1034
|
+
retainedAtEnd = new Set(retained);
|
|
1033
1035
|
if (retained.length > 0) {
|
|
1034
1036
|
refuse("drive.store.retained", "drive.store", `${taskId} has ended and the login's store still holds ${retained.join(", ")}: a task's keys go with the task, or the next offer of the same id inherits them`);
|
|
1035
1037
|
}
|
|
1036
1038
|
};
|
|
1039
|
+
// A key written about the task after its end -- a persist hung off a timer, a late command -- is the
|
|
1040
|
+
// same hazard arriving later, so the store is read once more when the drive is done.
|
|
1041
|
+
const nothingLate = () => {
|
|
1042
|
+
if (retainedAtEnd === undefined)
|
|
1043
|
+
return;
|
|
1044
|
+
const late = [...drive.held].filter(key => key.includes(taskId) && !retainedAtEnd.has(key));
|
|
1045
|
+
if (late.length > 0) {
|
|
1046
|
+
refuse("drive.store.late", "drive.store", `${taskId} had ended with its keys gone, and the login's store now holds ${late.join(", ")}: something wrote about the task after its end`);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1037
1049
|
// The task, as first seen: on the snapshot, or on the first offer.
|
|
1038
1050
|
let cursor = 0;
|
|
1039
1051
|
let task = isRecord(drive.snapshot) && Array.isArray(drive.snapshot.tasks) ? drive.snapshot.tasks.find(isTask) : undefined;
|
|
@@ -1260,6 +1272,9 @@ async function driveOneCall(drive) {
|
|
|
1260
1272
|
}
|
|
1261
1273
|
}
|
|
1262
1274
|
}
|
|
1275
|
+
// A timer fires after the microtasks: one turn of the loop is what a late persist needs to show itself.
|
|
1276
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1277
|
+
nothingLate();
|
|
1263
1278
|
return found;
|
|
1264
1279
|
}
|
|
1265
1280
|
// A request goes not-requested -> awaiting-decision | granted; a commit goes granted ->
|
package/dist/validation.js
CHANGED
|
@@ -709,7 +709,7 @@ function validateTaskAttributes(value, path, into) {
|
|
|
709
709
|
}
|
|
710
710
|
});
|
|
711
711
|
}
|
|
712
|
-
function validateHandlingHistory(value, path, into) {
|
|
712
|
+
function validateHandlingHistory(value, path, into, task = {}) {
|
|
713
713
|
if (value === undefined)
|
|
714
714
|
return;
|
|
715
715
|
if (!isPlainObject(value)) {
|
|
@@ -751,6 +751,17 @@ function validateHandlingHistory(value, path, into) {
|
|
|
751
751
|
// Omitted while a leg is still running. Nought is a claim that it took no time.
|
|
752
752
|
into.require(isDurationSeconds(entry.seconds) && entry.seconds > 0, "task.handlingHistory.seconds", `${at}.seconds`, "seconds must be a positive whole number; omit it while the step is still running");
|
|
753
753
|
}
|
|
754
|
+
else {
|
|
755
|
+
// An open leg is one still running, and the task says whether it can be: a hold runs only while
|
|
756
|
+
// the task is paused, and a mute only while its media is up. An open entry after that is a leg
|
|
757
|
+
// nobody closed, which reads exactly like a leg running now.
|
|
758
|
+
if (entry.step === "held") {
|
|
759
|
+
into.require(task.phase === "paused", "task.handlingHistory.held.open", `${at}.seconds`, `a held entry without seconds is a hold still running, and the task is ${String(task.phase)}: the hold has ended, and its duration is stated`);
|
|
760
|
+
}
|
|
761
|
+
if (entry.step === "muted") {
|
|
762
|
+
into.require(task.media !== "ended" && task.phase !== "completing", "task.handlingHistory.muted.open", `${at}.seconds`, "a muted entry without seconds is a mute still running, and the call is over: the host ends its legs when the media ends, and the duration is stated");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
754
765
|
// A muted entry carries whose the silence was, as the host reported it; no other step has it.
|
|
755
766
|
// And it names the agent: the host has exactly one, the provider knows who, so an unattributed
|
|
756
767
|
// muted leg is a record that dropped a fact it held rather than one it could not establish.
|
|
@@ -965,7 +976,7 @@ function validateTaskInto(task, context, path, into) {
|
|
|
965
976
|
validateContactInto(task.party, `${path}.party`, into, context.levels);
|
|
966
977
|
validateBrowsers(task.browsers, `${path}.browsers`, into);
|
|
967
978
|
validateTaskAttributes(task.attributes, `${path}.attributes`, into);
|
|
968
|
-
validateHandlingHistory(task.handlingHistory, `${path}.handlingHistory`, into);
|
|
979
|
+
validateHandlingHistory(task.handlingHistory, `${path}.handlingHistory`, into, { phase: task.phase, media: task.media });
|
|
969
980
|
validateOnCall(task.onCall, context.channel, `${path}.onCall`, into);
|
|
970
981
|
// The room is who is on the call now, and a task outlives its call by the whole of wrap-up: a
|
|
971
982
|
// task whose call has ended -- completing, or media ended -- carries nobody, or the last thing the
|
package/guide.md
CHANGED
|
@@ -1946,7 +1946,7 @@ Creates one live provider connection for the signed-in agent.
|
|
|
1946
1946
|
| `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login. Treated as `true` when omitted. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
1947
1947
|
| `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
|
|
1948
1948
|
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's host states what its Mute does, and a desk-phone or conversation login's host cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the host's**. |
|
|
1949
|
-
| `store` | The login's operational store, kept by the host for the life of the login, across a reload of the host, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the handling legs a host reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store a host may clear aggressively. **A task's keys carry the task id and go with the task**: a key written about a task names the task's id in the key, and is deleted before the task's end is published, because a platform retires a task id minutes after closing it and a requeue takes seconds, so a key that outlives its task is inherited by the next offer of the same id -- a record with legs the host never reported against it. A login-scoped key carries no task id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`); an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
1949
|
+
| `store` | The login's operational store, kept by the host for the life of the login, across a reload of the host, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the handling legs a host reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store a host may clear aggressively. **A task's keys carry the task id and go with the task**: a key written about a task names the task's id in the key, and is deleted before the task's end is published, because a platform retires a task id minutes after closing it and a requeue takes seconds, so a key that outlives its task is inherited by the next offer of the same id -- a record with legs the host never reported against it. A login-scoped key carries no task id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the host's, not the adapter's: a host reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.task-not-found` and writes nothing -- the host ends its own open legs at the task's end, so such a report is the host's error to see, and a host reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
1950
1950
|
| `host` | The host's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The host reports, the adapter decides**. |
|
|
1951
1951
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
1952
1952
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -2485,7 +2485,15 @@ Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conf
|
|
|
2485
2485
|
|
|
2486
2486
|
**The record is one entry per occurrence, oldest first.** Each hold is its own `held` entry — `at`
|
|
2487
2487
|
when it began, `seconds` once it ended and omitted while it runs — and a second hold is a second
|
|
2488
|
-
entry after the first, never a revision of it.
|
|
2488
|
+
entry after the first, never a revision of it. A leg that has ended states its duration, and the
|
|
2489
|
+
task says whether a leg can still be running: a hold runs only while the task is `paused`, a mute
|
|
2490
|
+
only while its media is up, so a `held` entry without `seconds` on a task that is not paused, or a
|
|
2491
|
+
`muted` one on a call that is over, is a leg nobody closed and reads exactly like a leg running now
|
|
2492
|
+
(`task.handlingHistory.held.open`, `.muted.open`). Whoever performs the leg closes it — the
|
|
2493
|
+
provider whose platform parks the caller, the host whose microphone it is — by restating the entry
|
|
2494
|
+
with its duration, and **an entry that cannot be closed is not written**: open for ever is a
|
|
2495
|
+
plausible nought one level down from a total. There is no resumed step; a resume is the end of a
|
|
2496
|
+
hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming it twice. The same goes for every step: two mutes are two
|
|
2489
2497
|
`muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
|
|
2490
2498
|
entries, one per join. Order is enforced: an entry earlier than the one before it is refused
|
|
2491
2499
|
(`task.handlingHistory.order`). **Intervals between steps are the host's to subtract**, never a
|