@xema/omni-protocol 0.1.7 → 0.1.9

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.
Files changed (3) hide show
  1. package/README.md +17 -2
  2. package/guide.md +65 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -59,9 +59,11 @@ assignments, break denial and retry, wrap timeout, browser isolation.
59
59
  > conforming one. A suite that only asserts "this conforming case does not throw" passes unchanged
60
60
  > if the helper is gutted, so pair every positive case with the violating twin.
61
61
 
62
- ## Two things TypeScript will not catch for you
62
+ ## Three things TypeScript will not catch for you
63
63
 
64
- Both found by adapters against this contract, and both produce a green build over a wrong shape.
64
+ All found by adapters against this contract, and all produce a green build over a wrong shape.
65
+ Two share one cause: TypeScript checks extra keys only on a literal that is the thing directly
66
+ assigned. Move the literal anywhere else and the check is gone.
65
67
 
66
68
  **Conditional spreads are the blind spot on a task literal.** A key inside
67
69
  `...(cond ? { … } : {})` is never checked against the task type, and `satisfies Task<C>` on the
@@ -88,6 +90,19 @@ switch (command.action) {
88
90
  }
89
91
  ```
90
92
 
93
+ **A `const` fixture escapes excess-property checking.** Park a literal in a variable and it is no
94
+ longer the thing directly assigned — the same reason the conditional spread escapes — so a shared
95
+ test fixture keeps a field the contract has dropped: `tsc` says nothing and the suite is
96
+ confidently green over a shape that no longer exists. Annotate the `const` or `satisfies` it where
97
+ it is declared; either names the field on the next build.
98
+
99
+ ```ts
100
+ take({ reasonId, requestedAt }); // error: requestedAt
101
+ const request = { reasonId, requestedAt }; take(request); // no error — the hole
102
+ const request: BreakRequest = { reasonId, requestedAt }; // error: requestedAt
103
+ const request = { reasonId, requestedAt } satisfies BreakRequest; // error: requestedAt
104
+ ```
105
+
91
106
  ## Building
92
107
 
93
108
  ```
package/guide.md CHANGED
@@ -914,10 +914,22 @@ is applied.
914
914
 
915
915
  ### 6. An unsettled result is unknown, and unknown is not retried
916
916
 
917
- A settled result is a fact. A promise that rejects with no result the transport died — means
918
- *unknown*: the provider may have done it or not, and neither Omni nor an adapter on the agent's PC
919
- can find out in time to make a repeat safe. Omni does not retry. The next snapshot shows what the
920
- provider actually did, and if the agent acts again it is a new command.
917
+ A settled result is a fact. A promise that rejects with no result means *unknown*: the provider
918
+ may have done it or not, and neither Omni nor an adapter on the agent's PC can find out in time to
919
+ make a repeat safe. Omni does not retry; if the agent acts again it is a new command.
920
+
921
+ On a persistent ordered transport there is only one way a command goes unsettled: the connection
922
+ went away underneath it. **An adapter that cannot settle a command has lost its transport, and
923
+ says so** — `provider-status` `connecting`, reconnect, snapshot — whichever channel the command
924
+ actually travelled on. An unsettled promise is therefore always followed by a snapshot, and that
925
+ snapshot is the answer; Omni waits for it rather than calling `snapshot()` itself. While the
926
+ transport is up, a result says the provider accepted the command, and the event that follows —
927
+ `task-updated`, `break-state` — says what it did.
928
+
929
+ **Classify on the rejection, never on a connection status published separately from it.** The
930
+ status is a report about the wire and races the rejection; the rejection is the event. A rejection
931
+ is the provider's answer only when it carries the provider's answer — a failure the provider
932
+ named. Every other rejection is transport loss, whatever the last `provider-status` said.
921
933
 
922
934
  A command therefore carries no key. The provider names its own records — a task, a lead request, a
923
935
  member — and Omni refers to them by those names; **Omni never asks a provider to remember a name
@@ -934,6 +946,11 @@ and the agent is on it; had nothing been placed, nothing arrives and the agent d
934
946
  Omni must not do is dial again on the agent's behalf — the one outcome worse than a lost answer is
