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.
- package/README.md +8 -10
- package/dist/extension/baseline.js +343 -361
- package/dist/extension/radar-children.js +108 -218
- package/dist/extension/radar-client.js +103 -15
- package/dist/extension/radar-execution.js +26 -17
- package/dist/extension/radar-inspection.js +0 -45
- package/dist/extension/radar-launch.js +24 -185
- package/dist/extension/radar-publication.js +26 -17
- package/dist/extension/radar-runtime.js +1 -1
- package/dist/index.js +317 -488
- package/extension/generated-contracts.ts +362 -13
- package/extension/radar-children.ts +60 -155
- package/extension/radar-client.ts +130 -19
- package/extension/radar-inspection.ts +2 -77
- package/extension/radar-launch.ts +52 -391
- package/extension/radar-publication.ts +36 -53
- package/extension/radar-runtime.ts +1 -1
- package/package.json +1 -1
- package/schema/public/v1/observation-get-response.deserialize.schema.json +5 -25
- package/schema/public/v1/observation-get-response.serialize.schema.json +7 -29
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +5 -25
- package/schema/public/v1/observation-snapshot.serialize.schema.json +7 -29
- package/schema/public/v1/spawn-list-page.deserialize.schema.json +40 -17
- package/schema/public/v1/spawn-list-page.serialize.schema.json +40 -17
- package/schema/public/v1/subject-assignment-acquire.deserialize.schema.json +78 -0
- package/schema/public/v1/subject-assignment-acquire.serialize.schema.json +79 -0
- package/schema/public/v1/subject-assignment-publish.deserialize.schema.json +310 -0
- package/schema/public/v1/subject-assignment-publish.serialize.schema.json +310 -0
- package/schema/public/v1/subject-assignment-retire.deserialize.schema.json +22 -0
- package/schema/public/v1/subject-assignment-retire.serialize.schema.json +22 -0
- package/schema/public/v1/subject-child-get-params.deserialize.schema.json +18 -0
- package/schema/public/v1/subject-child-get-params.serialize.schema.json +18 -0
- package/schema/public/v1/subject-children-params.deserialize.schema.json +51 -0
- package/schema/public/v1/subject-children-params.serialize.schema.json +54 -0
- package/schema/public/v1/subject-get-params.deserialize.schema.json +14 -0
- package/schema/public/v1/subject-get-params.serialize.schema.json +14 -0
- package/schema/public/v1/subject-get-result.deserialize.schema.json +566 -0
- package/schema/public/v1/subject-get-result.serialize.schema.json +573 -0
- package/schema/public/v1/subject-list-params.deserialize.schema.json +38 -0
- package/schema/public/v1/subject-list-params.serialize.schema.json +43 -0
- package/schema/public/v1/subject-list-result.deserialize.schema.json +575 -0
- 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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
309
|
-
return { kind: "
|
|
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: "
|
|
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
|
|
357
|
-
result: Extract<RadarResult<
|
|
99
|
+
export function launchFailure(
|
|
100
|
+
result: Extract<RadarResult<unknown>, { ok: false }>,
|
|
358
101
|
requestId: string,
|
|
359
|
-
): Extract<RadarLaunchOutcome, { kind: "
|
|
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
|
|
367
|
-
*
|
|
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
|
|
371
|
-
options: RadarLaunchOptions
|
|
372
|
-
): Promise<RadarLaunchOutcome
|
|
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 =
|
|
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));
|
|
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
|
-
|
|
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
|
-
}
|
|
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: "
|
|
176
|
+
| Extract<RadarLaunchOutcome, { kind: "failed" | "unresolved" }>;
|
|
476
177
|
|
|
477
178
|
/**
|
|
478
|
-
* Poll the edge this request id authored until
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
+
return unresolvedLaunch(requestId, answer.message);
|
|
570
235
|
}
|
|
571
|
-
|
|
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
|
}
|