pi-radar 0.9.0 → 0.11.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 (42) hide show
  1. package/README.md +8 -10
  2. package/dist/extension/baseline.js +343 -361
  3. package/dist/extension/radar-children.js +108 -218
  4. package/dist/extension/radar-client.js +103 -15
  5. package/dist/extension/radar-execution.js +26 -17
  6. package/dist/extension/radar-inspection.js +0 -45
  7. package/dist/extension/radar-launch.js +24 -185
  8. package/dist/extension/radar-publication.js +26 -17
  9. package/dist/extension/radar-runtime.js +1 -1
  10. package/dist/index.js +317 -488
  11. package/extension/generated-contracts.ts +362 -13
  12. package/extension/radar-children.ts +60 -155
  13. package/extension/radar-client.ts +130 -19
  14. package/extension/radar-inspection.ts +2 -77
  15. package/extension/radar-launch.ts +52 -391
  16. package/extension/radar-publication.ts +36 -53
  17. package/extension/radar-runtime.ts +1 -1
  18. package/package.json +1 -1
  19. package/schema/public/v1/observation-get-response.deserialize.schema.json +5 -25
  20. package/schema/public/v1/observation-get-response.serialize.schema.json +7 -29
  21. package/schema/public/v1/observation-snapshot.deserialize.schema.json +5 -25
  22. package/schema/public/v1/observation-snapshot.serialize.schema.json +7 -29
  23. package/schema/public/v1/spawn-list-page.deserialize.schema.json +40 -17
  24. package/schema/public/v1/spawn-list-page.serialize.schema.json +40 -17
  25. package/schema/public/v1/subject-assignment-acquire.deserialize.schema.json +78 -0
  26. package/schema/public/v1/subject-assignment-acquire.serialize.schema.json +79 -0
  27. package/schema/public/v1/subject-assignment-publish.deserialize.schema.json +310 -0
  28. package/schema/public/v1/subject-assignment-publish.serialize.schema.json +310 -0
  29. package/schema/public/v1/subject-assignment-retire.deserialize.schema.json +22 -0
  30. package/schema/public/v1/subject-assignment-retire.serialize.schema.json +22 -0
  31. package/schema/public/v1/subject-child-get-params.deserialize.schema.json +18 -0
  32. package/schema/public/v1/subject-child-get-params.serialize.schema.json +18 -0
  33. package/schema/public/v1/subject-children-params.deserialize.schema.json +51 -0
  34. package/schema/public/v1/subject-children-params.serialize.schema.json +54 -0
  35. package/schema/public/v1/subject-get-params.deserialize.schema.json +14 -0
  36. package/schema/public/v1/subject-get-params.serialize.schema.json +14 -0
  37. package/schema/public/v1/subject-get-result.deserialize.schema.json +566 -0
  38. package/schema/public/v1/subject-get-result.serialize.schema.json +573 -0
  39. package/schema/public/v1/subject-list-params.deserialize.schema.json +38 -0
  40. package/schema/public/v1/subject-list-params.serialize.schema.json +43 -0
  41. package/schema/public/v1/subject-list-result.deserialize.schema.json +575 -0
  42. package/schema/public/v1/subject-list-result.serialize.schema.json +583 -0
@@ -5,72 +5,15 @@ import {
5
5
  type RadarErrorCode,
6
6
  type RadarLocation,
7
7
  type RadarResult,
8
- type RadarSpawnEdge,
9
8
  } from "./radar-client.ts";
10
- import { radarPaneHandle } from "./radar-location.ts";
11
9
  import type { RadarPublication } from "./radar-publication.ts";
12
10
 
