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.
Files changed (42) hide show
  1. package/README.md +8 -10
  2. package/dist/extension/baseline.js +343 -361
  3. package/dist/extension/radar-children.js +108 -218
  4. package/dist/extension/radar-client.js +103 -15
  5. package/dist/extension/radar-execution.js +26 -17
  6. package/dist/extension/radar-inspection.js +0 -45
  7. package/dist/extension/radar-launch.js +24 -185
  8. package/dist/extension/radar-publication.js +26 -17
  9. package/dist/extension/radar-runtime.js +1 -1
  10. package/dist/index.js +317 -488
  11. package/extension/generated-contracts.ts +362 -13
  12. package/extension/radar-children.ts +60 -155
  13. package/extension/radar-client.ts +130 -19
  14. package/extension/radar-inspection.ts +2 -77
  15. package/extension/radar-launch.ts +52 -391
  16. package/extension/radar-publication.ts +36 -53
  17. package/extension/radar-runtime.ts +1 -1
  18. package/package.json +1 -1
  19. package/schema/public/v1/observation-get-response.deserialize.schema.json +5 -25
  20. package/schema/public/v1/observation-get-response.serialize.schema.json +7 -29
  21. package/schema/public/v1/observation-snapshot.deserialize.schema.json +5 -25
  22. package/schema/public/v1/observation-snapshot.serialize.schema.json +7 -29
  23. package/schema/public/v1/spawn-list-page.deserialize.schema.json +40 -17
  24. package/schema/public/v1/spawn-list-page.serialize.schema.json +40 -17
  25. package/schema/public/v1/subject-assignment-acquire.deserialize.schema.json +78 -0
  26. package/schema/public/v1/subject-assignment-acquire.serialize.schema.json +79 -0
  27. package/schema/public/v1/subject-assignment-publish.deserialize.schema.json +310 -0
  28. package/schema/public/v1/subject-assignment-publish.serialize.schema.json +310 -0
  29. package/schema/public/v1/subject-assignment-retire.deserialize.schema.json +22 -0
  30. package/schema/public/v1/subject-assignment-retire.serialize.schema.json +22 -0
  31. package/schema/public/v1/subject-child-get-params.deserialize.schema.json +18 -0
  32. package/schema/public/v1/subject-child-get-params.serialize.schema.json +18 -0
  33. package/schema/public/v1/subject-children-params.deserialize.schema.json +51 -0
  34. package/schema/public/v1/subject-children-params.serialize.schema.json +54 -0
  35. package/schema/public/v1/subject-get-params.deserialize.schema.json +14 -0
  36. package/schema/public/v1/subject-get-params.serialize.schema.json +14 -0
  37. package/schema/public/v1/subject-get-result.deserialize.schema.json +566 -0
  38. package/schema/public/v1/subject-get-result.serialize.schema.json +573 -0
  39. package/schema/public/v1/subject-list-params.deserialize.schema.json +38 -0
  40. package/schema/public/v1/subject-list-params.serialize.schema.json +43 -0
  41. package/schema/public/v1/subject-list-result.deserialize.schema.json +575 -0
  42. package/schema/public/v1/subject-list-result.serialize.schema.json +583 -0
@@ -1,176 +1,81 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  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";
3
+ import type { SubjectGetResult, SubjectChildrenParams } from "./generated-contracts.ts";
4
+ import type { RadarInspection } from "./radar-inspection.ts";
5
+ import { waitForChild, launchFailure, type RadarLaunchOutcome } from "./radar-launch.ts";
5
6
  import type { RadarPublication } from "./radar-publication.ts";
6
7
 
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. */
8
+ /** An agent, including its retained identity when no process is attached. */
9
+ export type RadarChild = SubjectGetResult["subject"];
21
10
  export type RadarChildAssignment = {
22
11
  update(snapshot: RadarChannelSnapshot): void;
23
12
  retire(): void;
24
13
  };
25
-
14
+ export type RadarChildProgram = {
15
+ executable: string;
16
+ argv: string[];
17
+ cwd?: string;
18
+ env?: Record<string, string>;
19
+ };
20
+ export type RadarChildObservation =
21
+ | { kind: "live"; subject: RadarChild }
22
+ | { kind: "gone"; subject: RadarChild }
23
+ | { kind: "unknown"; diagnostic: string };
26
24
  export type RadarChildren = {
27
- list(): Promise<RadarResult<RadarChild[]>>;
25
+ list(selection?: SubjectChildrenParams["selection"]): Promise<RadarResult<RadarChild[]>>;
26
+ restart(subject: string, program: RadarChildProgram, options?: { pollMs?: number; signal?: AbortSignal }): Promise<RadarLaunchOutcome>;
28
27
  assignment(subject: string, owner: string): RadarChildAssignment;
29
28
  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>;
29
+ observe(subject: string): Promise<RadarChildObservation>;
30
+ inspect(subject: string): Promise<RadarInspection>;
35
31
  };
