pi-radar 0.6.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +15 -1
  2. package/dist/extension/baseline.js +395 -125
  3. package/dist/extension/radar-children.js +240 -0
  4. package/dist/extension/radar-client.js +42 -8
  5. package/dist/extension/radar-execution.js +11 -10
  6. package/dist/extension/radar-launch.js +23 -24
  7. package/dist/extension/radar-publication.js +10 -9
  8. package/dist/extension/radar-runtime.js +1 -1
  9. package/dist/index.js +1772 -1615
  10. package/extension/baseline.ts +2 -0
  11. package/extension/generated-contracts.ts +12 -3
  12. package/extension/radar-children.ts +176 -0
  13. package/extension/radar-client.ts +60 -6
  14. package/extension/radar-execution.ts +5 -5
  15. package/extension/radar-launch.ts +42 -63
  16. package/extension/radar-publication.ts +12 -13
  17. package/extension/radar-runtime.ts +4 -1
  18. package/index.ts +1 -0
  19. package/package.json +8 -3
  20. package/schema/public/v1/observation-get-response.deserialize.schema.json +9 -1
  21. package/schema/public/v1/observation-get-response.serialize.schema.json +9 -1
  22. package/schema/public/v1/observation-snapshot.deserialize.schema.json +9 -1
  23. package/schema/public/v1/observation-snapshot.serialize.schema.json +9 -1
  24. package/schema/public/v1/presence-answer.deserialize.schema.json +1 -0
  25. package/schema/public/v1/presence-answer.serialize.schema.json +1 -0
  26. package/schema/public/v1/presence-report.deserialize.schema.json +0 -7
  27. package/schema/public/v1/presence-report.serialize.schema.json +0 -8
  28. package/schema/public/v1/spawn-list-page.deserialize.schema.json +7 -0
  29. package/schema/public/v1/spawn-list-page.serialize.schema.json +7 -0
package/README.md CHANGED
@@ -19,9 +19,13 @@ The baseline reports session presence through one flat request. The first manage
19
19
 
20
20
  ## Daemon contract
21
21
 
22
+ **Managed execution:** a launch Radar admits arrives with `PI_RADAR_REGISTRATION_ID` in its environment. The baseline captures it, removes it from this process's environment before any child starts, and reports it as `registration_id`, so a managed launch binds the registration Radar already allocated instead of registering itself. A session claim is still fresh per load, and a reload or `/resume` takes a new one under the same registration; nothing about the process identity is presented or reused as a claim. `launch.admit` is the method `radar run` uses and is not part of the client surface. `agent.list` entries and observation agents carry `scoped`: `true` for a process Radar launched into a scope, `false` for an observed process it did not.
23
+
24
+ **0.7.0 upgrade:** update Radar (0.3.0) and pi-radar together; the floor is wire revision 6. A managed launch now carries `PI_RADAR_REGISTRATION_ID` instead of a claim id, and pi-radar always presents its own fresh claim. Nothing in your code changes unless you read the launch environment directly.
25
+
22
26
  **0.6.0 upgrade:** update Radar and pi-radar together. Lifecycle controls require both `subject_id` and `claim_id`; handoff identities and spawn edges use `claim_id`, with no execution generation. Do not pass an agent's subject id where an attempt is required. Existing private launch records remain readable.
23
27
 
