pi-radar 0.6.0 → 0.9.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 +15 -1
- package/dist/extension/baseline.js +395 -125
- package/dist/extension/radar-children.js +240 -0
- package/dist/extension/radar-client.js +42 -8
- package/dist/extension/radar-execution.js +11 -10
- package/dist/extension/radar-launch.js +23 -24
- package/dist/extension/radar-publication.js +10 -9
- package/dist/extension/radar-runtime.js +1 -1
- package/dist/index.js +1772 -1615
- package/extension/baseline.ts +2 -0
- package/extension/generated-contracts.ts +12 -3
- package/extension/radar-children.ts +176 -0
- package/extension/radar-client.ts +60 -6
- package/extension/radar-execution.ts +5 -5
- package/extension/radar-launch.ts +42 -63
- package/extension/radar-publication.ts +12 -13
- 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 +9 -1
- package/schema/public/v1/observation-get-response.serialize.schema.json +9 -1
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +9 -1
- package/schema/public/v1/observation-snapshot.serialize.schema.json +9 -1
- package/schema/public/v1/presence-answer.deserialize.schema.json +1 -0
- package/schema/public/v1/presence-answer.serialize.schema.json +1 -0
- package/schema/public/v1/presence-report.deserialize.schema.json +0 -7
- package/schema/public/v1/presence-report.serialize.schema.json +0 -8
- package/schema/public/v1/spawn-list-page.deserialize.schema.json +7 -0
- package/schema/public/v1/spawn-list-page.serialize.schema.json +7 -0
package/extension/baseline.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { createRadarClient } from "./radar-client.ts";
|
|
4
|
+
import { createRadarChildren } from "./radar-children.ts";
|
|
4
5
|
import { createRadarHandoffPort } from "./radar-communication.ts";
|
|
5
6
|
import {
|
|
6
7
|
createRadarExecutionAdapter,
|
|
@@ -33,6 +34,7 @@ export default function registerPiRadar(pi: PiRadarHost): void {
|
|
|
33
34
|
const runtime = lazyRadarRuntime(async () => ({
|
|
34
35
|
publication: execution.publication,
|
|
35
36
|
communication: createRadarHandoffPort(client, execution.publication),
|
|
37
|
+
children: createRadarChildren(client, execution.publication),
|
|
36
38
|
execution,
|
|
37
39
|
}));
|
|
38
40
|
provideRadarRuntime(pi.events, runtime);
|
|
@@ -11,7 +11,6 @@ export interface PresenceReportDeserialize {
|
|
|
11
11
|
run?: string | null;
|
|
12
12
|
session_id: string;
|
|
13
13
|
source: string;
|
|
14
|
-
spawn_token?: string | null;
|
|
15
14
|
subject_id?: string | null;
|
|
16
15
|
}
|
|
17
16
|
/**
|
|
@@ -54,7 +53,7 @@ export interface PresenceReportProcessIdentity {
|
|
|
54
53
|
start_ticks: number;
|
|
55
54
|
[k: string]: unknown;
|
|
56
55
|
}
|
|
57
|
-
export type PresenceAnswerStanding = "pending" | "granted" | "duplicate" | "released";
|
|
56
|
+
export type PresenceAnswerStanding = "reserved" | "pending" | "granted" | "duplicate" | "released";
|
|
58
57
|
|
|
59
58
|
/**
|
|
60
59
|
* The small public receipt; session/location context stays inside Radar.
|
|
@@ -284,7 +283,7 @@ export type Freshness = "fresh" | "stale";
|
|
|
284
283
|
* refused rather than silently dropped.
|
|
285
284
|
*/
|
|
286
285
|
export type Report = Snapshot | AssignmentSnapshot | FactsSnapshot;
|
|
287
|
-
export type Standing = "pending" | "granted" | "duplicate" | "released";
|
|
286
|
+
export type Standing = "reserved" | "pending" | "granted" | "duplicate" | "released";
|
|
288
287
|
/**
|
|
289
288
|
* How a registration relates to the live panes.
|
|
290
289
|
*/
|
|
@@ -675,6 +674,11 @@ export interface RegistryAgent {
|
|
|
675
674
|
facts: PublicChannelFacts | null;
|
|
676
675
|
process: ProcessEvidence;
|
|
677
676
|
registration: PublicRegistration;
|
|
677
|
+
/**
|
|
678
|
+
* Whether this process runs inside a Radar-established execution scope. A
|
|
679
|
+
* process started outside the managed launch path has none and is observed.
|
|
680
|
+
*/
|
|
681
|
+
scoped?: boolean | null;
|
|
678
682
|
}
|
|
679
683
|
/**
|
|
680
684
|
* A channel read for a reader that is not its writer: the same identity,
|
|
@@ -1472,6 +1476,11 @@ export interface SpawnListPageSerializeBoundChild {
|
|
|
1472
1476
|
*/
|
|
1473
1477
|
registration_id?: string | null;
|
|
1474
1478
|
source: string;
|
|
1479
|
+
/**
|
|
1480
|
+
* The durable agent the bound child attaches as. Absent for an edge without
|
|
1481
|
+
* a tracked launch and in records written before it was recorded.
|
|
1482
|
+
*/
|
|
1483
|
+
subject_id?: string | null;
|
|
1475
1484
|
}
|
|
1476
1485
|
/**
|
|
1477
1486
|
* Where an agent's incarnation lives on one backend, in that backend's own
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { RadarClient, RadarChannelSnapshot, RadarResult } from "./radar-client.ts";
|
|
2
|
+
import type { SpawnListPageSerializePublicSpawnTopology as ListedEdge } from "./generated-contracts.ts";
|
|
3
|
+
import { inspectRadarChild, type RadarInspection } from "./radar-inspection.ts";
|
|
4
|
+
import { observeRadarChild, type RadarChildObservation } from "./radar-launch.ts";
|
|
5
|
+
import type { RadarPublication } from "./radar-publication.ts";
|
|
6
|
+
|
|
7
|
+
const PAGE = 100;
|
|
8
|
+
const MAX_PAGES = 20;
|
|
9
|
+
|
|
10
|
+
/** One child of the caller, named by the agent that survives its restarts. */
|
|
11
|
+
export type RadarChild = {
|
|
12
|
+
subject_id: string;
|
|
13
|
+
/** The launch that last bound this child. */
|
|
14
|
+
request_id: string;
|
|
15
|
+
pane_id?: string;
|
|
16
|
+
state: "unbound" | "bound" | "unresolved";
|
|
17
|
+
exited: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** The assignment a child is held to: published again when its process is replaced. */
|
|
21
|
+
export type RadarChildAssignment = {
|
|
22
|
+
update(snapshot: RadarChannelSnapshot): void;
|
|
23
|
+
retire(): void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type RadarChildren = {
|
|
27
|
+
list(): Promise<RadarResult<RadarChild[]>>;
|
|
28
|
+
assignment(subject: string, owner: string): RadarChildAssignment;
|
|
29
|
+
retire(subject: string): void;
|
|
30
|
+
observe(
|
|
31
|
+
subject: string,
|
|
32
|
+
session_id: string | undefined,
|
|
33
|
+
): Promise<RadarChildObservation>;
|
|
34
|
+
inspect(subject: string, session_id?: string): Promise<RadarInspection>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type Resolved = { child: RadarChild; registration_id: string };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The caller's children, addressed by subject. A subject gains an edge per launch,
|
|
41
|
+
* so the newest bound edge is the current process; its registration is read per
|
|
42
|
+
* call and never remembered, because a remembered registration is the stale key
|
|
43
|
+
* this surface exists to hide. An edge bound without a subject is not addressable.
|
|
44
|
+
*/
|
|
45
|
+
export function createRadarChildren(
|
|
46
|
+
client: Pick<RadarClient, "spawnList" | "spawnGet" | "agentGet" | "output" | "processInfo">,
|
|
47
|
+
publication: Pick<RadarPublication, "assignment" | "retireAssignment">,
|
|
48
|
+
): RadarChildren {
|
|
49
|
+
const held = new Map<
|
|
50
|
+
string,
|
|
51
|
+
{ writer: RadarChildAssignment; published?: string; publish?: (target: Resolved) => void }
|
|
52
|
+
>();
|
|
53
|
+
|
|
54
|
+
async function resolveAll(): Promise<RadarResult<Map<string, Resolved & { at: number }>>> {
|
|
55
|
+
const newest = new Map<string, Resolved & { at: number }>();
|
|
56
|
+
let after: string | undefined;
|
|
57
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
58
|
+
const read = await client.spawnList({ ...(after !== undefined ? { after } : {}), limit: PAGE });
|
|
59
|
+
if (read.ok === false) return read;
|
|
60
|
+
for (const edge of read.value.spawns as ListedEdge[]) {
|
|
61
|
+
const bound = edge.bound;
|
|
62
|
+
if (!bound?.subject_id || !bound.registration_id || edge.launched !== "completed") continue;
|
|
63
|
+
const at = Date.parse(bound.bound_at);
|
|
64
|
+
if (Number.isNaN(at)) continue;
|
|
65
|
+
const current = newest.get(bound.subject_id);
|
|
66
|
+
if (current !== undefined && current.at >= at) continue;
|
|
67
|
+
newest.set(bound.subject_id, {
|
|
68
|
+
at,
|
|
69
|
+
registration_id: bound.registration_id,
|
|
70
|
+
child: {
|
|
71
|
+
subject_id: bound.subject_id,
|
|
72
|
+
request_id: edge.request_id,
|
|
73
|
+
...(edge.location?.pane ? { pane_id: edge.location.pane } : {}),
|
|
74
|
+
state: edge.state,
|
|
75
|
+
exited: edge.exit_observation != null && edge.exit_observation.state !== "unknown",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (!read.value.next) return { ok: true, value: newest };
|
|
80
|
+
after = read.value.next;
|
|
81
|
+
}
|
|
82
|
+
return { ok: false, code: "protocol", message: "the caller's launches exceed the readable pages" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function resolve(subject: string): Promise<Resolved | undefined> {
|
|
86
|
+
const all = await resolveAll();
|
|
87
|
+
return all.ok ? all.value.get(subject) : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function resolveAndFollow(subject: string): Promise<Resolved | undefined> {
|
|
91
|
+
const all = await resolveAll();
|
|
92
|
+
if (!all.ok) return undefined;
|
|
93
|
+
follow(all.value);
|
|
94
|
+
return all.value.get(subject);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A child replaced under its subject has a new process with no assignment. Each
|
|
99
|
+
* read the caller already makes finds the new registration and moves the held
|
|
100
|
+
* assignment onto it, so no caller has to notice a replacement.
|
|
101
|
+
*/
|
|
102
|
+
function follow(all: Map<string, Resolved>): void {
|
|
103
|
+
for (const [subject, entry] of held) {
|
|
104
|
+
const target = all.get(subject);
|
|
105
|
+
if (target !== undefined && entry.published !== target.registration_id)
|
|
106
|
+
entry.publish?.(target);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
async list() {
|
|
112
|
+
const all = await resolveAll();
|
|
113
|
+
if (!all.ok) return all;
|
|
114
|
+
follow(all.value);
|
|
115
|
+
return { ok: true, value: [...all.value.values()].map((entry) => entry.child) };
|
|
116
|
+
},
|
|
117
|
+
assignment(subject, owner) {
|
|
118
|
+
const existing = held.get(subject);
|
|
119
|
+
if (existing !== undefined) return existing.writer;
|
|
120
|
+
let latest: RadarChannelSnapshot | undefined;
|
|
121
|
+
const entry: {
|
|
122
|
+
writer: RadarChildAssignment;
|
|
123
|
+
published?: string;
|
|
124
|
+
publish?: (target: Resolved) => void;
|
|
125
|
+
} = { writer: undefined as unknown as RadarChildAssignment };
|
|
126
|
+
const send = (target: Resolved): void => {
|
|
127
|
+
if (latest === undefined) return;
|
|
128
|
+
if (entry.published !== undefined && entry.published !== target.registration_id)
|
|
129
|
+
publication.retireAssignment(entry.published);
|
|
130
|
+
entry.published = target.registration_id;
|
|
131
|
+
publication.assignment(target.registration_id, owner).update(latest);
|
|
132
|
+
};
|
|
133
|
+
entry.publish = send;
|
|
134
|
+
entry.writer = {
|
|
135
|
+
update(snapshot) {
|
|
136
|
+
latest = snapshot;
|
|
137
|
+
void resolve(subject).then((target) => {
|
|
138
|
+
if (target !== undefined) send(target);
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
retire() {
|
|
142
|
+
latest = undefined;
|
|
143
|
+
if (entry.published !== undefined) publication.retireAssignment(entry.published);
|
|
144
|
+
entry.published = undefined;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
held.set(subject, entry);
|
|
148
|
+
return entry.writer;
|
|
149
|
+
},
|
|
150
|
+
retire(subject) {
|
|
151
|
+
held.get(subject)?.writer.retire();
|
|
152
|
+
held.delete(subject);
|
|
153
|
+
},
|
|
154
|
+
async observe(subject, session_id) {
|
|
155
|
+
const target = await resolveAndFollow(subject);
|
|
156
|
+
if (target === undefined)
|
|
157
|
+
return { kind: "unknown", diagnostic: "the daemon records no bound launch for this child" };
|
|
158
|
+
return observeRadarChild(client as RadarClient, {
|
|
159
|
+
request_id: target.child.request_id,
|
|
160
|
+
pane_id: target.child.pane_id,
|
|
161
|
+
registration_id: target.registration_id,
|
|
162
|
+
session_id,
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
async inspect(subject, session_id) {
|
|
166
|
+
const target = await resolveAndFollow(subject);
|
|
167
|
+
if (target?.child.pane_id === undefined)
|
|
168
|
+
throw new Error("Inspection target is not a bound child with a pane");
|
|
169
|
+
return inspectRadarChild(client as RadarClient, {
|
|
170
|
+
agentId: target.registration_id,
|
|
171
|
+
paneId: target.child.pane_id,
|
|
172
|
+
...(session_id !== undefined ? { session: session_id } : {}),
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -34,15 +34,18 @@ import type {
|
|
|
34
34
|
export const RADAR_PROTOCOL_VERSION = 2;
|
|
35
35
|
/**
|
|
36
36
|
* Revision 5 names attempts by claim_id and requires exact-claim controls.
|
|
37
|
+
* Revision 7 starts every managed launch through `radar run --launch` and lets a
|
|
38
|
+
* parent continue or restart its own child by subject.
|
|
37
39
|
* Earlier daemons expose different identity fields; require a coordinated update.
|
|
38
40
|
*/
|
|
39
|
-
export const RADAR_MINIMUM_REVISION =
|
|
41
|
+
export const RADAR_MINIMUM_REVISION = 7;
|
|
40
42
|
/** Public daemon contracts this client may preflight. */
|
|
41
43
|
export const RADAR_REGISTRY_CONTRACT = "registry.v1";
|
|
42
44
|
export const RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
|
|
43
45
|
export const RADAR_SPAWN_CONTRACT = "spawn.v1";
|
|
44
46
|
export const RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
|
|
45
47
|
export const RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
|
|
48
|
+
export const RADAR_LIFECYCLE_RESTART_CONTRACT = "lifecycle.restart.v1";
|
|
46
49
|
export const RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
|
|
47
50
|
export const RADAR_EVENTS_CONTRACT = "events.v1";
|
|
48
51
|
export const RADAR_COMMUNICATION_CONTRACT = "communication.handoff.v1";
|
|
@@ -304,7 +307,7 @@ export type RadarSpawnEdge = {
|
|
|
304
307
|
created: RadarEffectOutcome;
|
|
305
308
|
launched: RadarEffectOutcome;
|
|
306
309
|
location?: RadarLocation;
|
|
307
|
-
bound?: { source: string; incarnation: string; registration_id?: string };
|
|
310
|
+
bound?: { source: string; incarnation: string; registration_id?: string; subject_id?: string };
|
|
308
311
|
exit_observation?: RadarSpawnExitObservation;
|
|
309
312
|
};
|
|
310
313
|
|
|
@@ -517,7 +520,7 @@ function decodePresenceAnswer(value: unknown): RadarResult<RadarPresenceAnswer>
|
|
|
517
520
|
if (
|
|
518
521
|
!isUuid(registrationId) ||
|
|
519
522
|
!isUuid(claimId) ||
|
|
520
|
-
!["pending", "granted", "duplicate", "released"].includes(String(standing)) ||
|
|
523
|
+
!["reserved", "pending", "granted", "duplicate", "released"].includes(String(standing)) ||
|
|
521
524
|
(subjectId !== undefined && !isUuid(subjectId)) ||
|
|
522
525
|
(standing === "granted" && !isUuid(subjectId))
|
|
523
526
|
)
|
|
@@ -745,7 +748,7 @@ function unresolvedOperation(requestId: string, detail: string): RadarResult<nev
|
|
|
745
748
|
};
|
|
746
749
|
}
|
|
747
750
|
|
|
748
|
-
export const
|
|
751
|
+
export const RADAR_REGISTRATION_ENV = "PI_RADAR_REGISTRATION_ID";
|
|
749
752
|
const RADAR_MANAGED_IDENTITY_ENV = new Set([
|
|
750
753
|
"PI_RADAR_MANAGED_SUBJECT_ID",
|
|
751
754
|
"PI_RADAR_MANAGED_LAUNCH_ID",
|
|
@@ -825,6 +828,7 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
|
825
828
|
const source = stringAt(bound, "source");
|
|
826
829
|
const incarnation = stringAt(bound, "incarnation");
|
|
827
830
|
const agentId = stringAt(bound, "registration_id");
|
|
831
|
+
const subjectId = stringAt(bound, "subject_id");
|
|
828
832
|
const exit = exitAt(recordAt(value, "exit_observation"));
|
|
829
833
|
return {
|
|
830
834
|
ok: true,
|
|
@@ -840,6 +844,7 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
|
|
|
840
844
|
source,
|
|
841
845
|
incarnation,
|
|
842
846
|
...(isUuid(agentId) ? { registration_id: agentId } : {}),
|
|
847
|
+
...(isUuid(subjectId) ? { subject_id: subjectId } : {}),
|
|
843
848
|
},
|
|
844
849
|
}
|
|
845
850
|
: {}),
|
|
@@ -959,9 +964,12 @@ function spawnRequestShape(request: {
|
|
|
959
964
|
cwd?: string;
|
|
960
965
|
env?: Record<string, string>;
|
|
961
966
|
managed?: RadarManagedLaunchMetadata;
|
|
967
|
+
continue_subject_id?: string;
|
|
962
968
|
}): string | undefined {
|
|
963
969
|
if (!isUuid(request.request_id))
|
|
964
970
|
return "spawn requires a canonical request UUID";
|
|
971
|
+
if (request.continue_subject_id !== undefined && !isUuid(request.continue_subject_id))
|
|
972
|
+
return "continue_subject_id must be a canonical UUID";
|
|
965
973
|
if (!isUuid(request.parent))
|
|
966
974
|
return "spawn requires the parent's canonical agent UUID";
|
|
967
975
|
if (!isUuid(request.parent_session_id))
|
|
@@ -975,8 +983,8 @@ function spawnRequestShape(request: {
|
|
|
975
983
|
const env = Object.entries(request.env ?? {});
|
|
976
984
|
if (env.length > 256) return "spawn env names more than 256 entries";
|
|
977
985
|
for (const [name, value] of env) {
|
|
978
|
-
if (name ===
|
|
979
|
-
return `spawn env cannot set the reserved ${
|
|
986
|
+
if (name === RADAR_REGISTRATION_ENV)
|
|
987
|
+
return `spawn env cannot set the reserved ${RADAR_REGISTRATION_ENV}`;
|
|
980
988
|
if (RADAR_MANAGED_IDENTITY_ENV.has(name))
|
|
981
989
|
return `spawn env cannot set Radar-issued identity variable ${name}`;
|
|
982
990
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || /\p{Cc}/u.test(value))
|
|
@@ -1100,7 +1108,23 @@ export type RadarClient = {
|
|
|
1100
1108
|
cwd?: string;
|
|
1101
1109
|
env?: Record<string, string>;
|
|
1102
1110
|
managed?: RadarManagedLaunchMetadata;
|
|
1111
|
+
/** Start this released child agent again, in the session the daemon recorded for it. */
|
|
1112
|
+
continue_subject_id?: string;
|
|
1103
1113
|
}): Promise<RadarResult<RadarSpawnEdge>>;
|
|
1114
|
+
/**
|
|
1115
|
+
* Restart this caller's own live child under a new program. The daemon finds the
|
|
1116
|
+
* child's current process from the subject and replaces it in its own pane and
|
|
1117
|
+
* session; a program with a bare argument that may be a message is refused.
|
|
1118
|
+
*/
|
|
1119
|
+
restart(request: {
|
|
1120
|
+
request_id: string;
|
|
1121
|
+
parent: string;
|
|
1122
|
+
subject_id: string;
|
|
1123
|
+
executable: string;
|
|
1124
|
+
argv: string[];
|
|
1125
|
+
cwd?: string;
|
|
1126
|
+
env?: Record<string, string>;
|
|
1127
|
+
}): Promise<RadarResult<{ request_id: string }>>;
|
|
1104
1128
|
/** List launches owned by this client's registered process. */
|
|
1105
1129
|
spawnList(request: Pick<SpawnListParamsDeserialize, "after" | "limit">): Promise<RadarResult<SpawnListPageSerialize>>;
|
|
1106
1130
|
spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
|
|
@@ -1671,6 +1695,9 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1671
1695
|
...(request.managed !== undefined
|
|
1672
1696
|
? { managed: request.managed }
|
|
1673
1697
|
: {}),
|
|
1698
|
+
...(request.continue_subject_id !== undefined
|
|
1699
|
+
? { continue_subject_id: request.continue_subject_id }
|
|
1700
|
+
: {}),
|
|
1674
1701
|
},
|
|
1675
1702
|
requester,
|
|
1676
1703
|
},
|
|
@@ -1706,6 +1733,33 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
|
|
|
1706
1733
|
},
|
|
1707
1734
|
);
|
|
1708
1735
|
},
|
|
1736
|
+
async restart(request) {
|
|
1737
|
+
if (!isUuid(request.request_id) || !isUuid(request.parent) || !isUuid(request.subject_id))
|
|
1738
|
+
return {
|
|
1739
|
+
ok: false,
|
|
1740
|
+
code: "bad_params",
|
|
1741
|
+
message: "restart requires canonical request, parent and subject UUIDs",
|
|
1742
|
+
};
|
|
1743
|
+
return withContract(RADAR_LIFECYCLE_RESTART_CONTRACT, async () => {
|
|
1744
|
+
const answer = await call(
|
|
1745
|
+
"lifecycle.restart",
|
|
1746
|
+
{
|
|
1747
|
+
request: {
|
|
1748
|
+
subject_id: request.subject_id,
|
|
1749
|
+
parent: request.parent,
|
|
1750
|
+
executable: request.executable,
|
|
1751
|
+
argv: request.argv,
|
|
1752
|
+
...(request.cwd !== undefined ? { cwd: request.cwd } : {}),
|
|
1753
|
+
...(request.env !== undefined ? { env: request.env } : {}),
|
|
1754
|
+
},
|
|
1755
|
+
requester,
|
|
1756
|
+
},
|
|
1757
|
+
request.request_id,
|
|
1758
|
+
);
|
|
1759
|
+
if (answer.ok === false) return answer;
|
|
1760
|
+
return focusOutcome(recordAt(answer.value, "request"), request.request_id);
|
|
1761
|
+
});
|
|
1762
|
+
},
|
|
1709
1763
|
async spawnList(request) {
|
|
1710
1764
|
const registrationId = processRegistrationId;
|
|
1711
1765
|
if (registrationId === undefined)
|
|
@@ -51,10 +51,10 @@ export type RadarExecutionOptions = {
|
|
|
51
51
|
*/
|
|
52
52
|
location?: () => Promise<RadarLocationResolution>;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* Registration allocated by Radar before this process launched. Normally
|
|
55
|
+
* captured from the launch environment by the process-local publication.
|
|
56
56
|
*/
|
|
57
|
-
|
|
57
|
+
launchRegistrationId?: string;
|
|
58
58
|
/** Only a real fixed managed assignment is part of immutable registration. */
|
|
59
59
|
managedBinding?: { key: string; run: string; owner: string; label: string };
|
|
60
60
|
/**
|
|
@@ -207,8 +207,8 @@ export function createRadarExecutionAdapter(
|
|
|
207
207
|
? { location: options.location }
|
|
208
208
|
: {}),
|
|
209
209
|
...(managed !== undefined ? { binding: managed } : {}),
|
|
210
|
-
...(options.
|
|
211
|
-
? {
|
|
210
|
+
...(options.launchRegistrationId !== undefined
|
|
211
|
+
? { launchRegistrationId: options.launchRegistrationId }
|
|
212
212
|
: {}),
|
|
213
213
|
});
|
|
214
214
|
} catch {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type RadarSpawnEdge,
|
|
9
9
|
} from "./radar-client.ts";
|
|
10
10
|
import { radarPaneHandle } from "./radar-location.ts";
|
|
11
|
+
import type { RadarPublication } from "./radar-publication.ts";
|
|
11
12
|
|
|
12
13
|
/** The startup acknowledgement a launch waits for, named by its identity. */
|
|
13
14
|
export type StartupExpectation = {
|
|
@@ -64,21 +65,22 @@ export type LaunchMailbox<State extends LaunchState> = {
|
|
|
64
65
|
* different leaves the launch unresolved rather than placed.
|
|
65
66
|
*/
|
|
66
67
|
|
|
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
|
-
};
|
|
74
|
-
|
|
75
68
|
/** Everything one fresh managed worker is launched with. */
|
|
76
69
|
export type RadarWorkerLaunch = {
|
|
77
70
|
/** The caller's durable key, persisted before dispatch and never replaced. */
|
|
78
71
|
request_id: string;
|
|
79
72
|
/** The nonce the child must repeat in its startup acknowledgment. */
|
|
80
73
|
generation: string;
|
|
81
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Start this released child agent again rather than a new one. The daemon takes
|
|
76
|
+
* the session from its own record, so no session id or path is sent.
|
|
77
|
+
*/
|
|
78
|
+
continues?: string;
|
|
79
|
+
/**
|
|
80
|
+
* The pane this launch splits. The parent is the calling process itself, read
|
|
81
|
+
* from its own publication, and must be registered on exactly this pane.
|
|
82
|
+
*/
|
|
83
|
+
pane_id: string;
|
|
82
84
|
parent_session_id: string;
|
|
83
85
|
executable: string;
|
|
84
86
|
argv: string[];
|
|
@@ -94,35 +96,13 @@ export type RadarWorkerLaunch = {
|
|
|
94
96
|
workspaceId: string;
|
|
95
97
|
agentLabel: string;
|
|
96
98
|
};
|
|
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
99
|
};
|
|
114
100
|
|
|
115
101
|
export type RadarWorkerLaunched<State = unknown> = {
|
|
116
102
|
request_id: string;
|
|
117
|
-
|
|
103
|
+
/** The child's durable identity: the key that survives its restart and resume. */
|
|
104
|
+
subject_id: string;
|
|
118
105
|
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
106
|
state: State;
|
|
127
107
|
};
|
|
128
108
|
|
|
@@ -315,6 +295,8 @@ export async function relaunchRefusalForChild(
|
|
|
315
295
|
|
|
316
296
|
export type RadarLaunchOptions<State extends LaunchState = LaunchState> = {
|
|
317
297
|
client: RadarClient;
|
|
298
|
+
/** This process's own publication, which names the parent of the launch. */
|
|
299
|
+
publication: Pick<RadarPublication, "subject">;
|
|
318
300
|
readers: LaunchMailbox<State>;
|
|
319
301
|
launch: RadarWorkerLaunch;
|
|
320
302
|
timeoutMs: number;
|
|
@@ -392,29 +374,35 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
392
374
|
const pollMs = options.pollMs ?? 250;
|
|
393
375
|
const deadline = Date.now() + options.timeoutMs;
|
|
394
376
|
const remaining = (): number => Math.max(0, deadline - Date.now());
|
|
395
|
-
const
|
|
396
|
-
if (!isUuid(
|
|
377
|
+
const parent = await options.publication.subject();
|
|
378
|
+
if (parent === undefined || !isUuid(parent.registration_id))
|
|
397
379
|
return refusedLaunch(
|
|
398
|
-
"
|
|
380
|
+
"this process has no Radar registration, so a worker cannot be spawned under it",
|
|
399
381
|
);
|
|
400
|
-
if (!isUuid(
|
|
382
|
+
if (parent.subject_id === undefined || !isUuid(parent.subject_id))
|
|
401
383
|
return refusedLaunch(
|
|
402
|
-
"
|
|
384
|
+
"this process has no managed Radar subject, so a worker cannot be spawned under it",
|
|
403
385
|
);
|
|
404
386
|
// A spawn is created under the pane its parent registered, and this port
|
|
405
387
|
// registers no location it was not given: without one there is nothing to
|
|
406
388
|
// place the child beside, and a name or session lookup is not a substitute.
|
|
389
|
+
const pane = parent.location?.pane;
|
|
407
390
|
if (pane === undefined || pane === "")
|
|
408
391
|
return refusedLaunch(
|
|
409
|
-
"this process's Radar registration names no pane, so
|
|
392
|
+
"this process's Radar registration names no pane, so a worker cannot be placed beside it",
|
|
393
|
+
);
|
|
394
|
+
if (pane !== launch.pane_id)
|
|
395
|
+
return refusedLaunch(
|
|
396
|
+
`this process's Radar registration places it in pane ${pane}, not in the pane this launch would split`,
|
|
410
397
|
);
|
|
411
398
|
|
|
412
399
|
const spawned = await client.spawn({
|
|
413
400
|
request_id: launch.request_id,
|
|
414
|
-
parent:
|
|
401
|
+
parent: parent.registration_id,
|
|
415
402
|
parent_session_id: launch.parent_session_id,
|
|
416
403
|
executable: launch.executable,
|
|
417
|
-
managed: { parent_subject_id:
|
|
404
|
+
managed: { parent_subject_id: parent.subject_id },
|
|
405
|
+
...(launch.continues !== undefined ? { continue_subject_id: launch.continues } : {}),
|
|
418
406
|
argv: launch.argv,
|
|
419
407
|
...(launch.cwd !== "" ? { cwd: launch.cwd } : {}),
|
|
420
408
|
...(Object.keys(launch.env).length > 0 ? { env: launch.env } : {}),
|
|
@@ -438,20 +426,6 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
438
426
|
});
|
|
439
427
|
if (bound.kind !== "bound") return bound;
|
|
440
428
|
const agentId = bound.registration_id;
|
|
441
|
-
// The binding is recorded before the readiness wait, not after it: the child
|
|
442
|
-
// may never acknowledge, and the subject and pane the daemon named are still
|
|
443
|
-
// what a reload has to prove this child by.
|
|
444
|
-
if (launch.onBound !== undefined) {
|
|
445
|
-
try {
|
|
446
|
-
launch.onBound({ registration_id: agentId, pane_id: bound.pane_id });
|
|
447
|
-
} catch (error) {
|
|
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
429
|
try {
|
|
456
430
|
const state = await options.readers.waitForState(
|
|
457
431
|
launch.mailbox,
|
|
@@ -466,21 +440,15 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
466
440
|
// the pane the daemon created is what refuses a child that inherited
|
|
467
441
|
// another process's coordinates.
|
|
468
442
|
paneId: radarPaneHandle(bound.pane_id),
|
|
469
|
-
})
|
|
470
|
-
(launch.observedChildAgent === undefined ||
|
|
471
|
-
launch.observedChildAgent() === agentId),
|
|
443
|
+
}),
|
|
472
444
|
{ timeoutMs: remaining(), ...(signal !== undefined ? { signal } : {}) },
|
|
473
445
|
);
|
|
474
446
|
return {
|
|
475
447
|
kind: "ready",
|
|
476
448
|
value: {
|
|
477
449
|
request_id: launch.request_id,
|
|
478
|
-
|
|
450
|
+
subject_id: bound.subject_id,
|
|
479
451
|
pane_id: bound.pane_id,
|
|
480
|
-
bound_process: {
|
|
481
|
-
source: bound.source,
|
|
482
|
-
incarnation: bound.incarnation,
|
|
483
|
-
},
|
|
484
452
|
state,
|
|
485
453
|
},
|
|
486
454
|
};
|
|
@@ -498,6 +466,7 @@ export async function launchRadarWorker<State extends LaunchState>(
|
|
|
498
466
|
type BoundChild =
|
|
499
467
|
| {
|
|
500
468
|
kind: "bound";
|
|
469
|
+
subject_id: string;
|
|
501
470
|
registration_id: string;
|
|
502
471
|
pane_id: string;
|
|
503
472
|
source: string;
|
|
@@ -577,8 +546,18 @@ async function waitForChild(
|
|
|
577
546
|
requestId,
|
|
578
547
|
`the daemon bound the child ${agentId} without recording the process it registered as`,
|
|
579
548
|
);
|
|
549
|
+
// The edge names the agent as well as its process. Without it the child
|
|
550
|
+
// has no durable key to be returned by, and a registration is never
|
|
551
|
+
// offered in its place.
|
|
552
|
+
const subjectId = edge.bound?.subject_id;
|
|
553
|
+
if (subjectId === undefined || !isUuid(subjectId))
|
|
554
|
+
return unresolvedLaunch(
|
|
555
|
+
requestId,
|
|
556
|
+
`the daemon bound the child ${agentId} without recording its subject`,
|
|
557
|
+
);
|
|
580
558
|
return {
|
|
581
559
|
kind: "bound",
|
|
560
|
+
subject_id: subjectId,
|
|
582
561
|
registration_id: agentId,
|
|
583
562
|
pane_id: pane,
|
|
584
563
|
source,
|