pi-radar 0.9.0 → 0.10.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.
@@ -1,7 +1,8 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import type { RadarClient, RadarChannelSnapshot, RadarResult } from "./radar-client.ts";
2
3
  import type { SpawnListPageSerializePublicSpawnTopology as ListedEdge } from "./generated-contracts.ts";
3
4
  import { inspectRadarChild, type RadarInspection } from "./radar-inspection.ts";
4
- import { observeRadarChild, type RadarChildObservation } from "./radar-launch.ts";
5
+ import { observeRadarChild, waitForChild, launchFailure, type RadarChildObservation, type RadarLaunchOutcome } from "./radar-launch.ts";
5
6
  import type { RadarPublication } from "./radar-publication.ts";
6
7
 
7
8
  const PAGE = 100;
@@ -23,8 +24,25 @@ export type RadarChildAssignment = {
23
24
  retire(): void;
24
25
  };
25
26
 
27
+ /** The program a restarted child runs as. */
28
+ export type RadarChildProgram = {
29
+ executable: string;
30
+ argv: string[];
31
+ cwd?: string;
32
+ env?: Record<string, string>;
33
+ };
34
+
26
35
  export type RadarChildren = {
27
36
  list(): Promise<RadarResult<RadarChild[]>>;
37
+ /**
38
+ * Replace this caller's live child with a new process in its own pane and
39
+ * session, and wait for the daemon to grant it or withdraw the launch.
40
+ */
41
+ restart(
42
+ subject: string,
43
+ program: RadarChildProgram,
44
+ options?: { pollMs?: number; signal?: AbortSignal },
45
+ ): Promise<RadarLaunchOutcome>;
28
46
  assignment(subject: string, owner: string): RadarChildAssignment;
29
47
  retire(subject: string): void;
30
48
  observe(
@@ -43,8 +61,8 @@ type Resolved = { child: RadarChild; registration_id: string };
43
61
  * this surface exists to hide. An edge bound without a subject is not addressable.
44
62
  */
45
63
  export function createRadarChildren(
46
- client: Pick<RadarClient, "spawnList" | "spawnGet" | "agentGet" | "output" | "processInfo">,
47
- publication: Pick<RadarPublication, "assignment" | "retireAssignment">,
64
+ client: Pick<RadarClient, "spawnList" | "spawnGet" | "agentGet" | "output" | "processInfo" | "restart">,
65
+ publication: Pick<RadarPublication, "assignment" | "retireAssignment" | "subject">,
48
66
  ): RadarChildren {
49
67
  const held = new Map<
50
68
  string,
@@ -59,7 +77,7 @@ export function createRadarChildren(
59
77
  if (read.ok === false) return read;
60
78
  for (const edge of read.value.spawns as ListedEdge[]) {
61
79
  const bound = edge.bound;
62
- if (!bound?.subject_id || !bound.registration_id || edge.launched !== "completed") continue;
80
+ if (!bound?.subject_id || !bound.registration_id || edge.granted_at == null || edge.withdrawn != null) continue;
63
81
  const at = Date.parse(bound.bound_at);
64
82
  if (Number.isNaN(at)) continue;
65
83
  const current = newest.get(bound.subject_id);
@@ -114,6 +132,34 @@ export function createRadarChildren(
114
132
  follow(all.value);
115
133
  return { ok: true, value: [...all.value.values()].map((entry) => entry.child) };
116
134
  },
135
+ async restart(subject, program, options) {
136
+ const parent = await publication.subject();
137
+ if (parent?.subject_id === undefined)
138
+ return { kind: "failed", message: "this process has no Radar registration to restart a child under" };
139
+ const request_id = randomUUID();
140
+ const requested = await client.restart({
141
+ request_id,
142
+ parent: parent.registration_id,
143
+ subject_id: subject,
144
+ ...program,
145
+ });
146
+ if (requested.ok === false)
147
+ return launchFailure(requested, request_id);
148
+ const bound = await waitForChild(client, requested.value.launch_request_id, {
149
+ pollMs: options?.pollMs ?? 250,
150
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
151
+ });
152
+ if (bound.kind !== "bound") return bound;
153
+ await resolveAndFollow(subject);
154
+ return {
155
+ kind: "ready",
156
+ value: {
157
+ request_id: requested.value.launch_request_id,
158
+ subject_id: bound.subject_id,
159
+ pane_id: bound.pane_id,
160
+ },
161
+ };
162
+ },
117
163
  assignment(subject, owner) {
118
164
  const existing = held.get(subject);
119
165
  if (existing !== undefined) return existing.writer;
@@ -38,7 +38,7 @@ export const RADAR_PROTOCOL_VERSION = 2;
38
38
  * parent continue or restart its own child by subject.
39
39
  * Earlier daemons expose different identity fields; require a coordinated update.
40
40
  */
41
- export const RADAR_MINIMUM_REVISION = 7;
41
+ export const RADAR_MINIMUM_REVISION = 8;
42
42
  /** Public daemon contracts this client may preflight. */
43
43
  export const RADAR_REGISTRY_CONTRACT = "registry.v1";
44
44
  export const RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
@@ -309,6 +309,9 @@ export type RadarSpawnEdge = {
309
309
  location?: RadarLocation;
310
310
  bound?: { source: string; incarnation: string; registration_id?: string; subject_id?: string };
311
311
  exit_observation?: RadarSpawnExitObservation;
312
+ /** Set when the daemon withdrew the launch before its process presented. */
313
+ withdrawn?: { at: string; reason: string };
314
+ granted_at?: string;
312
315
  };
313
316
 
314
317
  /**
@@ -830,6 +833,9 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
830
833
  const agentId = stringAt(bound, "registration_id");
831
834
  const subjectId = stringAt(bound, "subject_id");
832
835
  const exit = exitAt(recordAt(value, "exit_observation"));
836
+ const withdrawnRecord = recordAt(value, "withdrawn");
837
+ const withdrawnAt = stringAt(withdrawnRecord, "at");
838
+ const withdrawnReason = stringAt(withdrawnRecord, "reason");
833
839
  return {
834
840
  ok: true,
835
841
  value: {
@@ -849,6 +855,10 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
849
855
  }
850
856
  : {}),
851
857
  ...(exit !== undefined ? { exit_observation: exit } : {}),
858
+ ...(stringAt(value, "granted_at") !== undefined ? { granted_at: stringAt(value, "granted_at") } : {}),
859
+ ...(withdrawnAt !== undefined && withdrawnReason !== undefined
860
+ ? { withdrawn: { at: withdrawnAt, reason: withdrawnReason } }
861
+ : {}),
852
862
  },
853
863
  };
854
864
  }
@@ -1124,7 +1134,7 @@ export type RadarClient = {
1124
1134
  argv: string[];
1125
1135
  cwd?: string;
1126
1136
  env?: Record<string, string>;
1127
- }): Promise<RadarResult<{ request_id: string }>>;
1137
+ }): Promise<RadarResult<{ request_id: string; launch_request_id: string }>>;
1128
1138
  /** List launches owned by this client's registered process. */
1129
1139
  spawnList(request: Pick<SpawnListParamsDeserialize, "after" | "limit">): Promise<RadarResult<SpawnListPageSerialize>>;
1130
1140
  spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
@@ -1757,7 +1767,11 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1757
1767
  request.request_id,
1758
1768
  );
1759
1769
  if (answer.ok === false) return answer;
1760
- return focusOutcome(recordAt(answer.value, "request"), request.request_id);
1770
+ const settled = focusOutcome(recordAt(answer.value, "request"), request.request_id);
1771
+ if (settled.ok === false) return settled;
1772
+ const launch = recordAt(recordAt(answer.value, "request"), "launch_request_id");
1773
+ if (!isUuid(launch)) return protocol("lifecycle.restart reply names no launch");
1774
+ return { ok: true, value: { ...settled.value, launch_request_id: launch } };
1761
1775
  });
1762
1776
  },
1763
1777
  async spawnList(request) {
@@ -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,33 +30,24 @@ 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
53
  /**
@@ -158,8 +92,8 @@ function trajectory(reason: string): string {
158
92
  *
159
93
  * The durable spawn record and the child's own binding say *which* subject this
160
94
  * 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
95
+ * child is gone. A presence grant is startup evidence, not perpetual liveness.
96
+ * Only a current topology read decides: `bound` has to name
163
97
  * the same subject, the read has to record the pane this launch claims, no
164
98
  * process exit may have been observed, and the read's own `freshness` has to
165
99
  * report that the daemon's inventory currently places that pane. Anything else -
@@ -293,26 +227,25 @@ export async function relaunchRefusalForChild(
293
227
  return `the child's registered process is ${verification === "verified" ? "still running" : "not proved gone"}`;
294
228
  }
295
229
 
296
- export type RadarLaunchOptions<State extends LaunchState = LaunchState> = {
230
+ export type RadarLaunchOptions = {
297
231
  client: RadarClient;
298
232
  /** This process's own publication, which names the parent of the launch. */
299
233
  publication: Pick<RadarPublication, "subject">;
300
- readers: LaunchMailbox<State>;
301
234
  launch: RadarWorkerLaunch;
302
- timeoutMs: number;
303
- /** How often the binding and readiness waits re-read; 0 in driven tests. */
235
+ /** How often the wait re-reads the launch; 0 in driven tests. */
304
236
  pollMs?: number;
237
+ /** Stops the wait. The daemon's own deadline still withdraws the launch. */
305
238
  signal?: AbortSignal;
306
239
  };
307
240
 
308
- function refusedLaunch(message: string): Extract<RadarLaunchOutcome, { kind: "refused" | "unresolved" }> {
309
- return { kind: "refused", message };
241
+ function refusedLaunch(message: string): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
242
+ return { kind: "failed", message };
310
243
  }
311
244
 
312
245
  function unresolvedLaunch(
313
246
  requestId: string,
314
247
  message: string,
315
- ): Extract<RadarLaunchOutcome, { kind: "refused" | "unresolved" }> {
248
+ ): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
316
249
  return {
317
250
  kind: "unresolved",
318
251
  request_id: requestId,
@@ -320,19 +253,6 @@ function unresolvedLaunch(
320
253
  };
321
254
  }
322
255
 
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
256
  /**
337
257
  * The daemon's own refusals, which prove no effect was dispatched. Every other
338
258
  * failure — a lost reply, a timeout, an internal error, an unreadable answer —
@@ -353,27 +273,24 @@ const PRE_EFFECT_CODES: readonly RadarErrorCode[] = [
353
273
  "backend_unavailable",
354
274
  ];
355
275
 
356
- function spawnFailure(
357
- result: Extract<RadarResult<RadarSpawnEdge>, { ok: false }>,
276
+ export function launchFailure(
277
+ result: Extract<RadarResult<unknown>, { ok: false }>,
358
278
  requestId: string,
359
- ): Extract<RadarLaunchOutcome, { kind: "refused" | "unresolved" }> {
279
+ ): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
360
280
  return PRE_EFFECT_CODES.includes(result.code)
361
281
  ? refusedLaunch(result.message)
362
282
  : unresolvedLaunch(requestId, result.message);
363
283
  }
364
284
 
365
285
  /**
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.
286
+ * Wait for the child's verified presence grant or the daemon's withdrawal. The
287
+ * same request UUID is read throughout; a lost reply never starts another child.
369
288
  */
370
- export async function launchRadarWorker<State extends LaunchState>(
371
- options: RadarLaunchOptions<State>,
372
- ): Promise<RadarLaunchOutcome<State>> {
289
+ export async function launchRadarWorker(
290
+ options: RadarLaunchOptions,
291
+ ): Promise<RadarLaunchOutcome> {
373
292
  const { client, launch, signal } = options;
374
293
  const pollMs = options.pollMs ?? 250;
375
- const deadline = Date.now() + options.timeoutMs;
376
- const remaining = (): number => Math.max(0, deadline - Date.now());
377
294
  const parent = await options.publication.subject();
378
295
  if (parent === undefined || !isUuid(parent.registration_id))
379
296
  return refusedLaunch(
@@ -408,87 +325,46 @@ export async function launchRadarWorker<State extends LaunchState>(
408
325
  ...(Object.keys(launch.env).length > 0 ? { env: launch.env } : {}),
409
326
  });
410
327
  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));
328
+ const refusal = launchFailure(spawned, launch.request_id);
329
+ if (refusal.kind === "failed") return refusal;
420
330
  }
421
331
 
422
332
  const bound = await waitForChild(client, launch.request_id, {
423
- remaining,
424
333
  pollMs,
425
334
  ...(signal !== undefined ? { signal } : {}),
426
335
  });
427
336
  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
- }
337
+ return {
338
+ kind: "ready",
339
+ value: {
340
+ request_id: launch.request_id,
341
+ subject_id: bound.subject_id,
342
+ pane_id: bound.pane_id,
343
+ },
344
+ };
464
345
  }
