@xema/omni-protocol 0.1.102 → 0.1.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -153,6 +153,12 @@ export interface Manifest<C extends Channel = Channel> {
153
153
  authenticationMethods: AuthenticationMethod[];
154
154
  idleCapabilities?: IdleCapabilities<C>;
155
155
  /** Optional provider-clock sampling through Connection.checkTime; absent means unsupported. */
156
+ /**
157
+ * Implements `checkTime`, and states `providerTime` on every snapshot. Required of a provider
158
+ * that publishes an instant the desk renders as a running duration -- a member's `since`, a
159
+ * lead's `listening.since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts
160
+ * from the provider's clock and two screens differ by their delay alone.
161
+ */
156
162
  timeCheck?: true;
157
163
  /** Provider uses its own timestamps for final records; host timestamps are advisory. Omission makes no trust promise. */
158
164
  timestampAuthority?: "provider";
@@ -179,12 +185,12 @@ export interface Manifest<C extends Channel = Channel> {
179
185
  */
180
186
  runningStepReports?: true;
181
187
  /**
182
- * How long after an applied completion -- `complete`, or a lead's `take-over-call` -- the provider's
183
- * `task-ended` is owed, in milliseconds. `applied` says the provider has completed the task;
184
- * the ending follows within this, or the host resyncs and shows the task as unsettled. Stated
185
- * per provider, since platforms settle at different speeds.
188
+ * How long after `applied` the wire shows what the provider did, in milliseconds: the
189
+ * `task-ended` after a `complete` or a lead's `take-over-call`, the `calendar-updated` after a
190
+ * `schedule`. Past it the host resyncs and shows the thing as unsettled. Stated per provider,
191
+ * since platforms settle at different speeds.
186
192
  */
187
- completionSettleMs: number;
193
+ settleMs: number;
188
194
  }
189
195
  export interface SecretStore {
190
196
  get(key: string): Promise<string | undefined>;
@@ -666,8 +672,10 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
666
672
  } : C extends "chat" ? SharedTaskCapabilities & {
667
673
  decline?: Lockable<true>;
668
674
  hold?: Lockable<true>;
675
+ schedule?: Lockable<true>;
669
676
  } : SharedTaskCapabilities & {
670
677
  decline?: Lockable<true>;
678
+ schedule?: Lockable<true>;
671
679
  };
672
680
  /**
673
681
  * How a reusing browser's session is keyed.
@@ -948,6 +956,13 @@ export type Task<C extends Channel = Channel> = {
948
956
  * Omni said not, and absent past `pending`.
949
957
  */
950
958
  acceptance?: AcceptanceMode;
959
+ /**
960
+ * In `pending` only: how long the offer has left, in whole seconds from this publication,
961
+ * restated on every publication that carries it so a reconnect snapshot says what is actually
962
+ * left. Absent, the offer stands until the provider says otherwise. Runs out: the provider ends
963
+ * the offer `expired` naming `pending`.
964
+ */
965
+ expiresInSeconds?: DurationSeconds;
951
966
  /**
952
967
  * In `preview` only, and together: how long the preparation has left, in whole seconds from
953
968
  * this publication -- restated on every publication that carries it, so a resync starts the
@@ -999,18 +1014,18 @@ export type TaskOutcome = {
999
1014
  by: "agent" | "provider" | "party";
1000
1015
  reason?: string;
1001
1016
  }
1002
- /** Only the phases in which somebody is still being waited on can expire; an offer whose `expiresInSeconds` runs out names `pending`. */
1017
+ /** Only the phases in which somebody is still being waited on can expire: an offer whose `expiresInSeconds` runs out names `pending`, accepted work the platform gave up on names `confirmed`. A preview never lapses: withdrawn, it is `cancelled` by the provider. */
1003
1018
  | {
1004
1019
  type: "expired";
1005
- phase: "pending" | "confirmed" | "preview";
1020
+ phase: "pending" | "confirmed";
1006
1021
  } | {
1007
1022
  type: "failed";
1008
1023
  failure: ProtocolFailure;
1009
1024
  };
1010
1025
  export declare const TASK_COMMAND_NAMES: {
1011
1026
  readonly voice: readonly ["answer", "decline", "dial", "hold", "resume", "end-call", "terminate-call", "connect-back", "lead-assist", "conference", "recording", "schedule", "complete"];
1012
- readonly chat: readonly ["accept", "decline", "pause", "resume", "complete"];
1013
- readonly email: readonly ["accept", "decline", "complete"];
1027
+ readonly chat: readonly ["accept", "decline", "pause", "resume", "schedule", "complete"];
1028
+ readonly email: readonly ["accept", "decline", "schedule", "complete"];
1014
1029
  };
1015
1030
  export type TaskCommandName<C extends keyof typeof TASK_COMMAND_NAMES> = (typeof TASK_COMMAND_NAMES)[C][number];
1016
1031
  export interface OutcomePayload {
@@ -1043,17 +1058,7 @@ export type VoiceTaskCommand = {
1043
1058
  | {
1044
1059
  type: "connect-back";
1045
1060
  dialId: DialId;
1046
- }
1047
- /**
1048
- * Put a time on the calendar for this party: a follow-up promised on the call. Gated by
1049
- * `schedule`, issuable in `in-progress`, `paused` or `completing`; the activity arrives on
1050
- * `calendar-updated`, which is the provider's word that it stands.
1051
- */
1052
- | {
1053
- type: "schedule";
1054
- at: IsoTimestamp;
1055
- note?: string;
1056
- }
1061
+ } | ScheduleCommand
1057
1062
  /** Ask a lead to join, with a note. Gated by `leadAssist`. */
1058
1063
  | {
1059
1064
  type: "lead-assist";
@@ -1093,6 +1098,18 @@ export type VoiceTaskCommand = {
1093
1098
  }) | ({
1094
1099
  type: "complete";
1095
1100
  } & OutcomePayload);
1101
+ /**
1102
+ * Put a time on the calendar for this party: a follow-up promised on the call or in the
1103
+ * conversation, or written up in wrap. Gated by `schedule` on any channel, issuable in
1104
+ * `in-progress`, `paused` or `completing`, under a manifest that declares `calendar` for it to
1105
+ * land on; `applied` says it is on the calendar, and the `calendar-updated` that shows it follows
1106
+ * within the manifest's `settleMs`.
1107
+ */
1108
+ export type ScheduleCommand = {
1109
+ type: "schedule";
1110
+ at: IsoTimestamp;
1111
+ note?: string;
1112
+ };
1096
1113
  export type ChatTaskCommand = {
1097
1114
  type: "accept";
1098
1115
  } | {
@@ -1101,14 +1118,14 @@ export type ChatTaskCommand = {
1101
1118
  type: "pause";
1102
1119
  } | {
1103
1120
  type: "resume";
1104
- } | ({
1121
+ } | ScheduleCommand | ({
1105
1122
  type: "complete";
1106
1123
  } & OutcomePayload);
1107
1124
  export type EmailTaskCommand = {
1108
1125
  type: "accept";
1109
1126
  } | {
1110
1127
  type: "decline";
1111
- } | ({
1128
+ } | ScheduleCommand | ({
1112
1129
  type: "complete";
1113
1130
  } & OutcomePayload);
1114
1131
  export interface CustomTaskCommand {
@@ -1187,14 +1204,21 @@ export interface BreakRequest {
1187
1204
  reasonId?: string;
1188
1205
  }
1189
1206
  /**
1190
- * A break forced on the agent; the agent must explicitly resume when ready. `by` is the lead who
1191
- * forced it, or `provider` where the platform itself did, on its own rule or a schedule: the agent
1192
- * sees who, and a person is named only when there is one.
1207
+ * A break forced on the agent; the agent must explicitly resume when ready. A lead's `force-break`
1208
+ * is a command to the provider, which forces the break: `by` names the lead who asked it to, or
1209
+ * `provider` where the platform itself did, on its own rule or a schedule. The agent sees who, a
1210
+ * person is named only when there is one, and a break the platform imposed is the platform's to
1211
+ * lift: a lead's `end-forced-break` on it fails with `omni.break-forced-by-provider`.
1193
1212
  */
1194
1213
  export type ForcedBreak = {
1195
1214
  by: UserId | "provider";
1196
- /** Optional expected time on break, in milliseconds from actual start. Advisory only. */
1197
- expectedDurationMs?: number;
1215
+ /**
1216
+ * How long the break is expected to run yet, in whole seconds from this publication, restated
1217
+ * on every publication that carries it so every screen counts down the same number from
1218
+ * receipt. Advisory only: nothing ends the break but the agent. Absent once it has run out, or
1219
+ * where none was expected.
1220
+ */
1221
+ expectedEndsInSeconds?: DurationSeconds;
1198
1222
  };
1199
1223
  export interface BreakState {
1200
1224
  status: BreakStatus;
@@ -1251,10 +1275,10 @@ export interface TeamMember {
1251
1275
  break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
1252
1276
  /** The member's open tasks, trimmed, kept current by the provider; `[]` when they hold none, omitted only where the provider cannot see them. */
1253
1277
  tasks?: MemberTask[];
1254
- /** Present while this lead is on the member's call, listening, coaching or joined; absent otherwise. */
1255
- listening?: MemberListening;
1256
- /** The member's own history for the day: sign-in, sign-out, breaks and productivity. Omitted where the provider cannot say. */
1257
- shift?: MemberShift;
1278
+ /** Every lead on this member's call, listening, coaching or joined, published to every lead alike; absent while none is. */
1279
+ listening?: MemberListening[];
1280
+ /** The member's own history for the day: sign-in, sign-out, breaks and productivity, the same numbers the member's own snapshot carries. Omitted where the provider cannot say. */
1281
+ shift?: Shift;
1258
1282
  /** The member's standing ask for a lead on one of their calls; absent when they are not asking. */
1259
1283
  request?: MemberRequest;
1260
1284
  }
@@ -1297,11 +1321,14 @@ export type MemberTask<C extends Channel = Channel> = {
1297
1321
  audio?: never;
1298
1322
  });
1299
1323
  /**
1300
- * The lead on this member's call: which of the member's calls, in the mode they are heard, since
1301
- * when. Its appearance on the team member list is the provider's word that the lead's audio is up,
1302
- * and the application opens `openAudio` on the assignment named; its disappearance closes it.
1324
+ * A lead on this member's call: who, which of the member's calls, in the mode they are heard,
1325
+ * since when. Every lead sees every entry; the application opens `openAudio` on the entry whose
1326
+ * `leadId` is the signed-in lead, and closes it when that entry goes. A lead who is heard --
1327
+ * `join-call` -- is also on the task's `onCall` as an `agent`; `listen` and `coach` leave no trace
1328
+ * on the member's task.
1303
1329
  */
1304
1330
  export interface MemberListening {
1331
+ leadId: UserId;
1305
1332
  assignmentId: AssignmentId;
1306
1333
  mode: ListeningMode;
1307
1334
  since: IsoTimestamp;
@@ -1313,15 +1340,18 @@ export interface ShiftEvent {
1313
1340
  kind: ShiftEventKind;
1314
1341
  }
1315
1342
  /**
1316
- * The member's own history for the day, about the person rather than any one call: when they
1343
+ * An agent's own history for the day, about the person rather than any one call: when they
1317
1344
  * signed in and out, today's totals as the provider counts them, each present only when the
1318
- * provider knows it, and the day's sign-in, sign-out and break events, oldest first.
1345
+ * provider knows it, and the day's sign-in, sign-out and break events, oldest first. Published
1346
+ * to the agent on their snapshot and on `shift-updated`, and to their lead on the team member
1347
+ * list: the same numbers from the same count.
1319
1348
  */
1320
- export interface MemberShift {
1349
+ export interface Shift {
1321
1350
  signedInAt: IsoTimestamp;
1322
1351
  signedOutAt?: IsoTimestamp;
1323
1352
  talkSeconds?: DurationSeconds;
1324
1353
  holdSeconds?: DurationSeconds;
1354
+ /** Every break the member was on today, including time held elsewhere for a break another provider forced: the provider counts it under a break kind of its own. */
1325
1355
  breakSeconds?: DurationSeconds;
1326
1356
  tasksHandled?: number;
1327
1357
  events?: ShiftEvent[];
@@ -1536,6 +1566,12 @@ export type PreferenceResult = {
1536
1566
  export interface Snapshot<C extends Channel = Channel> {
1537
1567
  transport: TransportStatus;
1538
1568
  loginId: string;
1569
+ /**
1570
+ * The provider's own instant of this read, required of a provider that declares `timeCheck`:
1571
+ * every duration the desk renders off the active call is counted from the provider's clock, and
1572
+ * this sample saves a `checkTime` for it. Every envelope's `occurredAt` is a sample too.
1573
+ */
1574
+ providerTime?: IsoTimestamp;
1539
1575
  break: BreakState;
1540
1576
  /** Every task currently owned by this agent for this provider. */
1541
1577
  tasks: Task<C>[];
@@ -1543,6 +1579,8 @@ export interface Snapshot<C extends Channel = Channel> {
1543
1579
  taskCount: number;
1544
1580
  contacts?: Contact[];
1545
1581
  calendar?: ScheduledActivity[];
1582
+ /** The agent's own day so far, as the provider counts it; replaced whole by `shift-updated`. Omitted where the provider cannot say. */
1583
+ shift?: Shift;
1546
1584
  team?: TeamMembers;
1547
1585
  }
1548
1586
  /**
@@ -1590,8 +1628,6 @@ export type ProviderEvent<C extends Channel = Channel> = {
1590
1628
  } | {
1591
1629
  type: "task-offered";
1592
1630
  task: Task<C>;
1593
- /** How long the agent has to answer, in whole seconds from this event; the offer expires when it runs out. Absent, it stands until the provider says otherwise. */
1594
- expiresInSeconds?: DurationSeconds;
1595
1631
  } | {
1596
1632
  type: "task-updated";
1597
1633
  task: Task<C>;
@@ -1656,6 +1692,11 @@ export type ProviderEvent<C extends Channel = Channel> = {
1656
1692
  } | {
1657
1693
  type: "calendar-updated";
1658
1694
  calendar: ScheduledActivity[];
1695
+ }
1696
+ /** The agent's own day, whole, whenever a total moves: a task ended, a break ended. */
1697
+ | {
1698
+ type: "shift-updated";
1699
+ shift: Shift;
1659
1700
  };
1660
1701
  /**
1661
1702
  * The `Provider` prefix survives here for a mechanical reason rather than a naming one: `Event`
@@ -1669,7 +1710,7 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
1669
1710
  occurredAt: IsoTimestamp;
1670
1711
  event: ProviderEvent<C>;
1671
1712
  }
1672
- export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.assignment-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed", "omni.recording-unsettled"];
1713
+ export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.assignment-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed", "omni.recording-unsettled", "omni.break-forced-by-provider"];
1673
1714
  export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
1674
1715
  /**
1675
1716
  * What the host refused, told to the adapter that published it: a snapshot the host would not
package/dist/index.js CHANGED
@@ -78,8 +78,8 @@ export const CAPABILITY_SOURCES = ["queue", "nobody", "not-yet-read"];
78
78
  export const TASK_COMMAND_NAMES = {
79
79
  voice: ["answer", "decline", "dial", "hold", "resume", "end-call", "terminate-call",
80
80
  "connect-back", "lead-assist", "conference", "recording", "schedule", "complete"],
81
- chat: ["accept", "decline", "pause", "resume", "complete"],
82
- email: ["accept", "decline", "complete"],
81
+ chat: ["accept", "decline", "pause", "resume", "schedule", "complete"],
82
+ email: ["accept", "decline", "schedule", "complete"],
83
83
  };
84
84
  /** The dial a command places, by the host's identity for it; `undefined` for a command that dials nothing. */
85
85
  export function commandDialId(command) {
@@ -105,6 +105,8 @@ export const OMNI_FAILURE_CODES = [
105
105
  "omni.break-already-committed",
106
106
  /** A recording command the provider could not settle either way: no start, stop, pause or resume it can vouch for. */
107
107
  "omni.recording-unsettled",
108
+ /** A lead asked to lift a break the platform itself imposed; the platform lifts its own. */
109
+ "omni.break-forced-by-provider",
108
110
  ];
109
111
  /**
110
112
  * Preserves the adapter's inferred concrete type while checking it implements `Adapter`.
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", "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", "shift", "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[];
@@ -249,6 +249,13 @@ export interface BreakCandidate {
249
249
  /** Whether the agent can currently receive work from it: connected, with a capacity stated. */
250
250
  holdsCapacity: boolean;
251
251
  }
252
+ /**
253
+ * A break one provider forces stops the agent everywhere: the host states a capacity of zero on
254
+ * every other usable provider holding capacity -- taken, never refused, and nothing the agent can
255
+ * cancel -- and on the forcing provider states nothing, since the break itself holds the agent
256
+ * there. A provider whose login is not usable is left alone, as a break attempt leaves it.
257
+ */
258
+ export declare function assertForcedBreakStopsTheRest(forcedOn: string, candidates: readonly BreakCandidate[], stopped: readonly string[]): void;
252
259
  /**
253
260
  * A break attempt asks every connected provider from which the agent can currently receive work.
254
261
  * A provider whose login is not usable -- `expired` above all -- is not asked, whatever else is
package/dist/testing.js CHANGED
@@ -28,6 +28,7 @@ const STATE_SUBJECTS = [
28
28
  "team.tasks",
29
29
  "team.listening",
30
30
  "team.shift",
31
+ "shift",
31
32
  "contacts",
32
33
  "calendar",
33
34
  "team.policies",
@@ -38,7 +39,7 @@ const EVENT_TYPES = {
38
39
  snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
39
40
  "task-audio-started": true, "task-audio-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
40
41
  "team-updated": true, "team-member-updated": true, "team-member-removed": true, "team-policies-updated": true,
41
- "contacts-updated": true, "calendar-updated": true,
42
+ "contacts-updated": true, "calendar-updated": true, "shift-updated": true,
42
43
  };
43
44
  const CONTRACT_SUBJECTS = [
44
45
  ...STATE_SUBJECTS,
@@ -94,7 +95,7 @@ function observeMember(member, seen) {
94
95
  return;
95
96
  if (some(member.tasks))
96
97
  seen.add("team.tasks");
97
- if (member.listening !== undefined)
98
+ if (some(member.listening))
98
99
  seen.add("team.listening");
99
100
  if (member.shift !== undefined)
100
101
  seen.add("team.shift");
@@ -126,6 +127,8 @@ function observeSnapshot(value, seen) {
126
127
  seen.add("contacts");
127
128
  if (some(value.calendar))
128
129
  seen.add("calendar");
130
+ if (value.shift !== undefined)
131
+ seen.add("shift");
129
132
  }
130
133
  function observeEvent(envelope, seen) {
131
134
  const event = isRecord(envelope) ? envelope.event : undefined;
@@ -162,6 +165,9 @@ function observeEvent(envelope, seen) {
162
165
  if (some(event.calendar))
163
166
  seen.add("calendar");
164
167
  break;
168
+ case "shift-updated":
169
+ seen.add("shift");
170
+ break;
165
171
  default: break;
166
172
  }
167
173
  }
@@ -719,9 +725,14 @@ function userIdsIn(snapshot) {
719
725
  add(snapshot?.break?.forced?.by);
720
726
  const team = snapshot?.team;
721
727
  if (isRecord(team)) {
722
- for (const member of Array.isArray(team.members) ? team.members : [])
723
- if (isRecord(member))
724
- add(member.id);
728
+ for (const member of Array.isArray(team.members) ? team.members : []) {
729
+ if (!isRecord(member))
730
+ continue;
731
+ add(member.id);
732
+ for (const entry of Array.isArray(member.listening) ? member.listening : [])
733
+ if (isRecord(entry))
734
+ add(entry.leadId);
735
+ }
725
736
  }
726
737
  for (const task of Array.isArray(snapshot?.tasks) ? snapshot.tasks : []) {
727
738
  const t = task;
@@ -1063,7 +1074,7 @@ const REACHABLE_PHASES = {
1063
1074
  export const SUPERSEDED_BY_A_SNAPSHOT = new Set([
1064
1075
  "snapshot", "transport-status", "break-state", "task-offered", "task-updated", "task-ended",
1065
1076
  "task-audio-started", "task-audio-ended", "team-updated", "team-member-updated", "team-member-removed", "team-policies-updated",
1066
- "contacts-updated", "calendar-updated",
1077
+ "contacts-updated", "calendar-updated", "shift-updated",
1067
1078
  ]);
1068
1079
  export class TaskStream {
1069
1080
  tasks = new Map();
@@ -1626,6 +1637,7 @@ async function driveOneCall(drive) {
1626
1637
  const own = validateTaskCommand(command, latestTask(), `drive.command.${String(command.type)}`, {
1627
1638
  levels: effectiveLevels(drive.manifest.orgLevels).map(level => level.id),
1628
1639
  dialOutcomesDeclared: drive.manifest.dialOutcomes !== undefined,
1640
+ calendarDeclared: drive.manifest.idleCapabilities?.calendar === true,
1629
1641
  autoAcceptTasks: drive.context.autoAcceptTasks,
1630
1642
  });
1631
1643
  found.push(...own);
@@ -1798,6 +1810,38 @@ async function driveOneCall(drive) {
1798
1810
  }
1799
1811
  }
1800
1812
  }
1813
+ // 4a. Schedule a follow-up, where the task offers it: applied says it is on the calendar, and the
1814
+ // calendar-updated that shows it is owed within the manifest's bound. The time is the provider's
1815
+ // own: a day after the instant it published the offer, so no clock of the runner's is read.
1816
+ if (latestTask().phase === "in-progress" && offers("schedule")) {
1817
+ const publishedAt = drive.events.slice(0, cursor).reverse().find(envelope => {
1818
+ const event = envelope.event;
1819
+ return (event.type === "task-offered" || event.type === "task-updated") && isTask(event.task) && event.task.assignmentId === taskId;
1820
+ })?.occurredAt;
1821
+ const at = new Date(Date.parse(String(publishedAt ?? "2026-01-01T00:00:00Z")) + 24 * 60 * 60 * 1000).toISOString();
1822
+ if (await send({ type: "schedule", at, note: "Follow-up promised on the call" }) !== undefined) {
1823
+ ruleEvaluated("drive.schedule.unsettled");
1824
+ const settle = Number(drive.manifest.settleMs);
1825
+ const carries = (calendar) => Array.isArray(calendar) && calendar.some(activity => isRecord(activity) && activity.startsAt === at);
1826
+ let unsettled = false;
1827
+ const shown = await waitFor("a calendar-updated carrying the follow-up", envelope => {
1828
+ const event = envelope.event;
1829
+ return event.type === "calendar-updated" && carries(event.calendar) ? event : undefined;
1830
+ }, cursor, { ms: settle, onExpiry: () => { unsettled = true; } });
1831
+ if (shown === undefined && unsettled) {
1832
+ let resync;
1833
+ try {
1834
+ resync = await drive.connection.snapshot();
1835
+ }
1836
+ catch (error) {
1837
+ refuse("drive.command.rejected", "drive.schedule", `snapshot() after an unsettled schedule rejected: ${String(error)}`);
1838
+ }
1839
+ if (!(isRecord(resync) && carries(resync.calendar))) {
1840
+ refuse("drive.schedule.unsettled", "drive.schedule", `schedule was applied and ${settle}ms later no calendar-updated carries a follow-up at ${at}, and a snapshot does not either: applied says the follow-up is on the calendar, and it is not`);
1841
+ }
1842
+ }
1843
+ }
1844
+ }
1801
1845
  // 5. End the call, where the agent may -- with the microphone muted, as agents do. The leg left
1802
1846
  // open at audio end is the provider's to close, since the provider knows the instant the audio
1803
1847
  // ended; the host's own closing report can only follow what it hears, is answered recorded, and
@@ -1853,7 +1897,7 @@ async function driveOneCall(drive) {
1853
1897
  // provider. A conversation has no audio to end and no completing phase to wait for: a chat or an
1854
1898
  // email, and a voice task offering no end-call, is completed from where it stands.
1855
1899
  const completable = latestTask().phase === "completing" || latestTask().phase === "in-progress" || latestTask().phase === "paused";
1856
- if (latestTask().wrapAllowance !== 0 && completable) {
1900
+ if (!(latestTask().completionMode === "provider-automatic" && latestTask().wrapAllowance === 0) && completable) {
1857
1901
  const command = { type: "complete" };
1858
1902
  const outcomes = isRecord(latestTask().capabilities) ? latestTask().capabilities.outcomes : undefined;
1859
1903
  if (isRecord(outcomes) && outcomes.required === true && Array.isArray(outcomes.codes) && isRecord(outcomes.codes[0])) {
@@ -1864,7 +1908,7 @@ async function driveOneCall(drive) {
1864
1908
  // the manifest stated. Past it the host resyncs: a snapshot still carrying the task is a task
1865
1909
  // held open by a provider that said it was done, and the desk shows it as unsettled.
1866
1910
  ruleEvaluated("drive.completion.unsettled");
1867
- const settle = Number(drive.manifest.completionSettleMs);
1911
+ const settle = Number(drive.manifest.settleMs);
1868
1912
  let unsettled = false;
1869
1913
  const end = await ended({ ms: settle, onExpiry: () => { unsettled = true; } });
1870
1914
  if (end !== undefined)
@@ -2083,6 +2127,35 @@ export function stillHost(report = { online: true }, guarantees = {}, mute) {
2083
2127
  return { guarantees, ...(mute === undefined ? {} : { mute }), report: () => report, subscribe: () => () => undefined };
2084
2128
  }
2085
2129
  const usableLogin = (status) => status === "authenticated" || status === "refreshing";
2130
+ /**
2131
+ * A break one provider forces stops the agent everywhere: the host states a capacity of zero on
2132
+ * every other usable provider holding capacity -- taken, never refused, and nothing the agent can
2133
+ * cancel -- and on the forcing provider states nothing, since the break itself holds the agent
2134
+ * there. A provider whose login is not usable is left alone, as a break attempt leaves it.
2135
+ */
2136
+ export function assertForcedBreakStopsTheRest(forcedOn, candidates, stopped) {
2137
+ if (!candidates.some(candidate => candidate.id === forcedOn))
2138
+ throw new Error(`${forcedOn} is not a provider the host knows`);
2139
+ const chosen = new Set(stopped);
2140
+ for (const id of stopped) {
2141
+ if (!candidates.some(candidate => candidate.id === id))
2142
+ throw new Error(`${id} is not a provider the host knows`);
2143
+ }
2144
+ if (chosen.has(forcedOn))
2145
+ throw new Error(`${forcedOn} forced the break and holds the agent itself: its capacity is not restated`);
2146
+ for (const candidate of candidates) {
2147
+ if (candidate.id === forcedOn)
2148
+ continue;
2149
+ const expected = usableLogin(candidate.authentication) && candidate.holdsCapacity;
2150
+ if (expected && !chosen.has(candidate.id)) {
2151
+ throw new Error(`${candidate.id} can give the agent work and must be stopped: capacity 0 stated`);
2152
+ }
2153
+ if (!expected && chosen.has(candidate.id)) {
2154
+ const why = usableLogin(candidate.authentication) ? "holds no capacity" : `is ${candidate.authentication}`;
2155
+ throw new Error(`${candidate.id} ${why} and is not stopped: there is nothing to stop`);
2156
+ }
2157
+ }
2158
+ }
2086
2159
  /**
2087
2160
  * A break attempt asks every connected provider from which the agent can currently receive work.
2088
2161
  * A provider whose login is not usable -- `expired` above all -- is not asked, whatever else is
@@ -39,6 +39,8 @@ export interface TaskValidationContext {
39
39
  autoAcceptTasks?: boolean;
40
40
  /** Whether the manifest declares `dialOutcomes`. A task that may dial needs it to; absent, the question is not asked. */
41
41
  dialOutcomesDeclared?: boolean;
42
+ /** Whether the manifest declares the `calendar` idle capability. A task that may schedule needs it to; absent, the question is not asked. */
43
+ calendarDeclared?: boolean;
42
44
  /**
43
45
  * The values the queue locked on this login's tasks -- a party's number or email -- as whoever
44
46
  * runs the validator knows them. Where a task's party stands locked, no other field of it may