pi-radar 0.7.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.
- package/README.md +12 -0
- package/dist/extension/baseline.js +386 -10
- package/dist/extension/radar-children.js +344 -0
- package/dist/extension/radar-client.js +47 -4
- package/dist/extension/radar-launch.js +42 -112
- package/dist/extension/radar-runtime.js +1 -1
- package/dist/index.js +471 -341
- package/extension/baseline.ts +2 -0
- package/extension/generated-contracts.ts +15 -2
- package/extension/radar-children.ts +222 -0
- package/extension/radar-client.ts +70 -2
- package/extension/radar-launch.ts +91 -274
- package/extension/radar-runtime.ts +4 -1
- package/index.ts +1 -0
- package/package.json +8 -3
- package/schema/public/v1/observation-get-response.deserialize.schema.json +1 -13
- package/schema/public/v1/observation-get-response.serialize.schema.json +3 -17
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +1 -13
- package/schema/public/v1/observation-snapshot.serialize.schema.json +3 -17
- package/schema/public/v1/spawn-list-page.deserialize.schema.json +7 -0
- package/schema/public/v1/spawn-list-page.serialize.schema.json +7 -0
|
@@ -5,135 +5,49 @@ import {
|
|
|
5
5
|
type RadarErrorCode,
|
|
6
6
|
type RadarLocation,
|
|
7
7
|
type RadarResult,
|
|
8
|
-
type RadarSpawnEdge,
|
|
9
8
|
} from "./radar-client.ts";
|
|
10
|
-
import {
|
|
9
|
+
import type { RadarPublication } from "./radar-publication.ts";
|
|
11
10
|
|
|
12
|
-
/**
|
|
13
|
-
export type StartupExpectation = {
|
|
14
|
-
generation: string;
|
|
15
|
-
runId: string;
|
|
16
|
-
ownerSessionId: string;
|
|
17
|
-
workspaceId: string;
|
|
18
|
-
agentLabel: string;
|
|
19
|
-
paneId: string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The mailbox readers a launch needs. The mailbox belongs to the caller (Herdsman
|
|
24
|
-
* supplies its own state shape and readers); this module only takes the path it
|
|
25
|
-
* is handed and asks these questions of it.
|
|
26
|
-
*/
|
|
27
|
-
export type LaunchState = { paneId: string };
|
|
28
|
-
|
|
29
|
-
export type LaunchMailbox<State extends LaunchState> = {
|
|
30
|
-
readState(path: string): State | undefined;
|
|
31
|
-
waitForState(
|
|
32
|
-
path: string,
|
|
33
|
-
predicate: (state: State) => boolean,
|
|
34
|
-
options: { timeoutMs: number; signal?: AbortSignal },
|
|
35
|
-
): Promise<State>;
|
|
36
|
-
startupAckMatches(state: State | undefined, expected: StartupExpectation): boolean;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Launching one fresh managed worker through Radar's spawn operation.
|
|
41
|
-
*
|
|
42
|
-
* A spawn is one durable effect behind the caller's own request UUID, and three
|
|
43
|
-
* separate facts decide whether a child can take an assignment:
|
|
44
|
-
*
|
|
45
|
-
* 1. the daemon created a pane under this process's registered pane, launched
|
|
46
|
-
* the child's exact command into it, and still reports that pane in its own
|
|
47
|
-
* inventory;
|
|
48
|
-
* 2. a child registered with the launch's private token, so the edge names the
|
|
49
|
-
* exact `registration_id` that child obtained — never a label, pane or session;
|
|
50
|
-
* 3. that child wrote a fresh startup acknowledgment for this launch generation
|
|
51
|
-
* after Herdsman startup finished, naming the pane the daemon created as its
|
|
52
|
-
* own location.
|
|
53
|
-
*
|
|
54
|
-
* Only the third is application readiness; the first two are prerequisites. A
|
|
55
|
-
* lost reply is reconciled against the same request UUID and never re-sent, so a
|
|
56
|
-
* failed launch cannot leave a second pane or a second child behind.
|
|
57
|
-
*
|
|
58
|
-
* The child's location is a fact about the pane the daemon created, not about
|
|
59
|
-
* the environment it was launched with: the daemon passes an environment overlay
|
|
60
|
-
* to a mux-created pane and cannot remove what that pane already carries, so a
|
|
61
|
-
* child that inherited its parent's pane coordinates would claim the parent's
|
|
62
|
-
* pane. The daemon's fresh record of the created pane and the child's own claim
|
|
63
|
-
* must therefore name the same pane, and either one being stale, missing or
|
|
64
|
-
* different leaves the launch unresolved rather than placed.
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
|
-
/** The exact managed parent this launch may use, and nothing inferred about it. */
|
|
68
|
-
export type RadarLaunchParent = {
|
|
69
|
-
registration_id: string;
|
|
70
|
-
/** Stable subject from this process's current Radar registration. */
|
|
71
|
-
subject_id: string;
|
|
72
|
-
location?: RadarLocation;
|
|
73
|
-
};
|
|
11
|
+
/** A launch succeeds when Radar grants its child's verified first presence. */
|
|
74
12
|
|
|
75
13
|
/** Everything one fresh managed worker is launched with. */
|
|
76
14
|
export type RadarWorkerLaunch = {
|
|
77
15
|
/** The caller's durable key, persisted before dispatch and never replaced. */
|
|
78
16
|
request_id: string;
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Start this released child agent again rather than a new one. The daemon takes
|
|
19
|
+
* the session from its own record, so no session id or path is sent.
|
|
20
|
+
*/
|
|
21
|
+
continues?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The pane this launch splits. The parent is the calling process itself, read
|
|
24
|
+
* from its own publication, and must be registered on exactly this pane.
|
|
25
|
+
*/
|
|
26
|
+
pane_id: string;
|
|
82
27
|
parent_session_id: string;
|
|
83
28
|
executable: string;
|
|
84
29
|
argv: string[];
|
|
85
30
|
cwd: string;
|
|
86
31
|
/** The environment overlay the child needs, not a replacement environment. */
|
|
87
32
|
env: Record<string, string>;
|
|
88
|
-
/** The child's mailbox: its own state is the readiness evidence. */
|
|
89
|
-
mailbox: string;
|
|
90
|
-
/** The identity the child's state and acknowledgment must repeat exactly. */
|
|
91
|
-
identity: {
|
|
92
|
-
runId: string;
|
|
93
|
-
ownerSessionId: string;
|
|
94
|
-
workspaceId: string;
|
|
95
|
-
agentLabel: string;
|
|
96
|
-
};
|
|
97
|
-
/**
|
|
98
|
-
* The child's own published registration, once it has one. It is the second
|
|
99
|
-
* witness that the edge's bound id is this launch's child, and it is read
|
|
100
|
-
* through the owner's publication store rather than assumed from the pane.
|
|
101
|
-
*/
|
|
102
|
-
observedChildAgent?: () => string | undefined;
|
|
103
|
-
/**
|
|
104
|
-
* Persist the daemon's own binding of this launch's edge, the subject it bound
|
|
105
|
-
* and the pane it recorded, before readiness is waited for. This is the only
|
|
106
|
-
* moment those facts are authoritative: the launch can end without ever
|
|
107
|
-
* reaching an acknowledgment, and a reload in that window still has to read
|
|
108
|
-
* where the daemon put this child. Throwing leaves the launch unresolved,
|
|
109
|
-
* because a binding that was not persisted is one no later reader can hold the
|
|
110
|
-
* child by.
|
|
111
|
-
*/
|
|
112
|
-
onBound?: (bound: { registration_id: string; pane_id: string }) => void;
|
|
113
33
|
};
|
|
114
34
|
|
|
115
|
-
export type RadarWorkerLaunched
|
|
35
|
+
export type RadarWorkerLaunched = {
|
|
116
36
|
request_id: string;
|
|
117
|
-
|
|
37
|
+
/** The child's durable identity: the key that survives its restart and resume. */
|
|
38
|
+
subject_id: string;
|
|
118
39
|
pane_id: string;
|
|
119
|
-
/**
|
|
120
|
-
* The process the daemon bound this launch's edge to, taken from that same
|
|
121
|
-
* answer: the source and incarnation the child registered under. They are what
|
|
122
|
-
* an assignment to this child is addressed with, so the first task of a fresh
|
|
123
|
-
* launch is addressed exactly as a retained continuation is.
|
|
124
|
-
*/
|
|
125
|
-
bound_process: { source: string; incarnation: string };
|
|
126
|
-
state: State;
|
|
127
40
|
};
|
|
128
41
|
|
|
129
42
|
/**
|
|
130
|
-
* `
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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.
|
|
133
47
|
*/
|
|
134
|
-
export type RadarLaunchOutcome
|
|
135
|
-
| { kind: "ready"; value: RadarWorkerLaunched
|
|
136
|
-
| { kind: "
|
|
48
|
+
export type RadarLaunchOutcome =
|
|
49
|
+
| { kind: "ready"; value: RadarWorkerLaunched }
|
|
50
|
+
| { kind: "failed"; message: string }
|
|
137
51
|
| { kind: "unresolved"; request_id: string; message: string };
|
|
138
52
|
|
|
139
53
|
/**
|
|
@@ -178,8 +92,8 @@ function trajectory(reason: string): string {
|
|
|
178
92
|
*
|
|
179
93
|
* The durable spawn record and the child's own binding say *which* subject this
|
|
180
94
|
* launch created; they are provenance, not liveness, and they stay true after a
|
|
181
|
-
* child is gone.
|
|
182
|
-
*
|
|
95
|
+
* child is gone. A presence grant is startup evidence, not perpetual liveness.
|
|
96
|
+
* Only a current topology read decides: `bound` has to name
|
|
183
97
|
* the same subject, the read has to record the pane this launch claims, no
|
|
184
98
|
* process exit may have been observed, and the read's own `freshness` has to
|
|
185
99
|
* report that the daemon's inventory currently places that pane. Anything else -
|
|
@@ -313,24 +227,25 @@ export async function relaunchRefusalForChild(
|
|
|
313
227
|
return `the child's registered process is ${verification === "verified" ? "still running" : "not proved gone"}`;
|
|
314
228
|
}
|
|
315
229
|
|
|
316
|
-
export type RadarLaunchOptions
|
|
230
|
+
export type RadarLaunchOptions = {
|
|
317
231
|
client: RadarClient;
|
|
318
|
-
|
|
232
|
+
/** This process's own publication, which names the parent of the launch. */
|
|
233
|
+
publication: Pick<RadarPublication, "subject">;
|
|
319
234
|
launch: RadarWorkerLaunch;
|
|
320
|
-
|
|
321
|
-
/** 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. */
|
|
322
236
|
pollMs?: number;
|
|
237
|
+
/** Stops the wait. The daemon's own deadline still withdraws the launch. */
|
|
323
238
|
signal?: AbortSignal;
|
|
324
239
|
};
|
|
325
240
|
|
|
326
|
-
function refusedLaunch(message: string): Extract<RadarLaunchOutcome, { kind: "
|
|
327
|
-
return { kind: "
|
|
241
|
+
function refusedLaunch(message: string): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
|
|
242
|
+
return { kind: "failed", message };
|
|
328
243
|
}
|
|
329
244
|
|
|
330
245
|
function unresolvedLaunch(
|
|
331
246
|
requestId: string,
|
|
332
247
|
message: string,
|
|
333
|
-
): Extract<RadarLaunchOutcome, { kind: "
|
|
248
|
+
): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
|
|
334
249
|
return {
|
|
335
250
|
kind: "unresolved",
|
|
336
251
|
request_id: requestId,
|
|
@@ -338,19 +253,6 @@ function unresolvedLaunch(
|
|
|
338
253
|
};
|
|
339
254
|
}
|
|
340
255
|
|
|
341
|
-
/** Whether the edge's own two effects already prove a launch happened. */
|
|
342
|
-
function edgeStanding(edge: RadarSpawnEdge): "launched" | "refused" | "unknown" {
|
|
343
|
-
if (edge.created === "refused" || edge.launched === "refused")
|
|
344
|
-
return "refused";
|
|
345
|
-
return edge.created === "completed" && edge.launched === "completed"
|
|
346
|
-
? "launched"
|
|
347
|
-
: "unknown";
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function edgeMessage(edge: RadarSpawnEdge): string {
|
|
351
|
-
return `the daemon's spawn record is created ${edge.created} and launched ${edge.launched}`;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
256
|
/**
|
|
355
257
|
* The daemon's own refusals, which prove no effect was dispatched. Every other
|
|
356
258
|
* failure — a lost reply, a timeout, an internal error, an unreadable answer —
|
|
@@ -371,155 +273,98 @@ const PRE_EFFECT_CODES: readonly RadarErrorCode[] = [
|
|
|
371
273
|
"backend_unavailable",
|
|
372
274
|
];
|
|
373
275
|
|
|
374
|
-
function
|
|
375
|
-
result: Extract<RadarResult<
|
|
276
|
+
export function launchFailure(
|
|
277
|
+
result: Extract<RadarResult<unknown>, { ok: false }>,
|
|
376
278
|
requestId: string,
|
|
377
|
-
): Extract<RadarLaunchOutcome, { kind: "
|
|
279
|
+
): Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }> {
|
|
378
280
|
return PRE_EFFECT_CODES.includes(result.code)
|
|
379
281
|
? refusedLaunch(result.message)
|
|
380
282
|
: unresolvedLaunch(requestId, result.message);
|
|
381
283
|
}
|
|
382
284
|
|
|
383
285
|
/**
|
|
384
|
-
* Wait for the child's
|
|
385
|
-
*
|
|
386
|
-
* 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.
|
|
387
288
|
*/
|
|
388
|
-
export async function launchRadarWorker
|
|
389
|
-
options: RadarLaunchOptions
|
|
390
|
-
): Promise<RadarLaunchOutcome
|
|
289
|
+
export async function launchRadarWorker(
|
|
290
|
+
options: RadarLaunchOptions,
|
|
291
|
+
): Promise<RadarLaunchOutcome> {
|
|
391
292
|
const { client, launch, signal } = options;
|
|
392
293
|
const pollMs = options.pollMs ?? 250;
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
const pane = launch.parent.location?.pane;
|
|
396
|
-
if (!isUuid(launch.parent.registration_id))
|
|
294
|
+
const parent = await options.publication.subject();
|
|
295
|
+
if (parent === undefined || !isUuid(parent.registration_id))
|
|
397
296
|
return refusedLaunch(
|
|
398
|
-
"
|
|
297
|
+
"this process has no Radar registration, so a worker cannot be spawned under it",
|
|
399
298
|
);
|
|
400
|
-
if (!isUuid(
|
|
299
|
+
if (parent.subject_id === undefined || !isUuid(parent.subject_id))
|
|
401
300
|
return refusedLaunch(
|
|
402
|
-
"
|
|
301
|
+
"this process has no managed Radar subject, so a worker cannot be spawned under it",
|
|
403
302
|
);
|
|
404
303
|
// A spawn is created under the pane its parent registered, and this port
|
|
405
304
|
// registers no location it was not given: without one there is nothing to
|
|
406
305
|
// place the child beside, and a name or session lookup is not a substitute.
|
|
306
|
+
const pane = parent.location?.pane;
|
|
407
307
|
if (pane === undefined || pane === "")
|
|
408
308
|
return refusedLaunch(
|
|
409
|
-
"this process's Radar registration names no pane, so
|
|
309
|
+
"this process's Radar registration names no pane, so a worker cannot be placed beside it",
|
|
310
|
+
);
|
|
311
|
+
if (pane !== launch.pane_id)
|
|
312
|
+
return refusedLaunch(
|
|
313
|
+
`this process's Radar registration places it in pane ${pane}, not in the pane this launch would split`,
|
|
410
314
|
);
|
|
411
315
|
|
|
412
316
|
const spawned = await client.spawn({
|
|
413
317
|
request_id: launch.request_id,
|
|
414
|
-
parent:
|
|
318
|
+
parent: parent.registration_id,
|
|
415
319
|
parent_session_id: launch.parent_session_id,
|
|
416
320
|
executable: launch.executable,
|
|
417
|
-
managed: { parent_subject_id:
|
|
321
|
+
managed: { parent_subject_id: parent.subject_id },
|
|
322
|
+
...(launch.continues !== undefined ? { continue_subject_id: launch.continues } : {}),
|
|
418
323
|
argv: launch.argv,
|
|
419
324
|
...(launch.cwd !== "" ? { cwd: launch.cwd } : {}),
|
|
420
325
|
...(Object.keys(launch.env).length > 0 ? { env: launch.env } : {}),
|
|
421
326
|
});
|
|
422
327
|
if (spawned.ok === false) {
|
|
423
|
-
const refusal =
|
|
424
|
-
if (refusal.kind === "
|
|
425
|
-
}
|
|
426
|
-
if (spawned.ok === true) {
|
|
427
|
-
const standing = edgeStanding(spawned.value);
|
|
428
|
-
if (standing === "refused")
|
|
429
|
-
return refusedLaunch(edgeMessage(spawned.value));
|
|
430
|
-
if (standing === "unknown")
|
|
431
|
-
return unresolvedLaunch(launch.request_id, edgeMessage(spawned.value));
|
|
328
|
+
const refusal = launchFailure(spawned, launch.request_id);
|
|
329
|
+
if (refusal.kind === "failed") return refusal;
|
|
432
330
|
}
|
|
433
331
|
|
|
434
332
|
const bound = await waitForChild(client, launch.request_id, {
|
|
435
|
-
remaining,
|
|
436
333
|
pollMs,
|
|
437
334
|
...(signal !== undefined ? { signal } : {}),
|
|
438
335
|
});
|
|
439
336
|
if (bound.kind !== "bound") return bound;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return unresolvedLaunch(
|
|
449
|
-
launch.request_id,
|
|
450
|
-
`the daemon's binding of the child ${agentId} could not be recorded (${String(error)})`,
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
try {
|
|
456
|
-
const state = await options.readers.waitForState(
|
|
457
|
-
launch.mailbox,
|
|
458
|
-
(candidate) =>
|
|
459
|
-
options.readers.startupAckMatches(candidate, {
|
|
460
|
-
generation: launch.generation,
|
|
461
|
-
runId: launch.identity.runId,
|
|
462
|
-
ownerSessionId: launch.identity.ownerSessionId,
|
|
463
|
-
workspaceId: launch.identity.workspaceId,
|
|
464
|
-
agentLabel: launch.identity.agentLabel,
|
|
465
|
-
// The child writes the raw TMUX_PANE of the pane it runs in. Requiring
|
|
466
|
-
// the pane the daemon created is what refuses a child that inherited
|
|
467
|
-
// another process's coordinates.
|
|
468
|
-
paneId: radarPaneHandle(bound.pane_id),
|
|
469
|
-
}) &&
|
|
470
|
-
(launch.observedChildAgent === undefined ||
|
|
471
|
-
launch.observedChildAgent() === agentId),
|
|
472
|
-
{ timeoutMs: remaining(), ...(signal !== undefined ? { signal } : {}) },
|
|
473
|
-
);
|
|
474
|
-
return {
|
|
475
|
-
kind: "ready",
|
|
476
|
-
value: {
|
|
477
|
-
request_id: launch.request_id,
|
|
478
|
-
registration_id: agentId,
|
|
479
|
-
pane_id: bound.pane_id,
|
|
480
|
-
bound_process: {
|
|
481
|
-
source: bound.source,
|
|
482
|
-
incarnation: bound.incarnation,
|
|
483
|
-
},
|
|
484
|
-
state,
|
|
485
|
-
},
|
|
486
|
-
};
|
|
487
|
-
} catch (error) {
|
|
488
|
-
const claimed = options.readers.readState(launch.mailbox)?.paneId;
|
|
489
|
-
return unresolvedLaunch(
|
|
490
|
-
launch.request_id,
|
|
491
|
-
claimed !== undefined && claimed !== radarPaneHandle(bound.pane_id)
|
|
492
|
-
? `the bound child ${agentId} claims the pane ${claimed}, not the pane ${bound.pane_id} this launch created for it`
|
|
493
|
-
: `the bound child ${agentId} has not acknowledged this launch's startup (${String(error)})`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
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
|
+
};
|
|
496
345
|
}
|
|
497
346
|
|
|
498
347
|
type BoundChild =
|
|
499
348
|
| {
|
|
500
349
|
kind: "bound";
|
|
501
|
-
|
|
350
|
+
subject_id: string;
|
|
502
351
|
pane_id: string;
|
|
503
|
-
source: string;
|
|
504
|
-
incarnation: string;
|
|
505
352
|
}
|
|
506
|
-
| Extract<RadarLaunchOutcome, { kind: "
|
|
353
|
+
| Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }>;
|
|
507
354
|
|
|
508
355
|
/**
|
|
509
|
-
* Poll the edge this request id authored until
|
|
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.
|
|
510
360
|
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* effect ends the wait at once: neither becomes a child by waiting longer. An
|
|
514
|
-
* edge whose recorded pane the daemon no longer reports ends it too: waiting
|
|
515
|
-
* cannot create the pane a launch claims, and a location read afterwards would
|
|
516
|
-
* 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.
|
|
517
363
|
*/
|
|
518
|
-
async function waitForChild(
|
|
519
|
-
client: RadarClient,
|
|
364
|
+
export async function waitForChild(
|
|
365
|
+
client: Pick<RadarClient, "spawnGet">,
|
|
520
366
|
requestId: string,
|
|
521
367
|
options: {
|
|
522
|
-
remaining: () => number;
|
|
523
368
|
pollMs: number;
|
|
524
369
|
signal?: AbortSignal;
|
|
525
370
|
},
|
|
@@ -530,69 +375,41 @@ async function waitForChild(
|
|
|
530
375
|
return unresolvedLaunch(requestId, `the launch was cancelled (${last})`);
|
|
531
376
|
const answer = await client.spawnGet(requestId);
|
|
532
377
|
if (answer.ok === true) {
|
|
533
|
-
const { edge
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
}
|
|
539
385
|
const pane = edge.location?.pane;
|
|
540
386
|
if (pane === undefined || pane === "")
|
|
541
387
|
return unresolvedLaunch(
|
|
542
388
|
requestId,
|
|
543
389
|
"the daemon's launch recorded no pane to verify the child against",
|
|
544
390
|
);
|
|
545
|
-
|
|
546
|
-
// and launched this child, and `fresh` is its inventory confirming that
|
|
547
|
-
// pane right now. Without that confirmation the launch has no verified
|
|
548
|
-
// location to give the child, and no later read can supply one.
|
|
549
|
-
if (freshness !== "fresh")
|
|
550
|
-
return unresolvedLaunch(
|
|
551
|
-
requestId,
|
|
552
|
-
state === "unresolved"
|
|
553
|
-
? `the daemon's inventory does not report the pane ${pane} its own record names for this child`
|
|
554
|
-
: `the daemon cannot confirm the pane ${pane} it recorded for this child`,
|
|
555
|
-
);
|
|
556
|
-
// An observed exit is the child's process, and a pane's shell can outlive it.
|
|
557
|
-
if (edge.exit_observation?.state === "exited")
|
|
558
|
-
return unresolvedLaunch(
|
|
559
|
-
requestId,
|
|
560
|
-
"the daemon observed the launched child's process exit",
|
|
561
|
-
);
|
|
391
|
+
|
|
562
392
|
const agentId = edge.bound?.registration_id;
|
|
563
393
|
if (agentId !== undefined && isUuid(agentId)) {
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const incarnation = edge.bound?.incarnation;
|
|
570
|
-
if (
|
|
571
|
-
source === undefined ||
|
|
572
|
-
source === "" ||
|
|
573
|
-
incarnation === undefined ||
|
|
574
|
-
incarnation === ""
|
|
575
|
-
)
|
|
394
|
+
// The edge names the agent as well as its process. Without it the child
|
|
395
|
+
// has no durable key to be returned by, and a registration is never
|
|
396
|
+
// offered in its place.
|
|
397
|
+
const subjectId = edge.bound?.subject_id;
|
|
398
|
+
if (subjectId === undefined || !isUuid(subjectId))
|
|
576
399
|
return unresolvedLaunch(
|
|
577
400
|
requestId,
|
|
578
|
-
`the daemon bound the child ${agentId} without recording
|
|
401
|
+
`the daemon bound the child ${agentId} without recording its subject`,
|
|
579
402
|
);
|
|
580
403
|
return {
|
|
581
404
|
kind: "bound",
|
|
582
|
-
|
|
405
|
+
subject_id: subjectId,
|
|
583
406
|
pane_id: pane,
|
|
584
|
-
source,
|
|
585
|
-
incarnation,
|
|
586
407
|
};
|
|
587
408
|
}
|
|
588
409
|
last = "no child has registered with this launch's token yet";
|
|
589
410
|
} else {
|
|
590
|
-
|
|
411
|
+
return unresolvedLaunch(requestId, answer.message);
|
|
591
412
|
}
|
|
592
|
-
|
|
593
|
-
return unresolvedLaunch(requestId, last);
|
|
594
|
-
await delay(options.pollMs, undefined, {
|
|
595
|
-
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
596
|
-
});
|
|
413
|
+
await delay(options.pollMs);
|
|
597
414
|
}
|
|
598
415
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { RadarExecutionAdapter } from "./radar-execution.ts";
|
|
2
|
+
import type { RadarChildren } from "./radar-children.ts";
|
|
2
3
|
import type { RadarHandoffPort } from "./radar-communication.ts";
|
|
3
4
|
import type { RadarPublication } from "./radar-publication.ts";
|
|
4
5
|
|
|
5
6
|
/** The event channel through which Pi extensions obtain the baseline runtime. */
|
|
6
7
|
export const RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
|
|
7
|
-
export const RADAR_RUNTIME_VERSION =
|
|
8
|
+
export const RADAR_RUNTIME_VERSION = 4 as const;
|
|
8
9
|
|
|
9
10
|
/** The sole baseline publisher's session-scoped runtime, shared with consumers. */
|
|
10
11
|
export type RadarRuntimeServices = {
|
|
@@ -12,6 +13,8 @@ export type RadarRuntimeServices = {
|
|
|
12
13
|
publication: RadarPublication;
|
|
13
14
|
/** Durable handoffs bound to this process's registration and managed parent. */
|
|
14
15
|
communication: RadarHandoffPort;
|
|
16
|
+
/** This process's own children, addressed by subject. */
|
|
17
|
+
children: RadarChildren;
|
|
15
18
|
execution: RadarExecutionAdapter;
|
|
16
19
|
};
|
|
17
20
|
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-radar",
|
|
3
|
-
"version": "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",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./index.ts",
|
|
24
24
|
"import": "./dist/index.js"
|
|
25
25
|
},
|
|
26
|
+
"./children": {
|
|
27
|
+
"types": "./extension/radar-children.ts",
|
|
28
|
+
"import": "./dist/extension/radar-children.js"
|
|
29
|
+
},
|
|
26
30
|
"./client": {
|
|
27
31
|
"types": "./extension/radar-client.ts",
|
|
28
32
|
"import": "./dist/extension/radar-client.js"
|
|
@@ -72,6 +76,7 @@
|
|
|
72
76
|
"README.md",
|
|
73
77
|
"schema/public/v1/**",
|
|
74
78
|
"extension/generated-contracts.ts",
|
|
79
|
+
"extension/radar-children.ts",
|
|
75
80
|
"extension/radar-client.ts",
|
|
76
81
|
"extension/radar-communication.ts",
|
|
77
82
|
"extension/radar-execution.ts",
|
|
@@ -83,10 +88,10 @@
|
|
|
83
88
|
"extension/baseline.ts"
|
|
84
89
|
],
|
|
85
90
|
"scripts": {
|
|
86
|
-
"build": "esbuild index.ts extension/generated-contracts.ts extension/radar-client.ts extension/radar-communication.ts extension/radar-execution.ts extension/radar-inspection.ts extension/radar-launch.ts extension/radar-location.ts extension/radar-publication.ts extension/radar-runtime.ts extension/baseline.ts --outbase=. --outdir=dist --bundle --platform=node --format=esm --target=node22",
|
|
91
|
+
"build": "esbuild index.ts extension/generated-contracts.ts extension/radar-children.ts extension/radar-client.ts extension/radar-communication.ts extension/radar-execution.ts extension/radar-inspection.ts extension/radar-launch.ts extension/radar-location.ts extension/radar-publication.ts extension/radar-runtime.ts extension/baseline.ts --outbase=. --outdir=dist --bundle --platform=node --format=esm --target=node22",
|
|
87
92
|
"generate:contracts": "node tools/generate-contracts.mjs",
|
|
88
93
|
"check:contracts": "node tools/generate-contracts.mjs --check",
|
|
89
|
-
"typecheck:contracts": "tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext --allowImportingTsExtensions --types node index.ts extension/baseline.ts extension/generated-contracts.ts extension/generated-contracts.typecheck.ts extension/radar-client.ts extension/radar-communication.ts extension/radar-execution.ts extension/radar-launch.test.ts extension/radar-publication.ts extension/radar-runtime.ts extension/radar-runtime.test.ts",
|
|
94
|
+
"typecheck:contracts": "tsc --noEmit --strict --skipLibCheck --target ES2022 --module NodeNext --moduleResolution NodeNext --allowImportingTsExtensions --types node index.ts extension/baseline.ts extension/generated-contracts.ts extension/generated-contracts.typecheck.ts extension/radar-children.ts extension/radar-client.ts extension/radar-communication.ts extension/radar-execution.ts extension/radar-launch.test.ts extension/radar-publication.ts extension/radar-runtime.ts extension/radar-runtime.test.ts",
|
|
90
95
|
"prepack": "npm run build",
|
|
91
96
|
"test": "npm run check:contracts && npm run typecheck:contracts && node --experimental-strip-types --test extension/*.test.ts"
|
|
92
97
|
},
|
|
@@ -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": [
|
|
@@ -2448,7 +2436,7 @@
|
|
|
2448
2436
|
},
|
|
2449
2437
|
{
|
|
2450
2438
|
"const": "captured",
|
|
2451
|
-
"description": "The
|
|
2439
|
+
"description": "The launch's process is captured and has been given its program.",
|
|
2452
2440
|
"type": "string"
|
|
2453
2441
|
},
|
|
2454
2442
|
{
|
|
@@ -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
|
},
|
|
@@ -2567,7 +2553,7 @@
|
|
|
2567
2553
|
},
|
|
2568
2554
|
{
|
|
2569
2555
|
"const": "captured",
|
|
2570
|
-
"description": "The
|
|
2556
|
+
"description": "The launch's process is captured and has been given its program.",
|
|
2571
2557
|
"type": "string"
|
|
2572
2558
|
},
|
|
2573
2559
|
{
|