@xema/omni-protocol 0.1.76 → 0.1.77

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/testing.d.ts CHANGED
@@ -170,6 +170,15 @@ export declare class TaskStream {
170
170
  /** What a restated record lost of the one read before it: nothing, or the entries by step and instant. */
171
171
  private static lost;
172
172
  private static stated;
173
+ /**
174
+ * Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
175
+ * state now, and first holds it to what the stream knew. A snapshot replaces state; it does not
176
+ * get to forget it. A task it carries that the stream already held may not go backwards, lose an
177
+ * entry of its record, return to undetermined terms, or be at work without the audio the stream
178
+ * held up: media ends on task-media-ended and the call moves on, so a task still in-progress or
179
+ * paused without its media is a snapshot that lost state, not a call that ended.
180
+ */
181
+ resync(snapshot: unknown, at: string): ProtocolViolation[];
173
182
  /** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
174
183
  seed(snapshot: unknown): void;
175
184
  /** Applies one envelope and returns what it may not say given what came before. */
package/dist/testing.js CHANGED
@@ -1047,6 +1047,49 @@ export class TaskStream {
1047
1047
  wrapAllowance: isRecord(task) && typeof task.wrapAllowance === "number" ? task.wrapAllowance : undefined,
1048
1048
  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) };
1049
1049
  }