36
32
 
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
- */
33
+ /** All child lifecycle resolution and assignment ownership belong to Radar. */
45
34
  export function createRadarChildren(
46
- client: Pick<RadarClient, "spawnList" | "spawnGet" | "agentGet" | "output" | "processInfo">,
47
- publication: Pick<RadarPublication, "assignment" | "retireAssignment">,
35
+ client: Pick<RadarClient, "subjectChildren" | "childGet" | "childInspect" | "spawnGet" | "restart">,
36
+ publication: Pick<RadarPublication, "assignment" | "retireAssignment" | "subject">,
48
37
  ): 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
38
  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;
39
+ async list(selection) {
40
+ const children: RadarChild[] = [];
41
+ let after: string | undefined;
42
+ do {
43
+ const page = await client.subjectChildren({ selection, after, limit: 100 });
44
+ if (!page.ok) return page;
45
+ children.push(...page.value.subjects);
46
+ if (page.value.next === after)
47
+ return { ok: false, code: "protocol", message: "child page did not advance" };
48
+ after = page.value.next ?? undefined;
49
+ } while (after !== undefined);
50
+ return { ok: true, value: children };
149
51
  },
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,
52
+ async restart(subject, program, options) {
53
+ const parent = await publication.subject();
54
+ if (parent?.subject_id === undefined)
55
+ return { kind: "failed", message: "this process has no granted subject" };
56
+ const request_id = randomUUID();
57
+ const requested = await client.restart({ request_id, parent: parent.registration_id, subject_id: subject, ...program });
58
+ if (!requested.ok) return launchFailure(requested, request_id);
59
+ const bound = await waitForChild(client, requested.value.launch_request_id, {
60
+ pollMs: options?.pollMs ?? 250,
61
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
163
62
  });
63
+ if (bound.kind !== "bound") return bound;
64
+ return { kind: "ready", value: { request_id: requested.value.launch_request_id, subject_id: bound.subject_id, pane_id: bound.pane_id } };
164
65
  },
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
- });
66
+ assignment: (subject, owner) => publication.assignment(subject, owner),
67
+ retire: (subject) => publication.retireAssignment(subject),
68
+ async observe(subject) {
69
+ const read = await client.childGet(subject);
70
+ if (!read.ok) return { kind: "unknown", diagnostic: `${read.code}: ${read.message}` };
71
+ if (read.value.state === "live") return { kind: "live", subject: read.value };
72
+ if (read.value.state === "detached") return { kind: "gone", subject: read.value };
73
+ return { kind: "unknown", diagnostic: `child attachment is ${read.value.state}` };
74
+ },
75
+ async inspect(subject) {
76
+ const read = await client.childInspect(subject);
77
+ if (!read.ok) throw new Error(`Child inspection unavailable (${read.code}: ${read.message})`);
78
+ return read.value;
174
79
  },
175
80
  };
176
81
  }
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import type { RadarInspection } from "./radar-inspection.ts";
2
3
  import { lstatSync } from "node:fs";
3
4
  import { createConnection } from "node:net";
4
5
  import { dirname, join } from "node:path";
@@ -13,6 +14,10 @@ import type {
13
14
  SpawnExitObservationSerialize,
14
15
  SpawnListParamsDeserialize,
15
16
  SpawnListPageSerialize,
17
+ SubjectChildGetParams,
18
+ SubjectChildrenParams,
19
+ SubjectGetResult,
20
+ SubjectListResult,
16
21
  } from "./generated-contracts.ts";
17
22
 
18
23
  /**
@@ -38,12 +43,11 @@ export const RADAR_PROTOCOL_VERSION = 2;
38
43
  * parent continue or restart its own child by subject.
39
44
  * Earlier daemons expose different identity fields; require a coordinated update.
40
45
  */
41
- export const RADAR_MINIMUM_REVISION = 7;
46
+ export const RADAR_MINIMUM_REVISION = 9;
42
47
  /** Public daemon contracts this client may preflight. */
43
48
  export const RADAR_REGISTRY_CONTRACT = "registry.v1";
44
49
  export const RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
