@xema/omni-protocol 0.1.104 → 0.1.105

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;
@@ -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
@@ -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;
@@ -882,7 +883,15 @@ 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
@@ -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 }
@@ -2073,6 +2080,12 @@ Creates one live provider connection for the signed-in agent.
2073
2080
  - Must resolve only when the connection can provide a meaningful snapshot.
2074
2081
  - May reject for authentication, configuration, or startup failure.
2075
2082
  - Must not create a second agent session merely because the underlying transport reconnects.
2083
+ - **One client at a time per login, and the later one wins.** A second `connect()` on a login
2084
+ that already has a live client is the agent moving desks -- a desk left open at work, signed in
2085
+ from home. The provider takes the new connection, and ends the first with `transport-status`
2086
+ `error` and `recovery: "displaced"`, so the first desk stops stating capacity and reporting legs
2087
+ and shows the agent they are signed in elsewhere. Two live clients on one login would be two
2088
+ sources for one agent, and the lead's list would follow whichever spoke last.
2076
2089
  - The returned connection owns reconnect until Omni calls `disconnect()` or aborts `context.signal`.
2077
2090
  - May be called again on the same login after that: once per `Connection`, not once per login.
2078
2091
  Omni disposes a connection whose `error` named `recovery: "reconnect"` with `disconnect()` and
@@ -2413,7 +2426,8 @@ time. Runtime conformance checks also require the task channel to match its prov
2413
2426
  | `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
2427
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
2415
2428
  | `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`). |
2429
+ | `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`). |
2430
+ | `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
2431
  | `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
2432
  | `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
2433
  | `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**. |
@@ -2649,6 +2663,15 @@ nothing acts on it -- the task waits for `complete` however long that takes, wit
2649
2663
  collected whenever the agent gives them. A provider that would end the task at a time is
2650
2664
  `provider-automatic`.
2651
2665
 
2666
+ **What is left travels with the task.** The allowance is the length; `wrapEndsInSeconds` is how
2667
+ much of it remains, stated on the `completing` task and restated on every publication that
2668
+ carries it, as `previewEndsInSeconds` is on a preview. A desk that reloads mid-wrap finds the
2669
+ number on the snapshot; a lead's screen finds it on the member; neither starts a clock of its own,
2670
+ and both count down from receipt. Under `provider-automatic` the provider ends the task when it
2671
+ reaches zero; under `agent-command` it is the expected wrap left, `0` once overrun, and nothing
2672
+ acts on it. Off voice, where `completing` is the provider's word that interaction ended, the field
2673
+ arrives with that word, so the clock starts nowhere else.
2674
+
2652
2675
  `wrapAllowance` is fixed, and when it starts depends on whether the channel carries real-time
2653
2676
  audio:
2654
2677
 
@@ -2809,6 +2832,10 @@ Migration from the earlier spellings:
2809
2832
  | TeamMember.listening as this lead's one entry | `MemberListening[]`, every lead's, each with `leadId` (`team.member.listening.leadId`, `.unique`, `.empty`, `.entry`, `.heard`) |
2810
2833
  | a member's history trimmed on the lead's list | the record whole, as the member's desk holds it |
2811
2834
  | 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`) |
2835
+ | a wrap the desk timed from an event's receipt | `Task.wrapEndsInSeconds` in `completing`, restated (`task.completing.wrapEndsInSeconds`, `.required`, `.unexpected`) |
2836
+ | MemberTask.capabilities, capabilitySource, browsers | gone: the workspace never travels to a lead (`team.member.task.workspace`); the record and the completion terms do |
2837
+ | a member republished on a hold, a mute, a take-over | on every publication of the member's task to the member |
2838
+ | two clients on one login | the later wins; the first is ended with `recovery: "displaced"` |
2812
2839
 
2813
2840
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
2814
2841
  History and report rule names use `history` and `historyReport`; assignment rules use
@@ -2930,7 +2957,8 @@ Four rules a provider has to keep:
2930
2957
  - **Report `seconds`; never expect Omni to derive it.** Omni does not subtract one timestamp from
2931
2958
  the next. An entry can be written while its leg is still running, so the arithmetic has no second
2932
2959
  operand, and a provider holding the authoritative number should not have it recomputed from
2933
- instants that may be rounded or clock-skewed.
2960
+ instants that may be rounded or clock-skewed. And once stated, `seconds` is what every screen
2961
+ shows for that leg: a count a screen kept while the leg ran is replaced by it, never kept beside it.
2934
2962
  - **Omit `seconds` while it is unknown. Never send `0`.** A leg still talking is not a zero-second
2935
2963
  conversation, and on live data that is the ordinary case rather than an edge. A zero is rejected.
2936
2964
  - **`by` is a bare `UserId`, and not necessarily an agent.** A lead or a manager takes part
