@xema/omni-protocol 0.1.31 → 0.1.33

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/guide.md CHANGED
@@ -35,6 +35,8 @@ are used precisely throughout and mean nothing looser here.
35
35
  | **Event** | One completed transaction reported after a snapshot established the baseline. |
36
36
  | **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
37
37
  | **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
38
+ | **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a transfer, a conference add, or a callback. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
39
+ | **On the call** | Who a voice task's audio joins right now, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in. |
38
40
 
39
41
  Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
40
42
  one thing, so a bare "status" in conversation is always the authentication session's, and a
@@ -94,6 +96,7 @@ the semantic name in every contract field rather than repeating the primitive ty
94
96
  type IsoTimestamp = string;
95
97
  type UserId = string;
96
98
  type TaskId = string;
99
+ type DialId = string;
97
100
  type DurationSeconds = number;
98
101
  ```
99
102
 
@@ -162,6 +165,8 @@ type DialDestinations = "contacts-only" | "any-number";
162
165
 
163
166
  type DialCapability = { destinations: DialDestinations };
164
167
 
168
+ type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected";
169
+
165
170
  type IdleCapabilities<C extends Channel = Channel> = {
166
171
  personalBrowser?: PersonalBrowserCapability;
167
172
  calendar?: true;
@@ -178,6 +183,8 @@ type Manifest<C extends Channel = Channel> = {
178
183
  phaseLabels?: TaskPhaseLabels;
179
184
  taskTypePresentation?: Record<string, TaskTypePresentation>;
180
185
  orgLevels?: LevelDeclaration[];
186
+ dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
187
+ runningStepReports?: true;
181
188
  };
182
189
  ```
183
190
 
@@ -360,6 +367,15 @@ type SetPreferenceRequest =
360
367
  | { id: PreferenceId; enabled: boolean }
361
368
  | { id: PreferenceId; inherit: true };
362
369
 
370
+ type HandlingReport = { taskId: TaskId; step: HandlingStep; at: IsoTimestamp } & (
371
+ | { ended: true; seconds: DurationSeconds }
372
+ | { ended?: never; seconds?: DurationSeconds }
373
+ );
374
+
375
+ type HandlingReportResult =
376
+ | { status: "recorded" }
377
+ | { status: "failed"; failure: ProtocolFailure };
378
+
363
379
  type PreferenceResult =
364
380
  | { status: "applied" }
365
381
  | { status: "failed"; failure: ProtocolFailure };
@@ -404,11 +420,12 @@ type ScheduledActivity = {
404
420
  };
405
421
 
406
422
  type DialRequest = {
423
+ dialId: DialId;
407
424
  destination: string;
408
425
  };
409
426
 
410
427
  type DialResult =
411
- | { status: "dialled" }
428
+ | { status: "dialling"; dialId: DialId }
412
429
  | { status: "failed"; failure: ProtocolFailure };
413
430
  ```
414
431
 
@@ -549,9 +566,19 @@ type HandlingStep =
549
566
  | "conferenced"
550
567
  | "unanswered";
551
568
 
569
+ type TaskHandlingHistory = {
570
+ steps: TaskHandlingStep[];
571
+ handleSeconds?: DurationSeconds;
572
+ holdSeconds?: DurationSeconds;
573
+ queueSeconds?: DurationSeconds;
574
+ transfers?: number;
575
+ };
576
+
552
577
  type TaskHandlingStep = {
553
578
  step: HandlingStep;
554
579
  at: IsoTimestamp;
580
+ dialId?: DialId;
581
+ destination?: string;
555
582
  seconds?: DurationSeconds;
556
583
  by?: UserId;
557
584
  };
@@ -560,11 +587,13 @@ type TaskCompletion =
560
587
  | { completionMode: "agent-command"; wrapAllowance?: DurationSeconds }
561
588
  | { completionMode: "provider-automatic"; wrapAllowance: DurationSeconds };
562
589
 
563
- type TaskConsultation = {
564
- destination: string;
565
- label?: string;
566
- since?: IsoTimestamp;
567
- };
590
+ type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
591
+
592
+ type OnCall = { since: IsoTimestamp; held?: true } & (
593
+ | { role: "party" }
594
+ | { role: "agent"; userId: UserId }
595
+ | { role: "consulted" | "conferenced"; destination: string; dialId?: DialId; label?: string }
596
+ );
568
597
 
569
598
  type TaskLead = {
570
599
  stage: "requested" | "joined";
@@ -607,11 +636,11 @@ type Task<C extends Channel = Channel> = {
607
636
  acceptance?: AcceptanceMode;
608
637
  reference?: string;
609
638
  attributes?: TaskAttribute[];
610
- handlingHistory?: TaskHandlingStep[];
639
+ handlingHistory?: TaskHandlingHistory;
611
640
  } & TaskCompletion & (
612
641
  C extends "voice"
613
- ? { consultation?: TaskConsultation; lead?: TaskLead; assisting?: TaskAssisting; media?: TaskMediaState }
614
- : { consultation?: never; lead?: never; assisting?: never; media?: never }
642
+ ? { onCall?: OnCall[]; lead?: TaskLead; assisting?: TaskAssisting; media?: TaskMediaState }
643
+ : { onCall?: never; lead?: never; assisting?: never; media?: never }
615
644
  );
616
645
 
617
646
  type AcceptanceMode =
@@ -664,16 +693,17 @@ type VoiceTaskCommand =
664
693
  | { type: "hold" }
665
694
  | { type: "resume" }
666
695
  | { type: "disconnect" }
667
- | { type: "callback" }
668
- | { type: "transfer"; destination: string; action?: never }
669
- | { type: "transfer"; action: "consult"; destination: string }
696
+ | { type: "callback"; dialId: DialId }
697
+ | { type: "transfer"; dialId: DialId; destination: string; action?: never }
698
+ | { type: "transfer"; action: "consult"; dialId: DialId; destination: string }
670
699
  | { type: "transfer"; action: "complete" }
671
700
  | { type: "transfer"; action: "cancel" }
672
701
  | { type: "lead"; action: "request"; note?: string }
673
702
  | { type: "lead"; action: "cancel" }
674
703
  | { type: "lead"; action: "take-over" }
675
704
  | { type: "lead"; action: "leave" }
676
- | { type: "conference"; participant: string; action: "add" | "remove" }
705
+ | { type: "conference"; action: "add"; dialId: DialId; destination: string }
706
+ | { type: "conference"; action: "remove"; destination: string }
677
707
  | { type: "recording"; action: "start" | "pause" | "resume" | "stop" }
678
708
  | ({ type: "complete" } & DispositionPayload);
679
709
 
@@ -702,6 +732,7 @@ type TaskCommandRequest<C extends Channel = Channel> = {
702
732
 
703
733
  type TaskCommandResult =
704
734
  | { status: "applied" }
735
+ | { status: "dialling"; dialId: DialId }
705
736
  | { status: "failed"; failure: ProtocolFailure };
706
737
  ```
