@xema/omni-protocol 0.1.77 → 0.1.79

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/guide.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  This document is the normative contract between Omni Agent and a provider adapter. It describes
4
4
  observable behavior in addition to TypeScript shapes. When an example and a type declaration
5
- appear to disagree, the exported TypeScript declaration is authoritative.
5
+ appear to disagree, the exported TypeScript declaration is authoritative over the example; when a
6
+ declaration's comment and this document's prose disagree, the prose is authoritative and the
7
+ comment is a defect.
6
8
 
7
9
  All protocol definitions and code examples are written as valid TypeScript. Samples should name
8
10
  their contract type or use `satisfies` so the relationship between the example and the contract is
@@ -163,7 +165,7 @@ type BrowserAccess = {
163
165
 
164
166
  type PersonalBrowserCapability = {
165
167
  access: BrowserAccess;
166
- accessAppliesTo?: "initial-url" | "all-navigation";
168
+ accessAppliesTo: "initial-url" | "all-navigation";
167
169
  };
168
170
 
169
171
  type DialDestinations = "contacts-only" | "any-number";
@@ -429,9 +431,7 @@ type AgentCapacity = {
429
431
  count: number; // absolute ceiling, zero or more; zero is host-stopped
430
432
  };
431
433
 
432
- type CapacityResult =
433
- | { status: "applied" }
434
- | { status: "failed"; failure: ProtocolFailure };
434
+ type CapacityResult = { status: "applied" };
435
435
  ```
436
436
 
437
437
  ### Idle contributions
@@ -489,7 +489,7 @@ type CustomCapability = {
489
489
  control: "button" | "toggle" | "menu-item";
490
490
  label: string;
491
491
  placement: "primary" | "secondary" | "overflow";
492
- render?: "inline" | "page";
492
+ render: "inline" | "page";
493
493
  };
494
494
  prompt?: { fields: CredentialField[] };
495
495
  };
@@ -630,12 +630,10 @@ type OnCall = { since: IsoTimestamp; held?: true } & (
630
630
  | { role: "consulted" | "conferenced"; destinationId: string; stage: OnCallStage; dialId?: DialId; label?: string }
631
631
  );
632
632
 
633
- type TaskLeadAssist = {
634
- stage: "requested" | "joined";
635
- leadId?: UserId;
636
- note?: string;
637
- since: IsoTimestamp;
638
- };
633
+ type TaskLeadAssist = { note?: string; since: IsoTimestamp } & (
634
+ | { stage: "requested"; leadId?: never }
635
+ | { stage: "joined"; leadId: UserId }
636
+ );
639
637
 
640
638
  type TaskAssisting = {
641
639
  memberId: UserId;
@@ -702,6 +700,7 @@ type AcceptanceMode =
702
700
  type TaskOutcome =
703
701
  | { type: "completed"; by: "agent" | "provider" }
704
702
  | { type: "transferred"; destinationId?: string }
703
+ | { type: "taken-over"; leadId: UserId }
705
704
  | { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
706
705
  | { type: "expired"; phase: "pending" | "confirmed" | "preview" }
707
706
  | { type: "left" }
@@ -924,13 +923,13 @@ type BreakEndResult =
924
923
  ### Team
925
924
 
926
925
  ```ts
927
- type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "signed-out";
926
+ type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "reserved" | "signed-out";
928
927
 
929
928
  type TeamMember = {
930
929
  id: UserId;
931
930
  availability: TeamMemberAvailability;
932
931
  since?: IsoTimestamp;
933
- break?: BreakApproval;
932
+ break?: Extract<BreakApproval, "awaiting-decision" | "granted" | "starting-after-task">;
934
933
  };
935
934
 
936
935
  type LeadRequest = {
@@ -976,7 +975,7 @@ type TeamLeadAssistCommand =
976
975
  type TeamBreakCommand =
977
976
  | { type: "decide"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
978
977
  | { type: "policy"; policy: "ask" | "auto-approve" | "suspended" }
979
- | { type: "place"; memberId: UserId; reason?: string }
978
+ | { type: "place"; memberId: UserId; reasonId?: string; reason?: string }
980
979
  | { type: "release"; memberId: UserId };
981
980
 
982
981
  type TeamBreakCommandRequest = {
@@ -1307,7 +1306,7 @@ dropped. An adapter that wanted only a better noun loses a field it never meant
1307
1306
 
1308
1307
  ## Protocol contract rules
1309
1308
 
1310
- Nine rules govern protocol data and behavior.
1309
+ Ten rules govern protocol data and behavior.
1311
1310
 
1312
1311
  ### 1. The protocol is authoritative
1313
1312
 
@@ -1378,8 +1377,9 @@ transport is up, a result says the provider accepted the command, and the event
1378
1377
 
1379
1378
  **Classify on the rejection, never on a connection status published separately from it.** The
1380
1379
  status is a report about the wire and races the rejection; the rejection is the event. A rejection
1381
- is the provider's answer only when it carries the provider's answer — a failure the provider
1382
- named. Every other rejection is transport loss, whatever the last `transport-status` said.
1380
+ is never the provider's answer: an answer travels only as a resolved `{ status: "failed", failure }`,
1381
+ and every rejection is transport loss, whatever the last `transport-status` said and whatever the
1382
+ rejection carries.
1383
1383
 
1384
1384
  A command therefore carries no key. The provider names its own records — a task, a lead request, a
1385
1385
  member — and Omni refers to them by those names; **Omni never asks a provider to remember a name
@@ -1515,7 +1515,7 @@ compile time.
1515
1515
  | `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the host) and/or `deskPhone` (a handset the platform rings; the host shows the call and opens nothing). The host picks one per login. See **How the agent hears the call**. |
1516
1516
  | `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that transfer, conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
1517
1517
  | `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
1518
- | `disposalSettleMs` | Required. How long after an applied disposal -- `complete`, `transfer` `complete`, `lead-assist` `take-over` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.disposalSettleMs`). Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1518
+ | `disposalSettleMs` | Required. How long after an applied disposal -- `complete`, or a lead's `take-over` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.disposalSettleMs`). A warm transfer's `complete` is not a disposal: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
1519
1519
 
1520
1520
  ### Authentication methods
1521
1521
 
@@ -1615,13 +1615,13 @@ idleCapabilities: {
1615
1615
  | `access.mode` | `allow-all` permits unmatched URLs; `block-all` denies unmatched URLs. |
1616
1616
  | `access.allowList` | URL-pattern exceptions permitted when the mode is `block-all`. |
1617
1617
  | `access.blockList` | Explicit denials. A match takes precedence over the same policy's allow list and mode. |
1618
- | `accessAppliesTo` | `all-navigation` by default: every redirect and navigation is checked. `initial-url` checks the starting URL alone. |
1618
+ | `accessAppliesTo` | Required. `all-navigation`: every redirect and navigation is checked. `initial-url`: the starting URL alone. Stated, never defaulted (`manifest.personalBrowser.accessAppliesTo`). |
1619
1619
 
1620
1620
  Patterns use the standard `URLPattern` syntax. Omni owns browser navigation, and the browser is
1621
1621
  hidden when no active provider contributes one.
1622
1622
 
1623
- `accessAppliesTo` defaults to `all-navigation`: every redirect and subsequent navigation is
1624
- validated against the current combined policy, not only the starting URL. A provider may set
1623
+ `accessAppliesTo: "all-navigation"` has every redirect and subsequent navigation validated
1624
+ against the current combined policy, not only the starting URL. A provider may set
1625
1625
  `initial-url` to check the first hop alone, but that has to be asked for. A `block-all` policy
1626
1626
  enforced only on the initial URL stops nothing — one redirect leaves it — so the permissive
1627
1627
  reading is not something to inherit from a default.
@@ -1807,8 +1807,11 @@ login fix, rendered apart from a provider Omni cannot reach. Commands meanwhile
1807
1807
  kept, under the same `loginId` — the old `flowId` died with the expiry, so the adapter issues a
1808
1808
  new challenge — and when the state returns to `authenticated` the connection and everything on it
1809
1809
  carry on: Omni does not call `connect()` again, since a second connection would be a second
1810
- session for one agent. What "signing in again replaces the login" describes is a new
1811
- `AuthenticationSession` under a new `loginId`, after `signed-out`.
1810
+ session for one agent. The exception is a transport that reported `error` with `recovery:
1811
+ "reauthenticate"`: that connection is finished, and once the login is restored Omni disposes it and
1812
+ calls `connect()` afresh, as for `reconnect` -- see **`transport-status`**. What "signing in again
1813
+ replaces the login" describes is a new `AuthenticationSession` under a new `loginId`, after
1814
+ `signed-out`.
1812
1815
 
1813
1816
  ### What the login may do
1814
1817
 
@@ -1960,7 +1963,10 @@ Creates one live provider connection for the signed-in agent.
1960
1963
  - The returned connection owns reconnect until Omni calls `disconnect()` or aborts `context.signal`.
1961
1964
  - May be called again on the same login after that: once per `Connection`, not once per login.
1962
1965
  Omni disposes a connection whose `error` named `recovery: "reconnect"` with `disconnect()` and
1963
- calls `connect()` afresh — see **`transport-status`**.
1966
+ calls `connect()` afresh — see **`transport-status`**. `recovery: "reauthenticate"` ends the same
1967
+ way, after the authentication flow has run on the same session and `loginId`: once the state
1968
+ returns to `authenticated`, Omni disposes the errored connection and calls `connect()` afresh.
1969
+ `expired` alone, with the transport not in `error`, keeps the connection.
1964
1970
 
1965
1971
  ### `ConnectContext`
1966
1972
 
@@ -2048,7 +2054,7 @@ surface in one place, and what obliges an adapter to implement each one.
2048
2054
  | `refused(report)` | Always. The host tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the host does with what it refuses**. |
2049
2055
  | `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
2050
2056
  | `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
2051
- | `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. Each `User` carries its `timeZone` where the provider knows it. |
2057
+ | `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. Each `User` carries its `timeZone`; a person whose zone the provider cannot name is omitted from the answer, as any unresolvable id is. |
2052
2058
  | `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
2053
2059
  | `requestBreak(request)` | The login declares `capabilities.breaks`. |
2054
2060
  | `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
@@ -2137,13 +2143,15 @@ Stops the connection and releases adapter-owned resources.
2137
2143
 
2138
2144
  The task-allocation lifecycle has five ordered stages:
2139
2145
 
2140
- **1. The agent signs in.** Omni initially places the agent in `not-ready`. The provisioning file's
2141
- `readyOnLogin` flag determines whether Omni transitions them to `ready` immediately and defaults to
2142
- `true`. A provider must allocate nothing while the agent remains `not-ready`.
2146
+ **1. The agent signs in.** Nothing on the wire says ready or not-ready: the agent is not ready
2147
+ until the host has stated a capacity, and ready once it has. A provider allocates nothing before a
2148
+ capacity is stated, and needs no other word for it.
2143
2149
 
2144
- **2. The agent becomes ready.** With `readyOnLogin: true`, Omni makes the transition automatically.
2145
- Otherwise, the agent explicitly signals that they are ready to take work. A successful connection,
2146
- a healthy provider, or the absence of a break does not imply readiness.
2150
+ **2. The agent becomes ready.** The host states the capacity when its provisioning says: at once,
2151
+ or when the agent presses Ready. A successful connection, a healthy provider, or the absence of a
2152
+ break does not imply readiness. After that, the only way an agent stops taking work is a break,
2153
+ under `capabilities.breaks`; a login that declares no breaks cannot go not-ready once its capacity
2154
+ is stated, and `count: 0` is the host's division of capacity, not the agent's choice.
2147
2155
 
2148
2156
  **3. Omni states their concurrent capacity.** Only now does Omni ask providers for work, saying how
2149
2157
  much the agent can take at once. It is a standing declaration rather than a poll: Omni restates the
@@ -2151,7 +2159,10 @@ capacity every time it changes, in either direction, and does not ask again whil
2151
2159
  Silence keeps the last one in force, so a provider that waits to be asked a second time will never
2152
2160
  deliver again once it has gone quiet. Hold the capacity and deliver when work arrives.
2153
2161
 
2154
- **4. A provider offers a task.** The provider emits `task-offered` within the stated capacity.
2162
+ **4. A provider offers a task.** The provider emits `task-offered` within the stated capacity, and
2163
+ the task is `pending`: an offer introduces work nobody has accepted, so it is the only phase an
2164
+ offer introduces (`event.taskOffered.phase`). `confirmed`, `preview` and the rest are reached from
2165
+ it, or carried on a snapshot.
2155
2166
 
2156
2167
  **5. Omni decides how the task is accepted.** When `autoAcceptTasks` is `false`, every task requires
2157
2168
  agent acceptance. When it is `true`, the pending task's `acceptance` states the provider's
@@ -2273,7 +2284,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2273
2284
  | `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
2274
2285
  | `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
2275
2286
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
2276
- | `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. Media names a task whose work has begun: on a `pending`, `confirmed` or `preview` task it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it. |
2287
+ | `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. Media names a task whose work has begun, or whose party the host is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it; with the party ringing by a host dial, ring-back is audio and media starts on `dialling`. |
2277
2288
  | `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
2278
2289
  | `previewEndsAt` | Voice only, in `preview`: when the system stops waiting for the agent to press Call. Absent, the agent has as long as they need. Always with `atDeadline`. See **Preview: the agent presses Call**. |
2279
2290
  | `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` places the call itself, `expires` takes the record back and the task ends `expired`. |
@@ -2377,8 +2388,11 @@ const pressed: TaskCommand<"voice"> = { type: "call", dialId: "dial-7f2" };
2377
2388
 
2378
2389
  **Call is a dial.** It carries the host's `dialId`, is answered `dialling`, and ends in exactly one
2379
2390
  `dial-outcome`; the phase is its gate and there is no capability, since a record put in front of an
2380
- agent is there to be called. On `answered` the task is `in-progress` and its media follows on
2381
- `task-media-started`. On any other outcome the task goes to `completing`, so the agent records the
2391
+ agent is there to be called. Its media starts on `dialling`, as every host-placed dial's does --
2392
+ ring-back is audio the agent hears -- so `task-media-started` arrives on the `preview` task with
2393
+ its party ringing. On `answered` the task is `in-progress`; on any other outcome the media ends,
2394
+ `task-media-ended` starts the wrap clock as on every voice task, and the task goes to
2395
+ `completing`, so the agent records the
2382
2396
  no-answer as the disposition it is -- in a campaign that is the commonest outcome there is, and it
2383
2397
  is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
2384
2398
  (`task.preview.dialOutcomes.required`).
@@ -2494,11 +2508,13 @@ whose outcome says so and a call whose media ended: the task returns to `complet
2494
2508
  same event and the clock starts again from there. At no point is an agent dialling against a
2495
2509
  deadline.
2496
2510
 
2497
- **The room shows the party being dialled again.** From the moment the connect-back is placed the
2498
- `party` entry carries `stage: "ringing"` and the host's `dialId`, exactly as a consulted or
2499
- conferenced entry does from its dial, and `joined` on the answered `dial-outcome` and never before
2500
- it (`stream.taskUpdated.stage`); a host shows a call being placed, not a party it asserts is on a
2501
- call that is still ringing. A callback the platform places itself on the same task -- the first
2511
+ **The room shows the party being dialled.** From the moment any host dial places the party -- a
2512
+ dialpad call, a preview Call, a connect-back -- the `party` entry carries `stage: "ringing"` and
2513
+ the host's `dialId`, exactly as a consulted or conferenced entry does from its dial; `joined` on
2514
+ the publication that follows the answered `dial-outcome` and never before it
2515
+ (`stream.taskUpdated.stage`); and no stage from the next publication on, so `joined` is transient
2516
+ and a later copy still carrying it is stale (`stream.taskUpdated.stage.lingering`). A host shows a
2517
+ call being placed, not a party it asserts is on a call that is still ringing. A callback the platform places itself on the same task -- the first
2502
2518
  call over, the platform dialling the party again without a command -- shows the same thing with no
2503
2519
  host `dialId`, as any platform-placed dial does. A party with no stage is on the call, and a dial
2504
2520
  with no stage is half a claim (`task.onCall.party.dial`). The stage is also what lets the stream
@@ -2647,7 +2663,9 @@ Four rules a provider has to keep:
2647
2663
  **An absent `by` means different things on different steps, and both are legitimate.** On
2648
2664
  `queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
2649
2665
  `HANDLING_STEPS_WITH_A_PERSON` and `handlingStepExpectsAPerson()` — so an absent `by` there says
2650
- *this was handled and the provider cannot say by whom*.
2666
+ *this was handled and the provider cannot say by whom*. The one step that is never unattributed is
2667
+ `muted`: the host has exactly one agent and the provider knows who, so `by` is required there
2668
+ (`task.handlingHistory.muted.by`).
2651
2669
 
2652
2670
  That case is ordinary rather than theoretical: a leg answered on a shared phone, a manager's
2653
2671
  handset, or a device the provider cannot resolve to a person. **Report the step without `by`
@@ -2889,16 +2907,17 @@ last statement stands and the failure is a `diagnostic`, and a task that was pub
2889
2907
  or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
2890
2908
  entry the same two ways (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`);
2891
2909
  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
2910
+ ends on `task-media-ended` and the call moves on (`stream.snapshot.media`); a voice task ends after
2911
+ its audio ends, never around it, whatever the outcome (`stream.taskEnded.mediaOpen`); and a task does not go
2893
2912
  backwards, on an update or on a resync (`stream.taskUpdated.phase`, `stream.snapshot.phase`): the
2894
2913
  stream sees publications, not
2895
2914
  transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
2896
2915
  with `confirmed` between them, and what is refused is a phase unreachable from the last one read by
2897
2916
  the transition table -- back to `pending`, back to `confirmed` or `preview` once work began, out of
2898
2917
  `completing` except by the party being dialled again, a connect-back or a platform's callback,
2899
- which the update itself shows: the party on the call carrying a `stage`. A completing task
2900
- republished as `in-progress` from a stale copy carries no such stage, and that is the ending the
2901
- agent never saw. Audio arrives only on a task at work: `task-media-started` on a `completing` task
2918
+ which the update itself shows: the party ringing, or joined by a dial whose answered outcome the
2919
+ stream saw in this life. A completing task republished as `in-progress` from a stale copy carries
2920
+ no such stage, and that is the ending the agent never saw. Audio arrives only on a task at work: `task-media-started` on a `completing` task
2902
2921
  is refused as it is on a pending one (`stream.taskMediaStarted.beforeWork`), since a connect-back
2903
2922
  returns the task to `in-progress` before any media. And a task completes after its audio ends,
2904
2923
  never around it: an update moving a task to `completing` while the stream holds its media as
@@ -3075,9 +3094,11 @@ strand the customer and the destination both, which is why all three are command
3075
3094
  that offers `warmTransfer` implements all three.
3076
3095
 
3077
3096
  `applied` on `complete` says the provider is bridging the customer to the destination and
3078
- dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
3079
- the provider reports `task-media-ended`, any wrap allowance runs, and the task ends with a
3080
- `transferred` outcome naming the destination. `applied` on `cancel` says the destination is
3097
+ dropping the agent's leg. What follows is what follows any call the agent leaves: the agent's
3098
+ media ends and the provider reports `task-media-ended`, the task moves to `completing`, any wrap
3099
+ allowance runs, and the task ends `completed` as any call does. A warm complete is not a disposal:
3100
+ the agent has a wrap to do, so nothing is owed within `disposalSettleMs`. `transferred` names a
3101
+ cold transfer alone, where the agent had no wrap. `applied` on `cancel` says the destination is
3081
3102
  dropped; the task returns to `in-progress` with the `consulted` entry gone. Omni waits for the
3082
3103
  provider's report of both, as it does for every command.
3083
3104
 
@@ -3118,7 +3139,7 @@ Custom capability IDs must be non-empty and unique within the task. `ui.control`
3118
3139
  or `menu-item`; `ui.placement` is `primary`, `secondary`, or `overflow`. `ui.render` says where the
3119
3140
  control's work appears: `inline`, in the workspace beside the task, or `page`, as a page of its own
3120
3141
  — a tab in the same work area as the task's browsers, beside them, and alone on a task that has
3121
- none; inline when absent. `prompt.fields` are what the agent supplies before the action runs — a
3142
+ none; stated on every control, never defaulted (`task.custom.ui.render`). `prompt.fields` are what the agent supplies before the action runs — a
3122
3143
  destination number, a reference — as `CredentialField`s Omni renders as a form; the values travel
3123
3144
  on the custom command under the fields' names, as strings, and Omni sends the command only once
3124
3145
  every `required` field has a value. The provider validates what arrives as it validates any
@@ -3160,7 +3181,7 @@ inside Omni.
3160
3181
 
3161
3182
  ## Every dial has an outcome
3162
3183
 
3163
- Four commands place a call: `dial` from the idle dialpad, a blind or consult `transfer`, a
3184
+ Five commands place a call: `dial` from the idle dialpad, `call` from preview, a cold or warm `transfer`, a
3164
3185
  `conference` `add`, and `connect-back`. Each is accepted or refused at once, and each then ends later
3165
3186
  and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
3166
3187
  late in a call routinely outlives the call. Nothing in between is reported: the wire says
@@ -3327,7 +3348,7 @@ nothing while none are being accepted — so they are not published separately.
3327
3348
  | `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
3328
3349
  | `retryAfterMs` | How long until the agent may retry, when the provider can say. |
3329
3350
  | `reasons` | Not-ready codes this provider offers. Omitted when it defines none; an empty list is refused, being a second spelling of the same fact. |
3330
- | `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break, or when the provider cannot say. |
3351
+ | `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break. Required on a break `in-effect` or `starting-after-task` where the provider publishes `reasons`, an imposed break included (`break.activeReasonId.required`): a break with a kind the provider cannot name is a break whose rules nobody can apply. |
3331
3352
  | `imposed` | Set when the break was placed on the agent rather than requested. |
3332
3353
 
3333
3354
  A request can be waiting for two unrelated things, and they are separate values because
@@ -3338,7 +3359,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
3338
3359
  | `not-requested` | No request outstanding. |
3339
3360
  | `awaiting-decision` | A person has to decide. The agent is waiting on somebody. |
3340
3361
  | `granted` | A person decided yes. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
3341
- | `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. |
3362
+ | `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. It waits on a task, so beside no task it is refused (`break.starting-after-task.tasks`): a committed break with nothing outstanding is `in-effect`. |
3342
3363
  | `in-effect` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `in-effect` beside a task is refused as `break.in-effect.tasks`. The one exception is a lead's monitoring task during a `coaching`, `administrative` or `training` break -- see **Monitoring a call**. |
3343
3364
 
3344
3365
  A denial is a decision, not a standing approval state. The provider transitions the request directly
@@ -3367,6 +3388,11 @@ them, not the machinery that carried it out.
3367
3388
  Omni resolves the name to show with `describeUsers()`, so a provider sends the identifier and never
3368
3389
  a display name.
3369
3390
 
3391
+ **An imposed break travels with `in-effect` or `starting-after-task`, and nothing else.** It is a
3392
+ break in progress or about to be; beside `granted` or `awaiting-decision` the host would read a
3393
+ request the agent never made and commit it (`break.imposed.approval`). Where the provider publishes
3394
+ `reasons`, the lead's `place` named one, and the member's state carries it as `activeReasonId`.
3395
+
3370
3396
  For example:
3371
3397
 
3372
3398
  ```ts
@@ -3435,18 +3461,23 @@ Your own tasks are the only ones you count. What the agent holds at other provid
3435
3461
  concern — Omni set `count` knowing it, and this is how: the agent is one person on several
3436
3462
  providers, and the host divides their capacity among them rather than telling each the whole. A
3437
3463
  provider that has none of it for now is told **`count: 0`, host-stopped**: allocate nothing, show
3438
- the member as `on-task` on the roster, since the agent is working, and take the next count as any
3439
- other when the host has capacity for this provider again. Zero is not a refusal to state, and a
3440
- provider that answers it `failed` is named (`connection.setCapacity.zero`); it is the one restatement
3464
+ the member as `reserved` on the roster -- signed in here, capacity held by the host for elsewhere,
3465
+ a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
3466
+ count as any other when the host has capacity for this provider again. Zero is the one restatement
3441
3467
  that follows work rather than provisioning.
3442
3468
 
3469
+ **A capacity is taken, never refused.** `setCapacity` answers `applied` and nothing else: a
3470
+ statement has no failure to report, and a `failed` arm gave four host-provider pairings four
3471
+ readings of which ceiling stood after it. A provider that cannot carry the count allocates within
3472
+ what it can and says so on a `diagnostic`, so the gap is visible and nothing is inferred from a
3473
+ refusal.
3474
+
3443
3475
  Capacity supersedes rather than accumulates: the latest value is the ceiling, and a decrease is as
3444
3476
  ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
3445
3477
  it was ever told, or returns early on a small one -- cannot be told to take less work, and a host
3446
3478
  taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
3447
- both ways after the drive, two then one then nought, and names a provider that will not come down
3448
- (`connection.setCapacity.lowered`, `connection.setCapacity.zero`); an offer after a lower count is
3449
- caught against it (`stream.taskOffered.overCapacity`).
3479
+ both ways after the drive, two then one then nought, and an offer after a lower count is caught
3480
+ against it (`stream.taskOffered.overCapacity`).
3450
3481
 
3451
3482
  **Capacity gates what the provider allocates, not what the agent starts.** A call placed from the
3452
3483
  idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
@@ -3718,6 +3749,7 @@ Each availability value means one thing:
3718
3749
  | `ready` | Signed in, able to take work, none assigned. |
3719
3750
  | `on-task` | Handling at least one task. It says nothing about how many, and nothing about whether more will fit. |
3720
3751
  | `on-break` | Stopped and not taking work, whether they asked or somebody stopped them. The reason lives on their own `BreakState`, not here. |
3752
+ | `reserved` | Signed in here, and the host holds this agent's capacity for another provider (`count: 0`, host-stopped): not receiving this provider's work, and not on a break. See **Capacity**. |
3721
3753
  | `signed-out` | Known to this team but not signed in to this provider. |
3722
3754
 
3723
3755
  **Always publish the complete roster, never a change to it.** Team presence typically reaches an
@@ -3755,7 +3787,7 @@ One method, `executeTeamBreak`, taking a discriminated command exactly as `execu
3755
3787
  | --- | --- |
3756
3788
  | `{ type: "decide", memberId: UserId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
3757
3789
  | `{ type: "policy", policy }` | `ask`, `auto-approve`, or `suspended`. |
3758
- | `{ type: "place", memberId: UserId, reason? }` | Puts a member on a break they did not ask for. |
3790
+ | `{ type: "place", memberId: UserId, reasonId?, reason? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's imposed break carries it as `activeReasonId`, so its kind is known. |
3759
3791
  | `{ type: "release", memberId: UserId }` | Ends an imposed break on that member, whoever placed it. |
3760
3792
 
3761
3793
  `memberId` is this provider's own identifier for the member, as published on its roster. It is
@@ -3787,10 +3819,10 @@ Required when the login declares `capabilities.team.leadAssistControl`, and gate
3787
3819
  ```ts
3788
3820
  // 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
3789
3821
  execute({ taskId: "call-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
3790
- // task.leadAssist = { status: "requested", note: "Refund dispute, needs approval", since }
3822
+ // task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
3791
3823
 
3792
3824
  // 2. Every lead entitled to it sees the request on their roster.
3793
- // team-updated: requests: [{ id: "req-7", memberId: "A-1", taskId: "call-42", note, since }]
3825
+ // team-updated: requests: [{ id: "req-7", memberId: "A-1", taskId: "call-42", allocationId: "alloc-42", note, since }]
3794
3826
 
3795
3827
  // 3. A lead joins, or declines.
3796
3828
  executeTeamLeadAssist({ command: { type: "join", requestId: "req-7" } })
@@ -3824,8 +3856,8 @@ choice that is no command at all:
3824
3856
 
3825
3857
  | The lead | The agent's task | The lead's task |
3826
3858
  | --- | --- | --- |
3827
- | `{ type: "lead-assist", action: "take-over" }` | `task-ended` with `{ type: "transferred", destinationId: leadId }`, straight from `in-progress`: **no `completing` window**, the agent is idle at once | Continues alone, and ends as any call does |
3828
- | `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-ended` with `{ type: "left" }` -- the call goes on without them |
3859
+ | `{ type: "lead-assist", action: "take-over" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
3860
+ | `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-media-ended`, then `task-ended` with `{ type: "left" }` -- the call goes on without them |
3829
3861
  | Stays until the customer hangs up | `task-media-ended`, `completing`, its own disposition | The same, independently: **both have the disposal window** |
3830
3862
 
3831
3863
  `left` is the one outcome that ends a task without ending the call: this agent left a call that
@@ -3947,18 +3979,17 @@ decision or it is not.
3947
3979
 
3948
3980
  ## Real-time media
3949
3981
 
3950
- Every voice provider has media. There is nothing to announce, no capability to declare and no
3951
- endpoint to choose: `channel: "voice"` says audio exists, and **Omni is the device it lands on**.
3982
+ Every voice provider has media: `channel: "voice"` says audio exists. Where the agent hears it
3983
+ depends on the `phone` the host selected at authentication and connect from the manifest's
3984
+ `phones`. On a softphone, audio lands in Omni; on a desk phone, it lands on the handset and Omni
3985
+ opens no audio. See **How the agent hears the call**. Media transitions are voice-only;
3986
+ chat and email publish none (`event.media.channel`, `stream.taskMedia.channel`).
3952
3987
 
3953
- Other endpoints exist in a deployment — desk phones, the provider's own hardware, whatever the
3954
- platform already rings — and none of them is the agent's. Omni does not enumerate them, map the
3955
- agent onto one, or follow a change made to one. There is no device list, no device selection and
3956
- no device on the snapshot, because there is no choice to record: audio for this agent arrives in
3957
- Omni, and Omni registers the endpoint for it.
3988
+ The provider owns signalling and the platform's endpoint configuration. The host selects the
3989
+ declared phone mode; it does not enumerate or reconfigure the platform's devices. The selected
3990
+ mode stays with the login rather than being inferred from a snapshot or a device failure.
3958
3991
 
3959
- That removes a whole class of state the provider would otherwise own and Omni would have to track,
3960
- and it removes the branch that came with it: no command has to ask where the audio went before
3961
- deciding who performs it. Nor does the audio ever stand in for the task: a task's presence and
3992
+ Nor does the audio ever stand in for the task: a task's presence and
3962
3993
  phase follow the provider's reports about the work, and the media — attaching, moving through a
3963
3994
  hold, a consult, a conference or a transfer, and ending — is transient beside it. See **A task is
3964
3995
  never its audio** under **Task allocation lifecycle**.
@@ -4011,7 +4042,8 @@ other side: it validates the shape of whatever host a test hands the adapter, re
4011
4042
  a softphone login and none elsewhere -- not on a desk phone, not off voice
4012
4043
  (`context.host.audio.required` / `.unexpected`), holds `host.mute` to the same line (`host.mute.required`
4013
4044
  / `.unexpected`, `host.mute` for a third word) -- and refuses a voice adapter that never asked the
4014
- host anything (`connection.host.consulted`).
4045
+ host anything (`connection.host.consulted`), and one that subscribed to nothing will never hear of a
4046
+ change (`connection.host.subscribed`): the obligation has two halves, and each is held.
4015
4047
 
4016
4048
  ### Capacity around setup
4017
4049
 
@@ -4132,7 +4164,7 @@ carries as `audio.input.localAudio`, and absent while that input is `unavailable
4132
4164
  bridges audio without a host-side input may ignore it; one that needs it and finds it absent
4133
4165
  answers `unavailable` with a failure Omni shows the agent.
4134
4166
 
4135
- **When to ask is the provider's word, not Omni's guess.** Omni opens media on `task-media-started`,
4167
+ **When to ask is the provider's word, not Omni's guess.** On a softphone login, Omni opens media on `task-media-started`,
4136
4168
  and on a task arriving with `media: "started"` on a snapshot; it closes on `task-media-ended` and
4137
4169
  when the task ends. Between those words, nothing Omni's own senses report — a stream that drops, a
4138
4170
  track that ends — moves the task or its audio.
@@ -4269,7 +4301,7 @@ declared:
4269
4301
  | `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
4270
4302
  | `lead-assist` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
4271
4303
  | `transfer` with a destination, `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
4272
- | `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying every field the control's `prompt` asked for (`command.custom.prompt`). |
4304
+ | `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying a non-empty string for each `required` prompt field and strings for any optional fields supplied (`command.custom.prompt`). A toggle carries its target `on` boolean (`command.custom.on`). |
4273
4305
  | Everything else | Its own named capability. |
4274
4306
 
4275
4307
  **A control on the contact belongs to the handling phases**, `in-progress` and `paused`:
@@ -4442,8 +4474,12 @@ died; the host knows how to run a login. `recovery` joins the two:
4442
4474
  re-establishes state exactly as a reconnect snapshot does. `connect()` is once per
4443
4475
  `Connection`, not once per login.
4444
4476
  - **`reauthenticate`** — the session under the login died: a token rejected, a remote logout. Omni
4445
- runs the authentication flow first; the authentication session decides whether stored material
4446
- refreshes it silently or the agent must act, exactly as at sign-in.
4477
+ runs the authentication flow first, on the same session and `loginId`, never through
4478
+ `signed-out`; the authentication session decides whether stored material refreshes it silently or
4479
+ the agent must act, exactly as at sign-in. Once the state returns to `authenticated`, Omni
4480
+ disposes the errored connection with `disconnect()` and calls `connect()` afresh, as for
4481
+ `reconnect`, and the fresh snapshot re-establishes state. The adapter tears nothing down itself
4482
+ and expects nothing to carry on: the connection that reported `error` is finished.
4447
4483
 
4448
4484
  **Patience is the host's.** An adapter in `connecting` retries for as long as it takes and never
4449
4485
  has to decide when to stop. Omni owns giving up: after however long it chooses to wait, it may
@@ -4543,8 +4579,8 @@ A `left` outcome ends the task for this agent alone: the call continues without
4543
4579
  when a lead who joined it leaves -- see **Lead assist**.
4544
4580
 
4545
4581
  A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
4546
- and not for ever: `applied` to a disposal -- `complete`, `transfer` `complete`, `lead-assist`
4547
- `take-over` -- says the provider has disposed of the task, and its `task-ended` follows within the
4582
+ and not for ever: `applied` to a disposal -- `complete`, or a lead's `take-over` -- says the
4583
+ provider has disposed of the task, and its `task-ended` follows within the
4548
4584
  manifest's `disposalSettleMs`. A provider never answers `applied` for a disposal it has not yet
4549
4585
  performed. Past the bound the host calls `snapshot()`: a snapshot still carrying the task is a task
4550
4586
  held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
@@ -4665,7 +4701,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
4665
4701
  | `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. |
4666
4702
  | `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. |
4667
4703
  | `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. |
4668
- | `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`). |
4704
+ | `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, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
4669
4705
  | `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. |
4670
4706
  | `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. |
4671
4707
  | `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. |
@@ -4766,7 +4802,10 @@ capacity, or beyond the count with nothing dialled on it, is named
4766
4802
  through `describeUsers` and the answer held to the shape, nobody unasked, nobody described as
4767
4803
  nothing (`describeUsers.user.*`, `describeUsers.unasked`, `connection.describeUsers.empty`); and
4768
4804
  the second adapter a `rebuild` gives comes up signed in as the same login from the secrets alone
4769
- before it reads anything (`drive.reload.login`). The result also says which rules the
4805
+ before it reads anything (`drive.reload.login`), and is then held to everything the first was on
4806
+ connect: the methods its declarations call for, the host's report, a capacity stated to it, its
4807
+ snapshot read as the connect snapshot was, and its media opened afresh on a task carried with
4808
+ `media: "started"`, since the first client's session died with it. The result also says which rules the
4770
4809
  run evaluated, pass or fail, in `rulesEvaluated`: the validators' as each was applied, the stream's
4771
4810
  as each case was considered, so a test that needs a rule to have run asserts it there rather than
4772
4811
  inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
@@ -4775,7 +4814,8 @@ the drive mutes it for one second and reports the leg through `recordStep`, begu
4775
4814
  expecting each report `recorded` (`drive.recordStep.failed`, `.rejected`); then it mutes again and
4776
4815
  ends the call muted, as agents do, so the leg is open when the media ends, the provider closes it
4777
4816
  in the completing publication or the open entry is refused (`task.handlingHistory.muted.open`),
4778
- and the drive's closing report after the media ended is expected `recorded` and to change nothing.
4817
+ and the drive's closing report after the media ended is expected `recorded` and to change nothing:
4818
+ a record restated afterwards with that leg's duration altered is named (`drive.recordStep.overwritten`).
4779
4819
  Where the provider restates the task's record afterwards, each leg is in it or the hole is named
4780
4820
  (`drive.recordStep.history`). Given `rebuild`, a way to build the adapter again as a host reload
4781
4821
  does, the drive reloads the host as a reload happens: the first client is unsubscribed,
@@ -4799,7 +4839,9 @@ open task back to, and nothing about the reload is exempt from any rule. The sec
4799
4839
  has already put what would rebuild it into the secrets store, from the moment it was handed one,
4800
4840
  not only when a flow completes -- the session is the adapter's, the store is the host's, and a
4801
4841
  host reload is exactly when no flow will run. And once the task has ended, the store the drive
4802
- handed the adapter holds no key carrying the task's id (`drive.store.retained`): a task's keys go
4842
+ handed the adapter holds no key naming the task's id, delimited, never as a run of characters inside
4843
+ another id (`drive.store.retained`); an adapter whose keys never named the task leaves the rule
4844
+ unevaluated, and the result says so rather than passing it: a task's keys go
4803
4845
  with the task, or the next offer of the same id inherits them. Outside the
4804
4846
  handling phases with `hold` still declared -- in
4805
4847
  `confirmed`, where the provider publishes it, and in `completing` once the call has ended -- the
@@ -4854,7 +4896,7 @@ cannot be established from TypeScript structure alone.
4854
4896
  | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
4855
4897
  | `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
4856
4898
  | `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
4857
- | `stillHost(report?)` | A host that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a voice adapter. |
4899
+ | `stillHost(report?, guarantees?, mute?)` | A host that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a softphone voice adapter, and never for a desk phone. |
4858
4900
  | `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for a host that wants the same rules at its boundary: `seed(snapshot)`, then `apply(envelope)` returns the violations. |
4859
4901
  | `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a placed break arriving in effect with `imposed`. The harness applies the same rules after the connect snapshot. |
4860
4902
  | `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-started` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,11 @@
30
30
  "scripts": {
31
31
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
32
32
  "build": "pnpm run clean && tsc -p tsconfig.json",
33
- "test": "tsc -p tsconfig.test.json && vitest run"
33
+ "typecheck": "tsc -p tsconfig.test.json",
34
+ "test": "pnpm run typecheck && vitest run",
35
+ "check:package": "node scripts/check-package.mjs",
36
+ "check": "pnpm run build && pnpm run test && pnpm run check:package",
37
+ "prepublishOnly": "pnpm run check"
34
38
  },
35
39
  "devDependencies": {
36
40
  "@types/node": "^22.20.1",