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