24
- pi-radar 0.6.0 speaks protocol 2 and requires wire revision 5. Revision 4 added Pi presence (`agent.presence`/`agent.release`), which is how a session acquires its subject. Revision 5 names the agent `subject_id` and one attempt `claim_id`, drops the execution-generation ordinal, and requires lifecycle controls to name the claim they were issued against, so a revision-4 client would read those fields with the old meanings: the floor is 5. Handoff methods additionally require the independently negotiated `communication.handoff.v1` contract; its timestamps remain exactly representable as JSON numbers. Clients that control managed executions must not target the legacy per-execution ID semantics. It reads `revision` and the public `contracts` list from `daemon.ping`, preserves unfamiliar contract names, and preflights only contracts needed for a multi-step or versioned operation. Backend `mux.*` primitives are private to the daemon; direct mux methods are dispatched and return the daemon's backend refusal when unavailable.
28
+ pi-radar 0.7.0 speaks protocol 2 and requires wire revision 6. Revision 6 gives a managed launch its process identity up front: Radar mints the `registration_id`, passes it as `PI_RADAR_REGISTRATION_ID`, and holds a `reserved` claim that grants nothing until the first verified report replaces it with the session's own claim, so a 0.6.0 client, which spends the spawn token as its first claim, cannot bind. Revision 4 added Pi presence (`agent.presence`/`agent.release`), which is how a session acquires its subject. Revision 5 names the agent `subject_id` and one attempt `claim_id`, drops the execution-generation ordinal, and requires lifecycle controls to name the claim they were issued against, so a revision-4 client would read those fields with the old meanings. Handoff methods additionally require the independently negotiated `communication.handoff.v1` contract; its timestamps remain exactly representable as JSON numbers. Clients that control managed executions must not target the legacy per-execution ID semantics. It reads `revision` and the public `contracts` list from `daemon.ping`, preserves unfamiliar contract names, and preflights only contracts needed for a multi-step or versioned operation. Backend `mux.*` primitives are private to the daemon; direct mux methods are dispatched and return the daemon's backend refusal when unavailable.
25
29
 
26
30
  The client uses `registry.v1` for registry operations and additionally requires `registry.facts.v1` for the facts channel and assignment extensions. Managed children require `spawn.v1`, child-pane removal requires `spawn.close.v1`, event reads require `events.v1`, and handoff operations require `communication.handoff.v1`. The expected wire examples are maintained in this repository's `docs/control-plane.fixture.jsonl` and `docs/agent-registration.fixture.jsonl` and are exercised by the package tests.
27
31
 
@@ -31,6 +35,16 @@ The client uses `registry.v1` for registry operations and additionally requires
31
35
 
32
36
  The handoff API stores immutable bounded content separately from the recipient receipt. `inbox()` returns pending IDs only; retrieve each payload with `get()` and persist it before `ack()`, so a batch cannot exceed the RPC response bound. A worker's first Herdsman handoff is addressed only to the stable `parent_subject_id` recorded by Radar for its accepted managed launch. The parent gets its stable subject ID from `RadarSubject` and supplies it as `RadarWorkerLaunch.parent.subject_id`; `launchRadarWorker` forwards it as `managed.parent_subject_id`, and Radar injects the accepted value into the worker as `PI_RADAR_MANAGED_PARENT_SUBJECT_ID`. The handoff port reads that process context; callers cannot choose a destination. The daemon checks it against the durable parent record. Do not substitute a registration ID, Pi session, owner-session field, pane, or inferred lineage; unmanaged/session routing is deferred. The owner must durably capture the payload locally before calling `ack`; acknowledgement does not settle Herdsman assignment state or authorize worker closure. Pending inbox scans recover work after daemon restart or missed pointer events, and acknowledged content remains retrievable during the retention window. See `docs/control-plane.md` for wire shapes, bounds and recovery semantics.
33
37
 
38
+ ## Launching a worker
39
+
40
+ `launchRadarWorker` takes the parent from the caller's own `publication`, so a consumer passes the pane it splits and no registration or subject for the parent. The result names the child by its `subject_id`, the key that stays the same when the child restarts or resumes; keep that as the child's identifier. A received handoff carries its sender's `subject_id`; match children by it.
41
+
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
+
44
+ ## Addressing children by subject
45
+
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.
47
+
34
48
  ## Generated protocol contracts
35
49
 
36
50
  The selected Rust wire DTOs are the source for the draft 2020-12 schemas in `schema/public/v1/`, shipped and exported under `pi-radar/schema/`. They are this package's contract of record: a daemon change lands as a schema regeneration, and the method-to-schema index lives in `docs/agent-registration.md` in the Radar repository. Serialize and deserialize schemas are separate because Serde defaults, null handling and omission differ by direction. The SDK's ping, Pi presence report/answer/release, handoff inbox, publication-parameter, immutable handoff, receipt, managed-launch-metadata input, facts-input, spawn-exit, and observation request/snapshot/response types are generated into `extension/generated-contracts.ts`; do not edit that file by hand.
