pi-radar 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,9 +41,11 @@ The handoff API stores immutable bounded content separately from the recipient r
41
41
 
42
42
  To start a released child again, pass `continues: subject_id`: the daemon takes the session from its own record, so no session id or path is sent, and it refuses a child that is not yours or is still running. `client.restart` replaces your own live child under a new program. Both need Radar 0.4.0 or newer.
43
43
 
44
+ A launch is ready when the daemon grants the child's first presence, and `failed` when the daemon withdraws it. The daemon owns a 30-second deadline and records the withdrawal on the launch, so `failed` means the launch is already cleaned up and a retry is safe. `launchRadarWorker` has no timeout of its own; pass a `signal` to stop waiting. `unresolved` is left for a launch whose outcome cannot be read at all. `children.restart(subject, program)` replaces a live child in its own pane and waits on the same outcome.
45
+
44
46
  ## Addressing children by subject
45
47
 
46
- The runtime's `children` service (`RADAR_RUNTIME_VERSION` 3) is the surface for everything after a launch. `list()` returns the caller's children as `{ subject_id, request_id, pane_id, state, exited }`; `observe`, `inspect`, `assignment` and `retire` take the subject. pi-radar resolves a subject to its newest bound launch on every call and never remembers the process, so a child replaced under the same subject is reached by the same key. A held assignment follows a replaced child: the next `list`, `observe` or `inspect` finds the new process and moves the assignment onto it, retiring it from the old one. Consumers need no registration or claim id.
48
+ The runtime's `children` service (`RADAR_RUNTIME_VERSION` 4) is the surface for everything after a launch. `list()` returns the caller's children as `{ subject_id, request_id, pane_id, state, exited }`; `observe`, `inspect`, `assignment` and `retire` take the subject. pi-radar resolves a subject to its newest bound launch on every call and never remembers the process, so a child replaced under the same subject is reached by the same key. A held assignment follows a replaced child: the next `list`, `observe` or `inspect` finds the new process and moves the assignment onto it, retiring it from the old one. Consumers need no registration or claim id.
47
49
 
48
50
  ## Generated protocol contracts
49
51
 
@@ -8,7 +8,7 @@ import { lstatSync } from "node:fs";
8
8
  import { createConnection } from "node:net";
9
9
  import { dirname, join } from "node:path";
10
10
  var RADAR_PROTOCOL_VERSION = 2;
11
- var RADAR_MINIMUM_REVISION = 7;
11
+ var RADAR_MINIMUM_REVISION = 8;
12
12
  var RADAR_REGISTRY_CONTRACT = "registry.v1";
13
13
  var RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
14
14
  var RADAR_SPAWN_CONTRACT = "spawn.v1";
@@ -318,6 +318,9 @@ function spawnEdge(value) {
318
318
  const agentId = stringAt(bound, "registration_id");
319
319
  const subjectId = stringAt(bound, "subject_id");
320
320
  const exit = exitAt(recordAt(value, "exit_observation"));
321
+ const withdrawnRecord = recordAt(value, "withdrawn");
322
+ const withdrawnAt = stringAt(withdrawnRecord, "at");
323
+ const withdrawnReason = stringAt(withdrawnRecord, "reason");
321
324
  return {
322
325
  ok: true,
323
326
  value: {
@@ -334,7 +337,9 @@ function spawnEdge(value) {
334
337
  ...isUuid(subjectId) ? { subject_id: subjectId } : {}
335
338
  }
336
339
  } : {},
337
- ...exit !== void 0 ? { exit_observation: exit } : {}
340
+ ...exit !== void 0 ? { exit_observation: exit } : {},
341
+ ...stringAt(value, "granted_at") !== void 0 ? { granted_at: stringAt(value, "granted_at") } : {},
342
+ ...withdrawnAt !== void 0 && withdrawnReason !== void 0 ? { withdrawn: { at: withdrawnAt, reason: withdrawnReason } } : {}
338
343
  }
339
344
  };
