@xema/omni-protocol 0.1.82 → 0.1.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +81 -35
- package/dist/index.js +7 -7
- package/dist/testing.d.ts +10 -7
- package/dist/testing.js +93 -86
- package/dist/validation.d.ts +34 -1
- package/dist/validation.js +368 -92
- package/guide.md +511 -172
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -22,10 +22,12 @@ are used precisely throughout and mean nothing looser here.
|
|
|
22
22
|
| **Host** | Omni, named that way where the contrast with a provider is the point — *the host carries the audio*, *host-side input*. |
|
|
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
|
-
| **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id;
|
|
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
26
|
| **Lead** | An agent whose login declares `capabilities.team`. The provider publishes a `TeamRoster` 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
|
+
| **Interaction** | One agent’s part in the call or conversation. The same call may return to that agent for a new interaction. |
|
|
30
|
+
| **Task** | The agent’s assigned work and its workspace, controls and completion work. A voice task represents one interaction, not the caller’s whole call. |
|
|
29
31
|
| **Channel** | The kind of work a provider carries: `voice`, `chat`, or `email`. Fixed per provider by its manifest. |
|
|
30
32
|
| **Task type** | The provider's own name for a category of work — a queue, a mailbox folder, a chat source. Free-form, and finer-grained than a channel. |
|
|
31
33
|
| **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*; the provider performs it, and the host only offers it — see **Where a command executes**. |
|
|
@@ -118,7 +120,7 @@ identifier of its own for an agent or a manager, and none crosses this boundary
|
|
|
118
120
|
operating-system account, not a directory identity, not a licence.
|
|
119
121
|
|
|
120
122
|
So a `UserId` means nothing outside the provider that issued it. Provider A's
|
|
121
|
-
`
|
|
123
|
+
`interactionHistory[].by` and provider B's roster `memberId` are unrelated strings that will
|
|
122
124
|
eventually collide, and one person on several providers has several identities that nothing here
|
|
123
125
|
pairs. Scope every user identifier with its provider ID before storing or comparing it, exactly as
|
|
124
126
|
`taskKey()` already does for tasks — see `userKey()` under **Utilities**.
|
|
@@ -189,13 +191,15 @@ type Manifest<C extends Channel = Channel> = {
|
|
|
189
191
|
supportedProtocolVersions: number[];
|
|
190
192
|
authenticationMethods: AuthenticationMethod[];
|
|
191
193
|
idleCapabilities?: IdleCapabilities<C>;
|
|
194
|
+
timeCheck?: true;
|
|
195
|
+
timestampAuthority?: "provider";
|
|
192
196
|
phaseLabels?: TaskPhaseLabels;
|
|
193
197
|
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
194
198
|
orgLevels?: LevelDeclaration[];
|
|
195
199
|
dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
|
|
196
200
|
phones?: C extends "voice" ? Phone[] : never;
|
|
197
201
|
runningStepReports?: true;
|
|
198
|
-
|
|
202
|
+
completionSettleMs: number;
|
|
199
203
|
};
|
|
200
204
|
```
|
|
201
205
|
|
|
@@ -350,7 +354,15 @@ type HostGuarantees = {
|
|
|
350
354
|
personConsent?: true;
|
|
351
355
|
};
|
|
352
356
|
|
|
357
|
+
type ProviderTimeScope = { providerId: string; loginId: string };
|
|
358
|
+
type ProviderTimeEstimate = ProviderTimeScope & {
|
|
359
|
+
at: IsoTimestamp;
|
|
360
|
+
clockId: string;
|
|
361
|
+
uncertaintyMs?: number;
|
|
362
|
+
};
|
|
363
|
+
|
|
353
364
|
type Host = {
|
|
365
|
+
estimateProviderTime?(scope: ProviderTimeScope): ProviderTimeEstimate | undefined;
|
|
354
366
|
recording?: HostRecording;
|
|
355
367
|
guarantees: HostGuarantees;
|
|
356
368
|
mute?: HostMute;
|
|
@@ -449,16 +461,16 @@ type SetPreferenceRequest =
|
|
|
449
461
|
| { id: PreferenceId; enabled: boolean }
|
|
450
462
|
| { id: PreferenceId; inherit: true };
|
|
451
463
|
|
|
452
|
-
type
|
|
464
|
+
type InteractionReport = { taskId: TaskId; allocationId: AllocationId; at: IsoTimestamp } & (
|
|
453
465
|
| { step: "muted"; mutedBy: MutedBy }
|
|
454
|
-
| { step: Exclude<
|
|
466
|
+
| { step: Exclude<InteractionStep, "muted">; mutedBy?: never }
|
|
455
467
|
) & (
|
|
456
468
|
| { ended: true; seconds: DurationSeconds }
|
|
457
469
|
| { ended?: never; seconds?: DurationSeconds }
|
|
458
470
|
);
|
|
459
471
|
|
|
460
|
-
type
|
|
461
|
-
| { status: "recorded" }
|
|
472
|
+
type InteractionReportResult =
|
|
473
|
+
| { status: "recorded"; at: IsoTimestamp }
|
|
462
474
|
| { status: "failed"; failure: ProtocolFailure };
|
|
463
475
|
|
|
464
476
|
type PreferenceResult =
|
|
@@ -636,7 +648,7 @@ type TaskAttribute = TaskAttributeBase & (
|
|
|
636
648
|
| { type: "timestamp"; at: IsoTimestamp }
|
|
637
649
|
);
|
|
638
650
|
|
|
639
|
-
type
|
|
651
|
+
type InteractionStep =
|
|
640
652
|
| "queued"
|
|
641
653
|
| "offered"
|
|
642
654
|
| "answered"
|
|
@@ -646,16 +658,16 @@ type HandlingStep =
|
|
|
646
658
|
| "conferenced"
|
|
647
659
|
| "unanswered";
|
|
648
660
|
|
|
649
|
-
type
|
|
650
|
-
steps:
|
|
651
|
-
|
|
661
|
+
type TaskInteractionHistory = {
|
|
662
|
+
steps: TaskInteractionStep[];
|
|
663
|
+
interactionSeconds?: DurationSeconds;
|
|
652
664
|
holdSeconds?: DurationSeconds;
|
|
653
665
|
queueSeconds?: DurationSeconds;
|
|
654
666
|
transfers?: number;
|
|
655
667
|
};
|
|
656
668
|
|
|
657
|
-
type
|
|
658
|
-
step:
|
|
669
|
+
type TaskInteractionStep = {
|
|
670
|
+
step: InteractionStep;
|
|
659
671
|
at: IsoTimestamp;
|
|
660
672
|
dialId?: DialId;
|
|
661
673
|
destinationId?: string;
|
|
@@ -732,14 +744,14 @@ type Task<C extends Channel = Channel> = {
|
|
|
732
744
|
atDeadline?: PreviewDeadline;
|
|
733
745
|
reference?: string;
|
|
734
746
|
attributes?: TaskAttribute[];
|
|
735
|
-
|
|
747
|
+
interactionHistory?: TaskInteractionHistory;
|
|
736
748
|
} & TaskCompletion & (
|
|
737
749
|
C extends "voice"
|
|
738
750
|
? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting; monitoring?: TaskMonitoring; media?: TaskMediaState }
|
|
739
751
|
: { recording?: never; onCall?: never; leadAssist?: never; assisting?: never; monitoring?: never; media?: never }
|
|
740
752
|
);
|
|
741
753
|
|
|
742
|
-
type PreviewDeadline = "calls" | "
|
|
754
|
+
type PreviewDeadline = "calls" | "host-calls" | "waits";
|
|
743
755
|
|
|
744
756
|
type AcceptanceMode =
|
|
745
757
|
| "no-preference"
|
|
@@ -1128,7 +1140,22 @@ type Refusal = {
|
|
|
1128
1140
|
violations: ProtocolViolation[];
|
|
1129
1141
|
};
|
|
1130
1142
|
|
|
1143
|
+
type ProviderTimeCheckPolicy = {
|
|
1144
|
+
intervalMs: number;
|
|
1145
|
+
timeoutMs: number;
|
|
1146
|
+
maxRoundTripMs: number;
|
|
1147
|
+
maxSampleAgeMs: number;
|
|
1148
|
+
};
|
|
1149
|
+
type ProviderTimeCheckRequest = { requestId: string };
|
|
1150
|
+
type ProviderTimeCheckResult = {
|
|
1151
|
+
requestId: string;
|
|
1152
|
+
loginId: string;
|
|
1153
|
+
clockId: string;
|
|
1154
|
+
providerTime: IsoTimestamp;
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1131
1157
|
type Connection<C extends Channel = Channel> = {
|
|
1158
|
+
checkTime?(request: ProviderTimeCheckRequest): Promise<ProviderTimeCheckResult>;
|
|
1132
1159
|
snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
|
|
1133
1160
|
subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
|
|
1134
1161
|
refused(report: Refusal): void;
|
|
@@ -1149,7 +1176,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
1149
1176
|
executeTeamMonitor?(request: TeamMonitorCommandRequest): Promise<TeamCommandResult>;
|
|
1150
1177
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1151
1178
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1152
|
-
recordStep?(report:
|
|
1179
|
+
recordStep?(report: InteractionReport): Promise<InteractionReportResult>;
|
|
1153
1180
|
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
1154
1181
|
};
|
|
1155
1182
|
|
|
@@ -1197,7 +1224,7 @@ const BREAK_KINDS = [
|
|
|
1197
1224
|
|
|
1198
1225
|
type BreakKind = (typeof BREAK_KINDS)[number];
|
|
1199
1226
|
|
|
1200
|
-
const
|
|
1227
|
+
const INTERACTION_STEPS_WITH_A_PERSON = [
|
|
1201
1228
|
"offered",
|
|
1202
1229
|
"answered",
|
|
1203
1230
|
"held",
|
|
@@ -1220,8 +1247,8 @@ const OMNI_FAILURE_CODES = [
|
|
|
1220
1247
|
type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1221
1248
|
```
|
|
1222
1249
|
|
|
1223
|
-
`
|
|
1224
|
-
takes part in. `
|
|
1250
|
+
`INTERACTION_STEPS_WITH_A_PERSON` is every `InteractionStep` except `queued`, which is the one nobody
|
|
1251
|
+
takes part in. `interactionStepExpectsAPerson()` tests membership.
|
|
1225
1252
|
|
|
1226
1253
|
### Failure and validation
|
|
1227
1254
|
|
|
@@ -1456,7 +1483,7 @@ fired into a state that has moved on.
|
|
|
1456
1483
|
Allocate only within the concurrent capacity Omni has stated for this agent.
|
|
1457
1484
|
|
|
1458
1485
|
An allocation beyond that capacity, or with none currently stated, is invalid and Omni rejects it
|
|
1459
|
-
as a protocol violation. Work the agent was already
|
|
1486
|
+
as a protocol violation. Work the agent was already working on when the connection came back is
|
|
1460
1487
|
reported in the snapshot; it is not an allocation.
|
|
1461
1488
|
|
|
1462
1489
|
### 8. State is authoritative at the provider
|
|
@@ -1467,7 +1494,7 @@ replaces its local provider view; it does not overwrite the provider.
|
|
|
1467
1494
|
|
|
1468
1495
|
### 9. Define a vocabulary; do not merely list it
|
|
1469
1496
|
|
|
1470
|
-
Every member of a closed set — break kinds,
|
|
1497
|
+
Every member of a closed set — break kinds, interaction steps, destination kinds — must have a
|
|
1471
1498
|
normative definition; matching names alone do not establish shared meaning.
|
|
1472
1499
|
|
|
1473
1500
|
### 10. Order on the wire is display order
|
|
@@ -1548,7 +1575,7 @@ export default defineAdapter({
|
|
|
1548
1575
|
channel: "voice",
|
|
1549
1576
|
supportedProtocolVersions: [OMNI_PROTOCOL_VERSION],
|
|
1550
1577
|
authenticationMethods: ["browser-sso"],
|
|
1551
|
-
|
|
1578
|
+
completionSettleMs: 5000,
|
|
1552
1579
|
idleCapabilities: {
|
|
1553
1580
|
dial: { destinations: "any-number" },
|
|
1554
1581
|
},
|
|
@@ -1583,8 +1610,10 @@ compile time.
|
|
|
1583
1610
|
| `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label a desk shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
|
|
1584
1611
|
| `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the host) and/or `deskPhone` (a handset the platform rings; the host shows the call and opens nothing). The host picks one per login. See **How the agent hears the call**. |
|
|
1585
1612
|
| `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that transfer, conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
|
|
1613
|
+
| `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; host polling is independently opt-in. |
|
|
1614
|
+
| `timestampAuthority` | Optional `"provider"`: provider timestamps are final; host instants are advisory. Omission makes no trust promise. |
|
|
1586
1615
|
| `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
|
|
1587
|
-
| `
|
|
1616
|
+
| `completionSettleMs` | Required. How long after an applied completion -- `complete`, or a lead's `take-over` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1588
1617
|
|
|
1589
1618
|
### Authentication methods
|
|
1590
1619
|
|
|
@@ -1994,7 +2023,7 @@ failure:
|
|
|
1994
2023
|
```
|
|
1995
2024
|
|
|
1996
2025
|
The `User` it carries is the **root of this provider's user namespace**. Every other person this
|
|
1997
|
-
provider names — a roster member, the manager on an imposed break, the agent on
|
|
2026
|
+
provider names — a roster member, the manager on an imposed break, the agent on an interaction step —
|
|
1998
2027
|
is identified from the same directory and carries the same `UserId` type.
|
|
1999
2028
|
|
|
2000
2029
|
| Field | Contract |
|
|
@@ -2046,7 +2075,7 @@ Creates one live provider connection for the signed-in agent.
|
|
|
2046
2075
|
| `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login, stated by the host on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
2047
2076
|
| `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
|
|
2048
2077
|
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's host states what its Mute does, and a desk-phone or conversation login's host cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the host's**. |
|
|
2049
|
-
| `store` | The login's operational store, kept by the host for the life of the login, across a reload of the host, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the
|
|
2078
|
+
| `store` | The login's operational store, kept by the host for the life of the login, across a reload of the host, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs a host reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store a host may clear aggressively. **A task's keys carry the task id and go with the task**: a key written about a task names the task's id in the key, and is deleted before the task's end is published, because a platform retires a task id minutes after closing it and a requeue takes seconds, so a key that outlives its task is inherited by the next offer of the same id -- a record with legs the host never reported against it. A login-scoped key carries no task id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the host's, not the adapter's: a host reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.task-not-found` and writes nothing -- the host ends its own open legs at the task's end, so such a report is the host's error to see, and a host reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
2050
2079
|
| `host` | The host's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The host reports, the adapter decides**. |
|
|
2051
2080
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
2052
2081
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -2118,12 +2147,13 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2118
2147
|
| Method | Implement it when |
|
|
2119
2148
|
| --- | --- |
|
|
2120
2149
|
| `snapshot()` | Always. |
|
|
2150
|
+
| `checkTime(request)` | The manifest declares `timeCheck: true`. Read-only clock check; no task command or accuracy guarantee. |
|
|
2121
2151
|
| `subscribe(listener)` | Always. |
|
|
2122
2152
|
| `disconnect()` | Always. |
|
|
2123
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**. |
|
|
2124
2154
|
| `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
|
|
2125
2155
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
2126
|
-
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `
|
|
2156
|
+
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, 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. |
|
|
2127
2157
|
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
2128
2158
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
2129
2159
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
@@ -2182,13 +2212,13 @@ describeUsers(ids: UserId[]): Promise<User[]>
|
|
|
2182
2212
|
```
|
|
2183
2213
|
|
|
2184
2214
|
Required of any adapter that publishes a `UserId` — on `ImposedBreak.by`, a team roster, or
|
|
2185
|
-
`
|
|
2215
|
+
`interactionHistory[].by`. Publishing an identifier Omni cannot resolve puts a name on screen
|
|
2186
2216
|
that reads as a database key.
|
|
2187
2217
|
|
|
2188
2218
|
- **Omit an id you cannot resolve; do not invent a name for it.** A missing entry says *I do not
|
|
2189
2219
|
know this person*, which Omni renders as such. Ordering is not significant and the response may
|
|
2190
2220
|
be shorter than the request.
|
|
2191
|
-
- **Take the whole list in one call.** Omni resolves a roster or
|
|
2221
|
+
- **Take the whole list in one call.** Omni resolves a roster or an interaction history as a batch, and
|
|
2192
2222
|
a per-id round trip multiplies that by its length.
|
|
2193
2223
|
- **Omni caches a result for one hour, then resolves it again.** The identifier is stable across
|
|
2194
2224
|
logins but the name behind it is not, so the cache expires on a clock rather than living for the
|
|
@@ -2196,7 +2226,7 @@ that reads as a database key.
|
|
|
2196
2226
|
particular value, or on Omni asking again at any particular moment.
|
|
2197
2227
|
|
|
2198
2228
|
This is the only place a name comes from. Task data carries identifiers alone —
|
|
2199
|
-
`
|
|
2229
|
+
`interactionHistory[].by` is an id and nothing more — so a name is never copied into a task, never
|
|
2200
2230
|
duplicated across tasks, and never stale.
|
|
2201
2231
|
|
|
2202
2232
|
### `Connection.disconnect()`
|
|
@@ -2326,7 +2356,7 @@ The provider does not introduce new work as already `in-progress`.
|
|
|
2326
2356
|
|
|
2327
2357
|
A task appears already `in-progress` only in a snapshot taken after a reconnect or a resync,
|
|
2328
2358
|
reporting work that began earlier in this login and never stopped. A fresh login has none to
|
|
2329
|
-
report: nothing has been allocated yet, and work the agent was
|
|
2359
|
+
report: nothing has been allocated yet, and work the agent was working elsewhere is not carried
|
|
2330
2360
|
into a new session.
|
|
2331
2361
|
|
|
2332
2362
|
## Tasks
|
|
@@ -2344,24 +2374,24 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2344
2374
|
| Field | Contract |
|
|
2345
2375
|
| --- | --- |
|
|
2346
2376
|
| `id` | Required `TaskId`, the platform's own identity for the task. Unique among the tasks open at once; a platform retires an id minutes after closing it, so the same id comes back for another customer. Omni scopes it with the provider ID. |
|
|
2347
|
-
| `allocationId` | Required `AllocationId`: this life of the task, minted once per offer and never reused for the life of the login, whatever the id does. Every event, command and report that names a task names its allocation too, so a late dial outcome or a late
|
|
2377
|
+
| `allocationId` | Required `AllocationId`: this life of the task, minted once per offer and never reused for the life of the login, whatever the id does. Every event, command and report that names a task names its allocation too, so a late dial outcome or a late interaction report for the first customer never lands on the next under the same id. The stream refuses an offer reusing one (`stream.taskOffered.allocation`), an update restating another life of the id (`stream.taskUpdated.allocation`), and a media or ending event naming a life that has ended or that nobody has seen (`stream.allocation.ended`, `.unknown`); a `dial-outcome` may name an ended life, since a dial placed late routinely outlives its call, and the host routes it there. See **A task's life on the wire**. |
|
|
2348
2378
|
| `title` | Agent-facing task title. |
|
|
2349
|
-
| `channel` | Channel
|
|
2379
|
+
| `channel` | Channel used for this task. It must equal the source provider's manifest channel. |
|
|
2350
2380
|
| `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
|
|
2351
2381
|
| `capabilities` | Controls and workspace features available for this specific task. |
|
|
2352
2382
|
| `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `ungoverned` when nothing handed the work over -- an agent's own outbound -- and `undetermined` when a queue was named and its terms could not be read, in which case `capabilities` is what the provider will honour, not what the platform permits. A host shows `undetermined` where the agent works. See **Task capabilities**. |
|
|
2353
2383
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2354
2384
|
| `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
|
|
2355
2385
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2356
|
-
| `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. Media names a task whose work has begun, or whose party the host is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it; with the party ringing by a host dial, ring-back
|
|
2386
|
+
| `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. Media names a task whose work has begun, or whose party the host is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it; with the party ringing by a host dial or provider-triggered preview dial, actual ring-back media may precede answer. |
|
|
2357
2387
|
| `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
|
|
2358
|
-
| `previewEndsAt` | Voice only, in `preview`:
|
|
2359
|
-
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls`
|
|
2388
|
+
| `previewEndsAt` | Voice only, in `preview`: the preparation target instant. Absent, the agent has as long as they need without a preparation countdown. Always with `atDeadline`. See **Preview: the agent presses Call**. |
|
|
2389
|
+
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` makes the provider initiate dialing, `host-calls` makes the host issue Call, `waits` keeps the task in preview awaiting the agent. |
|
|
2360
2390
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2361
2391
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2362
|
-
| `wrapAllowance` | Fixed time allowed to complete the task after primary
|
|
2392
|
+
| `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
|
|
2363
2393
|
| `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
|
|
2364
|
-
| `
|
|
2394
|
+
| `interactionHistory` | The call record: `steps` — the ordered interaction history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `interactionSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **Interaction history**. |
|
|
2365
2395
|
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer -- carrying a `stage` while being dialled again on the same task, a connect-back with the host's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where a host placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2366
2396
|
| `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
|
|
2367
2397
|
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead-assist` `take-over` and `leave` issuable. See **Lead assist**. |
|
|
@@ -2399,7 +2429,7 @@ The canonical task transitions are:
|
|
|
2399
2429
|
| `confirmed` | Work begins | `in-progress` |
|
|
2400
2430
|
| `preview` | Agent presses Call (`call`) and the customer answers, or the deadline `calls` and they answer | `in-progress` |
|
|
2401
2431
|
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2402
|
-
| `preview` | The
|
|
2432
|
+
| `preview` | The preparation target passes with `atDeadline: "waits"` | Remains `preview`, waiting for the agent to press Call |
|
|
2403
2433
|
| `pending` | Provider withdraws the allocation, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
|
|
2404
2434
|
| `pending` | The offer lapses at `allocationExpiresAt` | Removed by `task-ended` with `expired` outcome naming `pending` |
|
|
2405
2435
|
| No task | Snapshot reports work already underway | `in-progress` |
|
|
@@ -2407,7 +2437,7 @@ The canonical task transitions are:
|
|
|
2407
2437
|
| `in-progress` | Agent starts a warm transfer (`transfer` `warm`); the customer is parked | `paused` |
|
|
2408
2438
|
| `paused` | Provider or agent resumes the task | `in-progress` |
|
|
2409
2439
|
| `paused` | Agent cancels a warm transfer (`transfer` `cancel`) | `in-progress` |
|
|
2410
|
-
| `in-progress` or `paused` | Contact
|
|
2440
|
+
| `in-progress` or `paused` | Contact interaction ends and follow-up work remains | `completing` |
|
|
2411
2441
|
| `completing` | Agent connects back to the party (`connect-back`) | `in-progress` |
|
|
2412
2442
|
| Any phase | Provider emits `task-ended` | Removed |
|
|
2413
2443
|
|
|
@@ -2416,7 +2446,7 @@ The canonical task transitions are:
|
|
|
2416
2446
|
A task id is the platform's, and platforms reuse them: a closed id is retired minutes later, a
|
|
2417
2447
|
requeue takes seconds, and on one platform a call was offered twice, thirteen seconds apart, under
|
|
2418
2448
|
one id, through a close and a re-offer. Everything that names a task after the fact -- a dial
|
|
2419
|
-
outcome, a media event, an ending,
|
|
2449
|
+
outcome, a media event, an ending, an interaction report, a command -- would land on whichever life of
|
|
2420
2450
|
the id is open when it arrives. So every offer mints an `allocationId`, unique for the life of the
|
|
2421
2451
|
login, and every one of those names it beside the `taskId`. The host keys its state on the pair, a
|
|
2422
2452
|
late event finds the life it belongs to or is refused, and a task-scoped browser session
|
|
@@ -2437,8 +2467,9 @@ command.
|
|
|
2437
2467
|
#### Preview: the agent presses Call
|
|
2438
2468
|
|
|
2439
2469
|
An outbound campaign that lets the agent see who they are about to call is a **preview**: the
|
|
2440
|
-
record arrives as a task
|
|
2441
|
-
out. The agent
|
|
2470
|
+
record arrives as a task and sits in `preview` with the party on it before a call goes
|
|
2471
|
+
out. The agent prepares; manual or declared automatic initiation may then begin dialing. It
|
|
2472
|
+
remains preview while ringing until an evidenced answer or non-answer outcome. The agent may press Call. That is the whole phase, and it exists on voice alone --
|
|
2442
2473
|
chat and email have no call to place, and their reading is ordinary work in `in-progress`.
|
|
2443
2474
|
|
|
2444
2475
|
```ts
|
|
@@ -2466,14 +2497,51 @@ no-answer as the disposition it is -- in a campaign that is the commonest outcom
|
|
|
2466
2497
|
is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
|
|
2467
2498
|
(`task.preview.dialOutcomes.required`).
|
|
2468
2499
|
|
|
2469
|
-
**
|
|
2470
|
-
`previewEndsAt
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2500
|
+
**Preparation and trigger ownership are per task.** Unlimited preparation omits both
|
|
2501
|
+
`previewEndsAt` and `atDeadline`: only an agent pressing Call starts dialing. Fixed preparation
|
|
2502
|
+
carries the provider's authoritative preparation-end instant and exactly one trigger owner:
|
|
2503
|
+
|
|
2504
|
+
| Declaration | At preparation end |
|
|
2505
|
+
| --- | --- |
|
|
2506
|
+
| `atDeadline: "calls"` | The provider initiates dialing. The host never sends a timer-triggered Call. |
|
|
2507
|
+
| `atDeadline: "host-calls"` | The host sends the ordinary `call` command with a fresh host `dialId`. The provider does not independently auto-dial this task. |
|
|
2508
|
+
| `atDeadline: "waits"` | Neither side auto-dials. The task remains in preview until the agent presses Call. |
|
|
2509
|
+
|
|
2510
|
+
The agent may press Call early in either fixed-preparation dialing mode, or at any time in
|
|
2511
|
+
`waits` mode. With `waits`, the countdown is a preparation target, not an expiry: when it reaches
|
|
2512
|
+
zero the UI says "Waiting for agent" and keeps Call available. There is no automatic task-ended,
|
|
2513
|
+
completion, dialing or phase change. No repeating timer action occurs on later snapshots. This differs
|
|
2514
|
+
from unlimited preparation only by displaying a preparation target. The former `expires` deadline
|
|
2515
|
+
value is not supported; elapsed preparation must not withdraw the task. For `calls` and `host-calls`, the deadline ends
|
|
2516
|
+
preparation and triggers initiation; it does not promise ringing, playable audio or customer
|
|
2517
|
+
answer at that exact instant. Scheduling/dispatch delay must remain visible, and a failed or
|
|
2518
|
+
prevented initiation must be reported rather than leaving a silently expired countdown.
|
|
2519
|
+
A source eligibility threshold that may never trigger an attempt is not this promise.
|
|
2520
|
+
|
|
2521
|
+
The two deadline fields travel together and only in preview. The host uses trusted provider-domain
|
|
2522
|
+
time; clock estimation is allowed only under the explicit host-triggered preview exception below.
|
|
2523
|
+
Without usable provider-domain time
|
|
2524
|
+
it cannot safely auto-trigger and must expose the uncertainty and reconcile. No zero-duration,
|
|
2525
|
+
receipt-time or local-default deadline is inferred. Neither elapsed time nor submission changes
|
|
2526
|
+
the task phase: subsequent authoritative events state ringing, answer, media and completion.
|
|
2527
|
+
|
|
2528
|
+
A host must serialize manual clicks and its timer under the exact provider/login/task/allocation
|
|
2529
|
+
scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
|
|
2530
|
+
allocation and whether an attempt already exists before dispatch. A new snapshot or policy update
|
|
2531
|
+
cancels an obsolete timer; login loss, disconnect, task end and allocation replacement fence its
|
|
2532
|
+
callback. Reconnect past a deadline first reconciles current task and attempt state; it must not
|
|
2533
|
+
blindly submit again. An uncertain prior submission remains unresolved, without automatic retry.
|
|
2534
|
+
The provider atomically arbitrates any residual manual/timer race and does not accept a second
|
|
2535
|
+
command as ownership of an already-running independent attempt.
|
|
2536
|
+
|
|
2537
|
+
Host-triggered automatic dialing is an ordinary host dial: its result is `dialling` with the same
|
|
2538
|
+
host `dialId`, followed by exactly one correlated terminal `dial-outcome`. A provider-triggered
|
|
2539
|
+
dial has no invented host ID or host dial outcome. It reports source-evidenced party ringing,
|
|
2540
|
+
actual media and customer answer separately. In preview with `atDeadline: "calls"`, a ringing
|
|
2541
|
+
party without a host ID may carry actual pre-answer media; the deadline alone permits no media.
|
|
2542
|
+
Media is still introduced/ended by the corresponding events and must not imply customer answer.
|
|
2543
|
+
Submission acceptance is never an answered outcome or permission to publish `in-progress` early.
|
|
2544
|
+
Unsupported source correlation/outcome/media evidence remains an integration blocker.
|
|
2477
2545
|
|
|
2478
2546
|
**Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
|
|
2479
2547
|
spreading the `preview` one carries `previewEndsAt` and `atDeadline` with it, and the host refuses
|
|
@@ -2482,7 +2550,34 @@ provider is told through `refused` exactly which rule, so the state that looked
|
|
|
2482
2550
|
is named on its side. The task past preview has no deadline to wait for, so it carries neither
|
|
2483
2551
|
field.
|
|
2484
2552
|
|
|
2485
|
-
**
|
|
2553
|
+
**Provider execution and host controls are distinct.** Answer, hold/resume, transfer (including
|
|
2554
|
+
IVR routing), conference and interaction completion are executed by the provider
|
|
2555
|
+
through the adapter. The task's applicable capability, phase, completion mode and command-specific
|
|
2556
|
+
prerequisites decide which controls are offered. Caller disconnect also executes at the provider. Microphone mute is host-owned, never a provider
|
|
2557
|
+
TaskCommand or task capability. The separately declared host recording facility keeps its existing
|
|
2558
|
+
recording contract; this distinction does not remove it or move provider call operations to the host.
|
|
2559
|
+
|
|
2560
|
+
A caller journey may revisit the same agent while an earlier interaction still wraps. Public task
|
|
2561
|
+
IDs must remain distinct for concurrently open interactions. For source-owned interaction IDs allocated
|
|
2562
|
+
before offer and never reused per assignment, both public task ID and allocation ID may equal that
|
|
2563
|
+
interaction ID. Restore them unchanged on reconnect. Commands, media, endings and interaction reports
|
|
2564
|
+
retain the exact pair; never retarget an old command to the newest interaction of a journey.
|
|
2565
|
+
`validateTaskCommandRequest` checks the request's pair against the supplied published task and
|
|
2566
|
+
then its command prerequisites. The caller must select that task within the correct provider/login
|
|
2567
|
+
and recheck at the provider; the validator cannot prove source freshness or authorization.
|
|
2568
|
+
`task-ended` retires only that interaction's resources. Another interaction, caller channel or IVR/queue
|
|
2569
|
+
portion must not be cleared because this task ended. Snapshot counts include all still-owned wrap
|
|
2570
|
+
and active interactions; task completion is not caller hangup.
|
|
2571
|
+
|
|
2572
|
+
**Voice describes the agent's interaction within the wider call.** The task defines that agent's
|
|
2573
|
+
workspace, tools, permissions, media participation and completion work. The caller's channel may
|
|
2574
|
+
continue through IVR, queues, other agents, holds or conferences while this interaction ends or wraps.
|
|
2575
|
+
Completing this task ends this interaction responsibility; it is not evidence that the caller's channel
|
|
2576
|
+
or journey ended. Task media describes this agent's attachment, not the lifetime of every party's
|
|
2577
|
+
connection. Commands retain their explicit targets and effects; task completion must not silently
|
|
2578
|
+
become a caller-disconnect operation.
|
|
2579
|
+
|
|
2580
|
+
**A task is never its audio.** A voice task represents the agent’s interaction: the call is offered when it is
|
|
2486
2581
|
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
2487
2582
|
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
2488
2583
|
consult, a conference leg joining or leaving, a transfer, a connect-back — the media follows
|
|
@@ -2493,7 +2588,7 @@ are the adapter's and the platform's, transient, and decide neither when a task
|
|
|
2493
2588
|
phase it is in.
|
|
2494
2589
|
|
|
2495
2590
|
The line runs between the provider's word and Omni's own senses. `task-media-ended` is the
|
|
2496
|
-
provider's report that primary
|
|
2591
|
+
provider's report that primary interaction ended — a fact about the work, which is why the completion
|
|
2497
2592
|
allowance starts on it and the Connect back control appears on it — and Omni follows that report as it
|
|
2498
2593
|
follows any other. What Omni never does is derive a task's state from its own media session: a
|
|
2499
2594
|
stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
|
|
@@ -2519,7 +2614,7 @@ whether the channel carries real-time media:
|
|
|
2519
2614
|
| Email | The `task-updated` that moves the task to `completing`: the provider's word that the platform has accepted the outgoing message, the allowance running from that publication's `occurredAt` |
|
|
2520
2615
|
| Other non-media channels | The `task-updated` that moves the task to `completing` |
|
|
2521
2616
|
|
|
2522
|
-
**Off voice, `completing` is the provider's word that
|
|
2617
|
+
**Off voice, `completing` is the provider's word that interaction ended.** There is no media event to
|
|
2523
2618
|
carry it, so the phase does; a host starts the wrap clock at that publication and nowhere else. It
|
|
2524
2619
|
is optional: a conversation with nothing to wrap moves from `in-progress` to `task-ended` and
|
|
2525
2620
|
`completing` is never published. Under `provider-automatic` with a non-zero `wrapAllowance` it is
|
|
@@ -2592,7 +2687,7 @@ tell a task coming back from one going backwards (`stream.taskUpdated.phase`).
|
|
|
2592
2687
|
**The control exists only while there is a window to use it in.** Under `agent-command` the task
|
|
2593
2688
|
stays `completing` until the agent completes it, so the window is open for as long as they need.
|
|
2594
2689
|
Under `provider-automatic` the window is the allowance -- and with `wrapAllowance: 0` there
|
|
2595
|
-
is none: the provider
|
|
2690
|
+
is none: the provider completes the task at provider end, and Omni does not offer Connect back,
|
|
2596
2691
|
whatever the task declares. A capability names a control that can be used; on a task with no `completing`
|
|
2597
2692
|
window it cannot, and declaring it there changes nothing.
|
|
2598
2693
|
|
|
@@ -2619,13 +2714,38 @@ const immediateProviderCompletion = {
|
|
|
2619
2714
|
} satisfies Pick<Task, "completionMode" | "wrapAllowance">;
|
|
2620
2715
|
```
|
|
2621
2716
|
|
|
2622
|
-
###
|
|
2717
|
+
### Interaction history
|
|
2718
|
+
|
|
2719
|
+
The API uses the same interaction terminology: `interactionHistory`, `InteractionReport`,
|
|
2720
|
+
`InteractionReportResult`, and the corresponding step types and validators. Hosts and providers
|
|
2721
|
+
must adopt these names together; no legacy aliases are provided. The manifest's
|
|
2722
|
+
`completionSettleMs` bounds final task completion, not entry into the `completing` wrap-up phase.
|
|
2723
|
+
The `recordStep` method and `complete` command keep their existing names and behavior.
|
|
2623
2724
|
|
|
2624
|
-
|
|
2725
|
+
Migration from the earlier spelling:
|
|
2726
|
+
|
|
2727
|
+
| Former API | Current API |
|
|
2728
|
+
| --- | --- |
|
|
2729
|
+
| Task.handlingHistory | `Task.interactionHistory` |
|
|
2730
|
+
| TaskHandlingHistory, TaskHandlingStep, HandlingStep | `TaskInteractionHistory`, `TaskInteractionStep`, `InteractionStep` |
|
|
2731
|
+
| HandlingReport, HandlingReportResult, validateHandlingReport | `InteractionReport`, `InteractionReportResult`, `validateInteractionReport` |
|
|
2732
|
+
| HANDLING_STEPS_WITH_A_PERSON, HANDLING_STEPS_THAT_DIAL | `INTERACTION_STEPS_WITH_A_PERSON`, `INTERACTION_STEPS_THAT_DIAL` |
|
|
2733
|
+
| handlingStepExpectsAPerson, handlingStepDials | `interactionStepExpectsAPerson`, `interactionStepDials` |
|
|
2734
|
+
| handleSeconds | `interactionSeconds` |
|
|
2735
|
+
| Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
|
|
2736
|
+
|
|
2737
|
+
Update producers, consumers, saved task snapshots, and validation-rule assertions together.
|
|
2738
|
+
History/report rule names now use `interactionHistory`/`interactionReport`; phase and conformance
|
|
2739
|
+
rules use `interaction` and final-task rules use `completion`. Legacy fields are rejected even
|
|
2740
|
+
when the new field is also supplied. `DispositionCode`, `DispositionRules`, `DispositionPayload`,
|
|
2741
|
+
`dispositions`, and `disposition` still describe outcome codes and are unchanged.
|
|
2742
|
+
|
|
2743
|
+
|
|
2744
|
+
`Task.interactionHistory` is the call record: the steps that brought the task to the agent, oldest
|
|
2625
2745
|
first, and what they add up to before this agent:
|
|
2626
2746
|
|
|
2627
2747
|
```ts
|
|
2628
|
-
|
|
2748
|
+
interactionHistory: {
|
|
2629
2749
|
steps: [
|
|
2630
2750
|
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
2631
2751
|
{ step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
|
|
@@ -2633,7 +2753,7 @@ handlingHistory: {
|
|
|
2633
2753
|
{ step: "transferred", at: "2026-08-21T01:04:53Z", by: "a-17" },
|
|
2634
2754
|
{ step: "answered", at: "2026-08-21T01:05:02Z", by: "a-23" },
|
|
2635
2755
|
],
|
|
2636
|
-
|
|
2756
|
+
interactionSeconds: 312, // handled by others before this agent
|
|
2637
2757
|
holdSeconds: 35, // holds others put the caller on
|
|
2638
2758
|
queueSeconds: 41, // waiting before anyone answered
|
|
2639
2759
|
transfers: 1, // hands the call has changed
|
|
@@ -2648,7 +2768,7 @@ something else, which will arrive under its own name and must not be folded in h
|
|
|
2648
2768
|
It rides in the snapshot and is replaced whole like everything else there.
|
|
2649
2769
|
|
|
2650
2770
|
Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
2651
|
-
`unanswered`, and each is defined on `
|
|
2771
|
+
`unanswered`, and each is defined on `TaskInteractionStep`.
|
|
2652
2772
|
|
|
2653
2773
|
**The record is one entry per occurrence, oldest first.** Each hold is its own `held` entry — `at`
|
|
2654
2774
|
when it began, `seconds` once it ended and omitted while it runs — and a second hold is a second
|
|
@@ -2656,7 +2776,7 @@ entry after the first, never a revision of it. A leg that has ended states its d
|
|
|
2656
2776
|
task says whether a leg can still be running: a hold runs only while the task is `paused`, a mute
|
|
2657
2777
|
only while its media is up, so a `held` entry without `seconds` on a task that is not paused, or a
|
|
2658
2778
|
`muted` one on a call that is over, is a leg nobody closed and reads exactly like a leg running now
|
|
2659
|
-
(`task.
|
|
2779
|
+
(`task.interactionHistory.held.open`, `.muted.open`). Whoever performs the leg closes it — the
|
|
2660
2780
|
provider whose platform parks the caller, the host whose microphone it is — by restating the entry
|
|
2661
2781
|
with its duration, and **an entry that cannot be closed is not written**: open for ever is a
|
|
2662
2782
|
plausible nought one level down from a total. **A leg still open when the media ends is closed by
|
|
@@ -2669,7 +2789,7 @@ provider closed it. One publisher of the closed leg, and no order between them t
|
|
|
2669
2789
|
hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming it twice. The same goes for every step: two mutes are two
|
|
2670
2790
|
`muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
|
|
2671
2791
|
entries, one per join. Order is enforced: an entry earlier than the one before it is refused
|
|
2672
|
-
(`task.
|
|
2792
|
+
(`task.interactionHistory.order`). **Intervals between steps are the host's to subtract**, never a
|
|
2673
2793
|
total the provider adds: *time to offer*, from the call's arrival to the agent's screen lighting
|
|
2674
2794
|
up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
|
|
2675
2795
|
`queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
|
|
@@ -2683,17 +2803,32 @@ on a resync snapshot -- carries every entry the host has already read, by step a
|
|
|
2683
2803
|
may add to them; it never has fewer, and never drops the record while the task is open. A fact
|
|
2684
2804
|
stated to the host and then withdrawn without an event is a record contradicting itself, exactly
|
|
2685
2805
|
as a task that lost the terms it had read would be, and the stream refuses it the same way
|
|
2686
|
-
(`stream.taskUpdated.
|
|
2806
|
+
(`stream.taskUpdated.interactionHistory`, `stream.snapshot.interactionHistory`). A provider whose
|
|
2687
2807
|
platform cannot hold a leg the host reported keeps it in the login's `store` for the life of the
|
|
2688
2808
|
task, so a reload of the host restates the same record; the entry is keyed by `step` and `at`, so
|
|
2689
2809
|
a running hold restated with its final `seconds` is the same entry.
|
|
2690
2810
|
|
|
2691
|
-
**The
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2811
|
+
**The provider owns the timestamps in its record.** Host timestamps are advisory. The provider
|
|
2812
|
+
may retain a host instant it accepts or assign its own observation/receipt instant; it need not
|
|
2813
|
+
copy the host clock. A provider declaring `timestampAuthority: "provider"` uses its own timestamps
|
|
2814
|
+
for final records. Receipt time is an observation boundary, not a claim about when the physical
|
|
2815
|
+
host action happened. Records are ordered by their published provider-selected instants, with no
|
|
2816
|
+
rewriting of an already-published entry when a later host report arrives.
|
|
2817
|
+
|
|
2818
|
+
The incoming host `step` and `at`, scoped to the task and its current life, identify one reported
|
|
2819
|
+
leg for correlation only. The provider retains a binding from that key to its chosen history
|
|
2820
|
+
`at`. Every successful `recordStep` returns `{ status: "recorded", at }` with that same canonical
|
|
2821
|
+
history instant, even for subsequent duration/end reports. The host keeps sending the original
|
|
2822
|
+
report key, never the returned history timestamp. A changed host timestamp is not a correction
|
|
2823
|
+
to the same report: it names a different leg. Bindings survive reconnect/reload for the retained
|
|
2824
|
+
task lifetime. The host renders provider-published history unchanged. It never compares the
|
|
2825
|
+
provider instant back to its own clock, substitutes its own timestamp, or adjusts later messages
|
|
2826
|
+
to match an earlier host estimate. The acknowledgment identifies the provider record for
|
|
2827
|
+
correlation/conformance only; it is not a host-side timestamp reconciliation instruction. Conflicting reuse is refused visibly, not paired by arrival time or nearest instant.
|
|
2828
|
+
If two distinct entries would collide under the history's `(step, at)` key, do not invent a time
|
|
2829
|
+
or combine them: report the representation conflict. No ambiguous event is silently accepted.
|
|
2830
|
+
|
|
2831
|
+
**Handle time is anchored, not restarted.** It runs from the
|
|
2697
2832
|
`answered` step's `at` — from the task's first `in-progress` where the provider reports no
|
|
2698
2833
|
history — until the task's media ends, and a hold neither pauses nor resets it: the hold's own
|
|
2699
2834
|
duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
|
|
@@ -2703,12 +2838,12 @@ counting the wrong thing.
|
|
|
2703
2838
|
the host's** — so without a step the provider, which alone keeps the task's record, would have no
|
|
2704
2839
|
account of a period the agent could not be heard. Each `muted` entry carries `mutedBy`, as the host
|
|
2705
2840
|
reported it: `host` for the agent's own Mute, `station` for a headset or system mute the host
|
|
2706
|
-
observed. No other step has it (`task.
|
|
2841
|
+
observed. No other step has it (`task.interactionHistory.mutedBy`, `.mutedBy.unexpected`).
|
|
2707
2842
|
|
|
2708
2843
|
**A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
|
|
2709
2844
|
each end a dial, so the entry carries the `destinationId` it went to and, where a host placed it, the
|
|
2710
2845
|
`dialId` the host minted; no other step dialled, and either field on one is refused
|
|
2711
|
-
(`task.
|
|
2846
|
+
(`task.interactionHistory.dialId.unexpected`). This is how a dial made before a transfer is placeable
|
|
2712
2847
|
by whoever holds the task now: a `dial-outcome` naming a `dialId` the host never minted is looked
|
|
2713
2848
|
up in the record, not discarded.
|
|
2714
2849
|
|
|
@@ -2721,7 +2856,7 @@ Four rules a provider has to keep:
|
|
|
2721
2856
|
- **Omit `seconds` while it is unknown. Never send `0`.** A leg still talking is not a zero-second
|
|
2722
2857
|
conversation, and on live data that is the ordinary case rather than an edge. A zero is rejected.
|
|
2723
2858
|
- **`by` is a bare `UserId`, and not necessarily an agent.** A lead or a manager takes part
|
|
2724
|
-
|
|
2859
|
+
during an interaction too — a transfer accepted, a call conferenced in — so the field names whoever it was,
|
|
2725
2860
|
the same way `ImposedBreak.by` does. It comes from this provider's own directory, the same
|
|
2726
2861
|
namespace as `AuthenticationState.identity.id` and the team roster, so entries pair
|
|
2727
2862
|
within a provider and never across one.
|
|
@@ -2731,10 +2866,10 @@ Four rules a provider has to keep:
|
|
|
2731
2866
|
|
|
2732
2867
|
**An absent `by` means different things on different steps, and both are legitimate.** On
|
|
2733
2868
|
`queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
|
|
2734
|
-
`
|
|
2869
|
+
`INTERACTION_STEPS_WITH_A_PERSON` and `interactionStepExpectsAPerson()` — so an absent `by` there says
|
|
2735
2870
|
*this was handled and the provider cannot say by whom*. The one step that is never unattributed is
|
|
2736
2871
|
`muted`: the host has exactly one agent and the provider knows who, so `by` is required there
|
|
2737
|
-
(`task.
|
|
2872
|
+
(`task.interactionHistory.muted.by`).
|
|
2738
2873
|
|
|
2739
2874
|
That case is ordinary rather than theoretical: a leg answered on a shared phone, a manager's
|
|
2740
2875
|
handset, or a device the provider cannot resolve to a person. **Report the step without `by`
|
|
@@ -2746,7 +2881,7 @@ A host must render the two differently. Showing an unattributed `answered` the s
|
|
|
2746
2881
|
`queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
2747
2882
|
*"not recorded"* in the place the name would go.
|
|
2748
2883
|
|
|
2749
|
-
Omit `
|
|
2884
|
+
Omit `interactionHistory` entirely when the provider cannot observe the steps. Empty `steps` is a
|
|
2750
2885
|
different claim — it says the task has had none.
|
|
2751
2886
|
|
|
2752
2887
|
**What the record adds up to is stated, not summed.** A call that has changed hands arrives
|
|
@@ -2776,27 +2911,45 @@ void connection.recordStep?.({ taskId, allocationId, step: "muted", at, mutedBy:
|
|
|
2776
2911
|
void connection.recordStep?.({ taskId, allocationId, step: "muted", at, mutedBy: "host", seconds: 42, ended: true }); // the moment they unmute
|
|
2777
2912
|
```
|
|
2778
2913
|
|
|
2779
|
-
|
|
2780
|
-
|
|
2914
|
+
Every report about one leg repeats its original `step` and host `at` as a correlation key;
|
|
2915
|
+
the published history uses the provider-selected `at` returned by `recordStep`. The host reports
|
|
2916
|
+
the action it performed and its measured duration, not an authoritative provider timestamp. It may say how long so far — `seconds` is
|
|
2781
2917
|
elapsed while the leg runs and final once it has ended — and **the end is stated, never
|
|
2782
2918
|
inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
|
|
2783
2919
|
provider never asked for never crosses.** The running report is sent only to a provider whose
|
|
2784
2920
|
manifest declares `runningStepReports`; every other provider receives exactly two reports per
|
|
2785
|
-
leg, when it began and when it ended, and `
|
|
2786
|
-
a running one it was never asked for (`
|
|
2921
|
+
leg, when it began and when it ended, and `validateInteractionReport(report, path, manifest)` refuses
|
|
2922
|
+
a running one it was never asked for (`interactionReport.running.unexpected`). What a provider that
|
|
2787
2923
|
did ask for them forwards upstream, and how often, is its own business. The step appears in
|
|
2788
|
-
`
|
|
2924
|
+
`interactionHistory` when the *provider* publishes it: Omni never writes the record itself.
|
|
2789
2925
|
`recordStep` is required of every softphone login's connection, since every call on a softphone
|
|
2790
|
-
can be muted by the host, and answers `recorded`. A `muted` report says whose the silence was,
|
|
2791
|
-
`mutedBy: "host"` or `"station"`, and no other report has the word (`
|
|
2926
|
+
can be muted by the host, and answers `recorded` with the canonical history `at`. A `muted` report says whose the silence was,
|
|
2927
|
+
`mutedBy: "host"` or `"station"`, and no other report has the word (`interactionReport.mutedBy`,
|
|
2792
2928
|
`.mutedBy.unexpected`). On a desk phone the microphone is the phone's:
|
|
2793
2929
|
the host mutes nothing and records nothing.
|
|
2794
2930
|
|
|
2795
|
-
**
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
the
|
|
2799
|
-
the
|
|
2931
|
+
**The provider's confirmed end is decisive.** The host keeps local state for interaction and
|
|
2932
|
+
reporting, but follows the provider's authoritative state. When the provider publishes the end
|
|
2933
|
+
of the current mute leg, the host ends that local mute and releases its host-controlled mute
|
|
2934
|
+
on the matching task's media. It does not wait for its own timer, reopen the leg, replace the
|
|
2935
|
+
provider timestamp, or overwrite the provider's final duration with a later host report.
|
|
2936
|
+
A later agent mute is a new leg, never a reopening of the ended one.
|
|
2937
|
+
|
|
2938
|
+
Correlate the provider-published `muted` entry using the provider-selected `at` acknowledged
|
|
2939
|
+
for the current report key; a final `seconds` closes that leg under the history contract.
|
|
2940
|
+
This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
|
|
2941
|
+
task, or a stale connection cannot end a newer local mute. If publication precedes the acknowledgment,
|
|
2942
|
+
retain the current provider view and apply the matching closure once correlation is available;
|
|
2943
|
+
never guess a match from arrival order or nearest timestamp. Failure to release a host-controlled
|
|
2944
|
+
mute is reported visibly; local device reports still describe the actual device state.
|
|
2945
|
+
|
|
2946
|
+
At a provider-confirmed task/media end, the host also stops the associated local mute and reports
|
|
2947
|
+
its observed ending where the report is still accepted. A host closing report repeats its original
|
|
2948
|
+
key and may include its measured duration, but cannot reverse an already confirmed provider end.
|
|
2949
|
+
The provider acknowledges a known closed leg without rewriting its final record; after the task
|
|
2950
|
+
has been completed, it may refuse the report as task-not-found. The host reads that response rather
|
|
2951
|
+
than retrying or recreating the task. An observed host end before a provider closure is still
|
|
2952
|
+
reported normally; the provider decides the final timestamp and publishes the record.
|
|
2800
2953
|
|
|
2801
2954
|
### Browser capability
|
|
2802
2955
|
|
|
@@ -2928,7 +3081,7 @@ source is one more published fact about them, and the one that lets a host tell
|
|
|
2928
3081
|
governed by `completionMode` alone: under `agent-command` it is always available, whatever the set
|
|
2929
3082
|
says, and the `dispositions` capability decides only whether a code travels with it. Likewise
|
|
2930
3083
|
Answer on a pending task and Call on a preview are the phase's controls, not the set's. The
|
|
2931
|
-
capability set governs what the agent may do *with* the task;
|
|
3084
|
+
capability set governs what the agent may do *with* the task; completing it is never on the
|
|
2932
3085
|
list.
|
|
2933
3086
|
|
|
2934
3087
|
**A permission that changes while the task is open is republished on the task at the moment it
|
|
@@ -2974,7 +3127,7 @@ Terms once read stay read: a re-read that fails mid-task is not a new fact about
|
|
|
2974
3127
|
last statement stands and the failure is a `diagnostic`, and a task that was published under
|
|
2975
3128
|
`queue` or `ungoverned` never returns to `undetermined`, on an update (`stream.taskUpdated.capabilitySource`)
|
|
2976
3129
|
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
2977
|
-
entry the same two ways (`stream.taskUpdated.
|
|
3130
|
+
entry the same two ways (`stream.taskUpdated.interactionHistory`, `stream.snapshot.interactionHistory`);
|
|
2978
3131
|
a snapshot carrying a task still at work does not forget the audio the stream held up, since media
|
|
2979
3132
|
ends on `task-media-ended` and the call moves on (`stream.snapshot.media`); a voice task ends after
|
|
2980
3133
|
its audio ends, never around it, whatever the outcome (`stream.taskEnded.mediaOpen`); and a task does not go
|
|
@@ -3037,14 +3190,14 @@ See **Which commands need a capability**.
|
|
|
3037
3190
|
| --- | --- | --- |
|
|
3038
3191
|
| `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when provisioning also permits declining. |
|
|
3039
3192
|
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
3040
|
-
| `endCall` | Primary button: End call |
|
|
3041
|
-
| `connectBack` | Completing-task button: Connect back | Omni may have the provider connect the agent back to the task's party while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance
|
|
3193
|
+
| `endCall` | Primary button: End call | The provider ends the caller connection and all owned/inherited agent-added channels; wrap/completion remain separate. See **Ending a call, and removing one person from it**. |
|
|
3194
|
+
| `connectBack` | Completing-task button: Connect back | Omni may have the provider connect the agent back to the task's party while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance completes the task at provider end. See **Connecting back during completion**. |
|
|
3042
3195
|
| `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
|
|
3043
3196
|
| `warmTransfer` | Secondary menu item: Warm transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Warm transfer**. |
|
|
3044
3197
|
| `leadAssist` | Secondary menu item: Lead assist | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.leadAssist`. See **Lead assist**. |
|
|
3045
3198
|
| `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and remove one person from it -- a conferenced entry, one still ringing included, which calls the dial off, or the party, leaving the agent with the colleague. See **Ending a call, and removing one person from it**. |
|
|
3046
3199
|
| `recording` | Overflow menu item: Recording | Per-task provider and host policies expose only their permitted recording actions; each has independent state and routing. |
|
|
3047
|
-
| `dispositions` | Primary button: Complete | Omni may request task
|
|
3200
|
+
| `dispositions` | Primary button: Complete | Omni may request task completion with a provider disposition and notes. |
|
|
3048
3201
|
|
|
3049
3202
|
### Publishing codes and destinations
|
|
3050
3203
|
|
|
@@ -3103,12 +3256,12 @@ kind travels, nothing is typed, and a control with an empty directory has nothin
|
|
|
3103
3256
|
refused (`task.destinations.offer`). A control declared as bare `true` is refused for the same
|
|
3104
3257
|
reason (`task.destinations.shape`): once nothing is typed, the directory is the control.
|
|
3105
3258
|
|
|
3106
|
-
**
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
The
|
|
3111
|
-
|
|
3259
|
+
**The provider chooses the supported destinations.** A published item may route to an IVR,
|
|
3260
|
+
queue, named agent or another configured destination. No destination kind is inferred from its
|
|
3261
|
+
label. The host offers only the directory attached to this task's specific transfer capability,
|
|
3262
|
+
and sends the exact destination ID; it does not invent an agent picker or arbitrary dial target.
|
|
3263
|
+
The provider rechecks eligibility and executes the routing. Returning to an IVR or queue does not
|
|
3264
|
+
end the caller journey; a later assignment to the same agent is a new interaction.
|
|
3112
3265
|
|
|
3113
3266
|
#### Ending a call, and removing one person from it
|
|
3114
3267
|
|
|
@@ -3116,18 +3269,44 @@ A call has everyone on `Task.onCall`, and two commands take people off it, both
|
|
|
3116
3269
|
provider.
|
|
3117
3270
|
|
|
3118
3271
|
```ts
|
|
3119
|
-
{ type: "end-call" } //
|
|
3272
|
+
{ type: "end-call" } // provider ends caller and owned/inherited agent-added channels; wrap/completion are separate
|
|
3120
3273
|
{ type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
|
|
3121
3274
|
{ type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
|
|
3122
3275
|
```
|
|
3123
3276
|
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
the
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3277
|
+
The host sends the ordinary task-scoped `execute` request with `command: { type: "end-call" }`.
|
|
3278
|
+
The provider knows the caller and agent-added channel bindings; no channel list or owner details
|
|
3279
|
+
are sent by the host. The task's `endCall` capability is permission, not an executor selection.
|
|
3280
|
+
`validateTaskCommandRequest` checks the exact interaction/allocation and delegates the command's
|
|
3281
|
+
policy/phase checks. The provider rechecks current ownership atomically; no client validator can
|
|
3282
|
+
prove that its view is fresh. The host does not implement agent end-call; low-level removal of individual channels is not
|
|
3283
|
+
this agent action. There is no host end-call execution API or fallback route.
|
|
3284
|
+
|
|
3285
|
+
`end-call` is gated by `endCall` on the current interaction. It ends the caller connection and
|
|
3286
|
+
all agent-added channels owned by that interaction, including channels originally added by previous
|
|
3287
|
+
agents and inherited through transfer or takeover. Ownership is not inferred from who originally
|
|
3288
|
+
dialed, the current UI selection, or a person's label. The provider maintains that authority
|
|
3289
|
+
and checks the exact interaction/allocation when acting. Agent-added channels are not left behind
|
|
3290
|
+
merely because another agent added them.
|
|
3291
|
+
|
|
3292
|
+
On a successful transfer or takeover, ownership of those channels passes to the receiving
|
|
3293
|
+
interaction (lead or another agent), not just ownership of the caller leg. The outgoing interaction may
|
|
3294
|
+
still have its allowed wrap work, but cannot disconnect channels it no longer owns. A pending or
|
|
3295
|
+
unknown handover is not a confirmed ownership change: retain source reservations and reconcile.
|
|
3296
|
+
The provider must serialize handover and end-call so a delayed outgoing command cannot tear down
|
|
3297
|
+
the recipient's call. No public channel-owner field or guessed identity is introduced here;
|
|
3298
|
+
the source must provide authoritative ownership and command fencing before declaring permission.
|
|
3299
|
+
|
|
3300
|
+
The resulting end of this agent's media is published as `task-media-ended`. Any wrap remains
|
|
3301
|
+
until its own completion; `end-call` is not `task-ended`, and `complete` is not an implicit
|
|
3302
|
+
caller/channel disconnect. Other agents' independent tasks end or wrap only on their own source
|
|
3303
|
+
transitions. Neither completion nor transfer itself ends the continuing caller journey.
|
|
3304
|
+
|
|
3305
|
+
`conference` `remove` remains a person-specific provider operation, gated by `conference`.
|
|
3306
|
+
It does not implicitly complete any other interaction. Its person/consultation prerequisites
|
|
3307
|
+
remain separate from the permission to disconnect the caller. A remove that would leave the
|
|
3308
|
+
agent alone is not a remove but an `end-call`, and the provider answers it `failed`.
|
|
3309
|
+
The consulted destination of a warm transfer is not removed this way:
|
|
3131
3310
|
`transfer` `cancel` is its own step, which returns the agent to the parked customer, and a remove
|
|
3132
3311
|
never promises that.
|
|
3133
3312
|
|
|
@@ -3165,8 +3344,8 @@ that offers `warmTransfer` implements all three.
|
|
|
3165
3344
|
`applied` on `complete` says the provider is bridging the customer to the destination and
|
|
3166
3345
|
dropping the agent's leg. What follows is what follows any call the agent leaves: the agent's
|
|
3167
3346
|
media ends and the provider reports `task-media-ended`, the task moves to `completing`, any wrap
|
|
3168
|
-
allowance runs, and the task ends `completed` as any call does. A warm complete is not a
|
|
3169
|
-
the agent has a wrap to do, so nothing is owed within `
|
|
3347
|
+
allowance runs, and the task ends `completed` as any call does. A warm complete is not a completion:
|
|
3348
|
+
the agent has a wrap to do, so nothing is owed within `completionSettleMs`. `transferred` names a
|
|
3170
3349
|
cold transfer alone, where the agent had no wrap. `applied` on `cancel` says the destination is
|
|
3171
3350
|
dropped; the task returns to `in-progress` with the `consulted` entry gone. Omni waits for the
|
|
3172
3351
|
provider's report of both, as it does for every command.
|
|
@@ -3180,15 +3359,15 @@ customer.
|
|
|
3180
3359
|
| Capability | Omni UI | Contract |
|
|
3181
3360
|
| --- | --- | --- |
|
|
3182
3361
|
| `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when provisioning also permits declining. |
|
|
3183
|
-
| `hold` | Primary toggle: Hold | Omni may pause and resume agent
|
|
3184
|
-
| `dispositions` | Primary button: Complete | Omni may request task
|
|
3362
|
+
| `hold` | Primary toggle: Hold | Omni may pause and resume the agent’s interaction in the chat. |
|
|
3363
|
+
| `dispositions` | Primary button: Complete | Omni may request task completion with a provider disposition and notes. |
|
|
3185
3364
|
|
|
3186
3365
|
### Email capabilities
|
|
3187
3366
|
|
|
3188
3367
|
| Capability | Omni UI | Contract |
|
|
3189
3368
|
| --- | --- | --- |
|
|
3190
3369
|
| `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when provisioning also permits declining. |
|
|
3191
|
-
| `dispositions` | Primary button: Complete | Omni may request task
|
|
3370
|
+
| `dispositions` | Primary button: Complete | Omni may request task completion with a provider disposition and notes. |
|
|
3192
3371
|
|
|
3193
3372
|
### Custom capabilities
|
|
3194
3373
|
|
|
@@ -3274,13 +3453,13 @@ expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
|
3274
3453
|
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, allocationId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
3275
3454
|
|
|
3276
3455
|
// 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
|
|
3277
|
-
const step:
|
|
3456
|
+
const step: TaskInteractionStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
|
|
3278
3457
|
```
|
|
3279
3458
|
|
|
3280
3459
|
The identity is a correlation handle between host and provider and **is never shown to the
|
|
3281
3460
|
agent**. A host holding several tasks places an outcome by the `dialId` it minted; one it did not
|
|
3282
3461
|
mint -- a dial made by the previous agent before a transfer -- it finds on the task's `onCall` or in
|
|
3283
|
-
its `
|
|
3462
|
+
its `interactionHistory`, which is why the steps that dial carry it. The `dialId` is not a retry key:
|
|
3284
3463
|
Omni never repeats a dial, and a command still carries no key for that purpose.
|
|
3285
3464
|
|
|
3286
3465
|
**`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
|
|
@@ -3310,7 +3489,7 @@ and the harness places the outcome all the same (`TaskStream`, `stream.dialOutco
|
|
|
3310
3489
|
call it belonged to and never against the next one.
|
|
3311
3490
|
|
|
3312
3491
|
The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which a host calls when it
|
|
3313
|
-
places one; an entry on a task's `onCall`; and a step in its `
|
|
3492
|
+
places one; an entry on a task's `onCall`; and a step in its `interactionHistory`. Since a dialled entry
|
|
3314
3493
|
is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- a
|
|
3315
3494
|
cold transfer, a connect-back, an idle dialpad call -- or whose entry has already left the room; an
|
|
3316
3495
|
outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
|
|
@@ -3355,14 +3534,12 @@ the platform added itself carries none. `held: true` is presence as claim. A sna
|
|
|
3355
3534
|
after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
|
|
3356
3535
|
never saw.
|
|
3357
3536
|
|
|
3358
|
-
**`onCall`
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
say who is, which is a different claim, so a provider that publishes the room publishes one last
|
|
3365
|
-
change: the empty room.
|
|
3537
|
+
**`onCall` describes this agent's current interaction.** When the caller disconnects but the agent
|
|
3538
|
+
and added channels remain connected, the remaining room is still valid. When this interaction's
|
|
3539
|
+
media ends or the task enters `completing`, clear its `onCall` view; absence is used only by a
|
|
3540
|
+
provider that never publishes the room. This does not assert that the caller, bridge or other
|
|
3541
|
+
agents' channels ended. A published room receives a final empty view for this interaction, and the
|
|
3542
|
+
task may remain open for wrap. Completion is a separate task action, not the caller's disconnect.
|
|
3366
3543
|
|
|
3367
3544
|
**A field that describes the present is cleared by the transition that ends it.** `onCall`,
|
|
3368
3545
|
`previewEndsAt` and `atDeadline` are three instances of one shape, and there will be more: each
|
|
@@ -3552,6 +3729,40 @@ against it (`stream.taskOffered.overCapacity`).
|
|
|
3552
3729
|
idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
|
|
3553
3730
|
it: the ceiling binds allocation, not the agent's own hand.
|
|
3554
3731
|
|
|
3732
|
+
### Runtime break prerequisite checks
|
|
3733
|
+
|
|
3734
|
+
Use `validateBreakCommand(method, request, state, context)` from the validation entry point
|
|
3735
|
+
before any agent break dispatch. The context supplies current authentication and transport;
|
|
3736
|
+
all four methods require the live login's break capability and active transport. Pass the
|
|
3737
|
+
request object only to the request method, and undefined to the other three.
|
|
3738
|
+
|
|
3739
|
+
| Method | Required current state |
|
|
3740
|
+
| --- | --- |
|
|
3741
|
+
| `requestBreak` | `not-requested`; selected current reason code when codes exist; `mayAsk` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
|
|
3742
|
+
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `mayAsk` does not revoke the grant. |
|
|
3743
|
+
| `cancelBreak` | `awaiting-decision` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
|
|
3744
|
+
| `endBreak` | `in-effect` or `starting-after-task` during reconciliation; an agent cannot end an imposed break. |
|
|
3745
|
+
|
|
3746
|
+
Use `validateTeamBreakCommand(request, context)` for lead decisions, placement, release and
|
|
3747
|
+
policy commands. It requires the live lead capability and active transport, a current target
|
|
3748
|
+
roster for member commands, and the target's complete break state for placement/release.
|
|
3749
|
+
Approve/deny requires an awaiting decision; placement uses the target's reason codes; release
|
|
3750
|
+
requires an imposed committed break. The context's target state must belong to the named member;
|
|
3751
|
+
that association and backend authorization are provider responsibilities.
|
|
3752
|
+
|
|
3753
|
+
Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
|
|
3754
|
+
as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
|
|
3755
|
+
checks. Pending and completing tasks still count as work. Only the existing monitoring exception
|
|
3756
|
+
allows tasks beside a break in effect. Validate full task shapes and the envelope separately.
|
|
3757
|
+
|
|
3758
|
+
These validators report violations without dispatching, rewriting state or proving freshness.
|
|
3759
|
+
Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
|
|
3760
|
+
Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
|
|
3761
|
+
is not a grant, and no method response overwrites a newer state emission. Rejected promises
|
|
3762
|
+
remain unknown and recover by snapshot, not automatic retry. The host's frozen provider set,
|
|
3763
|
+
durable mutually exclusive commit/cancel decision and reconciliation obligations above remain
|
|
3764
|
+
required; per-provider validation does not implement the multi-provider coordinator.
|
|
3765
|
+
|
|
3555
3766
|
### `requestBreak(request)`
|
|
3556
3767
|
|
|
3557
3768
|
Requests permission to stop the agent later; it does not itself stop work. The provider continues
|
|
@@ -3609,7 +3820,7 @@ supports mapping them onto, and every member means something specific:
|
|
|
3609
3820
|
| `meal` | A meal: lunch, dinner, whatever the shift calls it. |
|
|
3610
3821
|
| `rest` | A rest period the agent is entitled to and a busy hour cannot cancel. Usually the reason also marked `alwaysAvailable`, though the two are separate: this says what the break *is*, that says whether policy can withdraw it. |
|
|
3611
3822
|
| `training` | Learning something the agent is expected to know afterwards: a course, e-learning, a product walkthrough. However it is delivered, and whoever attends. |
|
|
3612
|
-
| `coaching` | Reviewing this agent's own work with somebody accountable for it: a call listened back, quality feedback, a one-to-one about their
|
|
3823
|
+
| `coaching` | Reviewing this agent's own work with somebody accountable for it: a call listened back, quality feedback, a one-to-one about their interactions. Even where the outcome is that they learn something. |
|
|
3613
3824
|
| `meeting` | A scheduled gathering that is neither — a team huddle, a project call, a town hall. The agent attends and contributes; nobody is assessing their work and there is nothing they must know by the end. |
|
|
3614
3825
|
| `administrative` | Paperwork and follow-up not attached to a particular contact. |
|
|
3615
3826
|
| `technical` | Equipment or system trouble stopping the agent taking work: a dead headset, a phone that never registered, a tool that will not load. The one member that is not an activity — it says why the agent *cannot* work rather than what they are doing, which makes it the right home for a not-ready state raised about the agent's equipment. |
|
|
@@ -3816,7 +4027,7 @@ Each availability value means one thing:
|
|
|
3816
4027
|
| Value | Meaning |
|
|
3817
4028
|
| --- | --- |
|
|
3818
4029
|
| `ready` | Signed in, able to take work, none assigned. |
|
|
3819
|
-
| `on-task` |
|
|
4030
|
+
| `on-task` | Working on at least one task. It says nothing about how many, and nothing about whether more will fit. |
|
|
3820
4031
|
| `on-break` | Stopped and not taking work, whether they asked or somebody stopped them. The reason lives on their own `BreakState`, not here. |
|
|
3821
4032
|
| `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**. |
|
|
3822
4033
|
| `signed-out` | Known to this team but not signed in to this provider. |
|
|
@@ -3927,7 +4138,7 @@ choice that is no command at all:
|
|
|
3927
4138
|
| --- | --- | --- |
|
|
3928
4139
|
| `{ type: "lead-assist", action: "take-over" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
|
|
3929
4140
|
| `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-media-ended`, then `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
3930
|
-
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own disposition | The same, independently: **both have the
|
|
4141
|
+
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own disposition | The same, independently: **both have the completion window** |
|
|
3931
4142
|
|
|
3932
4143
|
`left` is the one outcome that ends a task without ending the call: this agent left a call that
|
|
3933
4144
|
continues without them. It reads as neither a completion nor a cancellation, because it is
|
|
@@ -3987,7 +4198,7 @@ the state, restated on every change, and a `whisper` or `barge` the login's list
|
|
|
3987
4198
|
is answered `failed`. **A lead listens to one call at a time** (`snapshot.monitoring.single`), and
|
|
3988
4199
|
a task is a joined call or a monitored one, never both (`task.monitoring.assisting`). The member's
|
|
3989
4200
|
call ending ends the lead's task as it ends the member's, with `left`, since the lead was never
|
|
3990
|
-
|
|
4201
|
+
working on it.
|
|
3991
4202
|
|
|
3992
4203
|
**A lead listens only while holding no work of their own.** A monitoring task is the lead's only
|
|
3993
4204
|
task, and a snapshot carrying one beside any other is refused (`snapshot.monitoring.alone`); Omni
|
|
@@ -4273,7 +4484,7 @@ an affordance, never the enforcement: the host asks, and the provider does or de
|
|
|
4273
4484
|
answers `failed` for a command whose capability it did not publish, and that is the provider
|
|
4274
4485
|
honouring its own declaration, not the host deciding policy -- a host that refuses a command on
|
|
4275
4486
|
its own reading of a queue's flag has decided something that was never its to decide. What a
|
|
4276
|
-
command means on the platform is the provider's to work out: `end-call`
|
|
4487
|
+
command means on the platform is the provider's to work out: `end-call` disconnects the caller channel, and
|
|
4277
4488
|
which legs on which bridge that touches is a fact about the switch, never a choice the host makes.
|
|
4278
4489
|
The one thing the host performs physically is the microphone, and that is not a command at all.
|
|
4279
4490
|
|
|
@@ -4308,14 +4519,14 @@ and an adapter would take the agent out of ready in the middle of a call. A brow
|
|
|
4308
4519
|
mute through the capture track's muted state, read-only, and cannot lift it; a native host reads
|
|
4309
4520
|
the endpoint and can clear an operating-system mute. A hardware slider is nobody's to clear.
|
|
4310
4521
|
|
|
4311
|
-
**The record carries the same word.** A `muted`
|
|
4522
|
+
**The record carries the same word.** A `muted` interaction leg is a period the agent could not be
|
|
4312
4523
|
heard, and the record exists so that period is not a hole. The host reports every such period
|
|
4313
4524
|
through `recordStep` -- its own Mute, and a station mute it observed during a call -- with
|
|
4314
4525
|
`mutedBy` saying whose the silence was, and the provider writes the word into the entry
|
|
4315
|
-
(`task.
|
|
4526
|
+
(`task.interactionHistory.mutedBy`). The report names no agent because the host has exactly one, and
|
|
4316
4527
|
the provider knows who that is: it attributes the leg to the login's agent in `by`, a fact it
|
|
4317
4528
|
holds and not an inference, for a station mute as much as for the host's own. An unattributed
|
|
4318
|
-
`muted` entry is refused (`task.
|
|
4529
|
+
`muted` entry is refused (`task.interactionHistory.muted.by`); a shared handset's unattributed hold
|
|
4319
4530
|
is a different claim, and stands. A supervisor reading the record then sees "the agent muted for
|
|
4320
4531
|
forty seconds" and "the agent's headset was muted for forty seconds" as the different things they
|
|
4321
4532
|
are. See **The host records what it performs**.
|
|
@@ -4373,14 +4584,15 @@ declared:
|
|
|
4373
4584
|
| `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying a non-empty string for each `required` prompt field and strings for any optional fields supplied (`command.custom.prompt`). A toggle carries its target `on` boolean (`command.custom.on`). |
|
|
4374
4585
|
| Everything else | Its own named capability. |
|
|
4375
4586
|
|
|
4376
|
-
**A control on the contact belongs to the
|
|
4587
|
+
**A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
|
|
4377
4588
|
`hold`, `resume` and `pause`, `end-call`, every `transfer` and `conference` action, and
|
|
4378
|
-
every `lead-assist` action.
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4589
|
+
every `lead-assist` action. These controls act within this agent's current interaction.
|
|
4590
|
+
In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
|
|
4591
|
+
an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
|
|
4592
|
+
of this interaction does not establish that the caller or bridge ended. The capability stays
|
|
4593
|
+
declared while the task remains open; the phase prevents this task from controlling an interaction
|
|
4594
|
+
it no longer owns. Omni shows these controls only in the two interaction phases, and
|
|
4595
|
+
`validateTaskCommand` refuses them outside those phases (`command.phase.interaction`).
|
|
4384
4596
|
The commands with a phase of their own -- `answer`, `accept` and `decline` in `pending`,
|
|
4385
4597
|
`call` in `preview`, `connect-back` in `completing`, `complete` in any -- are not among them.
|
|
4386
4598
|
|
|
@@ -4460,22 +4672,102 @@ They are published as `OMNI_FAILURE_CODES`.
|
|
|
4460
4672
|
| `occurredAt` | Valid RFC-3339 timestamp with an explicit timezone, representing provider observation time. |
|
|
4461
4673
|
| `event` | Typed `ProviderEvent` payload. |
|
|
4462
4674
|
|
|
4463
|
-
####
|
|
4675
|
+
#### Timestamp standard and explicit exceptions
|
|
4676
|
+
|
|
4677
|
+
Every provider event envelope carries `occurredAt` as an ISO timestamp in the contract's RFC-3339
|
|
4678
|
+
form, with `Z` or an explicit numeric timezone offset. Preserve the original instant. Payloads
|
|
4679
|
+
carry their source event/observation instants wherever available and declared by their type;
|
|
4680
|
+
publication and receipt time must not be substituted for a missing source instant. A required
|
|
4681
|
+
instant that the source cannot establish makes that evidence unavailable, not permission to
|
|
4682
|
+
invent a timestamp or silently omit a required field.
|
|
4464
4683
|
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
damaging direction is early — **Accept** withdrawn from an offer still ringing, a wrap timer
|
|
4470
|
-
expiring before the agent has finished.
|
|
4684
|
+
Clock estimation is an exception, not the standard interpretation of events. An ISO timestamp
|
|
4685
|
+
identifies an instant; it does not prove that the host and provider clocks are synchronized.
|
|
4686
|
+
Use an explicitly trusted clock in the relevant domain. Do not infer a general clock offset
|
|
4687
|
+
from event arrivals, shift source timestamps, or use an estimate to order events or create history.
|
|
4471
4688
|
|
|
4472
|
-
|
|
4473
|
-
running offset against the `occurredAt` inside it, and translates provider instants through that
|
|
4474
|
-
offset before counting down. What remains is network delay, which biases every deadline later —
|
|
4475
|
-
the direction that costs a second rather than an action.
|
|
4689
|
+
The current exceptions and their reasons are:
|
|
4476
4690
|
|
|
4477
|
-
|
|
4478
|
-
|
|
4691
|
+
| Case | Exception and reason |
|
|
4692
|
+
| --- | --- |
|
|
4693
|
+
| Host display of provider deadlines (`allocationExpiresAt`, `previewEndsAt`, wrap deadline) | A countdown may use an explicitly bounded provider-clock estimate when a trusted direct clock is unavailable, because the deadline belongs to another clock domain. This estimates the display only; it does not establish that the provider acted. Without usable time, show timing uncertainty. |
|
|
4694
|
+
| Host-triggered preview deadline | A bounded clock estimate with monotonic aging may schedule the host's Call command because the provider owns the deadline but the host owns the trigger. Do not trigger before the deadline is known to have passed; invalidate on clock discontinuity and reconcile when uncertain. |
|
|
4695
|
+
| Recording evidence expiry | A bounded observer-domain clock estimate with monotonic aging may assess freshness because observation and expiry belong to the recorder's clock. If time cannot be trusted, recording state is unknown; never renew evidence from receipt or replay. |
|
|
4696
|
+
| Unknown recording state | `observedAt` and `validUntil` are absent because there is no confirmed observation. The containing provider event still has its own `occurredAt`; that publication is not a recorder observation. |
|
|
4697
|
+
| Direct snapshot reads and method requests/results | These are reads/operations, not event envelopes, and their current types have no general event timestamp. A snapshot event still carries `occurredAt`; embedded source instants remain unchanged. Do not treat a method result or read completion time as an occurrence boundary. |
|
|
4698
|
+
| Host-provided provider-clock estimate | Best-effort ISO time may be supplied by the host for optional comparisons; it is not a source observation or accuracy guarantee. The reason is that the host does not own the provider clock. |
|
|
4699
|
+
| Provider receipt timestamps | The provider may use its own receipt instant for final records of receipt/processing because host timestamps are untrusted advisory input. Receipt must not be presented as an earlier action or capture boundary. |
|
|
4700
|
+
| Optional source instants and deadlines | Omit only where the declared type permits absence and the source has no evidence or the policy has no deadline (for example unlimited preview). Do not replace absence with host time. |
|
|
4701
|
+
|
|
4702
|
+
Every additional exception must be listed with its scope and reason before adoption. Estimates
|
|
4703
|
+
must specify their uncertainty and validity; elapsed time should use monotonic aging, and a clock
|
|
4704
|
+
jump invalidates the estimate. The optional check below samples provider time without changing event timestamps. Report
|
|
4705
|
+
source-measured durations where required; timestamp subtraction is not a substitute.
|
|
4706
|
+
|
|
4707
|
+
#### Optional periodic provider time checks and host estimates
|
|
4708
|
+
|
|
4709
|
+
A provider may declare `Manifest.timeCheck: true` and implement `Connection.checkTime(request)`.
|
|
4710
|
+
Omission means unsupported. The host separately opts in with `ProviderTimeCheckPolicy`; these
|
|
4711
|
+
host-local settings are not task policy or a guarantee. All four durations are explicit positive
|
|
4712
|
+
safe integer milliseconds, with `maxRoundTripMs <= timeoutMs <= intervalMs`. No default interval
|
|
4713
|
+
is assumed. Validate settings with `validateProviderTimeCheckPolicy`.
|
|
4714
|
+
|
|
4715
|
+
While the connection is active, the host checks immediately on opt-in and then at `intervalMs`
|
|
4716
|
+
using a monotonic scheduler. Only one check may be outstanding. Missed ticks are skipped, never
|
|
4717
|
+
replayed in a burst. Each request has a fresh ID. The provider samples its own event/deadline
|
|
4718
|
+
clock after receiving the request and before returning `providerTime` as an ISO/RFC3339 instant,
|
|
4719
|
+
with the request ID, authenticated login ID and `clockId`. No cached sample or adapter-local time
|
|
4720
|
+
may masquerade as provider time. A provider unable to sample that domain must not declare support.
|
|
4721
|
+
Clock authority replacement or a discontinuity changes `clockId`.
|
|
4722
|
+
|
|
4723
|
+
Validate requests and responses with `validateProviderTimeCheckRequest` and
|
|
4724
|
+
`validateProviderTimeCheckResult`. The host also checks the outstanding request, provider/connection
|
|
4725
|
+
and login, monotonic elapsed time and cancellation after the await: a late successful response is
|
|
4726
|
+
still rejected. Reject samples at or beyond `timeoutMs`, or above `maxRoundTripMs`. Cancel local
|
|
4727
|
+
tracking and invalidate estimates on disconnect, logout, policy disable, clock change or clock
|
|
4728
|
+
jump; late results cannot restore them. Resume with a fresh check after reconnection. Expire a
|
|
4729
|
+
sample at `maxSampleAgeMs` using monotonic elapsed time. Failure is visible through the host's
|
|
4730
|
+
existing diagnostic path, with the estimate unavailable; the next scheduled check can recover.
|
|
4731
|
+
Checking failure alone does not assert call/recording state or require a healthy connection to close.
|
|
4732
|
+
This read-only check is not a task command. Protocol supplies declarations and validators; the
|
|
4733
|
+
host implements the scheduler and the provider implements the clock read.
|
|
4734
|
+
|
|
4735
|
+
The host may also expose `Host.estimateProviderTime({ providerId, loginId })`. It returns an ISO
|
|
4736
|
+
`at` in that provider's clock domain, the same explicit scope and `clockId`, or `undefined` when
|
|
4737
|
+
unavailable. Optional `uncertaintyMs` is an estimate, not a guaranteed bound. The method is optional
|
|
4738
|
+
at the host level and is deliberately absent from `HostGuarantees`. A provider declaring time
|
|
4739
|
+
checks does not require the host to expose estimates, and an estimate need not originate from
|
|
4740
|
+
this check if the host has another explicitly configured source for that provider clock.
|
|
4741
|
+
|
|
4742
|
+
To estimate the difference, the host brackets the request with local send/receive instants and
|
|
4743
|
+
measures elapsed time monotonically. For provider sample P and local bracketing instants H0/H1,
|
|
4744
|
+
the offset under stable clocks lies between P-H1 and P-H0, before clock error and timestamp
|
|
4745
|
+
precision are considered. A midpoint is only an estimate; one-way delays need not be symmetric.
|
|
4746
|
+
Never treat round-trip uncertainty as proof of the source clock's accuracy. Advance a retained
|
|
4747
|
+
sample with monotonic elapsed time, and return unavailable after its configured validity ends.
|
|
4748
|
+
`validateProviderTimeEstimate` checks shape and scope, not actual accuracy or freshness.
|
|
4749
|
+
|
|
4750
|
+
A provider may declare `Manifest.timestampAuthority: "provider"`: its own timestamps are
|
|
4751
|
+
used for final records, and host-supplied timestamps are advisory. This declaration is independent
|
|
4752
|
+
of time-check support and the optional host estimate; neither changes who owns the final record.
|
|
4753
|
+
Omission makes no promise that host timestamps will be trusted. No host-authoritative default
|
|
4754
|
+
is inferred. Successful `recordStep` responses identify the provider-selected history instant;
|
|
4755
|
+
see **The host records what it performs** for correlation and retained bindings.
|
|
4756
|
+
|
|
4757
|
+
A provider need not trust or adopt any host-supplied timestamp. It may timestamp an incoming
|
|
4758
|
+
message using its own clock at receipt and use that for its own processing/accounting. The reason
|
|
4759
|
+
for this exception is that host clocks and host estimates are not authoritative at the provider.
|
|
4760
|
+
Receipt time must be described as receipt/observation time, not relabeled as the original host
|
|
4761
|
+
action, recorder capture boundary, or proof an operation applied. An existing field with a
|
|
4762
|
+
specific occurrence meaning still requires that evidence; this option does not silently redefine
|
|
4763
|
+
it. Host-only instants remain advisory input; the provider owns its authoritative publication.
|
|
4764
|
+
|
|
4765
|
+
This host estimate is another explicit timestamp exception: it helps optional displays and
|
|
4766
|
+
provider-clock comparisons when the host lacks that clock directly. It is not a source occurrence,
|
|
4767
|
+
may not replace event/history timestamps, and alone cannot authorize deadline actions or prove
|
|
4768
|
+
recording freshness. Uses requiring a trustworthy bound still need independently established
|
|
4769
|
+
clock accuracy/drift assumptions. It neither synchronizes the operating-system clock nor changes
|
|
4770
|
+
ISO timestamps already published by the provider.
|
|
4479
4771
|
|
|
4480
4772
|
#### Nothing is lost until the connection drops
|
|
4481
4773
|
|
|
@@ -4588,6 +4880,48 @@ from `in-effect` or `starting-after-task` to a grant or a request, or from `gran
|
|
|
4588
4880
|
`exerciseAdapter` holds the stream to that from the connect snapshot on;
|
|
4589
4881
|
`assertBreakFollowsItsRequests` holds any sequence.
|
|
4590
4882
|
|
|
4883
|
+
Adapters and hosts can call `validateBreakTransition(before, after)` from the package’s validation entry point
|
|
4884
|
+
with two complete break objects before applying an event. It checks shape and the transition
|
|
4885
|
+
rules above; it does not mutate either state or apply the event. Report a violation visibly,
|
|
4886
|
+
retain the last accepted state without claiming it remains current, and reconcile from the
|
|
4887
|
+
source. Continue to validate the full envelope/login and task/break consistency separately.
|
|
4888
|
+
The conformance `BreakStream` validates its baseline, retains the last accepted state on a
|
|
4889
|
+
rejected delta, and sets `needsRecovery`. Transport loss also requires reseeding; an `active` transport
|
|
4890
|
+
notification alone does not restore a break baseline. Until a fresh validated snapshot is seeded, further
|
|
4891
|
+
break deltas are refused (`stream.breakState.baseline`); the first delta is never an implicit
|
|
4892
|
+
snapshot. `seed(snapshot)` returns violations, which callers must check. This is a conformance
|
|
4893
|
+
tracker, not a complete production reducer: snapshot freshness, login/connection fencing and
|
|
4894
|
+
full task consistency remain separately required.
|
|
4895
|
+
|
|
4896
|
+
**Both sides must enforce the order.** The host validates immediately before dispatch; the
|
|
4897
|
+
provider rechecks current permission and state atomically before acting. The provider validates
|
|
4898
|
+
the resulting break/task state before publication and serializes those publications. The host
|
|
4899
|
+
validates the envelope, transition and full task consistency before replacing local state. A
|
|
4900
|
+
request result does not optimistically advance break state, and a delayed command cannot apply
|
|
4901
|
+
to a later attempt merely because its approval happens to look compatible. Keep at most one
|
|
4902
|
+
unresolved operation per provider/attempt; recovery must resolve uncertainty before another
|
|
4903
|
+
operation is dispatched. The source must fence delayed operations against its own attempt state.
|
|
4904
|
+
|
|
4905
|
+
Normal order is `not-requested` → `awaiting-decision` → `granted` →
|
|
4906
|
+
`starting-after-task` → `in-effect` → `not-requested`. Auto-approval may go directly to
|
|
4907
|
+
`granted`; a commit with no outstanding work may go directly to `in-effect`. Denial/cancel
|
|
4908
|
+
returns a precommit request to `not-requested`; an authorized end/release returns a committed
|
|
4909
|
+
break there. Same-state restatements are allowed. An evidenced imposed break is the explicit
|
|
4910
|
+
exception to requesting/granting, and must carry its imposed actor/state. Neither skipped
|
|
4911
|
+
publication nor a host-local guess creates another exception. A later normal attempt starts
|
|
4912
|
+
from `not-requested`, never by regressing an active break into a request.
|
|
4913
|
+
|
|
4914
|
+
A fresh authoritative snapshot establishes a new baseline; do not run this event-transition
|
|
4915
|
+
check across it. A snapshot may legitimately establish a later request or an already active
|
|
4916
|
+
break after reconnect. Its freshness must be established by the source/adapter's ordered
|
|
4917
|
+
snapshot/publication boundary and fencing of obsolete callbacks and reads. A delayed old
|
|
4918
|
+
snapshot is not detectable from break approval alone. Neither envelope time nor event ID
|
|
4919
|
+
supplies a break revision. Do not suppress all requests after an active break: a later attempt
|
|
4920
|
+
passes through `not-requested`, and then may request again. Do not replay earlier ProviderEvents
|
|
4921
|
+
after recovery. Raw backend break handlers must preserve source order too; this helper does
|
|
4922
|
+
not implement or assume invocation of Protocol's two-phase request/commit coordinator.
|
|
4923
|
+
|
|
4924
|
+
|
|
4591
4925
|
For a multi-provider break attempt, "every provider" is the set of providers frozen when the
|
|
4592
4926
|
attempt entered `requesting-break`. Omni commits only after every asked provider reports `granted` —
|
|
4593
4927
|
that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
|
|
@@ -4648,14 +4982,14 @@ A `left` outcome ends the task for this agent alone: the call continues without
|
|
|
4648
4982
|
when a lead who joined it leaves -- see **Lead assist**.
|
|
4649
4983
|
|
|
4650
4984
|
A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
|
|
4651
|
-
and not for ever: `applied` to a
|
|
4652
|
-
provider has
|
|
4653
|
-
manifest's `
|
|
4985
|
+
and not for ever: `applied` to a completion -- `complete`, or a lead's `take-over` -- says the
|
|
4986
|
+
provider has completed the task, and its `task-ended` follows within the
|
|
4987
|
+
manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
|
|
4654
4988
|
performed. Past the bound the host calls `snapshot()`: a snapshot still carrying the task is a task
|
|
4655
4989
|
held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
|
|
4656
4990
|
the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
|
|
4657
4991
|
task, since the ending was owed and lost. The drive holds a provider to the same bound
|
|
4658
|
-
(`drive.
|
|
4992
|
+
(`drive.completion.unsettled`). The `task-media-ended` event and the `completing` phase are likewise
|
|
4659
4993
|
non-terminal. A replacement
|
|
4660
4994
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|
|
4661
4995
|
same envelope ID is harmless, and a `task-ended` naming an allocation that has already ended is
|
|
@@ -4741,7 +5075,7 @@ The same treatment for a `UserId`, and needed for the same reason: user identifi
|
|
|
4741
5075
|
issued by each provider independently, so two providers will eventually issue the same string for
|
|
4742
5076
|
different people. Encode and join before storing or comparing.
|
|
4743
5077
|
|
|
4744
|
-
Use it for every `UserId` — `
|
|
5078
|
+
Use it for every `UserId` — `interactionHistory[].by`, roster members, `memberId` on a
|
|
4745
5079
|
lead command, `ImposedBreak.by`. A bare one is only ever compared against another from the **same** provider; anything
|
|
4746
5080
|
wider goes through this key.
|
|
4747
5081
|
|
|
@@ -4766,13 +5100,17 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
4766
5100
|
| `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
|
|
4767
5101
|
| `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
|
|
4768
5102
|
| `validateHostGuarantees(guarantees)` | What a host promises: only the guarantees this contract names, each declared by presence and never `false`. The harness validates the guarantees of whatever host a test hands the adapter. |
|
|
4769
|
-
| `
|
|
5103
|
+
| `validateProviderTimeCheckPolicy(policy)` | Explicit optional polling settings, positive safe-integer durations and round-trip/timeout/interval ordering. |
|
|
5104
|
+
| `validateProviderTimeCheckRequest(request)` | Fresh-request shape; the host enforces actual uniqueness and outstanding-request lifetime. |
|
|
5105
|
+
| `validateProviderTimeCheckResult(result, request, loginId)` | ISO timestamp, clock identity, exact request/login correlation; timing and source accuracy remain runtime checks. |
|
|
5106
|
+
| `validateProviderTimeEstimate(estimate, scope)` | Optional host estimate shape and provider/login scope; no accuracy guarantee. |
|
|
5107
|
+
| `validateInteractionReport(report, path?, manifest?)` | What the host reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
|
|
4770
5108
|
| `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
4771
5109
|
| `validateHostMute(mute, softphone)` | What the host's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the host holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
|
|
4772
5110
|
| `validateLoginStore(store)` | The login's store the host hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
|
|
4773
5111
|
| `validateCapacity(capacity)` | What the host states as capacity: a whole number of zero or more (`capacity.count`), zero being host-stopped. The harness states one on connect and two, one and zero after the drive, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
|
|
4774
5112
|
| `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
|
|
4775
|
-
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.
|
|
5113
|
+
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
|
|
4776
5114
|
| `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
|
|
4777
5115
|
| `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
|
|
4778
5116
|
|
|
@@ -4848,7 +5186,7 @@ latest the session published during the run, which differs only when the adapter
|
|
|
4848
5186
|
A capability granted by a later login requires its methods just as one declared at sign-in does.
|
|
4849
5187
|
|
|
4850
5188
|
`result.notExercised` lists what the run never reached — one subject per family of rules: each
|
|
4851
|
-
optional part of a task (`task.browsers`, `task.
|
|
5189
|
+
optional part of a task (`task.browsers`, `task.interactionHistory`, `task.leadAssist`, …), the break's
|
|
4852
5190
|
`reasons` and `imposed`, the roster's `members` and `requests`, each declared contribution, and
|
|
4853
5191
|
each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
|
|
4854
5192
|
Nothing there is a violation: an adapter with no team has nothing to exercise. But a fixture with
|
|
@@ -4880,9 +5218,9 @@ as each case was considered, so a test that needs a rule to have run asserts it
|
|
|
4880
5218
|
inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
|
|
4881
5219
|
gap and not a pass. With the audio open on a softphone,
|
|
4882
5220
|
the drive mutes it for one second and reports the leg through `recordStep`, begun and then ended,
|
|
4883
|
-
expecting each report `recorded` (`drive.recordStep.failed`, `.rejected`); then it mutes again and
|
|
5221
|
+
expecting each report `recorded` with the provider-selected history `at` (`drive.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
|
|
4884
5222
|
ends the call muted, as agents do, so the leg is open when the media ends, the provider closes it
|
|
4885
|
-
in the completing publication or the open entry is refused (`task.
|
|
5223
|
+
in the completing publication or the open entry is refused (`task.interactionHistory.muted.open`),
|
|
4886
5224
|
and the drive's closing report after the media ended is expected `recorded` and to change nothing:
|
|
4887
5225
|
a record restated afterwards with that leg's duration altered is named (`drive.recordStep.overwritten`).
|
|
4888
5226
|
Where the provider restates the task's record afterwards, each leg is in it or the hole is named
|
|
@@ -4890,12 +5228,12 @@ Where the provider restates the task's record afterwards, each leg is in it or t
|
|
|
4890
5228
|
does, the drive reloads the host as a reload happens: the first client is unsubscribed,
|
|
4891
5229
|
disconnected and its session closed (`drive.reload.handover`), and only then is a second adapter
|
|
4892
5230
|
built and connected for the same login with the same context and the same `store`; its snapshot
|
|
4893
|
-
must carry the task with that leg and the
|
|
5231
|
+
must carry the task with that leg at its acknowledged provider timestamp and the reported actor, and the run goes on with the second as its
|
|
4894
5232
|
connection to the end. A platform that holds the record hands it back, and an adapter that composed
|
|
4895
5233
|
the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
|
|
4896
5234
|
`.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
|
|
4897
5235
|
before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
|
|
4898
|
-
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.
|
|
5236
|
+
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.interactionHistory`,
|
|
4899
5237
|
`.media`) and a reload is a place those rules keep working, not one where they stop. The second
|
|
4900
5238
|
adapter is held to what the first was: the same provider
|
|
4901
5239
|
(`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
|
|
@@ -4912,11 +5250,11 @@ handed the adapter holds no key naming the task's id, delimited, never as a run
|
|
|
4912
5250
|
another id (`drive.store.retained`); an adapter whose keys never named the task leaves the rule
|
|
4913
5251
|
unevaluated, and the result says so rather than passing it: a task's keys go
|
|
4914
5252
|
with the task, or the next offer of the same id inherits them. Outside the
|
|
4915
|
-
|
|
4916
|
-
`confirmed`, where the provider publishes it, and in `completing` once
|
|
5253
|
+
interaction phases with `hold` still declared -- in
|
|
5254
|
+
`confirmed`, where the provider publishes it, and in `completing` once this agent's interaction has ended -- the
|
|
4917
5255
|
drive sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
|
|
4918
|
-
is the second gate on a control on the contact, and one that applies it
|
|
4919
|
-
|
|
5256
|
+
is the second gate on a control on the contact, and one that applies it outside this task's
|
|
5257
|
+
current interaction is named (`drive.command.interaction`). The drive cannot put a task into a phase the provider
|
|
4920
5258
|
never publishes, so a provider that goes straight from `pending` to `in-progress` is checked in
|
|
4921
5259
|
`completing` alone. A task the agent completes is
|
|
4922
5260
|
completed from wherever it stands once the drive has nothing left to do on it -- from `completing`
|
|
@@ -5021,10 +5359,10 @@ This package declares that contract; it supplies no recorder, media mixing, stor
|
|
|
5021
5359
|
|
|
5022
5360
|
Stop finishes the recording and retains captured audio. Cancel abandons the recording and discards
|
|
5023
5361
|
its captured audio; the task policy offers it with `cancel: true`. There is no configurable cancel
|
|
5024
|
-
effect. A recorder that cannot confirm
|
|
5362
|
+
effect. A recorder that cannot confirm completion must not offer Cancel; it can offer Stop instead.
|
|
5025
5363
|
Cancel never means cancelling an in-flight start request or deleting arbitrary past recordings.
|
|
5026
5364
|
Stop can be applied only after finalization/retention succeeds; Cancel only after both cessation
|
|
5027
|
-
and
|
|
5365
|
+
and completion of this recording's audio succeed under the recorder's storage contract.
|
|
5028
5366
|
Partial success (capture stopped but storage outcome unknown) cannot return failed with a claim
|
|
5029
5367
|
of no effect. It rejects with unknown outcome, reports the failure visibly and publishes whatever
|
|
5030
5368
|
current capture state is actually known. Retention is not a promise of sample-perfect audio.
|
|
@@ -5042,7 +5380,7 @@ host until its executor reconciles/finishes them, with visible unresolved cleanu
|
|
|
5042
5380
|
|
|
5043
5381
|
Only start/resume require an in-progress or paused task with started media. Pause/stop/cancel may
|
|
5044
5382
|
also finish an independently observed recorder while the task is completing. A pending task may
|
|
5045
|
-
show active recording, but agent controls wait until
|
|
5383
|
+
show active recording, but agent controls wait until interaction begins. The two recording paths may
|
|
5046
5384
|
both be active. A command to either path has no implied effect on the other.
|
|
5047
5385
|
|
|
5048
5386
|
Each confirmed observation has a fresh opaque observation identity, a canonical UTC millisecond
|
|
@@ -5050,7 +5388,8 @@ observation instant and an exclusive expiry. These times describe current eviden
|
|
|
5050
5388
|
capture boundaries. A trusted observer-domain current time must satisfy observedAt <= now < validUntil.
|
|
5051
5389
|
Use `effectiveRecordingState` with that explicitly trusted time; an unavailable clock yields unknown.
|
|
5052
5390
|
Receipt, replay and task publication never extend freshness. Clock discontinuity invalidates evidence;
|
|
5053
|
-
consumers
|
|
5391
|
+
consumers using the explicit recording-expiry clock-estimation exception must invalidate that
|
|
5392
|
+
estimate and use monotonic aging so clock rollback cannot
|
|
5054
5393
|
revive expired evidence. The executor compares observation identity and recording identity against
|
|
5055
5394
|
its latest state atomically before I/O. An intervening observation or allocation change refuses the
|
|
5056
5395
|
stale command; it never acts on a replacement recorder. Providers lacking trustworthy current-state
|
|
@@ -5070,7 +5409,7 @@ transition and disposition actually completed; publish the confirming task or ho
|
|
|
5070
5409
|
resolving applied. Failed means confirmed no effect. Rejected promise means outcome unknown:
|
|
5071
5410
|
show the failure, reconcile that path and do not automatically retry or pretend inactive. Authoritative
|
|
5072
5411
|
updates remain full current task/host views, not replayed provider events. These current-state fields
|
|
5073
|
-
create no
|
|
5412
|
+
create no interaction-history entries and infer no actors, durations or historical capture boundaries.
|
|
5074
5413
|
|
|
5075
5414
|
Migration is explicit: replace the old true recording capability with per-path action policies,
|
|
5076
5415
|
replace untargeted commands with scoped commands, and retain protocol version 1. During pre-release
|
|
@@ -5080,7 +5419,7 @@ This change does not publish a package or enable recording in any existing host/
|
|
|
5080
5419
|
|
|
5081
5420
|
`validateRecordingOutcome` checks confirming observations against recording-specific applied/failed
|
|
5082
5421
|
semantics, including pause/resume identity and rejection of a dialling result. It cannot prove audio
|
|
5083
|
-
retention/
|
|
5422
|
+
retention/completion or source truth from a status flag; those remain executor obligations.
|
|
5084
5423
|
|
|
5085
5424
|
Host destination selection binds storage when start actually creates a recording. Existing recordings
|
|
5086
5425
|
keep that binding through pause/resume/stop/cancel; a later policy cannot redirect their stored audio.
|