@xema/omni-protocol 0.1.68 → 0.1.69

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
@@ -1507,10 +1507,24 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
1507
1507
  }
1508
1508
  export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.task-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed"];
1509
1509
  export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
1510
+ /**
1511
+ * What the host refused, told to the adapter that published it: a snapshot the host would not
1512
+ * replace its state with, or an event it dropped, and every rule it broke. A refusal the provider
1513
+ * never hears of is a desk frozen at its last good state for a shift with nothing wrong on the
1514
+ * provider's side; this is how both sides see it.
1515
+ */
1516
+ export interface Refusal {
1517
+ artefact: "snapshot" | "event";
1518
+ /** The envelope refused, where the artefact is an event. */
1519
+ envelopeId?: string;
1520
+ violations: ProtocolViolation[];
1521
+ }
1510
1522
  export interface Connection<C extends Channel = Channel> {
1511
1523
  snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
1512
1524
  /** Delivery order must match the order the provider observes changes. Never replay. */
1513
1525
  subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
1526
+ /** Told what the host refused, snapshot or event, with every rule broken. Always; a refusal is visible on both sides. */
1527
+ refused(report: Refusal): void;
1514
1528
  /** Nothing may be allocated until a capacity is stated, so every connection receives it. */
1515
1529
  setCapacity(capacity: AgentCapacity): Promise<CapacityResult>;
1516
1530
  execute(request: TaskCommandRequest<C>): Promise<TaskCommandResult>;
package/dist/testing.js CHANGED
@@ -292,6 +292,19 @@ export async function exerciseAdapter(adapter, context, options = {}) {
292
292
  const connected = { ...context, host, store: watched.store };
293
293
  connection = await adapter.connect(connected);
294
294
  const live = connection;
295
+ // A refusal is visible on both sides: what the host would not take, the adapter is told, with the rules.
296
+ requireMethod(live, "refused", "every connection is told what the host refused");
297
+ const tellRefused = (report) => {
298
+ if (typeof live.refused !== "function")
299
+ return;
300
+ ruleEvaluated("connection.refused.rejected");
301
+ try {
302
+ live.refused(report);
303
+ }
304
+ catch (error) {
305
+ violations.push({ rule: "connection.refused.rejected", path: "connection.refused", message: `told of a refusal, the adapter threw: ${String(error)}` });
306
+ }
307
+ };
295
308
  // Dial is declared by presence: the capability object carries a destination policy rather
296
309
  // than an `enabled` flag, so its presence is the declaration.
297
310
  if (adapter.manifest.idleCapabilities?.dial !== undefined)
@@ -310,7 +323,10 @@ export async function exerciseAdapter(adapter, context, options = {}) {
310
323
  const waiters = new Set();
311
324
  unsubscribe = connection.subscribe(envelope => {
312
325
  observeEvent(envelope, seen);
313
- violations.push(...validateEventEnvelope(envelope, adapter.manifest, "event", reader()));
326
+ const broken = validateEventEnvelope(envelope, adapter.manifest, "event", reader());
327
+ violations.push(...broken);
328
+ if (broken.length > 0)
329
+ tellRefused({ artefact: "event", envelopeId: typeof envelope?.id === "string" ? envelope.id : undefined, violations: broken });
314
330
  // A diagnostic is informational to a host and a failure to a conformance run: the platform
315
331
  // under test broke a rule the adapter relies on, and a green result must not paper over it.
316
332
  // A reload in life has no first connection: the page is gone. The drive keeps its first
@@ -361,7 +377,12 @@ export async function exerciseAdapter(adapter, context, options = {}) {
361
377
  });
362
378
  const snapshot = await connection.snapshot();
363
379
  observeSnapshot(snapshot, seen);
364
- violations.push(...validateSnapshot(snapshot, adapter.manifest, "snapshot", reader()));
380
+ {
381
+ const broken = validateSnapshot(snapshot, adapter.manifest, "snapshot", reader());
382
+ violations.push(...broken);
383
+ if (broken.length > 0)
384
+ tellRefused({ artefact: "snapshot", violations: broken });
385
+ }
365
386
  violations.push(...undeterminedTasks(Array.isArray(snapshot?.tasks) ? snapshot.tasks : [], "snapshot.tasks"));
366
387
  stream.seed(snapshot);
367
388
  breaks.seed(snapshot);
package/guide.md CHANGED
@@ -1066,9 +1066,16 @@ adapter compiled against the browser lib.
1066
1066
  ### Adapter and connection
1067
1067
 
1068
1068
  ```ts
1069
+ type Refusal = {
1070
+ artefact: "snapshot" | "event";
1071
+ envelopeId?: string;
1072
+ violations: ProtocolViolation[];
1073
+ };
1074
+
1069
1075
  type Connection<C extends Channel = Channel> = {
1070
1076
  snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
1071
1077
  subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
1078
+ refused(report: Refusal): void;
1072
1079
  setCapacity(capacity: AgentCapacity): Promise<CapacityResult>;
1073
1080
  execute(request: TaskCommandRequest<C>): Promise<TaskCommandResult>;
1074
1081
  disconnect(): Promise<void>;
@@ -2028,6 +2035,7 @@ surface in one place, and what obliges an adapter to implement each one.
2028
2035
  | `snapshot()` | Always. |
2029
2036
  | `subscribe(listener)` | Always. |
2030
2037
  | `disconnect()` | Always. |
2038
+ | `refused(report)` | Always. The host tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the host does with what it refuses**. |
2031
2039
  | `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
2032
2040
  | `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
2033
2041
  | `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. Each `User` carries its `timeZone` where the provider knows it. |
@@ -2358,9 +2366,10 @@ Reaching the instant is not itself a transition: the provider reports what it di
2358
2366
 
2359
2367
  **Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
2360
2368
  spreading the `preview` one carries `previewEndsAt` and `atDeadline` with it, and the host refuses
2361
- the update (`task.preview.deadline.unexpected`) while the provider's own state looks right: the
2362
- desk hears nothing and the provider sees nothing wrong. The task past preview has no deadline to
2363
- wait for, so it carries neither field.
2369
+ the update (`task.preview.deadline.unexpected`): the desk keeps the task as it last stood, and the
2370
+ provider is told through `refused` exactly which rule, so the state that looked right on its side
2371
+ is named on its side. The task past preview has no deadline to wait for, so it carries neither
2372
+ field.
2364
2373
 
2365
2374
  **A task is never its audio.** A voice task is the allocation: the call is offered when it is
2366
2375
  routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
@@ -4605,6 +4614,25 @@ and may be compiled against a different protocol version, so its output is untru
4605
4614
  Validating a snapshot before it replaces provider state is what stops a malformed task from
4606
4615
  reaching the workspace.
4607
4616
 
4617
+ ### What the host does with what it refuses
4618
+
4619
+ A refusal has an aftermath on the desk and a report to the provider, and both are stated.
4620
+
4621
+ - **A refused snapshot replaces nothing.** The host keeps the last state it took from this
4622
+ provider whole, and shows the provider as faulted -- the transport as it stands, and the words
4623
+ "last update refused" with the rule -- so the agent knows the view is standing still rather than
4624
+ believing it current. It does not adopt the good half of a snapshot: a task the agent is on would
4625
+ vanish from the desk while the call is up.
4626
+ - **A refused event is dropped and counted.** The state the host holds does not move for it. The
4627
+ provider is told, and may republish a corrected state or raise a `diagnostic`; the host retries
4628
+ nothing.
4629
+ - **The provider is told, every time.** `refused(report)` carries the artefact, the envelope id for
4630
+ an event, and every violation with its rule and path. An adapter logs it at error and treats it as
4631
+ its own defect until shown otherwise: what the host refused never reached the agent, and a
4632
+ provider that hears nothing runs a whole shift beside a frozen desk with nothing wrong on its
4633
+ side. The harness tells an adapter under test exactly as a host does
4634
+ (`connection.refused.required`, `connection.refused.rejected`).
4635
+
4608
4636
  ## Conformance helpers
4609
4637
 
4610
4638
  ### `exerciseAdapter(adapter, context, options?)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",