@xema/omni-protocol 0.1.92 → 0.1.94
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 +14 -14
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +8 -8
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +35 -32
- package/guide.md +88 -51
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -236,8 +236,8 @@ export interface AuthenticationFailure {
|
|
|
236
236
|
/** What a lead may do with their team. Declared by presence. */
|
|
237
237
|
export interface TeamCapabilities {
|
|
238
238
|
/**
|
|
239
|
-
* This lead may act on their team's breaks through `executeTeamBreak` --
|
|
240
|
-
* set
|
|
239
|
+
* This lead may act on their team's breaks through `executeTeamBreak` -- force-break, end-forced-break, decide-break-request,
|
|
240
|
+
* set-break-policy -- as far as the provider supports; a command it lacks answers
|
|
241
241
|
* `omni.capability-not-enabled`. Requires `executeTeamBreak`.
|
|
242
242
|
*/
|
|
243
243
|
breakControl?: true;
|
|
@@ -1223,7 +1223,7 @@ export type BreakApproval = "not-requested"
|
|
|
1223
1223
|
/** Granted, and a promise to honour a later commit. */
|
|
1224
1224
|
| "granted"
|
|
1225
1225
|
/** Granted and begins when the current task ends. Nobody needs to act. */
|
|
1226
|
-
| "starting-after-task" | "
|
|
1226
|
+
| "starting-after-task" | "on-break";
|
|
1227
1227
|
export declare const BREAK_KINDS: readonly ["short-break", "meal", "rest", "training", "coaching", "meeting", "administrative", "technical", "personal", "other"];
|
|
1228
1228
|
/**
|
|
1229
1229
|
* The breaks a contact centre runs, named once so providers can agree.
|
|
@@ -1245,7 +1245,7 @@ export interface BreakReason {
|
|
|
1245
1245
|
label: string;
|
|
1246
1246
|
group?: string;
|
|
1247
1247
|
kind?: BreakKind;
|
|
1248
|
-
/** Survives `
|
|
1248
|
+
/** Survives `canRequestBreak: false`: a mandatory rest is not something a busy hour can cancel. */
|
|
1249
1249
|
alwaysAvailable?: true;
|
|
1250
1250
|
}
|
|
1251
1251
|
export interface BreakRequest {
|
|
@@ -1272,11 +1272,11 @@ export type ForcedBreak = {
|
|
|
1272
1272
|
export interface BreakState {
|
|
1273
1273
|
approval: BreakApproval;
|
|
1274
1274
|
/** Whether the agent may ask at all. Distinct from the fate of a request already made. */
|
|
1275
|
-
|
|
1276
|
-
/** Shown when `
|
|
1277
|
-
|
|
1275
|
+
canRequestBreak: boolean;
|
|
1276
|
+
/** Shown when `canRequestBreak` is false, such as "Busy hours". */
|
|
1277
|
+
requestUnavailableReason?: string;
|
|
1278
1278
|
decisionReason?: string;
|
|
1279
|
-
|
|
1279
|
+
retryRequestAfterMs?: number;
|
|
1280
1280
|
/** Not-ready codes this provider offers. Omitted when it defines none. */
|
|
1281
1281
|
reasons?: BreakReason[];
|
|
1282
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. */
|
|
@@ -1320,7 +1320,7 @@ export interface TeamMember {
|
|
|
1320
1320
|
availability: TeamMemberAvailability;
|
|
1321
1321
|
/** Omitted rather than invented: Omni renders it as a duration. */
|
|
1322
1322
|
since?: IsoTimestamp;
|
|
1323
|
-
/** A request in flight or a grant not yet in effect. `not-requested` is absence, and `
|
|
1323
|
+
/** A request in flight or a grant not yet in effect. `not-requested` is absence, and `on-break` is `availability: "on-break"`. */
|
|
1324
1324
|
break?: Extract<BreakApproval, "awaiting-decision" | "granted" | "starting-after-task">;
|
|
1325
1325
|
}
|
|
1326
1326
|
/** A member asking this lead to join their call. */
|
|
@@ -1390,22 +1390,22 @@ export interface TeamListenCommandRequest {
|
|
|
1390
1390
|
command: TeamListenCommand;
|
|
1391
1391
|
}
|
|
1392
1392
|
export type TeamBreakCommand = {
|
|
1393
|
-
type: "decide";
|
|
1393
|
+
type: "decide-break-request";
|
|
1394
1394
|
memberId: UserId;
|
|
1395
1395
|
decision: "granted" | "denied";
|
|
1396
1396
|
reason?: string;
|
|
1397
1397
|
} | {
|
|
1398
|
-
type: "policy";
|
|
1399
|
-
policy: "
|
|
1398
|
+
type: "set-break-policy";
|
|
1399
|
+
policy: "approval-required" | "automatically-approved" | "requests-suspended";
|
|
1400
1400
|
}
|
|
1401
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
|
-
type: "force";
|
|
1403
|
+
type: "force-break";
|
|
1404
1404
|
memberId: UserId;
|
|
1405
1405
|
reasonId?: string;
|
|
1406
1406
|
reason?: string;
|
|
1407
1407
|
} | {
|
|
1408
|
-
type: "
|
|
1408
|
+
type: "end-forced-break";
|
|
1409
1409
|
memberId: UserId;
|
|
1410
1410
|
};
|
|
1411
1411
|
export type TeamCommandResult = {
|
package/dist/testing.d.ts
CHANGED
|
@@ -248,7 +248,7 @@ export interface BreakOnTaskStep {
|
|
|
248
248
|
/**
|
|
249
249
|
* A break asked for on a task begins when the work ends. `steps` is the sequence the provider
|
|
250
250
|
* published, first to last: the request is made while work is outstanding, the commit is reported
|
|
251
|
-
* as `starting-after-task` while it remains, and `
|
|
251
|
+
* as `starting-after-task` while it remains, and `on-break` arrives only once nothing is
|
|
252
252
|
* outstanding -- never beside a task, and never later than the step that has none.
|
|
253
253
|
*/
|
|
254
254
|
export declare function assertBreakBeginsAfterTask(steps: readonly BreakOnTaskStep[]): void;
|
package/dist/testing.js
CHANGED
|
@@ -949,11 +949,11 @@ export function assertDeniedAndRetriedBreak(approvals) {
|
|
|
949
949
|
const refused = approvals.indexOf("not-requested", asked + 1);
|
|
950
950
|
if (refused < 0)
|
|
951
951
|
throw new Error("A refused break must return to not-requested, leaving nothing pending");
|
|
952
|
-
const granted = approvals.findIndex((approval, index) => index > refused && (approval === "granted" || approval === "
|
|
952
|
+
const granted = approvals.findIndex((approval, index) => index > refused && (approval === "granted" || approval === "on-break"));
|
|
953
953
|
if (granted < 0)
|
|
954
954
|
throw new Error("Break retry scenario must grant a later request");
|
|
955
955
|
const last = approvals.at(-1);
|
|
956
|
-
if (last !== "granted" && last !== "
|
|
956
|
+
if (last !== "granted" && last !== "on-break") {
|
|
957
957
|
throw new Error(`Break retry scenario must end granted or in effect, ended ${String(last)}`);
|
|
958
958
|
}
|
|
959
959
|
}
|
|
@@ -1866,8 +1866,8 @@ async function driveOneCall(drive) {
|
|
|
1866
1866
|
return found;
|
|
1867
1867
|
}
|
|
1868
1868
|
// A request goes not-requested -> awaiting-decision | granted; a commit goes granted ->
|
|
1869
|
-
// starting-after-task |
|
|
1870
|
-
// a cancel, an end or a
|
|
1869
|
+
// starting-after-task | on-break; work ending goes starting-after-task -> on-break; a denial,
|
|
1870
|
+
// a cancel, an agent end or a lead end goes back to not-requested; a forced break arrives on-break
|
|
1871
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 {
|
|
@@ -1990,7 +1990,7 @@ export function assertBreakAttemptProviders(candidates, asked) {
|
|
|
1990
1990
|
/**
|
|
1991
1991
|
* A break asked for on a task begins when the work ends. `steps` is the sequence the provider
|
|
1992
1992
|
* published, first to last: the request is made while work is outstanding, the commit is reported
|
|
1993
|
-
* as `starting-after-task` while it remains, and `
|
|
1993
|
+
* as `starting-after-task` while it remains, and `on-break` arrives only once nothing is
|
|
1994
1994
|
* outstanding -- never beside a task, and never later than the step that has none.
|
|
1995
1995
|
*/
|
|
1996
1996
|
export function assertBreakBeginsAfterTask(steps) {
|
|
@@ -2003,14 +2003,14 @@ export function assertBreakBeginsAfterTask(steps) {
|
|
|
2003
2003
|
if (committed < 0)
|
|
2004
2004
|
throw new Error("A break committed on a task is reported as starting-after-task while the work remains");
|
|
2005
2005
|
steps.forEach((step, index) => {
|
|
2006
|
-
if (step.approval === "
|
|
2007
|
-
throw new Error(`steps[${index}] reports
|
|
2006
|
+
if (step.approval === "on-break" && step.outstanding > 0) {
|
|
2007
|
+
throw new Error(`steps[${index}] reports on-break with ${step.outstanding} task(s) outstanding: a break begins when the work ends`);
|
|
2008
2008
|
}
|
|
2009
2009
|
if (step.approval === "starting-after-task" && step.outstanding < 1) {
|
|
2010
2010
|
throw new Error(`steps[${index}] reports starting-after-task with nothing outstanding: the break should have begun`);
|
|
2011
2011
|
}
|
|
2012
2012
|
});
|
|
2013
|
-
if (steps.at(-1)?.approval !== "
|
|
2013
|
+
if (steps.at(-1)?.approval !== "on-break") {
|
|
2014
2014
|
throw new Error(`Break-on-task scenario must end in effect, ended ${String(steps.at(-1)?.approval)}`);
|
|
2015
2015
|
}
|
|
2016
2016
|
}
|
package/dist/validation.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export declare function validateTask(task: unknown, context: TaskValidationConte
|
|
|
51
51
|
export declare function validateBreakStatus(state: unknown, tasks: unknown, path?: string): ProtocolViolation[];
|
|
52
52
|
/**
|
|
53
53
|
* Checks lead break dispatch using current authentication, transport, team member list (`team`) and,
|
|
54
|
-
* for force/
|
|
54
|
+
* for force-break/end-forced-break, the target's full `memberBreak`. The provider must authorize the target
|
|
55
55
|
* and recheck the decision atomically; a team member list is not authority to act after it has changed.
|
|
56
56
|
*/
|
|
57
57
|
export declare function validateTeamBreakCommand(request: unknown, context: unknown, path?: string): ProtocolViolation[];
|
package/dist/validation.js
CHANGED
|
@@ -52,7 +52,7 @@ const AUTHENTICATION_STATUSES = membersOf({
|
|
|
52
52
|
"signed-out": true, authenticating: true, authenticated: true, refreshing: true, expired: true,
|
|
53
53
|
});
|
|
54
54
|
const BREAK_APPROVALS = membersOf({
|
|
55
|
-
"not-requested": true, "awaiting-decision": true, granted: true, "starting-after-task": true, "
|
|
55
|
+
"not-requested": true, "awaiting-decision": true, granted: true, "starting-after-task": true, "on-break": true,
|
|
56
56
|
});
|
|
57
57
|
const TEAM_AVAILABILITIES = membersOf({
|
|
58
58
|
ready: true, "on-task": true, "on-break": true, reserved: true, "signed-out": true,
|
|
@@ -1331,13 +1331,13 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
|
|
|
1331
1331
|
ruleEvaluated("break.starting-after-task.tasks");
|
|
1332
1332
|
if (isPlainObject(state) && state.approval === "starting-after-task"
|
|
1333
1333
|
&& Array.isArray(tasks) && tasks.length === 0) {
|
|
1334
|
-
into.add("break.starting-after-task.tasks", `${path}.break.approval`, "a break starting after the task waits on a task, and the snapshot carries none: with nothing outstanding the break is
|
|
1334
|
+
into.add("break.starting-after-task.tasks", `${path}.break.approval`, "a break starting after the task waits on a task, and the snapshot carries none: with nothing outstanding the break is on-break");
|
|
1335
1335
|
}
|
|
1336
|
-
if (isPlainObject(state) && state.approval === "
|
|
1336
|
+
if (isPlainObject(state) && state.approval === "on-break"
|
|
1337
1337
|
&& Array.isArray(tasks) && tasks.length > 0) {
|
|
1338
1338
|
const listening = tasks.every((task) => isPlainObject(task) && task.listening !== undefined);
|
|
1339
1339
|
if (!listening) {
|
|
1340
|
-
into.add("break.
|
|
1340
|
+
into.add("break.on-break.tasks", `${path}.tasks`, "a break in effect holds no task: it begins when the work ends, and until then the state is starting-after-task");
|
|
1341
1341
|
}
|
|
1342
1342
|
else {
|
|
1343
1343
|
const reasons = Array.isArray(state.reasons) ? state.reasons : [];
|
|
@@ -1350,7 +1350,7 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
|
|
|
1350
1350
|
}
|
|
1351
1351
|
/**
|
|
1352
1352
|
* Checks lead break dispatch using current authentication, transport, team member list (`team`) and,
|
|
1353
|
-
* for force/
|
|
1353
|
+
* for force-break/end-forced-break, the target's full `memberBreak`. The provider must authorize the target
|
|
1354
1354
|
* and recheck the decision atomically; a team member list is not authority to act after it has changed.
|
|
1355
1355
|
*/
|
|
1356
1356
|
export function validateTeamBreakCommand(request, context, path = "teamBreakCommand") {
|
|
@@ -1366,8 +1366,8 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1366
1366
|
into.require(isPlainObject(caps.team) && caps.team.breakControl === true, "team.break.command.capability", path, "the login must declare team.breakControl");
|
|
1367
1367
|
const command = request.command;
|
|
1368
1368
|
const allowed = {
|
|
1369
|
-
decide: ["type", "memberId", "decision", "reason"], policy: ["type", "policy"],
|
|
1370
|
-
force: ["type", "memberId", "reasonId", "reason"],
|
|
1369
|
+
"decide-break-request": ["type", "memberId", "decision", "reason"], "set-break-policy": ["type", "policy"],
|
|
1370
|
+
"force-break": ["type", "memberId", "reasonId", "reason"], "end-forced-break": ["type", "memberId"],
|
|
1371
1371
|
};
|
|
1372
1372
|
const fields = typeof command.type === "string" && Object.hasOwn(allowed, command.type) ? allowed[command.type] : undefined;
|
|
1373
1373
|
if (!fields) {
|
|
@@ -1380,8 +1380,8 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1380
1380
|
into.require(fields.includes(key), "team.break.command.field", `${path}.${key}`, "field is not supported for this command");
|
|
1381
1381
|
if (command.reason !== undefined)
|
|
1382
1382
|
into.filled(command.reason, "team.break.command.reason", path, "reason must not be empty");
|
|
1383
|
-
if (command.type === "policy") {
|
|
1384
|
-
into.require(["
|
|
1383
|
+
if (command.type === "set-break-policy") {
|
|
1384
|
+
into.require(["approval-required", "automatically-approved", "requests-suspended"].includes(command.policy), "team.break.command.policy", path, "unknown break policy");
|
|
1385
1385
|
return into.violations;
|
|
1386
1386
|
}
|
|
1387
1387
|
into.filled(command.memberId, "team.break.command.member", path, "name the target member");
|
|
@@ -1390,20 +1390,20 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1390
1390
|
const member = members.find((m) => isPlainObject(m) && m.id === command.memberId);
|
|
1391
1391
|
const self = isPlainObject(auth.identity) ? auth.identity.id : undefined;
|
|
1392
1392
|
into.require(isPlainObject(member) && command.memberId !== self, "team.break.command.member", path, "the target must be another member of the current authorized team member list");
|
|
1393
|
-
if (command.type === "decide") {
|
|
1393
|
+
if (command.type === "decide-break-request") {
|
|
1394
1394
|
into.require(command.decision === "granted" || command.decision === "denied", "team.break.command.decision", path, "decide granted or denied");
|
|
1395
1395
|
into.require(isPlainObject(member) && member.break === "awaiting-decision", "team.break.command.awaiting", path, "decide only a currently awaiting-decision request");
|
|
1396
1396
|
}
|
|
1397
1397
|
else {
|
|
1398
1398
|
validateBreakState(context.memberBreak, `${path}.memberBreak`, into);
|
|
1399
1399
|
const state = isPlainObject(context.memberBreak) ? context.memberBreak : {};
|
|
1400
|
-
if (command.type === "
|
|
1401
|
-
into.require(state.forced !== undefined && (state.approval === "
|
|
1402
|
-
if (command.type === "force") {
|
|
1400
|
+
if (command.type === "end-forced-break")
|
|
1401
|
+
into.require(state.forced !== undefined && (state.approval === "on-break" || state.approval === "starting-after-task"), "team.break.command.endForcedBreak", path, "end a currently forced break");
|
|
1402
|
+
if (command.type === "force-break") {
|
|
1403
1403
|
if (command.reasonId !== undefined)
|
|
1404
1404
|
into.filled(command.reasonId, "team.break.command.reasonId", path, "reasonId must not be empty");
|
|
1405
1405
|
const reasons = Array.isArray(state.reasons) ? state.reasons : [];
|
|
1406
|
-
into.require(state.reasons === undefined ? command.reasonId === undefined : reasons.some((r) => isPlainObject(r) && r.id === command.reasonId), "team.break.command.reasonId", path, "force uses the target's currently published reason codes");
|
|
1406
|
+
into.require(state.reasons === undefined ? command.reasonId === undefined : reasons.some((r) => isPlainObject(r) && r.id === command.reasonId), "team.break.command.reasonId", path, "force-break uses the target's currently published reason codes");
|
|
1407
1407
|
}
|
|
1408
1408
|
}
|
|
1409
1409
|
return into.violations;
|
|
@@ -1449,17 +1449,17 @@ export function validateBreakCommand(method, request, state, context, path = "br
|
|
|
1449
1449
|
into.require(selected !== undefined, "break.request.reasonId", path, "choose a currently published reasonId; free text is not a substitute");
|
|
1450
1450
|
else
|
|
1451
1451
|
into.require(request.reasonId === undefined, "break.request.reasonId.unexpected", path, "no reason codes were published");
|
|
1452
|
-
into.require(state.
|
|
1452
|
+
into.require(state.canRequestBreak === true || (isPlainObject(selected) && selected.alwaysAvailable === true), "break.request.canRequestBreak", path, "asking is disabled except for the selected alwaysAvailable reason");
|
|
1453
1453
|
}
|
|
1454
1454
|
else {
|
|
1455
1455
|
into.require(request === undefined, "break.command.arguments", path, "this break method takes no arguments");
|
|
1456
1456
|
if (method === "commitBreak")
|
|
1457
|
-
into.require(approval === "granted" || approval === "starting-after-task" || approval === "
|
|
1457
|
+
into.require(approval === "granted" || approval === "starting-after-task" || approval === "on-break", "break.command.commit.grant", path, "commit requires a grant; repeated committed state is idempotent");
|
|
1458
1458
|
if (method === "cancelBreak")
|
|
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
|
-
into.require(approval === "starting-after-task" || approval === "
|
|
1462
|
-
into.require(state.forced === undefined, "break.command.end.forced", path, "an agent cannot end a forced break; an authorized lead
|
|
1461
|
+
into.require(approval === "starting-after-task" || approval === "on-break", "break.command.end.started", path, "end a committed break, including a returning provider still finishing work");
|
|
1462
|
+
into.require(state.forced === undefined, "break.command.end.forced", path, "an agent cannot end a forced break; an authorized lead ends it");
|
|
1463
1463
|
}
|
|
1464
1464
|
}
|
|
1465
1465
|
return into.violations;
|
|
@@ -1479,11 +1479,11 @@ export function validateBreakTransition(before, after, path = "break") {
|
|
|
1479
1479
|
return into.violations;
|
|
1480
1480
|
const from = before.approval;
|
|
1481
1481
|
const to = after.approval;
|
|
1482
|
-
const committed = to === "starting-after-task" || to === "
|
|
1482
|
+
const committed = to === "starting-after-task" || to === "on-break";
|
|
1483
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
|
-
const backwards = (from === "
|
|
1486
|
+
const backwards = (from === "on-break" && (to === "awaiting-decision" || to === "granted" || to === "starting-after-task")) ||
|
|
1487
1487
|
(from === "starting-after-task" && (to === "awaiting-decision" || to === "granted")) ||
|
|
1488
1488
|
(from === "granted" && to === "awaiting-decision");
|
|
1489
1489
|
if (backwards)
|
|
@@ -1497,18 +1497,21 @@ function validateBreakState(value, path, into) {
|
|
|
1497
1497
|
}
|
|
1498
1498
|
into.require(!Object.hasOwn(value, "imposed"), "break.forced.renamed", `${path}.imposed`, "imposed was renamed to forced; use only forced");
|
|
1499
1499
|
into.oneOf(value.approval, BREAK_APPROVALS, "break.approval", `${path}.approval`);
|
|
1500
|
-
into.require(
|
|
1501
|
-
|
|
1500
|
+
into.require(!Object.hasOwn(value, "mayAsk"), "break.canRequestBreak.renamed", `${path}.mayAsk`, "mayAsk was renamed to canRequestBreak; use only canRequestBreak");
|
|
1501
|
+
into.require(typeof value.canRequestBreak === "boolean", "break.canRequestBreak", `${path}.canRequestBreak`, "canRequestBreak says whether the agent may ask for a break: a boolean");
|
|
1502
|
+
into.require(!Object.hasOwn(value, "refusedReason"), "break.requestUnavailableReason.renamed", `${path}.refusedReason`, "refusedReason was renamed to requestUnavailableReason; use only requestUnavailableReason");
|
|
1503
|
+
for (const field of ["requestUnavailableReason", "decisionReason"]) {
|
|
1502
1504
|
if (value[field] !== undefined) {
|
|
1503
1505
|
into.filled(value[field], `break.${field}`, `${path}.${field}`, `${field} must not be empty when present`);
|
|
1504
1506
|
}
|
|
1505
1507
|
}
|
|
1506
|
-
// The refusal is the reason the control is withdrawn; beside `
|
|
1507
|
-
if (value.
|
|
1508
|
-
into.require(value.
|
|
1508
|
+
// The refusal is the reason the control is withdrawn; beside `canRequestBreak: true` it explains nothing.
|
|
1509
|
+
if (value.requestUnavailableReason !== undefined) {
|
|
1510
|
+
into.require(value.canRequestBreak !== true, "break.requestUnavailableReason.canRequestBreak", `${path}.requestUnavailableReason`, "requestUnavailableReason is shown when canRequestBreak is false; omit it while the agent may ask");
|
|
1509
1511
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
+
into.require(!Object.hasOwn(value, "retryAfterMs"), "break.retryRequestAfterMs.renamed", `${path}.retryAfterMs`, "retryAfterMs was renamed to retryRequestAfterMs on BreakState");
|
|
1513
|
+
if (value.retryRequestAfterMs !== undefined) {
|
|
1514
|
+
into.require(typeof value.retryRequestAfterMs === "number" && Number.isFinite(value.retryRequestAfterMs) && value.retryRequestAfterMs >= 0, "break.retryRequestAfterMs", `${path}.retryRequestAfterMs`, "retryRequestAfterMs must be a non-negative number when present");
|
|
1512
1515
|
}
|
|
1513
1516
|
if (value.activeReasonId !== undefined) {
|
|
1514
1517
|
into.filled(value.activeReasonId, "break.activeReasonId", `${path}.activeReasonId`, "activeReasonId must not be empty when present");
|
|
@@ -1519,9 +1522,9 @@ function validateBreakState(value, path, into) {
|
|
|
1519
1522
|
if (value.forced !== undefined) {
|
|
1520
1523
|
validateForcedBreak(value.forced, `${path}.forced`, into);
|
|
1521
1524
|
// A forced break is a break somebody forced; beside `not-requested` there is no break.
|
|
1522
|
-
// A forced break is a break in progress or about to be: it travels with
|
|
1525
|
+
// A forced break is a break in progress or about to be: it travels with on-break or
|
|
1523
1526
|
// starting-after-task and nothing else. Beside granted, the host would commit a break nobody asked for.
|
|
1524
|
-
into.require(value.approval === "
|
|
1527
|
+
into.require(value.approval === "on-break" || 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`);
|
|
1525
1528
|
}
|
|
1526
1529
|
if (value.reasons === undefined)
|
|
1527
1530
|
return;
|
|
@@ -1557,10 +1560,10 @@ function validateBreakState(value, path, into) {
|
|
|
1557
1560
|
into.require(seen.has(value.activeReasonId), "break.activeReasonId.known", `${path}.activeReasonId`, `activeReasonId names a reason the provider did not publish: ${value.activeReasonId}`);
|
|
1558
1561
|
}
|
|
1559
1562
|
// A break in effect, or about to be, on a provider that publishes reasons is on one of them: an
|
|
1560
|
-
// forced one included, since the lead's force command named it. A break of no kind is a break whose
|
|
1563
|
+
// forced one included, since the lead's force-break command named it. A break of no kind is a break whose
|
|
1561
1564
|
// rules -- who may listen through it, whether it counts -- nobody can apply.
|
|
1562
1565
|
ruleEvaluated("break.activeReasonId.required");
|
|
1563
|
-
if (seen.size > 0 && (value.approval === "
|
|
1566
|
+
if (seen.size > 0 && (value.approval === "on-break" || value.approval === "starting-after-task")) {
|
|
1564
1567
|
into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${describeValue(value.approval)} on a provider that publishes reasons names the one it is on`);
|
|
1565
1568
|
}
|
|
1566
1569
|
}
|
|
@@ -1652,7 +1655,7 @@ function validateTeamMembersInto(teamMembers, path, context, into) {
|
|
|
1652
1655
|
if (member.since !== undefined)
|
|
1653
1656
|
into.timestamp(member.since, "team.member.since", `${at}.since`);
|
|
1654
1657
|
if (member.break !== undefined) {
|
|
1655
|
-
// Only an outstanding request appears here: `not-requested` is absence, `
|
|
1658
|
+
// Only an outstanding request appears here: `not-requested` is absence, `on-break` is
|
|
1656
1659
|
// `availability: "on-break"`, and a denial never survives to be reported.
|
|
1657
1660
|
if (into.oneOf(member.break, MEMBER_BREAKS, "team.member.break", `${at}.break`)) {
|
|
1658
1661
|
into.require(member.availability !== "signed-out" && member.availability !== "on-break", "team.member.break.availability", `${at}.break`, "a member on a break or signed out has no request outstanding");
|
package/guide.md
CHANGED
|
@@ -53,7 +53,7 @@ transport, a task, a break and a call each have a word of their own:
|
|
|
53
53
|
| `media` | A task's audio | `started`, `ended` |
|
|
54
54
|
| `transport` | A connection | `connecting`, `active`, `error` |
|
|
55
55
|
| `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
|
|
56
|
-
| `approval` | A break request | `not-requested`, `awaiting-decision`, `granted`, `starting-after-task`, `
|
|
56
|
+
| `approval` | A break request | `not-requested`, `awaiting-decision`, `granted`, `starting-after-task`, `on-break` |
|
|
57
57
|
| `availability` | A team member | `ready`, `on-task`, `on-break`, `signed-out` |
|
|
58
58
|
|
|
59
59
|
## Versioning
|
|
@@ -934,7 +934,7 @@ type BreakApproval =
|
|
|
934
934
|
| "awaiting-decision"
|
|
935
935
|
| "granted"
|
|
936
936
|
| "starting-after-task"
|
|
937
|
-
| "
|
|
937
|
+
| "on-break";
|
|
938
938
|
|
|
939
939
|
type BreakReason = {
|
|
940
940
|
id: string;
|
|
@@ -955,10 +955,10 @@ type ForcedBreak =
|
|
|
955
955
|
|
|
956
956
|
type BreakState = {
|
|
957
957
|
approval: BreakApproval;
|
|
958
|
-
|
|
959
|
-
|
|
958
|
+
canRequestBreak: boolean;
|
|
959
|
+
requestUnavailableReason?: string;
|
|
960
960
|
decisionReason?: string;
|
|
961
|
-
|
|
961
|
+
retryRequestAfterMs?: number;
|
|
962
962
|
reasons?: BreakReason[];
|
|
963
963
|
activeReasonId?: string;
|
|
964
964
|
forced?: ForcedBreak;
|
|
@@ -1034,10 +1034,10 @@ type TeamLeadAssistCommand =
|
|
|
1034
1034
|
| { type: "decline"; requestId: string; reason?: string };
|
|
1035
1035
|
|
|
1036
1036
|
type TeamBreakCommand =
|
|
1037
|
-
| { type: "decide"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
|
|
1038
|
-
| { type: "policy"; policy: "
|
|
1039
|
-
| { type: "force"; memberId: UserId; reasonId?: string; reason?: string }
|
|
1040
|
-
| { type: "
|
|
1037
|
+
| { type: "decide-break-request"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
|
|
1038
|
+
| { type: "set-break-policy"; policy: "approval-required" | "automatically-approved" | "requests-suspended" }
|
|
1039
|
+
| { type: "force-break"; memberId: UserId; reasonId?: string; reason?: string }
|
|
1040
|
+
| { type: "end-forced-break"; memberId: UserId };
|
|
1041
1041
|
|
|
1042
1042
|
type TeamBreakCommandRequest = {
|
|
1043
1043
|
command: TeamBreakCommand;
|
|
@@ -1924,7 +1924,7 @@ them from what arrives later.
|
|
|
1924
1924
|
| --- | --- |
|
|
1925
1925
|
| `breaks` | This login may request a break. Requires the four break methods on the connection. |
|
|
1926
1926
|
| `team` | This login leads a team. The provider publishes a `TeamMembers` object to it on every snapshot — `members: []` when nobody is in it — and to nobody else. |
|
|
1927
|
-
| `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — force,
|
|
1927
|
+
| `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — force-break, end-forced-break, decide-break-request, set-break-policy — as far as the provider supports; a command it lacks answers `omni.capability-not-enabled`. Omni asks for a decision only against a member whose `break` is `awaiting-decision`, so a provider that grants on request is never asked to decide. Requires `executeTeamBreak`. |
|
|
1928
1928
|
| `team.leadAssistControl` | This lead may join a member's call on request. Requires `executeTeamLeadAssist`. |
|
|
1929
1929
|
| `team.listeningControl` | This lead may listen to a member's call unasked, in the listed modes and no others: `listen`, `coach`, `join-call`. The list always includes `listen`, since the other two begin from one. Requires `executeTeamListen`. See **Listening to a call**. |
|
|
1930
1930
|
| `team.policyControl` | This lead sets the team's policy per capability — on, off, or the person's — within what the queue allows. Requires `executeTeamPolicy`; the team member list carries `policies`. |
|
|
@@ -3608,12 +3608,12 @@ nothing while none are being accepted — so they are not published separately.
|
|
|
3608
3608
|
| Field | Contract |
|
|
3609
3609
|
| --- | --- |
|
|
3610
3610
|
| `approval` | Where the agent's current request stands. See the states below. |
|
|
3611
|
-
| `
|
|
3612
|
-
| `
|
|
3611
|
+
| `canRequestBreak` | Whether the agent may ask at all. Distinct from `approval`. |
|
|
3612
|
+
| `requestUnavailableReason` | Display-ready reason shown when `canRequestBreak` is false — a standing gate that applies to everyone. |
|
|
3613
3613
|
| `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
|
|
3614
|
-
| `
|
|
3614
|
+
| `retryRequestAfterMs` | Milliseconds until the agent may retry a break request, 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 `
|
|
3616
|
+
| `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break. Required on a break `on-break` 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
3617
|
| `forced` | Set when the break was forced 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
|
|
@@ -3624,8 +3624,8 @@ rendering one as the other tells an agent to wait for somebody who is never comi
|
|
|
3624
3624
|
| `not-requested` | No request outstanding. |
|
|
3625
3625
|
| `awaiting-decision` | A person has to decide. The agent is waiting on somebody. |
|
|
3626
3626
|
| `granted` | A person decided yes. 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. |
|
|
3627
|
-
| `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 `
|
|
3628
|
-
| `
|
|
3627
|
+
| `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`. |
|
|
3628
|
+
| `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 lead's listening task during a `coaching`, `administrative` or `training` break -- see **Listening to a call**. |
|
|
3629
3629
|
|
|
3630
3630
|
A denial is a decision, not a standing approval state. The provider transitions the request directly
|
|
3631
3631
|
to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
|
|
@@ -3636,19 +3636,56 @@ A provider reports `starting-after-task` only after Omni commits a `granted` req
|
|
|
3636
3636
|
work is still active. Omni does not send the request again, because asking again would not move
|
|
3637
3637
|
it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
|
|
3638
3638
|
|
|
3639
|
-
`
|
|
3639
|
+
`canRequestBreak: false` is what lets Omni withdraw the control rather than let an agent ask and be
|
|
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
|
+
Migration: the break-state field formerly named retryAfterMs is now `BreakState.retryRequestAfterMs`,
|
|
3646
|
+
with diagnostic `break.retryRequestAfterMs`. It remains an optional non-negative finite number
|
|
3647
|
+
of milliseconds; the former break field is rejected even alongside the new field.
|
|
3648
|
+
The separate authentication and general failure retryAfterMs fields are unchanged.
|
|
3649
|
+
|
|
3650
|
+
Migration: the former in-effect break approval value is now `on-break`. It means the
|
|
3651
|
+
agent is on the break, not merely granted permission. The old value is rejected without an
|
|
3652
|
+
alias. The task prerequisite, supported listening exceptions and ordering rules are unchanged;
|
|
3653
|
+
the task-conflict diagnostic is `break.on-break.tasks`. Team member availability already uses
|
|
3654
|
+
`on-break` and is unchanged.
|
|
3655
|
+
|
|
3656
|
+
Migration: the former refusedReason field is now `BreakState.requestUnavailableReason`.
|
|
3657
|
+
It explains why requests are unavailable when `canRequestBreak` is false; `decisionReason`
|
|
3658
|
+
continues to explain a particular approval or denial. The old field is rejected even alongside
|
|
3659
|
+
the new one, and related diagnostics use requestUnavailableReason.
|
|
3660
|
+
|
|
3661
|
+
Migration: the former mayAsk field is now `BreakState.canRequestBreak`. The old field is
|
|
3662
|
+
rejected even beside the new field. Validation diagnostics use canRequestBreak in place of
|
|
3663
|
+
mayAsk. Request eligibility remains distinct from approval; existing alwaysAvailable reason
|
|
3664
|
+
exceptions are unchanged. Hosts and providers must update together.
|
|
3665
|
+
|
|
3645
3666
|
Migration: ImposedBreak is now `ForcedBreak`, and the former imposed field is now
|
|
3646
3667
|
`BreakState.forced`. Diagnostic and harness coverage names use `break.forced`; the agent-end
|
|
3647
3668
|
diagnostic is `break.command.end.forced`. Update hosts and providers together. The old field
|
|
3648
3669
|
is rejected, including when both spellings are sent; no compatibility alias is provided.
|
|
3649
|
-
The
|
|
3650
|
-
|
|
3651
|
-
The
|
|
3670
|
+
The break policy value formerly named auto-approve is now `automatically-approved`.
|
|
3671
|
+
Requests are approved automatically; approval does not skip commitment or the prerequisites
|
|
3672
|
+
for starting a break. The former value is rejected without an alias.
|
|
3673
|
+
The break policy value formerly named suspended is now `requests-suspended`. The former
|
|
3674
|
+
value is rejected without an alias. It suspends new requests, not breaks already underway.
|
|
3675
|
+
The break policy value formerly named ask is now `approval-required`: requests require
|
|
3676
|
+
approval. The former value is rejected without an alias; automatically-approved and requests-suspended retain
|
|
3677
|
+
their existing behavior.
|
|
3678
|
+
The team break command formerly named policy is now `set-break-policy`. The former
|
|
3679
|
+
command is rejected without an alias; its policy field is unchanged. Hosts and providers must adopt the new name together.
|
|
3680
|
+
The team break command formerly named decide is now `decide-break-request`. The former
|
|
3681
|
+
spelling is rejected without an alias; the granted/denied decisions and pending-request
|
|
3682
|
+
prerequisite are unchanged.
|
|
3683
|
+
The team break command formerly named place is now `force-break`. Hosts and providers must
|
|
3684
|
+
adopt the new command together; place and the interim force spelling are rejected without aliases.
|
|
3685
|
+
The team break command formerly named release is now `end-forced-break`, and its diagnostic is
|
|
3686
|
+
`team.break.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
|
|
3687
|
+
**End a forced break** is a lead action through `executeTeamBreak`; the agent’s `endBreak`
|
|
3688
|
+
method still cannot end a forced break. Break ordering and permission rules are unchanged.
|
|
3652
3689
|
|
|
3653
3690
|
`ForcedBreak` says who forced the break, whether automatic ending is enabled, and, when enabled,
|
|
3654
3691
|
when the provider will end it. A break the agent did not choose is not manually resumable by them.
|
|
@@ -3661,10 +3698,10 @@ them, not the machinery that carried it out.
|
|
|
3661
3698
|
Omni resolves the name to show with `describeUsers()`, so a provider sends the identifier and never
|
|
3662
3699
|
a display name.
|
|
3663
3700
|
|
|
3664
|
-
**A forced break travels with `
|
|
3701
|
+
**A forced break travels with `on-break` or `starting-after-task`, and nothing else.** It is a
|
|
3665
3702
|
break in progress or about to be; beside `granted` or `awaiting-decision` the host would read a
|
|
3666
3703
|
request the agent never made and commit it (`break.forced.approval`). Where the provider publishes
|
|
3667
|
-
`reasons`, the lead's `force` named one, and the member's state carries it as `activeReasonId`.
|
|
3704
|
+
`reasons`, the lead's `force-break` named one, and the member's state carries it as `activeReasonId`.
|
|
3668
3705
|
|
|
3669
3706
|
For example:
|
|
3670
3707
|
|
|
@@ -3765,15 +3802,15 @@ request object only to the request method, and undefined to the other three.
|
|
|
3765
3802
|
|
|
3766
3803
|
| Method | Required current state |
|
|
3767
3804
|
| --- | --- |
|
|
3768
|
-
| `requestBreak` | `not-requested`; selected current reason code when codes exist; `
|
|
3769
|
-
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `
|
|
3805
|
+
| `requestBreak` | `not-requested`; selected current reason code when codes exist; `canRequestBreak` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
|
|
3806
|
+
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
|
|
3770
3807
|
| `cancelBreak` | `awaiting-decision` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
|
|
3771
|
-
| `endBreak` | `
|
|
3808
|
+
| `endBreak` | `on-break` or `starting-after-task` during reconciliation; an agent cannot end a forced break. |
|
|
3772
3809
|
|
|
3773
|
-
Use `validateTeamBreakCommand(request, context)` for lead decisions, forcing a break,
|
|
3810
|
+
Use `validateTeamBreakCommand(request, context)` for lead decisions, forcing a break, ending a forced break and
|
|
3774
3811
|
policy commands. It requires the live lead capability and active transport, a current target
|
|
3775
|
-
team member list for member commands, and the target's complete break state for forcing/
|
|
3776
|
-
Approve/deny requires an awaiting decision; forcing a break uses the target's reason codes;
|
|
3812
|
+
team member list for member commands, and the target's complete break state for forcing/ending a break.
|
|
3813
|
+
Approve/deny requires an awaiting decision; forcing a break uses the target's reason codes; ending a forced break
|
|
3777
3814
|
requires a forced committed break. The context's target state must belong to the named member;
|
|
3778
3815
|
that association and backend authorization are provider responsibilities.
|
|
3779
3816
|
|
|
@@ -3927,11 +3964,11 @@ Omni coordinates one break attempt as follows:
|
|
|
3927
3964
|
causes Omni to take the cancel path.
|
|
3928
3965
|
4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
|
|
3929
3966
|
and send `commitBreak()` to each of them. A provider then stops offering new work
|
|
3930
|
-
and reports `starting-after-task` or `
|
|
3967
|
+
and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
|
|
3931
3968
|
decision, tunable per deployment — decides who is kept: a provider that has not applied the
|
|
3932
3969
|
commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
|
|
3933
|
-
begins without it. `
|
|
3934
|
-
reports `
|
|
3970
|
+
begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
|
|
3971
|
+
reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
|
|
3935
3972
|
and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
|
|
3936
3973
|
settled and beginning when the task ends, and offers no cancel, because the commit is durable.
|
|
3937
3974
|
5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
|
|
@@ -3964,7 +4001,7 @@ Setting a provider aside is not a rollback and not a cancel. The commit stands a
|
|
|
3964
4001
|
obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
|
|
3965
4002
|
snapshot before anything else, and the snapshot decides:
|
|
3966
4003
|
|
|
3967
|
-
- `
|
|
4004
|
+
- `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
|
|
3968
4005
|
- still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
|
|
3969
4006
|
late behind it, the provider stops an agent who is already stopped; nothing happens, because a
|
|
3970
4007
|
commit is a state to be in, not an act to be done.
|
|
@@ -3977,7 +4014,7 @@ no capacity until it is reconciled.
|
|
|
3977
4014
|
|
|
3978
4015
|
**If the agent has already ended the break elsewhere, the break attempt is over** and the returning
|
|
3979
4016
|
provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
|
|
3980
|
-
would stop an agent who is working again; `starting-after-task` or `
|
|
4017
|
+
would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
|
|
3981
4018
|
Never rolling back is about a break that is still on, not one the agent has finished.
|
|
3982
4019
|
|
|
3983
4020
|
Omni may tell the agent which platforms the break has not yet reached, as it already does when a
|
|
@@ -4020,7 +4057,7 @@ reported through `break-state`.
|
|
|
4020
4057
|
|
|
4021
4058
|
Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
|
|
4022
4059
|
business reason. On commit the provider stops offering new work and reports
|
|
4023
|
-
`starting-after-task` while existing work finishes, or `
|
|
4060
|
+
`starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
|
|
4024
4061
|
Committing a break that is already in effect changes nothing and answers `committed`.
|
|
4025
4062
|
|
|
4026
4063
|
### `endBreak()`
|
|
@@ -4100,18 +4137,18 @@ One method, `executeTeamBreak`, taking a discriminated command exactly as `execu
|
|
|
4100
4137
|
|
|
4101
4138
|
| Command | Effect |
|
|
4102
4139
|
| --- | --- |
|
|
4103
|
-
| `{ 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`. |
|
|
4104
|
-
| `{ type: "policy", policy }` | `
|
|
4105
|
-
| `{ type: "force", 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. |
|
|
4106
|
-
| `{ type: "
|
|
4140
|
+
| `{ type: "decide-break-request", 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`. |
|
|
4141
|
+
| `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
|
|
4142
|
+
| `{ type: "force-break", 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. |
|
|
4143
|
+
| `{ type: "end-forced-break", memberId: UserId }` | Ends a forced break on that member, whoever forced it. |
|
|
4107
4144
|
|
|
4108
4145
|
`memberId` is this provider's own identifier for the member, as published on its team member list. It is
|
|
4109
4146
|
never an identifier from another provider, and Omni does not translate between them; names come
|
|
4110
4147
|
from `describeUsers()`.
|
|
4111
4148
|
|
|
4112
|
-
`suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
4113
|
-
approve them. A provider that suspends
|
|
4114
|
-
agents so they see it before asking. A `force` must likewise reach that member as a `forced` break
|
|
4149
|
+
`requests-suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
4150
|
+
approve them. A provider that suspends break requests must also publish `canRequestBreak: false` to the team's
|
|
4151
|
+
agents so they see it before asking. A `force-break` must likewise reach that member as a `forced` break
|
|
4115
4152
|
on their own `BreakState`, or they are stopped from working with no way to see why.
|
|
4116
4153
|
|
|
4117
4154
|
What happens when no lead is online — auto-approving, for instance — is the provider's decision and is
|
|
@@ -4158,7 +4195,7 @@ or until it is withdrawn or declined.
|
|
|
4158
4195
|
|
|
4159
4196
|
**A lead on a break does not join.** A break is a reported state in which the agent is not working,
|
|
4160
4197
|
and a join is work. Omni offers Join to a lead only while their own `BreakState.approval` is neither
|
|
4161
|
-
`starting-after-task` nor `
|
|
4198
|
+
`starting-after-task` nor `on-break` -- a committed break waiting for the lead's current work to
|
|
4162
4199
|
finish is not given more -- and a provider answers a `join` from a lead on such a break `failed`.
|
|
4163
4200
|
The request stands for another lead, as it does when this one is already on a call.
|
|
4164
4201
|
|
|
@@ -4313,7 +4350,7 @@ about whether anybody has to act on it, and the difference is a lead's entire ac
|
|
|
4313
4350
|
| `starting-after-task` | Already granted; it begins when their current task ends. Nobody needs to act. |
|
|
4314
4351
|
|
|
4315
4352
|
Those three are the only values that appear here. `not-requested` is absence — omit `break`
|
|
4316
|
-
instead. `
|
|
4353
|
+
instead. `on-break` is `availability: "on-break"`, and a denial transitions to `not-requested`,
|
|
4317
4354
|
so neither survives to be reported. It is otherwise the same `BreakApproval` the member's own
|
|
4318
4355
|
break state uses, rather than a parallel vocabulary for the lead's view, so the two cannot drift
|
|
4319
4356
|
apart.
|
|
@@ -4945,14 +4982,14 @@ covers a `diagnostic`: shown where the agent works, and counted.
|
|
|
4945
4982
|
|
|
4946
4983
|
Replaces this provider's complete `break` object. Its `approval` uses the canonical
|
|
4947
4984
|
`not-requested`, `awaiting-decision`, `granted`, `starting-after-task` and
|
|
4948
|
-
`
|
|
4985
|
+
`on-break` states defined under Breaks; the event also carries the corresponding may-ask state,
|
|
4949
4986
|
reasons, retry details, and any forced break.
|
|
4950
4987
|
|
|
4951
4988
|
Each state is also held to the one before it. A commit's states, `starting-after-task` and
|
|
4952
|
-
`
|
|
4989
|
+
`on-break`, follow a grant — the one arrival in a committed state nobody asked for is a forced
|
|
4953
4990
|
break, which says so with `forced`: in effect at once, or `starting-after-task` while the member
|
|
4954
4991
|
finishes the call they are on (`stream.breakState.commitBeforeGrant`); and a break never moves backwards —
|
|
4955
|
-
from `
|
|
4992
|
+
from `on-break` or `starting-after-task` to a grant or a request, or from `granted` to
|
|
4956
4993
|
`awaiting-decision` — a new request passes through `not-requested` (`stream.breakState.backwards`).
|
|
4957
4994
|
`exerciseAdapter` holds the stream to that from the connect snapshot on;
|
|
4958
4995
|
`assertBreakFollowsItsRequests` holds any sequence.
|
|
@@ -4980,9 +5017,9 @@ unresolved operation per provider/attempt; recovery must resolve uncertainty bef
|
|
|
4980
5017
|
operation is dispatched. The source must fence delayed operations against its own attempt state.
|
|
4981
5018
|
|
|
4982
5019
|
Normal order is `not-requested` → `awaiting-decision` → `granted` →
|
|
4983
|
-
`starting-after-task` → `
|
|
4984
|
-
`granted`; a commit with no outstanding work may go directly to `
|
|
4985
|
-
returns a precommit request to `not-requested`; an authorized end
|
|
5020
|
+
`starting-after-task` → `on-break` → `not-requested`. Auto-approval may go directly to
|
|
5021
|
+
`granted`; a commit with no outstanding work may go directly to `on-break`. Denial/cancel
|
|
5022
|
+
returns a precommit request to `not-requested`; an authorized agent or lead end returns a committed
|
|
4986
5023
|
break there. Same-state restatements are allowed. An evidenced forced break is the explicit
|
|
4987
5024
|
exception to requesting/granting, and must carry its forced actor/state. Neither skipped
|
|
4988
5025
|
publication nor a host-local guess creates another exception. A later normal attempt starts
|
|
@@ -5002,7 +5039,7 @@ not implement or assume invocation of Protocol's two-phase request/commit coordi
|
|
|
5002
5039
|
For a multi-provider break attempt, "every provider" is the set of providers frozen when the
|
|
5003
5040
|
attempt entered `requesting-break`. Omni commits only after every asked provider reports `granted` —
|
|
5004
5041
|
that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
|
|
5005
|
-
`on-break` once every kept provider reports `
|
|
5042
|
+
`on-break` once every kept provider reports `on-break`; the commit bound decides who is kept,
|
|
5006
5043
|
setting aside a provider that has not applied the commit rather than holding a break that has
|
|
5007
5044
|
already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
|
|
5008
5045
|
multi-provider break**.
|
|
@@ -5385,7 +5422,7 @@ cannot be established from TypeScript structure alone.
|
|
|
5385
5422
|
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a forced break arriving in effect with `forced`. The harness applies the same rules after the connect snapshot. |
|
|
5386
5423
|
| `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. |
|
|
5387
5424
|
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
5388
|
-
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `
|
|
5425
|
+
| `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. |
|
|
5389
5426
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
5390
5427
|
| `assertWrapTimeout(task, mediaEndedAt, deadline, toleranceMs?)` | The wrap deadline equals media 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. |
|
|
5391
5428
|
| `assertBrowserSessionIsolation(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
|