45
50
  export const RADAR_SPAWN_CONTRACT = "spawn.v1";
46
- export const RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
47
51
  export const RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
48
52
  export const RADAR_LIFECYCLE_RESTART_CONTRACT = "lifecycle.restart.v1";
49
53
  export const RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
@@ -309,6 +313,9 @@ export type RadarSpawnEdge = {
309
313
  location?: RadarLocation;
310
314
  bound?: { source: string; incarnation: string; registration_id?: string; subject_id?: string };
311
315
  exit_observation?: RadarSpawnExitObservation;
316
+ /** Set when the daemon withdrew the launch before its process presented. */
317
+ withdrawn?: { at: string; reason: string };
318
+ granted_at?: string;
312
319
  };
313
320
 
314
321
  /**
@@ -808,8 +815,7 @@ function locationAt(value: unknown): RadarLocation | undefined {
808
815
 
809
816
  /**
810
817
  * One durable edge, decoded strictly. The bound child's `registration_id` is kept only
811
- * when it is a canonical UUID: a legacy binding has none, and an unreadable one
812
- * is not an identity to correlate with.
818
+ * when it is a canonical UUID: an unreadable one is not an identity to correlate with.
813
819
  */
814
820
  function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
815
821
  const requestId = stringAt(value, "request_id");
@@ -830,6 +836,9 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
830
836
  const agentId = stringAt(bound, "registration_id");
831
837
  const subjectId = stringAt(bound, "subject_id");
832
838
  const exit = exitAt(recordAt(value, "exit_observation"));
839
+ const withdrawnRecord = recordAt(value, "withdrawn");
840
+ const withdrawnAt = stringAt(withdrawnRecord, "at");
841
+ const withdrawnReason = stringAt(withdrawnRecord, "reason");
833
842
  return {
834
843
  ok: true,
835
844
  value: {
@@ -849,6 +858,10 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
849
858
  }
850
859
  : {}),
851
860
  ...(exit !== undefined ? { exit_observation: exit } : {}),
861
+ ...(stringAt(value, "granted_at") !== undefined ? { granted_at: stringAt(value, "granted_at") } : {}),
862
+ ...(withdrawnAt !== undefined && withdrawnReason !== undefined
863
+ ? { withdrawn: { at: withdrawnAt, reason: withdrawnReason } }
864
+ : {}),
852
865
  },
853
866
  };
854
867
  }
@@ -867,6 +880,16 @@ const PROCESS_VERDICTS: readonly RadarProcessVerdict[] = [
867
880
  "unavailable",
868
881
  ];
869
882
 
883
+ function subjectAt(value: unknown): RadarResult<SubjectGetResult["subject"]> {
884
+ const subjectId = recordAt(value, "subject_id");
885
+ const session = recordAt(value, "session_id");
886
+ const state = recordAt(value, "state");
887
+ if (!isUuid(subjectId) || (session != null && !isUuid(session)) ||
888
+ !["starting", "live", "detached", "unknown"].includes(String(state)))
889
+ return protocol("subject read carries no valid identity or attachment state");
890
+ return { ok: true, value: value as SubjectGetResult["subject"] };
891
+ }
892
+
870
893
  function agentProcessAt(agent: unknown): RadarResult<RadarAgentProcess> {
871
894
  const process = recordAt(agent, "process");
872
895
  const claimed = recordAt(process, "claimed");
@@ -1051,13 +1074,15 @@ export type RadarClient = {
1051
1074
  registration: RadarRegistration,
1052
1075
  ): Promise<RadarResult<{ registration_id: string }>>;
1053
1076
  acquire(request: {
1054
- registration_id: string;
1077
+ registration_id?: string;
1078
+ subject_id?: string;
1055
1079
  channel: RadarChannel;
1056
1080
  publisher: RadarPublisherIdentity;
1057
1081
  replace?: { generation: number; handle: string };
1058
1082
  }): Promise<RadarResult<RadarWriterBinding>>;
1059
1083
  publish(request: {
1060
- registration_id: string;
1084
+ registration_id?: string;
1085
+ subject_id?: string;
1061
1086
  channel: RadarChannel;
1062
1087
  writer_handle: string;
1063
1088
  sequence: number;
@@ -1078,7 +1103,8 @@ export type RadarClient = {
1078
1103
  context: RadarContextValue;
1079
1104
  }): Promise<RadarResult<{ writer: RadarWriterBinding; warning?: string; management?: RadarManagement }>>;
1080
1105
  retire(request: {
1081
- registration_id: string;
1106
+ registration_id?: string;
1107
+ subject_id?: string;
1082
1108
  channel: RadarChannel;
1083
1109
  writer_handle: string;
1084
1110
  }): Promise<RadarResult<void>>;
@@ -1124,10 +1150,13 @@ export type RadarClient = {
1124
1150
  argv: string[];
1125
1151
  cwd?: string;
1126
1152
  env?: Record<string, string>;
1127
- }): Promise<RadarResult<{ request_id: string }>>;
1153
+ }): Promise<RadarResult<{ request_id: string; launch_request_id: string }>>;
1128
1154
  /** List launches owned by this client's registered process. */