465
346
 
466
347
  type BoundChild =
467
348
  | {
468
349
  kind: "bound";
469
350
  subject_id: string;
470
- registration_id: string;
471
351
  pane_id: string;
472
- source: string;
473
- incarnation: string;
474
352
  }
475
- | Extract<RadarLaunchOutcome, { kind: "refused" | "unresolved" }>;
353
+ | Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }>;
476
354
 
477
355
  /**
478
- * Poll the edge this request id authored until a child presents its token.
356
+ * Poll the edge this request id authored until the daemon grants its child or
357
+ * withdraws the launch. There is no timeout here: the daemon owns the deadline and
358
+ * records a withdrawal on the edge, so waiting longer than it would only wait for
359
+ * an outcome that is already decided.
479
360
  *
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.
361
+ * Registration binding alone is not success: a pending or duplicate presence
362
+ * can bind an edge without ever acquiring attachment authority.
486
363
  */
487
- async function waitForChild(
488
- client: RadarClient,
364
+ export async function waitForChild(
365
+ client: Pick<RadarClient, "spawnGet">,
489
366
  requestId: string,
490
367
  options: {
491
- remaining: () => number;
492
368
  pollMs: number;
493
369
  signal?: AbortSignal;
494
370
  },
@@ -499,53 +375,22 @@ async function waitForChild(
499
375
  return unresolvedLaunch(requestId, `the launch was cancelled (${last})`);
500
376
  const answer = await client.spawnGet(requestId);
501
377
  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));
378
+ const { edge } = answer.value;
379
+ if (edge.withdrawn !== undefined)
380
+ return refusedLaunch(`the daemon withdrew this launch: ${edge.withdrawn.reason}`);
381
+ if (edge.granted_at === undefined) {
382
+ await delay(options.pollMs);
383
+ continue;
384
+ }
508
385
  const pane = edge.location?.pane;
509
386
  if (pane === undefined || pane === "")
510
387
  return unresolvedLaunch(
511
388
  requestId,
512
389
  "the daemon's launch recorded no pane to verify the child against",
513
390
  );
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
- );
391
+
531
392
  const agentId = edge.bound?.registration_id;
532
393
  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
394
  // The edge names the agent as well as its process. Without it the child
550
395
  // has no durable key to be returned by, and a registration is never
551
396
  // offered in its place.
@@ -558,20 +403,13 @@ async function waitForChild(
558
403
  return {
559
404
  kind: "bound",
560
405
  subject_id: subjectId,
561
- registration_id: agentId,
562
406
  pane_id: pane,
563
- source,
564
- incarnation,
565
407
  };
566
408
  }
567
409
  last = "no child has registered with this launch's token yet";
568
410
  } else {
569
- last = answer.message;
411
+ return unresolvedLaunch(requestId, answer.message);
570
412
  }
571
- if (options.remaining() <= 0)
572
- return unresolvedLaunch(requestId, last);
573
- await delay(options.pollMs, undefined, {
574
- ...(options.signal !== undefined ? { signal: options.signal } : {}),
575
- });
413
+ await delay(options.pollMs);
576
414
  }
577
415
  }
@@ -5,7 +5,7 @@ import type { RadarPublication } from "./radar-publication.ts";
5
5
 
6
6
  /** The event channel through which Pi extensions obtain the baseline runtime. */
7
7
  export const RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
8
- export const RADAR_RUNTIME_VERSION = 3 as const;
8
+ export const RADAR_RUNTIME_VERSION = 4 as const;
9
9
 
