@xema/omni-protocol 0.1.75 → 0.1.77
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 +5 -2
- package/dist/testing.d.ts +9 -0
- package/dist/testing.js +60 -21
- package/dist/validation.js +23 -1
- package/guide.md +43 -15
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -904,11 +904,14 @@ export type TaskOutcome = {
|
|
|
904
904
|
} | {
|
|
905
905
|
type: "transferred";
|
|
906
906
|
destinationId?: string;
|
|
907
|
-
}
|
|
907
|
+
}
|
|
908
|
+
/** Who called the work off: the agent declining, the provider withdrawing or re-routing, the party abandoning. */
|
|
909
|
+
| {
|
|
908
910
|
type: "cancelled";
|
|
911
|
+
by: "agent" | "provider" | "party";
|
|
909
912
|
reason?: string;
|
|
910
913
|
}
|
|
911
|
-
/** Only the phases in which somebody is still being waited on can expire
|
|
914
|
+
/** Only the phases in which somebody is still being waited on can expire; an offer that lapses at `allocationExpiresAt` names `pending`. */
|
|
912
915
|
| {
|
|
913
916
|
type: "expired";
|
|
914
917
|
phase: "pending" | "confirmed" | "preview";
|
package/dist/testing.d.ts
CHANGED
|
@@ -170,6 +170,15 @@ export declare class TaskStream {
|
|
|
170
170
|
/** What a restated record lost of the one read before it: nothing, or the entries by step and instant. */
|
|
171
171
|
private static lost;
|
|
172
172
|
private static stated;
|
|
173
|
+
/**
|
|
174
|
+
* Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
|
|
175
|
+
* state now, and first holds it to what the stream knew. A snapshot replaces state; it does not
|
|
176
|
+
* get to forget it. A task it carries that the stream already held may not go backwards, lose an
|
|
177
|
+
* entry of its record, return to undetermined terms, or be at work without the audio the stream
|
|
178
|
+
* held up: media ends on task-media-ended and the call moves on, so a task still in-progress or
|
|
179
|
+
* paused without its media is a snapshot that lost state, not a call that ended.
|
|
180
|
+
*/
|
|
181
|
+
resync(snapshot: unknown, at: string): ProtocolViolation[];
|
|
173
182
|
/** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
|
|
174
183
|
seed(snapshot: unknown): void;
|
|
175
184
|
/** Applies one envelope and returns what it may not say given what came before. */
|
package/dist/testing.js
CHANGED
|
@@ -1043,7 +1043,52 @@ export class TaskStream {
|
|
|
1043
1043
|
stages.set(entry.dialId, entry.stage);
|
|
1044
1044
|
}
|
|
1045
1045
|
}
|
|
1046
|
-
return {
|
|
1046
|
+
return { channel: String(isRecord(task) ? task.channel : undefined), completionMode: String(isRecord(task) ? task.completionMode : undefined),
|
|
1047
|
+
wrapAllowance: isRecord(task) && typeof task.wrapAllowance === "number" ? task.wrapAllowance : undefined,
|
|
1048
|
+
phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages, record: TaskStream.record(task), allocation: String(isRecord(task) ? task.allocationId : undefined) };
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
|
|
1052
|
+
* state now, and first holds it to what the stream knew. A snapshot replaces state; it does not
|
|
1053
|
+
* get to forget it. A task it carries that the stream already held may not go backwards, lose an
|
|
1054
|
+
* entry of its record, return to undetermined terms, or be at work without the audio the stream
|
|
1055
|
+
* held up: media ends on task-media-ended and the call moves on, so a task still in-progress or
|
|
1056
|
+
* paused without its media is a snapshot that lost state, not a call that ended.
|
|
1057
|
+
*/
|
|
1058
|
+
resync(snapshot, at) {
|
|
1059
|
+
const found = [];
|
|
1060
|
+
const refuse = (rule, where, message) => found.push({ rule, path: where, message });
|
|
1061
|
+
ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.handlingHistory", "stream.snapshot.phase", "stream.snapshot.media");
|
|
1062
|
+
if (isRecord(snapshot) && Array.isArray(snapshot.tasks)) {
|
|
1063
|
+
snapshot.tasks.forEach((task, index) => {
|
|
1064
|
+
if (!isRecord(task) || typeof task.id !== "string")
|
|
1065
|
+
return;
|
|
1066
|
+
const was = this.tasks.get(task.id);
|
|
1067
|
+
if (was === undefined || was.allocation !== String(task.allocationId))
|
|
1068
|
+
return;
|
|
1069
|
+
if ((was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
|
|
1070
|
+
refuse("stream.snapshot.capabilitySource", `${at}.tasks[${index}].capabilitySource`, `${task.id} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
|
|
1071
|
+
}
|
|
1072
|
+
// A record once read is not unread: a resync restates it whole, or with more, never with less.
|
|
1073
|
+
const lost = TaskStream.lost(was.record, TaskStream.record(task));
|
|
1074
|
+
if (lost.length > 0) {
|
|
1075
|
+
refuse("stream.snapshot.handlingHistory", `${at}.tasks[${index}].handlingHistory`, `${task.id}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
|
|
1076
|
+
}
|
|
1077
|
+
const to = String(task.phase);
|
|
1078
|
+
const reachable = REACHABLE_PHASES[was.phase];
|
|
1079
|
+
if (reachable !== undefined && TASK_PHASES_ORDERED.includes(to) && !reachable.has(to)) {
|
|
1080
|
+
const connectingBack = was.phase === "completing" && TaskStream.partyDialled(task) && (to === "in-progress" || to === "paused");
|
|
1081
|
+
if (!connectingBack) {
|
|
1082
|
+
refuse("stream.snapshot.phase", `${at}.tasks[${index}].phase`, `${task.id} was ${was.phase} and the snapshot says ${to}: a task does not go backwards, on an update or on a resync`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (was.media === "started" && (to === "in-progress" || to === "paused") && task.media !== "started") {
|
|
1086
|
+
refuse("stream.snapshot.media", `${at}.tasks[${index}].media`, `${task.id}'s audio was up and the snapshot carries it ${to} without it: media ends on task-media-ended and the call moves on, so a snapshot that forgets the audio lost state`);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
this.seed(snapshot);
|
|
1091
|
+
return found;
|
|
1047
1092
|
}
|
|
1048
1093
|
/** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
|
|
1049
1094
|
seed(snapshot) {
|
|
@@ -1071,24 +1116,7 @@ export class TaskStream {
|
|
|
1071
1116
|
const known = id === undefined ? undefined : this.tasks.get(id);
|
|
1072
1117
|
switch (event.type) {
|
|
1073
1118
|
case "snapshot":
|
|
1074
|
-
|
|
1075
|
-
// A snapshot replaces what is known, and still may not say a task lost terms it had read.
|
|
1076
|
-
if (isRecord(event.snapshot) && Array.isArray(event.snapshot.tasks)) {
|
|
1077
|
-
event.snapshot.tasks.forEach((task, index) => {
|
|
1078
|
-
if (!isRecord(task) || typeof task.id !== "string")
|
|
1079
|
-
return;
|
|
1080
|
-
const was = this.tasks.get(task.id);
|
|
1081
|
-
if (was !== undefined && (was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
|
|
1082
|
-
refuse("stream.snapshot.capabilitySource", `${at}.snapshot.tasks[${index}].capabilitySource`, `${task.id} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
|
|
1083
|
-
}
|
|
1084
|
-
// A record once read is not unread: a resync restates it whole, or with more, never with less.
|
|
1085
|
-
const lost = TaskStream.lost(was?.record, TaskStream.record(task));
|
|
1086
|
-
if (lost.length > 0) {
|
|
1087
|
-
refuse("stream.snapshot.handlingHistory", `${at}.snapshot.tasks[${index}].handlingHistory`, `${task.id}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
|
|
1088
|
-
}
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
this.seed(event.snapshot);
|
|
1119
|
+
found.push(...this.resync(event.snapshot, `${at}.snapshot`));
|
|
1092
1120
|
break;
|
|
1093
1121
|
case "task-offered": {
|
|
1094
1122
|
ruleEvaluated("stream.taskOffered.duplicate", "stream.taskOffered.allocation");
|
|
@@ -1231,6 +1259,15 @@ export class TaskStream {
|
|
|
1231
1259
|
// A late ending for a life that is over must not end the life that is open under the same id.
|
|
1232
1260
|
if (!this.namesTheOpenLife(event, known, at, refuse))
|
|
1233
1261
|
break;
|
|
1262
|
+
// Off voice there is no media event, so the update that moves a task to completing is the
|
|
1263
|
+
// provider's word that handling ended and the moment the wrap allowance starts. A provider
|
|
1264
|
+
// that completes the task itself with an allowance to run has to have started the clock:
|
|
1265
|
+
// completed from in-progress, the allowance it stated was never given.
|
|
1266
|
+
ruleEvaluated("stream.taskEnded.unwrapped");
|
|
1267
|
+
if (known.channel !== "voice" && known.completionMode === "provider-automatic" && known.wrapAllowance !== undefined && known.wrapAllowance > 0
|
|
1268
|
+
&& known.phase !== "completing" && isRecord(event.outcome) && event.outcome.type === "completed") {
|
|
1269
|
+
refuse("stream.taskEnded.unwrapped", `${at}.outcome`, `${id} was completed from ${known.phase} with a wrap allowance of ${known.wrapAllowance}s under provider-automatic: off voice, completing is the provider's word that handling ended and the allowance's start, and it was never published`);
|
|
1270
|
+
}
|
|
1234
1271
|
this.endedAllocations.add(known.allocation);
|
|
1235
1272
|
this.tasks.delete(id);
|
|
1236
1273
|
break;
|
|
@@ -1335,9 +1372,11 @@ async function driveOneCall(drive) {
|
|
|
1335
1372
|
drive.handOver.up(session, second);
|
|
1336
1373
|
drive.connection = second;
|
|
1337
1374
|
const snapshot = await second.snapshot();
|
|
1338
|
-
// The second adapter's snapshot is held to everything a first one is, and the streams take it
|
|
1375
|
+
// The second adapter's snapshot is held to everything a first one is, and the streams take it
|
|
1376
|
+
// as the state now the way they take any resync: held to what they knew, then replaced. A reload
|
|
1377
|
+
// is a place the stream's rules keep working, not one where they all stop.
|
|
1339
1378
|
found.push(...validateSnapshot(snapshot, again.manifest, "drive.reload.snapshot", drive.reader()));
|
|
1340
|
-
drive.streams.stream.
|
|
1379
|
+
found.push(...drive.streams.stream.resync(snapshot, "drive.reload.snapshot"));
|
|
1341
1380
|
drive.streams.breaks.seed(snapshot);
|
|
1342
1381
|
const carried = isRecord(snapshot) && Array.isArray(snapshot.tasks)
|
|
1343
1382
|
? snapshot.tasks.find(task => isRecord(task) && task.id === taskId) : undefined;
|
package/dist/validation.js
CHANGED
|
@@ -89,6 +89,7 @@ const PHONES = membersOf({ softphone: true, deskPhone: true });
|
|
|
89
89
|
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true, monitorControl: true });
|
|
90
90
|
const MONITOR_MODES = membersOf({ monitor: true, whisper: true, barge: true });
|
|
91
91
|
const COMPLETED_BY = membersOf({ agent: true, provider: true });
|
|
92
|
+
const CANCELLED_BY = membersOf({ agent: true, provider: true, party: true });
|
|
92
93
|
const EXPIRABLE_PHASES = membersOf({
|
|
93
94
|
pending: true, confirmed: true, preview: true,
|
|
94
95
|
});
|
|
@@ -994,6 +995,12 @@ function validateMonitoring(value, channel, path, into) {
|
|
|
994
995
|
into.oneOf(value.mode, MONITOR_MODES, "task.monitoring.mode", `${path}.mode`);
|
|
995
996
|
into.timestamp(value.since, "task.monitoring.since", `${path}.since`);
|
|
996
997
|
}
|
|
998
|
+
/** Whether a task is the agent's own doing: a dial the host placed on its onCall, a lead's join, a lead's monitor. */
|
|
999
|
+
function originatedByTheAgent(task) {
|
|
1000
|
+
if (Array.isArray(task.onCall) && task.onCall.some(entry => isPlainObject(entry) && typeof entry.dialId === "string"))
|
|
1001
|
+
return true;
|
|
1002
|
+
return task.assisting !== undefined || task.monitoring !== undefined;
|
|
1003
|
+
}
|
|
997
1004
|
export function validateTask(task, context, path = "task") {
|
|
998
1005
|
const into = new Collector();
|
|
999
1006
|
validateTaskInto(task, context, path, into);
|
|
@@ -1032,15 +1039,27 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1032
1039
|
}
|
|
1033
1040
|
// Acceptance is the offer's word, carried on the pending task so a snapshot can say it: it
|
|
1034
1041
|
// travels exactly when Omni said tasks may be auto-accepted, and only while the task is pending.
|
|
1042
|
+
// Work the agent originated -- a dial or connect-back, carrying the host's dialId on onCall; a
|
|
1043
|
+
// lead's join, carrying assisting; a monitor, carrying monitoring -- was accepted by the command
|
|
1044
|
+
// that created it, and says so whatever the provisioning: the desk shows no Accept for a call
|
|
1045
|
+
// the agent placed. The provisioning governs work the queue routes, and nothing else.
|
|
1046
|
+
const originated = task.phase === "pending" && originatedByTheAgent(task);
|
|
1047
|
+
ruleEvaluated("task.acceptance.originated");
|
|
1035
1048
|
if (task.acceptance !== undefined) {
|
|
1036
1049
|
into.oneOf(task.acceptance, ACCEPTANCE_MODES, "task.acceptance", `${path}.acceptance`);
|
|
1037
1050
|
if (task.phase !== "pending") {
|
|
1038
1051
|
into.add("task.acceptance.unexpected", `${path}.acceptance`, "acceptance is an offer's word; a task past pending has been accepted");
|
|
1039
1052
|
}
|
|
1053
|
+
else if (originated) {
|
|
1054
|
+
into.require(task.acceptance === "automatic", "task.acceptance.originated", `${path}.acceptance`, "work the agent originated -- a dial, a connect-back, a join, a monitor -- was accepted by the command that created it, and says automatic whatever the provisioning");
|
|
1055
|
+
}
|
|
1040
1056
|
else if (context.autoAcceptTasks === false) {
|
|
1041
|
-
into.add("task.acceptance.unexpected", `${path}.acceptance`, "autoAcceptTasks is off, so every task requires agent acceptance and a pending task carries no acceptance");
|
|
1057
|
+
into.add("task.acceptance.unexpected", `${path}.acceptance`, "autoAcceptTasks is off, so every task the queue routes requires agent acceptance and a pending task carries no acceptance");
|
|
1042
1058
|
}
|
|
1043
1059
|
}
|
|
1060
|
+
else if (originated) {
|
|
1061
|
+
into.add("task.acceptance.originated", `${path}.acceptance`, "work the agent originated -- a dial, a connect-back, a join, a monitor -- says acceptance: automatic whatever the provisioning: the command that created it accepted it");
|
|
1062
|
+
}
|
|
1044
1063
|
else if (task.phase === "pending" && context.autoAcceptTasks === true) {
|
|
1045
1064
|
into.add("task.acceptance.required", `${path}.acceptance`, "autoAcceptTasks is on, so a pending task states how it is accepted");
|
|
1046
1065
|
}
|
|
@@ -1469,6 +1488,9 @@ function validateTaskOutcome(value, path, into) {
|
|
|
1469
1488
|
}
|
|
1470
1489
|
break;
|
|
1471
1490
|
case "cancelled":
|
|
1491
|
+
// One word covered the agent declining, the provider withdrawing and the party abandoning the
|
|
1492
|
+
// ring; a record that cannot tell them apart is not a record. by says who, as completed does.
|
|
1493
|
+
into.oneOf(value.by, CANCELLED_BY, "event.taskEnded.outcome.cancelled.by", `${path}.by`);
|
|
1472
1494
|
if (value.reason !== undefined) {
|
|
1473
1495
|
into.filled(value.reason, "event.taskEnded.outcome.cancelled", `${path}.reason`, "a reason must not be empty when present");
|
|
1474
1496
|
}
|
package/guide.md
CHANGED
|
@@ -702,7 +702,7 @@ type AcceptanceMode =
|
|
|
702
702
|
type TaskOutcome =
|
|
703
703
|
| { type: "completed"; by: "agent" | "provider" }
|
|
704
704
|
| { type: "transferred"; destinationId?: string }
|
|
705
|
-
| { type: "cancelled"; reason?: string }
|
|
705
|
+
| { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
|
|
706
706
|
| { type: "expired"; phase: "pending" | "confirmed" | "preview" }
|
|
707
707
|
| { type: "left" }
|
|
708
708
|
| { type: "failed"; failure: ProtocolFailure };
|
|
@@ -2170,8 +2170,8 @@ person's behalf for want of a word:
|
|
|
2170
2170
|
| `consent` | The provider requires the person's explicit consent: Omni presents **Accept** and waits, whatever its own policy would have done. A host that declares `guarantees.personConsent` promises exactly this; a provider checks it before offering work only a person may take. |
|
|
2171
2171
|
| `automatic` | Omni accepts immediately without agent interaction. |
|
|
2172
2172
|
|
|
2173
|
-
When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task
|
|
2174
|
-
requires agent acceptance. The two are never confused on the wire: `consent` is always the
|
|
2173
|
+
When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task the
|
|
2174
|
+
queue routes requires agent acceptance. The two are never confused on the wire: `consent` is always the
|
|
2175
2175
|
provider's requirement, stated on a wire where Omni was willing to accept for the agent; Omni's own
|
|
2176
2176
|
no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
|
|
2177
2177
|
press is Omni's doing, not the provider's.
|
|
@@ -2185,8 +2185,13 @@ belongs, whatever the host was configured with.
|
|
|
2185
2185
|
|
|
2186
2186
|
An automatically accepted task still arrives through `task-offered`.
|
|
2187
2187
|
|
|
2188
|
-
|
|
2189
|
-
`
|
|
2188
|
+
**Work the agent originated is accepted by the command that created it.** A task born of the
|
|
2189
|
+
agent's own act — a dialpad call or a connect-back, recognisable by the host's `dialId` on
|
|
2190
|
+
`onCall`; a lead's join, carrying `assisting`; a lead's monitor, carrying `monitoring` — arrives
|
|
2191
|
+
through `task-offered` with `acceptance: "automatic"` whatever `autoAcceptTasks` says, and the
|
|
2192
|
+
validator holds it there under either provisioning and under none (`task.acceptance.originated`):
|
|
2193
|
+
the desk shows no **Accept** for a call the agent placed. The provisioning governs work the queue
|
|
2194
|
+
routes to the agent, and nothing else.
|
|
2190
2195
|
|
|
2191
2196
|
### Pending
|
|
2192
2197
|
|
|
@@ -2220,14 +2225,20 @@ on a desk phone — and is not in question per call. See **How the agent hears t
|
|
|
2220
2225
|
Automatic acceptance still begins with `task-offered`.
|
|
2221
2226
|
|
|
2222
2227
|
`allocationExpiresAt` is the deadline after which the offer lapses. Where present, Omni counts
|
|
2223
|
-
down and stops offering **Accept** once it passes
|
|
2228
|
+
down and stops offering **Accept** once it passes; the provider ends the lapsed offer with
|
|
2229
|
+
`task-ended` and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.**
|
|
2224
2230
|
A provider that reports only elapsed ring time after the fact cannot say when an offer is due
|
|
2225
2231
|
to end, and a computed value would have Omni withdraw **Accept** from a task still pending.
|
|
2226
2232
|
|
|
2227
2233
|
A preview's deadline is not on the offer. It travels on the task, as `previewEndsAt` with
|
|
2228
2234
|
`atDeadline`, so a snapshot carries it too -- see **Preview: the agent presses Call**.
|
|
2229
2235
|
|
|
2230
|
-
A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome
|
|
2236
|
+
A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome, `by:
|
|
2237
|
+
"provider"`. **`cancelled` says who called the work off**, as `completed` says who completed it:
|
|
2238
|
+
`agent` for a decline, `provider` for a withdrawal or a re-route, `party` for a caller who abandoned
|
|
2239
|
+
the ring. One word for the three left a supervisor's record unable to tell them apart, so `by` is
|
|
2240
|
+
required and closed (`event.taskEnded.outcome.cancelled.by`). An offer nobody acted on before
|
|
2241
|
+
`allocationExpiresAt` is not cancelled by anyone: it lapses, and ends `expired` naming `pending`.
|
|
2231
2242
|
|
|
2232
2243
|
### Tasks already in progress
|
|
2233
2244
|
|
|
@@ -2309,7 +2320,8 @@ The canonical task transitions are:
|
|
|
2309
2320
|
| `preview` | Agent presses Call (`call`) and the customer answers, or the deadline `calls` and they answer | `in-progress` |
|
|
2310
2321
|
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2311
2322
|
| `preview` | The deadline `expires` | Removed by `task-ended` with `expired` outcome |
|
|
2312
|
-
| `pending` | Provider withdraws the allocation | Removed by `task-ended` with `cancelled` outcome |
|
|
2323
|
+
| `pending` | Provider withdraws the allocation, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
|
|
2324
|
+
| `pending` | The offer lapses at `allocationExpiresAt` | Removed by `task-ended` with `expired` outcome naming `pending` |
|
|
2313
2325
|
| No task | Snapshot reports work already underway | `in-progress` |
|
|
2314
2326
|
| `in-progress` | Provider or agent pauses the task | `paused` |
|
|
2315
2327
|
| `in-progress` | Agent starts a warm transfer (`transfer` `warm`); the customer is parked | `paused` |
|
|
@@ -2420,9 +2432,18 @@ whether the channel carries real-time media:
|
|
|
2420
2432
|
| Channel | Wrap allowance starts at |
|
|
2421
2433
|
| --- | --- |
|
|
2422
2434
|
| Voice and any channel with real-time media | The `task-media-ended` event |
|
|
2423
|
-
| Chat |
|
|
2424
|
-
| Email |
|
|
2425
|
-
| Other non-media channels | The
|
|
2435
|
+
| Chat | The `task-updated` that moves the task to `completing`: the provider's word that the conversation is closed on its side, the allowance running from that publication's `occurredAt` |
|
|
2436
|
+
| Email | The `task-updated` that moves the task to `completing`: the provider's word that the platform has accepted the outgoing message, the allowance running from that publication's `occurredAt` |
|
|
2437
|
+
| Other non-media channels | The `task-updated` that moves the task to `completing` |
|
|
2438
|
+
|
|
2439
|
+
**Off voice, `completing` is the provider's word that handling ended.** There is no media event to
|
|
2440
|
+
carry it, so the phase does; a host starts the wrap clock at that publication and nowhere else. It
|
|
2441
|
+
is optional: a conversation with nothing to wrap moves from `in-progress` to `task-ended` and
|
|
2442
|
+
`completing` is never published. Under `provider-automatic` with a non-zero `wrapAllowance` it is
|
|
2443
|
+
required, because the clock has to start somewhere: a chat or email task the provider completes
|
|
2444
|
+
from `in-progress` with an allowance to run gave the agent none of it, and the stream refuses the
|
|
2445
|
+
ending (`stream.taskEnded.unwrapped`). Under `agent-command` the agent's `complete` is the end,
|
|
2446
|
+
from `in-progress` or from `completing` alike.
|
|
2426
2447
|
|
|
2427
2448
|
```ts
|
|
2428
2449
|
const emailCompletion = {
|
|
@@ -2867,7 +2888,10 @@ last statement stands and the failure is a `diagnostic`, and a task that was pub
|
|
|
2867
2888
|
`queue` or `ungoverned` never returns to `undetermined`, on an update (`stream.taskUpdated.capabilitySource`)
|
|
2868
2889
|
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
2869
2890
|
entry the same two ways (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`);
|
|
2870
|
-
|
|
2891
|
+
a snapshot carrying a task still at work does not forget the audio the stream held up, since media
|
|
2892
|
+
ends on `task-media-ended` and the call moves on (`stream.snapshot.media`); and a task does not go
|
|
2893
|
+
backwards, on an update or on a resync (`stream.taskUpdated.phase`, `stream.snapshot.phase`): the
|
|
2894
|
+
stream sees publications, not
|
|
2871
2895
|
transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
|
|
2872
2896
|
with `confirmed` between them, and what is refused is a phase unreachable from the last one read by
|
|
2873
2897
|
the transition table -- back to `pending`, back to `confirmed` or `preview` once work began, out of
|
|
@@ -4268,8 +4292,8 @@ beside the mapped one is what the full check costs a host; an adapter has it for
|
|
|
4268
4292
|
validator holds a command only to a task that stands: a task handed in that is not one the wire
|
|
4269
4293
|
published is named (`command.task`) rather than checked against.
|
|
4270
4294
|
|
|
4271
|
-
Declining
|
|
4272
|
-
|
|
4295
|
+
Declining a pending offer ends it without accepting or completing it. The provider confirms the
|
|
4296
|
+
end with `task-ended` and a `cancelled` outcome, `by: "agent"`.
|
|
4273
4297
|
|
|
4274
4298
|
### `execute(request)`
|
|
4275
4299
|
|
|
@@ -4760,7 +4784,11 @@ built and connected for the same login with the same context and the same `store
|
|
|
4760
4784
|
must carry the task with that leg and the host's word, and the run goes on with the second as its
|
|
4761
4785
|
connection to the end. A platform that holds the record hands it back, and an adapter that composed
|
|
4762
4786
|
the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
|
|
4763
|
-
`.rejected`). The second adapter is held to what the
|
|
4787
|
+
`.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
|
|
4788
|
+
before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
|
|
4789
|
+
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.handlingHistory`,
|
|
4790
|
+
`.media`) and a reload is a place those rules keep working, not one where they stop. The second
|
|
4791
|
+
adapter is held to what the first was: the same provider
|
|
4764
4792
|
(`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
|
|
4765
4793
|
of the entries the first had published -- a record once read is not unread across a reload either.
|
|
4766
4794
|
A reconnect on the same adapter object would prove nothing, since an in-process adapter's memory
|