1129
1155
  spawnList(request: Pick<SpawnListParamsDeserialize, "after" | "limit">): Promise<RadarResult<SpawnListPageSerialize>>;
1130
1156
  spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
1157
+ subjectChildren(request?: Omit<SubjectChildrenParams, "registration_id">): Promise<RadarResult<SubjectListResult>>;
1158
+ childGet(subject_id: string): Promise<RadarResult<SubjectGetResult["subject"]>>;
1159
+ childInspect(subject_id: string): Promise<RadarResult<RadarInspection>>;
1131
1160
  /** The registered child's process verdict, read from its immutable claim. */
1132
1161
  agentGet(registration_id: string): Promise<RadarResult<RadarAgentProcess>>;
1133
1162
  observe(): Promise<RadarResult<RadarInventory>>;
@@ -1541,7 +1570,11 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1541
1570
  return withRegistry(async () => {
1542
1571
  const answer = await call("agent.presence", { ...request });
1543
1572
  if (!answer.ok) return answer;
1544
- return decodePresenceAnswer(answer.value);
1573
+ const decoded = decodePresenceAnswer(answer.value);
1574
+ // Presence names the caller's own registration, which is the
1575
+ // authority every subject-addressed child call is checked against.
1576
+ if (decoded.ok) processRegistrationId = decoded.value.registration_id;
1577
+ return decoded;
1545
1578
  });
1546
1579
  },
1547
1580
  async releasePresence(request) {
@@ -1582,9 +1615,13 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1582
1615
  },
