pi-radar 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/extension/baseline.js +380 -111
- package/dist/extension/radar-children.js +240 -0
- package/dist/extension/radar-client.js +37 -3
- package/dist/extension/radar-launch.js +23 -24
- package/dist/extension/radar-runtime.js +1 -1
- package/dist/index.js +1779 -1623
- package/extension/baseline.ts +2 -0
- package/extension/generated-contracts.ts +5 -0
- package/extension/radar-children.ts +176 -0
- package/extension/radar-client.ts +56 -2
- package/extension/radar-launch.ts +42 -63
- package/extension/radar-runtime.ts +4 -1
- package/index.ts +1 -0
- package/package.json +8 -3
- package/schema/public/v1/observation-get-response.deserialize.schema.json +1 -1
- package/schema/public/v1/observation-get-response.serialize.schema.json +1 -1
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +1 -1
- package/schema/public/v1/observation-snapshot.serialize.schema.json +1 -1
- package/schema/public/v1/spawn-list-page.deserialize.schema.json +7 -0
- package/schema/public/v1/spawn-list-page.serialize.schema.json +7 -0
package/README.md
CHANGED
|
@@ -35,6 +35,16 @@ The client uses `registry.v1` for registry operations and additionally requires
|
|
|
35
35
|
|
|
36
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.
|
|
37
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
|
+
|
|
38
48
|
## Generated protocol contracts
|
|
39
49
|
|
|
40
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 =
|
|
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";
|
|
@@ -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))
|
|
@@ -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() {
|
|
@@ -2450,116 +2825,9 @@ function createRadarExecutionAdapter(pi, options) {
|
|
|
2450
2825
|
};
|
|
2451
2826
|
}
|
|
2452
2827
|
|
|
2453
|
-
// extension/radar-location.ts
|
|
2454
|
-
import { basename } from "node:path";
|
|
2455
|
-
function radarLocalPane(env) {
|
|
2456
|
-
const pane = env.TMUX_PANE?.trim();
|
|
2457
|
-
return pane && /^%\d+$/.test(pane) ? pane : void 0;
|
|
2458
|
-
}
|
|
2459
|
-
function tmuxServerName(env) {
|
|
2460
|
-
const socket = env.TMUX?.trim().split(",")[0];
|
|
2461
|
-
return socket ? basename(socket) || void 0 : void 0;
|
|
2462
|
-
}
|
|
2463
|
-
function qualifiedOwnPane(env, backend, instance) {
|
|
2464
|
-
if (backend !== "tmux")
|
|
2465
|
-
return {
|
|
2466
|
-
absent: `the daemon serves the ${backend} backend, and Herdsman drives only tmux`
|
|
2467
|
-
};
|
|
2468
|
-
if (instance === void 0 || instance === "")
|
|
2469
|
-
return {
|
|
2470
|
-
pending: "the daemon did not name the tmux server it addresses"
|
|
2471
|
-
};
|
|
2472
|
-
const server = tmuxServerName(env);
|
|
2473
|
-
if (server === void 0)
|
|
2474
|
-
return {
|
|
2475
|
-
absent: "this process's own tmux client names no server, so it is not in the pane it would publish"
|
|
2476
|
-
};
|
|
2477
|
-
if (server !== instance)
|
|
2478
|
-
return {
|
|
2479
|
-
absent: `this process is attached to tmux server ${server}, not to ${instance}`
|
|
2480
|
-
};
|
|
2481
|
-
const pane = radarLocalPane(env);
|
|
2482
|
-
if (pane === void 0)
|
|
2483
|
-
return {
|
|
2484
|
-
absent: "this process's own environment names no tmux pane of that server"
|
|
2485
|
-
};
|
|
2486
|
-
return { pane: `tmux:${instance}:${pane}` };
|
|
2487
|
-
}
|
|
2488
|
-
function observedContainment(inventory, paneId) {
|
|
2489
|
-
const rows = inventory.panes.filter((pane) => pane.pane_id === paneId);
|
|
2490
|
-
if (rows.length === 0) return void 0;
|
|
2491
|
-
if (rows.length > 1) return { ambiguous: `topology reports pane ${paneId} twice` };
|
|
2492
|
-
const row = rows[0];
|
|
2493
|
-
const tab = inventory.tabs.find((candidate) => candidate.tab_id === row.tab_id);
|
|
2494
|
-
if (tab !== void 0 && tab.workspace_id !== row.workspace_id)
|
|
2495
|
-
return {
|
|
2496
|
-
ambiguous: `pane ${paneId} is reported in ${row.workspace_id}, but its tab belongs to ${tab.workspace_id}`
|
|
2497
|
-
};
|
|
2498
|
-
return { pane: row };
|
|
2499
|
-
}
|
|
2500
|
-
async function resolveRadarProcessLocation(client, env = process.env) {
|
|
2501
|
-
let ready;
|
|
2502
|
-
try {
|
|
2503
|
-
ready = await client.ping();
|
|
2504
|
-
} catch {
|
|
2505
|
-
return { state: "pending", reason: "the daemon did not answer this process" };
|
|
2506
|
-
}
|
|
2507
|
-
if (ready.ok === false)
|
|
2508
|
-
return {
|
|
2509
|
-
state: "pending",
|
|
2510
|
-
reason: `the daemon did not answer this process (${ready.code})`
|
|
2511
|
-
};
|
|
2512
|
-
const backend = ready.value.backend;
|
|
2513
|
-
if (backend === void 0 || backend === "none")
|
|
2514
|
-
return { state: "absent", reason: "the daemon serves no physical backend" };
|
|
2515
|
-
const instance = ready.value.instance ?? void 0;
|
|
2516
|
-
const own = qualifiedOwnPane(env, backend, instance);
|
|
2517
|
-
if ("absent" in own) return { state: "absent", reason: own.absent };
|
|
2518
|
-
if ("pending" in own) return { state: "pending", reason: own.pending };
|
|
2519
|
-
const paneId = own.pane;
|
|
2520
|
-
if (paneId === env.PI_HERDSMAN_PARENT_PANE_ID?.trim())
|
|
2521
|
-
return {
|
|
2522
|
-
state: "absent",
|
|
2523
|
-
reason: "this process is in the pane its launch was created beside, not in a pane of its own"
|
|
2524
|
-
};
|
|
2525
|
-
let inventory;
|
|
2526
|
-
try {
|
|
2527
|
-
inventory = await client.observe();
|
|
2528
|
-
} catch {
|
|
2529
|
-
return {
|
|
2530
|
-
state: "pending",
|
|
2531
|
-
reason: "the daemon's topology could not be read for this process's pane"
|
|
2532
|
-
};
|
|
2533
|
-
}
|
|
2534
|
-
if (inventory.ok === false)
|
|
2535
|
-
return {
|
|
2536
|
-
state: "pending",
|
|
2537
|
-
reason: `the daemon's topology could not be read (${inventory.code})`
|
|
2538
|
-
};
|
|
2539
|
-
const containment = observedContainment(inventory.value, paneId);
|
|
2540
|
-
if (containment === void 0)
|
|
2541
|
-
return {
|
|
2542
|
-
state: "pending",
|
|
2543
|
-
reason: `the daemon's topology does not report pane ${paneId} yet`
|
|
2544
|
-
};
|
|
2545
|
-
if ("ambiguous" in containment)
|
|
2546
|
-
return { state: "pending", reason: containment.ambiguous };
|
|
2547
|
-
const row = containment.pane;
|
|
2548
|
-
return {
|
|
2549
|
-
state: "resolved",
|
|
2550
|
-
location: {
|
|
2551
|
-
backend,
|
|
2552
|
-
...instance !== void 0 ? { instance } : {},
|
|
2553
|
-
workspace: row.workspace_id,
|
|
2554
|
-
tab: row.tab_id,
|
|
2555
|
-
pane: paneId
|
|
2556
|
-
}
|
|
2557
|
-
};
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
2828
|
// extension/radar-runtime.ts
|
|
2561
2829
|
var RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
|
|
2562
|
-
var RADAR_RUNTIME_VERSION =
|
|
2830
|
+
var RADAR_RUNTIME_VERSION = 3;
|
|
2563
2831
|
function lazyRadarRuntime(start) {
|
|
2564
2832
|
let pending;
|
|
2565
2833
|
return {
|
|
@@ -2588,6 +2856,7 @@ function registerPiRadar(pi) {
|
|
|
2588
2856
|
const runtime = lazyRadarRuntime(async () => ({
|
|
2589
2857
|
publication: execution.publication,
|
|
2590
2858
|
communication: createRadarHandoffPort(client, execution.publication),
|
|
2859
|
+
children: createRadarChildren(client, execution.publication),
|
|
2591
2860
|
execution
|
|
2592
2861
|
}));
|
|
2593
2862
|
provideRadarRuntime(pi.events, runtime);
|