13
- /** The startup acknowledgement a launch waits for, named by its identity. */
14
- export type StartupExpectation = {
15
- generation: string;
16
- runId: string;
17
- ownerSessionId: string;
18
- workspaceId: string;
19
- agentLabel: string;
20
- paneId: string;
21
- };
22
-
23
- /**
24
- * The mailbox readers a launch needs. The mailbox belongs to the caller (Herdsman
25
- * supplies its own state shape and readers); this module only takes the path it
26
- * is handed and asks these questions of it.
27
- */
28
- export type LaunchState = { paneId: string };
29
-
30
- export type LaunchMailbox<State extends LaunchState> = {
31
- readState(path: string): State | undefined;
32
- waitForState(
33
- path: string,
34
- predicate: (state: State) => boolean,
35
- options: { timeoutMs: number; signal?: AbortSignal },
36
- ): Promise<State>;
37
- startupAckMatches(state: State | undefined, expected: StartupExpectation): boolean;
38
- };
39
-
40
- /**
41
- * Launching one fresh managed worker through Radar's spawn operation.
42
- *
43
- * A spawn is one durable effect behind the caller's own request UUID, and three
44
- * separate facts decide whether a child can take an assignment:
45
- *
46
- * 1. the daemon created a pane under this process's registered pane, launched
47
- * the child's exact command into it, and still reports that pane in its own
48
- * inventory;
49
- * 2. a child registered with the launch's private token, so the edge names the
50
- * exact `registration_id` that child obtained — never a label, pane or session;
51
- * 3. that child wrote a fresh startup acknowledgment for this launch generation
52
- * after Herdsman startup finished, naming the pane the daemon created as its
53
- * own location.
54
- *
55
- * Only the third is application readiness; the first two are prerequisites. A
56
- * lost reply is reconciled against the same request UUID and never re-sent, so a
57
- * failed launch cannot leave a second pane or a second child behind.
58
- *
59
- * The child's location is a fact about the pane the daemon created, not about
60
- * the environment it was launched with: the daemon passes an environment overlay
61
- * to a mux-created pane and cannot remove what that pane already carries, so a
62
- * child that inherited its parent's pane coordinates would claim the parent's
63
- * pane. The daemon's fresh record of the created pane and the child's own claim
64
- * must therefore name the same pane, and either one being stale, missing or
65
- * different leaves the launch unresolved rather than placed.
66
- */
11
+ /** A launch succeeds when Radar grants its child's verified first presence. */
67
12
 
68
13
  /** Everything one fresh managed worker is launched with. */
69
14
  export type RadarWorkerLaunch = {
70
15
  /** The caller's durable key, persisted before dispatch and never replaced. */
71
16
  request_id: string;
72
- /** The nonce the child must repeat in its startup acknowledgment. */
73
- generation: string;
74
17
  /**
75
18
  * Start this released child agent again rather than a new one. The daemon takes
76
19
  * the session from its own record, so no session id or path is sent.
@@ -87,232 +30,45 @@ export type RadarWorkerLaunch = {
87
30
  cwd: string;
88
31
  /** The environment overlay the child needs, not a replacement environment. */
89
32
  env: Record<string, string>;
90
- /** The child's mailbox: its own state is the readiness evidence. */
91
- mailbox: string;
92
- /** The identity the child's state and acknowledgment must repeat exactly. */
93
- identity: {
94
- runId: string;
95
- ownerSessionId: string;
96
- workspaceId: string;
97
- agentLabel: string;
98
- };
99
33
  };
100
34
 
