@xema/omni-protocol 0.1.83 → 0.1.85

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.
@@ -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, HANDLING_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, TASK_COMMAND_NAMES, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
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 HANDLING_STEPS = membersOf({
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 });
@@ -98,11 +98,11 @@ const ISOLATION_SCHEME_VALUES = Object.values(BROWSER_ISOLATION_SCHEMES);
98
98
  /** The capabilities each channel arm of `TaskCapabilities` declares, keyed off the type itself. */
99
99
  const TASK_CAPABILITIES = {
100
100
  voice: membersOf({
101
- browsers: true, dispositions: true, custom: true, decline: true, hold: true,
101
+ browsers: true, outcomes: true, custom: true, decline: true, hold: true,
102
102
  endCall: true, connectBack: true, coldTransfer: true, warmTransfer: true, leadAssist: true, conference: true, recording: true,
103
103
  }),
104
- chat: membersOf({ browsers: true, dispositions: true, custom: true, decline: true, hold: true }),
105
- email: membersOf({ browsers: true, dispositions: true, custom: true, decline: true }),
104
+ chat: membersOf({ browsers: true, outcomes: true, custom: true, decline: true, hold: true }),
105
+ email: membersOf({ browsers: true, outcomes: true, custom: true, decline: true }),
106
106
  };
107
107
  // The published list and the type's keys are the same set, or one of them is wrong.
108
108
  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(typeof manifest.disposalSettleMs === "number" && Number.isInteger(manifest.disposalSettleMs) && manifest.disposalSettleMs > 0, "manifest.disposalSettleMs", `${path}.disposalSettleMs`, "disposalSettleMs is how long after an applied disposal the task-ended is owed, a positive whole number of milliseconds, stated by every provider");
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");
@@ -606,41 +607,41 @@ function validateDestinationDirectory(value, path, into) {
606
607
  into.filled(destination.label, "task.destination.label", `${at}.label`, "a destination needs a label");
607
608
  });
608
609
  }
609
- function validateDispositions(value, path, into) {
610
+ function validateOutcomes(value, path, into) {
610
611
  if (value === true)
611
612
  return;
612
613
  if (!isPlainObject(value)) {
613
- into.add("task.dispositions.shape", path, "must be true or a disposition policy");
614
+ into.add("task.outcomes.shape", path, "must be true or an outcome policy");
614
615
  return;
615
616
  }
616
617
  if (value.required !== undefined) {
617
- into.require(typeof value.required === "boolean", "task.dispositions.required", `${path}.required`, "required must be a boolean when present");
618
+ into.require(typeof value.required === "boolean", "task.outcomes.required", `${path}.required`, "required must be a boolean when present");
618
619
  }
619
620
  // A code must be collected before completion, so there must be one to collect.
620
621
  if (value.required === true && !(Array.isArray(value.codes) && value.codes.length > 0)) {
621
- into.add("task.dispositions.required.codes", `${path}.codes`, "a required disposition policy must publish at least one code");
622
+ into.add("task.outcomes.required.codes", `${path}.codes`, "a required outcome policy must publish at least one code");
622
623
  }
623
624
  if (value.notes !== undefined)
624
- into.oneOf(value.notes, NOTES_RULES, "task.dispositions.notes", `${path}.notes`);
625
+ into.oneOf(value.notes, NOTES_RULES, "task.outcomes.notes", `${path}.notes`);
625
626
  if (value.codes === undefined)
626
627
  return;
627
628
  if (!Array.isArray(value.codes)) {
628
- into.add("task.dispositions.codes", `${path}.codes`, "codes must be an array when present");
629
+ into.add("task.outcomes.codes", `${path}.codes`, "codes must be an array when present");
629
630
  return;
630
631
  }
631
632
  const seen = new Set();
632
633
  value.codes.forEach((code, index) => {
633
634
  const at = `${path}.codes[${index}]`;
634
635
  if (!isPlainObject(code)) {
635
- into.add("task.disposition.shape", at, "each disposition code must be an object");
636
+ into.add("task.outcome.shape", at, "each outcome code must be an object");
636
637
  return;
637
638
  }
638
- if (into.filled(code.id, "task.disposition.id", `${at}.id`, "a disposition code needs an id")) {
639
+ if (into.filled(code.id, "task.outcome.id", `${at}.id`, "an outcome code needs an id")) {
639
640
  if (seen.has(code.id))
640
- into.add("task.disposition.unique", `${at}.id`, `duplicate disposition code: ${code.id}`);
641
+ into.add("task.outcome.unique", `${at}.id`, `duplicate outcome code: ${code.id}`);
641
642
  seen.add(code.id);
642
643
  }
643
- into.filled(code.label, "task.disposition.label", `${at}.label`, "a disposition code needs a label");
644
+ into.filled(code.label, "task.outcome.label", `${at}.label`, "an outcome code needs a label");
644
645
  });
645
646
  }
646
647
  function validateCustomCapabilities(value, path, into) {
@@ -696,7 +697,7 @@ const DEFAULT_LEVEL_IDS = DEFAULT_LEVELS.map(level => level.id);
696
697
  const POLICY_SETTINGS = membersOf({ on: true, off: true, person: true });
697
698
  const POLICY_KEYS = new Set([
698
699
  ...TASK_CAPABILITIES.voice, ...TASK_CAPABILITIES.chat, ...TASK_CAPABILITIES.email, "dial",
699
- ].filter(name => name !== "browsers" && name !== "dispositions" && name !== "custom"));
700
+ ].filter(name => name !== "browsers" && name !== "outcomes" && name !== "custom"));
700
701
  const PERSON_SETTABLE = /^(hold|skill:.+)$/;
701
702
  const isLocked = (value) => isPlainObject(value) && value.lockedBy !== undefined;
702
703
  /** The level ids in force: the manifest's, or the defaults when the caller holds no manifest. */
@@ -879,81 +880,82 @@ function validateTaskAttributes(value, path, into, levels) {
879
880
  }
880
881
  });
