@xema/omni-protocol 0.1.93 → 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 +23 -29
- package/dist/testing.d.ts +5 -5
- package/dist/testing.js +13 -12
- package/dist/validation.js +53 -54
- package/guide.md +157 -87
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -237,7 +237,7 @@ export interface AuthenticationFailure {
|
|
|
237
237
|
export interface TeamCapabilities {
|
|
238
238
|
/**
|
|
239
239
|
* This lead may act on their team's breaks through `executeTeamBreak` -- force-break, end-forced-break, decide-break-request,
|
|
240
|
-
* set
|
|
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;
|
|
@@ -1217,13 +1217,13 @@ 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. */
|
|
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,38 +1245,29 @@ 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 {
|
|
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
|
-
|
|
1276
|
-
/** Shown when `
|
|
1277
|
-
|
|
1266
|
+
canRequestBreak: boolean;
|
|
1267
|
+
/** Shown when `canRequestBreak` is false, such as "Busy hours". */
|
|
1268
|
+
requestUnavailableReason?: string;
|
|
1278
1269
|
decisionReason?: string;
|
|
1279
|
-
|
|
1270
|
+
retryRequestAfterMs?: number;
|
|
1280
1271
|
/** Not-ready codes this provider offers. Omitted when it defines none. */
|
|
1281
1272
|
reasons?: BreakReason[];
|
|
1282
1273
|
/** 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,8 +1311,8 @@ export interface TeamMember {
|
|
|
1320
1311
|
availability: TeamMemberAvailability;
|
|
1321
1312
|
/** Omitted rather than invented: Omni renders it as a duration. */
|
|
1322
1313
|
since?: IsoTimestamp;
|
|
1323
|
-
/** A request in flight or a grant not yet in effect. `not-requested` is absence, and `
|
|
1324
|
-
break?: Extract<
|
|
1314
|
+
/** A request in flight or a grant not yet in effect. `not-requested` is absence, and `on-break` is `availability: "on-break"`. */
|
|
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 {
|
|
@@ -1395,16 +1386,19 @@ export type TeamBreakCommand = {
|
|
|
1395
1386
|
decision: "granted" | "denied";
|
|
1396
1387
|
reason?: string;
|
|
1397
1388
|
} | {
|
|
1398
|
-
type: "policy";
|
|
1399
|
-
policy: "
|
|
1389
|
+
type: "set-break-policy";
|
|
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,13 +242,13 @@ 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
|
/**
|
|
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
|
@@ -938,22 +938,22 @@ 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);
|
|
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
|
}
|
|
@@ -1865,9 +1865,10 @@ async function driveOneCall(drive) {
|
|
|
1865
1865
|
nothingLate();
|
|
1866
1866
|
return found;
|
|
1867
1867
|
}
|
|
1868
|
-
// A request goes not-requested -> awaiting-
|
|
1869
|
-
// starting-after-task |
|
|
1870
|
-
// a cancel
|
|
1868
|
+
// A request goes not-requested -> awaiting-approval | granted; a commit goes granted ->
|
|
1869
|
+
// starting-after-task | on-break; work ending goes starting-after-task -> on-break; a denial,
|
|
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 {
|
|
@@ -1990,11 +1991,11 @@ export function assertBreakAttemptProviders(candidates, asked) {
|
|
|
1990
1991
|
/**
|
|
1991
1992
|
* A break asked for on a task begins when the work ends. `steps` is the sequence the provider
|
|
1992
1993
|
* 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 `
|
|
1994
|
+
* as `starting-after-task` while it remains, and `on-break` arrives only once nothing is
|
|
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)
|
|
@@ -2003,14 +2004,14 @@ export function assertBreakBeginsAfterTask(steps) {
|
|
|
2003
2004
|
if (committed < 0)
|
|
2004
2005
|
throw new Error("A break committed on a task is reported as starting-after-task while the work remains");
|
|
2005
2006
|
steps.forEach((step, index) => {
|
|
2006
|
-
if (step.approval === "
|
|
2007
|
-
throw new Error(`steps[${index}] reports
|
|
2007
|
+
if (step.approval === "on-break" && step.outstanding > 0) {
|
|
2008
|
+
throw new Error(`steps[${index}] reports on-break with ${step.outstanding} task(s) outstanding: a break begins when the work ends`);
|
|
2008
2009
|
}
|
|
2009
2010
|
if (step.approval === "starting-after-task" && step.outstanding < 1) {
|
|
2010
2011
|
throw new Error(`steps[${index}] reports starting-after-task with nothing outstanding: the break should have begun`);
|
|
2011
2012
|
}
|
|
2012
2013
|
});
|
|
2013
|
-
if (steps.at(-1)?.approval !== "
|
|
2014
|
+
if (steps.at(-1)?.approval !== "on-break") {
|
|
2014
2015
|
throw new Error(`Break-on-task scenario must end in effect, ended ${String(steps.at(-1)?.approval)}`);
|
|
2015
2016
|
}
|
|
2016
2017
|
}
|
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,15 +1322,15 @@ 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) {
|
|
1340
|
-
into.add("break.
|
|
1333
|
+
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
1334
|
}
|
|
1342
1335
|
else {
|
|
1343
1336
|
const reasons = Array.isArray(state.reasons) ? state.reasons : [];
|
|
@@ -1366,8 +1359,8 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1366
1359
|
into.require(isPlainObject(caps.team) && caps.team.breakControl === true, "team.break.command.capability", path, "the login must declare team.breakControl");
|
|
1367
1360
|
const command = request.command;
|
|
1368
1361
|
const allowed = {
|
|
1369
|
-
"decide-break-request": ["type", "memberId", "decision", "reason"], policy: ["type", "policy"],
|
|
1370
|
-
"force-break": ["type", "memberId", "reasonId", "reason"], "end-forced-break": ["type", "memberId"],
|
|
1362
|
+
"decide-break-request": ["type", "memberId", "decision", "reason"], "set-break-policy": ["type", "policy"],
|
|
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) {
|
|
@@ -1380,8 +1373,8 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1380
1373
|
into.require(fields.includes(key), "team.break.command.field", `${path}.${key}`, "field is not supported for this command");
|
|
1381
1374
|
if (command.reason !== undefined)
|
|
1382
1375
|
into.filled(command.reason, "team.break.command.reason", path, "reason must not be empty");
|
|
1383
|
-
if (command.type === "policy") {
|
|
1384
|
-
into.require(["
|
|
1376
|
+
if (command.type === "set-break-policy") {
|
|
1377
|
+
into.require(["approval-required", "automatically-approved", "requests-suspended"].includes(command.policy), "team.break.command.policy", path, "unknown break policy");
|
|
1385
1378
|
return into.violations;
|
|
1386
1379
|
}
|
|
1387
1380
|
into.filled(command.memberId, "team.break.command.member", path, "name the target member");
|
|
@@ -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;
|
|
@@ -1449,17 +1444,17 @@ export function validateBreakCommand(method, request, state, context, path = "br
|
|
|
1449
1444
|
into.require(selected !== undefined, "break.request.reasonId", path, "choose a currently published reasonId; free text is not a substitute");
|
|
1450
1445
|
else
|
|
1451
1446
|
into.require(request.reasonId === undefined, "break.request.reasonId.unexpected", path, "no reason codes were published");
|
|
1452
|
-
into.require(state.
|
|
1447
|
+
into.require(state.canRequestBreak === true || (isPlainObject(selected) && selected.alwaysAvailable === true), "break.request.canRequestBreak", path, "asking is disabled except for the selected alwaysAvailable reason");
|
|
1453
1448
|
}
|
|
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.
|
|
1482
|
-
const committed = to === "starting-after-task" || to === "
|
|
1483
|
-
if (committed && (from === "not-requested" || from === "awaiting-
|
|
1484
|
-
into.add("stream.breakState.commitBeforeGrant", `${path}.after.
|
|
1485
|
-
}
|
|
1486
|
-
const backwards = (from === "
|
|
1487
|
-
(from === "starting-after-task" && (to === "awaiting-
|
|
1488
|
-
(from === "granted" && to === "awaiting-
|
|
1475
|
+
const from = before.status;
|
|
1476
|
+
const to = after.status;
|
|
1477
|
+
const committed = to === "starting-after-task" || to === "on-break";
|
|
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}`);
|
|
1480
|
+
}
|
|
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,32 +1491,36 @@ 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.
|
|
1500
|
-
into.
|
|
1501
|
-
|
|
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`);
|
|
1496
|
+
into.require(!Object.hasOwn(value, "mayAsk"), "break.canRequestBreak.renamed", `${path}.mayAsk`, "mayAsk was renamed to canRequestBreak; use only canRequestBreak");
|
|
1497
|
+
into.require(typeof value.canRequestBreak === "boolean", "break.canRequestBreak", `${path}.canRequestBreak`, "canRequestBreak says whether the agent may ask for a break: a boolean");
|
|
1498
|
+
into.require(!Object.hasOwn(value, "refusedReason"), "break.requestUnavailableReason.renamed", `${path}.refusedReason`, "refusedReason was renamed to requestUnavailableReason; use only requestUnavailableReason");
|
|
1499
|
+
for (const field of ["requestUnavailableReason", "decisionReason"]) {
|
|
1502
1500
|
if (value[field] !== undefined) {
|
|
1503
1501
|
into.filled(value[field], `break.${field}`, `${path}.${field}`, `${field} must not be empty when present`);
|
|
1504
1502
|
}
|
|
1505
1503
|
}
|
|
1506
|
-
// The refusal is the reason the control is withdrawn; beside `
|
|
1507
|
-
if (value.
|
|
1508
|
-
into.require(value.
|
|
1504
|
+
// The refusal is the reason the control is withdrawn; beside `canRequestBreak: true` it explains nothing.
|
|
1505
|
+
if (value.requestUnavailableReason !== undefined) {
|
|
1506
|
+
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
1507
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1508
|
+
into.require(!Object.hasOwn(value, "retryAfterMs"), "break.retryRequestAfterMs.renamed", `${path}.retryAfterMs`, "retryAfterMs was renamed to retryRequestAfterMs on BreakState");
|
|
1509
|
+
if (value.retryRequestAfterMs !== undefined) {
|
|
1510
|
+
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
1511
|
}
|
|
1513
1512
|
if (value.activeReasonId !== undefined) {
|
|
1514
1513
|
into.filled(value.activeReasonId, "break.activeReasonId", `${path}.activeReasonId`, "activeReasonId must not be empty when present");
|
|
1515
1514
|
// A break nobody is on has no reason. Reporting one beside `not-requested` describes a
|
|
1516
1515
|
// break that is not happening.
|
|
1517
|
-
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");
|
|
1518
1517
|
}
|
|
1519
1518
|
if (value.forced !== undefined) {
|
|
1520
1519
|
validateForcedBreak(value.forced, `${path}.forced`, into);
|
|
1521
1520
|
// 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
|
|
1521
|
+
// A forced break is a break in progress or about to be: it travels with on-break or
|
|
1523
1522
|
// starting-after-task and nothing else. Beside granted, the host would commit a break nobody asked for.
|
|
1524
|
-
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`);
|
|
1525
1524
|
}
|
|
1526
1525
|
if (value.reasons === undefined)
|
|
1527
1526
|
return;
|
|
@@ -1560,8 +1559,8 @@ function validateBreakState(value, path, into) {
|
|
|
1560
1559
|
// forced one included, since the lead's force-break command named it. A break of no kind is a break whose
|
|
1561
1560
|
// rules -- who may listen through it, whether it counts -- nobody can apply.
|
|
1562
1561
|
ruleEvaluated("break.activeReasonId.required");
|
|
1563
|
-
if (seen.size > 0 && (value.
|
|
1564
|
-
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`);
|
|
1565
1564
|
}
|
|
1566
1565
|
}
|
|
1567
1566
|
export function validateTeamMembers(teamMembers, path = "team", context = {}) {
|
|
@@ -1652,7 +1651,7 @@ function validateTeamMembersInto(teamMembers, path, context, into) {
|
|
|
1652
1651
|
if (member.since !== undefined)
|
|
1653
1652
|
into.timestamp(member.since, "team.member.since", `${at}.since`);
|
|
1654
1653
|
if (member.break !== undefined) {
|
|
1655
|
-
// Only an outstanding request appears here: `not-requested` is absence, `
|
|
1654
|
+
// Only an outstanding request appears here: `not-requested` is absence, `on-break` is
|
|
1656
1655
|
// `availability: "on-break"`, and a denial never survives to be reported.
|
|
1657
1656
|
if (into.oneOf(member.break, MEMBER_BREAKS, "team.member.break", `${at}.break`)) {
|
|
1658
1657
|
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
|
@@ -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,12 +928,12 @@ 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";
|
|
938
937
|
|
|
939
938
|
type BreakReason = {
|
|
940
939
|
id: string;
|
|
@@ -949,16 +948,17 @@ 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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
957
|
+
status: BreakStatus;
|
|
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;
|
|
@@ -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 = {
|
|
@@ -1035,8 +1035,8 @@ type TeamLeadAssistCommand =
|
|
|
1035
1035
|
|
|
1036
1036
|
type TeamBreakCommand =
|
|
1037
1037
|
| { type: "decide-break-request"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
|
|
1038
|
-
| { type: "policy"; policy: "
|
|
1039
|
-
| { type: "force-break"; memberId: UserId; reasonId?: string; reason?: string }
|
|
1038
|
+
| { type: "set-break-policy"; policy: "approval-required" | "automatically-approved" | "requests-suspended" }
|
|
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
|
|
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,45 +3607,88 @@ nothing while none are being accepted — so they are not published separately.
|
|
|
3607
3607
|
|
|
3608
3608
|
| Field | Contract |
|
|
3609
3609
|
| --- | --- |
|
|
3610
|
-
| `
|
|
3611
|
-
| `
|
|
3612
|
-
| `
|
|
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
|
+
| `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
|
|
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
|
-
| `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
|
|
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
|
|
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 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
|
+
|
|
3657
|
+
Migration: the break-state field formerly named retryAfterMs is now `BreakState.retryRequestAfterMs`,
|
|
3658
|
+
with diagnostic `break.retryRequestAfterMs`. It remains an optional non-negative finite number
|
|
3659
|
+
of milliseconds; the former break field is rejected even alongside the new field.
|
|
3660
|
+
The separate authentication and general failure retryAfterMs fields are unchanged.
|
|
3661
|
+
|
|
3662
|
+
Migration: the former in-effect break approval value is now `on-break`. It means the
|
|
3663
|
+
agent is on the break, not merely granted permission. The old value is rejected without an
|
|
3664
|
+
alias. The task prerequisite, supported listening exceptions and ordering rules are unchanged;
|
|
3665
|
+
the task-conflict diagnostic is `break.on-break.tasks`. Team member availability already uses
|
|
3666
|
+
`on-break` and is unchanged.
|
|
3667
|
+
|
|
3668
|
+
Migration: the former refusedReason field is now `BreakState.requestUnavailableReason`.
|
|
3669
|
+
It explains why requests are unavailable when `canRequestBreak` is false; `decisionReason`
|
|
3670
|
+
continues to explain a particular approval or denial. The old field is rejected even alongside
|
|
3671
|
+
the new one, and related diagnostics use requestUnavailableReason.
|
|
3672
|
+
|
|
3673
|
+
Migration: the former mayAsk field is now `BreakState.canRequestBreak`. The old field is
|
|
3674
|
+
rejected even beside the new field. Validation diagnostics use canRequestBreak in place of
|
|
3675
|
+
mayAsk. Request eligibility remains distinct from approval; existing alwaysAvailable reason
|
|
3676
|
+
exceptions are unchanged. Hosts and providers must update together.
|
|
3677
|
+
|
|
3645
3678
|
Migration: ImposedBreak is now `ForcedBreak`, and the former imposed field is now
|
|
3646
|
-
`BreakState.forced`. Diagnostic and harness coverage names use `break.forced
|
|
3647
|
-
|
|
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
|
|
3648
3681
|
is rejected, including when both spellings are sent; no compatibility alias is provided.
|
|
3682
|
+
The break policy value formerly named auto-approve is now `automatically-approved`.
|
|
3683
|
+
Requests are approved automatically; approval does not skip commitment or the prerequisites
|
|
3684
|
+
for starting a break. The former value is rejected without an alias.
|
|
3685
|
+
The break policy value formerly named suspended is now `requests-suspended`. The former
|
|
3686
|
+
value is rejected without an alias. It suspends new requests, not breaks already underway.
|
|
3687
|
+
The break policy value formerly named ask is now `approval-required`: requests require
|
|
3688
|
+
approval. The former value is rejected without an alias; automatically-approved and requests-suspended retain
|
|
3689
|
+
their existing behavior.
|
|
3690
|
+
The team break command formerly named policy is now `set-break-policy`. The former
|
|
3691
|
+
command is rejected without an alias; its policy field is unchanged. Hosts and providers must adopt the new name together.
|
|
3649
3692
|
The team break command formerly named decide is now `decide-break-request`. The former
|
|
3650
3693
|
spelling is rejected without an alias; the granted/denied decisions and pending-request
|
|
3651
3694
|
prerequisite are unchanged.
|
|
@@ -3653,11 +3696,11 @@ The team break command formerly named place is now `force-break`. Hosts and prov
|
|
|
3653
3696
|
adopt the new command together; place and the interim force spelling are rejected without aliases.
|
|
3654
3697
|
The team break command formerly named release is now `end-forced-break`, and its diagnostic is
|
|
3655
3698
|
`team.break.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
|
|
3656
|
-
|
|
3657
|
-
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.
|
|
3658
3700
|
|
|
3659
|
-
`ForcedBreak`
|
|
3660
|
-
|
|
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.
|
|
3661
3704
|
|
|
3662
3705
|
**Every forced break has a person behind it.** A lead or a manager forced it; there is no such
|
|
3663
3706
|
thing as a break the platform forced on its own. Where a platform applies one automatically, it is
|
|
@@ -3667,9 +3710,9 @@ them, not the machinery that carried it out.
|
|
|
3667
3710
|
Omni resolves the name to show with `describeUsers()`, so a provider sends the identifier and never
|
|
3668
3711
|
a display name.
|
|
3669
3712
|
|
|
3670
|
-
**A forced break travels with `
|
|
3671
|
-
break in progress or about to be; beside `granted` or `awaiting-
|
|
3672
|
-
request the agent never made and commit it (`break.forced.
|
|
3713
|
+
**A forced break travels with `on-break` or `starting-after-task`, and nothing else.** It is a
|
|
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
|
|
3673
3716
|
`reasons`, the lead's `force-break` named one, and the member's state carries it as `activeReasonId`.
|
|
3674
3717
|
|
|
3675
3718
|
For example:
|
|
@@ -3677,17 +3720,34 @@ For example:
|
|
|
3677
3720
|
```ts
|
|
3678
3721
|
forced: {
|
|
3679
3722
|
by: "manager-1042",
|
|
3680
|
-
|
|
3681
|
-
endsAt: "2026-08-21T10:00:00.000Z"
|
|
3723
|
+
expectedDurationMs: 600000
|
|
3682
3724
|
}
|
|
3683
3725
|
```
|
|
3684
3726
|
|
|
3685
|
-
The
|
|
3686
|
-
Resume
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
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.
|
|
3691
3751
|
|
|
3692
3752
|
A break applies to the **agent**, not to one provider. When a provider forces one, Omni immediately
|
|
3693
3753
|
requests a break on every other connected provider, or they would keep routing work to somebody who
|
|
@@ -3771,10 +3831,10 @@ request object only to the request method, and undefined to the other three.
|
|
|
3771
3831
|
|
|
3772
3832
|
| Method | Required current state |
|
|
3773
3833
|
| --- | --- |
|
|
3774
|
-
| `requestBreak` | `not-requested`; selected current reason code when codes exist; `
|
|
3775
|
-
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `
|
|
3776
|
-
| `cancelBreak` | `awaiting-
|
|
3777
|
-
| `endBreak` | `
|
|
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. |
|
|
3835
|
+
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
|
|
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. |
|
|
3778
3838
|
|
|
3779
3839
|
Use `validateTeamBreakCommand(request, context)` for lead decisions, forcing a break, ending a forced break and
|
|
3780
3840
|
policy commands. It requires the live lead capability and active transport, a current target
|
|
@@ -3798,8 +3858,18 @@ required; per-provider validation does not implement the multi-provider coordina
|
|
|
3798
3858
|
|
|
3799
3859
|
### `requestBreak(request)`
|
|
3800
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
|
+
|
|
3801
3871
|
Requests permission to stop the agent later; it does not itself stop work. The provider continues
|
|
3802
|
-
offering work and reports `awaiting-
|
|
3872
|
+
offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
|
|
3803
3873
|
provider reports `not-requested` directly, with `decisionReason` when one was supplied.
|
|
3804
3874
|
|
|
3805
3875
|
**An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
|
|
@@ -3928,22 +3998,22 @@ Omni coordinates one break attempt as follows:
|
|
|
3928
3998
|
capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
3929
3999
|
available and work continues. `assertBreakAttemptProviders` holds a host to this set.
|
|
3930
4000
|
2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
|
|
3931
|
-
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
|
|
3932
4002
|
`granted`; neither state stops work. A denial transitions directly to `not-requested` and
|
|
3933
4003
|
causes Omni to take the cancel path.
|
|
3934
4004
|
4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
|
|
3935
4005
|
and send `commitBreak()` to each of them. A provider then stops offering new work
|
|
3936
|
-
and reports `starting-after-task` or `
|
|
4006
|
+
and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
|
|
3937
4007
|
decision, tunable per deployment — decides who is kept: a provider that has not applied the
|
|
3938
4008
|
commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
|
|
3939
|
-
begins without it. `
|
|
3940
|
-
reports `
|
|
4009
|
+
begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
|
|
4010
|
+
reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
|
|
3941
4011
|
and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
|
|
3942
4012
|
settled and beginning when the task ends, and offers no cancel, because the commit is durable.
|
|
3943
4013
|
5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
|
|
3944
4014
|
decision timeout, or the agent cancels before commit, durably choose cancel and enter
|
|
3945
4015
|
`cancelling-break`. Send `cancelBreak()` to every provider still reporting
|
|
3946
|
-
`awaiting-
|
|
4016
|
+
`awaiting-approval` or `granted`. Work continues during cancellation because no stop was
|
|
3947
4017
|
committed. Return to `working` only after no provider retains either state.
|
|
3948
4018
|
|
|
3949
4019
|
Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
|
|
@@ -3970,7 +4040,7 @@ Setting a provider aside is not a rollback and not a cancel. The commit stands a
|
|
|
3970
4040
|
obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
|
|
3971
4041
|
snapshot before anything else, and the snapshot decides:
|
|
3972
4042
|
|
|
3973
|
-
- `
|
|
4043
|
+
- `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
|
|
3974
4044
|
- still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
|
|
3975
4045
|
late behind it, the provider stops an agent who is already stopped; nothing happens, because a
|
|
3976
4046
|
commit is a state to be in, not an act to be done.
|
|
@@ -3983,7 +4053,7 @@ no capacity until it is reconciled.
|
|
|
3983
4053
|
|
|
3984
4054
|
**If the agent has already ended the break elsewhere, the break attempt is over** and the returning
|
|
3985
4055
|
provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
|
|
3986
|
-
would stop an agent who is working again; `starting-after-task` or `
|
|
4056
|
+
would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
|
|
3987
4057
|
Never rolling back is about a break that is still on, not one the agent has finished.
|
|
3988
4058
|
|
|
3989
4059
|
Omni may tell the agent which platforms the break has not yet reached, as it already does when a
|
|
@@ -4013,11 +4083,11 @@ asked for, but only until the session ends — after a reload or reconnect, or w
|
|
|
4013
4083
|
the agent on the break itself, the provider is the only one who knows.
|
|
4014
4084
|
|
|
4015
4085
|
Omit it when you cannot say, and when there is no break: reporting a reason alongside
|
|
4016
|
-
`
|
|
4086
|
+
`status: "not-requested"` describes a break that is not happening, and is rejected.
|
|
4017
4087
|
|
|
4018
4088
|
### `cancelBreak()`
|
|
4019
4089
|
|
|
4020
|
-
Cancels the active pre-commit request while its
|
|
4090
|
+
Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
|
|
4021
4091
|
Cancellation releases the request but does not restore work because work never stopped. If
|
|
4022
4092
|
commit already won, the provider returns `omni.break-already-committed`. The resulting state is
|
|
4023
4093
|
reported through `break-state`.
|
|
@@ -4026,7 +4096,7 @@ reported through `break-state`.
|
|
|
4026
4096
|
|
|
4027
4097
|
Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
|
|
4028
4098
|
business reason. On commit the provider stops offering new work and reports
|
|
4029
|
-
`starting-after-task` while existing work finishes, or `
|
|
4099
|
+
`starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
|
|
4030
4100
|
Committing a break that is already in effect changes nothing and answers `committed`.
|
|
4031
4101
|
|
|
4032
4102
|
### `endBreak()`
|
|
@@ -4107,16 +4177,16 @@ One method, `executeTeamBreak`, taking a discriminated command exactly as `execu
|
|
|
4107
4177
|
| Command | Effect |
|
|
4108
4178
|
| --- | --- |
|
|
4109
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`. |
|
|
4110
|
-
| `{ type: "policy", policy }` | `
|
|
4111
|
-
| `{ 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. |
|
|
4112
|
-
| `{ type: "end-forced-break", memberId: UserId }` |
|
|
4180
|
+
| `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
|
|
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. |
|
|
4113
4183
|
|
|
4114
4184
|
`memberId` is this provider's own identifier for the member, as published on its team member list. It is
|
|
4115
4185
|
never an identifier from another provider, and Omni does not translate between them; names come
|
|
4116
4186
|
from `describeUsers()`.
|
|
4117
4187
|
|
|
4118
|
-
`suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
4119
|
-
approve them. A provider that suspends
|
|
4188
|
+
`requests-suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
4189
|
+
approve them. A provider that suspends break requests must also publish `canRequestBreak: false` to the team's
|
|
4120
4190
|
agents so they see it before asking. A `force-break` must likewise reach that member as a `forced` break
|
|
4121
4191
|
on their own `BreakState`, or they are stopped from working with no way to see why.
|
|
4122
4192
|
|
|
@@ -4163,8 +4233,8 @@ they joined -- is answered `failed`, whatever their ceiling; the request stands
|
|
|
4163
4233
|
or until it is withdrawn or declined.
|
|
4164
4234
|
|
|
4165
4235
|
**A lead on a break does not join.** A break is a reported state in which the agent is not working,
|
|
4166
|
-
and a join is work. Omni offers Join to a lead only while their own `BreakState.
|
|
4167
|
-
`starting-after-task` nor `
|
|
4236
|
+
and a join is work. Omni offers Join to a lead only while their own `BreakState.status` is neither
|
|
4237
|
+
`starting-after-task` nor `on-break` -- a committed break waiting for the lead's current work to
|
|
4168
4238
|
finish is not given more -- and a provider answers a `join` from a lead on such a break `failed`.
|
|
4169
4239
|
The request stands for another lead, as it does when this one is already on a call.
|
|
4170
4240
|
|
|
@@ -4314,17 +4384,17 @@ about whether anybody has to act on it, and the difference is a lead's entire ac
|
|
|
4314
4384
|
|
|
4315
4385
|
| `break` | Means |
|
|
4316
4386
|
| --- | --- |
|
|
4317
|
-
| `awaiting-
|
|
4387
|
+
| `awaiting-approval` | Somebody has to decide. This is the lead's queue. |
|
|
4318
4388
|
| `granted` | Decided yes, but Omni has not told the provider to stop yet. Work continues and nobody needs to decide. |
|
|
4319
4389
|
| `starting-after-task` | Already granted; it begins when their current task ends. Nobody needs to act. |
|
|
4320
4390
|
|
|
4321
4391
|
Those three are the only values that appear here. `not-requested` is absence — omit `break`
|
|
4322
|
-
instead. `
|
|
4323
|
-
so neither survives to be reported. It is otherwise the same `
|
|
4392
|
+
instead. `on-break` is `availability: "on-break"`, and a denial transitions to `not-requested`,
|
|
4393
|
+
so neither survives to be reported. It is otherwise the same `BreakStatus` the member's own
|
|
4324
4394
|
break state uses, rather than a parallel vocabulary for the lead's view, so the two cannot drift
|
|
4325
4395
|
apart.
|
|
4326
4396
|
|
|
4327
|
-
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
|
|
4328
4398
|
but not started, and shows `starting-after-task` as settled.
|
|
4329
4399
|
|
|
4330
4400
|
**Live status, not a record.** The provider derives it from what is true now — not stored, not
|
|
@@ -4949,17 +5019,17 @@ covers a `diagnostic`: shown where the agent works, and counted.
|
|
|
4949
5019
|
|
|
4950
5020
|
### `break-state`
|
|
4951
5021
|
|
|
4952
|
-
Replaces this provider's complete `break` object. Its `
|
|
4953
|
-
`not-requested`, `awaiting-
|
|
4954
|
-
`
|
|
5022
|
+
Replaces this provider's complete `break` object. Its `status` uses the canonical
|
|
5023
|
+
`not-requested`, `awaiting-approval`, `granted`, `starting-after-task` and
|
|
5024
|
+
`on-break` states defined under Breaks; the event also carries the corresponding may-ask state,
|
|
4955
5025
|
reasons, retry details, and any forced break.
|
|
4956
5026
|
|
|
4957
5027
|
Each state is also held to the one before it. A commit's states, `starting-after-task` and
|
|
4958
|
-
`
|
|
5028
|
+
`on-break`, follow a grant — the one arrival in a committed state nobody asked for is a forced
|
|
4959
5029
|
break, which says so with `forced`: in effect at once, or `starting-after-task` while the member
|
|
4960
5030
|
finishes the call they are on (`stream.breakState.commitBeforeGrant`); and a break never moves backwards —
|
|
4961
|
-
from `
|
|
4962
|
-
`awaiting-
|
|
5031
|
+
from `on-break` or `starting-after-task` to a grant or a request, or from `granted` to
|
|
5032
|
+
`awaiting-approval` — a new request passes through `not-requested` (`stream.breakState.backwards`).
|
|
4963
5033
|
`exerciseAdapter` holds the stream to that from the connect snapshot on;
|
|
4964
5034
|
`assertBreakFollowsItsRequests` holds any sequence.
|
|
4965
5035
|
|
|
@@ -4981,14 +5051,14 @@ provider rechecks current permission and state atomically before acting. The pro
|
|
|
4981
5051
|
the resulting break/task state before publication and serializes those publications. The host
|
|
4982
5052
|
validates the envelope, transition and full task consistency before replacing local state. A
|
|
4983
5053
|
request result does not optimistically advance break state, and a delayed command cannot apply
|
|
4984
|
-
to a later attempt merely because its
|
|
5054
|
+
to a later attempt merely because its status happens to look compatible. Keep at most one
|
|
4985
5055
|
unresolved operation per provider/attempt; recovery must resolve uncertainty before another
|
|
4986
5056
|
operation is dispatched. The source must fence delayed operations against its own attempt state.
|
|
4987
5057
|
|
|
4988
|
-
Normal order is `not-requested` → `awaiting-
|
|
4989
|
-
`starting-after-task` → `
|
|
4990
|
-
`granted`; a commit with no outstanding work may go directly to `
|
|
4991
|
-
returns a precommit request to `not-requested`; an
|
|
5058
|
+
Normal order is `not-requested` → `awaiting-approval` → `granted` →
|
|
5059
|
+
`starting-after-task` → `on-break` → `not-requested`. Auto-approval may go directly to
|
|
5060
|
+
`granted`; a commit with no outstanding work may go directly to `on-break`. Denial/cancel
|
|
5061
|
+
returns a precommit request to `not-requested`; an explicit agent end returns a committed
|
|
4992
5062
|
break there. Same-state restatements are allowed. An evidenced forced break is the explicit
|
|
4993
5063
|
exception to requesting/granting, and must carry its forced actor/state. Neither skipped
|
|
4994
5064
|
publication nor a host-local guess creates another exception. A later normal attempt starts
|
|
@@ -4998,7 +5068,7 @@ A fresh authoritative snapshot establishes a new baseline; do not run this event
|
|
|
4998
5068
|
check across it. A snapshot may legitimately establish a later request or an already active
|
|
4999
5069
|
break after reconnect. Its freshness must be established by the source/adapter's ordered
|
|
5000
5070
|
snapshot/publication boundary and fencing of obsolete callbacks and reads. A delayed old
|
|
5001
|
-
snapshot is not detectable from break
|
|
5071
|
+
snapshot is not detectable from break status alone. Neither envelope time nor event ID
|
|
5002
5072
|
supplies a break revision. Do not suppress all requests after an active break: a later attempt
|
|
5003
5073
|
passes through `not-requested`, and then may request again. Do not replay earlier ProviderEvents
|
|
5004
5074
|
after recovery. Raw backend break handlers must preserve source order too; this helper does
|
|
@@ -5008,7 +5078,7 @@ not implement or assume invocation of Protocol's two-phase request/commit coordi
|
|
|
5008
5078
|
For a multi-provider break attempt, "every provider" is the set of providers frozen when the
|
|
5009
5079
|
attempt entered `requesting-break`. Omni commits only after every asked provider reports `granted` —
|
|
5010
5080
|
that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
|
|
5011
|
-
`on-break` once every kept provider reports `
|
|
5081
|
+
`on-break` once every kept provider reports `on-break`; the commit bound decides who is kept,
|
|
5012
5082
|
setting aside a provider that has not applied the commit rather than holding a break that has
|
|
5013
5083
|
already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
|
|
5014
5084
|
multi-provider break**.
|
|
@@ -5391,7 +5461,7 @@ cannot be established from TypeScript structure alone.
|
|
|
5391
5461
|
| `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. |
|
|
5392
5462
|
| `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. |
|
|
5393
5463
|
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
5394
|
-
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `
|
|
5464
|
+
| `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. |
|
|
5395
5465
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
5396
5466
|
| `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. |
|
|
5397
5467
|
| `assertBrowserSessionIsolation(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
|