@xema/omni-protocol 0.1.94 → 0.1.95
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 +13 -19
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +6 -5
- package/dist/validation.js +34 -38
- package/guide.md +94 -55
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1217,9 +1217,9 @@ export type DialResult = {
|
|
|
1217
1217
|
status: "failed";
|
|
1218
1218
|
failure: ProtocolFailure;
|
|
1219
1219
|
};
|
|
1220
|
-
export type
|
|
1220
|
+
export type BreakStatus = "not-requested"
|
|
1221
1221
|
/** Somebody has to decide. The agent is waiting on a person. */
|
|
1222
|
-
| "awaiting-
|
|
1222
|
+
| "awaiting-approval"
|
|
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. */
|
|
@@ -1249,28 +1249,19 @@ export interface BreakReason {
|
|
|
1249
1249
|
alwaysAvailable?: true;
|
|
1250
1250
|
}
|
|
1251
1251
|
export interface BreakRequest {
|
|
1252
|
+
/** Optional explanatory text. Omit when no explanation is needed; separate from reasonId. */
|
|
1252
1253
|
reason?: string;
|
|
1253
1254
|
/** The chosen `BreakReason.id`, where the provider publishes codes. */
|
|
1254
1255
|
reasonId?: string;
|
|
1255
1256
|
}
|
|
1256
|
-
/**
|
|
1257
|
-
* A break forced on the agent rather than requested by them.
|
|
1258
|
-
*
|
|
1259
|
-
* `by` is required in both arms. Who put somebody off the floor survives whether or not the
|
|
1260
|
-
* break ends on a clock -- a forced break with no origin is a state the agent cannot reason
|
|
1261
|
-
* about, and one that ends on a condition is still somebody's decision.
|
|
1262
|
-
*/
|
|
1257
|
+
/** A break forced on the agent; the agent must explicitly resume when ready. */
|
|
1263
1258
|
export type ForcedBreak = {
|
|
1264
1259
|
by: UserId;
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
} | {
|
|
1268
|
-
by: UserId;
|
|
1269
|
-
endsAutomatically: false;
|
|
1270
|
-
endsAt?: never;
|
|
1260
|
+
/** Optional expected time on break, in milliseconds from actual start. Advisory only. */
|
|
1261
|
+
expectedDurationMs?: number;
|
|
1271
1262
|
};
|
|
1272
1263
|
export interface BreakState {
|
|
1273
|
-
|
|
1264
|
+
status: BreakStatus;
|
|
1274
1265
|
/** Whether the agent may ask at all. Distinct from the fate of a request already made. */
|
|
1275
1266
|
canRequestBreak: boolean;
|
|
1276
1267
|
/** Shown when `canRequestBreak` is false, such as "Busy hours". */
|
|
@@ -1321,7 +1312,7 @@ export interface TeamMember {
|
|
|
1321
1312
|
/** Omitted rather than invented: Omni renders it as a duration. */
|
|
1322
1313
|
since?: IsoTimestamp;
|
|
1323
1314
|
/** A request in flight or a grant not yet in effect. `not-requested` is absence, and `on-break` is `availability: "on-break"`. */
|
|
1324
|
-
break?: Extract<
|
|
1315
|
+
break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
|
|
1325
1316
|
}
|
|
1326
1317
|
/** A member asking this lead to join their call. */
|
|
1327
1318
|
export interface LeadRequest {
|
|
@@ -1398,13 +1389,16 @@ export type TeamBreakCommand = {
|
|
|
1398
1389
|
type: "set-break-policy";
|
|
1399
1390
|
policy: "approval-required" | "automatically-approved" | "requests-suspended";
|
|
1400
1391
|
}
|
|
1401
|
-
/** `reasonId`
|
|
1392
|
+
/** `reason` text is optional. `reasonId` selects a published `BreakReason.id`, required when reasons are published; the forced break carries it as `activeReasonId`. */
|
|
1402
1393
|
| {
|
|
1403
1394
|
type: "force-break";
|
|
1404
1395
|
memberId: UserId;
|
|
1405
1396
|
reasonId?: string;
|
|
1406
1397
|
reason?: string;
|
|
1407
|
-
|
|
1398
|
+
expectedDurationMs?: number;
|
|
1399
|
+
}
|
|
1400
|
+
/** Clears forced metadata only; preserves the committed break until the agent explicitly resumes. */
|
|
1401
|
+
| {
|
|
1408
1402
|
type: "end-forced-break";
|
|
1409
1403
|
memberId: UserId;
|
|
1410
1404
|
};
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type TaskCompletion, type
|
|
1
|
+
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type TaskCompletion, type BreakStatus, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostMute, type LoginStore, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
|
|
2
2
|
import { type ProtocolViolation } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
|
|
4
4
|
/**
|
|
@@ -135,11 +135,11 @@ export declare function assertReconnectWithMissedAssignments<C extends Channel>(
|
|
|
135
135
|
* pending request nobody is coming to decide is worse than none. So the scenario is a request
|
|
136
136
|
* that goes back to not-requested, and a later one that is granted.
|
|
137
137
|
*
|
|
138
|
-
* A request shows as `awaiting-
|
|
138
|
+
* A request shows as `awaiting-approval` where a person decides, and as `granted` at once where
|
|
139
139
|
* the provider decides alone; either is the request being made, and the rule is the same for
|
|
140
140
|
* both.
|
|
141
141
|
*/
|
|
142
|
-
export declare function assertDeniedAndRetriedBreak(approvals: readonly
|
|
142
|
+
export declare function assertDeniedAndRetriedBreak(approvals: readonly BreakStatus[]): void;
|
|
143
143
|
/**
|
|
144
144
|
* The event kinds a snapshot restates, and so supersedes when delivered while it is read. Every
|
|
145
145
|
* other kind reports a transaction no snapshot carries, and a host applies it after the snapshot.
|
|
@@ -242,7 +242,7 @@ export interface BreakCandidate {
|
|
|
242
242
|
export declare function assertBreakAttemptProviders(candidates: readonly BreakCandidate[], asked: readonly string[]): void;
|
|
243
243
|
/** One published moment of a break asked for on a task: the approval, and how many tasks were outstanding. */
|
|
244
244
|
export interface BreakOnTaskStep {
|
|
245
|
-
approval:
|
|
245
|
+
approval: BreakStatus;
|
|
246
246
|
outstanding: number;
|
|
247
247
|
}
|
|
248
248
|
/**
|
package/dist/testing.js
CHANGED
|
@@ -938,12 +938,12 @@ export function assertReconnectWithMissedAssignments(before, reconnect, missedTa
|
|
|
938
938
|
* pending request nobody is coming to decide is worse than none. So the scenario is a request
|
|
939
939
|
* that goes back to not-requested, and a later one that is granted.
|
|
940
940
|
*
|
|
941
|
-
* A request shows as `awaiting-
|
|
941
|
+
* A request shows as `awaiting-approval` where a person decides, and as `granted` at once where
|
|
942
942
|
* the provider decides alone; either is the request being made, and the rule is the same for
|
|
943
943
|
* both.
|
|
944
944
|
*/
|
|
945
945
|
export function assertDeniedAndRetriedBreak(approvals) {
|
|
946
|
-
const asked = approvals.findIndex(approval => approval === "awaiting-
|
|
946
|
+
const asked = approvals.findIndex(approval => approval === "awaiting-approval" || approval === "granted");
|
|
947
947
|
if (asked < 0)
|
|
948
948
|
throw new Error("Break retry scenario requires an initial request");
|
|
949
949
|
const refused = approvals.indexOf("not-requested", asked + 1);
|
|
@@ -1865,9 +1865,10 @@ async function driveOneCall(drive) {
|
|
|
1865
1865
|
nothingLate();
|
|
1866
1866
|
return found;
|
|
1867
1867
|
}
|
|
1868
|
-
// A request goes not-requested -> awaiting-
|
|
1868
|
+
// A request goes not-requested -> awaiting-approval | granted; a commit goes granted ->
|
|
1869
1869
|
// starting-after-task | on-break; work ending goes starting-after-task -> on-break; a denial,
|
|
1870
|
-
// a cancel
|
|
1870
|
+
// a cancel or an explicit agent end goes back to not-requested; a lead lifting forced status
|
|
1871
|
+
// preserves the committed break. A forced break arrives on-break
|
|
1871
1872
|
// with `forced`. Nothing else is a move the guide describes.
|
|
1872
1873
|
/** What a stream has said about the agent's break, and the moves it may not make. */
|
|
1873
1874
|
export class BreakStream {
|
|
@@ -1994,7 +1995,7 @@ export function assertBreakAttemptProviders(candidates, asked) {
|
|
|
1994
1995
|
* outstanding -- never beside a task, and never later than the step that has none.
|
|
1995
1996
|
*/
|
|
1996
1997
|
export function assertBreakBeginsAfterTask(steps) {
|
|
1997
|
-
const asked = steps.findIndex(step => step.approval === "awaiting-
|
|
1998
|
+
const asked = steps.findIndex(step => step.approval === "awaiting-approval" || step.approval === "granted");
|
|
1998
1999
|
if (asked < 0)
|
|
1999
2000
|
throw new Error("Break-on-task scenario requires a request");
|
|
2000
2001
|
if ((steps[asked]?.outstanding ?? 0) < 1)
|
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-
|
|
55
|
+
"not-requested": true, "awaiting-approval": 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,
|
|
@@ -80,7 +80,7 @@ const SNAPSHOT_REASONS = membersOf({
|
|
|
80
80
|
});
|
|
81
81
|
const SESSION_CAPABILITIES = membersOf({ breaks: true, team: true, preferences: true });
|
|
82
82
|
const MEMBER_BREAKS = membersOf({
|
|
83
|
-
"awaiting-
|
|
83
|
+
"awaiting-approval": true, granted: true, "starting-after-task": true,
|
|
84
84
|
});
|
|
85
85
|
const OFFERABLE_PHASES = membersOf({
|
|
86
86
|
pending: true,
|
|
@@ -1298,19 +1298,12 @@ function validateForcedBreak(value, path, into) {
|
|
|
1298
1298
|
into.add("break.forced.shape", path, "a forced break must be an object");
|
|
1299
1299
|
return;
|
|
1300
1300
|
}
|
|
1301
|
-
// `by` is required either way. Who put somebody off the floor survives whether or not the
|
|
1302
|
-
// break ends on a clock -- a forced break with no origin is a state the agent cannot
|
|
1303
|
-
// reason about.
|
|
1304
1301
|
into.require(isUserId(value.by), "break.forced.by", `${path}.by`, "a forced break must say who forced it");
|
|
1305
|
-
|
|
1306
|
-
into.
|
|
1307
|
-
}
|
|
1308
|
-
else if (value.endsAutomatically === false) {
|
|
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
|
-
}
|
|
1311
|
-
else {
|
|
1312
|
-
into.add("break.forced.endsAutomatically", `${path}.endsAutomatically`, "a forced break must say whether it ends automatically");
|
|
1302
|
+
for (const field of ["endsAutomatically", "endsAt"]) {
|
|
1303
|
+
into.require(!Object.hasOwn(value, field), "break.forced.manualResume", `${path}.${field}`, "automatic ending fields are unsupported; the agent must resume manually");
|
|
1313
1304
|
}
|
|
1305
|
+
if (value.expectedDurationMs !== undefined)
|
|
1306
|
+
into.require(typeof value.expectedDurationMs === "number" && Number.isFinite(value.expectedDurationMs) && value.expectedDurationMs > 0, "break.forced.expectedDurationMs", `${path}.expectedDurationMs`, "expectedDurationMs must be a positive finite number of milliseconds");
|
|
1314
1307
|
}
|
|
1315
1308
|
/** Checks break status against the full currently retained task set, after a transaction. */
|
|
1316
1309
|
export function validateBreakStatus(state, tasks, path = "snapshot") {
|
|
@@ -1329,11 +1322,11 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
|
|
|
1329
1322
|
// no task is a break waiting on work that does not exist, and an empty list reading as "still
|
|
1330
1323
|
// finishing" is the plausible nought.
|
|
1331
1324
|
ruleEvaluated("break.starting-after-task.tasks");
|
|
1332
|
-
if (isPlainObject(state) && state.
|
|
1325
|
+
if (isPlainObject(state) && state.status === "starting-after-task"
|
|
1333
1326
|
&& Array.isArray(tasks) && tasks.length === 0) {
|
|
1334
|
-
into.add("break.starting-after-task.tasks", `${path}.break.
|
|
1327
|
+
into.add("break.starting-after-task.tasks", `${path}.break.status`, "a break starting after the task waits on a task, and the snapshot carries none: with nothing outstanding the break is on-break");
|
|
1335
1328
|
}
|
|
1336
|
-
if (isPlainObject(state) && state.
|
|
1329
|
+
if (isPlainObject(state) && state.status === "on-break"
|
|
1337
1330
|
&& Array.isArray(tasks) && tasks.length > 0) {
|
|
1338
1331
|
const listening = tasks.every((task) => isPlainObject(task) && task.listening !== undefined);
|
|
1339
1332
|
if (!listening) {
|
|
@@ -1367,7 +1360,7 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1367
1360
|
const command = request.command;
|
|
1368
1361
|
const allowed = {
|
|
1369
1362
|
"decide-break-request": ["type", "memberId", "decision", "reason"], "set-break-policy": ["type", "policy"],
|
|
1370
|
-
"force-break": ["type", "memberId", "reasonId", "reason"], "end-forced-break": ["type", "memberId"],
|
|
1363
|
+
"force-break": ["type", "memberId", "reasonId", "reason", "expectedDurationMs"], "end-forced-break": ["type", "memberId"],
|
|
1371
1364
|
};
|
|
1372
1365
|
const fields = typeof command.type === "string" && Object.hasOwn(allowed, command.type) ? allowed[command.type] : undefined;
|
|
1373
1366
|
if (!fields) {
|
|
@@ -1392,14 +1385,16 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1392
1385
|
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
1386
|
if (command.type === "decide-break-request") {
|
|
1394
1387
|
into.require(command.decision === "granted" || command.decision === "denied", "team.break.command.decision", path, "decide granted or denied");
|
|
1395
|
-
into.require(isPlainObject(member) && member.break === "awaiting-
|
|
1388
|
+
into.require(isPlainObject(member) && member.break === "awaiting-approval", "team.break.command.awaiting", path, "decide only a currently awaiting-approval request");
|
|
1396
1389
|
}
|
|
1397
1390
|
else {
|
|
1398
1391
|
validateBreakState(context.memberBreak, `${path}.memberBreak`, into);
|
|
1399
1392
|
const state = isPlainObject(context.memberBreak) ? context.memberBreak : {};
|
|
1400
1393
|
if (command.type === "end-forced-break")
|
|
1401
|
-
into.require(state.forced !== undefined && (state.
|
|
1394
|
+
into.require(state.forced !== undefined && (state.status === "on-break" || state.status === "starting-after-task"), "team.break.command.endForcedBreak", path, "lift a current forced-break restriction without resuming the agent");
|
|
1402
1395
|
if (command.type === "force-break") {
|
|
1396
|
+
if (command.expectedDurationMs !== undefined)
|
|
1397
|
+
into.require(typeof command.expectedDurationMs === "number" && Number.isFinite(command.expectedDurationMs) && command.expectedDurationMs > 0, "team.break.command.expectedDurationMs", `${path}.expectedDurationMs`, "expectedDurationMs must be positive finite milliseconds; it never resumes the agent");
|
|
1403
1398
|
if (command.reasonId !== undefined)
|
|
1404
1399
|
into.filled(command.reasonId, "team.break.command.reasonId", path, "reasonId must not be empty");
|
|
1405
1400
|
const reasons = Array.isArray(state.reasons) ? state.reasons : [];
|
|
@@ -1431,9 +1426,9 @@ export function validateBreakCommand(method, request, state, context, path = "br
|
|
|
1431
1426
|
}
|
|
1432
1427
|
if (!isPlainObject(state) || into.violations.length)
|
|
1433
1428
|
return into.violations;
|
|
1434
|
-
const
|
|
1429
|
+
const status = state.status;
|
|
1435
1430
|
if (method === "requestBreak") {
|
|
1436
|
-
into.require(
|
|
1431
|
+
into.require(status === "not-requested", "break.command.request.pending", path, "a new request starts only from not-requested");
|
|
1437
1432
|
if (!isPlainObject(request)) {
|
|
1438
1433
|
into.add("break.request.shape", path, "requestBreak takes a request object");
|
|
1439
1434
|
return into.violations;
|
|
@@ -1454,12 +1449,12 @@ export function validateBreakCommand(method, request, state, context, path = "br
|
|
|
1454
1449
|
else {
|
|
1455
1450
|
into.require(request === undefined, "break.command.arguments", path, "this break method takes no arguments");
|
|
1456
1451
|
if (method === "commitBreak")
|
|
1457
|
-
into.require(
|
|
1452
|
+
into.require(status === "granted" || status === "starting-after-task" || status === "on-break", "break.command.commit.grant", path, "commit requires a grant; repeated committed state is idempotent");
|
|
1458
1453
|
if (method === "cancelBreak")
|
|
1459
|
-
into.require(
|
|
1454
|
+
into.require(status === "awaiting-approval" || status === "granted", "break.command.cancel.precommit", path, "cancel only a pre-commit request; a raced commit requires commit recovery");
|
|
1460
1455
|
if (method === "endBreak") {
|
|
1461
|
-
into.require(
|
|
1462
|
-
|
|
1456
|
+
into.require(status === "starting-after-task" || status === "on-break", "break.command.end.started", path, "end a committed break, including a returning provider still finishing work");
|
|
1457
|
+
// A forced break also ends only when the agent explicitly resumes.
|
|
1463
1458
|
}
|
|
1464
1459
|
}
|
|
1465
1460
|
return into.violations;
|
|
@@ -1477,17 +1472,17 @@ export function validateBreakTransition(before, after, path = "break") {
|
|
|
1477
1472
|
validateBreakState(after, `${path}.after`, into);
|
|
1478
1473
|
if (into.violations.length || !isPlainObject(before) || !isPlainObject(after))
|
|
1479
1474
|
return into.violations;
|
|
1480
|
-
const from = before.
|
|
1481
|
-
const to = after.
|
|
1475
|
+
const from = before.status;
|
|
1476
|
+
const to = after.status;
|
|
1482
1477
|
const committed = to === "starting-after-task" || to === "on-break";
|
|
1483
|
-
if (committed && (from === "not-requested" || from === "awaiting-
|
|
1484
|
-
into.add("stream.breakState.commitBeforeGrant", `${path}.after.
|
|
1478
|
+
if (committed && (from === "not-requested" || from === "awaiting-approval") && after.forced === undefined) {
|
|
1479
|
+
into.add("stream.breakState.commitBeforeGrant", `${path}.after.status`, `${to} follows a commit, and a commit follows granted; the break stood at ${from}`);
|
|
1485
1480
|
}
|
|
1486
|
-
const backwards = (from === "on-break" && (to === "awaiting-
|
|
1487
|
-
(from === "starting-after-task" && (to === "awaiting-
|
|
1488
|
-
(from === "granted" && to === "awaiting-
|
|
1481
|
+
const backwards = (from === "on-break" && (to === "awaiting-approval" || to === "granted" || to === "starting-after-task")) ||
|
|
1482
|
+
(from === "starting-after-task" && (to === "awaiting-approval" || to === "granted")) ||
|
|
1483
|
+
(from === "granted" && to === "awaiting-approval");
|
|
1489
1484
|
if (backwards)
|
|
1490
|
-
into.add("stream.breakState.backwards", `${path}.after.
|
|
1485
|
+
into.add("stream.breakState.backwards", `${path}.after.status`, `a break does not go from ${from} back to ${to}; a new request passes through not-requested`);
|
|
1491
1486
|
return into.violations;
|
|
1492
1487
|
}
|
|
1493
1488
|
function validateBreakState(value, path, into) {
|
|
@@ -1496,7 +1491,8 @@ function validateBreakState(value, path, into) {
|
|
|
1496
1491
|
return;
|
|
1497
1492
|
}
|
|
1498
1493
|
into.require(!Object.hasOwn(value, "imposed"), "break.forced.renamed", `${path}.imposed`, "imposed was renamed to forced; use only forced");
|
|
1499
|
-
into.
|
|
1494
|
+
into.require(!Object.hasOwn(value, "approval"), "break.status.renamed", `${path}.approval`, "BreakState.approval was renamed to status; use only status");
|
|
1495
|
+
into.oneOf(value.status, BREAK_APPROVALS, "break.status", `${path}.status`);
|
|
1500
1496
|
into.require(!Object.hasOwn(value, "mayAsk"), "break.canRequestBreak.renamed", `${path}.mayAsk`, "mayAsk was renamed to canRequestBreak; use only canRequestBreak");
|
|
1501
1497
|
into.require(typeof value.canRequestBreak === "boolean", "break.canRequestBreak", `${path}.canRequestBreak`, "canRequestBreak says whether the agent may ask for a break: a boolean");
|
|
1502
1498
|
into.require(!Object.hasOwn(value, "refusedReason"), "break.requestUnavailableReason.renamed", `${path}.refusedReason`, "refusedReason was renamed to requestUnavailableReason; use only requestUnavailableReason");
|
|
@@ -1517,14 +1513,14 @@ function validateBreakState(value, path, into) {
|
|
|
1517
1513
|
into.filled(value.activeReasonId, "break.activeReasonId", `${path}.activeReasonId`, "activeReasonId must not be empty when present");
|
|
1518
1514
|
// A break nobody is on has no reason. Reporting one beside `not-requested` describes a
|
|
1519
1515
|
// break that is not happening.
|
|
1520
|
-
into.require(value.
|
|
1516
|
+
into.require(value.status !== "not-requested", "break.activeReasonId.status", `${path}.activeReasonId`, "activeReasonId must be omitted when no break is requested or in effect");
|
|
1521
1517
|
}
|
|
1522
1518
|
if (value.forced !== undefined) {
|
|
1523
1519
|
validateForcedBreak(value.forced, `${path}.forced`, into);
|
|
1524
1520
|
// A forced break is a break somebody forced; beside `not-requested` there is no break.
|
|
1525
1521
|
// A forced break is a break in progress or about to be: it travels with on-break or
|
|
1526
1522
|
// starting-after-task and nothing else. Beside granted, the host would commit a break nobody asked for.
|
|
1527
|
-
into.require(value.
|
|
1523
|
+
into.require(value.status === "on-break" || value.status === "starting-after-task", "break.forced.status", `${path}.forced`, `a forced break is in effect or starting after the task; ${describeValue(value.status)} says the agent asked, or that there is none`);
|
|
1528
1524
|
}
|
|
1529
1525
|
if (value.reasons === undefined)
|
|
1530
1526
|
return;
|
|
@@ -1563,8 +1559,8 @@ function validateBreakState(value, path, into) {
|
|
|
1563
1559
|
// forced one included, since the lead's force-break command named it. A break of no kind is a break whose
|
|
1564
1560
|
// rules -- who may listen through it, whether it counts -- nobody can apply.
|
|
1565
1561
|
ruleEvaluated("break.activeReasonId.required");
|
|
1566
|
-
if (seen.size > 0 && (value.
|
|
1567
|
-
into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${describeValue(value.
|
|
1562
|
+
if (seen.size > 0 && (value.status === "on-break" || value.status === "starting-after-task")) {
|
|
1563
|
+
into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${describeValue(value.status)} on a provider that publishes reasons names the one it is on`);
|
|
1568
1564
|
}
|
|
1569
1565
|
}
|
|
1570
1566
|
export function validateTeamMembers(teamMembers, path = "team", context = {}) {
|
package/guide.md
CHANGED
|
@@ -43,9 +43,8 @@ are used precisely throughout and mean nothing looser here.
|
|
|
43
43
|
| **Listen** | A lead listening to a member's call unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. Nothing of it reaches the member's task, and there is no take-over in it. See **Listening to a call**. |
|
|
44
44
|
| **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in from the moment their dial is placed. |
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
transport, a task, a break and a call each have a word of their own:
|
|
46
|
+
These fields describe state within their containing objects. Both authentication and break
|
|
47
|
+
state use `status`; name the object when discussing them to avoid ambiguity.
|
|
49
48
|
|
|
50
49
|
| Word | Belongs to | Values |
|
|
51
50
|
| --- | --- | --- |
|
|
@@ -53,7 +52,7 @@ transport, a task, a break and a call each have a word of their own:
|
|
|
53
52
|
| `media` | A task's audio | `started`, `ended` |
|
|
54
53
|
| `transport` | A connection | `connecting`, `active`, `error` |
|
|
55
54
|
| `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
|
|
56
|
-
| `
|
|
55
|
+
| `status` | The agent’s break lifecycle | `not-requested`, `awaiting-approval`, `granted`, `starting-after-task`, `on-break` |
|
|
57
56
|
| `availability` | A team member | `ready`, `on-task`, `on-break`, `signed-out` |
|
|
58
57
|
|
|
59
58
|
## Versioning
|
|
@@ -929,9 +928,9 @@ keeps a choice about it. See **The station is the host's**.
|
|
|
929
928
|
## Breaks
|
|
930
929
|
|
|
931
930
|
```ts
|
|
932
|
-
type
|
|
931
|
+
type BreakStatus =
|
|
933
932
|
| "not-requested"
|
|
934
|
-
| "awaiting-
|
|
933
|
+
| "awaiting-approval"
|
|
935
934
|
| "granted"
|
|
936
935
|
| "starting-after-task"
|
|
937
936
|
| "on-break";
|
|
@@ -949,12 +948,13 @@ type BreakRequest = {
|
|
|
949
948
|
reasonId?: string;
|
|
950
949
|
};
|
|
951
950
|
|
|
952
|
-
type ForcedBreak =
|
|
953
|
-
|
|
954
|
-
|
|
951
|
+
type ForcedBreak = {
|
|
952
|
+
by: UserId;
|
|
953
|
+
expectedDurationMs?: number;
|
|
954
|
+
};
|
|
955
955
|
|
|
956
956
|
type BreakState = {
|
|
957
|
-
|
|
957
|
+
status: BreakStatus;
|
|
958
958
|
canRequestBreak: boolean;
|
|
959
959
|
requestUnavailableReason?: string;
|
|
960
960
|
decisionReason?: string;
|
|
@@ -990,7 +990,7 @@ type TeamMember = {
|
|
|
990
990
|
id: UserId;
|
|
991
991
|
availability: TeamMemberAvailability;
|
|
992
992
|
since?: IsoTimestamp;
|
|
993
|
-
break?: Extract<
|
|
993
|
+
break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
|
|
994
994
|
};
|
|
995
995
|
|
|
996
996
|
type LeadRequest = {
|
|
@@ -1036,7 +1036,7 @@ type TeamLeadAssistCommand =
|
|
|
1036
1036
|
type TeamBreakCommand =
|
|
1037
1037
|
| { type: "decide-break-request"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
|
|
1038
1038
|
| { type: "set-break-policy"; policy: "approval-required" | "automatically-approved" | "requests-suspended" }
|
|
1039
|
-
| { type: "force-break"; memberId: UserId; reasonId?: string; reason?: string }
|
|
1039
|
+
| { type: "force-break"; memberId: UserId; reasonId?: string; reason?: string; expectedDurationMs?: number }
|
|
1040
1040
|
| { type: "end-forced-break"; memberId: UserId };
|
|
1041
1041
|
|
|
1042
1042
|
type TeamBreakCommandRequest = {
|
|
@@ -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-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-
|
|
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-approval`, 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`. |
|
|
@@ -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
|
|
2134
|
+
| `break` | Complete break state, including status, 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. |
|
|
@@ -3607,8 +3607,8 @@ nothing while none are being accepted — so they are not published separately.
|
|
|
3607
3607
|
|
|
3608
3608
|
| Field | Contract |
|
|
3609
3609
|
| --- | --- |
|
|
3610
|
-
| `
|
|
3611
|
-
| `canRequestBreak` | Whether the agent may ask at all. Distinct from `
|
|
3610
|
+
| `status` | Where the agent’s break lifecycle stands. See the states below. |
|
|
3611
|
+
| `canRequestBreak` | Whether the agent may ask at all. Distinct from `status`. |
|
|
3612
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. |
|
|
@@ -3619,10 +3619,10 @@ nothing while none are being accepted — so they are not published separately.
|
|
|
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:
|
|
3621
3621
|
|
|
3622
|
-
| `
|
|
3622
|
+
| `status` | Meaning |
|
|
3623
3623
|
| --- | --- |
|
|
3624
3624
|
| `not-requested` | No request outstanding. |
|
|
3625
|
-
| `awaiting-
|
|
3625
|
+
| `awaiting-approval` | 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
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
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**. |
|
|
@@ -3630,7 +3630,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
|
|
|
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
|
|
3632
3632
|
answer and ask again when they want to. `decisionReason` may carry the words attached to that
|
|
3633
|
-
decision, but `
|
|
3633
|
+
decision, but `status` does not remain denied.
|
|
3634
3634
|
|
|
3635
3635
|
A provider reports `starting-after-task` only after Omni commits a `granted` request while
|
|
3636
3636
|
work is still active. Omni does not send the request again, because asking again would not move
|
|
@@ -3642,6 +3642,18 @@ something a busy hour can cancel, and Omni keeps offering those while the rest a
|
|
|
3642
3642
|
|
|
3643
3643
|
### Forced breaks
|
|
3644
3644
|
|
|
3645
|
+
Migration: the former awaiting-decision value is now `awaiting-approval` in break status
|
|
3646
|
+
and the team member’s pending break state. The old value is rejected without an alias;
|
|
3647
|
+
the request can still be granted or denied, and ordering rules are unchanged.
|
|
3648
|
+
|
|
3649
|
+
Migration: the former BreakApproval type is now `BreakStatus`, matching `BreakState.status`.
|
|
3650
|
+
The former type is not exported as an alias; its values and behavior are unchanged.
|
|
3651
|
+
|
|
3652
|
+
Migration: `BreakState.status` replaces the former approval field. It covers the full break
|
|
3653
|
+
lifecycle. The old field is rejected even alongside status; related diagnostics now use status.
|
|
3654
|
+
The `BreakStatus` union and its values, `reasons`, `activeReasonId`, and the separate testing
|
|
3655
|
+
helper BreakOnTaskStep.approval are unchanged. Hosts and providers must update together.
|
|
3656
|
+
|
|
3645
3657
|
Migration: the break-state field formerly named retryAfterMs is now `BreakState.retryRequestAfterMs`,
|
|
3646
3658
|
with diagnostic `break.retryRequestAfterMs`. It remains an optional non-negative finite number
|
|
3647
3659
|
of milliseconds; the former break field is rejected even alongside the new field.
|
|
@@ -3664,8 +3676,8 @@ mayAsk. Request eligibility remains distinct from approval; existing alwaysAvail
|
|
|
3664
3676
|
exceptions are unchanged. Hosts and providers must update together.
|
|
3665
3677
|
|
|
3666
3678
|
Migration: ImposedBreak is now `ForcedBreak`, and the former imposed field is now
|
|
3667
|
-
`BreakState.forced`. Diagnostic and harness coverage names use `break.forced
|
|
3668
|
-
|
|
3679
|
+
`BreakState.forced`. Diagnostic and harness coverage names use `break.forced`. The former agent-end prohibition
|
|
3680
|
+
has been removed: forced breaks also permit explicit agent resumption. Update hosts and providers together. The old field
|
|
3669
3681
|
is rejected, including when both spellings are sent; no compatibility alias is provided.
|
|
3670
3682
|
The break policy value formerly named auto-approve is now `automatically-approved`.
|
|
3671
3683
|
Requests are approved automatically; approval does not skip commitment or the prerequisites
|
|
@@ -3684,11 +3696,11 @@ The team break command formerly named place is now `force-break`. Hosts and prov
|
|
|
3684
3696
|
adopt the new command together; place and the interim force spelling are rejected without aliases.
|
|
3685
3697
|
The team break command formerly named release is now `end-forced-break`, and its diagnostic is
|
|
3686
3698
|
`team.break.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
|
|
3687
|
-
|
|
3688
|
-
method still cannot end a forced break. Break ordering and permission rules are unchanged.
|
|
3699
|
+
The agent’s `endBreak` now applies to forced breaks as well as requested breaks.
|
|
3689
3700
|
|
|
3690
|
-
`ForcedBreak`
|
|
3691
|
-
|
|
3701
|
+
`ForcedBreak` names who forced the break and may include an advisory `expectedDurationMs`.
|
|
3702
|
+
The agent must explicitly resume when ready. The retired endsAutomatically and endsAt fields
|
|
3703
|
+
are rejected, including when an expected duration is also provided.
|
|
3692
3704
|
|
|
3693
3705
|
**Every forced break has a person behind it.** A lead or a manager forced it; there is no such
|
|
3694
3706
|
thing as a break the platform forced on its own. Where a platform applies one automatically, it is
|
|
@@ -3699,8 +3711,8 @@ Omni resolves the name to show with `describeUsers()`, so a provider sends the i
|
|
|
3699
3711
|
a display name.
|
|
3700
3712
|
|
|
3701
3713
|
**A forced break travels with `on-break` or `starting-after-task`, and nothing else.** It is a
|
|
3702
|
-
break in progress or about to be; beside `granted` or `awaiting-
|
|
3703
|
-
request the agent never made and commit it (`break.forced.
|
|
3714
|
+
break in progress or about to be; beside `granted` or `awaiting-approval` the host would read a
|
|
3715
|
+
request the agent never made and commit it (`break.forced.status`). Where the provider publishes
|
|
3704
3716
|
`reasons`, the lead's `force-break` named one, and the member's state carries it as `activeReasonId`.
|
|
3705
3717
|
|
|
3706
3718
|
For example:
|
|
@@ -3708,17 +3720,34 @@ For example:
|
|
|
3708
3720
|
```ts
|
|
3709
3721
|
forced: {
|
|
3710
3722
|
by: "manager-1042",
|
|
3711
|
-
|
|
3712
|
-
endsAt: "2026-08-21T10:00:00.000Z"
|
|
3723
|
+
expectedDurationMs: 600000
|
|
3713
3724
|
}
|
|
3714
3725
|
```
|
|
3715
3726
|
|
|
3716
|
-
The
|
|
3717
|
-
Resume
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3727
|
+
The host keeps **Resume** available for an agent on a forced break. The agent explicitly chooses
|
|
3728
|
+
Resume; the host sends `endBreak()` and follows the provider-confirmed state before resuming
|
|
3729
|
+
work. An expected duration is a positive finite number of milliseconds, measured from actual
|
|
3730
|
+
entry into `on-break`, excluding any `starting-after-task` wait. Omission means no expected
|
|
3731
|
+
duration was supplied. Neither duration expiry nor an overdue indication authorizes the host
|
|
3732
|
+
or provider to end the break, restore availability, or route work to the agent.
|
|
3733
|
+
|
|
3734
|
+
**A lead lifting the restriction does not resume the agent.** On an applied
|
|
3735
|
+
`end-forced-break`, the provider clears `BreakState.forced` while preserving the current
|
|
3736
|
+
`on-break` or `starting-after-task` status and `activeReasonId`. This command cannot publish
|
|
3737
|
+
`not-requested`, restore readiness, or start routing work. Ordinary progress from
|
|
3738
|
+
`starting-after-task` to `on-break` still happens when work finishes. The host follows the
|
|
3739
|
+
published state and waits for the agent's explicit Resume, even after reconnect. Historical
|
|
3740
|
+
attribution is not rewritten by clearing the current forced marker.
|
|
3741
|
+
|
|
3742
|
+
The agent may explicitly resume either before or after the lead lifts the restriction. A lead
|
|
3743
|
+
command, timer, late snapshot or reconnect is never a substitute for that choice. Providers
|
|
3744
|
+
must correlate resumption to the authenticated agent's action; a state-only ordering validator
|
|
3745
|
+
cannot establish the cause of a transition. The usual multi-provider end/reconciliation flow
|
|
3746
|
+
still applies, and work resumes only on confirmed provider state.
|
|
3747
|
+
|
|
3748
|
+
A host may display the expected duration. A countdown requires an evidenced actual start;
|
|
3749
|
+
a received snapshot, replay or reconnect is not a new start and cannot restart the duration.
|
|
3750
|
+
Without that evidence, show the duration without inventing a start or return time.
|
|
3722
3751
|
|
|
3723
3752
|
A break applies to the **agent**, not to one provider. When a provider forces one, Omni immediately
|
|
3724
3753
|
requests a break on every other connected provider, or they would keep routing work to somebody who
|
|
@@ -3804,8 +3833,8 @@ request object only to the request method, and undefined to the other three.
|
|
|
3804
3833
|
| --- | --- |
|
|
3805
3834
|
| `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
3835
|
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
|
|
3807
|
-
| `cancelBreak` | `awaiting-
|
|
3808
|
-
| `endBreak` | `on-break` or `starting-after-task` during reconciliation;
|
|
3836
|
+
| `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
|
|
3837
|
+
| `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
|
|
3809
3838
|
|
|
3810
3839
|
Use `validateTeamBreakCommand(request, context)` for lead decisions, forcing a break, ending a forced break and
|
|
3811
3840
|
policy commands. It requires the live lead capability and active transport, a current target
|
|
@@ -3829,8 +3858,18 @@ required; per-provider validation does not implement the multi-provider coordina
|
|
|
3829
3858
|
|
|
3830
3859
|
### `requestBreak(request)`
|
|
3831
3860
|
|
|
3861
|
+
**Explanatory text is optional.** An agent may omit `BreakRequest.reason` for any break type.
|
|
3862
|
+
A lead may likewise omit reason text from `force-break`. Do not require a text explanation
|
|
3863
|
+
just because a break is agent-requested or forced. If supplied, text must be nonempty; omit
|
|
3864
|
+
it rather than sending an empty string. A selected `reasonId` identifies a published break
|
|
3865
|
+
choice and is separate from this optional text.
|
|
3866
|
+
|
|
3867
|
+
With published choices, `{ reasonId: "lunch" }` is a valid request without explanation;
|
|
3868
|
+
without published choices, `{}` is valid. A lead can similarly send a `force-break` command
|
|
3869
|
+
with its member ID and, where required, the selected reason ID, without reason text.
|
|
3870
|
+
|
|
3832
3871
|
Requests permission to stop the agent later; it does not itself stop work. The provider continues
|
|
3833
|
-
offering work and reports `awaiting-
|
|
3872
|
+
offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
|
|
3834
3873
|
provider reports `not-requested` directly, with `decisionReason` when one was supplied.
|
|
3835
3874
|
|
|
3836
3875
|
**An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
|
|
@@ -3959,7 +3998,7 @@ Omni coordinates one break attempt as follows:
|
|
|
3959
3998
|
capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
3960
3999
|
available and work continues. `assertBreakAttemptProviders` holds a host to this set.
|
|
3961
4000
|
2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
|
|
3962
|
-
3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-
|
|
4001
|
+
3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
|
|
3963
4002
|
`granted`; neither state stops work. A denial transitions directly to `not-requested` and
|
|
3964
4003
|
causes Omni to take the cancel path.
|
|
3965
4004
|
4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
|
|
@@ -3974,7 +4013,7 @@ Omni coordinates one break attempt as follows:
|
|
|
3974
4013
|
5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
|
|
3975
4014
|
decision timeout, or the agent cancels before commit, durably choose cancel and enter
|
|
3976
4015
|
`cancelling-break`. Send `cancelBreak()` to every provider still reporting
|
|
3977
|
-
`awaiting-
|
|
4016
|
+
`awaiting-approval` or `granted`. Work continues during cancellation because no stop was
|
|
3978
4017
|
committed. Return to `working` only after no provider retains either state.
|
|
3979
4018
|
|
|
3980
4019
|
Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
|
|
@@ -4044,11 +4083,11 @@ asked for, but only until the session ends — after a reload or reconnect, or w
|
|
|
4044
4083
|
the agent on the break itself, the provider is the only one who knows.
|
|
4045
4084
|
|
|
4046
4085
|
Omit it when you cannot say, and when there is no break: reporting a reason alongside
|
|
4047
|
-
`
|
|
4086
|
+
`status: "not-requested"` describes a break that is not happening, and is rejected.
|
|
4048
4087
|
|
|
4049
4088
|
### `cancelBreak()`
|
|
4050
4089
|
|
|
4051
|
-
Cancels the active pre-commit request while its
|
|
4090
|
+
Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
|
|
4052
4091
|
Cancellation releases the request but does not restore work because work never stopped. If
|
|
4053
4092
|
commit already won, the provider returns `omni.break-already-committed`. The resulting state is
|
|
4054
4093
|
reported through `break-state`.
|
|
@@ -4139,8 +4178,8 @@ One method, `executeTeamBreak`, taking a discriminated command exactly as `execu
|
|
|
4139
4178
|
| --- | --- |
|
|
4140
4179
|
| `{ 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
4180
|
| `{ 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 }` |
|
|
4181
|
+
| `{ type: "force-break", memberId: UserId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory and is published on the resulting forced break. |
|
|
4182
|
+
| `{ type: "end-forced-break", memberId: UserId }` | Lifts the forced-break restriction, whoever forced it, by clearing `BreakState.forced`. The committed break continues; only the agent resumes work. |
|
|
4144
4183
|
|
|
4145
4184
|
`memberId` is this provider's own identifier for the member, as published on its team member list. It is
|
|
4146
4185
|
never an identifier from another provider, and Omni does not translate between them; names come
|
|
@@ -4194,7 +4233,7 @@ they joined -- is answered `failed`, whatever their ceiling; the request stands
|
|
|
4194
4233
|
or until it is withdrawn or declined.
|
|
4195
4234
|
|
|
4196
4235
|
**A lead on a break does not join.** A break is a reported state in which the agent is not working,
|
|
4197
|
-
and a join is work. Omni offers Join to a lead only while their own `BreakState.
|
|
4236
|
+
and a join is work. Omni offers Join to a lead only while their own `BreakState.status` is neither
|
|
4198
4237
|
`starting-after-task` nor `on-break` -- a committed break waiting for the lead's current work to
|
|
4199
4238
|
finish is not given more -- and a provider answers a `join` from a lead on such a break `failed`.
|
|
4200
4239
|
The request stands for another lead, as it does when this one is already on a call.
|
|
@@ -4345,17 +4384,17 @@ about whether anybody has to act on it, and the difference is a lead's entire ac
|
|
|
4345
4384
|
|
|
4346
4385
|
| `break` | Means |
|
|
4347
4386
|
| --- | --- |
|
|
4348
|
-
| `awaiting-
|
|
4387
|
+
| `awaiting-approval` | Somebody has to decide. This is the lead's queue. |
|
|
4349
4388
|
| `granted` | Decided yes, but Omni has not told the provider to stop yet. Work continues and nobody needs to decide. |
|
|
4350
4389
|
| `starting-after-task` | Already granted; it begins when their current task ends. Nobody needs to act. |
|
|
4351
4390
|
|
|
4352
4391
|
Those three are the only values that appear here. `not-requested` is absence — omit `break`
|
|
4353
4392
|
instead. `on-break` is `availability: "on-break"`, and a denial transitions to `not-requested`,
|
|
4354
|
-
so neither survives to be reported. It is otherwise the same `
|
|
4393
|
+
so neither survives to be reported. It is otherwise the same `BreakStatus` the member's own
|
|
4355
4394
|
break state uses, rather than a parallel vocabulary for the lead's view, so the two cannot drift
|
|
4356
4395
|
apart.
|
|
4357
4396
|
|
|
4358
|
-
Omni offers Approve and Deny only while a member is `awaiting-
|
|
4397
|
+
Omni offers Approve and Deny only while a member is `awaiting-approval`, shows `granted` as agreed
|
|
4359
4398
|
but not started, and shows `starting-after-task` as settled.
|
|
4360
4399
|
|
|
4361
4400
|
**Live status, not a record.** The provider derives it from what is true now — not stored, not
|
|
@@ -4980,8 +5019,8 @@ covers a `diagnostic`: shown where the agent works, and counted.
|
|
|
4980
5019
|
|
|
4981
5020
|
### `break-state`
|
|
4982
5021
|
|
|
4983
|
-
Replaces this provider's complete `break` object. Its `
|
|
4984
|
-
`not-requested`, `awaiting-
|
|
5022
|
+
Replaces this provider's complete `break` object. Its `status` uses the canonical
|
|
5023
|
+
`not-requested`, `awaiting-approval`, `granted`, `starting-after-task` and
|
|
4985
5024
|
`on-break` states defined under Breaks; the event also carries the corresponding may-ask state,
|
|
4986
5025
|
reasons, retry details, and any forced break.
|
|
4987
5026
|
|
|
@@ -4990,7 +5029,7 @@ Each state is also held to the one before it. A commit's states, `starting-after
|
|
|
4990
5029
|
break, which says so with `forced`: in effect at once, or `starting-after-task` while the member
|
|
4991
5030
|
finishes the call they are on (`stream.breakState.commitBeforeGrant`); and a break never moves backwards —
|
|
4992
5031
|
from `on-break` or `starting-after-task` to a grant or a request, or from `granted` to
|
|
4993
|
-
`awaiting-
|
|
5032
|
+
`awaiting-approval` — a new request passes through `not-requested` (`stream.breakState.backwards`).
|
|
4994
5033
|
`exerciseAdapter` holds the stream to that from the connect snapshot on;
|
|
4995
5034
|
`assertBreakFollowsItsRequests` holds any sequence.
|
|
4996
5035
|
|
|
@@ -5012,14 +5051,14 @@ provider rechecks current permission and state atomically before acting. The pro
|
|
|
5012
5051
|
the resulting break/task state before publication and serializes those publications. The host
|
|
5013
5052
|
validates the envelope, transition and full task consistency before replacing local state. A
|
|
5014
5053
|
request result does not optimistically advance break state, and a delayed command cannot apply
|
|
5015
|
-
to a later attempt merely because its
|
|
5054
|
+
to a later attempt merely because its status happens to look compatible. Keep at most one
|
|
5016
5055
|
unresolved operation per provider/attempt; recovery must resolve uncertainty before another
|
|
5017
5056
|
operation is dispatched. The source must fence delayed operations against its own attempt state.
|
|
5018
5057
|
|
|
5019
|
-
Normal order is `not-requested` → `awaiting-
|
|
5058
|
+
Normal order is `not-requested` → `awaiting-approval` → `granted` →
|
|
5020
5059
|
`starting-after-task` → `on-break` → `not-requested`. Auto-approval may go directly to
|
|
5021
5060
|
`granted`; a commit with no outstanding work may go directly to `on-break`. Denial/cancel
|
|
5022
|
-
returns a precommit request to `not-requested`; an
|
|
5061
|
+
returns a precommit request to `not-requested`; an explicit agent end returns a committed
|
|
5023
5062
|
break there. Same-state restatements are allowed. An evidenced forced break is the explicit
|
|
5024
5063
|
exception to requesting/granting, and must carry its forced actor/state. Neither skipped
|
|
5025
5064
|
publication nor a host-local guess creates another exception. A later normal attempt starts
|
|
@@ -5029,7 +5068,7 @@ A fresh authoritative snapshot establishes a new baseline; do not run this event
|
|
|
5029
5068
|
check across it. A snapshot may legitimately establish a later request or an already active
|
|
5030
5069
|
break after reconnect. Its freshness must be established by the source/adapter's ordered
|
|
5031
5070
|
snapshot/publication boundary and fencing of obsolete callbacks and reads. A delayed old
|
|
5032
|
-
snapshot is not detectable from break
|
|
5071
|
+
snapshot is not detectable from break status alone. Neither envelope time nor event ID
|
|
5033
5072
|
supplies a break revision. Do not suppress all requests after an active break: a later attempt
|
|
5034
5073
|
passes through `not-requested`, and then may request again. Do not replay earlier ProviderEvents
|
|
5035
5074
|
after recovery. Raw backend break handlers must preserve source order too; this helper does
|