881
882
  }
882
- function validateHandlingHistory(value, path, into, task = {}) {
883
+ function validateInteractionHistory(value, path, into, task = {}) {
883
884
  if (value === undefined)
884
885
  return;
885
886
  if (!isPlainObject(value)) {
886
- into.add("task.handlingHistory.shape", path, "handlingHistory must be an object with its steps when present");
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
- for (const field of ["handleSeconds", "holdSeconds", "queueSeconds"]) {
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.handlingHistory.${field}`, `${path}.${field}`, `${field} must be a whole number of seconds, zero or more, or omitted when unknown`);
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.handlingHistory.transfers", `${path}.transfers`, "transfers must be a whole number, zero or more, or omitted when unknown");
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.handlingHistory.steps.shape", `${path}.steps`, "handlingHistory carries its steps as an array, empty when the task has had none");
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.handlingHistory.entry", at, "each handling step must be an object");
909
+ into.add("task.interactionHistory.entry", at, "each interaction step must be an object");
908
910
  return;
909
911
  }
910
- into.oneOf(entry.step, HANDLING_STEPS, "task.handlingHistory.step", `${at}.step`);
911
- if (into.timestamp(entry.at, "task.handlingHistory.at", `${at}.at`)) {
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.handlingHistory.order", `${at}.at`, "handling steps are oldest first; this entry is earlier than the one before it");
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.handlingHistory.seconds", `${at}.seconds`, "seconds must be a positive whole number; omit it while the step is still running");
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.handlingHistory.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`);
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.handlingHistory.muted.open", `${at}.seconds`, "a muted entry without seconds is still running, but this handling's media has ended: the provider closes its open leg and states the duration; other channels may continue");
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.handlingHistory.mutedBy", `${at}.mutedBy`);
940
- into.require(entry.by !== undefined, "task.handlingHistory.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
+ 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 (HANDLING_STEPS.includes(entry.step)) {
943
- into.require(entry.mutedBy === undefined, "task.handlingHistory.mutedBy.unexpected", `${at}.mutedBy`, "only a muted step says who silenced the microphone");
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.handlingHistory.by", `${at}.by`, "by must be a non-empty user id; omit it when the person cannot be identified");
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.handlingHistory.by.unexpected", `${at}.by`, "a queued step names nobody");
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" && HANDLING_STEPS_THAT_DIAL.includes(entry.step);
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.handlingHistory.${field}`, `${at}.${field}`, `${field} must not be empty when present`)) {
956
- into.require(dialled, `task.handlingHistory.${field}.unexpected`, `${at}.${field}`, `${describeValue(entry.step)} dialled nothing; ${field} belongs on transferred, conferenced, or unanswered`);
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
- validateHandlingHistory(task.handlingHistory, `${path}.handlingHistory`, into, { phase: task.phase, media: task.media });
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 handling, not the continuing caller journey.
1211
- // Once the handling's media ends or it enters wrap, its live room is cleared. Other
1212
- // channels may remain connected under another handling or in IVR/queue stages.
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 handling media has ended or never started; it cannot still be started during wrap");
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 handling is no longer connected (${task.phase === "completing" ? "the task is completing" : "its media ended"}); clear its onCall view without implying that other channels ended`);
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);
@@ -1257,9 +1260,9 @@ function validateTaskInto(task, context, path, into) {
1257
1260
  into.add("task.capability.dialOutcomes.required", `${path}.capabilities.${name}`, `${name} dials, and the manifest declares no dialOutcomes to say how a dial ends`);
1258
1261
  }
1259
1262
  // A control the queue could allow may stand locked in its place, saying whose. What the
1260
- // queue provides -- browsers, dispositions, custom controls -- is content, not a control.
1263
+ // queue provides -- browsers, outcomes, custom controls -- is content, not a control.
1261
1264
  if (isLocked(declared)) {
1262
- if (into.require(name !== "browsers" && name !== "dispositions" && name !== "custom", "task.capability.locked.unexpected", `${path}.capabilities.${name}`, `${name} is what the queue provides, not a control anyone locks`)) {
1265
+ if (into.require(name !== "browsers" && name !== "outcomes" && name !== "custom", "task.capability.locked.unexpected", `${path}.capabilities.${name}`, `${name} is what the queue provides, not a control anyone locks`)) {
1263
1266
  validateLockedInto(declared, "task.capability.locked", `${path}.capabilities.${name}`, context.levels, into);
1264
1267
  }
1265
1268
  continue;
@@ -1268,8 +1271,8 @@ function validateTaskInto(task, context, path, into) {
1268
1271
  case "recording":
1269
1272
  into.violations.push(...validateRecordingPolicy(declared, `${path}.capabilities.recording`));
1270
1273
  break;
1271
- case "dispositions":
1272
- validateDispositions(declared, `${path}.capabilities.dispositions`, into);
1274
+ case "outcomes":
1275
+ validateOutcomes(declared, `${path}.capabilities.outcomes`, into);
1273
1276
  break;
1274
1277
  case "custom":
1275
1278
  validateCustomCapabilities(declared, `${path}.capabilities.custom`, 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 validateHandlingReport(report, path = "handlingReport", manifest) {
2135
+ export function validateInteractionReport(report, path = "interactionReport", manifest) {
2133
2136
  const into = new Collector();
2134
2137
  if (!isPlainObject(report)) {
2135
- into.add("handlingReport.shape", path, "a handling report must be an object");
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), "handlingReport.taskId", `${path}.taskId`, "a report names the task");
2139
- into.require(isAllocationId(report.allocationId), "handlingReport.allocationId", `${path}.allocationId`, "a report names the task's allocation, so a late one never lands on the next life of the id");
2140
- into.oneOf(report.step, HANDLING_STEPS, "handlingReport.step", `${path}.step`);
2141
- into.timestamp(report.at, "handlingReport.at", `${path}.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, "handlingReport.mutedBy", `${path}.mutedBy`);
2145
- else if (HANDLING_STEPS.includes(report.step)) {
2146
- into.require(report.mutedBy === undefined, "handlingReport.mutedBy.unexpected", `${path}.mutedBy`, "only a muted leg says who silenced the microphone");
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, "handlingReport.seconds", `${path}.seconds`, "seconds must be a positive whole number; omit it rather than report nought");
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, "handlingReport.ended", `${path}.ended`, "ended is declared by presence, as true; a running leg omits it")) {
2153
- into.require(report.seconds !== undefined, "handlingReport.ended.seconds", `${path}.seconds`, "an ended leg states its final duration");
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, "handlingReport.running.unexpected", `${path}.seconds`, "this provider takes begin and end only; a running report was never asked for");
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 handling/allocation target before provider command dispatch. */
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 handling task");
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 handling");
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");
@@ -2295,7 +2298,7 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2295
2298
  fields.push("dialId");
2296
2299
  break;
2297
2300
  case "complete":
2298
- fields.push("disposition", "notes");
2301
+ fields.push("outcome", "notes");
2299
2302
  break;
2300
2303
  case "transfer":
2301
2304
  fields.push("action");
@@ -2356,8 +2359,8 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2356
2359
  into.violations.push(...validateRecordingCommandShape(command, "provider", path));
2357
2360
  break;
2358
2361
  case "complete":
2359
- if (command.disposition !== undefined)
2360
- into.filled(command.disposition, "command.complete.disposition", `${path}.disposition`, "a disposition must not be empty when present");
2362
+ if (command.outcome !== undefined)
2363
+ into.filled(command.outcome, "command.complete.outcome", `${path}.outcome`, "an outcome must not be empty when present");
2361
2364
  if (command.notes !== undefined)
2362
2365
  into.require(typeof command.notes === "string", "command.complete.notes", `${path}.notes`, "notes must be a string when present");
2363
2366
  break;
@@ -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 handling has ended. The caller and other
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 handling = () => inPhase("command.phase.handling", "in-progress", "paused");
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
- handling();
2443
+ interaction();
2441
2444
  break;
2442
2445
  case "end-call":
2443
2446
  offered("endCall");
2444
- handling();
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
- handling();
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.handling", "in-progress", "paused", "completing");
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
- handling();
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
- handling();
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
- handling();
2494
+ interaction();
2492
2495
  const conferencing = offered("conference");
2493
2496
  if (conferencing && command.action === "add")
2494
2497
  listed("conference");
@@ -2506,27 +2509,27 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2506
2509
  }
2507
2510
  case "complete": {
2508
2511
  into.require(task.completionMode === "agent-command", "command.complete.mode", path, "complete belongs to agent-command; a provider-automatic task completes itself");
2509
- // What travels with complete is what the dispositions capability published: a code from its
2512
+ // What travels with complete is what the outcomes capability published: a code from its
2510
2513
  // list where it has one, a code at all where it requires one, notes as it said, nothing where it published nothing.
2511
- const dispositions = capabilities.dispositions;
2512
- if (dispositions === undefined) {
2513
- into.require(command.disposition === undefined, "command.complete.disposition.unexpected", `${path}.disposition`, "the task declares no dispositions; complete travels with no code");
2514
- into.require(command.notes === undefined, "command.complete.notes.unexpected", `${path}.notes`, "the task declares no dispositions; complete travels with no notes");
2514
+ const outcomes = capabilities.outcomes;
2515
+ if (outcomes === undefined) {
2516
+ into.require(command.outcome === undefined, "command.complete.outcome.unexpected", `${path}.outcome`, "the task declares no outcomes; complete travels with no code");
2517
+ into.require(command.notes === undefined, "command.complete.notes.unexpected", `${path}.notes`, "the task declares no outcomes; complete travels with no notes");
2515
2518
  }
2516
- else if (dispositions === true) {
2517
- into.require(command.disposition === undefined, "command.complete.disposition.unexpected", `${path}.disposition`, "a bare dispositions control publishes no code, so complete carries none");
2519
+ else if (outcomes === true) {
2520
+ into.require(command.outcome === undefined, "command.complete.outcome.unexpected", `${path}.outcome`, "a bare outcomes control publishes no code, so complete carries none");
2518
2521
  }
2519
- else if (isPlainObject(dispositions)) {
2520
- if (Array.isArray(dispositions.codes) && command.disposition !== undefined) {
2521
- into.require(dispositions.codes.some(code => isPlainObject(code) && code.id === command.disposition), "command.complete.disposition.unknown", `${path}.disposition`, `${describeValue(command.disposition)} is not a code the task published`);
2522
+ else if (isPlainObject(outcomes)) {
2523
+ if (Array.isArray(outcomes.codes) && command.outcome !== undefined) {
2524
+ into.require(outcomes.codes.some(code => isPlainObject(code) && code.id === command.outcome), "command.complete.outcome.unknown", `${path}.outcome`, `${describeValue(command.outcome)} is not a code the task published`);
2522
2525
  }
2523
- if (dispositions.required === true) {
2524
- into.require(command.disposition !== undefined, "command.complete.disposition.required", `${path}.disposition`, "the task requires a disposition and complete carries none");
2526
+ if (outcomes.required === true) {
2527
+ into.require(command.outcome !== undefined, "command.complete.outcome.required", `${path}.outcome`, "the task requires an outcome and complete carries none");
2525
2528
  }
2526
- if (dispositions.notes === "none") {
2529
+ if (outcomes.notes === "none") {
2527
2530
  into.require(command.notes === undefined, "command.complete.notes.unexpected", `${path}.notes`, "the task takes no notes");
2528
2531
  }
2529
- else if (dispositions.notes === "required") {
2532
+ else if (outcomes.notes === "required") {
2530
2533
  into.require(typeof command.notes === "string" && command.notes.length > 0, "command.complete.notes.required", `${path}.notes`, "the task requires notes and complete carries none");
2531
2534
  }
2532
2535
  }