@xema/omni-protocol 0.1.120 → 0.1.122

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/GLOSSARY.md CHANGED
@@ -100,7 +100,7 @@ hands only through routing or a lead's take-over.
100
100
 
101
101
  | Term | In plain words | What others call it |
102
102
  | --- | --- | --- |
103
- | **History** | The steps a call went through before and with this agent: queued, answered, held, handed over. | *Contact trace record* (Amazon Connect), *conversation details* or *segments* (Genesys Cloud), *call detail record*. |
103
+ | **History** | The steps a call went through before and with this agent: arrived, queued, answered, held, handed over. | *Contact trace record* (Amazon Connect), *conversation details* or *segments* (Genesys Cloud), *call detail record*. |
104
104
  | **Recording** | A recording of one side of the call, the customer's or the agent's. | *Call recording*, *screen and voice recording*. |
105
105
  | **Paused for sensitive details** | Recording stops while card or account details are given. | *Secure pause* (Genesys Cloud), *PCI pause*. |
106
106
 
package/dist/index.d.ts CHANGED
@@ -177,11 +177,31 @@ export type IdleCapabilities<C extends Channel = Channel> = {
177
177
  } : {
178
178
  dial?: never;
179
179
  });
180
- export type TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, string>>>;
180
+ /** A BCP 47 language tag: `en`, `hi`, `en-IN`. */
181
+ export type LanguageTag = string;
182
+ /**
183
+ * One display text per language the provider words it in, keyed by language tag. The agent
184
+ * application shows the one for the agent's language, found with `localizedText()`, and its own
185
+ * word when the provider gave none in that language.
186
+ */
187
+ export type LocalizedText = Readonly<Record<LanguageTag, string>>;
188
+ export type TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, LocalizedText>>>;
189
+ export type HistoryStepLabels = Readonly<Partial<Record<HistoryStep, LocalizedText>>>;
181
190
  export interface TaskTypePresentation {
182
- singular: string;
183
- plural: string;
184
- referenceLabel?: string;
191
+ singular: LocalizedText;
192
+ plural: LocalizedText;
193
+ referenceLabel?: LocalizedText;
194
+ }
195
+ /**
196
+ * What the signed-in customer calls things, as its own admin set them in the provider: one
197
+ * customer's agents read "New call" where another's read "Entered". Each part is partial and
198
+ * renames only; the values underneath stay the protocol's, so counts by phase or step add up.
199
+ */
200
+ export interface Labels {
201
+ phases?: TaskPhaseLabels;
202
+ historySteps?: HistoryStepLabels;
203
+ /** Keyed by `taskType`. An entry replaces the channel default outright rather than merging. */
204
+ taskTypes?: Readonly<Record<string, TaskTypePresentation>>;
185
205
  }
