@xema/omni-protocol 0.1.24 → 0.1.26

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/README.md CHANGED
@@ -65,7 +65,7 @@ clean result is read for what it covers and not for the whole contract; `assertR
65
65
  subjects)` is the paired assertion.
66
66
 
67
67
  ```ts
68
- const context = { protocolVersion: OMNI_PROTOCOL_VERSION, sessionId: "session-1", host: stillHost(report) };
68
+ const context = { protocolVersion: OMNI_PROTOCOL_VERSION, loginId: "session-1", host: stillHost(report) };
69
69
  const result = await exerciseAdapter(adapter, context, { collectOnly: true });
70
70
  expect(result.violations).toEqual([]);
71
71
  expect(result.disconnectWasClean).toBe(true);
package/dist/index.d.ts CHANGED
@@ -54,19 +54,19 @@ export interface ProtocolViolation {
54
54
  message: string;
55
55
  }
56
56
  export type AuthenticationMethod = "browser-sso" | "credentials";
57
- export interface BrowserAccessPolicy {
57
+ export interface BrowserAccess {
58
58
  /** The decision when no list entry matches. */
59
59
  mode: "allow-all" | "block-all";
60
60
  allowList?: string[];
61
61
  blockList?: string[];
62
62
  }
63
63
  export interface PersonalBrowserCapability {
64
- access: BrowserAccessPolicy;
64
+ access: BrowserAccess;
65
65
  accessPolicyScope?: "initial-url" | "all-navigation";
66
66
  }
67
- export type DialDestinationPolicy = "contacts-only" | "any-number";
67
+ export type DialDestinations = "contacts-only" | "any-number";
68
68
  export interface DialCapability {
69
- destinationPolicy: DialDestinationPolicy;
69
+ destinations: DialDestinations;
70
70
  }
71
71
  /** Every idle capability a provider may declare. Only voice may `dial`; the channel arm says so. */
72
72
  export declare const IDLE_CAPABILITIES: readonly ["dial", "personalBrowser", "calendar", "contacts"];
@@ -111,8 +111,8 @@ export interface Manifest<C extends Channel = Channel> {
111
111
  phaseLabels?: TaskPhaseLabels;
112
112
  /** Keyed by `taskType`. An entry replaces the channel default outright rather than merging. */
113
113
  taskTypePresentation?: Record<string, TaskTypePresentation>;
114
- /** The organisation's whole ladder, stated outright, `person` included. Omitted for the typical four, `DEFAULT_TIERS`. */
115
- orgTiers?: TierDeclaration[];
114
+ /** The organisation's whole ladder, stated outright, `person` included. Omitted for the typical four, `DEFAULT_LEVELS`. */
115
+ orgLevels?: LevelDeclaration[];
116
116
  }
117
117
  export interface SecretStore {
118
118
  get(key: string): Promise<string | undefined>;
@@ -121,8 +121,8 @@ export interface SecretStore {
121
121
  }
122
122
  export interface AuthenticationContext {
123
123
  protocolVersion: number;
124
- /** Omni's identity for this login. The same value arrives later as `ConnectContext.sessionId`. */
125
- sessionId: string;
124
+ /** Omni's identity for this login. The same value arrives later as `ConnectContext.loginId`. */
125
+ loginId: string;
126
126
  /** Scoped to this provider's manifest id. */
127
127
  secrets: SecretStore;
128
128
  signal?: AbortSignal;
@@ -155,7 +155,7 @@ export interface TeamCapabilities {
155
155
  * of who the agent is on this provider: the provider knows the roles and says so at sign-in.
156
156
  * Current as of the latest `authenticated` state, never fixed for the login.
157
157
  */
158
- export interface SessionCapabilities {
158
+ export interface UserCapabilities {
159
159
  /** This login may request a break. Requires the four break methods. */
160
160
  breaks?: true;
161
161
  /** The choices the team left to this person, with where each stands. Omitted when there are none. Requires `setPreference`. */
@@ -175,12 +175,12 @@ export type AuthenticationState = {
175
175
  } | {
176
176
  status: "authenticated";
177
177
  identity: User;
178
- capabilities: SessionCapabilities;
178
+ capabilities: UserCapabilities;
179
179
  expiresAt?: IsoTimestamp;
180
180
  } | {
181
181
  status: "refreshing";
182
182
  identity: User;
183
- capabilities: SessionCapabilities;
183
+ capabilities: UserCapabilities;
184
184
  } | {
185
185
  status: "expired";
186
186
  identity?: User;
@@ -230,7 +230,7 @@ export type CompleteAuthenticationRequest = {
230
230
  export type CompleteAuthenticationResult = {
231
231
  status: "authenticated";
232
232
  identity: User;
233
- capabilities: SessionCapabilities;
233
+ capabilities: UserCapabilities;
234
234
  expiresAt?: IsoTimestamp;
235
235
  } | {
236
236
  status: "rejected";
@@ -295,10 +295,6 @@ export type HostAudioOutput = {
295
295
  /** What Omni's own chrome shows of a task browser's URL: nothing, the domain, or all of it. */
296
296
  export type UrlVisibility = "full" | "domain" | "hidden";
297
297
  export interface HostReport {
298
- /** What the agent can see of a task browser's URL in Omni's chrome. Task browsers only; the personal browser is the agent's. */
299
- browsers: {
300
- urlVisibility: UrlVisibility;
301
- };
302
298
  /** Whether the host has a network interface up. Not a claim that anything is reachable: the adapter knows its own platform's reachability better than the host does. */
303
299
  online: boolean;
304
300
  audio?: {
@@ -314,7 +310,7 @@ export interface Host {
314
310
  export interface ConnectContext {
315
311
  protocolVersion: number;
316
312
  /** The session that authenticated this connection. */
317
- sessionId: string;
313
+ loginId: string;
318
314
  /** Omni-side policy: whether the agent's tasks are accepted without asking them. */
319
315
  autoAcceptTasks?: boolean;
320
316
  /**
@@ -325,13 +321,13 @@ export interface ConnectContext {
325
321
  signal?: AbortSignal;
326
322
  log?: (entry: unknown) => void;
327
323
  }
328
- export type ConnectionStatus = "connecting" | "active" | "error";
324
+ export type TransportStatus = "connecting" | "active" | "error";
329
325
  /**
330
326
  * What revives a connection that reported `error`: `reconnect` -- the login is good, dispose this
331
327
  * connection and call `connect()` again -- or `reauthenticate` -- run the authentication flow
332
328
  * first. The adapter knows which; the host acts on its word.
333
329
  */
334
- export type ConnectionRecovery = "reconnect" | "reauthenticate";
330
+ export type TransportRecovery = "reconnect" | "reauthenticate";
335
331
  /** Every field is optional: a provider sends what it knows and omits what it does not. */
336
332
  export interface Contact {
337
333
  name?: string;
@@ -354,7 +350,7 @@ export interface DispositionCode {
354
350
  label: string;
355
351
  group?: string;
356
352
  }
357
- export interface DispositionPolicy {
353
+ export interface DispositionRules {
358
354
  required?: boolean;
359
355
  notes?: "required" | "optional" | "hidden";
360
356
  codes?: DispositionCode[];
@@ -373,7 +369,7 @@ export interface DestinationDirectory {
373
369
  export interface CustomCapability {
374
370
  id: string;
375
371
  ui: {
376
- kind: "button" | "toggle" | "menu-item";
372
+ control: "button" | "toggle" | "menu-item";
377
373
  label: string;
378
374
  placement: "primary" | "secondary" | "overflow";
379
375
  /** Where the control's work renders: inline in the workspace, or as a page of its own. Inline when absent. */
@@ -386,7 +382,7 @@ export interface CustomCapability {
386
382
  }
387
383
  export interface SharedTaskCapabilities {
388
384
  browsers?: true;
389
- dispositions?: true | DispositionPolicy;
385
+ dispositions?: true | DispositionRules;
390
386
  custom?: CustomCapability[];
391
387
  }
392
388
  /**
@@ -429,24 +425,32 @@ export declare const BROWSER_ISOLATION_SCHEMES: {
429
425
  readonly TASK_TYPE_NAME__TAB_NAME: "TaskTypeName.TabName";
430
426
  };
431
427
  export type BrowserIsolationScheme = (typeof BROWSER_ISOLATION_SCHEMES)[keyof typeof BROWSER_ISOLATION_SCHEMES];
432
- export interface TaskBrowserBase {
428
+ /** One tab, in either workspace. */
429
+ export interface Browser {
433
430
  id: string;
434
431
  name: string;
435
- purpose: string;
436
432
  /** `http:` or `https:` only. */
437
433
  url: string;
438
434
  }
439
435
  /**
440
- * The union is what makes a reusing browser with no scheme fail to compile rather than inherit
441
- * a default -- which is how two tasks end up sharing a session nobody intended.
436
+ * A tab the task brought: fixed at the task's definition -- count and details -- with why it is
437
+ * there, whether its session is shared across tasks, and whether the agent may read its URL. The
438
+ * union is what makes a reusing browser with no scheme fail to compile rather than inherit a
439
+ * default -- which is how two tasks end up sharing a session nobody intended.
442
440
  */
443
- export type TaskBrowser = TaskBrowserBase & ({
441
+ export type TaskBrowser = Browser & {
442
+ purpose: string;
443
+ /** Hide this tab's URL from the agent in Omni's chrome. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read. */
444
+ urlVisibility?: UrlVisibility;
445
+ } & ({
444
446
  reuse: false;
445
447
  isolationScheme?: never;
446
448
  } | {
447
449
  reuse: true;
448
450
  isolationScheme: BrowserIsolationScheme;
449
451
  });
452
+ /** A tab the agent opened in the personal workspace: theirs, as many as they like, and never on the wire. */
453
+ export type PersonalBrowser = Browser;
450
454
  export declare const ALLOWED_BROWSER_URL_SCHEMES: readonly ["http:", "https:"];
451
455
  export declare function isAllowedBrowserUrl(url: string): boolean;
452
456
  export type TaskPhase =
@@ -500,10 +504,10 @@ export interface TaskHandlingStep {
500
504
  */
501
505
  export type TaskCompletion = {
502
506
  completionMode: "agent-command";
503
- completionAllowance?: DurationSeconds;
507
+ wrapAllowance?: DurationSeconds;
504
508
  } | {
505
509
  completionMode: "provider-automatic";
506
- completionAllowance: DurationSeconds;
510
+ wrapAllowance: DurationSeconds;
507
511
  };
508
512
  /**
509
513
  * A consultation in progress on a task: who is being consulted, and since when where the provider
@@ -520,7 +524,7 @@ export interface TaskConsultation {
520
524
  * `requested` while nobody has joined; `joined`, with `leadId`, once somebody has.
521
525
  */
522
526
  export interface TaskLead {
523
- status: "requested" | "joined";
527
+ stage: "requested" | "joined";
524
528
  leadId?: UserId;
525
529
  note?: string;
526
530
  since: IsoTimestamp;
@@ -535,21 +539,21 @@ export interface TaskAssisting {
535
539
  since: IsoTimestamp;
536
540
  }
537
541
  /**
538
- * A tier of the organisation's structure, by the id its manifest declares -- or one of the four
539
- * a typical organisation has, `DEFAULT_TIERS`, when the manifest declares no ladder. The protocol
540
- * never describes the chain: which tiers a person passes through is the structure's to know.
542
+ * A level of the organisation's structure, by the id its manifest declares -- or one of the four
543
+ * a typical organisation has, `DEFAULT_LEVELS`, when the manifest declares no ladder. The protocol
544
+ * never describes the chain: which levels a person passes through is the structure's to know.
541
545
  */
542
- export type Tier = string;
543
- /** A tier the structure has, with the label a desk shows for "who decided". */
544
- export interface TierDeclaration {
545
- id: Tier;
546
+ export type Level = string;
547
+ /** A level the structure has, with the label a desk shows for "who decided". */
548
+ export interface LevelDeclaration {
549
+ id: Level;
546
550
  label: string;
547
551
  }
548
552
  /**
549
- * The tiers a typical organisation has: the ladder in force when a manifest declares no
550
- * `orgTiers`. A manifest that declares any states its whole ladder outright.
553
+ * The levels a typical organisation has: the ladder in force when a manifest declares no
554
+ * `orgLevels`. A manifest that declares any states its whole ladder outright.
551
555
  */
552
- export declare const DEFAULT_TIERS: readonly [{
556
+ export declare const DEFAULT_LEVELS: readonly [{
553
557
  readonly id: "org";
554
558
  readonly label: "Your organisation";
555
559
  }, {
@@ -563,14 +567,14 @@ export declare const DEFAULT_TIERS: readonly [{
563
567
  readonly label: "You";
564
568
  }];
565
569
  /** The ladder in force for a manifest: exactly what it declares, or the defaults when it declares none. */
566
- export declare function effectiveTiers(declared: readonly TierDeclaration[] | undefined): TierDeclaration[];
570
+ export declare function effectiveLevels(declared: readonly LevelDeclaration[] | undefined): LevelDeclaration[];
567
571
  /**
568
- * Something the queue could allow, locked above the person: the tier that made it unchangeable,
569
- * and why if they said. `person` never locks their own value, and the queue is not a tier --
572
+ * Something the queue could allow, locked above the person: the level that made it unchangeable,
573
+ * and why if they said. `person` never locks their own value, and the queue is not a level --
570
574
  * what the queue does not allow at all is simply absent.
571
575
  */
572
576
  export interface Locked {
573
- lockedBy: Tier;
577
+ lockedBy: Level;
574
578
  reason?: string;
575
579
  }
576
580
  /**
@@ -782,7 +786,7 @@ export interface BreakReason {
782
786
  label: string;
783
787
  group?: string;
784
788
  kind?: BreakKind;
785
- /** Survives `accepting: false`: a mandatory rest is not something a busy hour can cancel. */
789
+ /** Survives `mayAsk: false`: a mandatory rest is not something a busy hour can cancel. */
786
790
  alwaysAvailable?: true;
787
791
  }
788
792
  export interface BreakRequest {
@@ -809,8 +813,8 @@ export type ImposedBreak = {
809
813
  export interface BreakState {
810
814
  approval: BreakApproval;
811
815
  /** Whether the agent may ask at all. Distinct from the fate of a request already made. */
812
- accepting: boolean;
813
- /** Shown when `accepting` is false, such as "Busy hours". */
816
+ mayAsk: boolean;
817
+ /** Shown when `mayAsk` is false, such as "Busy hours". */
814
818
  refusedReason?: string;
815
819
  decisionReason?: string;
816
820
  retryAfterMs?: number;
@@ -883,7 +887,7 @@ export interface TeamRoster {
883
887
  export type PolicyKey = Exclude<keyof TaskCapabilities<"voice">, keyof SharedTaskCapabilities> | Exclude<keyof TaskCapabilities<"chat">, keyof SharedTaskCapabilities> | Exclude<keyof TaskCapabilities<"email">, keyof SharedTaskCapabilities> | "dial" | `skill:${string}`;
884
888
  /** On for everyone, off for everyone, or the agent's own choice. Only `hold`, `mute` and skills may be `agent`. */
885
889
  export type TeamPolicySetting = "on" | "off" | "agent";
886
- /** One policy as the lead sees it: the setting, who set it, and `lockedBy` when a tier above the team made it theirs to keep. */
890
+ /** One policy as the lead sees it: the setting, who set it, and `lockedBy` when a level above the team made it theirs to keep. */
887
891
  export interface TeamPolicy extends Resolved {
888
892
  setting: TeamPolicySetting;
889
893
  }
@@ -937,7 +941,7 @@ export interface TeamBreakCommandRequest {
937
941
  * `ended` once primary handling's audio ended, and the field omitted while none should be. The
938
942
  * provider's word -- a desk attaches and renders audio from it, never from its own senses.
939
943
  */
940
- export type TaskMediaState = "ready" | "ended";
944
+ export type TaskMediaState = "started" | "ended";
941
945
  export interface VoiceMediaSession {
942
946
  remoteAudio: MediaStream;
943
947
  setMuted(muted: boolean): void;
@@ -959,27 +963,27 @@ export type OpenMediaResult = {
959
963
  /**
960
964
  * What the team may leave to the person: a capability by its own name -- `hold`, `mute` -- or a
961
965
  * skill by its provider id. The same key as in `Task.capabilities`, because it is the same
962
- * capability seen at another tier. Callback and new call are never the person's; they are the
966
+ * capability seen at another level. Callback and new call are never the person's; they are the
963
967
  * team's, on or off, within what the queue allows.
964
968
  */
965
969
  export type PreferenceId = "hold" | "mute" | `skill:${string}`;
966
970
  /**
967
- * Who stated a value as it stands: a tier -- `person` among them -- or `provisioning`, the
968
- * protocol's own word for "no tier has said anything and the provider's default applies".
971
+ * Who stated a value as it stands: a level -- `person` among them -- or `provisioning`, the
972
+ * protocol's own word for "no level has said anything and the provider's default applies".
969
973
  * Nothing is hidden for want of a row.
970
974
  */
971
- export type SetBy = Tier | "provisioning";
975
+ export type SetBy = Level | "provider";
972
976
  /** What every resolved value carries: who set it, and who locked it if anyone did. */
973
977
  export interface Resolved {
974
978
  setBy: SetBy;
975
- lockedBy?: Tier;
979
+ lockedBy?: Level;
976
980
  /** Given with `lockedBy`, where whoever locked it said why. */
977
981
  reason?: string;
978
982
  }
979
983
  /**
980
984
  * One choice the team may leave to the person, with where it stands and who set it. The provider
981
985
  * keeps it: it is the person's across sessions, written through `setPreference`. Listed whether
982
- * or not anyone has stated it, and even when a tier above has since locked it.
986
+ * or not anyone has stated it, and even when a level above has since locked it.
983
987
  */
984
988
  export interface AgentPreference extends Resolved {
985
989
  id: PreferenceId;
@@ -1001,11 +1005,13 @@ export type PreferenceResult = {
1001
1005
  failure: ProtocolFailure;
1002
1006
  };
1003
1007
  export interface Snapshot<C extends Channel = Channel> {
1004
- status: ConnectionStatus;
1005
- sessionId: string;
1008
+ transport: TransportStatus;
1009
+ loginId: string;
1006
1010
  break: BreakState;
1007
1011
  /** Every task currently owned by this agent for this provider. */
1008
1012
  tasks: Task<C>[];
1013
+ /** The provider's own count of those tasks, stated rather than inferred: it must equal `tasks.length`, so a blank or unanswered state can never pass as a confirmed empty. */
1014
+ taskCount: number;
1009
1015
  contacts?: Contact[];
1010
1016
  scheduledActivities?: ScheduledActivity[];
1011
1017
  team?: TeamRoster;
@@ -1037,13 +1043,13 @@ export type ProviderEvent<C extends Channel = Channel> = {
1037
1043
  reason: "reconnected" | "provider-requested";
1038
1044
  snapshot: Snapshot<C>;
1039
1045
  } | {
1040
- type: "provider-status";
1046
+ type: "transport-status";
1041
1047
  status: "connecting" | "active";
1042
1048
  message?: string;
1043
1049
  } | {
1044
- type: "provider-status";
1050
+ type: "transport-status";
1045
1051
  status: "error";
1046
- recovery: ConnectionRecovery;
1052
+ recovery: TransportRecovery;
1047
1053
  message?: string;
1048
1054
  } | {
1049
1055
  type: "break-state";
@@ -1058,7 +1064,7 @@ export type ProviderEvent<C extends Channel = Channel> = {
1058
1064
  type: "task-updated";
1059
1065
  task: Task<C>;
1060
1066
  } | {
1061
- type: "task-media-ready";
1067
+ type: "task-media-started";
1062
1068
  taskId: TaskId;
1063
1069
  } | {
1064
1070
  type: "task-media-ended";
@@ -1094,7 +1100,7 @@ export type ProviderEvent<C extends Channel = Channel> = {
1094
1100
  export interface ProviderEventEnvelope<C extends Channel = Channel> {
1095
1101
  id: string;
1096
1102
  /** The login this belongs to. */
1097
- sessionId: string;
1103
+ loginId: string;
1098
1104
  occurredAt: IsoTimestamp;
1099
1105
  event: ProviderEvent<C>;
1100
1106
  }
@@ -1219,7 +1225,7 @@ export interface BrowserSessionKeyInput {
1219
1225
  * guard -- comparing the identity -- never fires on a demotion, because the thing that changed is
1220
1226
  * not the thing being compared. Key order does not matter, and `team: {}` is not `team` absent.
1221
1227
  */
1222
- export declare function sameCapabilities(a: SessionCapabilities, b: SessionCapabilities): boolean;
1228
+ export declare function sameCapabilities(a: UserCapabilities, b: UserCapabilities): boolean;
1223
1229
  /**
1224
1230
  * The storage-profile key a reusing browser shares, or `undefined` where it shares nothing.
1225
1231
  *
package/dist/index.js CHANGED
@@ -51,18 +51,18 @@ export function isAllowedBrowserUrl(url) {
51
51
  }
52
52
  }
53
53
  /**
54
- * The tiers a typical organisation has: the ladder in force when a manifest declares no
55
- * `orgTiers`. A manifest that declares any states its whole ladder outright.
54
+ * The levels a typical organisation has: the ladder in force when a manifest declares no
55
+ * `orgLevels`. A manifest that declares any states its whole ladder outright.
56
56
  */
57
- export const DEFAULT_TIERS = [
57
+ export const DEFAULT_LEVELS = [
58
58
  { id: "org", label: "Your organisation" },
59
59
  { id: "site", label: "Your site" },
60
60
  { id: "team", label: "Your team" },
61
61
  { id: "person", label: "You" },
62
62
  ];
63
63
  /** The ladder in force for a manifest: exactly what it declares, or the defaults when it declares none. */
64
- export function effectiveTiers(declared) {
65
- return [...(declared ?? DEFAULT_TIERS)];
64
+ export function effectiveLevels(declared) {
65
+ return [...(declared ?? DEFAULT_LEVELS)];
66
66
  }
67
67
  // ---------------------------------------------------------------------------
68
68
  // Task commands.
package/dist/testing.d.ts CHANGED
@@ -120,7 +120,7 @@ export declare class BreakStream {
120
120
  export declare function assertBreakFollowsItsRequests(envelopes: readonly ProviderEventEnvelope[], snapshot?: Snapshot): void;
121
121
  /**
122
122
  * The media follows the task and never decides it. Given a provider's stream -- optionally seeded
123
- * with the snapshot it began from -- every task is introduced once, `task-media-ready` and
123
+ * with the snapshot it began from -- every task is introduced once, `task-media-started` and
124
124
  * `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what
