@xema/omni-protocol 0.1.102 → 0.1.104
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 +81 -40
- package/dist/index.js +4 -2
- package/dist/testing.d.ts +8 -1
- package/dist/testing.js +81 -8
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +134 -41
- package/guide.md +199 -94
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -203,7 +203,7 @@ type Manifest<C extends Channel = Channel> = {
|
|
|
203
203
|
dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
|
|
204
204
|
phones?: C extends "voice" ? Phone[] : never;
|
|
205
205
|
runningStepReports?: true;
|
|
206
|
-
|
|
206
|
+
settleMs: number;
|
|
207
207
|
};
|
|
208
208
|
```
|
|
209
209
|
|
|
@@ -474,11 +474,13 @@ type PreferenceResult =
|
|
|
474
474
|
type Snapshot = {
|
|
475
475
|
transport: TransportStatus;
|
|
476
476
|
loginId: string;
|
|
477
|
+
providerTime?: IsoTimestamp;
|
|
477
478
|
break: BreakState;
|
|
478
479
|
tasks: Task[];
|
|
479
480
|
taskCount: number;
|
|
480
481
|
contacts?: Contact[];
|
|
481
482
|
calendar?: ScheduledActivity[];
|
|
483
|
+
shift?: Shift;
|
|
482
484
|
team?: TeamMembers;
|
|
483
485
|
};
|
|
484
486
|
|
|
@@ -569,8 +571,8 @@ type TaskCapabilities<C extends Channel = Channel> =
|
|
|
569
571
|
recording?: Lockable<TaskRecordingPolicy>;
|
|
570
572
|
}
|
|
571
573
|
: C extends "chat"
|
|
572
|
-
? SharedTaskCapabilities & { decline?: Lockable<true>; hold?: Lockable<true> }
|
|
573
|
-
: SharedTaskCapabilities & { decline?: Lockable<true> };
|
|
574
|
+
? SharedTaskCapabilities & { decline?: Lockable<true>; hold?: Lockable<true>; schedule?: Lockable<true> }
|
|
575
|
+
: SharedTaskCapabilities & { decline?: Lockable<true>; schedule?: Lockable<true> };
|
|
574
576
|
```
|
|
575
577
|
|
|
576
578
|
The channel arms are why `Task<"email">` rejects `hold` at compile time rather than at runtime.
|
|
@@ -723,6 +725,7 @@ type Task<C extends Channel = Channel> = {
|
|
|
723
725
|
party?: Contact;
|
|
724
726
|
phase: TaskPhase;
|
|
725
727
|
acceptance?: AcceptanceMode;
|
|
728
|
+
expiresInSeconds?: DurationSeconds;
|
|
726
729
|
previewEndsInSeconds?: DurationSeconds;
|
|
727
730
|
atDeadline?: PreviewDeadline;
|
|
728
731
|
reference?: string;
|
|
@@ -745,7 +748,7 @@ type TaskOutcome =
|
|
|
745
748
|
| { type: "completed"; by: "agent" | "provider" }
|
|
746
749
|
| { type: "taken-over"; leadId: UserId }
|
|
747
750
|
| { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
|
|
748
|
-
| { type: "expired"; phase: "pending" | "confirmed"
|
|
751
|
+
| { type: "expired"; phase: "pending" | "confirmed" }
|
|
749
752
|
| { type: "failed"; failure: ProtocolFailure };
|
|
750
753
|
```
|
|
751
754
|
|
|
@@ -768,8 +771,8 @@ const TASK_COMMAND_NAMES = {
|
|
|
768
771
|
"schedule",
|
|
769
772
|
"complete",
|
|
770
773
|
],
|
|
771
|
-
chat: ["accept", "decline", "pause", "resume", "complete"],
|
|
772
|
-
email: ["accept", "decline", "complete"],
|
|
774
|
+
chat: ["accept", "decline", "pause", "resume", "schedule", "complete"],
|
|
775
|
+
email: ["accept", "decline", "schedule", "complete"],
|
|
773
776
|
} as const;
|
|
774
777
|
|
|
775
778
|
type TaskCommandName<C extends keyof typeof TASK_COMMAND_NAMES> =
|
|
@@ -786,7 +789,7 @@ type VoiceTaskCommand =
|
|
|
786
789
|
| { type: "end-call" }
|
|
787
790
|
| { type: "terminate-call" }
|
|
788
791
|
| { type: "connect-back"; dialId: DialId }
|
|
789
|
-
|
|
|
792
|
+
| ScheduleCommand
|
|
790
793
|
| { type: "lead-assist"; action: "request"; note?: string }
|
|
791
794
|
| { type: "lead-assist"; action: "cancel" }
|
|
792
795
|
| { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
|
|
@@ -795,16 +798,20 @@ type VoiceTaskCommand =
|
|
|
795
798
|
| (RecordingCommand & { source: "provider" })
|
|
796
799
|
| ({ type: "complete" } & OutcomePayload);
|
|
797
800
|
|
|
801
|
+
type ScheduleCommand = { type: "schedule"; at: IsoTimestamp; note?: string };
|
|
802
|
+
|
|
798
803
|
type ChatTaskCommand =
|
|
799
804
|
| { type: "accept" }
|
|
800
805
|
| { type: "decline" }
|
|
801
806
|
| { type: "pause" }
|
|
802
807
|
| { type: "resume" }
|
|
808
|
+
| ScheduleCommand
|
|
803
809
|
| ({ type: "complete" } & OutcomePayload);
|
|
804
810
|
|
|
805
811
|
type EmailTaskCommand =
|
|
806
812
|
| { type: "accept" }
|
|
807
813
|
| { type: "decline" }
|
|
814
|
+
| ScheduleCommand
|
|
808
815
|
| ({ type: "complete" } & OutcomePayload);
|
|
809
816
|
|
|
810
817
|
type CustomTaskCommand = { type: "custom"; name: string; [key: string]: unknown };
|
|
@@ -928,7 +935,7 @@ type BreakRequest = {
|
|
|
928
935
|
|
|
929
936
|
type ForcedBreak = {
|
|
930
937
|
by: UserId | "provider";
|
|
931
|
-
|
|
938
|
+
expectedEndsInSeconds?: DurationSeconds;
|
|
932
939
|
};
|
|
933
940
|
|
|
934
941
|
type BreakState = {
|
|
@@ -970,8 +977,8 @@ type TeamMember = {
|
|
|
970
977
|
since?: IsoTimestamp;
|
|
971
978
|
break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
|
|
972
979
|
tasks?: MemberTask[];
|
|
973
|
-
listening?: MemberListening;
|
|
974
|
-
shift?:
|
|
980
|
+
listening?: MemberListening[];
|
|
981
|
+
shift?: Shift;
|
|
975
982
|
request?: MemberRequest;
|
|
976
983
|
};
|
|
977
984
|
|
|
@@ -1001,6 +1008,7 @@ type MemberTask<C extends Channel = Channel> = {
|
|
|
1001
1008
|
: { onCall?: never; leadAssist?: never; takenOver?: never; audio?: never });
|
|
1002
1009
|
|
|
1003
1010
|
type MemberListening = {
|
|
1011
|
+
leadId: UserId;
|
|
1004
1012
|
assignmentId: AssignmentId;
|
|
1005
1013
|
mode: ListeningMode;
|
|
1006
1014
|
since: IsoTimestamp;
|
|
@@ -1013,7 +1021,7 @@ type ShiftEvent = {
|
|
|
1013
1021
|
kind: ShiftEventKind;
|
|
1014
1022
|
};
|
|
1015
1023
|
|
|
1016
|
-
type
|
|
1024
|
+
type Shift = {
|
|
1017
1025
|
signedInAt: IsoTimestamp;
|
|
1018
1026
|
signedOutAt?: IsoTimestamp;
|
|
1019
1027
|
talkSeconds?: DurationSeconds;
|
|
@@ -1115,7 +1123,6 @@ type ProviderEvent =
|
|
|
1115
1123
|
| {
|
|
1116
1124
|
type: "task-offered";
|
|
1117
1125
|
task: Task;
|
|
1118
|
-
expiresInSeconds?: DurationSeconds;
|
|
1119
1126
|
}
|
|
1120
1127
|
| { type: "task-updated"; task: Task }
|
|
1121
1128
|
| { type: "task-audio-started"; assignmentId: AssignmentId }
|
|
@@ -1130,7 +1137,8 @@ type ProviderEvent =
|
|
|
1130
1137
|
| { type: "team-member-removed"; memberId: UserId }
|
|
1131
1138
|
| { type: "team-policies-updated"; policies: TeamPolicies }
|
|
1132
1139
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
1133
|
-
| { type: "calendar-updated"; calendar: ScheduledActivity[] }
|
|
1140
|
+
| { type: "calendar-updated"; calendar: ScheduledActivity[] }
|
|
1141
|
+
| { type: "shift-updated"; shift: Shift };
|
|
1134
1142
|
|
|
1135
1143
|
type ProviderEventEnvelope = {
|
|
1136
1144
|
id: string;
|
|
@@ -1254,6 +1262,7 @@ const OMNI_FAILURE_CODES = [
|
|
|
1254
1262
|
"omni.unavailable",
|
|
1255
1263
|
"omni.break-already-committed",
|
|
1256
1264
|
"omni.recording-unsettled",
|
|
1265
|
+
"omni.break-forced-by-provider",
|
|
1257
1266
|
] as const;
|
|
1258
1267
|
type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1259
1268
|
```
|
|
@@ -1586,7 +1595,7 @@ export default defineAdapter({
|
|
|
1586
1595
|
channel: "voice",
|
|
1587
1596
|
supportedProtocolVersions: [OMNI_PROTOCOL_VERSION],
|
|
1588
1597
|
authenticationMethods: ["browser-sso"],
|
|
1589
|
-
|
|
1598
|
+
settleMs: 5000,
|
|
1590
1599
|
idleCapabilities: {
|
|
1591
1600
|
dial: { destinations: "any-number" },
|
|
1592
1601
|
},
|
|
@@ -1621,10 +1630,10 @@ compile time.
|
|
|
1621
1630
|
| `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label a desk shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
|
|
1622
1631
|
| `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `deskPhone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call**. |
|
|
1623
1632
|
| `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
|
|
1624
|
-
| `timeCheck` |
|
|
1633
|
+
| `timeCheck` | `true`: implements `checkTime` and states `providerTime` on every snapshot. Required of a provider that publishes an instant the desk renders as a running duration -- a member's `since`, a lead's `listening.since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts from the provider's clock (`manifest.timeCheck.required`); agent application polling is independently opt-in. See **Every screen counts from the provider's clock**. |
|
|
1625
1634
|
| `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
|
|
1626
1635
|
| `runningStepReports` | The provider takes running reports of an agent application-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the agent application sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The agent application records what it performs**. |
|
|
1627
|
-
| `
|
|
1636
|
+
| `settleMs` | Required. How long after `applied` the wire shows what the provider did: the `task-ended` after a `complete`, the `calendar-updated` after a `schedule`. A positive whole number of milliseconds (`manifest.settleMs`). Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1628
1637
|
|
|
1629
1638
|
### Authentication methods
|
|
1630
1639
|
|
|
@@ -2137,11 +2146,13 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
|
|
|
2137
2146
|
| --- | --- |
|
|
2138
2147
|
| `transport` | Current `TransportStatus` — whether this provider's transport can serve the login. Defined under **`transport-status`**. |
|
|
2139
2148
|
| `loginId` | Identity of this login. It must match the connection context. |
|
|
2149
|
+
| `providerTime` | The provider's own instant of this read. Required of a provider that declares `timeCheck` (`snapshot.providerTime`), forbidden of one that does not (`snapshot.providerTime.unexpected`): a clock sample the desk counts durations from, saving it a `checkTime`. |
|
|
2140
2150
|
| `break` | Complete break state, including status, whether the agent may ask, reasons, retry details, and any forced break. |
|
|
2141
2151
|
| `tasks` | Complete set of tasks currently offered to or owned by this agent. |
|
|
2142
2152
|
| `taskCount` | The provider's own count of those tasks, stated rather than inferred, and it must equal `tasks.length`. A snapshot with no work says `taskCount: 0` in so many words — a blank or unanswered state lacks the count and cannot pass as a confirmed empty. |
|
|
2143
2153
|
| `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
|
|
2144
2154
|
| `calendar` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
|
|
2155
|
+
| `shift` | The agent's own day so far, as the provider counts it -- the same `Shift` their lead sees on the team member list, the same numbers from the same count. Replaced whole by `shift-updated`. Omitted only where the provider cannot say. See **The agent's day is on the wire**. |
|
|
2145
2156
|
| `team` | Required `TeamMembers` when the login declares `capabilities.lead` and the lead has the team feature on, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission, and a lead who turned the feature off gets nothing of the team. |
|
|
2146
2157
|
|
|
2147
2158
|
## Live connection
|
|
@@ -2318,8 +2329,7 @@ declare const task: Task;
|
|
|
2318
2329
|
|
|
2319
2330
|
const assignment = {
|
|
2320
2331
|
type: "task-offered",
|
|
2321
|
-
task: { ...task, phase: "pending", acceptance: "consent" },
|
|
2322
|
-
expiresInSeconds: 30,
|
|
2332
|
+
task: { ...task, phase: "pending", acceptance: "consent", expiresInSeconds: 30 },
|
|
2323
2333
|
} satisfies Extract<ProviderEvent, { type: "task-offered" }>;
|
|
2324
2334
|
```
|
|
2325
2335
|
|
|
@@ -2337,10 +2347,12 @@ on a desk phone — and is not in question per call. See **How the agent hears t
|
|
|
2337
2347
|
|
|
2338
2348
|
Automatic acceptance still begins with `task-offered`.
|
|
2339
2349
|
|
|
2340
|
-
`expiresInSeconds` is how long the offer
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2350
|
+
`expiresInSeconds` is how long the offer has left, in whole seconds from the publication that
|
|
2351
|
+
carries it. It rides on the pending task, as `acceptance` does, so a reconnect snapshot restates
|
|
2352
|
+
it from what is actually left; it belongs to `pending` alone and is gone once the task is accepted
|
|
2353
|
+
(`task.pending.expiresInSeconds`, `.unexpected`). Where present, Omni counts down from receipt and
|
|
2354
|
+
stops offering **Accept** once it runs out; the provider ends the lapsed offer with `task-ended`
|
|
2355
|
+
and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.** A provider that reports only elapsed
|
|
2344
2356
|
ring time after the fact cannot say how long an offer has left, and a computed value would have
|
|
2345
2357
|
Omni withdraw **Accept** from a task still pending.
|
|
2346
2358
|
|
|
@@ -2349,7 +2361,7 @@ different clocks, and a countdown that compares one against the other is a count
|
|
|
2349
2361
|
fast or slow by their skew. Seconds from the publication that carries them are the provider's own
|
|
2350
2362
|
arithmetic, counted down on the desk from the moment they arrive, and the transport delay is the
|
|
2351
2363
|
only error. Restated on every publication that carries them, so a snapshot after a reconnect
|
|
2352
|
-
starts the countdown afresh from what is actually left (`
|
|
2364
|
+
starts the countdown afresh from what is actually left (`task.pending.expiresInSeconds`,
|
|
2353
2365
|
`task.preview.previewEndsInSeconds`: a whole number, zero or more).
|
|
2354
2366
|
|
|
2355
2367
|
A preview's deadline is not on the offer. It travels on the task, as `previewEndsInSeconds` with
|
|
@@ -2396,6 +2408,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2396
2408
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2397
2409
|
| `audio` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-audio-started`**. Audio names a task whose work has begun, or whose party the agent application is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.audio.beforeWork`), on a snapshot as on the event, since an agent application opens the microphone on it; with the party ringing by an agent application dial or provider-triggered preview dial, actual ring-back audio may precede answer. |
|
|
2398
2410
|
| `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
|
|
2411
|
+
| `expiresInSeconds` | In `pending` only: how long the offer has left, in whole seconds from this publication, restated on every publication that carries it. Absent, the offer stands until the provider says otherwise. See **Pending**. |
|
|
2399
2412
|
| `previewEndsInSeconds` | Voice only, in `preview`: how long the preparation has left, in whole seconds from this publication, restated on every publication that carries it. Absent, the agent has as long as they need without a preparation countdown. Always with `atDeadline`. See **Preview: the agent presses Call**. |
|
|
2400
2413
|
| `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. |
|
|
2401
2414
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
@@ -2618,18 +2631,26 @@ provider may complete the task without receiving that command.
|
|
|
2618
2631
|
**The agent may finish early under either mode.** `complete` is issuable wherever there is a wrap
|
|
2619
2632
|
to cut short: under `agent-command` it is the end, and under `provider-automatic` it says the agent
|
|
2620
2633
|
is done before the allowance ran out, and the provider is free to end the task at once. Only a task
|
|
2621
|
-
with no wrap at all -- `wrapAllowance: 0` -- has nothing to complete, and
|
|
2622
|
-
there (`command.complete.wrapAllowance`). An ending the agent asked for says
|
|
2623
|
-
`by: "agent"`, whichever mode the task was under.
|
|
2634
|
+
with no wrap at all -- `provider-automatic` with `wrapAllowance: 0` -- has nothing to complete, and
|
|
2635
|
+
the command is refused there (`command.complete.wrapAllowance`). An ending the agent asked for says
|
|
2636
|
+
so, `completed` with `by: "agent"`, whichever mode the task was under.
|
|
2624
2637
|
|
|
2625
2638
|
**A required outcome is the agent's to give.** A task whose `outcomes` says `required: true` waits
|
|
2626
2639
|
for a code the agent chooses, so it completes on the agent's command: `provider-automatic` cannot
|
|
2627
2640
|
wait for it, and the pair is refused (`task.outcomes.required.mode`). Optional outcomes may ride on
|
|
2628
2641
|
a task the provider completes itself; the agent gives one if in time. And a code is given in wrap,
|
|
2629
|
-
so a task with `wrapAllowance: 0` publishes no `outcomes` at all
|
|
2642
|
+
so a `provider-automatic` task with `wrapAllowance: 0` publishes no `outcomes` at all
|
|
2643
|
+
(`task.outcomes.wrapAllowance`).
|
|
2644
|
+
|
|
2645
|
+
**The allowance means one thing per mode.** Under `provider-automatic` it is what the provider
|
|
2646
|
+
acts on: the task ends when it runs out. Under `agent-command` it is the expected wrap, stated so
|
|
2647
|
+
the agent can see it: the desk counts it down and, past zero, shows how far over they are, and
|
|
2648
|
+
nothing acts on it -- the task waits for `complete` however long that takes, with its outcomes
|
|
2649
|
+
collected whenever the agent gives them. A provider that would end the task at a time is
|
|
2650
|
+
`provider-automatic`.
|
|
2630
2651
|
|
|
2631
|
-
`wrapAllowance` is
|
|
2632
|
-
|
|
2652
|
+
`wrapAllowance` is fixed, and when it starts depends on whether the channel carries real-time
|
|
2653
|
+
audio:
|
|
2633
2654
|
|
|
2634
2655
|
| Channel | Wrap allowance starts at |
|
|
2635
2656
|
| --- | --- |
|
|
@@ -2658,9 +2679,9 @@ const emailCompletion = {
|
|
|
2658
2679
|
In this example, the agent has two minutes after sending the email to add notes, select a
|
|
2659
2680
|
outcome, and complete the task.
|
|
2660
2681
|
|
|
2661
|
-
`0` means
|
|
2662
|
-
|
|
2663
|
-
|
|
2682
|
+
`0` means no wrap under `provider-automatic`: the provider completes the task at the end of the
|
|
2683
|
+
interaction, and `complete` has nothing to cut short. Under `agent-command` it is an expected wrap
|
|
2684
|
+
of nothing -- the desk shows the overrun from the start -- and the task still waits for `complete`.
|
|
2664
2685
|
|
|
2665
2686
|
There is no value meaning "unlimited", because a number that is not a duration would be read as
|
|
2666
2687
|
one. A provider that imposes no deadline says so by **omitting** `wrapAllowance`, which
|
|
@@ -2748,7 +2769,7 @@ The record is the call's, not the task's: most of what it holds happened before
|
|
|
2748
2769
|
*Interaction* keeps the one meaning **Terms** gives it, the agent's time on the call, which is why
|
|
2749
2770
|
the total of earlier agents' time is `interactionSeconds` and the phase rules are
|
|
2750
2771
|
`command.phase.interaction`. Hosts and providers adopt these names together; no legacy aliases
|
|
2751
|
-
are provided. The manifest's `
|
|
2772
|
+
are provided. The manifest's `settleMs` bounds final task completion, not entry into
|
|
2752
2773
|
the `completing` wrap-up phase. The `recordStep` method and `complete` command keep their names.
|
|
2753
2774
|
|
|
2754
2775
|
Migration from the earlier spellings:
|
|
@@ -2765,7 +2786,7 @@ Migration from the earlier spellings:
|
|
|
2765
2786
|
| Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `expiresInSeconds` |
|
|
2766
2787
|
| Task.id, TaskId, and `taskId` on every event, command, report and lead request | gone: a task is named by `assignmentId` alone, and `assignmentKey(providerId, assignmentId)` scopes it |
|
|
2767
2788
|
| 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`) |
|
|
2768
|
-
| Manifest.disposalSettleMs | `Manifest.
|
|
2789
|
+
| Manifest.disposalSettleMs, Manifest.completionSettleMs | `Manifest.settleMs` (`manifest.settleMs`, `manifest.settleMs.renamed`): the bound after `applied` for `complete` and `schedule` alike |
|
|
2769
2790
|
| the call command, atDeadline calls and host-calls | `dial`, `"provider-dials"`, `"host-dials"` |
|
|
2770
2791
|
| 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 |
|
|
2771
2792
|
| capabilitySource values ungoverned and undetermined | `nobody`, `not-yet-read` (`capabilitySource.notYetRead`) |
|
|
@@ -2777,7 +2798,17 @@ Migration from the earlier spellings:
|
|
|
2777
2798
|
| TeamMembers.requests, LeadRequest | `TeamMember.request` (`MemberRequest`): the ask rides on the member (`team.member.request.*`) |
|
|
2778
2799
|
| team-updated on every change | `team-updated` whole once after the switch and on snapshots; `team-member-updated`, `team-member-removed`, `team-policies-updated` after (`stream.team.baseline`, `stream.teamMember.unknown`) |
|
|
2779
2800
|
| RecordingCommand.requestId | gone: no request identity; a partial effect is a settled `failed` under `omni.recording-unsettled` |
|
|
2780
|
-
| command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only with no wrap |
|
|
2801
|
+
| command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only on `provider-automatic` with no wrap |
|
|
2802
|
+
| expiresInSeconds on task-offered | `Task.expiresInSeconds`, in `pending` (`task.pending.expiresInSeconds`, `.unexpected`; `event.taskOffered.expiresInSeconds.unexpected`) |
|
|
2803
|
+
| the expired outcome naming preview | gone: nothing expires a preview; withdrawn, it is `cancelled` by the provider |
|
|
2804
|
+
| schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`drive.schedule.unsettled`) |
|
|
2805
|
+
| a forced break requesting breaks on the other providers | `setCapacity({ count: 0 })` on every other usable provider (`assertForcedBreakStopsTheRest`) |
|
|
2806
|
+
| end-forced-break on a break the platform imposed | refused: `team.command.endForcedBreak.provider`, `omni.break-forced-by-provider` |
|
|
2807
|
+
| ForcedBreak.expectedDurationMs | `expectedEndsInSeconds`: seconds left, restated (`break.forced.expectedEndsInSeconds`, `.expectedDurationMs.renamed`); the command keeps `expectedDurationMs` |
|
|
2808
|
+
| MemberShift; the day on the lead's list alone | `Shift`, on `Snapshot.shift` and `shift-updated` for the agent, on `TeamMember.shift` for the lead |
|
|
2809
|
+
| TeamMember.listening as this lead's one entry | `MemberListening[]`, every lead's, each with `leadId` (`team.member.listening.leadId`, `.unique`, `.empty`, `.entry`, `.heard`) |
|
|
2810
|
+
| a member's history trimmed on the lead's list | the record whole, as the member's desk holds it |
|
|
2811
|
+
| 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`) |
|
|
2781
2812
|
|
|
2782
2813
|
Update producers, consumers, saved task snapshots, and validation-rule assertions together.
|
|
2783
2814
|
History and report rule names use `history` and `historyReport`; assignment rules use
|
|
@@ -3377,28 +3408,39 @@ caller to somebody else.
|
|
|
3377
3408
|
|
|
3378
3409
|
An agent on a call promises to call back on Thursday, or in wrap writes up that the customer wants
|
|
3379
3410
|
a callback once the refund lands. The follow-up goes on the calendar, and the platform owns the
|
|
3380
|
-
calendar: `schedule` asks the provider to put it there.
|
|
3411
|
+
calendar: `schedule` asks the provider to put it there. A chat or an email agent promises a
|
|
3412
|
+
callback as often, so the control is on every channel.
|
|
3381
3413
|
|
|
3382
3414
|
```ts
|
|
3383
|
-
// From the call, or from its wrap: the time, and a note where the agent wrote one.
|
|
3415
|
+
// From the call or the conversation, or from its wrap: the time, and a note where the agent wrote one.
|
|
3384
3416
|
{ type: "schedule", at: "2026-08-28T10:00:00Z", note: "Call back about the refund" }
|
|
3385
3417
|
```
|
|
3386
3418
|
|
|
3387
3419
|
`schedule` is gated by the `schedule` capability and issuable in `in-progress`, `paused` or
|
|
3388
3420
|
`completing` (`command.phase.interaction`): a follow-up is promised on the call and written up in
|
|
3389
|
-
wrap alike, and the task names the party it is for.
|
|
3390
|
-
|
|
3391
|
-
|
|
3421
|
+
wrap alike, and the task names the party it is for. It lands on the calendar, so a task may offer
|
|
3422
|
+
it only under a manifest that declares the `calendar` idle capability
|
|
3423
|
+
(`task.capability.calendar.required`), as a control that dials needs `dialOutcomes`; a locked
|
|
3424
|
+
`schedule` is still a declared one. The command carries a time and a note and nothing else
|
|
3392
3425
|
(`command.schedule.at`, `command.schedule.note`, `command.field`): what the platform makes of a
|
|
3393
3426
|
follow-up -- a campaign record, a reminder, a scheduled dial -- is its own, and the agent sees it
|
|
3394
3427
|
on the calendar.
|
|
3395
3428
|
|
|
3429
|
+
**`applied` says the follow-up is on the calendar**, and the `calendar-updated` that shows it
|
|
3430
|
+
follows within the manifest's `settleMs`, as a `task-ended` follows an applied `complete`. Past
|
|
3431
|
+
the bound the agent application calls `snapshot()`: a calendar carrying an activity at that instant
|
|
3432
|
+
clears the wait; one without it shows the follow-up as unsettled -- "Scheduled... the provider has
|
|
3433
|
+
not confirmed" -- naming the command. The drive schedules one follow-up where the task offers it,
|
|
3434
|
+
at a time it takes from the provider's own publication, and holds the provider to the same bound
|
|
3435
|
+
(`drive.schedule.unsettled`).
|
|
3436
|
+
|
|
3396
3437
|
### Chat capabilities
|
|
3397
3438
|
|
|
3398
3439
|
| Capability | Omni UI | Contract |
|
|
3399
3440
|
| --- | --- | --- |
|
|
3400
3441
|
| `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when local policy also permits declining. |
|
|
3401
3442
|
| `hold` | Primary toggle: Hold | Omni may pause and resume the agent’s interaction in the chat. |
|
|
3443
|
+
| `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the conversation or from its wrap. See **Scheduling a follow-up**. |
|
|
3402
3444
|
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3403
3445
|
|
|
3404
3446
|
### Email capabilities
|
|
@@ -3406,6 +3448,7 @@ on the calendar.
|
|
|
3406
3448
|
| Capability | Omni UI | Contract |
|
|
3407
3449
|
| --- | --- | --- |
|
|
3408
3450
|
| `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when local policy also permits declining. |
|
|
3451
|
+
| `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the message or from its wrap. See **Scheduling a follow-up**. |
|
|
3409
3452
|
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3410
3453
|
|
|
3411
3454
|
### Custom capabilities
|
|
@@ -3715,14 +3758,14 @@ The team break command formerly named release is now `end-forced-break`, and its
|
|
|
3715
3758
|
`team.break.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
|
|
3716
3759
|
The agent’s `endBreak` now applies to forced breaks as well as requested breaks.
|
|
3717
3760
|
|
|
3718
|
-
`ForcedBreak` names who forced the break and may
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3761
|
+
`ForcedBreak` names who forced the break and may say how long it is expected to run yet, as
|
|
3762
|
+
`expectedEndsInSeconds`: whole seconds from the publication that carries it, restated on every
|
|
3763
|
+
publication, so every screen -- the agent's, reloaded or not, and every lead's -- counts down the
|
|
3764
|
+
same number from receipt (`break.forced.expectedEndsInSeconds`). The duration the lead asked for
|
|
3765
|
+
travels on the `force-break` command alone; the state says what is left, and drops the field once
|
|
3766
|
+
it has run out. The agent must explicitly resume when ready. The retired endsAutomatically and
|
|
3767
|
+
endsAt fields are rejected, including when an expectation is also provided. Who forced it is
|
|
3768
|
+
`by`, as **A forced break is the provider's act** below sets out.
|
|
3726
3769
|
|
|
3727
3770
|
Omni resolves the name to show with `getUserDetails()`, so a provider sends the identifier and never
|
|
3728
3771
|
a display name.
|
|
@@ -3737,16 +3780,24 @@ For example:
|
|
|
3737
3780
|
```ts
|
|
3738
3781
|
forced: {
|
|
3739
3782
|
by: "manager-1042",
|
|
3740
|
-
|
|
3783
|
+
expectedEndsInSeconds: 600
|
|
3741
3784
|
}
|
|
3742
3785
|
```
|
|
3743
3786
|
|
|
3744
3787
|
The agent application keeps **Resume** available for an agent on a forced break. The agent explicitly chooses
|
|
3745
3788
|
Resume; the agent application sends `endBreak()` and follows the provider-confirmed state before resuming
|
|
3746
|
-
work.
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3789
|
+
work. The expectation is whole seconds still to run, restated on every publication; omission
|
|
3790
|
+
means none was stated, or it has run out. Neither its reaching zero nor an overdue indication
|
|
3791
|
+
authorizes the agent application or provider to end the break, restore availability, or route
|
|
3792
|
+
work to the agent.
|
|
3793
|
+
|
|
3794
|
+
**A forced break is the provider's act.** A lead's `force-break` is a command to the provider,
|
|
3795
|
+
which forces the break on the member; `by` names the lead who asked it to. A break the platform
|
|
3796
|
+
imposed on its own -- a schedule, a compliance hold -- says `by: "provider"`, and is the platform's
|
|
3797
|
+
to lift: a lead's `end-forced-break` on it is refused before it is sent
|
|
3798
|
+
(`team.command.endForcedBreak.provider`), and a provider that receives one answers `failed` with
|
|
3799
|
+
`omni.break-forced-by-provider`. The desk offers End forced break on a member only where
|
|
3800
|
+
`forced.by` names a lead.
|
|
3750
3801
|
|
|
3751
3802
|
**A lead lifting the restriction does not resume the agent.** On an applied
|
|
3752
3803
|
`end-forced-break`, the provider clears `BreakState.forced` while preserving the current
|
|
@@ -3762,18 +3813,25 @@ must correlate resumption to the authenticated agent's action; a state-only orde
|
|
|
3762
3813
|
cannot establish the cause of a transition. The usual multi-provider end/reconciliation flow
|
|
3763
3814
|
still applies, and work resumes only on confirmed provider state.
|
|
3764
3815
|
|
|
3765
|
-
An agent application
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
A break applies to the **agent**, not to one provider. When a provider forces one, Omni
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
the agent
|
|
3776
|
-
|
|
3816
|
+
An agent application counts `expectedEndsInSeconds` down from receipt, on every publication that
|
|
3817
|
+
carries it, and shows nothing of it where the provider sends none. It invents no start and no
|
|
3818
|
+
return time, and nothing acts on the number reaching zero.
|
|
3819
|
+
|
|
3820
|
+
A break applies to the **agent**, not to one provider. When a provider forces one, Omni stops the
|
|
3821
|
+
agent everywhere else at once, and not by asking: it states `setCapacity({ count: 0 })` on every
|
|
3822
|
+
other usable provider holding capacity, which is taken and never refused, and which their team
|
|
3823
|
+
lists show as `elsewhere`. Nobody on those providers can deny it, and the agent cannot cancel it,
|
|
3824
|
+
because there is nothing to cancel: a break request on them would be the agent's own to withdraw
|
|
3825
|
+
and a lead's to refuse, and a forced break is neither. When the forcing provider ends the forced
|
|
3826
|
+
break and the agent resumes there, the agent application restates its capacity on the rest. A
|
|
3827
|
+
provider whose login is not usable is left alone, as a break attempt leaves it;
|
|
3828
|
+
`assertForcedBreakStopsTheRest` holds an agent application to this set. **The provider keeps its
|
|
3829
|
+
shift totals true.** A member held elsewhere is one the provider sees -- `elsewhere` is a state it
|
|
3830
|
+
publishes -- and the day's `shift` is its own account of the day, so the time the agent was
|
|
3831
|
+
stopped for a break forced on another provider is the provider's to count, as a break of a kind
|
|
3832
|
+
its platform has, or introduces, for exactly this. A `breakSeconds` that leaves it out is a total
|
|
3833
|
+
the provider knew to be short, and **Never report a value you cannot observe** cuts the other way
|
|
3834
|
+
here: the provider observed it.
|
|
3777
3835
|
|
|
3778
3836
|
`ForcedBreak.by` says who forced it: a lead, by user id, or `provider` where the platform itself
|
|
3779
3837
|
did -- on its own rule, a schedule, a compliance hold. The agent sees who, and `getUserDetails()`
|
|
@@ -4163,10 +4221,10 @@ computes nothing. See **`team-updated`** and its companions.
|
|
|
4163
4221
|
| `availability` | Required. What the member is doing now. |
|
|
4164
4222
|
| `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
|
|
4165
4223
|
| `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
|
|
4166
|
-
| `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
|
|
4167
|
-
| `listening` |
|
|
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. |
|
|
4225
|
+
| `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`). |
|
|
4168
4226
|
| `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**. |
|
|
4169
|
-
| `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. |
|
|
4227
|
+
| `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. |
|
|
4170
4228
|
|
|
4171
4229
|
Each availability value means one thing:
|
|
4172
4230
|
|
|
@@ -4189,11 +4247,19 @@ by it, as a colleague signing in is. `team-member-removed` takes one off. Never
|
|
|
4189
4247
|
member, never a task of theirs on its own: a hold on a member's desk is that member republished
|
|
4190
4248
|
whole on the lead's, and nothing else on the lead's screen moves.
|
|
4191
4249
|
|
|
4250
|
+
**The agent's day is on the wire.** The lead sees `shift` totals as the provider counts them, and
|
|
4251
|
+
the agent sees the same: `Snapshot.shift` and `shift-updated` carry the agent's own day in the same
|
|
4252
|
+
shape, from the same count, so "calls today: 12" is one number on both screens. The desk computes
|
|
4253
|
+
nothing of the day; `tasksHandled` moves when a task ends and `talkSeconds` when a leg closes, and
|
|
4254
|
+
the active call's running time is the screen's own until then, as **Every screen counts from the
|
|
4255
|
+
provider's clock** sets out.
|
|
4256
|
+
|
|
4192
4257
|
**Omit `since` rather than inventing one.** Omni renders it as a duration, so a timestamp
|
|
4193
4258
|
synthesised from the adapter's own clock at seed time reads as "on task for 0 seconds" for
|
|
4194
4259
|
everybody — worse than showing nothing, because it looks like data. Send it only when the provider
|
|
4195
4260
|
knows when the state actually began. It times the current `availability`, so it moves every time
|
|
4196
|
-
that value does.
|
|
4261
|
+
that value does. And it is counted from the provider's clock, never the screen's, so a provider
|
|
4262
|
+
that publishes it declares `timeCheck` -- see **Every screen counts from the provider's clock**.
|
|
4197
4263
|
|
|
4198
4264
|
**The login is the permission.** A team member list goes to a login that declares
|
|
4199
4265
|
`capabilities.lead`, once the team feature is on, and to nobody else. Omni never decides who
|
|
@@ -4241,7 +4307,7 @@ assignment from the member alone.
|
|
|
4241
4307
|
| `{ type: "decide-break-request", memberId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
|
|
4242
4308
|
| `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
|
|
4243
4309
|
| `{ type: "force-break", memberId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory and is published on the resulting forced break. |
|
|
4244
|
-
| `{ type: "end-forced-break", memberId }` |
|
|
4310
|
+
| `{ type: "end-forced-break", memberId }` | Asks the provider to lift a forced-break restriction a lead asked for, by clearing `BreakState.forced`; one the platform imposed is its own to lift (`team.command.endForcedBreak.provider`, `omni.break-forced-by-provider`). The committed break continues; only the agent resumes work. |
|
|
4245
4311
|
| `{ type: "join", memberId, assignmentId? }` | Answers the member's request: the provider bridges the lead's channel into the call. See **Lead assist**. |
|
|
4246
4312
|
| `{ type: "decline", memberId, assignmentId?, reason? }` | Refuses the member's request. |
|
|
4247
4313
|
| `{ type: "listen", memberId, assignmentId? }` | The lead's channel joins the member's call unasked, in silence. See **Listening to a call**. |
|
|
@@ -4286,9 +4352,10 @@ executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" }
|
|
|
4286
4352
|
```
|
|
4287
4353
|
|
|
4288
4354
|
**On `join` the provider bridges the lead's channel into the call.** The member's task moves to
|
|
4289
|
-
`leadAssist: { stage: "joined", leadId }
|
|
4290
|
-
|
|
4291
|
-
|
|
4355
|
+
`leadAssist: { stage: "joined", leadId }` and its `onCall` gains the lead as an `agent`, since a
|
|
4356
|
+
lead who is heard is in the room; the member on the team member list carries
|
|
4357
|
+
`listening: [{ leadId, assignmentId, mode: "join-call", since }]`, and the lead's audio opens on
|
|
4358
|
+
it as **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
|
|
4292
4359
|
work assigned to them.
|
|
4293
4360
|
|
|
4294
4361
|
**A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
|
|
@@ -4356,10 +4423,11 @@ history. The three modes determine who hears the lead:
|
|
|
4356
4423
|
All three come with the `lead` flag; a centre reserves none of them per lead.
|
|
4357
4424
|
|
|
4358
4425
|
**There is no team audio.** Audio is the agent's or the lead's, and the lead's audio while
|
|
4359
|
-
listening follows the provider's published state exactly as a task's does:
|
|
4360
|
-
member is the provider's word that the lead's channel is in the call.
|
|
4361
|
-
application opens `openAudio({ assignmentId })` on the lead's connection for
|
|
4362
|
-
and plays the `CallAudio` returned; when it goes, the application closes it.
|
|
4426
|
+
listening follows the provider's published state exactly as a task's does: the entry naming this
|
|
4427
|
+
lead in `listening` on the member is the provider's word that the lead's channel is in the call.
|
|
4428
|
+
When it appears, the application opens `openAudio({ assignmentId })` on the lead's connection for
|
|
4429
|
+
the call it names, and plays the `CallAudio` returned; when it goes, the application closes it.
|
|
4430
|
+
Another lead's entry opens nothing: it is shown, and that is all. Nothing else says
|
|
4363
4431
|
when the lead's audio is up, and a reload reopens it from the snapshot's team member list as a task
|
|
4364
4432
|
carried with `audio: "started"` is reopened.
|
|
4365
4433
|
|
|
@@ -4370,13 +4438,15 @@ The flow, end to end:
|
|
|
4370
4438
|
// 2. The lead's channel joins it in silence. Conditions: the lead's voice channel is free --
|
|
4371
4439
|
// not on a call, not listening elsewhere. Capacity is not involved: nothing is assigned.
|
|
4372
4440
|
executeTeam({ command: { type: "listen", memberId: "A-1", assignmentId: "alloc-42" } })
|
|
4373
|
-
// 3. The provider publishes the member whole on team-member-updated; the member carries
|
|
4374
|
-
// listening: { assignmentId: "alloc-42", mode: "listen", since }
|
|
4375
|
-
// --
|
|
4376
|
-
//
|
|
4441
|
+
// 3. The provider publishes the member whole on team-member-updated, to every lead; the member carries
|
|
4442
|
+
// listening: [{ leadId: "L-9", assignmentId: "alloc-42", mode: "listen", since }]
|
|
4443
|
+
// -- L-9's application opens openAudio({ assignmentId: "alloc-42" }) on its own connection; every
|
|
4444
|
+
// other lead's shows L-9 on the call and opens nothing.
|
|
4445
|
+
// 4. The lead changes how they are heard; the member is published again with the entry's new mode. The audio stays open.
|
|
4446
|
+
// join-call is heard by everyone, so the member's task is also republished with L-9 on onCall as an agent.
|
|
4377
4447
|
executeTeam({ command: { type: "coach", memberId: "A-1" } })
|
|
4378
4448
|
executeTeam({ command: { type: "join-call", memberId: "A-1" } })
|
|
4379
|
-
// 5. The lead leaves: the provider publishes the member without
|
|
4449
|
+
// 5. The lead leaves: the provider publishes the member without L-9's entry, and the task without L-9 in the room; the application closes the audio.
|
|
4380
4450
|
executeTeam({ command: { type: "leave", memberId: "A-1" } })
|
|
4381
4451
|
// 6. The member's call ends: the same as leave, from the provider's side.
|
|
4382
4452
|
// 7. The lead takes the call: the member loses listening no later than the offer, the application
|
|
@@ -4387,22 +4457,29 @@ executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
|
|
|
4387
4457
|
// 8. A join on the member's request is the same flow entered at step 3 with mode: "join-call",
|
|
4388
4458
|
// and the member's task shows leadAssist: joined.
|
|
4389
4459
|
// 9. A reload of the lead's application: the switch is sent again, the team-updated that follows
|
|
4390
|
-
// still carries listening on the member, so the audio is reopened.
|
|
4460
|
+
// still carries this lead's entry in listening on the member, so the audio is reopened.
|
|
4391
4461
|
```
|
|
4392
4462
|
|
|
4393
|
-
**`listening` on the member is the state**,
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4463
|
+
**`listening` on the member is the state**, one entry per lead on the call, restated on every
|
|
4464
|
+
change of mode, naming the lead (`team.member.listening.leadId`, once per member,
|
|
4465
|
+
`team.member.listening.unique`) and the call, so the application knows which audio is its own to
|
|
4466
|
+
open (`team.member.listening.assignmentId`, and one the member's tasks carry,
|
|
4467
|
+
`team.member.listening.assignment`). Every lead sees every entry: a silent lead is hidden from
|
|
4468
|
+
the agent, never from the other leads and managers watching the same member on other screens.
|
|
4469
|
+
A lead is on one member's call at a time (`team.listening.single`).
|
|
4397
4470
|
|
|
4398
4471
|
**A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
|
|
4399
4472
|
channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
|
|
4400
4473
|
includes a lead on a break: a lead working as lead alone is on a break of a working kind, and
|
|
4401
4474
|
listens through it.
|
|
4402
4475
|
|
|
4403
|
-
**
|
|
4404
|
-
|
|
4405
|
-
platform's business and travels on the audio, not on this wire.
|
|
4476
|
+
**A silent lead reaches nobody but the leads.** In `listen` and `coach` the member's task carries
|
|
4477
|
+
no trace of the lead: not on `onCall`, not in the record. Whether coaching is announced to the
|
|
4478
|
+
agent is the platform's business and travels on the audio, not on this wire. In `join-call`, and
|
|
4479
|
+
on a `join` answering a request, the lead is heard by everyone and is in the room: the task's
|
|
4480
|
+
`onCall` carries them as an `agent` by user id, on the member's desk and on every lead's copy of
|
|
4481
|
+
the task alike, and `leave` takes the entry off as it takes the channel off
|
|
4482
|
+
(`team.member.listening.heard`).
|
|
4406
4483
|
|
|
4407
4484
|
### A member waiting for a break
|
|
4408
4485
|
|
|
@@ -4756,9 +4833,9 @@ declared:
|
|
|
4756
4833
|
| `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`. |
|
|
4757
4834
|
| `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
|
|
4758
4835
|
| `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`. |
|
|
4759
|
-
| `complete` | A wrap to cut short: any task but one with `wrapAllowance: 0`, under either completion mode (`command.complete.wrapAllowance`). The `outcomes` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. What travels is what the capability published: a code from its list where it has one (`command.complete.outcome.unknown`), a code at all where it requires one (`.outcome.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no outcomes (`.outcome.unexpected`). |
|
|
4836
|
+
| `complete` | A wrap to cut short: any task but a `provider-automatic` one with `wrapAllowance: 0`, under either completion mode (`command.complete.wrapAllowance`). The `outcomes` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. What travels is what the capability published: a code from its list where it has one (`command.complete.outcome.unknown`), a code at all where it requires one (`.outcome.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no outcomes (`.outcome.unexpected`). |
|
|
4760
4837
|
| `connect-back` | The `connectBack` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
|
|
4761
|
-
| `schedule` | The `schedule` capability, in `in-progress`, `paused` or `completing`: a follow-up is promised on the call and written up in wrap alike. |
|
|
4838
|
+
| `schedule` | The `schedule` capability, on any channel, in `in-progress`, `paused` or `completing`: a follow-up is promised on the call and written up in wrap alike. The manifest declares `calendar` for it to land on. |
|
|
4762
4839
|
| `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
|
|
4763
4840
|
| `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
|
|
4764
4841
|
| `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying a non-empty string for each `required` prompt field and strings for any optional fields supplied (`command.custom.prompt`). A toggle carries its target `on` boolean (`command.custom.on`). |
|
|
@@ -4884,6 +4961,24 @@ must specify their uncertainty and validity; elapsed time should use monotonic a
|
|
|
4884
4961
|
jump invalidates the estimate. The optional check below samples provider time without changing event timestamps. Report
|
|
4885
4962
|
source-measured durations where required; timestamp subtraction is not a substitute.
|
|
4886
4963
|
|
|
4964
|
+
#### Every screen counts from the provider's clock
|
|
4965
|
+
|
|
4966
|
+
Two screens watching the same agent -- their own desk, their lead's, a manager's on another floor
|
|
4967
|
+
-- must show the same numbers. Delay between them is fine; arithmetic that differs is not. So
|
|
4968
|
+
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.
|
|
4971
|
+
|
|
4972
|
+
Durations off the active call -- how long a member has been `on-break` from their `since`, how
|
|
4973
|
+
long a lead has been listening, how long since `signedInAt` -- are counted from the provider's
|
|
4974
|
+
clock, never the screen's. The desk keeps the latest provider instant it has received -- a
|
|
4975
|
+
snapshot's `providerTime`, an envelope's `occurredAt`, a `checkTime` result -- with the monotonic
|
|
4976
|
+
elapsed time since receipt, and renders every such duration from that; a sample older than
|
|
4977
|
+
`maxSampleAgeMs` is refreshed with `checkTime`. Without a usable sample the desk shows the instant
|
|
4978
|
+
and no count. A provider that publishes any of these instants therefore declares `timeCheck`
|
|
4979
|
+
(`manifest.timeCheck.required`) and states `providerTime` on every snapshot
|
|
4980
|
+
(`snapshot.providerTime`). Two screens then differ by their delay and nothing else.
|
|
4981
|
+
|
|
4887
4982
|
#### Optional periodic provider time checks and agent application estimates
|
|
4888
4983
|
|
|
4889
4984
|
A provider may declare `Manifest.timeCheck: true` and implement `Connection.checkTime(request)`.
|
|
@@ -5170,7 +5265,7 @@ take-over as it follows an `end-call` -- see **Lead assist**.
|
|
|
5170
5265
|
A successful `complete` command does not clear the task. Omni waits for `task-ended`,
|
|
5171
5266
|
and not for ever: `applied` to a `complete` says the
|
|
5172
5267
|
provider has completed the task, and its `task-ended` follows within the
|
|
5173
|
-
manifest's `
|
|
5268
|
+
manifest's `settleMs`. A provider never answers `applied` for a completion it has not yet
|
|
5174
5269
|
performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
|
|
5175
5270
|
held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
|
|
5176
5271
|
the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
|
|
@@ -5269,6 +5364,13 @@ the `contacts` idle capability.
|
|
|
5269
5364
|
Replaces this provider's complete scheduled-activity contribution. It is emitted only when the manifest
|
|
5270
5365
|
declares the `calendar` idle capability.
|
|
5271
5366
|
|
|
5367
|
+
### `shift-updated`
|
|
5368
|
+
|
|
5369
|
+
The agent's own day, whole, replacing `Snapshot.shift`: emitted whenever a total moves -- a task
|
|
5370
|
+
ended, a break ended, a sign-out -- and never to say nothing changed. It carries the same `Shift`
|
|
5371
|
+
the agent's lead sees for them on the team member list, so the two screens show one day. See
|
|
5372
|
+
**The agent's day is on the wire**.
|
|
5373
|
+
|
|
5272
5374
|
## Utilities
|
|
5273
5375
|
|
|
5274
5376
|
### `assignmentKey(providerId, assignmentId)`
|
|
@@ -5333,7 +5435,9 @@ carries that agent reports `team.member.self`, on a `team-member-updated` and a
|
|
|
5333
5435
|
reports `team.unentitled`, and a team event to such a login reports `event.team.capability`; given
|
|
5334
5436
|
`leadFeatures: true`, a snapshot without a team reports `team.required`, and given `leadFeatures: false`,
|
|
5335
5437
|
anything of the team reaching the lead reports `team.unexpected` or `event.team.features`. Without them those rules are not checked, because they cannot be.
|
|
5336
|
-
`exerciseAdapter` passes all three, holding the switch off until it has sent it.
|
|
5438
|
+
`exerciseAdapter` passes all three, holding the switch off until it has sent it. Given the manifest, a
|
|
5439
|
+
snapshot or event that carries an instant the desk renders as a running duration under a manifest
|
|
5440
|
+
without `timeCheck` reports `manifest.timeCheck.required`.
|
|
5337
5441
|
|
|
5338
5442
|
`assertNoViolations(violations)` throws `ProtocolConformanceError` — which carries the full
|
|
5339
5443
|
`violations` array — when the list is non-empty.
|
|
@@ -5515,6 +5619,7 @@ cannot be established from TypeScript structure alone.
|
|
|
5515
5619
|
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a forced break arriving in effect with `forced`. The harness applies the same rules after the connect snapshot. |
|
|
5516
5620
|
| `assertAudioFollowsTheTask(envelopes, snapshot?)` | The audio follows the task and never decides it: every task is introduced once, `task-audio-started` and `task-audio-ended` alternate on work that has begun, audio ends only where it arrived, and what follows the audio ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no audio satisfies it by never testing it — pair it with the assertion that the audio end is present. |
|
|
5517
5621
|
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
5622
|
+
| `assertForcedBreakStopsTheRest(forcedOn, candidates, stopped)` | A forced break on one provider stops the agent everywhere else: capacity zero stated on every other usable provider holding capacity, nothing on the forcing one, nothing on a dead login. See **Forced breaks**. |
|
|
5518
5623
|
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `on-break` only once nothing is outstanding — never beside a task, never later than the step that has none. |
|
|
5519
5624
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
5520
5625
|
| `assertWrapTimeout(task, audioEndedAt, deadline, toleranceMs?)` | The wrap deadline equals audio end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |
|