101
- export type RadarWorkerLaunched<State = unknown> = {
35
+ export type RadarWorkerLaunched = {
102
36
  request_id: string;
103
37
  /** The child's durable identity: the key that survives its restart and resume. */
104
38
  subject_id: string;
105
39
  pane_id: string;
106
- state: State;
107
40
  };
108
41
 
109
42
  /**
110
- * `refused` is a launch that provably did not begin; `unresolved` is one whose
111
- * pane, child or readiness cannot be established, which the caller reports
112
- * rather than retries.
43
+ * `ready` is a child whose first presence the daemon granted. `failed` is a launch
44
+ * the daemon withdrew (or that provably never began), so a retry is safe.
45
+ * `unresolved` is the rare launch whose outcome cannot be read at all, which the
46
+ * caller reports rather than retries.
113
47
  */
114
- export type RadarLaunchOutcome<State = unknown> =
115
- | { kind: "ready"; value: RadarWorkerLaunched<State> }
116
- | { kind: "refused"; message: string }
48
+ export type RadarLaunchOutcome =
49
+ | { kind: "ready"; value: RadarWorkerLaunched }
50
+ | { kind: "failed"; message: string }
117
51
  | { kind: "unresolved"; request_id: string; message: string };
118
52
 
119
- /**
120
- * What the daemon's current read proves about the child of one launch: the exact
121
- * subject the edge is bound to and the pane it recorded, both taken from the
122
- * daemon's own answer rather than from this process's record of the launch.
123
- */
124
- export type RadarChildEvidence = {
125
- request_id: string;
126
- registration_id: string;
127
- source: string;
128
- incarnation: string;
129
- pane_id: string;
130
- };
131
-
132
- /**
133
- * `live` is current evidence for the exact child; `unknown` is every other
134
- * answer, including the ones that look like death. A pane the daemon no longer
135
- * reports is not process exit, and a read that could not be made proves nothing.
136
- */
137
- export type RadarChildObservation =
138
- | { kind: "live"; evidence: RadarChildEvidence }
139
- /**
140
- * Unproved by location, but the registered process is proved gone by its
141
- * verdict. Only read for a child with an outstanding assignment.
142
- */
143
- | { kind: "gone"; verification: ProcessGone; diagnostic: string }
144
- | { kind: "unknown"; diagnostic: string };
145
-
146
- export type ProcessGone = "absent" | "mismatched";
147
-
148
- function unknownChild(reason: string): RadarChildObservation {
149
- return { kind: "unknown", diagnostic: trajectory(reason) };
150
- }
151
-
152
- function trajectory(reason: string): string {
153
- return `the daemon's current record of this launch's child is ${reason}`;
154
- }
155
-
156
- /**
157
- * Observe whether this launch's exact child is live now.
158
- *
159
- * The durable spawn record and the child's own binding say *which* subject this
160
- * launch created; they are provenance, not liveness, and they stay true after a
161
- * child is gone. The startup acknowledgment says the child is ready, which is
162
- * also not liveness. Only a current topology read decides: `bound` has to name
163
- * the same subject, the read has to record the pane this launch claims, no
164
- * process exit may have been observed, and the read's own `freshness` has to
165
- * report that the daemon's inventory currently places that pane. Anything else -
166
- * a read that could not be made, an edge bound to another child, an observed
167
- * exit, a pane the daemon does not report, a pane this launch never recorded -
168
- * leaves the child unproved.
169
- */
170
- export async function observeRadarChild(
171
- client: RadarClient,
172
- provenance: {
173
- request_id: string;
174
- pane_id: string | undefined;
175
- registration_id: string | undefined;
176
- /** The session this launch's mailbox records for the child. */
177
- session_id: string | undefined;
178
- },
179
- ): Promise<RadarChildObservation> {
180
- if (provenance.registration_id === undefined)
181
- return unknownChild("not bound to a child by this launch");
182
- // A launch that recorded no pane has nothing to compare the daemon's read
183
- // against: the child's own claim about where it runs is not where the launch
184
- // put it, and a claim cannot supply the location a launch never recorded.
185
- if (provenance.pane_id === undefined)
186
- return unknownChild("without a pane this launch recorded for it");
187
- const read = await client.spawnGet(provenance.request_id);
188
- if (read.ok === false)
189
- return unknownChild(
190
- `not readable (${read.code}: ${read.message})`,
191
- );
192
- const topology = read.value;
193
- const edge = topology.edge;
194
- if (edge.bound === undefined || edge.bound.registration_id === undefined)
195
- return unknownChild("not bound to a child by the daemon");
196
- if (edge.bound.registration_id !== provenance.registration_id)
197
- return unknownChild(
198
- `bound to another subject (${edge.bound.registration_id}), not to ${provenance.registration_id}`,
199
- );
200
- if (edge.location?.pane === undefined)
201
- return unknownChild("recorded without a pane");
202
- if (edge.location.pane !== provenance.pane_id)
203
- return unknownChild(
204
- `recorded on pane ${edge.location.pane}, not on ${provenance.pane_id}`,
205
- );
206
- // An observed exit outranks a pane the inventory still reports: the pane's
207
- // shell can outlive the child, and this launch's child is the process.
208
- if (edge.exit_observation?.state !== "unknown")
209
- return unknownChild("recorded with an observed process exit");
210
- // Freshness first: an inventory that does not report the recorded pane is
211
- // the reason a read leaves unresolved, and it is stale both when the backend
212
- // did not report it and when it could not be asked.
213
- if (topology.freshness !== "fresh")
214
- return unknownChild(
215
- "on a pane the daemon's own inventory does not currently report",
216
- );
217
- if (topology.state !== "bound")
218
- return unknownChild(
219
- `not currently placed (state ${topology.state ?? "unreported"})`,
220
- );
221
- // The edge says where the child was bound, not that its process still runs.
222
- // The registry's verdict on the bound subject, and the location it registered
223
- // with, are what prove the child live.
224
- const reply = await client.agentGet(edge.bound.registration_id);
225
- if (reply.ok === false)
226
- return unknownChild(
227
- `process verdict not readable (${reply.code}: ${reply.message})`,
228
- );
229
- const { verification, location, contextSession } = reply.value;
230
- if (verification === "absent" || verification === "mismatched")
231
- return {
232
- kind: "gone",
233
- verification,
234
- diagnostic: trajectory(`the registered process is ${verification}`),
235
- };
236
- if (verification !== "verified")
237
- return unknownChild(`process verdict is ${verification}`);
238
- if (location?.pane !== provenance.pane_id)
239
- return unknownChild(
240
- `registered on ${location?.pane ?? "no pane"}, not on ${provenance.pane_id}`,
241
- );
242
- // The subject must currently publish the session this launch's mailbox names:
243
- // a live process in another session, or one publishing none, is not this child.
244
- if (provenance.session_id === undefined)
245
- return unknownChild("without a session this launch recorded for it");
246
- if (contextSession !== provenance.session_id)
247
- return unknownChild(
248
- `publishing session ${contextSession ?? "none"}, not ${provenance.session_id}`,
249
- );
250
- return {
251
- kind: "live",
252
- evidence: {
253
- request_id: provenance.request_id,
254
- registration_id: edge.bound.registration_id,
255
- source: edge.bound.source,
256
- incarnation: edge.bound.incarnation,
257
- pane_id: edge.location.pane,
258
- },
259
- };
260
- }
261
-
262
- /**
263
- * The daemon's positive statement that a registered child's process is gone, or
264
- * `undefined`. Only `absent` and `mismatched` establish that; `verified` means it
265
- * still runs, and `unavailable` (including a registration with no process claim)
266
- * and an unreadable or missing registration are not evidence of absence.
267
- */
268
- export async function readProcessGone(
269
- client: Pick<RadarClient, "agentGet">,
270
- agentId: string,
271
- ): Promise<ProcessGone | undefined> {
272
- const read = await client.agentGet(agentId);
273
- if (read.ok === false) return undefined;
274
- const { verification } = read.value;
275
- return verification === "absent" || verification === "mismatched"
276
- ? verification
277
- : undefined;
278
- }
279
-
280
- /**
281
- * Why a relaunch may not replace this registered child, or `undefined` when the
282
- * daemon positively says its registered process is gone (see `processGone`).
283
- */
284
- export async function relaunchRefusalForChild(
285
- client: Pick<RadarClient, "agentGet">,
286
- agentId: string,
287
- ): Promise<string | undefined> {
288
- const read = await client.agentGet(agentId);
289
- if (read.ok === false)
290
- return `the child's process cannot be read (${read.code}: ${read.message})`;
291
- const { verification } = read.value;
292
- if (verification === "absent" || verification === "mismatched") return undefined;
293
- return `the child's registered process is ${verification === "verified" ? "still running" : "not proved gone"}`;
294
- }
295
-
296
- export type RadarLaunchOptions<State extends LaunchState = LaunchState> = {
53
+ export type RadarLaunchOptions = {
297
54
  client: RadarClient;
298
55
  /** This process's own publication, which names the parent of the launch. */
299
56
  publication: Pick<RadarPublication, "subject">;
300
- readers: LaunchMailbox<State>;
301
57
  launch: RadarWorkerLaunch;
302
- timeoutMs: number;
303
- /** How often the binding and readiness waits re-read; 0 in driven tests. */
58
+ /** How often the wait re-reads the launch; 0 in driven tests. */
304
59
  pollMs?: number;
60
+ /** Stops the wait. The daemon's own deadline still withdraws the launch. */
305
61
  signal?: AbortSignal;
306
62
  };
307
63
 
308
- function refusedLaunch(message: string): Extract<RadarLaunchOutcome, { kind: "refused" | "unresolved" }> {
309
- return { kind: "refused", message };
64
+ function refusedLaunch(message: string): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
65
+ return { kind: "failed", message };
310
66
  }
311
67
 
312
68
  function unresolvedLaunch(
313
69
  requestId: string,
314
70
  message: string,
315
- ): Extract<RadarLaunchOutcome, { kind: "refused" | "unresolved" }> {
71
+ ): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
316
72
  return {
317
73
  kind: "unresolved",
318
74
  request_id: requestId,
@@ -320,19 +76,6 @@ function unresolvedLaunch(
320
76
  };
321
77
  }
322
78
 
323
- /** Whether the edge's own two effects already prove a launch happened. */
324
- function edgeStanding(edge: RadarSpawnEdge): "launched" | "refused" | "unknown" {
325
- if (edge.created === "refused" || edge.launched === "refused")
326
- return "refused";
327
- return edge.created === "completed" && edge.launched === "completed"
328
- ? "launched"
329
- : "unknown";
330
- }
331
-
332
- function edgeMessage(edge: RadarSpawnEdge): string {
333
- return `the daemon's spawn record is created ${edge.created} and launched ${edge.launched}`;
334
- }
335
-
336
79
  /**
337
80
  * The daemon's own refusals, which prove no effect was dispatched. Every other
338
81
  * failure — a lost reply, a timeout, an internal error, an unreadable answer —
@@ -353,27 +96,24 @@ const PRE_EFFECT_CODES: readonly RadarErrorCode[] = [
353
96
  "backend_unavailable",
354
97
  ];
355
98
 
356
- function spawnFailure(
357
- result: Extract<RadarResult<RadarSpawnEdge>, { ok: false }>,
99
+ export function launchFailure(
100
+ result: Extract<RadarResult<unknown>, { ok: false }>,
358
101
  requestId: string,
359
- ): Extract<RadarLaunchOutcome, { kind: "refused" | "unresolved" }> {
102
+ ): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
360
103
  return PRE_EFFECT_CODES.includes(result.code)
361
104
  ? refusedLaunch(result.message)
362
105
  : unresolvedLaunch(requestId, result.message);
363
106
  }
364
107
 
365
108
  /**
366
- * Wait for the child's own registration to bind this edge, then for its startup
367
- * acknowledgment. The edge is re-read by the same request UUID: a spawn that
368
- * already answered is never sent again.
109
+ * Wait for the child's verified presence grant or the daemon's withdrawal. The
110
+ * same request UUID is read throughout; a lost reply never starts another child.
369
111
  */
370
- export async function launchRadarWorker<State extends LaunchState>(
371
- options: RadarLaunchOptions<State>,
372
- ): Promise<RadarLaunchOutcome<State>> {
112
+ export async function launchRadarWorker(
113
+ options: RadarLaunchOptions,
114
+ ): Promise<RadarLaunchOutcome> {
373
115
  const { client, launch, signal } = options;
374
116
  const pollMs = options.pollMs ?? 250;
375
- const deadline = Date.now() + options.timeoutMs;
376
- const remaining = (): number => Math.max(0, deadline - Date.now());
377
117
  const parent = await options.publication.subject();
378
118
  if (parent === undefined || !isUuid(parent.registration_id))
379
119
  return refusedLaunch(
@@ -408,87 +148,46 @@ export async function launchRadarWorker<State extends LaunchState>(
408
148
  ...(Object.keys(launch.env).length > 0 ? { env: launch.env } : {}),
409
149
  });
410
150
  if (spawned.ok === false) {
411
- const refusal = spawnFailure(spawned, launch.request_id);
412
- if (refusal.kind === "refused") return refusal;
413
- }
414
- if (spawned.ok === true) {
415
- const standing = edgeStanding(spawned.value);
416
- if (standing === "refused")
417
- return refusedLaunch(edgeMessage(spawned.value));
418
- if (standing === "unknown")
419
- return unresolvedLaunch(launch.request_id, edgeMessage(spawned.value));
151
+ const refusal = launchFailure(spawned, launch.request_id);
152
+ if (refusal.kind === "failed") return refusal;
420
153
  }
421
154
 
422
155
  const bound = await waitForChild(client, launch.request_id, {
423
- remaining,
424
156
  pollMs,
425
157
  ...(signal !== undefined ? { signal } : {}),
426
158
  });
427
159
  if (bound.kind !== "bound") return bound;
428
- const agentId = bound.registration_id;
429
- try {
430
- const state = await options.readers.waitForState(
431
- launch.mailbox,
432
- (candidate) =>
433
- options.readers.startupAckMatches(candidate, {
434
- generation: launch.generation,
435
- runId: launch.identity.runId,
436
- ownerSessionId: launch.identity.ownerSessionId,
437
- workspaceId: launch.identity.workspaceId,
438
- agentLabel: launch.identity.agentLabel,
439
- // The child writes the raw TMUX_PANE of the pane it runs in. Requiring
440
- // the pane the daemon created is what refuses a child that inherited
441
- // another process's coordinates.
442
- paneId: radarPaneHandle(bound.pane_id),
443
- }),
444
- { timeoutMs: remaining(), ...(signal !== undefined ? { signal } : {}) },
445
- );
446
- return {
447
- kind: "ready",
448
- value: {
449
- request_id: launch.request_id,
450
- subject_id: bound.subject_id,
451
- pane_id: bound.pane_id,
452
- state,
453
- },
454
- };
455
- } catch (error) {
456
- const claimed = options.readers.readState(launch.mailbox)?.paneId;
457
- return unresolvedLaunch(
458
- launch.request_id,
459
- claimed !== undefined && claimed !== radarPaneHandle(bound.pane_id)
460
- ? `the bound child ${agentId} claims the pane ${claimed}, not the pane ${bound.pane_id} this launch created for it`
461
- : `the bound child ${agentId} has not acknowledged this launch's startup (${String(error)})`,
462
- );
463
- }
160
+ return {
161
+ kind: "ready",
162
+ value: {
163
+ request_id: launch.request_id,
164
+ subject_id: bound.subject_id,
165
+ pane_id: bound.pane_id,
166
+ },
167
+ };
464
168
  }
465
169
 
466
170
  type BoundChild =
467
171
  | {
468
172
  kind: "bound";
469
173
  subject_id: string;
470
- registration_id: string;
471
174
  pane_id: string;
472
- source: string;
473
- incarnation: string;
474
175
  }
475
- | Extract<RadarLaunchOutcome, { kind: "refused" | "unresolved" }>;
176
+ | Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }>;
476
177
 
477
178
  /**
478
- * Poll the edge this request id authored until a child presents its token.
179
+ * Poll the edge this request id authored until the daemon grants its child or
180
+ * withdraws the launch. There is no timeout here: the daemon owns the deadline and
181
+ * records a withdrawal on the edge, so waiting longer than it would only wait for
182
+ * an outcome that is already decided.
479
183
  *
480
- * The daemon binds the edge once, to the exact registration the token produced,
481
- * so `bound.registration_id` is the child identity. A refusal or an unknown launch
482
- * effect ends the wait at once: neither becomes a child by waiting longer. An
483
- * edge whose recorded pane the daemon no longer reports ends it too: waiting
484
- * cannot create the pane a launch claims, and a location read afterwards would
485
- * be that of whatever moved in.
184
+ * Registration binding alone is not success: a pending or duplicate presence
185
+ * can bind an edge without ever acquiring attachment authority.
486
186
  */
487
- async function waitForChild(
488
- client: RadarClient,
187
+ export async function waitForChild(
188
+ client: Pick<RadarClient, "spawnGet">,
489
189
  requestId: string,
490
190
  options: {
491
- remaining: () => number;
492
191
  pollMs: number;
493
192
  signal?: AbortSignal;
494
193
  },
@@ -499,53 +198,22 @@ async function waitForChild(
499
198
  return unresolvedLaunch(requestId, `the launch was cancelled (${last})`);
500
199
  const answer = await client.spawnGet(requestId);
501
200
  if (answer.ok === true) {
502
- const { edge, state, freshness } = answer.value;
503
- const standing = edgeStanding(edge);
504
- if (standing === "refused")
505
- return refusedLaunch(`${edgeMessage(edge)}`);
506
- if (standing === "unknown")
507
- return unresolvedLaunch(requestId, edgeMessage(edge));
201
+ const { edge } = answer.value;
202
+ if (edge.withdrawn !== undefined)
203
+ return refusedLaunch(`the daemon withdrew this launch: ${edge.withdrawn.reason}`);
204
+ if (edge.granted_at === undefined) {
205
+ await delay(options.pollMs);
206
+ continue;
207
+ }
508
208
  const pane = edge.location?.pane;
509
209
  if (pane === undefined || pane === "")
510
210
  return unresolvedLaunch(
511
211
  requestId,
512
212
  "the daemon's launch recorded no pane to verify the child against",
513
213
  );
514
- // The recorded pane is the daemon's own statement about where it created
515
- // and launched this child, and `fresh` is its inventory confirming that
516
- // pane right now. Without that confirmation the launch has no verified
517
- // location to give the child, and no later read can supply one.
518
- if (freshness !== "fresh")
519
- return unresolvedLaunch(
520
- requestId,
521
- state === "unresolved"
522
- ? `the daemon's inventory does not report the pane ${pane} its own record names for this child`
523
- : `the daemon cannot confirm the pane ${pane} it recorded for this child`,
524
- );
525
- // An observed exit is the child's process, and a pane's shell can outlive it.
526
- if (edge.exit_observation?.state === "exited")
527
- return unresolvedLaunch(
528
- requestId,
529
- "the daemon observed the launched child's process exit",
530
- );
214
+
531
215
  const agentId = edge.bound?.registration_id;
532
216
  if (agentId !== undefined && isUuid(agentId)) {
533
- // A child the daemon bound without the process it registered as is a
534
- // child no assignment can be addressed to, and an unaddressed Radar
535
- // assignment is exactly what addressing exists to prevent: the launch
536
- // stays unresolved rather than degrading to a V5 request.
537
- const source = edge.bound?.source;
538
- const incarnation = edge.bound?.incarnation;
539
- if (
540
- source === undefined ||
541
- source === "" ||
542
- incarnation === undefined ||
543
- incarnation === ""
544
- )
545
- return unresolvedLaunch(
546
- requestId,
547
- `the daemon bound the child ${agentId} without recording the process it registered as`,
548
- );
549
217
  // The edge names the agent as well as its process. Without it the child
550
218
  // has no durable key to be returned by, and a registration is never
551
219
  // offered in its place.
@@ -558,20 +226,13 @@ async function waitForChild(
558
226
  return {
559
227
  kind: "bound",
560
228
  subject_id: subjectId,
561
- registration_id: agentId,
562
229
  pane_id: pane,
563
- source,
564
- incarnation,
565
230
  };
566
231
  }
567
232
  last = "no child has registered with this launch's token yet";
568
233
  } else {
569
- last = answer.message;
234
+ return unresolvedLaunch(requestId, answer.message);
570
235
  }
571
- if (options.remaining() <= 0)
572
- return unresolvedLaunch(requestId, last);
573
- await delay(options.pollMs, undefined, {
574
- ...(options.signal !== undefined ? { signal: options.signal } : {}),
575
- });
236
+ await delay(options.pollMs);
576
237
  }
577
238
  }