@xema/omni-protocol 0.1.69 → 0.1.71

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
@@ -1397,11 +1397,15 @@ export interface Snapshot<C extends Channel = Channel> {
1397
1397
  /**
1398
1398
  * How many tasks this provider may have allocated to the agent at once.
1399
1399
  *
1400
- * An absolute ceiling, never less than 1, standing until Omni restates it. The provider counts
1401
- * its own outstanding tasks against it and needs no new signal when one ends. What the agent
1402
- * holds at other providers is not this provider's concern -- Omni set `count` knowing it.
1400
+ * An absolute ceiling standing until Omni restates it. The provider counts its own outstanding
1401
+ * tasks against it and needs no new signal when one ends. The agent is one person on several
1402
+ * providers, and the host divides their capacity among them: `0` is host-stopped -- the agent's
1403
+ * capacity is elsewhere for now, this provider allocates nothing and shows the member as
1404
+ * `on-task` -- and is not a break, which is the agent not working at all. The host restates a
1405
+ * count of one or more when this provider has the agent's capacity again.
1403
1406
  */
1404
1407
  export interface AgentCapacity {
1408
+ /** A whole number, zero or more; zero is host-stopped. */
1405
1409
  count: number;
1406
1410
  }
1407
1411
  export interface SummaryMetric {
package/dist/testing.js CHANGED
@@ -168,8 +168,10 @@ export async function exerciseAdapter(adapter, context, options = {}) {
168
168
  set: async (key, value) => { storedSecrets.set(key, value); },
169
169
  delete: async (key) => { storedSecrets.delete(key); },
170
170
  };
171
- const authentication = await adapter.createAuthenticationSession({ ...context, secrets: authenticationSecrets });
171
+ let authentication = await adapter.createAuthenticationSession({ ...context, secrets: authenticationSecrets });
172
172
  let connection;
173
+ /** Whether the run holds a client at all: false between a reload's first client going down and its second standing, and after a reload that failed. */
174
+ let clientLive = true;
173
175
  let unsubscribe;
174
176
  let unsubscribeAuthentication;
175
177
  let unsubscribeHost;
@@ -318,10 +320,9 @@ export async function exerciseAdapter(adapter, context, options = {}) {
318
320
  requireMethod(live, "recordStep", "the login is on a softphone, whose microphone the host mutes");
319
321
  const eventPayloads = new Map();
320
322
  let capacityStated;
321
- const reloading = { now: false };
322
323
  // The drive waits on events: each waiter is offered every envelope as it lands.
323
324
  const waiters = new Set();
324
- unsubscribe = connection.subscribe(envelope => {
325
+ const onEnvelope = (envelope) => {
325
326
  observeEvent(envelope, seen);
326
327
  const broken = validateEventEnvelope(envelope, adapter.manifest, "event", reader());
327
328
  violations.push(...broken);
@@ -334,7 +335,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
334
335
  // client pushes it to the old one too, and an adapter that calls a re-offer of answered work a
335
336
  // defect is right to. That diagnostic is the drive's own artefact, and is not counted while the
336
337
  // second adapter is up; every other moment it is.
337
- if (isRecord(envelope?.event) && envelope.event.type === "diagnostic" && !reloading.now) {
338
+ if (isRecord(envelope?.event) && envelope.event.type === "diagnostic") {
338
339
  violations.push({ rule: "diagnostic.raised", path: "event.diagnostic",
339
340
  message: `the provider reported a diagnostic: expected ${String(envelope.event.expected)}; observed ${String(envelope.event.observed)}` });
340
341
  }
@@ -374,7 +375,45 @@ export async function exerciseAdapter(adapter, context, options = {}) {
374
375
  events.push(envelope);
375
376
  for (const waiter of waiters)
376
377
  waiter(envelope);
377
- });
378
+ };
379
+ unsubscribe = connection.subscribe(onEnvelope);
380
+ // A host reload is the first client dying and a second coming up in its place. The drive hands
381
+ // the run over: the first connection is unsubscribed, disconnected and its session closed before
382
+ // the second connects, and the run continues on the second -- so there is no first client for a
383
+ // platform to push the open task back to, and nothing to exempt.
384
+ const handOver = {
385
+ isLive: () => clientLive,
386
+ down: async () => {
387
+ let clean = true;
388
+ try {
389
+ unsubscribe?.();
390
+ }
391
+ catch {
392
+ clean = false;
393
+ }
394
+ unsubscribe = undefined;
395
+ try {
396
+ await connection?.disconnect();
397
+ }
398
+ catch {
399
+ clean = false;
400
+ }
401
+ try {
402
+ await authentication.close();
403
+ }
404
+ catch {
405
+ clean = false;
406
+ }
407
+ clientLive = false;
408
+ return clean;
409
+ },
410
+ up: (session, second) => {
411
+ authentication = session;
412
+ connection = second;
413
+ unsubscribe = second.subscribe(onEnvelope);
414
+ clientLive = true;
415
+ },
416
+ };
378
417
  const snapshot = await connection.snapshot();
379
418
  observeSnapshot(snapshot, seen);
380
419
  {
@@ -449,9 +488,30 @@ export async function exerciseAdapter(adapter, context, options = {}) {
449
488
  violations.push(...await driveOneCall({
450
489
  connection: live, manifest: adapter.manifest, channel: adapter.manifest.channel, softphone, snapshot, events, waiters, stream, localAudio,
451
490
  timeoutMs: options.driveTimeoutMs ?? 5000,
452
- context: connected, secrets: authenticationSecrets, reader, rebuild: options.rebuild, held: watched.held, reloading,
491
+ context: connected, secrets: authenticationSecrets, reader, rebuild: options.rebuild, held: watched.held, handOver,
492
+ streams: { stream, breaks },
453
493
  }));
454
494
  }
495
+ // Capacity supersedes rather than accumulates, so a decrease is as ordinary as an increase: the
496
+ // host raises it, lowers it, and takes it away, and the provider takes each as the ceiling it is.
497
+ // A provider whose ceiling can only rise passes a zero it special-cases and still cannot go from
498
+ // five to three, so the exercise moves the axis both ways. Zero is host-stopped: the agent's
499
+ // capacity is elsewhere, and the provider allocates nothing and refuses nothing for it. Any offer
500
+ // after a lower count is caught against it (stream.taskOffered.overCapacity).
501
+ ruleEvaluated("connection.setCapacity.lowered", "connection.setCapacity.zero");
502
+ for (const count of clientLive ? [2, 1, 0] : []) {
503
+ capacityStated = count;
504
+ const restated = await connection.setCapacity({ count });
505
+ const shape = validateResult(restated, "setCapacity", "connection.setCapacity");
506
+ violations.push(...shape);
507
+ if (shape.length === 0 && restated.status === "failed") {
508
+ violations.push({ rule: count === 0 ? "connection.setCapacity.zero" : "connection.setCapacity.lowered", path: "connection.setCapacity",
509
+ message: count === 0
510
+ ? `the provider would not take a capacity of zero: ${restated.failure.code}; zero is host-stopped, the agent's capacity being elsewhere, and a provider allocates nothing and refuses nothing for it`
511
+ : `the provider would not take a capacity of ${count} after a higher one: ${restated.failure.code}; capacity supersedes, and a decrease is as ordinary as an increase` });
512
+ break;
513
+ }
514
+ }
455
515
  }
456
516
  finally {
457
517
  let clean = true;
@@ -473,17 +533,20 @@ export async function exerciseAdapter(adapter, context, options = {}) {
473
533
  catch {
474
534
  clean = false;
475
535
  }
476
- try {
477
- await connection?.disconnect();
478
- }
479
- catch {
480
- clean = false;
481
- }
482
- try {
483
- await authentication.close();
484
- }
485
- catch {
486
- clean = false;
536
+ // A run whose reload took the first client down and stood no second holds nothing to close.
537
+ if (clientLive) {
538
+ try {
539
+ await connection?.disconnect();
540
+ }
541
+ catch {
542
+ clean = false;
543
+ }
544
+ try {
545
+ await authentication.close();
546
+ }
547
+ catch {
548
+ clean = false;
549
+ }
487
550
  }
488
551
  disconnectWasClean = clean;
489
552
  }
@@ -1169,7 +1232,7 @@ async function driveOneCall(drive) {
1169
1232
  drive.waiters.add(waiter);
1170
1233
  });
1171
1234
  const recordSurvivesReload = async (at) => {
1172
- ruleEvaluated("drive.reload.rejected", "drive.reload.manifest", "drive.reload.login", "drive.reload.snapshot", "drive.reload.allocation", "drive.reload.history");
1235
+ ruleEvaluated("drive.reload.rejected", "drive.reload.manifest", "drive.reload.login", "drive.reload.snapshot", "drive.reload.allocation", "drive.reload.history", "drive.reload.handover");
1173
1236
  let again;
1174
1237
  try {
1175
1238
  again = drive.rebuild();
@@ -1178,16 +1241,17 @@ async function driveOneCall(drive) {
1178
1241
  refuse("drive.reload.rejected", "drive.reload", `building the adapter again threw: ${String(error)}`);
1179
1242
  return;
1180
1243
  }
1181
- let session;
1182
- let second;
1244
+ // The same provider, built again: a different manifest is a different adapter, and proves nothing about this one.
1245
+ if (again.manifest.id !== drive.manifest.id) {
1246
+ refuse("drive.reload.manifest", "drive.reload.manifest", `rebuild returned an adapter for ${String(again.manifest.id)}; the reload is of ${String(drive.manifest.id)}`);
1247
+ return;
1248
+ }
1249
+ // The first client dies first, as it does on a reload: from here the run has no connection until the second stands.
1250
+ if (!await drive.handOver.down()) {
1251
+ refuse("drive.reload.handover", "drive.reload", "taking the first client down threw: an unsubscribe, disconnect() or close() failed");
1252
+ }
1183
1253
  try {
1184
- // The same provider, built again: a different manifest is a different adapter, and proves nothing about this one.
1185
- if (again.manifest.id !== drive.manifest.id) {
1186
- refuse("drive.reload.manifest", "drive.reload.manifest", `rebuild returned an adapter for ${String(again.manifest.id)}; the reload is of ${String(drive.manifest.id)}`);
1187
- return;
1188
- }
1189
- drive.reloading.now = true;
1190
- session = await again.createAuthenticationSession({ ...drive.context, secrets: drive.secrets });
1254
+ const session = await again.createAuthenticationSession({ ...drive.context, secrets: drive.secrets });
1191
1255
  // The reload is a restore before it is anything else: the second session stands authenticated
1192
1256
  // as the same person, from the secrets alone, or nothing it reads afterwards is this login's.
1193
1257
  const restored = await session.state();
@@ -1195,12 +1259,22 @@ async function driveOneCall(drive) {
1195
1259
  const same = isRecord(restored) && restored.status === "authenticated" && isRecord(restored.identity) && restored.identity.id === drive.reader().self;
1196
1260
  if (!same) {
1197
1261
  refuse("drive.reload.login", "drive.reload.login", `a second adapter built from the same login and secrets did not restore it: the session says ${String(isRecord(restored) ? restored.status : restored)}${isRecord(restored) && isRecord(restored.identity) ? ` as ${String(restored.identity.id)}` : ""}, and the login is ${String(drive.reader().self)}`);
1262
+ // A reload that cannot restore the login is a sign-in screen, not a connection: the run ends here.
1263
+ try {
1264
+ await session.close();
1265
+ }
1266
+ catch { /* the session that would not restore is closed as far as it can be */ }
1198
1267
  return;
1199
1268
  }
1200
- second = await again.connect(drive.context);
1269
+ const second = await again.connect(drive.context);
1270
+ // From here the second client is the run's connection: what it publishes is validated as before.
1271
+ drive.handOver.up(session, second);
1272
+ drive.connection = second;
1201
1273
  const snapshot = await second.snapshot();
1202
- // The second adapter's snapshot is held to everything a first one is.
1274
+ // The second adapter's snapshot is held to everything a first one is, and the streams take it as the state now.
1203
1275
  found.push(...validateSnapshot(snapshot, again.manifest, "drive.reload.snapshot", drive.reader()));
1276
+ drive.streams.stream.seed(snapshot);
1277
+ drive.streams.breaks.seed(snapshot);
1204
1278
  const carried = isRecord(snapshot) && Array.isArray(snapshot.tasks)
1205
1279
  ? snapshot.tasks.find(task => isRecord(task) && task.id === taskId) : undefined;
1206
1280
  if (carried === undefined) {
@@ -1228,25 +1302,12 @@ async function driveOneCall(drive) {
1228
1302
  else if (leg.mutedBy !== "host") {
1229
1303
  refuse("drive.reload.history", "drive.reload.history", `the reloaded record's muted leg at ${at} says mutedBy ${String(leg.mutedBy)}; the host reported host`);
1230
1304
  }
1305
+ // The drive goes on with the task as the reloaded client holds it.
1306
+ task = carried;
1231
1307
  }
1232
1308
  catch (error) {
1233
1309
  refuse("drive.reload.rejected", "drive.reload", `the second adapter rejected rather than answered: ${String(error)}`);
1234
1310
  }
1235
- finally {
1236
- try {
1237
- await second?.disconnect();
1238
- }
1239
- catch (error) {
1240
- refuse("drive.reload.rejected", "drive.reload", `the second adapter's disconnect threw: ${String(error)}`);
1241
- }
1242
- try {
1243
- await session?.close();
1244
- }
1245
- catch (error) {
1246
- refuse("drive.reload.rejected", "drive.reload", `the second adapter's session threw on close: ${String(error)}`);
1247
- }
1248
- drive.reloading.now = false;
1249
- }
1250
1311
  };
1251
1312
  // A task's keys are gone with the task. A task-scoped key carries the task id, so the watched
1252
1313
  // store says which keys were the task's; one still held after task-ended is a key about to be
@@ -1438,8 +1499,12 @@ async function driveOneCall(drive) {
1438
1499
  // 3c. A host reload destroys the adapter object and keeps the login's store. Built again from
1439
1500
  // the same login, a second adapter carries the task and the leg -- from its platform, or from
1440
1501
  // the store -- or it composed the record in memory and the record died with it.
1441
- if (drive.rebuild !== undefined)
1502
+ if (drive.rebuild !== undefined) {
1442
1503
  await recordSurvivesReload(at);
1504
+ // A reload that stood no client ends the run: there is nothing left to drive.
1505
+ if (!drive.handOver.isLive())
1506
+ return found;
1507
+ }
1443
1508
  }
1444
1509
  // 4. Hold and resume, where offered.
1445
1510
  if (latestTask().phase === "in-progress" && offers("hold")) {
@@ -64,6 +64,8 @@ export declare function validateEventEnvelope(envelope: unknown, manifest: unkno
64
64
  * phone the microphone is the phone's, and off voice there is none. Neither the value nor its
65
65
  * absence is inferred from what kind of application the host is.
66
66
  */
67
+ /** What the host states as capacity: a whole number, zero or more, zero being host-stopped. */
68
+ export declare function validateCapacity(capacity: unknown, path?: string): ProtocolViolation[];
67
69
  /** The login's store, which the host provides on every connection: three functions, and nothing else is a store. */
68
70
  export declare function validateLoginStore(store: unknown, path?: string): ProtocolViolation[];
69
71
  export declare function validateHostMute(mute: unknown, softphone: boolean, path?: string): ProtocolViolation[];
@@ -1700,6 +1700,16 @@ const HOST_MUTES = membersOf({ stream: true, station: true });
1700
1700
  * phone the microphone is the phone's, and off voice there is none. Neither the value nor its
1701
1701
  * absence is inferred from what kind of application the host is.
1702
1702
  */
1703
+ /** What the host states as capacity: a whole number, zero or more, zero being host-stopped. */
1704
+ export function validateCapacity(capacity, path = "capacity") {
1705
+ const into = new Collector();
1706
+ if (!isPlainObject(capacity)) {
1707
+ into.add("capacity.shape", path, "a capacity is an object carrying count");
1708
+ return into.violations;
1709
+ }
1710
+ into.require(typeof capacity.count === "number" && Number.isInteger(capacity.count) && capacity.count >= 0, "capacity.count", `${path}.count`, "count is a whole number, zero or more: how many tasks this provider may allocate at once, zero while the agent's capacity is elsewhere");
1711
+ return into.violations;
1712
+ }
1703
1713
  /** The login's store, which the host provides on every connection: three functions, and nothing else is a store. */
1704
1714
  export function validateLoginStore(store, path = "store") {
1705
1715
  const into = new Collector();
package/guide.md CHANGED
@@ -425,7 +425,7 @@ type Snapshot = {
425
425
  };
426
426
 
427
427
  type AgentCapacity = {
428
- count: number; // absolute ceiling, at least 1
428
+ count: number; // absolute ceiling, zero or more; zero is host-stopped
429
429
  };
430
430
 
431
431
  type CapacityResult =
@@ -3370,9 +3370,10 @@ hold applied. A `held` result would repeat the discriminant that travelled with
3370
3370
 
3371
3371
  States how many tasks this provider may have allocated to the agent **at once**.
3372
3372
 
3373
- `count` is an absolute ceiling, not an increment and never less than 1. An agent's capacity is a
3374
- property of the agent, not of the moment: it is stated when the agent is set up and restated only
3375
- when it genuinely changes, which is a provisioning change rather than a task starting or ending.
3373
+ `count` is an absolute ceiling, not an increment, a whole number of zero or more
3374
+ (`capacity.count`). An agent's capacity is a property of the agent, not of the moment: it is
3375
+ stated when the agent is set up and restated only when it genuinely changes, which is a
3376
+ provisioning change rather than a task starting or ending -- with one exception below.
3376
3377
 
3377
3378
  **The provider counts its own outstanding tasks against it.** Allocate while you hold fewer than
3378
3379
  `count` tasks for this agent, and stop when you hold that many; when one of yours ends you have
@@ -3380,9 +3381,21 @@ room again and need no new signal to know it. Omni does not re-state capacity as
3380
3381
  go, and a provider that waits for it will stall.
3381
3382
 
3382
3383
  Your own tasks are the only ones you count. What the agent holds at other providers is not your
3383
- concern — Omni set `count` knowing it.
3384
-
3385
- Capacity supersedes rather than accumulates: the latest value is the ceiling.
3384
+ concern — Omni set `count` knowing it, and this is how: the agent is one person on several
3385
+ providers, and the host divides their capacity among them rather than telling each the whole. A
3386
+ provider that has none of it for now is told **`count: 0`, host-stopped**: allocate nothing, show
3387
+ the member as `on-task` on the roster, since the agent is working, and take the next count as any
3388
+ other when the host has capacity for this provider again. Zero is not a refusal to state, and a
3389
+ provider that answers it `failed` is named (`connection.setCapacity.zero`); it is the one restatement
3390
+ that follows work rather than provisioning.
3391
+
3392
+ Capacity supersedes rather than accumulates: the latest value is the ceiling, and a decrease is as
3393
+ ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
3394
+ it was ever told, or returns early on a small one -- cannot be told to take less work, and a host
3395
+ taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
3396
+ both ways after the drive, two then one then nought, and names a provider that will not come down
3397
+ (`connection.setCapacity.lowered`, `connection.setCapacity.zero`); an offer after a lower count is
3398
+ caught against it (`stream.taskOffered.overCapacity`).
3386
3399
 
3387
3400
  **Capacity gates what the provider allocates, not what the agent starts.** A call placed from the
3388
3401
  idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
@@ -3974,10 +3987,12 @@ anything to become available.
3974
3987
  | Omni commits a break | `commitBreak`. The break stops allocation, not the ceiling. |
3975
3988
  | Agent returns from break | `endBreak` |
3976
3989
 
3977
- **Stopping is a break, not a capacity of zero.** Capacity says how much this agent can carry at
3978
- once; a break says they are not working. Collapsing the two would leave a provider unable to tell
3979
- an agent at their limit from an agent who has gone to lunch, and only one of those needs a reason,
3980
- a decision and a return.
3990
+ **A break is not a capacity of zero, and neither is the reverse.** Capacity says how much of this
3991
+ agent this provider may carry; a break says they are not working at all. A provider told
3992
+ `count: 0` shows an agent whose capacity is elsewhere, `on-task`; a provider told a break shows
3993
+ `on-break`. Collapsing the two would leave a provider unable to tell an agent working on another
3994
+ provider from an agent who has gone to lunch, and only one of those needs a reason, a decision and
3995
+ a return.
3981
3996
 
3982
3997
  ### How the agent hears the call
3983
3998
 
@@ -4587,6 +4602,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
4587
4602
  | `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
4588
4603
  | `validateHostMute(mute, softphone)` | What the host's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the host holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
4589
4604
  | `validateLoginStore(store)` | The login's store the host hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
4605
+ | `validateCapacity(capacity)` | What the host states as capacity: a whole number of zero or more (`capacity.count`), zero being host-stopped. The harness states one on connect and two, one and zero after the drive, and names a provider that will not come down (`connection.setCapacity.lowered`) or take zero (`connection.setCapacity.zero`). |
4590
4606
  | `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
4591
4607
  | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.handling` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
4592
4608
  | `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
@@ -4625,7 +4641,9 @@ A refusal has an aftermath on the desk and a report to the provider, and both ar
4625
4641
  vanish from the desk while the call is up.
4626
4642
  - **A refused event is dropped and counted.** The state the host holds does not move for it. The
4627
4643
  provider is told, and may republish a corrected state or raise a `diagnostic`; the host retries
4628
- nothing.
4644
+ nothing. The desk shows nothing for it: the view kept moving for every other event, so "last
4645
+ update refused" would be false of it, and a chip that said nothing would be right about the
4646
+ transport. Counted, and the provider told, is all a host can truthfully do for one event.
4629
4647
  - **The provider is told, every time.** `refused(report)` carries the artefact, the envelope id for
4630
4648
  an event, and every violation with its rule and path. An adapter logs it at error and treats it as
4631
4649
  its own defect until shown otherwise: what the host refused never reached the agent, and a
@@ -4694,15 +4712,19 @@ the drive mutes it for one second and reports the leg through `recordStep`, begu
4694
4712
  expecting each report `recorded` (`drive.recordStep.failed`, `.rejected`); and where the provider
4695
4713
  restates the task's record afterwards, the leg is in it or the hole is named
4696
4714
  (`drive.recordStep.history`). Given `rebuild`, a way to build the adapter again as a host reload
4697
- does, the drive then connects a second adapter for the same login with the same context and the
4698
- same `store`, and expects its snapshot to carry the task with that leg and the host's word: a
4699
- platform that holds the record hands it back, and an adapter that composed the record in memory
4700
- has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`, `.rejected`). The second
4701
- adapter is held to what the first was: the same provider (`drive.reload.manifest`), a snapshot that
4702
- stands as any snapshot must, and a record that lost none of the entries the first had published --
4703
- a record once read is not unread across a reload either. A reconnect on the same adapter object
4704
- would prove nothing, since an in-process adapter's memory survives it; only a second object
4705
- separates kept-in-the-store from never-lost. The second adapter opens its session from the same
4715
+ does, the drive reloads the host as a reload happens: the first client is unsubscribed,
4716
+ disconnected and its session closed (`drive.reload.handover`), and only then is a second adapter
4717
+ built and connected for the same login with the same context and the same `store`; its snapshot
4718
+ must carry the task with that leg and the host's word, and the run goes on with the second as its
4719
+ connection to the end. A platform that holds the record hands it back, and an adapter that composed
4720
+ the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
4721
+ `.rejected`). The second adapter is held to what the first was: the same provider
4722
+ (`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
4723
+ of the entries the first had published -- a record once read is not unread across a reload either.
4724
+ A reconnect on the same adapter object would prove nothing, since an in-process adapter's memory
4725
+ survives it; only a second object separates kept-in-the-store from never-lost. And because the
4726
+ first client is gone before the second connects, there is no client for the platform to push the
4727
+ open task back to, and nothing about the reload is exempt from any rule. The second adapter opens its session from the same
4706
4728
  `secrets`, so the reload is a restore before it is anything else: an adapter that holds a session
4707
4729
  has already put what would rebuild it into the secrets store, from the moment it was handed one,
4708
4730
  not only when a flow completes -- the session is the adapter's, the store is the host's, and a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.69",
3
+ "version": "0.1.71",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",