125
125
  * follows the media ending is `completing` or `task-ended`.
126
126
  */
@@ -162,7 +162,7 @@ export declare function assertBreakBeginsAfterTask(steps: readonly BreakOnTaskSt
162
162
  * nothing down when the provider set a clock. `toleranceMs` absorbs scheduler jitter in a real
163
163
  * implementation; pass 0 to demand an exact match.
164
164
  */
165
- export declare function assertWrapTimeout(task: Pick<TaskCompletion, "completionMode" | "completionAllowance">, mediaEndedAt: string, observedDeadline: string | undefined, toleranceMs?: number): void;
165
+ export declare function assertWrapTimeout(task: Pick<TaskCompletion, "completionMode" | "wrapAllowance">, mediaEndedAt: string, observedDeadline: string | undefined, toleranceMs?: number): void;
166
166
  /** One browser in one task of one provider. `providerId` is `Manifest.id`, never `displayName`. */
167
167
  export type BrowserIsolationScenario = BrowserSessionKeyInput;
168
168
  /** Validates whether two task-browser definitions should share one browser session. */
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { browserSessionKey, effectiveTiers, sameCapabilities, } from "./index.js";
1
+ import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
2
2
  import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostReport, validateManifest, validateResult, validateSnapshot, } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
4
4
  /**
@@ -31,8 +31,8 @@ const STATE_SUBJECTS = [
31
31
  // Pinned to the event union the way validation pins its closed sets: a type added to
32
32
  // `ProviderEvent` without a row here, or a row it lacks, is a compile error.
33
33
  const EVENT_TYPES = {
34
- snapshot: true, "provider-status": true, "break-state": true, "task-offered": true, "task-updated": true,
35
- "task-media-ready": true, "task-media-ended": true, "task-ended": true, announcement: true, "provider-summary": true,
34
+ snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
35
+ "task-media-started": true, "task-media-ended": true, "task-ended": true, announcement: true, "provider-summary": true,
36
36
  "team-updated": true, "contacts-updated": true, "calendar-updated": true,
37
37
  };
38
38
  const CONTRACT_SUBJECTS = [
@@ -172,8 +172,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
172
172
  let disconnectWasClean = false;
173
173
  try {
174
174
  authenticationState = await authentication.state();
175
- const tiers = effectiveTiers(adapter.manifest.orgTiers).map(tier => tier.id);
176
- violations.push(...validateAuthenticationState(authenticationState, "authentication", { tiers }));
175
+ const levels = effectiveLevels(adapter.manifest.orgLevels).map(level => level.id);
176
+ violations.push(...validateAuthenticationState(authenticationState, "authentication", { levels }));
177
177
  if (authenticationState.status !== "authenticated") {
178
178
  throw new Error(`Adapter contract exercise requires authenticated test state, received ${authenticationState.status}`);
179
179
  }
@@ -193,7 +193,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
193
193
  const reader = () => ({
194
194
  self: current().identity.id,
195
195
  capabilities: current().capabilities,
196
- sessionId: context.sessionId,
196
+ loginId: context.loginId,
197
197
  autoAcceptTasks: context.autoAcceptTasks ?? true,
198
198
  });
199
199
  // The optional methods are optional only until something declares a need for them. Each
@@ -228,7 +228,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
228
228
  requireMethod(on, "setPreference", "the login declares capabilities.preferences");
229
229
  };
230
230
  unsubscribeAuthentication = authentication.subscribe(state => {
231
- const own = validateAuthenticationState(state, "authentication", { tiers });
231
+ const own = validateAuthenticationState(state, "authentication", { levels });
232
232
  violations.push(...own);
233
233
  if (own.length > 0)
234
234
  return;
@@ -590,7 +590,7 @@ const WORK_BEGUN = new Set(["in-progress", "paused", "completing"]);
590
590
  export class TaskStream {
591
591
  tasks = new Map();
592
592
  static stated(task) {
593
- const media = isRecord(task) && (task.media === "ready" || task.media === "ended") ? task.media : "none";
593
+ const media = isRecord(task) && (task.media === "started" || task.media === "ended") ? task.media : "none";
594
594
  return { phase: String(isRecord(task) ? task.phase : undefined), media };
595
595
  }
596
596
  /** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
@@ -637,23 +637,31 @@ export class TaskStream {
637
637
  refuse("stream.taskMediaEnded.follow", `${at}.task.phase`, `after its media ended, ${id} completes or ends; ${phase} is a phase the audio does not decide`);
638
638
  }
639
639
  }
640
- // A task replaces the task: the update's own media field is the state now stated.
641
- this.tasks.set(id, TaskStream.stated(event.task));
640
+ // A task replaces the task, and an update re-states media without moving it: the
641
+ // transitions belong to task-media-started and task-media-ended. Releasing ended is the
642
+ // one move an update may make, since wrapped audio has nothing left to end.
643
+ {
644
+ const next = TaskStream.stated(event.task);
645
+ if (known.media !== next.media && !(known.media === "ended" && next.media === "none")) {
646
+ refuse("stream.taskUpdated.media", `${at}.task.media`, `a task-updated re-states media, it does not move it: ${id} held ${known.media} and the update says ${next.media}; audio arrives on task-media-started and ends on task-media-ended`);
647
+ }
648
+ this.tasks.set(id, next);
649
+ }
642
650
  break;
643
- case "task-media-ready":
651
+ case "task-media-started":
644
652
  if (id === undefined)
645
653
  break;
646
654
  if (known === undefined) {
647
- refuse("stream.taskMediaReady.unknown", `${at}.taskId`, `${id} was never offered or carried on a snapshot`);
655
+ refuse("stream.taskMediaStarted.unknown", `${at}.taskId`, `${id} was never offered or carried on a snapshot`);
648
656
  break;
649
657
  }
650
658
  if (!WORK_BEGUN.has(known.phase)) {
651
- refuse("stream.taskMediaReady.beforeWork", `${at}.taskId`, `media cannot arrive on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
659
+ refuse("stream.taskMediaStarted.beforeWork", `${at}.taskId`, `media cannot arrive on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
652
660
  }
653
- if (known.media === "ready") {
654
- refuse("stream.taskMediaReady.duplicate", `${at}.taskId`, `media is already ready on ${id}; ready and ended alternate`);
661
+ if (known.media === "started") {
662
+ refuse("stream.taskMediaStarted.duplicate", `${at}.taskId`, `media already started on ${id}; started and ended alternate`);
655
663
  }
656
- known.media = "ready";
664
+ known.media = "started";
657
665
  break;
658
666
  case "task-media-ended":
659
667
  if (id === undefined)
@@ -665,8 +673,8 @@ export class TaskStream {
665
673
  if (!WORK_BEGUN.has(known.phase)) {
666
674
  refuse("stream.taskMediaEnded.beforeWork", `${at}.taskId`, `media cannot end on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
667
675
  }
668
- if (known.media !== "ready") {
669
- refuse("stream.taskMediaEnded.silent", `${at}.taskId`, `media cannot end on ${id} where none arrived: audio attaches on task-media-ready, or on a task carried with media ready`);
676
+ if (known.media !== "started") {
677
+ refuse("stream.taskMediaEnded.silent", `${at}.taskId`, `media cannot end on ${id} where none arrived: audio attaches on task-media-started, or on a task carried with media started`);
670
678
  }
671
679
  known.media = "ended";
672
680
  break;
@@ -748,7 +756,7 @@ export function assertBreakFollowsItsRequests(envelopes, snapshot) {
748
756
  }
749
757
  /**
750
758
  * The media follows the task and never decides it. Given a provider's stream -- optionally seeded
751
- * with the snapshot it began from -- every task is introduced once, `task-media-ready` and
759
+ * with the snapshot it began from -- every task is introduced once, `task-media-started` and
752
760
  * `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what
753
761
  * follows the media ending is `completing` or `task-ended`.
754
762
  */