1583
1616
  async acquire(request) {
1584
1617
  return withRegistryChannel(request.channel, async () => {
1585
- const answer = await call("agent.acquire", {
1586
- registration_id: request.registration_id,
1587
- channel: request.channel,
1618
+ const assignment = request.channel === "assignment";
1619
+ if (assignment && processRegistrationId === undefined)
1620
+ return { ok: false, code: "refused", message: "assignment requires a granted parent" };
1621
+ if (assignment && request.subject_id === undefined)
1622
+ return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
1623
+ const answer = await call(assignment ? "subject.assignment.acquire" : "agent.acquire", {
1624
+ ...(assignment ? { registration_id: processRegistrationId, subject_id: request.subject_id } : { registration_id: request.registration_id, channel: request.channel }),
1588
1625
  publisher: request.publisher,
1589
1626
  ...(request.replace !== undefined ? { replace: request.replace } : {}),
1590
1627
  });
@@ -1611,15 +1648,21 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1611
1648
  }
1612
1649
  }
1613
1650
  return withRegistryChannel(request.channel, async () => {
1614
- const answer = await call("agent.publish", {
1615
- registration_id: request.registration_id,
1616
- channel: request.channel,
1651
+ const update = {
1617
1652
  writer_handle: request.writer_handle,
1618
1653
  sequence: request.sequence,
1619
1654
  ...(request.lease_ms !== undefined ? { lease_ms: request.lease_ms } : {}),
1620
1655
  ...(request.observed_at !== undefined ? { observed_at: request.observed_at } : {}),
1621
1656
  snapshot,
1622
- });
1657
+ };
1658
+ const assignment = request.channel === "assignment";
1659
+ if (assignment && processRegistrationId === undefined)
1660
+ return { ok: false, code: "refused", message: "assignment requires a granted parent" };
1661
+ if (assignment && request.subject_id === undefined)
1662
+ return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
1663
+ const answer = await call(assignment ? "subject.assignment.publish" : "agent.publish", assignment
1664
+ ? { registration_id: processRegistrationId, subject_id: request.subject_id, update }
1665
+ : { registration_id: request.registration_id, channel: request.channel, ...update });
1623
1666
  if (answer.ok === false) return answer;
1624
1667
  const channel = (answer.value as { channel?: unknown }).channel;
1625
1668
  const writer = (channel as { writer?: unknown } | undefined)?.writer;
@@ -1664,7 +1707,17 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1664
1707
  },
1665
1708
  async retire(request) {
1666
1709
  return withRegistryChannel(request.channel, async () => {
1667
- const answer = await call("agent.retire", request);
1710
+ const assignment = request.channel === "assignment";
1711
+ if (assignment && processRegistrationId === undefined)
1712
+ return { ok: false, code: "refused", message: "assignment requires a granted parent" };
1713
+ if (assignment && request.subject_id === undefined)
1714
+ return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
1715
+ const answer = await call(assignment ? "subject.assignment.retire" : "agent.retire", {
1716
+ ...(assignment
1717
+ ? { registration_id: processRegistrationId, subject_id: request.subject_id }
1718
+ : { registration_id: request.registration_id, channel: request.channel }),
1719
+ writer_handle: request.writer_handle,
1720
+ });
1668
1721
  return answer.ok === true ? { ok: true, value: undefined } : answer;
1669
1722
  });
1670
1723
  },
@@ -1757,14 +1810,72 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1757
1810
  request.request_id,
1758
1811
  );
1759
1812
  if (answer.ok === false) return answer;
1760
- return focusOutcome(recordAt(answer.value, "request"), request.request_id);
1813
+ const settled = focusOutcome(recordAt(answer.value, "request"), request.request_id);
1814
+ if (settled.ok === false) return settled;
1815
+ const launch = recordAt(recordAt(answer.value, "request"), "launch_request_id");
1816
+ if (!isUuid(launch)) return protocol("lifecycle.restart reply names no launch");
1817
+ return { ok: true, value: { ...settled.value, launch_request_id: launch } };
1818
+ });
1819
+ },
1820
+ async subjectChildren(request = {}) {
1821
+ const registrationId = processRegistrationId;
1822
+ if (registrationId === undefined)
1823
+ return { ok: false, code: "refused", message: "this process has no Radar registration" };
1824
+ return withRegistry(async () => {
1825
+ const params: SubjectChildrenParams = { ...request, registration_id: registrationId };
1826
+ const answer = await call("subject.children", { ...params });
1827
+ if (!answer.ok) return answer;
1828
+ const subjects = recordAt(answer.value, "subjects");
1829
+ const next = recordAt(answer.value, "next");
1830
+ if (!Array.isArray(subjects) || (next != null && !isUuid(next)))
1831
+ return protocol("subject.children reply carries no valid page");
1832
+ for (const subject of subjects) {
1833
+ const decoded = subjectAt(subject);
1834
+ if (!decoded.ok) return decoded;
1835
+ }
1836
+ return { ok: true, value: answer.value as SubjectListResult };
1837
+ });
1838
+ },
1839
+ async childGet(subject_id) {
1840
+ const registrationId = processRegistrationId;
1841
+ if (registrationId === undefined)
1842
+ return { ok: false, code: "refused", message: "this process has no Radar registration" };
1843
+ if (!isUuid(subject_id))
1844
+ return { ok: false, code: "bad_params", message: "child requires a subject UUID" };
1845
+ return withRegistry(async () => {
1846
+ const params: SubjectChildGetParams = { registration_id: registrationId, subject_id };
1847
+ const answer = await call("subject.child.get", { ...params });
1848
+ return answer.ok ? subjectAt(recordAt(answer.value, "subject")) : answer;
1849
+ });
1850
+ },
1851
+ async childInspect(subject_id) {
1852
+ const registration_id = processRegistrationId;
1853
+ if (registration_id === undefined)
1854
+ return { ok: false, code: "refused", message: "this process has no Radar registration" };
1855
+ return withRegistry(async () => {
1856
+ const answer = await call("subject.child.inspect", { registration_id, subject_id });
1857
+ if (!answer.ok) return answer;
1858
+ const subject = subjectAt(recordAt(answer.value, "subject"));
1859
+ if (!subject.ok) return subject;
1860
+ const output = recordAt(answer.value, "output");
1861
+ const text = stringAt(output, "text");
1862
+ if (text === undefined || typeof recordAt(output, "truncated") !== "boolean")
1863
+ return protocol("child inspection has no output evidence");
1864
+ const foreground = recordAt(answer.value, "foreground");
1865
+ const activity = subject.value.execution?.snapshot?.snapshot;
1866
+ return { ok: true, value: {
1867
+ capturedAt: Date.now(), recentOutputTruncated: recordAt(output, "truncated") as boolean,
1868
+ ...(text ? { recentOutput: text } : {}),
1869
+ ...(foreground ? { foreground: foreground as RadarForeground } : {}),
1870
+ ...(activity && "activity" in activity ? { activity: activity.activity } : {}),
1871
+ } };
1761
1872
  });
1762
1873
  },