707
738
 
@@ -949,6 +980,7 @@ type ProviderEvent =
949
980
  | { type: "task-media-started"; taskId: TaskId }
950
981
  | { type: "task-media-ended"; taskId: TaskId }
951
982
  | { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
983
+ | { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId; destination?: string; reason?: string }
952
984
  | { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
953
985
  | { type: "queue-summary"; summary: QueueSummary }
954
986
  | { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
@@ -990,6 +1022,7 @@ type Connection<C extends Channel = Channel> = {
990
1022
  executeTeamConsult?(request: TeamConsultCommandRequest): Promise<TeamCommandResult>;
991
1023
  openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
992
1024
  setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
1025
+ recordStep?(report: HandlingReport): Promise<HandlingReportResult>;
993
1026
  executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
994
1027
  };
995
1028
 
@@ -1277,11 +1310,12 @@ already committed stops an agent who is already stopped. A command whose second
1277
1310
  cost — a dial places a second call — is not made safe by anyone, which is why it is never repeated
1278
1311
  without a person deciding to.
1279
1312
 
1280
- An agent dials. The provider places the call and the answer is lost. Omni shows the dial as
1281
- unknown, not failed. Within a moment the provider offers the resulting call through `task-offered`
1282
- and the agent is on it; had nothing been placed, nothing arrives and the agent dials again. What
1283
- Omni must not do is dial again on the agent's behalf — the one outcome worse than a lost answer is
1284
- two phones ringing at the customer.
1313
+ An agent dials. The provider places the call and the `dialling` answer is lost. Omni shows the dial
1314
+ as unknown, not failed. Within a moment the provider offers the resulting call through
1315
+ `task-offered` and reports the dial's `dial-outcome` under the `dialId` Omni sent, and the agent is
1316
+ on it; had nothing been placed, nothing arrives and the agent dials again. What Omni must not do is
1317
+ dial again on the agent's behalf — the one outcome worse than a lost answer is two phones ringing
1318
+ at the customer.
1285
1319
 
1286
1320
  An agent presses Hold while the provider is reconnecting. Nothing is queued at either end: the
1287
1321
  adapter answers `failed` with `omni.unavailable`, Omni shows the refusal, and the agent presses
@@ -1358,6 +1392,7 @@ export default defineAdapter({
1358
1392
  idleCapabilities: {
1359
1393
  dial: { destinations: "any-number" },
1360
1394
  },
1395
+ dialOutcomes: ["answered", "no-answer"],
1361
1396
  },
1362
1397
  createAuthenticationSession: context => createAcmeAuthentication(context),
1363
1398
  connect: context => createConnection(context),
@@ -1382,6 +1417,8 @@ compile time.
1382
1417
  | `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
1383
1418
  | `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
1384
1419
  | `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**. |
1420
+ | `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**. |
1421
+ | `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**. |
1385
1422
 
1386
1423
  ### Authentication methods
1387
1424
 
@@ -1451,9 +1488,9 @@ aggregated from every provider, but a `contacts-only` dialpad offers only the en
1451
1488
  contributed. Any other reading lets a second provider put destinations into a directory the first
1452
1489
  restricted precisely so it could control what was dialled.
1453
1490
 
1454
- A declared dial capability requires `Connection.dial()`. Omni sends the original
1455
- destination and whether it came from a `contact` or `manual` entry. With `contacts-only`, Omni must
1456
- never send `source: "manual"`.
1491
+ A declared dial capability requires `Connection.dial()` and a `dialOutcomes` declaration, since a
1492
+ dialpad dials. Omni sends the destination as the agent selected or typed it, under a `dialId` of
1493
+ its own minting, and the provider holds it to the declared `destinations`.
1457
1494
 
1458
1495
  #### Personal Browser (`personalBrowser`)
1459
1496
 
@@ -1878,7 +1915,7 @@ surface in one place, and what obliges an adapter to implement each one.
1878
1915
  | `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
1879
1916
  | `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
1880
1917
  | `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. |
1881
- | `dial(request)` | The manifest declares `idleCapabilities.dial`. |
1918
+ | `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
1882
1919
  | `requestBreak(request)` | The login declares `capabilities.breaks`. |
1883
1920
  | `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
1884
1921
  | `cancelBreak()` | The login declares `capabilities.breaks`. |
@@ -1886,6 +1923,7 @@ surface in one place, and what obliges an adapter to implement each one.
1886
1923
  | `executeTeamBreak(command)` | The login declares `capabilities.team.breakControl`. |
1887
1924
  | `executeTeamConsult(command)` | The login declares `capabilities.team.consultControl`. |
1888
1925
  | `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
1926
+ | `recordStep(report)` | A task declares `mute`: the host performs that leg, and the provider's record has to have somewhere to take it. See **The host records what it performs**. |
1889
1927
  | `executeTeamPolicy(command)` | The login declares `capabilities.team.policyControl`. |
1890
1928
  | `openMedia(request)` | The manifest channel is `voice`. Every voice task's audio lands in Omni, so there is no voice adapter that does not implement it. |
1891
1929
 
@@ -2090,8 +2128,8 @@ time. Runtime conformance checks also require the task channel to match its prov
2090
2128
  | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
2091
2129
  | `wrapAllowance` | Fixed time allowed to complete the task after primary handling 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. |
2092
2130
  | `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. |
2093
- | `handlingHistory` | Optional ordered handling history for this currently open task. It is live task data, not a permanent archive. |
2094
- | `consultation` | Voice only. Present while the agent is consulting a transfer destination: who is being consulted, and since when where the provider records it. Its presence is what makes `transfer` `complete` and `cancel` issuable. `label` is a name for the destination -- a person, a queue -- not a phrase; the host supplies the verb. See **Consult transfer**. |
2131
+ | `handlingHistory` | The call record: `steps` — the ordered handling history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `handleSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **How a task has been handled**. |
2132
+ | `onCall` | Voice only. Who is on the call right now, as the provider states it, replaced whole with the task: `party` is the customer, `agent` a person by user id, `consulted` and `conferenced` somebody a dial brought in -- with the `destination` dialled, the `dialId` where a host placed it, and `held: true` on anyone 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**. |
2095
2133
  | `lead` | 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 **Consulting a lead**. |
2096
2134
  | `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` `take-over` and `leave` issuable. See **Consulting a lead**. |
2097
2135
 
@@ -2254,15 +2292,23 @@ const immediateProviderCompletion = {
2254
2292
 
2255
2293
  ### How a task has been handled
2256
2294
 
2257
- `Task.handlingHistory` is the sequence of steps that brought the task to the agent, oldest first:
2295
+ `Task.handlingHistory` is the call record: the steps that brought the task to the agent, oldest
2296
+ first, and what they add up to before this agent:
2258
2297
 
2259
2298
  ```ts
2260
- handlingHistory: [
2261
- { step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
2262
- { step: "answered", at: "2026-08-21T00:59:41Z", by: "a-17" },
2263
- { step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
2264
- { step: "held", at: "2026-08-21T01:06:48Z", by: "a-17" },
2265
- ]
2299
+ handlingHistory: {
2300
+ steps: [
2301
+ { step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
2302
+ { step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
2303
+ { step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
2304
+ { step: "transferred", at: "2026-08-21T01:04:53Z", by: "a-17" },
2305
+ { step: "answered", at: "2026-08-21T01:05:02Z", by: "a-23" },
2306
+ ],
2307
+ handleSeconds: 312, // handled by others before this agent
2308
+ holdSeconds: 35, // holds others put the caller on
2309
+ queueSeconds: 41, // waiting before anyone answered
2310
+ transfers: 1, // hands the call has changed
2311
+ }
2266
2312
  ```
2267
2313
 
2268
2314
  **This is not an archive.** It is live data about a task that is still open: it travels with the task
@@ -2286,9 +2332,16 @@ duration is the `held` entry's `seconds`, and a desk that restarts its counter o
2286
2332
  counting the wrong thing.
2287
2333
 
2288
2334
  `muted` is there because Omni performs the mute rather than the provider — see **Where a command
2289
- executes** — so without a step the one participant that keeps the task's record would have no
2335
+ executes** — so without a step the provider, which alone keeps the task's record, would have no
2290
2336
  account of a period the agent could not be heard.
2291
2337
 
2338
+ **A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
2339
+ each end a dial, so the entry carries the `destination` it went to and, where a host placed it, the
2340
+ `dialId` the host minted; no other step dialled, and either field on one is refused
2341
+ (`task.handlingHistory.dialId.unexpected`). This is how a dial made before a transfer is placeable
2342
+ by whoever holds the task now: a `dial-outcome` naming a `dialId` the host never minted is looked
2343
+ up in the record, not discarded.
2344
+
2292
2345
  Four rules a provider has to keep:
2293
2346
 
2294
2347
  - **Report `seconds`; never expect Omni to derive it.** Omni does not subtract one timestamp from
@@ -2321,8 +2374,52 @@ A host must render the two differently. Showing an unattributed `answered` the s
2321
2374
  `queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
2322
2375
  *"not recorded"* in the place the name would go.
2323
2376
 
2324
- Omit `handlingHistory` entirely when the provider cannot observe the steps. An empty array is a different
2325
- claim — it says the task has had none.
2377
+ Omit `handlingHistory` entirely when the provider cannot observe the steps. Empty `steps` is a
2378
+ different claim — it says the task has had none.
2379
+
2380
+ **What the record adds up to is stated, not summed.** A call that has changed hands arrives
2381
+ carrying what others already used of it, and the agent reads that before they say hello: the
2382
+ totals are the provider's own — never a desk summing instants that may be rounded or skewed, for
2383
+ the same reason `seconds` is reported and not derived — and each is present when the provider
2384
+ knows it and absent when it does not, never a plausible nought. A fresh call from the queue has
2385
+ `steps` with no prior `answered` and no totals to state, and a desk shows nothing rather than
2386
+ zeros. The record is restated with the task on every `task-updated` and snapshot, so an entry
2387
+ that arrives late corrects the sums. These four are what every platform reports; more will be
2388
+ added here as a need is shown, not invented ahead of one.
2389
+
2390
+ #### The host records what it performs
2391
+
2392
+ `muted` is the one leg Omni performs rather than the provider, and a record kept by the provider
2393
+ would have a hole exactly there. So the host reports it, through `recordStep`, and the provider
2394
+ writes it into its record as it writes every other leg:
2395
+
2396
+ ```ts
2397
+ declare const connection: Connection<"voice">;
2398
+ declare const taskId: TaskId;
2399
+ declare const at: IsoTimestamp;
2400
+
2401
+ void connection.recordStep?.({ taskId, step: "muted", at }); // the moment the agent mutes
2402
+ void connection.recordStep?.({ taskId, step: "muted", at, seconds: 15 }); // still running, if the host chooses to say
2403
+ void connection.recordStep?.({ taskId, step: "muted", at, seconds: 42, ended: true }); // the moment they unmute
2404
+ ```
2405
+
2406
+ The entry is keyed by `step` and `at`, so every report about one leg names the same instant. The
2407
+ host is the authority for the legs it performs, so it may say how long so far — `seconds` is
2408
+ elapsed while the leg runs and final once it has ended — and **the end is stated, never
2409
+ inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
2410
+ provider never asked for never crosses.** The running report is sent only to a provider whose
2411
+ manifest declares `runningStepReports`; every other provider receives exactly two reports per
2412
+ leg, when it began and when it ended, and `validateHandlingReport(report, path, manifest)` refuses
2413
+ a running one it was never asked for (`handlingReport.running.unexpected`). What a provider that
2414
+ did ask for them forwards upstream, and how often, is its own business. The step appears in
2415
+ `handlingHistory` when the *provider* publishes it: Omni never writes the record itself.
2416
+ `recordStep` is required of a connection whose tasks declare `mute`, and answers `recorded`.
2417
+
2418
+ **A host-performed leg still open when the task's media ends, or the task ends, is ended by the
2419
+ host at that instant** — `ended: true`, `seconds` to the end, the same `at` — since a provider
2420
+ left to close it would be guessing at a host-performed duration. Both ends are read off one clock,
2421
+ the host's, and a leg shorter than a second is reported as the second it was, never as the nought
2422
+ the record refuses.
2326
2423
 
2327
2424
  ### Browser capability
2328
2425
 
@@ -2461,7 +2558,7 @@ const taskCapabilities = {
2461
2558
  | `blindTransfer` | Secondary menu item: Blind transfer | Omni may transfer the caller directly to a destination. |
2462
2559
  | `consultTransfer` | Secondary menu item: Consult transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Consult transfer**. |
2463
2560
  | `consultLead` | Secondary menu item: Consult lead | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.lead`. See **Consulting a lead**. |
2464
- | `conference` | Secondary button: Conference | Omni may add or remove participants from the active call. |
2561
+ | `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and drop a `conferenced` entry from it. |
2465
2562
  | `recording` | Overflow menu item: Recording | Omni may expose start, pause, resume, and stop recording controls. |
2466
2563
  | `dispositions` | Primary button: Complete | Omni may request task disposal with a provider disposition and notes. |
2467
2564
 
@@ -2510,7 +2607,7 @@ capabilities: {
2510
2607
  | Field | Contract |
2511
2608
  | --- | --- |
2512
2609
  | `destinations` | Directory Omni renders, with unique `id` values. |
2513
- | `address` | The value Omni sends as `TaskCommand.transfer.destination` or `conference.participant`. |
2610
+ | `address` | The value Omni sends as `destination` on a `transfer` or a `conference` command. |
2514
2611
  | `kind` | Where the contact is going. See below. |
2515
2612
  | `allowManualEntry` | Whether the agent may type a destination that is not in the directory. A directory with no destinations must allow manual entry, or the control has nothing to offer. |
2516
2613
 
@@ -2534,9 +2631,10 @@ from `blindTransfer` (a hand-over with nobody consulted) and from `conference` (
2534
2631
  call): a queue may offer any of the three without the others, and each is declared on its own.
2535
2632
 
2536
2633
  ```ts
2537
- // 1. Consult. The provider parks the customer and calls the destination; the task reports
2538
- // `paused` and carries `consultation` while the call to the destination stands.
2539
- { type: "transfer", action: "consult", destination: "+14155550111" }
2634
+ // 1. Consult. A dial: the provider parks the customer and calls the destination, answers
2635
+ // `dialling`, and the task reports `paused` with the destination `consulted` on `onCall`
2636
+ // while the call to it stands.
2637
+ { type: "transfer", action: "consult", dialId: "dial-7f2", destination: "+14155550111" }
2540
2638
 
2541
2639
  // 2a. Hand the customer to the consulted destination and leave.
2542
2640
  { type: "transfer", action: "complete" }
@@ -2546,21 +2644,25 @@ call): a queue may offer any of the three without the others, and each is declar
2546
2644
  ```
2547
2645
 
2548
2646
  `consult` is gated by the `consultTransfer` capability and takes a destination exactly as a blind
2549
- transfer does, from the same kind of directory. While the consultation stands the task carries
2550
- `consultation`, and that presence is what makes `complete` and `cancel` issuable -- they name no
2551
- destination because there is exactly one they could mean. A consultation that could be started
2552
- but not finished would strand the customer and the destination both, which is why all three are
2553
- commands and a provider that offers `consultTransfer` implements all three.
2647
+ transfer does, from the same kind of directory. It is a dial, so it carries the host's `dialId`
2648
+ and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
2649
+ the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
2650
+ is what makes `complete` and `cancel` issuable -- they name no destination because there is exactly
2651
+ one they could mean, which is why a task carries at most one `consulted` entry
2652
+ (`task.onCall.consulted.single`). A consultation that could be started but not finished would
2653
+ strand the customer and the destination both, which is why all three are commands and a provider
2654
+ that offers `consultTransfer` implements all three.
2554
2655
 
2555
2656
  `applied` on `complete` says the provider is bridging the customer to the destination and
2556
2657
  dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
2557
2658
  the provider reports `task-media-ended`, any wrap allowance runs, and the task ends with a
2558
2659
  `transferred` outcome naming the destination. `applied` on `cancel` says the destination is
2559
- dropped; the task returns to `in-progress` with `consultation` gone. Omni waits for the
2660
+ dropped; the task returns to `in-progress` with the `consulted` entry gone. Omni waits for the
2560
2661
  provider's report of both, as it does for every command.
2561
2662
 
2562
- A destination that does not answer is a consultation that ended: the provider clears
2563
- `consultation`, returns the task to `in-progress`, and the agent is back with the customer.
2663
+ A destination that does not answer is a dial that ended: the provider reports its `dial-outcome`,
2664
+ returns the task to `in-progress` with no `consulted` entry, and the agent is back with the
2665
+ customer.
2564
2666
 
2565
2667
  ### Chat capabilities
2566
2668
 
@@ -2623,14 +2725,101 @@ Custom capabilities must not redefine the meaning of a standard channel capabili
2623
2725
  Starts one outbound call from the idle dialpad. It is present only when the voice provider
2624
2726
  declares `dial`.
2625
2727
 
2728
+ - `dialId` is the host's identity for this dial, minted before the request leaves.
2626
2729
  - `destination` is the original number selected or entered by the agent.
2627
2730
  - The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
2628
2731
  number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
2629
- - `dialled` confirms that outbound call creation completed.
2732
+ - `dialling` says the request was accepted and the call is being placed, and restates the
2733
+ `dialId`. It says nothing about whether anyone will answer.
2630
2734
  - `failed` contains a `ProtocolFailure` and confirms no call was placed.
2631
2735
 
2632
- The resulting call is offered through the normal `task-offered` event. A successful dial result
2633
- does not manufacture a task inside Omni.
2736
+ The resulting call is offered through the normal `task-offered` event, and how the dial ended
2737
+ arrives on `dial-outcome` under the same `dialId`. A `dialling` result does not manufacture a task
2738
+ inside Omni.
2739
+
2740
+ ## Every dial has an outcome
2741
+
2742
+ Four commands place a call: `dial` from the idle dialpad, a blind or consult `transfer`, a
2743
+ `conference` `add`, and `callback`. Each is accepted or refused at once, and each then ends later
2744
+ and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
2745
+ late in a call routinely outlives the call. Nothing in between is reported: the wire says
2746
+ `dialling`, then says how it ended, once.
2747
+
2748
+ **The host mints the identity.** Every command that dials carries a `dialId` the host made, unique
2749
+ across the installation, and it travels four ways so no leg is a lookup:
2750
+
2751
+ ```ts
2752
+ declare const connection: Connection<"voice">;
2753
+ declare const taskId: TaskId;
2754
+
2755
+ // 1. Out, minted by the host.
2756
+ const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2", destination: "+14155550111" } });
2757
+
2758
+ // 2. Back on the result, restated, so the host compares and confirms.
2759
+ expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
2760
+
2761
+ // 3. On the outcome, however late, against the task it belonged to.
2762
+ const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, destination: "+14155550111", outcome: "no-answer", reason: "No route to destination" };
2763
+
2764
+ // 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
2765
+ const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destination: "+14155550111" };
2766
+ ```
2767
+
2768
+ The identity is a correlation handle between host and provider and **is never shown to the
2769
+ agent**. A host holding several tasks places an outcome by the `dialId` it minted; one it did not
2770
+ mint -- a dial made by the previous agent before a transfer -- it finds on the task's `onCall` or in
2771
+ its `handlingHistory`, which is why the steps that dial carry it. The `dialId` is not a retry key:
2772
+ Omni never repeats a dial, and a command still carries no key for that purpose.
2773
+
2774
+ **`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
2775
+ `{ status: "dialling", dialId }`, restating the host's identity (`result.dialId`,
2776
+ `result.dialId.mismatch`); `applied` from such a command, or `dialling` from one that dialled
2777
+ nothing, is refused (`result.status`). `validateResult(result, method, path, dialId)` takes the
2778
+ `dialId` the host sent for that reason.
2779
+
2780
+ **The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
2781
+ there was one, the destination where the provider knows it, and how the dial ended -- from a closed
2782
+ set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`. `answered` is stated rather than
2783
+ read off somebody appearing on the call, because a host that infers success cannot tell "reached"
2784
+ from "still ringing". `reason` is the switch's own words, optional, and shown to the agent
2785
+ attributed to the switch rather than to Omni. The task named may already have ended, and the
2786
+ harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
2787
+ `stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached.
2788
+
2789
+ **A provider says which outcomes it can tell apart.** No switch distinguishes all five, and a
2790
+ provider that cannot tell busy from unreachable must not pick one. The manifest declares
2791
+ `dialOutcomes`, and a `dial-outcome` carries only a declared member
2792
+ (`event.dialOutcome.undeclared`). The list includes `answered`, or the provider could never state a
2793
+ success (`manifest.dialOutcomes.answered`), and at least one other member, or it could never state
2794
+ a failure (`manifest.dialOutcomes.failure`). A provider that dials at all declares it: an idle
2795
+ dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `callback`,
2796
+ `blindTransfer`, `consultTransfer` or `conference` under a manifest without it is refused
2797
+ (`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
2798
+ error there.
2799
+
2800
+ **Who is on the call is stated on the task.** `Task.onCall` lists everyone the task's audio joins
2801
+ right now, replaced whole with the task like every other field, and present when the provider
2802
+ knows the room:
2803
+
2804
+ ```ts
2805
+ const onCall: OnCall[] = [
2806
+ { role: "party", since: "2026-08-21T09:12:00Z" },
2807
+ { role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
2808
+ { role: "conferenced", destination: "+14155550111", dialId: "dial-7f2", held: true, since: "2026-08-21T09:15:30Z" },
2809
+ ];
2810
+ ```
2811
+
2812
+ `party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
2813
+ neither carries a destination. `consulted` and `conferenced` were, so both carry the `destination`
2814
+ and, where a host's dial brought them, its `dialId`; a person the platform added itself carries
2815
+ none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
2816
+ after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
2817
+ never saw. A `conference` `remove` names the `destination` of the `conferenced` entry to drop.
2818
+
2819
+ The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
2820
+ they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
2821
+ belonged to -- with no retry, since a wrong number is not worth redialling and the decision is the
2822
+ agent's.
2634
2823
 
2635
2824
  ## Breaks
2636
2825
 
@@ -2718,6 +2907,7 @@ everywhere; success is not.
2718
2907
  | Method | Succeeded |
2719
2908
  | --- | --- |
2720
2909
  | `setCapacity` | `applied` |
2910
+ | `recordStep` | `recorded` |
2721
2911
  | `requestBreak` | `requested` |
2722
2912
  | `commitBreak` | `committed` |
2723
2913
  | `cancelBreak` | `cancelled` |
@@ -2880,36 +3070,36 @@ Omni offers the aggregate Break control only when every provider currently holdi
2880
3070
  declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
2881
3071
  knowingly permit partial availability.
2882
3072
 
2883
- Omni coordinates one attempt as follows:
3073
+ Omni coordinates one break attempt as follows:
2884
3074
 
2885
- 1. Freeze the participant set to every connected provider from which the agent can currently
2886
- receive work. A provider joining during the attempt is given no capacity until it finishes.
2887
- A provider whose authentication is `expired` is not one the agent can receive work from and is
2888
- not a participant: nothing is asked of it, the break proceeds without it, and when the login
2889
- is restored it is reconciled from its snapshot as a set-aside provider is, with no capacity
2890
- until then. `refreshing` keeps a provider in — its identity and capabilities remain available
2891
- and work continues. `assertBreakParticipants` holds a host to this set.
3075
+ 1. Freeze the providers the attempt asks: every connected provider from which the agent can
3076
+ currently receive work. A provider joining during the attempt is given no capacity until it
3077
+ finishes. A provider whose authentication is `expired` is not one the agent can receive work
3078
+ from and is not asked: nothing is asked of it, the break proceeds without it, and when the
3079
+ login is restored it is reconciled from its snapshot as a set-aside provider is, with no
3080
+ capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
3081
+ available and work continues. `assertBreakAttemptProviders` holds a host to this set.
2892
3082
  2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
2893
- 3. Send one `requestBreak` to every participant. A provider reports `awaiting-decision` or
3083
+ 3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-decision` or
2894
3084
  `granted`; neither state stops work. A denial transitions directly to `not-requested` and
2895
3085
  causes Omni to take the cancel path.
2896
- 4. If every participant reports `granted`, durably choose commit, enter `committing-break`,
2897
- and send `commitBreak()` to every participant. A provider then stops offering new work
3086
+ 4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
3087
+ and send `commitBreak()` to each of them. A provider then stops offering new work
2898
3088
  and reports `starting-after-task` or `in-effect`. The **commit bound** — ten seconds from the
2899
- decision, tunable per deployment — decides who is kept: a participant that has not applied the
3089
+ decision, tunable per deployment — decides who is kept: a provider that has not applied the
2900
3090
  commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
2901
- begins without it. `in-effect` decides `on-break`: Omni enters it once every kept participant
2902
- reports `in-effect`. A kept participant reporting `starting-after-task` has applied the commit
3091
+ begins without it. `in-effect` decides `on-break`: Omni enters it once every kept provider
3092
+ reports `in-effect`. A kept provider reporting `starting-after-task` has applied the commit
2903
3093
  and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
2904
3094
  settled and beginning when the task ends, and offers no cancel, because the commit is durable.
2905
- 5. If any participant fails or denies the request, cannot be reconciled within the bounded
3095
+ 5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
2906
3096
  decision timeout, or the agent cancels before commit, durably choose cancel and enter
2907
- `cancelling-break`. Send `cancelBreak()` to every participant still reporting
3097
+ `cancelling-break`. Send `cancelBreak()` to every provider still reporting
2908
3098
  `awaiting-decision` or `granted`. Work continues during cancellation because no stop was
2909
- committed. Return to `working` only after no participant retains either state.
3099
+ committed. Return to `working` only after no provider retains either state.
2910
3100
 
2911
- Commit and cancel are mutually exclusive decisions for one attempt. Once Omni chooses commit it
2912
- never rolls that attempt back: it is reconciled by snapshot until every participant is stopped.
3101
+ Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
3102
+ it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
2913
3103
  A provider that reports `granted` must therefore preserve the request across reconnects and must
2914
3104
  honour a later commit or cancel. This
2915
3105
  durable promise prevents a provider from failing the commit after another provider has already
@@ -2918,7 +3108,7 @@ stopped the agent.
2918
3108
  #### Why the commit phase is bounded and the decision is not
2919
3109
 
2920
3110
  Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
2921
- durable and cannot be rolled back, so a participant that crashes, has its authentication revoked,
3111
+ durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
2922
3112
  or is uninstalled between granting and committing would hold Omni in `committing-break` with no
2923
3113
  exit: the providers that did commit have already stopped the agent, and the agent is neither
2924
3114
  working nor on a break.
@@ -2928,7 +3118,7 @@ on one platform while another keeps routing work to them — and **a provider Om
2928
3118
  routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
2929
3119
  for it costs the agent their break.
2930
3120
 
2931
- Setting a participant aside is not a rollback and not a cancel. The commit stands and the
3121
+ Setting a provider aside is not a rollback and not a cancel. The commit stands and the
2932
3122
  obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
2933
3123
  snapshot before anything else, and the snapshot decides:
2934
3124
 
@@ -2943,7 +3133,7 @@ snapshot before anything else, and the snapshot decides:
2943
3133
  The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
2944
3134
  no capacity until it is reconciled.
2945
3135
 
2946
- **If the agent has already ended the break elsewhere, the attempt is over** and the returning
3136
+ **If the agent has already ended the break elsewhere, the break attempt is over** and the returning
2947
3137
  provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
2948
3138
  would stop an agent who is working again; `starting-after-task` or `in-effect` gets `endBreak()`.
2949
3139
  Never rolling back is about a break that is still on, not one the agent has finished.
@@ -2952,7 +3142,7 @@ Omni may tell the agent which platforms the break has not yet reached, as it alr
2952
3142
  break cannot be paired across every provider.
2953
3143
 
2954
3144
  The decision phase needs no such bound, because nothing has stopped. Work continues throughout
2955
- `requesting-break`, so a participant that never answers costs the agent a wait rather than their
3145
+ `requesting-break`, so a provider that never answers costs the agent a wait rather than their
2956
3146
  availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
2957
3147
  because no stop was ever committed.
2958
3148
 
@@ -3368,7 +3558,7 @@ declared:
3368
3558
  | `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. |
3369
3559
  | `callback` | The `callback` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
3370
3560
  | `transfer` with `action: "consult"` | The `consultTransfer` capability. Blind `transfer` is gated by `blindTransfer`; the two are declared and offered separately. |
3371
- | `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- `Task.consultation` present. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
3561
+ | `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
3372
3562
  | `lead` with `action: "request"` or `"cancel"` | The `consultLead` capability. `cancel` needs a request standing -- `Task.lead` with status `requested`. |
3373
3563
  | `lead` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
3374
3564
  | Everything else | Its own named capability. |
@@ -3559,11 +3749,11 @@ from `in-effect` or `starting-after-task` to a grant or a request, or from `gran
3559
3749
  `exerciseAdapter` holds the stream to that from the connect snapshot on;
3560
3750
  `assertBreakFollowsItsRequests` holds any sequence.
3561
3751
 
3562
- For a multi-provider attempt, "every provider" is the participant set frozen when the attempt
3563
- entered `requesting-break`. Omni commits only after every participant reports `granted` —
3752
+ For a multi-provider break attempt, "every provider" is the set of providers frozen when the
3753
+ attempt entered `requesting-break`. Omni commits only after every asked provider reports `granted` —
3564
3754
  that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
3565
- `on-break` once every kept participant reports `in-effect`; the commit bound decides who is kept,
3566
- setting aside a participant that has not applied the commit rather than holding a break that has
3755
+ `on-break` once every kept provider reports `in-effect`; the commit bound decides who is kept,
3756
+ setting aside a provider that has not applied the commit rather than holding a break that has
3567
3757
  already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
3568
3758
  multi-provider break**.
3569
3759
 
@@ -3620,6 +3810,15 @@ The `task-media-ended` event and the `completing` phase are likewise non-termina
3620
3810
  snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
3621
3811
  same envelope ID is harmless.
3622
3812
 
3813
+ ### `dial-outcome`
3814
+
3815
+ How a dial the host placed ended, once, either way, named by the `dialId` the host sent: `answered`,
3816
+ or one of the declared ways of not reaching the destination, with the switch's `reason` where it
3817
+ gave one. `taskId` names the task the dial was on, where there was one, and that task may already
3818
+ have ended. Omni shows it to the agent against the call it belonged to and does nothing else: no
3819
+ audio moves, since the provider bridges the leg on its side, and no dial is repeated. See **Every
3820
+ dial has an outcome**.
3821
+
3623
3822
  ### `announcement`
3624
3823
 
3625
3824
  Publishes an agent-facing message. `text` is always required and is the accessible fallback.
@@ -3714,6 +3913,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
3714
3913
  | `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
3715
3914
  | `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
3716
3915
  | `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. |
3916
+ | `validateHandlingReport(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`. |
3717
3917
  | `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. |
3718
3918
  | `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. |
3719
3919
  | `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. |
@@ -3805,7 +4005,7 @@ cannot be established from TypeScript structure alone.
3805
4005
  | `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for a host that wants the same rules at its boundary: `seed(snapshot)`, then `apply(envelope)` returns the violations. |
3806
4006
  | `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a placed break arriving in effect with `imposed`. The harness applies the same rules after the connect snapshot. |
3807
4007
  | `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-started` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
3808
- | `assertBreakParticipants(candidates, participants)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
4008
+ | `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
3809
4009
  | `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `in-effect` only once nothing is outstanding — never beside a task, never later than the step that has none. |
3810
4010
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
3811
4011
  | `assertWrapTimeout(task, mediaEndedAt, deadline, toleranceMs?)` | The wrap deadline equals media end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |