@jr2/orchestrator 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/bin/server.ts +23 -0
  4. package/console/canvas.ts +843 -0
  5. package/console/components/app.ts +79 -0
  6. package/console/components/drawer.ts +131 -0
  7. package/console/components/fleet.ts +117 -0
  8. package/console/components/machine-pane.ts +85 -0
  9. package/console/components/nav.ts +81 -0
  10. package/console/components/schema-form.ts +137 -0
  11. package/console/main.ts +383 -0
  12. package/console/page.html +28 -0
  13. package/console/store.ts +336 -0
  14. package/console/style.css +700 -0
  15. package/console/tsconfig.json +18 -0
  16. package/package.json +61 -0
  17. package/src/actor.ts +562 -0
  18. package/src/agent.ts +124 -0
  19. package/src/ambient.ts +50 -0
  20. package/src/config.ts +297 -0
  21. package/src/customize.ts +348 -0
  22. package/src/durability.ts +135 -0
  23. package/src/fingerprint.ts +92 -0
  24. package/src/gate.ts +76 -0
  25. package/src/harness-client.ts +503 -0
  26. package/src/http.ts +753 -0
  27. package/src/images.ts +303 -0
  28. package/src/index.ts +40 -0
  29. package/src/instance.ts +294 -0
  30. package/src/machine-doc.ts +334 -0
  31. package/src/names.ts +78 -0
  32. package/src/open.ts +17 -0
  33. package/src/parts.ts +500 -0
  34. package/src/pool.ts +284 -0
  35. package/src/registration.ts +340 -0
  36. package/src/repo-fetch.ts +259 -0
  37. package/src/repo-identity.ts +145 -0
  38. package/src/repos.ts +330 -0
  39. package/src/run-host.ts +1095 -0
  40. package/src/sandbox-kubectl.ts +1136 -0
  41. package/src/server.ts +220 -0
  42. package/src/setup.ts +360 -0
  43. package/src/snapshot-store.ts +150 -0
  44. package/src/stub-harness.ts +217 -0
  45. package/src/tokens.ts +126 -0
  46. package/src/vocabulary.ts +99 -0
  47. package/src/wire.ts +103 -0
  48. package/src/workspace.ts +874 -0
  49. package/tsconfig.instance.json +26 -0