340
345
  }
@@ -866,7 +871,11 @@ function createRadarClient(options = {}) {
866
871
  request.request_id
867
872
  );
868
873
  if (answer.ok === false) return answer;
869
- return focusOutcome(recordAt(answer.value, "request"), request.request_id);
874
+ const settled = focusOutcome(recordAt(answer.value, "request"), request.request_id);
875
+ if (settled.ok === false) return settled;
876
+ const launch = recordAt(recordAt(answer.value, "request"), "launch_request_id");
877
+ if (!isUuid(launch)) return protocol("lifecycle.restart reply names no launch");
878
+ return { ok: true, value: { ...settled.value, launch_request_id: launch } };
870
879
  });
871
880
  },
872
881
  async spawnList(request) {
@@ -1236,6 +1245,9 @@ function boundedRadarSnapshot(snapshot) {
1236
1245
  };
1237
1246
  }
1238
1247
 
1248
+ // extension/radar-children.ts
1249
+ import { randomUUID as randomUUID2 } from "node:crypto";
1250
+
1239
1251
  // extension/radar-inspection.ts
1240
1252
  var INSPECTION_LINES = 80;
1241
1253
  var INSPECTION_OUTPUT_BYTES = 16 * 1024;
@@ -1278,114 +1290,8 @@ function utf8Tail(value, maxBytes) {
1278
1290
  return { text: bytes.subarray(start).toString(), truncated: true };
1279
1291
  }
1280
1292
 
1281
- // extension/radar-location.ts
1282
- import { basename } from "node:path";
1283
- function radarLocalPane(env) {
1284
- const pane = env.TMUX_PANE?.trim();
1285
- return pane && /^%\d+$/.test(pane) ? pane : void 0;
1286
- }
1287
- function tmuxServerName(env) {
1288
- const socket = env.TMUX?.trim().split(",")[0];
1289
- return socket ? basename(socket) || void 0 : void 0;
1290
- }
1291
- function qualifiedOwnPane(env, backend, instance) {
1292
- if (backend !== "tmux")
1293
- return {
1294
- absent: `the daemon serves the ${backend} backend, and Herdsman drives only tmux`
1295
- };
1296
- if (instance === void 0 || instance === "")
1297
- return {
1298
- pending: "the daemon did not name the tmux server it addresses"
1299
- };
1300
- const server = tmuxServerName(env);
1301
- if (server === void 0)
1302
- return {
1303
- absent: "this process's own tmux client names no server, so it is not in the pane it would publish"
1304
- };
1305
- if (server !== instance)
1306
- return {
1307
- absent: `this process is attached to tmux server ${server}, not to ${instance}`
1308
- };
1309
- const pane = radarLocalPane(env);
1310
- if (pane === void 0)
1311
- return {
1312
- absent: "this process's own environment names no tmux pane of that server"
1313
- };
1314
- return { pane: `tmux:${instance}:${pane}` };
1315
- }
1316
- function observedContainment(inventory, paneId) {
1317
- const rows = inventory.panes.filter((pane) => pane.pane_id === paneId);
1318
- if (rows.length === 0) return void 0;
1319
- if (rows.length > 1) return { ambiguous: `topology reports pane ${paneId} twice` };
1320
- const row = rows[0];
1321
- const tab = inventory.tabs.find((candidate) => candidate.tab_id === row.tab_id);
1322
- if (tab !== void 0 && tab.workspace_id !== row.workspace_id)
1323
- return {
1324
- ambiguous: `pane ${paneId} is reported in ${row.workspace_id}, but its tab belongs to ${tab.workspace_id}`
1325
- };
1326
- return { pane: row };
1327
- }
1328
- async function resolveRadarProcessLocation(client, env = process.env) {
1329
- let ready;
1330
- try {
1331
- ready = await client.ping();
1332
- } catch {
1333
- return { state: "pending", reason: "the daemon did not answer this process" };
1334
- }
1335
- if (ready.ok === false)
1336
- return {
1337
- state: "pending",
1338
- reason: `the daemon did not answer this process (${ready.code})`
1339
- };
1340
- const backend = ready.value.backend;
1341
- if (backend === void 0 || backend === "none")
1342
- return { state: "absent", reason: "the daemon serves no physical backend" };
1343
- const instance = ready.value.instance ?? void 0;
1344
- const own = qualifiedOwnPane(env, backend, instance);
1345
- if ("absent" in own) return { state: "absent", reason: own.absent };
1346
- if ("pending" in own) return { state: "pending", reason: own.pending };
1347
- const paneId = own.pane;
1348
- if (paneId === env.PI_HERDSMAN_PARENT_PANE_ID?.trim())
1349
- return {
1350
- state: "absent",
1351
- reason: "this process is in the pane its launch was created beside, not in a pane of its own"
1352
- };
1353
- let inventory;
1354
- try {
1355
- inventory = await client.observe();
1356
- } catch {
1357
- return {
1358
- state: "pending",
1359
- reason: "the daemon's topology could not be read for this process's pane"
1360
- };
1361
- }
1362
- if (inventory.ok === false)
1363
- return {
1364
- state: "pending",
1365
- reason: `the daemon's topology could not be read (${inventory.code})`
1366
- };
1367
- const containment = observedContainment(inventory.value, paneId);
1368
- if (containment === void 0)
1369
- return {
1370
- state: "pending",
1371
- reason: `the daemon's topology does not report pane ${paneId} yet`
1372
- };
1373
- if ("ambiguous" in containment)
1374
- return { state: "pending", reason: containment.ambiguous };
1375
- const row = containment.pane;
1376
- return {
1377
- state: "resolved",
1378
- location: {
1379
- backend,
1380
- ...instance !== void 0 ? { instance } : {},
1381
- workspace: row.workspace_id,
1382
- tab: row.tab_id,
1383
- pane: paneId
1384
- }
1385
- };
1386
- }
1387
-
1388
1293
  // extension/radar-launch.ts
1294
+ import { setTimeout as delay } from "node:timers/promises";
1389
1295
  function unknownChild(reason) {
1390
1296
  return { kind: "unknown", diagnostic: trajectory(reason) };
1391
1297
  }
@@ -1461,6 +1367,72 @@ async function observeRadarChild(client, provenance) {
1461
1367
  }
1462
1368
  };
1463
1369
  }
1370
+ function refusedLaunch(message) {
1371
+ return { kind: "failed", message };
1372
+ }
1373
+ function unresolvedLaunch(requestId, message) {
1374
+ return {
1375
+ kind: "unresolved",
1376
+ request_id: requestId,
1377
+ message: `spawn ${requestId} is unresolved (${message})`
1378
+ };
1379
+ }
1380
+ var PRE_EFFECT_CODES = [
1381
+ "absent",
1382
+ "unsupported",
1383
+ "bad_version",
1384
+ "bad_request",
1385
+ "bad_params",
1386
+ "unknown_method",
1387
+ "refused",
1388
+ "not_found",
1389
+ "busy",
1390
+ "backend_unavailable"
1391
+ ];
1392
+ function launchFailure(result, requestId) {
1393
+ return PRE_EFFECT_CODES.includes(result.code) ? refusedLaunch(result.message) : unresolvedLaunch(requestId, result.message);
1394
+ }
1395
+ async function waitForChild(client, requestId, options) {
1396
+ let last = "the daemon has not answered";
1397
+ for (; ; ) {
1398
+ if (options.signal?.aborted)
1399
+ return unresolvedLaunch(requestId, `the launch was cancelled (${last})`);
1400
+ const answer = await client.spawnGet(requestId);
1401
+ if (answer.ok === true) {
1402
+ const { edge } = answer.value;
1403
+ if (edge.withdrawn !== void 0)
1404
+ return refusedLaunch(`the daemon withdrew this launch: ${edge.withdrawn.reason}`);
1405
+ if (edge.granted_at === void 0) {
1406
+ await delay(options.pollMs);
1407
+ continue;
1408
+ }
1409
+ const pane = edge.location?.pane;
1410
+ if (pane === void 0 || pane === "")
1411
+ return unresolvedLaunch(
1412
+ requestId,
1413
+ "the daemon's launch recorded no pane to verify the child against"
1414
+ );
1415
+ const agentId = edge.bound?.registration_id;
1416
+ if (agentId !== void 0 && isUuid(agentId)) {
1417
+ const subjectId = edge.bound?.subject_id;
1418
+ if (subjectId === void 0 || !isUuid(subjectId))
1419
+ return unresolvedLaunch(
1420
+ requestId,
1421
+ `the daemon bound the child ${agentId} without recording its subject`
1422
+ );
1423
+ return {
1424
+ kind: "bound",
1425
+ subject_id: subjectId,
1426
+ pane_id: pane
1427
+ };
1428
+ }
1429
+ last = "no child has registered with this launch's token yet";
1430
+ } else {
1431
+ return unresolvedLaunch(requestId, answer.message);
1432
+ }
1433
+ await delay(options.pollMs);
1434
+ }
1435
+ }
1464
1436
 
1465
1437
  // extension/radar-children.ts
1466
1438
  var PAGE = 100;
@@ -1475,7 +1447,7 @@ function createRadarChildren(client, publication) {
1475
1447
  if (read.ok === false) return read;
1476
1448
  for (const edge of read.value.spawns) {
1477
1449
  const bound = edge.bound;
1478
- if (!bound?.subject_id || !bound.registration_id || edge.launched !== "completed") continue;
1450
+ if (!bound?.subject_id || !bound.registration_id || edge.granted_at == null || edge.withdrawn != null) continue;
1479
1451
  const at = Date.parse(bound.bound_at);
1480
1452
  if (Number.isNaN(at)) continue;
1481
1453
  const current = newest.get(bound.subject_id);
@@ -1521,6 +1493,34 @@ function createRadarChildren(client, publication) {
1521
1493
  follow(all.value);
1522
1494
  return { ok: true, value: [...all.value.values()].map((entry) => entry.child) };
1523
1495
  },
1496
+ async restart(subject, program, options) {
1497
+ const parent = await publication.subject();
1498
+ if (parent?.subject_id === void 0)
1499
+ return { kind: "failed", message: "this process has no Radar registration to restart a child under" };
1500
+ const request_id = randomUUID2();
1501
+ const requested = await client.restart({
1502
+ request_id,
1503
+ parent: parent.registration_id,
1504
+ subject_id: subject,
1505
+ ...program
1506
+ });
1507
+ if (requested.ok === false)
1508
+ return launchFailure(requested, request_id);
1509
+ const bound = await waitForChild(client, requested.value.launch_request_id, {
1510
+ pollMs: options?.pollMs ?? 250,
1511
+ ...options?.signal !== void 0 ? { signal: options.signal } : {}
1512
+ });
1513
+ if (bound.kind !== "bound") return bound;
1514
+ await resolveAndFollow(subject);
1515
+ return {
1516
+ kind: "ready",
1517
+ value: {
1518
+ request_id: requested.value.launch_request_id,
1519
+ subject_id: bound.subject_id,
1520
+ pane_id: bound.pane_id
1521
+ }
1522
+ };
1523
+ },
1524
1524
  assignment(subject, owner) {
1525
1525
  const existing = held.get(subject);
1526
1526
  if (existing !== void 0) return existing.writer;
@@ -1616,7 +1616,7 @@ function createRadarHandoffPort(client, publication, parentSubjectId = () => pro
1616
1616
  }
1617
1617
 
1618
1618
  // extension/radar-publication.ts
1619
- import { createHash, randomUUID as randomUUID2 } from "node:crypto";
1619
+ import { createHash, randomUUID as randomUUID3 } from "node:crypto";
1620
1620
  import {
1621
1621
  closeSync,
1622
1622
  constants,
@@ -1746,7 +1746,7 @@ function createPublicationStore(root) {
1746
1746
  ensureDirectory(directory);
1747
1747
  const temporary = join2(
1748
1748
  directory,
1749
- `.${name.slice(name.lastIndexOf("/") + 1)}.${randomUUID2()}.tmp`
1749
+ `.${name.slice(name.lastIndexOf("/") + 1)}.${randomUUID3()}.tmp`
1750
1750
  );
1751
1751
  const bytes = Buffer.from(JSON.stringify(value), "utf8");
1752
1752
  if (bytes.length > MAX_RECORD_BYTES)
@@ -1802,7 +1802,7 @@ function radarProcessSlot() {
1802
1802
  return host[PROCESS_SLOT];
1803
1803
  }
1804
1804
  function radarProcessIncarnation(slot = radarProcessSlot()) {
1805
- return slot.incarnation ??= randomUUID2();
1805
+ return slot.incarnation ??= randomUUID3();
1806
1806
  }
1807
1807
  function recordAt2(value, key) {
1808
1808
  if (typeof value !== "object" || value === null) return void 0;
@@ -2267,7 +2267,7 @@ function createPresenceReporter(input) {
2267
2267
  releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
2268
2268
  }
2269
2269
  standing = void 0;
2270
- state = { version: 2, session, claim_id: randomUUID2(), releases };
2270
+ state = { version: 2, session, claim_id: randomUUID3(), releases };
2271
2271
  save();
2272
2272
  stopped = false;
2273
2273
  void ensure();
@@ -2304,7 +2304,7 @@ var STORE_IDENTITY = /* @__PURE__ */ new WeakMap();
2304
2304
  function storeIdentity(store) {
2305
2305
  const known = STORE_IDENTITY.get(store);
2306
2306
  if (known !== void 0) return known;
2307
- const created = randomUUID2();
2307
+ const created = randomUUID3();
2308
2308
  STORE_IDENTITY.set(store, created);
2309
2309
  return created;
2310
2310
  }
@@ -2825,9 +2825,116 @@ function createRadarExecutionAdapter(pi, options) {
2825
2825
  };
2826
2826
  }
2827
2827
 
2828
+ // extension/radar-location.ts
2829
+ import { basename } from "node:path";
2830
+ function radarLocalPane(env) {
2831
+ const pane = env.TMUX_PANE?.trim();
2832
+ return pane && /^%\d+$/.test(pane) ? pane : void 0;
2833
+ }
2834
+ function tmuxServerName(env) {
2835
+ const socket = env.TMUX?.trim().split(",")[0];
2836
+ return socket ? basename(socket) || void 0 : void 0;
2837
+ }
2838
+ function qualifiedOwnPane(env, backend, instance) {
2839
+ if (backend !== "tmux")
2840
+ return {
2841
+ absent: `the daemon serves the ${backend} backend, and Herdsman drives only tmux`
2842
+ };
2843
+ if (instance === void 0 || instance === "")
2844
+ return {
2845
+ pending: "the daemon did not name the tmux server it addresses"
2846
+ };
2847
+ const server = tmuxServerName(env);
2848
+ if (server === void 0)
2849
+ return {
2850
+ absent: "this process's own tmux client names no server, so it is not in the pane it would publish"
2851
+ };
2852
+ if (server !== instance)
2853
+ return {
2854
+ absent: `this process is attached to tmux server ${server}, not to ${instance}`
2855
+ };
2856
+ const pane = radarLocalPane(env);
2857
+ if (pane === void 0)
2858
+ return {
2859
+ absent: "this process's own environment names no tmux pane of that server"
2860
+ };
2861
+ return { pane: `tmux:${instance}:${pane}` };
2862
+ }
2863
+ function observedContainment(inventory, paneId) {
2864
+ const rows = inventory.panes.filter((pane) => pane.pane_id === paneId);
2865
+ if (rows.length === 0) return void 0;
2866
+ if (rows.length > 1) return { ambiguous: `topology reports pane ${paneId} twice` };
2867
+ const row = rows[0];
2868
+ const tab = inventory.tabs.find((candidate) => candidate.tab_id === row.tab_id);
2869
+ if (tab !== void 0 && tab.workspace_id !== row.workspace_id)
2870
+ return {
2871
+ ambiguous: `pane ${paneId} is reported in ${row.workspace_id}, but its tab belongs to ${tab.workspace_id}`
2872
+ };
2873
+ return { pane: row };
2874
+ }
2875
+ async function resolveRadarProcessLocation(client, env = process.env) {
2876
+ let ready;
2877
+ try {
2878
+ ready = await client.ping();
2879
+ } catch {
2880
+ return { state: "pending", reason: "the daemon did not answer this process" };
2881
+ }
2882
+ if (ready.ok === false)
2883
+ return {
2884
+ state: "pending",
2885
+ reason: `the daemon did not answer this process (${ready.code})`
2886
+ };
2887
+ const backend = ready.value.backend;
2888
+ if (backend === void 0 || backend === "none")
2889
+ return { state: "absent", reason: "the daemon serves no physical backend" };
2890
+ const instance = ready.value.instance ?? void 0;
2891
+ const own = qualifiedOwnPane(env, backend, instance);
2892
+ if ("absent" in own) return { state: "absent", reason: own.absent };
2893
+ if ("pending" in own) return { state: "pending", reason: own.pending };
2894
+ const paneId = own.pane;
2895
+ if (paneId === env.PI_HERDSMAN_PARENT_PANE_ID?.trim())
2896
+ return {
2897
+ state: "absent",
2898
+ reason: "this process is in the pane its launch was created beside, not in a pane of its own"
2899
+ };
2900
+ let inventory;
2901
+ try {
2902
+ inventory = await client.observe();
2903
+ } catch {
2904
+ return {
2905
+ state: "pending",
2906
+ reason: "the daemon's topology could not be read for this process's pane"
2907
+ };
2908
+ }
2909
+ if (inventory.ok === false)
2910
+ return {
2911
+ state: "pending",
2912
+ reason: `the daemon's topology could not be read (${inventory.code})`
2913
+ };
2914
+ const containment = observedContainment(inventory.value, paneId);
2915
+ if (containment === void 0)
2916
+ return {
2917
+ state: "pending",
2918
+ reason: `the daemon's topology does not report pane ${paneId} yet`
2919
+ };
2920
+ if ("ambiguous" in containment)
2921
+ return { state: "pending", reason: containment.ambiguous };
2922
+ const row = containment.pane;
2923
+ return {
2924
+ state: "resolved",
2925
+ location: {
2926
+ backend,
2927
+ ...instance !== void 0 ? { instance } : {},
2928
+ workspace: row.workspace_id,
2929
+ tab: row.tab_id,
2930
+ pane: paneId
2931
+ }
2932
+ };
2933
+ }
2934
+
2828
2935
  // extension/radar-runtime.ts
2829
2936
  var RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
2830
- var RADAR_RUNTIME_VERSION = 3;
2937
+ var RADAR_RUNTIME_VERSION = 4;
2831
2938
  function lazyRadarRuntime(start) {
2832
2939
  let pending;
2833
2940
  return {
@@ -1,3 +1,6 @@
1
+ // extension/radar-children.ts
2
+ import { randomUUID } from "node:crypto";
3
+
1
4
  // extension/radar-inspection.ts
2
5
  var INSPECTION_LINES = 80;
3
6
  var INSPECTION_OUTPUT_BYTES = 16 * 1024;
@@ -40,8 +43,15 @@ function utf8Tail(value, maxBytes) {
40
43
  return { text: bytes.subarray(start).toString(), truncated: true };
41
44
  }
42
45
 
46
+ // extension/radar-launch.ts
47
+ import { setTimeout as delay } from "node:timers/promises";
48
+
43
49
  // extension/radar-client.ts
44
50
  var RADAR_MAX_LINE_BYTES = 1024 * 1024;
51
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
52
+ function isUuid(value) {
53
+ return typeof value === "string" && UUID.test(value);
54
+ }
45
55
 
46
56
  // extension/radar-launch.ts
47
57
  function unknownChild(reason) {
@@ -119,6 +129,72 @@ async function observeRadarChild(client, provenance) {
119
129
  }
120
130
  };
121
131
  }
132
+ function refusedLaunch(message) {
133
+ return { kind: "failed", message };
134
+ }
135
+ function unresolvedLaunch(requestId, message) {
136
+ return {
137
+ kind: "unresolved",
138
+ request_id: requestId,
139
+ message: `spawn ${requestId} is unresolved (${message})`
140
+ };
141
+ }
142
+ var PRE_EFFECT_CODES = [
143
+ "absent",
144
+ "unsupported",
145
+ "bad_version",
146
+ "bad_request",
147
+ "bad_params",
148
+ "unknown_method",
149
+ "refused",
150
+ "not_found",
151
+ "busy",
152
+ "backend_unavailable"
153
+ ];
154
+ function launchFailure(result, requestId) {
155
+ return PRE_EFFECT_CODES.includes(result.code) ? refusedLaunch(result.message) : unresolvedLaunch(requestId, result.message);
156
+ }
157
+ async function waitForChild(client, requestId, options) {
158
+ let last = "the daemon has not answered";
159
+ for (; ; ) {
160
+ if (options.signal?.aborted)
161
+ return unresolvedLaunch(requestId, `the launch was cancelled (${last})`);
162
+ const answer = await client.spawnGet(requestId);
163
+ if (answer.ok === true) {
164
+ const { edge } = answer.value;
165
+ if (edge.withdrawn !== void 0)
166
+ return refusedLaunch(`the daemon withdrew this launch: ${edge.withdrawn.reason}`);
167
+ if (edge.granted_at === void 0) {
168
+ await delay(options.pollMs);
169
+ continue;
170
+ }
171
+ const pane = edge.location?.pane;
172
+ if (pane === void 0 || pane === "")
173
+ return unresolvedLaunch(
174
+ requestId,
175
+ "the daemon's launch recorded no pane to verify the child against"
176
+ );
177
+ const agentId = edge.bound?.registration_id;
178
+ if (agentId !== void 0 && isUuid(agentId)) {
179
+ const subjectId = edge.bound?.subject_id;
180
+ if (subjectId === void 0 || !isUuid(subjectId))
181
+ return unresolvedLaunch(
182
+ requestId,
183
+ `the daemon bound the child ${agentId} without recording its subject`
184
+ );
185
+ return {
186
+ kind: "bound",
187
+ subject_id: subjectId,
188
+ pane_id: pane
189
+ };
190
+ }
191
+ last = "no child has registered with this launch's token yet";
192
+ } else {
193
+ return unresolvedLaunch(requestId, answer.message);
194
+ }
195
+ await delay(options.pollMs);
196
+ }
197
+ }
122
198
 
123
199
  // extension/radar-children.ts
124
200
  var PAGE = 100;
@@ -133,7 +209,7 @@ function createRadarChildren(client, publication) {
133
209
  if (read.ok === false) return read;
134
210
  for (const edge of read.value.spawns) {
135
211
  const bound = edge.bound;
136
- if (!bound?.subject_id || !bound.registration_id || edge.launched !== "completed") continue;
212
+ if (!bound?.subject_id || !bound.registration_id || edge.granted_at == null || edge.withdrawn != null) continue;
137
213
  const at = Date.parse(bound.bound_at);
138
214
  if (Number.isNaN(at)) continue;
139
215
  const current = newest.get(bound.subject_id);
@@ -179,6 +255,34 @@ function createRadarChildren(client, publication) {
179
255
  follow(all.value);
180
256
  return { ok: true, value: [...all.value.values()].map((entry) => entry.child) };
181
257
  },
258
+ async restart(subject, program, options) {
259
+ const parent = await publication.subject();
260
+ if (parent?.subject_id === void 0)
261
+ return { kind: "failed", message: "this process has no Radar registration to restart a child under" };
262
+ const request_id = randomUUID();
263
+ const requested = await client.restart({
264
+ request_id,
265
+ parent: parent.registration_id,
266
+ subject_id: subject,
267
+ ...program
268
+ });
269
+ if (requested.ok === false)
270
+ return launchFailure(requested, request_id);
271
+ const bound = await waitForChild(client, requested.value.launch_request_id, {
272
+ pollMs: options?.pollMs ?? 250,
273
+ ...options?.signal !== void 0 ? { signal: options.signal } : {}
274
+ });
275
+ if (bound.kind !== "bound") return bound;
276
+ await resolveAndFollow(subject);
277
+ return {
278
+ kind: "ready",
279
+ value: {
280
+ request_id: requested.value.launch_request_id,
281
+ subject_id: bound.subject_id,
282
+ pane_id: bound.pane_id
283
+ }
284
+ };
285
+ },
182
286
  assignment(subject, owner) {
183
287
  const existing = held.get(subject);
184
288
  if (existing !== void 0) return existing.writer;
@@ -4,7 +4,7 @@ import { lstatSync } from "node:fs";
4
4
  import { createConnection } from "node:net";
5
5
  import { dirname, join } from "node:path";
6
6
  var RADAR_PROTOCOL_VERSION = 2;
7
- var RADAR_MINIMUM_REVISION = 7;
7
+ var RADAR_MINIMUM_REVISION = 8;
8
8
  var RADAR_REGISTRY_CONTRACT = "registry.v1";
9
9
  var RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
10
10
  var RADAR_SPAWN_CONTRACT = "spawn.v1";
@@ -318,6 +318,9 @@ function spawnEdge(value) {
318
318
  const agentId = stringAt(bound, "registration_id");
319
319
  const subjectId = stringAt(bound, "subject_id");
320
320
  const exit = exitAt(recordAt(value, "exit_observation"));
321
+ const withdrawnRecord = recordAt(value, "withdrawn");
322
+ const withdrawnAt = stringAt(withdrawnRecord, "at");
323
+ const withdrawnReason = stringAt(withdrawnRecord, "reason");
321
324
  return {
322
325
  ok: true,
323
326
  value: {
@@ -334,7 +337,9 @@ function spawnEdge(value) {
334
337
  ...isUuid(subjectId) ? { subject_id: subjectId } : {}
335
338
  }
336
339
  } : {},
337
- ...exit !== void 0 ? { exit_observation: exit } : {}
340
+ ...exit !== void 0 ? { exit_observation: exit } : {},
341
+ ...stringAt(value, "granted_at") !== void 0 ? { granted_at: stringAt(value, "granted_at") } : {},
342
+ ...withdrawnAt !== void 0 && withdrawnReason !== void 0 ? { withdrawn: { at: withdrawnAt, reason: withdrawnReason } } : {}
338
343
  }
339
344
  };
340
345
  }
@@ -924,7 +929,11 @@ function createRadarClient(options = {}) {
924
929
  request.request_id
925
930
  );
926
931
  if (answer.ok === false) return answer;
927
- return focusOutcome(recordAt(answer.value, "request"), request.request_id);
932
+ const settled = focusOutcome(recordAt(answer.value, "request"), request.request_id);
933
+ if (settled.ok === false) return settled;
934
+ const launch = recordAt(recordAt(answer.value, "request"), "launch_request_id");
935
+ if (!isUuid(launch)) return protocol("lifecycle.restart reply names no launch");
936
+ return { ok: true, value: { ...settled.value, launch_request_id: launch } };
928
937
  });
929
938
  },
930
939
  async spawnList(request) {