@@ -4221,7 +4249,7 @@ computes nothing. See **`team-updated`** and its companions.
4221
4249
  | `availability` | Required. What the member is doing now. |
4222
4250
  | `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
4223
4251
  | `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. |
4252
+ | `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
4253
  | `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
4254
  | `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
4255
  | `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 +4275,15 @@ by it, as a colleague signing in is. `team-member-removed` takes one off. Never
4247
4275
  member, never a task of theirs on its own: a hold on a member's desk is that member republished
4248
4276
  whole on the lead's, and nothing else on the lead's screen moves.
4249
4277
 
4278
+ **Whatever the member's desk hears of a task, every lead hears of the member.** The trigger is
4279
+ not a list of changes but the publication itself: each `task-offered`, `task-updated`,
4280
+ `task-audio-started`, `task-audio-ended` and `task-ended` the provider sends to the member is
4281
+ matched by a `team-member-updated` to every lead with the feature on, carrying the member whole
4282
+ with the task as it now stands, or without it once it has ended. A held leg closing with its
4283
+ `seconds`, a room restated, a phase moving, the wrap left ticking on a restatement: each reaches
4284
+ the lead's copy in the same round it reaches the member's, and the two copies of the record never
4285
+ part for longer than the delay between them.
4286
+
4250
4287
  **The agent's day is on the wire.** The lead sees `shift` totals as the provider counts them, and
4251
4288
  the agent sees the same: `Snapshot.shift` and `shift-updated` carry the agent's own day in the same
4252
4289
  shape, from the same count, so "calls today: 12" is one number on both screens. The desk computes
@@ -4947,7 +4984,7 @@ The current exceptions and their reasons are:
4947
4984
 
4948
4985
  | Case | Exception and reason |
4949
4986
  | --- | --- |
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. |
4987
+ | 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. |
4951
4988
  | 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. |
4952
4989
  | 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
4990
  | 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. |
@@ -4966,8 +5003,14 @@ source-measured durations where required; timestamp subtraction is not a substit
4966
5003
  Two screens watching the same agent -- their own desk, their lead's, a manager's on another floor
4967
5004
  -- must show the same numbers. Delay between them is fine; arithmetic that differs is not. So
4968
5005
  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.
5006
+ task or call, which a screen times itself from the moment it saw the call begin -- the
5007
+ `task-audio-started` on a softphone, the answer it sent on a desk phone -- and which two such
5008
+ screens may therefore disagree on, since their clocks differ. That is the whole exception. A call
5009
+ a screen found already up -- on a connect or reconnect snapshot, on a lead's list, after a reload
5010
+ -- is counted from the provider's `answered` step's `at` against the provider's clock, like every
5011
+ other duration, so a reloaded desk shows 12:40 beside the lead's 12:40 and not 0:05. And a leg
5012
+ that has closed shows the provider's `seconds` on every screen, never the count a screen kept.
5013
+ Everything else comes from the wire.
4971
5014
 
4972
5015
  Durations off the active call -- how long a member has been `on-break` from their `since`, how
4973
5016
  long a lead has been listening, how long since `signedInAt` -- are counted from the provider's
@@ -5116,6 +5159,11 @@ died; the agent application knows how to run a login. `recovery` joins the two:
5116
5159
  disposes the errored connection with `disconnect()` and calls `connect()` afresh, as for
5117
5160
  `reconnect`, and the fresh snapshot re-establishes state. The adapter tears nothing down itself
5118
5161
  and expects nothing to carry on: the connection that reported `error` is finished.
5162
+ - **`displaced`** — another client connected on this login and took it: the agent moved desks.
5163
+ Nothing revives this connection on its own -- Omni does not reconnect, or it would take the
5164
+ login straight back from the desk the agent just sat down at -- and the desk shows the agent
5165
+ they are signed in elsewhere, with a way to take the login back, which is a `connect()` that
5166
+ displaces the other. See **`Adapter.connect(context)`**.
5119
5167
 
5120
5168
  **Patience is the agent application's.** An adapter in `connecting` retries for as long as it takes and never
5121
5169
  has to decide when to stop. Omni owns giving up: after however long it chooses to wait, it may
@@ -5340,8 +5388,8 @@ turned the feature off (`event.team.features`).
5340
5388
  One member, whole, as they now stand: their availability, their tasks, their listening, their
5341
5389
  shift, their request. It replaces the member of that `id` on the lead's list, or adds them where
5342
5390
  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.
5391
+ team has (`stream.team.baseline`), and it is what every publication of a member's task to the
5392
+ member publishes to the lead, as **Team leads** sets out: never fewer.
5345
5393
 
5346
5394
  ### `team-member-removed`
5347
5395
 
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.105",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",