@xema/omni-protocol 0.1.99 → 0.1.100

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
@@ -15,7 +15,7 @@ const STATE_SUBJECTS = [
15
15
  "task.onCall",
16
16
  "task.leadAssist",
17
17
  "task.takenOver",
18
- "task.media",
18
+ "task.audio",
19
19
  "task.acceptance",
20
20
  "task.outcomes",
21
21
  "task.destinations",
@@ -28,15 +28,15 @@ const STATE_SUBJECTS = [
28
28
  "team.tasks",
29
29
  "team.listening",
30
30
  "contacts",
31
- "scheduledActivities",
31
+ "calendar",
32
32
  "team.policies",
33
33
  ];
34
34
  // Pinned to the event union the way validation pins its closed sets: a type added to
35
35
  // `ProviderEvent` without a row here, or a row it lacks, is a compile error.
36
36
  const EVENT_TYPES = {
37
37
  snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
38
- "task-media-started": true, "task-media-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
39
- "team-updated": true, "team-media-started": true, "team-media-ended": true, "contacts-updated": true, "calendar-updated": true,
38
+ "task-audio-started": true, "task-audio-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
39
+ "team-updated": true, "team-audio-started": true, "team-audio-ended": true, "contacts-updated": true, "calendar-updated": true,
40
40
  };
41
41
  const CONTRACT_SUBJECTS = [
42
42
  ...STATE_SUBJECTS,
@@ -62,8 +62,8 @@ function observeTask(value, seen) {
62
62
  seen.add("task.leadAssist");
63
63
  if (value.takenOver !== undefined)
64
64
  seen.add("task.takenOver");
65
- if (value.media !== undefined)
66
- seen.add("task.media");
65
+ if (value.audio !== undefined)
66
+ seen.add("task.audio");
67
67
  if (value.acceptance !== undefined)
68
68
  seen.add("task.acceptance");
69
69
  const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
@@ -115,8 +115,8 @@ function observeSnapshot(value, seen) {
115
115
  observeTeam(value.team, seen);
116
116
  if (some(value.contacts))
117
117
  seen.add("contacts");
118
- if (some(value.scheduledActivities))
119
- seen.add("scheduledActivities");
118
+ if (some(value.calendar))
119
+ seen.add("calendar");
120
120
  }
121
121
  function observeEvent(envelope, seen) {
122
122
  const event = isRecord(envelope) ? envelope.event : undefined;
@@ -143,8 +143,8 @@ function observeEvent(envelope, seen) {
143
143
  seen.add("contacts");
144
144
  break;
145
145
  case "calendar-updated":
146
- if (some(event.scheduledActivities))
147
- seen.add("scheduledActivities");
146
+ if (some(event.calendar))
147
+ seen.add("calendar");
148
148
  break;
149
149
  default: break;
150
150
  }
@@ -316,7 +316,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
316
316
  requireMethod(on, "dial", "the manifest declares dial");
317
317
  // On a softphone the call's audio lands in Omni, so the adapter has to open it; on a desk phone the host opens nothing.
318
318
  if (softphone)
319
- requireMethod(on, "openMedia", "the login is on a softphone");
319
+ requireMethod(on, "openAudio", "the login is on a softphone");
320
320
  // The microphone is the host's, so on a softphone every call can be muted by it, and the
321
321
  // record of that leg is the provider's to take; on a desk phone the host holds no microphone.
322
322
  if (softphone)
@@ -355,7 +355,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
355
355
  violations.push({ rule: "diagnostic.raised", path: "event.diagnostic",
356
356
  message: `the provider reported a diagnostic: expected ${String(envelope.event.expected)}; observed ${String(envelope.event.observed)}` });
357
357
  }
358
- violations.push(...undeterminedTasks(eventTasks(envelope), "event"));
358
+ violations.push(...notYetReadTasks(eventTasks(envelope), "event"));
359
359
  lockedPartySeen ||= eventTasks(envelope).some(locksParty);
360
360
  if (eventNamesUsers(envelope))
361
361
  requireMethod(live, "getUserDetails", "an event publishes a UserId");
@@ -457,7 +457,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
457
457
  if (broken.length > 0)
458
458
  tellRefused({ artefact: "snapshot", violations: broken });
459
459
  const tasks = isRecord(read) && Array.isArray(read.tasks) ? read.tasks : [];
460
- violations.push(...undeterminedTasks(tasks, `${path}.tasks`));
460
+ violations.push(...notYetReadTasks(tasks, `${path}.tasks`));
461
461
  lockedPartySeen ||= tasks.some(locksParty);
462
462
  if (publishesUserIds(read)) {
463
463
  requireMethod(source, "getUserDetails", "the snapshot publishes a UserId");
@@ -797,13 +797,13 @@ function eventTasks(envelope) {
797
797
  return [];
798
798
  }
799
799
  /**
800
- * A task published under `undetermined` terms is a fact to a host and a failure to a conformance
800
+ * A task published under `not-yet-read` terms is a fact to a host and a failure to a conformance
801
801
  * run, as a diagnostic is: the platform under test could not say what it permits, and a green
802
802
  * result must not paper over it.
803
803
  */
804
- function undeterminedTasks(tasks, path) {
805
- return tasks.flatMap((task, index) => isRecord(task) && task.capabilitySource === "undetermined"
806
- ? [{ rule: "capabilitySource.undetermined", path: `${path}[${index}].capabilitySource`,
804
+ function notYetReadTasks(tasks, path) {
805
+ return tasks.flatMap((task, index) => isRecord(task) && task.capabilitySource === "not-yet-read"
806
+ ? [{ rule: "capabilitySource.notYetRead", path: `${path}[${index}].capabilitySource`,
807
807
  message: `task ${String(task.assignmentId)} was published under terms the provider could not determine` }]
808
808
  : []);
809
809
  }
@@ -974,12 +974,12 @@ export function assertDeniedAndRetriedBreak(approvals) {
974
974
  // ---------------------------------------------------------------------------
975
975
  // The task stream. Each event is validated on its own; what one event may say about a task
976
976
  // depends on what was said before, and only something that watched the whole stream can hold a
977
- // provider to it. A task is never its audio: media arrives on the provider's word, ends only
978
- // where it arrived and on work that has begun, and what follows the media ending is the work
977
+ // provider to it. A task is never its audio: audio arrives on the provider's word, ends only
978
+ // where it arrived and on work that has begun, and what follows the audio ending is the work
979
979
  // completing or ending, never a phase the audio decided.
980
980
  // ---------------------------------------------------------------------------
981
981
  const WORK_BEGUN = new Set(["in-progress", "paused", "completing"]);
982
- /** Where audio can arrive: a task at work. A completing task's call is over; a connect-back returns it to in-progress before any media. */
982
+ /** Where audio can arrive: a task at work. A completing task's call is over; a connect-back returns it to in-progress before any audio. */
983
983
  const AT_WORK = new Set(["in-progress", "paused"]);
984
984
  /** What a stream has said about the tasks it carries, and the rules across events. */
985
985
  const TASK_PHASES_ORDERED = ["pending", "confirmed", "preview", "in-progress", "paused", "completing"];
@@ -998,7 +998,7 @@ const REACHABLE_PHASES = {
998
998
  */
999
999
  export const SUPERSEDED_BY_A_SNAPSHOT = new Set([
1000
1000
  "snapshot", "transport-status", "break-state", "task-offered", "task-updated", "task-ended",
1001
- "task-media-started", "task-media-ended", "team-updated", "contacts-updated", "calendar-updated",
1001
+ "task-audio-started", "task-audio-ended", "team-updated", "contacts-updated", "calendar-updated",
1002
1002
  ]);
1003
1003
  export class TaskStream {
1004
1004
  tasks = new Map();
@@ -1074,7 +1074,7 @@ export class TaskStream {
1074
1074
  return [...was].filter(key => !now.has(key));
1075
1075
  }
1076
1076
  static stated(task) {
1077
- const media = isRecord(task) && (task.media === "started" || task.media === "ended") ? task.media : "none";
1077
+ const audio = isRecord(task) && (task.audio === "started" || task.audio === "ended") ? task.audio : "none";
1078
1078
  // The stage of every dialled entry the room names by its dial, so an update can be held to the
1079
1079
  // outcome that moves it.
1080
1080
  const stages = new Map();
@@ -1085,23 +1085,23 @@ export class TaskStream {
1085
1085
  }
1086
1086
  }
1087
1087
  const partyRingingBeforeWork = isRecord(task) && Array.isArray(task.onCall)
1088
- && 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)));
1088
+ && task.onCall.some(entry => isRecord(entry) && entry.role === "party" && entry.stage === "ringing" && (typeof entry.dialId === "string" || (task.phase === "preview" && task.atDeadline === "provider-dials" && entry.dialId === undefined)));
1089
1089
  return { partyRingingBeforeWork, channel: String(isRecord(task) ? task.channel : undefined), completionMode: String(isRecord(task) ? task.completionMode : undefined),
1090
1090
  wrapAllowance: isRecord(task) && typeof task.wrapAllowance === "number" ? task.wrapAllowance : undefined,
1091
- 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) };
1091
+ phase: String(isRecord(task) ? task.phase : undefined), audio, source: String(isRecord(task) ? task.capabilitySource : undefined), stages, record: TaskStream.record(task), assignment: String(isRecord(task) ? task.assignmentId : undefined) };
1092
1092
  }
1093
1093
  /**
1094
1094
  * Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
1095
1095
  * state now, and first holds it to what the stream knew. A snapshot replaces state; it does not
1096
1096
  * get to forget it. A task it carries that the stream already held may not go backwards, lose an
1097
- * entry of its record, return to undetermined terms, or be at work without the audio the stream
1098
- * held up: media ends on task-media-ended and the call moves on, so a task still in-progress or
1099
- * paused without its media is a snapshot that lost state, not a call that ended.
1097
+ * entry of its record, return to not-yet-read terms, or be at work without the audio the stream
1098
+ * held up: audio ends on task-audio-ended and the call moves on, so a task still in-progress or
1099
+ * paused without its audio is a snapshot that lost state, not a call that ended.
1100
1100
  */
1101
1101
  resync(snapshot, at) {
1102
1102
  const found = [];
1103
1103
  const refuse = (rule, where, message) => found.push({ rule, path: where, message });
1104
- ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.history", "stream.snapshot.phase", "stream.snapshot.media");
1104
+ ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.history", "stream.snapshot.phase", "stream.snapshot.audio");
1105
1105
  if (isRecord(snapshot) && Array.isArray(snapshot.tasks)) {
1106
1106
  snapshot.tasks.forEach((task, index) => {
1107
1107
  if (!isRecord(task) || typeof task.assignmentId !== "string")
@@ -1109,8 +1109,8 @@ export class TaskStream {
1109
1109
  const was = this.tasks.get(task.assignmentId);
1110
1110
  if (was === undefined)
1111
1111
  return;
1112
- if ((was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
1113
- 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`);
1112
+ if ((was.source === "queue" || was.source === "nobody") && task.capabilitySource === "not-yet-read") {
1113
+ refuse("stream.snapshot.capabilitySource", `${at}.tasks[${index}].capabilitySource`, `${task.assignmentId} was published under ${was.source} terms and the snapshot says not-yet-read: terms once read stay read`);
1114
1114
  }
1115
1115
  // A record once read is not unread: a resync restates it whole, or with more, never with less.
1116
1116
  const lost = TaskStream.lost(was.record, TaskStream.record(task));
@@ -1125,8 +1125,8 @@ export class TaskStream {
1125
1125
  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`);
1126
1126
  }
1127
1127
  }
1128
- if (was.media === "started" && (to === "in-progress" || to === "paused") && task.media !== "started") {
1129
- 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`);
1128
+ if (was.audio === "started" && (to === "in-progress" || to === "paused") && task.audio !== "started") {
1129
+ refuse("stream.snapshot.audio", `${at}.tasks[${index}].audio`, `${task.assignmentId}'s audio was up and the snapshot carries it ${to} without it: audio ends on task-audio-ended and the call moves on, so a snapshot that forgets the audio lost state`);
1130
1130
  }
1131
1131
  });
1132
1132
  }
@@ -1184,12 +1184,12 @@ export class TaskStream {
1184
1184
  break;
1185
1185
  }
1186
1186
  // The rules about a known task are evaluated only once there is one.
1187
- 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");
1187
+ ruleEvaluated("stream.taskUpdated.capabilitySource", "stream.taskUpdated.phase", "stream.taskUpdated.audioOpen", "stream.taskUpdated.history", "stream.taskAudioEnded.follow", "stream.taskUpdated.audio", "stream.taskUpdated.stage", "stream.taskUpdated.stage.lingering");
1188
1188
  // Terms once read stay read. A re-read that fails is not a new fact about the task, so the
1189
- // last statement stands and the failure is a diagnostic; undetermined is a place a task
1189
+ // last statement stands and the failure is a diagnostic; not-yet-read is a place a task
1190
1190
  // starts from, never one it returns to.
1191
- if ((known.source === "queue" || known.source === "ungoverned") && isRecord(event.task) && event.task.capabilitySource === "undetermined") {
1192
- 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`);
1191
+ if ((known.source === "queue" || known.source === "nobody") && isRecord(event.task) && event.task.capabilitySource === "not-yet-read") {
1192
+ refuse("stream.taskUpdated.capabilitySource", `${at}.task.capabilitySource`, `${id} was published under ${known.source} terms and now says not-yet-read: terms once read stay read, and a re-read that fails is a diagnostic, not a republish`);
1193
1193
  }
1194
1194
  // A task does not go backwards. The stream sees publications, not transitions, and a task may
1195
1195
  // pass through a phase between two, so what is refused is a phase unreachable from the last
@@ -1214,29 +1214,29 @@ export class TaskStream {
1214
1214
  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`);
1215
1215
  }
1216
1216
  }
1217
- // A task completes after its media ends, never around it: an update that moves a task to
1217
+ // A task completes after its audio ends, never around it: an update that moves a task to
1218
1218
  // completing while the stream holds its audio as started is a call whose audio never ended,
1219
1219
  // whoever caused the ending -- the drive's end-call, a transfer, the provider's own hand.
1220
1220
  {
1221
1221
  const to = isRecord(event.task) ? String(event.task.phase) : "";
1222
- if (to === "completing" && known.media === "started" && known.phase !== "completing") {
1223
- refuse("stream.taskUpdated.mediaOpen", `${at}.task.phase`, `${id} moves to completing with its media still started: the audio ends first, on task-media-ended, and a wrap-up with the call still up is audio that never ended`);
1222
+ if (to === "completing" && known.audio === "started" && known.phase !== "completing") {
1223
+ refuse("stream.taskUpdated.audioOpen", `${at}.task.phase`, `${id} moves to completing with its audio still started: the audio ends first, on task-audio-ended, and a wrap-up with the call still up is audio that never ended`);
1224
1224
  }
1225
1225
  }
1226
1226
  // The connect-back that brings a completing task back is the one exception here too.
1227
- if (known.media === "ended") {
1227
+ if (known.audio === "ended") {
1228
1228
  const phase = isRecord(event.task) ? String(event.task.phase) : "";
1229
1229
  if (phase !== "completing" && !(known.phase === "completing" && this.partyDialled(event.task))) {
1230
- refuse("stream.taskMediaEnded.follow", `${at}.task.phase`, `after its media ended, ${id} completes or ends; ${phase} is a phase the audio does not decide`);
1230
+ refuse("stream.taskAudioEnded.follow", `${at}.task.phase`, `after its audio ended, ${id} completes or ends; ${phase} is a phase the audio does not decide`);
1231
1231
  }
1232
1232
  }
1233
- // A task replaces the task, and an update re-states media without moving it: the
1234
- // transitions belong to task-media-started and task-media-ended. Releasing ended is the
1233
+ // A task replaces the task, and an update re-states audio without moving it: the
1234
+ // transitions belong to task-audio-started and task-audio-ended. Releasing ended is the
1235
1235
  // one move an update may make, since wrapped audio has nothing left to end.
1236
1236
  {
1237
1237
  const next = TaskStream.stated(event.task);
1238
- if (known.media !== next.media && !(known.media === "ended" && next.media === "none")) {
1239
- refuse("stream.taskUpdated.media", `${at}.task.media`, `a task-updated re-states media, it does not move it: ${id} held ${known.media} and the update says ${next.media}; audio arrives on task-media-started and ends on task-media-ended`);
1238
+ if (known.audio !== next.audio && !(known.audio === "ended" && next.audio === "none")) {
1239
+ refuse("stream.taskUpdated.audio", `${at}.task.audio`, `a task-updated re-states audio, it does not move it: ${id} held ${known.audio} and the update says ${next.audio}; audio arrives on task-audio-started and ends on task-audio-ended`);
1240
1240
  }
1241
1241
  // The same pairing for a dialled entry: the dial-outcome is the transition, the stage is
1242
1242
  // the state, and the outcome comes first. An update that joins an entry on its own is
@@ -1255,30 +1255,30 @@ export class TaskStream {
1255
1255
  }
1256
1256
  this.noteDials(event.task);
1257
1257
  break;
1258
- case "task-media-started":
1259
- ruleEvaluated("stream.taskMediaStarted.beforeWork", "stream.taskMediaStarted.duplicate", "stream.taskMedia.channel", "stream.assignment.ended", "stream.assignment.unknown");
1258
+ case "task-audio-started":
1259
+ ruleEvaluated("stream.taskAudioStarted.beforeWork", "stream.taskAudioStarted.duplicate", "stream.taskAudio.channel", "stream.assignment.ended", "stream.assignment.unknown");
1260
1260
  if (id === undefined)
1261
1261
  break;
1262
1262
  if (known === undefined) {
1263
1263
  this.notOpen(event, id, at, refuse);
1264
1264
  break;
1265
1265
  }
1266
- // Ring-back is audio: a task whose party the host is dialling has media from dialling on,
1266
+ // Ring-back is audio: a task whose party the host is dialling has audio from dialling on,
1267
1267
  // whatever its phase; every other task not at work has none.
1268
1268
  if (known.channel !== "voice") {
1269
- refuse("stream.taskMedia.channel", `${at}.type`, "only a voice task has media transitions");
1269
+ refuse("stream.taskAudio.channel", `${at}.type`, "only a voice task has audio transitions");
1270
1270
  break;
1271
1271
  }
1272
1272
  if (!AT_WORK.has(known.phase) && !known.partyRingingBeforeWork) {
1273
- 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`);
1273
+ refuse("stream.taskAudioStarted.beforeWork", `${at}.assignmentId`, `audio cannot arrive on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
1274
1274
  }
1275
- if (known.media === "started") {
1276
- refuse("stream.taskMediaStarted.duplicate", `${at}.assignmentId`, `media already started on ${id}; started and ended alternate`);
1275
+ if (known.audio === "started") {
1276
+ refuse("stream.taskAudioStarted.duplicate", `${at}.assignmentId`, `audio already started on ${id}; started and ended alternate`);
1277
1277
  }
1278
- known.media = "started";
1278
+ known.audio = "started";
1279
1279
  break;
1280
- case "task-media-ended":
1281
- ruleEvaluated("stream.taskMediaEnded.beforeWork", "stream.taskMediaEnded.silent", "stream.taskMedia.channel", "stream.assignment.ended", "stream.assignment.unknown");
1280
+ case "task-audio-ended":
1281
+ ruleEvaluated("stream.taskAudioEnded.beforeWork", "stream.taskAudioEnded.silent", "stream.taskAudio.channel", "stream.assignment.ended", "stream.assignment.unknown");
1282
1282
  if (id === undefined)
1283
1283
  break;
1284
1284
  if (known === undefined) {
@@ -1286,16 +1286,16 @@ export class TaskStream {
1286
1286
  break;
1287
1287
  }
1288
1288
  if (known.channel !== "voice") {
1289
- refuse("stream.taskMedia.channel", `${at}.type`, "only a voice task has media transitions");
1289
+ refuse("stream.taskAudio.channel", `${at}.type`, "only a voice task has audio transitions");
1290
1290
  break;
1291
1291
  }
1292
1292
  if (!WORK_BEGUN.has(known.phase) && !known.partyRingingBeforeWork) {
1293
- 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`);
1293
+ refuse("stream.taskAudioEnded.beforeWork", `${at}.assignmentId`, `audio cannot end on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
1294
1294
  }
1295
- if (known.media !== "started") {
1296
- 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`);
1295
+ if (known.audio !== "started") {
1296
+ refuse("stream.taskAudioEnded.silent", `${at}.assignmentId`, `audio cannot end on ${id} where none arrived: audio attaches on task-audio-started, or on a task carried with audio started`);
1297
1297
  }
1298
- known.media = "ended";
1298
+ known.audio = "ended";
1299
1299
  break;
1300
1300
  case "task-ended":
1301
1301
  ruleEvaluated("stream.assignment.ended", "stream.assignment.unknown");
@@ -1307,12 +1307,12 @@ export class TaskStream {
1307
1307
  }
1308
1308
  // A voice task ends after its audio ends, never around it, whatever the outcome: a take-over
1309
1309
  // and a lead leaving included. An ending with the audio still up leaves the host holding an
1310
- // open media session and an open leg on a task that no longer exists.
1311
- ruleEvaluated("stream.taskEnded.mediaOpen");
1312
- if (known.media === "started") {
1313
- refuse("stream.taskEnded.mediaOpen", `${at}.assignmentId`, `${id} ended with its media still started: the audio ends first, on task-media-ended, whatever the outcome`);
1310
+ // open audio session and an open leg on a task that no longer exists.
1311
+ ruleEvaluated("stream.taskEnded.audioOpen");
1312
+ if (known.audio === "started") {
1313
+ refuse("stream.taskEnded.audioOpen", `${at}.assignmentId`, `${id} ended with its audio still started: the audio ends first, on task-audio-ended, whatever the outcome`);
1314
1314
  }
1315
- // Off voice there is no media event, so the update that moves a task to completing is the
1315
+ // Off voice there is no audio event, so the update that moves a task to completing is the
1316
1316
  // provider's word that interaction ended and the moment the wrap allowance starts. A provider
1317
1317
  // that completes the task itself with an allowance to run has to have started the clock:
1318
1318
  // completed from in-progress, the allowance it stated was never given.
@@ -1385,7 +1385,7 @@ async function driveOneCall(drive) {
1385
1385
  };
1386
1386
  drive.waiters.add(waiter);
1387
1387
  });
1388
- // The call's media session, opened on the first client and opened again on a reloaded second.
1388
+ // The call's audio session, opened on the first client and opened again on a reloaded second.
1389
1389
  let session;
1390
1390
  const recordSurvivesReload = async (at) => {
1391
1391
  // Each reload rule is marked where it is decided, never up front: a rebuild that throws has looked at nothing else.
@@ -1463,28 +1463,28 @@ async function driveOneCall(drive) {
1463
1463
  else if (leg.mutedBy !== "host") {
1464
1464
  refuse("drive.reload.history", "drive.reload.history", `the reloaded record's muted leg at ${at} says mutedBy ${String(leg.mutedBy)}; the host reported host`);
1465
1465
  }
1466
- // The drive goes on with the task as the reloaded client holds it. The first client's media
1467
- // session died with it: a host opens media on a task arriving with media started on a
1466
+ // The drive goes on with the task as the reloaded client holds it. The first client's audio
1467
+ // session died with it: a host opens audio on a task arriving with audio started on a
1468
1468
  // snapshot, so the drive opens it again on the second and holds the answer as it held the first.
1469
1469
  task = carried;
1470
- if (drive.softphone && carried.media === "started") {
1470
+ if (drive.softphone && carried.audio === "started") {
1471
1471
  if (session !== undefined && typeof session.close === "function") {
1472
1472
  try {
1473
1473
  session.close();
1474
1474
  }
1475
1475
  catch {
1476
- refuse("drive.openMedia.close", "drive.openMedia", "the first client's media session threw on close");
1476
+ refuse("drive.openAudio.close", "drive.openAudio", "the first client's audio session threw on close");
1477
1477
  }
1478
1478
  }
1479
1479
  session = undefined;
1480
- ruleEvaluated("drive.reload.openMedia");
1481
- const reopened = await second.openMedia?.({ assignmentId: taskId, localAudio: drive.localAudio });
1482
- const malformed = validateResult(reopened, "openMedia", "drive.reload.openMedia");
1480
+ ruleEvaluated("drive.reload.openAudio");
1481
+ const reopened = await second.openAudio?.({ assignmentId: taskId, localAudio: drive.localAudio });
1482
+ const malformed = validateResult(reopened, "openAudio", "drive.reload.openAudio");
1483
1483
  found.push(...malformed);
1484
1484
  if (malformed.length === 0 && isRecord(reopened) && reopened.status === "opened")
1485
- session = reopened.session;
1485
+ session = reopened.audio;
1486
1486
  else if (malformed.length === 0)
1487
- refuse("drive.reload.openMedia", "drive.reload.openMedia", `a second adapter built from the same login could not open the audio of ${taskId}, which its own snapshot carries with media started`);
1487
+ refuse("drive.reload.openAudio", "drive.reload.openAudio", `a second adapter built from the same login could not open the audio of ${taskId}, which its own snapshot carries with audio started`);
1488
1488
  }
1489
1489
  }
1490
1490
  catch (error) {
@@ -1581,9 +1581,9 @@ async function driveOneCall(drive) {
1581
1581
  const declared = capabilities[name];
1582
1582
  return declared !== undefined && !(isRecord(declared) && declared.lockedBy !== undefined);
1583
1583
  };
1584
- // Media may arrive any time after the accept, before or after the task's own update says
1584
+ // Audio may arrive any time after the accept, before or after the task's own update says
1585
1585
  // in-progress: the event is the provider's word that the audio should attach, never a reply to
1586
- // openMedia, so the drive looks for it from the accept rather than from the last update it read.
1586
+ // openAudio, so the drive looks for it from the accept rather than from the last update it read.
1587
1587
  const acceptedAt = drive.events.length;
1588
1588
  // 1. Accept the offer, if it is one.
1589
1589
  if (latestTask().phase === "pending") {
@@ -1596,7 +1596,7 @@ async function driveOneCall(drive) {
1596
1596
  if (latestTask().phase === "preview") {
1597
1597
  const dialId = `drive-${taskId}`;
1598
1598
  drive.stream.dialled(dialId);
1599
- if (await send({ type: "call", dialId }, dialId) === undefined)
1599
+ if (await send({ type: "dial", dialId }, dialId) === undefined)
1600
1600
  return found;
1601
1601
  if (await updated(t => t.phase === "in-progress" || t.phase === "completing", "the task leaving preview after Call") === undefined)
1602
1602
  return found;
@@ -1626,22 +1626,22 @@ async function driveOneCall(drive) {
1626
1626
  }
1627
1627
  // 3. On a softphone, the audio arrives and the host opens it.
1628
1628
  if (drive.softphone && latestTask().phase === "in-progress") {
1629
- if (latestTask().media !== "started") {
1630
- const started = await waitFor("task-media-started for the driven task", envelope => {
1629
+ if (latestTask().audio !== "started") {
1630
+ const started = await waitFor("task-audio-started for the driven task", envelope => {
1631
1631
  const event = envelope.event;
1632
- return event.type === "task-media-started" && event.assignmentId === taskId ? event : undefined;
1632
+ return event.type === "task-audio-started" && event.assignmentId === taskId ? event : undefined;
1633
1633
  }, acceptedAt);
1634
1634
  if (started === undefined)
1635
1635
  return found;
1636
1636
  cursor = Math.max(cursor, started.at);
1637
1637
  }
1638
- const opened = await drive.connection.openMedia?.({ assignmentId: taskId, localAudio: drive.localAudio });
1639
- const malformed = validateResult(opened, "openMedia", "drive.openMedia");
1638
+ const opened = await drive.connection.openAudio?.({ assignmentId: taskId, localAudio: drive.localAudio });
1639
+ const malformed = validateResult(opened, "openAudio", "drive.openAudio");
1640
1640
  found.push(...malformed);
1641
1641
  if (malformed.length === 0 && isRecord(opened) && opened.status === "opened")
1642
- session = opened.session;
1642
+ session = opened.audio;
1643
1643
  else if (malformed.length === 0 && isRecord(opened))
1644
- refuse("drive.openMedia.unavailable", "drive.openMedia", "a softphone login's adapter could not open the call's audio");
1644
+ refuse("drive.openAudio.unavailable", "drive.openAudio", "a softphone login's adapter could not open the call's audio");
1645
1645
  }
1646
1646
  // 3b. The microphone is the host's. With the audio open, the drive mutes it for a moment and
1647
1647
  // reports the leg the provider's record would otherwise miss -- begun, then ended -- and
@@ -1693,7 +1693,7 @@ async function driveOneCall(drive) {
1693
1693
  session.setMuted(muted);
1694
1694
  }
1695
1695
  catch {
1696
- refuse("drive.openMedia.setMuted", "drive.openMedia", `the media session threw on setMuted(${String(muted)})`);
1696
+ refuse("drive.openAudio.setMuted", "drive.openAudio", `the audio session threw on setMuted(${String(muted)})`);
1697
1697
  }
1698
1698
  };
1699
1699
  if (canRecordMute) {
@@ -1727,10 +1727,10 @@ async function driveOneCall(drive) {
1727
1727
  }
1728
1728
  }
1729
1729
  // 5. End the call, where the agent may -- with the microphone muted, as agents do. The leg left
1730
- // open at media end is the provider's to close, since the provider knows the instant the media
1730
+ // open at audio end is the provider's to close, since the provider knows the instant the audio
1731
1731
  // ended; the host's own closing report can only follow what it hears, is answered recorded, and
1732
1732
  // changes nothing. So the drive mutes, does not report the end before end-call, and reports it
1733
- // after the media has ended.
1733
+ // after the audio has ended.
1734
1734
  if (drive.channel === "voice" && latestTask().phase === "in-progress" && offers("endCall")) {
1735
1735
  let openLeg;
1736
1736
  if (canRecordMute && session !== undefined) {
@@ -1741,22 +1741,22 @@ async function driveOneCall(drive) {
1741
1741
  }
1742
1742
  if (await send({ type: "end-call" }) !== undefined) {
1743
1743
  // A task that completes with its audio still up is refused by the stream as it passes
1744
- // (stream.taskUpdated.mediaOpen); the drive does not also wait out the clock for an ending
1744
+ // (stream.taskUpdated.audioOpen); the drive does not also wait out the clock for an ending
1745
1745
  // that is not coming, so the run reads by the rule and not by its timeout.
1746
- const mediaEnded = await waitFor("task-media-ended after end-call", envelope => {
1746
+ const audioEnded = await waitFor("task-audio-ended after end-call", envelope => {
1747
1747
  const event = envelope.event;
1748
- if (event.type === "task-media-ended" && event.assignmentId === taskId)
1748
+ if (event.type === "task-audio-ended" && event.assignmentId === taskId)
1749
1749
  return "ended";
1750
1750
  if (event.type === "task-updated" && isTask(event.task) && event.task.assignmentId === taskId && event.task.phase === "completing")
1751
1751
  return "completing";
1752
1752
  return undefined;
1753
1753
  }, cursor);
1754
- if (mediaEnded?.found === "ended")
1755
- cursor = mediaEnded.at;
1756
- const completing = await updated(t => t.phase === "completing", "the task completing after its media ended");
1754
+ if (audioEnded?.found === "ended")
1755
+ cursor = audioEnded.at;
1756
+ const completing = await updated(t => t.phase === "completing", "the task completing after its audio ended");
1757
1757
  if (openLeg !== undefined) {
1758
1758
  setMuted(false);
1759
- // The provider closed this leg at media end, in the completing publication; the host's report,
1759
+ // The provider closed this leg at audio end, in the completing publication; the host's report,
1760
1760
  // stated at the record's grain (a leg that rounds to nought is 1), changes nothing, and the
1761
1761
  // record as it stands now is what every later restatement is held to.
1762
1762
  const before = closedAs(openLeg.at);
@@ -1773,11 +1773,11 @@ async function driveOneCall(drive) {
1773
1773
  session.close();
1774
1774
  }
1775
1775
  catch {
1776
- refuse("drive.openMedia.close", "drive.openMedia", "the media session threw on close");
1776
+ refuse("drive.openAudio.close", "drive.openAudio", "the audio session threw on close");
1777
1777
  }
1778
1778
  }
1779
1779
  // 6. Complete, where the agent completes; otherwise the provider does, and the drive waits for it.
1780
- // A conversation has no media to end and no completing phase to wait for: a chat or an email,
1780
+ // A conversation has no audio to end and no completing phase to wait for: a chat or an email,
1781
1781
  // and a voice task offering no end-call, is completed from where it stands.
1782
1782
  const completable = latestTask().phase === "completing" || latestTask().phase === "in-progress" || latestTask().phase === "paused";
1783
1783
  if (latestTask().completionMode === "agent-command" && completable) {
@@ -1844,7 +1844,7 @@ async function driveOneCall(drive) {
1844
1844
  else if (reported.closedAs !== undefined) {
1845
1845
  ruleEvaluated("drive.recordStep.overwritten");
1846
1846
  if (leg.seconds !== reported.closedAs) {
1847
- refuse("drive.recordStep.overwritten", "drive.recordStep", `the provider closed the leg at ${at} with ${reported.closedAs}s at media end and the record now says ${String(leg.seconds)}s: the host's closing report changes nothing`);
1847
+ refuse("drive.recordStep.overwritten", "drive.recordStep", `the provider closed the leg at ${at} with ${reported.closedAs}s at audio end and the record now says ${String(leg.seconds)}s: the host's closing report changes nothing`);
1848
1848
  }
1849
1849
  }
1850
1850
  }
@@ -1913,18 +1913,18 @@ export function assertBreakFollowsItsRequests(envelopes, snapshot) {
1913
1913
  assertNoViolations(found, "A break follows its requests");
1914
1914
  }
1915
1915
  /**
1916
- * The media follows the task and never decides it. Given a provider's stream -- optionally seeded
1917
- * with the snapshot it began from -- every task is introduced once, `task-media-started` and
1918
- * `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what
1919
- * follows the media ending is `completing` or `task-ended`.
1916
+ * The audio follows the task and never decides it. Given a provider's stream -- optionally seeded
1917
+ * with the snapshot it began from -- every task is introduced once, `task-audio-started` and
1918
+ * `task-audio-ended` alternate on work that has begun, audio ends only where it arrived, and what
1919
+ * follows the audio ending is `completing` or `task-ended`.
1920
1920
  */
1921
- export function assertMediaFollowsTheTask(envelopes, snapshot) {
1921
+ export function assertAudioFollowsTheTask(envelopes, snapshot) {
1922
1922
  const stream = new TaskStream();
1923
1923
  if (snapshot !== undefined)
1924
1924
  stream.seed(snapshot);
1925
1925
  const found = [];
1926
1926
  envelopes.forEach((envelope, index) => found.push(...stream.apply(envelope, `envelopes[${index}]`)));
1927
- assertNoViolations(found, "The media follows the task");
1927
+ assertNoViolations(found, "The audio follows the task");
1928
1928
  }
1929
1929
  /** The store as handed to the adapter, and the keys it holds at any moment, seen rather than reported. */
1930
1930
  function watchStore(store) {
@@ -2005,14 +2005,14 @@ export function assertBreakBeginsAfterTask(steps) {
2005
2005
  }
2006
2006
  }
2007
2007
  /**
2008
- * Validates the deadline derived from media end and the task's fixed wrap allowance.
2008
+ * Validates the deadline derived from audio end and the task's fixed wrap allowance.
2009
2009
  *
2010
2010
  * A task with no allowance has no deadline, so `observedDeadline` must then be `undefined`: a
2011
2011
  * host counting down what the provider left open is the violation, and so is a host counting
2012
2012
  * nothing down when the provider set a clock. `toleranceMs` absorbs scheduler jitter in a real
2013
2013
  * implementation; pass 0 to demand an exact match.
2014
2014
  */
2015
- export function assertWrapTimeout(task, mediaEndedAt, observedDeadline, toleranceMs = 1_000) {
2015
+ export function assertWrapTimeout(task, audioEndedAt, observedDeadline, toleranceMs = 1_000) {
2016
2016
  if (task.wrapAllowance === undefined) {
2017
2017
  if (observedDeadline !== undefined) {
2018
2018
  throw new Error(`Wrap deadline mismatch: the task states no allowance, so there is no deadline, received ${observedDeadline}`);
@@ -2022,7 +2022,7 @@ export function assertWrapTimeout(task, mediaEndedAt, observedDeadline, toleranc
2022
2022
  if (observedDeadline === undefined) {
2023
2023
  throw new Error(`Wrap deadline mismatch: the task allows ${task.wrapAllowance}s, but no deadline was observed`);
2024
2024
  }
2025
- const ended = Date.parse(mediaEndedAt);
2025
+ const ended = Date.parse(audioEndedAt);
2026
2026
  const deadline = Date.parse(observedDeadline);
2027
2027
  if (Number.isNaN(ended) || Number.isNaN(deadline))
2028
2028
  throw new Error("Wrap scenario requires valid ISO-8601 times");
@@ -149,7 +149,7 @@ export declare function validateTaskCommandRequest(request: unknown, task: unkno
149
149
  */
150
150
  export declare function validateTaskCommand(command: unknown, task?: unknown, path?: string, taskContext?: Omit<TaskValidationContext, "channel">): ProtocolViolation[];
151
151
  /** The connection methods whose results `validateResult` knows. */
152
- export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeam" | "openMedia" | "setPreference" | "recordStep";
152
+ export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeam" | "openAudio" | "setPreference" | "recordStep";
153
153
  /**
154
154
  * Validates what a connection method answered. A result is untrusted for the same reason a
155
155
  * snapshot is: it comes from an adapter that may be compiled against another version, and Omni