@xema/omni-protocol 0.1.107 → 0.1.109

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/queue.md ADDED
@@ -0,0 +1,360 @@
1
+ # `@xema/omni-protocol`: The agent's own queue
2
+
3
+ The next call, lined up for this agent while they finish the one they are on: the ask, the lined-up call, and the release. 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
+ ## The agent's own queue
6
+
7
+ The phone takes one call at a time, so "I am ready for the next one" cannot mean a second
8
+ assignment now. It means: line the next queued call up for me, to start the moment my wrap ends,
9
+ unless somebody else takes it first. That is a queue of the agent's own, one deep, beside the
10
+ platform's queue and never instead of it, and the contract carries it in three parts: the ask,
11
+ the lined-up call, and the release.
12
+
13
+ ```ts
14
+ // the login declares it, as it declares breaks
15
+ { status: "authenticated", identity: {...}, capabilities: { breaks: true, nextCall: true } }
16
+
17
+ // 1. The ask. The agent presses Next call during the call or the wrap; the desk offers the button
18
+ // when it sees fit -- past the midpoint of the handling time is one way.
19
+ await connection.requestNextCall()
20
+ // -> { status: "requested" }: the provider holds the ask; nothing is promised
21
+ { type: "next-call", nextCall: { since: "2026-09-12T10:41:00Z" } }
22
+
23
+ // 2. The lined-up call. The provider fills the ask from its queue: a caller waiting for this agent,
24
+ // still in the queue, not a task, ringing nothing.
25
+ { type: "next-call" } // the ask is met
26
+ { type: "lined-up", linedUp: { party: { name: "Priya S", number: { lockedBy: "org" } }, queue: "Billing", queuedSince: "2026-09-12T10:39:20Z", since: "2026-09-12T10:41:30Z", release: true } }
27
+
28
+ // The agent's wrap ends and the call is theirs: the entry goes and the task arrives, as any assignment does.
29
+ { type: "task-ended", assignmentId: "alloc-42", outcome: { type: "completed", by: "agent" } }
30
+ { type: "lined-up" }
31
+ { type: "task-offered", task: { assignmentId: "alloc-57", phase: "pending", acceptance: "automatic", party: {...}, history: { steps: [{ step: "queued", at: "2026-09-12T10:39:20Z", seconds: 131 }] }, ... } }
32
+
33
+ // Or somebody else answered first, or the caller gave up: the entry goes and no task ever was.
34
+ { type: "lined-up" }
35
+
36
+ // 3. The release. Where the entry grants it, the agent lets the call go back to the queue for anyone.
37
+ await connection.releaseLinedUp()
38
+ // -> { status: "released" }, then { type: "lined-up" } with nothing
39
+ ```
40
+
41
+ **The ask lives only as long as its purpose.** It needs an assignment at work to be next after,
42
+ so an idle agent has nothing to press (`snapshot.nextCall.idle`), and the provider clears it on
43
+ its own when that assignment ends, when a call is lined up to meet it, and when the agent
44
+ withdraws it with `cancelNextCall()`. The desk never keeps it past any of those. `requested` says
45
+ the provider holds the ask, not that a call will come: a queue may be empty.
46
+
47
+ **The lined-up call is not a task, and rides no task event.** Until the offer there is no
48
+ assignment and nothing to accept, so it has no phase, no capabilities and no history, and it
49
+ rings nothing: the caller is still in the platform's queue, the phone has no channel for it, and
50
+ the desk shows "next for you" and nothing to act on but the release. It becomes a task by the
51
+ ordinary `task-offered`, with `acceptance: "automatic"` since the agent asked, and counts against
52
+ capacity from that moment as any assignment does; the `queued` step in its history carries the
53
+ caller's whole wait. One at a time: the phone only ever takes the next one, so `linedUp` is an
54
+ object, not a list. Nothing is guaranteed: the queue may give the call to whoever is free first,
55
+ and then the entry goes and no task ever was.
56
+
57
+ **A repeat caller is the same shape, unasked.** A provider whose routing prefers the agent a
58
+ caller spoke to last lines the call up for that agent without a press, so they see it in their own
59
+ queue and can take it before another agent does. The ask is only what the agent adds on top of
60
+ the platform's routing: releasing one call, or withdrawing the ask, ends nothing of that routing,
61
+ and the provider may line a repeat caller up again.
62
+
63
+ **Letting go is the provider's say.** `release: true` on the lined-up call grants the agent Let
64
+ go; absent, the platform means them to take the call and the desk offers nothing
65
+ (`linedUp.release`). `releaseLinedUp()` against an entry without it is refused with
66
+ `omni.capability-not-enabled`, as any control the provider did not grant. `cancelNextCall()`
67
+ withdraws the ask alone: once a call is lined up the ask is met, and what happens to that call is
68
+ the release.
69
+
70
+ **The lead sees the member's queue as the member does.** The member on the team member list
71
+ carries `nextCall` and `linedUp` as the member's own snapshot does, republished with the member.
72
+
73
+ ### Runtime break prerequisite checks
74
+
75
+ Use `validateBreakCommand(method, request, state, context)` from the validation entry point
76
+ before any agent break dispatch. The context supplies current authentication and transport;
77
+ all four methods require the live login's break capability and active transport. Pass the
78
+ request object only to the request method, and undefined to the other three.
79
+
80
+ | Method | Required current state |
81
+ | --- | --- |
82
+ | `requestBreak` | `not-requested`; selected current reason code when codes exist; `canRequestBreak` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
83
+ | `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
84
+ | `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
85
+ | `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
86
+
87
+ Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
88
+ decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
89
+ the live `lead` flag and active transport, a current team member list for any command naming a
90
+ member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
91
+ awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
92
+ forced committed break; join and decline need the member's request on the list; coach, join-call
93
+ and leave need the lead already on that member's call; an assignment named must be one the list
94
+ shows the member holding. The context's target state must belong to the named member; that
95
+ association and backend authorization are provider responsibilities.
96
+
97
+ Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
98
+ as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
99
+ checks. Pending and completing tasks still count as work. Only a call a lead took over
100
+ (`takenOver`) sits beside a break in effect. Validate full task shapes and the envelope separately.
101
+
102
+ These validators report violations without dispatching, rewriting state or proving freshness.
103
+ Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
104
+ Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
105
+ is not a grant, and no method response overwrites a newer state emission. Rejected promises
106
+ remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
107
+ durable mutually exclusive commit/cancel decision and reconciliation obligations under
108
+ **Coordinating a multi-provider break** remain
109
+ required; per-provider validation does not implement the multi-provider coordinator.
110
+
111
+ ### `requestBreak(request)`
112
+
113
+ **Explanatory text is optional.** An agent may omit `BreakRequest.reason` for any break type.
114
+ A lead may likewise omit reason text from `force-break`. Do not require a text explanation
115
+ just because a break is agent-requested or forced. If supplied, text must be nonempty; omit
116
+ it rather than sending an empty string. A selected `reasonId` identifies a published break
117
+ choice and is separate from this optional text.
118
+
119
+ With published choices, `{ reasonId: "lunch" }` is a valid request without explanation;
120
+ without published choices, `{}` is valid. A lead can similarly send a `force-break` command
121
+ with its member ID and, where required, the selected reason ID, without reason text.
122
+
123
+ Requests permission to stop the agent later; it does not itself stop work. The provider continues
124
+ offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
125
+ provider reports `not-requested` directly, with `decisionReason` when one was supplied.
126
+
127
+ **An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
128
+ workspace as it does on the idle dashboard. Asked on a task, the break is decided and committed like
129
+ any other and begins when the work ends: that is `starting-after-task`, and
130
+ `assertBreakBeginsAfterTask` is the scenario that holds a provider to it.
131
+
132
+ #### Break reasons
133
+
134
+ A provider that defines not-ready reason codes publishes them on `Snapshot.break.reasons`,
135
+ and Omni returns the agent's choice as `BreakRequest.reasonId`:
136
+
137
+ ```ts
138
+ break: {
139
+ reasons: [
140
+ { id: "lunch", label: "Lunch" },
141
+ { id: "training", label: "Training", group: "Scheduled" },
142
+ ]
143
+ }
144
+ ```
145
+
146
+ Reason ids are non-empty and unique within the provider. They live on the snapshot rather than the
147
+ manifest because a provider may change the codes it offers during a shift; a `snapshot` event
148
+ replaces the list. `BreakRequest.reason` remains available for free text and is never a substitute
149
+ for `reasonId` when the provider publishes codes. A provider that defines no codes omits the
150
+ field.
151
+
152
+ #### One break across several providers
153
+
154
+ An agent connected to several providers takes **one** break, not one per platform. Omni gathers
155
+ what every connected provider offers, shows the distinct breaks, and on a choice sends one request
156
+ per provider: the same `reason`, and each provider's own `reasonId`.
157
+
158
+ To do that, Omni has to know when two providers mean the same break. Say so with `kind`:
159
+
160
+ ```ts
161
+ break: {
162
+ reasons: [
163
+ { id: "lunch", label: "Lunch", kind: "meal" },
164
+ { id: "rest", label: "Mandatory rest period", kind: "rest", alwaysAvailable: true },
165
+ ]
166
+ }
167
+ ```
168
+
169
+ A provider decides which breaks it offers and what it calls them. `BREAK_KINDS` is the list Omni
170
+ supports mapping them onto, and every member means something specific:
171
+
172
+ | Kind | What it means |
173
+ | --- | --- |
174
+ | `short-break` | A brief rest between contacts — the comfort break a shift plan allows for. |
175
+ | `meal` | A meal: lunch, dinner, whatever the shift calls it. |
176
+ | `rest` | A rest period the agent is entitled to and a busy hour cannot cancel. Usually the reason also marked `alwaysAvailable`, though the two are separate: this says what the break *is*, that says whether policy can withdraw it. |
177
+ | `training` | Learning something the agent is expected to know afterwards: a course, e-learning, a product walkthrough. However it is delivered, and whoever attends. |
178
+ | `coaching` | Reviewing this agent's own work with somebody accountable for it: a call listened back, quality feedback, a one-to-one about their interactions. Even where the outcome is that they learn something. |
179
+ | `meeting` | A scheduled gathering that is neither — a team huddle, a project call, a town hall. The agent attends and contributes; nobody is assessing their work and there is nothing they must know by the end. |
180
+ | `administrative` | Paperwork and follow-up not attached to a particular contact. |
181
+ | `technical` | Equipment or system trouble stopping the agent taking work: a dead headset, a phone that never registered, a tool that will not load. The one member that is not an activity — it says why the agent *cannot* work rather than what they are doing, which makes it the right home for a not-ready state raised about the agent's equipment. |
182
+ | `personal` | Personal time the deployment does not classify further. |
183
+ | `other` | None of the above. Matches nothing, including another provider's `other`. |
184
+
185
+ **The definitions are the point, not decoration.** Ten undefined strings would be the label
186
+ problem one level up: two providers could both declare `technical`, one meaning a dead headset
187
+ and the other scheduled maintenance, match on it, and nothing could tell the difference.
188
+
189
+ `training`, `coaching` and `meeting` can all fit one session, so take them in that order of
190
+ specificity: about this agent's own work is `coaching`, else something they must know afterwards is
191
+ `training`, else `meeting`.
192
+
193
+ **A break somebody forced on the agent is not automatically one of these.** Where the agent was
194
+ stopped rather than choosing to stop — see **Forced breaks** — set `BreakState.forced` and prefer
195
+ omitting `kind` to reaching for `other`. None of the ten describes "something was done to this
196
+ agent", and `other` claims a classification that was never made.
197
+
198
+ Omni matches in this order:
199
+
200
+ 1. **`kind`**, where both providers declare one. This wins over the label, so `{ id: "MEAL",
201
+ label: "Meal", kind: "meal" }` lines up with `{ id: "lunch", label: "Lunch", kind: "meal" }`
202
+ and nobody has to word their codes the same way.
203
+ 2. **The label**, folded for case and surrounding spacing, where a kind is missing. `"other"`
204
+ counts as missing: two providers saying `"other"` have only said their break is not on the
205
+ list, which is no evidence they mean the same thing.
206
+
207
+ Nothing else is matched. A break Omni cannot pair stays on its own, the agent is told how many
208
+ platforms their break will actually reach, and they can pair the odd one by hand — Omni remembers
209
+ that for next time.
210
+
211
+ Three rules for a provider:
212
+
213
+ - **Declare `kind` where you can, and leave it out where you cannot.** Leaving it out costs a
214
+ little precision. Getting it wrong sends an agent to lunch on one platform and to training on
215
+ another, and nothing can detect that.
216
+ - **You may publish several reasons of one kind** — two meal slots, say. Omni will not choose
217
+ between them for the agent; saying they are the same kind is not saying they are interchangeable.
218
+ - **Do not read `reason` as your own label.** It is the agent's single choice, in whichever
219
+ provider's wording they picked it from, sent unchanged to every provider in the break.
220
+
221
+ A provider that publishes no reason codes still receives the request, with `reason` set and
222
+ `reasonId` omitted.
223
+
224
+ #### Coordinating a multi-provider break
225
+
226
+ Sending the requests is a two-phase operation, not independent best effort. A provider first grants
227
+ permission for the agent to stop, then Omni either commits or cancels that permission.
228
+
229
+ `granted` is provider-visible state. It means only that this provider has granted the active
230
+ request and is ready to stop the agent when Omni asks. It does not reveal that other providers exist
231
+ or why Omni has not committed yet. While reporting `granted`, the provider remains available,
232
+ continues to honour Omni's current capacity, and continues offering work normally.
233
+
234
+ Omni separately tracks the aggregate agent application states `working`, `requesting-break`, `committing-break`,
235
+ `cancelling-break`, and `on-break`. While `requesting-break`, Omni shows which providers remain
236
+ outstanding and offers **Cancel break request**, but does not tell the agent that their break has
237
+ begun.
238
+
239
+ Omni offers the aggregate Break control only when every provider currently holding capacity
240
+ declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
241
+ knowingly permit partial availability.
242
+
243
+ Omni coordinates one break attempt as follows:
244
+
245
+ 1. Freeze the providers the attempt asks: every connected provider from which the agent can
246
+ currently receive work. A provider joining during the attempt is given no capacity until it
247
+ finishes. A provider whose authentication is `expired` is not one the agent can receive work
248
+ from and is not asked: nothing is asked of it, the break proceeds without it, and when the
249
+ login is restored it is reconciled from its snapshot as a set-aside provider is, with no
250
+ capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
251
+ available and work continues. `assertBreakAttemptProviders` holds an agent application to this set.
252
+ 2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
253
+ 3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
254
+ `granted`; neither state stops work. A denial transitions directly to `not-requested` and
255
+ causes Omni to take the cancel path.
256
+ 4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
257
+ and send `commitBreak()` to each of them. A provider then stops offering new work
258
+ and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
259
+ decision, tunable per deployment — decides who is kept: a provider that has not applied the
260
+ commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
261
+ begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
262
+ reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
263
+ and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
264
+ settled and beginning when the task ends, and offers no cancel, because the commit is durable.
265
+ 5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
266
+ decision timeout, or the agent cancels before commit, durably choose cancel and enter
267
+ `cancelling-break`. Send `cancelBreak()` to every provider still reporting
268
+ `awaiting-approval` or `granted`. Work continues during cancellation because no stop was
269
+ committed. Return to `working` only after no provider retains either state.
270
+
271
+ Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
272
+ it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
273
+ A provider that reports `granted` must therefore preserve the request across reconnects and must
274
+ honour a later commit or cancel. This
275
+ durable promise prevents a provider from failing the commit after another provider has already
276
+ stopped the agent.
277
+
278
+ #### Why the commit phase is bounded and the decision is not
279
+
280
+ Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
281
+ durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
282
+ or is uninstalled between granting and committing would hold Omni in `committing-break` with no
283
+ exit: the providers that did commit have already stopped the agent, and the agent is neither
284
+ working nor on a break.
285
+
286
+ Unanimity is required for a reason that survives the bound. It exists so the agent is not stopped
287
+ on one platform while another keeps routing work to them — and **a provider Omni cannot reach is
288
+ routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
289
+ for it costs the agent their break.
290
+
291
+ Setting a provider aside is not a rollback and not a cancel. The commit stands and the
292
+ obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
293
+ snapshot before anything else, and the snapshot decides:
294
+
295
+ - `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
296
+ - still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
297
+ late behind it, the provider stops an agent who is already stopped; nothing happens, because a
298
+ commit is a state to be in, not an act to be done.
299
+ - `not-requested` — the grant did not survive. Omni makes a new request for that provider alone,
300
+ against an agent who is already on break elsewhere. **A new login is this case too**: the grant
301
+ belonged to the old session.
302
+
303
+ The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
304
+ no capacity until it is reconciled.
305
+
306
+ **If the agent has already ended the break elsewhere, the break attempt is over** and the returning
307
+ provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
308
+ would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
309
+ Never rolling back is about a break that is still on, not one the agent has finished.
310
+
311
+ Omni may tell the agent which platforms the break has not yet reached, as it already does when a
312
+ break cannot be paired across every provider.
313
+
314
+ The decision phase needs no such bound, because nothing has stopped. Work continues throughout
315
+ `requesting-break`, so a provider that never answers costs the agent a wait rather than their
316
+ availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
317
+ because no stop was ever committed.
318
+
319
+ If cancel races with a late approval, the provider remains on the cancel path. If commit has already
320
+ won, cancel returns `omni.break-already-committed`, and Omni resumes commit recovery rather than
321
+ returning the agent to `working`.
322
+
323
+ A forced break is not rolled back by this algorithm. If one appears during either the request or
324
+ cancellation, the attempt is abandoned: Omni cancels every grant still outstanding and states
325
+ `setCapacity({ count: 0 })` on every other usable provider, as **Forced breaks** sets out; no grant
326
+ is committed, because the agent has already stopped everywhere.
327
+
328
+ This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
329
+ the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
330
+
331
+ #### Reporting the break the agent is on
332
+
333
+ `BreakState.activeReasonId` is the `BreakReason.id` the current break is on. Omni remembers what it
334
+ asked for, but only until the session ends — after a reload or reconnect, or where the provider put
335
+ the agent on the break itself, the provider is the only one who knows.
336
+
337
+ Omit it when you cannot say, and when there is no break: reporting a reason alongside
338
+ `status: "not-requested"` describes a break that is not happening, and is rejected.
339
+
340
+ ### `cancelBreak()`
341
+
342
+ Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
343
+ Cancellation releases the request but does not restore work because work never stopped. If
344
+ commit already won, the provider returns `omni.break-already-committed`. The resulting state is
345
+ reported through `break-state`.
346
+
347
+ ### `commitBreak()`
348
+
349
+ Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
350
+ business reason. On commit the provider stops offering new work and reports
351
+ `starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
352
+ Committing a break that is already in effect changes nothing and answers `committed`.
353
+
354
+ ### `endBreak()`
355
+
356
+ Tells a provider that an agent already on a break wants to become available again. The provider
357
+ reports the resulting state through `break-state` and provider status events.
358
+
359
+ It ends the break, which is the thing that started. Nothing about the connection was ever paused,
360
+ so there is nothing on it to resume.
package/guide.md CHANGED
@@ -25,6 +25,7 @@ every cross-reference names the file it points into.
25
25
  | `guide/phone.md` | **The phone.** How the agent hears the call: softphone and desk phone, the station and its headset, opening the audio, the microphone and the record. |
26
26
  | `guide/chat.md` | **Chat.** What is true of a chat and of nothing else. |
27
27
  | `guide/email.md` | **Email.** What is true of an email and of nothing else. |
28
+ | `guide/queue.md` | **The agent's own queue.** The next call, lined up for this agent while they finish the one they are on: the ask, the lined-up call, and the release. |
28
29
  | `guide/breaks.md` | **Breaks.** Asking for a break, a break forced on the agent, capacity, and one break across several providers. |
29
30
  | `guide/lead.md` | **Team leads.** The lead's own surface: the team member list, the lead's commands, lead assist, listening to a call. |
30
31
 
@@ -199,6 +200,24 @@ type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected
199
200
 
200
201
  type Phone = "softphone" | "deskPhone";
201
202
 
203
+ type PhoneStatus = "ready" | "unregistered" | "do-not-disturb" | "off-hook";
204
+
205
+ type PhoneChannelState = "ringing" | "active" | "held";
206
+
207
+ type PhoneChannel = {
208
+ state: PhoneChannelState;
209
+ since: IsoTimestamp;
210
+ assignmentId?: AssignmentId;
211
+ };
212
+
213
+ type PhoneState = {
214
+ phone: Phone;
215
+ status: PhoneStatus;
216
+ muted?: true;
217
+ since?: IsoTimestamp;
218
+ channels: PhoneChannel[];
219
+ };
220
+
202
221
  type IdleCapabilities<C extends Channel = Channel> = {
203
222
  personalBrowser?: PersonalBrowserCapability;
204
223
  calendar?: true;
@@ -219,6 +238,7 @@ type Manifest<C extends Channel = Channel> = {
219
238
  orgLevels?: LevelDeclaration[];
220
239
  dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
221
240
  phones?: C extends "voice" ? Phone[] : never;
241
+ phoneStatus?: C extends "voice" ? true : never;
222
242
  runningStepReports?: true;
223
243
  settleMs: number;
224
244
  };
@@ -273,6 +293,7 @@ type UserCapabilities = {
273
293
  breaks?: true;
274
294
  preferences?: AgentPreference[];
275
295
  lead?: true;
296
+ nextCall?: true;
276
297
  };
277
298
 
278
299
  type AuthenticationState =
@@ -498,6 +519,9 @@ type Snapshot = {
498
519
  contacts?: Contact[];
499
520
  calendar?: ScheduledActivity[];
500
521
  shift?: Shift;
522
+ phone?: PhoneState;
523
+ nextCall?: NextCallRequest;
524
+ linedUp?: LinedUpCall;
501
525
  team?: TeamMembers;
502
526
  };
503
527
 
@@ -990,6 +1014,30 @@ type BreakCancelResult =
990
1014
  type BreakEndResult =
991
1015
  | { status: "ended" }
992
1016
  | { status: "failed"; failure: ProtocolFailure };
1017
+
1018
+ type NextCallRequest = {
1019
+ since: IsoTimestamp;
1020
+ };
1021
+
1022
+ type LinedUpCall = {
1023
+ party?: Contact;
1024
+ queue?: string;
1025
+ queuedSince?: IsoTimestamp;
1026
+ since: IsoTimestamp;
1027
+ release?: true;
1028
+ };
1029
+
1030
+ type NextCallResult =
1031
+ | { status: "requested" }
1032
+ | { status: "failed"; failure: ProtocolFailure };
1033
+
1034
+ type NextCallCancelResult =
1035
+ | { status: "cancelled" }
1036
+ | { status: "failed"; failure: ProtocolFailure };
1037
+
1038
+ type LinedUpReleaseResult =
1039
+ | { status: "released" }
1040
+ | { status: "failed"; failure: ProtocolFailure };
993
1041
  ```