@@ -8,12 +8,13 @@ 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 = 5;
11
+ var RADAR_MINIMUM_REVISION = 7;
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
15
  var RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
16
16
  var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
17
+ var RADAR_LIFECYCLE_RESTART_CONTRACT = "lifecycle.restart.v1";
17
18
  var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
18
19
  var RADAR_EVENTS_CONTRACT = "events.v1";
19
20
  var RADAR_COMMUNICATION_CONTRACT = "communication.handoff.v1";
@@ -119,7 +120,7 @@ function decodePresenceAnswer(value) {
119
120
  const claimId = recordAt(value, "claim_id");
120
121
  const standing = recordAt(value, "standing");
121
122
  const subjectId = recordAt(value, "subject_id");
122
- if (!isUuid(registrationId) || !isUuid(claimId) || !["pending", "granted", "duplicate", "released"].includes(String(standing)) || subjectId !== void 0 && !isUuid(subjectId) || standing === "granted" && !isUuid(subjectId))
123
+ if (!isUuid(registrationId) || !isUuid(claimId) || !["reserved", "pending", "granted", "duplicate", "released"].includes(String(standing)) || subjectId !== void 0 && !isUuid(subjectId) || standing === "granted" && !isUuid(subjectId))
123
124
  return protocol("presence reply carries no valid claim standing");
124
125
  return { ok: true, value };
125
126
  }
@@ -264,7 +265,7 @@ function unresolvedOperation(requestId, detail) {
264
265
  message: `focus ${requestId} is unresolved (${detail})`
265
266
  };
266
267
  }
