@xema/omni-protocol 0.1.96 → 0.1.98

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/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateBreakTransition, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateUserDetails, observeRules, ruleEvaluated, validateHostMute, validateHostRecording, validateLoginStore, validateInteractionReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateBreakTransition, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateUserDetails, observeRules, ruleEvaluated, validateHostMute, validateHostRecording, validateLoginStore, validateHistoryReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
4
4
  /**
5
5
  * A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
@@ -11,7 +11,7 @@ const STATE_SUBJECTS = [
11
11
  "tasks",
12
12
  "task.browsers",
13
13
  "task.attributes",
14
- "task.interactionHistory",
14
+ "task.history",
15
15
  "task.onCall",
16
16
  "task.leadAssist",
17
17
  "task.assisting",
@@ -53,8 +53,8 @@ function observeTask(value, seen) {
53
53
  seen.add("task.browsers");
54
54
  if (some(value.attributes))
55
55
  seen.add("task.attributes");
56
- if (isRecord(value.interactionHistory) && some(value.interactionHistory.steps))
57
- seen.add("task.interactionHistory");
56
+ if (isRecord(value.history) && some(value.history.steps))
57
+ seen.add("task.history");
58
58
  if (some(value.onCall))
59
59
  seen.add("task.onCall");
60
60
  if (value.leadAssist !== undefined)
@@ -357,13 +357,13 @@ export async function exerciseAdapter(adapter, context, options = {}) {
357
357
  lockedPartySeen ||= eventTasks(envelope).some(locksParty);
358
358
  if (eventNamesUsers(envelope))
359
359
  requireMethod(live, "getUserDetails", "an event publishes a UserId");
360
- // Work is pulled, never pushed: an offer before the host stated capacity is an allocation
360
+ // Work is pulled, never pushed: an offer before the host stated capacity is an assignment
361
361
  // against nothing, and an offer beyond the count is one too many -- unless the task is the
362
362
  // host's own dial arriving, which counts against nothing.
363
363
  if (isRecord(envelope?.event) && envelope.event.type === "task-offered") {
364
364
  if (capacityStated === undefined) {
365
365
  violations.push({ rule: "stream.taskOffered.beforeCapacity", path: "event.task",
366
- message: "a task was offered before the host stated any capacity: work is pulled, and nothing is allocated against a capacity nobody stated" });
366
+ message: "a task was offered before the host stated any capacity: work is pulled, and nothing is assigned against a capacity nobody stated" });
367
367
  }
368
368
  else if (seeded && stream.openCount() >= capacityStated && !TaskStream.carriesDial(envelope.event.task)) {
369
369
  violations.push({ rule: "stream.taskOffered.overCapacity", path: "event.task",
@@ -431,7 +431,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
431
431
  live = second;
432
432
  // The second client is held to everything the first was on connect: its methods, its login's
433
433
  // methods, the login it publishes from here, and a capacity stated to it, since the first's
434
- // statement died with the first and nothing may be allocated against a count nobody stated to it.
434
+ // statement died with the first and nothing may be assigned against a count nobody stated to it.
435
435
  connectObligations(second);
436
436
  requireCapabilityMethods(second, current().capabilities);
437
437
  try {
@@ -489,8 +489,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
489
489
  // transactions no snapshot carries, and are applied after it, in order.
490
490
  const carried = new Set();
491
491
  for (const task of Array.isArray(snapshot?.tasks) ? snapshot.tasks : []) {
492
- if (isRecord(task) && typeof task.allocationId === "string")
493
- carried.add(task.allocationId);
492
+ if (isRecord(task) && typeof task.assignmentId === "string")
493
+ carried.add(task.assignmentId);
494
494
  }
495
495
  for (const held of duringRead) {
496
496
  const event = held.event;
@@ -500,18 +500,18 @@ export async function exerciseAdapter(adapter, context, options = {}) {
500
500
  }
501
501
  // Evaluated where a superseded event was held: with none, the question was never asked.
502
502
  ruleEvaluated("snapshot.accounts.task", "snapshot.accounts.ended");
503
- if ((event.type === "task-offered" || event.type === "task-updated") && isRecord(event.task) && typeof event.task.allocationId === "string" && !carried.has(event.task.allocationId)) {
503
+ if ((event.type === "task-offered" || event.type === "task-updated") && isRecord(event.task) && typeof event.task.assignmentId === "string" && !carried.has(event.task.assignmentId)) {
504
504
  violations.push({ rule: "snapshot.accounts.task", path: "snapshot.tasks",
505
- message: `${String(event.task.id)} (${event.task.allocationId}) was published while the snapshot was read and the snapshot does not carry it: a snapshot accounts for everything the adapter emitted before it resolved` });
505
+ message: `${String(event.task.assignmentId)} (${event.task.assignmentId}) was published while the snapshot was read and the snapshot does not carry it: a snapshot accounts for everything the adapter emitted before it resolved` });
506
506
  }
507
- if (event.type === "task-ended" && typeof event.allocationId === "string" && carried.has(event.allocationId)) {
507
+ if (event.type === "task-ended" && typeof event.assignmentId === "string" && carried.has(event.assignmentId)) {
508
508
  violations.push({ rule: "snapshot.accounts.ended", path: "snapshot.tasks",
509
- message: `${String(event.taskId)} (${event.allocationId}) ended while the snapshot was read and the snapshot still carries it: a snapshot accounts for everything the adapter emitted before it resolved` });
509
+ message: `${String(event.assignmentId)} (${event.assignmentId}) ended while the snapshot was read and the snapshot still carries it: a snapshot accounts for everything the adapter emitted before it resolved` });
510
510
  }
511
511
  }
512
512
  duringRead.length = 0;
513
513
  requireCapabilityMethods(live, current().capabilities);
514
- // Capacity is stated, not requested: nothing may be allocated until it is, so a connection
514
+ // Capacity is stated, not requested: nothing may be assigned until it is, so a connection
515
515
  // that will not accept one is a connection nothing can be given to.
516
516
  // The guide's obligation on a voice adapter: consult the host before declaring the agent
517
517
  // ready, and on every change. An adapter that never asked cannot have.
@@ -557,7 +557,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
557
557
  // host raises it, lowers it, and takes it away, and the provider takes each as the ceiling it is.
558
558
  // A provider whose ceiling can only rise passes a zero it special-cases and still cannot go from
559
559
  // five to three, so the exercise moves the axis both ways. Zero is host-stopped: the agent's
560
- // capacity is elsewhere, and the provider allocates nothing and refuses nothing for it. Any offer
560
+ // capacity is elsewhere, and the provider assigns nothing and refuses nothing for it. Any offer
561
561
  // after a lower count is caught against it (stream.taskOffered.overCapacity).
562
562
  // A capacity is taken, never refused: each restatement is answered applied and nothing else
563
563
  // (validateResult), and any offer after a lower count is caught against it.
@@ -661,8 +661,8 @@ function userIdsIn(snapshot) {
661
661
  }
662
662
  for (const task of Array.isArray(snapshot?.tasks) ? snapshot.tasks : []) {
663
663
  const t = task;
664
- if (isRecord(t.interactionHistory) && Array.isArray(t.interactionHistory.steps))
665
- for (const step of t.interactionHistory.steps)
664
+ if (isRecord(t.history) && Array.isArray(t.history.steps))
665
+ for (const step of t.history.steps)
666
666
  if (isRecord(step))
667
667
  add(step.by);
668
668
  if (Array.isArray(t.onCall))
@@ -681,7 +681,7 @@ function userIdsIn(snapshot) {
681
681
  return [...ids];
682
682
  }
683
683
  const teamNamesUsers = (team) => isRecord(team) && (some(team.members) || some(team.requests));
684
- const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.interactionHistory) && Array.isArray(task.interactionHistory.steps) && task.interactionHistory.steps.some(step => isRecord(step) && step.by !== undefined)) ||
684
+ const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.history) && Array.isArray(task.history.steps) && task.history.steps.some(step => isRecord(step) && step.by !== undefined)) ||
685
685
  (isRecord(task.leadAssist) && task.leadAssist.leadId !== undefined) ||
686
686
  isRecord(task.assisting) ||
687
687
  isRecord(task.listening));
@@ -786,7 +786,7 @@ function eventTasks(envelope) {
786
786
  function undeterminedTasks(tasks, path) {
787
787
  return tasks.flatMap((task, index) => isRecord(task) && task.capabilitySource === "undetermined"
788
788
  ? [{ rule: "capabilitySource.undetermined", path: `${path}[${index}].capabilitySource`,
789
- message: `task ${String(task.id)} was published under terms the provider could not determine` }]
789
+ message: `task ${String(task.assignmentId)} was published under terms the provider could not determine` }]
790
790
  : []);
791
791
  }
792
792
  /**
@@ -866,8 +866,8 @@ export function assertTaskCapabilityWithdrawal(tasks, manifest, command) {
866
866
  };
867
867
  tasks.forEach((task, index) => {
868
868
  assertNoViolations(validateTask(task, context, `tasks[${index}]`), `Task capability withdrawal: tasks[${index}] must be a task the wire could carry`);
869
- if (task.id !== first.id) {
870
- throw new Error(`Task capability withdrawal must keep the task: tasks[${index}] is ${task.id}, and the offer was ${first.id}`);
869
+ if (task.assignmentId !== first.assignmentId) {
870
+ throw new Error(`Task capability withdrawal must keep the task: tasks[${index}] is ${task.assignmentId}, and the offer was ${first.assignmentId}`);
871
871
  }
872
872
  });
873
873
  const withdrawn = Object.keys(first.capabilities).filter(name => last.capabilities[name] === undefined);
@@ -922,8 +922,8 @@ export function assertReconnectWithMissedAssignments(before, reconnect, missedTa
922
922
  if (reconnect.event.type !== "snapshot" || reconnect.event.reason !== "reconnected") {
923
923
  throw new Error("Reconnect scenario requires a reconnected snapshot event");
924
924
  }
925
- const beforeIds = new Set(before.tasks.map(task => task.id));
926
- const refreshedIds = new Set(reconnect.event.snapshot.tasks.map(task => task.id));
925
+ const beforeIds = new Set(before.tasks.map(task => task.assignmentId));
926
+ const refreshedIds = new Set(reconnect.event.snapshot.tasks.map(task => task.assignmentId));
927
927
  for (const taskId of missedTaskIds) {
928
928
  if (beforeIds.has(taskId))
929
929
  throw new Error(`Task '${taskId}' was not missed before reconnect`);
@@ -988,11 +988,11 @@ export const SUPERSEDED_BY_A_SNAPSHOT = new Set([
988
988
  ]);
989
989
  export class TaskStream {
990
990
  tasks = new Map();
991
- // Every allocation this login has seen, and the ones whose task has ended: an offer never reuses
992
- // one, an event about a task names the life that is open, and a late event for a life that ended
993
- // is recognised as that rather than landing on the next customer under the same id.
994
- allocations = new Set();
995
- endedAllocations = new Set();
991
+ // Every assignment this stream has seen, and the ones whose task has ended: an assignment is
992
+ // introduced once, and an event naming one that ended is the late event it is, never applied to
993
+ // a task that is open.
994
+ assignments = new Set();
995
+ endedAssignments = new Set();
996
996
  // Every dial the stream can place an outcome against: one the host said it placed, or one a
997
997
  // task carried on `onCall` or in its record -- which is how a dial made before a transfer is known
998
998
  // to whoever holds the task now. `answered` or `ended` once its outcome arrived, since it comes once.
@@ -1007,7 +1007,7 @@ export class TaskStream {
1007
1007
  return;
1008
1008
  const entries = [
1009
1009
  ...(Array.isArray(task.onCall) ? task.onCall : []),
1010
- ...(isRecord(task.interactionHistory) && Array.isArray(task.interactionHistory.steps) ? task.interactionHistory.steps : []),
1010
+ ...(isRecord(task.history) && Array.isArray(task.history.steps) ? task.history.steps : []),
1011
1011
  ];
1012
1012
  for (const entry of entries) {
1013
1013
  if (isRecord(entry) && typeof entry.dialId === "string")
@@ -1015,21 +1015,16 @@ export class TaskStream {
1015
1015
  }
1016
1016
  }
1017
1017
  /**
1018
- * Whether an event about a task names the life that is open under its id. One naming a life that
1019
- * has ended is the late event the allocation exists to catch; one naming a life nobody has seen is
1020
- * a different fault. Either way it is not applied to the open task.
1018
+ * Why an event names no open task: the assignment ended, so this is the late event that must not
1019
+ * land on anything, or nobody has seen it. Either way it is not applied.
1021
1020
  */
