@xema/omni-protocol 0.1.116 → 0.1.118

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
@@ -246,7 +246,8 @@ export interface SecretStore {
246
246
  * A small store for one login's operational state -- what an adapter holds about open work that
247
247
  * its platform cannot hold for it, such as the interaction legs a host reported. Kept by the host for
248
248
  * the life of the login, across a reload of the host, and cleared at sign-out. Never for anything
249
- * sensitive: that is `SecretStore`, whose contract is that a host may clear it aggressively.
249
+ * sensitive: that is `SecretStore`, whose contract is that a host may clear it aggressively, and
250
+ * clears it once the session has ended.
250
251
  */
251
252
  export interface LoginStore {
252
253
  get(key: string): Promise<string | undefined>;
@@ -318,6 +319,13 @@ export interface UserCapabilities {
318
319
  * Only `authenticated` and `refreshing` know who the agent is and what they may do, and only
319
320
  * `authenticated` has something to expire. A state carrying more than it knows is a state Omni
320
321
  * would render as fact.
322
+ *
323
+ * `signed-out` is the agent's own Sign out on this agent computer, and nothing else. `terminated`
324
+ * is a session ended from outside it -- a supervisor, the agent on another screen, the platform
325
+ * itself -- and says who: a person by id, or `provider` where no person was behind it, with the
326
+ * reason as a failure that is never retryable. `expired` is a session whose time ran out or whose
327
+ * refresh is failing. The stored secrets go with every ending: signed out, terminated, and expired
328
+ * unless its failure says `retryable: true`.
321
329
  */
322
330
  export type AuthenticationState = {
323
331
  status: "signed-out";
@@ -336,6 +344,13 @@ export type AuthenticationState = {
336
344
  status: "expired";
337
345
  identity?: User;
338
346
  failure?: AuthenticationFailure;
347
+ } | {
348
+ status: "terminated";
349
+ by: UserId | "provider";
350
+ identity?: User;
351
+ failure: AuthenticationFailure & {
352
+ retryable: false;
353
+ };
339
354
  };
340
355
  export interface CredentialField {
341
356
  name: string;
@@ -401,6 +416,11 @@ export interface AuthenticationSession {
401
416
  complete(request: CompleteAuthenticationRequest): Promise<CompleteAuthenticationResult>;
402
417
  /** Cancels an abandoned SSO window or credentials form. */
403
418
  cancelAuthentication(flowId: string): Promise<AuthenticationActionResult>;
419
+ /**
420
+ * The agent's own Sign out. Tells the provider where it can, deletes the stored secrets whatever
421
+ * happened, and moves to `signed-out`; answers `failed` when the provider could not be told, so
422
+ * the host can show that a session may remain there. Never called for a `terminated` session.
423
+ */
404
424
  signOut(): Promise<AuthenticationActionResult>;
405
425
  close(): Promise<void>;
406
426
  }
@@ -493,40 +513,61 @@ export interface TaskRecordingPolicy {
493
513
  storageId: string;
494
514
  };
495
515
  }
496
- /** Current evidence only. Expiry/disconnect means unknown, never a historical stop. */
497
- export type RecordingState = {
498
- status: "unknown";
499
- observationId?: never;
500
- observedAt?: never;
501
- validUntil?: never;
502
- recordingId?: never;
503
- } | ({
504
- observationId: string;
505
- observedAt: IsoTimestamp;
506
- validUntil: IsoTimestamp;
516
+ /** Whose side of the call a recording captures: the party's channel, or the agent's own. */
517
+ export type RecordingSide = "party" | "agent";
518
+ export declare const RECORDING_SIDES: readonly ["party", "agent"];
519
+ /**
520
+ * Where a recording stands, as its recorder last stated it. `starting` is asked for and not yet
521
+ * confirmed. A stop or a pause in flight stays `recording` until the recorder confirms it, since
522
+ * telling the agent they are not recorded when they may be is the one thing this must never say.
523
+ */
524
+ export type RecordingStatus = "starting" | "recording" | "paused" | "not-recording" | "unknown";
525
+ export declare const RECORDING_STATUSES: readonly ["starting", "recording", "paused", "not-recording", "unknown"];
526
+ /** Why a recording is paused: someone asked, or the platform paused it while sensitive details are taken. */
527
+ export type RecordingPausedReason = "requested" | "sensitive-details";
528
+ export declare const RECORDING_PAUSED_REASONS: readonly ["requested", "sensitive-details"];
529
+ /** Why a recording's state is unknown: its recorder cannot be reached, or a command could not be settled either way. */
530
+ export type RecordingUnknownReason = "recorder-unreachable" | "unsettled";
531
+ export declare const RECORDING_UNKNOWN_REASONS: readonly ["recorder-unreachable", "unsettled"];
532
+ /**
533
+ * One recording of one side of the call, published whenever it changes and restated whole with
534
+ * every publication of the task and on every snapshot. Nothing expires: the mark stands as last
535
+ * stated until its recorder says otherwise. `id` is the recorder's, minted when the start is
536
+ * accepted -- before the recorder has answered -- and kept through pause and resume; a later
537
+ * start is a new recording with a new id.
538
+ */
539
+ export type Recording = {
540
+ id: string;
541
+ follows: RecordingSide;
507
542
  } & ({
508
- status: "inactive";
509
- recordingId?: never;
543
+ status: "starting" | "recording" | "not-recording";
544
+ reason?: never;
510
545
  } | {
511
- status: "active" | "paused";
512
- recordingId: string;
513
- }));
514
- /** Opaque IDs are scoped by login, task and recorder owner; preserved across pause/resume. */
546
+ status: "paused";
547
+ reason: RecordingPausedReason;
548
+ } | {
549
+ status: "unknown";
550
+ reason: RecordingUnknownReason;
551
+ });
552
+ /**
553
+ * A start names the side to record; every other action names the recording by its id, and the
554
+ * recorder refuses one that no longer stands where the action needs it.
555
+ */
515
556
  export type RecordingCommand = {
516
557
  type: "recording";
517
558
  source: RecordingSource;
518
- /** Compare-and-set against current evidence; never act on a replacement recording. */
519
- observationId: string;
520
559
  } & ({
521
560
  action: "start";
561
+ follows: RecordingSide;
522
562
  recordingId?: never;
523
563
  } | {
524
564
  action: "pause" | "resume" | "stop" | "cancel";
525
565
  recordingId: string;
566
+ follows?: never;
526
567
  });
527
568
  export interface HostRecordingReport {
528
569
  assignmentId: AssignmentId;
529
- state: RecordingState;
570
+ recordings: Recording[];
530
571
  }
531
572
  /** Host declaration in ConnectContext, not the provider-owned Manifest. */
532
573
  export interface HostRecording {
@@ -814,7 +855,7 @@ export type TaskAttribute = TaskAttributeBase & ({
814
855
  type: "timestamp";
815
856
  at: IsoTimestamp;
816
857
  });
817
- export type HistoryStep = "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered";
858
+ export type HistoryStep = "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered" | "callback-requested";
818
859
  /**
819
860
  * The call record: the steps that brought the task here, one entry per occurrence and oldest
820
861
  * first, and what they add up to before this agent -- stated by the provider from its own record,
@@ -849,8 +890,9 @@ export interface TaskHistoryStep {
849
890
  */
850
891
  seconds?: DurationSeconds;
851
892
  /**
852
- * Who took part. Absent on `queued`, where nobody does; absent on any other step means the
853
- * provider could not attribute it, which is a different claim and a legitimate one.
893
+ * Who took part. Absent on `queued`, where nobody does; absent on `callback-requested`, where the
894
+ * caller chose it in the queue, and present there when an agent arranged it; absent on any other
895
+ * step means the provider could not attribute it, which is a different claim and a legitimate one.
854
896
  */
855
897
  by?: UserId;
856
898
  /** On a `muted` step, and only there: whose the silence was, as the host reported it. */
@@ -1044,15 +1086,13 @@ export type Task<C extends Channel = Channel> = {
1044
1086
  attributes?: TaskAttribute[];
1045
1087
  history?: TaskHistory;
1046
1088
  } & TaskCompletion & (C extends "voice" ? {
1047
- recording?: {
1048
- provider?: RecordingState;
1049
- };
1089
+ recordings?: Recording[];
1050
1090
  onCall?: OnCall[];
1051
1091
  leadAssist?: TaskLeadAssist;
1052
1092
  takenOver?: TaskTakenOver;
1053
1093
  audio?: TaskAudioState;
1054
1094
  } : {
1055
- recording?: never;
1095
+ recordings?: never;
1056
1096
  onCall?: never;
1057
1097
  leadAssist?: never;
1058
1098
  takenOver?: never;
@@ -1858,7 +1898,7 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
1858
1898
  occurredAt: IsoTimestamp;
1859
1899
  event: ProviderEvent<C>;
1860
1900
  }
1861
- 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"];
1901
+ 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.on-hold", "omni.break-forced-by-provider"];
1862
1902
  export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
1863
1903
  /**
1864
1904
  * What the host refused, told to the adapter that published it: a snapshot the host would not
@@ -2002,7 +2042,10 @@ export declare const assignmentKey: (providerId: string, assignmentId: Assignmen
2002
2042
  * same provider; anything wider goes through this.
2003
2043
  */
2004
2044
  export declare const userKey: (providerId: string, userId: UserId) => string;
2005
- /** Every history step somebody takes part in. `queued` is the one nobody does. */
2045
+ /**
2046
+ * Every history step somebody from the provider takes part in. `queued` is the one nobody does, and
2047
+ * `callback-requested` is the caller's choice unless an agent arranged it, so neither is here.
2048
+ */
2006
2049
  export declare const HISTORY_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
2007
2050
  /** The steps a dial writes, and so the only ones that carry a `dialId` and a `destinationId`. */
2008
2051
  export declare const HISTORY_STEPS_THAT_DIAL: readonly ["transferred", "conferenced", "unanswered"];
package/dist/index.js CHANGED
@@ -32,6 +32,10 @@ export const LISTENING_MODES = ["listen", "coach", "join-call"];
32
32
  export const MUTED_BY = ["host", "station"];
33
33
  export const HOST_MUTES = ["stream", "station"];
34
34
  export const RECORDING_ACTIONS = ["start", "pause", "resume", "stop", "cancel"];
35
+ export const RECORDING_SIDES = ["party", "agent"];
36
+ export const RECORDING_STATUSES = ["starting", "recording", "paused", "not-recording", "unknown"];
37
+ export const RECORDING_PAUSED_REASONS = ["requested", "sensitive-details"];
38
+ export const RECORDING_UNKNOWN_REASONS = ["recorder-unreachable", "unsettled"];
35
39
  // ---------------------------------------------------------------------------
36
40
  // Task workspace.
37
41
  // ---------------------------------------------------------------------------
@@ -107,6 +111,8 @@ export const OMNI_FAILURE_CODES = [
107
111
  "omni.break-already-committed",
108
112
  /** A recording command the provider could not settle either way: no start, stop, pause or resume it can vouch for. */
109
113
  "omni.recording-unsettled",
114
+ /** Refused because the caller is on hold: the host's Mute, or end-call, waits for resume. */
115
+ "omni.on-hold",
110
116
  /** A lead asked to lift a break the platform itself imposed; the platform lifts its own. */
111
117
  "omni.break-forced-by-provider",
112
118
  ];
@@ -158,7 +164,10 @@ export const assignmentKey = (providerId, assignmentId) => `${encodeURIComponent
158
164
  * same provider; anything wider goes through this.
159
165
  */
160
166
  export const userKey = (providerId, userId) => `${encodeURIComponent(providerId)}:${encodeURIComponent(userId)}`;
161
- /** Every history step somebody takes part in. `queued` is the one nobody does. */
167
+ /**
168
+ * Every history step somebody from the provider takes part in. `queued` is the one nobody does, and
169
+ * `callback-requested` is the caller's choice unless an agent arranged it, so neither is here.
170
+ */
162
171
  export const HISTORY_STEPS_WITH_A_PERSON = [
163
172
  "offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered",
164
173
  ];
package/dist/testing.d.ts CHANGED
@@ -87,7 +87,7 @@ export declare function assertAuthenticationRestoreAndExpiry(states: readonly Au
87
87
  * Capabilities are current, not fixed. A provider that withdraws one republishes `authenticated`
88
88
  * with the new set, and the next snapshot agrees with it. `states` is what the authentication
89
89
  * session published, first to last: beginning and ending `authenticated` for the same identity,
90
- * passing only through usable states (`expired` or `signed-out` ends the login instead), with at
90
+ * passing only through usable states (`expired`, `terminated` or `signed-out` ends the login instead), with at
91
91
  * least one capability gone by the end. `snapshot` is the first snapshot published after the last
92
92
  * state, validated against that login -- so a team member list still published to a login that no longer
93
93
  * leads, or requests to one that may no longer join, is the failure.
@@ -160,8 +160,13 @@ export declare class TaskStream {
160
160
  */
161
161
  private holdCountdowns;
162
162
  private readonly dials;
163
+ private readonly placedHere;
163
164
  /** The host placed a dial: its outcome, whenever it arrives, is one the stream expects. */
164
165
  dialled(dialId: string): void;
166
+ /** A dial the test placed itself and the provider answered dialling: it is owed exactly one outcome. */
167
+ placedByTest(dialId: string): void;
168
+ /** The dials the test placed that never got their outcome. */
169
+ dialsWithoutOutcome(): string[];
165
170
  private noteDials;
166
171
  /**
167
172
  * Why an event names no open task: the assignment ended, so this is the late event that must not
@@ -250,6 +255,15 @@ export declare function stillHost(report?: HostReport, guarantees?: HostGuarante
250
255
  mute?: never;
251
256
  recording?: never;
252
257
  };
258
+ /**
259
+ * One call's history as it reaches a second task: an earlier agent's task and a later one of the same
260
+ * call -- a hand-over, a take-over, a callback. The later task restates every earlier step, in order,
261
+ * as the earlier task had it: the same step, instant, person, dial and destination, and the same
262
+ * duration wherever the earlier task had already stated one (a leg still open there may be closed
263
+ * here). Throws on the first step that disagrees, and when either task carries no history, since
264
+ * two absent records agree about nothing.
265
+ */
266
+ export declare function assertHistoryAgreesAcrossTasks(earlier: Task, later: Task): void;
253
267
  /** One provider as the host sees it when freezing the providers a break attempt asks. */
254
268
  export interface BreakCandidate {
255
269
  id: string;
package/dist/testing.js CHANGED
@@ -309,6 +309,17 @@ export async function testAdapter(adapter, context, options = {}) {
309
309
  violations.push(...own);
310
310
  if (own.length > 0)
311
311
  return;
312
+ // A session that has ended takes its secrets with it: the adapter deletes them before it
313
+ // publishes the ending, so a reload has nothing to restore. Only an expiry whose failure is
314
+ // retryable keeps them, for the retry.
315
+ if (sessionEnded(state)) {
316
+ ruleTested("authentication.secrets.retained");
317
+ if (storedSecrets.size > 0) {
318
+ violations.push({ rule: "authentication.secrets.retained", path: "authentication.status",
319
+ message: `the session is ${state.status} and the adapter still holds ${storedSecrets.size} stored secret(s): ` +
320
+ "an ended session's secrets are deleted before the ending is published, or a reload restores it" });
321
+ }
322
+ }
312
323
  if (state.status === "refreshing") {
313
324
  violations.push(...refreshingCarriesOver(current(), state, "authentication"));
314
325
  }
@@ -678,6 +689,12 @@ export async function testAdapter(adapter, context, options = {}) {
678
689
  violations.push({ rule: "stream.taskOffered.unended", path: "event.task-ended",
679
690
  message: `${id} was offered and never ended: every offer is owed a task-ended, whatever became of the call` });
680
691
  }
692
+ // Every dial the test placed, and the provider answered dialling, is owed exactly one outcome.
693
+ ruleTested("stream.dialOutcome.missing");
694
+ for (const id of stream.dialsWithoutOutcome()) {
695
+ violations.push({ rule: "stream.dialOutcome.missing", path: "event.dial-outcome",
696
+ message: `${id} was answered dialling and never got its dial-outcome: every dial ends in exactly one, however it ended` });
697
+ }
681
698
  }
682
699
  // Capacity supersedes rather than accumulates, so a decrease is as ordinary as an increase: the
683
700
  // host raises it, lowers it, and takes it away, and the provider takes each as the ceiling it is.
@@ -904,6 +921,15 @@ export function assertAuthenticationRestoreAndExpiry(states) {
904
921
  }
905
922
  }
906
923
  /** The tasks an envelope carries: the one a task event names, or a snapshot event's list. */
924
+ /** The failure code of a failed answer, as the provider stated it. */
925
+ function failureCode(answer) {
926
+ return isRecord(answer.failure) ? answer.failure.code : undefined;
927
+ }
928
+ /** Whether a published state ends the session, so its stored secrets must already be gone. */
929
+ function sessionEnded(state) {
930
+ return state.status === "signed-out" || state.status === "terminated"
931
+ || (state.status === "expired" && state.failure?.retryable !== true);
932
+ }
907
933
  /** Whether a store key names the task: the id as a delimited token, never as a run of characters inside another id. */
908
934
  function namesTask(key, taskId) {
909
935
  return key.split(/[^A-Za-z0-9_-]+/).includes(taskId);
@@ -940,7 +966,7 @@ function notYetReadTasks(tasks, path) {
940
966
  * Capabilities are current, not fixed. A provider that withdraws one republishes `authenticated`
941
967
  * with the new set, and the next snapshot agrees with it. `states` is what the authentication
942
968
  * session published, first to last: beginning and ending `authenticated` for the same identity,
943
- * passing only through usable states (`expired` or `signed-out` ends the login instead), with at
969
+ * passing only through usable states (`expired`, `terminated` or `signed-out` ends the login instead), with at
944
970
  * least one capability gone by the end. `snapshot` is the first snapshot published after the last
945
971
  * state, validated against that login -- so a team member list still published to a login that no longer
946
972
  * leads, or requests to one that may no longer join, is the failure.
@@ -1173,11 +1199,19 @@ export class TaskStream {
1173
1199
  // task carried on `onCall` or in its record -- which is how a dial made before a take-over is known
1174
1200
  // to whoever holds the task now. `answered` or `ended` once its outcome arrived, since it comes once.
1175
1201
  dials = new Map();
1202
+ placedHere = new Set();
1176
1203
  /** The host placed a dial: its outcome, whenever it arrives, is one the stream expects. */
1177
1204
  dialled(dialId) {
1178
1205
  if (!this.dials.has(dialId))
1179
1206
  this.dials.set(dialId, "placed");
1180
1207
  }
1208
+ /** A dial the test placed itself and the provider answered dialling: it is owed exactly one outcome. */
1209
+ placedByTest(dialId) {
1210
+ this.dialled(dialId);
1211
+ this.placedHere.add(dialId);
1212
+ }
1213
+ /** The dials the test placed that never got their outcome. */
1214
+ dialsWithoutOutcome() { return [...this.placedHere].filter(id => this.dials.get(id) === "placed"); }
1181
1215
  noteDials(task) {
1182
1216
  if (!isRecord(task))
1183
1217
  return;
@@ -1840,6 +1874,7 @@ async function testOneCall(call) {
1840
1874
  call.stream.dialled(dialId);
1841
1875
  if (await send({ type: "dial", dialId }, dialId) === undefined)
1842
1876
  return found;
1877
+ call.stream.placedByTest(dialId);
1843
1878
  }
1844
1879
  if (latestTask().phase === "preview" && await updated(t => t.phase === "in-progress" || t.phase === "completing", pressCall ? "the task leaving preview after Call" : "the task leaving preview after the provider's dial") === undefined)
1845
1880
  return found;
@@ -1976,6 +2011,9 @@ async function testOneCall(call) {
1976
2011
  if (isRecord(answer) && answer.status !== "failed") {
1977
2012
  refuse("test.recordStep.held", "test.recordStep", "the provider recorded the host's mute begun with the caller on hold: the Mute waits for resume, and the adapter is the second gate");
1978
2013
  }
2014
+ else if (isRecord(answer) && failureCode(answer) !== "omni.on-hold") {
2015
+ refuse("test.recordStep.held", "test.recordStep", `the provider refused the mute begun on hold as ${String(failureCode(answer))}: a refusal because the caller is on hold answers omni.on-hold, so the agent application can say resume first`);
2016
+ }
1979
2017
  };
1980
2018
  // Ending my part from hold would leave the caller in the hold with nobody coming back to them: a
1981
2019
  // host holds it back (command.endCall.held), and an adapter that receives it anyway must refuse
@@ -1995,6 +2033,9 @@ async function testOneCall(call) {
1995
2033
  // Applied, the task leaves paused; the run reads where the adapter put it rather than resuming a call it ended.
1996
2034
  await updated(t => t.phase !== "paused", "the task after an end-call applied from hold");
1997
2035
  }
2036
+ else if (isRecord(answer) && failureCode(answer) !== "omni.on-hold") {
2037
+ refuse("test.command.held", "test.command.end-call", `the provider refused end-call on hold as ${String(failureCode(answer))}: a refusal because the caller is on hold answers omni.on-hold, so the agent application can say resume first`);
2038
+ }
1998
2039
  };
1999
2040
  // 4. Hold and resume, where offered.
2000
2041
  if (latestTask().phase === "in-progress" && offers("hold")) {
@@ -2327,6 +2368,39 @@ export function memoryStore() {
2327
2368
  export function stillHost(report = { online: true }, guarantees = {}, mute) {
2328
2369
  return { guarantees, ...(mute === undefined ? {} : { mute }), report: () => report, subscribe: () => () => undefined };
2329
2370
  }
2371
+ /**
2372
+ * One call's history as it reaches a second task: an earlier agent's task and a later one of the same
2373
+ * call -- a hand-over, a take-over, a callback. The later task restates every earlier step, in order,
2374
+ * as the earlier task had it: the same step, instant, person, dial and destination, and the same
2375
+ * duration wherever the earlier task had already stated one (a leg still open there may be closed
2376
+ * here). Throws on the first step that disagrees, and when either task carries no history, since
2377
+ * two absent records agree about nothing.
2378
+ */
2379
+ export function assertHistoryAgreesAcrossTasks(earlier, later) {
2380
+ const steps = (task, which) => {
2381
+ const history = task.history;
2382
+ if (history === undefined || !Array.isArray(history.steps)) {
2383
+ throw new Error(`The ${which} task ${task.assignmentId} carries no history to compare`);
2384
+ }
2385
+ return history.steps;
2386
+ };
2387
+ const before = steps(earlier, "earlier");
2388
+ const after = steps(later, "later");
2389
+ if (after.length < before.length) {
2390
+ throw new Error(`${later.assignmentId} restates ${after.length} step(s) of the call and ${earlier.assignmentId} already had ${before.length}: a later task carries every earlier step`);
2391
+ }
2392
+ before.forEach((step, index) => {
2393
+ const restated = after[index];
2394
+ for (const field of ["step", "at", "by", "dialId", "destinationId", "mutedBy"]) {
2395
+ if (step[field] !== restated[field]) {
2396
+ throw new Error(`steps[${index}].${field} is ${String(step[field])} on ${earlier.assignmentId} and ${String(restated[field])} on ${later.assignmentId}: one call has one history`);
2397
+ }
2398
+ }
2399
+ if (step.seconds !== undefined && restated.seconds !== step.seconds) {
2400
+ throw new Error(`steps[${index}].seconds is ${step.seconds} on ${earlier.assignmentId} and ${String(restated.seconds)} on ${later.assignmentId}: a stated duration does not change`);
2401
+ }
2402
+ });
2403
+ }
2330
2404
  const usableLogin = (status) => status === "authenticated" || status === "refreshing";
2331
2405
  /**
2332
2406
  * A break one provider forces stops the agent everywhere: the host states a capacity of zero on
@@ -1,4 +1,4 @@
1
- import { type RecordingState, type Channel, type Phone, type ProtocolViolation, type UserCapabilities, type UserId } from "./index.js";
1
+ import { type Channel, type Phone, type ProtocolViolation, type UserCapabilities, type UserId } from "./index.js";
2
2
  export type { ProtocolViolation } from "./index.js";
3
3
  export declare class ProtocolConformanceError extends Error {
4
4
  readonly violations: readonly ProtocolViolation[];
@@ -198,24 +198,34 @@ export declare function validateAuthenticationFailure(failure: unknown, path?: s
198
198
  */
199
199
  export declare function validateAuthenticationResult(result: unknown, method: "start" | "complete", path?: string, context?: LoginValidationContext): ProtocolViolation[];
200
200
  export declare function validateAuthenticationState(state: unknown, path?: string, context?: LoginValidationContext): ProtocolViolation[];
201
- export declare function validateRecordingState(value: unknown, path?: string): ProtocolViolation[];
202
- /** Supply a trusted current instant in the observer's clock domain. Undefined means clock unknown. */
203
- export declare function effectiveRecordingState(state: RecordingState | undefined, now: number | undefined): RecordingState;
201
+ /**
202
+ * One recorder's recordings for one task, as published: each with its id, the side it follows,
203
+ * its status, and a reason where it is paused or unknown; ids unique, and at most one recording
204
+ * live on each side at once.
205
+ */
206
+ export declare function validateRecordings(value: unknown, path?: string): ProtocolViolation[];
204
207
  export declare function validateRecordingPolicy(value: unknown, path?: string): ProtocolViolation[];
205
208
  export declare function validateHostRecording(value: unknown, softphone: boolean, path?: string): ProtocolViolation[];
206
209
  export declare function validateHostRecordings(value: unknown, path?: string): ProtocolViolation[];
207
210
  export declare function validateRecordingCommandShape(value: unknown, source: "provider" | "host", path?: string): ProtocolViolation[];
208
- /** Structural/state permission check. Dispatch MUST also call validateRecordingRequest for scope and freshness. */
209
- export declare function validateRecordingCommandState(command: unknown, policy: unknown, state: unknown, path?: string): ProtocolViolation[];
210
- /** Validate immediately before dispatch using current task, host report and trusted observer-domain time. */
211
+ /**
212
+ * Whether the action is permitted on this recorder, and whether the recordings as last published
213
+ * let it act: a start on a side with nothing capturing, every other action on the recording it
214
+ * names, standing where the action needs it. Dispatch MUST also call validateRecordingRequest.
215
+ */
216
+ export declare function validateRecordingCommandState(command: unknown, policy: unknown, recordings: unknown, path?: string): ProtocolViolation[];
217
+ /** Validate immediately before dispatch, against the current task and, for the host's recorder, its declaration and report. */
211
218
  export declare function validateRecordingRequest(request: unknown, task: unknown, context: {
212
219
  source: "provider" | "host";
213
- now: number | undefined;
214
220
  host?: unknown;
215
221
  hostReport?: unknown;
216
222
  softphone?: boolean;
217
223
  /** The same provider/task context used to validate the published task. */
218
224
  taskContext?: Omit<TaskValidationContext, "channel">;
219
225
  }, path?: string): ProtocolViolation[];
220
- /** Check recording-specific result semantics against the owner's confirming observations. */
226
+ /**
227
+ * A recording command's answer against the recordings published before and after it: applied
228
+ * means the recorder confirmed what was asked -- a new recording on the side for a start, the
229
+ * same recording in its new status for anything else -- and failed means nothing changed there.
230
+ */
221
231
  export declare function validateRecordingOutcome(command: unknown, before: unknown, after: unknown, result: unknown, path?: string): ProtocolViolation[];