1763
1874
  async spawnList(request) {
1764
1875
  const registrationId = processRegistrationId;
1765
1876
  if (registrationId === undefined)
1766
1877
  return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
1767
- return withContract(RADAR_SPAWN_LIST_CONTRACT, async () => {
1878
+ return withRegistry(async () => {
1768
1879
  const params: SpawnListParamsDeserialize = {
1769
1880
  registration_id: registrationId,
1770
1881
  ...(request.after !== undefined ? { after: request.after } : {}),
@@ -1,85 +1,10 @@
1
- import {
2
- type RadarAgentProcess,
3
- type RadarClient,
4
- type RadarForeground,
5
- } from "./radar-client.ts";
6
-
7
- // The caps the removed Herdr inspection applied; evidence is returned, never
8
- // the pane's full scrollback.
9
- const INSPECTION_LINES = 80;
10
- const INSPECTION_OUTPUT_BYTES = 16 * 1024;
1
+ import type { RadarForeground } from "./radar-client.ts";
11
2
 
3
+ /** Bounded evidence captured and identity-checked by Radar for one child subject. */
12
4
  export type RadarInspection = {
13
5
  capturedAt: number;
14
6
  recentOutputTruncated: boolean;
15
7
  recentOutput?: string;
16
8
  foreground?: RadarForeground;
17
- /** The child's published activity from the proof read; unknown when absent. */
18
9
  activity?: string;
19
10
  };
20
-
21
- /**
22
- * The registration's own read when the registered child `agentId` still runs on
23
- * `paneId` (and, when `session` is given, still publishes that session): a
24
- * `verified` verdict. Absent, mismatched, unavailable and unreadable are not
25
- * proof.
26
- */
27
- export async function provedRead(
28
- client: Pick<RadarClient, "agentGet">,
29
- target: { agentId: string; paneId: string; session?: string },
30
- ): Promise<RadarAgentProcess | undefined> {
31
- const read = await client.agentGet(target.agentId);
32
- return read.ok === true &&
33
- read.value.verification === "verified" &&
34
- read.value.location?.pane === target.paneId &&
35
- (target.session === undefined ||
36
- read.value.contextSession === target.session)
37
- ? read.value
38
- : undefined;
39
- }
40
-
41
- /**
42
- * Reads a managed child's recent output and foreground process, once the
43
- * registered child is proved on its pane before and after the read.
44
- */
45
- export async function inspectRadarChild(
46
- client: RadarClient,
47
- target: { agentId: string; paneId: string; session?: string },
48
- ): Promise<RadarInspection> {
49
- const before = await provedRead(client, target);
50
- if (before === undefined)
51
- throw new Error("Inspection target identity did not match");
52
- const output = await client.output({
53
- pane_id: target.paneId,
54
- source: "recent_unwrapped",
55
- lines: INSPECTION_LINES,
56
- });
57
- if (output.ok === false)
58
- throw new Error(
59
- `Inspection output unavailable (${output.code}: ${output.message})`,
60
- );
61
- const foreground = await client.processInfo({ pane_id: target.paneId });
62
- const after = await provedRead(client, target);
63
- if (after === undefined)
64
- throw new Error("Inspection target changed during capture");
65
- const lines = output.value.text.trim().split(/\r?\n/).slice(-INSPECTION_LINES);
66
- const bounded = utf8Tail(lines.join("\n"), INSPECTION_OUTPUT_BYTES);
67
- return Object.freeze({
68
- capturedAt: Date.now(),
69
- recentOutputTruncated: output.value.truncated || bounded.truncated,
70
- ...(bounded.text ? { recentOutput: bounded.text } : {}),
71
- ...(foreground.ok === true ? { foreground: foreground.value } : {}),
72
- ...(after.activity !== undefined ? { activity: after.activity } : {}),
73
- });
74
- }
75
-
76
- function utf8Tail(
77
- value: string,
78
- maxBytes: number,
79
- ): { text: string; truncated: boolean } {
80
- const bytes = Buffer.from(value);
81
- if (bytes.length <= maxBytes) return { text: value, truncated: false };
82
- let start = bytes.length - maxBytes;
83
- while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start++;
84
- return { text: bytes.subarray(start).toString(), truncated: true };
85
- }