@xema/omni-protocol 0.1.96 → 0.1.97

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.
@@ -122,9 +122,9 @@ export declare function validateHostGuarantees(guarantees: unknown, path?: strin
122
122
  * a task, a step, when it began, how long so far if the host says, and an explicit end that
123
123
  * carries the final duration.
124
124
  */
125
- export declare function validateInteractionReport(report: unknown, path?: string, manifest?: unknown): ProtocolViolation[];
125
+ export declare function validateHistoryReport(report: unknown, path?: string, manifest?: unknown): ProtocolViolation[];
126
126
  export declare function validateHostReport(report: unknown, path?: string): ProtocolViolation[];
127
- /** Validate the exact interaction/allocation target before provider command dispatch. */
127
+ /** Validate the exact interaction/assignment target before provider command dispatch. */
128
128
  export declare function validateTaskCommandRequest(request: unknown, task: unknown, path?: string, taskContext?: Omit<TaskValidationContext, "channel">): ProtocolViolation[];
129
129
  /**
130
130
  * What a command needs to be issuable, checked against the task it names: the capability the
@@ -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, LISTENING_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";
12
+ import { RECORDING_ACTIONS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, LISTENING_BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, HISTORY_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 INTERACTION_STEPS = membersOf({
60
+ const HISTORY_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 });
@@ -155,8 +155,7 @@ const isIsoTimestamp = (value) => {
155
155
  const isDurationSeconds = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
156
156
  /** Opaque, non-empty, provider-issued. Never parsed and never compared across providers. */
157
157
  const isUserId = isFilled;
158
- const isTaskId = isFilled;
159
- const isAllocationId = isFilled;
158
+ const isAssignmentId = isFilled;
160
159
  const ruleListeners = new Set();
161
160
  /**
162
161
  * Hears every rule a validator evaluates, pass or fail, until the returned function is called. A
@@ -880,82 +879,82 @@ function validateTaskAttributes(value, path, into, levels) {
880
879
  }
881
880
  });
882
881
  }
883
- function validateInteractionHistory(value, path, into, task = {}) {
882
+ function validateHistory(value, path, into, task = {}) {
884
883
  if (value === undefined)
885
884
  return;
886
885
  if (!isPlainObject(value)) {
887
- into.add("task.interactionHistory.shape", path, "interactionHistory must be an object with its steps when present");
886
+ into.add("task.history.shape", path, "history must be an object with its steps when present");
888
887
  return;
889
888
  }
890
889
  // What the record adds up to before this agent: each total present when the provider knows it
891
890
  // and absent when it does not, never a plausible nought.
892
- into.require(!Object.hasOwn(value, "handleSeconds"), "task.interactionHistory.interactionSeconds.renamed", `${path}.handleSeconds`, "use interactionSeconds; the former field is not accepted");
891
+ into.require(!Object.hasOwn(value, "handleSeconds"), "task.history.interactionSeconds.renamed", `${path}.handleSeconds`, "use interactionSeconds; the former field is not accepted");
893
892
  for (const field of ["interactionSeconds", "holdSeconds", "queueSeconds"]) {
894
893
  if (value[field] !== undefined) {
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
+ into.require(isDurationSeconds(value[field]), `task.history.${field}`, `${path}.${field}`, `${field} must be a whole number of seconds, zero or more, or omitted when unknown`);
896
895
  }
897
896
  }
898
897
  if (value.transfers !== undefined) {
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
+ into.require(typeof value.transfers === "number" && Number.isInteger(value.transfers) && value.transfers >= 0, "task.history.transfers", `${path}.transfers`, "transfers must be a whole number, zero or more, or omitted when unknown");
900
899
  }
901
900
  if (!Array.isArray(value.steps)) {
902
- into.add("task.interactionHistory.steps.shape", `${path}.steps`, "interactionHistory carries its steps as an array, empty when the task has had none");
901
+ into.add("task.history.steps.shape", `${path}.steps`, "history carries its steps as an array, empty when the task has had none");
903
902
  return;
904
903
  }
905
904
  let previous;
906
905
  value.steps.forEach((entry, index) => {
907
906
  const at = `${path}.steps[${index}]`;
908
907
  if (!isPlainObject(entry)) {
909
- into.add("task.interactionHistory.entry", at, "each interaction step must be an object");
908
+ into.add("task.history.entry", at, "each history step must be an object");
910
909
  return;
911
910
  }
912
- into.oneOf(entry.step, INTERACTION_STEPS, "task.interactionHistory.step", `${at}.step`);
913
- if (into.timestamp(entry.at, "task.interactionHistory.at", `${at}.at`)) {
911
+ into.oneOf(entry.step, HISTORY_STEPS, "task.history.step", `${at}.step`);
912
+ if (into.timestamp(entry.at, "task.history.at", `${at}.at`)) {
914
913
  // The record is one entry per occurrence, oldest first: a second hold is a second entry
915
914
  // after the first, never a revision of it or an entry filed out of its turn.
916
915
  const instant = Date.parse(entry.at);
917
916
  if (previous !== undefined && instant < previous) {
918
- into.add("task.interactionHistory.order", `${at}.at`, "interaction steps are oldest first; this entry is earlier than the one before it");
917
+ into.add("task.history.order", `${at}.at`, "history steps are oldest first; this entry is earlier than the one before it");
919
918
  }
920
919
  previous = instant;
921
920
  }
922
921
  if (entry.seconds !== undefined) {
923
922
  // Omitted while a leg is still running. Nought is a claim that it took no time.
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
+ into.require(isDurationSeconds(entry.seconds) && entry.seconds > 0, "task.history.seconds", `${at}.seconds`, "seconds must be a positive whole number; omit it while the step is still running");
925
924
  }
926
925
  else {
927
926
  // An open leg is one still running, and the task says whether it can be: a hold runs only while
928
927
  // the task is paused, and a mute only while its media is up. An open entry after that is a leg
929
928
  // nobody closed, which reads exactly like a leg running now.
930
929
  if (entry.step === "held") {
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
+ into.require(task.phase === "paused", "task.history.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`);
932
931
  }
933
932
  if (entry.step === "muted") {
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
+ into.require(task.media !== "ended" && task.phase !== "completing", "task.history.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");
935
934
  }
936
935
  }
937
936
  // A muted entry carries whose the silence was, as the host reported it; no other step has it.
938
937
  // And it names the agent: the host has exactly one, the provider knows who, so an unattributed
939
938
  // muted leg is a record that dropped a fact it held rather than one it could not establish.
940
939
  if (entry.step === "muted") {
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");
940
+ into.oneOf(entry.mutedBy, MUTED_BY, "task.history.mutedBy", `${at}.mutedBy`);
941
+ into.require(entry.by !== undefined, "task.history.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");
943
942
  }
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");
943
+ else if (HISTORY_STEPS.includes(entry.step)) {
944
+ into.require(entry.mutedBy === undefined, "task.history.mutedBy.unexpected", `${at}.mutedBy`, "only a muted step says who silenced the microphone");
946
945
  }
947
946
  if (entry.by !== undefined) {
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
+ into.require(isUserId(entry.by), "task.history.by", `${at}.by`, "by must be a non-empty user id; omit it when the person cannot be identified");
949
948
  // On `queued` nobody takes part, so there is nothing to name.
950
- into.require(entry.step !== "queued", "task.interactionHistory.by.unexpected", `${at}.by`, "a queued step names nobody");
949
+ into.require(entry.step !== "queued", "task.history.by.unexpected", `${at}.by`, "a queued step names nobody");
951
950
  }
952
951
  // A step that dialled says where to and, when a host placed it, which dial; no other step dialled.
953
- const dialled = typeof entry.step === "string" && INTERACTION_STEPS_THAT_DIAL.includes(entry.step);
952
+ const dialled = typeof entry.step === "string" && HISTORY_STEPS_THAT_DIAL.includes(entry.step);
954
953
  for (const field of ["dialId", "destinationId"]) {
955
954
  if (entry[field] === undefined)
956
955
  continue;
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`);
956
+ if (into.filled(entry[field], `task.history.${field}`, `${at}.${field}`, `${field} must not be empty when present`)) {
957
+ into.require(dialled, `task.history.${field}.unexpected`, `${at}.${field}`, `${describeValue(entry.step)} dialled nothing; ${field} belongs on transferred, conferenced, or unanswered`);
959
958
  }
960
959
  }
961
960
  });
@@ -1085,6 +1084,7 @@ function validateAssisting(value, channel, path, into) {
1085
1084
  return;
1086
1085
  }
1087
1086
  into.require(isUserId(value.memberId), "task.assisting.memberId", `${path}.memberId`, "a joined call names the member who asked");
1087
+ into.require(isAssignmentId(value.assignmentId), "task.assisting.assignmentId", `${path}.assignmentId`, "a joined call names the member's assignment");
1088
1088
  if (value.note !== undefined)
1089
1089
  into.filled(value.note, "task.assisting.note", `${path}.note`, "a note must not be empty when present");
1090
1090
  into.timestamp(value.since, "task.assisting.since", `${path}.since`);
@@ -1100,8 +1100,7 @@ function validateListening(value, channel, path, into) {
1100
1100
  return;
1101
1101
  }
1102
1102
  into.require(isUserId(value.memberId), "task.listening.memberId", `${path}.memberId`, "a call being listened to names the member on it");
1103
- into.require(isTaskId(value.taskId), "task.listening.taskId", `${path}.taskId`, "a call being listened to names the member's task");
1104
- into.require(isAllocationId(value.allocationId), "task.listening.allocationId", `${path}.allocationId`, "a call being listened to names the member's task by its allocation too");
1103
+ into.require(isAssignmentId(value.assignmentId), "task.listening.assignmentId", `${path}.assignmentId`, "a call being listened to names the member's assignment");
1105
1104
  into.oneOf(value.mode, LISTENING_MODES, "task.listening.mode", `${path}.mode`);
1106
1105
  into.timestamp(value.since, "task.listening.since", `${path}.since`);
1107
1106
  }
@@ -1122,9 +1121,13 @@ function validateTaskInto(task, context, path, into) {
1122
1121
  return;
1123
1122
  }
1124
1123
  into.require(!Object.hasOwn(task, "monitoring"), "task.listening.renamed", `${path}.monitoring`, "use listening; the former field is not accepted");
1125
- into.require(!Object.hasOwn(task, "handlingHistory"), "task.interactionHistory.renamed", `${path}.handlingHistory`, "use interactionHistory; the former field is not accepted");
1126
- into.require(isTaskId(task.id), "task.id", `${path}.id`, "a task needs a non-empty id");
1127
- 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
+ for (const former of ["handlingHistory", "interactionHistory"]) {
1125
+ into.require(!Object.hasOwn(task, former), "task.history.renamed", `${path}.${former}`, "use history; the former field is not accepted");
1126
+ }
1127
+ for (const former of ["id", "allocationId"]) {
1128
+ into.require(!Object.hasOwn(task, former), "task.assignmentId.renamed", `${path}.${former}`, "use assignmentId; a task is named by its assignment alone, and the former field is not accepted");
1129
+ }
1130
+ into.require(isAssignmentId(task.assignmentId), "task.assignmentId", `${path}.assignmentId`, "a task names the assignment it is the record of: the provider's name for it, unique within the provider and never reused");
1128
1131
  into.filled(task.title, "task.title", `${path}.title`, "a task needs a title");
1129
1132
  into.filled(task.taskType, "task.taskType", `${path}.taskType`, "a task needs a task type");
1130
1133
  if (into.oneOf(task.phase, TASK_PHASES, "task.phase", `${path}.phase`) && task.phase === "preview") {
@@ -1209,7 +1212,7 @@ function validateTaskInto(task, context, path, into) {
1209
1212
  }
1210
1213
  validateBrowsers(task.browsers, `${path}.browsers`, into);
1211
1214
  validateTaskAttributes(task.attributes, `${path}.attributes`, into, context.levels);
1212
- validateInteractionHistory(task.interactionHistory, `${path}.interactionHistory`, into, { phase: task.phase, media: task.media });
1215
+ validateHistory(task.history, `${path}.history`, into, { phase: task.phase, media: task.media });
1213
1216
  validateOnCall(task.onCall, context.channel, `${path}.onCall`, into);
1214
1217
  // Media and onCall describe this agent's interaction, not the continuing caller journey.
1215
1218
  // Once the interaction's media ends or it enters wrap, its live room is cleared. Other
@@ -1622,8 +1625,7 @@ function validateTeamMembersInto(teamMembers, path, context, into) {
1622
1625
  if (into.require(isUserId(request.memberId), "team.request.memberId", `${at}.memberId`, "a request names the member asking")) {
1623
1626
  into.require(request.memberId !== context.self, "team.request.self", `${at}.memberId`, "the team member list carries the reader's own ask: an agent's request for a lead goes to whoever leads them");
1624
1627
  }
1625
- into.require(isTaskId(request.taskId), "team.request.taskId", `${at}.taskId`, "a request names the task the lead would join");
1626
- into.require(isAllocationId(request.allocationId), "team.request.allocationId", `${at}.allocationId`, "a request names the task by its allocation too");
1628
+ into.require(isAssignmentId(request.assignmentId), "team.request.assignmentId", `${at}.assignmentId`, "a request names the member's assignment the lead would join");
1627
1629
  if (request.note !== undefined)
1628
1630
  into.filled(request.note, "team.request.note", `${at}.note`, "a note must not be empty when present");
1629
1631
  into.timestamp(request.since, "team.request.since", `${at}.since`);
@@ -1698,10 +1700,10 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
1698
1700
  into.add("snapshot.listening.single", `${path}.tasks[${index}].listening`, "a lead listens to one call at a time");
1699
1701
  listening = index;
1700
1702
  }
1701
- if (isPlainObject(task) && isTaskId(task.id)) {
1702
- if (seen.has(task.id))
1703
- into.add("task.id.unique", `${path}.tasks[${index}].id`, `duplicate task id: ${task.id}`);
1704
- seen.add(task.id);
1703
+ if (isPlainObject(task) && isAssignmentId(task.assignmentId)) {
1704
+ if (seen.has(task.assignmentId))
1705
+ into.add("task.assignmentId.unique", `${path}.tasks[${index}].assignmentId`, `duplicate assignment id: ${task.assignmentId}`);
1706
+ seen.add(task.assignmentId);
1705
1707
  }
1706
1708
  });
1707
1709
  }
@@ -1864,6 +1866,7 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1864
1866
  return into.violations;
1865
1867
  }
1866
1868
  const at = `${path}.event`;
1869
+ into.require(!Object.hasOwn(event, "taskId"), "event.assignmentId.renamed", `${at}.taskId`, "an event names a task by its assignment alone; the former field is not accepted");
1867
1870
  switch (event.type) {
1868
1871
  case "snapshot":
1869
1872
  into.oneOf(event.reason, SNAPSHOT_REASONS, "event.snapshot.reason", `${at}.reason`);
@@ -1893,25 +1896,22 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1893
1896
  if (isPlainObject(event.task) && typeof event.task.phase === "string") {
1894
1897
  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}`);
1895
1898
  }
1896
- if (event.allocationExpiresAt !== undefined)
1897
- into.timestamp(event.allocationExpiresAt, "event.taskOffered.allocationExpiresAt", `${at}.allocationExpiresAt`);
1899
+ if (event.assignmentExpiresAt !== undefined)
1900
+ into.timestamp(event.assignmentExpiresAt, "event.taskOffered.assignmentExpiresAt", `${at}.assignmentExpiresAt`);
1898
1901
  break;
1899
1902
  case "task-updated":
1900
1903
  validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest), locked: context.locked }, `${at}.task`, into);
1901
1904
  break;
1902
1905
  case "task-media-started":
1903
1906
  into.require(channel === "voice", "event.media.channel", `${at}.type`, "only a voice provider publishes media transitions");
1904
- into.require(isTaskId(event.taskId), "event.taskMediaStarted.taskId", `${at}.taskId`, "a task id is required");
1905
- into.require(isAllocationId(event.allocationId), "event.taskMediaStarted.allocationId", `${at}.allocationId`, "an event about a task names its allocation");
1907
+ into.require(isAssignmentId(event.assignmentId), "event.taskMediaStarted.assignmentId", `${at}.assignmentId`, "an event about a task names its assignment");
1906
1908
  break;
1907
1909
  case "task-media-ended":
1908
1910
  into.require(channel === "voice", "event.media.channel", `${at}.type`, "only a voice provider publishes media transitions");
1909
- into.require(isTaskId(event.taskId), "event.taskMediaEnded.taskId", `${at}.taskId`, "a task id is required");
1910
- into.require(isAllocationId(event.allocationId), "event.taskMediaEnded.allocationId", `${at}.allocationId`, "an event about a task names its allocation");
1911
+ into.require(isAssignmentId(event.assignmentId), "event.taskMediaEnded.assignmentId", `${at}.assignmentId`, "an event about a task names its assignment");
1911
1912
  break;
1912
1913
  case "task-ended":
1913
- into.require(isTaskId(event.taskId), "event.taskEnded.taskId", `${at}.taskId`, "a task id is required");
1914
- into.require(isAllocationId(event.allocationId), "event.taskEnded.allocationId", `${at}.allocationId`, "an event about a task names its allocation");
1914
+ into.require(isAssignmentId(event.assignmentId), "event.taskEnded.assignmentId", `${at}.assignmentId`, "an event about a task names its assignment");
1915
1915
  validateTaskOutcome(event.outcome, `${at}.outcome`, into);
1916
1916
  break;
1917
1917
  case "dial-outcome": {
@@ -1921,9 +1921,8 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1921
1921
  if (into.oneOf(event.outcome, DIAL_OUTCOMES, "event.dialOutcome.outcome", `${at}.outcome`)) {
1922
1922
  into.require(declared !== undefined && declared.includes(event.outcome), "event.dialOutcome.undeclared", `${at}.outcome`, `the manifest does not declare ${describeValue(event.outcome)} among its dialOutcomes`);
1923
1923
  }
1924
- if (event.taskId !== undefined)
1925
- into.require(isTaskId(event.taskId), "event.dialOutcome.taskId", `${at}.taskId`, "taskId must be a task id when present");
1926
- allocationWithTask(event, "event.dialOutcome", at, into);
1924
+ if (event.assignmentId !== undefined)
1925
+ into.require(isAssignmentId(event.assignmentId), "event.dialOutcome.assignmentId", `${at}.assignmentId`, "assignmentId must name an assignment when present");
1927
1926
  if (event.destinationId !== undefined)
1928
1927
  into.filled(event.destinationId, "event.dialOutcome.destinationId", `${at}.destinationId`, "a destinationId must not be empty when present");
1929
1928
  if (event.reason !== undefined)
@@ -1942,9 +1941,8 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1942
1941
  case "diagnostic":
1943
1942
  into.filled(event.expected, "event.diagnostic.expected", `${at}.expected`, "a diagnostic states the rule that was broken, as a sentence");
1944
1943
  into.filled(event.observed, "event.diagnostic.observed", `${at}.observed`, "a diagnostic states what was observed instead");
1945
- if (event.taskId !== undefined)
1946
- into.require(isTaskId(event.taskId), "event.diagnostic.taskId", `${at}.taskId`, "taskId must be a task id when present");
1947
- allocationWithTask(event, "event.diagnostic", at, into);
1944
+ if (event.assignmentId !== undefined)
1945
+ into.require(isAssignmentId(event.assignmentId), "event.diagnostic.assignmentId", `${at}.assignmentId`, "assignmentId must name an assignment when present");
1948
1946
  break;
1949
1947
  case "queue-summary":
1950
1948
  validateQueueSummary(event.summary, `${at}.summary`, into);
@@ -2084,7 +2082,7 @@ export function validateCapacity(capacity, path = "capacity") {
2084
2082
  into.add("capacity.shape", path, "a capacity is an object carrying count");
2085
2083
  return into.violations;
2086
2084
  }
2087
- into.require(typeof capacity.count === "number" && Number.isInteger(capacity.count) && capacity.count >= 0, "capacity.count", `${path}.count`, "count is a whole number, zero or more: how many tasks this provider may allocate at once, zero while the agent's capacity is elsewhere");
2085
+ into.require(typeof capacity.count === "number" && Number.isInteger(capacity.count) && capacity.count >= 0, "capacity.count", `${path}.count`, "count is a whole number, zero or more: how many tasks this provider may assign at once, zero while the agent's capacity is elsewhere");
2088
2086
  return into.violations;
2089
2087
  }
2090
2088
  /** The login's store, which the host provides on every connection: three functions, and nothing else is a store. */
@@ -2133,34 +2131,34 @@ export function validateHostGuarantees(guarantees, path = "host.guarantees") {
2133
2131
  * a task, a step, when it began, how long so far if the host says, and an explicit end that
2134
2132
  * carries the final duration.
2135
2133
  */
2136
- export function validateInteractionReport(report, path = "interactionReport", manifest) {
2134
+ export function validateHistoryReport(report, path = "historyReport", manifest) {
2137
2135
  const into = new Collector();
2138
2136
  if (!isPlainObject(report)) {
2139
- into.add("interactionReport.shape", path, "an interaction report must be an object");
2137
+ into.add("historyReport.shape", path, "a history report must be an object");
2140
2138
  return into.violations;
2141
2139
  }
2142
- into.require(isTaskId(report.taskId), "interactionReport.taskId", `${path}.taskId`, "a report names the task");
2143
- 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");
2144
- into.oneOf(report.step, INTERACTION_STEPS, "interactionReport.step", `${path}.step`);
2145
- into.timestamp(report.at, "interactionReport.at", `${path}.at`);
2140
+ into.require(!Object.hasOwn(report, "taskId"), "historyReport.assignmentId.renamed", `${path}.taskId`, "a report names the assignment alone; the former field is not accepted");
2141
+ into.require(isAssignmentId(report.assignmentId), "historyReport.assignmentId", `${path}.assignmentId`, "a report names the task's assignment, so a late one never lands on the next life of the id");
2142
+ into.oneOf(report.step, HISTORY_STEPS, "historyReport.step", `${path}.step`);
2143
+ into.timestamp(report.at, "historyReport.at", `${path}.at`);
2146
2144
  // A muted leg says whose the silence was; no other leg has anyone to name for it.
2147
2145
  if (report.step === "muted")
2148
- into.oneOf(report.mutedBy, MUTED_BY, "interactionReport.mutedBy", `${path}.mutedBy`);
2149
- else if (INTERACTION_STEPS.includes(report.step)) {
2150
- into.require(report.mutedBy === undefined, "interactionReport.mutedBy.unexpected", `${path}.mutedBy`, "only a muted leg says who silenced the microphone");
2146
+ into.oneOf(report.mutedBy, MUTED_BY, "historyReport.mutedBy", `${path}.mutedBy`);
2147
+ else if (HISTORY_STEPS.includes(report.step)) {
2148
+ into.require(report.mutedBy === undefined, "historyReport.mutedBy.unexpected", `${path}.mutedBy`, "only a muted leg says who silenced the microphone");
2151
2149
  }
2152
2150
  if (report.seconds !== undefined) {
2153
- 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");
2151
+ into.require(isDurationSeconds(report.seconds) && report.seconds > 0, "historyReport.seconds", `${path}.seconds`, "seconds must be a positive whole number; omit it rather than report nought");
2154
2152
  }
2155
2153
  if (report.ended !== undefined) {
2156
- if (into.require(report.ended === true, "interactionReport.ended", `${path}.ended`, "ended is declared by presence, as true; a running leg omits it")) {
2157
- into.require(report.seconds !== undefined, "interactionReport.ended.seconds", `${path}.seconds`, "an ended leg states its final duration");
2154
+ if (into.require(report.ended === true, "historyReport.ended", `${path}.ended`, "ended is declared by presence, as true; a running leg omits it")) {
2155
+ into.require(report.seconds !== undefined, "historyReport.ended.seconds", `${path}.seconds`, "an ended leg states its final duration");
2158
2156
  }
2159
2157
  }
2160
2158
  else if (report.seconds !== undefined && manifest !== undefined) {
2161
2159
  // A running report crosses only to a provider that asked for one: begin and end are the whole
2162
2160
  // of what the rest receive.
2163
- 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");
2161
+ into.require(isPlainObject(manifest) && manifest.runningStepReports === true, "historyReport.running.unexpected", `${path}.seconds`, "this provider takes begin and end only; a running report was never asked for");
2164
2162
  }
2165
2163
  return into.violations;
2166
2164
  }
@@ -2173,15 +2171,6 @@ function validateMutedBy(device, rule, path, into) {
2173
2171
  into.require(device.mutedBy === undefined, `${rule}.mutedBy.unexpected`, `${path}.mutedBy`, "mutedBy says who silenced a device that is not flowing; a flowing one names nobody");
2174
2172
  }
2175
2173
  }
2176
- /** An event that may name a task names its allocation with it, and never an allocation alone. */
2177
- function allocationWithTask(event, rule, at, into) {
2178
- if (event.taskId !== undefined) {
2179
- into.require(isAllocationId(event.allocationId), `${rule}.allocationId`, `${at}.allocationId`, "an event that names a task names its allocation too");
2180
- }
2181
- else {
2182
- into.require(event.allocationId === undefined, `${rule}.allocationId.unexpected`, `${at}.allocationId`, "an allocation is named with its task, never alone");
2183
- }
2184
- }
2185
2174
  export function validateHostReport(report, path = "host") {
2186
2175
  const into = new Collector();
2187
2176
  if (!isPlainObject(report)) {
@@ -2249,19 +2238,18 @@ export function validateHostReport(report, path = "host") {
2249
2238
  const TRANSFER_ACTIONS = ["cold", "warm", "complete", "cancel"];
2250
2239
  const LEAD_ASSIST_ACTIONS = ["request", "cancel", "take-over-call", "leave"];
2251
2240
  const CONFERENCE_ACTIONS = ["add", "remove"];
2252
- /** Validate the exact interaction/allocation target before provider command dispatch. */
2241
+ /** Validate the exact interaction/assignment target before provider command dispatch. */
2253
2242
  export function validateTaskCommandRequest(request, task, path = "request", taskContext) {
2254
2243
  const into = new Collector();
2255
2244
  if (!isPlainObject(request) || !isPlainObject(task)) {
2256
2245
  into.add("command.request.shape", path, "a request and the current published task are required");
2257
2246
  return into.violations;
2258
2247
  }
2259
- into.filled(request.taskId, "command.request.taskId", `${path}.taskId`, "name the interaction task");
2260
- into.filled(request.allocationId, "command.request.allocationId", `${path}.allocationId`, "name its allocation");
2261
- into.require(request.taskId === task.id, "command.request.taskId.mismatch", `${path}.taskId`, "the command must target this exact interaction");
2262
- into.require(request.allocationId === task.allocationId, "command.request.allocationId.mismatch", `${path}.allocationId`, "the command must target this exact allocation");
2248
+ into.require(!Object.hasOwn(request, "taskId"), "command.request.assignmentId.renamed", `${path}.taskId`, "a command names the assignment alone; the former field is not accepted");
2249
+ into.filled(request.assignmentId, "command.request.assignmentId", `${path}.assignmentId`, "name the assignment");
2250
+ into.require(request.assignmentId === task.assignmentId, "command.request.assignmentId.mismatch", `${path}.assignmentId`, "the command must target this exact assignment");
2263
2251
  for (const key of Object.keys(request))
2264
- into.require(["taskId", "allocationId", "command"].includes(key), "command.request.field", `${path}.${key}`, "unsupported task command request field");
2252
+ into.require(["assignmentId", "command"].includes(key), "command.request.field", `${path}.${key}`, "unsupported task command request field");
2265
2253
  into.violations.push(...validateTaskCommand(request.command, task, `${path}.command`, taskContext));
2266
2254
  return into.violations;
2267
2255
  }
@@ -2987,10 +2975,10 @@ export function validateHostRecordings(value, path = "host.recordings") {
2987
2975
  check(false, "report.shape", at, "expected scoped report");
2988
2976
  return;
2989
2977
  }
2990
- keys(report, ["taskId", "allocationId", "state"], at);
2991
- check(filled(report.taskId) && filled(report.allocationId), "report.scope", at, "report identifies task and allocation");
2992
- const key = JSON.stringify([report.taskId, report.allocationId]);
2993
- check(!seen.has(key), "report.duplicate", at, "one host state per task allocation");
2978
+ keys(report, ["assignmentId", "state"], at);
2979
+ check(filled(report.assignmentId), "report.scope", at, "report identifies the assignment");
2980
+ const key = JSON.stringify([report.assignmentId]);
2981
+ check(!seen.has(key), "report.duplicate", at, "one host state per assignment");
2994
2982
  seen.add(key);
2995
2983
  violations.push(...validateRecordingState(report.state, `${at}.state`));
2996
2984
  });
@@ -3053,8 +3041,8 @@ export function validateRecordingRequest(request, task, context, path = "request
3053
3041
  return violations;
3054
3042
  }
3055
3043
  violations.push(...validateTask(task, { ...context.taskContext, channel: "voice" }, `${path}.task`));
3056
- keys(request, ["taskId", "allocationId", "command"], path);
3057
- check(filled(request.taskId) && filled(request.allocationId) && request.taskId === task.id && request.allocationId === task.allocationId, "request.scope", path, "task allocation changed or scope missing");
3044
+ keys(request, ["assignmentId", "command"], path);
3045
+ check(filled(request.assignmentId) && request.assignmentId === task.assignmentId, "request.scope", path, "task assignment changed or scope missing");
3058
3046
  check(task.channel === "voice", "request.channel", path, "recording is voice-only");
3059
3047
  violations.push(...validateRecordingCommandShape(request.command, context.source, `${path}.command`));
3060
3048
  if (!object(request.command))
@@ -3069,7 +3057,7 @@ export function validateRecordingRequest(request, task, context, path = "request
3069
3057
  check(object(context.host), "host.required", path, "host did not declare recording support");
3070
3058
  const reports = object(context.hostReport) ? context.hostReport.recordings : undefined;
3071
3059
  violations.push(...validateHostRecordings(reports));
3072
- state = Array.isArray(reports) ? reports.find(r => object(r) && r.taskId === task.id && r.allocationId === task.allocationId)?.state : undefined;
3060
+ state = Array.isArray(reports) ? reports.find(r => object(r) && r.assignmentId === task.assignmentId)?.state : undefined;
3073
3061
  if (object(context.host)) {
3074
3062
  check(Array.isArray(context.host.actions) && context.host.actions.includes(command.action), "host.action", path, "host does not support action");
3075
3063
  check(object(policy) && Array.isArray(context.host.destinationIds) && context.host.destinationIds.includes(policy.destinationId), "host.destination", path, "task destination is not provisioned by this host");