@xema/omni-protocol 0.1.83 → 0.1.84
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 +29 -29
- package/dist/index.js +7 -7
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +34 -34
- package/dist/validation.d.ts +2 -2
- package/dist/validation.js +59 -56
- package/guide.md +157 -130
- package/package.json +1 -1
package/dist/validation.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Each list is pinned to its type both ways -- a member the type lacks, or a member the list
|
|
10
10
|
// lacks, fails to compile -- so what the validators accept cannot drift from what the
|
|
11
11
|
// declarations say.
|
|
12
|
-
import { RECORDING_ACTIONS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, MONITORING_BREAK_KINDS, BROWSER_ISOLATION_SCHEMES,
|
|
12
|
+
import { RECORDING_ACTIONS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, MONITORING_BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, INTERACTION_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, TASK_COMMAND_NAMES, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
|
|
13
13
|
export class ProtocolConformanceError extends Error {
|
|
14
14
|
violations;
|
|
15
15
|
constructor(violations, summary = "Adapter violates the Omni protocol") {
|
|
@@ -57,7 +57,7 @@ const BREAK_APPROVALS = membersOf({
|
|
|
57
57
|
const TEAM_AVAILABILITIES = membersOf({
|
|
58
58
|
ready: true, "on-task": true, "on-break": true, reserved: true, "signed-out": true,
|
|
59
59
|
});
|
|
60
|
-
const
|
|
60
|
+
const INTERACTION_STEPS = membersOf({
|
|
61
61
|
queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
|
|
62
62
|
});
|
|
63
63
|
const CUSTOM_UI_CONTROLS = membersOf({ button: true, toggle: true, "menu-item": true });
|
|
@@ -522,7 +522,8 @@ export function validateManifest(manifest, path = "manifest") {
|
|
|
522
522
|
if (manifest.runningStepReports !== undefined) {
|
|
523
523
|
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");
|
|
524
524
|
}
|
|
525
|
-
into.require(
|
|
525
|
+
into.require(!Object.hasOwn(manifest, "disposalSettleMs"), "manifest.completionSettleMs.renamed", `${path}.disposalSettleMs`, "use completionSettleMs; the former field is not accepted");
|
|
526
|
+
into.require(typeof manifest.completionSettleMs === "number" && Number.isInteger(manifest.completionSettleMs) && manifest.completionSettleMs > 0, "manifest.completionSettleMs", `${path}.completionSettleMs`, "completionSettleMs is how long after an applied completion the task-ended is owed, a positive whole number of milliseconds, stated by every provider");
|
|
526
527
|
if (manifest.orgLevels !== undefined) {
|
|
527
528
|
if (!Array.isArray(manifest.orgLevels)) {
|
|
528
529
|
into.add("manifest.orgLevels.shape", `${path}.orgLevels`, "orgLevels must be an array when present");
|
|
@@ -879,81 +880,82 @@ function validateTaskAttributes(value, path, into, levels) {
|
|
|
879
880
|
}
|
|
880
881
|
});
|
|
881
882
|
}
|
|
882
|
-
function
|
|
883
|
+
function validateInteractionHistory(value, path, into, task = {}) {
|
|
883
884
|
if (value === undefined)
|
|
884
885
|
return;
|
|
885
886
|
if (!isPlainObject(value)) {
|
|
886
|
-
into.add("task.
|
|
887
|
+
into.add("task.interactionHistory.shape", path, "interactionHistory must be an object with its steps when present");
|
|
887
888
|
return;
|
|
888
889
|
}
|
|
889
890
|
// What the record adds up to before this agent: each total present when the provider knows it
|
|
890
891
|
// and absent when it does not, never a plausible nought.
|
|
891
|
-
|
|
892
|
+
into.require(!Object.hasOwn(value, "handleSeconds"), "task.interactionHistory.interactionSeconds.renamed", `${path}.handleSeconds`, "use interactionSeconds; the former field is not accepted");
|
|
893
|
+
for (const field of ["interactionSeconds", "holdSeconds", "queueSeconds"]) {
|
|
892
894
|
if (value[field] !== undefined) {
|
|
893
|
-
into.require(isDurationSeconds(value[field]), `task.
|
|
895
|
+
into.require(isDurationSeconds(value[field]), `task.interactionHistory.${field}`, `${path}.${field}`, `${field} must be a whole number of seconds, zero or more, or omitted when unknown`);
|
|
894
896
|
}
|
|
895
897
|
}
|
|
896
898
|
if (value.transfers !== undefined) {
|
|
897
|
-
into.require(typeof value.transfers === "number" && Number.isInteger(value.transfers) && value.transfers >= 0, "task.
|
|
899
|
+
into.require(typeof value.transfers === "number" && Number.isInteger(value.transfers) && value.transfers >= 0, "task.interactionHistory.transfers", `${path}.transfers`, "transfers must be a whole number, zero or more, or omitted when unknown");
|
|
898
900
|
}
|
|
899
901
|
if (!Array.isArray(value.steps)) {
|
|
900
|
-
into.add("task.
|
|
902
|
+
into.add("task.interactionHistory.steps.shape", `${path}.steps`, "interactionHistory carries its steps as an array, empty when the task has had none");
|
|
901
903
|
return;
|
|
902
904
|
}
|
|
903
905
|
let previous;
|
|
904
906
|
value.steps.forEach((entry, index) => {
|
|
905
907
|
const at = `${path}.steps[${index}]`;
|
|
906
908
|
if (!isPlainObject(entry)) {
|
|
907
|
-
into.add("task.
|
|
909
|
+
into.add("task.interactionHistory.entry", at, "each interaction step must be an object");
|
|
908
910
|
return;
|
|
909
911
|
}
|
|
910
|
-
into.oneOf(entry.step,
|
|
911
|
-
if (into.timestamp(entry.at, "task.
|
|
912
|
+
into.oneOf(entry.step, INTERACTION_STEPS, "task.interactionHistory.step", `${at}.step`);
|
|
913
|
+
if (into.timestamp(entry.at, "task.interactionHistory.at", `${at}.at`)) {
|
|
912
914
|
// The record is one entry per occurrence, oldest first: a second hold is a second entry
|
|
913
915
|
// after the first, never a revision of it or an entry filed out of its turn.
|
|
914
916
|
const instant = Date.parse(entry.at);
|
|
915
917
|
if (previous !== undefined && instant < previous) {
|
|
916
|
-
into.add("task.
|
|
918
|
+
into.add("task.interactionHistory.order", `${at}.at`, "interaction steps are oldest first; this entry is earlier than the one before it");
|
|
917
919
|
}
|
|
918
920
|
previous = instant;
|
|
919
921
|
}
|
|
920
922
|
if (entry.seconds !== undefined) {
|
|
921
923
|
// Omitted while a leg is still running. Nought is a claim that it took no time.
|
|
922
|
-
into.require(isDurationSeconds(entry.seconds) && entry.seconds > 0, "task.
|
|
924
|
+
into.require(isDurationSeconds(entry.seconds) && entry.seconds > 0, "task.interactionHistory.seconds", `${at}.seconds`, "seconds must be a positive whole number; omit it while the step is still running");
|
|
923
925
|
}
|
|
924
926
|
else {
|
|
925
927
|
// An open leg is one still running, and the task says whether it can be: a hold runs only while
|
|
926
928
|
// the task is paused, and a mute only while its media is up. An open entry after that is a leg
|
|
927
929
|
// nobody closed, which reads exactly like a leg running now.
|
|
928
930
|
if (entry.step === "held") {
|
|
929
|
-
into.require(task.phase === "paused", "task.
|
|
931
|
+
into.require(task.phase === "paused", "task.interactionHistory.held.open", `${at}.seconds`, `a held entry without seconds is a hold still running, and the task is ${describeValue(task.phase)}: the hold has ended, and its duration is stated`);
|
|
930
932
|
}
|
|
931
933
|
if (entry.step === "muted") {
|
|
932
|
-
into.require(task.media !== "ended" && task.phase !== "completing", "task.
|
|
934
|
+
into.require(task.media !== "ended" && task.phase !== "completing", "task.interactionHistory.muted.open", `${at}.seconds`, "a muted entry without seconds is still running, but this interaction's media has ended: the provider closes its open leg and states the duration; other channels may continue");
|
|
933
935
|
}
|
|
934
936
|
}
|
|
935
937
|
// A muted entry carries whose the silence was, as the host reported it; no other step has it.
|
|
936
938
|
// And it names the agent: the host has exactly one, the provider knows who, so an unattributed
|
|
937
939
|
// muted leg is a record that dropped a fact it held rather than one it could not establish.
|
|
938
940
|
if (entry.step === "muted") {
|
|
939
|
-
into.oneOf(entry.mutedBy, MUTED_BY, "task.
|
|
940
|
-
into.require(entry.by !== undefined, "task.
|
|
941
|
+
into.oneOf(entry.mutedBy, MUTED_BY, "task.interactionHistory.mutedBy", `${at}.mutedBy`);
|
|
942
|
+
into.require(entry.by !== undefined, "task.interactionHistory.muted.by", `${at}.by`, "a muted leg is attributed to the login's agent: the host reported it, and the provider knows who the host's agent is");
|
|
941
943
|
}
|
|
942
|
-
else if (
|
|
943
|
-
into.require(entry.mutedBy === undefined, "task.
|
|
944
|
+
else if (INTERACTION_STEPS.includes(entry.step)) {
|
|
945
|
+
into.require(entry.mutedBy === undefined, "task.interactionHistory.mutedBy.unexpected", `${at}.mutedBy`, "only a muted step says who silenced the microphone");
|
|
944
946
|
}
|
|
945
947
|
if (entry.by !== undefined) {
|
|
946
|
-
into.require(isUserId(entry.by), "task.
|
|
948
|
+
into.require(isUserId(entry.by), "task.interactionHistory.by", `${at}.by`, "by must be a non-empty user id; omit it when the person cannot be identified");
|
|
947
949
|
// On `queued` nobody takes part, so there is nothing to name.
|
|
948
|
-
into.require(entry.step !== "queued", "task.
|
|
950
|
+
into.require(entry.step !== "queued", "task.interactionHistory.by.unexpected", `${at}.by`, "a queued step names nobody");
|
|
949
951
|
}
|
|
950
952
|
// A step that dialled says where to and, when a host placed it, which dial; no other step dialled.
|
|
951
|
-
const dialled = typeof entry.step === "string" &&
|
|
953
|
+
const dialled = typeof entry.step === "string" && INTERACTION_STEPS_THAT_DIAL.includes(entry.step);
|
|
952
954
|
for (const field of ["dialId", "destinationId"]) {
|
|
953
955
|
if (entry[field] === undefined)
|
|
954
956
|
continue;
|
|
955
|
-
if (into.filled(entry[field], `task.
|
|
956
|
-
into.require(dialled, `task.
|
|
957
|
+
if (into.filled(entry[field], `task.interactionHistory.${field}`, `${at}.${field}`, `${field} must not be empty when present`)) {
|
|
958
|
+
into.require(dialled, `task.interactionHistory.${field}.unexpected`, `${at}.${field}`, `${describeValue(entry.step)} dialled nothing; ${field} belongs on transferred, conferenced, or unanswered`);
|
|
957
959
|
}
|
|
958
960
|
}
|
|
959
961
|
});
|
|
@@ -1119,6 +1121,7 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1119
1121
|
into.add("task.shape", path, "a task must be an object");
|
|
1120
1122
|
return;
|
|
1121
1123
|
}
|
|
1124
|
+
into.require(!Object.hasOwn(task, "handlingHistory"), "task.interactionHistory.renamed", `${path}.handlingHistory`, "use interactionHistory; the former field is not accepted");
|
|
1122
1125
|
into.require(isTaskId(task.id), "task.id", `${path}.id`, "a task needs a non-empty id");
|
|
1123
1126
|
into.require(isAllocationId(task.allocationId), "task.allocationId", `${path}.allocationId`, "a task names this life of itself: an allocation id minted once per offer, never reused for the life of the login");
|
|
1124
1127
|
into.filled(task.title, "task.title", `${path}.title`, "a task needs a title");
|
|
@@ -1205,16 +1208,16 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1205
1208
|
}
|
|
1206
1209
|
validateBrowsers(task.browsers, `${path}.browsers`, into);
|
|
1207
1210
|
validateTaskAttributes(task.attributes, `${path}.attributes`, into, context.levels);
|
|
1208
|
-
|
|
1211
|
+
validateInteractionHistory(task.interactionHistory, `${path}.interactionHistory`, into, { phase: task.phase, media: task.media });
|
|
1209
1212
|
validateOnCall(task.onCall, context.channel, `${path}.onCall`, into);
|
|
1210
|
-
// Media and onCall describe this agent's
|
|
1211
|
-
// Once the
|
|
1212
|
-
// channels may remain connected under another
|
|
1213
|
+
// Media and onCall describe this agent's interaction, not the continuing caller journey.
|
|
1214
|
+
// Once the interaction's media ends or it enters wrap, its live room is cleared. Other
|
|
1215
|
+
// channels may remain connected under another interaction or in IVR/queue stages.
|
|
1213
1216
|
if (task.phase === "completing" && task.media === "started") {
|
|
1214
|
-
into.add("task.media.completing", `${path}.media`, "a completing task's
|
|
1217
|
+
into.add("task.media.completing", `${path}.media`, "a completing task's interaction media has ended or never started; it cannot still be started during wrap");
|
|
1215
1218
|
}
|
|
1216
1219
|
if (Array.isArray(task.onCall) && task.onCall.length > 0 && (task.phase === "completing" || task.media === "ended")) {
|
|
1217
|
-
into.add("task.onCall.ended", `${path}.onCall`, `this
|
|
1220
|
+
into.add("task.onCall.ended", `${path}.onCall`, `this interaction is no longer connected (${task.phase === "completing" ? "the task is completing" : "its media ended"}); clear its onCall view without implying that other channels ended`);
|
|
1218
1221
|
}
|
|
1219
1222
|
validateTaskMedia(task, task.media, context.channel, task.phase, `${path}.media`, into);
|
|
1220
1223
|
validateLeadAssist(task.leadAssist, context.channel, `${path}.leadAssist`, into);
|
|
@@ -2129,34 +2132,34 @@ export function validateHostGuarantees(guarantees, path = "host.guarantees") {
|
|
|
2129
2132
|
* a task, a step, when it began, how long so far if the host says, and an explicit end that
|
|
2130
2133
|
* carries the final duration.
|
|
2131
2134
|
*/
|
|
2132
|
-
export function
|
|
2135
|
+
export function validateInteractionReport(report, path = "interactionReport", manifest) {
|
|
2133
2136
|
const into = new Collector();
|
|
2134
2137
|
if (!isPlainObject(report)) {
|
|
2135
|
-
into.add("
|
|
2138
|
+
into.add("interactionReport.shape", path, "an interaction report must be an object");
|
|
2136
2139
|
return into.violations;
|
|
2137
2140
|
}
|
|
2138
|
-
into.require(isTaskId(report.taskId), "
|
|
2139
|
-
into.require(isAllocationId(report.allocationId), "
|
|
2140
|
-
into.oneOf(report.step,
|
|
2141
|
-
into.timestamp(report.at, "
|
|
2141
|
+
into.require(isTaskId(report.taskId), "interactionReport.taskId", `${path}.taskId`, "a report names the task");
|
|
2142
|
+
into.require(isAllocationId(report.allocationId), "interactionReport.allocationId", `${path}.allocationId`, "a report names the task's allocation, so a late one never lands on the next life of the id");
|
|
2143
|
+
into.oneOf(report.step, INTERACTION_STEPS, "interactionReport.step", `${path}.step`);
|
|
2144
|
+
into.timestamp(report.at, "interactionReport.at", `${path}.at`);
|
|
2142
2145
|
// A muted leg says whose the silence was; no other leg has anyone to name for it.
|
|
2143
2146
|
if (report.step === "muted")
|
|
2144
|
-
into.oneOf(report.mutedBy, MUTED_BY, "
|
|
2145
|
-
else if (
|
|
2146
|
-
into.require(report.mutedBy === undefined, "
|
|
2147
|
+
into.oneOf(report.mutedBy, MUTED_BY, "interactionReport.mutedBy", `${path}.mutedBy`);
|
|
2148
|
+
else if (INTERACTION_STEPS.includes(report.step)) {
|
|
2149
|
+
into.require(report.mutedBy === undefined, "interactionReport.mutedBy.unexpected", `${path}.mutedBy`, "only a muted leg says who silenced the microphone");
|
|
2147
2150
|
}
|
|
2148
2151
|
if (report.seconds !== undefined) {
|
|
2149
|
-
into.require(isDurationSeconds(report.seconds) && report.seconds > 0, "
|
|
2152
|
+
into.require(isDurationSeconds(report.seconds) && report.seconds > 0, "interactionReport.seconds", `${path}.seconds`, "seconds must be a positive whole number; omit it rather than report nought");
|
|
2150
2153
|
}
|
|
2151
2154
|
if (report.ended !== undefined) {
|
|
2152
|
-
if (into.require(report.ended === true, "
|
|
2153
|
-
into.require(report.seconds !== undefined, "
|
|
2155
|
+
if (into.require(report.ended === true, "interactionReport.ended", `${path}.ended`, "ended is declared by presence, as true; a running leg omits it")) {
|
|
2156
|
+
into.require(report.seconds !== undefined, "interactionReport.ended.seconds", `${path}.seconds`, "an ended leg states its final duration");
|
|
2154
2157
|
}
|
|
2155
2158
|
}
|
|
2156
2159
|
else if (report.seconds !== undefined && manifest !== undefined) {
|
|
2157
2160
|
// A running report crosses only to a provider that asked for one: begin and end are the whole
|
|
2158
2161
|
// of what the rest receive.
|
|
2159
|
-
into.require(isPlainObject(manifest) && manifest.runningStepReports === true, "
|
|
2162
|
+
into.require(isPlainObject(manifest) && manifest.runningStepReports === true, "interactionReport.running.unexpected", `${path}.seconds`, "this provider takes begin and end only; a running report was never asked for");
|
|
2160
2163
|
}
|
|
2161
2164
|
return into.violations;
|
|
2162
2165
|
}
|
|
@@ -2245,16 +2248,16 @@ export function validateHostReport(report, path = "host") {
|
|
|
2245
2248
|
const TRANSFER_ACTIONS = ["cold", "warm", "complete", "cancel"];
|
|
2246
2249
|
const LEAD_ASSIST_ACTIONS = ["request", "cancel", "take-over", "leave"];
|
|
2247
2250
|
const CONFERENCE_ACTIONS = ["add", "remove"];
|
|
2248
|
-
/** Validate the exact
|
|
2251
|
+
/** Validate the exact interaction/allocation target before provider command dispatch. */
|
|
2249
2252
|
export function validateTaskCommandRequest(request, task, path = "request", taskContext) {
|
|
2250
2253
|
const into = new Collector();
|
|
2251
2254
|
if (!isPlainObject(request) || !isPlainObject(task)) {
|
|
2252
2255
|
into.add("command.request.shape", path, "a request and the current published task are required");
|
|
2253
2256
|
return into.violations;
|
|
2254
2257
|
}
|
|
2255
|
-
into.filled(request.taskId, "command.request.taskId", `${path}.taskId`, "name the
|
|
2258
|
+
into.filled(request.taskId, "command.request.taskId", `${path}.taskId`, "name the interaction task");
|
|
2256
2259
|
into.filled(request.allocationId, "command.request.allocationId", `${path}.allocationId`, "name its allocation");
|
|
2257
|
-
into.require(request.taskId === task.id, "command.request.taskId.mismatch", `${path}.taskId`, "the command must target this exact
|
|
2260
|
+
into.require(request.taskId === task.id, "command.request.taskId.mismatch", `${path}.taskId`, "the command must target this exact interaction");
|
|
2258
2261
|
into.require(request.allocationId === task.allocationId, "command.request.allocationId.mismatch", `${path}.allocationId`, "the command must target this exact allocation");
|
|
2259
2262
|
for (const key of Object.keys(request))
|
|
2260
2263
|
into.require(["taskId", "allocationId", "command"].includes(key), "command.request.field", `${path}.${key}`, "unsupported task command request field");
|
|
@@ -2412,9 +2415,9 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2412
2415
|
};
|
|
2413
2416
|
const inPhase = (rule, ...phases) => into.require(typeof task.phase === "string" && phases.includes(task.phase), rule, path, `${describeValue(type)} belongs to ${phases.join(" or ")}, and the task is ${describeValue(task.phase)}`);
|
|
2414
2417
|
// A control on the contact acts on a contact being handled. Before in-progress there is nothing
|
|
2415
|
-
// to act on yet, and in completing this agent's
|
|
2418
|
+
// to act on yet, and in completing this agent's interaction has ended. The caller and other
|
|
2416
2419
|
// channels may continue elsewhere; this task's phase no longer permits these controls.
|
|
2417
|
-
const
|
|
2420
|
+
const interaction = () => inPhase("command.phase.interaction", "in-progress", "paused");
|
|
2418
2421
|
// A destination is one the directory offered: the id Omni sends is the id the provider published.
|
|
2419
2422
|
const listed = (name) => {
|
|
2420
2423
|
const declared = capabilities[name];
|
|
@@ -2437,11 +2440,11 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2437
2440
|
case "resume":
|
|
2438
2441
|
case "pause":
|
|
2439
2442
|
offered("hold");
|
|
2440
|
-
|
|
2443
|
+
interaction();
|
|
2441
2444
|
break;
|
|
2442
2445
|
case "end-call":
|
|
2443
2446
|
offered("endCall");
|
|
2444
|
-
|
|
2447
|
+
interaction();
|
|
2445
2448
|
break;
|
|
2446
2449
|
case "recording": {
|
|
2447
2450
|
if (!offered("recording"))
|
|
@@ -2450,11 +2453,11 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2450
2453
|
const state = isPlainObject(task.recording) ? task.recording.provider : undefined;
|
|
2451
2454
|
into.violations.push(...validateRecordingCommandState(command, policy, state, path));
|
|
2452
2455
|
if (command.action === "start" || command.action === "resume") {
|
|
2453
|
-
|
|
2456
|
+
interaction();
|
|
2454
2457
|
into.require(task.media === "started", "recording.request.phase", path, "capture needs live media");
|
|
2455
2458
|
}
|
|
2456
2459
|
else
|
|
2457
|
-
inPhase("command.phase.
|
|
2460
|
+
inPhase("command.phase.interaction", "in-progress", "paused", "completing");
|
|
2458
2461
|
break;
|
|
2459
2462
|
}
|
|
2460
2463
|
case "connect-back":
|
|
@@ -2462,7 +2465,7 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2462
2465
|
inPhase("command.phase.completing", "completing");
|
|
2463
2466
|
break;
|
|
2464
2467
|
case "transfer":
|
|
2465
|
-
|
|
2468
|
+
interaction();
|
|
2466
2469
|
if (command.action === "cold") {
|
|
2467
2470
|
if (offered("coldTransfer"))
|
|
2468
2471
|
listed("coldTransfer");
|
|
@@ -2476,7 +2479,7 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2476
2479
|
}
|
|
2477
2480
|
break;
|
|
2478
2481
|
case "lead-assist":
|
|
2479
|
-
|
|
2482
|
+
interaction();
|
|
2480
2483
|
if (command.action === "request" || command.action === "cancel") {
|
|
2481
2484
|
offered("leadAssist");
|
|
2482
2485
|
if (command.action === "cancel") {
|
|
@@ -2488,7 +2491,7 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2488
2491
|
}
|
|
2489
2492
|
break;
|
|
2490
2493
|
case "conference": {
|
|
2491
|
-
|
|
2494
|
+
interaction();
|
|
2492
2495
|
const conferencing = offered("conference");
|
|
2493
2496
|
if (conferencing && command.action === "add")
|
|
2494
2497
|
listed("conference");
|