@xema/omni-protocol 0.1.61 → 0.1.62

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/index.d.ts CHANGED
@@ -172,6 +172,17 @@ export interface SecretStore {
172
172
  set(key: string, value: string): Promise<void>;
173
173
  delete(key: string): Promise<void>;
174
174
  }
175
+ /**
176
+ * A small store for one login's operational state -- what an adapter holds about open work that
177
+ * its platform cannot hold for it, such as the handling legs a host reported. Kept by the host for
178
+ * the life of the login, across a reload of the host, and cleared at sign-out. Never for anything
179
+ * sensitive: that is `SecretStore`, whose contract is that a host may clear it aggressively.
180
+ */
181
+ export interface LoginStore {
182
+ get(key: string): Promise<string | undefined>;
183
+ set(key: string, value: string): Promise<void>;
184
+ delete(key: string): Promise<void>;
185
+ }
175
186
  export interface AuthenticationContext {
176
187
  protocolVersion: number;
177
188
  /** Omni's identity for this login. The same value arrives later as `ConnectContext.loginId`. */
@@ -427,7 +438,7 @@ export interface Host {
427
438
  report(): HostReport;
428
439
  subscribe(listener: (report: HostReport) => void): Unsubscribe;
429
440
  }
430
- export interface ConnectContext {
441
+ export type ConnectContext = {
431
442
  protocolVersion: number;
432
443
  /** The session that authenticated this connection. */
433
444
  loginId: string;
@@ -439,16 +450,35 @@ export interface ConnectContext {
439
450
  * identity; a roaming agent corrects it by signing in.
440
451
  */
441
452
  timeZone: TimeZone;
442
- /** The same value passed as `AuthenticationContext.phone`: how this login hears its calls. */
453
+ /** Where the adapter keeps this login's operational state across a reload of the host. */
454
+ store: LoginStore;
455
+ /** The same value passed as `AuthenticationContext.phone`: how this login hears its calls. Narrowed by the arms below. */
443
456
  phone?: Phone;
444
457
  /**
445
458
  * The host's report of the agent's station, to consult before declaring the agent ready to the
446
- * platform and whenever it changes. Omni reports; the adapter decides.
459
+ * platform and whenever it changes. Omni reports; the adapter decides. What it states about its
460
+ * Mute is tied to the phone by the arms below.
447
461
  */
448
462
  host: Host;
449
463
  signal?: AbortSignal;
450
464
  log?: (entry: unknown) => void;
451
- }
465
+ } & (
466
+ /**
467
+ * On a softphone the host holds the microphone and states what its Mute does; on a desk phone,
468
+ * or off voice, it holds none and states nothing. The arms make the omission a compile error
469
+ * where a live seat would otherwise be the first to find it.
470
+ */
471
+ {
472
+ phone: "softphone";
473
+ host: {
474
+ mute: HostMute;
475
+ };
476
+ } | {
477
+ phone?: "deskPhone";
478
+ host: {
479
+ mute?: never;
480
+ };
481
+ });
452
482
  export type TransportStatus = "connecting" | "active" | "error";
453
483
  /**
454
484
  * What revives a connection that reported `error`: `reconnect` -- the login is good, dispose this
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostMute, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
1
+ import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostMute, type LoginStore, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
2
2
  import { type ProtocolViolation } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
4
4
  /**
@@ -126,6 +126,10 @@ export declare class TaskStream {
126
126
  /** The host placed a dial: its outcome, whenever it arrives, is one the stream expects. */
127
127
  dialled(dialId: string): void;
128
128
  private noteDials;
129
+ /** The entries of a task's record, each by step and instant, or undefined where the task carries no record. */
130
+ private static record;
131
+ /** What a restated record lost of the one read before it: nothing, or the entries by step and instant. */
132
+ private static lost;
129
133
  private static stated;
130
134
  /** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
131
135
  seed(snapshot: unknown): void;
@@ -153,11 +157,18 @@ export declare function assertBreakFollowsItsRequests(envelopes: readonly Provid
153
157
  * follows the media ending is `completing` or `task-ended`.
154
158
  */
155
159
  export declare function assertMediaFollowsTheTask(envelopes: readonly ProviderEventEnvelope[], snapshot?: Snapshot): void;
160
+ /** A login store that lives in memory for the test: what most adapter tests hand `exerciseAdapter` as `context.store`. */
161
+ export declare function memoryStore(): LoginStore;
156
162
  /**
157
163
  * A host that reports one thing and never changes: what most adapter tests hand `exerciseAdapter`.
158
164
  * A softphone host states what its Mute does; a desk-phone or conversation host has no microphone and states nothing.
159
165
  */
160
- export declare function stillHost(report?: HostReport, guarantees?: HostGuarantees, mute?: HostMute): Host;
166
+ export declare function stillHost(report: HostReport, guarantees: HostGuarantees, mute: HostMute): Host & {
167
+ mute: HostMute;
168
+ };
169
+ export declare function stillHost(report?: HostReport, guarantees?: HostGuarantees): Host & {
170
+ mute?: never;
171
+ };
161
172
  /** One provider as the host sees it when freezing the providers a break attempt asks. */
162
173
  export interface BreakCandidate {
163
174
  id: string;
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateHostMute, validateHandlingReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateHostMute, validateLoginStore, validateHandlingReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
4
4
  /**
5
5
  * A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
@@ -273,12 +273,16 @@ export async function exerciseAdapter(adapter, context, options = {}) {
273
273
  }
274
274
  // What the host's Mute does is stated where the host holds a microphone, and nowhere else.
275
275
  violations.push(...validateHostMute(context.host.mute, softphone, "context.host.mute"));
276
+ // The login's store is the host's to provide: an adapter that composes a record keeps in it what its platform cannot.
277
+ violations.push(...validateLoginStore(context.store, "context.store"));
276
278
  unsubscribeHost = context.host.subscribe(report => {
277
279
  violations.push(...validateHostReport(report, "context.host"));
278
280
  });
279
281
  let consulted = false;
282
+ // The wrapped host carries the mute kind the test's host stated, so the arms of ConnectContext still hold.
280
283
  const host = {
281
284
  guarantees: context.host.guarantees,
285
+ ...(context.host.mute === undefined ? {} : { mute: context.host.mute }),
282
286
  report: () => { consulted = true; return context.host.report(); },
283
287
  subscribe: listener => { consulted = true; return context.host.subscribe(listener); },
284
288
  };
@@ -737,6 +741,20 @@ export class TaskStream {
737
741
  this.dialled(entry.dialId);
738
742
  }
739
743
  }
744
+ /** The entries of a task's record, each by step and instant, or undefined where the task carries no record. */
745
+ static record(task) {
746
+ if (!isRecord(task) || !isRecord(task.handlingHistory) || !Array.isArray(task.handlingHistory.steps))
747
+ return undefined;
748
+ return new Set(task.handlingHistory.steps.filter(isRecord).map(entry => `${String(entry.step)}@${String(entry.at)}`));
749
+ }
750
+ /** What a restated record lost of the one read before it: nothing, or the entries by step and instant. */
751
+ static lost(was, now) {
752
+ if (was === undefined || was.size === 0)
753
+ return [];
754
+ if (now === undefined)
755
+ return [...was];
756
+ return [...was].filter(key => !now.has(key));
757
+ }
740
758
  static stated(task) {
741
759
  const media = isRecord(task) && (task.media === "started" || task.media === "ended") ? task.media : "none";
742
760
  // The stage of every dialled entry the room names by its dial, so an update can be held to the
@@ -748,7 +766,7 @@ export class TaskStream {
748
766
  stages.set(entry.dialId, entry.stage);
749
767
  }
750
768
  }
751
- return { phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages };
769
+ return { phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages, record: TaskStream.record(task) };
752
770
  }
753
771
  /** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
754
772
  seed(snapshot) {
@@ -782,6 +800,11 @@ export class TaskStream {
782
800
  if (was !== undefined && (was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
783
801
  refuse("stream.snapshot.capabilitySource", `${at}.snapshot.tasks[${index}].capabilitySource`, `${task.id} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
784
802
  }
803
+ // A record once read is not unread: a resync restates it whole, or with more, never with less.
804
+ const lost = TaskStream.lost(was?.record, TaskStream.record(task));
805
+ if (lost.length > 0) {
806
+ refuse("stream.snapshot.handlingHistory", `${at}.snapshot.tasks[${index}].handlingHistory`, `${task.id}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
807
+ }
785
808
  });
786
809
  }
787
810
  this.seed(event.snapshot);
@@ -807,6 +830,13 @@ export class TaskStream {
807
830
  if ((known.source === "queue" || known.source === "ungoverned") && isRecord(event.task) && event.task.capabilitySource === "undetermined") {
808
831
  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`);
809
832
  }
833
+ // A record once read is not unread: an update restates it whole, or with more, never with less.
834
+ {
835
+ const lost = TaskStream.lost(known.record, TaskStream.record(event.task));
836
+ if (lost.length > 0) {
837
+ refuse("stream.taskUpdated.handlingHistory", `${at}.task.handlingHistory`, `${id}'s record lost ${lost.join(", ")} on the update: an entry read by the host stays in the record until the task ends`);
838
+ }
839
+ }
810
840
  if (known.media === "ended") {
811
841
  const phase = isRecord(event.task) ? String(event.task.phase) : "";
812
842
  if (phase !== "completing") {
@@ -1225,10 +1255,15 @@ export function assertMediaFollowsTheTask(envelopes, snapshot) {
1225
1255
  envelopes.forEach((envelope, index) => found.push(...stream.apply(envelope, `envelopes[${index}]`)));
1226
1256
  assertNoViolations(found, "The media follows the task");
1227
1257
  }
1228
- /**
1229
- * A host that reports one thing and never changes: what most adapter tests hand `exerciseAdapter`.
1230
- * A softphone host states what its Mute does; a desk-phone or conversation host has no microphone and states nothing.
1231
- */
1258
+ /** A login store that lives in memory for the test: what most adapter tests hand `exerciseAdapter` as `context.store`. */
1259
+ export function memoryStore() {
1260
+ const kept = new Map();
1261
+ return {
1262
+ get: async (key) => kept.get(key),
1263
+ set: async (key, value) => { kept.set(key, value); },
1264
+ delete: async (key) => { kept.delete(key); },
1265
+ };
1266
+ }
1232
1267
  export function stillHost(report = { online: true }, guarantees = {}, mute) {
1233
1268
  return { guarantees, ...(mute === undefined ? {} : { mute }), report: () => report, subscribe: () => () => undefined };
1234
1269
  }
@@ -56,6 +56,8 @@ export declare function validateEventEnvelope(envelope: unknown, manifest: unkno
56
56
  * phone the microphone is the phone's, and off voice there is none. Neither the value nor its
57
57
  * absence is inferred from what kind of application the host is.
58
58
  */
59
+ /** The login's store, which the host provides on every connection: three functions, and nothing else is a store. */
60
+ export declare function validateLoginStore(store: unknown, path?: string): ProtocolViolation[];
59
61
  export declare function validateHostMute(mute: unknown, softphone: boolean, path?: string): ProtocolViolation[];
60
62
  /**
61
63
  * What a host promises. Presence is the guarantee, so a key declared `false` is refused: a
@@ -752,8 +752,12 @@ function validateHandlingHistory(value, path, into) {
752
752
  into.require(isDurationSeconds(entry.seconds) && entry.seconds > 0, "task.handlingHistory.seconds", `${at}.seconds`, "seconds must be a positive whole number; omit it while the step is still running");
753
753
  }
754
754
  // A muted entry carries whose the silence was, as the host reported it; no other step has it.
755
- if (entry.step === "muted")
755
+ // And it names the agent: the host has exactly one, the provider knows who, so an unattributed
756
+ // muted leg is a record that dropped a fact it held rather than one it could not establish.
757
+ if (entry.step === "muted") {
756
758
  into.oneOf(entry.mutedBy, MUTED_BY, "task.handlingHistory.mutedBy", `${at}.mutedBy`);
759
+ into.require(entry.by !== undefined, "task.handlingHistory.muted.by", `${at}.by`, "a muted leg is attributed to the login's agent: the host reported it, and the provider knows who the host's agent is");
760
+ }
757
761
  else if (HANDLING_STEPS.includes(entry.step)) {
758
762
  into.require(entry.mutedBy === undefined, "task.handlingHistory.mutedBy.unexpected", `${at}.mutedBy`, "only a muted step says who silenced the microphone");
759
763
  }
@@ -1639,6 +1643,18 @@ const HOST_MUTES = membersOf({ stream: true, station: true });
1639
1643
  * phone the microphone is the phone's, and off voice there is none. Neither the value nor its
1640
1644
  * absence is inferred from what kind of application the host is.
1641
1645
  */
1646
+ /** The login's store, which the host provides on every connection: three functions, and nothing else is a store. */
1647
+ export function validateLoginStore(store, path = "store") {
1648
+ const into = new Collector();
1649
+ if (!isPlainObject(store)) {
1650
+ into.add("store.shape", path, "a connection carries the login's store: an object with get, set and delete");
1651
+ return into.violations;
1652
+ }
1653
+ for (const method of ["get", "set", "delete"]) {
1654
+ into.require(typeof store[method] === "function", `store.${method}`, `${path}.${method}`, `a login store ${method}s by key`);
1655
+ }
1656
+ return into.violations;
1657
+ }
1642
1658
  export function validateHostMute(mute, softphone, path = "host.mute") {
1643
1659
  const into = new Collector();
1644
1660
  if (mute === undefined) {
package/guide.md CHANGED
@@ -305,16 +305,26 @@ type Host = {
305
305
  subscribe(listener: (report: HostReport) => void): Unsubscribe;
306
306
  };
307
307
 
308
+ type LoginStore = {
309
+ get(key: string): Promise<string | undefined>;
310
+ set(key: string, value: string): Promise<void>;
311
+ delete(key: string): Promise<void>;
312
+ };
313
+
308
314
  type ConnectContext = {
309
315
  protocolVersion: number;
310
316
  loginId: string;
311
317
  autoAcceptTasks?: boolean;
312
318
  timeZone: TimeZone;
319
+ store: LoginStore;
313
320
  phone?: Phone;
314
321
  host: Host;
315
322
  signal?: AbortSignal;
316
323
  log?: (entry: unknown) => void;
317
- };
324
+ } & (
325
+ | { phone: "softphone"; host: { mute: HostMute } }
326
+ | { phone?: "deskPhone"; host: { mute?: never } }
327
+ );
318
328
 
319
329
  type TransportStatus = "connecting" | "active" | "error";
320
330
 
@@ -1388,6 +1398,16 @@ replaces its local provider view; it does not overwrite the provider.
1388
1398
  Every member of a closed set — break kinds, handling steps, destination kinds — must have a
1389
1399
  normative definition; matching names alone do not establish shared meaning.
1390
1400
 
1401
+ ### 10. Order on the wire is display order
1402
+
1403
+ A list the provider publishes — a destination directory, disposition codes, a task's custom
1404
+ controls, its browsers — is in the order the provider wants it shown, and a host keeps that order.
1405
+ A reader will find meaning in position unless something says there is none, so the provider
1406
+ decides: where it has an intention, an administrator's sequence, it lists in that order; where it
1407
+ has none, it orders by the label, so the order is visibly arbitrary and does not move under the
1408
+ reader. A directory that reorders itself as colleagues become free is a list lying about what it
1409
+ means. A record's steps are the exception with a stated order of their own, oldest first.
1410
+
1391
1411
  ## Provider adapter requirements
1392
1412
 
1393
1413
  ### 1. One adapter is one provider
@@ -1925,7 +1945,8 @@ Creates one live provider connection for the signed-in agent.
1925
1945
  | `loginId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.loginId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
1926
1946
  | `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login. Treated as `true` when omitted. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
1927
1947
  | `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
1928
- | `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. |
1948
+ | `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's host states what its Mute does, and a desk-phone or conversation login's host cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the host's**. |
1949
+ | `store` | The login's operational store, kept by the host for the life of the login, across a reload of the host, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the handling legs a host reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store a host may clear aggressively. The harness requires it of every connection (`store.shape`, `store.get`, `.set`, `.delete`). |
1929
1950
  | `host` | The host's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The host reports, the adapter decides**. |
1930
1951
  | `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
1931
1952
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
@@ -2471,7 +2492,24 @@ entries, one per join. Order is enforced: an entry earlier than the one before i
2471
2492
  total the provider adds: *time to offer*, from the call's arrival to the agent's screen lighting
2472
2493
  up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
2473
2494
  `queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
2474
- that interval. **Handle time is anchored, not restarted.** It runs from the
2495
+ that interval.
2496
+
2497
+ **A record once read is not unread.** Every restatement of a task's record -- on `task-updated`,
2498
+ on a resync snapshot -- carries every entry the host has already read, by step and instant, and
2499
+ may add to them; it never has fewer, and never drops the record while the task is open. A fact
2500
+ stated to the host and then withdrawn without an event is a record contradicting itself, exactly
2501
+ as a task that lost the terms it had read would be, and the stream refuses it the same way
2502
+ (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`). A provider whose
2503
+ platform cannot hold a leg the host reported keeps it in the login's `store` for the life of the
2504
+ task, so a reload of the host restates the same record; the entry is keyed by `step` and `at`, so
2505
+ a running hold restated with its final `seconds` is the same entry.
2506
+
2507
+ **The record is ordered by the instants stated in it, whoever stamped them.** The host stamps the
2508
+ legs it performs from the same clock it reports everything else with, and the provider writes a
2509
+ host leg into the record with the host's `at`, in its place among the others by that instant, and
2510
+ answers `recorded`: it never refuses a leg, or the task, for a timestamp it did not write. A host
2511
+ whose clock runs ahead of the platform's puts its own mute before the platform's answer, and the
2512
+ record shows what was stated; that is the host's clock to fix, not the provider's record to edit. **Handle time is anchored, not restarted.** It runs from the
2475
2513
  `answered` step's `at` — from the task's first `in-progress` where the provider reports no
2476
2514
  history — until the task's media ends, and a hold neither pauses nor resets it: the hold's own
2477
2515
  duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
@@ -2748,7 +2786,8 @@ republish as any other permission that changed while the task was open. The move
2748
2786
  Terms once read stay read: a re-read that fails mid-task is not a new fact about the task, so the
2749
2787
  last statement stands and the failure is a `diagnostic`, and a task that was published under
2750
2788
  `queue` or `ungoverned` never returns to `undetermined`, on an update (`stream.taskUpdated.capabilitySource`)
2751
- or on a resync snapshot (`stream.snapshot.capabilitySource`).
2789
+ or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
2790
+ entry the same two ways (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`).
2752
2791
 
2753
2792
  What the agent is told differs by source, and only one source tells them anything. Under `queue`
2754
2793
  and `ungoverned` the agent sees controls and nothing about where they came from: both are facts,
@@ -4036,14 +4075,19 @@ the endpoint and can clear an operating-system mute. A hardware slider is nobody
4036
4075
  heard, and the record exists so that period is not a hole. The host reports every such period
4037
4076
  through `recordStep` -- its own Mute, and a station mute it observed during a call -- with
4038
4077
  `mutedBy` saying whose the silence was, and the provider writes the word into the entry
4039
- (`task.handlingHistory.mutedBy`). A supervisor reading the record then sees "the agent muted for
4078
+ (`task.handlingHistory.mutedBy`). The report names no agent because the host has exactly one, and
4079
+ the provider knows who that is: it attributes the leg to the login's agent in `by`, a fact it
4080
+ holds and not an inference, for a station mute as much as for the host's own. An unattributed
4081
+ `muted` entry is refused (`task.handlingHistory.muted.by`); a shared handset's unattributed hold
4082
+ is a different claim, and stands. A supervisor reading the record then sees "the agent muted for
4040
4083
  forty seconds" and "the agent's headset was muted for forty seconds" as the different things they
4041
4084
  are. See **The host records what it performs**.
4042
4085
 
4043
4086
  **Mute has a lifecycle, and none of it is inferred.** A call that starts while the station is
4044
4087
  already muted begins a `muted` leg the moment its media starts, `mutedBy: "station"`. Media that
4045
4088
  ends while any leg is open ends the leg at that instant, as every host-performed leg ends. And the
4046
- host's own Mute starts off on every call: an agent is never muted by the call before.
4089
+ host's own Mute starts off on every call, which is every `task-media-started` -- a connect-back
4090
+ opens a second call on the same task with no offer -- so an agent is never muted by the call before.
4047
4091
 
4048
4092
  **Every press on a headset is the agent's own press**, performed the host's way, and the lights
4049
4093
  follow the host's state. The `personConsent` guarantee is honoured by a hook-switch press exactly
@@ -4469,6 +4513,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
4469
4513
  | `validateHandlingReport(report, path?, manifest?)` | What the host reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
4470
4514
  | `validateHostReport(report)` | The host'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 host a test hands the adapter; `stillHost(report)` builds one that never changes. |
4471
4515
  | `validateHostMute(mute, softphone)` | What the host's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the host holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
4516
+ | `validateLoginStore(store)` | The login's store the host hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
4472
4517
  | `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
4473
4518
  | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.handling` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
4474
4519
  | `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.61",
3
+ "version": "0.1.62",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",