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
@@ -8,11 +8,10 @@ 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 = 9;
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";
15
- var RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
16
15
  var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
17
16
  var RADAR_LIFECYCLE_RESTART_CONTRACT = "lifecycle.restart.v1";
18
17
  var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
@@ -318,6 +317,9 @@ function spawnEdge(value) {
318
317
  const agentId = stringAt(bound, "registration_id");
319
318
  const subjectId = stringAt(bound, "subject_id");
320
319
  const exit = exitAt(recordAt(value, "exit_observation"));
320
+ const withdrawnRecord = recordAt(value, "withdrawn");
321
+ const withdrawnAt = stringAt(withdrawnRecord, "at");
322
+ const withdrawnReason = stringAt(withdrawnRecord, "reason");
321
323
  return {
322
324
  ok: true,
323
325
  value: {
@@ -334,7 +336,9 @@ function spawnEdge(value) {
334
336
  ...isUuid(subjectId) ? { subject_id: subjectId } : {}
335
337
  }
336
338
  } : {},
337
- ...exit !== void 0 ? { exit_observation: exit } : {}
339
+ ...exit !== void 0 ? { exit_observation: exit } : {},
340
+ ...stringAt(value, "granted_at") !== void 0 ? { granted_at: stringAt(value, "granted_at") } : {},
341
+ ...withdrawnAt !== void 0 && withdrawnReason !== void 0 ? { withdrawn: { at: withdrawnAt, reason: withdrawnReason } } : {}
338
342
  }
339
343
  };
340
344
  }
@@ -344,6 +348,14 @@ var PROCESS_VERDICTS = [
344
348
  "mismatched",
345
349
  "unavailable"
346
350
  ];
