@xema/omni-protocol 0.1.108 → 0.1.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ system.
12
12
  | Path | |
13
13
  | --- | --- |
14
14
  | `guide.md` | **The protocol.** The terms, the shapes, the rules, and how an adapter is declared, connected and held to them. |
15
- | `guide/` | One file per role and channel: `agent.md`, `lead.md`, `breaks.md`, `phone.md`, `voice.md`, `chat.md`, `email.md`. Each points back into `guide.md` for what it relies on. |
15
+ | `guide/` | One file per role and channel: `agent.md`, `lead.md`, `queue.md`, `breaks.md`, `phone.md`, `voice.md`, `chat.md`, `email.md`. Each points back into `guide.md` for what it relies on. |
16
16
  | `src/index.ts` | The TypeScript declarations. |
17
17
  | `src/validation.ts` | Runtime validators Omni applies to adapter output. |
18
18
  | `src/testing.ts` | Conformance helpers an adapter runs against its own test state. |
package/dist/index.d.ts CHANGED
@@ -296,6 +296,12 @@ export declare const LISTENING_MODES: readonly ["listen", "coach", "join-call"];
296
296
  export interface UserCapabilities {
297
297
  /** This login may request a break. Requires the four break methods. */
298
298
  breaks?: true;
299
+ /**
300
+ * The platform lines calls up for this agent: the agent may press Next call while on one to
301
+ * have the next queued call put in their own queue, to start when their wrap ends unless
302
+ * somebody else takes it first. Requires `requestNextCall`, `cancelNextCall` and `releaseLinedUp`.
303
+ */
304
+ nextCall?: true;
299
305
  /** The choices the team left to this person, with where each stands. Omitted when there are none. Requires `setPreference`. */
300
306
  preferences?: AgentPreference[];
301
307
  /**
@@ -1318,6 +1324,53 @@ export type BreakEndResult = {
1318
1324
  status: "failed";
1319
1325
  failure: ProtocolFailure;
1320
1326
  };
1327
+ /**
1328
+ * The agent's standing ask for the next call, made by pressing Next call while an assignment is
1329
+ * at work. It lives only as long as its purpose: the provider clears it when a call is lined up,
1330
+ * when the agent withdraws it, and when the assignment it was pressed on ends.
1331
+ */
1332
+ export interface NextCallRequest {
1333
+ since: IsoTimestamp;
1334
+ }
1335
+ /**
1336
+ * A call waiting for this agent in their own queue: still in the platform's queue, not yet a task,
1337
+ * ringing nothing. It starts when the agent's wrap ends, as an ordinary `task-offered` with
1338
+ * `acceptance: "automatic"`, unless somebody else answered first or the caller gave up, and then
1339
+ * the entry goes and no task ever was. One at a time: the phone only ever takes the next one.
1340
+ * Lined up on the agent's ask, or on the platform's own routing -- a repeat caller put in front
1341
+ * of the agent they spoke to last.
1342
+ */
1343
+ export interface LinedUpCall {
1344
+ /** The caller, as the provider shows them to this audience. */
1345
+ party?: Contact;
1346
+ /** The queue the call is in, by the platform's label. */
1347
+ queue?: string;
1348
+ /** When the caller entered the queue: their wait, counted from the provider's clock. */
1349
+ queuedSince?: IsoTimestamp;
1350
+ /** When the call was lined up for this agent. */
1351
+ since: IsoTimestamp;
1352
+ /** The agent may let this call go back to the queue for anyone: the provider's say, per call, by presence. */
1353
+ release?: true;
1354
+ }
1355
+ /** `requested` says the provider holds the ask, not that anything is lined up. */
1356
+ export type NextCallResult = {
1357
+ status: "requested";
1358
+ } | {
1359
+ status: "failed";
1360
+ failure: ProtocolFailure;
1361
+ };
1362
+ export type NextCallCancelResult = {
1363
+ status: "cancelled";
1364
+ } | {
1365
+ status: "failed";
1366
+ failure: ProtocolFailure;
1367
+ };
1368
+ export type LinedUpReleaseResult = {
1369
+ status: "released";
1370
+ } | {
1371
+ status: "failed";
1372
+ failure: ProtocolFailure;
1373
+ };
1321
1374
  /** `elsewhere`: signed in here, and the host holds this agent's capacity for another provider (`count: 0`, host-stopped); not receiving this provider's work. */
1322
1375
  export type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "elsewhere" | "signed-out";
1323
1376
  export interface TeamMember {
@@ -1337,6 +1390,10 @@ export interface TeamMember {
1337
1390
  request?: MemberRequest;
1338
1391
  /** The member's phone as the platform sees it, the same `PhoneState` the member's own snapshot carries; present where the provider declares `phoneStatus`. */
1339
1392
  phone?: PhoneState;
1393
+ /** The member's standing ask for the next call, as their own snapshot carries it. */
1394
+ nextCall?: NextCallRequest;
1395
+ /** The call lined up in the member's own queue, as their own snapshot carries it. */
1396
+ linedUp?: LinedUpCall;
1340
1397
  }
1341
1398
  /** This member asking their lead to join a call: which assignment, the note they wrote, since when. */
1342
1399
  export interface MemberRequest {
@@ -1638,6 +1695,10 @@ export interface Snapshot<C extends Channel = Channel> {
1638
1695
  shift?: Shift;
1639
1696
  /** The phone as the platform sees it, from a provider that declares `phoneStatus`; replaced whole by `phone-updated`. */
1640
1697
  phone?: PhoneState;
1698
+ /** The agent's standing ask for the next call, while it stands; replaced whole by `next-call`. */
1699
+ nextCall?: NextCallRequest;
1700
+ /** The call waiting in the agent's own queue, while one is; replaced whole by `lined-up`. */
1701
+ linedUp?: LinedUpCall;
1641
1702
  team?: TeamMembers;
1642
1703
  }
1643
1704
  /**
@@ -1759,6 +1820,16 @@ export type ProviderEvent<C extends Channel = Channel> = {
1759
1820
  | {
1760
1821
  type: "phone-updated";
1761
1822
  phone: PhoneState;
1823
+ }
1824
+ /** The agent's ask for the next call as it now stands: present while it stands, absent once met, withdrawn or outlived. */
1825
+ | {
1826
+ type: "next-call";
1827
+ nextCall?: NextCallRequest;
1828
+ }
1829
+ /** The agent's own queue as it now stands: the call lined up for them, or nothing. */
1830
+ | {
1831
+ type: "lined-up";
1832
+ linedUp?: LinedUpCall;
1762
1833
  };
1763
1834
  /**
1764
1835
  * The `Provider` prefix survives here for a mechanical reason rather than a naming one: `Event`
@@ -1830,6 +1901,14 @@ export interface Connection<C extends Channel = Channel> {
1830
1901
  commitBreak?(): Promise<BreakCommitResult>;
1831
1902
  cancelBreak?(): Promise<BreakCancelResult>;
1832
1903
  endBreak?(): Promise<BreakEndResult>;
1904
+ /**
1905
+ * The three stand together with `capabilities.nextCall`. Next call asks for the next queued
1906
+ * call to be lined up in the agent's own queue; cancel withdraws the ask before it is met;
1907
+ * release lets a lined-up call go back to the queue for anyone, where the entry grants it.
1908
+ */
1909
+ requestNextCall?(): Promise<NextCallResult>;
1910
+ cancelNextCall?(): Promise<NextCallCancelResult>;
1911
+ releaseLinedUp?(): Promise<LinedUpReleaseResult>;
1833
1912
  /** Required when the login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. */
1834
1913
  executeTeam?(request: TeamCommandRequest): Promise<TeamCommandResult>;
1835
1914
  /** Required of a voice adapter whose manifest lists `softphone`: on one, the call's audio lands in Omni. */
package/dist/index.js CHANGED
@@ -181,10 +181,11 @@ export function historyStepExpectsAPerson(step) {
181
181
  export function sameCapabilities(a, b) {
182
182
  // Every field, so a capability added later cannot be missed here: `satisfies` pins the field
183
183
  // list to the type, and a new key is a compile error until it is compared.
184
- void { breaks: true, preferences: true, lead: true };
184
+ void { breaks: true, preferences: true, lead: true, nextCall: true };
185
185
  const preferences = (list) => list === undefined ? undefined : list.map(p => [p.id, p.label, p.enabled, p.setBy, p.lockedBy ?? "", p.reason ?? ""].join("\u0000")).sort().join("\u0001");
186
186
  return a.breaks === b.breaks &&
187
187
  a.lead === b.lead &&
188
+ a.nextCall === b.nextCall &&
188
189
  preferences(a.preferences) === preferences(b.preferences);
189
190
  }
190
191
  /**
package/dist/testing.d.ts CHANGED
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
7
7
  * rules -- each optional part of a task, each optional part of the break state and team member list, each
8
8
  * declared contribution, and each event type.
9
9
  */
10
- declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.request", "team.tasks", "team.listening", "team.shift", "team.phone", "shift", "phone", "contacts", "calendar", "team.policies"];
10
+ declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.request", "team.tasks", "team.listening", "team.shift", "team.phone", "team.nextCall", "team.linedUp", "shift", "phone", "nextCall", "linedUp", "contacts", "calendar", "team.policies"];
11
11
  export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
12
12
  export interface AdapterContractResult {
13
13
  events: ProviderEventEnvelope[];
package/dist/testing.js CHANGED
@@ -29,8 +29,12 @@ const STATE_SUBJECTS = [
29
29
  "team.listening",
30
30
  "team.shift",
31
31
  "team.phone",
32
+ "team.nextCall",
33
+ "team.linedUp",
32
34
  "shift",
33
35
  "phone",
36
+ "nextCall",
37
+ "linedUp",
34
38
  "contacts",
35
39
  "calendar",
36
40
  "team.policies",
@@ -41,7 +45,7 @@ const EVENT_TYPES = {
41
45
  snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
42
46
  "task-audio-started": true, "task-audio-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
43
47
  "team-updated": true, "team-member-updated": true, "team-member-removed": true, "team-policies-updated": true,
44
- "contacts-updated": true, "calendar-updated": true, "shift-updated": true, "phone-updated": true,
48
+ "contacts-updated": true, "calendar-updated": true, "shift-updated": true, "phone-updated": true, "next-call": true, "lined-up": true,
45
49
  };
46
50
  const CONTRACT_SUBJECTS = [
47
51
  ...STATE_SUBJECTS,
@@ -105,6 +109,10 @@ function observeMember(member, seen) {
105
109
  seen.add("team.request");
106
110
  if (member.phone !== undefined)
107
111
  seen.add("team.phone");
112
+ if (member.nextCall !== undefined)
113
+ seen.add("team.nextCall");
114
+ if (member.linedUp !== undefined)
115
+ seen.add("team.linedUp");
108
116
  }
109
117
  function observePolicies(value, seen) {
110
118
  if (isRecord(value) && Object.keys(value).length > 0)
@@ -135,6 +143,10 @@ function observeSnapshot(value, seen) {
135
143
  seen.add("shift");
136
144
  if (value.phone !== undefined)
137
145
  seen.add("phone");
146
+ if (value.nextCall !== undefined)
147
+ seen.add("nextCall");
148
+ if (value.linedUp !== undefined)
149
+ seen.add("linedUp");
138
150
  }
139
151
  function observeEvent(envelope, seen) {
140
152
  const event = isRecord(envelope) ? envelope.event : undefined;
@@ -177,6 +189,14 @@ function observeEvent(envelope, seen) {
177
189
  case "phone-updated":
178
190
  seen.add("phone");
179
191
  break;
192
+ case "next-call":
193
+ if (event.nextCall !== undefined)
194
+ seen.add("nextCall");
195
+ break;
196
+ case "lined-up":
197
+ if (event.linedUp !== undefined)
198
+ seen.add("linedUp");
199
+ break;
180
200
  default: break;
181
201
  }
182
202
  }
@@ -275,6 +295,12 @@ export async function exerciseAdapter(adapter, context, options = {}) {
275
295
  }
276
296
  if (capabilities.lead === true)
277
297
  requireMethod(on, "executeTeam", "the login declares capabilities.lead");
298
+ if (capabilities.nextCall === true) {
299
+ // The ask, its withdrawal and the release stand together: a lined-up call the agent could neither withdraw nor let go is a promise with no way out.
300
+ for (const method of ["requestNextCall", "cancelNextCall", "releaseLinedUp"]) {
301
+ requireMethod(on, method, "the login declares capabilities.nextCall");
302
+ }
303
+ }
278
304
  if (some(capabilities.preferences))
279
305
  requireMethod(on, "setPreference", "the login declares capabilities.preferences");
280
306
  };
@@ -408,6 +434,16 @@ export async function exerciseAdapter(adapter, context, options = {}) {
408
434
  message: `a task was offered with ${stream.openCount()} already open against a stated capacity of ${capacityStated}` });
409
435
  }
410
436
  }
437
+ // The agent's own queue lives within capacity: told count 0, the provider clears the ask and
438
+ // lets the lined-up call go, so an entry published after that is a promise it cannot keep.
439
+ if (capacityStated === 0 && isRecord(envelope?.event)) {
440
+ if (envelope.event.type === "next-call" && envelope.event.nextCall !== undefined) {
441
+ violations.push({ rule: "stream.nextCall.stopped", path: "event.nextCall", message: "an ask for the next call was published after the host stated count 0: the agent's capacity is elsewhere, and the ask is cleared with it" });
442
+ }
443
+ if (envelope.event.type === "lined-up" && envelope.event.linedUp !== undefined) {
444
+ violations.push({ rule: "stream.linedUp.stopped", path: "event.linedUp", message: "a call was lined up after the host stated count 0: the agent's capacity is elsewhere, and the call goes back to the queue for anyone" });
445
+ }
446
+ }
411
447
  // A re-delivered envelope is harmless and applied once; the same id carrying a different
412
448
  // payload is a reused id, which no dedupe can make harmless, and is named.
413
449
  if (typeof envelope?.id === "string") {
@@ -1084,7 +1120,7 @@ const REACHABLE_PHASES = {
1084
1120
  export const SUPERSEDED_BY_A_SNAPSHOT = new Set([
1085
1121
  "snapshot", "transport-status", "break-state", "task-offered", "task-updated", "task-ended",
1086
1122
  "task-audio-started", "task-audio-ended", "team-updated", "team-member-updated", "team-member-removed", "team-policies-updated",
1087
- "contacts-updated", "calendar-updated", "shift-updated", "phone-updated",
1123
+ "contacts-updated", "calendar-updated", "shift-updated", "phone-updated", "next-call", "lined-up",
1088
1124
  ]);
1089
1125
  export class TaskStream {
1090
1126
  tasks = new Map();
@@ -126,6 +126,10 @@ export declare function validateTeamMembers(teamMembers: unknown, path?: string,
126
126
  export declare function validateTeamMember(member: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
127
127
  export declare function validateSnapshot(snapshot: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
128
128
  export declare function validateEventEnvelope(envelope: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
129
+ /** The agent's standing ask for the next call: since when. */
130
+ export declare function validateNextCall(value: unknown, path?: string): ProtocolViolation[];
131
+ /** A call waiting in the agent's own queue: the caller as shown to this audience, the queue, the waits, and whether the agent may let it go. */
132
+ export declare function validateLinedUp(value: unknown, path?: string, levels?: readonly string[]): ProtocolViolation[];
129
133
  /** The phone as the platform sees it: the agent's device for this provider, whether it can take a call, its own mute where observed. */
130
134
  export declare function validatePhoneState(value: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
131
135
  /**
@@ -161,7 +165,7 @@ export declare function validateTaskCommandRequest(request: unknown, task: unkno
161
165
  */
162
166
  export declare function validateTaskCommand(command: unknown, task?: unknown, path?: string, taskContext?: Omit<TaskValidationContext, "channel">): ProtocolViolation[];
163
167
  /** The connection methods whose results `validateResult` knows. */
164
- export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeam" | "openAudio" | "setPreference" | "recordStep";
168
+ export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "requestNextCall" | "cancelNextCall" | "releaseLinedUp" | "executeTeam" | "openAudio" | "setPreference" | "recordStep";
165
169
  /**
166
170
  * Validates what a connection method answered. A result is untrusted for the same reason a
167
171
  * snapshot is: it comes from an adapter that may be compiled against another version, and Omni
@@ -78,7 +78,7 @@ const DIALLING_CAPABILITIES = ["connectBack", "conference"];
78
78
  const SNAPSHOT_REASONS = membersOf({
79
79
  reconnected: true, "provider-requested": true,
80
80
  });
81
- const SESSION_CAPABILITIES = membersOf({ breaks: true, lead: true, preferences: true });
81
+ const SESSION_CAPABILITIES = membersOf({ breaks: true, lead: true, preferences: true, nextCall: true });
82
82
  const MEMBER_BREAKS = membersOf({
83
83
  "awaiting-approval": true, granted: true, "starting-after-task": true,
84
84
  });
@@ -1751,6 +1751,11 @@ function validateTeamMemberInto(member, at, context, into) {
1751
1751
  // The member's phone as the platform sees it, the same state the member's own desk holds.
1752
1752
  if (member.phone !== undefined)
1753
1753
  validatePhoneStateInto(member.phone, `${at}.phone`, {}, into);
1754
+ // The member's own queue, as their own snapshot carries it.
1755
+ if (member.nextCall !== undefined)
1756
+ validateNextCallInto(member.nextCall, `${at}.nextCall`, into);
1757
+ if (member.linedUp !== undefined)
1758
+ validateLinedUpInto(member.linedUp, `${at}.linedUp`, into, context.levels);
1754
1759
  // The member's standing ask for a lead, on one of the calls they hold.
1755
1760
  if (member.request !== undefined) {
1756
1761
  if (!isPlainObject(member.request)) {
@@ -1790,6 +1795,21 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
1790
1795
  validateShiftInto(snapshot.shift, `${path}.shift`, into, "shift");
1791
1796
  // The phone as the platform sees it: owed by a provider that declares it can, and by nobody else.
1792
1797
  const seesPhone = isPlainObject(manifest) && manifest.phoneStatus === true;
1798
+ // The agent's own queue: the ask and the lined-up call need an assignment at work to be next
1799
+ // after, and neither stands beside a break the agent asked for or was put on -- the break wins.
1800
+ const atWork = Array.isArray(snapshot.tasks) && snapshot.tasks.some((task) => isPlainObject(task) && (task.phase === "in-progress" || task.phase === "paused" || task.phase === "completing"));
1801
+ const breakStatus = isPlainObject(snapshot.break) ? snapshot.break.status : undefined;
1802
+ const breakStands = typeof breakStatus === "string" && breakStatus !== "not-requested";
1803
+ if (snapshot.nextCall !== undefined) {
1804
+ validateNextCallInto(snapshot.nextCall, `${path}.nextCall`, into);
1805
+ into.require(atWork, "snapshot.nextCall.idle", `${path}.nextCall`, "an ask for the next call stands only while an assignment is at work to be next after: the provider clears it when that assignment ends");
1806
+ into.require(!breakStands, "snapshot.nextCall.break", `${path}.nextCall`, `an ask for the next call does not stand beside a break that is ${String(breakStatus)}: the break wins, and the provider clears the ask`);
1807
+ }
1808
+ if (snapshot.linedUp !== undefined) {
1809
+ validateLinedUpInto(snapshot.linedUp, `${path}.linedUp`, into, levels);
1810
+ into.require(atWork, "snapshot.linedUp.idle", `${path}.linedUp`, "a call is lined up for an agent with an assignment at work; with nothing at work it is an offer, and travels as task-offered");
1811
+ into.require(!breakStands, "snapshot.linedUp.break", `${path}.linedUp`, `a lined-up call does not stand beside a break that is ${String(breakStatus)}: the break wins, and the call goes back to the queue for anyone`);
1812
+ }
1793
1813
  if (seesPhone && snapshot.phone === undefined) {
1794
1814
  into.add("snapshot.phone.required", `${path}.phone`, "the manifest declares phoneStatus, so every snapshot carries the phone as the platform sees it");
1795
1815
  }
@@ -2107,6 +2127,14 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
2107
2127
  event.contacts.forEach((contact, index) => validateContactInto(contact, `${at}.contacts[${index}]`, into, levels));
2108
2128
  }
2109
2129
  break;
2130
+ case "next-call":
2131
+ if (event.nextCall !== undefined)
2132
+ validateNextCallInto(event.nextCall, `${at}.nextCall`, into);
2133
+ break;
2134
+ case "lined-up":
2135
+ if (event.linedUp !== undefined)
2136
+ validateLinedUpInto(event.linedUp, `${at}.linedUp`, into, levels);
2137
+ break;
2110
2138
  case "phone-updated":
2111
2139
  into.require(isPlainObject(manifest) && manifest.phoneStatus === true, "event.phone.capability", `${at}.phone`, "phone-updated comes from a provider whose manifest declares phoneStatus");
2112
2140
  validatePhoneStateInto(event.phone, `${at}.phone`, context, into);
@@ -2192,6 +2220,44 @@ function requireClockInto(value, manifest, path, into) {
2192
2220
  return;
2193
2221
  into.add("manifest.timeCheck.required", path, "this publishes an instant the desk renders as a running duration -- a since, a signedInAt -- and the manifest declares no timeCheck: every screen counts such a duration from the provider's clock, so the provider states one");
2194
2222
  }
2223
+ /** The agent's standing ask for the next call: since when. */
2224
+ export function validateNextCall(value, path = "nextCall") {
2225
+ const into = new Collector();
2226
+ validateNextCallInto(value, path, into);
2227
+ return into.violations;
2228
+ }
2229
+ function validateNextCallInto(value, path, into) {
2230
+ if (!isPlainObject(value)) {
2231
+ into.add("nextCall.shape", path, "the ask must be an object");
2232
+ return;
2233
+ }
2234
+ into.timestamp(value.since, "nextCall.since", `${path}.since`);
2235
+ for (const key of Object.keys(value))
2236
+ into.require(key === "since", "nextCall.field", `${path}.${key}`, "unsupported field on the ask");
2237
+ }
2238
+ /** A call waiting in the agent's own queue: the caller as shown to this audience, the queue, the waits, and whether the agent may let it go. */
2239
+ export function validateLinedUp(value, path = "linedUp", levels) {
2240
+ const into = new Collector();
2241
+ validateLinedUpInto(value, path, into, levels);
2242
+ return into.violations;
2243
+ }
2244
+ function validateLinedUpInto(value, path, into, levels) {
2245
+ if (!isPlainObject(value)) {
2246
+ into.add("linedUp.shape", path, "a lined-up call must be an object");
2247
+ return;
2248
+ }
2249
+ if (value.party !== undefined)
2250
+ validateContactInto(value.party, `${path}.party`, into, levels);
2251
+ if (value.queue !== undefined)
2252
+ into.filled(value.queue, "linedUp.queue", `${path}.queue`, "the queue's label must not be empty when present");
2253
+ if (value.queuedSince !== undefined)
2254
+ into.timestamp(value.queuedSince, "linedUp.queuedSince", `${path}.queuedSince`);
2255
+ into.timestamp(value.since, "linedUp.since", `${path}.since`);
2256
+ if (value.release !== undefined)
2257
+ into.require(value.release === true, "linedUp.release", `${path}.release`, "letting the call go is granted by presence: send true or omit it");
2258
+ for (const key of Object.keys(value))
2259
+ into.require(["party", "queue", "queuedSince", "since", "release"].includes(key), "linedUp.field", `${path}.${key}`, "unsupported field on a lined-up call");
2260
+ }
2195
2261
  /** The phone as the platform sees it: the agent's device for this provider, whether it can take a call, its own mute where observed. */
2196
2262
  export function validatePhoneState(value, path = "phone", context = {}) {
2197
2263
  const into = new Collector();
@@ -2790,6 +2856,9 @@ const RESULT_STATUSES = {
2790
2856
  commitBreak: { success: "committed", failure: "failed" },
2791
2857
  cancelBreak: { success: "cancelled", failure: "failed" },
2792
2858
  endBreak: { success: "ended", failure: "failed" },
2859
+ requestNextCall: { success: "requested", failure: "failed" },
2860
+ cancelNextCall: { success: "cancelled", failure: "failed" },
2861
+ releaseLinedUp: { success: "released", failure: "failed" },
2793
2862
  executeTeam: { success: "applied", failure: "failed" },
2794
2863
  openAudio: { success: "opened", failure: "unavailable" },
2795
2864
  setPreference: { success: "applied", failure: "failed" },
package/guide/agent.md CHANGED
@@ -382,6 +382,7 @@ Migration from the earlier spellings:
382
382
  | a member republished on a hold, a mute, a take-over | on every publication of the member's task to the member |
383
383
  | two clients on one login | the later wins; the first is ended with `recovery: "displaced"` |
384
384
  | the phone unspoken | `PhoneState` -- the device, its mute, its channels -- on `Snapshot.phone`, `phone-updated` and `TeamMember.phone`, from a manifest declaring `phoneStatus` (`snapshot.phone.required`, `.unexpected`, `phone.*`, `phone.channel.*`, `event.phone.capability`) |
385
+ | the agent's readiness for the next call read into setCapacity | the agent's own queue: `capabilities.nextCall`, `requestNextCall`/`cancelNextCall`/`releaseLinedUp`, `Snapshot.nextCall` and `linedUp`, the `next-call` and `lined-up` events (`snapshot.nextCall.idle`, `nextCall.*`, `linedUp.*`) |
385
386
 
386
387
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
387
388
  History and report rule names use `history` and `historyReport`; assignment rules use
package/guide/breaks.md CHANGED
@@ -250,291 +250,9 @@ against it (`stream.taskOffered.overCapacity`).
250
250
  idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
251
251
  it: the ceiling binds assignment, not the agent's own hand.
252
252
 
253
- ### Runtime break prerequisite checks
254
-
255
- Use `validateBreakCommand(method, request, state, context)` from the validation entry point
256
- before any agent break dispatch. The context supplies current authentication and transport;
257
- all four methods require the live login's break capability and active transport. Pass the
258
- request object only to the request method, and undefined to the other three.
259
-
260
- | Method | Required current state |
261
- | --- | --- |
262
- | `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. |
263
- | `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
264
- | `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
265
- | `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
266
-
267
- Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
268
- decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
269
- the live `lead` flag and active transport, a current team member list for any command naming a
270
- member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
271
- awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
272
- forced committed break; join and decline need the member's request on the list; coach, join-call
273
- and leave need the lead already on that member's call; an assignment named must be one the list
274
- shows the member holding. The context's target state must belong to the named member; that
275
- association and backend authorization are provider responsibilities.
276
-
277
- Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
278
- as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
279
- checks. Pending and completing tasks still count as work. Only a call a lead took over
280
- (`takenOver`) sits beside a break in effect. Validate full task shapes and the envelope separately.
281
-
282
- These validators report violations without dispatching, rewriting state or proving freshness.
283
- Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
284
- Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
285
- is not a grant, and no method response overwrites a newer state emission. Rejected promises
286
- remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
287
- durable mutually exclusive commit/cancel decision and reconciliation obligations under
288
- **Coordinating a multi-provider break** remain
289
- required; per-provider validation does not implement the multi-provider coordinator.
290
-
291
- ### `requestBreak(request)`
292
-
293
- **Explanatory text is optional.** An agent may omit `BreakRequest.reason` for any break type.
294
- A lead may likewise omit reason text from `force-break`. Do not require a text explanation
295
- just because a break is agent-requested or forced. If supplied, text must be nonempty; omit
296
- it rather than sending an empty string. A selected `reasonId` identifies a published break
297
- choice and is separate from this optional text.
298
-
299
- With published choices, `{ reasonId: "lunch" }` is a valid request without explanation;
300
- without published choices, `{}` is valid. A lead can similarly send a `force-break` command
301
- with its member ID and, where required, the selected reason ID, without reason text.
302
-
303
- Requests permission to stop the agent later; it does not itself stop work. The provider continues
304
- offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
305
- provider reports `not-requested` directly, with `decisionReason` when one was supplied.
306
-
307
- **An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
308
- workspace as it does on the idle dashboard. Asked on a task, the break is decided and committed like
309
- any other and begins when the work ends: that is `starting-after-task`, and
310
- `assertBreakBeginsAfterTask` is the scenario that holds a provider to it.
311
-
312
- #### Break reasons
313
-
314
- A provider that defines not-ready reason codes publishes them on `Snapshot.break.reasons`,
315
- and Omni returns the agent's choice as `BreakRequest.reasonId`:
316
-
317
- ```ts
318
- break: {
319
- reasons: [
320
- { id: "lunch", label: "Lunch" },
321
- { id: "training", label: "Training", group: "Scheduled" },
322
- ]
323
- }
324
- ```
325
-
326
- Reason ids are non-empty and unique within the provider. They live on the snapshot rather than the
327
- manifest because a provider may change the codes it offers during a shift; a `snapshot` event
328
- replaces the list. `BreakRequest.reason` remains available for free text and is never a substitute
329
- for `reasonId` when the provider publishes codes. A provider that defines no codes omits the
330
- field.
331
-
332
- #### One break across several providers
333
-
334
- An agent connected to several providers takes **one** break, not one per platform. Omni gathers
335
- what every connected provider offers, shows the distinct breaks, and on a choice sends one request
336
- per provider: the same `reason`, and each provider's own `reasonId`.
337
-
338
- To do that, Omni has to know when two providers mean the same break. Say so with `kind`:
339
-
340
- ```ts
341
- break: {
342
- reasons: [
343
- { id: "lunch", label: "Lunch", kind: "meal" },
344
- { id: "rest", label: "Mandatory rest period", kind: "rest", alwaysAvailable: true },
345
- ]
346
- }
347
- ```
348
-
349
- A provider decides which breaks it offers and what it calls them. `BREAK_KINDS` is the list Omni
350
- supports mapping them onto, and every member means something specific:
351
-
352
- | Kind | What it means |
353
- | --- | --- |
354
- | `short-break` | A brief rest between contacts — the comfort break a shift plan allows for. |
355
- | `meal` | A meal: lunch, dinner, whatever the shift calls it. |
356
- | `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. |
357
- | `training` | Learning something the agent is expected to know afterwards: a course, e-learning, a product walkthrough. However it is delivered, and whoever attends. |
358
- | `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. |
359
- | `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. |
360
- | `administrative` | Paperwork and follow-up not attached to a particular contact. |
361
- | `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. |
362
- | `personal` | Personal time the deployment does not classify further. |
363
- | `other` | None of the above. Matches nothing, including another provider's `other`. |
364
-
365
- **The definitions are the point, not decoration.** Ten undefined strings would be the label
366
- problem one level up: two providers could both declare `technical`, one meaning a dead headset
367
- and the other scheduled maintenance, match on it, and nothing could tell the difference.
368
-
369
- `training`, `coaching` and `meeting` can all fit one session, so take them in that order of
370
- specificity: about this agent's own work is `coaching`, else something they must know afterwards is
371
- `training`, else `meeting`.
372
-
373
- **A break somebody forced on the agent is not automatically one of these.** Where the agent was
374
- stopped rather than choosing to stop — see **Forced breaks** — set `BreakState.forced` and prefer
375
- omitting `kind` to reaching for `other`. None of the ten describes "something was done to this
376
- agent", and `other` claims a classification that was never made.
377
-
378
- Omni matches in this order:
379
-
380
- 1. **`kind`**, where both providers declare one. This wins over the label, so `{ id: "MEAL",
381
- label: "Meal", kind: "meal" }` lines up with `{ id: "lunch", label: "Lunch", kind: "meal" }`
382
- and nobody has to word their codes the same way.
383
- 2. **The label**, folded for case and surrounding spacing, where a kind is missing. `"other"`
384
- counts as missing: two providers saying `"other"` have only said their break is not on the
385
- list, which is no evidence they mean the same thing.
386
-
387
- Nothing else is matched. A break Omni cannot pair stays on its own, the agent is told how many
388
- platforms their break will actually reach, and they can pair the odd one by hand — Omni remembers
389
- that for next time.
390
-
391
- Three rules for a provider:
392
-
393
- - **Declare `kind` where you can, and leave it out where you cannot.** Leaving it out costs a
394
- little precision. Getting it wrong sends an agent to lunch on one platform and to training on
395
- another, and nothing can detect that.
396
- - **You may publish several reasons of one kind** — two meal slots, say. Omni will not choose
397
- between them for the agent; saying they are the same kind is not saying they are interchangeable.
398
- - **Do not read `reason` as your own label.** It is the agent's single choice, in whichever
399
- provider's wording they picked it from, sent unchanged to every provider in the break.
400
-
401
- A provider that publishes no reason codes still receives the request, with `reason` set and
402
- `reasonId` omitted.
403
-
404
- #### Coordinating a multi-provider break
405
-
406
- Sending the requests is a two-phase operation, not independent best effort. A provider first grants
407
- permission for the agent to stop, then Omni either commits or cancels that permission.
408
-
409
- `granted` is provider-visible state. It means only that this provider has granted the active
410
- request and is ready to stop the agent when Omni asks. It does not reveal that other providers exist
411
- or why Omni has not committed yet. While reporting `granted`, the provider remains available,
412
- continues to honour Omni's current capacity, and continues offering work normally.
413
-
414
- Omni separately tracks the aggregate agent application states `working`, `requesting-break`, `committing-break`,
415
- `cancelling-break`, and `on-break`. While `requesting-break`, Omni shows which providers remain
416
- outstanding and offers **Cancel break request**, but does not tell the agent that their break has
417
- begun.
418
-
419
- Omni offers the aggregate Break control only when every provider currently holding capacity
420
- declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
421
- knowingly permit partial availability.
422
-
423
- Omni coordinates one break attempt as follows:
424
-
425
- 1. Freeze the providers the attempt asks: every connected provider from which the agent can
426
- currently receive work. A provider joining during the attempt is given no capacity until it
427
- finishes. A provider whose authentication is `expired` is not one the agent can receive work
428
- from and is not asked: nothing is asked of it, the break proceeds without it, and when the
429
- login is restored it is reconciled from its snapshot as a set-aside provider is, with no
430
- capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
431
- available and work continues. `assertBreakAttemptProviders` holds an agent application to this set.
432
- 2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
433
- 3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
434
- `granted`; neither state stops work. A denial transitions directly to `not-requested` and
435
- causes Omni to take the cancel path.
436
- 4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
437
- and send `commitBreak()` to each of them. A provider then stops offering new work
438
- and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
439
- decision, tunable per deployment — decides who is kept: a provider that has not applied the
440
- commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
441
- begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
442
- reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
443
- and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
444
- settled and beginning when the task ends, and offers no cancel, because the commit is durable.
445
- 5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
446
- decision timeout, or the agent cancels before commit, durably choose cancel and enter
447
- `cancelling-break`. Send `cancelBreak()` to every provider still reporting
448
- `awaiting-approval` or `granted`. Work continues during cancellation because no stop was
449
- committed. Return to `working` only after no provider retains either state.
450
-
451
- Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
452
- it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
453
- A provider that reports `granted` must therefore preserve the request across reconnects and must
454
- honour a later commit or cancel. This
455
- durable promise prevents a provider from failing the commit after another provider has already
456
- stopped the agent.
457
-
458
- #### Why the commit phase is bounded and the decision is not
459
-
460
- Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
461
- durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
462
- or is uninstalled between granting and committing would hold Omni in `committing-break` with no
463
- exit: the providers that did commit have already stopped the agent, and the agent is neither
464
- working nor on a break.
465
-
466
- Unanimity is required for a reason that survives the bound. It exists so the agent is not stopped
467
- on one platform while another keeps routing work to them — and **a provider Omni cannot reach is
468
- routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
469
- for it costs the agent their break.
470
-
471
- Setting a provider aside is not a rollback and not a cancel. The commit stands and the
472
- obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
473
- snapshot before anything else, and the snapshot decides:
474
-
475
- - `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
476
- - still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
477
- late behind it, the provider stops an agent who is already stopped; nothing happens, because a
478
- commit is a state to be in, not an act to be done.
479
- - `not-requested` — the grant did not survive. Omni makes a new request for that provider alone,
480
- against an agent who is already on break elsewhere. **A new login is this case too**: the grant
481
- belonged to the old session.
482
-
483
- The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
484
- no capacity until it is reconciled.
485
-
486
- **If the agent has already ended the break elsewhere, the break attempt is over** and the returning
487
- provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
488
- would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
489
- Never rolling back is about a break that is still on, not one the agent has finished.
490
-
491
- Omni may tell the agent which platforms the break has not yet reached, as it already does when a
492
- break cannot be paired across every provider.
493
-
494
- The decision phase needs no such bound, because nothing has stopped. Work continues throughout
495
- `requesting-break`, so a provider that never answers costs the agent a wait rather than their
496
- availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
497
- because no stop was ever committed.
498
-
499
- If cancel races with a late approval, the provider remains on the cancel path. If commit has already
500
- won, cancel returns `omni.break-already-committed`, and Omni resumes commit recovery rather than
501
- returning the agent to `working`.
502
-
503
- A forced break is not rolled back by this algorithm. If one appears during either the request or
504
- cancellation, the attempt is abandoned: Omni cancels every grant still outstanding and states
505
- `setCapacity({ count: 0 })` on every other usable provider, as **Forced breaks** sets out; no grant
506
- is committed, because the agent has already stopped everywhere.
507
-
508
- This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
509
- the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
510
-
511
- #### Reporting the break the agent is on
512
-
513
- `BreakState.activeReasonId` is the `BreakReason.id` the current break is on. Omni remembers what it
514
- asked for, but only until the session ends — after a reload or reconnect, or where the provider put
515
- the agent on the break itself, the provider is the only one who knows.
516
-
517
- Omit it when you cannot say, and when there is no break: reporting a reason alongside
518
- `status: "not-requested"` describes a break that is not happening, and is rejected.
519
-
520
- ### `cancelBreak()`
521
-
522
- Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
523
- Cancellation releases the request but does not restore work because work never stopped. If
524
- commit already won, the provider returns `omni.break-already-committed`. The resulting state is
525
- reported through `break-state`.
526
-
527
- ### `commitBreak()`
528
-
529
- Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
530
- business reason. On commit the provider stops offering new work and reports
531
- `starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
532
- Committing a break that is already in effect changes nothing and answers `committed`.
533
-
534
- ### `endBreak()`
535
-
536
- Tells a provider that an agent already on a break wants to become available again. The provider
537
- reports the resulting state through `break-state` and provider status events.
538
-
539
- It ends the break, which is the thing that started. Nothing about the connection was ever paused,
540
- so there is nothing on it to resume.
253
+ **Capacity is not the agent's readiness.** It is the desk's division of one person across
254
+ providers, a ceiling on assignments at once, and it says nothing about the moment. An agent who
255
+ is about to finish a call and wants the next one lined up has a different thing to say, and says
256
+ it as **The agent's own queue** in `guide/queue.md` sets out; the ceiling stands unchanged while they do,
257
+ and the queue lives within it: `count: 0` clears the ask and lets a lined-up call go, and a break
258
+ in flight or in effect does the same, since the break wins.
package/guide/lead.md CHANGED
@@ -32,6 +32,7 @@ computes nothing. See **`team-updated`** in `guide.md` and its companions.
32
32
  | `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, less the workspace. The assignment, title, task type, phase, party, room, audio and completion terms -- `completionMode`, `wrapAllowance`, `wrapEndsInSeconds` -- are what the lead reads, and the task's record -- `history`, steps and totals -- travels whole, exactly as the member's desk holds it, so the lead's screen and the member's show the same record. The workspace -- `capabilities`, `capabilitySource`, `browsers` and their URLs -- is the member's desk's alone and never travels (`team.member.task.workspace`). The party's number and email are shown to leads or locked as the provider decides for leads, apart from what it decides for the member -- see **What the queue locks is locked on the whole task**. `[]` when the member holds none; omitted only where the provider cannot see them. |
33
33
  | `listening` | Every lead on this member's call — listening, coaching or joined — published to every lead alike: each entry names the lead by `leadId`, which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. The entry naming the signed-in lead is what their own audio opens on. Absent while nobody is on the call, never `[]` (`team.member.listening.*`); a lead is on one member's call at a time (`team.listening.single`), and a lead who is heard is also in the task's `onCall` (`team.member.listening.heard`). |
34
34
  | `request` | Present while the member is asking a lead to join one of their calls: which assignment, the note they wrote, since when. The call named is one their `tasks` carry (`team.member.request.assignment`). Absent when they are not asking. See **Lead assist**. |
35
+ | `nextCall`, `linedUp` | The member's own queue as their own snapshot carries it: their standing ask for the next call, and the call lined up for them. See **The agent's own queue** in `guide/queue.md`. |
35
36
  | `phone` | The member's phone as the platform sees it: the same `PhoneState` the member's own snapshot carries, so a lead sees a desk phone that is down, or a call on it that is no task, as the member does. Present where the provider declares `phoneStatus`. See **The phone's own view** in `guide/phone.md`. |
36
37
  | `shift` | The member's own history for the day, about the person rather than any one call: `signedInAt`, `signedOutAt` once it has happened, today's totals as the provider counts them — `talkSeconds`, `holdSeconds`, `breakSeconds`, `tasksHandled`, each present only when the provider knows it — and the day's sign-in, sign-out and break `events`, oldest first. The same `Shift` the member's own snapshot carries, the same numbers from the same count. Omitted where the provider cannot say. |
37
38
 
package/guide/queue.md ADDED
@@ -0,0 +1,378 @@
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 queue lives within capacity, and never beside a break.** The ask and the lined-up call are
48
+ promises to assign, and a promise the provider can no longer keep goes:
49
+
50
+ - **Told `count: 0`**, host-stopped, the provider assigns nothing, so it clears the ask and lets the
51
+ lined-up call go back to the queue for anyone; an entry published after that is refused
52
+ (`stream.nextCall.stopped`, `stream.linedUp.stopped`). The queue is per provider, as the ask
53
+ was made on that provider's call, and the desk's division of capacity is what it was.
54
+ - **A break wins.** An agent who has asked for a break, been granted one starting after this
55
+ task, or been put on one has said the opposite of "line up my next call", and the two never
56
+ stand together: while a break is in flight or in effect -- any status but `not-requested` --
57
+ `requestNextCall()` is refused with `omni.unavailable`, and a break arriving after a
58
+ call was lined up drops the call, the caller going back to the queue for anyone
59
+ (`snapshot.nextCall.break`, `snapshot.linedUp.break`). The desk shows Next call or the break
60
+ controls, never both.
61
+ - **A lined-up call needs an agent at work** as the ask does: a call lined up for an agent with
62
+ nothing at work is an offer that has not been made, and travels as `task-offered`
63
+ (`snapshot.linedUp.idle`). The repeat caller for an idle agent is an ordinary offer.
64
+
65
+ **The lined-up call is not a task, and rides no task event.** Until the offer there is no
66
+ assignment and nothing to accept, so it has no phase, no capabilities and no history, and it
67
+ rings nothing: the caller is still in the platform's queue, the phone has no channel for it, and
68
+ the desk shows "next for you" and nothing to act on but the release. It becomes a task by the
69
+ ordinary `task-offered`, with `acceptance: "automatic"` since the agent asked, and counts against
70
+ capacity from that moment as any assignment does; the `queued` step in its history carries the
71
+ caller's whole wait. One at a time: the phone only ever takes the next one, so `linedUp` is an
72
+ object, not a list. Nothing is guaranteed: the queue may give the call to whoever is free first,
73
+ and then the entry goes and no task ever was.
74
+
75
+ **A repeat caller is the same shape, unasked.** A provider whose routing prefers the agent a
76
+ caller spoke to last lines the call up for that agent without a press, so they see it in their own
77
+ queue and can take it before another agent does. The ask is only what the agent adds on top of
78
+ the platform's routing: releasing one call, or withdrawing the ask, ends nothing of that routing,
79
+ and the provider may line a repeat caller up again.
80
+
81
+ **Letting go is the provider's say.** `release: true` on the lined-up call grants the agent Let
82
+ go; absent, the platform means them to take the call and the desk offers nothing
83
+ (`linedUp.release`). `releaseLinedUp()` against an entry without it is refused with
84
+ `omni.capability-not-enabled`, as any control the provider did not grant. `cancelNextCall()`
85
+ withdraws the ask alone: once a call is lined up the ask is met, and what happens to that call is
86
+ the release.
87
+
88
+ **The lead sees the member's queue as the member does.** The member on the team member list
89
+ carries `nextCall` and `linedUp` as the member's own snapshot does, republished with the member.
90
+
91
+ ### Runtime break prerequisite checks
92
+
93
+ Use `validateBreakCommand(method, request, state, context)` from the validation entry point
94
+ before any agent break dispatch. The context supplies current authentication and transport;
95
+ all four methods require the live login's break capability and active transport. Pass the
96
+ request object only to the request method, and undefined to the other three.
97
+
98
+ | Method | Required current state |
99
+ | --- | --- |
100
+ | `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. |
101
+ | `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
102
+ | `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
103
+ | `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
104
+
105
+ Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
106
+ decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
107
+ the live `lead` flag and active transport, a current team member list for any command naming a
108
+ member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
109
+ awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
110
+ forced committed break; join and decline need the member's request on the list; coach, join-call
111
+ and leave need the lead already on that member's call; an assignment named must be one the list
112
+ shows the member holding. The context's target state must belong to the named member; that
113
+ association and backend authorization are provider responsibilities.
114
+
115
+ Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
116
+ as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
117
+ checks. Pending and completing tasks still count as work. Only a call a lead took over
118
+ (`takenOver`) sits beside a break in effect. Validate full task shapes and the envelope separately.
119
+
120
+ These validators report violations without dispatching, rewriting state or proving freshness.
121
+ Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
122
+ Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
123
+ is not a grant, and no method response overwrites a newer state emission. Rejected promises
124
+ remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
125
+ durable mutually exclusive commit/cancel decision and reconciliation obligations under
126
+ **Coordinating a multi-provider break** remain
127
+ required; per-provider validation does not implement the multi-provider coordinator.
128
+
129
+ ### `requestBreak(request)`
130
+
131
+ **Explanatory text is optional.** An agent may omit `BreakRequest.reason` for any break type.
132
+ A lead may likewise omit reason text from `force-break`. Do not require a text explanation
133
+ just because a break is agent-requested or forced. If supplied, text must be nonempty; omit
134
+ it rather than sending an empty string. A selected `reasonId` identifies a published break
135
+ choice and is separate from this optional text.
136
+
137
+ With published choices, `{ reasonId: "lunch" }` is a valid request without explanation;
138
+ without published choices, `{}` is valid. A lead can similarly send a `force-break` command
139
+ with its member ID and, where required, the selected reason ID, without reason text.
140
+
141
+ Requests permission to stop the agent later; it does not itself stop work. The provider continues
142
+ offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
143
+ provider reports `not-requested` directly, with `decisionReason` when one was supplied.
144
+
145
+ **An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
146
+ workspace as it does on the idle dashboard. Asked on a task, the break is decided and committed like
147
+ any other and begins when the work ends: that is `starting-after-task`, and
148
+ `assertBreakBeginsAfterTask` is the scenario that holds a provider to it.
149
+
150
+ #### Break reasons
151
+
152
+ A provider that defines not-ready reason codes publishes them on `Snapshot.break.reasons`,
153
+ and Omni returns the agent's choice as `BreakRequest.reasonId`:
154
+
155
+ ```ts
156
+ break: {
157
+ reasons: [
158
+ { id: "lunch", label: "Lunch" },
159
+ { id: "training", label: "Training", group: "Scheduled" },
160
+ ]
161
+ }
162
+ ```
163
+
164
+ Reason ids are non-empty and unique within the provider. They live on the snapshot rather than the
165
+ manifest because a provider may change the codes it offers during a shift; a `snapshot` event
166
+ replaces the list. `BreakRequest.reason` remains available for free text and is never a substitute
167
+ for `reasonId` when the provider publishes codes. A provider that defines no codes omits the
168
+ field.
169
+
170
+ #### One break across several providers
171
+
172
+ An agent connected to several providers takes **one** break, not one per platform. Omni gathers
173
+ what every connected provider offers, shows the distinct breaks, and on a choice sends one request
174
+ per provider: the same `reason`, and each provider's own `reasonId`.
175
+
176
+ To do that, Omni has to know when two providers mean the same break. Say so with `kind`:
177
+
178
+ ```ts
179
+ break: {
180
+ reasons: [
181
+ { id: "lunch", label: "Lunch", kind: "meal" },
182
+ { id: "rest", label: "Mandatory rest period", kind: "rest", alwaysAvailable: true },
183
+ ]
184
+ }
185
+ ```
186
+
187
+ A provider decides which breaks it offers and what it calls them. `BREAK_KINDS` is the list Omni
188
+ supports mapping them onto, and every member means something specific:
189
+
190
+ | Kind | What it means |
191
+ | --- | --- |
192
+ | `short-break` | A brief rest between contacts — the comfort break a shift plan allows for. |
193
+ | `meal` | A meal: lunch, dinner, whatever the shift calls it. |
194
+ | `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. |
195
+ | `training` | Learning something the agent is expected to know afterwards: a course, e-learning, a product walkthrough. However it is delivered, and whoever attends. |
196
+ | `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. |
197
+ | `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. |
198
+ | `administrative` | Paperwork and follow-up not attached to a particular contact. |
199
+ | `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. |
200
+ | `personal` | Personal time the deployment does not classify further. |
201
+ | `other` | None of the above. Matches nothing, including another provider's `other`. |
202
+
203
+ **The definitions are the point, not decoration.** Ten undefined strings would be the label
204
+ problem one level up: two providers could both declare `technical`, one meaning a dead headset
205
+ and the other scheduled maintenance, match on it, and nothing could tell the difference.
206
+
207
+ `training`, `coaching` and `meeting` can all fit one session, so take them in that order of
208
+ specificity: about this agent's own work is `coaching`, else something they must know afterwards is
209
+ `training`, else `meeting`.
210
+
211
+ **A break somebody forced on the agent is not automatically one of these.** Where the agent was
212
+ stopped rather than choosing to stop — see **Forced breaks** — set `BreakState.forced` and prefer
213
+ omitting `kind` to reaching for `other`. None of the ten describes "something was done to this
214
+ agent", and `other` claims a classification that was never made.
215
+
216
+ Omni matches in this order:
217
+
218
+ 1. **`kind`**, where both providers declare one. This wins over the label, so `{ id: "MEAL",
219
+ label: "Meal", kind: "meal" }` lines up with `{ id: "lunch", label: "Lunch", kind: "meal" }`
220
+ and nobody has to word their codes the same way.
221
+ 2. **The label**, folded for case and surrounding spacing, where a kind is missing. `"other"`
222
+ counts as missing: two providers saying `"other"` have only said their break is not on the
223
+ list, which is no evidence they mean the same thing.
224
+
225
+ Nothing else is matched. A break Omni cannot pair stays on its own, the agent is told how many
226
+ platforms their break will actually reach, and they can pair the odd one by hand — Omni remembers
227
+ that for next time.
228
+
229
+ Three rules for a provider:
230
+
231
+ - **Declare `kind` where you can, and leave it out where you cannot.** Leaving it out costs a
232
+ little precision. Getting it wrong sends an agent to lunch on one platform and to training on
233
+ another, and nothing can detect that.
234
+ - **You may publish several reasons of one kind** — two meal slots, say. Omni will not choose
235
+ between them for the agent; saying they are the same kind is not saying they are interchangeable.
236
+ - **Do not read `reason` as your own label.** It is the agent's single choice, in whichever
237
+ provider's wording they picked it from, sent unchanged to every provider in the break.
238
+
239
+ A provider that publishes no reason codes still receives the request, with `reason` set and
240
+ `reasonId` omitted.
241
+
242
+ #### Coordinating a multi-provider break
243
+
244
+ Sending the requests is a two-phase operation, not independent best effort. A provider first grants
245
+ permission for the agent to stop, then Omni either commits or cancels that permission.
246
+
247
+ `granted` is provider-visible state. It means only that this provider has granted the active
248
+ request and is ready to stop the agent when Omni asks. It does not reveal that other providers exist
249
+ or why Omni has not committed yet. While reporting `granted`, the provider remains available,
250
+ continues to honour Omni's current capacity, and continues offering work normally.
251
+
252
+ Omni separately tracks the aggregate agent application states `working`, `requesting-break`, `committing-break`,
253
+ `cancelling-break`, and `on-break`. While `requesting-break`, Omni shows which providers remain
254
+ outstanding and offers **Cancel break request**, but does not tell the agent that their break has
255
+ begun.
256
+
257
+ Omni offers the aggregate Break control only when every provider currently holding capacity
258
+ declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
259
+ knowingly permit partial availability.
260
+
261
+ Omni coordinates one break attempt as follows:
262
+
263
+ 1. Freeze the providers the attempt asks: every connected provider from which the agent can
264
+ currently receive work. A provider joining during the attempt is given no capacity until it
265
+ finishes. A provider whose authentication is `expired` is not one the agent can receive work
266
+ from and is not asked: nothing is asked of it, the break proceeds without it, and when the
267
+ login is restored it is reconciled from its snapshot as a set-aside provider is, with no
268
+ capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
269
+ available and work continues. `assertBreakAttemptProviders` holds an agent application to this set.
270
+ 2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
271
+ 3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
272
+ `granted`; neither state stops work. A denial transitions directly to `not-requested` and
273
+ causes Omni to take the cancel path.
274
+ 4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
275
+ and send `commitBreak()` to each of them. A provider then stops offering new work
276
+ and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
277
+ decision, tunable per deployment — decides who is kept: a provider that has not applied the
278
+ commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
279
+ begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
280
+ reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
281
+ and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
282
+ settled and beginning when the task ends, and offers no cancel, because the commit is durable.
283
+ 5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
284
+ decision timeout, or the agent cancels before commit, durably choose cancel and enter
285
+ `cancelling-break`. Send `cancelBreak()` to every provider still reporting
286
+ `awaiting-approval` or `granted`. Work continues during cancellation because no stop was
287
+ committed. Return to `working` only after no provider retains either state.
288
+
289
+ Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
290
+ it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
291
+ A provider that reports `granted` must therefore preserve the request across reconnects and must
292
+ honour a later commit or cancel. This
293
+ durable promise prevents a provider from failing the commit after another provider has already
294
+ stopped the agent.
295
+
296
+ #### Why the commit phase is bounded and the decision is not
297
+
298
+ Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
299
+ durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
300
+ or is uninstalled between granting and committing would hold Omni in `committing-break` with no
301
+ exit: the providers that did commit have already stopped the agent, and the agent is neither
302
+ working nor on a break.
303
+
304
+ Unanimity is required for a reason that survives the bound. It exists so the agent is not stopped
305
+ on one platform while another keeps routing work to them — and **a provider Omni cannot reach is
306
+ routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
307
+ for it costs the agent their break.
308
+
309
+ Setting a provider aside is not a rollback and not a cancel. The commit stands and the
310
+ obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
311
+ snapshot before anything else, and the snapshot decides:
312
+
313
+ - `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
314
+ - still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
315
+ late behind it, the provider stops an agent who is already stopped; nothing happens, because a
316
+ commit is a state to be in, not an act to be done.
317
+ - `not-requested` — the grant did not survive. Omni makes a new request for that provider alone,
318
+ against an agent who is already on break elsewhere. **A new login is this case too**: the grant
319
+ belonged to the old session.
320
+
321
+ The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
322
+ no capacity until it is reconciled.
323
+
324
+ **If the agent has already ended the break elsewhere, the break attempt is over** and the returning
325
+ provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
326
+ would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
327
+ Never rolling back is about a break that is still on, not one the agent has finished.
328
+
329
+ Omni may tell the agent which platforms the break has not yet reached, as it already does when a
330
+ break cannot be paired across every provider.
331
+
332
+ The decision phase needs no such bound, because nothing has stopped. Work continues throughout
333
+ `requesting-break`, so a provider that never answers costs the agent a wait rather than their
334
+ availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
335
+ because no stop was ever committed.
336
+
337
+ If cancel races with a late approval, the provider remains on the cancel path. If commit has already
338
+ won, cancel returns `omni.break-already-committed`, and Omni resumes commit recovery rather than
339
+ returning the agent to `working`.
340
+
341
+ A forced break is not rolled back by this algorithm. If one appears during either the request or
342
+ cancellation, the attempt is abandoned: Omni cancels every grant still outstanding and states
343
+ `setCapacity({ count: 0 })` on every other usable provider, as **Forced breaks** sets out; no grant
344
+ is committed, because the agent has already stopped everywhere.
345
+
346
+ This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
347
+ the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
348
+
349
+ #### Reporting the break the agent is on
350
+
351
+ `BreakState.activeReasonId` is the `BreakReason.id` the current break is on. Omni remembers what it
352
+ asked for, but only until the session ends — after a reload or reconnect, or where the provider put
353
+ the agent on the break itself, the provider is the only one who knows.
354
+
355
+ Omit it when you cannot say, and when there is no break: reporting a reason alongside
356
+ `status: "not-requested"` describes a break that is not happening, and is rejected.
357
+
358
+ ### `cancelBreak()`
359
+
360
+ Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
361
+ Cancellation releases the request but does not restore work because work never stopped. If
362
+ commit already won, the provider returns `omni.break-already-committed`. The resulting state is
363
+ reported through `break-state`.
364
+
365
+ ### `commitBreak()`
366
+
367
+ Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
368
+ business reason. On commit the provider stops offering new work and reports
369
+ `starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
370
+ Committing a break that is already in effect changes nothing and answers `committed`.
371
+
372
+ ### `endBreak()`
373
+
374
+ Tells a provider that an agent already on a break wants to become available again. The provider
375
+ reports the resulting state through `break-state` and provider status events.
376
+
377
+ It ends the break, which is the thing that started. Nothing about the connection was ever paused,
378
+ 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
 
@@ -292,6 +293,7 @@ type UserCapabilities = {
292
293
  breaks?: true;
293
294
  preferences?: AgentPreference[];
294
295
  lead?: true;
296
+ nextCall?: true;
295
297
  };
296
298
 
297
299
  type AuthenticationState =
@@ -518,6 +520,8 @@ type Snapshot = {
518
520
  calendar?: ScheduledActivity[];
519
521
  shift?: Shift;
520
522
  phone?: PhoneState;
523
+ nextCall?: NextCallRequest;
524
+ linedUp?: LinedUpCall;
521
525
  team?: TeamMembers;
522
526
  };
523
527
 
@@ -1010,6 +1014,30 @@ type BreakCancelResult =
1010
1014
  type BreakEndResult =
1011
1015
  | { status: "ended" }
1012
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 };
1013
1041
  ```
1014
1042
 
1015
1043
  ### Team
@@ -1027,6 +1055,8 @@ type TeamMember = {
1027
1055
  shift?: Shift;
1028
1056
  request?: MemberRequest;
1029
1057
  phone?: PhoneState;
1058
+ nextCall?: NextCallRequest;
1059
+ linedUp?: LinedUpCall;
1030
1060
  };
1031
1061
 
1032
1062
  type MemberRequest = {
@@ -1184,7 +1214,9 @@ type ProviderEvent =
1184
1214
  | { type: "contacts-updated"; contacts: Contact[] }
1185
1215
  | { type: "calendar-updated"; calendar: ScheduledActivity[] }
1186
1216
  | { type: "shift-updated"; shift: Shift }
1187
- | { type: "phone-updated"; phone: PhoneState };
1217
+ | { type: "phone-updated"; phone: PhoneState }
1218
+ | { type: "next-call"; nextCall?: NextCallRequest }
1219
+ | { type: "lined-up"; linedUp?: LinedUpCall };
1188
1220
 
1189
1221
  type ProviderEventEnvelope = {
1190
1222
  id: string;
@@ -1237,6 +1269,9 @@ type Connection<C extends Channel = Channel> = {
1237
1269
  commitBreak?(): Promise<BreakCommitResult>;
1238
1270
  cancelBreak?(): Promise<BreakCancelResult>;
1239
1271
  endBreak?(): Promise<BreakEndResult>;
1272
+ requestNextCall?(): Promise<NextCallResult>;
1273
+ cancelNextCall?(): Promise<NextCallCancelResult>;
1274
+ releaseLinedUp?(): Promise<LinedUpReleaseResult>;
1240
1275
 
1241
1276
  executeTeam?(request: TeamCommandRequest): Promise<TeamCommandResult>;
1242
1277
  openAudio?(request: OpenAudioRequest): Promise<OpenAudioResult>;
@@ -1990,6 +2025,7 @@ to infer them from what arrives later.
1990
2025
  | Field | Contract |
1991
2026
  | --- | --- |
1992
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`. |
1993
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`. |
1994
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**. |
1995
2031
 
@@ -2207,6 +2243,8 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
2207
2243
  | `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
2208
2244
  | `calendar` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
2209
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`. |
2210
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**. |
2211
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. |
2212
2250
 
@@ -2231,6 +2269,7 @@ surface in one place, and what obliges an adapter to implement each one.
2231
2269
  | `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
2232
2270
  | `cancelBreak()` | The login declares `capabilities.breaks`. |
2233
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`. |
2234
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`. |
2235
2274
  | `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
2236
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`. |
@@ -2767,6 +2806,19 @@ the `contacts` idle capability.
2767
2806
  Replaces this provider's complete scheduled-activity contribution. It is emitted only when the manifest
2768
2807
  declares the `calendar` idle capability.
2769
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
+
2770
2822
  ### `phone-updated`
2771
2823
 
2772
2824
  The phone as the platform now sees it, whole, replacing `Snapshot.phone`: a registration lost or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.108",
3
+ "version": "0.1.110",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",