@xema/omni-protocol 0.1.82 → 0.1.83

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,10 @@ export interface Manifest<C extends Channel = Channel> {
153
153
  supportedProtocolVersions: number[];
154
154
  authenticationMethods: AuthenticationMethod[];
155
155
  idleCapabilities?: IdleCapabilities<C>;
156
+ /** Optional provider-clock sampling through Connection.checkTime; absent means unsupported. */
157
+ timeCheck?: true;
158
+ /** Provider uses its own timestamps for final records; host timestamps are advisory. Omission makes no trust promise. */
159
+ timestampAuthority?: "provider";
156
160
  phaseLabels?: TaskPhaseLabels;
157
161
  /** Keyed by `taskType`. An entry replaces the channel default outright rather than merging. */
158
162
  taskTypePresentation?: Record<string, TaskTypePresentation>;
@@ -526,7 +530,20 @@ export interface HostGuarantees {
526
530
  /** A `consent` offer is accepted only by the person's own explicit act, never on their behalf. */
527
531
  personConsent?: true;
528
532
  }
533
+ export interface ProviderTimeScope {
534
+ providerId: string;
535
+ loginId: string;
536
+ }
537
+ /** Best-effort host estimate, never a provider observation or accuracy guarantee. The provider may ignore it and timestamp receipt itself. */
538
+ export interface ProviderTimeEstimate extends ProviderTimeScope {
539
+ at: IsoTimestamp;
540
+ clockId: string;
541
+ /** Estimated uncertainty only; not a guaranteed error bound. */
542
+ uncertaintyMs?: number;
543
+ }
529
544
  export interface Host {
545
+ /** Optional current provider-clock estimate; undefined when no usable estimate exists. */
546
+ estimateProviderTime?(scope: ProviderTimeScope): ProviderTimeEstimate | undefined;
530
547
  /** Voice softphone recording support; policy and destination remain per task. */
531
548
  recording?: HostRecording;
532
549
  guarantees: HostGuarantees;
@@ -615,7 +632,7 @@ export interface DispositionRules {
615
632
  /**
616
633
  * One item the queue configured for the agent to send a contact to: a button or a menu item. The
617
634
  * protocol does not say what it does -- a queue, a menu, a line -- the provider executes it when a
618
- * command names its `id`. Never a named agent: who takes a contact next is the queue's decision.
635
+ * command names its `id`. The provider may publish a queue, IVR, named agent or other supported destination; the host does not invent one.
619
636
  */
620
637
  export interface Destination {
621
638
  id: string;
@@ -652,7 +669,7 @@ export interface SharedTaskCapabilities {
652
669
  export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ? SharedTaskCapabilities & {
653
670
  decline?: Lockable<true>;
654
671
  hold?: Lockable<true>;
655
- /** The agent may end the whole call: everyone leaves and the media ends, the task stays for its wrap-up. */
672
+ /** The provider ends the caller connection and agent-added channels owned by this handling, including inherited channels after transfer/takeover. Handling disposal remains separate. */
656
673
  endCall?: Lockable<true>;
657
674
  /** Connect back to the party while `completing`, whoever placed the call; the task returns to `in-progress`. */
658
675
  connectBack?: Lockable<true>;
@@ -718,14 +735,20 @@ export type TaskPhase =
718
735
  /** Accepted, and not yet started. */
719
736
  | "confirmed"
720
737
  /**
721
- * Voice only: the customer's record is on the agent's screen and no call has gone out. The agent
738
+ * Voice only: the customer's record is available for preparation, then dialing before answer. The agent
722
739
  * presses Call, or `previewEndsAt` arrives and `atDeadline` says what the system does instead.
723
740
  */
724
741
  | "preview" | "in-progress" | "paused"
725
742
  /** The work is over and the agent is finishing up. */
726
743
  | "completing";
727
744
  /** What the system does when a preview's deadline passes with no Call pressed. */
728
- export type PreviewDeadline = "calls" | "expires";
745
+ export type PreviewDeadline =
746
+ /** Provider initiates dialing when preparation ends; never a host timer command. */
747
+ "calls"
748
+ /** Host initiates the ordinary call command when preparation ends. */
749
+ | "host-calls"
750
+ /** Preparation target has elapsed; remain in preview until the agent presses Call. */
751
+ | "waits";
729
752
  /** Who ends the task: the agent issuing `complete`, or the provider deciding it is over. */
730
753
  export type CompletionMode = "agent-command" | "provider-automatic";
731
754
  export interface TaskAttributeBase {
@@ -958,9 +981,10 @@ export type Task<C extends Channel = Channel> = {
958
981
  */
959
982
  acceptance?: AcceptanceMode;
960
983
  /**
961
- * In `preview` only, and together: when the system stops waiting for the agent to press Call,
962
- * and what it does then -- `calls` places the call itself, `expires` takes the record back and
963
- * the task ends `expired`. Absent, the agent has as long as they need.
984
+ * In `preview` only, and together: the preparation target and behavior when it elapses,
985
+ * and who acts then -- `calls` makes the provider initiate dialing, `host-calls` makes the
986
+ * host issue Call, and `waits` keeps waiting for the agent without dialing or ending the task.
987
+ * Absent, the agent has as long as they need, without a preparation countdown.
964
988
  */
965
989
  previewEndsAt?: IsoTimestamp;
966
990
  atDeadline?: PreviewDeadline;
@@ -1048,7 +1072,7 @@ export type VoiceTaskCommand = {
1048
1072
  } | {
1049
1073
  type: "resume";
1050
1074
  }
1051
- /** End the whole call: everyone leaves and the task's media ends; the task stays for its wrap-up. Gated by `endCall`. */
1075
+ /** End the caller connection and all agent-added channels owned or inherited by this handling. Wrap/disposal remain separate. Gated by `endCall`. */
1052
1076
  | {
1053
1077
  type: "end-call";
1054
1078
  }
@@ -1485,6 +1509,7 @@ export type HandlingReport = {
1485
1509
  });
1486
1510
  export type HandlingReportResult = {
1487
1511
  status: "recorded";
1512
+ at: IsoTimestamp;
1488
1513
  } | {
1489
1514
  status: "failed";
1490
1515
  failure: ProtocolFailure;
@@ -1636,7 +1661,28 @@ export interface Refusal {
1636
1661
  envelopeId?: string;
1637
1662
  violations: ProtocolViolation[];
1638
1663
  }
1664
+ /** Host-local opt-in configuration; no implicit interval or timeout. */
1665
+ export interface ProviderTimeCheckPolicy {
1666
+ intervalMs: number;
1667
+ timeoutMs: number;
1668
+ maxRoundTripMs: number;
1669
+ maxSampleAgeMs: number;
1670
+ }
1671
+ export interface ProviderTimeCheckRequest {
1672
+ /** Fresh opaque correlation ID for one outstanding check, never reused on retry. */
1673
+ requestId: string;
1674
+ }
1675
+ export interface ProviderTimeCheckResult {
1676
+ requestId: string;
1677
+ loginId: string;
1678
+ /** Provider clock domain/incarnation; changes on clock discontinuity or authority replacement. */
1679
+ clockId: string;
1680
+ /** Provider-domain ISO instant sampled after this request arrived, before its response leaves. */
1681
+ providerTime: IsoTimestamp;
1682
+ }
1639
1683
  export interface Connection<C extends Channel = Channel> {
1684
+ /** Required only when Manifest.timeCheck is true. A read-only clock sample, never cached/replayed. */
1685
+ checkTime?(request: ProviderTimeCheckRequest): Promise<ProviderTimeCheckResult>;
1640
1686
  snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
1641
1687
  /** Delivery order must match the order the provider observes changes. Never replay. */
1642
1688
  subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
package/dist/testing.d.ts CHANGED
@@ -190,15 +190,18 @@ export declare class TaskStream {
190
190
  }
191
191
  /** What a stream has said about the agent's break, and the moves it may not make. */
192
192
  export declare class BreakStream {
193
- private approval;
194
- /** Takes the break state a snapshot carries as the point the stream continues from. */
195
- seed(snapshot: unknown): void;
196
- /** Applies one envelope and returns the moves it may not make given where the break stood. */
193
+ private state;
194
+ private recovery;
195
+ /** True until a validated authoritative snapshot establishes a usable baseline. */
196
+ get needsRecovery(): boolean;
197
+ /** Caller establishes snapshot freshness/login scope separately; malformed state never replaces the baseline. */
198
+ seed(snapshot: unknown): ProtocolViolation[];
199
+ /** Rejected transitions keep accepted state and require snapshot recovery before further deltas. */
197
200
  apply(envelope: unknown, path?: string): ProtocolViolation[];
198
201
  }
199
202
  /**
200
- * A break follows its requests. Given a provider's stream -- optionally seeded with the snapshot
201
- * it began from -- every `break-state` moves the way the guide describes: a commit's states only
203
+ * A break follows its requests. Given a provider's stream -- seeded with its initial snapshot,
204
+ * either supplied separately or carried by a snapshot event -- every `break-state` moves the way the guide describes: a commit's states only
202
205
  * after a grant, never backwards, and a break placed on the agent arriving in effect with `imposed`.
203
206
  */
204
207
  export declare function assertBreakFollowsItsRequests(envelopes: readonly ProviderEventEnvelope[], snapshot?: Snapshot): void;
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateDescribedUsers, observeRules, ruleEvaluated, validateHostMute, validateHostRecording, validateLoginStore, validateHandlingReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateBreakTransition, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateDescribedUsers, observeRules, ruleEvaluated, validateHostMute, validateHostRecording, validateLoginStore, validateHandlingReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
4
4
  /**
5
5
  * A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
@@ -305,6 +305,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
305
305
  // What every connection owes on connect, the first and a reloaded second alike.
306
306
  const connectObligations = (on) => {
307
307
  // A refusal is visible on both sides: what the host would not take, the adapter is told, with the rules.
308
+ if (adapter.manifest.timeCheck === true)
309
+ requireMethod(on, "checkTime", "the manifest declares timeCheck");
308
310
  requireMethod(on, "refused", "every connection is told what the host refused");
309
311
  // Dial is declared by presence: the capability object carries a destination policy rather
310
312
  // than an `enabled` flag, so its presence is the declaration.
@@ -479,7 +481,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
479
481
  const snapshot = await connection.snapshot();
480
482
  await snapshotRead(live, snapshot, "snapshot");
481
483
  stream.seed(snapshot);
482
- breaks.seed(snapshot);
484
+ violations.push(...breaks.seed(snapshot));
483
485
  seeded = true;
484
486
  // A snapshot supersedes what it restates and nothing else. Of the events held during the read,
485
487
  // a state-replacing kind is dropped, since the snapshot carries that state and must account for
@@ -1073,9 +1075,9 @@ export class TaskStream {
1073
1075
  stages.set(entry.dialId, entry.stage);
1074
1076
  }
1075
1077
  }
1076
- const partyRingingByHost = isRecord(task) && Array.isArray(task.onCall)
1077
- && task.onCall.some(entry => isRecord(entry) && entry.role === "party" && entry.stage === "ringing" && typeof entry.dialId === "string");
1078
- return { partyRingingByHost, channel: String(isRecord(task) ? task.channel : undefined), completionMode: String(isRecord(task) ? task.completionMode : undefined),
1078
+ const partyRingingBeforeWork = isRecord(task) && Array.isArray(task.onCall)
1079
+ && task.onCall.some(entry => isRecord(entry) && entry.role === "party" && entry.stage === "ringing" && (typeof entry.dialId === "string" || (task.phase === "preview" && task.atDeadline === "calls" && entry.dialId === undefined)));
1080
+ return { partyRingingBeforeWork, channel: String(isRecord(task) ? task.channel : undefined), completionMode: String(isRecord(task) ? task.completionMode : undefined),
1079
1081
  wrapAllowance: isRecord(task) && typeof task.wrapAllowance === "number" ? task.wrapAllowance : undefined,
1080
1082
  phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages, record: TaskStream.record(task), allocation: String(isRecord(task) ? task.allocationId : undefined) };
1081
1083
  }
@@ -1268,7 +1270,7 @@ export class TaskStream {
1268
1270
  refuse("stream.taskMedia.channel", `${at}.type`, "only a voice task has media transitions");
1269
1271
  break;
1270
1272
  }
1271
- if (!AT_WORK.has(known.phase) && !known.partyRingingByHost) {
1273
+ if (!AT_WORK.has(known.phase) && !known.partyRingingBeforeWork) {
1272
1274
  refuse("stream.taskMediaStarted.beforeWork", `${at}.taskId`, `media cannot arrive on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
1273
1275
  }
1274
1276
  if (known.media === "started") {
@@ -1290,7 +1292,7 @@ export class TaskStream {
1290
1292
  refuse("stream.taskMedia.channel", `${at}.type`, "only a voice task has media transitions");
1291
1293
  break;
1292
1294
  }
1293
- if (!WORK_BEGUN.has(known.phase)) {
1295
+ if (!WORK_BEGUN.has(known.phase) && !known.partyRingingBeforeWork) {
1294
1296
  refuse("stream.taskMediaEnded.beforeWork", `${at}.taskId`, `media cannot end on ${id} while it is ${known.phase}: a task is never its audio, and its work has not begun`);
1295
1297
  }
1296
1298
  if (known.media !== "started") {
@@ -1441,7 +1443,7 @@ async function driveOneCall(drive) {
1441
1443
  // is a place the stream's rules keep working, not one where they all stop.
1442
1444
  await drive.snapshotRead(second, snapshot, "drive.reload.snapshot");
1443
1445
  found.push(...drive.streams.stream.resync(snapshot, "drive.reload.snapshot"));
1444
- drive.streams.breaks.seed(snapshot);
1446
+ found.push(...drive.streams.breaks.seed(snapshot));
1445
1447
  const carried = isRecord(snapshot) && Array.isArray(snapshot.tasks)
1446
1448
  ? snapshot.tasks.find(task => isRecord(task) && task.id === taskId) : undefined;
1447
1449
  ruleEvaluated("drive.reload.snapshot");
@@ -1655,12 +1657,13 @@ async function driveOneCall(drive) {
1655
1657
  // 3b. The microphone is the host's. With the audio open, the drive mutes it for a moment and
1656
1658
  // reports the leg the provider's record would otherwise miss -- begun, then ended -- and
1657
1659
  // expects each report recorded. If the provider restates the task's record afterwards, the leg is in it.
1660
+ const canonicalTimes = new Map();
1658
1661
  const reportedLegs = [];
1659
1662
  /** The duration the provider's record states for the leg at `at`, as the task stands now. */
1660
1663
  const closedAs = (at) => {
1661
1664
  const history = latestTask().handlingHistory;
1662
1665
  const leg = isRecord(history) && Array.isArray(history.steps)
1663
- ? history.steps.find(entry => isRecord(entry) && entry.step === "muted" && entry.at === at) : undefined;
1666
+ ? history.steps.find(entry => isRecord(entry) && entry.step === "muted" && entry.at === canonicalTimes.get(at)) : undefined;
1664
1667
  return typeof leg?.seconds === "number" ? leg.seconds : undefined;
1665
1668
  };
1666
1669
  const canRecordMute = session !== undefined && typeof session.setMuted === "function" && typeof drive.connection.recordStep === "function";
@@ -1680,7 +1683,15 @@ async function driveOneCall(drive) {
1680
1683
  refuse("drive.recordStep.rejected", "drive.recordStep", `recordStep rejected rather than answered: ${String(error)}`);
1681
1684
  return;
1682
1685
  }
1683
- found.push(...validateResult(answer, "recordStep", "drive.recordStep.result"));
1686
+ const resultErrors = validateResult(answer, "recordStep", "drive.recordStep.result");
1687
+ found.push(...resultErrors);
1688
+ if (!resultErrors.length && isRecord(answer) && answer.status === "recorded" && typeof answer.at === "string" && typeof body.at === "string") {
1689
+ const prior = canonicalTimes.get(body.at);
1690
+ if (prior !== undefined && prior !== answer.at)
1691
+ refuse("drive.recordStep.identity", "drive.recordStep.result.at", "one reported leg must retain its provider-assigned history instant");
1692
+ else
1693
+ canonicalTimes.set(body.at, answer.at);
1694
+ }
1684
1695
  if (isRecord(answer) && answer.status === "failed") {
1685
1696
  refuse("drive.recordStep.failed", "drive.recordStep", `the provider refused to record the host's muted leg: ${String(isRecord(answer.failure) ? answer.failure.code : answer.failure)}`);
1686
1697
  }
@@ -1705,12 +1716,12 @@ async function driveOneCall(drive) {
1705
1716
  await new Promise(resolve => setTimeout(resolve, 1000));
1706
1717
  setMuted(false);
1707
1718
  await report({ at, seconds: Math.round((Date.now() - began) / 1000), ended: true });
1708
- reportedLegs.push({ at, task: latestTask() });
1719
+ reportedLegs.push({ at: canonicalTimes.get(at), task: latestTask() });
1709
1720
  // 3c. A host reload destroys the adapter object and keeps the login's store. Built again from
1710
1721
  // the same login, a second adapter carries the task and the leg -- from its platform, or from
1711
1722
  // the store -- or it composed the record in memory and the record died with it.
1712
- if (drive.rebuild !== undefined) {
1713
- await recordSurvivesReload(at);
1723
+ if (drive.rebuild !== undefined && canonicalTimes.has(at)) {
1724
+ await recordSurvivesReload(canonicalTimes.get(at));
1714
1725
  // A reload that stood no client ends the run: there is nothing left to drive.
1715
1726
  if (!drive.handOver.isLive())
1716
1727
  return found;
@@ -1761,7 +1772,7 @@ async function driveOneCall(drive) {
1761
1772
  // record as it stands now is what every later restatement is held to.
1762
1773
  const before = closedAs(openLeg.at);
1763
1774
  await report({ at: openLeg.at, seconds: Math.max(1, Math.round((Date.now() - openLeg.began) / 1000)), ended: true });
1764
- reportedLegs.push({ at: openLeg.at, task: openLeg.task, closedAs: before });
1775
+ reportedLegs.push({ at: canonicalTimes.get(openLeg.at), task: openLeg.task, closedAs: before });
1765
1776
  }
1766
1777
  if (completing !== undefined && offers("hold")) {
1767
1778
  await holdRefusedOutsideHandling();
@@ -1826,6 +1837,8 @@ async function driveOneCall(drive) {
1826
1837
  return latestTask();
1827
1838
  };
1828
1839
  for (const reported of reportedLegs) {
1840
+ if (reported.at === undefined)
1841
+ continue; // A refused/malformed report provides no accepted history identity.
1829
1842
  const published = lastPublished();
1830
1843
  if (published === reported.task)
1831
1844
  continue;
@@ -1856,62 +1869,56 @@ async function driveOneCall(drive) {
1856
1869
  // starting-after-task | in-effect; work ending goes starting-after-task -> in-effect; a denial,
1857
1870
  // a cancel, an end or a release goes back to not-requested; a placed break arrives in-effect
1858
1871
  // with `imposed`. Nothing else is a move the guide describes.
1859
- const COMMITTED = new Set(["starting-after-task", "in-effect"]);
1860
- const BACKWARDS = {
1861
- "in-effect": ["awaiting-decision", "granted", "starting-after-task"],
1862
- "starting-after-task": ["awaiting-decision", "granted"],
1863
- granted: ["awaiting-decision"],
1864
- };
1865
1872
  /** What a stream has said about the agent's break, and the moves it may not make. */
1866
1873
  export class BreakStream {
1867
- approval;
1868
- /** Takes the break state a snapshot carries as the point the stream continues from. */
1874
+ state;
1875
+ recovery = true;
1876
+ /** True until a validated authoritative snapshot establishes a usable baseline. */
1877
+ get needsRecovery() { return this.recovery; }
1878
+ /** Caller establishes snapshot freshness/login scope separately; malformed state never replaces the baseline. */
1869
1879
  seed(snapshot) {
1870
1880
  const state = isRecord(snapshot) ? snapshot.break : undefined;
1871
- this.approval = isRecord(state) && typeof state.approval === "string" ? state.approval : undefined;
1881
+ const found = validateBreakTransition(state, state, "snapshot.break");
1882
+ if (found.length) {
1883
+ this.recovery = true;
1884
+ return found;
1885
+ }
1886
+ this.state = state;
1887
+ this.recovery = false;
1888
+ return [];
1872
1889
  }
1873
- /** Applies one envelope and returns the moves it may not make given where the break stood. */
1890
+ /** Rejected transitions keep accepted state and require snapshot recovery before further deltas. */
1874
1891
  apply(envelope, path = "event") {
1875
- const found = [];
1876
1892
  const event = isRecord(envelope) ? envelope.event : undefined;
1877
1893
  if (!isRecord(event))
1878
- return found;
1879
- if (event.type === "snapshot") {
1880
- this.seed(event.snapshot);
1881
- return found;
1882
- }
1883
- if (event.type !== "break-state" || !isRecord(event.break) || typeof event.break.approval !== "string")
1884
- return found;
1885
- const from = this.approval;
1886
- const to = event.break.approval;
1887
- const at = `${path}.event.break.approval`;
1888
- if (from !== undefined) {
1889
- // A commit's states need a grant behind them. A placed break is the one arrival in a
1890
- // committed state that nobody asked for -- in effect at once, or starting-after-task while
1891
- // the member finishes a call -- and it says so with `imposed`.
1892
- if (COMMITTED.has(to) && (from === "not-requested" || from === "awaiting-decision") && event.break.imposed === undefined) {
1893
- found.push({ rule: "stream.breakState.commitBeforeGrant", path: at,
1894
- message: `${to} follows a commit, and a commit follows granted; the break stood at ${from}` });
1895
- }
1896
- if ((BACKWARDS[from] ?? []).includes(to)) {
1897
- found.push({ rule: "stream.breakState.backwards", path: at,
1898
- message: `a break does not go from ${from} back to ${to}; a new request passes through not-requested` });
1899
- }
1894
+ return [];
1895
+ if (event.type === "transport-status" && event.status !== "active") {
1896
+ this.recovery = true;
1897
+ return [];
1900
1898
  }
1901
- this.approval = to;
1899
+ if (event.type === "snapshot")
1900
+ return this.seed(event.snapshot);
1901
+ if (event.type !== "break-state")
1902
+ return [];
1903
+ if (this.recovery)
1904
+ return [{ rule: "stream.breakState.baseline", path: `${path}.event.break`,
1905
+ message: "a validated authoritative snapshot is required before accepting break updates" }];
1906
+ const found = validateBreakTransition(this.state, event.break, `${path}.event.break`);
1907
+ if (found.length)
1908
+ this.recovery = true;
1909
+ else
1910
+ this.state = event.break;
1902
1911
  return found;
1903
1912
  }
1904
1913
  }
1905
1914
  /**
1906
- * A break follows its requests. Given a provider's stream -- optionally seeded with the snapshot
1907
- * it began from -- every `break-state` moves the way the guide describes: a commit's states only
1915
+ * A break follows its requests. Given a provider's stream -- seeded with its initial snapshot,
1916
+ * either supplied separately or carried by a snapshot event -- every `break-state` moves the way the guide describes: a commit's states only
1908
1917
  * after a grant, never backwards, and a break placed on the agent arriving in effect with `imposed`.
1909
1918
  */
1910
1919
  export function assertBreakFollowsItsRequests(envelopes, snapshot) {
1911
1920
  const stream = new BreakStream();
1912
- if (snapshot !== undefined)
1913
- stream.seed(snapshot);
1914
- const found = [];
1921
+ const found = snapshot === undefined ? [] : stream.seed(snapshot);
1915
1922
  envelopes.forEach((envelope, index) => found.push(...stream.apply(envelope, `envelopes[${index}]`)));
1916
1923
  assertNoViolations(found, "A break follows its requests");
1917
1924
  }
@@ -22,6 +22,13 @@ export declare function validateScheduledActivity(activity: unknown, path?: stri
22
22
  * on voice, absent elsewhere.
23
23
  */
24
24
  export declare function validatePhone(value: unknown, manifest: unknown, path?: string): ProtocolViolation[];
25
+ /** Undefined explicitly means unavailable; a returned estimate is not a clock guarantee. */
26
+ export declare function validateProviderTimeEstimate(value: unknown, scope: unknown, path?: string): ProtocolViolation[];
27
+ /** Validate host-local periodic clock-check configuration. Omission disables checking. */
28
+ export declare function validateProviderTimeCheckPolicy(value: unknown, path?: string): ProtocolViolation[];
29
+ export declare function validateProviderTimeCheckRequest(value: unknown, path?: string): ProtocolViolation[];
30
+ /** Checks response shape/correlation, not source-clock accuracy, round-trip delay or freshness. */
31
+ export declare function validateProviderTimeCheckResult(value: unknown, request: unknown, loginId: string, path?: string): ProtocolViolation[];
25
32
  export declare function validateManifest(manifest: unknown, path?: string): ProtocolViolation[];
26
33
  export interface TaskValidationContext {
27
34
  /** The provider's channel, from its manifest. A task must agree with it. */
@@ -40,6 +47,30 @@ export interface TaskValidationContext {
40
47
  locked?: readonly string[];
41
48
  }
42
49
  export declare function validateTask(task: unknown, context: TaskValidationContext, path?: string): ProtocolViolation[];
50
+ /** Checks break status against the full currently retained task set, after a transaction. */
51
+ export declare function validateBreakStatus(state: unknown, tasks: unknown, path?: string): ProtocolViolation[];
52
+ /**
53
+ * Checks lead break dispatch using current authentication, transport, roster (`team`) and,
54
+ * for place/release, the target's full `memberBreak`. The provider must authorize the target
55
+ * and recheck the decision atomically; a roster is not authority to act after it has changed.
56
+ */
57
+ export declare function validateTeamBreakCommand(request: unknown, context: unknown, path?: string): ProtocolViolation[];
58
+ /** The four agent break methods; this is a validation API, not a wire command. */
59
+ export type BreakMethod = "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak";
60
+ /**
61
+ * Validate immediately before dispatch against current provider state. Context must contain
62
+ * the current authentication and transport. This cannot fence a concurrent backend change;
63
+ * the provider must recheck atomically. Result validation never replaces authoritative state.
64
+ */
65
+ export declare function validateBreakCommand(method: BreakMethod, request: unknown, state: unknown, context: unknown, path?: string): ProtocolViolation[];
66
+ /**
67
+ * Checks two complete break states within one provider/login's ordered event stream.
68
+ * Call before replacing accepted state; report violations and reconcile on failure.
69
+ * This is not a freshness check: validate a fresh authoritative snapshot separately and
70
+ * use it as a new baseline, never as an event transition. Source ordering and connection
71
+ * fencing remain required; neither timestamps nor approval rank identify a break attempt.
72
+ */
73
+ export declare function validateBreakTransition(before: unknown, after: unknown, path?: string): ProtocolViolation[];
43
74
  /**
44
75
  * Who is reading what the adapter published, and what their login declares. The validators check
45
76
  * structure without it; given it, they also hold what the adapter publishes to the login.
@@ -93,6 +124,8 @@ export declare function validateHostGuarantees(guarantees: unknown, path?: strin
93
124
  */
94
125
  export declare function validateHandlingReport(report: unknown, path?: string, manifest?: unknown): ProtocolViolation[];
95
126
  export declare function validateHostReport(report: unknown, path?: string): ProtocolViolation[];
127
+ /** Validate the exact handling/allocation target before provider command dispatch. */
128
+ export declare function validateTaskCommandRequest(request: unknown, task: unknown, path?: string, taskContext?: Omit<TaskValidationContext, "channel">): ProtocolViolation[];
96
129
  /**
97
130
  * What a command needs to be issuable, checked against the task it names: the capability the
98
131
  * guide's table gates it on, the phase it belongs to, and the state that has to stand -- a