186
206
  export interface Manifest<C extends Channel = Channel> {
187
207
  /** Stable, installation-wide unique, and unchanged between launches. */
@@ -202,9 +222,6 @@ export interface Manifest<C extends Channel = Channel> {
202
222
  timeCheck?: true;
203
223
  /** Provider uses its own timestamps for final records; host timestamps are advisory. Omission makes no trust promise. */
204
224
  timestampAuthority?: "provider";
205
- phaseLabels?: TaskPhaseLabels;
206
- /** Keyed by `taskType`. An entry replaces the channel default outright rather than merging. */
207
- taskTypePresentation?: Record<string, TaskTypePresentation>;
208
225
  /** The organisation's whole ladder, stated outright, `person` included. Omitted for the typical four, `DEFAULT_LEVELS`. */
209
226
  orgLevels?: LevelDeclaration[];
210
227
  /**
@@ -855,7 +872,7 @@ export type TaskAttribute = TaskAttributeBase & ({
855
872
  type: "timestamp";
856
873
  at: IsoTimestamp;
857
874
  });
858
- export type HistoryStep = "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered" | "callback-requested";
875
+ export type HistoryStep = "arrived" | "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered" | "callback-requested";
859
876
  /**
860
877
  * The call record: the steps that brought the task here, one entry per occurrence and oldest
861
878
  * first, and what they add up to before this agent -- stated by the provider from its own record,
@@ -890,7 +907,7 @@ export interface TaskHistoryStep {
890
907
  */
891
908
  seconds?: DurationSeconds;
892
909
  /**
893
- * Who took part. Absent on `queued`, where nobody does; absent on `callback-requested`, where the
910
+ * Who took part. Absent on `arrived` and `queued`, where nobody does; absent on `callback-requested`, where the
894
911
  * caller chose it in the queue, and present there when an agent arranged it; absent on any other
895
912
  * step means the provider could not attribute it, which is a different claim and a legitimate one.
896
913
  */
@@ -1022,12 +1039,13 @@ export interface Locked {
1022
1039
  */
1023
1040
  export type Lockable<T> = T | Locked;
1024
1041
  /**
1025
- * Who chose a task's capabilities. `not-yet-read` is a fault the provider states on the task rather
1026
- * than hides: the platform's terms could not be read, and the set published is what the provider
1027
- * will honour, not what the platform permits.
1042
+ * Who chose a task's capabilities: the queue that handed the work over, the dialer that declared
1043
+ * outbound work, or nobody for an agent's own outbound. `not-yet-read` is a fault the provider
1044
+ * states on the task rather than hides: the platform's terms could not be read, and the set
1045
+ * published is what the provider will honour, not what the platform permits.
1028
1046
  */
1029
- export type CapabilitySource = "queue" | "nobody" | "not-yet-read";
1030
- export declare const CAPABILITY_SOURCES: readonly ["queue", "nobody", "not-yet-read"];
1047
+ export type CapabilitySource = "queue" | "dialer" | "nobody" | "not-yet-read";
1048
+ export declare const CAPABILITY_SOURCES: readonly ["queue", "dialer", "nobody", "not-yet-read"];
1031
1049
  export type Task<C extends Channel = Channel> = {
1032
1050
  /** The assignment this task is the record of, and its one identity. See `AssignmentId`. */
1033
1051
  assignmentId: AssignmentId;
@@ -1755,6 +1773,8 @@ export interface Snapshot<C extends Channel = Channel> {
1755
1773
  /** The call waiting in the agent's own queue, while one is; replaced whole by `lined-up`. */
1756
1774
  linedUp?: LinedUpCall;
1757
1775
  team?: TeamMembers;
1776
+ /** The signed-in customer's own words, from its admin's settings; replaced whole by `labels-updated`. Omitted where it set none. */
1777
+ labels?: Labels;
1758
1778
  }
1759
1779
  /**
1760
1780
  * How many tasks this provider may have assigned to the agent at once.
@@ -1885,6 +1905,11 @@ export type ProviderEvent<C extends Channel = Channel> = {
1885
1905
  | {
1886
1906
  type: "lined-up";
1887
1907
  linedUp?: LinedUpCall;
1908
+ }
1909
+ /** The customer's words as its admin now has them, whole: present with what it set, empty when it set nothing. */
1910
+ | {
1911
+ type: "labels-updated";
1912
+ labels: Labels;
1888
1913
  };
1889
1914
  /**
1890
1915
  * The `Provider` prefix survives here for a mechanical reason rather than a naming one: `Event`
@@ -2026,6 +2051,24 @@ export declare const DEFAULT_TASK_TYPE_PRESENTATION: {
2026
2051
  readonly referenceLabel: "Email ID";
2027
2052
  };
2028
2053
  };
2054
+ export declare const DEFAULT_HISTORY_STEP_LABELS: {
2055
+ readonly arrived: "Incoming call";
2056
+ readonly queued: "Queued";
2057
+ readonly offered: "Offered";
2058
+ readonly answered: "Answered";
2059
+ readonly held: "On hold";
2060
+ readonly muted: "Muted";
2061
+ readonly transferred: "Transferred";
2062
+ readonly conferenced: "Conferenced";
2063
+ readonly unanswered: "Unanswered";
2064
+ readonly "callback-requested": "Callback requested";
2065
+ };
2066
+ /**
2067
+ * The provider's text for the first of `languages` it has, matched as BCP 47 lookup does: the tag
2068
+ * whole, then with its last subtag dropped, so `hi-IN` finds `hi`. Undefined when it has none of
2069
+ * them, and the agent application shows its own word; never another language in their place.
2070
+ */
2071
+ export declare function localizedText(text: LocalizedText | undefined, languages: readonly LanguageTag[]): string | undefined;
2029
2072
  /**
2030
2073
  * A collision-safe global task key.
2031
2074
  *
@@ -2043,8 +2086,9 @@ export declare const assignmentKey: (providerId: string, assignmentId: Assignmen
2043
2086
  */
2044
2087
  export declare const userKey: (providerId: string, userId: UserId) => string;
2045
2088
  /**
2046
- * Every history step somebody from the provider takes part in. `queued` is the one nobody does, and
2047
- * `callback-requested` is the caller's choice unless an agent arranged it, so neither is here.
2089
+ * Every history step somebody from the provider takes part in. `arrived` and `queued` are the ones
2090
+ * nobody does, and `callback-requested` is the caller's choice unless an agent arranged it, so none
2091
+ * of them is here.
2048
2092
  */
2049
2093
  export declare const HISTORY_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
2050
2094
  /** The steps a dial writes, and so the only ones that carry a `dialId` and a `destinationId`. */
package/dist/index.js CHANGED
@@ -77,7 +77,7 @@ export const DEFAULT_LEVELS = [
77
77
  export function effectiveLevels(declared) {
78
78
  return [...(declared ?? DEFAULT_LEVELS)];
79
79
  }
80
- export const CAPABILITY_SOURCES = ["queue", "nobody", "not-yet-read"];
80
+ export const CAPABILITY_SOURCES = ["queue", "dialer", "nobody", "not-yet-read"];
81
81
  // ---------------------------------------------------------------------------
82
82
  // Task commands.
83
83
  // ---------------------------------------------------------------------------
@@ -145,6 +145,29 @@ export const DEFAULT_TASK_TYPE_PRESENTATION = {
145
145
  chat: { singular: "Chat", plural: "Chats", referenceLabel: "Chat ID" },
146
146
  email: { singular: "Email", plural: "Emails", referenceLabel: "Email ID" },
147
147
  };
148
+ export const DEFAULT_HISTORY_STEP_LABELS = {
149
+ arrived: "Incoming call", queued: "Queued", offered: "Offered", answered: "Answered", held: "On hold",
150
+ muted: "Muted", transferred: "Transferred", conferenced: "Conferenced", unanswered: "Unanswered",
151
+ "callback-requested": "Callback requested",
152
+ };
153
+ /**
154
+ * The provider's text for the first of `languages` it has, matched as BCP 47 lookup does: the tag
155
+ * whole, then with its last subtag dropped, so `hi-IN` finds `hi`. Undefined when it has none of
156
+ * them, and the agent application shows its own word; never another language in their place.
157
+ */
158
+ export function localizedText(text, languages) {
159
+ if (text === undefined)
160
+ return undefined;
161
+ const byTag = new Map(Object.entries(text).map(([tag, value]) => [tag.toLowerCase(), value]));
162
+ for (const language of languages) {
163
+ for (let tag = language.toLowerCase(); tag !== ""; tag = tag.includes("-") ? tag.slice(0, tag.lastIndexOf("-")) : "") {
164
+ const found = byTag.get(tag);
165
+ if (found !== undefined)
166
+ return found;
167
+ }
168
+ }
169
+ return undefined;
170
+ }
148
171
  // ---------------------------------------------------------------------------
149
172
  // Utilities.
150
173
  // ---------------------------------------------------------------------------
@@ -165,8 +188,9 @@ export const assignmentKey = (providerId, assignmentId) => `${encodeURIComponent
165
188
  */
166
189
  export const userKey = (providerId, userId) => `${encodeURIComponent(providerId)}:${encodeURIComponent(userId)}`;
167
190
  /**
168
- * Every history step somebody from the provider takes part in. `queued` is the one nobody does, and
169
- * `callback-requested` is the caller's choice unless an agent arranged it, so neither is here.
191
+ * Every history step somebody from the provider takes part in. `arrived` and `queued` are the ones
192
+ * nobody does, and `callback-requested` is the caller's choice unless an agent arranged it, so none
193
+ * of them is here.
170
194
  */
171
195
  export const HISTORY_STEPS_WITH_A_PERSON = [
172
196
  "offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered",
package/dist/testing.d.ts CHANGED
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
7
7
  * rules -- each optional part of a task, each optional part of the break state and team member list, each
8
8
  * declared contribution, and each event type.
9
9
  */
10
- declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.request", "team.tasks", "team.listening", "team.shift", "team.phone", "team.nextCall", "team.linedUp", "shift", "phone", "nextCall", "linedUp", "contacts", "calendar", "team.policies"];
10
+ declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.request", "team.tasks", "team.listening", "team.shift", "team.phone", "team.nextCall", "team.linedUp", "shift", "phone", "nextCall", "linedUp", "labels", "contacts", "calendar", "team.policies"];
11
11
  export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
12
12
  export interface AdapterContractResult {
13
13
  events: ProviderEventEnvelope[];
package/dist/testing.js CHANGED
@@ -35,6 +35,7 @@ const STATE_SUBJECTS = [
35
35
  "phone",
36
36
  "nextCall",
37
37
  "linedUp",
38
+ "labels",
38
39
  "contacts",
39
40
  "calendar",
40
41
  "team.policies",
@@ -46,6 +47,7 @@ const EVENT_TYPES = {
46
47
  "task-audio-started": true, "task-audio-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
47
48
  "team-updated": true, "team-member-updated": true, "team-member-removed": true, "team-policies-updated": true,
48
49
  "contacts-updated": true, "calendar-updated": true, "shift-updated": true, "phone-updated": true, "next-call": true, "lined-up": true,
50
+ "labels-updated": true,
49
51
  };
50
52
  const CONTRACT_SUBJECTS = [
51
53
  ...STATE_SUBJECTS,
@@ -147,6 +149,8 @@ function observeSnapshot(value, seen) {
147
149
  seen.add("nextCall");
148
150
  if (value.linedUp !== undefined)
149
151
  seen.add("linedUp");
152
+ if (value.labels !== undefined)
153
+ seen.add("labels");
150
154
  }
151
155
  function observeEvent(envelope, seen) {
152
156
  const event = isRecord(envelope) ? envelope.event : undefined;
@@ -197,6 +201,9 @@ function observeEvent(envelope, seen) {
197
201
  if (event.linedUp !== undefined)
198
202
  seen.add("linedUp");
199
203
  break;
204
+ case "labels-updated":
205
+ seen.add("labels");
206
+ break;
200
207
  default: break;
201
208
  }
202
209
  }
@@ -1154,7 +1161,7 @@ const REACHABLE_PHASES = {
1154
1161
  export const SUPERSEDED_BY_A_SNAPSHOT = new Set([
1155
1162
  "snapshot", "transport-status", "break-state", "task-offered", "task-updated", "task-ended",
1156
1163
  "task-audio-started", "task-audio-ended", "team-updated", "team-member-updated", "team-member-removed", "team-policies-updated",
1157
- "contacts-updated", "calendar-updated", "shift-updated", "phone-updated", "next-call", "lined-up",
1164
+ "contacts-updated", "calendar-updated", "shift-updated", "phone-updated", "next-call", "lined-up", "labels-updated",
1158
1165
  ]);
1159
1166
  export class TaskStream {
1160
1167
  tasks = new Map();
@@ -30,6 +30,8 @@ export declare function validateProviderTimeCheckRequest(value: unknown, path?:
30
30
  /** Checks response shape/correlation, not source-clock accuracy, round-trip delay or freshness. */
31
31
  export declare function validateProviderTimeCheckResult(value: unknown, request: unknown, loginId: string, path?: string): ProtocolViolation[];
32
32
  export declare function validateManifest(manifest: unknown, path?: string): ProtocolViolation[];
33
+ /** The customer's own words, as `Snapshot.labels` and `labels-updated` carry them. */
34
+ export declare function validateLabels(value: unknown, path?: string): ProtocolViolation[];
33
35
  export interface TaskValidationContext {
34
36
  /** The provider's channel, from its manifest. A task must agree with it. */
35
37
  channel: string;
@@ -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, nobody: true, "not-yet-read": true });
44
+ const CAPABILITY_SOURCES = membersOf({ queue: true, dialer: true, nobody: true, "not-yet-read": true });
45
45
  const ACCEPTANCE_MODES = membersOf({
46
46
  "no-preference": true, "consent": true, "automatic": true,
47
47
  });
@@ -58,7 +58,7 @@ const TEAM_AVAILABILITIES = membersOf({
58
58
  ready: true, "on-task": true, "on-break": true, elsewhere: true, "signed-out": true,
59
59
  });
60
60
  const HISTORY_STEPS = membersOf({
61
- queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
61
+ arrived: true, queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
62
62
  "callback-requested": true,
63
63
  });
64
64
  const CUSTOM_UI_CONTROLS = membersOf({ button: true, toggle: true, "menu-item": true });
@@ -517,16 +517,9 @@ export function validateManifest(manifest, path = "manifest") {
517
517
  into.require(manifest.timeCheck === true, "manifest.timeCheck", `${path}.timeCheck`, "declare true or omit unsupported clock checks");
518
518
  validateDialOutcomes(manifest, `${path}.dialOutcomes`, into);
519
519
  validatePhones(manifest, `${path}.phones`, into);
520
- if (manifest.phaseLabels !== undefined) {
521
- if (!isPlainObject(manifest.phaseLabels)) {
522
- into.add("manifest.phaseLabels.shape", `${path}.phaseLabels`, "phaseLabels must be an object when present");
523
- }
524
- else {
525
- for (const [phase, label] of Object.entries(manifest.phaseLabels)) {
526
- into.oneOf(phase, TASK_PHASES, "manifest.phaseLabels.phase", `${path}.phaseLabels.${phase}`);
527
- into.filled(label, "manifest.phaseLabels.label", `${path}.phaseLabels.${phase}`, "a phase label must not be empty");
528
- }
529
- }
520
+ // Labels are the signed-in customer's, set by its own admin: a manifest is fixed when the adapter ships.
521
+ for (const former of ["phaseLabels", "historyStepLabels", "taskTypePresentation"]) {
522
+ into.require(!Object.hasOwn(manifest, former), "manifest.labels.moved", `${path}.${former}`, "labels are the signed-in customer's own words and travel as Snapshot.labels and labels-updated; the manifest carries none");
530
523
  }
531
524
  if (manifest.runningStepReports !== undefined) {
532
525
  into.require(manifest.runningStepReports === true, "manifest.runningStepReports", `${path}.runningStepReports`, "runningStepReports is declared by presence, as true; a provider that takes begin and end only omits it");
@@ -566,26 +559,67 @@ export function validateManifest(manifest, path = "manifest") {
566
559
  }
567
560
  }
568
561
  }
569
- if (manifest.taskTypePresentation !== undefined) {
570
- if (!isPlainObject(manifest.taskTypePresentation)) {
571
- into.add("manifest.taskTypePresentation.shape", `${path}.taskTypePresentation`, "taskTypePresentation must be an object when present");
562
+ return into.violations;
563
+ }
564
+ /** Language tags as BCP 47 writes them: a language, then subtags of up to eight letters or digits. */
565
+ const LANGUAGE_TAG = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$/;
566
+ /** The customer's own words, as `Snapshot.labels` and `labels-updated` carry them. */
567
+ export function validateLabels(value, path = "labels") {
568
+ const into = new Collector();
569
+ validateLabelsInto(value, path, into);
570
+ return into.violations;
571
+ }
572
+ function validateLabelsInto(value, path, into) {
573
+ if (!isPlainObject(value)) {
574
+ into.add("labels.shape", path, "labels are an object, empty when the customer set none");
575
+ return;
576
+ }
577
+ for (const [field, rule, allowed] of [["phases", "phase", TASK_PHASES], ["historySteps", "step", HISTORY_STEPS]]) {
578
+ if (value[field] === undefined)
579
+ continue;
580
+ if (!isPlainObject(value[field])) {
581
+ into.add(`labels.${field}.shape`, `${path}.${field}`, `${field} must be an object when present`);
582
+ continue;
572
583
  }
573
- else {
574
- for (const [taskType, presentation] of Object.entries(manifest.taskTypePresentation)) {
575
- const at = `${path}.taskTypePresentation.${taskType}`;
576
- if (!isPlainObject(presentation)) {
577
- into.add("manifest.taskTypePresentation.entry", at, "each presentation must be an object");
578
- continue;
579
- }
580
- into.filled(presentation.singular, "manifest.taskTypePresentation.singular", `${at}.singular`, "a presentation needs a singular name");
581
- into.filled(presentation.plural, "manifest.taskTypePresentation.plural", `${at}.plural`, "a presentation needs a plural name");
582
- if (presentation.referenceLabel !== undefined) {
583
- into.filled(presentation.referenceLabel, "manifest.taskTypePresentation.referenceLabel", `${at}.referenceLabel`, "referenceLabel must not be empty when present");
584
- }
584
+ for (const [key, label] of Object.entries(value[field])) {
585
+ // Renames only: a label never adds a phase or a step the protocol does not have.
586
+ into.oneOf(key, allowed, `labels.${field}.${rule}`, `${path}.${field}.${key}`);
587
+ validateLocalizedText(label, `labels.${field}.label`, `${path}.${field}.${key}`, into);
588
+ }
589
+ }
590
+ if (value.taskTypes !== undefined) {
591
+ if (!isPlainObject(value.taskTypes)) {
592
+ into.add("labels.taskTypes.shape", `${path}.taskTypes`, "taskTypes must be an object when present");
593
+ return;
594
+ }
595
+ for (const [taskType, presentation] of Object.entries(value.taskTypes)) {
596
+ const at = `${path}.taskTypes.${taskType}`;
597
+ if (!isPlainObject(presentation)) {
598
+ into.add("labels.taskTypes.entry", at, "each task type's names must be an object");
599
+ continue;
600
+ }
601
+ validateLocalizedText(presentation.singular, "labels.taskTypes.singular", `${at}.singular`, into);
602
+ validateLocalizedText(presentation.plural, "labels.taskTypes.plural", `${at}.plural`, into);
603
+ if (presentation.referenceLabel !== undefined) {
604
+ validateLocalizedText(presentation.referenceLabel, "labels.taskTypes.referenceLabel", `${at}.referenceLabel`, into);
585
605
  }
586
606
  }
587
607
  }
588
- return into.violations;
608
+ }
609
+ /** One text per language: at least one, each tag well-formed and stated once, each text filled. */
610
+ function validateLocalizedText(value, rule, path, into) {
611
+ if (!isPlainObject(value) || Object.keys(value).length === 0) {
612
+ into.add(rule, path, `a label is one text per language, keyed by language tag, as { en: "On Call" }; received ${describeValue(value)}`);
613
+ return;
614
+ }
615
+ const seen = new Set();
616
+ for (const [tag, text] of Object.entries(value)) {
617
+ if (!into.require(LANGUAGE_TAG.test(tag), `${rule}.language`, `${path}.${tag}`, `${describeValue(tag)} is not a language tag such as en, hi or en-IN`))
618
+ continue;
619
+ into.require(!seen.has(tag.toLowerCase()), `${rule}.language.unique`, `${path}.${tag}`, "language tags are case-insensitive, and each language is stated once");
620
+ seen.add(tag.toLowerCase());
621
+ into.filled(text, `${rule}.text`, `${path}.${tag}`, "a label must not be empty in any language it is given in");
622
+ }
589
623
  }
590
624
  // ---------------------------------------------------------------------------
591
625
  // Task.
@@ -921,6 +955,10 @@ function validateHistory(value, path, into, task = {}) {
921
955
  return;
922
956
  }
923
957
  into.oneOf(entry.step, HISTORY_STEPS, "task.history.step", `${at}.step`);
958
+ // A call arrives once, before anything else happens to it.
959
+ if (entry.step === "arrived") {
960
+ into.require(index === 0, "task.history.arrived.first", `${at}.step`, "arrived is when the call reached the provider: the first step, and only once");
961
+ }
924
962
  if (into.timestamp(entry.at, "task.history.at", `${at}.at`)) {
925
963
  // The record is one entry per occurrence, oldest first: a second hold is a second entry
926
964
  // after the first, never a revision of it or an entry filed out of its turn.
@@ -957,8 +995,8 @@ function validateHistory(value, path, into, task = {}) {
957
995
  }
958
996
  if (entry.by !== undefined) {
959
997
  into.require(isUserId(entry.by), "task.history.by", `${at}.by`, "by must be a non-empty user id; omit it when the person cannot be identified");
960
- // On `queued` nobody takes part, so there is nothing to name.
961
- into.require(entry.step !== "queued", "task.history.by.unexpected", `${at}.by`, "a queued step names nobody");
998
+ // On `arrived` and `queued` nobody takes part, so there is nothing to name.
999
+ into.require(entry.step !== "arrived" && entry.step !== "queued", "task.history.by.unexpected", `${at}.by`, `${describeValue(entry.step)} names nobody`);
962
1000
  }
963
1001
  // A step that dialled says where to and, when a host placed it, which dial; no other step dialled.
964
1002
  const dialled = typeof entry.step === "string" && HISTORY_STEPS_THAT_DIAL.includes(entry.step);
@@ -1789,6 +1827,8 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
1789
1827
  // The agent's own day, as the provider counts it: the same numbers their lead sees.
1790
1828
  if (snapshot.shift !== undefined)
1791
1829
  validateShiftInto(snapshot.shift, `${path}.shift`, into, "shift");
1830
+ if (snapshot.labels !== undefined)
1831
+ validateLabelsInto(snapshot.labels, `${path}.labels`, into);
1792
1832
  // The phone as the platform sees it: owed by a provider that declares it can, and by nobody else.
1793
1833
  const seesPhone = isPlainObject(manifest) && manifest.phoneStatus === true;
1794
1834
  // The agent's own queue: the ask and the lined-up call need an assignment at work to be next
@@ -2138,6 +2178,9 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
2138
2178
  case "shift-updated":
2139
2179
  validateShiftInto(event.shift, `${at}.shift`, into, "shift");
2140
2180
  break;
2181
+ case "labels-updated":
2182
+ validateLabelsInto(event.labels, `${at}.labels`, into);
2183
+ break;
2141
2184
  case "calendar-updated":
2142
2185
  into.require(idle.calendar === true, "event.calendar.capability", `${at}.calendar`, "calendar-updated requires the calendar idle capability");
2143
2186
  if (!Array.isArray(event.calendar)) {
package/guide/agent.md CHANGED
@@ -154,7 +154,7 @@ time. Runtime conformance checks also require the task channel to match its prov
154
154
  | `channel` | Channel used for this task. It must equal the source provider's manifest channel. |
155
155
  | `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
156
156
  | `capabilities` | Controls and workspace features available for this specific task. |
157
- | `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `nobody` when nothing handed the work over -- an agent's own outbound -- and `not-yet-read` when a queue was named and its terms could not be read, in which case `capabilities` is what the provider will honour, not what the platform permits. An agent application shows `not-yet-read` where the agent works. See **Task capabilities**. |
157
+ | `capabilitySource` | Required. Who chose the capabilities: `queue` when the queue that handed the work over carries these terms, `dialer` when the dialer that declared outbound work does -- a manual dialer may permit everything, another state narrower terms -- `nobody` when nothing handed the work over -- an agent's own outbound -- and `not-yet-read` when a queue was named and its terms could not be read, in which case `capabilities` is what the provider will honour, not what the platform permits. An agent application shows `not-yet-read` where the agent works. See **Task capabilities**. |
158
158
  | `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
159
159
  | `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
160
160
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
@@ -387,6 +387,7 @@ Migration from the earlier spellings:
387
387
  | a `callback-requested` step, and a callback task starting its history afresh | a `callback-requested` step (`by` when an agent arranged it); a callback's task carries the earlier call's steps, oldest first |
388
388
  | a recording state with an observation id, an observation instant and a validity window, and the helpers that aged it | `recordings` on the task and on `HostRecordingReport`: a list, each recording with `id`, `follows` (`party` or `agent`), `status` (`starting`, `recording`, `paused`, `not-recording`, `unknown`) and a `reason` where paused or unknown; nothing expires; commands name `follows` for a start and `recordingId` otherwise; `validateRecordings` (`recording.task.renamed` refuses the former field) |
389
389
  | a refusal on hold under `omni.command-not-permitted` | `omni.on-hold` |
390
+ | outbound work's terms reported as `queue` | `dialer`: the dialer that declared the task chose its capabilities |
390
391
  | an `end-call` while the caller was on hold | refused: a `paused` task is resumed first (`command.endCall.held`), the agent computer shows End call disabled on hold, the provider answers `failed`, and the test names an adapter that ends from hold (`test.command.held`); `terminate-call` is not gated, it ends the caller's channel too |
391
392
  | the handset spelled by its place on a desk | `hardphone`, on the manifest's `phones` and the login's `phone`; the former spelling is refused |
392
393
  | the desk | the agent computer, in the guide's prose |
@@ -408,6 +409,7 @@ first, and what they add up to before this agent:
408
409
  ```ts
409
410
  history: {
410
411
  steps: [
412
+ { step: "arrived", at: "2026-08-21T00:58:40Z", seconds: 20 },
411
413
  { step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
412
414
  { step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
413
415
  { step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
@@ -428,9 +430,18 @@ something else, which will arrive under its own name and must not be folded in h
428
430
 
429
431
  It rides in the snapshot and is replaced whole like everything else there.
430
432
 
431
- Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
433
+ Steps are `arrived`, `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
432
434
  `unanswered`, `callback-requested`, and each is defined on `TaskHistoryStep`.
433
435
 
436
+ **The record starts where the call started.** `arrived` is the instant the call reached the
437
+ provider, before any menu or queue, and its `seconds`, when the provider measured them, are how
438
+ long it took to move on to the next step. Nobody takes part in it, so it never has `by`. It is the
439
+ first step and appears once (`task.history.arrived.first`); a provider that did not see the call
440
+ arrive omits it rather than guessing an instant. Omni shows it as *Incoming call* unless the customer words it in
441
+ `labels.historySteps`, the way
442
+ Amazon Connect keeps an initiation time, Genesys Cloud a conversation start and NICE CXone a contact
443
+ start, separate from the queue.
444
+
434
445
  **A callback carries the call it comes back to.** When a caller asks to be called back, or an agent
435
446
  arranges it, the provider writes a `callback-requested` step: `by` names the agent who arranged it, and
436
447
  is absent when the caller chose it in the queue. The callback reaches an agent as a new task, and that
@@ -464,7 +475,7 @@ hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming
464
475
  `muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
465
476
  entries, one per join. Order is enforced: an entry earlier than the one before it is refused
466
477
  (`task.history.order`). **Intervals between steps are the agent application's to subtract**, never a
467
- total the provider adds: *time to offer*, from the call's arrival to the agent's screen lighting
478
+ total the provider adds: *time to offer*, from the call joining its queue to the agent's screen lighting
468
479
  up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
469
480
  `queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
470
481
  that interval. **The second is the record's grain.** A duration is a positive whole number of
@@ -540,7 +551,7 @@ Four rules a provider has to keep:
540
551
  wrongly; carrying the name here would also copy it into every task and leave it to go stale.
541
552
 
542
553
  **An absent `by` means different things on different steps, and both are legitimate.** On
543
- `queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
554
+ `arrived` and `queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
544
555
  `HISTORY_STEPS_WITH_A_PERSON` and `historyStepExpectsAPerson()` — so an absent `by` there says
545
556
  *this was handled and the provider cannot say by whom*. The one step that is never unattributed is
546
557
  `muted`: the agent application has exactly one agent and the provider knows who, so `by` is required there
@@ -553,7 +564,7 @@ worse than one saying plainly it could not attribute a leg — and far better th
553
564
  nothing because a single leg could not be named.
554
565
 
555
566
  An agent application must render the two differently. Showing an unattributed `answered` the same way as
556
- `queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
567
+ `queued` or `arrived` tells the agent nobody was involved, which is not what was said. Omni renders it as
557
568
  *"not recorded"* in the place the name would go.
558
569
 
559
570
  Omit `history` entirely when the provider cannot observe the steps. Empty `steps` is a
package/guide.md CHANGED
@@ -233,8 +233,6 @@ type Manifest<C extends Channel = Channel> = {
233
233
  idleCapabilities?: IdleCapabilities<C>;
234
234
  timeCheck?: true;
235
235
  timestampAuthority?: "provider";
236
- phaseLabels?: TaskPhaseLabels;
237
- taskTypePresentation?: Record<string, TaskTypePresentation>;
238
236
  orgLevels?: LevelDeclaration[];
239
237
  dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
240
238
  phones?: C extends "voice" ? Phone[] : never;
@@ -259,12 +257,22 @@ These serialized strings are stable protocol values and must not be renamed or r
259
257
  ### Presentation
260
258
 
261
259
  ```ts
262
- type TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, string>>>;
260
+ type LanguageTag = string; // BCP 47: "en", "hi", "en-IN"
261
+ type LocalizedText = Readonly<Record<LanguageTag, string>>;
262
+
263
+ type TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, LocalizedText>>>;
264
+ type HistoryStepLabels = Readonly<Partial<Record<HistoryStep, LocalizedText>>>;
263
265
 
264
266
  type TaskTypePresentation = {
265
- singular: string;
266
- plural: string;
267
- referenceLabel?: string;
267
+ singular: LocalizedText;
268
+ plural: LocalizedText;
269
+ referenceLabel?: LocalizedText;
270
+ };
271
+
272
+ type Labels = {
273
+ phases?: TaskPhaseLabels;
274
+ historySteps?: HistoryStepLabels;
275
+ taskTypes?: Readonly<Record<string, TaskTypePresentation>>;
268
276
  };
269
277
  ```
270
278
 
@@ -524,6 +532,7 @@ type Snapshot = {
524
532
  nextCall?: NextCallRequest;
525
533
  linedUp?: LinedUpCall;
526
534
  team?: TeamMembers;
535
+ labels?: Labels;
527
536
  };
528
537
 
529
538
  type AgentCapacity = {
@@ -686,6 +695,7 @@ type TaskAttribute = TaskAttributeBase & (
686
695
  );
687
696
 
688
697
  type HistoryStep =
698
+ | "arrived"
689
699
  | "queued"
690
700
  | "offered"
691
701
  | "answered"
@@ -755,7 +765,7 @@ type Lockable<T> = T | Locked;
755
765
 
756
766
  type TaskAudioState = "started" | "ended";
757
767
 
758
- type CapabilitySource = "queue" | "nobody" | "not-yet-read";
768
+ type CapabilitySource = "queue" | "dialer" | "nobody" | "not-yet-read";
759
769
 
760
770
  type Task<C extends Channel = Channel> = {
761
771
  assignmentId: AssignmentId;
@@ -1218,7 +1228,8 @@ type ProviderEvent =
1218
1228
  | { type: "shift-updated"; shift: Shift }
1219
1229
  | { type: "phone-updated"; phone: PhoneState }
1220
1230
  | { type: "next-call"; nextCall?: NextCallRequest }
1221
- | { type: "lined-up"; linedUp?: LinedUpCall };
1231
+ | { type: "lined-up"; linedUp?: LinedUpCall }
1232
+ | { type: "labels-updated"; labels: Labels };
1222
1233
 
1223
1234
  type ProviderEventEnvelope = {
1224
1235
  id: string;
@@ -1351,8 +1362,8 @@ const OMNI_FAILURE_CODES = [
1351
1362
  type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
1352
1363
  ```
1353
1364
 
1354
- `HISTORY_STEPS_WITH_A_PERSON` is every `HistoryStep` except `queued`, which is the one nobody
1355
- takes part in. `historyStepExpectsAPerson()` tests membership.
1365
+ `HISTORY_STEPS_WITH_A_PERSON` is every `HistoryStep` except `arrived` and `queued`, which nobody
1366
+ takes part in, and `callback-requested`, which the caller may choose alone. `historyStepExpectsAPerson()` tests membership.
1356
1367
 
1357
1368
  ### Failure and validation
1358
1369
 
@@ -1373,25 +1384,82 @@ interface ProtocolViolation {
1373
1384
 
1374
1385
  ## Presentation labels
1375
1386
 
1376
- Adapters may supply static display labels for canonical protocol values. Labels affect presentation
1377
- only and cannot vary by connection, task, snapshot, or event.
1387
+ **Each customer's admin chooses the words its agents see, in each language it serves**, in the
1388
+ provider, and the provider publishes them for the signed-in customer as `Snapshot.labels`,
1389
+ replaced whole by `labels-updated` when the admin changes them. Customer A's agents read *New call*
1390
+ where customer B's read *Entered*, on the same adapter, and a change reaches agents without an
1391
+ adapter release. That is how Genesys Cloud loads an organisation's presence names at sign-in and
1392
+ pushes the change when an admin edits one, and how Amazon Connect and NICE CXone keep agent state
1393
+ names per instance or business unit. A manifest is fixed when the adapter ships, so it carries no
1394
+ labels (`manifest.labels.moved`). `labels` is omitted, or `labels-updated` carries `{}`, when the
1395
+ customer set nothing, and the agent application shows its own words throughout.
1396
+
1397
+ ```ts
1398
+ labels: {
1399
+ phases: { "in-progress": { en: "On Call", hi: "कॉल पर" } },
1400
+ historySteps: { arrived: { en: "New call", hi: "नई कॉल" }, queued: { en: "Waiting" } },
1401
+ taskTypes: { Support: { singular: { en: "Support call" }, plural: { en: "Support calls" }, referenceLabel: { en: "Case" } } },
1402
+ }
1403
+ ```
1404
+
1405
+ `validateLabels(value, path?)` checks one on its own.
1406
+
1407
+ **Every word is per language.** Every label is a
1408
+ `LocalizedText`: one text per BCP 47 language tag, as `{ en: "On Call", hi: "कॉल पर" }`. The agent
1409
+ application finds the agent's language with `localizedText(text, languages)`, which tries each of
1410
+ the agent's languages in order, the tag whole and then without its last subtag, so `hi-IN` finds
1411
+ `hi`. When the provider wrote none of them, it returns nothing and the agent application shows its
1412
+ own word; it never shows another language in their place. A single string is the former shape and
1413
+ is refused (`labels.phases.label`, `labels.historySteps.label`,
1414
+ `labels.taskTypes.singular`), as are an empty text (`….text`), a key that is not a language tag
1415
+ (`….language`) and one language given twice in different case (`….language.unique`). This is how
1416
+ Genesys Cloud words its presence definitions and NICE CXone its agent states: the value is the
1417
+ platform's, and the words are per language.
1418
+
1419
+ The values underneath never change: a customer renames what its agents see, so a lead's counts by
1420
+ phase or by step still add up.
1378
1421
 
1379
1422
  ### Renaming a phase
1380
1423
 
1381
- `manifest.phaseLabels` renames the canonical `TaskPhase` values for the agent. It never adds a
1382
- phase or removes one.
1424
+ `labels.phases` renames the canonical `TaskPhase` values for the agent. It never adds a
1425
+ phase or removes one (`labels.phases.phase`).
1383
1426
 
1384
1427
  `TaskPhaseLabels` is `Partial` so that renaming one phase keeps the default wording for the rest —
1385
- an adapter changing "On Call" does not restate the other five.
1428
+ a customer changing "On Call" does not restate the other five.
1386
1429
 
1387
- **A task is labelled by the provider that owns it**, from that provider's manifest merged over its
1430
+ **A task is labelled by the provider that owns it**, from that provider's `labels` merged over its
1388
1431
  channel defaults. Two adapters may word the same phase differently and each is right about its own
1389
1432
  tasks: a call reads as the voice platform names it while a chat beside it reads as its own does.
1390
1433
  How Omni words a view spanning several providers is its own presentation problem, not a provider's.
1391
1434
 
1435
+ ### Naming the history steps
1436
+
1437
+ `labels.historySteps` words the `HistoryStep` values the way `labels.phases` words phases: per
1438
+ step, per language, and partial, so a customer that calls `arrived` *Entered* restates nothing
1439
+ else. It never adds a step; a provider's own stages that the protocol has no step for are not
1440
+ labelled into existence (`labels.historySteps.step`).
1441
+
1442
+ `DEFAULT_HISTORY_STEP_LABELS` is what Omni shows when the customer words a step in none of the
1443
+ agent's languages:
1444
+
1445
+ ```ts
1446
+ const DEFAULT_HISTORY_STEP_LABELS = {
1447
+ arrived: "Incoming call",
1448
+ queued: "Queued",
1449
+ offered: "Offered",
1450
+ answered: "Answered",
1451
+ held: "On hold",
1452
+ muted: "Muted",
1453
+ transferred: "Transferred",
1454
+ conferenced: "Conferenced",
1455
+ unanswered: "Unanswered",
1456
+ "callback-requested": "Callback requested",
1457
+ } as const satisfies Readonly<Record<HistoryStep, string>>;
1458
+ ```
1459
+
1392
1460
  ### `DEFAULT_TASK_PHASE_LABELS`
1393
1461
 
1394
- What Omni shows when an adapter overrides nothing.
1462
+ What Omni shows when the customer renames nothing.
1395
1463
 
1396
1464
  ```ts
1397
1465
  const DEFAULT_TASK_PHASE_LABELS = {
@@ -1426,14 +1494,14 @@ const DEFAULT_TASK_PHASE_LABELS = {
1426
1494
 
1427
1495
  ### Naming a kind of work
1428
1496
 
1429
- `TaskTypePresentation` says what one kind of work is called. Unlike `phaseLabels`, an entry
1430
- **replaces the channel default outright** rather than merging.
1497
+ `TaskTypePresentation` says what one kind of work is called, in `labels.taskTypes`. Unlike
1498
+ `labels.phases`, an entry **replaces the channel default outright** rather than merging.
1431
1499
  `singular` and `plural` are required together because pluralisation is not mechanical, and a
1432
1500
  half-supplied entry would leave Omni pairing one provider's noun with another's plural.
1433
1501
 
1434
1502
  ### `DEFAULT_TASK_TYPE_PRESENTATION`
1435
1503
 
1436
- The per-channel fallback, used for any `taskType` the adapter does not name.
1504
+ The per-channel fallback, used for any `taskType` the customer does not name.
1437
1505
 
1438
1506
  ```ts
1439
1507
  const DEFAULT_TASK_TYPE_PRESENTATION = {
@@ -1453,21 +1521,21 @@ const DEFAULT_TASK_TYPE_PRESENTATION = {
1453
1521
  referenceLabel: "Email ID",
1454
1522
  },
1455
1523
  } as const satisfies Readonly<
1456
- Record<"voice" | "chat" | "email", TaskTypePresentation>
1524
+ Record<"voice" | "chat" | "email", { singular: string; plural: string; referenceLabel: string }>
1457
1525
  >;
1458
1526
  ```
1459
1527
 
1460
1528
  ### Naming task types
1461
1529
 
1462
1530
  `channel` is too coarse for agent-facing names. A WhatsApp call and a PSTN call are both `voice`,
1463
- but the provider may name them differently through static manifest metadata:
1531
+ but the customer may name them differently:
1464
1532
 
1465
1533
  ```ts
1466
- const taskTypePresentation = {
1534
+ const taskTypes = {
1467
1535
  WhatsApp: {
1468
- singular: "Conversation",
1469
- plural: "Conversations",
1470
- referenceLabel: "Conversation ID",
1536
+ singular: { en: "Conversation", hi: "बातचीत" },
1537
+ plural: { en: "Conversations", hi: "बातचीतें" },
1538
+ referenceLabel: { en: "Conversation ID" },
1471
1539
  },
1472
1540
  } satisfies Record<string, TaskTypePresentation>;
1473
1541
  ```
@@ -1710,8 +1778,6 @@ compile time.
1710
1778
  | `supportedProtocolVersions` | Required non-empty list of versions this adapter can speak. Must share at least one with Omni. |
1711
1779
  | `authenticationMethods` | Required non-empty list of supported login methods. |
1712
1780
  | `idleCapabilities` | Declares actions Omni may offer while the agent has no active task, such as voice dialing. Task controls do not belong here. |
1713
- | `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
1714
- | `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
1715
1781
  | `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label an agent computer shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
1716
1782
  | `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `hardphone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call** in `guide/phone.md`. |
1717
1783
  | `phoneStatus` | Voice only, declared by presence as `true`: the platform sees the phone itself -- its registration, its do-not-disturb, its hook, its calls -- and publishes it as `Snapshot.phone` and `phone-updated`. A platform that cannot see the phone omits it, and then publishes no phone (`manifest.phoneStatus`). See **The phone's own view** in `guide/phone.md`. |
@@ -2283,6 +2349,7 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
2283
2349
  | `phone` | The phone as the platform sees it -- the device, whether it can take a call, its own mute where observed -- from a provider whose manifest declares `phoneStatus`; required there (`snapshot.phone.required`) and forbidden otherwise (`snapshot.phone.unexpected`). Replaced whole by `phone-updated`. See **The phone's own view** in `guide/phone.md`. |
2284
2350
  | `nextCall` | The agent's standing ask for the next call, while it stands: `since`. It needs an assignment at work to be next after (`snapshot.nextCall.idle`) and the provider clears it when that assignment ends, when a call is lined up, and when the agent withdraws it. Replaced whole by `next-call`. See **The agent's own queue** in `guide/queue.md`. |
2285
2351
  | `linedUp` | The call waiting in the agent's own queue, while one is: the caller as shown to this audience, the queue, the caller's wait, when it was lined up, and `release` where the provider lets the agent let it go. Not a task yet. Replaced whole by `lined-up`. See **The agent's own queue** in `guide/queue.md`. |
2352
+ | `labels` | The signed-in customer's own words for phases, history steps and kinds of work, per language, as its admin set them in the provider. Replaced whole by `labels-updated`. Omitted where the customer set none. See **Presentation labels**. |
2286
2353
  | `shift` | The agent's own day so far, as the provider counts it -- the same `Shift` their lead sees on the team member list, the same numbers from the same count. Replaced whole by `shift-updated`. Omitted only where the provider cannot say. See **The agent's day is on the wire**. |
2287
2354
  | `team` | Required `TeamMembers` when the login declares `capabilities.lead` and the lead has the team feature on, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission, and a lead who turned the feature off gets nothing of the team. |
2288
2355
 
@@ -2326,7 +2393,7 @@ Returns the provider's complete authoritative state at one point in time.
2326
2393
  snapshot is read is held until it lands, and what happens to it then depends on what it is. A
2327
2394
  snapshot restates state, so an event of a state-replacing kind -- `task-offered`, `task-updated`,
2328
2395
  `task-ended`, `task-audio-started`, `task-audio-ended`, `break-state`, `team-updated`,
2329
- `team-member-updated`, `team-member-removed`, `team-policies-updated`, `shift-updated`,
2396
+ `team-member-updated`, `team-member-removed`, `team-policies-updated`, `shift-updated`, `labels-updated`,
2330
2397
  `contacts-updated`, `calendar-updated`, `transport-status`, `snapshot` -- is dropped, because the
2331
2398
  snapshot accounts for it. An event that reports a transaction no snapshot carries --
2332
2399
  `dial-outcome`, `diagnostic`, `announcement`, `queue-summary` -- is applied after the snapshot,
@@ -2892,6 +2959,11 @@ ended, a break ended, a sign-out -- and never to say nothing changed. It carries
2892
2959
  the agent's lead sees for them on the team member list, so the two screens show one day. See
2893
2960
  **The agent's day is on the wire**.
2894
2961
 
2962
+ ### `labels-updated`
2963
+
2964
+ The customer's words, whole, replacing `Snapshot.labels`: emitted when its admin changes them, and
2965
+ carrying `{}` when the admin has cleared them all. See **Presentation labels**.
2966
+
2895
2967
  ## Utilities
2896
2968
 
2897
2969
  ### `assignmentKey(providerId, assignmentId)`
@@ -2939,6 +3011,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
2939
3011
  | `validateHostReport(report)` | The agent application's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever agent application a test hands the adapter; `stillHost(report)` builds one that never changes. |
2940
3012
  | `validateNextCall(value)` | The agent's standing ask for the next call, on its own as `next-call` and `Snapshot.nextCall` carry it: since when, and nothing else (`nextCall.since`, `nextCall.field`). |
2941
3013
  | `validateLinedUp(value, path?, levels?)` | A call lined up in the agent's own queue, on its own as `lined-up` and `Snapshot.linedUp` carry it: the caller under the same contact rules as a task's party, the queue's label, the waits, and `release` by presence (`linedUp.*`). |
3014
+ | `validateLabels(value, path?)` | The customer's words on their own, as `labels-updated` and `Snapshot.labels` carry them: renames of the protocol's phases and steps only, and every word one text per well-formed language tag (`labels.*`). |
2942
3015
  | `validatePhoneState(value, path?, context?)` | The phone as the platform sees it, on its own as `phone-updated` and `Snapshot.phone` carry it: its status, its one active channel, its mute where a hardphone's is observed (`phone.*`). |
2943
3016
  | `validateHostMute(mute, softphone)` | What the agent application's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the agent application holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
2944
3017
  | `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
@@ -3167,6 +3240,6 @@ Adapters should run the relevant scenarios against deterministic test state befo
3167
3240
 
3168
3241
  How a deployment themes Omni is the agent application's concern and is specified with the agent application, not here. What
3169
3242
  belongs in this contract is the boundary. A provider says what a control **is** through its
3170
- capabilities and what its work is **called** through `phaseLabels` and `taskTypePresentation`; how
3243
+ capabilities and what its work is **called** through the customer's `labels`; how
3171
3244
  any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
3172
3245
  override the agent's theme and font preferences.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.120",
3
+ "version": "0.1.122",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",