267
- var RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
268
+ var RADAR_REGISTRATION_ENV = "PI_RADAR_REGISTRATION_ID";
268
269
  var RADAR_MANAGED_IDENTITY_ENV = /* @__PURE__ */ new Set([
269
270
  "PI_RADAR_MANAGED_SUBJECT_ID",
270
271
  "PI_RADAR_MANAGED_LAUNCH_ID",
@@ -315,6 +316,7 @@ function spawnEdge(value) {
315
316
  const source = stringAt(bound, "source");
316
317
  const incarnation = stringAt(bound, "incarnation");
317
318
  const agentId = stringAt(bound, "registration_id");
319
+ const subjectId = stringAt(bound, "subject_id");
318
320
  const exit = exitAt(recordAt(value, "exit_observation"));
319
321
  return {
320
322
  ok: true,
@@ -328,7 +330,8 @@ function spawnEdge(value) {
328
330
  bound: {
329
331
  source,
330
332
  incarnation,
331
- ...isUuid(agentId) ? { registration_id: agentId } : {}
333
+ ...isUuid(agentId) ? { registration_id: agentId } : {},
334
+ ...isUuid(subjectId) ? { subject_id: subjectId } : {}
332
335
  }
333
336
  } : {},
334
337
  ...exit !== void 0 ? { exit_observation: exit } : {}
@@ -400,6 +403,8 @@ function spawnTopology(value) {
400
403
  function spawnRequestShape(request) {
401
404
  if (!isUuid(request.request_id))
402
405
  return "spawn requires a canonical request UUID";
406
+ if (request.continue_subject_id !== void 0 && !isUuid(request.continue_subject_id))
407
+ return "continue_subject_id must be a canonical UUID";
403
408
  if (!isUuid(request.parent))
404
409
  return "spawn requires the parent's canonical agent UUID";
405
410
  if (!isUuid(request.parent_session_id))
@@ -413,8 +418,8 @@ function spawnRequestShape(request) {
413
418
  const env = Object.entries(request.env ?? {});
414
419
  if (env.length > 256) return "spawn env names more than 256 entries";
415
420
  for (const [name, value] of env) {
416
- if (name === RADAR_SPAWN_TOKEN_ENV)
417
- return `spawn env cannot set the reserved ${RADAR_SPAWN_TOKEN_ENV}`;
421
+ if (name === RADAR_REGISTRATION_ENV)
422
+ return `spawn env cannot set the reserved ${RADAR_REGISTRATION_ENV}`;
418
423
  if (RADAR_MANAGED_IDENTITY_ENV.has(name))
419
424
  return `spawn env cannot set Radar-issued identity variable ${name}`;
420
425
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || new RegExp("\\p{Cc}", "u").test(value))
@@ -808,7 +813,8 @@ function createRadarClient(options = {}) {
808
813
  argv: request.argv,
809
814
  ...request.cwd !== void 0 ? { cwd: request.cwd } : {},
810
815
  ...carriesContext && request.env !== void 0 ? { env: request.env } : {},
811
- ...request.managed !== void 0 ? { managed: request.managed } : {}
816
+ ...request.managed !== void 0 ? { managed: request.managed } : {},
817
+ ...request.continue_subject_id !== void 0 ? { continue_subject_id: request.continue_subject_id } : {}
812
818
  },
813
819
  requester
814
820
  },
@@ -836,6 +842,33 @@ function createRadarClient(options = {}) {
836
842
  }
837
843
  );
838
844
  },
845
+ async restart(request) {
846
+ if (!isUuid(request.request_id) || !isUuid(request.parent) || !isUuid(request.subject_id))
847
+ return {
848
+ ok: false,
849
+ code: "bad_params",
850
+ message: "restart requires canonical request, parent and subject UUIDs"
851
+ };
852
+ return withContract(RADAR_LIFECYCLE_RESTART_CONTRACT, async () => {
853
+ const answer = await call(
854
+ "lifecycle.restart",
855
+ {
856
+ request: {
857
+ subject_id: request.subject_id,
858
+ parent: request.parent,
859
+ executable: request.executable,
860
+ argv: request.argv,
861
+ ...request.cwd !== void 0 ? { cwd: request.cwd } : {},
862
+ ...request.env !== void 0 ? { env: request.env } : {}
863
+ },
864
+ requester
865
+ },
866
+ request.request_id
867
+ );
868
+ if (answer.ok === false) return answer;
869
+ return focusOutcome(recordAt(answer.value, "request"), request.request_id);
870
+ });
871
+ },
839
872
  async spawnList(request) {
840
873
  const registrationId = processRegistrationId;
841
874
  if (registrationId === void 0)
@@ -1203,6 +1236,348 @@ function boundedRadarSnapshot(snapshot) {
1203
1236
  };
1204
1237
  }
1205
1238
 
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
+ }
1280
+
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
+ // 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
1461
+ }
1462
+ };
1463
+ }
1464
+
1465
+ // extension/radar-children.ts
1466
+ var PAGE = 100;
1467
+ var MAX_PAGES = 20;
1468
+ 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
+ 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) };
1523
+ },
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
1566
+ });
1567
+ },
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
+ });
1577
+ }
1578
+ };
1579
+ }
1580
+
1206
1581
  // extension/radar-communication.ts
