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