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.
- package/README.md +3 -1
- package/dist/extension/baseline.js +224 -117
- package/dist/extension/radar-children.js +105 -1
- package/dist/extension/radar-client.js +12 -3
- package/dist/extension/radar-launch.js +25 -94
- package/dist/extension/radar-runtime.js +1 -1
- package/dist/index.js +190 -216
- package/extension/generated-contracts.ts +10 -2
- package/extension/radar-children.ts +50 -4
- package/extension/radar-client.ts +17 -3
- package/extension/radar-launch.ts +54 -216
- package/extension/radar-runtime.ts +1 -1
- package/package.json +1 -1
- package/schema/public/v1/observation-get-response.deserialize.schema.json +0 -12
- package/schema/public/v1/observation-get-response.serialize.schema.json +2 -16
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +0 -12
- package/schema/public/v1/observation-snapshot.serialize.schema.json +2 -16
|
@@ -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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
* `
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
|
115
|
-
| { kind: "ready"; value: RadarWorkerLaunched
|
|
116
|
-
| { kind: "
|
|
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.
|
|
162
|
-
*
|
|
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
|
|
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
|
-
|
|
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: "
|
|
309
|
-
return { kind: "
|
|
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: "
|
|
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
|
|
357
|
-
result: Extract<RadarResult<
|
|
276
|
+
export function launchFailure(
|
|
277
|
+
result: Extract<RadarResult<unknown>, { ok: false }>,
|
|
358
278
|
requestId: string,
|
|
359
|
-
): Extract<RadarLaunchOutcome, { kind: "
|
|
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
|
|
367
|
-
*
|
|
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
|
|
371
|
-
options: RadarLaunchOptions
|
|
372
|
-
): Promise<RadarLaunchOutcome
|
|
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 =
|
|
412
|
-
if (refusal.kind === "
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
launch.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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: "
|
|
353
|
+
| Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }>;
|
|
476
354
|
|
|
477
355
|
/**
|
|
478
|
-
* 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.
|
|
479
360
|
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
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
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
+
return unresolvedLaunch(requestId, answer.message);
|
|
570
412
|
}
|
|
571
|
-
|
|
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 =
|
|
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
|
@@ -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": [
|