@xema/omni-protocol 0.1.6 → 0.1.8
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 +18 -3
- package/dist/index.d.ts +16 -32
- package/dist/testing.d.ts +1 -5
- package/dist/testing.js +0 -33
- package/dist/validation.js +13 -4
- package/guide.md +124 -117
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,15 +53,17 @@ expect(result.disconnectWasClean).toBe(true);
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Run the contract scenarios beside it — authentication restore and expiry, reconnect with missed
|
|
56
|
-
assignments, break denial and retry,
|
|
56
|
+
assignments, break denial and retry, wrap timeout, browser isolation.
|
|
57
57
|
|
|
58
58
|
> **Assert both directions.** Every helper rejects a violating input as well as accepting a
|
|
59
59
|
> conforming one. A suite that only asserts "this conforming case does not throw" passes unchanged
|
|
60
60
|
> if the helper is gutted, so pair every positive case with the violating twin.
|
|
61
61
|
|
|
62
|
-
##
|
|
62
|
+
## Three things TypeScript will not catch for you
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
All found by adapters against this contract, and all produce a green build over a wrong shape.
|
|
65
|
+
Two share one cause: TypeScript checks extra keys only on a literal that is the thing directly
|
|
66
|
+
assigned. Move the literal anywhere else and the check is gone.
|
|
65
67
|
|
|
66
68
|
**Conditional spreads are the blind spot on a task literal.** A key inside
|
|
67
69
|
`...(cond ? { … } : {})` is never checked against the task type, and `satisfies Task<C>` on the
|
|
@@ -88,6 +90,19 @@ switch (command.action) {
|
|
|
88
90
|
}
|
|
89
91
|
```
|
|
90
92
|
|
|
93
|
+
**A `const` fixture escapes excess-property checking.** Park a literal in a variable and it is no
|
|
94
|
+
longer the thing directly assigned — the same reason the conditional spread escapes — so a shared
|
|
95
|
+
test fixture keeps a field the contract has dropped: `tsc` says nothing and the suite is
|
|
96
|
+
confidently green over a shape that no longer exists. Annotate the `const` or `satisfies` it where
|
|
97
|
+
it is declared; either names the field on the next build.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
take({ reasonId, requestedAt }); // error: requestedAt
|
|
101
|
+
const request = { reasonId, requestedAt }; take(request); // no error — the hole
|
|
102
|
+
const request: BreakRequest = { reasonId, requestedAt }; // error: requestedAt
|
|
103
|
+
const request = { reasonId, requestedAt } satisfies BreakRequest; // error: requestedAt
|
|
104
|
+
```
|
|
105
|
+
|
|
91
106
|
## Building
|
|
92
107
|
|
|
93
108
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -579,33 +579,29 @@ export interface CustomTaskCommand {
|
|
|
579
579
|
}
|
|
580
580
|
export type TaskCommand<C extends Channel = Channel> = (C extends "voice" ? VoiceTaskCommand : C extends "chat" ? ChatTaskCommand : EmailTaskCommand) | CustomTaskCommand;
|
|
581
581
|
export interface TaskCommandRequest<C extends Channel = Channel> {
|
|
582
|
-
/** Stable across retries. Processing one twice must not repeat its side effects. */
|
|
583
|
-
commandId: string;
|
|
584
582
|
taskId: TaskId;
|
|
585
583
|
command: TaskCommand<C>;
|
|
586
584
|
}
|
|
587
585
|
/**
|
|
588
|
-
* `applied`
|
|
589
|
-
*
|
|
590
|
-
*
|
|
586
|
+
* `applied` rather than a verb per command: the command travels in the request, so
|
|
587
|
+
* `execute({ command: { type: "hold" } })` returning `applied` already says the hold applied.
|
|
588
|
+
*
|
|
589
|
+
* A promise that rejects with no result means *unknown*, not `failed`. Omni does not retry --
|
|
590
|
+
* no adapter on the agent's PC can make a repeat safe -- and the next snapshot shows what the
|
|
591
|
+
* provider did. A command therefore carries no key of Omni's making.
|
|
591
592
|
*/
|
|
592
593
|
export type TaskCommandResult = {
|
|
593
|
-
|
|
594
|
-
status: "applied" | "already-applied";
|
|
594
|
+
status: "applied";
|
|
595
595
|
} | {
|
|
596
|
-
commandId: string;
|
|
597
596
|
status: "failed";
|
|
598
597
|
failure: ProtocolFailure;
|
|
599
598
|
};
|
|
600
599
|
export interface DialRequest {
|
|
601
|
-
commandId: string;
|
|
602
600
|
destination: string;
|
|
603
601
|
}
|
|
604
602
|
export type DialResult = {
|
|
605
|
-
|
|
606
|
-
status: "dialled" | "already-dialled";
|
|
603
|
+
status: "dialled";
|
|
607
604
|
} | {
|
|
608
|
-
commandId: string;
|
|
609
605
|
status: "failed";
|
|
610
606
|
failure: ProtocolFailure;
|
|
611
607
|
};
|
|
@@ -635,8 +631,6 @@ export interface BreakReason {
|
|
|
635
631
|
alwaysAvailable?: true;
|
|
636
632
|
}
|
|
637
633
|
export interface BreakRequest {
|
|
638
|
-
/** Stable across retries, and what makes `already-requested` recognisable. */
|
|
639
|
-
requestId: string;
|
|
640
634
|
reason?: string;
|
|
641
635
|
/** The chosen `BreakReason.id`, where the provider publishes codes. */
|
|
642
636
|
reasonId?: string;
|
|
@@ -659,7 +653,6 @@ export type ImposedBreak = {
|
|
|
659
653
|
};
|
|
660
654
|
export interface BreakState {
|
|
661
655
|
approval: BreakApproval;
|
|
662
|
-
requestId?: string;
|
|
663
656
|
/** Whether the agent may ask at all. Distinct from the fate of a request already made. */
|
|
664
657
|
accepting: boolean;
|
|
665
658
|
/** Shown when `accepting` is false, such as "Busy hours". */
|
|
@@ -680,31 +673,26 @@ export type CapacityResult = {
|
|
|
680
673
|
};
|
|
681
674
|
/** Succeeding is not the outcome: `requested` says the provider holds it, not that it was granted. */
|
|
682
675
|
export type BreakRequestResult = {
|
|
683
|
-
|
|
684
|
-
status: "requested" | "already-requested";
|
|
676
|
+
status: "requested";
|
|
685
677
|
} | {
|
|
686
|
-
requestId: string;
|
|
687
678
|
status: "failed";
|
|
688
679
|
failure: ProtocolFailure;
|
|
689
680
|
};
|
|
681
|
+
/** Committing a break already in effect changes nothing and answers `committed`. */
|
|
690
682
|
export type BreakCommitResult = {
|
|
691
|
-
|
|
692
|
-
status: "committed" | "already-committed";
|
|
683
|
+
status: "committed";
|
|
693
684
|
} | {
|
|
694
|
-
requestId: string;
|
|
695
685
|
status: "failed";
|
|
696
686
|
failure: ProtocolFailure;
|
|
697
687
|
};
|
|
698
688
|
export type BreakCancelResult = {
|
|
699
|
-
|
|
700
|
-
status: "cancelled" | "already-cancelled";
|
|
689
|
+
status: "cancelled";
|
|
701
690
|
} | {
|
|
702
|
-
requestId: string;
|
|
703
691
|
status: "failed";
|
|
704
692
|
failure: ProtocolFailure;
|
|
705
693
|
};
|
|
706
694
|
export type BreakEndResult = {
|
|
707
|
-
status: "ended"
|
|
695
|
+
status: "ended";
|
|
708
696
|
} | {
|
|
709
697
|
status: "failed";
|
|
710
698
|
failure: ProtocolFailure;
|
|
@@ -746,7 +734,6 @@ export type TeamConsultCommand = {
|
|
|
746
734
|
reason?: string;
|
|
747
735
|
};
|
|
748
736
|
export interface TeamConsultCommandRequest {
|
|
749
|
-
commandId: string;
|
|
750
737
|
command: TeamConsultCommand;
|
|
751
738
|
}
|
|
752
739
|
export type TeamBreakCommand = {
|
|
@@ -766,15 +753,12 @@ export type TeamBreakCommand = {
|
|
|
766
753
|
memberId: UserId;
|
|
767
754
|
};
|
|
768
755
|
export type TeamCommandResult = {
|
|
769
|
-
|
|
770
|
-
status: "applied" | "already-applied";
|
|
756
|
+
status: "applied";
|
|
771
757
|
} | {
|
|
772
|
-
commandId: string;
|
|
773
758
|
status: "failed";
|
|
774
759
|
failure: ProtocolFailure;
|
|
775
760
|
};
|
|
776
761
|
export interface TeamBreakCommandRequest {
|
|
777
|
-
commandId: string;
|
|
778
762
|
command: TeamBreakCommand;
|
|
779
763
|
}
|
|
780
764
|
export interface VoiceMediaSession {
|
|
@@ -909,8 +893,8 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
909
893
|
* never start, and the two-phase coordination has no way to report it.
|
|
910
894
|
*/
|
|
911
895
|
requestBreak?(request: BreakRequest): Promise<BreakRequestResult>;
|
|
912
|
-
commitBreak?(
|
|
913
|
-
cancelBreak?(
|
|
896
|
+
commitBreak?(): Promise<BreakCommitResult>;
|
|
897
|
+
cancelBreak?(): Promise<BreakCancelResult>;
|
|
914
898
|
endBreak?(): Promise<BreakEndResult>;
|
|
915
899
|
/** Required when the adapter publishes a `TeamRoster` carrying `breakControl`. */
|
|
916
900
|
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type
|
|
1
|
+
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext } from "./index.js";
|
|
2
2
|
import { type ProtocolViolation } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
|
|
4
4
|
export interface AdapterContractResult {
|
|
@@ -25,8 +25,6 @@ export interface ExerciseAdapterOptions {
|
|
|
25
25
|
* asynchronous one — which would let a non-conforming async adapter pass.
|
|
26
26
|
*/
|
|
27
27
|
export declare function exerciseAdapter<C extends Channel>(adapter: Adapter<C>, context: ConnectContext, options?: ExerciseAdapterOptions): Promise<AdapterContractResult>;
|
|
28
|
-
/** Verifies the at-most-once contract by issuing the same command twice. */
|
|
29
|
-
export declare function assertCommandIdempotency(connection: Pick<Connection, "execute">, request: TaskCommandRequest): Promise<void>;
|
|
30
28
|
/** Validates restored authentication followed by a refresh failure or expiry. */
|
|
31
29
|
export declare function assertAuthenticationRestoreAndExpiry(states: readonly AuthenticationState[]): void;
|
|
32
30
|
/** Validates duplicate delivery and returns the event sequence Omni applies once per ID. */
|
|
@@ -45,8 +43,6 @@ export declare function assertReconnectWithMissedAssignments<C extends Channel>(
|
|
|
45
43
|
* both.
|
|
46
44
|
*/
|
|
47
45
|
export declare function assertDeniedAndRetriedBreak(approvals: readonly BreakApproval[]): void;
|
|
48
|
-
/** Verifies that retrying one dial command cannot place a second call. */
|
|
49
|
-
export declare function assertDialIdempotency(connection: Pick<Connection, "dial">, request: DialRequest): Promise<void>;
|
|
50
46
|
/**
|
|
51
47
|
* Validates the deadline derived from media end and the task's fixed wrap allowance.
|
|
52
48
|
*
|
package/dist/testing.js
CHANGED
|
@@ -143,21 +143,6 @@ function publishesUserIds(snapshot) {
|
|
|
143
143
|
return false;
|
|
144
144
|
return snapshot.tasks.some(task => Array.isArray(task?.handlingHistory) && task.handlingHistory.some(step => step?.by !== undefined));
|
|
145
145
|
}
|
|
146
|
-
/** Verifies the at-most-once contract by issuing the same command twice. */
|
|
147
|
-
export async function assertCommandIdempotency(connection, request) {
|
|
148
|
-
const first = await connection.execute(request);
|
|
149
|
-
if (first.commandId !== request.commandId)
|
|
150
|
-
throw new Error("Command result id mismatch");
|
|
151
|
-
if (first.status === "failed") {
|
|
152
|
-
throw new Error(`Command failed: ${first.failure.code}`);
|
|
153
|
-
}
|
|
154
|
-
const retry = await connection.execute(request);
|
|
155
|
-
if (retry.commandId !== request.commandId)
|
|
156
|
-
throw new Error("Retried command result id mismatch");
|
|
157
|
-
if (retry.status !== "already-applied") {
|
|
158
|
-
throw new Error(`Retried command must return already-applied, received ${retry.status}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
146
|
/** Validates restored authentication followed by a refresh failure or expiry. */
|
|
162
147
|
export function assertAuthenticationRestoreAndExpiry(states) {
|
|
163
148
|
if (states.length < 2 || states[0]?.status !== "authenticated") {
|
|
@@ -231,24 +216,6 @@ export function assertDeniedAndRetriedBreak(approvals) {
|
|
|
231
216
|
throw new Error(`Break retry scenario must end granted or in effect, ended ${String(last)}`);
|
|
232
217
|
}
|
|
233
218
|
}
|
|
234
|
-
/** Verifies that retrying one dial command cannot place a second call. */
|
|
235
|
-
export async function assertDialIdempotency(connection, request) {
|
|
236
|
-
if (!connection.dial)
|
|
237
|
-
throw new Error("Dial capability requires Connection.dial()");
|
|
238
|
-
const first = await connection.dial(request);
|
|
239
|
-
if (first.commandId !== request.commandId)
|
|
240
|
-
throw new Error("Dial result id mismatch");
|
|
241
|
-
if (first.status === "failed")
|
|
242
|
-
throw new Error(`Dial failed: ${first.failure.code}`);
|
|
243
|
-
const retry = await connection.dial(request);
|
|
244
|
-
if (retry.commandId !== request.commandId)
|
|
245
|
-
throw new Error("Retried dial result id mismatch");
|
|
246
|
-
// Each method answers in its own words: a retried dial says already-dialled, not
|
|
247
|
-
// already-applied, because what it did was dial.
|
|
248
|
-
if (retry.status !== "already-dialled") {
|
|
249
|
-
throw new Error(`Retried dial must return already-dialled, received ${retry.status}`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
219
|
/**
|
|
253
220
|
* Validates the deadline derived from media end and the task's fixed wrap allowance.
|
|
254
221
|
*
|
package/dist/validation.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Each list is pinned to its type both ways -- a member the type lacks, or a member the list
|
|
10
10
|
// lacks, fails to compile -- so what the validators accept cannot drift from what the
|
|
11
11
|
// declarations say.
|
|
12
|
-
import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, IDLE_CAPABILITIES, } from "./index.js";
|
|
12
|
+
import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, IDLE_CAPABILITIES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, } from "./index.js";
|
|
13
13
|
export class ProtocolConformanceError extends Error {
|
|
14
14
|
violations;
|
|
15
15
|
constructor(violations, summary = "Adapter violates the Omni protocol") {
|
|
@@ -271,6 +271,10 @@ export function validateManifest(manifest, path = "manifest") {
|
|
|
271
271
|
versions.forEach((version, index) => {
|
|
272
272
|
into.require(typeof version === "number" && Number.isInteger(version) && version > 0, "manifest.supportedProtocolVersions.value", `${path}.supportedProtocolVersions[${index}]`, "a protocol version must be a positive integer");
|
|
273
273
|
});
|
|
274
|
+
// Interoperability: the adapter must speak a version this package does, or Omni must refuse
|
|
275
|
+
// to connect. Reported here so it is found with the manifest rather than at connect time.
|
|
276
|
+
const declared = versions.filter((version) => typeof version === "number");
|
|
277
|
+
into.require(negotiateProtocolVersion(declared) !== undefined, "manifest.supportedProtocolVersions.interoperable", `${path}.supportedProtocolVersions`, `this host speaks protocol version${OMNI_SUPPORTED_PROTOCOL_VERSIONS.length === 1 ? "" : "s"} ${OMNI_SUPPORTED_PROTOCOL_VERSIONS.join(", ")}; the adapter declares none of them`);
|
|
274
278
|
}
|
|
275
279
|
const methods = manifest.authenticationMethods;
|
|
276
280
|
if (!Array.isArray(methods) || methods.length === 0) {
|
|
@@ -434,9 +438,14 @@ function validateBrowsers(value, path, into) {
|
|
|
434
438
|
}
|
|
435
439
|
// Reuse and its scheme travel together. A reusing browser with no scheme would otherwise
|
|
436
440
|
// inherit whatever a host happened to default to, which is how two tasks end up sharing a
|
|
437
|
-
// session nobody intended.
|
|
441
|
+
// session nobody intended. The guide names the rule for the missing case.
|
|
438
442
|
if (browser.reuse === true) {
|
|
439
|
-
|
|
443
|
+
if (browser.isolationScheme === undefined) {
|
|
444
|
+
into.add("task.browser.isolationScheme.required", `${at}.isolationScheme`, `a reusing browser must declare one of: ${ISOLATION_SCHEME_VALUES.join(", ")}`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
into.require(ISOLATION_SCHEME_VALUES.includes(browser.isolationScheme), "task.browser.isolationScheme", `${at}.isolationScheme`, `an isolation scheme must be one of: ${ISOLATION_SCHEME_VALUES.join(", ")}`);
|
|
448
|
+
}
|
|
440
449
|
}
|
|
441
450
|
else if (browser.reuse === false) {
|
|
442
451
|
into.require(browser.isolationScheme === undefined, "task.browser.isolationScheme.unexpected", `${at}.isolationScheme`, "a browser that does not reuse must not declare an isolation scheme");
|
|
@@ -654,7 +663,7 @@ function validateBreakState(value, path, into) {
|
|
|
654
663
|
}
|
|
655
664
|
into.oneOf(value.approval, BREAK_APPROVALS, "break.approval", `${path}.approval`);
|
|
656
665
|
into.require(typeof value.accepting === "boolean", "break.accepting", `${path}.accepting`, "accepting must be a boolean");
|
|
657
|
-
for (const field of ["
|
|
666
|
+
for (const field of ["refusedReason", "decisionReason"]) {
|
|
658
667
|
if (value[field] !== undefined) {
|
|
659
668
|
into.filled(value[field], `break.${field}`, `${path}.${field}`, `${field} must not be empty when present`);
|
|
660
669
|
}
|
package/guide.md
CHANGED
|
@@ -542,7 +542,6 @@ type TaskCommand<C extends Channel = Channel> =
|
|
|
542
542
|
| CustomTaskCommand;
|
|
543
543
|
|
|
544
544
|
type TaskCommandRequest<C extends Channel = Channel> = {
|
|
545
|
-
commandId: string;
|
|
546
545
|
taskId: TaskId;
|
|
547
546
|
command: TaskCommand<C>;
|
|
548
547
|
};
|
|
@@ -567,7 +566,6 @@ type BreakReason = {
|
|
|
567
566
|
};
|
|
568
567
|
|
|
569
568
|
type BreakRequest = {
|
|
570
|
-
requestId: string;
|
|
571
569
|
reason?: string;
|
|
572
570
|
reasonId?: string;
|
|
573
571
|
};
|
|
@@ -578,7 +576,6 @@ type ImposedBreak =
|
|
|
578
576
|
|
|
579
577
|
type BreakState = {
|
|
580
578
|
approval: BreakApproval;
|
|
581
|
-
requestId?: string;
|
|
582
579
|
accepting: boolean;
|
|
583
580
|
refusedReason?: string;
|
|
584
581
|
decisionReason?: string;
|
|
@@ -915,18 +912,39 @@ Events report completed transactions after that baseline. Nothing is missed whil
|
|
|
915
912
|
holds; when it drops, the reconnect snapshot re-establishes the baseline before any further event
|
|
916
913
|
is applied.
|
|
917
914
|
|
|
918
|
-
### 6.
|
|
915
|
+
### 6. An unsettled result is unknown, and unknown is not retried
|
|
919
916
|
|
|
920
|
-
|
|
921
|
-
|
|
917
|
+
A settled result is a fact. A promise that rejects with no result means *unknown*: the provider
|
|
918
|
+
may have done it or not, and neither Omni nor an adapter on the agent's PC can find out in time to
|
|
919
|
+
make a repeat safe. Omni does not retry; if the agent acts again it is a new command.
|
|
922
920
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
921
|
+
On a persistent ordered transport there is only one way a command goes unsettled: the connection
|
|
922
|
+
went away underneath it. **An adapter that cannot settle a command has lost its transport, and
|
|
923
|
+
says so** — `provider-status` `connecting`, reconnect, snapshot — whichever channel the command
|
|
924
|
+
actually travelled on. An unsettled promise is therefore always followed by a snapshot, and that
|
|
925
|
+
snapshot is the answer; Omni waits for it rather than calling `snapshot()` itself. While the
|
|
926
|
+
transport is up, a result says the provider accepted the command, and the event that follows —
|
|
927
|
+
`task-updated`, `break-state` — says what it did.
|
|
927
928
|
|
|
928
|
-
|
|
929
|
-
|
|
929
|
+
A command therefore carries no key. The provider names its own records — a task, a lead request, a
|
|
930
|
+
member — and Omni refers to them by those names; **Omni never asks a provider to remember a name
|
|
931
|
+
Omni made up.**
|
|
932
|
+
|
|
933
|
+
A command whose second execution changes nothing needs no protection: committing a break that is
|
|
934
|
+
already committed stops an agent who is already stopped. A command whose second execution has a
|
|
935
|
+
cost — a dial places a second call — is not made safe by anyone, which is why it is never repeated
|
|
936
|
+
without a person deciding to.
|
|
937
|
+
|
|
938
|
+
An agent dials. The provider places the call and the answer is lost. Omni shows the dial as
|
|
939
|
+
unknown, not failed. Within a moment the provider offers the resulting call through `task-offered`
|
|
940
|
+
and the agent is on it; had nothing been placed, nothing arrives and the agent dials again. What
|
|
941
|
+
Omni must not do is dial again on the agent's behalf — the one outcome worse than a lost answer is
|
|
942
|
+
two phones ringing at the customer.
|
|
943
|
+
|
|
944
|
+
An agent presses Hold while the provider is reconnecting. Nothing is queued at either end: the
|
|
945
|
+
adapter answers `failed` with `omni.unavailable`, Omni shows the refusal, and the agent presses
|
|
946
|
+
again once the provider is `active` — against the state as it is then, rather than a held press
|
|
947
|
+
fired into a state that has moved on.
|
|
930
948
|
|
|
931
949
|
### 7. Work is pulled, never pushed
|
|
932
950
|
|
|
@@ -1394,7 +1412,7 @@ authorization codes, tokens, or provider responses containing secrets.
|
|
|
1394
1412
|
### Sign-out
|
|
1395
1413
|
|
|
1396
1414
|
`signOut(requestId)` revokes or invalidates the provider session where supported, deletes stored
|
|
1397
|
-
session secrets, and moves state to `signed-out`.
|
|
1415
|
+
session secrets, and moves state to `signed-out`.
|
|
1398
1416
|
`close()` stops authentication-state observation but does not sign the agent out.
|
|
1399
1417
|
|
|
1400
1418
|
### Secure-storage boundary
|
|
@@ -1471,8 +1489,8 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
1471
1489
|
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. |
|
|
1472
1490
|
| `dial(request)` | The manifest declares `idleCapabilities.dial`. |
|
|
1473
1491
|
| `requestBreak(request)` | `sessionCapabilities.breaks` is declared. |
|
|
1474
|
-
| `commitBreak(
|
|
1475
|
-
| `cancelBreak(
|
|
1492
|
+
| `commitBreak()` | `sessionCapabilities.breaks` is declared. Commit and cancel are not optional halves of it. |
|
|
1493
|
+
| `cancelBreak()` | `sessionCapabilities.breaks` is declared. |
|
|
1476
1494
|
| `endBreak()` | `sessionCapabilities.breaks` is declared. |
|
|
1477
1495
|
| `executeTeamBreak(command)` | The adapter publishes a `TeamRoster` carrying `breakControl`. |
|
|
1478
1496
|
| `executeTeamConsult(command)` | The adapter publishes a `TeamRoster` carrying `consultControl`. |
|
|
@@ -2158,11 +2176,9 @@ Custom capabilities must not redefine the meaning of a standard channel capabili
|
|
|
2158
2176
|
Starts one outbound call from the idle dialpad. It is present only when the voice provider
|
|
2159
2177
|
declares `dial`.
|
|
2160
2178
|
|
|
2161
|
-
- `commandId` remains stable across retries; the provider must place at most one call for it.
|
|
2162
2179
|
- `destination` is the original number selected or entered by the agent.
|
|
2163
2180
|
- `source` is `contact` or `manual` and must comply with `destinationPolicy`.
|
|
2164
2181
|
- `dialled` confirms that outbound call creation completed.
|
|
2165
|
-
- `already-dialled` confirms a retry that placed no second call.
|
|
2166
2182
|
- `failed` contains a `ProtocolFailure` and confirms no call was placed.
|
|
2167
2183
|
|
|
2168
2184
|
The resulting call is offered through the normal `task-offered` event. A successful dial result
|
|
@@ -2178,7 +2194,6 @@ nothing while none are being accepted — so they are not published separately.
|
|
|
2178
2194
|
| Field | Contract |
|
|
2179
2195
|
| --- | --- |
|
|
2180
2196
|
| `approval` | Where the agent's current request stands. See the states below. |
|
|
2181
|
-
| `requestId` | Correlates an agent-requested break while approval is `awaiting-decision`, `granted`, `starting-after-task`, or `in-effect`. Omitted for imposed breaks and when no request is active. |
|
|
2182
2197
|
| `accepting` | Whether the agent may ask at all. Distinct from `approval`. |
|
|
2183
2198
|
| `refusedReason` | Display-ready reason shown when `accepting` is false — a standing gate that applies to everyone. |
|
|
2184
2199
|
| `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
|
|
@@ -2204,8 +2219,8 @@ answer and ask again when they want to. `decisionReason` may carry the words att
|
|
|
2204
2219
|
decision, but `approval` does not remain denied.
|
|
2205
2220
|
|
|
2206
2221
|
A provider reports `starting-after-task` only after Omni commits a `granted` request while
|
|
2207
|
-
work is still active. Omni does not
|
|
2208
|
-
it; it
|
|
2222
|
+
work is still active. Omni does not send the request again, because asking again would not move
|
|
2223
|
+
it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
|
|
2209
2224
|
|
|
2210
2225
|
`accepting: false` is what lets Omni withdraw the control rather than let an agent ask and be
|
|
2211
2226
|
refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
|
|
@@ -2252,13 +2267,13 @@ branched on by code — in a log, a support ticket, a conformance failure — so
|
|
|
2252
2267
|
rather than that something happened. `failed` is shared, because failing is the same act
|
|
2253
2268
|
everywhere; success is not.
|
|
2254
2269
|
|
|
2255
|
-
| Method | Succeeded |
|
|
2256
|
-
| --- | --- |
|
|
2257
|
-
| `setCapacity` | `accepted` |
|
|
2258
|
-
| `requestBreak` | `requested` |
|
|
2259
|
-
| `commitBreak` | `committed` |
|
|
2260
|
-
| `cancelBreak` | `cancelled` |
|
|
2261
|
-
| `endBreak` | `ended` |
|
|
2270
|
+
| Method | Succeeded |
|
|
2271
|
+
| --- | --- |
|
|
2272
|
+
| `setCapacity` | `accepted` |
|
|
2273
|
+
| `requestBreak` | `requested` |
|
|
2274
|
+
| `commitBreak` | `committed` |
|
|
2275
|
+
| `cancelBreak` | `cancelled` |
|
|
2276
|
+
| `endBreak` | `ended` |
|
|
2262
2277
|
|
|
2263
2278
|
`failed` carries a typed `ProtocolFailure` and means the provider did not take the action, whether
|
|
2264
2279
|
it would not or could not.
|
|
@@ -2269,15 +2284,7 @@ again.
|
|
|
2269
2284
|
Every break method reports its real result through `break-state`; `setCapacity` reports none at
|
|
2270
2285
|
all, because capacity is a statement rather than a request.
|
|
2271
2286
|
|
|
2272
|
-
`
|
|
2273
|
-
accumulates, and re-sending the current one changes nothing. The four break methods carry a
|
|
2274
|
-
`requestId`
|
|
2275
|
-
precisely so a retry can be recognised, and `already-committed` is the one commit recovery lives
|
|
2276
|
-
on — retrying `commitBreak` into a partially delivered attempt, it is the difference between *I
|
|
2277
|
-
have committed now* and *I committed before you asked*, which is how Omni knows the attempt has
|
|
2278
|
-
converged rather than only that a message arrived.
|
|
2279
|
-
|
|
2280
|
-
`execute` keeps `applied` and `already-applied` rather than a verb per command, because the command
|
|
2287
|
+
`execute` keeps `applied` rather than a verb per command, because the command
|
|
2281
2288
|
is in the request: `execute({ command: { type: "hold" } })` returning `applied` already says the
|
|
2282
2289
|
hold applied. A `held` result would repeat the discriminant that travelled with it.
|
|
2283
2290
|
|
|
@@ -2297,8 +2304,7 @@ go, and a provider that waits for it will stall.
|
|
|
2297
2304
|
Your own tasks are the only ones you count. What the agent holds at other providers is not your
|
|
2298
2305
|
concern — Omni set `count` knowing it.
|
|
2299
2306
|
|
|
2300
|
-
Capacity supersedes rather than accumulates
|
|
2301
|
-
the latest value is the ceiling.
|
|
2307
|
+
Capacity supersedes rather than accumulates: the latest value is the ceiling.
|
|
2302
2308
|
|
|
2303
2309
|
**Capacity gates what the provider allocates, not what the agent starts.** A call placed from the
|
|
2304
2310
|
idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
|
|
@@ -2306,9 +2312,8 @@ it: the ceiling binds allocation, not the agent's own hand.
|
|
|
2306
2312
|
|
|
2307
2313
|
### `requestBreak(request)`
|
|
2308
2314
|
|
|
2309
|
-
Requests permission to stop the agent later; it does not itself stop work.
|
|
2310
|
-
|
|
2311
|
-
`awaiting-decision` or `granted` through `break-state` events. If the request is denied, the
|
|
2315
|
+
Requests permission to stop the agent later; it does not itself stop work. The provider continues
|
|
2316
|
+
offering work and reports `awaiting-decision` or `granted` through `break-state` events. If the request is denied, the
|
|
2312
2317
|
provider reports `not-requested` directly, with `decisionReason` when one was supplied.
|
|
2313
2318
|
|
|
2314
2319
|
#### Break reasons
|
|
@@ -2427,26 +2432,28 @@ Omni coordinates one attempt as follows:
|
|
|
2427
2432
|
1. Freeze the participant set to every connected provider from which the agent can currently
|
|
2428
2433
|
receive work. A provider joining during the attempt is given no capacity until it finishes.
|
|
2429
2434
|
2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
|
|
2430
|
-
3. Send one `requestBreak` to every participant
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
`not-requested` and causes Omni to take the cancel path.
|
|
2435
|
+
3. Send one `requestBreak` to every participant. A provider reports `awaiting-decision` or
|
|
2436
|
+
`granted`; neither state stops work. A denial transitions directly to `not-requested` and
|
|
2437
|
+
causes Omni to take the cancel path.
|
|
2434
2438
|
4. If every participant reports `granted`, durably choose commit, enter `committing-break`,
|
|
2435
|
-
and send `commitBreak(
|
|
2436
|
-
and reports `starting-after-task` or `in-effect`.
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2439
|
+
and send `commitBreak()` to every participant. A provider then stops offering new work
|
|
2440
|
+
and reports `starting-after-task` or `in-effect`. The **commit bound** — ten seconds from the
|
|
2441
|
+
decision, tunable per deployment — decides who is kept: a participant that has not applied the
|
|
2442
|
+
commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
|
|
2443
|
+
begins without it. `in-effect` decides `on-break`: Omni enters it once every kept participant
|
|
2444
|
+
reports `in-effect`. A kept participant reporting `starting-after-task` has applied the commit
|
|
2445
|
+
and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
|
|
2446
|
+
settled and beginning when the task ends, and offers no cancel, because the commit is durable.
|
|
2440
2447
|
5. If any participant fails or denies the request, cannot be reconciled within the bounded
|
|
2441
2448
|
decision timeout, or the agent cancels before commit, durably choose cancel and enter
|
|
2442
|
-
`cancelling-break`. Send `cancelBreak(
|
|
2449
|
+
`cancelling-break`. Send `cancelBreak()` to every participant still reporting
|
|
2443
2450
|
`awaiting-decision` or `granted`. Work continues during cancellation because no stop was
|
|
2444
2451
|
committed. Return to `working` only after no participant retains either state.
|
|
2445
2452
|
|
|
2446
2453
|
Commit and cancel are mutually exclusive decisions for one attempt. Once Omni chooses commit it
|
|
2447
|
-
never rolls that attempt back:
|
|
2448
|
-
|
|
2449
|
-
|
|
2454
|
+
never rolls that attempt back: it is reconciled by snapshot until every participant is stopped.
|
|
2455
|
+
A provider that reports `granted` must therefore preserve the request across reconnects and must
|
|
2456
|
+
honour a later commit or cancel. This
|
|
2450
2457
|
durable promise prevents a provider from failing the commit after another provider has already
|
|
2451
2458
|
stopped the agent.
|
|
2452
2459
|
|
|
@@ -2463,18 +2470,25 @@ on one platform while another keeps routing work to them — and **a provider Om
|
|
|
2463
2470
|
routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
|
|
2464
2471
|
for it costs the agent their break.
|
|
2465
2472
|
|
|
2466
|
-
Setting a participant aside is not a rollback and not a cancel. The commit stands
|
|
2467
|
-
stands
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2473
|
+
Setting a participant aside is not a rollback and not a cancel. The commit stands and the
|
|
2474
|
+
obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
|
|
2475
|
+
snapshot before anything else, and the snapshot decides:
|
|
2476
|
+
|
|
2477
|
+
- `in-effect` or `starting-after-task` — the commit arrived after all. Nothing to send.
|
|
2478
|
+
- still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
|
|
2479
|
+
late behind it, the provider stops an agent who is already stopped; nothing happens, because a
|
|
2480
|
+
commit is a state to be in, not an act to be done.
|
|
2481
|
+
- `not-requested` — the grant did not survive. Omni makes a new request for that provider alone,
|
|
2482
|
+
against an agent who is already on break elsewhere. **A new login is this case too**: the grant
|
|
2483
|
+
belonged to the old session.
|
|
2484
|
+
|
|
2485
|
+
The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
|
|
2486
|
+
no capacity until it is reconciled.
|
|
2471
2487
|
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
recover that attempt against a fresh session. It makes a new request for that provider alone,
|
|
2477
|
-
against an agent who is already on break elsewhere.
|
|
2488
|
+
**If the agent has already ended the break elsewhere, the attempt is over** and the returning
|
|
2489
|
+
provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
|
|
2490
|
+
would stop an agent who is working again; `starting-after-task` or `in-effect` gets `endBreak()`.
|
|
2491
|
+
Never rolling back is about a break that is still on, not one the agent has finished.
|
|
2478
2492
|
|
|
2479
2493
|
Omni may tell the agent which platforms the break has not yet reached, as it already does when a
|
|
2480
2494
|
break cannot be paired across every provider.
|
|
@@ -2494,7 +2508,7 @@ soon as it reaches `granted`; it does not wait for unanimity because the agent h
|
|
|
2494
2508
|
stopped elsewhere.
|
|
2495
2509
|
|
|
2496
2510
|
This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
|
|
2497
|
-
the durable commit decision and
|
|
2511
|
+
the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
|
|
2498
2512
|
|
|
2499
2513
|
#### Reporting the break the agent is on
|
|
2500
2514
|
|
|
@@ -2505,19 +2519,19 @@ the agent on the break itself, the provider is the only one who knows.
|
|
|
2505
2519
|
Omit it when you cannot say, and when there is no break: reporting a reason alongside
|
|
2506
2520
|
`approval: "not-requested"` describes a break that is not happening, and is rejected.
|
|
2507
2521
|
|
|
2508
|
-
### `cancelBreak(
|
|
2522
|
+
### `cancelBreak()`
|
|
2509
2523
|
|
|
2510
|
-
Cancels the active pre-commit request
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2524
|
+
Cancels the active pre-commit request while its approval is `awaiting-decision` or `granted`.
|
|
2525
|
+
Cancellation releases the request but does not restore work because work never stopped. If
|
|
2526
|
+
commit already won, the provider returns `omni.break-already-committed`. The resulting state is
|
|
2527
|
+
reported through `break-state`.
|
|
2514
2528
|
|
|
2515
|
-
### `commitBreak(
|
|
2529
|
+
### `commitBreak()`
|
|
2516
2530
|
|
|
2517
|
-
Commits the
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2531
|
+
Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
|
|
2532
|
+
business reason. On commit the provider stops offering new work and reports
|
|
2533
|
+
`starting-after-task` while existing work finishes, or `in-effect` when the break is in effect.
|
|
2534
|
+
Committing a break that is already in effect changes nothing and answers `committed`.
|
|
2521
2535
|
|
|
2522
2536
|
### `endBreak()`
|
|
2523
2537
|
|
|
@@ -2603,7 +2617,7 @@ asks for a manager, a moment the agent wants a second pair of ears. The capabili
|
|
|
2603
2617
|
team, and a second lead method beside `executeTeamBreak`:
|
|
2604
2618
|
|
|
2605
2619
|
```ts
|
|
2606
|
-
executeTeamConsult({
|
|
2620
|
+
executeTeamConsult({ command: TeamConsultCommand }): Promise<TeamCommandResult>
|
|
2607
2621
|
```
|
|
2608
2622
|
|
|
2609
2623
|
Required when the roster carries `consultControl`, and gated by it exactly as `executeTeamBreak`
|
|
@@ -2611,15 +2625,15 @@ is by `breakControl`. The flow, in order:
|
|
|
2611
2625
|
|
|
2612
2626
|
```ts
|
|
2613
2627
|
// 1. The agent asks, with a small note. Their task carries `lead` from here on.
|
|
2614
|
-
execute({
|
|
2628
|
+
execute({ taskId: "call-42", command: { type: "lead", action: "request", note: "Refund dispute, needs approval" } })
|
|
2615
2629
|
// task.lead = { status: "requested", note: "Refund dispute, needs approval", since }
|
|
2616
2630
|
|
|
2617
2631
|
// 2. Every lead entitled to it sees the request on their roster.
|
|
2618
2632
|
// team-updated: requests: [{ id: "req-7", memberId: "A-1", taskId: "call-42", note, since }]
|
|
2619
2633
|
|
|
2620
2634
|
// 3. A lead joins, or declines.
|
|
2621
|
-
executeTeamConsult({
|
|
2622
|
-
executeTeamConsult({
|
|
2635
|
+
executeTeamConsult({ command: { type: "join", requestId: "req-7" } })
|
|
2636
|
+
executeTeamConsult({ command: { type: "decline", requestId: "req-7", reason: "In a call" } })
|
|
2623
2637
|
```
|
|
2624
2638
|
|
|
2625
2639
|
**On `join` the provider bridges three parties and the lead is on a task of their own**, on the
|
|
@@ -2779,12 +2793,11 @@ Command names follow the channel's operational vocabulary, and each channel's co
|
|
|
2779
2793
|
discriminated by `type` — the same discriminant `executeTeamBreak` and `custom` already use. The
|
|
2780
2794
|
unions are declared under **Shapes**.
|
|
2781
2795
|
|
|
2782
|
-
`taskId` is not repeated on the command. It travels on the `TaskCommandRequest` around it
|
|
2783
|
-
`commandId`.
|
|
2796
|
+
`taskId` is not repeated on the command. It travels on the `TaskCommandRequest` around it.
|
|
2784
2797
|
|
|
2785
|
-
**A toggle carries the state it wants, not a flip.** Inverting whatever is found cannot
|
|
2786
|
-
|
|
2787
|
-
|
|
2798
|
+
**A toggle carries the state it wants, not a flip.** Inverting whatever is found cannot converge
|
|
2799
|
+
with a stale view: a flip against a state the provider has already changed turns something on and
|
|
2800
|
+
then off again. `mute` therefore carries `muted`, and a custom `toggle` control carries its own
|
|
2788
2801
|
boolean. `hold` and `resume`, `pause` and `resume` need no flag, being pairs rather than toggles.
|
|
2789
2802
|
|
|
2790
2803
|
**`complete` sends a disposition only where one was published.** `disposition` is a
|
|
@@ -2807,8 +2820,8 @@ is already done; a failure means only that the provider did not record it, leavi
|
|
|
2807
2820
|
until the next snapshot. That is the safe direction to fail in, and it is the one place where
|
|
2808
2821
|
`failed` does not mean *nothing happened* — everywhere else it does.
|
|
2809
2822
|
|
|
2810
|
-
`mute` carries `muted` rather than flipping, so a
|
|
2811
|
-
|
|
2823
|
+
`mute` carries `muted` rather than flipping, so a stale view converges on the stated state
|
|
2824
|
+
instead of flipping it back — see **Task commands**.
|
|
2812
2825
|
|
|
2813
2826
|
### Which commands need a capability
|
|
2814
2827
|
|
|
@@ -2836,22 +2849,23 @@ confirms the end with `task-ended` and a `cancelled` outcome.
|
|
|
2836
2849
|
|
|
2837
2850
|
Applies a `TaskCommandRequest` to one provider-local task.
|
|
2838
2851
|
|
|
2839
|
-
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2852
|
+
- Omni serializes commands per task and sends the next only after the previous settled or was
|
|
2853
|
+
given up as unknown; it stops sending when `task-ended` arrives.
|
|
2854
|
+
- `applied` confirms the side effect completed. `failed` confirms it did **not**, with a typed
|
|
2855
|
+
`ProtocolFailure`; a provider that will not and one that cannot report the same shape, and `code`
|
|
2856
|
+
says which.
|
|
2857
|
+
- A command sent while `provider-status` is not `active` answers `failed` with `omni.unavailable`.
|
|
2858
|
+
Neither Omni nor the adapter queues it.
|
|
2859
|
+
- **A command that asks for a state answers `applied` when that state holds, whoever brought it
|
|
2860
|
+
about; a command that acts answers `failed` when it cannot act.** Declining a lead request
|
|
2861
|
+
already gone is `applied`; joining one already gone is `failed`, since nobody joined. A lead
|
|
2862
|
+
deciding a member's break that another lead has already decided is `applied` when the decisions
|
|
2863
|
+
agree and `failed`, saying so in `message`, when they differ. `commitBreak()` on a break already
|
|
2864
|
+
in effect is `committed` for the same reason.
|
|
2851
2865
|
- **A settled result is a fact; an unsettled promise is not.** Transport uncertainty may reject the
|
|
2852
|
-
promise with no result at all, and that means *unknown*, not *failed
|
|
2853
|
-
|
|
2854
|
-
|
|
2866
|
+
promise with no result at all, and that means *unknown*, not *failed*, and a snapshot follows —
|
|
2867
|
+
see **An unsettled result is unknown**. `failed` must never be returned for something the
|
|
2868
|
+
provider is unsure of, because Omni will show the agent it did not happen.
|
|
2855
2869
|
|
|
2856
2870
|
### `ProtocolFailure`
|
|
2857
2871
|
|
|
@@ -2873,7 +2887,7 @@ react rather than only display the message:
|
|
|
2873
2887
|
| `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
|
|
2874
2888
|
| `omni.destination-not-permitted` | The dial or transfer destination violates the provider's policy. |
|
|
2875
2889
|
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
2876
|
-
| `omni.unavailable` | The provider is temporarily unable to serve the action
|
|
2890
|
+
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `provider-status` is not `active`. |
|
|
2877
2891
|
| `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
|
|
2878
2892
|
|
|
2879
2893
|
They are published as `OMNI_FAILURE_CODES`.
|
|
@@ -2930,9 +2944,10 @@ healthy provider from a dead one.
|
|
|
2930
2944
|
|
|
2931
2945
|
#### Requesting a resync
|
|
2932
2946
|
|
|
2933
|
-
Omni
|
|
2934
|
-
|
|
2935
|
-
|
|
2947
|
+
Omni calls `snapshot()` at connect; after that, snapshots come to it — on reconnect, and when the
|
|
2948
|
+
provider asks. It may still call `snapshot()` at any time, but never to learn a command's fate:
|
|
2949
|
+
the reconnect snapshot already carries it. `reason: "provider-requested"` covers the opposite
|
|
2950
|
+
direction — the provider asking Omni to reconcile — and neither replaces the other.
|
|
2936
2951
|
|
|
2937
2952
|
### `snapshot`
|
|
2938
2953
|
|
|
@@ -2974,9 +2989,9 @@ reasons, retry details, and any imposed break.
|
|
|
2974
2989
|
For a multi-provider attempt, "every provider" is the participant set frozen when the attempt
|
|
2975
2990
|
entered `requesting-break`. Omni commits only after every participant reports `granted` —
|
|
2976
2991
|
that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
|
|
2977
|
-
`on-break` once every participant
|
|
2978
|
-
|
|
2979
|
-
|
|
2992
|
+
`on-break` once every kept participant reports `in-effect`; the commit bound decides who is kept,
|
|
2993
|
+
setting aside a participant that has not applied the commit rather than holding a break that has
|
|
2994
|
+
already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
|
|
2980
2995
|
multi-provider break**.
|
|
2981
2996
|
|
|
2982
2997
|
### `task-offered`
|
|
@@ -3125,12 +3140,6 @@ Two properties of the harness matter to adapter authors:
|
|
|
3125
3140
|
`close()` run in a `finally` block, and a throw from any of them is reported as
|
|
3126
3141
|
`disconnectWasClean: false` rather than being hidden.
|
|
3127
3142
|
|
|
3128
|
-
### `assertCommandIdempotency(connection, request)`
|
|
3129
|
-
|
|
3130
|
-
Issues the same command twice and verifies that the first call applies (or was already applied)
|
|
3131
|
-
and the retry returns `already-applied`. Use a deterministic test task because this helper invokes
|
|
3132
|
-
the adapter command method twice.
|
|
3133
|
-
|
|
3134
3143
|
### Contract scenarios
|
|
3135
3144
|
|
|
3136
3145
|
The testing entry point also exports deterministic, reusable checks for lifecycle behavior that
|
|
@@ -3141,8 +3150,6 @@ cannot be established from TypeScript structure alone.
|
|
|
3141
3150
|
| `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. |
|
|
3142
3151
|
| `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
|
|
3143
3152
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
3144
|
-
| `assertCommandIdempotency(connection, request)` | Retrying a task command does not repeat its side effect. |
|
|
3145
|
-
| `assertDialIdempotency(connection, request)` | Retrying a dial command does not place another call. |
|
|
3146
3153
|
| `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. |
|
|
3147
3154
|
| `assertBrowserIsolationAndReuse(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
|
|
3148
3155
|
| `assertNoBrowserSessionKeyCollisions(scenarios)` | No two distinct scenarios derive the same session key. Feed it adversarial names. |
|