@xema/omni-protocol 0.1.105 → 0.1.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/testing.d.ts +1 -10
- package/dist/testing.js +0 -27
- package/guide/agent.md +1090 -0
- package/guide/breaks.md +540 -0
- package/guide/chat.md +12 -0
- package/guide/email.md +11 -0
- package/guide/lead.md +333 -0
- package/guide/phone.md +274 -0
- package/guide/voice.md +602 -0
- package/guide.md +66 -2884
- package/package.json +2 -1
package/guide/voice.md
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
# `@xema/omni-protocol`: Voice
|
|
2
|
+
|
|
3
|
+
What is true of a call and of nothing else: preview, connecting back, ending a call, conference, every dial and its outcome, recording. Part of the contract in `guide.md`, which holds the terms, the shapes and the rules every file here relies on; a reference in bold names the file it points into where that is not this one.
|
|
4
|
+
|
|
5
|
+
## Preview: the agent presses Call
|
|
6
|
+
|
|
7
|
+
An outbound campaign that lets the agent see who they are about to call is a **preview**: the
|
|
8
|
+
record arrives as a task and sits in `preview` with the party on it before a call goes
|
|
9
|
+
out. The agent prepares; manual or declared automatic initiation may then begin dialing. It
|
|
10
|
+
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 --
|
|
11
|
+
chat and email have no call to place, and their reading is ordinary work in `in-progress`.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const previewed = {
|
|
15
|
+
channel: "voice",
|
|
16
|
+
capabilities: {},
|
|
17
|
+
phase: "preview",
|
|
18
|
+
party: { name: "Maya Rao", number: "+919876543210" },
|
|
19
|
+
previewEndsInSeconds: 120,
|
|
20
|
+
atDeadline: "provider-dials",
|
|
21
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "party" | "previewEndsInSeconds" | "atDeadline">;
|
|
22
|
+
|
|
23
|
+
// The agent presses Call. It is a dial like any other.
|
|
24
|
+
const pressed: TaskCommand<"voice"> = { type: "dial", dialId: "dial-7f2" };
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Call is a dial.** It carries the agent application's `dialId`, is answered `dialling`, and ends in exactly one
|
|
28
|
+
`dial-outcome`; the phase is its gate and there is no capability, since a record put in front of an
|
|
29
|
+
agent is there to be called. Its audio starts on `dialling`, as every agent application-placed dial's does --
|
|
30
|
+
ring-back is audio the agent hears -- so `task-audio-started` arrives on the `preview` task with
|
|
31
|
+
its party ringing. On `answered` the task is `in-progress`; on any other outcome the audio ends,
|
|
32
|
+
`task-audio-ended` starts the wrap clock as on every voice task, and the task goes to
|
|
33
|
+
`completing`, so the agent records the
|
|
34
|
+
no-answer as the outcome it is -- in a campaign that is the commonest outcome there is, and it
|
|
35
|
+
is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
|
|
36
|
+
(`task.preview.dialOutcomes.required`).
|
|
37
|
+
|
|
38
|
+
**Preparation and trigger ownership are per task.** Unlimited preparation omits both
|
|
39
|
+
`previewEndsInSeconds` and `atDeadline`: only an agent pressing Call starts dialing. Fixed
|
|
40
|
+
preparation carries the seconds the provider says are left, counted down on the desk from the
|
|
41
|
+
publication that carried them and restated on every later one, and exactly one trigger owner:
|
|
42
|
+
|
|
43
|
+
| Declaration | At preparation end |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `atDeadline: "provider-dials"` | The provider initiates dialing. The agent application never sends a timer-triggered Call. |
|
|
46
|
+
| `atDeadline: "host-dials"` | The agent application sends the ordinary `dial` command with a fresh agent application `dialId`. The provider does not independently auto-dial this task. |
|
|
47
|
+
| `atDeadline: "waits"` | Neither side auto-dials. The task remains in preview until the agent presses Call. |
|
|
48
|
+
|
|
49
|
+
The agent may press Call early in either fixed-preparation dialing mode, or at any time in
|
|
50
|
+
`waits` mode. With `waits`, the countdown is a preparation target, not an expiry: when it reaches
|
|
51
|
+
zero the UI says "Waiting for agent" and keeps Call available. There is no automatic task-ended,
|
|
52
|
+
completion, dialing or phase change. No repeating timer action occurs on later snapshots. This differs
|
|
53
|
+
from unlimited preparation only by displaying a preparation target. The former `expires` deadline
|
|
54
|
+
value is not supported; elapsed preparation must not withdraw the task. For `provider-dials` and `host-dials`, the deadline ends
|
|
55
|
+
preparation and triggers initiation; it does not promise ringing, playable audio or customer
|
|
56
|
+
answer at that exact instant. Scheduling/dispatch delay must remain visible, and a failed or
|
|
57
|
+
prevented initiation must be reported rather than leaving a silently expired countdown.
|
|
58
|
+
A source eligibility threshold that may never trigger an attempt is not this promise.
|
|
59
|
+
|
|
60
|
+
The two deadline fields travel together and only in preview. The countdown is the seconds the
|
|
61
|
+
publication carried, counted from its receipt: the agent application infers no deadline where none
|
|
62
|
+
was stated and reads no clock against another. Neither elapsed time nor submission changes the
|
|
63
|
+
task phase: subsequent authoritative events state ringing, answer, audio and completion.
|
|
64
|
+
|
|
65
|
+
An agent application must serialize manual clicks and its timer under the exact provider/login/task/assignment
|
|
66
|
+
scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
|
|
67
|
+
assignment and whether an attempt already exists before dispatch. A new snapshot or policy update
|
|
68
|
+
cancels an obsolete timer; login loss, disconnect, task end and assignment replacement fence its
|
|
69
|
+
callback. Reconnect past a deadline first reconciles current task and attempt state; it must not
|
|
70
|
+
blindly submit again. An uncertain prior submission remains unresolved, without automatic retry.
|
|
71
|
+
The provider atomically arbitrates any residual manual/timer race and does not accept a second
|
|
72
|
+
command as ownership of an already-running independent attempt.
|
|
73
|
+
|
|
74
|
+
Agent application-triggered automatic dialing is an ordinary agent application dial: its result is `dialling` with the same
|
|
75
|
+
agent application `dialId`, followed by exactly one correlated terminal `dial-outcome`. A provider-triggered
|
|
76
|
+
dial has no invented agent application ID or agent application dial outcome. It reports source-evidenced party ringing,
|
|
77
|
+
actual audio and customer answer separately. In preview with `atDeadline: "provider-dials"`, a ringing
|
|
78
|
+
party without an agent application ID may carry actual pre-answer audio; the deadline alone permits no audio.
|
|
79
|
+
Audio is still introduced/ended by the corresponding events and must not imply customer answer.
|
|
80
|
+
Submission acceptance is never an answered outcome or permission to publish `in-progress` early.
|
|
81
|
+
Unsupported source correlation/outcome/audio evidence remains an integration blocker.
|
|
82
|
+
|
|
83
|
+
**Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
|
|
84
|
+
spreading the `preview` one carries `previewEndsInSeconds` and `atDeadline` with it, and the agent application refuses
|
|
85
|
+
the update (`task.preview.deadline.unexpected`): the desk keeps the task as it last stood, and the
|
|
86
|
+
provider is told through `refused` exactly which rule, so the state that looked right on its side
|
|
87
|
+
is named on its side. The task past preview has no deadline to wait for, so it carries neither
|
|
88
|
+
field.
|
|
89
|
+
|
|
90
|
+
**Provider execution and agent application controls are distinct.** Answer, hold/resume, conference,
|
|
91
|
+
lead assist and interaction completion are executed by the provider
|
|
92
|
+
through the adapter. The task's applicable capability, phase, completion mode and command-specific
|
|
93
|
+
prerequisites decide which controls are offered. Caller disconnect also executes at the provider. Microphone mute is agent application-owned, never a provider
|
|
94
|
+
TaskCommand or task capability. The separately declared agent application recording facility keeps its existing
|
|
95
|
+
recording contract; this distinction does not remove it or move provider call operations to the agent application.
|
|
96
|
+
|
|
97
|
+
A caller journey may revisit the same agent while an earlier interaction still wraps. Each is its
|
|
98
|
+
own assignment, and two open at once carry two assignment ids. Restore them unchanged on
|
|
99
|
+
reconnect. Commands, audio, endings and history reports name the exact assignment; never retarget
|
|
100
|
+
an old command to the newest interaction of a journey. `validateTaskCommandRequest` checks the
|
|
101
|
+
request's assignment against the supplied published task and then its command prerequisites. The
|
|
102
|
+
caller must select that task within the correct provider/login and recheck at the provider; the
|
|
103
|
+
validator cannot prove source freshness or authorization.
|
|
104
|
+
`task-ended` retires only that interaction's resources. Another interaction, caller channel or IVR/queue
|
|
105
|
+
portion must not be cleared because this task ended. Snapshot counts include all still-owned wrap
|
|
106
|
+
and active interactions; task completion is not caller hangup.
|
|
107
|
+
|
|
108
|
+
**Voice describes the agent's interaction within the wider call.** The task defines that agent's
|
|
109
|
+
workspace, tools, permissions, audio participation and completion work. The caller's channel may
|
|
110
|
+
continue through IVR, queues, other agents, holds or conferences while this interaction ends or wraps.
|
|
111
|
+
Completing this task ends this interaction responsibility; it is not evidence that the caller's channel
|
|
112
|
+
or journey ended. Task audio describes this agent's attachment, not the lifetime of every party's
|
|
113
|
+
connection. Commands retain their explicit targets and effects; task completion must not silently
|
|
114
|
+
become a caller-disconnect operation.
|
|
115
|
+
|
|
116
|
+
**A task is never its audio.** A voice task represents the agent’s interaction: the call is offered when it is
|
|
117
|
+
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
118
|
+
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
119
|
+
conference leg joining or leaving, a lead joining, a take-over, a connect-back — the audio follows
|
|
120
|
+
separately, arriving on `task-audio-started`, attaching through `openAudio` and ending with
|
|
121
|
+
`task-audio-ended`. Omni does not ring,
|
|
122
|
+
bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
|
|
123
|
+
are the adapter's and the platform's, transient, and decide neither when a task exists nor what
|
|
124
|
+
phase it is in.
|
|
125
|
+
|
|
126
|
+
The line runs between the provider's word and Omni's own senses. `task-audio-ended` is the
|
|
127
|
+
provider's report that primary interaction ended — a fact about the work, which is why the completion
|
|
128
|
+
allowance starts on it and the Connect back control appears on it — and Omni follows that report as it
|
|
129
|
+
follows any other. What Omni never does is derive a task's state from its own audio session: a
|
|
130
|
+
stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
|
|
131
|
+
endpoint re-registering change nothing about the task until the provider says so. Structurally:
|
|
132
|
+
`task-audio-started` and `task-audio-ended` alternate on a task whose work has begun, audio ends
|
|
133
|
+
only where it arrived, what follows the audio ending is `completing` or `task-ended`, and every
|
|
134
|
+
task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
|
|
135
|
+
and `assertAudioFollowsTheTask` holds any sequence.
|
|
136
|
+
|
|
137
|
+
## Connecting back during completion
|
|
138
|
+
|
|
139
|
+
A task that declares `connectBack` lets the agent connect back to the party while the task is
|
|
140
|
+
`completing` -- to finish what the call left unfinished, on the same task rather than a new one,
|
|
141
|
+
whoever placed the call in the first place. Most such calls came in, so this is not a redial: the
|
|
142
|
+
agent rings a party who rang them. Omni issues `{ type: "connect-back", dialId }`; it is issuable
|
|
143
|
+
only in `completing`, and only where the capability is declared. The provider knows who the party
|
|
144
|
+
is; the command carries no destination, and it is a dial like any other, so it carries the agent application's
|
|
145
|
+
`dialId`, is answered `dialling`, and ends in one `dial-outcome`.
|
|
146
|
+
|
|
147
|
+
On `dialling` the provider is placing the call and the task returns to `in-progress`: the agent is
|
|
148
|
+
working again, and the wrap allowance is **discarded, not paused**. From there the call is
|
|
149
|
+
reported as any call is -- `paused`, `in-progress`, and when its audio ends, `task-audio-ended`
|
|
150
|
+
again, which starts a fresh allowance from that instant. A party who does not answer is a dial
|
|
151
|
+
whose outcome says so and a call whose audio ended: the task returns to `completing` through the
|
|
152
|
+
same event and the clock starts again from there. At no point is an agent dialling against a
|
|
153
|
+
deadline.
|
|
154
|
+
|
|
155
|
+
**The room shows the party being dialled.** From the moment any agent application dial places the party -- a
|
|
156
|
+
dialpad call, a preview Call, a connect-back -- the `party` entry carries `stage: "ringing"` and
|
|
157
|
+
the agent application's `dialId`, exactly as a conferenced entry does from its dial; `joined` on
|
|
158
|
+
the publication that follows the answered `dial-outcome` and never before it
|
|
159
|
+
(`stream.taskUpdated.stage`); and no stage from the next publication on, so `joined` is transient
|
|
160
|
+
and a later copy still carrying it is stale (`stream.taskUpdated.stage.lingering`). An agent application shows a
|
|
161
|
+
call being placed, not a party it asserts is on a call that is still ringing. A callback the platform places itself on the same task -- the first
|
|
162
|
+
call over, the platform dialling the party again without a command -- shows the same thing with no
|
|
163
|
+
agent application `dialId`, as any platform-placed dial does. A party with no stage is on the call, and a dial
|
|
164
|
+
with no stage is half a claim (`task.onCall.party.dial`). The stage is also what lets the stream
|
|
165
|
+
tell a task coming back from one going backwards (`stream.taskUpdated.phase`).
|
|
166
|
+
|
|
167
|
+
**The control exists only while there is a window to use it in.** Under `agent-command` the task
|
|
168
|
+
stays `completing` until the agent completes it, so the window is open for as long as they need.
|
|
169
|
+
Under `provider-automatic` the window is the allowance -- and with `wrapAllowance: 0` there
|
|
170
|
+
is none: the provider completes the task at provider end, and Omni does not offer Connect back,
|
|
171
|
+
whatever the task declares. A capability names a control that can be used; on a task with no `completing`
|
|
172
|
+
window it cannot, and declaring it there changes nothing.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const connectBackCapable = {
|
|
176
|
+
channel: "voice",
|
|
177
|
+
capabilities: { hold: true, connectBack: true, outcomes: true },
|
|
178
|
+
phase: "completing",
|
|
179
|
+
completionMode: "provider-automatic",
|
|
180
|
+
wrapAllowance: 30,
|
|
181
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "wrapAllowance">;
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
With ten seconds of the thirty left, the agent presses Connect back: `execute({ command: { type:
|
|
185
|
+
"connect-back", dialId } })` returns `dialling`, the task is `in-progress`, and the thirty seconds
|
|
186
|
+
are gone.
|
|
187
|
+
The second call ends: `task-audio-ended`, the task is `completing`, and a new thirty seconds runs
|
|
188
|
+
from that instant.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const immediateProviderCompletion = {
|
|
192
|
+
completionMode: "provider-automatic",
|
|
193
|
+
wrapAllowance: 0,
|
|
194
|
+
} satisfies Pick<Task, "completionMode" | "wrapAllowance">;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Voice capabilities
|
|
198
|
+
|
|
199
|
+
| Capability | Omni UI | Contract |
|
|
200
|
+
| --- | --- | --- |
|
|
201
|
+
| `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
|
|
202
|
+
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
203
|
+
| `endCall` | Primary button: End call | The provider ends the agent's channel and every channel the agent added; the caller continues on the provider's path. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
|
|
204
|
+
| `terminateCall` | Primary button: Terminate call | The provider ends the whole call: every channel on it, the agent's, the caller's, any colleague's and anyone else's. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
|
|
205
|
+
| `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**. |
|
|
206
|
+
| `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the call or from its wrap: a time, and a note. See **Scheduling a follow-up** in `guide/agent.md`. |
|
|
207
|
+
| `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** in `guide/lead.md`. |
|
|
208
|
+
| `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**. |
|
|
209
|
+
| `recording` | Overflow menu item: Recording | Per-task provider and agent application policies expose only their permitted recording actions; each has independent state and routing. |
|
|
210
|
+
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
211
|
+
|
|
212
|
+
## `conference`
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
capabilities: {
|
|
216
|
+
conference: {
|
|
217
|
+
destinations: [
|
|
218
|
+
{ id: "tier2", label: "Tier 2 support" },
|
|
219
|
+
{ id: "main-menu", label: "Back to the main menu" },
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Field | Contract |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `destinations` | The items the control offers, at least one, with unique `id` values. Each is a button or a menu item on the desk. |
|
|
228
|
+
| `id` | What Omni sends as `destinationId` on a `conference` command, and what the provider executes. |
|
|
229
|
+
| `label` | What the agent reads. |
|
|
230
|
+
|
|
231
|
+
**The protocol does not say what an item does.** A queue, an IVR menu, an outside line, a
|
|
232
|
+
skill group -- that is the queue's configuration and the provider's business. The agent sees a
|
|
233
|
+
list of labels, picks one, and Omni sends its `id`; the provider executes whatever the item is. No
|
|
234
|
+
kind travels, nothing is typed, and a control with an empty directory has nothing to offer and is
|
|
235
|
+
refused (`task.destinations.offer`). A control declared as bare `true` is refused for the same
|
|
236
|
+
reason (`task.destinations.shape`): once nothing is typed, the directory is the control.
|
|
237
|
+
|
|
238
|
+
**The provider chooses the supported destinations.** A published item may route to an IVR,
|
|
239
|
+
queue, named agent or another configured destination. No destination kind is inferred from its
|
|
240
|
+
label. The agent application offers only the directory attached to this task's `conference` capability,
|
|
241
|
+
and sends the exact destination ID; it does not invent an agent picker or arbitrary dial target.
|
|
242
|
+
The provider rechecks eligibility and executes the routing. Returning to an IVR or queue does not
|
|
243
|
+
end the caller journey; a later assignment to the same agent is a new interaction.
|
|
244
|
+
|
|
245
|
+
## Ending a call, and removing one person from it
|
|
246
|
+
|
|
247
|
+
A call has everyone on `Task.onCall`, and three commands take people off it, all performed by the
|
|
248
|
+
provider. Two of them end the agent's part; they differ in what happens to the caller.
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
{ type: "end-call" } // the agent's channel and the channels the agent added end; the caller continues
|
|
252
|
+
{ type: "terminate-call" } // every channel on the call ends
|
|
253
|
+
{ type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
|
|
254
|
+
{ type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**`end-call` ends my part.** The agent's channel and every channel the agent added -- a
|
|
258
|
+
conferenced colleague, a lead who joined -- end, and the caller continues on the path the
|
|
259
|
+
provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
|
|
260
|
+
|
|
261
|
+
**`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
|
|
262
|
+
the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
|
|
263
|
+
line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
|
|
264
|
+
different thing from putting my own side down.
|
|
265
|
+
|
|
266
|
+
Both are commands to the provider through `execute`, and nothing happens on the desk until the
|
|
267
|
+
provider reports it: the agent's audio ends on `task-audio-ended`, the task moves to `completing`,
|
|
268
|
+
any wrap allowance runs, and the agent completes. Neither is `task-ended`, and `complete` is
|
|
269
|
+
neither of them. Both capabilities are the queue's to grant and a level's to lock, and both may
|
|
270
|
+
change while the task is open: the provider republishes the task at the moment a permission
|
|
271
|
+
changes, and a command that arrives after its capability was withdrawn is refused, as **Task
|
|
272
|
+
capabilities** sets out. The provider knows which channels are the agent's and which were added;
|
|
273
|
+
the agent application sends the command and no channel list, and the provider rechecks ownership
|
|
274
|
+
when it acts.
|
|
275
|
+
|
|
276
|
+
`conference` `remove` takes one person off, named as the room names them, and the call goes on
|
|
277
|
+
for the rest; it is gated by `conference`, since it only means something with a third person on
|
|
278
|
+
the line. Removing the party leaves the agent with the colleague, a warm hand-over in reverse. A
|
|
279
|
+
remove that would leave the agent alone is not a remove but an `end-call`, and a provider answers
|
|
280
|
+
it `failed`.
|
|
281
|
+
|
|
282
|
+
**There is no transfer.** The agent hands the call to nobody, cold or warm: help arrives by
|
|
283
|
+
**Lead assist** in `guide/lead.md`, where a lead joins the call or takes it over, and a call that changes hands does
|
|
284
|
+
so on the platform's own routing -- an IVR return, a queue, a lead's take-over -- with the record
|
|
285
|
+
saying so in a `transferred` step and the `transfers` total. Nothing the agent commands moves the
|
|
286
|
+
caller to somebody else.
|
|
287
|
+
|
|
288
|
+
## Idle actions
|
|
289
|
+
|
|
290
|
+
### `dial(request)`
|
|
291
|
+
|
|
292
|
+
Starts one outbound call from the idle dialpad. It is present only when the voice provider
|
|
293
|
+
declares `dial`.
|
|
294
|
+
|
|
295
|
+
- `dialId` is the agent application's identity for this dial, minted before the request leaves.
|
|
296
|
+
- `destination` is the original number selected or entered by the agent.
|
|
297
|
+
- The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
|
|
298
|
+
number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
|
|
299
|
+
- `dialling` says the request was accepted and the call is being placed, and restates the
|
|
300
|
+
`dialId`. It says nothing about whether anyone will answer.
|
|
301
|
+
- `failed` contains a `ProtocolFailure` and confirms no call was placed.
|
|
302
|
+
|
|
303
|
+
The resulting call is offered through the normal `task-offered` event, and how the dial ended
|
|
304
|
+
arrives on `dial-outcome` under the same `dialId`. A `dialling` result does not manufacture a task
|
|
305
|
+
inside Omni.
|
|
306
|
+
|
|
307
|
+
## Every dial has an outcome
|
|
308
|
+
|
|
309
|
+
Four commands place a call: `dial()` from the idle dialpad, the `dial` command from preview, a
|
|
310
|
+
`conference` `add`, and `connect-back`. Each is accepted or refused at once, and each then ends later
|
|
311
|
+
and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
|
|
312
|
+
late in a call routinely outlives the call. Nothing in between is reported: the wire says
|
|
313
|
+
`dialling`, then says how it ended, once.
|
|
314
|
+
|
|
315
|
+
**The agent application mints the identity.** Every command that dials carries a `dialId` the agent application made, unique
|
|
316
|
+
across the installation, and it travels four ways so no leg is a lookup:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
declare const connection: Connection<"voice">;
|
|
320
|
+
declare const assignmentId: AssignmentId;
|
|
321
|
+
|
|
322
|
+
// 1. Out, minted by the agent application.
|
|
323
|
+
const result = await connection.execute({ assignmentId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
|
|
324
|
+
|
|
325
|
+
// 2. Back on the result, restated, so the agent application compares and confirms.
|
|
326
|
+
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
327
|
+
|
|
328
|
+
// 3. On the outcome, however late, against the task it belonged to.
|
|
329
|
+
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", assignmentId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
330
|
+
|
|
331
|
+
// 4. In the record, so a dial made before a take-over is placeable by whoever holds the task now.
|
|
332
|
+
const step: TaskHistoryStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The identity is a correlation handle between agent application and provider and **is never shown to the
|
|
336
|
+
agent**. An agent application holding several tasks places an outcome by the `dialId` it minted; one it did not
|
|
337
|
+
mint -- a dial made by the previous agent before a take-over -- it finds on the task's `onCall` or in
|
|
338
|
+
its `history`, which is why the steps that dial carry it. The `dialId` is not a retry key:
|
|
339
|
+
Omni never repeats a dial, and a command still carries no key for that purpose.
|
|
340
|
+
|
|
341
|
+
**`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
|
|
342
|
+
`{ status: "dialling", dialId }`, restating the agent application's identity (`result.dialId`,
|
|
343
|
+
`result.dialId.mismatch`); `applied` from such a command, or `dialling` from one that dialled
|
|
344
|
+
nothing, is refused (`result.status`). `validateResult(result, method, path, dialId)` takes the
|
|
345
|
+
`dialId` the agent application sent for that reason.
|
|
346
|
+
|
|
347
|
+
**The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
|
|
348
|
+
there was one, the directory item it went to where there was one, and how the dial ended -- from a closed
|
|
349
|
+
set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `unexplained`.
|
|
350
|
+
`answered` is stated rather than read off somebody appearing on the call, because an agent application that infers
|
|
351
|
+
success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
|
|
352
|
+
before anyone answered -- the agent hanging up while a conferenced colleague still rings -- which is not a
|
|
353
|
+
`no-answer` the destination never gave. `unexplained` is the switch dropping the call with no cause among
|
|
354
|
+
those named here: a statement about the switch, sent instead of the nearest cause it did not give.
|
|
355
|
+
`reason` is the switch's own words, optional, and shown to the agent attributed to the switch rather
|
|
356
|
+
than to Omni. The two are different things and may travel together: a switch can say "could not
|
|
357
|
+
create dialog" in words and still name no cause a code would carry, so `unexplained` with a
|
|
358
|
+
`reason` is a dial the switch described but did not classify, and a desk renders the words without
|
|
359
|
+
inventing the class.
|
|
360
|
+
|
|
361
|
+
**`assignmentId` is the task the dial was placed on**, resolved from the dial's own identity and never from
|
|
362
|
+
whatever task the agent is looking at when the outcome arrives. That task may already have ended,
|
|
363
|
+
and the harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
|
|
364
|
+
`stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached, against the
|
|
365
|
+
call it belonged to and never against the next one.
|
|
366
|
+
|
|
367
|
+
The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which an agent application calls when it
|
|
368
|
+
places one; an entry on a task's `onCall`; and a step in its `history`. Since a dialled entry
|
|
369
|
+
is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- an
|
|
370
|
+
idle dialpad call, which has no task yet -- or whose entry has already left the room; an
|
|
371
|
+
outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
|
|
372
|
+
|
|
373
|
+
**`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
|
|
374
|
+
claims, and both are true at once when a leg answers and never joins. A desk shows `answered` as the
|
|
375
|
+
dial's conclusion and says "on the call" only when the person appears on `Task.onCall`; the obvious
|
|
376
|
+
reading of `answered` -- that they are on the line -- is the one it does not make.
|
|
377
|
+
|
|
378
|
+
**A provider says which outcomes it can tell apart.** No switch distinguishes all five, and a
|
|
379
|
+
provider that cannot tell busy from unreachable must not pick one. The manifest declares
|
|
380
|
+
`dialOutcomes`, and a `dial-outcome` carries only a declared member
|
|
381
|
+
(`event.dialOutcome.undeclared`). The list includes `answered`, or the provider could never state a
|
|
382
|
+
success (`manifest.dialOutcomes.answered`), and at least one other member, or it could never state
|
|
383
|
+
a failure (`manifest.dialOutcomes.failure`). `unexplained` is declarable only beside a cause the
|
|
384
|
+
provider does report -- `busy`, `no-answer`, `unreachable`, `rejected` or `cancelled`
|
|
385
|
+
(`manifest.dialOutcomes.unexplained.alone`): a provider that reports causes may honestly say the
|
|
386
|
+
switch gave none, while one that reports none would be sending the generic failure this set exists
|
|
387
|
+
to refuse. A provider that dials at all declares it: an idle
|
|
388
|
+
dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `connectBack`
|
|
389
|
+
or `conference` under a manifest without it is refused
|
|
390
|
+
(`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
|
|
391
|
+
error there.
|
|
392
|
+
|
|
393
|
+
**Who is on the call is stated on the task.** `Task.onCall` lists everyone the task's audio joins
|
|
394
|
+
right now, replaced whole with the task like every other field, and present when the provider
|
|
395
|
+
knows the room:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
const onCall: OnCall[] = [
|
|
399
|
+
{ role: "party", since: "2026-08-21T09:12:00Z" },
|
|
400
|
+
{ role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
|
|
401
|
+
{ role: "conferenced", destinationId: "tier2", dialId: "dial-7f2", stage: "joined", held: true, since: "2026-08-21T09:15:30Z" },
|
|
402
|
+
{ role: "conferenced", destinationId: "main-menu", dialId: "dial-3c9", stage: "ringing", since: "2026-08-21T09:16:02Z" },
|
|
403
|
+
];
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
|
|
407
|
+
neither carries a destination. A `conferenced` colleague was, so the entry carries the
|
|
408
|
+
`destinationId` of the directory item and, where an agent application's dial brought them, its `dialId`; a person
|
|
409
|
+
the platform added itself carries none. `held: true` is presence as claim. A snapshot establishes state, so an agent application that attaches
|
|
410
|
+
after a take-over reads the room from here rather than inferring it from a sequence of outcomes it
|
|
411
|
+
never saw.
|
|
412
|
+
|
|
413
|
+
**`onCall` describes this agent's current interaction.** When the caller disconnects but the agent
|
|
414
|
+
and added channels remain connected, the remaining room is still valid. When this interaction's
|
|
415
|
+
audio ends or the task enters `completing`, clear its `onCall` view; absence is used only by a
|
|
416
|
+
provider that never publishes the room. This does not assert that the caller, bridge or other
|
|
417
|
+
agents' channels ended. A published room receives a final empty view for this interaction, and the
|
|
418
|
+
task may remain open for wrap. Completion is a separate task action, not the caller's disconnect.
|
|
419
|
+
|
|
420
|
+
**A field that describes the present is cleared by the transition that ends it.** `onCall`,
|
|
421
|
+
`previewEndsInSeconds` and `atDeadline` are three instances of one shape, and there will be more: each
|
|
422
|
+
describes a state that is true now, and each is carried past the moment it stops being true by the
|
|
423
|
+
most natural implementation there is, building the next task by spreading the last one. The
|
|
424
|
+
provider's own state looks right, the claim on the wire is stale, and only the agent application sees it. An
|
|
425
|
+
adapter that builds each transition from what is true after it, rather than from what was true
|
|
426
|
+
before, needs none of these rules named. Two agents were
|
|
427
|
+
once shown on calls for the better part of an hour while callers queued, because the last message
|
|
428
|
+
describing the room live was never followed by one saying it had emptied, and every consumer that
|
|
429
|
+
derived anything from it was wrong in a way that looked like its own bug.
|
|
430
|
+
|
|
431
|
+
**A dialled entry is listed from the moment the dial is placed, not from the answer.** The entry is
|
|
432
|
+
what `conference` `remove` names, and a destination the agent cannot call off while it rings is a
|
|
433
|
+
customer kept waiting with no way back -- which is the very case `cancelled` exists for. The
|
|
434
|
+
`conferenced` entry appears with its `dialId` when the dial is placed, and a `conference` `remove`
|
|
435
|
+
naming a still-ringing `destinationId` calls that dial off, with `cancelled` as its outcome.
|
|
436
|
+
|
|
437
|
+
**The entry says where it stands.** A dialled entry carries `stage`: `ringing` until its dial is
|
|
438
|
+
answered, `joined` after. The `dial-outcome` is the transition and the stage is the state, the same
|
|
439
|
+
pairing as `task-audio-started` and `audio`: an `answered` outcome and the task restated with the
|
|
440
|
+
entry `joined` say one thing twice, and any other outcome removes the entry. The outcome comes
|
|
441
|
+
first: a `task-updated` that itself moves an entry from `ringing` to `joined` before an `answered`
|
|
442
|
+
outcome for its dial is refused (`stream.taskUpdated.stage`), as an update that moves `audio` is.
|
|
443
|
+
The rule keys on the entry's `dialId`, so an entry the platform added itself, with none, is one
|
|
444
|
+
whose move the stream cannot hold to an outcome: a clean stream proves nothing about ordering there. It is stated rather
|
|
445
|
+
than left to whoever placed the dial because that knowledge lives in one process: after a reload,
|
|
446
|
+
on a second session, or on a supervisor's screen the snapshot is the only source, and a room that
|
|
447
|
+
cannot tell ringing from joined says it contains someone who may never arrive. Nobody ringing is
|
|
448
|
+
held (`task.onCall.held.ringing`). A colleague who never answers is a dial that ended: the
|
|
449
|
+
provider restates the room without the entry, and the agent is back with the customer.
|
|
450
|
+
|
|
451
|
+
The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
|
|
452
|
+
they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
|
|
453
|
+
belonged to -- with no retry, since a wrong number is not worth redialling and the decision is the
|
|
454
|
+
agent's.
|
|
455
|
+
|
|
456
|
+
## Independent task recording
|
|
457
|
+
|
|
458
|
+
Recording is voice-only. A task can arrive already recording, including while pending, and offer
|
|
459
|
+
no recording controls. The provider publishes only its own current state on
|
|
460
|
+
the provider field of the task’s `recording` state. The agent application publishes its own full scoped view in `HostReport.recordings`.
|
|
461
|
+
A provider task update cannot replace agent application state. Neither a capability nor an accepted command
|
|
462
|
+
establishes recording. Missing state, lost recorder observation, transport loss and expired
|
|
463
|
+
observation mean unknown; they never mean stopped. Paused means a recording remains open without
|
|
464
|
+
capturing. Task hold, task pause and recording pause are independent.
|
|
465
|
+
|
|
466
|
+
The per-task policy is `Task.capabilities.recording`, with independently optional provider and agent application
|
|
467
|
+
action sets. The outer capability may be locked. Absence grants no permission, and a state may
|
|
468
|
+
exist without any permission. A provider may offer only stop for a recording started automatically,
|
|
469
|
+
or withdraw a control on a later task update. There is no global recording mode and no automatic
|
|
470
|
+
start from a capability declaration. Agent application support is declared on `ConnectContext.host.recording`,
|
|
471
|
+
not the provider-owned manifest. The task names one of the storage places the agent application provisioned, by `storageId`;
|
|
472
|
+
an unknown storage place or an unsupported action is refused visibly, never redirected to provider
|
|
473
|
+
recording or a default upload location. Initial agent application support requires voice softphone audio and
|
|
474
|
+
capture of both local and remote audio; an agent application unable to capture either must refuse start/resume.
|
|
475
|
+
This package declares that contract; it supplies no recorder, audio mixing, storage or upload.
|
|
476
|
+
|
|
477
|
+
| Action | Required current recording state | Confirmed outcome |
|
|
478
|
+
| --- | --- | --- |
|
|
479
|
+
| start | inactive | active with a new recording identity |
|
|
480
|
+
| pause | active | paused, preserving recording identity and captured audio |
|
|
481
|
+
| resume | paused | active with the same recording identity |
|
|
482
|
+
| stop | active or paused | inactive; finish and retain captured audio |
|
|
483
|
+
| cancel | active or paused | inactive; abandon and discard captured audio |
|
|
484
|
+
|
|
485
|
+
Stop finishes the recording and retains captured audio. Cancel abandons the recording and discards
|
|
486
|
+
its captured audio; the task policy offers it with `cancel: true`. There is no configurable cancel
|
|
487
|
+
effect. A recorder that cannot confirm completion must not offer Cancel; it can offer Stop instead.
|
|
488
|
+
Cancel never means cancelling an in-flight start request or deleting arbitrary past recordings.
|
|
489
|
+
Stop can be applied only after finalization/retention succeeds; Cancel only after both cessation
|
|
490
|
+
and completion of this recording's audio succeed under the recorder's storage contract.
|
|
491
|
+
Partial success -- capture stopped but the storage outcome unknown, a start the recorder cannot
|
|
492
|
+
vouch for either way -- is a settled `failed`, under the code `omni.recording-unsettled`: the
|
|
493
|
+
command did not do what it promised, and the state the provider publishes with it is whatever
|
|
494
|
+
capture state is actually known. It is never `applied`, and never an unsettled promise the agent
|
|
495
|
+
application would be left to resync, since the provider knows exactly what it could not settle.
|
|
496
|
+
There is no request identity on a recording command: the outcome is the state, and what the
|
|
497
|
+
agent sees is the state. Retention is not a promise of sample-perfect audio.
|
|
498
|
+
|
|
499
|
+
Provider commands go exclusively to `Connection.execute`; agent application commands go exclusively to
|
|
500
|
+
`HostRecording.execute`. Both include the assignment and observation identities; all
|
|
501
|
+
non-start commands identify the particular recording. IDs are opaque and scoped by provider login,
|
|
502
|
+
task and recorder owner. They are never inferred from filenames or current agent identity. A
|
|
503
|
+
recording ID survives pause/resume and reassignment only where the same recorder confirms continuity;
|
|
504
|
+
commands always name the current assignment. A later start gets a different ID. Reconnect does not
|
|
505
|
+
create a new recording or fresh evidence. After lost continuity, use unknown until reconciled.
|
|
506
|
+
There is no automatic restart, hand-over to another recorder, or stop on task hold/disconnect.
|
|
507
|
+
Task removal does not prove recording stopped: outstanding agent application recorders remain tracked by the
|
|
508
|
+
agent application until its executor reconciles/finishes them, with visible unresolved cleanup failures.
|
|
509
|
+
|
|
510
|
+
Only start/resume require an in-progress or paused task with started audio. Pause/stop/cancel may
|
|
511
|
+
also finish an independently observed recorder while the task is completing. A pending task may
|
|
512
|
+
show active recording, but agent controls wait until interaction begins. The two recording paths may
|
|
513
|
+
both be active. A command to either path has no implied effect on the other.
|
|
514
|
+
|
|
515
|
+
Each confirmed observation has a fresh opaque observation identity, a canonical UTC millisecond
|
|
516
|
+
observation instant and an exclusive expiry. These times describe current evidence, never historical
|
|
517
|
+
capture boundaries. A trusted observer-domain current time must satisfy observedAt <= now < validUntil.
|
|
518
|
+
Use `effectiveRecordingState` with that explicitly trusted time; an unavailable clock yields unknown.
|
|
519
|
+
Receipt, replay and task publication never extend freshness. Clock discontinuity invalidates evidence;
|
|
520
|
+
consumers using the explicit recording-expiry clock-estimation exception must invalidate that
|
|
521
|
+
estimate and use monotonic aging so clock rollback cannot
|
|
522
|
+
revive expired evidence. The executor compares observation identity and recording identity against
|
|
523
|
+
its latest state atomically before I/O. An intervening observation or assignment change refuses the
|
|
524
|
+
stale command; it never acts on a replacement recorder. Providers lacking trustworthy current-state
|
|
525
|
+
identity or observation time publish unknown and offer no issuable controls.
|
|
526
|
+
|
|
527
|
+
Use `validateTask` and `validateTaskCommand` for published shape and static permissions.
|
|
528
|
+
Immediately before dispatch, additionally use `validateRecordingRequest` against the full current
|
|
529
|
+
task and explicit observer-domain time. For agent application commands also supply the current agent application declaration,
|
|
530
|
+
full agent application report and softphone context. The same checks must run at the executing boundary;
|
|
531
|
+
client validation alone is not authorization. The executor serializes operations on each recorder
|
|
532
|
+
and compares `observationId` and `recordingId` against its latest evidence before acting: a
|
|
533
|
+
command against superseded evidence is refused, and nothing is retried on the executor's own
|
|
534
|
+
account.
|
|
535
|
+
|
|
536
|
+
Keep command progress in agent application UI separately from authoritative state. Applied means the requested
|
|
537
|
+
recording transition and any retain/discard effect actually completed; publish the confirming task or agent application report before
|
|
538
|
+
resolving applied. Failed means confirmed no effect. Rejected promise means outcome unknown:
|
|
539
|
+
show the failure, reconcile that path and do not automatically retry or pretend inactive. Authoritative
|
|
540
|
+
updates remain full current task/agent application views, not replayed provider events. These current-state fields
|
|
541
|
+
create no interaction-history entries and infer no actors, durations or historical capture boundaries.
|
|
542
|
+
|
|
543
|
+
Migration is explicit: replace the old true recording capability with per-path action policies,
|
|
544
|
+
replace untargeted commands with scoped commands, and retain protocol version 1. During pre-release
|
|
545
|
+
development, hosts and adapters must align their recording contracts; negotiation does not detect
|
|
546
|
+
differences between package revisions that share that protocol number. No legacy-shape fallback is provided.
|
|
547
|
+
This change does not publish a package or enable recording in any existing agent application/provider by itself.
|
|
548
|
+
|
|
549
|
+
`validateRecordingOutcome` checks confirming observations against recording-specific applied/failed
|
|
550
|
+
semantics, including pause/resume identity and rejection of a dialling result. It cannot prove audio
|
|
551
|
+
retention/completion or source truth from a status flag; those remain executor obligations.
|
|
552
|
+
|
|
553
|
+
The storage place named on the task is bound when start actually creates a recording. Existing recordings
|
|
554
|
+
keep that binding through pause/resume/stop/cancel; a later policy cannot redirect their stored audio.
|
|
555
|
+
The executor rejects a mismatched storage place rather than moving or discarding another binding.
|
|
556
|
+
Action permission does not authorize unattended invocation: agent application controls require the agent's explicit
|
|
557
|
+
act, and provider authorization remains enforced at its authenticated command boundary.
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
Recording dispatch must receive the same known task-validation context as the published task.
|
|
561
|
+
Pass organisation levels and other known restrictions through the optional fourth argument of
|
|
562
|
+
`validateTaskCommand`, and through `taskContext` in `validateRecordingRequest`. This keeps a valid
|
|
563
|
+
custom organisation lock from being rejected against the default ladder, and prevents known
|
|
564
|
+
capability restrictions from disappearing at dispatch. The standalone `validateRecordingCommandState`
|
|
565
|
+
requires an affirmative permission; false, malformed permission declarations and unknown actions grant nothing.
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
### Agent application recording announcements to the caller
|
|
569
|
+
|
|
570
|
+
`HostRecording.announcesToCaller`, when true, is a guarantee that the agent application delivers audible recording
|
|
571
|
+
status messages to the remote party over the call's outgoing audio. It belongs inside the optional
|
|
572
|
+
agent application recording capability: without that capability there is no such guarantee. Omission makes no
|
|
573
|
+
promise; false is invalid. It does not belong in the provider manifest, the task's recording policy
|
|
574
|
+
or the general `HostGuarantees` object. A provider can inspect this agent application guarantee when deciding
|
|
575
|
+
whether to permit agent application recording on a task.
|
|
576
|
+
|
|
577
|
+
The guarantee applies only to agent application-owned recordings. Providers implement announcements for their
|
|
578
|
+
own recordings at their end; provider state updates must not cause the agent application to announce those
|
|
579
|
+
recordings. Both recorders can operate independently on the same call.
|
|
580
|
+
|
|
581
|
+
On a confirmed agent application start, the remote party hears that recording started. On a confirmed stop or
|
|
582
|
+
cancel, they hear that recording stopped. If pause/resume is supported, say paused/resumed so a
|
|
583
|
+
pause is not presented as a finished recording. Announcements follow actual capture transitions,
|
|
584
|
+
not button clicks, accepted requests or task-policy changes. When attaching to an already-active
|
|
585
|
+
agent application recording, announce that recording is active, without inventing its original start time.
|
|
586
|
+
Repeated observations of the same state do not repeat announcements. Unknown or expired evidence
|
|
587
|
+
must never produce a stopped announcement.
|
|
588
|
+
|
|
589
|
+
A local sound, UI indicator, screen-reader message to the agent, or sound leaking through the
|
|
590
|
+
microphone does not fulfil this guarantee. The agent application must deliver the message directly in the audio
|
|
591
|
+
sent to the remote party, even when the agent microphone is muted. It may also notify the agent.
|
|
592
|
+
Only declare the guarantee when the agent application can provide that outgoing audio path. An announcement
|
|
593
|
+
that cannot be delivered, including after the remote party has disconnected, is a visible failure;
|
|
594
|
+
never claim delivery or replay the notice into a different call.
|
|
595
|
+
|
|
596
|
+
For a command under this guarantee, applied requires both the recording action and its announcement
|
|
597
|
+
to succeed. If capture changes but announcement delivery fails, publish the actual recording state,
|
|
598
|
+
report the announcement failure visibly and answer `failed` under `omni.recording-unsettled`. Do not claim no
|
|
599
|
+
effect, automatically repeat the recording action or silently switch to an agent-only notice.
|
|
600
|
+
|
|
601
|
+
`validateHostRecording` checks the declaration, including its true-or-absent guarantee. Actual audio
|
|
602
|
+
delivery remains the agent application implementation's responsibility and must be exercised in its own tests.
|