@xema/omni-protocol 0.1.42 → 0.1.44
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/README.md +7 -8
- package/dist/index.d.ts +30 -16
- package/dist/index.js +6 -6
- package/dist/testing.js +1 -1
- package/dist/validation.js +32 -12
- package/guide.md +88 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,17 +97,16 @@ const task = {
|
|
|
97
97
|
};
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
without a cast. Dropping an arm then fails the build — indirectly, as a missing return.
|
|
100
|
+
**Switch on `command.action` with no `default`.** A `default` turns a future action into whatever
|
|
101
|
+
the default does, silently; naming every arm means dropping one fails the build — indirectly, as a
|
|
102
|
+
missing return — and keeps the narrowing that lets you read `destination` without a cast.
|
|
104
103
|
|
|
105
104
|
```ts
|
|
106
105
|
switch (command.action) {
|
|
107
|
-
case
|
|
108
|
-
case "
|
|
109
|
-
case "complete": return
|
|
110
|
-
case "cancel": return
|
|
106
|
+
case "cold": return coldTransfer(command.destination);
|
|
107
|
+
case "warm": return warmTransfer(command.destination);
|
|
108
|
+
case "complete": return completeWarmTransfer();
|
|
109
|
+
case "cancel": return cancelWarmTransfer();
|
|
111
110
|
}
|
|
112
111
|
```
|
|
113
112
|
|
package/dist/index.d.ts
CHANGED
|
@@ -451,9 +451,9 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
|
|
|
451
451
|
agentDisconnect?: Lockable<true>;
|
|
452
452
|
/** Connect back to the party while `completing`, whoever placed the call; the task returns to `in-progress`. */
|
|
453
453
|
connectBack?: Lockable<true>;
|
|
454
|
-
|
|
454
|
+
coldTransfer?: Lockable<true | DestinationDirectory>;
|
|
455
455
|
/** Park the customer and call a destination first; then `complete` or `cancel`. */
|
|
456
|
-
|
|
456
|
+
warmTransfer?: Lockable<true | DestinationDirectory>;
|
|
457
457
|
/** Ask a lead to join this call, with a note. The lead's decision arrives on `Task.leadAssist`. */
|
|
458
458
|
leadAssist?: Lockable<true>;
|
|
459
459
|
conference?: Lockable<true | DestinationDirectory>;
|
|
@@ -512,10 +512,15 @@ export type TaskPhase =
|
|
|
512
512
|
"pending"
|
|
513
513
|
/** Accepted, and not yet started. */
|
|
514
514
|
| "confirmed"
|
|
515
|
-
/**
|
|
516
|
-
|
|
515
|
+
/**
|
|
516
|
+
* Voice only: the customer's record is on the agent's screen and no call has gone out. The agent
|
|
517
|
+
* presses Call, or `previewEndsAt` arrives and `atDeadline` says what the system does instead.
|
|
518
|
+
*/
|
|
519
|
+
| "preview" | "in-progress" | "paused"
|
|
517
520
|
/** The work is over and the agent is finishing up. */
|
|
518
521
|
| "completing";
|
|
522
|
+
/** What the system does when a preview's deadline passes with no Call pressed. */
|
|
523
|
+
export type PreviewDeadline = "calls" | "expires";
|
|
519
524
|
/** Who ends the task: the agent issuing `complete`, or the provider deciding it is over. */
|
|
520
525
|
export type CompletionMode = "agent-command" | "provider-automatic";
|
|
521
526
|
export interface TaskAttributeBase {
|
|
@@ -710,6 +715,13 @@ export type Task<C extends Channel = Channel> = {
|
|
|
710
715
|
* (`autoAcceptTasks: true`), forbidden when it said not, and absent past `pending`.
|
|
711
716
|
*/
|
|
712
717
|
acceptance?: AcceptanceMode;
|
|
718
|
+
/**
|
|
719
|
+
* In `preview` only, and together: when the system stops waiting for the agent to press Call,
|
|
720
|
+
* and what it does then -- `calls` places the call itself, `expires` takes the record back and
|
|
721
|
+
* the task ends `expired`. Absent, the agent has as long as they need.
|
|
722
|
+
*/
|
|
723
|
+
previewEndsAt?: IsoTimestamp;
|
|
724
|
+
atDeadline?: PreviewDeadline;
|
|
713
725
|
/** The identifier an agent reads back to a customer, where the provider has one. */
|
|
714
726
|
reference?: string;
|
|
715
727
|
attributes?: TaskAttribute[];
|
|
@@ -747,7 +759,7 @@ export type TaskOutcome = {
|
|
|
747
759
|
/** Only the phases in which somebody is still being waited on can expire. */
|
|
748
760
|
| {
|
|
749
761
|
type: "expired";
|
|
750
|
-
phase: "pending" | "confirmed" | "
|
|
762
|
+
phase: "pending" | "confirmed" | "preview";
|
|
751
763
|
}
|
|
752
764
|
/** This agent left a call that continues without them: a lead who joined and dropped. */
|
|
753
765
|
| {
|
|
@@ -757,7 +769,7 @@ export type TaskOutcome = {
|
|
|
757
769
|
failure: ProtocolFailure;
|
|
758
770
|
};
|
|
759
771
|
export declare const TASK_COMMAND_NAMES: {
|
|
760
|
-
readonly voice: readonly ["answer", "decline", "
|
|
772
|
+
readonly voice: readonly ["answer", "decline", "call", "mute", "hold", "resume", "disconnect", "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"];
|
|
761
773
|
readonly chat: readonly ["accept", "decline", "pause", "resume", "complete"];
|
|
762
774
|
readonly email: readonly ["accept", "decline", "complete"];
|
|
763
775
|
};
|
|
@@ -770,8 +782,11 @@ export type VoiceTaskCommand = {
|
|
|
770
782
|
type: "answer";
|
|
771
783
|
} | {
|
|
772
784
|
type: "decline";
|
|
773
|
-
}
|
|
774
|
-
|
|
785
|
+
}
|
|
786
|
+
/** In `preview`: place the call to the party whose record the agent has read. A dial, gated by the phase alone. */
|
|
787
|
+
| {
|
|
788
|
+
type: "call";
|
|
789
|
+
dialId: DialId;
|
|
775
790
|
} | {
|
|
776
791
|
type: "mute";
|
|
777
792
|
muted: boolean;
|
|
@@ -787,17 +802,17 @@ export type VoiceTaskCommand = {
|
|
|
787
802
|
type: "connect-back";
|
|
788
803
|
dialId: DialId;
|
|
789
804
|
}
|
|
790
|
-
/**
|
|
805
|
+
/** Cold: hand the customer to `destination` with nobody spoken to first. Gated by `coldTransfer`. */
|
|
791
806
|
| {
|
|
792
807
|
type: "transfer";
|
|
808
|
+
action: "cold";
|
|
793
809
|
dialId: DialId;
|
|
794
810
|
destination: string;
|
|
795
|
-
action?: never;
|
|
796
811
|
}
|
|
797
|
-
/**
|
|
812
|
+
/** Warm: park the customer and call `destination` first. Gated by `warmTransfer`. */
|
|
798
813
|
| {
|
|
799
814
|
type: "transfer";
|
|
800
|
-
action: "
|
|
815
|
+
action: "warm";
|
|
801
816
|
dialId: DialId;
|
|
802
817
|
destination: string;
|
|
803
818
|
}
|
|
@@ -1255,7 +1270,6 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1255
1270
|
type: "task-offered";
|
|
1256
1271
|
task: Task<C>;
|
|
1257
1272
|
allocationExpiresAt?: IsoTimestamp;
|
|
1258
|
-
preparationEndsAt?: IsoTimestamp;
|
|
1259
1273
|
} | {
|
|
1260
1274
|
type: "task-updated";
|
|
1261
1275
|
task: Task<C>;
|
|
@@ -1372,7 +1386,7 @@ export declare const DEFAULT_TASK_PHASE_LABELS: {
|
|
|
1372
1386
|
readonly voice: {
|
|
1373
1387
|
readonly pending: "Offered";
|
|
1374
1388
|
readonly confirmed: "Accepted";
|
|
1375
|
-
readonly
|
|
1389
|
+
readonly preview: "Preview";
|
|
1376
1390
|
readonly "in-progress": "On Call";
|
|
1377
1391
|
readonly paused: "On Hold";
|
|
1378
1392
|
readonly completing: "After Call Work";
|
|
@@ -1380,7 +1394,7 @@ export declare const DEFAULT_TASK_PHASE_LABELS: {
|
|
|
1380
1394
|
readonly chat: {
|
|
1381
1395
|
readonly pending: "Incoming Chat";
|
|
1382
1396
|
readonly confirmed: "Accepted";
|
|
1383
|
-
readonly
|
|
1397
|
+
readonly preview: "Preview";
|
|
1384
1398
|
readonly "in-progress": "In Chat";
|
|
1385
1399
|
readonly paused: "Paused";
|
|
1386
1400
|
readonly completing: "Wrap-up";
|
|
@@ -1388,7 +1402,7 @@ export declare const DEFAULT_TASK_PHASE_LABELS: {
|
|
|
1388
1402
|
readonly email: {
|
|
1389
1403
|
readonly pending: "Assigned";
|
|
1390
1404
|
readonly confirmed: "Accepted";
|
|
1391
|
-
readonly
|
|
1405
|
+
readonly preview: "Preview";
|
|
1392
1406
|
readonly "in-progress": "Working";
|
|
1393
1407
|
readonly paused: "Paused";
|
|
1394
1408
|
readonly completing: "Completing";
|
package/dist/index.js
CHANGED
|
@@ -71,16 +71,16 @@ export function effectiveLevels(declared) {
|
|
|
71
71
|
// Task commands.
|
|
72
72
|
// ---------------------------------------------------------------------------
|
|
73
73
|
export const TASK_COMMAND_NAMES = {
|
|
74
|
-
voice: ["answer", "decline", "
|
|
74
|
+
voice: ["answer", "decline", "call", "mute", "hold", "resume", "disconnect",
|
|
75
75
|
"connect-back", "transfer", "lead-assist", "conference", "recording", "complete"],
|
|
76
76
|
chat: ["accept", "decline", "pause", "resume", "complete"],
|
|
77
77
|
email: ["accept", "decline", "complete"],
|
|
78
78
|
};
|
|
79
79
|
/** The dial a command places, by the host's identity for it; `undefined` for a command that dials nothing. */
|
|
80
80
|
export function commandDialId(command) {
|
|
81
|
-
if (command.type === "connect-back")
|
|
81
|
+
if (command.type === "connect-back" || command.type === "call")
|
|
82
82
|
return command.dialId;
|
|
83
|
-
if (command.type === "transfer" && (command.action ===
|
|
83
|
+
if (command.type === "transfer" && (command.action === "cold" || command.action === "warm"))
|
|
84
84
|
return command.dialId;
|
|
85
85
|
if (command.type === "conference" && command.action === "add")
|
|
86
86
|
return command.dialId;
|
|
@@ -117,15 +117,15 @@ export function defineAdapter(adapter) {
|
|
|
117
117
|
// ---------------------------------------------------------------------------
|
|
118
118
|
export const DEFAULT_TASK_PHASE_LABELS = {
|
|
119
119
|
voice: {
|
|
120
|
-
pending: "Offered", confirmed: "Accepted",
|
|
120
|
+
pending: "Offered", confirmed: "Accepted", preview: "Preview",
|
|
121
121
|
"in-progress": "On Call", paused: "On Hold", completing: "After Call Work",
|
|
122
122
|
},
|
|
123
123
|
chat: {
|
|
124
|
-
pending: "Incoming Chat", confirmed: "Accepted",
|
|
124
|
+
pending: "Incoming Chat", confirmed: "Accepted", preview: "Preview",
|
|
125
125
|
"in-progress": "In Chat", paused: "Paused", completing: "Wrap-up",
|
|
126
126
|
},
|
|
127
127
|
email: {
|
|
128
|
-
pending: "Assigned", confirmed: "Accepted",
|
|
128
|
+
pending: "Assigned", confirmed: "Accepted", preview: "Preview",
|
|
129
129
|
"in-progress": "Working", paused: "Paused", completing: "Completing",
|
|
130
130
|
},
|
|
131
131
|
};
|
package/dist/testing.js
CHANGED
|
@@ -70,7 +70,7 @@ function observeTask(value, seen) {
|
|
|
70
70
|
const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
|
|
71
71
|
if (isRecord(capabilities.dispositions))
|
|
72
72
|
seen.add("task.dispositions");
|
|
73
|
-
for (const directory of ["
|
|
73
|
+
for (const directory of ["coldTransfer", "warmTransfer", "conference"]) {
|
|
74
74
|
const declared = capabilities[directory];
|
|
75
75
|
if (isRecord(declared) && some(declared.destinations))
|
|
76
76
|
seen.add("task.destinations");
|
package/dist/validation.js
CHANGED
|
@@ -38,7 +38,7 @@ const membersOf = (members) => Object.keys(members);
|
|
|
38
38
|
const CHANNELS = membersOf({ voice: true, chat: true, email: true });
|
|
39
39
|
const TRANSPORT_RECOVERIES = membersOf({ reconnect: true, reauthenticate: true });
|
|
40
40
|
const TASK_PHASES = membersOf({
|
|
41
|
-
pending: true, confirmed: true,
|
|
41
|
+
pending: true, confirmed: true, preview: true, "in-progress": true, paused: true, completing: true,
|
|
42
42
|
});
|
|
43
43
|
const COMPLETION_MODES = membersOf({ "agent-command": true, "provider-automatic": true });
|
|
44
44
|
const ACCEPTANCE_MODES = membersOf({
|
|
@@ -73,7 +73,7 @@ const EXPLAINED_FAILURES = ["busy", "no-answer", "unreachable", "rejected", "can
|
|
|
73
73
|
const ON_CALL_ROLES = membersOf({ party: true, agent: true, consulted: true, conferenced: true });
|
|
74
74
|
const ON_CALL_STAGES = membersOf({ ringing: true, joined: true });
|
|
75
75
|
/** The task capabilities under which a command dials, and so need the manifest to say how a dial ends. */
|
|
76
|
-
const DIALLING_CAPABILITIES = ["connectBack", "
|
|
76
|
+
const DIALLING_CAPABILITIES = ["connectBack", "coldTransfer", "warmTransfer", "conference"];
|
|
77
77
|
const SNAPSHOT_REASONS = membersOf({
|
|
78
78
|
reconnected: true, "provider-requested": true,
|
|
79
79
|
});
|
|
@@ -82,20 +82,21 @@ const MEMBER_BREAKS = membersOf({
|
|
|
82
82
|
"awaiting-decision": true, granted: true, "starting-after-task": true,
|
|
83
83
|
});
|
|
84
84
|
const OFFERABLE_PHASES = membersOf({
|
|
85
|
-
pending: true, confirmed: true,
|
|
85
|
+
pending: true, confirmed: true, preview: true,
|
|
86
86
|
});
|
|
87
|
+
const PREVIEW_DEADLINES = membersOf({ calls: true, expires: true });
|
|
87
88
|
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true, monitorControl: true });
|
|
88
89
|
const MONITOR_MODES = membersOf({ monitor: true, whisper: true, barge: true });
|
|
89
90
|
const COMPLETED_BY = membersOf({ agent: true, provider: true });
|
|
90
91
|
const EXPIRABLE_PHASES = membersOf({
|
|
91
|
-
pending: true, confirmed: true,
|
|
92
|
+
pending: true, confirmed: true, preview: true,
|
|
92
93
|
});
|
|
93
94
|
const ISOLATION_SCHEME_VALUES = Object.values(BROWSER_ISOLATION_SCHEMES);
|
|
94
95
|
/** The capabilities each channel arm of `TaskCapabilities` declares, keyed off the type itself. */
|
|
95
96
|
const TASK_CAPABILITIES = {
|
|
96
97
|
voice: membersOf({
|
|
97
98
|
browsers: true, dispositions: true, custom: true, decline: true, mute: true, hold: true,
|
|
98
|
-
agentDisconnect: true, connectBack: true,
|
|
99
|
+
agentDisconnect: true, connectBack: true, coldTransfer: true, warmTransfer: true, leadAssist: true, conference: true, recording: true,
|
|
99
100
|
}),
|
|
100
101
|
chat: membersOf({ browsers: true, dispositions: true, custom: true, decline: true, hold: true }),
|
|
101
102
|
email: membersOf({ browsers: true, dispositions: true, custom: true, decline: true }),
|
|
@@ -864,7 +865,28 @@ function validateTaskInto(task, context, path, into) {
|
|
|
864
865
|
into.require(isTaskId(task.id), "task.id", `${path}.id`, "a task needs a non-empty id");
|
|
865
866
|
into.filled(task.title, "task.title", `${path}.title`, "a task needs a title");
|
|
866
867
|
into.filled(task.taskType, "task.taskType", `${path}.taskType`, "a task needs a task type");
|
|
867
|
-
into.oneOf(task.phase, TASK_PHASES, "task.phase", `${path}.phase`)
|
|
868
|
+
if (into.oneOf(task.phase, TASK_PHASES, "task.phase", `${path}.phase`) && task.phase === "preview") {
|
|
869
|
+
// A preview is a record waiting for the agent to press Call: only voice has a call to place,
|
|
870
|
+
// and pressing Call is a dial, so the manifest has to have said how a dial ends.
|
|
871
|
+
into.require(context.channel === "voice", "task.phase.channel", `${path}.phase`, `a ${context.channel} task has no call to preview`);
|
|
872
|
+
into.require(context.dialOutcomesDeclared !== false, "task.preview.dialOutcomes.required", `${path}.phase`, "a preview ends in the agent pressing Call, which dials, and the manifest declares no dialOutcomes to say how a dial ends");
|
|
873
|
+
}
|
|
874
|
+
// The deadline and what happens at it travel together, and only while the record is being previewed.
|
|
875
|
+
if (task.previewEndsAt !== undefined || task.atDeadline !== undefined) {
|
|
876
|
+
into.require(task.phase === "preview", "task.preview.deadline.unexpected", `${path}.previewEndsAt`, "previewEndsAt and atDeadline belong to a task in preview; past it there is nothing to wait for");
|
|
877
|
+
if (task.previewEndsAt === undefined) {
|
|
878
|
+
into.add("task.preview.previewEndsAt.required", `${path}.previewEndsAt`, "atDeadline says what happens at a deadline, so there has to be one");
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
into.timestamp(task.previewEndsAt, "task.preview.previewEndsAt", `${path}.previewEndsAt`);
|
|
882
|
+
}
|
|
883
|
+
if (task.atDeadline === undefined) {
|
|
884
|
+
into.add("task.preview.atDeadline.required", `${path}.atDeadline`, "a preview with a deadline says what the system does at it: calls, or expires -- an agent counting down has to know which");
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
into.oneOf(task.atDeadline, PREVIEW_DEADLINES, "task.preview.atDeadline", `${path}.atDeadline`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
868
890
|
// Acceptance is the offer's word, carried on the pending task so a snapshot can say it: it
|
|
869
891
|
// travels exactly when Omni said tasks may be auto-accepted, and only while the task is pending.
|
|
870
892
|
if (task.acceptance !== undefined) {
|
|
@@ -948,8 +970,8 @@ function validateTaskInto(task, context, path, into) {
|
|
|
948
970
|
case "custom":
|
|
949
971
|
validateCustomCapabilities(declared, `${path}.capabilities.custom`, into);
|
|
950
972
|
break;
|
|
951
|
-
case "
|
|
952
|
-
case "
|
|
973
|
+
case "coldTransfer":
|
|
974
|
+
case "warmTransfer":
|
|
953
975
|
case "conference":
|
|
954
976
|
validateDestinationDirectory(declared, `${path}.capabilities.${name}`, into);
|
|
955
977
|
break;
|
|
@@ -1386,10 +1408,8 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1386
1408
|
if (isPlainObject(event.task) && typeof event.task.phase === "string") {
|
|
1387
1409
|
into.require(OFFERABLE_PHASES.includes(event.task.phase), "event.taskOffered.phase", `${at}.task.phase`, `task-offered introduces a task as ${OFFERABLE_PHASES.join(", ")}, never as ${event.task.phase}`);
|
|
1388
1410
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
into.timestamp(event[field], `event.taskOffered.${field}`, `${at}.${field}`);
|
|
1392
|
-
}
|
|
1411
|
+
if (event.allocationExpiresAt !== undefined)
|
|
1412
|
+
into.timestamp(event.allocationExpiresAt, "event.taskOffered.allocationExpiresAt", `${at}.allocationExpiresAt`);
|
|
1393
1413
|
break;
|
|
1394
1414
|
case "task-updated":
|
|
1395
1415
|
validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest) }, `${at}.task`, into);
|
package/guide.md
CHANGED
|
@@ -35,7 +35,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
35
35
|
| **Event** | One completed transaction reported after a snapshot established the baseline. |
|
|
36
36
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
37
37
|
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
|
|
38
|
-
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a transfer, a conference add, or a connect-back. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
|
|
38
|
+
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a cold or warm transfer, a conference add, or a connect-back. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
|
|
39
39
|
| **Monitor** | A lead listening to a member's call unasked, from the roster: `monitor` in silence, `whisper` heard by the agent alone, `barge` heard by everyone. Nothing of it reaches the member's task, and there is no take-over in it. See **Monitoring a call**. |
|
|
40
40
|
| **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in from the moment their dial is placed. |
|
|
41
41
|
|
|
@@ -45,7 +45,7 @@ transport, a task, a break and a call each have a word of their own:
|
|
|
45
45
|
|
|
46
46
|
| Word | Belongs to | Values |
|
|
47
47
|
| --- | --- | --- |
|
|
48
|
-
| `phase` | A task | `pending`, `confirmed`, `
|
|
48
|
+
| `phase` | A task | `pending`, `confirmed`, `preview`, `in-progress`, `paused`, `completing` |
|
|
49
49
|
| `media` | A task's audio | `started`, `ended` |
|
|
50
50
|
| `transport` | A connection | `connecting`, `active`, `error` |
|
|
51
51
|
| `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
|
|
@@ -481,8 +481,8 @@ type TaskCapabilities<C extends Channel = Channel> =
|
|
|
481
481
|
hold?: Lockable<true>;
|
|
482
482
|
agentDisconnect?: Lockable<true>;
|
|
483
483
|
connectBack?: Lockable<true>;
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
coldTransfer?: Lockable<true | DestinationDirectory>;
|
|
485
|
+
warmTransfer?: Lockable<true | DestinationDirectory>;
|
|
486
486
|
leadAssist?: Lockable<true>;
|
|
487
487
|
conference?: Lockable<true | DestinationDirectory>;
|
|
488
488
|
recording?: Lockable<true>;
|
|
@@ -542,7 +542,7 @@ default — see **Choosing an isolation scheme**.
|
|
|
542
542
|
type TaskPhase =
|
|
543
543
|
| "pending"
|
|
544
544
|
| "confirmed"
|
|
545
|
-
| "
|
|
545
|
+
| "preview"
|
|
546
546
|
| "in-progress"
|
|
547
547
|
| "paused"
|
|
548
548
|
| "completing";
|
|
@@ -647,6 +647,8 @@ type Task<C extends Channel = Channel> = {
|
|
|
647
647
|
party?: Contact;
|
|
648
648
|
phase: TaskPhase;
|
|
649
649
|
acceptance?: AcceptanceMode;
|
|
650
|
+
previewEndsAt?: IsoTimestamp;
|
|
651
|
+
atDeadline?: PreviewDeadline;
|
|
650
652
|
reference?: string;
|
|
651
653
|
attributes?: TaskAttribute[];
|
|
652
654
|
handlingHistory?: TaskHandlingHistory;
|
|
@@ -656,6 +658,8 @@ type Task<C extends Channel = Channel> = {
|
|
|
656
658
|
: { onCall?: never; leadAssist?: never; assisting?: never; monitoring?: never; media?: never }
|
|
657
659
|
);
|
|
658
660
|
|
|
661
|
+
type PreviewDeadline = "calls" | "expires";
|
|
662
|
+
|
|
659
663
|
type AcceptanceMode =
|
|
660
664
|
| "no-preference"
|
|
661
665
|
| "consent"
|
|
@@ -665,7 +669,7 @@ type TaskOutcome =
|
|
|
665
669
|
| { type: "completed"; by: "agent" | "provider" }
|
|
666
670
|
| { type: "transferred"; destination?: string }
|
|
667
671
|
| { type: "cancelled"; reason?: string }
|
|
668
|
-
| { type: "expired"; phase: "pending" | "confirmed" | "
|
|
672
|
+
| { type: "expired"; phase: "pending" | "confirmed" | "preview" }
|
|
669
673
|
| { type: "left" }
|
|
670
674
|
| { type: "failed"; failure: ProtocolFailure };
|
|
671
675
|
```
|
|
@@ -677,7 +681,7 @@ const TASK_COMMAND_NAMES = {
|
|
|
677
681
|
voice: [
|
|
678
682
|
"answer",
|
|
679
683
|
"decline",
|
|
680
|
-
"
|
|
684
|
+
"call",
|
|
681
685
|
"mute",
|
|
682
686
|
"hold",
|
|
683
687
|
"resume",
|
|
@@ -701,14 +705,14 @@ type DispositionPayload = { disposition?: string; notes?: string };
|
|
|
701
705
|
type VoiceTaskCommand =
|
|
702
706
|
| { type: "answer" }
|
|
703
707
|
| { type: "decline" }
|
|
704
|
-
| { type: "
|
|
708
|
+
| { type: "call"; dialId: DialId }
|
|
705
709
|
| { type: "mute"; muted: boolean }
|
|
706
710
|
| { type: "hold" }
|
|
707
711
|
| { type: "resume" }
|
|
708
712
|
| { type: "disconnect" }
|
|
709
713
|
| { type: "connect-back"; dialId: DialId }
|
|
710
|
-
| { type: "transfer"; dialId: DialId; destination: string
|
|
711
|
-
| { type: "transfer"; action: "
|
|
714
|
+
| { type: "transfer"; action: "cold"; dialId: DialId; destination: string }
|
|
715
|
+
| { type: "transfer"; action: "warm"; dialId: DialId; destination: string }
|
|
712
716
|
| { type: "transfer"; action: "complete" }
|
|
713
717
|
| { type: "transfer"; action: "cancel" }
|
|
714
718
|
| { type: "lead-assist"; action: "request"; note?: string }
|
|
@@ -997,7 +1001,6 @@ type ProviderEvent =
|
|
|
997
1001
|
type: "task-offered";
|
|
998
1002
|
task: Task;
|
|
999
1003
|
allocationExpiresAt?: IsoTimestamp;
|
|
1000
|
-
preparationEndsAt?: IsoTimestamp;
|
|
1001
1004
|
}
|
|
1002
1005
|
| { type: "task-updated"; task: Task }
|
|
1003
1006
|
| { type: "task-media-started"; taskId: TaskId }
|
|
@@ -1163,7 +1166,7 @@ const DEFAULT_TASK_PHASE_LABELS = {
|
|
|
1163
1166
|
voice: {
|
|
1164
1167
|
pending: "Offered",
|
|
1165
1168
|
confirmed: "Accepted",
|
|
1166
|
-
|
|
1169
|
+
preview: "Preview",
|
|
1167
1170
|
"in-progress": "On Call",
|
|
1168
1171
|
paused: "On Hold",
|
|
1169
1172
|
completing: "After Call Work",
|
|
@@ -1171,7 +1174,7 @@ const DEFAULT_TASK_PHASE_LABELS = {
|
|
|
1171
1174
|
chat: {
|
|
1172
1175
|
pending: "Incoming Chat",
|
|
1173
1176
|
confirmed: "Accepted",
|
|
1174
|
-
|
|
1177
|
+
preview: "Preview",
|
|
1175
1178
|
"in-progress": "In Chat",
|
|
1176
1179
|
paused: "Paused",
|
|
1177
1180
|
completing: "Wrap-up",
|
|
@@ -1179,7 +1182,7 @@ const DEFAULT_TASK_PHASE_LABELS = {
|
|
|
1179
1182
|
email: {
|
|
1180
1183
|
pending: "Assigned",
|
|
1181
1184
|
confirmed: "Accepted",
|
|
1182
|
-
|
|
1185
|
+
preview: "Preview",
|
|
1183
1186
|
"in-progress": "Working",
|
|
1184
1187
|
paused: "Paused",
|
|
1185
1188
|
completing: "Completing",
|
|
@@ -1283,7 +1286,7 @@ only where the corresponding capability is declared; an absent capability means
|
|
|
1283
1286
|
there is no separate permission flag.
|
|
1284
1287
|
|
|
1285
1288
|
The commands every task has are authorized by other fields the provider declared — a task that was
|
|
1286
|
-
offered can be accepted, one in `
|
|
1289
|
+
offered can be accepted, one in `preview` can be called, one whose `completionMode` is
|
|
1287
1290
|
`agent-command` can be completed. Nothing is issuable that the provider did not publish; only
|
|
1288
1291
|
which field says so varies. See **Which commands need a capability**.
|
|
1289
1292
|
|
|
@@ -2092,7 +2095,6 @@ const allocation = {
|
|
|
2092
2095
|
type: "task-offered",
|
|
2093
2096
|
task: { ...task, phase: "pending", acceptance: "consent" },
|
|
2094
2097
|
allocationExpiresAt: "2026-08-25T10:41:07.000Z",
|
|
2095
|
-
preparationEndsAt: "2026-08-25T10:40:37.000Z",
|
|
2096
2098
|
} satisfies Extract<ProviderEvent, { type: "task-offered" }>;
|
|
2097
2099
|
```
|
|
2098
2100
|
|
|
@@ -2114,10 +2116,8 @@ down and stops offering **Accept** once it passes. **Omit it unless the provider
|
|
|
2114
2116
|
A provider that reports only elapsed ring time after the fact cannot say when an offer is due
|
|
2115
2117
|
to end, and a computed value would have Omni withdraw **Accept** from a task still pending.
|
|
2116
2118
|
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
before the agent must start the contact. Reaching the timestamp does not imply a transition; the
|
|
2120
|
-
provider reports what happens next through an event.
|
|
2119
|
+
A preview's deadline is not on the offer. It travels on the task, as `previewEndsAt` with
|
|
2120
|
+
`atDeadline`, so a snapshot carries it too -- see **Preview: the agent presses Call**.
|
|
2121
2121
|
|
|
2122
2122
|
A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome.
|
|
2123
2123
|
|
|
@@ -2151,9 +2151,11 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2151
2151
|
| `capabilities` | Controls and workspace features available for this specific task. |
|
|
2152
2152
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2153
2153
|
| `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
|
|
2154
|
-
| `phase` | Current canonical task phase: `pending`, `confirmed`, `
|
|
2154
|
+
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2155
2155
|
| `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. |
|
|
2156
2156
|
| `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
|
|
2157
|
+
| `previewEndsAt` | Voice only, in `preview`: when the system stops waiting for the agent to press Call. Absent, the agent has as long as they need. Always with `atDeadline`. See **Preview: the agent presses Call**. |
|
|
2158
|
+
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` places the call itself, `expires` takes the record back and the task ends `expired`. |
|
|
2157
2159
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2158
2160
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2159
2161
|
| `wrapAllowance` | Fixed time allowed to complete the task after primary handling ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
|
|
@@ -2192,24 +2194,64 @@ The canonical task transitions are:
|
|
|
2192
2194
|
| --- | --- | --- |
|
|
2193
2195
|
| No task | Provider allocates a task | `pending` |
|
|
2194
2196
|
| `pending` | Task is accepted | `confirmed` |
|
|
2195
|
-
| `confirmed` |
|
|
2196
|
-
| `confirmed` | Work begins
|
|
2197
|
-
| `
|
|
2197
|
+
| `confirmed` | The customer's record is put in front of the agent before any call goes out (voice) | `preview` |
|
|
2198
|
+
| `confirmed` | Work begins | `in-progress` |
|
|
2199
|
+
| `preview` | Agent presses Call (`call`) and the customer answers, or the deadline `calls` and they answer | `in-progress` |
|
|
2200
|
+
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2201
|
+
| `preview` | The deadline `expires` | Removed by `task-ended` with `expired` outcome |
|
|
2198
2202
|
| `pending` | Provider withdraws the allocation | Removed by `task-ended` with `cancelled` outcome |
|
|
2199
2203
|
| No task | Snapshot reports work already underway | `in-progress` |
|
|
2200
2204
|
| `in-progress` | Provider or agent pauses the task | `paused` |
|
|
2201
|
-
| `in-progress` | Agent
|
|
2205
|
+
| `in-progress` | Agent starts a warm transfer (`transfer` `warm`); the customer is parked | `paused` |
|
|
2202
2206
|
| `paused` | Provider or agent resumes the task | `in-progress` |
|
|
2203
|
-
| `paused` | Agent cancels a
|
|
2207
|
+
| `paused` | Agent cancels a warm transfer (`transfer` `cancel`) | `in-progress` |
|
|
2204
2208
|
| `in-progress` or `paused` | Contact handling ends and follow-up work remains | `completing` |
|
|
2205
2209
|
| `completing` | Agent connects back to the party (`connect-back`) | `in-progress` |
|
|
2206
2210
|
| Any phase | Provider emits `task-ended` | Removed |
|
|
2207
2211
|
|
|
2208
2212
|
Allocation, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
2209
2213
|
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
2210
|
-
subsequent transitions to `
|
|
2214
|
+
subsequent transitions to `preview` or `in-progress`; Omni does not infer them from the acceptance
|
|
2211
2215
|
command.
|
|
2212
2216
|
|
|
2217
|
+
#### Preview: the agent presses Call
|
|
2218
|
+
|
|
2219
|
+
An outbound campaign that lets the agent see who they are about to call is a **preview**: the
|
|
2220
|
+
record arrives as a task, the task sits in `preview` with the party on it, and no call has gone
|
|
2221
|
+
out. The agent reads and presses Call. That is the whole phase, and it exists on voice alone --
|
|
2222
|
+
chat and email have no call to place, and their reading is ordinary work in `in-progress`.
|
|
2223
|
+
|
|
2224
|
+
```ts
|
|
2225
|
+
const previewed = {
|
|
2226
|
+
channel: "voice",
|
|
2227
|
+
capabilities: {},
|
|
2228
|
+
phase: "preview",
|
|
2229
|
+
party: { name: "Maya Rao", number: "+919876543210" },
|
|
2230
|
+
previewEndsAt: "2026-08-25T10:40:37.000Z",
|
|
2231
|
+
atDeadline: "calls",
|
|
2232
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "party" | "previewEndsAt" | "atDeadline">;
|
|
2233
|
+
|
|
2234
|
+
// The agent presses Call. It is a dial like any other.
|
|
2235
|
+
const pressed: TaskCommand<"voice"> = { type: "call", dialId: "dial-7f2" };
|
|
2236
|
+
```
|
|
2237
|
+
|
|
2238
|
+
**Call is a dial.** It carries the host's `dialId`, is answered `dialling`, and ends in exactly one
|
|
2239
|
+
`dial-outcome`; the phase is its gate and there is no capability, since a record put in front of an
|
|
2240
|
+
agent is there to be called. On `answered` the task is `in-progress` and its media follows on
|
|
2241
|
+
`task-media-started`. On any other outcome the task goes to `completing`, so the agent records the
|
|
2242
|
+
no-answer as the disposition it is -- in a campaign that is the commonest outcome there is, and it
|
|
2243
|
+
is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
|
|
2244
|
+
(`task.preview.dialOutcomes.required`).
|
|
2245
|
+
|
|
2246
|
+
**The deadline says what the system will do.** A provider that will not wait forever states
|
|
2247
|
+
`previewEndsAt`, and with it `atDeadline`: `calls` means the system places the call itself when the
|
|
2248
|
+
time runs out, which is progressive dialling; `expires` means the record is taken back and the task
|
|
2249
|
+
ends with an `expired` outcome naming `preview`. The two travel together
|
|
2250
|
+
(`task.preview.atDeadline.required`, `task.preview.previewEndsAt.required`), only while the task is in
|
|
2251
|
+
`preview` (`task.preview.deadline.unexpected`), and Omni's countdown reads "calling in 12s" or
|
|
2252
|
+
"expires in 12s" from the provider's word. With neither, the agent has as long as they need.
|
|
2253
|
+
Reaching the instant is not itself a transition: the provider reports what it did, as an event.
|
|
2254
|
+
|
|
2213
2255
|
**A task is never its audio.** A voice task is the allocation: the call is offered when it is
|
|
2214
2256
|
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
2215
2257
|
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
@@ -2591,8 +2633,8 @@ const taskCapabilities = {
|
|
|
2591
2633
|
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
2592
2634
|
| `agentDisconnect` | Primary button: Disconnect | Omni may disconnect real-time media without disposing the task. |
|
|
2593
2635
|
| `connectBack` | Completing-task button: Connect back | Omni may have the provider connect the agent back to the task's party while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance disposes at provider end. See **Connecting back during completion**. |
|
|
2594
|
-
| `
|
|
2595
|
-
| `
|
|
2636
|
+
| `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
|
|
2637
|
+
| `warmTransfer` | Secondary menu item: Warm transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Warm transfer**. |
|
|
2596
2638
|
| `leadAssist` | Secondary menu item: Lead assist | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.leadAssist`. See **Lead assist**. |
|
|
2597
2639
|
| `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and drop a `conferenced` entry from it -- one still ringing included, which calls the dial off. |
|
|
2598
2640
|
| `recording` | Overflow menu item: Recording | Omni may expose start, pause, resume, and stop recording controls. |
|
|
@@ -2627,11 +2669,11 @@ capabilities: {
|
|
|
2627
2669
|
With `dispositions: true` Omni shows a Complete control and sends `complete` with no code, because
|
|
2628
2670
|
the provider published none.
|
|
2629
2671
|
|
|
2630
|
-
#### `
|
|
2672
|
+
#### `coldTransfer`, `warmTransfer` and `conference`
|
|
2631
2673
|
|
|
2632
2674
|
```ts
|
|
2633
2675
|
capabilities: {
|
|
2634
|
-
|
|
2676
|
+
coldTransfer: {
|
|
2635
2677
|
allowManualEntry: false,
|
|
2636
2678
|
destinations: [
|
|
2637
2679
|
{ id: "tier2", label: "Tier 2 support", address: "+14155550111", kind: "queue" },
|
|
@@ -2659,18 +2701,19 @@ A destination the agent types is not in the directory and has no `kind`. Omni tr
|
|
|
2659
2701
|
`external` unless the provider says otherwise in its response, because that is the assumption that
|
|
2660
2702
|
does not overstate what the provider can still see.
|
|
2661
2703
|
|
|
2662
|
-
####
|
|
2704
|
+
#### Warm transfer
|
|
2663
2705
|
|
|
2664
|
-
A
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2706
|
+
A warm transfer parks the customer, calls the destination so the agent can speak to it first, and
|
|
2707
|
+
then either hands the customer over or returns to them. Cold and warm are the words agents use;
|
|
2708
|
+
telephony says blind and consult. It is its own capability, distinct from `coldTransfer` (a
|
|
2709
|
+
hand-over with nobody spoken to first) and from `conference` (everybody on one call): a queue may
|
|
2710
|
+
offer any of the three without the others, and each is declared on its own.
|
|
2668
2711
|
|
|
2669
2712
|
```ts
|
|
2670
|
-
// 1.
|
|
2713
|
+
// 1. Warm. A dial: the provider parks the customer and calls the destination, answers
|
|
2671
2714
|
// `dialling`, and the task reports `paused` with the destination `consulted` on `onCall`
|
|
2672
2715
|
// while the call to it stands.
|
|
2673
|
-
{ type: "transfer", action: "
|
|
2716
|
+
{ type: "transfer", action: "warm", dialId: "dial-7f2", destination: "+14155550111" }
|
|
2674
2717
|
|
|
2675
2718
|
// 2a. Hand the customer to the consulted destination and leave.
|
|
2676
2719
|
{ type: "transfer", action: "complete" }
|
|
@@ -2679,7 +2722,7 @@ call): a queue may offer any of the three without the others, and each is declar
|
|
|
2679
2722
|
{ type: "transfer", action: "cancel" }
|
|
2680
2723
|
```
|
|
2681
2724
|
|
|
2682
|
-
`
|
|
2725
|
+
`warm` is gated by the `warmTransfer` capability and takes a destination exactly as a cold
|
|
2683
2726
|
transfer does, from the same kind of directory. It is a dial, so it carries the host's `dialId`
|
|
2684
2727
|
and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
|
|
2685
2728
|
the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
|
|
@@ -2687,7 +2730,7 @@ is what makes `complete` and `cancel` issuable -- they name no destination becau
|
|
|
2687
2730
|
one they could mean, which is why a task carries at most one `consulted` entry
|
|
2688
2731
|
(`task.onCall.consulted.single`). A consultation that could be started but not finished would
|
|
2689
2732
|
strand the customer and the destination both, which is why all three are commands and a provider
|
|
2690
|
-
that offers `
|
|
2733
|
+
that offers `warmTransfer` implements all three.
|
|
2691
2734
|
|
|
2692
2735
|
`applied` on `complete` says the provider is bridging the customer to the destination and
|
|
2693
2736
|
dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
|
|
@@ -2836,7 +2879,7 @@ call it belonged to and never against the next one.
|
|
|
2836
2879
|
The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which a host calls when it
|
|
2837
2880
|
places one; an entry on a task's `onCall`; and a step in its `handlingHistory`. Since a dialled entry
|
|
2838
2881
|
is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- a
|
|
2839
|
-
|
|
2882
|
+
cold transfer, a connect-back, an idle dialpad call -- or whose entry has already left the room; an
|
|
2840
2883
|
outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
|
|
2841
2884
|
|
|
2842
2885
|
**`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
|
|
@@ -2855,7 +2898,7 @@ provider does report -- `busy`, `no-answer`, `unreachable`, `rejected` or `cance
|
|
|
2855
2898
|
switch gave none, while one that reports none would be sending the generic failure this set exists
|
|
2856
2899
|
to refuse. A provider that dials at all declares it: an idle
|
|
2857
2900
|
dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `connectBack`,
|
|
2858
|
-
`
|
|
2901
|
+
`coldTransfer`, `warmTransfer` or `conference` under a manifest without it is refused
|
|
2859
2902
|
(`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
|
|
2860
2903
|
error there.
|
|
2861
2904
|
|
|
@@ -3706,10 +3749,10 @@ declared:
|
|
|
3706
3749
|
| --- | --- |
|
|
3707
3750
|
| `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
|
|
3708
3751
|
| `decline` | The `decline` capability on any channel, **and** Omni provisioning permitting it. One word for refusing an offer, whatever the channel. |
|
|
3709
|
-
| `
|
|
3752
|
+
| `call` | The `preview` phase. A record put in front of an agent is there to be called, so the phase is the gate and there is no capability. It is a dial, with a `dialId` and a `dial-outcome`. |
|
|
3710
3753
|
| `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. |
|
|
3711
3754
|
| `connect-back` | The `connectBack` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
|
|
3712
|
-
| `transfer` with `action: "
|
|
3755
|
+
| `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
|
|
3713
3756
|
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
|
|
3714
3757
|
| `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
|
|
3715
3758
|
| `lead-assist` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
@@ -3784,7 +3827,7 @@ They are published as `OMNI_FAILURE_CODES`.
|
|
|
3784
3827
|
#### Provider instants are read against a provider clock
|
|
3785
3828
|
|
|
3786
3829
|
Every deadline in this contract is a provider instant that Omni counts down: `allocationExpiresAt`,
|
|
3787
|
-
`
|
|
3830
|
+
`previewEndsAt`, and the wrap deadline of `task-media-ended` plus `wrapAllowance` where
|
|
3788
3831
|
one is stated.
|
|
3789
3832
|
Comparing those against the host clock is wrong by whatever the two machines disagree by, and the
|
|
3790
3833
|
damaging direction is early — **Accept** withdrawn from an offer still ringing, a wrap timer
|