351
+ function subjectAt(value) {
352
+ const subjectId = recordAt(value, "subject_id");
353
+ const session = recordAt(value, "session_id");
354
+ const state = recordAt(value, "state");
355
+ if (!isUuid(subjectId) || session != null && !isUuid(session) || !["starting", "live", "detached", "unknown"].includes(String(state)))
356
+ return protocol("subject read carries no valid identity or attachment state");
357
+ return { ok: true, value };
358
+ }
347
359
  function agentProcessAt(agent) {
348
360
  const process2 = recordAt(agent, "process");
349
361
  const claimed = recordAt(process2, "claimed");
@@ -678,7 +690,9 @@ function createRadarClient(options = {}) {
678
690
  return withRegistry(async () => {
679
691
  const answer = await call("agent.presence", { ...request });
680
692
  if (!answer.ok) return answer;
681
- return decodePresenceAnswer(answer.value);
693
+ const decoded = decodePresenceAnswer(answer.value);
694
+ if (decoded.ok) processRegistrationId = decoded.value.registration_id;
695
+ return decoded;
682
696
  });
683
697
  },
684
698
  async releasePresence(request) {
@@ -713,9 +727,13 @@ function createRadarClient(options = {}) {
713
727
  },
714
728
  async acquire(request) {
715
729
  return withRegistryChannel(request.channel, async () => {
716
- const answer = await call("agent.acquire", {
717
- registration_id: request.registration_id,
718
- channel: request.channel,
730
+ const assignment = request.channel === "assignment";
731
+ if (assignment && processRegistrationId === void 0)
732
+ return { ok: false, code: "refused", message: "assignment requires a granted parent" };
733
+ if (assignment && request.subject_id === void 0)
734
+ return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
735
+ const answer = await call(assignment ? "subject.assignment.acquire" : "agent.acquire", {
736
+ ...assignment ? { registration_id: processRegistrationId, subject_id: request.subject_id } : { registration_id: request.registration_id, channel: request.channel },
719
737
  publisher: request.publisher,
720
738
  ...request.replace !== void 0 ? { replace: request.replace } : {}
721
739
  });
@@ -742,15 +760,19 @@ function createRadarClient(options = {}) {
742
760
  }
743
761
  }
744
762
  return withRegistryChannel(request.channel, async () => {
745
- const answer = await call("agent.publish", {
746
- registration_id: request.registration_id,
747
- channel: request.channel,
763
+ const update = {
748
764
  writer_handle: request.writer_handle,
749
765
  sequence: request.sequence,
750
766
  ...request.lease_ms !== void 0 ? { lease_ms: request.lease_ms } : {},
751
767
  ...request.observed_at !== void 0 ? { observed_at: request.observed_at } : {},
752
768
  snapshot
753
- });
769
+ };
770
+ const assignment = request.channel === "assignment";
771
+ if (assignment && processRegistrationId === void 0)
772
+ return { ok: false, code: "refused", message: "assignment requires a granted parent" };
773
+ if (assignment && request.subject_id === void 0)
774
+ return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
775
+ const answer = await call(assignment ? "subject.assignment.publish" : "agent.publish", assignment ? { registration_id: processRegistrationId, subject_id: request.subject_id, update } : { registration_id: request.registration_id, channel: request.channel, ...update });
754
776
  if (answer.ok === false) return answer;
755
777
  const channel = answer.value.channel;
756
778
  const writer = channel?.writer;
@@ -791,7 +813,15 @@ function createRadarClient(options = {}) {
791
813
  },
792
814
  async retire(request) {
793
815
  return withRegistryChannel(request.channel, async () => {
794
- const answer = await call("agent.retire", request);
816
+ const assignment = request.channel === "assignment";
817
+ if (assignment && processRegistrationId === void 0)
818
+ return { ok: false, code: "refused", message: "assignment requires a granted parent" };
819
+ if (assignment && request.subject_id === void 0)
820
+ return { ok: false, code: "bad_params", message: "an assignment names a child subject" };
821
+ const answer = await call(assignment ? "subject.assignment.retire" : "agent.retire", {
822
+ ...assignment ? { registration_id: processRegistrationId, subject_id: request.subject_id } : { registration_id: request.registration_id, channel: request.channel },
823
+ writer_handle: request.writer_handle
824
+ });
795
825
  return answer.ok === true ? { ok: true, value: void 0 } : answer;
796
826
  });
797
827
  },
@@ -866,14 +896,73 @@ function createRadarClient(options = {}) {
866
896
  request.request_id
867
897
  );
868
898
  if (answer.ok === false) return answer;
869
- return focusOutcome(recordAt(answer.value, "request"), request.request_id);
899
+ const settled = focusOutcome(recordAt(answer.value, "request"), request.request_id);
900
+ if (settled.ok === false) return settled;
901
+ const launch = recordAt(recordAt(answer.value, "request"), "launch_request_id");
902
+ if (!isUuid(launch)) return protocol("lifecycle.restart reply names no launch");
903
+ return { ok: true, value: { ...settled.value, launch_request_id: launch } };
904
+ });
905
+ },
906
+ async subjectChildren(request = {}) {
907
+ const registrationId = processRegistrationId;
908
+ if (registrationId === void 0)
909
+ return { ok: false, code: "refused", message: "this process has no Radar registration" };
910
+ return withRegistry(async () => {
911
+ const params = { ...request, registration_id: registrationId };
912
+ const answer = await call("subject.children", { ...params });
913
+ if (!answer.ok) return answer;
914
+ const subjects = recordAt(answer.value, "subjects");
915
+ const next = recordAt(answer.value, "next");
916
+ if (!Array.isArray(subjects) || next != null && !isUuid(next))
917
+ return protocol("subject.children reply carries no valid page");
918
+ for (const subject of subjects) {
919
+ const decoded = subjectAt(subject);
920
+ if (!decoded.ok) return decoded;
921
+ }
922
+ return { ok: true, value: answer.value };
923
+ });
924
+ },
925
+ async childGet(subject_id) {
926
+ const registrationId = processRegistrationId;
927
+ if (registrationId === void 0)
928
+ return { ok: false, code: "refused", message: "this process has no Radar registration" };
929
+ if (!isUuid(subject_id))
930
+ return { ok: false, code: "bad_params", message: "child requires a subject UUID" };
931
+ return withRegistry(async () => {
932
+ const params = { registration_id: registrationId, subject_id };
933
+ const answer = await call("subject.child.get", { ...params });
934
+ return answer.ok ? subjectAt(recordAt(answer.value, "subject")) : answer;
935
+ });
936
+ },
937
+ async childInspect(subject_id) {
938
+ const registration_id = processRegistrationId;
939
+ if (registration_id === void 0)
940
+ return { ok: false, code: "refused", message: "this process has no Radar registration" };
941
+ return withRegistry(async () => {
942
+ const answer = await call("subject.child.inspect", { registration_id, subject_id });
943
+ if (!answer.ok) return answer;
944
+ const subject = subjectAt(recordAt(answer.value, "subject"));
945
+ if (!subject.ok) return subject;
946
+ const output = recordAt(answer.value, "output");
947
+ const text = stringAt(output, "text");
948
+ if (text === void 0 || typeof recordAt(output, "truncated") !== "boolean")
949
+ return protocol("child inspection has no output evidence");
950
+ const foreground = recordAt(answer.value, "foreground");
951
+ const activity = subject.value.execution?.snapshot?.snapshot;
952
+ return { ok: true, value: {
953
+ capturedAt: Date.now(),
954
+ recentOutputTruncated: recordAt(output, "truncated"),
955
+ ...text ? { recentOutput: text } : {},
956
+ ...foreground ? { foreground } : {},
957
+ ...activity && "activity" in activity ? { activity: activity.activity } : {}
958
+ } };
870
959
  });
871
960
  },
872
961
  async spawnList(request) {
873
962
  const registrationId = processRegistrationId;
874
963
  if (registrationId === void 0)
875
964
  return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
876
- return withContract(RADAR_SPAWN_LIST_CONTRACT, async () => {
965
+ return withRegistry(async () => {
877
966
  const params = {
878
967
  registration_id: registrationId,
879
968
  ...request.after !== void 0 ? { after: request.after } : {},
@@ -1236,344 +1325,121 @@ function boundedRadarSnapshot(snapshot) {
1236
1325
  };
1237
1326
  }
1238
1327
 
1239
- // extension/radar-inspection.ts
1240
- var INSPECTION_LINES = 80;
1241
- var INSPECTION_OUTPUT_BYTES = 16 * 1024;
1242
- async function provedRead(client, target) {
1243
- const read = await client.agentGet(target.agentId);
1244
- return read.ok === true && read.value.verification === "verified" && read.value.location?.pane === target.paneId && (target.session === void 0 || read.value.contextSession === target.session) ? read.value : void 0;
1245
- }
1246
- async function inspectRadarChild(client, target) {
1247
- const before = await provedRead(client, target);
1248
- if (before === void 0)
1249
- throw new Error("Inspection target identity did not match");
1250
- const output = await client.output({
1251
- pane_id: target.paneId,
1252
- source: "recent_unwrapped",
1253
- lines: INSPECTION_LINES
1254
- });
1255
- if (output.ok === false)
1256
- throw new Error(
1257
- `Inspection output unavailable (${output.code}: ${output.message})`
1258
- );
1259
- const foreground = await client.processInfo({ pane_id: target.paneId });
1260
- const after = await provedRead(client, target);
1261
- if (after === void 0)
1262
- throw new Error("Inspection target changed during capture");
1263
- const lines = output.value.text.trim().split(/\r?\n/).slice(-INSPECTION_LINES);
1264
- const bounded2 = utf8Tail(lines.join("\n"), INSPECTION_OUTPUT_BYTES);
1265
- return Object.freeze({
1266
- capturedAt: Date.now(),
1267
- recentOutputTruncated: output.value.truncated || bounded2.truncated,
1268
- ...bounded2.text ? { recentOutput: bounded2.text } : {},
1269
- ...foreground.ok === true ? { foreground: foreground.value } : {},
1270
- ...after.activity !== void 0 ? { activity: after.activity } : {}
1271
- });
1272
- }
1273
- function utf8Tail(value, maxBytes) {
1274
- const bytes = Buffer.from(value);
1275
- if (bytes.length <= maxBytes) return { text: value, truncated: false };
1276
- let start = bytes.length - maxBytes;
1277
- while (start < bytes.length && (bytes[start] & 192) === 128) start++;
1278
- return { text: bytes.subarray(start).toString(), truncated: true };
1279
- }
1328
+ // extension/radar-children.ts
1329
+ import { randomUUID as randomUUID2 } from "node:crypto";
1280
1330
 
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 };
1331
+ // extension/radar-launch.ts
1332
+ import { setTimeout as delay } from "node:timers/promises";
1333
+ function refusedLaunch(message) {
1334
+ return { kind: "failed", message };
1327
1335
  }
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;
1336
+ function unresolvedLaunch(requestId, message) {
1376
1337
  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
- }
1338
+ kind: "unresolved",
1339
+ request_id: requestId,
1340
+ message: `spawn ${requestId} is unresolved (${message})`
1385
1341
  };
1386
1342
  }
1387
-
1388
- // extension/radar-launch.ts
1389
- function unknownChild(reason) {
1390
- return { kind: "unknown", diagnostic: trajectory(reason) };
1391
- }
1392
- function trajectory(reason) {
1393
- return `the daemon's current record of this launch's child is ${reason}`;
1394
- }
1395
- async function observeRadarChild(client, provenance) {
1396
- if (provenance.registration_id === void 0)
1397
- return unknownChild("not bound to a child by this launch");
1398
- if (provenance.pane_id === void 0)
1399
- return unknownChild("without a pane this launch recorded for it");
1400
- const read = await client.spawnGet(provenance.request_id);
1401
- if (read.ok === false)
1402
- return unknownChild(
1403
- `not readable (${read.code}: ${read.message})`
1404
- );
1405
- const topology = read.value;
1406
- const edge = topology.edge;
1407
- if (edge.bound === void 0 || edge.bound.registration_id === void 0)
1408
- return unknownChild("not bound to a child by the daemon");
1409
- if (edge.bound.registration_id !== provenance.registration_id)
1410
- return unknownChild(
1411
- `bound to another subject (${edge.bound.registration_id}), not to ${provenance.registration_id}`
1412
- );
1413
- if (edge.location?.pane === void 0)
1414
- return unknownChild("recorded without a pane");
1415
- if (edge.location.pane !== provenance.pane_id)
1416
- return unknownChild(
1417
- `recorded on pane ${edge.location.pane}, not on ${provenance.pane_id}`
1418
- );
1419
- if (edge.exit_observation?.state !== "unknown")
1420
- return unknownChild("recorded with an observed process exit");
1421
- if (topology.freshness !== "fresh")
1422
- return unknownChild(
1423
- "on a pane the daemon's own inventory does not currently report"
1424
- );
1425
- if (topology.state !== "bound")
1426
- return unknownChild(
1427
- `not currently placed (state ${topology.state ?? "unreported"})`
1428
- );
1429
- const reply = await client.agentGet(edge.bound.registration_id);
1430
- if (reply.ok === false)
1431
- return unknownChild(
1432
- `process verdict not readable (${reply.code}: ${reply.message})`
1433
- );
1434
- const { verification, location, contextSession } = reply.value;
1435
- if (verification === "absent" || verification === "mismatched")
1436
- return {
1437
- kind: "gone",
1438
- verification,
1439
- diagnostic: trajectory(`the registered process is ${verification}`)
1440
- };
1441
- if (verification !== "verified")
1442
- return unknownChild(`process verdict is ${verification}`);
1443
- if (location?.pane !== provenance.pane_id)
1444
- return unknownChild(
1445
- `registered on ${location?.pane ?? "no pane"}, not on ${provenance.pane_id}`
1446
- );
1447
- if (provenance.session_id === void 0)
1448
- return unknownChild("without a session this launch recorded for it");
1449
- if (contextSession !== provenance.session_id)
1450
- return unknownChild(
1451
- `publishing session ${contextSession ?? "none"}, not ${provenance.session_id}`
1452
- );
1453
- return {
1454
- kind: "live",
1455
- evidence: {
1456
- request_id: provenance.request_id,
1457
- registration_id: edge.bound.registration_id,
1458
- source: edge.bound.source,
1459
- incarnation: edge.bound.incarnation,
1460
- pane_id: edge.location.pane
1343
+ var PRE_EFFECT_CODES = [
1344
+ "absent",
1345
+ "unsupported",
1346
+ "bad_version",
1347
+ "bad_request",
1348
+ "bad_params",
1349
+ "unknown_method",
1350
+ "refused",
1351
+ "not_found",
1352
+ "busy",
1353
+ "backend_unavailable"
1354
+ ];
1355
+ function launchFailure(result, requestId) {
1356
+ return PRE_EFFECT_CODES.includes(result.code) ? refusedLaunch(result.message) : unresolvedLaunch(requestId, result.message);
1357
+ }
1358
+ async function waitForChild(client, requestId, options) {
1359
+ let last = "the daemon has not answered";
1360
+ for (; ; ) {
1361
+ if (options.signal?.aborted)
1362
+ return unresolvedLaunch(requestId, `the launch was cancelled (${last})`);
1363
+ const answer = await client.spawnGet(requestId);
1364
+ if (answer.ok === true) {
1365
+ const { edge } = answer.value;
1366
+ if (edge.withdrawn !== void 0)
1367
+ return refusedLaunch(`the daemon withdrew this launch: ${edge.withdrawn.reason}`);
1368
+ if (edge.granted_at === void 0) {
1369
+ await delay(options.pollMs);
1370
+ continue;
1371
+ }
1372
+ const pane = edge.location?.pane;
1373
+ if (pane === void 0 || pane === "")
1374
+ return unresolvedLaunch(
1375
+ requestId,
1376
+ "the daemon's launch recorded no pane to verify the child against"
1377
+ );
1378
+ const agentId = edge.bound?.registration_id;
1379
+ if (agentId !== void 0 && isUuid(agentId)) {
1380
+ const subjectId = edge.bound?.subject_id;
1381
+ if (subjectId === void 0 || !isUuid(subjectId))
1382
+ return unresolvedLaunch(
1383
+ requestId,
1384
+ `the daemon bound the child ${agentId} without recording its subject`
1385
+ );
1386
+ return {
1387
+ kind: "bound",
1388
+ subject_id: subjectId,
1389
+ pane_id: pane
1390
+ };
1391
+ }
1392
+ last = "no child has registered with this launch's token yet";
1393
+ } else {
1394
+ return unresolvedLaunch(requestId, answer.message);
1461
1395
  }
1462
- };
1396
+ await delay(options.pollMs);
1397
+ }
1463
1398
  }
1464
1399
 
1465
1400
  // extension/radar-children.ts
1466
- var PAGE = 100;
1467
- var MAX_PAGES = 20;
1468
1401
  function createRadarChildren(client, publication) {
1469
- const held = /* @__PURE__ */ new Map();
1470
- async function resolveAll() {
1471
- const newest = /* @__PURE__ */ new Map();
1472
- let after;
1473
- for (let page = 0; page < MAX_PAGES; page++) {
1474
- const read = await client.spawnList({ ...after !== void 0 ? { after } : {}, limit: PAGE });
1475
- if (read.ok === false) return read;
1476
- for (const edge of read.value.spawns) {
1477
- const bound = edge.bound;
1478
- if (!bound?.subject_id || !bound.registration_id || edge.launched !== "completed") continue;
1479
- const at = Date.parse(bound.bound_at);
1480
- if (Number.isNaN(at)) continue;
1481
- const current = newest.get(bound.subject_id);
1482
- if (current !== void 0 && current.at >= at) continue;
1483
- newest.set(bound.subject_id, {
1484
- at,
1485
- registration_id: bound.registration_id,
1486
- child: {
1487
- subject_id: bound.subject_id,
1488
- request_id: edge.request_id,
1489
- ...edge.location?.pane ? { pane_id: edge.location.pane } : {},
1490
- state: edge.state,
1491
- exited: edge.exit_observation != null && edge.exit_observation.state !== "unknown"
1492
- }
1493
- });
1494
- }
1495
- if (!read.value.next) return { ok: true, value: newest };
1496
- after = read.value.next;
1497
- }
1498
- return { ok: false, code: "protocol", message: "the caller's launches exceed the readable pages" };
1499
- }
1500
- async function resolve2(subject) {
1501
- const all = await resolveAll();
1502
- return all.ok ? all.value.get(subject) : void 0;
1503
- }
1504
- async function resolveAndFollow(subject) {
1505
- const all = await resolveAll();
1506
- if (!all.ok) return void 0;
1507
- follow(all.value);
1508
- return all.value.get(subject);
1509
- }
1510
- function follow(all) {
1511
- for (const [subject, entry] of held) {
1512
- const target = all.get(subject);
1513
- if (target !== void 0 && entry.published !== target.registration_id)
1514
- entry.publish?.(target);
1515
- }
1516
- }
1517
1402
  return {
1518
- async list() {
1519
- const all = await resolveAll();
1520
- if (!all.ok) return all;
1521
- follow(all.value);
1522
- return { ok: true, value: [...all.value.values()].map((entry) => entry.child) };
1403
+ async list(selection) {
1404
+ const children = [];
1405
+ let after;
1406
+ do {
1407
+ const page = await client.subjectChildren({ selection, after, limit: 100 });
1408
+ if (!page.ok) return page;
1409
+ children.push(...page.value.subjects);
1410
+ if (page.value.next === after)
1411
+ return { ok: false, code: "protocol", message: "child page did not advance" };
1412
+ after = page.value.next ?? void 0;
1413
+ } while (after !== void 0);
1414
+ return { ok: true, value: children };
1523
1415
  },
1524
- assignment(subject, owner) {
1525
- const existing = held.get(subject);
1526
- if (existing !== void 0) return existing.writer;
1527
- let latest;
1528
- const entry = { writer: void 0 };
1529
- const send = (target) => {
1530
- if (latest === void 0) return;
1531
- if (entry.published !== void 0 && entry.published !== target.registration_id)
1532
- publication.retireAssignment(entry.published);
1533
- entry.published = target.registration_id;
1534
- publication.assignment(target.registration_id, owner).update(latest);
1535
- };
1536
- entry.publish = send;
1537
- entry.writer = {
1538
- update(snapshot) {
1539
- latest = snapshot;
1540
- void resolve2(subject).then((target) => {
1541
- if (target !== void 0) send(target);
1542
- });
1543
- },
1544
- retire() {
1545
- latest = void 0;
1546
- if (entry.published !== void 0) publication.retireAssignment(entry.published);
1547
- entry.published = void 0;
1548
- }
1549
- };
1550
- held.set(subject, entry);
1551
- return entry.writer;
1552
- },
1553
- retire(subject) {
1554
- held.get(subject)?.writer.retire();
1555
- held.delete(subject);
1556
- },
1557
- async observe(subject, session_id) {
1558
- const target = await resolveAndFollow(subject);
1559
- if (target === void 0)
1560
- return { kind: "unknown", diagnostic: "the daemon records no bound launch for this child" };
1561
- return observeRadarChild(client, {
1562
- request_id: target.child.request_id,
1563
- pane_id: target.child.pane_id,
1564
- registration_id: target.registration_id,
1565
- session_id
1416
+ async restart(subject, program, options) {
1417
+ const parent = await publication.subject();
1418
+ if (parent?.subject_id === void 0)
1419
+ return { kind: "failed", message: "this process has no granted subject" };
1420
+ const request_id = randomUUID2();
1421
+ const requested = await client.restart({ request_id, parent: parent.registration_id, subject_id: subject, ...program });
1422
+ if (!requested.ok) return launchFailure(requested, request_id);
1423
+ const bound = await waitForChild(client, requested.value.launch_request_id, {
1424
+ pollMs: options?.pollMs ?? 250,
1425
+ ...options?.signal !== void 0 ? { signal: options.signal } : {}
1566
1426
  });
1427
+ if (bound.kind !== "bound") return bound;
1428
+ return { kind: "ready", value: { request_id: requested.value.launch_request_id, subject_id: bound.subject_id, pane_id: bound.pane_id } };
1567
1429
  },
1568
- async inspect(subject, session_id) {
1569
- const target = await resolveAndFollow(subject);
1570
- if (target?.child.pane_id === void 0)
1571
- throw new Error("Inspection target is not a bound child with a pane");
1572
- return inspectRadarChild(client, {
1573
- agentId: target.registration_id,
1574
- paneId: target.child.pane_id,
1575
- ...session_id !== void 0 ? { session: session_id } : {}
1576
- });
1430
+ assignment: (subject, owner) => publication.assignment(subject, owner),
1431
+ retire: (subject) => publication.retireAssignment(subject),
1432
+ async observe(subject) {
1433
+ const read = await client.childGet(subject);
1434
+ if (!read.ok) return { kind: "unknown", diagnostic: `${read.code}: ${read.message}` };
1435
+ if (read.value.state === "live") return { kind: "live", subject: read.value };
1436
+ if (read.value.state === "detached") return { kind: "gone", subject: read.value };
1437
+ return { kind: "unknown", diagnostic: `child attachment is ${read.value.state}` };
1438
+ },
1439
+ async inspect(subject) {
1440
+ const read = await client.childInspect(subject);
1441
+ if (!read.ok) throw new Error(`Child inspection unavailable (${read.code}: ${read.message})`);
1442
+ return read.value;
1577
1443
  }
1578
1444
  };
1579
1445
  }
@@ -1616,7 +1482,7 @@ function createRadarHandoffPort(client, publication, parentSubjectId = () => pro
1616
1482
  }
1617
1483
 
1618
1484
  // extension/radar-publication.ts
1619
- import { createHash, randomUUID as randomUUID2 } from "node:crypto";
1485
+ import { createHash, randomUUID as randomUUID3 } from "node:crypto";
1620
1486
  import {
1621
1487
  closeSync,
1622
1488
  constants,
@@ -1746,7 +1612,7 @@ function createPublicationStore(root) {
1746
1612
  ensureDirectory(directory);
1747
1613
  const temporary = join2(
1748
1614
  directory,
1749
- `.${name.slice(name.lastIndexOf("/") + 1)}.${randomUUID2()}.tmp`
1615
+ `.${name.slice(name.lastIndexOf("/") + 1)}.${randomUUID3()}.tmp`
1750
1616
  );
1751
1617
  const bytes = Buffer.from(JSON.stringify(value), "utf8");
1752
1618
  if (bytes.length > MAX_RECORD_BYTES)
@@ -1802,7 +1668,7 @@ function radarProcessSlot() {
1802
1668
  return host[PROCESS_SLOT];
1803
1669
  }
1804
1670
  function radarProcessIncarnation(slot = radarProcessSlot()) {
1805
- return slot.incarnation ??= randomUUID2();
1671
+ return slot.incarnation ??= randomUUID3();
1806
1672
  }
1807
1673
  function recordAt2(value, key) {
1808
1674
  if (typeof value !== "object" || value === null) return void 0;
@@ -1959,15 +1825,18 @@ function createChannelWriter(input) {
1959
1825
  function blank(agentId) {
1960
1826
  return {
1961
1827
  version: RADAR_PUBLICATION_VERSION,
1962
- registration_id: agentId,
1828
+ target_id: agentId,
1963
1829
  channel,
1964
1830
  publisher,
1965
1831
  sequence: 0
1966
1832
  };
1967
1833
  }
1834
+ function addressed(target) {
1835
+ return channel === "assignment" ? { subject_id: target } : { registration_id: target };
1836
+ }
1968
1837
  function loadRecord() {
1969
1838
  const value = store.read(recordName);
1970
- const agentId = textAt(value, "registration_id");
1839
+ const agentId = textAt(value, "target_id");
1971
1840
  const storedPublisher = recordAt2(value, "publisher");
1972
1841
  if (numberAt2(value, "version") !== RADAR_PUBLICATION_VERSION || agentId === void 0 || !isUuid(agentId) || textAt(value, "channel") !== channel || textAt(storedPublisher, "source") !== publisher.source || textAt(storedPublisher, "incarnation") !== publisher.incarnation || textAt(storedPublisher, "reporting_owner") !== publisher.reporting_owner)
1973
1842
  return blank("");
@@ -1982,7 +1851,7 @@ function createChannelWriter(input) {
1982
1851
  const sentAt = numberAt2(acceptedValue, "sent_at");
1983
1852
  return {
1984
1853
  version: RADAR_PUBLICATION_VERSION,
1985
- registration_id: agentId,
1854
+ target_id: agentId,
1986
1855
  channel,
1987
1856
  publisher,
1988
1857
  ...handle !== void 0 && generation !== void 0 ? { writer: { handle, generation } } : {},
@@ -2023,15 +1892,11 @@ function createChannelWriter(input) {
2023
1892
  if (record.writer !== void 0) return true;
2024
1893
  const agentId = await input.subject(recovering);
2025
1894
  if (agentId === void 0) return false;
2026
- if (record.registration_id !== agentId) {
1895
+ if (record.target_id !== agentId) {
2027
1896
  record = blank(agentId);
2028
1897
  save();
2029
1898
  }
2030
- const answer = await client.acquire({
2031
- registration_id: agentId,
2032
- channel,
2033
- publisher
2034
- });
1899
+ const answer = await client.acquire({ ...addressed(agentId), channel, publisher });
2035
1900
  if (answer.ok === false) {
2036
1901
  if (answer.code === "not_found") {
2037
1902
  recovering = true;
@@ -2058,7 +1923,7 @@ function createChannelWriter(input) {
2058
1923
  const writer = record.writer;
2059
1924
  if (writer === void 0) return;
2060
1925
  const answer = await client.publish({
2061
- registration_id: record.registration_id,
1926
+ ...addressed(record.target_id),
2062
1927
  channel,
2063
1928
  writer_handle: writer.handle,
2064
1929
  sequence: pending.sequence,
@@ -2152,9 +2017,9 @@ function createChannelWriter(input) {
2152
2017
  stopPump();
2153
2018
  const writer = record.writer;
2154
2019
  if (writer === void 0) return;
2155
- const agentId = record.registration_id;
2020
+ const agentId = record.target_id;
2156
2021
  try {
2157
- void client.retire({ registration_id: agentId, channel, writer_handle: writer.handle }).then((answer) => {
2022
+ void client.retire({ ...addressed(agentId), channel, writer_handle: writer.handle }).then((answer) => {
2158
2023
  if (answer.ok === true) {
2159
2024
  try {
2160
2025
  record.retired_at = new Date(scheduler.now()).toISOString();
@@ -2244,6 +2109,7 @@ function createPresenceReporter(input) {
2244
2109
  }
2245
2110
  claim.registration_id = answer.value.registration_id;
2246
2111
  claim.subject_id = answer.value.standing === "granted" ? answer.value.subject_id ?? void 0 : void 0;
2112
+ if (claim.subject_id !== void 0) input.granted?.(claim.subject_id);
2247
2113
  standing = answer.value.standing === "granted" ? { state: "managed", subject_id: claim.subject_id } : answer.value.standing === "duplicate" ? { state: "duplicate" } : answer.value.standing === "pending" ? { state: "pending" } : { state: "unmanaged" };
2248
2114
  diagnostic = answer.value.standing === "duplicate" ? DUPLICATE_SESSION_DIAGNOSTIC : void 0;
2249
2115
  save();
@@ -2267,7 +2133,7 @@ function createPresenceReporter(input) {
2267
2133
  releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
2268
2134
  }
2269
2135
  standing = void 0;
2270
- state = { version: 2, session, claim_id: randomUUID2(), releases };
2136
+ state = { version: 2, session, claim_id: randomUUID3(), releases };
2271
2137
  save();
2272
2138
  stopped = false;
2273
2139
  void ensure();
@@ -2304,7 +2170,7 @@ var STORE_IDENTITY = /* @__PURE__ */ new WeakMap();
2304
2170
  function storeIdentity(store) {
2305
2171
  const known = STORE_IDENTITY.get(store);
2306
2172
  if (known !== void 0) return known;
2307
- const created = randomUUID2();
2173
+ const created = randomUUID3();
2308
2174
  STORE_IDENTITY.set(store, created);
2309
2175
  return created;
2310
2176
  }
@@ -2425,7 +2291,7 @@ function createRadarPublicationInner(options) {
2425
2291
  agentId = registered;
2426
2292
  processRecord = { ...record, registration_id: agentId };
2427
2293
  store.write(registrationName, processRecord);
2428
- if (options.binding !== void 0) writeBinding(options.binding);
2294
+ if (options.binding !== void 0) writeBinding(options.binding, grantedSubject());
2429
2295
  return agentId;
2430
2296
  })();
2431
2297
  try {
@@ -2434,11 +2300,16 @@ function createRadarPublicationInner(options) {
2434
2300
  subjectReady = void 0;
2435
2301
  }
2436
2302
  }
2437
- function writeBinding(binding) {
2303
+ function grantedSubject() {
2304
+ const standing = presence.management();
2305
+ return standing?.state === "managed" ? standing.subject_id : void 0;
2306
+ }
2307
+ function writeBinding(binding, subjectId) {
2438
2308
  if (agentId === void 0) return;
2439
2309
  store.write(`bindings/${binding.key}.json`, {
2440
2310
  version: RADAR_PUBLICATION_VERSION,
2441
2311
  registration_id: agentId,
2312
+ ...subjectId !== void 0 ? { subject_id: subjectId } : {},
2442
2313
  incarnation,
2443
2314
  run: binding.run,
2444
2315
  owner: binding.owner,
@@ -2478,6 +2349,9 @@ function createRadarPublicationInner(options) {
2478
2349
  ...launchRegistrationId ? { registration_id: launchRegistrationId } : {}
2479
2350
  };
2480
2351
  },
2352
+ granted: (subjectId) => {
2353
+ if (options.binding !== void 0) writeBinding(options.binding, subjectId);
2354
+ },
2481
2355
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
2482
2356
  });
2483
2357
  const facts = createChannelWriter({
@@ -2509,8 +2383,7 @@ function createRadarPublicationInner(options) {
2509
2383
  channel: "assignment",
2510
2384
  publisher: publisher("herdsman-owner", reportingOwner),
2511
2385
  codec: assignmentCodec,
2512
- // The owner publishes about a subject it did not register: the exact
2513
- // registration_id from the child's sidecar is the whole correlation.
2386
+ // Radar owns this assignment across the child's process replacements.
2514
2387
  subject: async () => target,
2515
2388
  leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
2516
2389
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
@@ -2531,16 +2404,18 @@ function createRadarPublicationInner(options) {
2531
2404
  return void 0;
2532
2405
  }
2533
2406
  const bound = textAt(value, "registration_id");
2407
+ const subjectId = textAt(value, "subject_id");
2534
2408
  const boundIncarnation = textAt(value, "incarnation");
2535
2409
  const run = textAt(value, "run");
2536
2410
  const owner = textAt(value, "owner");
2537
2411
  const label = textAt(value, "label");
2538
2412
  const observedAt = textAt(value, "observed_at");
2539
- if (!isUuid(bound) || !isUuid(boundIncarnation) || run === void 0 || owner === void 0 || label === void 0 || observedAt === void 0)
2413
+ if (!isUuid(bound) || subjectId !== void 0 && !isUuid(subjectId) || !isUuid(boundIncarnation) || run === void 0 || owner === void 0 || label === void 0 || observedAt === void 0)
2540
2414
  return void 0;
2541
2415
  return {
2542
2416
  version: RADAR_PUBLICATION_VERSION,
2543
2417
  registration_id: bound,
2418
+ ...subjectId !== void 0 ? { subject_id: subjectId } : {},
2544
2419
  incarnation: boundIncarnation,
2545
2420
  run,
2546
2421
  owner,
@@ -2825,9 +2700,116 @@ function createRadarExecutionAdapter(pi, options) {
2825
2700
  };
2826
2701
  }
2827
2702
 
2703
+ // extension/radar-location.ts
2704
+ import { basename } from "node:path";
2705
+ function radarLocalPane(env) {
2706
+ const pane = env.TMUX_PANE?.trim();
2707
+ return pane && /^%\d+$/.test(pane) ? pane : void 0;
2708
+ }
2709
+ function tmuxServerName(env) {
2710
+ const socket = env.TMUX?.trim().split(",")[0];
2711
+ return socket ? basename(socket) || void 0 : void 0;
2712
+ }
2713
+ function qualifiedOwnPane(env, backend, instance) {
2714
+ if (backend !== "tmux")
2715
+ return {
2716
+ absent: `the daemon serves the ${backend} backend, and Herdsman drives only tmux`
2717
+ };
2718
+ if (instance === void 0 || instance === "")
2719
+ return {
2720
+ pending: "the daemon did not name the tmux server it addresses"
2721
+ };
2722
+ const server = tmuxServerName(env);
2723
+ if (server === void 0)
2724
+ return {
2725
+ absent: "this process's own tmux client names no server, so it is not in the pane it would publish"
2726
+ };
2727
+ if (server !== instance)
2728
+ return {
2729
+ absent: `this process is attached to tmux server ${server}, not to ${instance}`
2730
+ };
2731
+ const pane = radarLocalPane(env);
2732
+ if (pane === void 0)
2733
+ return {
2734
+ absent: "this process's own environment names no tmux pane of that server"
2735
+ };
2736
+ return { pane: `tmux:${instance}:${pane}` };
2737
+ }
2738
+ function observedContainment(inventory, paneId) {
2739
+ const rows = inventory.panes.filter((pane) => pane.pane_id === paneId);
2740
+ if (rows.length === 0) return void 0;
2741
+ if (rows.length > 1) return { ambiguous: `topology reports pane ${paneId} twice` };
2742
+ const row = rows[0];
2743
+ const tab = inventory.tabs.find((candidate) => candidate.tab_id === row.tab_id);
2744
+ if (tab !== void 0 && tab.workspace_id !== row.workspace_id)
2745
+ return {
2746
+ ambiguous: `pane ${paneId} is reported in ${row.workspace_id}, but its tab belongs to ${tab.workspace_id}`
2747
+ };
2748
+ return { pane: row };
2749
+ }
2750
+ async function resolveRadarProcessLocation(client, env = process.env) {
2751
+ let ready;
2752
+ try {
2753
+ ready = await client.ping();
2754
+ } catch {
2755
+ return { state: "pending", reason: "the daemon did not answer this process" };
2756
+ }
2757
+ if (ready.ok === false)
2758
+ return {
2759
+ state: "pending",
2760
+ reason: `the daemon did not answer this process (${ready.code})`
2761
+ };
2762
+ const backend = ready.value.backend;
2763
+ if (backend === void 0 || backend === "none")
2764
+ return { state: "absent", reason: "the daemon serves no physical backend" };
2765
+ const instance = ready.value.instance ?? void 0;
2766
+ const own = qualifiedOwnPane(env, backend, instance);
2767
+ if ("absent" in own) return { state: "absent", reason: own.absent };
2768
+ if ("pending" in own) return { state: "pending", reason: own.pending };
2769
+ const paneId = own.pane;
2770
+ if (paneId === env.PI_HERDSMAN_PARENT_PANE_ID?.trim())
2771
+ return {
2772
+ state: "absent",
2773
+ reason: "this process is in the pane its launch was created beside, not in a pane of its own"
2774
+ };
2775
+ let inventory;
2776
+ try {
2777
+ inventory = await client.observe();
2778
+ } catch {
2779
+ return {
2780
+ state: "pending",
2781
+ reason: "the daemon's topology could not be read for this process's pane"
2782
+ };
2783
+ }
2784
+ if (inventory.ok === false)
2785
+ return {
2786
+ state: "pending",
2787
+ reason: `the daemon's topology could not be read (${inventory.code})`
2788
+ };
2789
+ const containment = observedContainment(inventory.value, paneId);
2790
+ if (containment === void 0)
2791
+ return {
2792
+ state: "pending",
2793
+ reason: `the daemon's topology does not report pane ${paneId} yet`
2794
+ };
2795
+ if ("ambiguous" in containment)
2796
+ return { state: "pending", reason: containment.ambiguous };
2797
+ const row = containment.pane;
2798
+ return {
2799
+ state: "resolved",
2800
+ location: {
2801
+ backend,
2802
+ ...instance !== void 0 ? { instance } : {},
2803
+ workspace: row.workspace_id,
2804
+ tab: row.tab_id,
2805
+ pane: paneId
2806
+ }
2807
+ };
2808
+ }
2809
+
2828
2810
  // extension/radar-runtime.ts
2829
2811
  var RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
2830
- var RADAR_RUNTIME_VERSION = 3;
2812
+ var RADAR_RUNTIME_VERSION = 5;
2831
2813
  function lazyRadarRuntime(start) {
2832
2814
  let pending;
2833
2815
  return {