@xema/omni-protocol 0.1.85 → 0.1.87
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/README.md +3 -3
- package/dist/index.d.ts +12 -12
- package/dist/index.js +1 -1
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +7 -7
- package/dist/validation.d.ts +5 -5
- package/dist/validation.js +36 -36
- package/guide.md +79 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,9 +42,9 @@ assertNoViolations(violations);
|
|
|
42
42
|
A violation carries a stable `rule` id such as `task.browser.url.scheme`, the `path` it was found
|
|
43
43
|
at such as `snapshot.tasks[0].browsers[1].url`, and a `message`.
|
|
44
44
|
|
|
45
|
-
Some rules need more than the object in hand. A
|
|
45
|
+
Some rules need more than the object in hand. A team member list never carries the agent it is published to,
|
|
46
46
|
a lead's snapshot always carries one and nobody else's ever does — and a validator cannot know who
|
|
47
|
-
is reading, or what their login declares, from the snapshot alone. `
|
|
47
|
+
is reading, or what their login declares, from the snapshot alone. `validateTeamMembers`,
|
|
48
48
|
`validateSnapshot`, and `validateEventEnvelope` each take an optional final `{ self, capabilities }`
|
|
49
49
|
from the `authenticated` state; given them, they report `team.member.self`, `team.request.self`,
|
|
50
50
|
`team.required`, `team.unentitled`, `team.requests.capability`, and `team.requests.required`.
|
|
@@ -61,7 +61,7 @@ validateSnapshot(snapshot, manifest, "snapshot", { self: identity.id, capabiliti
|
|
|
61
61
|
required capability methods, subscribes, validates the snapshot, every delivered event, and every
|
|
62
62
|
authentication state published during the run — against the latest login — states a capacity,
|
|
63
63
|
then unsubscribes and disconnects. `result.notExercised` names what the run never reached — each
|
|
64
|
-
optional part of a task, of the break state and
|
|
64
|
+
optional part of a task, of the break state and team member list, each contribution, each event type — so a
|
|
65
65
|
clean result is read for what it covers and not for the whole contract; `assertReached(result,
|
|
66
66
|
subjects)` is the paired assertion.
|
|
67
67
|
|
package/dist/index.d.ts
CHANGED
|
@@ -245,7 +245,7 @@ export interface TeamCapabilities {
|
|
|
245
245
|
leadAssistControl?: true;
|
|
246
246
|
/**
|
|
247
247
|
* This lead may listen to a member's call unasked, in these modes and no others. Whisper and
|
|
248
|
-
*
|
|
248
|
+
* join-call begin from a monitor, so the list always includes `monitor`. Nothing of it reaches the
|
|
249
249
|
* member's task. Requires `executeTeamMonitor`.
|
|
250
250
|
*/
|
|
251
251
|
monitorControl?: MonitorMode[];
|
|
@@ -254,10 +254,10 @@ export interface TeamCapabilities {
|
|
|
254
254
|
}
|
|
255
255
|
/**
|
|
256
256
|
* How a lead listening to a member's call is heard: `monitor` hears both sides in silence,
|
|
257
|
-
* `whisper` is heard by the agent alone, `
|
|
257
|
+
* `whisper` is heard by the agent alone, `join-call` by everyone on the call.
|
|
258
258
|
*/
|
|
259
|
-
export type MonitorMode = "monitor" | "whisper" | "
|
|
260
|
-
export declare const MONITOR_MODES: readonly ["monitor", "whisper", "
|
|
259
|
+
export type MonitorMode = "monitor" | "whisper" | "join-call";
|
|
260
|
+
export declare const MONITOR_MODES: readonly ["monitor", "whisper", "join-call"];
|
|
261
261
|
/**
|
|
262
262
|
* What this login may do, beyond any one task. It travels with the identity because it is part
|
|
263
263
|
* of who the agent is on this provider: the provider knows the roles and says so at sign-in.
|
|
@@ -268,7 +268,7 @@ export interface UserCapabilities {
|
|
|
268
268
|
breaks?: true;
|
|
269
269
|
/** The choices the team left to this person, with where each stands. Omitted when there are none. Requires `setPreference`. */
|
|
270
270
|
preferences?: AgentPreference[];
|
|
271
|
-
/** This login leads a team: a `
|
|
271
|
+
/** This login leads a team: a `TeamMembers` object is published to it on every snapshot, including `members: []`. */
|
|
272
272
|
team?: TeamCapabilities;
|
|
273
273
|
}
|
|
274
274
|
/**
|
|
@@ -1336,7 +1336,7 @@ export interface LeadRequest {
|
|
|
1336
1336
|
* The login is the permission: published to a login that declares `capabilities.team`, on every
|
|
1337
1337
|
* snapshot, and to nobody else. What the lead may do with it is on the login too, not here.
|
|
1338
1338
|
*/
|
|
1339
|
-
export interface
|
|
1339
|
+
export interface TeamMembers {
|
|
1340
1340
|
members: TeamMember[];
|
|
1341
1341
|
/** Omitted when the login lacks `team.leadAssistControl`; `[]` when nobody is asking. */
|
|
1342
1342
|
requests?: LeadRequest[];
|
|
@@ -1372,8 +1372,8 @@ export interface TeamLeadAssistCommandRequest {
|
|
|
1372
1372
|
command: TeamLeadAssistCommand;
|
|
1373
1373
|
}
|
|
1374
1374
|
/**
|
|
1375
|
-
* A lead listening to a member's call. `monitor` starts one, silent, on a member from the
|
|
1376
|
-
* `whisper` and `
|
|
1375
|
+
* A lead listening to a member's call. `monitor` starts one, silent, on a member from the team member list;
|
|
1376
|
+
* `whisper` and `join-call` change how the standing one is heard; `leave` ends it. One member at a
|
|
1377
1377
|
* time, and only the modes the login's `monitorControl` lists.
|
|
1378
1378
|
*/
|
|
1379
1379
|
export type TeamMonitorCommand = {
|
|
@@ -1382,7 +1382,7 @@ export type TeamMonitorCommand = {
|
|
|
1382
1382
|
} | {
|
|
1383
1383
|
type: "whisper";
|
|
1384
1384
|
} | {
|
|
1385
|
-
type: "
|
|
1385
|
+
type: "join-call";
|
|
1386
1386
|
} | {
|
|
1387
1387
|
type: "leave";
|
|
1388
1388
|
};
|
|
@@ -1530,7 +1530,7 @@ export interface Snapshot<C extends Channel = Channel> {
|
|
|
1530
1530
|
taskCount: number;
|
|
1531
1531
|
contacts?: Contact[];
|
|
1532
1532
|
scheduledActivities?: ScheduledActivity[];
|
|
1533
|
-
team?:
|
|
1533
|
+
team?: TeamMembers;
|
|
1534
1534
|
}
|
|
1535
1535
|
/**
|
|
1536
1536
|
* How many tasks this provider may have allocated to the agent at once.
|
|
@@ -1627,7 +1627,7 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1627
1627
|
allocationId?: AllocationId;
|
|
1628
1628
|
} | {
|
|
1629
1629
|
type: "team-updated";
|
|
1630
|
-
team:
|
|
1630
|
+
team: TeamMembers;
|
|
1631
1631
|
} | {
|
|
1632
1632
|
type: "contacts-updated";
|
|
1633
1633
|
contacts: Contact[];
|
|
@@ -1692,7 +1692,7 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
1692
1692
|
setCapacity(capacity: AgentCapacity): Promise<CapacityResult>;
|
|
1693
1693
|
execute(request: TaskCommandRequest<C>): Promise<TaskCommandResult>;
|
|
1694
1694
|
disconnect(): Promise<void>;
|
|
1695
|
-
/** Required of any adapter publishing a `UserId` -- on an imposed break, a
|
|
1695
|
+
/** Required of any adapter publishing a `UserId` -- on an imposed break, a team member list, or history. */
|
|
1696
1696
|
describeUsers?(ids: UserId[]): Promise<User[]>;
|
|
1697
1697
|
/** Required when the manifest declares `idleCapabilities.dial`. */
|
|
1698
1698
|
dial?(request: DialRequest): Promise<DialResult>;
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export const IDLE_CAPABILITY_UI = {
|
|
|
26
26
|
calendar: "Calendar",
|
|
27
27
|
contacts: "Contacts",
|
|
28
28
|
};
|
|
29
|
-
export const MONITOR_MODES = ["monitor", "whisper", "
|
|
29
|
+
export const MONITOR_MODES = ["monitor", "whisper", "join-call"];
|
|
30
30
|
export const MUTED_BY = ["host", "station"];
|
|
31
31
|
export const HOST_MUTES = ["stream", "station"];
|
|
32
32
|
export const RECORDING_ACTIONS = ["start", "pause", "resume", "stop", "cancel"];
|
package/dist/testing.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
|
|
|
4
4
|
/**
|
|
5
5
|
* A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
|
|
6
6
|
* a fixture without it exercises none of its rules and passes clean. One subject per family of
|
|
7
|
-
* rules -- each optional part of a task, each optional part of the break state and
|
|
7
|
+
* rules -- each optional part of a task, each optional part of the break state and team member list, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
10
|
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.interactionHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.monitoring", "task.media", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
|
|
@@ -89,7 +89,7 @@ export declare function assertAuthenticationRestoreAndExpiry(states: readonly Au
|
|
|
89
89
|
* session published, first to last: beginning and ending `authenticated` for the same identity,
|
|
90
90
|
* passing only through usable states (`expired` or `signed-out` ends the login instead), with at
|
|
91
91
|
* least one capability gone by the end. `snapshot` is the first snapshot published after the last
|
|
92
|
-
* state, validated against that login -- so a
|
|
92
|
+
* state, validated against that login -- so a team member list still published to a login that no longer
|
|
93
93
|
* leads, or requests to one that may no longer join, is the failure.
|
|
94
94
|
*/
|
|
95
95
|
export declare function assertCapabilityWithdrawal(states: readonly AuthenticationState[], snapshot: Snapshot, manifest: Manifest): void;
|
|
@@ -121,7 +121,7 @@ export declare function assertCommandRefusedAfterWithdrawal(result: {
|
|
|
121
121
|
export declare function assertTaskCapabilityWithdrawal(tasks: readonly Task[], manifest: Manifest, command: Record<string, unknown>): void;
|
|
122
122
|
/**
|
|
123
123
|
* Throws unless the run reached every subject named: the paired assertion beside a clean result,
|
|
124
|
-
* so a fixture that never produced a
|
|
124
|
+
* so a fixture that never produced a team member list cannot pass a test that meant to check one.
|
|
125
125
|
*/
|
|
126
126
|
export declare function assertReached(result: AdapterContractResult, subjects: readonly ContractSubject[]): void;
|
|
127
127
|
/** Validates duplicate delivery and returns the event sequence Omni applies once per ID. */
|
package/dist/testing.js
CHANGED
|
@@ -4,7 +4,7 @@ export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
|
|
6
6
|
* a fixture without it exercises none of its rules and passes clean. One subject per family of
|
|
7
|
-
* rules -- each optional part of a task, each optional part of the break state and
|
|
7
|
+
* rules -- each optional part of a task, each optional part of the break state and team member list, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
10
|
const STATE_SUBJECTS = [
|
|
@@ -188,7 +188,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
188
188
|
throw new Error(`Adapter contract exercise requires authenticated test state, received ${authenticationState.status}`);
|
|
189
189
|
}
|
|
190
190
|
// The harness knows who is signed in and what their login declares, so it can hold the
|
|
191
|
-
// adapter to rules the structural validators cannot check alone: a
|
|
191
|
+
// adapter to rules the structural validators cannot check alone: a team member list never carries the
|
|
192
192
|
// agent it is published to, a lead's snapshot always carries one, nobody else's ever does.
|
|
193
193
|
// Capabilities are current, not fixed, so the login is read when something is validated,
|
|
194
194
|
// never captured at sign-in: a provider that withdraws one republishes `authenticated`, and
|
|
@@ -460,7 +460,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
460
460
|
if (publishesUserIds(read)) {
|
|
461
461
|
requireMethod(source, "describeUsers", "the snapshot publishes a UserId");
|
|
462
462
|
// Required by presence is not enough: the names the snapshot published are looked up, and what
|
|
463
|
-
// comes back is held to the shape -- an empty answer to a
|
|
463
|
+
// comes back is held to the shape -- an empty answer to a team member list of colleagues is named.
|
|
464
464
|
const named = userIdsIn(read);
|
|
465
465
|
if (typeof source.describeUsers === "function" && named.length > 0) {
|
|
466
466
|
let described;
|
|
@@ -644,7 +644,7 @@ function publishesUserIds(snapshot) {
|
|
|
644
644
|
return false;
|
|
645
645
|
return snapshot.tasks.some(taskNamesUsers);
|
|
646
646
|
}
|
|
647
|
-
/** Every UserId a snapshot publishes: on the record, the room, a lead request, an imposed break, the
|
|
647
|
+
/** Every UserId a snapshot publishes: on the record, the room, a lead request, an imposed break, the team member list. */
|
|
648
648
|
function userIdsIn(snapshot) {
|
|
649
649
|
const ids = new Set();
|
|
650
650
|
const add = (value) => { if (typeof value === "string" && value.length > 0)
|
|
@@ -685,7 +685,7 @@ const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.interactionHi
|
|
|
685
685
|
(isRecord(task.leadAssist) && task.leadAssist.leadId !== undefined) ||
|
|
686
686
|
isRecord(task.assisting) ||
|
|
687
687
|
isRecord(task.monitoring));
|
|
688
|
-
/** Whether an event publishes a `UserId`, on a
|
|
688
|
+
/** Whether an event publishes a `UserId`, on a team member list, a task, or the snapshot a reconnect carries. */
|
|
689
689
|
function eventNamesUsers(envelope) {
|
|
690
690
|
const event = isRecord(envelope) ? envelope.event : undefined;
|
|
691
691
|
if (!isRecord(event))
|
|
@@ -795,7 +795,7 @@ function undeterminedTasks(tasks, path) {
|
|
|
795
795
|
* session published, first to last: beginning and ending `authenticated` for the same identity,
|
|
796
796
|
* passing only through usable states (`expired` or `signed-out` ends the login instead), with at
|
|
797
797
|
* least one capability gone by the end. `snapshot` is the first snapshot published after the last
|
|
798
|
-
* state, validated against that login -- so a
|
|
798
|
+
* state, validated against that login -- so a team member list still published to a login that no longer
|
|
799
799
|
* leads, or requests to one that may no longer join, is the failure.
|
|
800
800
|
*/
|
|
801
801
|
export function assertCapabilityWithdrawal(states, snapshot, manifest) {
|
|
@@ -890,7 +890,7 @@ export function assertTaskCapabilityWithdrawal(tasks, manifest, command) {
|
|
|
890
890
|
}
|
|
891
891
|
/**
|
|
892
892
|
* Throws unless the run reached every subject named: the paired assertion beside a clean result,
|
|
893
|
-
* so a fixture that never produced a
|
|
893
|
+
* so a fixture that never produced a team member list cannot pass a test that meant to check one.
|
|
894
894
|
*/
|
|
895
895
|
export function assertReached(result, subjects) {
|
|
896
896
|
const missed = subjects.filter(subject => result.notExercised.includes(subject));
|
package/dist/validation.d.ts
CHANGED
|
@@ -50,9 +50,9 @@ export declare function validateTask(task: unknown, context: TaskValidationConte
|
|
|
50
50
|
/** Checks break status against the full currently retained task set, after a transaction. */
|
|
51
51
|
export declare function validateBreakStatus(state: unknown, tasks: unknown, path?: string): ProtocolViolation[];
|
|
52
52
|
/**
|
|
53
|
-
* Checks lead break dispatch using current authentication, transport,
|
|
53
|
+
* Checks lead break dispatch using current authentication, transport, team member list (`team`) and,
|
|
54
54
|
* for place/release, the target's full `memberBreak`. The provider must authorize the target
|
|
55
|
-
* and recheck the decision atomically; a
|
|
55
|
+
* and recheck the decision atomically; a team member list is not authority to act after it has changed.
|
|
56
56
|
*/
|
|
57
57
|
export declare function validateTeamBreakCommand(request: unknown, context: unknown, path?: string): ProtocolViolation[];
|
|
58
58
|
/** The four agent break methods; this is a validation API, not a wire command. */
|
|
@@ -78,12 +78,12 @@ export declare function validateBreakTransition(before: unknown, after: unknown,
|
|
|
78
78
|
export interface ReaderContext {
|
|
79
79
|
/**
|
|
80
80
|
* The signed-in agent, `AuthenticationState.identity.id`. A lead does not report to themself:
|
|
81
|
-
* a
|
|
81
|
+
* a team member list that lists them in `members`, or their own ask in `requests`, is a violation.
|
|
82
82
|
*/
|
|
83
83
|
self?: UserId;
|
|
84
84
|
/**
|
|
85
85
|
* The login's `AuthenticationState.capabilities`. The login is the permission: a lead's
|
|
86
|
-
* snapshot carries a
|
|
86
|
+
* snapshot carries a team member list, nobody else's does, and `requests` need `team.leadAssistControl`.
|
|
87
87
|
*/
|
|
88
88
|
capabilities?: UserCapabilities;
|
|
89
89
|
/** The level ids in force. Filled from the manifest by `validateSnapshot` and `validateEventEnvelope`; the defaults otherwise. */
|
|
@@ -99,7 +99,7 @@ export interface ReaderContext {
|
|
|
99
99
|
/** `ConnectContext.autoAcceptTasks` as sent: whether a pending task states its `acceptance`. Unknown to a caller without the context, and then unchecked. */
|
|
100
100
|
autoAcceptTasks?: boolean;
|
|
101
101
|
}
|
|
102
|
-
export declare function
|
|
102
|
+
export declare function validateTeamMembers(teamMembers: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
103
103
|
export declare function validateSnapshot(snapshot: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
104
104
|
export declare function validateEventEnvelope(envelope: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
105
105
|
/**
|
package/dist/validation.js
CHANGED
|
@@ -88,7 +88,7 @@ const OFFERABLE_PHASES = membersOf({
|
|
|
88
88
|
const PREVIEW_DEADLINES = membersOf({ calls: true, "host-calls": true, waits: true });
|
|
89
89
|
const PHONES = membersOf({ softphone: true, deskPhone: true });
|
|
90
90
|
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true, monitorControl: true });
|
|
91
|
-
const MONITOR_MODES = membersOf({ monitor: true, whisper: true,
|
|
91
|
+
const MONITOR_MODES = membersOf({ monitor: true, whisper: true, "join-call": true });
|
|
92
92
|
const COMPLETED_BY = membersOf({ agent: true, provider: true });
|
|
93
93
|
const CANCELLED_BY = membersOf({ agent: true, provider: true, party: true });
|
|
94
94
|
const EXPIRABLE_PHASES = membersOf({
|
|
@@ -1348,9 +1348,9 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
|
|
|
1348
1348
|
return into.violations;
|
|
1349
1349
|
}
|
|
1350
1350
|
/**
|
|
1351
|
-
* Checks lead break dispatch using current authentication, transport,
|
|
1351
|
+
* Checks lead break dispatch using current authentication, transport, team member list (`team`) and,
|
|
1352
1352
|
* for place/release, the target's full `memberBreak`. The provider must authorize the target
|
|
1353
|
-
* and recheck the decision atomically; a
|
|
1353
|
+
* and recheck the decision atomically; a team member list is not authority to act after it has changed.
|
|
1354
1354
|
*/
|
|
1355
1355
|
export function validateTeamBreakCommand(request, context, path = "teamBreakCommand") {
|
|
1356
1356
|
const into = new Collector();
|
|
@@ -1388,7 +1388,7 @@ export function validateTeamBreakCommand(request, context, path = "teamBreakComm
|
|
|
1388
1388
|
const members = Array.isArray(team.members) ? team.members : [];
|
|
1389
1389
|
const member = members.find((m) => isPlainObject(m) && m.id === command.memberId);
|
|
1390
1390
|
const self = isPlainObject(auth.identity) ? auth.identity.id : undefined;
|
|
1391
|
-
into.require(isPlainObject(member) && command.memberId !== self, "team.break.command.member", path, "the target must be another member of the current authorized
|
|
1391
|
+
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");
|
|
1392
1392
|
if (command.type === "decide") {
|
|
1393
1393
|
into.require(command.decision === "granted" || command.decision === "denied", "team.break.command.decision", path, "decide granted or denied");
|
|
1394
1394
|
into.require(isPlainObject(member) && member.break === "awaiting-decision", "team.break.command.awaiting", path, "decide only a currently awaiting-decision request");
|
|
@@ -1562,38 +1562,38 @@ function validateBreakState(value, path, into) {
|
|
|
1562
1562
|
into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${describeValue(value.approval)} on a provider that publishes reasons names the one it is on`);
|
|
1563
1563
|
}
|
|
1564
1564
|
}
|
|
1565
|
-
export function
|
|
1565
|
+
export function validateTeamMembers(teamMembers, path = "team", context = {}) {
|
|
1566
1566
|
const into = new Collector();
|
|
1567
|
-
|
|
1567
|
+
validateTeamMembersInto(teamMembers, path, context, into);
|
|
1568
1568
|
return into.violations;
|
|
1569
1569
|
}
|
|
1570
|
-
function
|
|
1571
|
-
if (!isPlainObject(
|
|
1572
|
-
into.add("team.shape", path, "a team
|
|
1570
|
+
function validateTeamMembersInto(teamMembers, path, context, into) {
|
|
1571
|
+
if (!isPlainObject(teamMembers)) {
|
|
1572
|
+
into.add("team.shape", path, "a team member list must be an object");
|
|
1573
1573
|
return;
|
|
1574
1574
|
}
|
|
1575
|
-
// The login is the permission: a
|
|
1575
|
+
// The login is the permission: a team member list reaches a login that declares `capabilities.team` and
|
|
1576
1576
|
// nobody else. Only a caller holding the login can check it.
|
|
1577
1577
|
if (context.capabilities !== undefined && context.capabilities.team === undefined) {
|
|
1578
|
-
into.add("team.unentitled", path, "a
|
|
1578
|
+
into.add("team.unentitled", path, "a team member list published to a login that does not declare capabilities.team: the login is the permission");
|
|
1579
1579
|
}
|
|
1580
|
-
// The team's policies travel with the
|
|
1580
|
+
// The team's policies travel with the team member list exactly when the login may set them.
|
|
1581
1581
|
if (context.capabilities !== undefined) {
|
|
1582
1582
|
const may = context.capabilities.team?.policyControl === true;
|
|
1583
|
-
if (may &&
|
|
1584
|
-
into.add("team.policies.required", `${path}.policies`, "the login declares team.policyControl, so the
|
|
1583
|
+
if (may && teamMembers.policies === undefined) {
|
|
1584
|
+
into.add("team.policies.required", `${path}.policies`, "the login declares team.policyControl, so the team member list carries the team's policies");
|
|
1585
1585
|
}
|
|
1586
|
-
if (!may &&
|
|
1586
|
+
if (!may && teamMembers.policies !== undefined) {
|
|
1587
1587
|
into.add("team.policies.capability", `${path}.policies`, "policies require team.policyControl on the login: a lead who may not set them has nothing to see");
|
|
1588
1588
|
}
|
|
1589
1589
|
}
|
|
1590
|
-
if (
|
|
1591
|
-
validateTeamPoliciesInto(
|
|
1592
|
-
if (
|
|
1590
|
+
if (teamMembers.policies !== undefined)
|
|
1591
|
+
validateTeamPoliciesInto(teamMembers.policies, `${path}.policies`, into, context.levels);
|
|
1592
|
+
if (teamMembers.requests === undefined) {
|
|
1593
1593
|
// `[]` says nobody is asking; omission says the lead may not be asked. A login that may be
|
|
1594
1594
|
// asked therefore always carries the list.
|
|
1595
1595
|
if (context.capabilities?.team?.leadAssistControl === true) {
|
|
1596
|
-
into.add("team.requests.required", `${path}.requests`, "the login declares team.leadAssistControl, so the
|
|
1596
|
+
into.add("team.requests.required", `${path}.requests`, "the login declares team.leadAssistControl, so the team member list carries requests: [] when nobody is asking");
|
|
1597
1597
|
}
|
|
1598
1598
|
}
|
|
1599
1599
|
else {
|
|
@@ -1602,12 +1602,12 @@ function validateTeamRosterInto(roster, path, context, into) {
|
|
|
1602
1602
|
if (context.capabilities !== undefined) {
|
|
1603
1603
|
into.require(context.capabilities.team?.leadAssistControl === true, "team.requests.capability", `${path}.requests`, "requests require team.leadAssistControl on the login: a lead who may not join has nothing to decide");
|
|
1604
1604
|
}
|
|
1605
|
-
if (!Array.isArray(
|
|
1605
|
+
if (!Array.isArray(teamMembers.requests)) {
|
|
1606
1606
|
into.add("team.requests.shape", `${path}.requests`, "requests must be an array when present");
|
|
1607
1607
|
}
|
|
1608
1608
|
else {
|
|
1609
1609
|
const seenRequests = new Set();
|
|
1610
|
-
|
|
1610
|
+
teamMembers.requests.forEach((request, index) => {
|
|
1611
1611
|
const at = `${path}.requests[${index}]`;
|
|
1612
1612
|
if (!isPlainObject(request)) {
|
|
1613
1613
|
into.add("team.request.shape", at, "each request must be an object");
|
|
@@ -1619,7 +1619,7 @@ function validateTeamRosterInto(roster, path, context, into) {
|
|
|
1619
1619
|
seenRequests.add(request.id);
|
|
1620
1620
|
}
|
|
1621
1621
|
if (into.require(isUserId(request.memberId), "team.request.memberId", `${at}.memberId`, "a request names the member asking")) {
|
|
1622
|
-
into.require(request.memberId !== context.self, "team.request.self", `${at}.memberId`, "the
|
|
1622
|
+
into.require(request.memberId !== context.self, "team.request.self", `${at}.memberId`, "the team member list carries the reader's own ask: an agent's request for a lead goes to whoever leads them");
|
|
1623
1623
|
}
|
|
1624
1624
|
into.require(isTaskId(request.taskId), "team.request.taskId", `${at}.taskId`, "a request names the task the lead would join");
|
|
1625
1625
|
into.require(isAllocationId(request.allocationId), "team.request.allocationId", `${at}.allocationId`, "a request names the task by its allocation too");
|
|
@@ -1629,22 +1629,22 @@ function validateTeamRosterInto(roster, path, context, into) {
|
|
|
1629
1629
|
});
|
|
1630
1630
|
}
|
|
1631
1631
|
}
|
|
1632
|
-
if (!Array.isArray(
|
|
1633
|
-
into.add("team.members.shape", `${path}.members`, "a
|
|
1632
|
+
if (!Array.isArray(teamMembers.members)) {
|
|
1633
|
+
into.add("team.members.shape", `${path}.members`, "a team member list must carry a members array");
|
|
1634
1634
|
return;
|
|
1635
1635
|
}
|
|
1636
1636
|
const seen = new Set();
|
|
1637
|
-
|
|
1637
|
+
teamMembers.members.forEach((member, index) => {
|
|
1638
1638
|
const at = `${path}.members[${index}]`;
|
|
1639
1639
|
if (!isPlainObject(member)) {
|
|
1640
|
-
into.add("team.member.shape", at, "each
|
|
1640
|
+
into.add("team.member.shape", at, "each team member must be an object");
|
|
1641
1641
|
return;
|
|
1642
1642
|
}
|
|
1643
|
-
if (into.require(isUserId(member.id), "team.member.id", `${at}.id`, "a
|
|
1643
|
+
if (into.require(isUserId(member.id), "team.member.id", `${at}.id`, "a team member needs a user id")) {
|
|
1644
1644
|
if (seen.has(member.id))
|
|
1645
|
-
into.add("team.member.unique", `${at}.id`, `duplicate
|
|
1645
|
+
into.add("team.member.unique", `${at}.id`, `duplicate team member: ${member.id}`);
|
|
1646
1646
|
seen.add(member.id);
|
|
1647
|
-
into.require(member.id !== context.self, "team.member.self", `${at}.id`, "the
|
|
1647
|
+
into.require(member.id !== context.self, "team.member.self", `${at}.id`, "the team member list carries the agent it is published to: a lead does not report to themself");
|
|
1648
1648
|
}
|
|
1649
1649
|
into.oneOf(member.availability, TEAM_AVAILABILITIES, "team.member.availability", `${at}.availability`);
|
|
1650
1650
|
if (member.since !== undefined)
|
|
@@ -1752,13 +1752,13 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
1752
1752
|
into.add("snapshot.calendar.shape", `${path}.scheduledActivities`, "scheduledActivities must be an array when present");
|
|
1753
1753
|
}
|
|
1754
1754
|
}
|
|
1755
|
-
// The login is the permission: a lead's snapshot carries a
|
|
1756
|
-
// direction -- a
|
|
1755
|
+
// The login is the permission: a lead's snapshot carries a team member list, `[]` included. The other
|
|
1756
|
+
// direction -- a team member list to a login that does not lead -- is the team member list's own rule.
|
|
1757
1757
|
if (context.capabilities?.team !== undefined && snapshot.team === undefined) {
|
|
1758
|
-
into.add("team.required", `${path}.team`, "the login declares capabilities.team, so every snapshot carries a
|
|
1758
|
+
into.add("team.required", `${path}.team`, "the login declares capabilities.team, so every snapshot carries a team member list: [] when nobody is in it");
|
|
1759
1759
|
}
|
|
1760
1760
|
if (snapshot.team !== undefined)
|
|
1761
|
-
|
|
1761
|
+
validateTeamMembersInto(snapshot.team, `${path}.team`, { ...context, levels }, into);
|
|
1762
1762
|
return into.violations;
|
|
1763
1763
|
}
|
|
1764
1764
|
// ---------------------------------------------------------------------------
|
|
@@ -1949,7 +1949,7 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1949
1949
|
validateQueueSummary(event.summary, `${at}.summary`, into);
|
|
1950
1950
|
break;
|
|
1951
1951
|
case "team-updated":
|
|
1952
|
-
|
|
1952
|
+
validateTeamMembersInto(event.team, `${at}.team`, { ...context, levels }, into);
|
|
1953
1953
|
break;
|
|
1954
1954
|
case "contacts-updated":
|
|
1955
1955
|
into.require(idle.contacts === true, "event.contacts.capability", `${at}.contacts`, "contacts-updated requires the contacts idle capability");
|
|
@@ -2716,7 +2716,7 @@ function validateUserCapabilitiesInto(value, path, into, levels) {
|
|
|
2716
2716
|
}
|
|
2717
2717
|
}
|
|
2718
2718
|
/**
|
|
2719
|
-
* The modes a lead may listen in, as a list: whisper and
|
|
2719
|
+
* The modes a lead may listen in, as a list: whisper and join-call begin from a monitor, so a list
|
|
2720
2720
|
* without `monitor` names modes the lead could never reach.
|
|
2721
2721
|
*/
|
|
2722
2722
|
function validateMonitorControl(value, path, into) {
|
|
@@ -2729,7 +2729,7 @@ function validateMonitorControl(value, path, into) {
|
|
|
2729
2729
|
into.add("authentication.capability.team.monitorControl.unique", `${path}[${index}]`, `duplicate monitor mode: ${describeValue(mode)}`);
|
|
2730
2730
|
}
|
|
2731
2731
|
});
|
|
2732
|
-
into.require(value.includes("monitor"), "authentication.capability.team.monitorControl.monitor", path, "whisper and
|
|
2732
|
+
into.require(value.includes("monitor"), "authentication.capability.team.monitorControl.monitor", path, "whisper and join-call begin from a monitor: a lead who may listen in any mode may monitor");
|
|
2733
2733
|
}
|
|
2734
2734
|
/**
|
|
2735
2735
|
* A failure an authentication reports: on a `rejected` start or completion, or on an `expired`
|
package/guide.md
CHANGED
|
@@ -23,7 +23,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
23
23
|
| **Provider** | One independently connected external system: a voice platform, a chat platform, a mail platform. |
|
|
24
24
|
| **Adapter** | The package implementing this contract for one provider. One adapter is one provider, so the words are often interchangeable; *provider* names the system, *adapter* the code speaking for it. |
|
|
25
25
|
| **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; transfer destinations may include an agent when the provider publishes that directory item. |
|
|
26
|
-
| **Lead** | An agent whose login declares `capabilities.team`. The provider publishes a `
|
|
26
|
+
| **Lead** | An agent whose login declares `capabilities.team`. The provider publishes a `TeamMembers` object to them and to nobody else: **the login is the permission**. |
|
|
27
27
|
| **Provisioning** | Omni-side policy about this agent, configured outside the protocol and never sent to a provider. It gates whether an offer may be rejected, whether the agent goes ready on login, and whether tasks are auto-accepted. Where a capability and provisioning disagree, the stricter wins. |
|
|
28
28
|
| **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. |
|
|
29
29
|
| **Interaction** | One agent’s part in the call or conversation. The same call may return to that agent for a new interaction. |
|
|
@@ -38,9 +38,9 @@ are used precisely throughout and mean nothing looser here.
|
|
|
38
38
|
| **Snapshot** | The provider's complete state at one moment. It replaces what Omni holds; it is never a patch. |
|
|
39
39
|
| **Event** | One completed transaction reported after a snapshot established the baseline. |
|
|
40
40
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
41
|
-
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar,
|
|
41
|
+
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, team member list. |
|
|
42
42
|
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a cold or warm transfer, a conference add, or a connect-back. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
|
|
43
|
-
| **Monitor** | A lead listening to a member's call unasked, from the
|
|
43
|
+
| **Monitor** | A lead listening to a member's call unasked, from the team member list: `monitor` in silence, `whisper` 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 **Monitoring 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
46
|
Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
|
|
@@ -54,7 +54,7 @@ transport, a task, a break and a call each have a word of their own:
|
|
|
54
54
|
| `transport` | A connection | `connecting`, `active`, `error` |
|
|
55
55
|
| `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
|
|
56
56
|
| `approval` | A break request | `not-requested`, `awaiting-decision`, `granted`, `starting-after-task`, `in-effect` |
|
|
57
|
-
| `availability` | A
|
|
57
|
+
| `availability` | A team member | `ready`, `on-task`, `on-break`, `signed-out` |
|
|
58
58
|
|
|
59
59
|
## Versioning
|
|
60
60
|
|
|
@@ -120,7 +120,7 @@ identifier of its own for an agent or a manager, and none crosses this boundary
|
|
|
120
120
|
operating-system account, not a directory identity, not a licence.
|
|
121
121
|
|
|
122
122
|
So a `UserId` means nothing outside the provider that issued it. Provider A's
|
|
123
|
-
`interactionHistory[].by` and provider B's
|
|
123
|
+
`interactionHistory[].by` and provider B's team member list `memberId` are unrelated strings that will
|
|
124
124
|
eventually collide, and one person on several providers has several identities that nothing here
|
|
125
125
|
pairs. Scope every user identifier with its provider ID before storing or comparing it, exactly as
|
|
126
126
|
`taskKey()` already does for tasks — see `userKey()` under **Utilities**.
|
|
@@ -246,7 +246,7 @@ type AuthenticationContext = {
|
|
|
246
246
|
log?: (entry: unknown) => void;
|
|
247
247
|
};
|
|
248
248
|
|
|
249
|
-
type MonitorMode = "monitor" | "whisper" | "
|
|
249
|
+
type MonitorMode = "monitor" | "whisper" | "join-call";
|
|
250
250
|
|
|
251
251
|
type TeamCapabilities = {
|
|
252
252
|
breakControl?: true;
|
|
@@ -485,7 +485,7 @@ type Snapshot = {
|
|
|
485
485
|
taskCount: number;
|
|
486
486
|
contacts?: Contact[];
|
|
487
487
|
scheduledActivities?: ScheduledActivity[];
|
|
488
|
-
team?:
|
|
488
|
+
team?: TeamMembers;
|
|
489
489
|
};
|
|
490
490
|
|
|
491
491
|
type AgentCapacity = {
|
|
@@ -901,11 +901,11 @@ or email states those values in `lockedValues`, since a run that cannot ask the
|
|
|
901
901
|
pass. A host never has the value and never asks. A name is not locked. What the queue provides rather than permits — browsers, outcomes, custom
|
|
902
902
|
controls — is content, and is never locked.
|
|
903
903
|
|
|
904
|
-
**A lead sets the team's policy from their
|
|
904
|
+
**A lead sets the team's policy from their team member list.** A login that declares
|
|
905
905
|
`capabilities.team.policyControl` may `executeTeamPolicy({ type: "set", capability, setting })`
|
|
906
906
|
with `on`, `off`, or `person`, for any task control, `dial`, or a skill — and only `hold`
|
|
907
907
|
and skills may be `person`; connect back and new call are the team's, on or off, within what the queue
|
|
908
|
-
allows. The
|
|
908
|
+
allows. The team member list carries `policies` for such a login: every policy as it stands, who set it, and
|
|
909
909
|
`lockedBy` where a level above the team made it theirs to keep, which the lead sees and cannot
|
|
910
910
|
change — `executeTeamPolicy` on it answers `failed` with `omni.capability-not-enabled`.
|
|
911
911
|
|
|
@@ -1002,7 +1002,7 @@ type LeadRequest = {
|
|
|
1002
1002
|
since: IsoTimestamp;
|
|
1003
1003
|
};
|
|
1004
1004
|
|
|
1005
|
-
type
|
|
1005
|
+
type TeamMembers = {
|
|
1006
1006
|
members: TeamMember[];
|
|
1007
1007
|
requests?: LeadRequest[];
|
|
1008
1008
|
policies?: TeamPolicies;
|
|
@@ -1050,7 +1050,7 @@ type TeamLeadAssistCommandRequest = {
|
|
|
1050
1050
|
type TeamMonitorCommand =
|
|
1051
1051
|
| { type: "monitor"; memberId: UserId }
|
|
1052
1052
|
| { type: "whisper" }
|
|
1053
|
-
| { type: "
|
|
1053
|
+
| { type: "join-call" }
|
|
1054
1054
|
| { type: "leave" };
|
|
1055
1055
|
|
|
1056
1056
|
type TeamMonitorCommandRequest = {
|
|
@@ -1115,7 +1115,7 @@ type ProviderEvent =
|
|
|
1115
1115
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
1116
1116
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
1117
1117
|
| { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId; allocationId?: AllocationId }
|
|
1118
|
-
| { type: "team-updated"; team:
|
|
1118
|
+
| { type: "team-updated"; team: TeamMembers }
|
|
1119
1119
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
1120
1120
|
| { type: "calendar-updated"; scheduledActivities: ScheduledActivity[] };
|
|
1121
1121
|
|
|
@@ -1923,11 +1923,11 @@ them from what arrives later.
|
|
|
1923
1923
|
| Field | Contract |
|
|
1924
1924
|
| --- | --- |
|
|
1925
1925
|
| `breaks` | This login may request a break. Requires the four break methods on the connection. |
|
|
1926
|
-
| `team` | This login leads a team. The provider publishes a `
|
|
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
1927
|
| `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — place, release, decide, set policy — as far as the provider supports; a command it lacks answers `omni.capability-not-enabled`. Omni asks for a decision only against a member whose `break` is `awaiting-decision`, so a provider that grants on request is never asked to decide. Requires `executeTeamBreak`. |
|
|
1928
1928
|
| `team.leadAssistControl` | This lead may join a member's call on request. Requires `executeTeamLeadAssist`. |
|
|
1929
|
-
| `team.monitorControl` | This lead may listen to a member's call unasked, in the listed modes and no others: `monitor`, `whisper`, `
|
|
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
|
|
1929
|
+
| `team.monitorControl` | This lead may listen to a member's call unasked, in the listed modes and no others: `monitor`, `whisper`, `join-call`. The list always includes `monitor`, since the other two begin from one. Requires `executeTeamMonitor`. See **Monitoring a call**. |
|
|
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`. |
|
|
1931
1931
|
| `preferences` | What the team left to this person, with where each stands and who set it. Omitted when nothing was. Requires `setPreference`. See **Who decides what an agent may do**. |
|
|
1932
1932
|
|
|
1933
1933
|
A session action is available only when both the capability and Omni provisioning permit it.
|
|
@@ -1937,7 +1937,7 @@ state. A provider that reads roles live — a lead demoted mid-shift — republi
|
|
|
1937
1937
|
with the new set through `subscribe()` on the authentication session, which Omni keeps open for
|
|
1938
1938
|
the life of the connection for exactly this reason, and the next snapshot agrees with it. Omni
|
|
1939
1939
|
provisions what the capabilities call for at sign-in — a team panel for a lead, empty until the
|
|
1940
|
-
|
|
1940
|
+
team member list arrives, and nothing for anybody else — and withdraws it on the next render when the
|
|
1941
1941
|
capability goes. A command that arrives after its capability was withdrawn is answered `failed`
|
|
1942
1942
|
with `omni.capability-not-enabled`: the provider names it, so Omni never has to infer from a
|
|
1943
1943
|
capability change it may not have rendered yet that "you are no longer a lead" is the message
|
|
@@ -2023,7 +2023,7 @@ failure:
|
|
|
2023
2023
|
```
|
|
2024
2024
|
|
|
2025
2025
|
The `User` it carries is the **root of this provider's user namespace**. Every other person this
|
|
2026
|
-
provider names — a
|
|
2026
|
+
provider names — a team member, the manager on an imposed break, the agent on an interaction step —
|
|
2027
2027
|
is identified from the same directory and carries the same `UserId` type.
|
|
2028
2028
|
|
|
2029
2029
|
| Field | Contract |
|
|
@@ -2136,7 +2136,7 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
|
|
|
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. |
|
|
2138
2138
|
| `scheduledActivities` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
|
|
2139
|
-
| `team` | Required `
|
|
2139
|
+
| `team` | Required `TeamMembers` when the login declares `capabilities.team`, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission. |
|
|
2140
2140
|
|
|
2141
2141
|
## Live connection
|
|
2142
2142
|
|
|
@@ -2153,7 +2153,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2153
2153
|
| `refused(report)` | Always. The host tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the host does with what it refuses**. |
|
|
2154
2154
|
| `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
|
|
2155
2155
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
2156
|
-
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a
|
|
2156
|
+
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a team member list, or `interactionHistory[].by`. Each `User` carries its `timeZone`; a person whose zone the provider cannot name is omitted from the answer, as any unresolvable id is. |
|
|
2157
2157
|
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
2158
2158
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
2159
2159
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
@@ -2211,14 +2211,14 @@ Turns `UserId` values into something an agent can read.
|
|
|
2211
2211
|
describeUsers(ids: UserId[]): Promise<User[]>
|
|
2212
2212
|
```
|
|
2213
2213
|
|
|
2214
|
-
Required of any adapter that publishes a `UserId` — on `ImposedBreak.by`, a team
|
|
2214
|
+
Required of any adapter that publishes a `UserId` — on `ImposedBreak.by`, a team member list, or
|
|
2215
2215
|
`interactionHistory[].by`. Publishing an identifier Omni cannot resolve puts a name on screen
|
|
2216
2216
|
that reads as a database key.
|
|
2217
2217
|
|
|
2218
2218
|
- **Omit an id you cannot resolve; do not invent a name for it.** A missing entry says *I do not
|
|
2219
2219
|
know this person*, which Omni renders as such. Ordering is not significant and the response may
|
|
2220
2220
|
be shorter than the request.
|
|
2221
|
-
- **Take the whole list in one call.** Omni resolves a
|
|
2221
|
+
- **Take the whole list in one call.** Omni resolves a team member list or an interaction history as a batch, and
|
|
2222
2222
|
a per-id round trip multiplies that by its length.
|
|
2223
2223
|
- **Omni caches a result for one hour, then resolves it again.** The identifier is stable across
|
|
2224
2224
|
logins but the name behind it is not, so the cache expires on a clock rather than living for the
|
|
@@ -2857,7 +2857,7 @@ Four rules a provider has to keep:
|
|
|
2857
2857
|
- **`by` is a bare `UserId`, and not necessarily an agent.** A lead or a manager takes part
|
|
2858
2858
|
during an interaction too — a transfer accepted, a call conferenced in — so the field names whoever it was,
|
|
2859
2859
|
the same way `ImposedBreak.by` does. It comes from this provider's own directory, the same
|
|
2860
|
-
namespace as `AuthenticationState.identity.id` and the team
|
|
2860
|
+
namespace as `AuthenticationState.identity.id` and the team member list, so entries pair
|
|
2861
2861
|
within a provider and never across one.
|
|
2862
2862
|
- **A task carries no names.** Omni resolves what to display with `describeUsers()`. Two people
|
|
2863
2863
|
called Arun on one site is ordinary, and anything pairing entries on a display name pairs them
|
|
@@ -3726,7 +3726,7 @@ Your own tasks are the only ones you count. What the agent holds at other provid
|
|
|
3726
3726
|
concern — Omni set `count` knowing it, and this is how: the agent is one person on several
|
|
3727
3727
|
providers, and the host divides their capacity among them rather than telling each the whole. A
|
|
3728
3728
|
provider that has none of it for now is told **`count: 0`, host-stopped**: allocate nothing, show
|
|
3729
|
-
the member as `reserved` on the
|
|
3729
|
+
the member as `reserved` on the team member list -- signed in here, capacity held by the host for elsewhere,
|
|
3730
3730
|
a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
|
|
3731
3731
|
count as any other when the host has capacity for this provider again. Zero is the one restatement
|
|
3732
3732
|
that follows work rather than provisioning.
|
|
@@ -3764,7 +3764,7 @@ request object only to the request method, and undefined to the other three.
|
|
|
3764
3764
|
|
|
3765
3765
|
Use `validateTeamBreakCommand(request, context)` for lead decisions, placement, release and
|
|
3766
3766
|
policy commands. It requires the live lead capability and active transport, a current target
|
|
3767
|
-
|
|
3767
|
+
team member list for member commands, and the target's complete break state for placement/release.
|
|
3768
3768
|
Approve/deny requires an awaiting decision; placement uses the target's reason codes; release
|
|
3769
3769
|
requires an imposed committed break. The context's target state must belong to the named member;
|
|
3770
3770
|
that association and backend authorization are provider responsibilities.
|
|
@@ -4026,19 +4026,27 @@ so there is nothing on it to resume.
|
|
|
4026
4026
|
## Team leads
|
|
4027
4027
|
|
|
4028
4028
|
A lead who also takes calls sees their team on the idle dashboard. `Snapshot.team` carries a
|
|
4029
|
-
`
|
|
4029
|
+
`TeamMembers` object, replaced whole by `team-updated`.
|
|
4030
|
+
|
|
4031
|
+
Migration: TeamRoster is now `TeamMembers`, and validateTeamRoster is now
|
|
4032
|
+
`validateTeamMembers`. Update type imports and validator calls; no legacy export aliases exist.
|
|
4033
|
+
The object still contains `members` and the permitted `requests` and `policies`; it is not a
|
|
4034
|
+
bare array. `Snapshot.team`, the `team-updated` event and its `team` payload, login permissions,
|
|
4035
|
+
and `team.*` validation rules keep their names and semantics. An empty team has `members: []`;
|
|
4036
|
+
an absent `team` still means the login is not entitled to that contribution.
|
|
4037
|
+
|
|
4030
4038
|
|
|
4031
4039
|
| Field | Contract |
|
|
4032
4040
|
| --- | --- |
|
|
4033
|
-
| `members` | Every member of this lead's team, whatever their state. `[]` says the lead has a team with nobody in it; omitting the
|
|
4041
|
+
| `members` | Every member of this lead's team, whatever their state. `[]` says the lead has a team with nobody in it; omitting the team member list says something else entirely — see **The login is the permission** below. |
|
|
4034
4042
|
| `requests` | The members currently asking this lead to join a call, each with the task and the note. Required when the login declares `team.leadAssistControl`, `[]` when nobody is asking; omitted when it does not. See **Lead assist**. |
|
|
4035
4043
|
| `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a level above the team made it theirs to keep. Required when the login declares `team.policyControl`; omitted when it does not. See **Who decides what an agent may do**. |
|
|
4036
4044
|
|
|
4037
4045
|
| `TeamMember` field | Contract |
|
|
4038
4046
|
| --- | --- |
|
|
4039
|
-
| `id` | Required `UserId`. A task carries no names and neither does a
|
|
4047
|
+
| `id` | Required `UserId`. A task carries no names and neither does a team member list: Omni resolves what to display with `describeUsers()`. |
|
|
4040
4048
|
| `availability` | Required. What the member is doing now. |
|
|
4041
|
-
| `since` | Optional. When the current `availability` began — not when they signed in, and not when the
|
|
4049
|
+
| `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
|
|
4042
4050
|
| `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
|
|
4043
4051
|
|
|
4044
4052
|
Each availability value means one thing:
|
|
@@ -4051,7 +4059,7 @@ Each availability value means one thing:
|
|
|
4051
4059
|
| `reserved` | Signed in here, and the host holds this agent's capacity for another provider (`count: 0`, host-stopped): not receiving this provider's work, and not on a break. See **Capacity**. |
|
|
4052
4060
|
| `signed-out` | Known to this team but not signed in to this provider. |
|
|
4053
4061
|
|
|
4054
|
-
**Always publish the complete
|
|
4062
|
+
**Always publish the complete team member list, never a change to it.** Team presence typically reaches an
|
|
4055
4063
|
adapter over a best-effort channel with no ordering and no delivery guarantee, so a stream of deltas
|
|
4056
4064
|
cannot be trusted to reconstruct the truth. The adapter reconciles against its own authoritative
|
|
4057
4065
|
read and publishes the result.
|
|
@@ -4062,15 +4070,15 @@ everybody — worse than showing nothing, because it looks like data. Send it on
|
|
|
4062
4070
|
knows when the state actually began. It times the current `availability`, so it moves every time
|
|
4063
4071
|
that value does.
|
|
4064
4072
|
|
|
4065
|
-
**The login is the permission.** A
|
|
4073
|
+
**The login is the permission.** A team member list goes to a login that declares `capabilities.team`, on
|
|
4066
4074
|
every snapshot, and to nobody else. Omni never decides who leads a team: the provider said so at
|
|
4067
|
-
sign-in, and the
|
|
4075
|
+
sign-in, and the team member list agrees with it — present, `[]` included, for a lead; absent for everybody
|
|
4068
4076
|
else, which is the correct rendering for an agent who leads nobody. What the lead may do with the
|
|
4069
|
-
|
|
4077
|
+
team member list is on the login too, `team.breakControl` and `team.leadAssistControl`, never on the team member list.
|
|
4070
4078
|
|
|
4071
|
-
**The
|
|
4079
|
+
**The team member list never carries the agent it is published to — not in `members`, and not in
|
|
4072
4080
|
`requests`.** A lead does not report to themself: their own break request and their own ask for a
|
|
4073
|
-
lead go up to whoever leads them and appear on *that* person's
|
|
4081
|
+
lead go up to whoever leads them and appear on *that* person's team member list, while the requester sees
|
|
4074
4082
|
only their own `BreakState` and their task's `leadAssist` move. An adapter whose platform lists the lead
|
|
4075
4083
|
among their own members filters the signed-in identity out before publishing. **Being a lead is a
|
|
4076
4084
|
role the provider knows, never inferred from who is listed:** it is declared at sign-in, a lead
|
|
@@ -4089,7 +4097,7 @@ One method, `executeTeamBreak`, taking a discriminated command exactly as `execu
|
|
|
4089
4097
|
| `{ type: "place", memberId: UserId, reasonId?, reason? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's imposed break carries it as `activeReasonId`, so its kind is known. |
|
|
4090
4098
|
| `{ type: "release", memberId: UserId }` | Ends an imposed break on that member, whoever placed it. |
|
|
4091
4099
|
|
|
4092
|
-
`memberId` is this provider's own identifier for the member, as published on its
|
|
4100
|
+
`memberId` is this provider's own identifier for the member, as published on its team member list. It is
|
|
4093
4101
|
never an identifier from another provider, and Omni does not translate between them; names come
|
|
4094
4102
|
from `describeUsers()`.
|
|
4095
4103
|
|
|
@@ -4105,7 +4113,7 @@ never expressed here.
|
|
|
4105
4113
|
|
|
4106
4114
|
An agent on a call may ask a lead to join it -- a dispute that needs approval, a customer who
|
|
4107
4115
|
asks for a manager, a moment the agent wants a second pair of ears. A call centre calls this
|
|
4108
|
-
assistance or escalation, and the name says who assists: the capability is `leadAssist` on the task; the lead's side is the
|
|
4116
|
+
assistance or escalation, and the name says who assists: the capability is `leadAssist` on the task; the lead's side is the team member list, which is already the lead's view of the
|
|
4109
4117
|
team, and a second lead method beside `executeTeamBreak`:
|
|
4110
4118
|
|
|
4111
4119
|
```ts
|
|
@@ -4120,7 +4128,7 @@ Required when the login declares `capabilities.team.leadAssistControl`, and gate
|
|
|
4120
4128
|
execute({ taskId: "call-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
|
|
4121
4129
|
// task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
|
|
4122
4130
|
|
|
4123
|
-
// 2. Every lead entitled to it sees the request on their
|
|
4131
|
+
// 2. Every lead entitled to it sees the request on their team member list.
|
|
4124
4132
|
// team-updated: requests: [{ id: "req-7", memberId: "A-1", taskId: "call-42", allocationId: "alloc-42", note, since }]
|
|
4125
4133
|
|
|
4126
4134
|
// 3. A lead joins, or declines.
|
|
@@ -4147,7 +4155,7 @@ finish is not given more -- and a provider answers a `join` from a lead on such
|
|
|
4147
4155
|
The request stands for another lead, as it does when this one is already on a call.
|
|
4148
4156
|
|
|
4149
4157
|
**On `decline`, or a request the agent withdraws with `{ type: "lead-assist", action: "cancel" }`, the
|
|
4150
|
-
provider clears `leadAssist` from the agent's task** and drops the request from every
|
|
4158
|
+
provider clears `leadAssist` from the agent's task** and drops the request from every team member list. Nothing
|
|
4151
4159
|
else changes; the agent is still on the call.
|
|
4152
4160
|
|
|
4153
4161
|
The lead then has two commands on their copy, gated by `assisting` being present, and a third
|
|
@@ -4173,30 +4181,37 @@ const leadAssistCapable = {
|
|
|
4173
4181
|
```
|
|
4174
4182
|
|
|
4175
4183
|
Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
|
|
4176
|
-
`describeUsers()`; names never travel on a task or a
|
|
4184
|
+
`describeUsers()`; names never travel on a task or a team member list.
|
|
4177
4185
|
|
|
4178
4186
|
### Monitoring a call
|
|
4179
4187
|
|
|
4188
|
+
The user-facing action is **Join call**. The command and monitoring mode are `join-call`.
|
|
4189
|
+
Migration: replace the former barge literal in `TeamMonitorCommand`, `MonitorMode`,
|
|
4190
|
+
`team.monitorControl`, and the task’s `monitoring.mode`. `MONITOR_MODES` exports the new literal;
|
|
4191
|
+
no legacy alias is accepted. Update commands, capability declarations and retained snapshots
|
|
4192
|
+
together. The lead must already be monitoring and permitted to use this mode. Everyone on the
|
|
4193
|
+
call hears the lead; this does not transfer ownership or invoke lead-assist takeover.
|
|
4194
|
+
|
|
4180
4195
|
A lead may listen to a member's call without being asked -- to coach, to check quality, to step in
|
|
4181
4196
|
when it goes wrong. This is not lead assist: nobody requested it, the member's task says nothing
|
|
4182
4197
|
about it, and there is no take-over in it. A lead who wants the call itself uses lead assist. The
|
|
4183
|
-
three modes
|
|
4198
|
+
three modes determine who hears the lead:
|
|
4184
4199
|
|
|
4185
4200
|
| Mode | Who hears the lead |
|
|
4186
4201
|
| --- | --- |
|
|
4187
4202
|
| `monitor` | Nobody. The lead hears both sides in silence. |
|
|
4188
4203
|
| `whisper` | The agent alone. The customer hears nothing. |
|
|
4189
|
-
| `
|
|
4204
|
+
| `join-call` | Everyone on the call. |
|
|
4190
4205
|
|
|
4191
4206
|
The login says which modes this lead has, as a list on `team.monitorControl`, and it always
|
|
4192
|
-
includes `monitor`, because whisper and
|
|
4207
|
+
includes `monitor`, because whisper and join-call begin from one
|
|
4193
4208
|
(`authentication.capability.team.monitorControl.monitor`). A centre that lets every lead listen but
|
|
4194
|
-
reserves
|
|
4209
|
+
reserves joining the call declares `["monitor", "whisper"]`, and Omni offers no Join call to that lead. The
|
|
4195
4210
|
declaration requires `executeTeamMonitor`, gated exactly as `executeTeamLeadAssist` is by
|
|
4196
4211
|
`team.leadAssistControl`.
|
|
4197
4212
|
|
|
4198
4213
|
```ts
|
|
4199
|
-
// 1. The lead picks a member from the
|
|
4214
|
+
// 1. The lead picks a member from the team member list and starts silent.
|
|
4200
4215
|
executeTeamMonitor({ command: { type: "monitor", memberId: "A-1" } })
|
|
4201
4216
|
|
|
4202
4217
|
// 2. The lead's own task arrives -- task-offered with `automatic`, as a joined call does -- and
|
|
@@ -4205,7 +4220,7 @@ executeTeamMonitor({ command: { type: "monitor", memberId: "A-1" } })
|
|
|
4205
4220
|
|
|
4206
4221
|
// 3. The lead changes how they are heard; the provider restates the task with the new mode.
|
|
4207
4222
|
executeTeamMonitor({ command: { type: "whisper" } })
|
|
4208
|
-
executeTeamMonitor({ command: { type: "
|
|
4223
|
+
executeTeamMonitor({ command: { type: "join-call" } })
|
|
4209
4224
|
|
|
4210
4225
|
// 4. The lead leaves; their task ends with `{ type: "left" }`. The member's call goes on.
|
|
4211
4226
|
executeTeamMonitor({ command: { type: "leave" } })
|
|
@@ -4213,9 +4228,10 @@ executeTeamMonitor({ command: { type: "leave" } })
|
|
|
4213
4228
|
|
|
4214
4229
|
**The lead's task is what gives them audio**: Omni opens media on it as on any voice task, and the
|
|
4215
4230
|
provider bridges the lead's leg into the member's call in the mode stated. `monitoring.mode` is
|
|
4216
|
-
the state, restated on every change, and a `whisper` or `
|
|
4231
|
+
the state, restated on every change, and a `whisper` or `join-call` the login's list does not include
|
|
4217
4232
|
is answered `failed`. **A lead listens to one call at a time** (`snapshot.monitoring.single`), and
|
|
4218
|
-
a task
|
|
4233
|
+
a task carries lead assistance or monitoring, never both (`task.monitoring.assisting`).
|
|
4234
|
+
The `join-call` mode still belongs to monitoring; it does not create `assisting`. The member's
|
|
4219
4235
|
call ending ends the lead's task as it ends the member's, with `left`, since the lead was never
|
|
4220
4236
|
working on it.
|
|
4221
4237
|
|
|
@@ -4264,7 +4280,7 @@ Omni offers Approve and Deny only while a member is `awaiting-decision`, shows `
|
|
|
4264
4280
|
but not started, and shows `starting-after-task` as settled.
|
|
4265
4281
|
|
|
4266
4282
|
**Live status, not a record.** The provider derives it from what is true now — not stored, not
|
|
4267
|
-
historical, carrying no decision made earlier. Like the
|
|
4283
|
+
historical, carrying no decision made earlier. Like the team member list it belongs to, it is published
|
|
4268
4284
|
whole and replaced whole, and a provider that cannot say omits it.
|
|
4269
4285
|
|
|
4270
4286
|
**An agent is not waiting on one person.** Authority is held by several, everyone who holds it
|
|
@@ -4829,8 +4845,8 @@ direction — the provider asking Omni to reconcile — and neither replaces the
|
|
|
4829
4845
|
|
|
4830
4846
|
Carries a complete `Snapshot` after reconnect or when the provider explicitly requests
|
|
4831
4847
|
reconciliation. `reason` is `reconnected` or `provider-requested`. Omni replaces the provider's
|
|
4832
|
-
current status, break state, tasks, contacts, scheduled activities and team
|
|
4833
|
-
snapshot. It carries what the login's capabilities call for — a
|
|
4848
|
+
current status, break state, tasks, contacts, scheduled activities and team member list with this
|
|
4849
|
+
snapshot. It carries what the login's capabilities call for — a team member list for a lead, on every
|
|
4834
4850
|
snapshot — and nothing they do not; a capability is withdrawn by a republished `authenticated`,
|
|
4835
4851
|
never by an omission from a snapshot.
|
|
4836
4852
|
|
|
@@ -5066,9 +5082,9 @@ each connected provider.
|
|
|
5066
5082
|
|
|
5067
5083
|
### `team-updated`
|
|
5068
5084
|
|
|
5069
|
-
Replaces this provider's complete `
|
|
5070
|
-
publishes a
|
|
5071
|
-
reason set out under **Team leads**. A lead's snapshot always carries the
|
|
5085
|
+
Replaces this provider's complete `TeamMembers`. It is emitted only for an agent the provider
|
|
5086
|
+
publishes a team member list to, and it carries the whole team every time — never a change to it, for the
|
|
5087
|
+
reason set out under **Team leads**. A lead's snapshot always carries the team member list; it goes only when
|
|
5072
5088
|
a republished `authenticated` no longer declares `capabilities.team`.
|
|
5073
5089
|
|
|
5074
5090
|
### `contacts-updated`
|
|
@@ -5094,7 +5110,7 @@ The same treatment for a `UserId`, and needed for the same reason: user identifi
|
|
|
5094
5110
|
issued by each provider independently, so two providers will eventually issue the same string for
|
|
5095
5111
|
different people. Encode and join before storing or comparing.
|
|
5096
5112
|
|
|
5097
|
-
Use it for every `UserId` — `interactionHistory[].by`,
|
|
5113
|
+
Use it for every `UserId` — `interactionHistory[].by`, team members, `memberId` on a
|
|
5098
5114
|
lead command, `ImposedBreak.by`. A bare one is only ever compared against another from the **same** provider; anything
|
|
5099
5115
|
wider goes through this key.
|
|
5100
5116
|
|
|
@@ -5114,7 +5130,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
5114
5130
|
| --- | --- |
|
|
5115
5131
|
| `validateManifest(manifest)` | Identity, protocol-version interoperability, authentication methods, and idle-capability shapes. |
|
|
5116
5132
|
| `validateTask(task, { channel, locked? })` | Identity, channel agreement, phase, wrap allowance, capability shapes, custom controls, and browsers. Given `locked`, the values the queue locked, a task whose party stands locked carries none of them anywhere else (`task.locked.leak`). |
|
|
5117
|
-
| `validateSnapshot(snapshot, manifest)` | Status, break state, break reasons, team
|
|
5133
|
+
| `validateSnapshot(snapshot, manifest)` | Status, break state, break reasons, team member list, the stated `taskCount` reconciled against the tasks carried, and every task, contact, and activity, including idle-capability gating both ways: a contribution the manifest never declared is refused, and one it declares is required, `[]` included. |
|
|
5118
5134
|
| `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
|
|
5119
5135
|
| `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
|
|
5120
5136
|
| `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
|
|
@@ -5137,13 +5153,13 @@ Each returns `ProtocolViolation[]` rather than throwing, so a caller can report
|
|
|
5137
5153
|
once. A violation carries a stable `rule` id such as `task.browser.url.scheme`, the `path` it was
|
|
5138
5154
|
found at such as `snapshot.tasks[0].browsers[1].url`, and a `message`.
|
|
5139
5155
|
|
|
5140
|
-
Some rules need to know who is reading. `
|
|
5156
|
+
Some rules need to know who is reading. `validateTeamMembers`, `validateSnapshot`, and
|
|
5141
5157
|
`validateEventEnvelope` take an optional final `{ self, capabilities }` — the signed-in agent's
|
|
5142
|
-
`AuthenticationState.identity.id` and their login's `capabilities`. Given `self`, a
|
|
5158
|
+
`AuthenticationState.identity.id` and their login's `capabilities`. Given `self`, a team member list that
|
|
5143
5159
|
carries that agent reports `team.member.self` or `team.request.self`. Given `capabilities`, a lead's
|
|
5144
|
-
snapshot without a
|
|
5145
|
-
reports `team.unentitled`, `requests` on a
|
|
5146
|
-
`team.requests.capability`, and a
|
|
5160
|
+
snapshot without a team member list reports `team.required`, a team member list published to a login that does not lead
|
|
5161
|
+
reports `team.unentitled`, `requests` on a team member list whose login lacks `team.leadAssistControl` reports
|
|
5162
|
+
`team.requests.capability`, and a team member list without them on a login that declares it reports
|
|
5147
5163
|
`team.requests.required`. Without them those rules are not checked, because they cannot be.
|
|
5148
5164
|
`exerciseAdapter` always passes both.
|
|
5149
5165
|
|
|
@@ -5206,7 +5222,7 @@ A capability granted by a later login requires its methods just as one declared
|
|
|
5206
5222
|
|
|
5207
5223
|
`result.notExercised` lists what the run never reached — one subject per family of rules: each
|
|
5208
5224
|
optional part of a task (`task.browsers`, `task.interactionHistory`, `task.leadAssist`, …), the break's
|
|
5209
|
-
`reasons` and `imposed`, the
|
|
5225
|
+
`reasons` and `imposed`, the team member list's `members` and `requests`, each declared contribution, and
|
|
5210
5226
|
each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
|
|
5211
5227
|
Nothing there is a violation: an adapter with no team has nothing to exercise. But a fixture with
|
|
5212
5228
|
no tasks exercises no task rule, and a pass over it reads as coverage it is not.
|
|
@@ -5291,7 +5307,7 @@ expect(driven.violations).toEqual([]);
|
|
|
5291
5307
|
assertReached(driven, ["task.onCall", "task.media", "event.task-ended"]);
|
|
5292
5308
|
```
|
|
5293
5309
|
`assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
|
|
5294
|
-
never met, so a test that meant to check a
|
|
5310
|
+
never met, so a test that meant to check a team member list cannot pass on a fixture that never produced one.
|
|
5295
5311
|
It reads like a guarantee and is a claim the adopter keeps making: it catches an adapter that
|
|
5296
5312
|
stopped reaching a subject, not a list that stopped asking, so a list can rot to nothing and stay
|
|
5297
5313
|
green. Keep it honest with a control beside it -- one subject the run genuinely cannot reach,
|
|
@@ -5316,7 +5332,7 @@ cannot be established from TypeScript structure alone.
|
|
|
5316
5332
|
|
|
5317
5333
|
| Helper | Contract checked |
|
|
5318
5334
|
| --- | --- |
|
|
5319
|
-
| `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no
|
|
5335
|
+
| `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no team member list for a login that no longer leads, no requests for one that may no longer join. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
|
|
5320
5336
|
| `assertTaskCapabilityWithdrawal(tasks, manifest, command)` | A capability withdrawn by a republish of the task is gone from the task: every task in the sequence is validated, all carry the offer's id, at least one capability the offer declared is absent at the end (a locked control is present, not withdrawn), and `command` is clean against the first task and refused against the last for want of a withdrawn capability and nothing else. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's answer. |
|
|
5321
5337
|
| `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. The same assertion serves a command the provider never supported under a capability it declares. |
|
|
5322
5338
|
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
|