@@ -0,0 +1,503 @@
1
+ // The real, Harness-wire-backed `AgentRunPort` (ADR-0027, on ADR-0002/0007/0016) — the client that
2
+ // drives one Agent run over the five-endpoint wire — and `agent(definition)`, the Agent slot built
3
+ // on it (ADR-0049: a Machine carries its Agents; the client is constructed from `input.endpoint`).
4
+ //
5
+ // The wire is jr2's own (`./wire.ts` is the shape contract as this client reads it; the stub Harness
6
+ // is the normative server model), so this module speaks plain `fetch` — no SDK. Keeping it here (not in `actor.ts`)
7
+ // is what keeps the run-lifecycle actor and its unit tests wire-free (see actor.ts header). The
8
+ // port is built over an INJECTABLE client (`harnessAgentRunPort(client)`) so the mapping logic is
9
+ // unit-testable against a fake; `createHarnessClient(opts)` is the real thing, itself testable
10
+ // socket-free through an injected `fetch`.
11
+ //
12
+ // Channel split (ADR-0002, refined by ADR-0016): the Harness wire carries **lifecycle only** —
13
+ // domain events go up the MCP channel via the Adapter. The client's `send`/`wait` pair is exactly
14
+ // that lifecycle surface: `send` answers with a serializable Admission
15
+ // (`{ streamUrl, offset, submissionId }` — jr2's durable re-attach handle, stored in the host
16
+ // ledger), and `wait(admission)` follows the durable stream from the admission offset to the
17
+ // Submission's Settlement — a long-poll loop that advances by the `stream-next-offset` header and
18
+ // reconnects from the SAME offset on network failure (capped backoff, indefinitely: pod death is
19
+ // `workspace.lost`'s job to report, not this loop's to guess). Re-attach after a restart is `wait`
20
+ // with the SAME persisted admission; replay cost is bounded by one Submission's chunks. A 404 is a
21
+ // LOST conversation (ADR-0027: a conversation lives as long as its Harness process) — a
22
+ // `SettlementFault`, never an endless poll.
23
+ //
24
+ // Both verbs re-send on a network failure, for one reason stated twice: an unanswered request is
25
+ // not an answer. They differ in bound, and the difference is where the Submission is — `wait`'s is
26
+ // already admitted, so the lease may report a dead pod and this loop need never give up, while
27
+ // `send`'s does not exist yet, so its window closes and faults. `send` also re-sends ONLY what
28
+ // provably never left this host (`postAdmission` — ADR-0042: it is the first thing ever to dial a
29
+ // Sandbox's Service, and a CR at `phase: Ready` is not yet routable).
30
+ //
31
+ // `abort` is the third verb, and ADR-0024 wires it to the END OF THE INVOCATION: a turn ends with
32
+ // the state that asked for it. It sweeps the running Submission and everything queued behind it to
33
+ // the distinct `aborted` Settlement — worth having for observability, though jr2 never reads it
34
+ // (the actor is stopped by then; see actor.ts).
35
+
36
+ import { agentActorWith } from "./actor.ts";
37
+ import type { AgentAdmission, AgentAdmitOptions, AgentLogic, AgentRunInput, AgentRunPort } from "./actor.ts";
38
+ import type { AgentDeclaration, AgentDefinition, ThinkingLevel } from "./agent.ts";
39
+ import {
40
+ LIVE_LONG_POLL,
41
+ STREAM_NEXT_OFFSET_HEADER,
42
+ VIEW_UPDATES,
43
+ type EchoEvent,
44
+ type Settlement,
45
+ type StreamEvent,
46
+ type SubmissionSettledEvent,
47
+ } from "./wire.ts";
48
+
49
+ /** The three-verb wire client (the injectable seam — structurally what `@flue/sdk`'s
50
+ * `agents.{send,wait,abort}` was, minus the SDK). */
51
+ export type HarnessClient = {
52
+ /** `POST /agents/:name/:id {message, definition, model?, thinkingLevel?}` → the Admission, with
53
+ * `streamUrl` resolved absolute. The DEFINITION is the slot's, carried by the Machine and sent
54
+ * with every admission (ADR-0049) — the Harness keeps no roster, so `:name` alone would name
55
+ * nothing there. The optional dials are this Submission's override layer (ADR-0018); omitted,
56
+ * the Harness runs the definition's own values. */
57
+ send(
58
+ agentName: string,
59
+ instanceId: string,
60
+ options: {
61
+ message: string;
62
+ definition: AgentDefinition;
63
+ model?: string;
64
+ thinkingLevel?: ThinkingLevel;
65
+ signal?: AbortSignal;
66
+ },
67
+ ): Promise<AgentAdmission>;
68
+ /** Follow the stream to this Submission's Settlement: resolve on `completed`, reject with
69
+ * `SettlementFault` on `failed`/`aborted`/404. */
70
+ wait(admission: AgentAdmission, opts?: { signal?: AbortSignal }): Promise<void>;
71
+ /** `POST /agents/:name/:id/abort`. The `{ aborted }` answer is dropped (ADR-0024). */
72
+ abort(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<void>;
73
+ };
74
+
75
+ export type HarnessClientOptions = {
76
+ /** The Harness base URL — what a workspace publishes, or a stub's `url`. */
77
+ baseUrl: string;
78
+ /** Injectable for socket-free tests. Default: global `fetch`. */
79
+ fetch?: typeof fetch;
80
+ /** Reconnect backoff floor/ceiling (capped exponential, jittered per rung — see `jittered`;
81
+ * network failures only, an answered long-poll re-polls immediately). Defaults 250ms → 5s; tests
82
+ * shrink them. */
83
+ backoffInitialMs?: number;
84
+ backoffMaxMs?: number;
85
+ /**
86
+ * How long `send` keeps re-POSTing an admission that never reached the Harness. See
87
+ * `postAdmission` for why this window exists and why it is BOUNDED where `wait`'s reconnect is
88
+ * not.
89
+ *
90
+ * Default 90s, taken from the longest delays anyone has MEASURED rather than from the mechanism.
91
+ * The mechanism argues for much less — kube-proxy programs the EndpointSlice in well under a
92
+ * second, and CoreDNS's 30s negative TTL is the only other obvious floor — but kubernetes#88986
93
+ * records 63s on bare metal (a SYN-retransmit ladder, 1-2-4-8-16-32: dropped, not refused) and
94
+ * kind#2280 records up to 77s with the EndpointSlices already populated. This started at 60s,
95
+ * reasoned from the mechanism alone, and cleared neither. Tests shrink it.
96
+ */
97
+ admitWindowMs?: number;
98
+ /** Where the routability line goes when an admission had to retry — see `routabilityLine`.
99
+ * Default `console.warn`; tests capture it. */
100
+ log?: (line: string) => void;
101
+ };
102
+
103
+ /** A Submission that settled `failed`/`aborted` — or whose conversation the Harness no longer
104
+ * has (404) — surfaced to the actor as an `agent.fault`. Replaces the SDK's execution error. */
105
+ export class SettlementFault extends Error {
106
+ /** The Settlement as the stream carried it; absent when the conversation itself was lost. */
107
+ readonly settlement?: Settlement;
108
+ constructor(message: string, settlement?: Settlement) {
109
+ super(message);
110
+ this.name = "SettlementFault";
111
+ this.settlement = settlement;
112
+ }
113
+ }
114
+
115
+ /** Build the real wire client from connection options. */
116
+ export function createHarnessClient(options: HarnessClientOptions): HarnessClient {
117
+ const { baseUrl } = options;
118
+ const fetchImpl = options.fetch ?? fetch;
119
+ const backoffInitialMs = options.backoffInitialMs ?? 250;
120
+ const backoffMaxMs = options.backoffMaxMs ?? 5_000;
121
+ const admitWindowMs = options.admitWindowMs ?? 90_000;
122
+ const log = options.log ?? ((line: string) => console.warn(line));
123
+
124
+ const conversationUrl = (agentName: string, instanceId: string) =>
125
+ new URL(`/agents/${encodeURIComponent(agentName)}/${encodeURIComponent(instanceId)}`, baseUrl).toString();
126
+
127
+ /**
128
+ * POST the admission, re-sending it while the request DEMONSTRABLY never left this host.
129
+ *
130
+ * This is `wait`'s reconnect rule, applied one step earlier and for the same reason: a request
131
+ * that could not connect is not a Harness that refused the prompt. It matters here because the
132
+ * admission is the FIRST thing jr2 ever sends over the Sandbox's Service — provisioning waits on
133
+ * the CR's `phase: Ready`, which the operator computes from the POD, and the attach reaches the
134
+ * pod through the API server, so nothing before this has proven the Service dialable. Ready is
135
+ * not routable: the EndpointSlice behind the ClusterIP is programmed after the pod passes its
136
+ * probe, and until it is, kube-proxy REJECTs — which arrives here as a refused connection.
137
+ * Un-retried, that single blip lost the whole turn (the actor calls an admission failure a
138
+ * terminal `agent.fault`), which is exactly the flake that kept the `@kind` tier serial.
139
+ *
140
+ * Only a NEVER-DELIVERED failure is re-sent. Admission is accept-and-queue (ADR-0027): a POST
141
+ * the Harness received but could not answer has already queued a Submission, so re-sending it
142
+ * would run the turn twice — worse than losing it. `neverDelivered` is therefore the narrow
143
+ * question "did this reach the wire at all", never "does this look transient".
144
+ *
145
+ * And the window is BOUNDED where `wait`'s is not. `wait` may reconnect forever because its
146
+ * Submission is already admitted, so the lease owns the reporting (`workspace.lost` — ADR-0021).
147
+ * Nothing is admitted yet here, so there is no turn for a lease to be about: an address that
148
+ * never answers is a fault this call has to name itself.
149
+ */
150
+ const postAdmission = async (url: string, init: RequestInit, signal?: AbortSignal): Promise<Response> => {
151
+ const startedAt = Date.now();
152
+ const deadline = startedAt + admitWindowMs;
153
+ let backoffMs = backoffInitialMs;
154
+ let attempts = 0;
155
+ let lastCode = "?";
156
+ for (;;) {
157
+ attempts += 1;
158
+ try {
159
+ const res = await fetchImpl(url, init);
160
+ // Only when it actually cost something. A window that is never approached should be silent,
161
+ // so that a line appearing at all is already the signal (see `routabilityLine`).
162
+ if (attempts > 1) log(routabilityLine("admission", url, attempts, Date.now() - startedAt, lastCode));
163
+ return res;
164
+ } catch (err) {
165
+ lastCode = transportCode(err);
166
+ // A local abandon propagates untranslated, exactly as in `wait` — the stopped actor
167
+ // swallows it, and it is not a fault.
168
+ if (signal?.aborted) throw err;
169
+ if (!neverDelivered(err)) throw err;
170
+ if (Date.now() >= deadline) {
171
+ throw new Error(
172
+ `harness admission never connected to ${url} after ${attempts} attempt(s) over ` +
173
+ `${admitWindowMs}ms: ${transportDetail(err)}`,
174
+ { cause: err },
175
+ );
176
+ }
177
+ await sleep(jittered(backoffMs), signal);
178
+ backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
179
+ }
180
+ }
181
+ };
182
+
183
+ return {
184
+ async send(agentName, instanceId, sendOptions) {
185
+ const url = conversationUrl(agentName, instanceId);
186
+ const res = await postAdmission(
187
+ url,
188
+ {
189
+ method: "POST",
190
+ headers: { "content-type": "application/json" },
191
+ body: JSON.stringify({
192
+ message: sendOptions.message,
193
+ // The Agent this Turn runs (ADR-0049) — plain data, validated at admission by the
194
+ // Harness, which 400s naming the slot rather than settling a Submission `failed`.
195
+ definition: sendOptions.definition,
196
+ // Omitted when unset, so an admission with no dials is byte-identical to before.
197
+ ...(sendOptions.model ? { model: sendOptions.model } : {}),
198
+ ...(sendOptions.thinkingLevel ? { thinkingLevel: sendOptions.thinkingLevel } : {}),
199
+ }),
200
+ signal: sendOptions.signal,
201
+ },
202
+ sendOptions.signal,
203
+ );
204
+ if (!res.ok) {
205
+ throw new Error(`harness admission failed (${res.status}): ${await errorDetail(res)}`);
206
+ }
207
+ const admission = (await res.json()) as AgentAdmission;
208
+ // The Harness may mint `streamUrl` relative to itself; the ledgered handle must re-attach
209
+ // without remembering this client's baseUrl, so absolutize it here (absolute passes through).
210
+ return { ...admission, streamUrl: new URL(admission.streamUrl, baseUrl).toString() };
211
+ },
212
+
213
+ async wait(admission, opts) {
214
+ const signal = opts?.signal;
215
+ let offset = admission.offset;
216
+ let backoffMs = backoffInitialMs;
217
+ for (;;) {
218
+ signal?.throwIfAborted();
219
+ const url = new URL(admission.streamUrl, baseUrl);
220
+ url.searchParams.set("offset", offset);
221
+ url.searchParams.set("view", VIEW_UPDATES);
222
+ url.searchParams.set("live", LIVE_LONG_POLL);
223
+
224
+ let events: StreamEvent[];
225
+ let nextOffset: string | null;
226
+ try {
227
+ const res = await fetchImpl(url.toString(), { signal });
228
+ if (res.status === 404) {
229
+ throw new SettlementFault(
230
+ `conversation lost: the harness answered 404 for submission "${admission.submissionId}" — ` +
231
+ `a conversation lives as long as its Harness process (ADR-0027)`,
232
+ );
233
+ }
234
+ if (!res.ok && res.status !== 204) {
235
+ throw new ReconnectableError(`harness stream read failed (${res.status})`);
236
+ }
237
+ nextOffset = res.headers.get(STREAM_NEXT_OFFSET_HEADER);
238
+ events = res.status === 204 ? [] : ((await res.json()) as StreamEvent[]);
239
+ } catch (err) {
240
+ // Local abandon propagates untranslated (the stopped actor swallows it); a lost
241
+ // conversation is final. Everything else is the network — reconnect from the SAME
242
+ // offset, forever, backing off to the cap.
243
+ if (signal?.aborted) throw err;
244
+ if (err instanceof SettlementFault) throw err;
245
+ await sleep(jittered(backoffMs), signal);
246
+ backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
247
+ continue;
248
+ }
249
+ backoffMs = backoffInitialMs;
250
+
251
+ for (const event of events) {
252
+ if (event.type !== "submission-settled" || event.submissionId !== admission.submissionId) continue;
253
+ if (event.outcome === "completed") return;
254
+ throw new SettlementFault(faultMessage(event), toSettlement(event));
255
+ }
256
+ if (nextOffset !== null) offset = nextOffset;
257
+ }
258
+ },
259
+
260
+ async abort(agentName, instanceId, opts) {
261
+ const res = await fetchImpl(`${conversationUrl(agentName, instanceId)}/abort`, {
262
+ method: "POST",
263
+ signal: opts?.signal,
264
+ });
265
+ if (!res.ok) {
266
+ throw new Error(`harness abort failed (${res.status}): ${await errorDetail(res)}`);
267
+ }
268
+ // Drain the `{ aborted }` answer so the socket is released; the value is dropped (ADR-0024).
269
+ await res.json().catch(() => undefined);
270
+ },
271
+ };
272
+ }
273
+
274
+ /** Build an `AgentRunPort` over an injected wire client. Stateless — the admission IS the handle. */
275
+ export function harnessAgentRunPort(client: HarnessClient): AgentRunPort {
276
+ return {
277
+ async admit(input: AgentRunInput, opts: AgentAdmitOptions): Promise<AgentAdmission> {
278
+ if (input.prompt === undefined) {
279
+ throw new Error("harness admit needs a prompt (a re-attach rides input.attach, set by the host on restore)");
280
+ }
281
+ return await client.send(input.agentName, input.instanceId, {
282
+ message: input.prompt,
283
+ definition: opts.definition,
284
+ model: input.model,
285
+ thinkingLevel: input.thinkingLevel,
286
+ signal: opts.signal,
287
+ });
288
+ },
289
+
290
+ async settle(admission: AgentAdmission, opts?: { signal?: AbortSignal }): Promise<void> {
291
+ await client.wait(admission, { signal: opts?.signal });
292
+ },
293
+
294
+ async abort(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<void> {
295
+ // The result (`{ aborted }` — whether there was work to end) is dropped: by the time this
296
+ // runs the state has already moved on, and an idle instance is exactly as fine as a
297
+ // stopped one. Settlement is asynchronous and nothing here is listening (ADR-0024).
298
+ await client.abort(agentName, instanceId, { signal: opts?.signal });
299
+ },
300
+ };
301
+ }
302
+
303
+ /** Convenience: build the real wire client from connection options, then the `AgentRunPort`. */
304
+ export function createHarnessAgentRunClient(options: HarnessClientOptions): AgentRunPort {
305
+ return harnessAgentRunPort(createHarnessClient(options));
306
+ }
307
+
308
+ /**
309
+ * The run-narrative echo push (ADR-0023): `POST /echo { events }` against one Workspace Harness,
310
+ * bearing the Instance token (the endpoint is instance-token-gated — the Harness verifies the
311
+ * bearer against the token's sha-256, never holding the token itself). The payload is the
312
+ * STRUCTURED feed events; the Harness renders. A non-OK answer rejects, and the CALLER treats
313
+ * that as log-and-continue — fire-and-forget lives in the tee (run-host.ts), not here, so a test
314
+ * can still assert a push failed.
315
+ */
316
+ export function createEchoPush(options: {
317
+ baseUrl: string;
318
+ /** The Instance token — the echo bearer. */
319
+ token: string;
320
+ /** Injectable for socket-free tests. Default: global `fetch`. */
321
+ fetch?: typeof fetch;
322
+ }): (events: EchoEvent[]) => Promise<void> {
323
+ const fetchImpl = options.fetch ?? fetch;
324
+ return async (events) => {
325
+ const url = new URL("/echo", options.baseUrl).toString();
326
+ // The echo is log-only, un-retried, and fires at workspace attach — which makes it the FIRST
327
+ // thing to touch a Sandbox's Service and therefore jr2's earliest witness that the Service is
328
+ // not routable yet (ADR-0042). It is only a witness if it says what went wrong: bare
329
+ // `fetch failed` in the pod log is what let that condition hide.
330
+ const res = await fetchImpl(url, {
331
+ method: "POST",
332
+ headers: { "content-type": "application/json", authorization: `Bearer ${options.token}` },
333
+ body: JSON.stringify({ events }),
334
+ }).catch((err: unknown) => {
335
+ throw new Error(`harness echo to ${url} failed: ${transportDetail(err)}`, { cause: err });
336
+ });
337
+ if (!res.ok) {
338
+ throw new Error(`harness echo failed (${res.status}): ${await errorDetail(res)}`);
339
+ }
340
+ // Drain the `{ printed }` answer so the socket is released; the count is nobody's contract.
341
+ await res.json().catch(() => undefined);
342
+ };
343
+ }
344
+
345
+ /**
346
+ * THE authoring surface for an Agent (ADR-0049): one definition in, one actor slot out —
347
+ * `jr2Setup({ actors: { coder: agent({ model, instructions }) } })`, invoked as `src: "coder"`.
348
+ * The slot key is the Agent's name (the Harness route, the minted iid, the markers), so a name
349
+ * the Machine does not carry is a compile error on `src`, and two Machines in one run may each
350
+ * carry their own `coder`.
351
+ *
352
+ * Everything live is constructed per-invocation from serializable input: the wire client from
353
+ * `input.endpoint` (which Sandbox's Harness — or the wire-compatible dev stub; either way it's
354
+ * only a URL, one code path). Lives here, not in actor.ts, so the actor logic and its unit tests
355
+ * never touch the wire.
356
+ *
357
+ * It takes a DECLARATION (ADR-0054), so a packaged Machine may write `agent({ model: open, … })`
358
+ * and leave the one part it cannot honestly fill to whoever registers it. What rides the Turn is
359
+ * still an `AgentDefinition`: the actor narrows on start and refuses an Open one there.
360
+ */
361
+ export function agent(declaration: AgentDeclaration): AgentLogic {
362
+ return agentActorWith((endpoint) => createHarnessAgentRunClient({ baseUrl: endpoint }), declaration);
363
+ }
364
+
365
+ /** A stream read worth retrying (server hiccup) — internal to the reconnect loop, never thrown out. */
366
+ class ReconnectableError extends Error {}
367
+
368
+ /** Errnos that mean no connection was ever established. `getaddrinfo`/`connect` say it by syscall;
369
+ * undici's own connect timeout says it by code. Deliberately short — anything not on this list is
370
+ * treated as possibly-delivered (see `postAdmission`). */
371
+ const NEVER_DELIVERED_CODES = new Set(["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH"]);
372
+ const NEVER_DELIVERED_SYSCALLS = new Set(["connect", "getaddrinfo"]);
373
+
374
+ /**
375
+ * Did this rejection happen BEFORE any byte of the request reached the wire?
376
+ *
377
+ * `fetch` reports transport failures as a generic `TypeError: fetch failed` and puts the real
378
+ * errno on `cause`, which for a dual-stack address is an `AggregateError` over one attempt per
379
+ * family — so the answer is only ever legible by walking down. Read structurally (syscall/code),
380
+ * never by matching the message: the message is the part that changes between Node versions.
381
+ */
382
+ /**
383
+ * The most specific thing a transport rejection says about itself. `fetch` reports every one of
384
+ * them as the same three words — `fetch failed` — and that string, arriving as an `agent.fault`
385
+ * reason or a log line, names nothing an operator can act on. The errno one level down
386
+ * (`connect ECONNREFUSED 10.96.0.7:8080`, `getaddrinfo ENOTFOUND …`) is the whole diagnosis.
387
+ */
388
+ function transportDetail(err: unknown, depth = 0): string {
389
+ if (depth > 4 || typeof err !== "object" || err === null) return String(err);
390
+ const { message, cause, errors } = err as { message?: unknown; cause?: unknown; errors?: unknown };
391
+ const nested = Array.isArray(errors) ? errors[0] : cause;
392
+ if (nested !== undefined && nested !== null) {
393
+ const deeper = transportDetail(nested, depth + 1);
394
+ if (deeper) return deeper;
395
+ }
396
+ return typeof message === "string" ? message : String(err);
397
+ }
398
+
399
+ function neverDelivered(err: unknown, depth = 0): boolean {
400
+ if (depth > 4 || typeof err !== "object" || err === null) return false;
401
+ const { code, syscall, cause, errors } = err as {
402
+ code?: unknown;
403
+ syscall?: unknown;
404
+ cause?: unknown;
405
+ errors?: unknown;
406
+ };
407
+ if (typeof syscall === "string" && NEVER_DELIVERED_SYSCALLS.has(syscall)) return true;
408
+ if (typeof code === "string" && (code === "UND_ERR_CONNECT_TIMEOUT" || NEVER_DELIVERED_CODES.has(code))) return true;
409
+ if (Array.isArray(errors) && errors.some((nested) => neverDelivered(nested, depth + 1))) return true;
410
+ return neverDelivered(cause, depth + 1);
411
+ }
412
+
413
+ /** A readable fault message from a non-`completed` Settlement. */
414
+ function faultMessage(settlement: Settlement): string {
415
+ const detail = settlement.error?.message ?? settlement.error?.type;
416
+ return detail ? `submission settled ${settlement.outcome}: ${detail}` : `submission settled ${settlement.outcome}`;
417
+ }
418
+
419
+ /** The Settlement fields alone, off the stream chunk's envelope. */
420
+ function toSettlement(event: SubmissionSettledEvent): Settlement {
421
+ const { submissionId, outcome, error } = event;
422
+ return { submissionId, outcome, ...(error ? { error } : {}) };
423
+ }
424
+
425
+ /** Best-effort error body for a non-OK answer (the wire's error shape is `{ error }`). */
426
+ async function errorDetail(res: Response): Promise<string> {
427
+ const body = (await res.json().catch(() => undefined)) as { error?: unknown } | undefined;
428
+ return typeof body?.error === "string" ? body.error : res.statusText || "no detail";
429
+ }
430
+
431
+ /**
432
+ * The marker the `@kind` tier greps for, and therefore a CONTRACT — duplicated verbatim in
433
+ * `@jr2/adapter` (the two packages share no runtime dependency) and matched in
434
+ * `features/steps/kind.steps.ts`. Renaming it on one side does not break a build; it silently turns
435
+ * the tier's routability budget into a check that passes because it matches nothing.
436
+ */
437
+ const ROUTABILITY_MARKER = "jr2.routability";
438
+
439
+ /**
440
+ * What a retry at a lifecycle edge COST, emitted once, only when there was a cost.
441
+ *
442
+ * ADR-0042 absorbs these retries (ADR-0016's principle: no event, no budget on the authoring
443
+ * surface), and absorption is the right call — but a fault that is absorbed and never measured is
444
+ * how "Ready is not routable" stayed invisible for three sessions. Absorbing and measuring are not
445
+ * in conflict: this is a log line, not a run-feed event, so nothing about the workflow surface
446
+ * changes and the `@kind` tier can still assert a budget on the window it currently only benefits
447
+ * from. Scoped deliberately to the two hops ADR-0042 is about — `wait`'s reconnect is a live
448
+ * stream re-attaching, not a Service coming into existence, and does not belong in this number.
449
+ *
450
+ * `attempts` and `ms` are BOTH here because they measure different refusals, and the tier's first
451
+ * live reading proved it: 2 attempts costing 10667ms, which is one dropped SYN sitting on undici's
452
+ * 10s connect timeout, not a ladder being climbed. A REJECT (kube-proxy with no ready backend)
453
+ * spends attempts and almost no time; a DROP spends time and almost no attempts. `last` carries the
454
+ * errno of the final failure so the line says WHICH without anyone having to do that arithmetic.
455
+ */
456
+ function routabilityLine(
457
+ seat: "admission" | "surface",
458
+ url: string,
459
+ attempts: number,
460
+ ms: number,
461
+ lastCode: string,
462
+ ): string {
463
+ return `${ROUTABILITY_MARKER} seat=${seat} attempts=${attempts} ms=${ms} last=${lastCode} url=${url}`;
464
+ }
465
+
466
+ /** The errno of a transport rejection, read structurally down the cause chain (`transportDetail`
467
+ * gives the prose; this gives the one token worth aggregating on). */
468
+ function transportCode(err: unknown, depth = 0): string {
469
+ if (depth > 4 || typeof err !== "object" || err === null) return "?";
470
+ const { code, cause, errors } = err as { code?: unknown; cause?: unknown; errors?: unknown };
471
+ if (typeof code === "string") return code;
472
+ const nested = Array.isArray(errors) ? errors[0] : cause;
473
+ return nested === undefined || nested === null ? "?" : transportCode(nested, depth + 1);
474
+ }
475
+
476
+ /**
477
+ * One rung of the ladder, drawn from its TOP HALF (equal jitter).
478
+ *
479
+ * The ladder on its own is synchronized, and the callers arrive together by construction: Sandboxes
480
+ * that converge together cross the same routability window together, so their retries land
481
+ * together, miss together, and re-land together — the ladder turns one late Service into a
482
+ * lockstep herd. Randomizing half the interval decorrelates them. Keeping the other half as a floor
483
+ * is the part worth stating: it is what still holds four clients off a Service that is genuinely
484
+ * down, which full jitter (uniform over the whole interval) would trade away.
485
+ */
486
+ function jittered(ms: number): number {
487
+ return ms / 2 + Math.random() * (ms / 2);
488
+ }
489
+
490
+ /** An abortable pause — the reconnect backoff. Rejects with the signal's reason, untranslated. */
491
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
492
+ return new Promise((resolve, reject) => {
493
+ const onAbort = () => {
494
+ clearTimeout(timer);
495
+ reject(signal?.reason as Error);
496
+ };
497
+ const timer = setTimeout(() => {
498
+ signal?.removeEventListener("abort", onAbort);
499
+ resolve();
500
+ }, ms);
501
+ signal?.addEventListener("abort", onAbort, { once: true });
502
+ });
503
+ }