935
947
  two phones ringing at the customer.
936
948
 
949
+ An agent presses Hold while the provider is reconnecting. Nothing is queued at either end: the
950
+ adapter answers `failed` with `omni.unavailable`, Omni shows the refusal, and the agent presses
951
+ again once the provider is `active` — against the state as it is then, rather than a held press
952
+ fired into a state that has moved on.
953
+
937
954
  ### 7. Work is pulled, never pushed
938
955
 
939
956
  Allocate only within the concurrent capacity Omni has stated for this agent.
@@ -2207,8 +2224,8 @@ answer and ask again when they want to. `decisionReason` may carry the words att
2207
2224
  decision, but `approval` does not remain denied.
2208
2225
 
2209
2226
  A provider reports `starting-after-task` only after Omni commits a `granted` request while
2210
- work is still active. Omni does not retry the original request, because asking again would not move
2211
- it; it retries the commit when its delivery is uncertain.
2227
+ work is still active. Omni does not send the request again, because asking again would not move
2228
+ it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
2212
2229
 
2213
2230
  `accepting: false` is what lets Omni withdraw the control rather than let an agent ask and be
2214
2231
  refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
@@ -2425,10 +2442,13 @@ Omni coordinates one attempt as follows:
2425
2442
  causes Omni to take the cancel path.
2426
2443
  4. If every participant reports `granted`, durably choose commit, enter `committing-break`,
2427
2444
  and send `commitBreak()` to every participant. A provider then stops offering new work
2428
- and reports `starting-after-task` or `in-effect`. Omni enters `on-break` once every participant
2429
- it can still reach reports `in-effect`, and no later than the **commit bound** ten seconds
2430
- from the decision, tunable per deployment. A participant that has not applied the commit by then
2431
- is set aside as unreconciled; the break begins without it.
2445
+ and reports `starting-after-task` or `in-effect`. The **commit bound** ten seconds from the
2446
+ decision, tunable per deployment decides who is kept: a participant that has not applied the
2447
+ commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
2448
+ begins without it. `in-effect` decides `on-break`: Omni enters it once every kept participant
2449
+ reports `in-effect`. A kept participant reporting `starting-after-task` has applied the commit
2450
+ and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
2451
+ settled and beginning when the task ends, and offers no cancel, because the commit is durable.
2432
2452
  5. If any participant fails or denies the request, cannot be reconciled within the bounded
2433
2453
  decision timeout, or the agent cancels before commit, durably choose cancel and enter
2434
2454
  `cancelling-break`. Send `cancelBreak()` to every participant still reporting
@@ -2467,7 +2487,13 @@ snapshot before anything else, and the snapshot decides:
2467
2487
  against an agent who is already on break elsewhere. **A new login is this case too**: the grant
2468
2488
  belonged to the old session.
2469
2489
 
2470
- The provider must not offer work in the meantime, and the commit is what stops it.
2490
+ The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
2491
+ no capacity until it is reconciled.
2492
+
2493
+ **If the agent has already ended the break elsewhere, the attempt is over** and the returning
2494
+ provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
2495
+ would stop an agent who is working again; `starting-after-task` or `in-effect` gets `endBreak()`.
2496
+ Never rolling back is about a break that is still on, not one the agent has finished.
2471
2497
 
2472
2498
  Omni may tell the agent which platforms the break has not yet reached, as it already does when a
2473
2499
  break cannot be paired across every provider.
@@ -2564,6 +2590,15 @@ decides who leads a team: no roster means nothing is shown, which is the correct
2564
2590
  agent who leads nobody. The same rule governs `TeamRoster.breakControl` — present when this lead
2565
2591
  decides their team's breaks, absent when they do not.
2566
2592
 
2593
+ **The roster never carries the agent it is published to — not in `members`, and not in
2594
+ `requests`.** A lead does not report to themself: their own break request and their own ask for a
2595
+ lead go up to whoever leads them and appear on *that* person's roster, while the requester sees
2596
+ only their own `BreakState` and their task's `lead` move. An adapter whose platform lists the lead
2597
+ among their own members filters the signed-in identity out before publishing. **Entitlement is a
2598
+ role the provider knows, never inferred from who is listed:** a lead with nobody in their team
2599
+ publishes `[]`, an agent with no such role publishes nothing, and no member count can tell those
2600
+ two apart.
2601
+
2567
2602
  ### Lead commands
