@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 +16 -8
- package/dist/validation.js +24 -7
- package/guide.md +61 -13
- package/package.json +1 -1
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
|
-
|
|
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,
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
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/validation.js
CHANGED
|
@@ -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
|
|
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
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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.
|
|
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
|
|
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,
|
|
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`,
|
|
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
|
|
4970
|
-
|
|
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
|
|
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
|
|