1022
- namesTheOpenLife(event, known, at, refuse) {
1023
- const named = event.allocationId;
1024
- if (typeof named !== "string" || named === known.allocation)
1025
- return true;
1026
- if (this.endedAllocations.has(named)) {
1027
- refuse("stream.allocation.ended", `${at}.allocationId`, `${String(event.type)} names allocation ${named}, a life of ${String(event.taskId)} that has ended; the life open under that id is ${known.allocation}, and a late event lands on the life it names, never the next`);
1021
+ notOpen(event, id, at, refuse) {
1022
+ if (this.endedAssignments.has(id)) {
1023
+ refuse("stream.assignment.ended", `${at}.assignmentId`, `${String(event.type)} names ${id}, an assignment that has ended: a late event lands on the assignment it names, never on another`);
1028
1024
  }
1029
1025
  else {
1030
- refuse("stream.allocation.unknown", `${at}.allocationId`, `${named} is not an allocation this login has seen`);
1026
+ refuse("stream.assignment.unknown", `${at}.assignmentId`, `${id} is not an assignment this stream has seen`);
1031
1027
  }
1032
- return false;
1033
1028
  }
1034
1029
  /** How many tasks the stream currently holds open. */
1035
1030
  openCount() { return this.tasks.size; }
@@ -1052,9 +1047,9 @@ export class TaskStream {
1052
1047
  }
1053
1048
  /** The entries of a task's record, each by step and instant, or undefined where the task carries no record. */
1054
1049
  static record(task) {
1055
- if (!isRecord(task) || !isRecord(task.interactionHistory) || !Array.isArray(task.interactionHistory.steps))
1050
+ if (!isRecord(task) || !isRecord(task.history) || !Array.isArray(task.history.steps))
1056
1051
  return undefined;
1057
- return new Set(task.interactionHistory.steps.filter(isRecord).map(entry => `${String(entry.step)}@${String(entry.at)}`));
1052
+ return new Set(task.history.steps.filter(isRecord).map(entry => `${String(entry.step)}@${String(entry.at)}`));
1058
1053
  }
1059
1054
  /** What a restated record lost of the one read before it: nothing, or the entries by step and instant. */
1060
1055
  static lost(was, now) {
@@ -1079,7 +1074,7 @@ export class TaskStream {
1079
1074
  && task.onCall.some(entry => isRecord(entry) && entry.role === "party" && entry.stage === "ringing" && (typeof entry.dialId === "string" || (task.phase === "preview" && task.atDeadline === "calls" && entry.dialId === undefined)));
1080
1075
  return { partyRingingBeforeWork, channel: String(isRecord(task) ? task.channel : undefined), completionMode: String(isRecord(task) ? task.completionMode : undefined),
1081
1076
  wrapAllowance: isRecord(task) && typeof task.wrapAllowance === "number" ? task.wrapAllowance : undefined,
1082
- phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages, record: TaskStream.record(task), allocation: String(isRecord(task) ? task.allocationId : undefined) };
1077
+ phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages, record: TaskStream.record(task), assignment: String(isRecord(task) ? task.assignmentId : undefined) };
1083
1078
  }
1084
1079
  /**
1085
1080
  * Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
@@ -1092,32 +1087,32 @@ export class TaskStream {
1092
1087
  resync(snapshot, at) {
1093
1088
  const found = [];
1094
1089
  const refuse = (rule, where, message) => found.push({ rule, path: where, message });
1095
- ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.interactionHistory", "stream.snapshot.phase", "stream.snapshot.media");
1090
+ ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.history", "stream.snapshot.phase", "stream.snapshot.media");
1096
1091
  if (isRecord(snapshot) && Array.isArray(snapshot.tasks)) {
1097
1092
  snapshot.tasks.forEach((task, index) => {
1098
- if (!isRecord(task) || typeof task.id !== "string")
1093
+ if (!isRecord(task) || typeof task.assignmentId !== "string")
1099
1094
  return;
1100
- const was = this.tasks.get(task.id);
1101
- if (was === undefined || was.allocation !== String(task.allocationId))
1095
+ const was = this.tasks.get(task.assignmentId);
1096
+ if (was === undefined)
1102
1097
  return;
1103
1098
  if ((was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
1104
- refuse("stream.snapshot.capabilitySource", `${at}.tasks[${index}].capabilitySource`, `${task.id} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
1099
+ refuse("stream.snapshot.capabilitySource", `${at}.tasks[${index}].capabilitySource`, `${task.assignmentId} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
1105
1100
  }
1106
1101
  // A record once read is not unread: a resync restates it whole, or with more, never with less.
1107
1102
  const lost = TaskStream.lost(was.record, TaskStream.record(task));
1108
1103
  if (lost.length > 0) {
1109
- refuse("stream.snapshot.interactionHistory", `${at}.tasks[${index}].interactionHistory`, `${task.id}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
1104
+ refuse("stream.snapshot.history", `${at}.tasks[${index}].history`, `${task.assignmentId}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
1110
1105
  }
1111
1106
  const to = String(task.phase);
1112
1107
  const reachable = REACHABLE_PHASES[was.phase];
1113
1108
  if (reachable !== undefined && TASK_PHASES_ORDERED.includes(to) && !reachable.has(to)) {
1114
1109
  const connectingBack = was.phase === "completing" && this.partyDialled(task) && (to === "in-progress" || to === "paused");
1115
1110
  if (!connectingBack) {
1116
- refuse("stream.snapshot.phase", `${at}.tasks[${index}].phase`, `${task.id} was ${was.phase} and the snapshot says ${to}: a task does not go backwards, on an update or on a resync`);
1111
+ refuse("stream.snapshot.phase", `${at}.tasks[${index}].phase`, `${task.assignmentId} was ${was.phase} and the snapshot says ${to}: a task does not go backwards, on an update or on a resync`);
1117
1112
  }
1118
1113
  }
1119
1114
  if (was.media === "started" && (to === "in-progress" || to === "paused") && task.media !== "started") {
1120
- refuse("stream.snapshot.media", `${at}.tasks[${index}].media`, `${task.id}'s audio was up and the snapshot carries it ${to} without it: media ends on task-media-ended and the call moves on, so a snapshot that forgets the audio lost state`);
1115
+ refuse("stream.snapshot.media", `${at}.tasks[${index}].media`, `${task.assignmentId}'s audio was up and the snapshot carries it ${to} without it: media ends on task-media-ended and the call moves on, so a snapshot that forgets the audio lost state`);
1121
1116
  }
1122
1117
  });
1123
1118
  }
@@ -1130,10 +1125,10 @@ export class TaskStream {
1130
1125
  if (!isRecord(snapshot) || !Array.isArray(snapshot.tasks))
1131
1126
  return;
1132
1127
  for (const task of snapshot.tasks) {
1133
- if (isRecord(task) && typeof task.id === "string") {
1134
- this.tasks.set(task.id, TaskStream.stated(task));
1135
- if (typeof task.allocationId === "string")
1136
- this.allocations.add(task.allocationId);
1128
+ if (isRecord(task) && typeof task.assignmentId === "string") {
1129
+ this.tasks.set(task.assignmentId, TaskStream.stated(task));
1130
+ if (typeof task.assignmentId === "string")
1131
+ this.assignments.add(task.assignmentId);
1137
1132
  }
1138
1133
  this.noteDials(task);
1139
1134
  }
@@ -1146,25 +1141,22 @@ export class TaskStream {
1146
1141
  return found;
1147
1142
  const at = `${path}.event`;
1148
1143
  const refuse = (rule, where, message) => found.push({ rule, path: where, message });
1149
- const id = typeof event.taskId === "string" ? event.taskId : isRecord(event.task) && typeof event.task.id === "string" ? event.task.id : undefined;
1144
+ const id = typeof event.assignmentId === "string" ? event.assignmentId : isRecord(event.task) && typeof event.task.assignmentId === "string" ? event.task.assignmentId : undefined;
1150
1145
  const known = id === undefined ? undefined : this.tasks.get(id);
1151
1146
  switch (event.type) {
1152
1147
  case "snapshot":
1153
1148
  found.push(...this.resync(event.snapshot, `${at}.snapshot`));
1154
1149
  break;
1155
1150
  case "task-offered": {
1156
- ruleEvaluated("stream.taskOffered.duplicate", "stream.taskOffered.allocation");
1151
+ ruleEvaluated("stream.taskOffered.duplicate");
1157
1152
  if (id === undefined)
1158
1153
  break;
1159
- if (known !== undefined)
1160
- refuse("stream.taskOffered.duplicate", `${at}.task.id`, `${id} is already on the stream; an offer introduces a task once`);
1161
- const allocation = isRecord(event.task) ? event.task.allocationId : undefined;
1162
- if (typeof allocation === "string") {
1163
- if (this.allocations.has(allocation)) {
1164
- refuse("stream.taskOffered.allocation", `${at}.task.allocationId`, `${allocation} was already an allocation on this login: an allocation is minted once per offer and never reused, whatever the task id does`);
1165
- }
1166
- this.allocations.add(allocation);
1154
+ // An assignment is introduced once: not while it is open, and never again once it has ended,
1155
+ // whatever the platform did with its own handle.
1156
+ if (this.assignments.has(id)) {
1157
+ refuse("stream.taskOffered.duplicate", `${at}.task.assignmentId`, known !== undefined ? `${id} is already on the stream; an offer introduces an assignment once` : `${id} was already an assignment on this stream: an assignment is named once per offer and never reused, whatever the platform does with its own handle`);
1167
1158
  }
1159
+ this.assignments.add(id);
1168
1160
  this.tasks.set(id, TaskStream.stated(event.task));
1169
1161
  this.noteDials(event.task);
1170
1162
  break;
@@ -1174,22 +1166,17 @@ export class TaskStream {
1174
1166
  break;
1175
1167
  ruleEvaluated("stream.taskUpdated.unknown");
1176
1168
  if (known === undefined) {
1177
- refuse("stream.taskUpdated.unknown", `${at}.task.id`, `${id} was never offered or carried on a snapshot`);
1169
+ refuse("stream.taskUpdated.unknown", `${at}.task.assignmentId`, `${id} was never offered or carried on a snapshot`);
1178
1170
  break;
1179
1171
  }
1180
1172
  // The rules about a known task are evaluated only once there is one.
1181
- ruleEvaluated("stream.taskUpdated.capabilitySource", "stream.taskUpdated.phase", "stream.taskUpdated.allocation", "stream.taskUpdated.mediaOpen", "stream.taskUpdated.interactionHistory", "stream.taskMediaEnded.follow", "stream.taskUpdated.media", "stream.taskUpdated.stage", "stream.taskUpdated.stage.lingering");
1173
+ ruleEvaluated("stream.taskUpdated.capabilitySource", "stream.taskUpdated.phase", "stream.taskUpdated.mediaOpen", "stream.taskUpdated.history", "stream.taskMediaEnded.follow", "stream.taskUpdated.media", "stream.taskUpdated.stage", "stream.taskUpdated.stage.lingering");
1182
1174
  // Terms once read stay read. A re-read that fails is not a new fact about the task, so the
1183
1175
  // last statement stands and the failure is a diagnostic; undetermined is a place a task
1184
1176
  // starts from, never one it returns to.
1185
1177
  if ((known.source === "queue" || known.source === "ungoverned") && isRecord(event.task) && event.task.capabilitySource === "undetermined") {
1186
1178
  refuse("stream.taskUpdated.capabilitySource", `${at}.task.capabilitySource`, `${id} was published under ${known.source} terms and now says undetermined: terms once read stay read, and a re-read that fails is a diagnostic, not a republish`);
1187
1179
  }
1188
- // An update is of the life that is open. One carrying another allocation is a copy of a
1189
- // different life of this id -- a stale republish of the last customer's call, or the next one's.
1190
- if (isRecord(event.task) && String(event.task.allocationId) !== known.allocation) {
1191
- refuse("stream.taskUpdated.allocation", `${at}.task.allocationId`, `${id} is open as allocation ${known.allocation} and the update says ${String(event.task.allocationId)}: an update restates the life that is open, never another`);
1192
- }
1193
1180
  // A task does not go backwards. The stream sees publications, not transitions, and a task may
1194
1181
  // pass through a phase between two, so what is refused is a phase unreachable from the last
1195
1182
  // one read by the guide's table: back to pending, back to confirmed once work began, out of
@@ -1210,7 +1197,7 @@ export class TaskStream {
1210
1197
  {
1211
1198
  const lost = TaskStream.lost(known.record, TaskStream.record(event.task));
1212
1199
  if (lost.length > 0) {
1213
- refuse("stream.taskUpdated.interactionHistory", `${at}.task.interactionHistory`, `${id}'s record lost ${lost.join(", ")} on the update: an entry read by the host stays in the record until the task ends`);
1200
+ refuse("stream.taskUpdated.history", `${at}.task.history`, `${id}'s record lost ${lost.join(", ")} on the update: an entry read by the host stays in the record until the task ends`);
1214
1201
  }
1215
1202
  }
1216
1203
  // A task completes after its media ends, never around it: an update that moves a task to
@@ -1255,15 +1242,13 @@ export class TaskStream {
1255
1242
  this.noteDials(event.task);
1256
1243
  break;
1257
1244
  case "task-media-started":
1258
- ruleEvaluated("stream.taskMediaStarted.unknown", "stream.taskMediaStarted.beforeWork", "stream.taskMediaStarted.duplicate", "stream.taskMedia.channel", "stream.allocation.ended", "stream.allocation.unknown");
1245
+ ruleEvaluated("stream.taskMediaStarted.beforeWork", "stream.taskMediaStarted.duplicate", "stream.taskMedia.channel", "stream.assignment.ended", "stream.assignment.unknown");
1259
1246
  if (id === undefined)
1260
1247
  break;
1261
1248
  if (known === undefined) {
1262
- refuse("stream.taskMediaStarted.unknown", `${at}.taskId`, `${id} was never offered or carried on a snapshot`);
1249
+ this.notOpen(event, id, at, refuse);
1263
1250
  break;
1264
1251
  }
1265
- if (!this.namesTheOpenLife(event, known, at, refuse))
1266
- break;
1267
1252
  // Ring-back is audio: a task whose party the host is dialling has media from dialling on,
1268
1253
  // whatever its phase; every other task not at work has none.
1269
1254
  if (known.channel !== "voice") {
@@ -1271,52 +1256,47 @@ export class TaskStream {
1271
1256
  break;
1272
1257
  }
1273
1258
  if (!AT_WORK.has(known.phase) && !known.partyRingingBeforeWork) {
1274
- refuse("stream.taskMediaStarted.beforeWork", `${at}.taskId`, `media cannot arrive on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
1259
+ refuse("stream.taskMediaStarted.beforeWork", `${at}.assignmentId`, `media cannot arrive on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
1275
1260
  }
1276
1261
  if (known.media === "started") {
1277
- refuse("stream.taskMediaStarted.duplicate", `${at}.taskId`, `media already started on ${id}; started and ended alternate`);
1262
+ refuse("stream.taskMediaStarted.duplicate", `${at}.assignmentId`, `media already started on ${id}; started and ended alternate`);
1278
1263
  }
1279
1264
  known.media = "started";
1280
1265
  break;
1281
1266
  case "task-media-ended":
1282
- ruleEvaluated("stream.taskMediaEnded.unknown", "stream.taskMediaEnded.beforeWork", "stream.taskMediaEnded.silent", "stream.taskMedia.channel", "stream.allocation.ended", "stream.allocation.unknown");
1267
+ ruleEvaluated("stream.taskMediaEnded.beforeWork", "stream.taskMediaEnded.silent", "stream.taskMedia.channel", "stream.assignment.ended", "stream.assignment.unknown");
1283
1268
  if (id === undefined)
1284
1269
  break;
1285
1270
  if (known === undefined) {
1286
- refuse("stream.taskMediaEnded.unknown", `${at}.taskId`, `${id} was never offered or carried on a snapshot`);
1271
+ this.notOpen(event, id, at, refuse);
1287
1272
  break;
1288
1273
  }
1289
- if (!this.namesTheOpenLife(event, known, at, refuse))
1290
- break;
1291
1274
  if (known.channel !== "voice") {
1292
1275
  refuse("stream.taskMedia.channel", `${at}.type`, "only a voice task has media transitions");
1293
1276
  break;
1294
1277
  }
1295
1278
  if (!WORK_BEGUN.has(known.phase) && !known.partyRingingBeforeWork) {
1296
- refuse("stream.taskMediaEnded.beforeWork", `${at}.taskId`, `media cannot end on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
1279
+ refuse("stream.taskMediaEnded.beforeWork", `${at}.assignmentId`, `media cannot end on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
1297
1280
  }
1298
1281
  if (known.media !== "started") {
1299
- refuse("stream.taskMediaEnded.silent", `${at}.taskId`, `media cannot end on ${id} where none arrived: audio attaches on task-media-started, or on a task carried with media started`);
1282
+ refuse("stream.taskMediaEnded.silent", `${at}.assignmentId`, `media cannot end on ${id} where none arrived: audio attaches on task-media-started, or on a task carried with media started`);
1300
1283
  }
1301
1284
  known.media = "ended";
1302
1285
  break;
1303
1286
  case "task-ended":
1304
- ruleEvaluated("stream.taskEnded.unknown", "stream.allocation.ended", "stream.allocation.unknown");
1287
+ ruleEvaluated("stream.assignment.ended", "stream.assignment.unknown");
1305
1288
  if (id === undefined)
1306
1289
  break;
1307
1290
  if (known === undefined) {
1308
- refuse("stream.taskEnded.unknown", `${at}.taskId`, `${id} was never offered or carried on a snapshot`);
1291
+ this.notOpen(event, id, at, refuse);
1309
1292
  break;
1310
1293
  }
1311
- // A late ending for a life that is over must not end the life that is open under the same id.
1312
- if (!this.namesTheOpenLife(event, known, at, refuse))
1313
- break;
1314
1294
  // A voice task ends after its audio ends, never around it, whatever the outcome: a take-over
1315
1295
  // and a lead leaving included. An ending with the audio still up leaves the host holding an
1316
1296
  // open media session and an open leg on a task that no longer exists.
1317
1297
  ruleEvaluated("stream.taskEnded.mediaOpen");
1318
1298
  if (known.media === "started") {
1319
- refuse("stream.taskEnded.mediaOpen", `${at}.taskId`, `${id} ended with its media still started: the audio ends first, on task-media-ended, whatever the outcome`);
1299
+ refuse("stream.taskEnded.mediaOpen", `${at}.assignmentId`, `${id} ended with its media still started: the audio ends first, on task-media-ended, whatever the outcome`);
1320
1300
  }
1321
1301
  // Off voice there is no media event, so the update that moves a task to completing is the
1322
1302
  // provider's word that interaction ended and the moment the wrap allowance starts. A provider
@@ -1327,16 +1307,16 @@ export class TaskStream {
1327
1307
  && known.phase !== "completing" && isRecord(event.outcome) && event.outcome.type === "completed") {
1328
1308
  refuse("stream.taskEnded.unwrapped", `${at}.outcome`, `${id} was completed from ${known.phase} with a wrap allowance of ${known.wrapAllowance}s under provider-automatic: off voice, completing is the provider's word that interaction ended and the allowance's start, and it was never published`);
1329
1309
  }
1330
- this.endedAllocations.add(known.allocation);
1310
+ this.endedAssignments.add(id);
1331
1311
  this.tasks.delete(id);
1332
1312
  break;
1333
1313
  case "dial-outcome": {
1334
1314
  // An outcome ends a dial somebody placed, once. The task it names may already have ended;
1335
1315
  // a dial placed late routinely outlives its call, which is why the dial has its own identity,
1336
- // and why the outcome names the allocation: the host routes it to that life, ended or not.
1337
- ruleEvaluated("stream.allocation.unknown");
1338
- if (typeof event.allocationId === "string" && !this.allocations.has(event.allocationId)) {
1339
- refuse("stream.allocation.unknown", `${at}.allocationId`, `${event.allocationId} is not an allocation this login has seen`);
1316
+ // and why the outcome names the assignment: the host routes it to that life, ended or not.
1317
+ ruleEvaluated("stream.assignment.unknown");
1318
+ if (typeof event.assignmentId === "string" && !this.assignments.has(event.assignmentId)) {
1319
+ refuse("stream.assignment.unknown", `${at}.assignmentId`, `${event.assignmentId} is not an assignment this stream has seen`);
1340
1320
  }
1341
1321
  if (typeof event.dialId !== "string")
1342
1322
  break;
@@ -1366,7 +1346,7 @@ export class TaskStream {
1366
1346
  async function driveOneCall(drive) {
1367
1347
  const found = [];
1368
1348
  const refuse = (rule, path, message) => found.push({ rule, path, message });
1369
- const isTask = (value) => isRecord(value) && typeof value.id === "string";
1349
+ const isTask = (value) => isRecord(value) && typeof value.assignmentId === "string";
1370
1350
  // Wait for an envelope that satisfies `matches`, looking first at what already arrived.
1371
1351
  const waitFor = (what, matches, from, within = { ms: drive.timeoutMs, onExpiry: () => refuse("drive.timeout", "drive", `the provider owed ${what} within ${drive.timeoutMs}ms and it never arrived`) }) => new Promise(resolve => {
1372
1352
  for (let index = from; index < drive.events.length; index += 1) {
@@ -1445,21 +1425,17 @@ async function driveOneCall(drive) {
1445
1425
  found.push(...drive.streams.stream.resync(snapshot, "drive.reload.snapshot"));
1446
1426
  found.push(...drive.streams.breaks.seed(snapshot));
1447
1427
  const carried = isRecord(snapshot) && Array.isArray(snapshot.tasks)
1448
- ? snapshot.tasks.find(task => isRecord(task) && task.id === taskId) : undefined;
1428
+ ? snapshot.tasks.find(task => isRecord(task) && task.assignmentId === taskId) : undefined;
1449
1429
  ruleEvaluated("drive.reload.snapshot");
1450
1430
  if (carried === undefined) {
1451
1431
  refuse("drive.reload.snapshot", "drive.reload.snapshot", `a second adapter built from the same login does not carry ${taskId} on its snapshot, and the task is still open`);
1452
1432
  return;
1453
1433
  }
1454
- // The allocation is part of the task, not memory beside it: a reload brings the same life back.
1455
- ruleEvaluated("drive.reload.allocation", "drive.reload.history");
1456
- if (carried.allocationId !== allocationOf()) {
1457
- refuse("drive.reload.allocation", "drive.reload.allocation", `a second adapter built from the same login carries ${taskId} as allocation ${String(carried.allocationId)}; the first published ${allocationOf()}, and a life does not change its name on a reload`);
1458
- }
1459
- const history = carried.interactionHistory;
1434
+ ruleEvaluated("drive.reload.history");
1435
+ const history = carried.history;
1460
1436
  const steps = isRecord(history) && Array.isArray(history.steps) ? history.steps : [];
1461
1437
  // A record once read is not unread across a reload either: every entry the first adapter published is here.
1462
- const before = latestTask().interactionHistory;
1438
+ const before = latestTask().history;
1463
1439
  const wasRead = isRecord(before) && Array.isArray(before.steps) ? before.steps.filter(isRecord) : [];
1464
1440
  const lost = wasRead.filter(entry => !steps.some(now => isRecord(now) && now.step === entry.step && now.at === entry.at))
1465
1441
  .map(entry => `${String(entry.step)}@${String(entry.at)}`);
@@ -1488,7 +1464,7 @@ async function driveOneCall(drive) {
1488
1464
  }
1489
1465
  session = undefined;
1490
1466
  ruleEvaluated("drive.reload.openMedia");
1491
- const reopened = await second.openMedia?.({ taskId, allocationId: allocationOf(), localAudio: drive.localAudio });
1467
+ const reopened = await second.openMedia?.({ assignmentId: taskId, localAudio: drive.localAudio });
1492
1468
  const malformed = validateResult(reopened, "openMedia", "drive.reload.openMedia");
1493
1469
  found.push(...malformed);
1494
1470
  if (malformed.length === 0 && isRecord(reopened) && reopened.status === "opened")
@@ -1543,12 +1519,11 @@ async function driveOneCall(drive) {
1543
1519
  task = offered.found;
1544
1520
  cursor = offered.at;
1545
1521
  }
1546
- const taskId = task.id;
1547
- const allocationOf = () => String(latestTask().allocationId);
1522
+ const taskId = task.assignmentId;
1548
1523
  const latestTask = () => task;
1549
1524
  const updated = (until, what) => waitFor(what, envelope => {
1550
1525
  const event = envelope.event;
1551
- if ((event.type === "task-updated" || event.type === "task-offered") && isTask(event.task) && event.task.id === taskId) {
1526
+ if ((event.type === "task-updated" || event.type === "task-offered") && isTask(event.task) && event.task.assignmentId === taskId) {
1552
1527
  task = event.task;
1553
1528
  return until(event.task) ? event.task : undefined;
1554
1529
  }
@@ -1557,7 +1532,7 @@ async function driveOneCall(drive) {
1557
1532
  cursor = hit.at; return hit; });
1558
1533
  const ended = (within) => waitFor("a task-ended for the driven task", envelope => {
1559
1534
  const event = envelope.event;
1560
- return event.type === "task-ended" && event.taskId === taskId ? event : undefined;
1535
+ return event.type === "task-ended" && event.assignmentId === taskId ? event : undefined;
1561
1536
  }, cursor, within);
1562
1537
  // Every command the drive sends is validated against the task as published, and its answer for its method.
1563
1538
  ruleEvaluated("drive.timeout");
@@ -1573,7 +1548,7 @@ async function driveOneCall(drive) {
1573
1548
  ruleEvaluated("drive.command.rejected");
1574
1549
  let result;
1575
1550
  try {
1576
- result = await drive.connection.execute({ taskId, allocationId: allocationOf(), command });
1551
+ result = await drive.connection.execute({ assignmentId: taskId, command });
1577
1552
  }
1578
1553
  catch (error) {
1579
1554
  refuse("drive.command.rejected", `drive.command.${String(command.type)}`, `execute rejected rather than answered: ${String(error)}`);
@@ -1620,7 +1595,7 @@ async function driveOneCall(drive) {
1620
1595
  const phase = String(latestTask().phase);
1621
1596
  let answer;
1622
1597
  try {
1623
- answer = await drive.connection.execute({ taskId, allocationId: allocationOf(), command: { type: "hold" } });
1598
+ answer = await drive.connection.execute({ assignmentId: taskId, command: { type: "hold" } });
1624
1599
  }
1625
1600
  catch (error) {
1626
1601
  refuse("drive.command.rejected", "drive.command.hold", `execute rejected rather than answered: ${String(error)}`);
@@ -1640,13 +1615,13 @@ async function driveOneCall(drive) {
1640
1615
  if (latestTask().media !== "started") {
1641
1616
  const started = await waitFor("task-media-started for the driven task", envelope => {
1642
1617
  const event = envelope.event;
1643
- return event.type === "task-media-started" && event.taskId === taskId ? event : undefined;
1618
+ return event.type === "task-media-started" && event.assignmentId === taskId ? event : undefined;
1644
1619
  }, acceptedAt);
1645
1620
  if (started === undefined)
1646
1621
  return found;
1647
1622
  cursor = Math.max(cursor, started.at);
1648
1623
  }
1649
- const opened = await drive.connection.openMedia?.({ taskId, allocationId: allocationOf(), localAudio: drive.localAudio });
1624
+ const opened = await drive.connection.openMedia?.({ assignmentId: taskId, localAudio: drive.localAudio });
1650
1625
  const malformed = validateResult(opened, "openMedia", "drive.openMedia");
1651
1626
  found.push(...malformed);
1652
1627
  if (malformed.length === 0 && isRecord(opened) && opened.status === "opened")
@@ -1661,7 +1636,7 @@ async function driveOneCall(drive) {
1661
1636
  const reportedLegs = [];
1662
1637
  /** The duration the provider's record states for the leg at `at`, as the task stands now. */
1663
1638
  const closedAs = (at) => {
1664
- const history = latestTask().interactionHistory;
1639
+ const history = latestTask().history;
1665
1640
  const leg = isRecord(history) && Array.isArray(history.steps)
1666
1641
  ? history.steps.find(entry => isRecord(entry) && entry.step === "muted" && entry.at === canonicalTimes.get(at)) : undefined;
1667
1642
  return typeof leg?.seconds === "number" ? leg.seconds : undefined;
@@ -1670,8 +1645,8 @@ async function driveOneCall(drive) {
1670
1645
  const report = async (body) => {
1671
1646
  ruleEvaluated("drive.recordStep.rejected", "drive.recordStep.failed");
1672
1647
  // The drive holds its own report to the contract before it crosses, as a host must.
1673
- const leg = { taskId, allocationId: allocationOf(), step: "muted", mutedBy: "host", ...body };
1674
- const own = validateInteractionReport(leg, "drive.recordStep.report", drive.manifest);
1648
+ const leg = { assignmentId: taskId, step: "muted", mutedBy: "host", ...body };
1649
+ const own = validateHistoryReport(leg, "drive.recordStep.report", drive.manifest);
1675
1650
  found.push(...own);
1676
1651
  if (own.length > 0)
1677
1652
  return;
@@ -1756,9 +1731,9 @@ async function driveOneCall(drive) {
1756
1731
  // that is not coming, so the run reads by the rule and not by its timeout.
1757
1732
  const mediaEnded = await waitFor("task-media-ended after end-call", envelope => {
1758
1733
  const event = envelope.event;
1759
- if (event.type === "task-media-ended" && event.taskId === taskId)
1734
+ if (event.type === "task-media-ended" && event.assignmentId === taskId)
1760
1735
  return "ended";
1761
- if (event.type === "task-updated" && isTask(event.task) && event.task.id === taskId && event.task.phase === "completing")
1736
+ if (event.type === "task-updated" && isTask(event.task) && event.task.assignmentId === taskId && event.task.phase === "completing")
1762
1737
  return "completing";
1763
1738
  return undefined;
1764
1739
  }, cursor);
@@ -1815,7 +1790,7 @@ async function driveOneCall(drive) {
1815
1790
  catch (error) {
1816
1791
  refuse("drive.command.rejected", "drive.completion", `snapshot() after an unsettled completion rejected: ${String(error)}`);
1817
1792
  }
1818
- const still = isRecord(resync) && Array.isArray(resync.tasks) && resync.tasks.some(t => isRecord(t) && t.id === taskId && t.allocationId === allocationOf());
1793
+ const still = isRecord(resync) && Array.isArray(resync.tasks) && resync.tasks.some(t => isRecord(t) && t.assignmentId === taskId);
1819
1794
  refuse("drive.completion.unsettled", "drive.completion", still
1820
1795
  ? `complete was applied and ${settle}ms later the task-ended has not come and a snapshot still carries ${taskId}: applied says the provider completed the task, and it has not`
1821
1796
  : `complete was applied and ${settle}ms later the task-ended has not come; a snapshot no longer carries ${taskId}, so the ending was owed and never sent`);
@@ -1831,7 +1806,7 @@ async function driveOneCall(drive) {
1831
1806
  const lastPublished = () => {
1832
1807
  for (let index = drive.events.length - 1; index >= 0; index -= 1) {
1833
1808
  const event = drive.events[index].event;
1834
- if ((event.type === "task-updated" || event.type === "task-offered") && isTask(event.task) && event.task.id === taskId)
1809
+ if ((event.type === "task-updated" || event.type === "task-offered") && isTask(event.task) && event.task.assignmentId === taskId)
1835
1810
  return event.task;
1836
1811
  }
1837
1812
  return latestTask();
@@ -1842,7 +1817,7 @@ async function driveOneCall(drive) {
1842
1817
  const published = lastPublished();
1843
1818
  if (published === reported.task)
1844
1819
  continue;
1845
- const history = published.interactionHistory;
1820
+ const history = published.history;
1846
1821
  if (isRecord(history) && Array.isArray(history.steps)) {
1847
1822
  const { at } = reported;
1848
1823
  const leg = history.steps.find(entry => isRecord(entry) && entry.step === "muted" && entry.at === at);