@xema/omni-protocol 0.1.104 → 0.1.106

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
@@ -589,7 +589,8 @@ export type TransportStatus = "connecting" | "active" | "error";
589
589
  * connection and call `connect()` again -- or `reauthenticate` -- run the authentication flow
590
590
  * first. The adapter knows which; the host acts on its word.
591
591
  */
592
- export type TransportRecovery = "reconnect" | "reauthenticate";
592
+ /** What revives an errored connection: `displaced` says another client took the login, and nothing revives this one until the agent takes it back. */
593
+ export type TransportRecovery = "reconnect" | "reauthenticate" | "displaced";
593
594
  /** Every field is optional: a provider sends what it knows and omits what it does not. */
594
595
  export interface Contact {
595
596
  name?: string;
@@ -973,6 +974,14 @@ export type Task<C extends Channel = Channel> = {
973
974
  */
974
975
  previewEndsInSeconds?: DurationSeconds;
975
976
  atDeadline?: PreviewDeadline;
977
+ /**
978
+ * In `completing`, wherever `wrapAllowance` is stated: how much of the wrap is left, in whole
979
+ * seconds from this publication, restated on every publication that carries it, so a reloaded
980
+ * desk and a lead's screen count down the same number. Under `provider-automatic` the provider
981
+ * ends the task when it reaches zero; under `agent-command` it is the expected wrap left, `0`
982
+ * once overrun, and nothing acts on it.
983
+ */
984
+ wrapEndsInSeconds?: DurationSeconds;
976
985
  /** The identifier an agent reads back to a customer, where the provider has one. */
977
986
  reference?: string;
978
987
  attributes?: TaskAttribute[];
@@ -1289,10 +1298,11 @@ export interface MemberRequest {
1289
1298
  since: IsoTimestamp;
1290
1299
  }
1291
1300
  /**
1292
- * A member's task as the lead sees it: the same task the member's desk holds, trimmed by the
1293
- * provider. The assignment, what the call is and where it stands, the room and the full history
1294
- * are always here; the workspace -- browsers, controls, completion terms -- is the member's and
1295
- * travels only if the provider chooses to send it.
1301
+ * A member's task as the lead sees it: the same task the member's desk holds, less the workspace.
1302
+ * The assignment, what the call is and where it stands, the party as the provider shows it to
1303
+ * leads, the room, the audio, the completion terms and the whole record are here; the controls,
1304
+ * their source and the browsers are the member's desk's alone and never travel. Published to every
1305
+ * lead whenever the member's own desk hears of the task.
1296
1306
  */
1297
1307
  export type MemberTask<C extends Channel = Channel> = {
1298
1308
  assignmentId: AssignmentId;
@@ -1304,11 +1314,9 @@ export type MemberTask<C extends Channel = Channel> = {
1304
1314
  reference?: string;
1305
1315
  attributes?: TaskAttribute[];
1306
1316
  history?: TaskHistory;
1307
- capabilities?: TaskCapabilities<C>;
1308
- capabilitySource?: CapabilitySource;
1309
- browsers?: TaskBrowser[];
1310
1317
  completionMode?: CompletionMode;
1311
1318
  wrapAllowance?: DurationSeconds;
1319
+ wrapEndsInSeconds?: DurationSeconds;
1312
1320
  } & (C extends "voice" ? {
1313
1321
  onCall?: OnCall[];
1314
1322
  leadAssist?: TaskLeadAssist;
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 BreakStatus, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostMute, type LoginStore, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
1
+ import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type BreakStatus, 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
  /**
@@ -276,15 +276,6 @@ export interface BreakOnTaskStep {
276
276
  * outstanding -- never beside a task, and never later than the step that has none.
277
277
  */
278
278
  export declare function assertBreakBeginsAfterTask(steps: readonly BreakOnTaskStep[]): void;
279
- /**
280
- * Validates the deadline derived from audio end and the task's fixed wrap allowance.
281
- *
282
- * A task with no allowance has no deadline, so `observedDeadline` must then be `undefined`: a
283
- * host counting down what the provider left open is the violation, and so is a host counting
284
- * nothing down when the provider set a clock. `toleranceMs` absorbs scheduler jitter in a real
285
- * implementation; pass 0 to demand an exact match.
286
- */
287
- export declare function assertWrapTimeout(task: Pick<TaskCompletion, "completionMode" | "wrapAllowance">, audioEndedAt: string, observedDeadline: string | undefined, toleranceMs?: number): void;
288
279
  /** One browser in one task of one provider. `providerId` is `Manifest.id`, never `displayName`. */
289
280
  export type BrowserIsolationScenario = BrowserSessionKeyInput;
290
281
  /** Validates whether two task-browser definitions should share one browser session. */
package/dist/testing.js CHANGED
@@ -2207,33 +2207,6 @@ export function assertBreakBeginsAfterTask(steps) {
2207
2207
  throw new Error(`Break-on-task scenario must end in effect, ended ${String(steps.at(-1)?.approval)}`);
2208
2208
  }
2209
2209
  }
2210
- /**
2211
- * Validates the deadline derived from audio end and the task's fixed wrap allowance.
2212
- *
2213
- * A task with no allowance has no deadline, so `observedDeadline` must then be `undefined`: a
2214
- * host counting down what the provider left open is the violation, and so is a host counting
2215
- * nothing down when the provider set a clock. `toleranceMs` absorbs scheduler jitter in a real
2216
- * implementation; pass 0 to demand an exact match.
2217
- */
2218
- export function assertWrapTimeout(task, audioEndedAt, observedDeadline, toleranceMs = 1_000) {
2219
- if (task.wrapAllowance === undefined) {
2220
- if (observedDeadline !== undefined) {
2221
- throw new Error(`Wrap deadline mismatch: the task states no allowance, so there is no deadline, received ${observedDeadline}`);
2222
- }
2223
- return;
2224
- }
2225
- if (observedDeadline === undefined) {
2226
- throw new Error(`Wrap deadline mismatch: the task allows ${task.wrapAllowance}s, but no deadline was observed`);
2227
- }
2228
- const ended = Date.parse(audioEndedAt);
2229
- const deadline = Date.parse(observedDeadline);
2230
- if (Number.isNaN(ended) || Number.isNaN(deadline))
2231
- throw new Error("Wrap scenario requires valid ISO-8601 times");
2232
- const expected = ended + task.wrapAllowance * 1_000;
2233
- if (Math.abs(deadline - expected) > toleranceMs) {
2234
- throw new Error(`Wrap deadline mismatch: expected ${new Date(expected).toISOString()} within ${toleranceMs}ms, received ${observedDeadline}`);
2235
- }
2236
- }
2237
2210
  /** The session key one scenario derives, or `undefined` where the browser shares nothing. */
2238
2211
  const sessionKeyFor = (scenario) => browserSessionKey(scenario);
2239
2212
  /** Validates whether two task-browser definitions should share one browser session. */
@@ -36,7 +36,7 @@ export function assertNoViolations(violations, summary) {
36
36
  */
37
37
  const membersOf = (members) => Object.keys(members);
38
38
  const CHANNELS = membersOf({ voice: true, chat: true, email: true });
39
- const TRANSPORT_RECOVERIES = membersOf({ reconnect: true, reauthenticate: true });
39
+ const TRANSPORT_RECOVERIES = membersOf({ reconnect: true, reauthenticate: true, displaced: true });
40
40
  const TASK_PHASES = membersOf({
41
41
  pending: true, confirmed: true, preview: true, "in-progress": true, paused: true, completing: true,
42
42
  });
@@ -1185,6 +1185,18 @@ function validateTaskInto(task, context, path, into) {
1185
1185
  else {
1186
1186
  into.require(isDurationSeconds(task.wrapAllowance), "task.wrapAllowance", `${path}.wrapAllowance`, "wrapAllowance must be a whole number of seconds, zero or more, or omitted under agent-command");
1187
1187
  }
1188
+ // How much wrap is left travels with the completing task, restated, so every screen counts the same number.
1189
+ if (task.phase === "completing" && task.wrapAllowance !== undefined) {
1190
+ if (task.wrapEndsInSeconds === undefined) {
1191
+ into.add("task.completing.wrapEndsInSeconds.required", `${path}.wrapEndsInSeconds`, "a completing task with a wrap allowance says how much of it is left, so a reloaded desk and a lead's screen count down the same number");
1192
+ }
1193
+ else {
1194
+ into.require(isDurationSeconds(task.wrapEndsInSeconds), "task.completing.wrapEndsInSeconds", `${path}.wrapEndsInSeconds`, "wrapEndsInSeconds is how much wrap is left: a whole number of seconds, zero or more, counted from this publication");
1195
+ }
1196
+ }
1197
+ else if (task.wrapEndsInSeconds !== undefined) {
1198
+ into.add("task.completing.wrapEndsInSeconds.unexpected", `${path}.wrapEndsInSeconds`, task.phase === "completing" ? "no wrap allowance was stated, so there is nothing to count down" : "wrapEndsInSeconds belongs to a completing task; before the wrap there is nothing to count");
1199
+ }
1188
1200
  // The channel is fixed per provider by its manifest, so a task claiming another one is a
1189
1201
  // task Omni would render with the wrong controls.
1190
1202
  into.require(task.channel === context.channel, "task.channel", `${path}.channel`, `a ${context.channel} provider may not publish a ${describeValue(task.channel)} task`);
@@ -1207,7 +1219,7 @@ function validateTaskInto(task, context, path, into) {
1207
1219
  into.violations.push(...validateRecordingState(task.recording.provider, `${path}.recording.provider`));
1208
1220
  }
1209
1221
  }
1210
- if (context.member !== true || task.browsers !== undefined)
1222
+ if (context.member !== true)
1211
1223
  validateBrowsers(task.browsers, `${path}.browsers`, into);
1212
1224
  validateTaskAttributes(task.attributes, `${path}.attributes`, into, context.levels);
1213
1225
  validateHistory(task.history, `${path}.history`, into, { phase: task.phase, audio: task.audio });
@@ -1226,11 +1238,16 @@ function validateTaskInto(task, context, path, into) {
1226
1238
  validateTakenOver(task.takenOver, context.channel, `${path}.takenOver`, into);
1227
1239
  // Where the terms came from is stated with them: a host cannot tell "the platform permits
1228
1240
  // nothing" from "the terms could not be read" from the set alone, and the provider knows which.
1229
- if (context.member !== true || task.capabilitySource !== undefined)
1230
- into.oneOf(task.capabilitySource, CAPABILITY_SOURCES, "task.capabilitySource", `${path}.capabilitySource`);
1231
- const capabilities = task.capabilities;
1232
- if (context.member === true && capabilities === undefined && task.browsers === undefined)
1241
+ if (context.member === true) {
1242
+ // The lead's copy is the record, the room and the terms; the workspace stays on the member's desk.
1243
+ for (const field of ["capabilities", "capabilitySource", "browsers"]) {
1244
+ if (task[field] !== undefined)
1245
+ into.add("team.member.task.workspace", `${path}.${field}`, `${field} is the member's workspace, and never travels to a lead`);
1246
+ }
1233
1247
  return;
1248
+ }
1249
+ into.oneOf(task.capabilitySource, CAPABILITY_SOURCES, "task.capabilitySource", `${path}.capabilitySource`);
1250
+ const capabilities = task.capabilities;
1234
1251
  if (!isPlainObject(capabilities)) {
1235
1252
  into.add("task.capabilities.shape", `${path}.capabilities`, "a task needs a capabilities object");
1236
1253
  return;
@@ -1680,7 +1697,7 @@ function validateTeamMemberInto(member, at, context, into) {
1680
1697
  const held = new Set();
1681
1698
  member.tasks.forEach((task, index) => {
1682
1699
  const channel = isPlainObject(task) && typeof task.channel === "string" && isChannel(task.channel) ? task.channel : context.channel ?? "voice";
1683
- validateTaskInto(task, { channel, levels: context.levels, member: true }, `${at}.tasks[${index}]`, into);
1700
+ validateTaskInto(task, { channel, levels: context.levels, member: true, locked: context.locked }, `${at}.tasks[${index}]`, into);
1684
1701
  if (isPlainObject(task) && isAssignmentId(task.assignmentId)) {
1685
1702
  if (held.has(task.assignmentId))
1686
1703
  into.add("team.member.tasks.unique", `${at}.tasks[${index}].assignmentId`, `duplicate assignment on one member: ${task.assignmentId}`);
package/guide.md CHANGED
@@ -23,7 +23,7 @@ are used precisely throughout and mean nothing looser here.
23
23
  | **Provider** | One independently connected external system: a voice platform, a chat platform, a mail platform. |
24
24
  | **Adapter** | The package implementing this contract for one provider. One adapter is one provider, so the words are often interchangeable; *provider* names the system, *adapter* the code speaking for it. |
25
25
  | **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; a conference directory may include an agent when the provider publishes that directory item. |
26
- | **Lead** | An agent whose login declares `capabilities.lead`. The flag alone turns on the team feature in the agent application: the lead sees their members with their tasks and stats, and acts on them with the authority the flag carries. While the feature is on, the provider publishes a `TeamMembers` object to them and to nobody else: **the login is the permission**. |
26
+ | **Lead** | An agent whose login declares `capabilities.lead`. The flag lets the agent application offer the team feature; the application switches it on with `lead-features` on every connect, and only then does the provider publish the team -- whole on `team-updated`, then one member at a time -- to that login and to nobody else: **the login is the permission**. The lead sees their members with their tasks and stats, and acts on them with the authority the flag carries. |
27
27
  | **Local policy** | Rules configured by the agent application about this agent, outside the provider protocol and never sent to a provider. It gates whether an offer may be rejected, whether the agent goes ready on login, and whether tasks are auto-accepted. Where a capability and local policy disagree, the stricter wins. |
28
28
  | **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. The protocol never describes the IVR or the queue; it carries the call's history and details to whichever agent holds it now. |
29
29
  | **Assignment** | The call routed to this agent, from the offer until the task ends. It is the one thing that crosses: the provider assigns, names the assignment with `assignmentId`, and that is the only name the desk ever uses back. A call that comes back is a new assignment on the same call, and a declined or lapsed offer is an assignment that became nothing more. What the platform calls its own record, and whether it reuses that name, is the adapter's business and unknown to the desk. |
@@ -41,7 +41,7 @@ are used precisely throughout and mean nothing looser here.
41
41
  | **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
42
42
  | **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, team member list. |
43
43
  | **Dial** | One outbound call the agent application asks a provider to place — from the idle dialpad, a preview's Call, a conference add, or a connect-back. Identified by the agent application's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
44
- | **Listen** | A lead's channel in a member's call, unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. It is a team act naming the member, never a task on the lead's desk; nothing of it reaches the member's task. A lead who wants the call takes it over. See **Listening to a call**. |
44
+ | **Listen** | A lead's channel in a member's call, unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. It is a team act naming the member, never a task on the lead's desk; `listen` and `coach` leave no trace on the member's task, while a lead heard by everyone is on its `onCall` as an `agent`. Every lead sees every lead on the call. A lead who wants the call takes it over. See **Listening to a call**. |
45
45
  | **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone conferenced in from the moment their dial is placed. |
46
46
 
47
47
  These fields describe state within their containing objects. Both authentication and break
@@ -54,7 +54,7 @@ state use `status`; name the object when discussing them to avoid ambiguity.
54
54
  | `transport` | A connection | `connecting`, `active`, `error` |
55
55
  | `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
56
56
  | `status` | The agent’s break lifecycle | `not-requested`, `awaiting-approval`, `granted`, `starting-after-task`, `on-break` |
57
- | `availability` | A team member | `ready`, `on-task`, `on-break`, `signed-out` |
57
+ | `availability` | A team member | `ready`, `on-task`, `on-break`, `elsewhere`, `signed-out` |
58
58
 
59
59
  Migration: describeUsers is now `getUserDetails`, and validateDescribedUsers is now
60
60
  `validateUserDetails`. Related diagnostics use getUserDetails. Hosts and providers must
@@ -124,7 +124,7 @@ identifier of its own for an agent or a manager, and none crosses this boundary
124
124
  operating-system account, not a directory identity, not a licence.
125
125
 
126
126
  So a `UserId` means nothing outside the provider that issued it. Provider A's
127
- `history[].by` and provider B's team member list `memberId` are unrelated strings that will
127
+ `history[].by` and provider B's team member `id` are unrelated strings that will
128
128
  eventually collide, and one person on several providers has several identities that nothing here
129
129
  pairs. Scope every user identifier with its provider ID before storing or comparing it, exactly as
130
130
  `assignmentKey()` already does for tasks — see `userKey()` under **Utilities**.
@@ -728,6 +728,7 @@ type Task<C extends Channel = Channel> = {
728
728
  expiresInSeconds?: DurationSeconds;
729
729
  previewEndsInSeconds?: DurationSeconds;
730
730
  atDeadline?: PreviewDeadline;
731
+ wrapEndsInSeconds?: DurationSeconds;
731
732
  reference?: string;
732
733
  attributes?: TaskAttribute[];
733
734
  history?: TaskHistory;
@@ -834,7 +835,7 @@ type TaskCommandResult =
834
835
  ### Who decides what an agent may do
835
836
 
836
837
  An agent desk has two managers, not one. **The queue** — a process, a work type — is owned by a
837
- process manager and **allows** a set of capabilities: hold, connect back, new call, conference,
838
+ process manager and **allows** a set of capabilities: hold, connect back, dial, conference,
838
839
  whether the number is visible, the actions it offers, the skills it needs. **The people** are
839
840
  managed through the organisation's structure — a team, a location, the organisation itself, in
840
841
  whatever combination the structure defines for a person — and **decide** per capability within
@@ -882,13 +883,21 @@ The adapter is the one that knows the value, so it is the one held to it: given
882
883
  queue locked, the validator refuses any other field carrying one, digits compared as digits so no
883
884
  formatting hides them (`task.locked.leak`), and a conformance run whose tasks lock a party's number
884
885
  or email states those values in `lockedValues`, since a run that cannot ask the question is not a
885
- pass. An agent application never has the value and never asks. A name is not locked. What the queue provides rather than permits — browsers, outcomes, custom
886
+ pass. An agent application never has the value and never asks. A name is not locked.
887
+
888
+ **The lock is per audience.** Who sees the number is the provider's choice for each copy of the
889
+ task: the member's, on their desk, and the leads', on the team member list. Hidden from the agent
890
+ and shown to the lead, shown to the agent and hidden from the lead, hidden from both, shown to
891
+ both -- any of the four, as the platform's policy says, and each copy is the provider's statement
892
+ to that audience. Each copy keeps its own lock whole: where the lead's copy locks the number,
893
+ nothing else on that copy carries it, exactly as on the member's (`task.locked.leak`, applied to a
894
+ member's tasks with the same `lockedValues`). What the queue provides rather than permits — browsers, outcomes, custom
886
895
  controls — is content, and is never locked.
887
896
 
888
897
  **A lead sets the team's policy from their team member list.** A login that declares
889
898
  `capabilities.lead` may `executeTeam({ command: { type: "set-policy", capability, setting } })`, where the provider offers policy control and says so by publishing `policies`,
890
899
  with `on`, `off`, or `person`, for any task control, `dial`, or a skill — and only `hold`
891
- and skills may be `person`; connect back and new call are the team's, on or off, within what the queue
900
+ and skills may be `person`; connect back and dial are the team's, on or off, within what the queue
892
901
  allows. The team member list carries `policies` for such a login: every policy as it stands, who set it, and
893
902
  `lockedBy` where a level above the team made it theirs to keep, which the lead sees and cannot
894
903
  change — `set-policy` on it answers `failed` with `omni.capability-not-enabled`.
@@ -998,11 +1007,9 @@ type MemberTask<C extends Channel = Channel> = {
998
1007
  reference?: string;
999
1008
  attributes?: TaskAttribute[];
1000
1009
  history?: TaskHistory;
1001
- capabilities?: TaskCapabilities<C>;
1002
- capabilitySource?: CapabilitySource;
1003
- browsers?: TaskBrowser[];
1004
1010
  completionMode?: CompletionMode;
1005
1011
  wrapAllowance?: DurationSeconds;
1012
+ wrapEndsInSeconds?: DurationSeconds;
1006
1013
  } & (C extends "voice"
1007
1014
  ? { onCall?: OnCall[]; leadAssist?: TaskLeadAssist; takenOver?: TaskTakenOver; audio?: TaskAudioState }
1008
1015
  : { onCall?: never; leadAssist?: never; takenOver?: never; audio?: never });
@@ -1113,7 +1120,7 @@ type QueueSummary = {
1113
1120
  metrics?: SummaryMetric[];
1114
1121
  };
1115
1122
 
1116
- type TransportRecovery = "reconnect" | "reauthenticate";
1123
+ type TransportRecovery = "reconnect" | "reauthenticate" | "displaced";
1117
1124
 
1118
1125
  type ProviderEvent =
1119
1126
  | { type: "snapshot"; reason: "reconnected" | "provider-requested"; snapshot: Snapshot }
@@ -1392,7 +1399,7 @@ The map key is the exact `Task.taskType`. When no entry exists, Omni uses the pr
1392
1399
  channel's entry in `DEFAULT_TASK_TYPE_PRESENTATION`. A mixed-channel list falls back to the words
1393
1400
  "Task" and "Tasks" when its items do not share one label.
1394
1401
 
1395
- `referenceLabel` labels `Task.reference`; it does not label the protocol `id`. Omni shows a
1402
+ `referenceLabel` labels `Task.reference`; it does not label `assignmentId`. Omni shows a
1396
1403
  reference only when both values are present.
1397
1404
 
1398
1405
  **So supply it whenever the task type has references.** Because an entry replaces the channel
@@ -1434,7 +1441,8 @@ only where the corresponding capability is declared; an absent capability means
1434
1441
  there is no separate permission flag.
1435
1442
 
1436
1443
  The commands every task has are authorized by other fields the provider declared — a task that was
1437
- offered can be accepted, one in `preview` can be called, one with a wrap can be completed.
1444
+ offered can be accepted, one in `preview` can be called, any task under `agent-command` and one
1445
+ with a wrap under `provider-automatic` can be completed.
1438
1446
  Nothing is issuable that the provider did not publish; only
1439
1447
  which field says so varies. See **Which commands need a capability**.
1440
1448
 
@@ -1477,7 +1485,7 @@ is never the provider's answer: an answer travels only as a resolved `{ status:
1477
1485
  and every rejection is transport loss, whatever the last `transport-status` said and whatever the
1478
1486
  rejection carries.
1479
1487
 
1480
- A command therefore carries no key. The provider names the assignment, the lead request and the
1488
+ A command therefore carries no key. The provider names the assignment, the member's request and the
1481
1489
  member, and Omni refers to them by those names; **Omni never asks a provider to remember a name
1482
1490
  Omni made up.**
1483
1491
 
@@ -1630,7 +1638,7 @@ compile time.
1630
1638
  | `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label a desk 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**. |
1631
1639
  | `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 `deskPhone` (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**. |
1632
1640
  | `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
1633
- | `timeCheck` | `true`: implements `checkTime` and states `providerTime` on every snapshot. Required of a provider that publishes an instant the desk renders as a running duration -- a member's `since`, a lead's `listening.since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts from the provider's clock (`manifest.timeCheck.required`); agent application polling is independently opt-in. See **Every screen counts from the provider's clock**. |
1641
+ | `timeCheck` | `true`: implements `checkTime` and states `providerTime` on every snapshot. Required of a provider that publishes an instant the desk renders as a running duration -- a member's `since`, a `listening[].since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts from the provider's clock (`manifest.timeCheck.required`); agent application polling is independently opt-in. See **Every screen counts from the provider's clock**. |
1634
1642
  | `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
1635
1643
  | `runningStepReports` | The provider takes running reports of an agent application-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the agent application sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The agent application records what it performs**. |
1636
1644
  | `settleMs` | Required. How long after `applied` the wire shows what the provider did: the `task-ended` after a `complete`, the `calendar-updated` after a `schedule`. A positive whole number of milliseconds (`manifest.settleMs`). Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
@@ -1764,8 +1772,8 @@ in the agent's view would show that it had happened.
1764
1772
 
1765
1773
  `calendar` is available to voice, chat, and email providers. It contributes a calendar to the idle
1766
1774
  dashboard for callbacks and other scheduled activities associated with that provider. Calendar is
1767
- read-only in protocol v1: Omni can display activities but cannot create, reschedule, cancel, or
1768
- complete them.
1775
+ the provider's: Omni displays activities and, where a task offers `schedule`, asks the provider to
1776
+ create one -- see **Scheduling a follow-up** -- but cannot reschedule, cancel, or complete them.
1769
1777
 
1770
1778
  ```ts
1771
1779
  idleCapabilities: { calendar: true }
@@ -1793,9 +1801,9 @@ matched by nobody and defined by nobody, and calendars merge across providers ex
1793
1801
  do, so one provider's `Callback` and another's `Call back` fragment a list that looks organised.
1794
1802
  Send the category as an attribute and Omni displays it.
1795
1803
 
1796
- Nothing is lost by that here, because there is nothing for a category to drive. A read-only
1797
- calendar has no action to vary by kind, and what an activity *is* already shows in what it carries:
1798
- a callback has a `contact`, a training does not.
1804
+ Nothing is lost by that here, because there is nothing for a category to drive. Nothing on the
1805
+ calendar varies by kind -- `schedule` takes no category -- and what an activity *is* already shows
1806
+ in what it carries: a callback has a `party`, a training does not.
1799
1807
 
1800
1808
  #### Contacts (`contacts`)
1801
1809
 
@@ -2073,6 +2081,12 @@ Creates one live provider connection for the signed-in agent.
2073
2081
  - Must resolve only when the connection can provide a meaningful snapshot.
2074
2082
  - May reject for authentication, configuration, or startup failure.
2075
2083
  - Must not create a second agent session merely because the underlying transport reconnects.
2084
+ - **One client at a time per login, and the later one wins.** A second `connect()` on a login
2085
+ that already has a live client is the agent moving desks -- a desk left open at work, signed in
2086
+ from home. The provider takes the new connection, and ends the first with `transport-status`
2087
+ `error` and `recovery: "displaced"`, so the first desk stops stating capacity and reporting legs
2088
+ and shows the agent they are signed in elsewhere. Two live clients on one login would be two
2089
+ sources for one agent, and the lead's list would follow whichever spoke last.
2076
2090
  - The returned connection owns reconnect until Omni calls `disconnect()` or aborts `context.signal`.
2077
2091
  - May be called again on the same login after that: once per `Connection`, not once per login.
2078
2092
  Omni disposes a connection whose `error` named `recovery: "reconnect"` with `disconnect()` and
@@ -2194,6 +2208,7 @@ Returns the provider's complete authoritative state at one point in time.
2194
2208
  snapshot is read is held until it lands, and what happens to it then depends on what it is. A
2195
2209
  snapshot restates state, so an event of a state-replacing kind -- `task-offered`, `task-updated`,
2196
2210
  `task-ended`, `task-audio-started`, `task-audio-ended`, `break-state`, `team-updated`,
2211
+ `team-member-updated`, `team-member-removed`, `team-policies-updated`, `shift-updated`,
2197
2212
  `contacts-updated`, `calendar-updated`, `transport-status`, `snapshot` -- is dropped, because the
2198
2213
  snapshot accounts for it. An event that reports a transaction no snapshot carries --
2199
2214
  `dial-outcome`, `diagnostic`, `announcement`, `queue-summary` -- is applied after the snapshot,
@@ -2262,9 +2277,11 @@ capacity is stated, and needs no other word for it.
2262
2277
 
2263
2278
  **2. The agent becomes ready.** The agent application states the capacity when its local policy says: at once,
2264
2279
  or when the agent presses Ready. A successful connection, a healthy provider, or the absence of a
2265
- break does not imply readiness. After that, the only way an agent stops taking work is a break,
2266
- under `capabilities.breaks`; a login that declares no breaks cannot go not-ready once its capacity
2267
- is stated, and `count: 0` is the agent application's division of capacity, not the agent's choice.
2280
+ break does not imply readiness. After that, an agent stops taking work on this provider only by a
2281
+ break under `capabilities.breaks`, or by the agent application restating `count: 0` -- which is how
2282
+ a forced break on another provider reaches this one; a login that declares no breaks has no
2283
+ not-ready of its own, and `count: 0` is the agent application's division of capacity, not the
2284
+ agent's choice.
2268
2285
 
2269
2286
  **3. Omni states their concurrent capacity.** Only now does Omni ask providers for work, saying how
2270
2287
  much the agent can take at once. It is a standing declaration rather than a poll: Omni restates the
@@ -2413,7 +2430,8 @@ time. Runtime conformance checks also require the task channel to match its prov
2413
2430
  | `atDeadline` | Voice only, in `preview`, with `previewEndsInSeconds`: what the system does when it runs out -- `provider-dials` makes the provider initiate dialing, `host-dials` makes the agent application issue Call, `waits` keeps the task in preview awaiting the agent. |
2414
2431
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
2415
2432
  | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one, and takes the agent's `complete` as finishing early. A required outcome is the agent's to give, so it needs `agent-command` (`task.outcomes.required.mode`). |
2416
- | `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time audio, it begins after `task-audio-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. `0` is no wrap at all, so a task with it publishes no `outcomes` (`task.outcomes.wrapAllowance`). |
2433
+ | `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time audio, it begins after `task-audio-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down; stated, it is the expected wrap, shown and never acted on. `0` under `provider-automatic` is no wrap at all, so such a task publishes no `outcomes` (`task.outcomes.wrapAllowance`). |
2434
+ | `wrapEndsInSeconds` | In `completing`, wherever `wrapAllowance` is stated: how much of the wrap is left, in whole seconds from this publication, restated on every publication that carries it (`task.completing.wrapEndsInSeconds`, `.required`, `.unexpected`). A reloaded desk and a lead's screen count down the same number. See **Completion timing**. |
2417
2435
  | `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
2418
2436
  | `history` | The call record: `steps` — the ordered interaction history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `interactionSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **Interaction history**. |
2419
2437
  | `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer -- carrying a `stage` while being dialled again on the same task, a connect-back with the agent application's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where an agent application placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. `label` names a destination -- a person, a queue -- not a phrase; the agent application supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
@@ -2543,12 +2561,10 @@ answer at that exact instant. Scheduling/dispatch delay must remain visible, and
2543
2561
  prevented initiation must be reported rather than leaving a silently expired countdown.
2544
2562
  A source eligibility threshold that may never trigger an attempt is not this promise.
2545
2563
 
2546
- The two deadline fields travel together and only in preview. The agent application uses trusted provider-domain
2547
- time; clock estimation is allowed only under the explicit agent application-triggered preview exception below.
2548
- Without usable provider-domain time
2549
- it cannot safely auto-trigger and must expose the uncertainty and reconcile. No zero-duration,
2550
- receipt-time or local-default deadline is inferred. Neither elapsed time nor submission changes
2551
- the task phase: subsequent authoritative events state ringing, answer, audio and completion.
2564
+ The two deadline fields travel together and only in preview. The countdown is the seconds the
2565
+ publication carried, counted from its receipt: the agent application infers no deadline where none
2566
+ was stated and reads no clock against another. Neither elapsed time nor submission changes the
2567
+ task phase: subsequent authoritative events state ringing, answer, audio and completion.
2552
2568
 
2553
2569
  An agent application must serialize manual clicks and its timer under the exact provider/login/task/assignment
2554
2570
  scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
@@ -2649,14 +2665,23 @@ nothing acts on it -- the task waits for `complete` however long that takes, wit
2649
2665
  collected whenever the agent gives them. A provider that would end the task at a time is
2650
2666
  `provider-automatic`.
2651
2667
 
2668
+ **What is left travels with the task.** The allowance is the length; `wrapEndsInSeconds` is how
2669
+ much of it remains, stated on the `completing` task and restated on every publication that
2670
+ carries it, as `previewEndsInSeconds` is on a preview. A desk that reloads mid-wrap finds the
2671
+ number on the snapshot; a lead's screen finds it on the member; neither starts a clock of its own,
2672
+ and both count down from receipt. Under `provider-automatic` the provider ends the task when it
2673
+ reaches zero; under `agent-command` it is the expected wrap left, `0` once overrun, and nothing
2674
+ acts on it. Off voice, where `completing` is the provider's word that interaction ended, the field
2675
+ arrives with that word, so the clock starts nowhere else.
2676
+
2652
2677
  `wrapAllowance` is fixed, and when it starts depends on whether the channel carries real-time
2653
2678
  audio:
2654
2679
 
2655
2680
  | Channel | Wrap allowance starts at |
2656
2681
  | --- | --- |
2657
2682
  | Voice and any channel with real-time audio | The `task-audio-ended` event |
2658
- | Chat | The `task-updated` that moves the task to `completing`: the provider's word that the conversation is closed on its side, the allowance running from that publication's `occurredAt` |
2659
- | Email | The `task-updated` that moves the task to `completing`: the provider's word that the platform has accepted the outgoing message, the allowance running from that publication's `occurredAt` |
2683
+ | Chat | The `task-updated` that moves the task to `completing`: the provider's word that the conversation is closed on its side; what is left arrives on that publication as `wrapEndsInSeconds` and is counted down from receipt |
2684
+ | Email | The `task-updated` that moves the task to `completing`: the provider's word that the platform has accepted the outgoing message; what is left arrives on that publication as `wrapEndsInSeconds` and is counted down from receipt |
2660
2685
  | Other non-audio channels | The `task-updated` that moves the task to `completing` |
2661
2686
 
2662
2687
  **Off voice, `completing` is the provider's word that interaction ended.** There is no audio event to
@@ -2770,7 +2795,7 @@ The record is the call's, not the task's: most of what it holds happened before
2770
2795
  the total of earlier agents' time is `interactionSeconds` and the phase rules are
2771
2796
  `command.phase.interaction`. Hosts and providers adopt these names together; no legacy aliases
2772
2797
  are provided. The manifest's `settleMs` bounds final task completion, not entry into
2773
- the `completing` wrap-up phase. The `recordStep` method and `complete` command keep their names.
2798
+ the `completing` wrap-up phase, and bounds an applied `schedule` the same way. The `recordStep` method and `complete` command keep their names.
2774
2799
 
2775
2800
  Migration from the earlier spellings:
2776
2801
 
@@ -2809,6 +2834,10 @@ Migration from the earlier spellings:
2809
2834
  | TeamMember.listening as this lead's one entry | `MemberListening[]`, every lead's, each with `leadId` (`team.member.listening.leadId`, `.unique`, `.empty`, `.entry`, `.heard`) |
2810
2835
  | a member's history trimmed on the lead's list | the record whole, as the member's desk holds it |
2811
2836
  | durations counted from the screen's clock | the provider's: `timeCheck` required of a provider publishing a running instant (`manifest.timeCheck.required`), `Snapshot.providerTime` with it (`snapshot.providerTime`, `.unexpected`) |
2837
+ | a wrap the desk timed from an event's receipt | `Task.wrapEndsInSeconds` in `completing`, restated (`task.completing.wrapEndsInSeconds`, `.required`, `.unexpected`) |
2838
+ | MemberTask.capabilities, capabilitySource, browsers | gone: the workspace never travels to a lead (`team.member.task.workspace`); the record and the completion terms do |
2839
+ | a member republished on a hold, a mute, a take-over | on every publication of the member's task to the member |
2840
+ | two clients on one login | the later wins; the first is ended with `recovery: "displaced"` |
2812
2841
 
2813
2842
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
2814
2843
  History and report rule names use `history` and `historyReport`; assignment rules use
@@ -2906,7 +2935,7 @@ correlation/conformance only; it is not an application-side timestamp reconcilia
2906
2935
  If two distinct entries would collide under the history's `(step, at)` key, do not invent a time
2907
2936
  or combine them: report the representation conflict. No ambiguous event is silently accepted.
2908
2937
 
2909
- **Handle time is anchored, not restarted.** It runs from the
2938
+ **Interaction time is anchored, not restarted.** It runs from the
2910
2939
  `answered` step's `at` — from the task's first `in-progress` where the provider reports no
2911
2940
  history — until the task's audio ends, and a hold neither pauses nor resets it: the hold's own
2912
2941
  duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
@@ -2918,9 +2947,9 @@ account of a period the agent could not be heard. Each `muted` entry carries `mu
2918
2947
  reported it: `host` for the agent's own Mute, `station` for a headset or system mute the agent application
2919
2948
  observed. No other step has it (`task.history.mutedBy`, `.mutedBy.unexpected`).
2920
2949
 
2921
- **A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
2922
- each end a dial, so the entry carries the `destinationId` it went to and, where an agent application placed it, the
2923
- `dialId` the agent application minted; no other step dialled, and either field on one is refused
2950
+ **A step that dialled says which dial and where.** `conferenced` and `unanswered` each end a dial,
2951
+ and `transferred` a hand-over on the platform's own routing, so the entry carries the `destinationId`
2952
+ it went to and, where an agent application placed the dial, the `dialId` the agent application minted; no other step dialled, and either field on one is refused
2924
2953
  (`task.history.dialId.unexpected`). This is how a dial made before a take-over is placeable
2925
2954
  by whoever holds the task now: a `dial-outcome` naming a `dialId` the agent application never minted is looked
2926
2955
  up in the record, not discarded.
@@ -2930,12 +2959,13 @@ Four rules a provider has to keep:
2930
2959
  - **Report `seconds`; never expect Omni to derive it.** Omni does not subtract one timestamp from
2931
2960
  the next. An entry can be written while its leg is still running, so the arithmetic has no second
2932
2961
  operand, and a provider holding the authoritative number should not have it recomputed from
2933
- instants that may be rounded or clock-skewed.
2962
+ instants that may be rounded or clock-skewed. And once stated, `seconds` is what every screen
2963
+ shows for that leg: a count a screen kept while the leg ran is replaced by it, never kept beside it.
2934
2964
  - **Omit `seconds` while it is unknown. Never send `0`.** A leg still talking is not a zero-second
2935
2965
  conversation, and on live data that is the ordinary case rather than an edge. A zero is rejected.
2936
2966
  - **`by` is a bare `UserId`, and not necessarily an agent.** A lead or a manager takes part
2937
2967
  during an interaction too — a call taken over, a call conferenced in — so the field names whoever it was,
2938
- the same way `ForcedBreak.by` does. It comes from this provider's own directory, the same
2968
+ from the same directory `ForcedBreak.by` names a lead from. It comes from this provider's own directory, the same
2939
2969
  namespace as `AuthenticationState.identity.id` and the team member list, so entries pair
2940
2970
  within a provider and never across one.
2941
2971
  - **A task carries no names.** Omni resolves what to display with `getUserDetails()`. Two people
@@ -3259,7 +3289,7 @@ const taskCapabilities = {
3259
3289
 
3260
3290
  A capability says the control may be offered; the task's phase says whether there is anything to use
3261
3291
  it on. Every control below that acts on the call or the conversation -- everything but `decline`,
3262
- `connectBack` and `outcomes` -- is offered only while the task is `in-progress` or `paused`.
3292
+ `connectBack`, `schedule` and `outcomes` -- is offered only while the task is `in-progress` or `paused`.
3263
3293
  See **Which commands need a capability**.
3264
3294
 
3265
3295
  ### Voice capabilities
@@ -3554,7 +3584,7 @@ there was one, the directory item it went to where there was one, and how the di
3554
3584
  set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `unexplained`.
3555
3585
  `answered` is stated rather than read off somebody appearing on the call, because an agent application that infers
3556
3586
  success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
3557
- before anyone answered -- the agent hanging up while a consult still rings -- which is not a
3587
+ before anyone answered -- the agent hanging up while a conferenced colleague still rings -- which is not a
3558
3588
  `no-answer` the destination never gave. `unexplained` is the switch dropping the call with no cause among
3559
3589
  those named here: a statement about the switch, sent instead of the nearest cause it did not give.
3560
3590
  `reason` is the switch's own words, optional, and shown to the agent attributed to the switch rather
@@ -3571,8 +3601,8 @@ call it belonged to and never against the next one.
3571
3601
 
3572
3602
  The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which an agent application calls when it
3573
3603
  places one; an entry on a task's `onCall`; and a step in its `history`. Since a dialled entry
3574
- is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- a
3575
- connect-back, an idle dialpad call -- or whose entry has already left the room; an
3604
+ is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- an
3605
+ idle dialpad call, which has no task yet -- or whose entry has already left the room; an
3576
3606
  outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
3577
3607
 
3578
3608
  **`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
@@ -3683,7 +3713,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
3683
3713
  | --- | --- |
3684
3714
  | `not-requested` | No request outstanding. |
3685
3715
  | `awaiting-approval` | A person has to decide. The agent is waiting on somebody. |
3686
- | `granted` | A person decided yes. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
3716
+ | `granted` | The request was granted, by a person or by an `automatically-approved` policy. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
3687
3717
  | `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. It waits on a task, so beside no task it is refused (`break.starting-after-task.tasks`): a committed break with nothing outstanding is `on-break`. |
3688
3718
  | `on-break` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `on-break` beside a task is refused as `break.on-break.tasks`. The one exception is a call a lead took over, which the provider assigns whatever break the lead is on -- see **Lead assist**. |
3689
3719
 
@@ -3721,7 +3751,7 @@ The separate authentication and general failure retryAfterMs fields are unchange
3721
3751
 
3722
3752
  Migration: the former in-effect break approval value is now `on-break`. It means the
3723
3753
  agent is on the break, not merely granted permission. The old value is rejected without an
3724
- alias. The task prerequisite, supported listening exceptions and ordering rules are unchanged;
3754
+ alias. The task prerequisite, the take-over exception and ordering rules are unchanged;
3725
3755
  the task-conflict diagnostic is `break.on-break.tasks`. Team member availability already uses
3726
3756
  `on-break` and is unchanged.
3727
3757
 
@@ -3755,7 +3785,7 @@ prerequisite are unchanged.
3755
3785
  The team break command formerly named place is now `force-break`. Hosts and providers must
3756
3786
  adopt the new command together; place and the interim force spelling are rejected without aliases.
3757
3787
  The team break command formerly named release is now `end-forced-break`, and its diagnostic is
3758
- `team.break.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
3788
+ `team.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
3759
3789
  The agent’s `endBreak` now applies to forced breaks as well as requested breaks.
3760
3790
 
3761
3791
  `ForcedBreak` names who forced the break and may say how long it is expected to run yet, as
@@ -3932,15 +3962,16 @@ association and backend authorization are provider responsibilities.
3932
3962
 
3933
3963
  Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
3934
3964
  as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
3935
- checks. Pending and completing tasks still count as work. Only the existing listening exception
3936
- allows tasks beside a break in effect. Validate full task shapes and the envelope separately.
3965
+ checks. Pending and completing tasks still count as work. Only a call a lead took over
3966
+ (`takenOver`) sits beside a break in effect. Validate full task shapes and the envelope separately.
3937
3967
 
3938
3968
  These validators report violations without dispatching, rewriting state or proving freshness.
3939
3969
  Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
3940
3970
  Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
3941
3971
  is not a grant, and no method response overwrites a newer state emission. Rejected promises
3942
3972
  remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
3943
- durable mutually exclusive commit/cancel decision and reconciliation obligations above remain
3973
+ durable mutually exclusive commit/cancel decision and reconciliation obligations under
3974
+ **Coordinating a multi-provider break** remain
3944
3975
  required; per-provider validation does not implement the multi-provider coordinator.
3945
3976
 
3946
3977
  ### `requestBreak(request)`
@@ -4156,9 +4187,9 @@ won, cancel returns `omni.break-already-committed`, and Omni resumes commit reco
4156
4187
  returning the agent to `working`.
4157
4188
 
4158
4189
  A forced break is not rolled back by this algorithm. If one appears during either the request or
4159
- cancellation, Omni follows the forced-break rule on the other providers and commits each grant as
4160
- soon as it reaches `granted`; it does not wait for unanimity because the agent has already
4161
- stopped elsewhere.
4190
+ cancellation, the attempt is abandoned: Omni cancels every grant still outstanding and states
4191
+ `setCapacity({ count: 0 })` on every other usable provider, as **Forced breaks** sets out; no grant
4192
+ is committed, because the agent has already stopped everywhere.
4162
4193
 
4163
4194
  This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
4164
4195
  the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
@@ -4221,7 +4252,7 @@ computes nothing. See **`team-updated`** and its companions.
4221
4252
  | `availability` | Required. What the member is doing now. |
4222
4253
  | `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
4223
4254
  | `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
4224
- | `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, trimmed by the provider. The assignment, title, task type, phase, party, room and audio are what the lead reads, and the task's record -- `history`, steps and totals -- travels whole, exactly as the member's desk holds it, so the lead's screen and the member's show the same record; only the workspace — controls, their source, browsers, completion terms — is the member's and travels only if the provider sends it. `[]` when the member holds none; omitted only where the provider cannot see them. |
4255
+ | `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, less the workspace. The assignment, title, task type, phase, party, room, audio and completion terms -- `completionMode`, `wrapAllowance`, `wrapEndsInSeconds` -- are what the lead reads, and the task's record -- `history`, steps and totals -- travels whole, exactly as the member's desk holds it, so the lead's screen and the member's show the same record. The workspace -- `capabilities`, `capabilitySource`, `browsers` and their URLs -- is the member's desk's alone and never travels (`team.member.task.workspace`). The party's number and email are shown to leads or locked as the provider decides for leads, apart from what it decides for the member -- see **What the queue locks is locked on the whole task**. `[]` when the member holds none; omitted only where the provider cannot see them. |
4225
4256
  | `listening` | Every lead on this member's call — listening, coaching or joined — published to every lead alike: each entry names the lead by `leadId`, which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. The entry naming the signed-in lead is what their own audio opens on. Absent while nobody is on the call, never `[]` (`team.member.listening.*`); a lead is on one member's call at a time (`team.listening.single`), and a lead who is heard is also in the task's `onCall` (`team.member.listening.heard`). |
4226
4257
  | `request` | Present while the member is asking a lead to join one of their calls: which assignment, the note they wrote, since when. The call named is one their `tasks` carry (`team.member.request.assignment`). Absent when they are not asking. See **Lead assist**. |
4227
4258
  | `shift` | The member's own history for the day, about the person rather than any one call: `signedInAt`, `signedOutAt` once it has happened, today's totals as the provider counts them — `talkSeconds`, `holdSeconds`, `breakSeconds`, `tasksHandled`, each present only when the provider knows it — and the day's sign-in, sign-out and break `events`, oldest first. The same `Shift` the member's own snapshot carries, the same numbers from the same count. Omitted where the provider cannot say. |
@@ -4247,6 +4278,15 @@ by it, as a colleague signing in is. `team-member-removed` takes one off. Never
4247
4278
  member, never a task of theirs on its own: a hold on a member's desk is that member republished
4248
4279
  whole on the lead's, and nothing else on the lead's screen moves.
4249
4280
 
4281
+ **Whatever the member's desk hears of a task, every lead hears of the member.** The trigger is
4282
+ not a list of changes but the publication itself: each `task-offered`, `task-updated`,
4283
+ `task-audio-started`, `task-audio-ended` and `task-ended` the provider sends to the member is
4284
+ matched by a `team-member-updated` to every lead with the feature on, carrying the member whole
4285
+ with the task as it now stands, or without it once it has ended. A held leg closing with its
4286
+ `seconds`, a room restated, a phase moving, the wrap left ticking on a restatement: each reaches
4287
+ the lead's copy in the same round it reaches the member's, and the two copies of the record never
4288
+ part for longer than the delay between them.
4289
+
4250
4290
  **The agent's day is on the wire.** The lead sees `shift` totals as the provider counts them, and
4251
4291
  the agent sees the same: `Snapshot.shift` and `shift-updated` carry the agent's own day in the same
4252
4292
  shape, from the same count, so "calls today: 12" is one number on both screens. The desk computes
@@ -4306,7 +4346,7 @@ assignment from the member alone.
4306
4346
  | `{ type: "lead-features", enabled }` | The team feature on or off. See **The lead switches the feature on and off** above. |
4307
4347
  | `{ type: "decide-break-request", memberId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
4308
4348
  | `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
4309
- | `{ type: "force-break", memberId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory and is published on the resulting forced break. |
4349
+ | `{ type: "force-break", memberId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory; the resulting forced break publishes what is left of it as `expectedEndsInSeconds`, restated on every publication. |
4310
4350
  | `{ type: "end-forced-break", memberId }` | Asks the provider to lift a forced-break restriction a lead asked for, by clearing `BreakState.forced`; one the platform imposed is its own to lift (`team.command.endForcedBreak.provider`, `omni.break-forced-by-provider`). The committed break continues; only the agent resumes work. |
4311
4351
  | `{ type: "join", memberId, assignmentId? }` | Answers the member's request: the provider bridges the lead's channel into the call. See **Lead assist**. |
4312
4352
  | `{ type: "decline", memberId, assignmentId?, reason? }` | Refuses the member's request. |
@@ -4368,7 +4408,7 @@ provider clears `leadAssist` from the agent's task** and drops the request from
4368
4408
  else changes; the agent is still on the call.
4369
4409
 
4370
4410
  **On `leave`, the lead's channel drops and the call goes on**: the member's task loses its
4371
- `leadAssist`, the member loses `listening`, and the lead is free. Nothing ended for the member.
4411
+ `leadAssist`, the member loses this lead's entry in `listening`, and the lead is free. Nothing ended for the member.
4372
4412
 
4373
4413
  **On `take-over-call`, the provider moves the call from the member to the lead**, and the lead is
4374
4414
  its new agent. There are two paths to it -- the agent asked and the lead joined, or the lead was
@@ -4376,7 +4416,7 @@ listening unasked -- and one act at the end of either. What follows is the same
4376
4416
 
4377
4417
  | | The member's task | The lead's desk |
4378
4418
  | --- | --- | --- |
4379
- | Take-over | `task-audio-ended`, then `completing`: the member's interaction is over and their wrap runs, exactly as after `end-call`. The audio ends first, as before every voice ending (`stream.taskEnded.audioOpen`). The member loses `listening` on the team member list no later than the offer, and the lead's listening audio closes. | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`, within the lead's capacity. |
4419
+ | Take-over | `task-audio-ended`, then `completing`: the member's interaction is over and their wrap runs, exactly as after `end-call`. The audio ends first, as before every voice ending (`stream.taskEnded.audioOpen`). The member loses this lead's `listening` entry on the team member list no later than the offer, and the lead's listening audio closes. | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`, within the lead's capacity. |
4380
4420
  | The member completes | `task-ended` with `{ type: "taken-over", leadId }`, at the member's own completion. The lead is named by user id, since a lead is not a directory item. | The lead works the call as any agent would, and ends it as any call ends. |
4381
4421
 
4382
4422
  **The taken-over call is offered whatever break the lead is on, and counted like any other.** A
@@ -4449,7 +4489,7 @@ executeTeam({ command: { type: "join-call", memberId: "A-1" } })
4449
4489
  // 5. The lead leaves: the provider publishes the member without L-9's entry, and the task without L-9 in the room; the application closes the audio.
4450
4490
  executeTeam({ command: { type: "leave", memberId: "A-1" } })
4451
4491
  // 6. The member's call ends: the same as leave, from the provider's side.
4452
- // 7. The lead takes the call: the member loses listening no later than the offer, the application
4492
+ // 7. The lead takes the call: the member loses this lead's listening entry no later than the offer, the application
4453
4493
  // closes the listening audio, and the lead's own call arrives as an ordinary assignment marked
4454
4494
  // takenOver, counted against capacity, with its audio following task-audio-started as any call's does.
4455
4495
  // At capacity the take-over is refused and the lead stays listening.
@@ -4610,7 +4650,7 @@ anything to become available.
4610
4650
 
4611
4651
  **A break is not a capacity of zero, and neither is the reverse.** Capacity says how much of this
4612
4652
  agent this provider may carry; a break says they are not working at all. A provider told
4613
- `count: 0` shows an agent whose capacity is elsewhere, `on-task`; a provider told a break shows
4653
+ `count: 0` shows an agent whose capacity is elsewhere, `elsewhere`; a provider told a break shows
4614
4654
  `on-break`. Collapsing the two would leave a provider unable to tell an agent working on another
4615
4655
  provider from an agent who has gone to lunch, and only one of those needs a reason, a decision and
4616
4656
  a return.
@@ -4836,7 +4876,7 @@ declared:
4836
4876
  | `complete` | A wrap to cut short: any task but a `provider-automatic` one with `wrapAllowance: 0`, under either completion mode (`command.complete.wrapAllowance`). The `outcomes` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. What travels is what the capability published: a code from its list where it has one (`command.complete.outcome.unknown`), a code at all where it requires one (`.outcome.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no outcomes (`.outcome.unexpected`). |
4837
4877
  | `connect-back` | The `connectBack` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
4838
4878
  | `schedule` | The `schedule` capability, on any channel, in `in-progress`, `paused` or `completing`: a follow-up is promised on the call and written up in wrap alike. The manifest declares `calendar` for it to land on. |
4839
- | `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
4879
+ | `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with `stage: "requested"`. |
4840
4880
  | `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
4841
4881
  | `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying a non-empty string for each `required` prompt field and strings for any optional fields supplied (`command.custom.prompt`). A toggle carries its target `on` boolean (`command.custom.on`). |
4842
4882
  | Everything else | Its own named capability. |
@@ -4915,6 +4955,8 @@ react rather than only display the message:
4915
4955
  | `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
4916
4956
  | `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
4917
4957
  | `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
4958
+ | `omni.recording-unsettled` | A recording command the provider could not settle either way: a settled `failed`, never a promise left unresolved. See **Independent task recording**. |
4959
+ | `omni.break-forced-by-provider` | A lead asked to lift a break the platform imposed; the platform lifts its own. See **Forced breaks**. |
4918
4960
 
4919
4961
  They are published as `OMNI_FAILURE_CODES`.
4920
4962
 
@@ -4947,8 +4989,8 @@ The current exceptions and their reasons are:
4947
4989
 
4948
4990
  | Case | Exception and reason |
4949
4991
  | --- | --- |
4950
- | Agent application display of provider deadlines (`expiresInSeconds`, `previewEndsInSeconds`, wrap deadline) | Each is seconds from the publication that carried it, counted down on the desk from receipt: the provider's own arithmetic, so no clock is compared and the transport delay is the only error. The countdown estimates the display only; it does not establish that the provider acted. Without usable time, show timing uncertainty. |
4951
- | Agent application-triggered preview deadline | A bounded clock estimate with monotonic aging may schedule the agent application's Call command because the provider owns the deadline but the agent application owns the trigger. Do not trigger before the deadline is known to have passed; invalidate on clock discontinuity and reconcile when uncertain. |
4992
+ | Agent application display of provider deadlines (`expiresInSeconds`, `previewEndsInSeconds`, `wrapEndsInSeconds`) | Each is seconds from the publication that carried it, counted down on the desk from receipt: the provider's own arithmetic, so no clock is compared and the transport delay is the only error. The countdown estimates the display only; it does not establish that the provider acted. Without usable time, show timing uncertainty. |
4993
+ | Agent application-triggered preview end | The desk counts `previewEndsInSeconds` down from receipt and issues `dial` when it reaches zero: the provider owns the deadline and the desk owns the trigger. It does not dial before the countdown has run out, and a reload restarts it from the next publication. |
4952
4994
  | Recording evidence expiry | A bounded observer-domain clock estimate with monotonic aging may assess freshness because observation and expiry belong to the recorder's clock. If time cannot be trusted, recording state is unknown; never renew evidence from receipt or replay. |
4953
4995
  | Unknown recording state | `observedAt` and `validUntil` are absent because there is no confirmed observation. The containing provider event still has its own `occurredAt`; that publication is not a recorder observation. |
4954
4996
  | Direct snapshot reads and method requests/results | These are reads/operations, not event envelopes, and their current types have no general event timestamp. A snapshot event still carries `occurredAt`; embedded source instants remain unchanged. Do not treat a method result or read completion time as an occurrence boundary. |
@@ -4966,8 +5008,14 @@ source-measured durations where required; timestamp subtraction is not a substit
4966
5008
  Two screens watching the same agent -- their own desk, their lead's, a manager's on another floor
4967
5009
  -- must show the same numbers. Delay between them is fine; arithmetic that differs is not. So
4968
5010
  nothing the desk shows is its own count, with one exception: the running duration of the active
4969
- task or call, which each screen times from the moment it saw the call begin, and which two screens
4970
- may therefore disagree on, since their clocks differ. Everything else comes from the wire.
5011
+ task or call, which a screen times itself from the moment it saw the call begin -- the
5012
+ `task-audio-started` on a softphone, the answer it sent on a desk phone -- and which two such
5013
+ screens may therefore disagree on, since their clocks differ. That is the whole exception. A call
5014
+ a screen found already up -- on a connect or reconnect snapshot, on a lead's list, after a reload
5015
+ -- is counted from the provider's `answered` step's `at` against the provider's clock, like every
5016
+ other duration, so a reloaded desk shows 12:40 beside the lead's 12:40 and not 0:05. And a leg
5017
+ that has closed shows the provider's `seconds` on every screen, never the count a screen kept.
5018
+ Everything else comes from the wire.
4971
5019
 
4972
5020
  Durations off the active call -- how long a member has been `on-break` from their `since`, how
4973
5021
  long a lead has been listening, how long since `signedInAt` -- are counted from the provider's
@@ -5085,9 +5133,9 @@ direction — the provider asking Omni to reconcile — and neither replaces the
5085
5133
 
5086
5134
  Carries a complete `Snapshot` after reconnect or when the provider explicitly requests
5087
5135
  reconciliation. `reason` is `reconnected` or `provider-requested`. Omni replaces the provider's
5088
- current status, break state, tasks, contacts, scheduled activities and team member list with this
5089
- snapshot. It carries what the login's capabilities call for — a team member list for a lead, on every
5090
- snapshot — and nothing they do not; a capability is withdrawn by a republished `authenticated`,
5136
+ current status, break state, tasks, contacts, scheduled activities, the agent's day and the team
5137
+ member list with this snapshot. It carries what the login and its switches call for — the whole
5138
+ team for a lead with the feature on, on every snapshot — and nothing they do not; a capability is withdrawn by a republished `authenticated`,
5091
5139
  never by an omission from a snapshot.
5092
5140
 
5093
5141
  ### `transport-status`
@@ -5116,6 +5164,11 @@ died; the agent application knows how to run a login. `recovery` joins the two:
5116
5164
  disposes the errored connection with `disconnect()` and calls `connect()` afresh, as for
5117
5165
  `reconnect`, and the fresh snapshot re-establishes state. The adapter tears nothing down itself
5118
5166
  and expects nothing to carry on: the connection that reported `error` is finished.
5167
+ - **`displaced`** — another client connected on this login and took it: the agent moved desks.
5168
+ Nothing revives this connection on its own -- Omni does not reconnect, or it would take the
5169
+ login straight back from the desk the agent just sat down at -- and the desk shows the agent
5170
+ they are signed in elsewhere, with a way to take the login back, which is a `connect()` that
5171
+ displaces the other. See **`Adapter.connect(context)`**.
5119
5172
 
5120
5173
  **Patience is the agent application's.** An adapter in `connecting` retries for as long as it takes and never
5121
5174
  has to decide when to stop. Omni owns giving up: after however long it chooses to wait, it may
@@ -5245,8 +5298,10 @@ wrapped audio has nothing left to end.
5245
5298
 
5246
5299
  ### `task-audio-ended`
5247
5300
 
5248
- Signals that a task's real-time audio ended. For voice and similar channels, this starts the fixed
5249
- completion timer. It does not remove the task, and it ends only audio that `task-audio-started` — or
5301
+ Signals that a task's real-time audio ended. For voice and similar channels, the `completing`
5302
+ publication that follows carries `wrapEndsInSeconds` where a wrap allowance is stated: under
5303
+ `provider-automatic` the provider ends the task when it runs out, under `agent-command` it is shown
5304
+ and nothing more. It does not remove the task, and it ends only audio that `task-audio-started` — or
5250
5305
  a task carried with `audio: "started"` — attached.
5251
5306
 
5252
5307
  ### `task-ended`
@@ -5340,8 +5395,8 @@ turned the feature off (`event.team.features`).
5340
5395
  One member, whole, as they now stand: their availability, their tasks, their listening, their
5341
5396
  shift, their request. It replaces the member of that `id` on the lead's list, or adds them where
5342
5397
  the list did not carry them, so applying it twice changes nothing. It comes only after the whole
5343
- team has (`stream.team.baseline`), and it is what a member's hold, answer, mute or take-over
5344
- publishes to the lead.
5398
+ team has (`stream.team.baseline`), and it is what every publication of a member's task to the
5399
+ member publishes to the lead, as **Team leads** sets out: never fewer.
5345
5400
 
5346
5401
  ### `team-member-removed`
5347
5402
 
@@ -5386,7 +5441,7 @@ issued by each provider independently, so two providers will eventually issue th
5386
5441
  different people. Encode and join before storing or comparing.
5387
5442
 
5388
5443
  Use it for every `UserId` — `history[].by`, team members, `memberId` on a
5389
- lead command, `ForcedBreak.by`. A bare one is only ever compared against another from the **same** provider; anything
5444
+ lead command, `ForcedBreak.by` where it names a person rather than `"provider"`. A bare one is only ever compared against another from the **same** provider; anything
5390
5445
  wider goes through this key.
5391
5446
 
5392
5447
  ### `sameCapabilities(a, b)`
@@ -5429,7 +5484,7 @@ once. A violation carries a stable `rule` id such as `task.browser.url.scheme`,
5429
5484
  found at such as `snapshot.tasks[0].browsers[1].url`, and a `message`.
5430
5485
 
5431
5486
  Some rules need to know who is reading. `validateTeamMembers`, `validateSnapshot`, and
5432
- `validateEventEnvelope` take an optional final `{ self, capabilities }` — the signed-in agent's
5487
+ `validateEventEnvelope` take an optional final `{ self, capabilities, leadFeatures }` — the signed-in agent's
5433
5488
  `AuthenticationState.identity.id` and their login's `capabilities`. Given `self`, a team member list that
5434
5489
  carries that agent reports `team.member.self`, on a `team-member-updated` and a `team-member-removed` as on the list. Given `capabilities`, a team member list published to a login that does not lead
5435
5490
  reports `team.unentitled`, and a team event to such a login reports `event.team.capability`; given
@@ -5622,7 +5677,6 @@ cannot be established from TypeScript structure alone.
5622
5677
  | `assertForcedBreakStopsTheRest(forcedOn, candidates, stopped)` | A forced break on one provider stops the agent everywhere else: capacity zero stated on every other usable provider holding capacity, nothing on the forcing one, nothing on a dead login. See **Forced breaks**. |
5623
5678
  | `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `on-break` only once nothing is outstanding — never beside a task, never later than the step that has none. |
5624
5679
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
5625
- | `assertWrapTimeout(task, audioEndedAt, deadline, toleranceMs?)` | The wrap deadline equals audio end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |
5626
5680
  | `assertBrowserSessionIsolation(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
5627
5681
  | `assertNoBrowserSessionKeyCollisions(scenarios)` | No two distinct scenarios derive the same session key. Feed it adversarial names. |
5628
5682
 
@@ -5716,9 +5770,9 @@ Immediately before dispatch, additionally use `validateRecordingRequest` against
5716
5770
  task and explicit observer-domain time. For agent application commands also supply the current agent application declaration,
5717
5771
  full agent application report and softphone context. The same checks must run at the executing boundary;
5718
5772
  client validation alone is not authorization. The executor serializes operations on each recorder
5719
- and durably correlates request identity with scope, action and outcome: an exact retry refers to the
5720
- same operation; reuse with different contents is refused. It never silently retries an ambiguous
5721
- operation. Implementations without reliable request correlation must refuse controls.
5773
+ and compares `observationId` and `recordingId` against its latest evidence before acting: a
5774
+ command against superseded evidence is refused, and nothing is retried on the executor's own
5775
+ account.
5722
5776
 
5723
5777
  Keep command progress in agent application UI separately from authoritative state. Applied means the requested
5724
5778
  recording transition and any retain/discard effect actually completed; publish the confirming task or agent application report before
@@ -5782,7 +5836,7 @@ never claim delivery or replay the notice into a different call.
5782
5836
 
5783
5837
  For a command under this guarantee, applied requires both the recording action and its announcement
5784
5838
  to succeed. If capture changes but announcement delivery fails, publish the actual recording state,
5785
- report the announcement failure visibly and reject with unknown command outcome. Do not claim no
5839
+ report the announcement failure visibly and answer `failed` under `omni.recording-unsettled`. Do not claim no
5786
5840
  effect, automatically repeat the recording action or silently switch to an agent-only notice.
5787
5841
 
5788
5842
  `validateHostRecording` checks the declaration, including its true-or-absent guarantee. Actual audio
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",