@@ -761,7 +769,7 @@ export function assertMediaFollowsTheTask(envelopes, snapshot) {
761
769
  assertNoViolations(found, "The media follows the task");
762
770
  }
763
771
  /** A host that reports one thing and never changes: what most adapter tests hand `exerciseAdapter`. */
764
- export function stillHost(report = { online: true, browsers: { urlVisibility: "hidden" } }) {
772
+ export function stillHost(report = { online: true }) {
765
773
  return { report: () => report, subscribe: () => () => undefined };
766
774
  }
767
775
  const usableLogin = (status) => status === "authenticated" || status === "refreshing";
@@ -825,20 +833,20 @@ export function assertBreakBeginsAfterTask(steps) {
825
833
  * implementation; pass 0 to demand an exact match.
826
834
  */
827
835
  export function assertWrapTimeout(task, mediaEndedAt, observedDeadline, toleranceMs = 1_000) {
828
- if (task.completionAllowance === undefined) {
836
+ if (task.wrapAllowance === undefined) {
829
837
  if (observedDeadline !== undefined) {
830
838
  throw new Error(`Wrap deadline mismatch: the task states no allowance, so there is no deadline, received ${observedDeadline}`);
831
839
  }
832
840
  return;
833
841
  }
834
842
  if (observedDeadline === undefined) {
835
- throw new Error(`Wrap deadline mismatch: the task allows ${task.completionAllowance}s, but no deadline was observed`);
843
+ throw new Error(`Wrap deadline mismatch: the task allows ${task.wrapAllowance}s, but no deadline was observed`);
836
844
  }
837
845
  const ended = Date.parse(mediaEndedAt);
838
846
  const deadline = Date.parse(observedDeadline);
839
847
  if (Number.isNaN(ended) || Number.isNaN(deadline))
840
848
  throw new Error("Wrap scenario requires valid ISO-8601 times");
841
- const expected = ended + task.completionAllowance * 1_000;
849
+ const expected = ended + task.wrapAllowance * 1_000;
842
850
  if (Math.abs(deadline - expected) > toleranceMs) {
843
851
  throw new Error(`Wrap deadline mismatch: expected ${new Date(expected).toISOString()} within ${toleranceMs}ms, received ${observedDeadline}`);
844
852
  }
@@ -1,4 +1,4 @@
1
- import { type ProtocolViolation, type SessionCapabilities, type UserId } from "./index.js";
1
+ import { type ProtocolViolation, type UserCapabilities, type UserId } from "./index.js";
2
2
  export type { ProtocolViolation } from "./index.js";
3
3
  export declare class ProtocolConformanceError extends Error {
4
4
  readonly violations: readonly ProtocolViolation[];
@@ -13,8 +13,8 @@ export declare function validateManifest(manifest: unknown, path?: string): Prot
13
13
  export interface TaskValidationContext {
14
14
  /** The provider's channel, from its manifest. A task must agree with it. */
15
15
  channel: string;
16
- /** The tier ids in force, from the manifest. The defaults when absent. */
17
- tiers?: readonly string[];
16
+ /** The level ids in force, from the manifest. The defaults when absent. */
17
+ levels?: readonly string[];
18
18
  }
19
19
  export declare function validateTask(task: unknown, context: TaskValidationContext, path?: string): ProtocolViolation[];
20
20
  /**
@@ -31,11 +31,11 @@ export interface ReaderContext {
31
31
  * The login's `AuthenticationState.capabilities`. The login is the permission: a lead's
32
32
  * snapshot carries a roster, nobody else's does, and `requests` need `team.consultControl`.
33
33
  */
34
- capabilities?: SessionCapabilities;
35
- /** The tier ids in force. Filled from the manifest by `validateSnapshot` and `validateEventEnvelope`; the defaults otherwise. */
36
- tiers?: readonly string[];
37
- /** The login's `sessionId`. A snapshot or event naming another belongs to a login that is gone. */
38
- sessionId?: string;
34
+ capabilities?: UserCapabilities;
35
+ /** The level ids in force. Filled from the manifest by `validateSnapshot` and `validateEventEnvelope`; the defaults otherwise. */
36
+ levels?: readonly string[];
37
+ /** The login's `loginId`. A snapshot or event naming another belongs to a login that is gone. */
38
+ loginId?: string;
39
39
  /** `ConnectContext.autoAcceptTasks` as sent, absent meaning `true`: whether `task-offered` carries an `acceptanceMode`. */
40
40
  autoAcceptTasks?: boolean;
41
41
  }
@@ -59,7 +59,7 @@ export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" |
59
59
  export declare function validateResult(result: unknown, method: ResultMethod, path?: string): ProtocolViolation[];
60
60
  /** What a login is validated against beyond its own shape. */
61
61
  export interface LoginValidationContext {
62
- /** The tier ids in force, from the manifest. The defaults when absent. */
63
- tiers?: readonly string[];
62
+ /** The level ids in force, from the manifest. The defaults when absent. */
63
+ levels?: readonly string[];
64
64
  }
65
65
  export declare function validateAuthenticationState(state: unknown, path?: string, context?: LoginValidationContext): ProtocolViolation[];