994
1042
 
995
1043
  ### Team
@@ -1006,6 +1054,9 @@ type TeamMember = {
1006
1054
  listening?: MemberListening[];
1007
1055
  shift?: Shift;
1008
1056
  request?: MemberRequest;
1057
+ phone?: PhoneState;
1058
+ nextCall?: NextCallRequest;
1059
+ linedUp?: LinedUpCall;
1009
1060
  };
1010
1061
 
1011
1062
  type MemberRequest = {
@@ -1162,7 +1213,10 @@ type ProviderEvent =
1162
1213
  | { type: "team-policies-updated"; policies: TeamPolicies }
1163
1214
  | { type: "contacts-updated"; contacts: Contact[] }
1164
1215
  | { type: "calendar-updated"; calendar: ScheduledActivity[] }
1165
- | { type: "shift-updated"; shift: Shift };
1216
+ | { type: "shift-updated"; shift: Shift }
1217
+ | { type: "phone-updated"; phone: PhoneState }
1218
+ | { type: "next-call"; nextCall?: NextCallRequest }
1219
+ | { type: "lined-up"; linedUp?: LinedUpCall };
1166
1220
 
1167
1221
  type ProviderEventEnvelope = {
1168
1222
  id: string;
@@ -1215,6 +1269,9 @@ type Connection<C extends Channel = Channel> = {
1215
1269
  commitBreak?(): Promise<BreakCommitResult>;
1216
1270
  cancelBreak?(): Promise<BreakCancelResult>;
1217
1271
  endBreak?(): Promise<BreakEndResult>;
1272
+ requestNextCall?(): Promise<NextCallResult>;
1273
+ cancelNextCall?(): Promise<NextCallCancelResult>;
1274
+ releaseLinedUp?(): Promise<LinedUpReleaseResult>;
1218
1275
 
1219
1276
  executeTeam?(request: TeamCommandRequest): Promise<TeamCommandResult>;
1220
1277
  openAudio?(request: OpenAudioRequest): Promise<OpenAudioResult>;
@@ -1654,6 +1711,7 @@ compile time.
1654
1711
  | `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
1655
1712
  | `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**. |
1656
1713
  | `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `deskPhone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call** in `guide/phone.md`. |
1714
+ | `phoneStatus` | Voice only, declared by presence as `true`: the platform sees the phone itself -- its registration, its do-not-disturb, its hook, its calls -- and publishes it as `Snapshot.phone` and `phone-updated`. A platform that cannot see the phone omits it, and then publishes no phone (`manifest.phoneStatus`). See **The phone's own view** in `guide/phone.md`. |
1657
1715
  | `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 conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome** in `guide/voice.md`. |
1658
1716
  | `timeCheck` | `true`: implements `checkTime` and states `providerTime` on every snapshot. Required of a provider that publishes an instant the desk renders as a running duration -- a member's `since`, a `listening[].since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts from the provider's clock (`manifest.timeCheck.required`); agent application polling is independently opt-in. See **Every screen counts from the provider's clock**. |
1659
1717
  | `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
@@ -1967,6 +2025,7 @@ to infer them from what arrives later.
1967
2025
  | Field | Contract |
1968
2026
  | --- | --- |
1969
2027
  | `breaks` | This login may request a break. Requires the four break methods on the connection. |
2028
+ | `nextCall` | The platform lines calls up for this agent: pressing Next call while on a call puts the next queued call in the agent's own queue, to start when their wrap ends unless somebody else takes it first. Requires `requestNextCall`, `cancelNextCall` and `releaseLinedUp` on the connection. See **The agent's own queue** in `guide/queue.md`. |
1970
2029
  | `lead` | This login leads a team. The flag alone turns on the team feature in the agent application, and every lead act comes with it; there is no per-action permission beside it. While the lead has the feature on, the provider publishes a `TeamMembers` object to them on every snapshot — `members: []` when nobody is in it — and to nobody else. Requires `executeTeam`. See **Team leads** in `guide/lead.md`. |
1971
2030
  | `preferences` | What the team left to this person, with where each stands and who set it. Omitted when nothing was. Requires `setPreference`. See **Who decides what an agent may do**. |
1972
2031
 
@@ -2183,6 +2242,9 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
2183
2242
  | `taskCount` | The provider's own count of those tasks, stated rather than inferred, and it must equal `tasks.length`. A snapshot with no work says `taskCount: 0` in so many words — a blank or unanswered state lacks the count and cannot pass as a confirmed empty. |
2184
2243
  | `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
2185
2244
  | `calendar` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
2245
+ | `phone` | The phone as the platform sees it -- the device, whether it can take a call, its own mute where observed -- from a provider whose manifest declares `phoneStatus`; required there (`snapshot.phone.required`) and forbidden otherwise (`snapshot.phone.unexpected`). Replaced whole by `phone-updated`. See **The phone's own view** in `guide/phone.md`. |
2246
+ | `nextCall` | The agent's standing ask for the next call, while it stands: `since`. It needs an assignment at work to be next after (`snapshot.nextCall.idle`) and the provider clears it when that assignment ends, when a call is lined up, and when the agent withdraws it. Replaced whole by `next-call`. See **The agent's own queue** in `guide/queue.md`. |
2247
+ | `linedUp` | The call waiting in the agent's own queue, while one is: the caller as shown to this audience, the queue, the caller's wait, when it was lined up, and `release` where the provider lets the agent let it go. Not a task yet. Replaced whole by `lined-up`. See **The agent's own queue** in `guide/queue.md`. |
2186
2248
  | `shift` | The agent's own day so far, as the provider counts it -- the same `Shift` their lead sees on the team member list, the same numbers from the same count. Replaced whole by `shift-updated`. Omitted only where the provider cannot say. See **The agent's day is on the wire**. |
2187
2249
  | `team` | Required `TeamMembers` when the login declares `capabilities.lead` and the lead has the team feature on, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission, and a lead who turned the feature off gets nothing of the team. |
2188
2250
 
@@ -2207,6 +2269,7 @@ surface in one place, and what obliges an adapter to implement each one.
2207
2269
  | `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
2208
2270
  | `cancelBreak()` | The login declares `capabilities.breaks`. |
2209
2271
  | `endBreak()` | The login declares `capabilities.breaks`. |
2272
+ | `requestNextCall()`, `cancelNextCall()`, `releaseLinedUp()` | The login declares `capabilities.nextCall`. The three stand together: a lined-up call the agent could neither withdraw nor let go is a promise with no way out. See **The agent's own queue** in `guide/queue.md`. |
2210
2273
  | `executeTeam(request)` | The login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. See **Lead commands** in `guide/lead.md`. |
2211
2274
  | `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
2212
2275
  | `recordStep(report)` | The manifest lists `softphone` among its `phones`. On a softphone the agent application mutes its own microphone on any call, and the provider's record has to have somewhere to take that leg; a desk phone's microphone is the phone's. See **The agent application records what it performs** in `guide/agent.md`. |
@@ -2743,6 +2806,27 @@ the `contacts` idle capability.
2743
2806
  Replaces this provider's complete scheduled-activity contribution. It is emitted only when the manifest
2744
2807
  declares the `calendar` idle capability.
2745
2808
 
2809
+ ### `next-call`
2810
+
2811
+ The agent's ask for the next call as it now stands, replacing `Snapshot.nextCall`: present while
2812
+ the ask stands, absent once it was met by a lined-up call, withdrawn by the agent, or outlived by
2813
+ the assignment it was pressed on. See **The agent's own queue** in `guide/queue.md`.
2814
+
2815
+ ### `lined-up`
2816
+
2817
+ The agent's own queue as it now stands, replacing `Snapshot.linedUp`: the call lined up for them,
2818
+ or nothing. It clears when the call becomes a task, on the ordinary `task-offered`, and when the
2819
+ queue gave the call to somebody else, the caller gave up, or the agent let it go. See **The
2820
+ agent's own queue** in `guide/queue.md`.
2821
+
2822
+ ### `phone-updated`
2823
+
2824
+ The phone as the platform now sees it, whole, replacing `Snapshot.phone`: a registration lost or
2825
+ back, do-not-disturb pressed or cleared, the handset lifted with no call or replaced, the desk
2826
+ phone's own mute. Emitted only by a provider whose manifest declares `phoneStatus`
2827
+ (`event.phone.capability`). The lead's member carries the same `PhoneState`, republished with the
2828
+ member. See **The phone's own view** in `guide/phone.md`.
2829
+
2746
2830
  ### `shift-updated`
2747
2831
 
2748
2832
  The agent's own day, whole, replacing `Snapshot.shift`: emitted whenever a total moves -- a task
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.107",
3
+ "version": "0.1.109",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",