@xema/omni-protocol 0.1.99 → 0.1.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  // Each list is pinned to its type both ways -- a member the type lacks, or a member the list
10
10
  // lacks, fails to compile -- so what the validators accept cannot drift from what the
11
11
  // declarations say.
12
- import { RECORDING_ACTIONS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, 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";
12
+ import { RECORDING_ACTIONS, SHIFT_EVENT_KINDS, ALLOWED_BROWSER_URL_SCHEMES, 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") {
@@ -41,7 +41,7 @@ const TASK_PHASES = membersOf({
41
41
  pending: true, confirmed: true, preview: true, "in-progress": true, paused: true, completing: true,
42
42
  });
43
43
  const COMPLETION_MODES = membersOf({ "agent-command": true, "provider-automatic": true });
44
- const CAPABILITY_SOURCES = membersOf({ queue: true, ungoverned: true, undetermined: true });
44
+ const CAPABILITY_SOURCES = membersOf({ queue: true, nobody: true, "not-yet-read": true });
45
45
  const ACCEPTANCE_MODES = membersOf({
46
46
  "no-preference": true, "consent": true, "automatic": true,
47
47
  });
@@ -55,7 +55,7 @@ const BREAK_APPROVALS = membersOf({
55
55
  "not-requested": true, "awaiting-approval": true, granted: true, "starting-after-task": true, "on-break": true,
56
56
  });
57
57
  const TEAM_AVAILABILITIES = membersOf({
58
- ready: true, "on-task": true, "on-break": true, reserved: true, "signed-out": true,
58
+ ready: true, "on-task": true, "on-break": true, elsewhere: true, "signed-out": true,
59
59
  });
60
60
  const HISTORY_STEPS = membersOf({
61
61
  queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
@@ -85,7 +85,7 @@ const MEMBER_BREAKS = membersOf({
85
85
  const OFFERABLE_PHASES = membersOf({
86
86
  pending: true,
87
87
  });
88
- const PREVIEW_DEADLINES = membersOf({ calls: true, "host-calls": true, waits: true });
88
+ const PREVIEW_DEADLINES = membersOf({ "provider-dials": true, "host-dials": true, waits: true });
89
89
  const PHONES = membersOf({ softphone: true, deskPhone: true });
90
90
  const LISTENING_MODES = membersOf({ listen: true, coach: true, "join-call": true });
91
91
  const COMPLETED_BY = membersOf({ agent: true, provider: true });
@@ -923,13 +923,13 @@ function validateHistory(value, path, into, task = {}) {
923
923
  }
924
924
  else {
925
925
  // An open leg is one still running, and the task says whether it can be: a hold runs only while
926
- // the task is paused, and a mute only while its media is up. An open entry after that is a leg
926
+ // the task is paused, and a mute only while its audio is up. An open entry after that is a leg
927
927
  // nobody closed, which reads exactly like a leg running now.
928
928
  if (entry.step === "held") {
929
929
  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`);
930
930
  }
931
931
  if (entry.step === "muted") {
932
- 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");
932
+ into.require(task.audio !== "ended" && task.phase !== "completing", "task.history.muted.open", `${at}.seconds`, "a muted entry without seconds is still running, but this interaction's audio has ended: the provider closes its open leg and states the duration; other channels may continue");
933
933
  }
934
934
  }
935
935
  // A muted entry carries whose the silence was, as the host reported it; no other step has it.
@@ -960,22 +960,22 @@ function validateHistory(value, path, into, task = {}) {
960
960
  }
961
961
  /** Present only while consulting, and only on voice: elsewhere there is nobody to consult. */
962
962
  const TASK_MEDIA_STATES = membersOf({ started: true, ended: true });
963
- /** Real-time media is a voice affair, and its state is one of two words. */
963
+ /** Real-time audio is a voice affair, and its state is one of two words. */
964
964
  /** A host dial, or explicitly provider-triggered preview dial, is ringing the party. */
965
965
  function partyRingingBeforeWork(task) {
966
- return Array.isArray(task.onCall) && task.onCall.some(entry => isPlainObject(entry) && entry.role === "party" && entry.stage === "ringing" && (isFilled(entry.dialId) || (task.phase === "preview" && task.atDeadline === "calls" && entry.dialId === undefined)));
966
+ return Array.isArray(task.onCall) && task.onCall.some(entry => isPlainObject(entry) && entry.role === "party" && entry.stage === "ringing" && (isFilled(entry.dialId) || (task.phase === "preview" && task.atDeadline === "provider-dials" && entry.dialId === undefined)));
967
967
  }
968
- function validateTaskMedia(task, value, channel, phase, path, into) {
968
+ function validateTaskAudio(task, value, channel, phase, path, into) {
969
969
  if (value === undefined)
970
970
  return;
971
- if (!into.require(channel === "voice", "task.media.channel", path, `a ${channel} task carries no real-time media state`))
971
+ if (!into.require(channel === "voice", "task.audio.channel", path, `a ${channel} task carries no real-time audio state`))
972
972
  return;
973
- if (into.oneOf(value, TASK_MEDIA_STATES, "task.media", path)) {
974
- // Nothing is acquired while pending, and a preview has placed no call: media names a task whose
973
+ if (into.oneOf(value, TASK_MEDIA_STATES, "task.audio", path)) {
974
+ // Nothing is acquired while pending, and a preview has placed no call: audio names a task whose
975
975
  // work has begun, on a snapshot as on the event, or a host would open the microphone on an offer.
976
976
  // Ring-back may precede answer for a host dial or an explicitly provider-triggered preview.
977
- // A deadline alone is never evidence that media started.
978
- into.require(!WORK_NOT_BEGUN.includes(phase) || partyRingingBeforeWork(task), "task.media.beforeWork", path, `a ${describeValue(phase)} task has no media: audio arrives once its work has begun, or once an evidenced host/provider preview dial is ringing its party`);
977
+ // A deadline alone is never evidence that audio started.
978
+ into.require(!WORK_NOT_BEGUN.includes(phase) || partyRingingBeforeWork(task), "task.audio.beforeWork", path, `a ${describeValue(phase)} task has no audio: audio arrives once its work has begun, or once an evidenced host/provider preview dial is ringing its party`);
979
979
  }
980
980
  }
981
981
  const WORK_NOT_BEGUN = ["pending", "confirmed", "preview"];
@@ -1131,7 +1131,7 @@ function validateTaskInto(task, context, path, into) {
1131
1131
  into.timestamp(task.previewEndsAt, "task.preview.previewEndsAt", `${path}.previewEndsAt`);
1132
1132
  }
1133
1133
  if (task.atDeadline === undefined) {
1134
- into.add("task.preview.atDeadline.required", `${path}.atDeadline`, "a preview with a deadline says what happens at it: provider calls, host calls, or waits for the agent -- an agent counting down has to know which");
1134
+ into.add("task.preview.atDeadline.required", `${path}.atDeadline`, "a preview with a deadline says what happens at it: provider dials, host dials, or waits for the agent -- an agent counting down has to know which");
1135
1135
  }
1136
1136
  else {
1137
1137
  into.oneOf(task.atDeadline, PREVIEW_DEADLINES, "task.preview.atDeadline", `${path}.atDeadline`);
@@ -1200,18 +1200,18 @@ function validateTaskInto(task, context, path, into) {
1200
1200
  if (context.member !== true || task.browsers !== undefined)
1201
1201
  validateBrowsers(task.browsers, `${path}.browsers`, into);
1202
1202
  validateTaskAttributes(task.attributes, `${path}.attributes`, into, context.levels);
1203
- validateHistory(task.history, `${path}.history`, into, { phase: task.phase, media: task.media });
1203
+ validateHistory(task.history, `${path}.history`, into, { phase: task.phase, audio: task.audio });
1204
1204
  validateOnCall(task.onCall, context.channel, `${path}.onCall`, into);
1205
- // Media and onCall describe this agent's interaction, not the continuing caller journey.
1206
- // Once the interaction's media ends or it enters wrap, its live room is cleared. Other
1205
+ // Audio and onCall describe this agent's interaction, not the continuing caller journey.
1206
+ // Once the interaction's audio ends or it enters wrap, its live room is cleared. Other
1207
1207
  // channels may remain connected under another interaction or in IVR/queue stages.
1208
- if (task.phase === "completing" && task.media === "started") {
1209
- into.add("task.media.completing", `${path}.media`, "a completing task's interaction media has ended or never started; it cannot still be started during wrap");
1208
+ if (task.phase === "completing" && task.audio === "started") {
1209
+ into.add("task.audio.completing", `${path}.audio`, "a completing task's interaction audio has ended or never started; it cannot still be started during wrap");
1210
1210
  }
1211
- if (Array.isArray(task.onCall) && task.onCall.length > 0 && (task.phase === "completing" || task.media === "ended")) {
1212
- into.add("task.onCall.ended", `${path}.onCall`, `this interaction is no longer connected (${task.phase === "completing" ? "the task is completing" : "its media ended"}); clear its onCall view without implying that other channels ended`);
1211
+ if (Array.isArray(task.onCall) && task.onCall.length > 0 && (task.phase === "completing" || task.audio === "ended")) {
1212
+ into.add("task.onCall.ended", `${path}.onCall`, `this interaction is no longer connected (${task.phase === "completing" ? "the task is completing" : "its audio ended"}); clear its onCall view without implying that other channels ended`);
1213
1213
  }
1214
- validateTaskMedia(task, task.media, context.channel, task.phase, `${path}.media`, into);
1214
+ validateTaskAudio(task, task.audio, context.channel, task.phase, `${path}.audio`, into);
1215
1215
  validateLeadAssist(task.leadAssist, context.channel, `${path}.leadAssist`, into);
1216
1216
  validateTakenOver(task.takenOver, context.channel, `${path}.takenOver`, into);
1217
1217
  // Where the terms came from is stated with them: a host cannot tell "the platform permits
@@ -1671,10 +1671,17 @@ function validateTeamMembersInto(teamMembers, path, context, into) {
1671
1671
  into.add("team.member.listening.shape", `${at}.listening`, "listening must be an object when present");
1672
1672
  }
1673
1673
  else {
1674
+ const listened = member.listening;
1675
+ if (into.require(isAssignmentId(member.listening.assignmentId), "team.member.listening.assignmentId", `${at}.listening.assignmentId`, "listening names which of the member's calls the lead is on: the application opens the lead's audio on it") && Array.isArray(member.tasks)) {
1676
+ into.require(member.tasks.some((task) => isPlainObject(task) && task.assignmentId === listened.assignmentId), "team.member.listening.assignment", `${at}.listening.assignmentId`, "the call the lead is on is one the member's tasks carry");
1677
+ }
1674
1678
  into.oneOf(member.listening.mode, LISTENING_MODES, "team.member.listening.mode", `${at}.listening.mode`);
1675
1679
  into.timestamp(member.listening.since, "team.member.listening.since", `${at}.listening.since`);
1676
1680
  }
1677
1681
  }
1682
+ // The member's own history for the day: about the person, not any one call.
1683
+ if (member.shift !== undefined)
1684
+ validateShiftInto(member.shift, `${at}.shift`, into);
1678
1685
  });
1679
1686
  // One member's call at a time.
1680
1687
  const listened = teamMembers.members.filter((member) => isPlainObject(member) && member.listening !== undefined);
@@ -1724,8 +1731,8 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
1724
1731
  if (snapshot.contacts === undefined && idle.contacts === true) {
1725
1732
  into.add("snapshot.contacts.required", `${path}.contacts`, "the manifest declares contacts, so every snapshot carries the contribution: [] when there are none");
1726
1733
  }
1727
- if (snapshot.scheduledActivities === undefined && idle.calendar === true) {
1728
- into.add("snapshot.calendar.required", `${path}.scheduledActivities`, "the manifest declares calendar, so every snapshot carries the contribution: [] when there are none");
1734
+ if (snapshot.calendar === undefined && idle.calendar === true) {
1735
+ into.add("snapshot.calendar.required", `${path}.calendar`, "the manifest declares calendar, so every snapshot carries the contribution: [] when there are none");
1729
1736
  }
1730
1737
  if (snapshot.contacts !== undefined) {
1731
1738
  into.require(idle.contacts === true, "snapshot.contacts.capability", `${path}.contacts`, "contacts require the contacts idle capability");
@@ -1736,22 +1743,22 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
1736
1743
  into.add("snapshot.contacts.shape", `${path}.contacts`, "contacts must be an array when present");
1737
1744
  }
1738
1745
  }
1739
- if (snapshot.scheduledActivities !== undefined) {
1740
- into.require(idle.calendar === true, "snapshot.calendar.capability", `${path}.scheduledActivities`, "scheduled activities require the calendar idle capability");
1741
- if (Array.isArray(snapshot.scheduledActivities)) {
1746
+ if (snapshot.calendar !== undefined) {
1747
+ into.require(idle.calendar === true, "snapshot.calendar.capability", `${path}.calendar`, "scheduled activities require the calendar idle capability");
1748
+ if (Array.isArray(snapshot.calendar)) {
1742
1749
  const seen = new Set();
1743
- snapshot.scheduledActivities.forEach((activity, index) => {
1744
- validateScheduledActivityInto(activity, `${path}.scheduledActivities[${index}]`, into, levels);
1750
+ snapshot.calendar.forEach((activity, index) => {
1751
+ validateScheduledActivityInto(activity, `${path}.calendar[${index}]`, into, levels);
1745
1752
  if (isPlainObject(activity) && isFilled(activity.id)) {
1746
1753
  if (seen.has(activity.id)) {
1747
- into.add("activity.id.unique", `${path}.scheduledActivities[${index}].id`, `duplicate activity id: ${activity.id}`);
1754
+ into.add("activity.id.unique", `${path}.calendar[${index}].id`, `duplicate activity id: ${activity.id}`);
1748
1755
  }
1749
1756
  seen.add(activity.id);
1750
1757
  }
1751
1758
  });
1752
1759
  }
1753
1760
  else {
1754
- into.add("snapshot.calendar.shape", `${path}.scheduledActivities`, "scheduledActivities must be an array when present");
1761
+ into.add("snapshot.calendar.shape", `${path}.calendar`, "calendar must be an array when present");
1755
1762
  }
1756
1763
  }
1757
1764
  // The login is the permission: a lead's snapshot carries a team member list, `members: []` included,
@@ -1903,13 +1910,13 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1903
1910
  case "task-updated":
1904
1911
  validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest), locked: context.locked }, `${at}.task`, into);
1905
1912
  break;
1906
- case "task-media-started":
1907
- into.require(channel === "voice", "event.media.channel", `${at}.type`, "only a voice provider publishes media transitions");
1908
- into.require(isAssignmentId(event.assignmentId), "event.taskMediaStarted.assignmentId", `${at}.assignmentId`, "an event about a task names its assignment");
1913
+ case "task-audio-started":
1914
+ into.require(channel === "voice", "event.audio.channel", `${at}.type`, "only a voice provider publishes audio transitions");
1915
+ into.require(isAssignmentId(event.assignmentId), "event.taskAudioStarted.assignmentId", `${at}.assignmentId`, "an event about a task names its assignment");
1909
1916
  break;
1910
- case "task-media-ended":
1911
- into.require(channel === "voice", "event.media.channel", `${at}.type`, "only a voice provider publishes media transitions");
1912
- into.require(isAssignmentId(event.assignmentId), "event.taskMediaEnded.assignmentId", `${at}.assignmentId`, "an event about a task names its assignment");
1917
+ case "task-audio-ended":
1918
+ into.require(channel === "voice", "event.audio.channel", `${at}.type`, "only a voice provider publishes audio transitions");
1919
+ into.require(isAssignmentId(event.assignmentId), "event.taskAudioEnded.assignmentId", `${at}.assignmentId`, "an event about a task names its assignment");
1913
1920
  break;
1914
1921
  case "task-ended":
1915
1922
  into.require(isAssignmentId(event.assignmentId), "event.taskEnded.assignmentId", `${at}.assignmentId`, "an event about a task names its assignment");
@@ -1953,14 +1960,6 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1953
1960
  into.require(context.leadFeatures !== false, "event.team.features", `${at}.team`, "the lead turned the team feature off, so nothing of the team reaches them");
1954
1961
  validateTeamMembersInto(event.team, `${at}.team`, { ...context, levels, channel: isChannel(channel) ? channel : undefined }, into);
1955
1962
  break;
1956
- case "team-media-started":
1957
- case "team-media-ended":
1958
- // The lead's channel in a member's call: audio the application opens on the member's assignment.
1959
- into.require(channel === "voice", "event.media.channel", `${at}.type`, "only a voice provider publishes media transitions");
1960
- into.require(context.capabilities === undefined || context.capabilities.lead === true, "event.teamMedia.capability", `${at}.type`, "team media reaches a login that declares lead, and nobody else");
1961
- into.require(isUserId(event.memberId), "event.teamMedia.memberId", `${at}.memberId`, "team media names the member whose call the lead is on");
1962
- into.require(isAssignmentId(event.assignmentId), "event.teamMedia.assignmentId", `${at}.assignmentId`, "team media names the member's assignment");
1963
- break;
1964
1963
  case "contacts-updated":
1965
1964
  into.require(idle.contacts === true, "event.contacts.capability", `${at}.contacts`, "contacts-updated requires the contacts idle capability");
1966
1965
  if (!Array.isArray(event.contacts)) {
@@ -1971,17 +1970,17 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1971
1970
  }
1972
1971
  break;
1973
1972
  case "calendar-updated":
1974
- into.require(idle.calendar === true, "event.calendar.capability", `${at}.scheduledActivities`, "calendar-updated requires the calendar idle capability");
1975
- if (!Array.isArray(event.scheduledActivities)) {
1976
- into.add("event.calendar.shape", `${at}.scheduledActivities`, "scheduledActivities must be an array");
1973
+ into.require(idle.calendar === true, "event.calendar.capability", `${at}.calendar`, "calendar-updated requires the calendar idle capability");
1974
+ if (!Array.isArray(event.calendar)) {
1975
+ into.add("event.calendar.shape", `${at}.calendar`, "calendar must be an array");
1977
1976
  }
1978
1977
  else {
1979
1978
  const ids = new Set();
1980
- event.scheduledActivities.forEach((activity, index) => {
1981
- validateScheduledActivityInto(activity, `${at}.scheduledActivities[${index}]`, into, levels);
1979
+ event.calendar.forEach((activity, index) => {
1980
+ validateScheduledActivityInto(activity, `${at}.calendar[${index}]`, into, levels);
1982
1981
  if (isPlainObject(activity) && isFilled(activity.id)) {
1983
1982
  if (ids.has(activity.id))
1984
- into.add("activity.id.unique", `${at}.scheduledActivities[${index}].id`, `duplicate activity id: ${activity.id}`);
1983
+ into.add("activity.id.unique", `${at}.calendar[${index}].id`, `duplicate activity id: ${activity.id}`);
1985
1984
  ids.add(activity.id);
1986
1985
  }
1987
1986
  });
@@ -2021,6 +2020,43 @@ function validatePreferencesInto(value, path, into, levels) {
2021
2020
  });
2022
2021
  }
2023
2022
  /** The team's policy per capability as the lead sees it: the setting, who set it, who locked it. */
2023
+ function validateShiftInto(value, path, into) {
2024
+ if (!isPlainObject(value)) {
2025
+ into.add("team.member.shift.shape", path, "shift must be an object when present");
2026
+ return;
2027
+ }
2028
+ const signedIn = into.timestamp(value.signedInAt, "team.member.shift.signedInAt", `${path}.signedInAt`);
2029
+ if (value.signedOutAt !== undefined && into.timestamp(value.signedOutAt, "team.member.shift.signedOutAt", `${path}.signedOutAt`) && signedIn) {
2030
+ into.require(Date.parse(value.signedOutAt) >= Date.parse(value.signedInAt), "team.member.shift.signedOutAt.order", `${path}.signedOutAt`, "a member signs out after signing in");
2031
+ }
2032
+ for (const field of ["talkSeconds", "holdSeconds", "breakSeconds"]) {
2033
+ if (value[field] !== undefined)
2034
+ into.require(isDurationSeconds(value[field]), `team.member.shift.${field}`, `${path}.${field}`, `${field} must be a whole number of seconds, zero or more, or omitted when unknown`);
2035
+ }
2036
+ if (value.tasksHandled !== undefined)
2037
+ into.require(Number.isInteger(value.tasksHandled) && value.tasksHandled >= 0, "team.member.shift.tasksHandled", `${path}.tasksHandled`, "tasksHandled must be a whole number, zero or more, or omitted when unknown");
2038
+ if (value.events !== undefined) {
2039
+ if (!Array.isArray(value.events)) {
2040
+ into.add("team.member.shift.events.shape", `${path}.events`, "events must be an array when present");
2041
+ return;
2042
+ }
2043
+ let previous;
2044
+ value.events.forEach((event, index) => {
2045
+ const at = `${path}.events[${index}]`;
2046
+ if (!isPlainObject(event)) {
2047
+ into.add("team.member.shift.event.shape", at, "each shift event must be an object");
2048
+ return;
2049
+ }
2050
+ into.oneOf(event.kind, SHIFT_EVENT_KINDS, "team.member.shift.event.kind", `${at}.kind`);
2051
+ if (into.timestamp(event.at, "team.member.shift.event.at", `${at}.at`)) {
2052
+ const instant = Date.parse(event.at);
2053
+ if (previous !== undefined && instant < previous)
2054
+ into.add("team.member.shift.events.order", `${at}.at`, "shift events are oldest first; this event is earlier than the one before it");
2055
+ previous = instant;
2056
+ }
2057
+ });
2058
+ }
2059
+ }
2024
2060
  function validateTeamPoliciesInto(value, path, into, levels) {
2025
2061
  if (!isPlainObject(value)) {
2026
2062
  into.add("team.policies.shape", path, "policies must be an object keyed by capability");
@@ -2293,7 +2329,7 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2293
2329
  if (type !== "custom" && type !== "recording") {
2294
2330
  let fields = ["type"];
2295
2331
  switch (type) {
2296
- case "call":
2332
+ case "dial":
2297
2333
  case "connect-back":
2298
2334
  fields.push("dialId");
2299
2335
  break;
@@ -2320,8 +2356,8 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2320
2356
  }
2321
2357
  const dial = (rule) => into.filled(command.dialId, rule, `${path}.dialId`, "a command that dials carries the host's dialId");
2322
2358
  switch (type) {
2323
- case "call":
2324
- dial("command.call.dialId");
2359
+ case "dial":
2360
+ dial("command.dial.dialId");
2325
2361
  break;
2326
2362
  case "connect-back":
2327
2363
  dial("command.connectBack.dialId");
@@ -2433,7 +2469,7 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2433
2469
  if (type === "decline")
2434
2470
  offered("decline");
2435
2471
  break;
2436
- case "call":
2472
+ case "dial":
2437
2473
  inPhase("command.phase.preview", "preview");
2438
2474
  break;
2439
2475
  case "hold":
@@ -2458,7 +2494,7 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2458
2494
  into.violations.push(...validateRecordingCommandState(command, policy, state, path));
2459
2495
  if (command.action === "start" || command.action === "resume") {
2460
2496
  interaction();
2461
- into.require(task.media === "started", "recording.request.phase", path, "capture needs live media");
2497
+ into.require(task.audio === "started", "recording.request.phase", path, "capture needs live audio");
2462
2498
  }
2463
2499
  else
2464
2500
  inPhase("command.phase.interaction", "in-progress", "paused", "completing");
@@ -2551,7 +2587,7 @@ const RESULT_STATUSES = {
2551
2587
  cancelBreak: { success: "cancelled", failure: "failed" },
2552
2588
  endBreak: { success: "ended", failure: "failed" },
2553
2589
  executeTeam: { success: "applied", failure: "failed" },
2554
- openMedia: { success: "opened", failure: "unavailable" },
2590
+ openAudio: { success: "opened", failure: "unavailable" },
2555
2591
  setPreference: { success: "applied", failure: "failed" },
2556
2592
  recordStep: { success: "recorded", failure: "failed" },
2557
2593
  };
@@ -2571,22 +2607,22 @@ export function validateResult(result, method, path = "result", dialId) {
2571
2607
  into.add("result.shape", path, `${method} must answer an object`);
2572
2608
  return into.violations;
2573
2609
  }
2574
- if (method === "openMedia" && result.status !== "opened") {
2575
- into.require(result.session === undefined, "result.session.unexpected", `${path}.session`, "only opened carries a media session");
2610
+ if (method === "openAudio" && result.status !== "opened") {
2611
+ into.require(result.audio === undefined, "result.audio.unexpected", `${path}.audio`, "only opened carries an audio session");
2576
2612
  }
2577
2613
  if (result.status === statuses.success) {
2578
2614
  into.require(result.failure === undefined, "result.failure.unexpected", `${path}.failure`, `${statuses.success} carries no failure`);
2579
- if (method === "openMedia") {
2580
- if (isPlainObject(result.session)) {
2615
+ if (method === "openAudio") {
2616
+ if (isPlainObject(result.audio)) {
2581
2617
  // Streams may come from another realm, and Node conformance runs have no MediaStream
2582
2618
  // constructor. Check the boundary shape without claiming to verify live audio.
2583
- into.require(isPlainObject(result.session.remoteAudio), "result.session.remoteAudio", `${path}.session.remoteAudio`, "an opened session carries its remote audio stream");
2619
+ into.require(isPlainObject(result.audio.remoteAudio), "result.audio.remoteAudio", `${path}.audio.remoteAudio`, "an opened session carries its remote audio stream");
2584
2620
  for (const method of ["setMuted", "close"]) {
2585
- into.require(typeof result.session[method] === "function", `result.session.${method}`, `${path}.session.${method}`, `a media session implements ${method}`);
2621
+ into.require(typeof result.audio[method] === "function", `result.audio.${method}`, `${path}.audio.${method}`, `an audio session implements ${method}`);
2586
2622
  }
2587
2623
  }
2588
2624
  else {
2589
- into.add("result.session", `${path}.session`, "opened carries the media session");
2625
+ into.add("result.audio", `${path}.audio`, "opened carries the audio session");
2590
2626
  }
2591
2627
  }
2592
2628
  if (dials) {
@@ -2893,7 +2929,7 @@ export function validateRecordingPolicy(value, path = "capabilities.recording")
2893
2929
  check(false, "policy.shape", at, "expected permitted actions");
2894
2930
  continue;
2895
2931
  }
2896
- keys(policy, [...RECORDING_ACTIONS, ...(source === "host" ? ["destinationId"] : [])], at);
2932
+ keys(policy, [...RECORDING_ACTIONS, ...(source === "host" ? ["storageId"] : [])], at);
2897
2933
  check(RECORDING_ACTIONS.some(action => policy[action] !== undefined), "policy.empty", at, "omit paths offering no actions");
2898
2934
  for (const action of RECORDING_ACTIONS) {
2899
2935
  if (policy[action] === undefined)
@@ -2901,7 +2937,7 @@ export function validateRecordingPolicy(value, path = "capabilities.recording")
2901
2937
  check(policy[action] === true, "policy.action", `${at}.${action}`, "permission is true or absent");
2902
2938
  }
2903
2939
  if (source === "host")
2904
- check(filled(policy.destinationId), "destination", `${at}.destinationId`, "host storage destination must be explicit per task");
2940
+ check(filled(policy.storageId), "storage", `${at}.storageId`, "host storage must be named per task");
2905
2941
  }
2906
2942
  return violations;
2907
2943
  }
@@ -2909,15 +2945,15 @@ export function validateHostRecording(value, softphone, path = "host.recording")
2909
2945
  if (value === undefined)
2910
2946
  return [];
2911
2947
  const { violations, check, keys } = collector();
2912
- check(softphone, "host.channel", path, "initial host recording requires voice softphone media");
2948
+ check(softphone, "host.channel", path, "initial host recording requires voice softphone audio");
2913
2949
  if (!object(value)) {
2914
2950
  check(false, "host.shape", path, "expected host recording declaration and executor");
2915
2951
  return violations;
2916
2952
  }
2917
- keys(value, ["actions", "destinationIds", "execute", "announcesToCaller"], path);
2953
+ keys(value, ["actions", "storageIds", "execute", "announcesToCaller"], path);
2918
2954
  if (value.announcesToCaller !== undefined)
2919
2955
  check(value.announcesToCaller === true, "host.announcesToCaller", `${path}.announcesToCaller`, "caller announcement guarantee is true or absent");
2920
- for (const [key, allowed] of [["actions", RECORDING_ACTIONS], ["destinationIds", undefined]]) {
2956
+ for (const [key, allowed] of [["actions", RECORDING_ACTIONS], ["storageIds", undefined]]) {
2921
2957
  const list = value[key];
2922
2958
  if (!Array.isArray(list)) {
2923
2959
  check(false, "host.list", `${path}.${key}`, "expected explicit array");
@@ -3033,14 +3069,14 @@ export function validateRecordingRequest(request, task, context, path = "request
3033
3069
  state = Array.isArray(reports) ? reports.find(r => object(r) && r.assignmentId === task.assignmentId)?.state : undefined;
3034
3070
  if (object(context.host)) {
3035
3071
  check(Array.isArray(context.host.actions) && context.host.actions.includes(command.action), "host.action", path, "host does not support action");
3036
- 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");
3072
+ check(object(policy) && Array.isArray(context.host.storageIds) && context.host.storageIds.includes(policy.storageId), "host.storage", path, "task storage is not one this host provisions");
3037
3073
  }
3038
3074
  }
3039
3075
  const fresh = effectiveRecordingState(state, context.now);
3040
3076
  check(fresh.status !== "unknown", "request.freshness", path, "current state or trusted observer time unavailable/expired");
3041
3077
  violations.push(...validateRecordingCommandState(command, policy, fresh, `${path}.command`));
3042
3078
  const opening = command.action === "start" || command.action === "resume";
3043
- check(opening ? ["in-progress", "paused"].includes(task.phase) && task.media === "started" : ["in-progress", "paused", "completing"].includes(task.phase), "request.phase", path, "capture needs live task media; terminal controls may finish recordings during wrap-up");
3079
+ check(opening ? ["in-progress", "paused"].includes(task.phase) && task.audio === "started" : ["in-progress", "paused", "completing"].includes(task.phase), "request.phase", path, "capture needs live task audio; terminal controls may finish recordings during wrap-up");
3044
3080
  return violations;
3045
3081
  }
3046
3082
  /** Check recording-specific result semantics against the owner's confirming observations. */