@xema/omni-protocol 0.1.89 → 0.1.91
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 +9 -9
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +9 -9
- package/dist/validation.js +18 -17
- package/guide.md +52 -39
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -180,7 +180,7 @@ export interface Manifest<C extends Channel = Channel> {
|
|
|
180
180
|
*/
|
|
181
181
|
runningStepReports?: true;
|
|
182
182
|
/**
|
|
183
|
-
* How long after an applied completion -- `complete`, or a lead's `take-over` -- the provider's
|
|
183
|
+
* How long after an applied completion -- `complete`, or a lead's `take-over-call` -- the provider's
|
|
184
184
|
* `task-ended` is owed, in milliseconds. A warm transfer's `complete` is not one: the agent's
|
|
185
185
|
* wrap runs after it as after any call. `applied` says the provider
|
|
186
186
|
* has completed the task; the ending follows within this, or the host resyncs and shows the
|
|
@@ -879,7 +879,7 @@ export type TaskLeadAssist = {
|
|
|
879
879
|
});
|
|
880
880
|
/**
|
|
881
881
|
* On the lead's own task for a call they joined: which member asked, with their note. Its
|
|
882
|
-
* presence is what makes `lead-assist` `take-over` and `leave` issuable.
|
|
882
|
+
* presence is what makes `lead-assist` `take-over-call` and `leave` issuable.
|
|
883
883
|
*/
|
|
884
884
|
export interface TaskAssisting {
|
|
885
885
|
memberId: UserId;
|
|
@@ -1119,7 +1119,7 @@ export type VoiceTaskCommand = {
|
|
|
1119
1119
|
/** The lead keeps the customer; the agent's task ends `transferred`. Needs `Task.assisting`. */
|
|
1120
1120
|
| {
|
|
1121
1121
|
type: "lead-assist";
|
|
1122
|
-
action: "take-over";
|
|
1122
|
+
action: "take-over-call";
|
|
1123
1123
|
}
|
|
1124
1124
|
/** The lead drops; the agent continues. The lead's task ends `left`. Needs `Task.assisting`. */
|
|
1125
1125
|
| {
|
|
@@ -1257,10 +1257,10 @@ export interface BreakRequest {
|
|
|
1257
1257
|
* A break placed on the agent rather than requested by them.
|
|
1258
1258
|
*
|
|
1259
1259
|
* `by` is required in both arms. Who put somebody off the floor survives whether or not the
|
|
1260
|
-
* break ends on a clock --
|
|
1260
|
+
* break ends on a clock -- a forced break with no origin is a state the agent cannot reason
|
|
1261
1261
|
* about, and one that ends on a condition is still somebody's decision.
|
|
1262
1262
|
*/
|
|
1263
|
-
export type
|
|
1263
|
+
export type ForcedBreak = {
|
|
1264
1264
|
by: UserId;
|
|
1265
1265
|
endsAutomatically: true;
|
|
1266
1266
|
endsAt: IsoTimestamp;
|
|
@@ -1279,9 +1279,9 @@ export interface BreakState {
|
|
|
1279
1279
|
retryAfterMs?: number;
|
|
1280
1280
|
/** Not-ready codes this provider offers. Omitted when it defines none. */
|
|
1281
1281
|
reasons?: BreakReason[];
|
|
1282
|
-
/** Which reason the current break is on, a published `BreakReason.id`. Omitted when there is no break; required on a break in effect or starting after the task where the provider publishes `reasons`,
|
|
1282
|
+
/** Which reason the current break is on, a published `BreakReason.id`. Omitted when there is no break; required on a break in effect or starting after the task where the provider publishes `reasons`, a forced one included. */
|
|
1283
1283
|
activeReasonId?: string;
|
|
1284
|
-
|
|
1284
|
+
forced?: ForcedBreak;
|
|
1285
1285
|
}
|
|
1286
1286
|
/** Capacity is a statement, not a request: it is taken, never refused. A provider that cannot carry the count allocates within what it can and says so on a `diagnostic`. */
|
|
1287
1287
|
export type CapacityResult = {
|
|
@@ -1398,7 +1398,7 @@ export type TeamBreakCommand = {
|
|
|
1398
1398
|
type: "policy";
|
|
1399
1399
|
policy: "ask" | "auto-approve" | "suspended";
|
|
1400
1400
|
}
|
|
1401
|
-
/** `reasonId` names a published `BreakReason.id`, required whenever the provider publishes reasons: the member's
|
|
1401
|
+
/** `reasonId` names a published `BreakReason.id`, required whenever the provider publishes reasons: the member's forced break carries it as `activeReasonId`. */
|
|
1402
1402
|
| {
|
|
1403
1403
|
type: "place";
|
|
1404
1404
|
memberId: UserId;
|
|
@@ -1692,7 +1692,7 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
1692
1692
|
setCapacity(capacity: AgentCapacity): Promise<CapacityResult>;
|
|
1693
1693
|
execute(request: TaskCommandRequest<C>): Promise<TaskCommandResult>;
|
|
1694
1694
|
disconnect(): Promise<void>;
|
|
1695
|
-
/** Required of any adapter publishing a `UserId` -- on
|
|
1695
|
+
/** Required of any adapter publishing a `UserId` -- on a forced break, a team member list, or history. */
|
|
1696
1696
|
describeUsers?(ids: UserId[]): Promise<User[]>;
|
|
1697
1697
|
/** Required when the manifest declares `idleCapabilities.dial`. */
|
|
1698
1698
|
dial?(request: DialRequest): Promise<DialResult>;
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
|
|
|
7
7
|
* rules -- each optional part of a task, each optional part of the break state and team member list, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.interactionHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.listening", "task.media", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.interactionHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.listening", "task.media", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
|
|
11
11
|
export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
|
|
12
12
|
export interface AdapterContractResult {
|
|
13
13
|
events: ProviderEventEnvelope[];
|
|
@@ -202,7 +202,7 @@ export declare class BreakStream {
|
|
|
202
202
|
/**
|
|
203
203
|
* A break follows its requests. Given a provider's stream -- seeded with its initial snapshot,
|
|
204
204
|
* either supplied separately or carried by a snapshot event -- every `break-state` moves the way the guide describes: a commit's states only
|
|
205
|
-
* after a grant, never backwards, and a break placed on the agent arriving in effect with `
|
|
205
|
+
* after a grant, never backwards, and a break placed on the agent arriving in effect with `forced`.
|
|
206
206
|
*/
|
|
207
207
|
export declare function assertBreakFollowsItsRequests(envelopes: readonly ProviderEventEnvelope[], snapshot?: Snapshot): void;
|
|
208
208
|
/**
|
package/dist/testing.js
CHANGED
|
@@ -23,7 +23,7 @@ const STATE_SUBJECTS = [
|
|
|
23
23
|
"task.custom",
|
|
24
24
|
"task.locked",
|
|
25
25
|
"break.reasons",
|
|
26
|
-
"break.
|
|
26
|
+
"break.forced",
|
|
27
27
|
"team.members",
|
|
28
28
|
"team.requests",
|
|
29
29
|
"contacts",
|
|
@@ -85,8 +85,8 @@ function observeBreak(value, seen) {
|
|
|
85
85
|
return;
|
|
86
86
|
if (some(value.reasons))
|
|
87
87
|
seen.add("break.reasons");
|
|
88
|
-
if (value.
|
|
89
|
-
seen.add("break.
|
|
88
|
+
if (value.forced !== undefined)
|
|
89
|
+
seen.add("break.forced");
|
|
90
90
|
}
|
|
91
91
|
function observeTeam(value, seen) {
|
|
92
92
|
if (!isRecord(value))
|
|
@@ -636,7 +636,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
636
636
|
* its shape.
|
|
637
637
|
*/
|
|
638
638
|
function publishesUserIds(snapshot) {
|
|
639
|
-
if (snapshot?.break?.
|
|
639
|
+
if (snapshot?.break?.forced?.by !== undefined)
|
|
640
640
|
return true;
|
|
641
641
|
if (teamNamesUsers(snapshot?.team))
|
|
642
642
|
return true;
|
|
@@ -644,12 +644,12 @@ function publishesUserIds(snapshot) {
|
|
|
644
644
|
return false;
|
|
645
645
|
return snapshot.tasks.some(taskNamesUsers);
|
|
646
646
|
}
|
|
647
|
-
/** Every UserId a snapshot publishes: on the record, the room, a lead request,
|
|
647
|
+
/** Every UserId a snapshot publishes: on the record, the room, a lead request, a forced break, the team member list. */
|
|
648
648
|
function userIdsIn(snapshot) {
|
|
649
649
|
const ids = new Set();
|
|
650
650
|
const add = (value) => { if (typeof value === "string" && value.length > 0)
|
|
651
651
|
ids.add(value); };
|
|
652
|
-
add(snapshot?.break?.
|
|
652
|
+
add(snapshot?.break?.forced?.by);
|
|
653
653
|
const team = snapshot?.team;
|
|
654
654
|
if (isRecord(team)) {
|
|
655
655
|
for (const member of Array.isArray(team.members) ? team.members : [])
|
|
@@ -692,7 +692,7 @@ function eventNamesUsers(envelope) {
|
|
|
692
692
|
return false;
|
|
693
693
|
switch (event.type) {
|
|
694
694
|
case "snapshot": return publishesUserIds(event.snapshot);
|
|
695
|
-
case "break-state": return isRecord(event.break) && isRecord(event.break.
|
|
695
|
+
case "break-state": return isRecord(event.break) && isRecord(event.break.forced) && event.break.forced.by !== undefined;
|
|
696
696
|
case "task-offered":
|
|
697
697
|
case "task-updated": return taskNamesUsers(event.task);
|
|
698
698
|
case "team-updated": return teamNamesUsers(event.team);
|
|
@@ -1868,7 +1868,7 @@ async function driveOneCall(drive) {
|
|
|
1868
1868
|
// A request goes not-requested -> awaiting-decision | granted; a commit goes granted ->
|
|
1869
1869
|
// starting-after-task | in-effect; work ending goes starting-after-task -> in-effect; a denial,
|
|
1870
1870
|
// a cancel, an end or a release goes back to not-requested; a placed break arrives in-effect
|
|
1871
|
-
// with `
|
|
1871
|
+
// with `forced`. Nothing else is a move the guide describes.
|
|
1872
1872
|
/** What a stream has said about the agent's break, and the moves it may not make. */
|
|
1873
1873
|
export class BreakStream {
|
|
1874
1874
|
state;
|
|
@@ -1914,7 +1914,7 @@ export class BreakStream {
|
|
|
1914
1914
|
/**
|
|
1915
1915
|
* A break follows its requests. Given a provider's stream -- seeded with its initial snapshot,
|
|
1916
1916
|
* either supplied separately or carried by a snapshot event -- every `break-state` moves the way the guide describes: a commit's states only
|
|
1917
|
-
* after a grant, never backwards, and a break placed on the agent arriving in effect with `
|
|
1917
|
+
* after a grant, never backwards, and a break placed on the agent arriving in effect with `forced`.
|
|
1918
1918
|
*/
|
|
1919
1919
|
export function assertBreakFollowsItsRequests(envelopes, snapshot) {
|
|
1920
1920
|
const stream = new BreakStream();
|
package/dist/validation.js
CHANGED
|
@@ -1293,23 +1293,23 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1293
1293
|
// ---------------------------------------------------------------------------
|
|
1294
1294
|
// Breaks, team, snapshot.
|
|
1295
1295
|
// ---------------------------------------------------------------------------
|
|
1296
|
-
function
|
|
1296
|
+
function validateForcedBreak(value, path, into) {
|
|
1297
1297
|
if (!isPlainObject(value)) {
|
|
1298
|
-
into.add("break.
|
|
1298
|
+
into.add("break.forced.shape", path, "a forced break must be an object");
|
|
1299
1299
|
return;
|
|
1300
1300
|
}
|
|
1301
1301
|
// `by` is required either way. Who put somebody off the floor survives whether or not the
|
|
1302
|
-
// break ends on a clock --
|
|
1302
|
+
// break ends on a clock -- a forced break with no origin is a state the agent cannot
|
|
1303
1303
|
// reason about.
|
|
1304
|
-
into.require(isUserId(value.by), "break.
|
|
1304
|
+
into.require(isUserId(value.by), "break.forced.by", `${path}.by`, "a forced break must say who placed it");
|
|
1305
1305
|
if (value.endsAutomatically === true) {
|
|
1306
|
-
into.timestamp(value.endsAt, "break.
|
|
1306
|
+
into.timestamp(value.endsAt, "break.forced.endsAt", `${path}.endsAt`);
|
|
1307
1307
|
}
|
|
1308
1308
|
else if (value.endsAutomatically === false) {
|
|
1309
|
-
into.require(value.endsAt === undefined, "break.
|
|
1309
|
+
into.require(value.endsAt === undefined, "break.forced.endsAt.unexpected", `${path}.endsAt`, "a break that does not end automatically must not carry an end time");
|
|
1310
1310
|
}
|
|
1311
1311
|
else {
|
|
1312
|
-
into.add("break.
|
|
1312
|
+
into.add("break.forced.endsAutomatically", `${path}.endsAutomatically`, "a forced break must say whether it ends automatically");
|
|
1313
1313
|
}
|
|
1314
1314
|
}
|
|
1315
1315
|
/** Checks break status against the full currently retained task set, after a transaction. */
|
|
@@ -1398,7 +1398,7 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1398
1398
|
validateBreakState(context.memberBreak, `${path}.memberBreak`, into);
|
|
1399
1399
|
const state = isPlainObject(context.memberBreak) ? context.memberBreak : {};
|
|
1400
1400
|
if (command.type === "release")
|
|
1401
|
-
into.require(state.
|
|
1401
|
+
into.require(state.forced !== undefined && (state.approval === "in-effect" || state.approval === "starting-after-task"), "team.break.command.release", path, "release a currently forced break");
|
|
1402
1402
|
if (command.type === "place") {
|
|
1403
1403
|
if (command.reasonId !== undefined)
|
|
1404
1404
|
into.filled(command.reasonId, "team.break.command.reasonId", path, "reasonId must not be empty");
|
|
@@ -1459,7 +1459,7 @@ export function validateBreakCommand(method, request, state, context, path = "br
|
|
|
1459
1459
|
into.require(approval === "awaiting-decision" || approval === "granted", "break.command.cancel.precommit", path, "cancel only a pre-commit request; a raced commit requires commit recovery");
|
|
1460
1460
|
if (method === "endBreak") {
|
|
1461
1461
|
into.require(approval === "starting-after-task" || approval === "in-effect", "break.command.end.started", path, "end a committed break, including a returning provider still finishing work");
|
|
1462
|
-
into.require(state.
|
|
1462
|
+
into.require(state.forced === undefined, "break.command.end.forced", path, "an agent cannot end a forced break; an authorized lead releases it");
|
|
1463
1463
|
}
|
|
1464
1464
|
}
|
|
1465
1465
|
return into.violations;
|
|
@@ -1480,7 +1480,7 @@ export function validateBreakTransition(before, after, path = "break") {
|
|
|
1480
1480
|
const from = before.approval;
|
|
1481
1481
|
const to = after.approval;
|
|
1482
1482
|
const committed = to === "starting-after-task" || to === "in-effect";
|
|
1483
|
-
if (committed && (from === "not-requested" || from === "awaiting-decision") && after.
|
|
1483
|
+
if (committed && (from === "not-requested" || from === "awaiting-decision") && after.forced === undefined) {
|
|
1484
1484
|
into.add("stream.breakState.commitBeforeGrant", `${path}.after.approval`, `${to} follows a commit, and a commit follows granted; the break stood at ${from}`);
|
|
1485
1485
|
}
|
|
1486
1486
|
const backwards = (from === "in-effect" && (to === "awaiting-decision" || to === "granted" || to === "starting-after-task")) ||
|
|
@@ -1495,6 +1495,7 @@ function validateBreakState(value, path, into) {
|
|
|
1495
1495
|
into.add("break.shape", path, "break state must be an object");
|
|
1496
1496
|
return;
|
|
1497
1497
|
}
|
|
1498
|
+
into.require(!Object.hasOwn(value, "imposed"), "break.forced.renamed", `${path}.imposed`, "imposed was renamed to forced; use only forced");
|
|
1498
1499
|
into.oneOf(value.approval, BREAK_APPROVALS, "break.approval", `${path}.approval`);
|
|
1499
1500
|
into.require(typeof value.mayAsk === "boolean", "break.mayAsk", `${path}.mayAsk`, "mayAsk says whether the agent may ask for a break: a boolean");
|
|
1500
1501
|
for (const field of ["refusedReason", "decisionReason"]) {
|
|
@@ -1515,12 +1516,12 @@ function validateBreakState(value, path, into) {
|
|
|
1515
1516
|
// break that is not happening.
|
|
1516
1517
|
into.require(value.approval !== "not-requested", "break.activeReasonId.approval", `${path}.activeReasonId`, "activeReasonId must be omitted when no break is requested or in effect");
|
|
1517
1518
|
}
|
|
1518
|
-
if (value.
|
|
1519
|
-
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1519
|
+
if (value.forced !== undefined) {
|
|
1520
|
+
validateForcedBreak(value.forced, `${path}.forced`, into);
|
|
1521
|
+
// A forced break is a break somebody placed; beside `not-requested` there is no break.
|
|
1522
|
+
// A forced break is a break in progress or about to be: it travels with in-effect or
|
|
1522
1523
|
// starting-after-task and nothing else. Beside granted, the host would commit a break nobody asked for.
|
|
1523
|
-
into.require(value.approval === "in-effect" || value.approval === "starting-after-task", "break.
|
|
1524
|
+
into.require(value.approval === "in-effect" || value.approval === "starting-after-task", "break.forced.approval", `${path}.forced`, `a forced break is in effect or starting after the task; ${describeValue(value.approval)} says the agent asked, or that there is none`);
|
|
1524
1525
|
}
|
|
1525
1526
|
if (value.reasons === undefined)
|
|
1526
1527
|
return;
|
|
@@ -1556,7 +1557,7 @@ function validateBreakState(value, path, into) {
|
|
|
1556
1557
|
into.require(seen.has(value.activeReasonId), "break.activeReasonId.known", `${path}.activeReasonId`, `activeReasonId names a reason the provider did not publish: ${value.activeReasonId}`);
|
|
1557
1558
|
}
|
|
1558
1559
|
// A break in effect, or about to be, on a provider that publishes reasons is on one of them: an
|
|
1559
|
-
//
|
|
1560
|
+
// forced one included, since the lead's place named it. A break of no kind is a break whose
|
|
1560
1561
|
// rules -- who may listen through it, whether it counts -- nobody can apply.
|
|
1561
1562
|
ruleEvaluated("break.activeReasonId.required");
|
|
1562
1563
|
if (seen.size > 0 && (value.approval === "in-effect" || value.approval === "starting-after-task")) {
|
|
@@ -2247,7 +2248,7 @@ export function validateHostReport(report, path = "host") {
|
|
|
2247
2248
|
// Commands.
|
|
2248
2249
|
// ---------------------------------------------------------------------------
|
|
2249
2250
|
const TRANSFER_ACTIONS = ["cold", "warm", "complete", "cancel"];
|
|
2250
|
-
const LEAD_ASSIST_ACTIONS = ["request", "cancel", "take-over", "leave"];
|
|
2251
|
+
const LEAD_ASSIST_ACTIONS = ["request", "cancel", "take-over-call", "leave"];
|
|
2251
2252
|
const CONFERENCE_ACTIONS = ["add", "remove"];
|
|
2252
2253
|
/** Validate the exact interaction/allocation target before provider command dispatch. */
|
|
2253
2254
|
export function validateTaskCommandRequest(request, task, path = "request", taskContext) {
|
package/guide.md
CHANGED
|
@@ -809,7 +809,7 @@ type VoiceTaskCommand =
|
|
|
809
809
|
| { type: "transfer"; action: "cancel" }
|
|
810
810
|
| { type: "lead-assist"; action: "request"; note?: string }
|
|
811
811
|
| { type: "lead-assist"; action: "cancel" }
|
|
812
|
-
| { type: "lead-assist"; action: "take-over" }
|
|
812
|
+
| { type: "lead-assist"; action: "take-over-call" }
|
|
813
813
|
| { type: "lead-assist"; action: "leave" }
|
|
814
814
|
| { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
|
|
815
815
|
| { type: "conference"; action: "remove"; destinationId: string; party?: never }
|
|
@@ -949,7 +949,7 @@ type BreakRequest = {
|
|
|
949
949
|
reasonId?: string;
|
|
950
950
|
};
|
|
951
951
|
|
|
952
|
-
type
|
|
952
|
+
type ForcedBreak =
|
|
953
953
|
| { by: UserId; endsAutomatically: true; endsAt: IsoTimestamp }
|
|
954
954
|
| { by: UserId; endsAutomatically: false; endsAt?: never };
|
|
955
955
|
|
|
@@ -961,7 +961,7 @@ type BreakState = {
|
|
|
961
961
|
retryAfterMs?: number;
|
|
962
962
|
reasons?: BreakReason[];
|
|
963
963
|
activeReasonId?: string;
|
|
964
|
-
|
|
964
|
+
forced?: ForcedBreak;
|
|
965
965
|
};
|
|
966
966
|
|
|
967
967
|
type BreakRequestResult =
|
|
@@ -1613,7 +1613,7 @@ compile time.
|
|
|
1613
1613
|
| `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; host polling is independently opt-in. |
|
|
1614
1614
|
| `timestampAuthority` | Optional `"provider"`: provider timestamps are final; host instants are advisory. Omission makes no trust promise. |
|
|
1615
1615
|
| `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
|
|
1616
|
-
| `completionSettleMs` | Required. How long after an applied completion -- `complete`, or a lead's `take-over` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1616
|
+
| `completionSettleMs` | Required. How long after an applied completion -- `complete`, or a lead's `take-over-call` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1617
1617
|
|
|
1618
1618
|
### Authentication methods
|
|
1619
1619
|
|
|
@@ -2023,7 +2023,7 @@ failure:
|
|
|
2023
2023
|
```
|
|
2024
2024
|
|
|
2025
2025
|
The `User` it carries is the **root of this provider's user namespace**. Every other person this
|
|
2026
|
-
provider names — a team member, the manager on
|
|
2026
|
+
provider names — a team member, the manager on a forced break, the agent on an interaction step —
|
|
2027
2027
|
is identified from the same directory and carries the same `UserId` type.
|
|
2028
2028
|
|
|
2029
2029
|
| Field | Contract |
|
|
@@ -2131,7 +2131,7 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
|
|
|
2131
2131
|
| --- | --- |
|
|
2132
2132
|
| `transport` | Current `TransportStatus` — whether this provider's transport can serve the login. Defined under **`transport-status`**. |
|
|
2133
2133
|
| `loginId` | Identity of this login. It must match the connection context. |
|
|
2134
|
-
| `break` | Complete break state, including approval, whether the agent may ask, reasons, retry details, and any
|
|
2134
|
+
| `break` | Complete break state, including approval, whether the agent may ask, reasons, retry details, and any forced break. |
|
|
2135
2135
|
| `tasks` | Complete set of tasks currently offered to or owned by this agent. |
|
|
2136
2136
|
| `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. |
|
|
2137
2137
|
| `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
|
|
@@ -2153,7 +2153,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2153
2153
|
| `refused(report)` | Always. The host tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the host does with what it refuses**. |
|
|
2154
2154
|
| `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
|
|
2155
2155
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
2156
|
-
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `
|
|
2156
|
+
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ForcedBreak.by`, a team member list, or `interactionHistory[].by`. Each `User` carries its `timeZone`; a person whose zone the provider cannot name is omitted from the answer, as any unresolvable id is. |
|
|
2157
2157
|
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
2158
2158
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
2159
2159
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
@@ -2211,7 +2211,7 @@ Turns `UserId` values into something an agent can read.
|
|
|
2211
2211
|
describeUsers(ids: UserId[]): Promise<User[]>
|
|
2212
2212
|
```
|
|
2213
2213
|
|
|
2214
|
-
Required of any adapter that publishes a `UserId` — on `
|
|
2214
|
+
Required of any adapter that publishes a `UserId` — on `ForcedBreak.by`, a team member list, or
|
|
2215
2215
|
`interactionHistory[].by`. Publishing an identifier Omni cannot resolve puts a name on screen
|
|
2216
2216
|
that reads as a database key.
|
|
2217
2217
|
|
|
@@ -2394,7 +2394,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2394
2394
|
| `interactionHistory` | 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**. |
|
|
2395
2395
|
| `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 host's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where a host placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2396
2396
|
| `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
|
|
2397
|
-
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead-assist` `take-over` and `leave` issuable. See **Lead assist**. |
|
|
2397
|
+
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead-assist` `take-over-call` and `leave` issuable. See **Lead assist**. |
|
|
2398
2398
|
| `listening` | Voice only, on the lead's own task while they listen to a member's call: whose call, which call, and the `mode` they are heard in, restated on every change. Never on the member's task, and never together with `assisting`. See **Listening to a call**. |
|
|
2399
2399
|
|
|
2400
2400
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
@@ -2856,7 +2856,7 @@ Four rules a provider has to keep:
|
|
|
2856
2856
|
conversation, and on live data that is the ordinary case rather than an edge. A zero is rejected.
|
|
2857
2857
|
- **`by` is a bare `UserId`, and not necessarily an agent.** A lead or a manager takes part
|
|
2858
2858
|
during an interaction too — a transfer accepted, a call conferenced in — so the field names whoever it was,
|
|
2859
|
-
the same way `
|
|
2859
|
+
the same way `ForcedBreak.by` does. It comes from this provider's own directory, the same
|
|
2860
2860
|
namespace as `AuthenticationState.identity.id` and the team member list, so entries pair
|
|
2861
2861
|
within a provider and never across one.
|
|
2862
2862
|
- **A task carries no names.** Omni resolves what to display with `describeUsers()`. Two people
|
|
@@ -3613,8 +3613,8 @@ nothing while none are being accepted — so they are not published separately.
|
|
|
3613
3613
|
| `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
|
|
3614
3614
|
| `retryAfterMs` | How long until the agent may retry, when the provider can say. |
|
|
3615
3615
|
| `reasons` | Not-ready codes this provider offers. Omitted when it defines none; an empty list is refused, being a second spelling of the same fact. |
|
|
3616
|
-
| `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break. Required on a break `in-effect` or `starting-after-task` where the provider publishes `reasons`,
|
|
3617
|
-
| `
|
|
3616
|
+
| `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break. Required on a break `in-effect` or `starting-after-task` where the provider publishes `reasons`, a forced break included (`break.activeReasonId.required`): a break with a kind the provider cannot name is a break whose rules nobody can apply. |
|
|
3617
|
+
| `forced` | Set when the break was placed on the agent rather than requested. |
|
|
3618
3618
|
|
|
3619
3619
|
A request can be waiting for two unrelated things, and they are separate values because
|
|
3620
3620
|
rendering one as the other tells an agent to wait for somebody who is never coming:
|
|
@@ -3640,42 +3640,48 @@ it; it sends the commit again only from a reconnect snapshot that shows the gran
|
|
|
3640
3640
|
refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
|
|
3641
3641
|
something a busy hour can cancel, and Omni keeps offering those while the rest are withdrawn.
|
|
3642
3642
|
|
|
3643
|
-
###
|
|
3643
|
+
### Forced breaks
|
|
3644
3644
|
|
|
3645
|
-
|
|
3645
|
+
Migration: ImposedBreak is now `ForcedBreak`, and the former imposed field is now
|
|
3646
|
+
`BreakState.forced`. Diagnostic and harness coverage names use `break.forced`; the agent-end
|
|
3647
|
+
diagnostic is `break.command.end.forced`. Update hosts and providers together. The old field
|
|
3648
|
+
is rejected, including when both spellings are sent; no compatibility alias is provided.
|
|
3649
|
+
The `place` and `release` commands and break ordering rules are unchanged.
|
|
3650
|
+
|
|
3651
|
+
`ForcedBreak` says who placed the break, whether automatic ending is enabled, and, when enabled,
|
|
3646
3652
|
when the provider will end it. A break the agent did not choose is not manually resumable by them.
|
|
3647
3653
|
|
|
3648
|
-
**Every
|
|
3649
|
-
thing as a break the platform
|
|
3654
|
+
**Every forced break has a person behind it.** A lead or a manager placed it; there is no such
|
|
3655
|
+
thing as a break the platform forced on its own. Where a platform applies one automatically, it is
|
|
3650
3656
|
executing a preference somebody configured, and that person is the owner of the action — `by` names
|
|
3651
3657
|
them, not the machinery that carried it out.
|
|
3652
3658
|
|
|
3653
3659
|
Omni resolves the name to show with `describeUsers()`, so a provider sends the identifier and never
|
|
3654
3660
|
a display name.
|
|
3655
3661
|
|
|
3656
|
-
**
|
|
3662
|
+
**A forced break travels with `in-effect` or `starting-after-task`, and nothing else.** It is a
|
|
3657
3663
|
break in progress or about to be; beside `granted` or `awaiting-decision` the host would read a
|
|
3658
|
-
request the agent never made and commit it (`break.
|
|
3664
|
+
request the agent never made and commit it (`break.forced.approval`). Where the provider publishes
|
|
3659
3665
|
`reasons`, the lead's `place` named one, and the member's state carries it as `activeReasonId`.
|
|
3660
3666
|
|
|
3661
3667
|
For example:
|
|
3662
3668
|
|
|
3663
3669
|
```ts
|
|
3664
|
-
|
|
3670
|
+
forced: {
|
|
3665
3671
|
by: "manager-1042",
|
|
3666
3672
|
endsAutomatically: true,
|
|
3667
3673
|
endsAt: "2026-08-21T10:00:00.000Z"
|
|
3668
3674
|
}
|
|
3669
3675
|
```
|
|
3670
3676
|
|
|
3671
|
-
The presence of `
|
|
3677
|
+
The presence of `forced` means the agent cannot end the break manually, so Omni withdraws its
|
|
3672
3678
|
Resume control from that agent. With `endsAutomatically: true`, the provider ends the break at
|
|
3673
3679
|
`endsAt`; with `endsAutomatically: false`, it does not end the break on a timer. An authorized lead
|
|
3674
3680
|
may end either form with **Resume**, not only whoever placed it. Omni shows **Stopped by <who>**,
|
|
3675
3681
|
resolving the name with `describeUsers()`, and shows when the break will end where automatic ending
|
|
3676
3682
|
is enabled.
|
|
3677
3683
|
|
|
3678
|
-
A break applies to the **agent**, not to one provider. When a provider
|
|
3684
|
+
A break applies to the **agent**, not to one provider. When a provider forces one, Omni immediately
|
|
3679
3685
|
requests a break on every other connected provider, or they would keep routing work to somebody who
|
|
3680
3686
|
is not there. Providers should expect that follow-on request.
|
|
3681
3687
|
|
|
@@ -3760,13 +3766,13 @@ request object only to the request method, and undefined to the other three.
|
|
|
3760
3766
|
| `requestBreak` | `not-requested`; selected current reason code when codes exist; `mayAsk` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
|
|
3761
3767
|
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `mayAsk` does not revoke the grant. |
|
|
3762
3768
|
| `cancelBreak` | `awaiting-decision` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
|
|
3763
|
-
| `endBreak` | `in-effect` or `starting-after-task` during reconciliation; an agent cannot end
|
|
3769
|
+
| `endBreak` | `in-effect` or `starting-after-task` during reconciliation; an agent cannot end a forced break. |
|
|
3764
3770
|
|
|
3765
3771
|
Use `validateTeamBreakCommand(request, context)` for lead decisions, placement, release and
|
|
3766
3772
|
policy commands. It requires the live lead capability and active transport, a current target
|
|
3767
3773
|
team member list for member commands, and the target's complete break state for placement/release.
|
|
3768
3774
|
Approve/deny requires an awaiting decision; placement uses the target's reason codes; release
|
|
3769
|
-
requires
|
|
3775
|
+
requires a forced committed break. The context's target state must belong to the named member;
|
|
3770
3776
|
that association and backend authorization are provider responsibilities.
|
|
3771
3777
|
|
|
3772
3778
|
Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
|
|
@@ -3855,7 +3861,7 @@ specificity: about this agent's own work is `coaching`, else something they must
|
|
|
3855
3861
|
`training`, else `meeting`.
|
|
3856
3862
|
|
|
3857
3863
|
**A break somebody placed on the agent is not automatically one of these.** Where the agent was
|
|
3858
|
-
stopped rather than choosing to stop — see **
|
|
3864
|
+
stopped rather than choosing to stop — see **Forced breaks** — set `BreakState.forced` and prefer
|
|
3859
3865
|
omitting `kind` to reaching for `other`. None of the ten describes "something was done to this
|
|
3860
3866
|
agent", and `other` claims a classification that was never made.
|
|
3861
3867
|
|
|
@@ -3984,8 +3990,8 @@ If cancel races with a late approval, the provider remains on the cancel path. I
|
|
|
3984
3990
|
won, cancel returns `omni.break-already-committed`, and Omni resumes commit recovery rather than
|
|
3985
3991
|
returning the agent to `working`.
|
|
3986
3992
|
|
|
3987
|
-
|
|
3988
|
-
cancellation, Omni follows the
|
|
3993
|
+
A forced break is not rolled back by this algorithm. If one appears during either the request or
|
|
3994
|
+
cancellation, Omni follows the forced-break rule on the other providers and commits each grant as
|
|
3989
3995
|
soon as it reaches `granted`; it does not wait for unanimity because the agent has already
|
|
3990
3996
|
stopped elsewhere.
|
|
3991
3997
|
|
|
@@ -4094,8 +4100,8 @@ One method, `executeTeamBreak`, taking a discriminated command exactly as `execu
|
|
|
4094
4100
|
| --- | --- |
|
|
4095
4101
|
| `{ type: "decide", memberId: UserId, 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`. |
|
|
4096
4102
|
| `{ type: "policy", policy }` | `ask`, `auto-approve`, or `suspended`. |
|
|
4097
|
-
| `{ type: "place", memberId: UserId, reasonId?, reason? }` | 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
|
|
4098
|
-
| `{ type: "release", memberId: UserId }` | Ends
|
|
4103
|
+
| `{ type: "place", memberId: UserId, reasonId?, reason? }` | 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. |
|
|
4104
|
+
| `{ type: "release", memberId: UserId }` | Ends a forced break on that member, whoever placed it. |
|
|
4099
4105
|
|
|
4100
4106
|
`memberId` is this provider's own identifier for the member, as published on its team member list. It is
|
|
4101
4107
|
never an identifier from another provider, and Omni does not translate between them; names come
|
|
@@ -4103,7 +4109,7 @@ from `describeUsers()`.
|
|
|
4103
4109
|
|
|
4104
4110
|
`suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
4105
4111
|
approve them. A provider that suspends breaks must also publish `mayAsk: false` to the team's
|
|
4106
|
-
agents so they see it before asking. A `place` must likewise reach that member as an `
|
|
4112
|
+
agents so they see it before asking. A `place` must likewise reach that member as an `forced` break
|
|
4107
4113
|
on their own `BreakState`, or they are stopped from working with no way to see why.
|
|
4108
4114
|
|
|
4109
4115
|
What happens when no lead is online — auto-approving, for instance — is the provider's decision and is
|
|
@@ -4158,12 +4164,19 @@ The request stands for another lead, as it does when this one is already on a ca
|
|
|
4158
4164
|
provider clears `leadAssist` from the agent's task** and drops the request from every team member list. Nothing
|
|
4159
4165
|
else changes; the agent is still on the call.
|
|
4160
4166
|
|
|
4167
|
+
**Take over call** uses `{ type: "lead-assist", action: "take-over-call" }`.
|
|
4168
|
+
The lead assumes the agent’s interaction; the caller’s wider journey continues.
|
|
4169
|
+
Migration: replace the former take-over action literal with `take-over-call` in hosts and
|
|
4170
|
+
providers together. The former spelling is rejected, with no compatibility alias.
|
|
4171
|
+
The `taken-over` task outcome and existing completion behavior remain unchanged.
|
|
4172
|
+
This action is distinct from `join-call`, which lets everyone hear a lead listening to a call.
|
|
4173
|
+
|
|
4161
4174
|
The lead then has two commands on their copy, gated by `assisting` being present, and a third
|
|
4162
4175
|
choice that is no command at all:
|
|
4163
4176
|
|
|
4164
4177
|
| The lead | The agent's task | The lead's task |
|
|
4165
4178
|
| --- | --- | --- |
|
|
4166
|
-
| `{ type: "lead-assist", action: "take-over" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
|
|
4179
|
+
| `{ type: "lead-assist", action: "take-over-call" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
|
|
4167
4180
|
| `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-media-ended`, then `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
4168
4181
|
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own outcome | The same, independently: **both have the completion window** |
|
|
4169
4182
|
|
|
@@ -4641,7 +4654,7 @@ declared:
|
|
|
4641
4654
|
| `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
|
|
4642
4655
|
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
|
|
4643
4656
|
| `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
|
|
4644
|
-
| `lead-assist` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
4657
|
+
| `lead-assist` with `action: "take-over-call"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
4645
4658
|
| `transfer` with a destination, `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`). |
|
|
4646
4659
|
| `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`). |
|
|
4647
4660
|
| Everything else | Its own named capability. |
|
|
@@ -4931,11 +4944,11 @@ covers a `diagnostic`: shown where the agent works, and counted.
|
|
|
4931
4944
|
Replaces this provider's complete `break` object. Its `approval` uses the canonical
|
|
4932
4945
|
`not-requested`, `awaiting-decision`, `granted`, `starting-after-task` and
|
|
4933
4946
|
`in-effect` states defined under Breaks; the event also carries the corresponding may-ask state,
|
|
4934
|
-
reasons, retry details, and any
|
|
4947
|
+
reasons, retry details, and any forced break.
|
|
4935
4948
|
|
|
4936
4949
|
Each state is also held to the one before it. A commit's states, `starting-after-task` and
|
|
4937
4950
|
`in-effect`, follow a grant — the one arrival in a committed state nobody asked for is a placed
|
|
4938
|
-
break, which says so with `
|
|
4951
|
+
break, which says so with `forced`: in effect at once, or `starting-after-task` while the member
|
|
4939
4952
|
finishes the call they are on (`stream.breakState.commitBeforeGrant`); and a break never moves backwards —
|
|
4940
4953
|
from `in-effect` or `starting-after-task` to a grant or a request, or from `granted` to
|
|
4941
4954
|
`awaiting-decision` — a new request passes through `not-requested` (`stream.breakState.backwards`).
|
|
@@ -4968,8 +4981,8 @@ Normal order is `not-requested` → `awaiting-decision` → `granted` →
|
|
|
4968
4981
|
`starting-after-task` → `in-effect` → `not-requested`. Auto-approval may go directly to
|
|
4969
4982
|
`granted`; a commit with no outstanding work may go directly to `in-effect`. Denial/cancel
|
|
4970
4983
|
returns a precommit request to `not-requested`; an authorized end/release returns a committed
|
|
4971
|
-
break there. Same-state restatements are allowed. An evidenced
|
|
4972
|
-
exception to requesting/granting, and must carry its
|
|
4984
|
+
break there. Same-state restatements are allowed. An evidenced forced break is the explicit
|
|
4985
|
+
exception to requesting/granting, and must carry its forced actor/state. Neither skipped
|
|
4973
4986
|
publication nor a host-local guess creates another exception. A later normal attempt starts
|
|
4974
4987
|
from `not-requested`, never by regressing an active break into a request.
|
|
4975
4988
|
|
|
@@ -5044,7 +5057,7 @@ A `left` outcome ends the task for this agent alone: the call continues without
|
|
|
5044
5057
|
when a lead who joined it leaves -- see **Lead assist**.
|
|
5045
5058
|
|
|
5046
5059
|
A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
|
|
5047
|
-
and not for ever: `applied` to a completion -- `complete`, or a lead's `take-over` -- says the
|
|
5060
|
+
and not for ever: `applied` to a completion -- `complete`, or a lead's `take-over-call` -- says the
|
|
5048
5061
|
provider has completed the task, and its `task-ended` follows within the
|
|
5049
5062
|
manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
|
|
5050
5063
|
performed. Past the bound the host calls `snapshot()`: a snapshot still carrying the task is a task
|
|
@@ -5138,7 +5151,7 @@ issued by each provider independently, so two providers will eventually issue th
|
|
|
5138
5151
|
different people. Encode and join before storing or comparing.
|
|
5139
5152
|
|
|
5140
5153
|
Use it for every `UserId` — `interactionHistory[].by`, team members, `memberId` on a
|
|
5141
|
-
lead command, `
|
|
5154
|
+
lead command, `ForcedBreak.by`. A bare one is only ever compared against another from the **same** provider; anything
|
|
5142
5155
|
wider goes through this key.
|
|
5143
5156
|
|
|
5144
5157
|
### `sameCapabilities(a, b)`
|
|
@@ -5249,7 +5262,7 @@ A capability granted by a later login requires its methods just as one declared
|
|
|
5249
5262
|
|
|
5250
5263
|
`result.notExercised` lists what the run never reached — one subject per family of rules: each
|
|
5251
5264
|
optional part of a task (`task.browsers`, `task.interactionHistory`, `task.leadAssist`, …), the break's
|
|
5252
|
-
`reasons` and `
|
|
5265
|
+
`reasons` and `forced`, the team member list's `members` and `requests`, each declared contribution, and
|
|
5253
5266
|
each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
|
|
5254
5267
|
Nothing there is a violation: an adapter with no team has nothing to exercise. But a fixture with
|
|
5255
5268
|
no tasks exercises no task rule, and a pass over it reads as coverage it is not.
|
|
@@ -5367,7 +5380,7 @@ cannot be established from TypeScript structure alone.
|
|
|
5367
5380
|
| `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
|
|
5368
5381
|
| `stillHost(report?, guarantees?, mute?)` | A host that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a softphone voice adapter, and never for a desk phone. |
|
|
5369
5382
|
| `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for a host that wants the same rules at its boundary: `seed(snapshot)`, then `apply(envelope)` returns the violations. |
|
|
5370
|
-
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a placed break arriving in effect with `
|
|
5383
|
+
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a placed break arriving in effect with `forced`. The harness applies the same rules after the connect snapshot. |
|
|
5371
5384
|
| `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-started` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
|
|
5372
5385
|
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
5373
5386
|
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `in-effect` only once nothing is outstanding — never beside a task, never later than the step that has none. |
|