1207
1582
  function createRadarHandoffPort(client, publication, parentSubjectId = () => process.env.PI_RADAR_MANAGED_PARENT_SUBJECT_ID) {
1208
1583
  async function agentId() {
@@ -1418,7 +1793,13 @@ function createRadarProcessSlot() {
1418
1793
  }
1419
1794
  function radarProcessSlot() {
1420
1795
  const host = globalThis;
1421
- return host[PROCESS_SLOT] ??= createRadarProcessSlot();
1796
+ if (!host[PROCESS_SLOT]) {
1797
+ const slot = createRadarProcessSlot();
1798
+ slot.launchRegistrationId = process.env.PI_RADAR_REGISTRATION_ID;
1799
+ delete process.env.PI_RADAR_REGISTRATION_ID;
1800
+ host[PROCESS_SLOT] = slot;
1801
+ }
1802
+ return host[PROCESS_SLOT];
1422
1803
  }
1423
1804
  function radarProcessIncarnation(slot = radarProcessSlot()) {
1424
1805
  return slot.incarnation ??= randomUUID2();
@@ -1848,11 +2229,6 @@ function createPresenceReporter(input) {
1848
2229
  await releasePending();
1849
2230
  const registration = await input.registration();
1850
2231
  if (!registration?.process || state !== claim || stopped) return void 0;
1851
- if (claim.initial_load) {
1852
- if (registration.spawn_token) claim.claim_id = registration.spawn_token;
1853
- claim.initial_load = false;
1854
- if (!save()) return void 0;
1855
- }
1856
2232
  const answer = await client.presence({
1857
2233
  ...registration,
1858
2234
  process: registration.process,
@@ -1891,7 +2267,7 @@ function createPresenceReporter(input) {
1891
2267
  releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
1892
2268
  }
1893
2269
  standing = void 0;
1894
- state = { version: 2, session, claim_id: randomUUID2(), initial_load: state === void 0, releases };
2270
+ state = { version: 2, session, claim_id: randomUUID2(), releases };
1895
2271
  save();
1896
2272
  stopped = false;
1897
2273
  void ensure();
@@ -1974,8 +2350,8 @@ function createRadarPublicationInner(options) {
1974
2350
  const store = options.store ?? createPublicationStore(dataRoot);
1975
2351
  const scheduler = options.scheduler ?? systemScheduler;
1976
2352
  const client = options.client;
1977
- const spawnToken = options.spawnToken;
1978
2353
  const slot = options.slot ?? radarProcessSlot();
2354
+ const launchRegistrationId = options.launchRegistrationId ?? slot.launchRegistrationId;
1979
2355
  const incarnation = options.incarnation ?? radarProcessIncarnation(slot);
1980
2356
  const cacheKey = `${options.store === void 0 ? dataRoot : storeIdentity(options.store)}\0${incarnation}`;
1981
2357
  const cached = slot.publications.get(cacheKey);
@@ -2099,7 +2475,7 @@ function createRadarPublicationInner(options) {
2099
2475
  return {
2100
2476
  ...identity,
2101
2477
  ...location?.state === "resolved" ? { location: location.location } : {},
2102
- ...spawnToken ? { spawn_token: spawnToken } : {}
2478
+ ...launchRegistrationId ? { registration_id: launchRegistrationId } : {}
2103
2479
  };
2104
2480
  },
2105
2481
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
@@ -2286,7 +2662,7 @@ function createRadarExecutionAdapter(pi, options) {
2286
2662
  },
2287
2663
  ...options.location !== void 0 ? { location: options.location } : {},
2288
2664
  ...managed !== void 0 ? { binding: managed } : {},
2289
- ...options.spawnToken !== void 0 ? { spawnToken: options.spawnToken } : {}
2665
+ ...options.launchRegistrationId !== void 0 ? { launchRegistrationId: options.launchRegistrationId } : {}
2290
2666
  });
2291
2667
  } catch {
2292
2668
  publication = unavailablePublication2();
@@ -2449,116 +2825,9 @@ function createRadarExecutionAdapter(pi, options) {
2449
2825
  };
2450
2826
  }
2451
2827
 
2452
- // extension/radar-location.ts
2453
- import { basename } from "node:path";
2454
- function radarLocalPane(env) {
2455
- const pane = env.TMUX_PANE?.trim();
2456
- return pane && /^%\d+$/.test(pane) ? pane : void 0;
2457
- }
2458
- function tmuxServerName(env) {
2459
- const socket = env.TMUX?.trim().split(",")[0];
2460
- return socket ? basename(socket) || void 0 : void 0;
2461
- }
2462
- function qualifiedOwnPane(env, backend, instance) {
2463
- if (backend !== "tmux")
2464
- return {
2465
- absent: `the daemon serves the ${backend} backend, and Herdsman drives only tmux`
2466
- };
2467
- if (instance === void 0 || instance === "")
2468
- return {
2469
- pending: "the daemon did not name the tmux server it addresses"
2470
- };
2471
- const server = tmuxServerName(env);
2472
- if (server === void 0)
2473
- return {
2474
- absent: "this process's own tmux client names no server, so it is not in the pane it would publish"
2475
- };
2476
- if (server !== instance)
2477
- return {
2478
- absent: `this process is attached to tmux server ${server}, not to ${instance}`
2479
- };
2480
- const pane = radarLocalPane(env);
2481
- if (pane === void 0)
2482
- return {
2483
- absent: "this process's own environment names no tmux pane of that server"
2484
- };
2485
- return { pane: `tmux:${instance}:${pane}` };
2486
- }
2487
- function observedContainment(inventory, paneId) {
2488
- const rows = inventory.panes.filter((pane) => pane.pane_id === paneId);
2489
- if (rows.length === 0) return void 0;
2490
- if (rows.length > 1) return { ambiguous: `topology reports pane ${paneId} twice` };
2491
- const row = rows[0];
2492
- const tab = inventory.tabs.find((candidate) => candidate.tab_id === row.tab_id);
2493
- if (tab !== void 0 && tab.workspace_id !== row.workspace_id)
2494
- return {
2495
- ambiguous: `pane ${paneId} is reported in ${row.workspace_id}, but its tab belongs to ${tab.workspace_id}`
2496
- };
2497
- return { pane: row };
2498
- }
2499
- async function resolveRadarProcessLocation(client, env = process.env) {
2500
- let ready;
2501
- try {
2502
- ready = await client.ping();
2503
- } catch {
2504
- return { state: "pending", reason: "the daemon did not answer this process" };
2505
- }
2506
- if (ready.ok === false)
2507
- return {
2508
- state: "pending",
2509
- reason: `the daemon did not answer this process (${ready.code})`
2510
- };
2511
- const backend = ready.value.backend;
2512
- if (backend === void 0 || backend === "none")
2513
- return { state: "absent", reason: "the daemon serves no physical backend" };
2514
- const instance = ready.value.instance ?? void 0;
2515
- const own = qualifiedOwnPane(env, backend, instance);
2516
- if ("absent" in own) return { state: "absent", reason: own.absent };
2517
- if ("pending" in own) return { state: "pending", reason: own.pending };
2518
- const paneId = own.pane;
2519
- if (paneId === env.PI_HERDSMAN_PARENT_PANE_ID?.trim())
2520
- return {
2521
- state: "absent",
2522
- reason: "this process is in the pane its launch was created beside, not in a pane of its own"
2523
- };
2524
- let inventory;
2525
- try {
2526
- inventory = await client.observe();
2527
- } catch {
2528
- return {
2529
- state: "pending",
2530
- reason: "the daemon's topology could not be read for this process's pane"
2531
- };
2532
- }
2533
- if (inventory.ok === false)
2534
- return {
2535
- state: "pending",
2536
- reason: `the daemon's topology could not be read (${inventory.code})`
2537
- };
2538
- const containment = observedContainment(inventory.value, paneId);
2539
- if (containment === void 0)
2540
- return {
2541
- state: "pending",
2542
- reason: `the daemon's topology does not report pane ${paneId} yet`
2543
- };
2544
- if ("ambiguous" in containment)
2545
- return { state: "pending", reason: containment.ambiguous };
2546
- const row = containment.pane;
2547
- return {
2548
- state: "resolved",
2549
- location: {
2550
- backend,
2551
- ...instance !== void 0 ? { instance } : {},
2552
- workspace: row.workspace_id,
2553
- tab: row.tab_id,
2554
- pane: paneId
2555
- }
2556
- };
2557
- }
2558
-
2559
2828
  // extension/radar-runtime.ts
2560
2829
  var RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
2561
- var RADAR_RUNTIME_VERSION = 2;
2830
+ var RADAR_RUNTIME_VERSION = 3;
2562
2831
  function lazyRadarRuntime(start) {
2563
2832
  let pending;
2564
2833
  return {
@@ -2587,6 +2856,7 @@ function registerPiRadar(pi) {
2587
2856
  const runtime = lazyRadarRuntime(async () => ({
2588
2857
  publication: execution.publication,
2589
2858
  communication: createRadarHandoffPort(client, execution.publication),
2859
+ children: createRadarChildren(client, execution.publication),
2590
2860
  execution
2591
2861
  }));
2592
2862
  provideRadarRuntime(pi.events, runtime);