10
10
  /** The sole baseline publisher's session-scoped runtime, shared with consumers. */
11
11
  export type RadarRuntimeServices = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-radar",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Radar daemon client, communication, publication, execution, location, launch and inspection surface for Pi extensions.",
6
6
  "license": "Apache-2.0",
@@ -893,12 +893,6 @@
893
893
  },
894
894
  "subject_id": {
895
895
  "type": "string"
896
- },
897
- "successor": {
898
- "type": [
899
- "string",
900
- "null"
901
- ]
902
896
  }
903
897
  },
904
898
  "required": [
@@ -1060,12 +1054,6 @@
1060
1054
  },
1061
1055
  "subject_id": {
1062
1056
  "type": "string"
1063
- },
1064
- "successor": {
1065
- "type": [
1066
- "string",
1067
- "null"
1068
- ]
1069
1057
  }
1070
1058
  },
1071
1059
  "required": [
@@ -941,12 +941,6 @@
941
941
  },
942
942
  "subject_id": {
943
943
  "type": "string"
944
- },
945
- "successor": {
946
- "type": [
947
- "string",
948
- "null"
949
- ]
950
944
  }
951
945
  },
952
946
  "required": [
@@ -954,8 +948,7 @@
954
948
  "bound_registration_id",
955
949
  "process",
956
950
  "state",
957
- "session",
958
- "successor"
951
+ "session"
959
952
  ],
960
953
  "type": "object"
961
954
  },
@@ -1119,12 +1112,6 @@
1119
1112
  },
1120
1113
  "subject_id": {
1121
1114
  "type": "string"
1122
- },
1123
- "successor": {
1124
- "type": [
1125
- "string",
1126
- "null"
1127
- ]
1128
1115
  }
1129
1116
  },
1130
1117
  "required": [
@@ -1135,8 +1122,7 @@
1135
1122
  "metadata",
1136
1123
  "child_close",
1137
1124
  "state",
1138
- "session",
1139
- "successor"
1125
+ "session"
1140
1126
  ],
1141
1127
  "type": "object"
1142
1128
  },
@@ -893,12 +893,6 @@
893
893
  },
894
894
  "subject_id": {
895
895
  "type": "string"
896
- },
897
- "successor": {
898
- "type": [
899
- "string",
900
- "null"
901
- ]
902
896
  }
903
897
  },
904
898
  "required": [
@@ -1060,12 +1054,6 @@
1060
1054
  },
1061
1055
  "subject_id": {
1062
1056
  "type": "string"
1063
- },
1064
- "successor": {
1065
- "type": [
1066
- "string",
1067
- "null"
1068
- ]
1069
1057
  }
1070
1058
  },
1071
1059
  "required": [