@xema/omni-protocol 0.1.100 → 0.1.101

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
@@ -866,7 +866,7 @@ export type TaskLeadAssist = {
866
866
  * On a task that reached this agent by a lead's take-over: which member the call was taken from,
867
867
  * and when. The provider moves the call from one agent to another, and the lead is the new agent,
868
868
  * so the task is an ordinary assignment with the call's history -- offered whatever break the lead
869
- * is on, counted against no capacity, provided their voice channel is free.
869
+ * is on, counted against their capacity like any other call, provided they are under it and their voice channel is free.
870
870
  */
871
871
  export interface TaskTakenOver {
872
872
  memberId: UserId;
@@ -1041,7 +1041,7 @@ export type VoiceTaskCommand = {
1041
1041
  | {
1042
1042
  type: "end-call";
1043
1043
  }
1044
- /** End the caller channel. Gated by `terminateCall`. */
1044
+ /** End the whole call at the provider: every channel on it, the agent's, the caller's and anyone else's. Gated by `terminateCall`. */
1045
1045
  | {
1046
1046
  type: "terminate-call";
1047
1047
  }
@@ -1269,6 +1269,8 @@ export interface TeamMember {
1269
1269
  tasks?: MemberTask[];
1270
1270
  /** Present while this lead is on the member's call, listening, coaching or joined; absent otherwise. */
1271
1271
  listening?: MemberListening;
1272
+ /** The member's own history for the day: sign-in, sign-out, breaks and productivity. Omitted where the provider cannot say. */
1273
+ shift?: MemberShift;
1272
1274
  }
1273
1275
  /** A member asking this lead to join their call, named by the member and the assignment. */
1274
1276
  export interface LeadRequest {
@@ -1309,11 +1311,36 @@ export type MemberTask<C extends Channel = Channel> = {
1309
1311
  takenOver?: never;
1310
1312
  audio?: never;
1311
1313
  });
1312
- /** The lead on this member's call, in the mode they are heard, since when. */
1314
+ /**
1315
+ * The lead on this member's call: which of the member's calls, in the mode they are heard, since
1316
+ * when. Its appearance on the team member list is the provider's word that the lead's audio is up,
1317
+ * and the application opens `openAudio` on the assignment named; its disappearance closes it.
1318
+ */
1313
1319
  export interface MemberListening {
1320
+ assignmentId: AssignmentId;
1314
1321
  mode: ListeningMode;
1315
1322
  since: IsoTimestamp;
1316
1323
  }
1324
+ export type ShiftEventKind = "signed-in" | "signed-out" | "break-started" | "break-ended";
1325
+ export declare const SHIFT_EVENT_KINDS: readonly ["signed-in", "signed-out", "break-started", "break-ended"];
1326
+ export interface ShiftEvent {
1327
+ at: IsoTimestamp;
1328
+ kind: ShiftEventKind;
1329
+ }
1330
+ /**
1331
+ * The member's own history for the day, about the person rather than any one call: when they
1332
+ * signed in and out, today's totals as the provider counts them, each present only when the
1333
+ * provider knows it, and the day's sign-in, sign-out and break events, oldest first.
1334
+ */
1335
+ export interface MemberShift {
1336
+ signedInAt: IsoTimestamp;
1337
+ signedOutAt?: IsoTimestamp;
1338
+ talkSeconds?: DurationSeconds;
1339
+ holdSeconds?: DurationSeconds;
1340
+ breakSeconds?: DurationSeconds;
1341
+ tasksHandled?: number;
1342
+ events?: ShiftEvent[];
1343
+ }
1317
1344
  /**
1318
1345
  * The lead's view of their team: published to a login that declares `capabilities.lead` while the
1319
1346
  * lead has the team feature on, on every snapshot, and to nobody else. The provider keeps it
@@ -1588,20 +1615,6 @@ export type ProviderEvent<C extends Channel = Channel> = {
1588
1615
  } | {
1589
1616
  type: "task-audio-ended";
1590
1617
  assignmentId: AssignmentId;
1591
- }
1592
- /**
1593
- * The lead's channel is in a member's call -- listening, coaching or joined -- and its audio should
1594
- * attach: on a softphone the application opens audio on the member's assignment as it does for a
1595
- * task. Voice, to a login that declares `lead`, never to the member.
1596
- */
1597
- | {
1598
- type: "team-audio-started";
1599
- memberId: UserId;
1600
- assignmentId: AssignmentId;
1601
- } | {
1602
- type: "team-audio-ended";
1603
- memberId: UserId;
1604
- assignmentId: AssignmentId;
1605
1618
  } | {
1606
1619
  type: "task-ended";
1607
1620
  assignmentId: AssignmentId;
package/dist/index.js CHANGED
@@ -95,6 +95,7 @@ export const BREAK_KINDS = [
95
95
  "short-break", "meal", "rest", "training", "coaching",
96
96
  "meeting", "administrative", "technical", "personal", "other",
97
97
  ];
98
+ export const SHIFT_EVENT_KINDS = ["signed-in", "signed-out", "break-started", "break-ended"];
98
99
  export const OMNI_FAILURE_CODES = [
99
100
  "omni.not-authenticated",
100
101
  "omni.capability-not-enabled",
package/dist/testing.d.ts CHANGED
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
7
7
  * rules -- each optional part of a task, each optional part of the break state and team member list, each
8
8
  * declared contribution, and each event type.
9
9
  */
10
- declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "team.tasks", "team.listening", "contacts", "calendar", "team.policies"];
10
+ declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "team.tasks", "team.listening", "team.shift", "contacts", "calendar", "team.policies"];
11
11
  export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
12
12
  export interface AdapterContractResult {
13
13
  events: ProviderEventEnvelope[];
package/dist/testing.js CHANGED
@@ -27,6 +27,7 @@ const STATE_SUBJECTS = [
27
27
  "team.requests",
28
28
  "team.tasks",
29
29
  "team.listening",
30
+ "team.shift",
30
31
  "contacts",
31
32
  "calendar",
32
33
  "team.policies",
@@ -36,7 +37,7 @@ const STATE_SUBJECTS = [
36
37
  const EVENT_TYPES = {
37
38
  snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
38
39
  "task-audio-started": true, "task-audio-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
39
- "team-updated": true, "team-audio-started": true, "team-audio-ended": true, "contacts-updated": true, "calendar-updated": true,
40
+ "team-updated": true, "contacts-updated": true, "calendar-updated": true,
40
41
  };
41
42
  const CONTRACT_SUBJECTS = [
42
43
  ...STATE_SUBJECTS,
@@ -102,6 +103,8 @@ function observeTeam(value, seen) {
102
103
  seen.add("team.tasks");
103
104
  if (member.listening !== undefined)
104
105
  seen.add("team.listening");
106
+ if (member.shift !== undefined)
107
+ seen.add("team.shift");
105
108
  }
106
109
  if (isRecord(value.policies) && Object.keys(value.policies).length > 0)
107
110
  seen.add("team.policies");
@@ -9,7 +9,7 @@
9
9
  // Each list is pinned to its type both ways -- a member the type lacks, or a member the list
10
10
  // lacks, fails to compile -- so what the validators accept cannot drift from what the
11
11
  // declarations say.
12
- import { RECORDING_ACTIONS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, HISTORY_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, TASK_COMMAND_NAMES, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
12
+ import { RECORDING_ACTIONS, SHIFT_EVENT_KINDS, ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, HISTORY_STEPS_THAT_DIAL, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, TASK_COMMAND_NAMES, negotiateProtocolVersion, DEFAULT_LEVELS, effectiveLevels, } from "./index.js";
13
13
  export class ProtocolConformanceError extends Error {
14
14
  violations;
15
15
  constructor(violations, summary = "Adapter violates the Omni protocol") {
@@ -1671,10 +1671,17 @@ function validateTeamMembersInto(teamMembers, path, context, into) {
1671
1671
  into.add("team.member.listening.shape", `${at}.listening`, "listening must be an object when present");
1672
1672
  }
1673
1673
  else {
1674
+ const listened = member.listening;
1675
+ if (into.require(isAssignmentId(member.listening.assignmentId), "team.member.listening.assignmentId", `${at}.listening.assignmentId`, "listening names which of the member's calls the lead is on: the application opens the lead's audio on it") && Array.isArray(member.tasks)) {
1676
+ into.require(member.tasks.some((task) => isPlainObject(task) && task.assignmentId === listened.assignmentId), "team.member.listening.assignment", `${at}.listening.assignmentId`, "the call the lead is on is one the member's tasks carry");
1677
+ }
1674
1678
  into.oneOf(member.listening.mode, LISTENING_MODES, "team.member.listening.mode", `${at}.listening.mode`);
1675
1679
  into.timestamp(member.listening.since, "team.member.listening.since", `${at}.listening.since`);
1676
1680
  }
1677
1681
  }
1682
+ // The member's own history for the day: about the person, not any one call.
1683
+ if (member.shift !== undefined)
1684
+ validateShiftInto(member.shift, `${at}.shift`, into);
1678
1685
  });
1679
1686
  // One member's call at a time.
1680
1687
  const listened = teamMembers.members.filter((member) => isPlainObject(member) && member.listening !== undefined);
@@ -1953,14 +1960,6 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1953
1960
  into.require(context.leadFeatures !== false, "event.team.features", `${at}.team`, "the lead turned the team feature off, so nothing of the team reaches them");
1954
1961
  validateTeamMembersInto(event.team, `${at}.team`, { ...context, levels, channel: isChannel(channel) ? channel : undefined }, into);
1955
1962
  break;
1956
- case "team-audio-started":
1957
- case "team-audio-ended":
1958
- // The lead's channel in a member's call: audio the application opens on the member's assignment.
1959
- into.require(channel === "voice", "event.audio.channel", `${at}.type`, "only a voice provider publishes audio transitions");
1960
- into.require(context.capabilities === undefined || context.capabilities.lead === true, "event.teamAudio.capability", `${at}.type`, "team audio reaches a login that declares lead, and nobody else");
1961
- into.require(isUserId(event.memberId), "event.teamAudio.memberId", `${at}.memberId`, "team audio names the member whose call the lead is on");
1962
- into.require(isAssignmentId(event.assignmentId), "event.teamAudio.assignmentId", `${at}.assignmentId`, "team audio names the member's assignment");
1963
- break;
1964
1963
  case "contacts-updated":
1965
1964
  into.require(idle.contacts === true, "event.contacts.capability", `${at}.contacts`, "contacts-updated requires the contacts idle capability");
1966
1965
  if (!Array.isArray(event.contacts)) {
@@ -2021,6 +2020,43 @@ function validatePreferencesInto(value, path, into, levels) {
2021
2020
  });
2022
2021
  }
2023
2022
  /** The team's policy per capability as the lead sees it: the setting, who set it, who locked it. */
2023
+ function validateShiftInto(value, path, into) {
2024
+ if (!isPlainObject(value)) {
2025
+ into.add("team.member.shift.shape", path, "shift must be an object when present");
2026
+ return;
2027
+ }
2028
+ const signedIn = into.timestamp(value.signedInAt, "team.member.shift.signedInAt", `${path}.signedInAt`);
2029
+ if (value.signedOutAt !== undefined && into.timestamp(value.signedOutAt, "team.member.shift.signedOutAt", `${path}.signedOutAt`) && signedIn) {
2030
+ into.require(Date.parse(value.signedOutAt) >= Date.parse(value.signedInAt), "team.member.shift.signedOutAt.order", `${path}.signedOutAt`, "a member signs out after signing in");
2031
+ }
2032
+ for (const field of ["talkSeconds", "holdSeconds", "breakSeconds"]) {
2033
+ if (value[field] !== undefined)
2034
+ into.require(isDurationSeconds(value[field]), `team.member.shift.${field}`, `${path}.${field}`, `${field} must be a whole number of seconds, zero or more, or omitted when unknown`);
2035
+ }
2036
+ if (value.tasksHandled !== undefined)
2037
+ into.require(Number.isInteger(value.tasksHandled) && value.tasksHandled >= 0, "team.member.shift.tasksHandled", `${path}.tasksHandled`, "tasksHandled must be a whole number, zero or more, or omitted when unknown");
2038
+ if (value.events !== undefined) {
2039
+ if (!Array.isArray(value.events)) {
2040
+ into.add("team.member.shift.events.shape", `${path}.events`, "events must be an array when present");
2041
+ return;
2042
+ }
2043
+ let previous;
2044
+ value.events.forEach((event, index) => {
2045
+ const at = `${path}.events[${index}]`;
2046
+ if (!isPlainObject(event)) {
2047
+ into.add("team.member.shift.event.shape", at, "each shift event must be an object");
2048
+ return;
2049
+ }
2050
+ into.oneOf(event.kind, SHIFT_EVENT_KINDS, "team.member.shift.event.kind", `${at}.kind`);
2051
+ if (into.timestamp(event.at, "team.member.shift.event.at", `${at}.at`)) {
2052
+ const instant = Date.parse(event.at);
2053
+ if (previous !== undefined && instant < previous)
2054
+ into.add("team.member.shift.events.order", `${at}.at`, "shift events are oldest first; this event is earlier than the one before it");
2055
+ previous = instant;
2056
+ }
2057
+ });
2058
+ }
2059
+ }
2024
2060
  function validateTeamPoliciesInto(value, path, into, levels) {
2025
2061
  if (!isPlainObject(value)) {
2026
2062
  into.add("team.policies.shape", path, "policies must be an object keyed by capability");
package/guide.md CHANGED
@@ -977,6 +977,7 @@ type TeamMember = {
977
977
  break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
978
978
  tasks?: MemberTask[];
979
979
  listening?: MemberListening;
980
+ shift?: MemberShift;
980
981
  };
981
982
 
982
983
  type LeadRequest = {
@@ -1006,10 +1007,28 @@ type MemberTask<C extends Channel = Channel> = {
1006
1007
  : { onCall?: never; leadAssist?: never; takenOver?: never; audio?: never });
1007
1008
 
1008
1009
  type MemberListening = {
1010
+ assignmentId: AssignmentId;
1009
1011
  mode: ListeningMode;
1010
1012
  since: IsoTimestamp;
1011
1013
  };
1012
1014
 
1015
+ type ShiftEventKind = "signed-in" | "signed-out" | "break-started" | "break-ended";
1016
+
1017
+ type ShiftEvent = {
1018
+ at: IsoTimestamp;
1019
+ kind: ShiftEventKind;
1020
+ };
1021
+
1022
+ type MemberShift = {
1023
+ signedInAt: IsoTimestamp;
1024
+ signedOutAt?: IsoTimestamp;
1025
+ talkSeconds?: DurationSeconds;
1026
+ holdSeconds?: DurationSeconds;
1027
+ breakSeconds?: DurationSeconds;
1028
+ tasksHandled?: number;
1029
+ events?: ShiftEvent[];
1030
+ };
1031
+
1013
1032
  type TeamMembers = {
1014
1033
  members: TeamMember[];
1015
1034
  requests: LeadRequest[];
@@ -1108,8 +1127,6 @@ type ProviderEvent =
1108
1127
  | { type: "task-updated"; task: Task }
1109
1128
  | { type: "task-audio-started"; assignmentId: AssignmentId }
1110
1129
  | { type: "task-audio-ended"; assignmentId: AssignmentId }
1111
- | { type: "team-audio-started"; memberId: UserId; assignmentId: AssignmentId }
1112
- | { type: "team-audio-ended"; memberId: UserId; assignmentId: AssignmentId }
1113
1130
  | { type: "task-ended"; assignmentId: AssignmentId; outcome: TaskOutcome }
1114
1131
  | { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; assignmentId?: AssignmentId; destinationId?: string; reason?: string }
1115
1132
  | { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
@@ -2382,7 +2399,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2382
2399
  | `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**. |
2383
2400
  | `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, `consulted` and `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. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `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**. |
2384
2401
  | `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
2385
- | `takenOver` | Voice only, on a task that reached this agent by a lead's take-over: which member the call was taken from, and when. It is an ordinary assignment with the call's history, offered whatever break the lead is on and counted against no capacity. See **Lead assist**. |
2402
+ | `takenOver` | Voice only, on a task that reached this agent by a lead's take-over: which member the call was taken from, and when. It is an ordinary assignment with the call's history, offered whatever break the lead is on and counted against their capacity like any other call. See **Lead assist**. |
2386
2403
 
2387
2404
  `TaskAttribute` entries carry typed detail alongside the task:
2388
2405
 
@@ -2730,7 +2747,7 @@ Migration from the earlier spellings:
2730
2747
  | taskKey, omni.task-not-found, PROVIDER_NAME__TASK_ID__TAB_NAME (ProviderName.TaskId.TabName) | `assignmentKey`, `omni.assignment-not-found`, `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` (`ProviderName.AssignmentId.TabName`) |
2731
2748
  | Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
2732
2749
  | the call command, atDeadline calls and host-calls | `dial`, `"provider-dials"`, `"host-dials"` |
2733
- | media: task-media-started/-ended, team-media-*, Task.media, TaskMediaState, openMedia, OpenMediaRequest/Result, VoiceMediaSession and its session field | audio: `task-audio-started`, `task-audio-ended`, `team-audio-started`, `team-audio-ended`, `audio` on the task, `TaskAudioState`, `openAudio`, `OpenAudioRequest`, `OpenAudioResult`, `CallAudio` on the result's `audio` |
2750
+ | media: task-media-started/-ended, team-media-*, Task.media, TaskMediaState, openMedia, OpenMediaRequest/Result, VoiceMediaSession and its session field | audio: `task-audio-started`, `task-audio-ended`, `audio` on the task, `TaskAudioState`, `openAudio`, `OpenAudioRequest`, `OpenAudioResult`, `CallAudio` on the result's `audio`; there is no team audio, the lead's follows `listening` on the member |
2734
2751
  | capabilitySource values ungoverned and undetermined | `nobody`, `not-yet-read` (`capabilitySource.notYetRead`) |
2735
2752
  | Snapshot.scheduledActivities, and scheduledActivities on calendar-updated | `calendar` in both |
2736
2753
  | availability reserved | `elsewhere` |
@@ -3194,7 +3211,7 @@ See **Which commands need a capability**.
3194
3211
  | `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
3195
3212
  | `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
3196
3213
  | `endCall` | Primary button: End call | The provider ends the agent's channel and every channel the agent added; the caller continues on the provider's path. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
3197
- | `terminateCall` | Primary button: Terminate call | The provider ends the caller channel. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
3214
+ | `terminateCall` | Primary button: Terminate call | The provider ends the whole call: every channel on it, the agent's, the caller's, any colleague's and anyone else's. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
3198
3215
  | `connectBack` | Completing-task button: Connect back | Omni may have the provider connect the agent back to the task's party while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance completes the task at provider end. See **Connecting back during completion**. |
3199
3216
  | `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
3200
3217
  | `warmTransfer` | Secondary menu item: Warm transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Warm transfer**. |
@@ -3294,7 +3311,7 @@ provider. Two of them end the agent's part; they differ in what happens to the c
3294
3311
 
3295
3312
  ```ts
3296
3313
  { type: "end-call" } // the agent's channel and the channels the agent added end; the caller continues
3297
- { type: "terminate-call" } // the caller channel ends
3314
+ { type: "terminate-call" } // every channel on the call ends
3298
3315
  { type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
3299
3316
  { type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
3300
3317
  ```
@@ -3303,9 +3320,10 @@ provider. Two of them end the agent's part; they differ in what happens to the c
3303
3320
  conferenced colleague, a consulted destination -- end, and the caller continues on the path the
3304
3321
  provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
3305
3322
 
3306
- **`terminate-call` ends the caller channel.** The call is over for the caller. Gated by
3307
- `terminateCall`, a separate permission, since ending a customer's call is a different thing from
3308
- putting my own side down.
3323
+ **`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
3324
+ the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
3325
+ line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
3326
+ different thing from putting my own side down.
3309
3327
 
3310
3328
  Both are commands to the provider through `execute`, and nothing happens on the desk until the
3311
3329
  provider reports it: the agent's audio ends on `task-audio-ended`, the task moves to `completing`,
@@ -4131,8 +4149,9 @@ for nothing and computes nothing.
4131
4149
  | `availability` | Required. What the member is doing now. |
4132
4150
  | `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
4133
4151
  | `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
4134
- | `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, audio and the full history are what the lead reads; 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. |
4135
- | `listening` | Present while this lead's channel is in this member's call — listening, coaching or joined — with the `mode` they are heard in and since when. One member's call at a time (`team.listening.single`). |
4152
+ | `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; the task's history is carried in the same shape as on the member's desk, as much of it as the provider chooses to send; 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. |
4153
+ | `listening` | Present while this lead's channel is in this member's call — listening, coaching or joined — naming which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. Its appearance is what the lead's audio opens on. One member's call at a time (`team.listening.single`). |
4154
+ | `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. Omitted where the provider cannot say. |
4136
4155
 
4137
4156
  Each availability value means one thing:
4138
4157
 
@@ -4239,8 +4258,8 @@ executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" }
4239
4258
 
4240
4259
  **On `join` the provider bridges the lead's channel into the call.** The member's task moves to
4241
4260
  `leadAssist: { stage: "joined", leadId }`, the member on the team member list carries
4242
- `listening: { mode: "join-call", since }`, and the lead's audio arrives as **Listening to a call**
4243
- describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
4261
+ `listening: { assignmentId, mode: "join-call", since }`, and the lead's audio opens on it as
4262
+ **Listening to a call** describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
4244
4263
  work assigned to them.
4245
4264
 
4246
4265
  **A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
@@ -4261,15 +4280,17 @@ listening unasked -- and one act at the end of either. What follows is the same
4261
4280
 
4262
4281
  | | The member's task | The lead's desk |
4263
4282
  | --- | --- | --- |
4264
- | 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`). | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`. |
4283
+ | 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. |
4265
4284
  | 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. |
4266
4285
 
4267
- **The taken-over call is offered whatever break the lead is on.** A lead working as lead alone is
4268
- on a break of a working kind, and the provider assigns the call regardless: it is the one task a
4269
- break in effect may hold (`break.on-break.tasks` allows it, and nothing else). It is the lead's own
4270
- act, so it counts against no capacity, like the agent's own dial; the only conditions are that the
4271
- lead is not already on another call and their voice channel is free. A lead with the team feature
4272
- off cannot take over, since they have no member's assignment to name.
4286
+ **The taken-over call is offered whatever break the lead is on, and counted like any other.** A
4287
+ lead working as lead alone is on a break of a working kind, and the provider assigns the call
4288
+ regardless: it is the one task a break in effect may hold (`break.on-break.tasks` allows it, and
4289
+ nothing else). It is a call landing on the lead's desk, so it counts against their capacity as
4290
+ every call does, with no exemption: a lead at capacity cannot take over, and the provider answers
4291
+ `failed`. The other conditions are that the lead is not already on another call and their voice
4292
+ channel is free. A lead with the team feature off cannot take over, since they have no member's
4293
+ assignment to name.
4273
4294
 
4274
4295
  ```ts
4275
4296
  const leadAssistCapable = {
@@ -4303,34 +4324,47 @@ history. The three modes determine who hears the lead:
4303
4324
  | `coach` | The agent alone. The customer hears nothing. |
4304
4325
  | `join-call` | Everyone on the call. |
4305
4326
 
4306
- All three come with the `lead` flag; a centre reserves none of them per lead. The flow:
4327
+ All three come with the `lead` flag; a centre reserves none of them per lead.
4307
4328
 
4308
- ```ts
4309
- // 1. The lead picks a member from the team member list and starts silent.
4310
- executeTeam({ command: { type: "listen", memberId: "A-1" } })
4311
- // team-updated: the member carries listening: { mode: "listen", since }
4312
- // team-audio-started: { memberId: "A-1", assignmentId: "alloc-42" } -- the lead's audio attaches
4329
+ **There is no team audio.** Audio is the agent's or the lead's, and the lead's audio while
4330
+ listening follows the provider's published state exactly as a task's does: `listening` on the
4331
+ member is the provider's word that the lead's channel is in the call. When it appears, the
4332
+ application opens `openAudio({ assignmentId })` on the lead's connection for the call it names,
4333
+ and plays the `CallAudio` returned; when it goes, the application closes it. Nothing else says
4334
+ when the lead's audio is up, and a reload reopens it from the snapshot's team member list as a task
4335
+ carried with `audio: "started"` is reopened.
4336
+
4337
+ The flow, end to end:
4313
4338
 
4314
- // 2. The lead changes how they are heard; the provider restates the member with the new mode.
4339
+ ```ts
4340
+ // 1. The lead, with the feature on, sees a member on-task with their tasks, and picks a call.
4341
+ // 2. The lead's channel joins it in silence. Conditions: the lead's voice channel is free --
4342
+ // not on a call, not listening elsewhere. Capacity is not involved: nothing is assigned.
4343
+ executeTeam({ command: { type: "listen", memberId: "A-1", assignmentId: "alloc-42" } })
4344
+ // 3. The provider republishes the team member list; the member carries
4345
+ // listening: { assignmentId: "alloc-42", mode: "listen", since }
4346
+ // -- the application opens openAudio({ assignmentId: "alloc-42" }) on the lead's connection.
4347
+ // 4. The lead changes how they are heard; the provider restates listening.mode. The audio stays open.
4315
4348
  executeTeam({ command: { type: "coach", memberId: "A-1" } })
4316
4349
  executeTeam({ command: { type: "join-call", memberId: "A-1" } })
4317
-
4318
- // 3. The lead leaves, or takes the call.
4350
+ // 5. The lead leaves: the provider republishes the member without listening; the application closes the audio.
4319
4351
  executeTeam({ command: { type: "leave", memberId: "A-1" } })
4352
+ // 6. The member's call ends: the same as leave, from the provider's side.
4353
+ // 7. The lead takes the call: the member loses listening no later than the offer, the application
4354
+ // closes the listening audio, and the lead's own call arrives as an ordinary assignment marked
4355
+ // takenOver, counted against capacity, with its audio following task-audio-started as any call's does.
4356
+ // At capacity the take-over is refused and the lead stays listening.
4320
4357
  executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
4358
+ // 8. A join on the member's request is the same flow entered at step 3 with mode: "join-call",
4359
+ // and the member's task shows leadAssist: joined.
4360
+ // 9. A reload of the lead's application: the snapshot's team member list still carries listening,
4361
+ // so the audio is reopened.
4321
4362
  ```
4322
4363
 
4323
- **The lead's audio arrives through team audio.** While the lead's channel is in a member's call
4324
- -- listening, coaching or joined on request -- the lead's connection receives `team-audio-started`
4325
- naming the member and the member's assignment, and on a softphone the agent application opens
4326
- `openAudio` on that assignment as it does for a task; `team-audio-ended` closes it. Voice only, and
4327
- to a login that declares `lead` (`event.teamAudio.*`). The member's own audio events are the
4328
- member's and never reach the lead.
4329
-
4330
- **`listening` on the member is the state**, restated on every change of mode, and a lead is on
4331
- one member's call at a time (`team.listening.single`). The member's call ending ends the lead's
4332
- listening with it: the provider republishes the member without `listening` and sends
4333
- `team-audio-ended`.
4364
+ **`listening` on the member is the state**, restated on every change of mode, naming the call so
4365
+ the application knows which audio to open (`team.member.listening.assignmentId`, and one the
4366
+ member's tasks carry, `team.member.listening.assignment`), and a lead is on one member's call at a
4367
+ time (`team.listening.single`).
4334
4368
 
4335
4369
  **A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
4336
4370
  channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
@@ -4689,7 +4723,7 @@ declared:
4689
4723
  | --- | --- |
4690
4724
  | `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
4691
4725
  | `end-call` | The `endCall` capability: ends my part, the caller continues. |
4692
- | `terminate-call` | The `terminateCall` capability: ends the caller channel. |
4726
+ | `terminate-call` | The `terminateCall` capability: ends the whole call, every channel on it. |
4693
4727
  | `conference` with `action: "remove"` | The `conference` capability, and somebody else on the call: a remove that would leave the agent alone is `end-call`, and a provider answers it `failed`. |
4694
4728
  | `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
4695
4729
  | `dial` | The `preview` phase. A record put in front of an agent is there to be called, so the phase is the gate and there is no capability. It is a dial, with a `dialId` and a `dial-outcome`. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.100",
3
+ "version": "0.1.101",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",