@urun-sh/core 0.4.2 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,140 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - **BREAKING: `end()` PARKS, and `cancel()` is the admission-ticket verb** (owner ruling D3,
6
+ 2026-09-06; Review A §5.1 "two end verbs", §3.3 residue).
7
+ - `Session.end()` / `sessionDirectory().end(id)` both go through the one implementation
8
+ `endSession()` → `POST /api/sessions/{id}/end` → `urun_park_session(client_ended)`.
9
+ It resolves `{ status: 'paused', sessionId, reason }`. `'paused'` can never throw; a
10
+ TERMINAL word on that route is now the loud error (`invalid_end_response`), because a
11
+ terminal write on the session plane is what the model forbids.
12
+ - `Session.cancel()` (new) → `POST /api/session-requests/{id}/cancel`. Request plane only,
13
+ never addressed by session id. Every outcome the control plane can answer maps to
14
+ `{ status: 'canceled' }`; an unrecognized word stays loud.
15
+ - `SessionEndResult` is now `{ status: 'paused'; sessionId; reason } | { status: 'canceled';
16
+ requestId; platformStatus? }` — the `'ended'` member is gone.
17
+ - `SessionEndError.requestId` is optional and `sessionId` is new.
18
+ - `allocateSession`'s `onRequest` handle carries `sessionId` as soon as the create envelope
19
+ names one, so a hang-up racing allocation parks the session instead of cancelling the
20
+ ticket of a session that already exists.
21
+ - `SessionGatewayStatus` gains `'paused'`.
22
+ - The end request is `keepalive`, like the teardown beacon: an `end()` can race the tab
23
+ close that triggered it. It sends NO body: the verb names the cause server-side, and
24
+ session-api reads a wire `reason` only to tolerate it — so `end()` now speaks the same
25
+ wire shape as the pagehide beacon hitting that same route.
26
+ - **The one routing rule holds on the paths that are not the two verbs** (review findings):
27
+ a page teardown that cancelled a still-queued TICKET finishes on the SESSION plane if
28
+ promotion won the race (otherwise the promoted session holds a GPU seat with no client),
29
+ and the give-up cancel parks instead of cancelling when allocation threw AFTER the control
30
+ plane allocated (an unusable `session_api_base` / `ws_url` — the ticket-cancel route
31
+ refuses those with 409 `request_has_session`).
32
+ - A REFUSED `cancel()` is not a cancel: the release claim and the re-attach bookmark are
33
+ only taken on success, so a later `pagehide` still beacons the ticket cancel instead of
34
+ being suppressed by a release that never happened.
35
+
36
+ ## 0.4.3
37
+
38
+ - **FIX: no server status word can reach a user as a benign lie — on EITHER path.**
39
+ Wave 0 (0.4.0) made the SFU WebSocket path honest: `STATUS_TO_PHASE` maps every
40
+ server word explicitly, an unmapped one goes LOUD (`console.error` via the
41
+ diagnostic mirror + an `unknown-server-status` diagnostic + phase `unknown`
42
+ carrying the literal word), and `reason` rides every phase. **It landed on the
43
+ WebSocket path only.** A brand-new user's cold start spends ALL of its time on
44
+ the HTTP admission path, and that path was documented as *"tolerant by
45
+ design"*: `admissionUpdateFrom` collapsed every 202 status word except
46
+ `queued` into `'pending'`, dropped `reason` entirely, and `reportAdmission`
47
+ rendered `provisioning`.
48
+
49
+ The consequence was that **`waiting_for_capacity` could not reach any client
50
+ at all**. The control plane answers a starved pull pool with
51
+ `202 {status:"waiting_for_capacity", reason:"no runtime worker is registered
52
+ for this pool — waiting for GPU capacity"}`; the phase exists in this SDK and
53
+ renders "Waiting for GPU capacity", but it was reachable ONLY from `_onStatus`
54
+ — i.e. only over a WebSocket that does not exist until allocation has already
55
+ succeeded, by which time capacity is no longer the question. A user whose org
56
+ has zero registered GPU workers watched "Provisioning…" for up to 600 s and
57
+ then got `never-live` "the backend is busy or wedged". The whole server-side
58
+ capacity observer was dead end-to-end.
59
+
60
+ Both entry points now share ONE `mapServerStatus(status)` and ONE loud-unknown
61
+ branch. `AdmissionUpdate.status` **widens from `'queued' | 'pending'` to
62
+ `string`** and carries the server's `reason`, plus `decisionReason` /
63
+ `refusalSource` / `refusalMessage` so a `provisioning_refused` 202 delivers
64
+ the PROVIDER's own message ("0/12 nodes are available: 12 Insufficient
65
+ nvidia.com/gpu…") instead of silence. The additive `runtime_state` detail may
66
+ still refine the two GENERIC waiting words (`queued`/`pending`) but may no
67
+ longer overrule a specific verdict — a starved answer carries
68
+ `runtime_state:'starting'` too, and letting `starting` win is exactly how the
69
+ capacity truth was lost.
70
+
71
+ - **FIX: every pause cause the server can write now has human copy, and a
72
+ conformance guard fails the build when the two lists diverge.**
73
+ `describePauseReason` knew 3 of the server's causes; the other 16 fell through
74
+ to the verbatim arm, so the canonical first-run failure printed as
75
+ *"Paused — connect_deadline. Resume any time."* — and the only thing relating
76
+ the two vocabularies was a COMMENT claiming the list was "grepped from the
77
+ server sources". The vocabulary is now VENDORED at
78
+ `src/server-pause-reasons.json`, GENERATED by
79
+ `scripts/refresh-pause-reason-fixture.mjs` from the SFU `PauseReason` union,
80
+ the `sessions.pause_reason` VOCABULARY clause (urun-infra migration
81
+ `20270906000000`) and the SFU `RuntimeNotDeliveringReason` union, each with
82
+ its path and blob sha. `src/pause-reason-vocabulary.test.ts` fails when any
83
+ vendored cause still hits the default arm. Every line keeps the invariant:
84
+ never "ended", never "start a new session", always resumable.
85
+
86
+ - **FIX: the default copy path shows an actionable hint, not developer text.**
87
+ `describeSessionPhase`'s `error` arm rendered ``Session failed — ${reason}``,
88
+ which for a create failure is the raw gateway string — an end user read
89
+ `[urun] session allocation failed at https://session-api.usw2.prod.cloud.urun.sh:
90
+ queue_full: … (HTTP 429)`. It now delegates to `describeSessionFailure`, so
91
+ message + hint are one code path. The `create-failed` hint gains arms for
92
+ **429** (names the load shed and surfaces the server's `Retry-After`) and
93
+ **5xx** (names the outage, says the retry is automatic) — both previously told
94
+ the user to "check the app/function name and auth", which is wrong about whose
95
+ fault it is. A lapsed queued REQUEST (`queue_ttl_expired` /
96
+ `stale_poll_expired`) no longer repeats the server's *"create a new session"* —
97
+ the request lapsed, the named object did not.
98
+
99
+ - **FIX: the server's load-shed pacing is no longer advisory.** A 429 sets a
100
+ jittered `Retry-After: 2-6`; the SDK read it only on the 202
101
+ legacy-immediate branch and DROPPED it on the throw path, so the outer sweep
102
+ re-dialled an overloaded control plane on its own full-jitter schedule.
103
+ `SessionAllocationError.retryAfterSeconds` now carries it, the sweep waits the
104
+ MAX of that and its own jitter (bounded by `BACKOFF_MAX_MS`), and
105
+ `SessionPhaseError.retryAfterSeconds` surfaces it to the copy.
106
+
107
+ The pacing floor is the **sweep-wide maximum**, not the last url's: every
108
+ gateway failure overwrites `lastError`, so a paced 429 from the primary
109
+ followed by an unpaced retryable failure from a regional fallback used to
110
+ erase the primary's request entirely and re-dial it on client jitter. The
111
+ same floor now applies to `resolveViewerConnect`, whose refusal ALSO carries
112
+ the server's `Retry-After` (it paced itself against a field that was never
113
+ populated). `samePhase` compares `retryAfterSeconds`, so a refusal that only
114
+ changed how long the backend is asking for re-stamps the phase instead of
115
+ leaving the UI quoting the previous number.
116
+
117
+ - **FIX: an HTTP 408 is a timeout, not a full admission queue.** The
118
+ `create-failed` hint folded 408 into the 429 arm and told the user "its
119
+ admission queue is full" — a confident FALSE cause for a status that only
120
+ establishes that a request timed out (`backoff.ts` treats it as a generic
121
+ retryable status; the durable-poll path throws `poll_http_408`). 408 now has
122
+ its own timeout copy, and still names the server's `Retry-After` when it sent
123
+ one.
124
+
125
+ - **FIX: the display sanitizer no longer has holes.** It knew exactly TWO
126
+ gateway wrappers, so token hydration (`session allocation at <url> never got
127
+ an access token`), the redirect envelope / self-redirect (`session create at
128
+ <url> …`), the poll give-up and the region-forward loop all still showed an
129
+ internal hostname in end-user copy — and a reason that is NOTHING BUT a
130
+ wrapper (the bare allocation timeout) stripped to empty and hit the
131
+ `|| reason` fallback, printing the raw string the strip existed to clean.
132
+ Every wrapper is listed, and a residual redaction is the backstop: no gateway
133
+ URL survives into display copy by any route.
134
+
135
+ - `whenLive`'s patience clocks (45 s / 600 s) are deliberately UNCHANGED here —
136
+ they are a separate item.
137
+
3
138
  ## 0.4.2
4
139
 
5
140
  - **Lockstep release** — version bump to match the `@urun-sh/openai` omp launcher cohort
@@ -7,7 +142,6 @@
7
142
 
8
143
  ## 0.4.1
9
144
 
10
-
11
145
  - **FIX: a re-dial no longer leaves the playout element bound to a DEAD track**
12
146
  (urun-infra#2132; the prod qwen-voice "assistant audio drops mid-session and
13
147
  never self-heals, reload fixes it" signature). `recvTransport.consume()` /