1050
+ /**
1051
+ * Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
1052
+ * state now, and first holds it to what the stream knew. A snapshot replaces state; it does not
1053
+ * get to forget it. A task it carries that the stream already held may not go backwards, lose an
1054
+ * entry of its record, return to undetermined terms, or be at work without the audio the stream
1055
+ * held up: media ends on task-media-ended and the call moves on, so a task still in-progress or
1056
+ * paused without its media is a snapshot that lost state, not a call that ended.
1057
+ */
1058
+ resync(snapshot, at) {
1059
+ const found = [];
1060
+ const refuse = (rule, where, message) => found.push({ rule, path: where, message });
1061
+ ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.handlingHistory", "stream.snapshot.phase", "stream.snapshot.media");
1062
+ if (isRecord(snapshot) && Array.isArray(snapshot.tasks)) {
1063
+ snapshot.tasks.forEach((task, index) => {
1064
+ if (!isRecord(task) || typeof task.id !== "string")
1065
+ return;
1066
+ const was = this.tasks.get(task.id);
1067
+ if (was === undefined || was.allocation !== String(task.allocationId))
1068
+ return;
1069
+ if ((was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
1070
+ refuse("stream.snapshot.capabilitySource", `${at}.tasks[${index}].capabilitySource`, `${task.id} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
1071
+ }
1072
+ // A record once read is not unread: a resync restates it whole, or with more, never with less.
1073
+ const lost = TaskStream.lost(was.record, TaskStream.record(task));
1074
+ if (lost.length > 0) {
1075
+ refuse("stream.snapshot.handlingHistory", `${at}.tasks[${index}].handlingHistory`, `${task.id}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
1076
+ }
1077
+ const to = String(task.phase);
1078
+ const reachable = REACHABLE_PHASES[was.phase];
1079
+ if (reachable !== undefined && TASK_PHASES_ORDERED.includes(to) && !reachable.has(to)) {
1080
+ const connectingBack = was.phase === "completing" && TaskStream.partyDialled(task) && (to === "in-progress" || to === "paused");
1081
+ if (!connectingBack) {
1082
+ refuse("stream.snapshot.phase", `${at}.tasks[${index}].phase`, `${task.id} was ${was.phase} and the snapshot says ${to}: a task does not go backwards, on an update or on a resync`);
1083
+ }
1084
+ }
1085
+ if (was.media === "started" && (to === "in-progress" || to === "paused") && task.media !== "started") {
1086
+ refuse("stream.snapshot.media", `${at}.tasks[${index}].media`, `${task.id}'s audio was up and the snapshot carries it ${to} without it: media ends on task-media-ended and the call moves on, so a snapshot that forgets the audio lost state`);
1087
+ }
1088
+ });
1089
+ }
1090
+ this.seed(snapshot);
1091
+ return found;
1092
+ }
1050
1093
  /** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
1051
1094
  seed(snapshot) {
1052
1095
  this.tasks.clear();
@@ -1073,24 +1116,7 @@ export class TaskStream {
1073
1116
  const known = id === undefined ? undefined : this.tasks.get(id);
1074
1117
  switch (event.type) {
1075
1118
  case "snapshot":
1076
- ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.handlingHistory");
1077
- // A snapshot replaces what is known, and still may not say a task lost terms it had read.
1078
- if (isRecord(event.snapshot) && Array.isArray(event.snapshot.tasks)) {
1079
- event.snapshot.tasks.forEach((task, index) => {
1080
- if (!isRecord(task) || typeof task.id !== "string")
1081
- return;
1082
- const was = this.tasks.get(task.id);
1083
- if (was !== undefined && (was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
1084
- refuse("stream.snapshot.capabilitySource", `${at}.snapshot.tasks[${index}].capabilitySource`, `${task.id} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
1085
- }
1086
- // A record once read is not unread: a resync restates it whole, or with more, never with less.
1087
- const lost = TaskStream.lost(was?.record, TaskStream.record(task));
1088
- if (lost.length > 0) {
1089
- refuse("stream.snapshot.handlingHistory", `${at}.snapshot.tasks[${index}].handlingHistory`, `${task.id}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
1090
- }
1091
- });
1092
- }
1093
- this.seed(event.snapshot);
1119
+ found.push(...this.resync(event.snapshot, `${at}.snapshot`));
1094
1120
  break;
1095
1121
  case "task-offered": {
1096
1122
  ruleEvaluated("stream.taskOffered.duplicate", "stream.taskOffered.allocation");
@@ -1346,9 +1372,11 @@ async function driveOneCall(drive) {
1346
1372
  drive.handOver.up(session, second);
1347
1373
  drive.connection = second;
1348
1374
  const snapshot = await second.snapshot();
1349
- // The second adapter's snapshot is held to everything a first one is, and the streams take it as the state now.
1375
+ // The second adapter's snapshot is held to everything a first one is, and the streams take it
1376
+ // as the state now the way they take any resync: held to what they knew, then replaced. A reload
1377
+ // is a place the stream's rules keep working, not one where they all stop.
1350
1378
  found.push(...validateSnapshot(snapshot, again.manifest, "drive.reload.snapshot", drive.reader()));
1351
- drive.streams.stream.seed(snapshot);
1379
+ found.push(...drive.streams.stream.resync(snapshot, "drive.reload.snapshot"));
1352
1380
  drive.streams.breaks.seed(snapshot);
1353
1381
  const carried = isRecord(snapshot) && Array.isArray(snapshot.tasks)
1354
1382
  ? snapshot.tasks.find(task => isRecord(task) && task.id === taskId) : undefined;
package/guide.md CHANGED
@@ -2888,7 +2888,10 @@ last statement stands and the failure is a `diagnostic`, and a task that was pub
2888
2888
  `queue` or `ungoverned` never returns to `undetermined`, on an update (`stream.taskUpdated.capabilitySource`)
2889
2889
  or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
2890
2890
  entry the same two ways (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`);
2891
- and a task does not go backwards (`stream.taskUpdated.phase`): the stream sees publications, not
2891
+ a snapshot carrying a task still at work does not forget the audio the stream held up, since media
2892
+ ends on `task-media-ended` and the call moves on (`stream.snapshot.media`); and a task does not go
2893
+ backwards, on an update or on a resync (`stream.taskUpdated.phase`, `stream.snapshot.phase`): the
2894
+ stream sees publications, not
2892
2895
  transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
2893
2896
  with `confirmed` between them, and what is refused is a phase unreachable from the last one read by
2894
2897
  the transition table -- back to `pending`, back to `confirmed` or `preview` once work began, out of
@@ -4781,7 +4784,11 @@ built and connected for the same login with the same context and the same `store
4781
4784
  must carry the task with that leg and the host's word, and the run goes on with the second as its
4782
4785
  connection to the end. A platform that holds the record hands it back, and an adapter that composed
4783
4786
  the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
4784
- `.rejected`). The second adapter is held to what the first was: the same provider
4787
+ `.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
4788
+ before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
4789
+ still at work is named by the stream's own rules (`stream.snapshot.phase`, `.handlingHistory`,
4790
+ `.media`) and a reload is a place those rules keep working, not one where they stop. The second
4791
+ adapter is held to what the first was: the same provider
4785
4792
  (`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
4786
4793
  of the entries the first had published -- a record once read is not unread across a reload either.
4787
4794
  A reconnect on the same adapter object would prove nothing, since an in-process adapter's memory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",