2568
2603
 
2569
2604
  One method, `executeTeamBreak`, taking a discriminated command exactly as `execute` takes a
@@ -2833,10 +2868,18 @@ Applies a `TaskCommandRequest` to one provider-local task.
2833
2868
  - `applied` confirms the side effect completed. `failed` confirms it did **not**, with a typed
2834
2869
  `ProtocolFailure`; a provider that will not and one that cannot report the same shape, and `code`
2835
2870
  says which.
2871
+ - A command sent while `provider-status` is not `active` answers `failed` with `omni.unavailable`.
2872
+ Neither Omni nor the adapter queues it.
2873
+ - **A command that asks for a state answers `applied` when that state holds, whoever brought it
2874
+ about; a command that acts answers `failed` when it cannot act.** Declining a lead request
2875
+ already gone is `applied`; joining one already gone is `failed`, since nobody joined. A lead
2876
+ deciding a member's break that another lead has already decided is `applied` when the decisions
2877
+ agree and `failed`, saying so in `message`, when they differ. `commitBreak()` on a break already
2878
+ in effect is `committed` for the same reason.
2836
2879
  - **A settled result is a fact; an unsettled promise is not.** Transport uncertainty may reject the
2837
- promise with no result at all, and that means *unknown*, not *failed*. `failed` must never be
2838
- returned for something the provider is unsure of, because Omni will show the agent it did not
2839
- happen.
2880
+ promise with no result at all, and that means *unknown*, not *failed*, and a snapshot follows —
2881
+ see **An unsettled result is unknown**. `failed` must never be returned for something the
2882
+ provider is unsure of, because Omni will show the agent it did not happen.
2840
2883
 
2841
2884
  ### `ProtocolFailure`
2842
2885
 
@@ -2858,7 +2901,7 @@ react rather than only display the message:
2858
2901
  | `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
2859
2902
  | `omni.destination-not-permitted` | The dial or transfer destination violates the provider's policy. |
2860
2903
  | `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
2861
- | `omni.unavailable` | The provider is temporarily unable to serve the action. |
2904
+ | `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `provider-status` is not `active`. |
2862
2905
  | `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
2863
2906
 
2864
2907
  They are published as `OMNI_FAILURE_CODES`.
@@ -2915,9 +2958,10 @@ healthy provider from a dead one.
2915
2958
 
2916
2959
  #### Requesting a resync
2917
2960
 
2918
- Omni may call `snapshot()` at any time, not only at connect, and must do so on any loss of
2919
- confidence in its provider state. `reason: "provider-requested"` covers the
2920
- opposite direction — the provider asking Omni to reconcile and neither replaces the other.
2961
+ Omni calls `snapshot()` at connect; after that, snapshots come to it on reconnect, and when the
2962
+ provider asks. It may still call `snapshot()` at any time, but never to learn a command's fate:
2963
+ the reconnect snapshot already carries it. `reason: "provider-requested"` covers the opposite
2964
+ direction — the provider asking Omni to reconcile — and neither replaces the other.
2921
2965
 
2922
2966
  ### `snapshot`
2923
2967
 
@@ -2959,9 +3003,9 @@ reasons, retry details, and any imposed break.
2959
3003
  For a multi-provider attempt, "every provider" is the participant set frozen when the attempt
2960
3004
  entered `requesting-break`. Omni commits only after every participant reports `granted` —
2961
3005
  that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
2962
- `on-break` once every participant it can still reach reports `in-effect`, and no later than the
2963
- commit bound: past that a participant is set aside as unreconciled rather than holding a break that
2964
- has already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
3006
+ `on-break` once every kept participant reports `in-effect`; the commit bound decides who is kept,
3007
+ setting aside a participant that has not applied the commit rather than holding a break that has
3008
+ already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
2965
3009
  multi-provider break**.
2966
3010
 
2967
3011
  ### `task-offered`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",