pi-radar 0.5.1 → 0.6.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 +72 -2
- package/dist/extension/baseline.js +37 -6
- package/dist/extension/radar-client.js +32 -5
- package/dist/extension/radar-execution.js +6 -1
- package/dist/extension/radar-publication.js +6 -1
- package/dist/index.js +38 -6
- package/extension/generated-contracts.ts +108 -5
- package/extension/radar-client.ts +38 -11
- package/extension/radar-publication.ts +9 -1
- package/package.json +1 -1
- package/schema/public/v1/handoff-receipt.deserialize.schema.json +2 -9
- package/schema/public/v1/handoff-receipt.serialize.schema.json +2 -9
- package/schema/public/v1/handoff.deserialize.schema.json +2 -9
- package/schema/public/v1/handoff.serialize.schema.json +2 -9
- package/schema/public/v1/observation-get-response.deserialize.schema.json +10 -6
- package/schema/public/v1/observation-get-response.serialize.schema.json +11 -7
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +10 -6
- package/schema/public/v1/observation-snapshot.serialize.schema.json +11 -7
- package/schema/public/v1/spawn-list-page.deserialize.schema.json +291 -0
- package/schema/public/v1/spawn-list-page.serialize.schema.json +296 -0
- package/schema/public/v1/spawn-list-params.deserialize.schema.json +33 -0
- package/schema/public/v1/spawn-list-params.serialize.schema.json +38 -0
package/README.md
CHANGED
|
@@ -15,11 +15,13 @@ Focused subpaths are also exported: `pi-radar/client`, `pi-radar/communication`,
|
|
|
15
15
|
|
|
16
16
|
Installing `pi-radar` directly as a Pi package loads `extension/baseline.ts`, which creates one baseline publisher and exposes its lazy runtime handle through Pi's event bus. Herdsman must request that handle rather than starting another `RadarExecutionAdapter`. Do not load the baseline extension with an older Herdsman integration that still creates its own publisher; first migrate Herdsman to the runtime handle.
|
|
17
17
|
|
|
18
|
-
The baseline reports session presence through one flat request.
|
|
18
|
+
The baseline reports session presence through one flat request. The first managed `session_start` uses its pre-minted launch claim; subsequent session loads create fresh claims. Heartbeat/reconnect retries reuse the same claim. `publication.presence.management()` reports `managed`, `duplicate`, `pending` or `unmanaged`; `session_shutdown` awaits release of the exact claim. A duplicate remains rejected until a fresh Pi session load. `subject()` returns Radar's current subject only after a grant; no session marker is written to the Pi transcript.
|
|
19
19
|
|
|
20
20
|
## Daemon contract
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
**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
|
+
|
|
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.
|
|
23
25
|
|
|
24
26
|
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.
|
|
25
27
|
|
|
@@ -41,6 +43,74 @@ Lifecycle execution generations are bounded to `u32` and remain exactly represen
|
|
|
41
43
|
|
|
42
44
|
Run `npm pack` to build the bundled runtime entrypoints before creating the tarball.
|
|
43
45
|
|
|
46
|
+
## Testing against this SDK
|
|
47
|
+
|
|
48
|
+
Fake the port, not the daemon. Every consumer-facing class here depends on a
|
|
49
|
+
narrow `Pick` of the client, so a test double implements a handful of methods and
|
|
50
|
+
no socket, framing, IDs, error envelope or durable state:
|
|
51
|
+
|
|
52
|
+
| Port | Methods | Where |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `RadarPublicationClient` | `ping`, `presence`, `releasePresence`, `acquire`, `publish`, `retire` | `pi-radar/publication` |
|
|
55
|
+
| `RadarHandoffPort` | `publish`, `inbox`, `get`, `ack` | `pi-radar/communication` |
|
|
56
|
+
| anything else | `Pick<RadarClient, ...>` of the reads you call | `pi-radar/client` |
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import type { RadarPublicationClient } from "pi-radar/publication";
|
|
60
|
+
import { createRadarPublication } from "pi-radar/publication";
|
|
61
|
+
|
|
62
|
+
const client: RadarPublicationClient = {
|
|
63
|
+
ping: async () => ({
|
|
64
|
+
ok: true,
|
|
65
|
+
value: {
|
|
66
|
+
protocol: 2, backend: "tmux", revision: 4, instance: null,
|
|
67
|
+
contracts: ["registry.v1"], tasks: { available: false, error: null },
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
presence: async (report) => ({
|
|
71
|
+
ok: true,
|
|
72
|
+
value: {
|
|
73
|
+
registration_id: REGISTRATION,
|
|
74
|
+
claim_id: report.claim_id,
|
|
75
|
+
standing: "granted",
|
|
76
|
+
subject_id: SUBJECT,
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
releasePresence: async (release) => ({
|
|
80
|
+
ok: true,
|
|
81
|
+
value: {
|
|
82
|
+
registration_id: release.registration_id,
|
|
83
|
+
claim_id: release.claim_id,
|
|
84
|
+
standing: "released",
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
acquire: async () => ({
|
|
88
|
+
ok: true,
|
|
89
|
+
value: {
|
|
90
|
+
handle: "writer", source: "consumer", incarnation: INCARNATION,
|
|
91
|
+
generation: 1, sequence: 0,
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
publish: async ({ sequence }) => ({ ok: true, value: { sequence } }),
|
|
95
|
+
retire: async () => ({ ok: true, value: undefined }),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const publication = createRadarPublication({
|
|
99
|
+
dataRoot, client, registration: { source: "consumer", label: "lead" },
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A socket-level fake daemon is a second implementation of Radar's protocol
|
|
104
|
+
maintained by someone who does not own it. It drifts silently: it keeps passing
|
|
105
|
+
while the real daemon refuses the same call, which is worse than having no fake
|
|
106
|
+
at all. That is not hypothetical — a consumer's fake still answered the retired
|
|
107
|
+
registration methods after the daemon had moved to presence, and its suite stayed
|
|
108
|
+
green through the whole contract change.
|
|
109
|
+
|
|
110
|
+
Wire-level proof belongs to one integration test against the real daemon (a
|
|
111
|
+
temporary socket path and state root are enough; `tests/agent_publisher.py` in
|
|
112
|
+
the Radar repository shows the shape), not to a fake every consumer re-implements.
|
|
113
|
+
|
|
44
114
|
## Releasing
|
|
45
115
|
|
|
46
116
|
Publishing is one tag push and it runs in CI; no npm token is stored anywhere.
|
|
@@ -8,10 +8,11 @@ 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 = 5;
|
|
12
12
|
var RADAR_REGISTRY_CONTRACT = "registry.v1";
|
|
13
13
|
var RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
|
|
14
14
|
var RADAR_SPAWN_CONTRACT = "spawn.v1";
|
|
15
|
+
var RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
|
|
15
16
|
var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
|
|
16
17
|
var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
|
|
17
18
|
var RADAR_EVENTS_CONTRACT = "events.v1";
|
|
@@ -146,22 +147,20 @@ function handoffAt(value) {
|
|
|
146
147
|
const version = numberAt(value, "version");
|
|
147
148
|
const id = stringAt(value, "id");
|
|
148
149
|
const sender = recordAt(value, "sender");
|
|
149
|
-
const senderGeneration = numberAt(sender, "execution_generation");
|
|
150
150
|
const target = stringAt(value, "recipient_subject_id");
|
|
151
151
|
const summary = stringAt(value, "summary");
|
|
152
152
|
const content = stringAt(value, "content");
|
|
153
153
|
const published = numberAt(value, "published_at_unix_ms");
|
|
154
154
|
const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
|
|
155
|
-
if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "
|
|
155
|
+
if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "claim_id")) || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
|
|
156
156
|
return value;
|
|
157
157
|
}
|
|
158
158
|
function handoffReceiptAt(value) {
|
|
159
159
|
const version = numberAt(value, "version");
|
|
160
160
|
const handoffId = stringAt(value, "handoff_id");
|
|
161
161
|
const recipient = recordAt(value, "recipient");
|
|
162
|
-
const generation = numberAt(recipient, "execution_generation");
|
|
163
162
|
const received = numberAt(value, "received_at_unix_ms");
|
|
164
|
-
if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "
|
|
163
|
+
if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "claim_id")) || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
|
|
165
164
|
return value;
|
|
166
165
|
}
|
|
167
166
|
function writerBinding(value) {
|
|
@@ -621,6 +620,7 @@ function createRadarClient(options = {}) {
|
|
|
621
620
|
};
|
|
622
621
|
return answer;
|
|
623
622
|
};
|
|
623
|
+
let processRegistrationId;
|
|
624
624
|
let handshake;
|
|
625
625
|
const negotiated = async () => {
|
|
626
626
|
handshake ??= probe();
|
|
@@ -702,6 +702,7 @@ function createRadarClient(options = {}) {
|
|
|
702
702
|
const record = answer.value.registration;
|
|
703
703
|
const agentId = stringAt(record, "registration_id");
|
|
704
704
|
if (!isUuid(agentId)) return protocol("registration reply carries no registration_id");
|
|
705
|
+
processRegistrationId = agentId;
|
|
705
706
|
return { ok: true, value: { registration_id: agentId } };
|
|
706
707
|
});
|
|
707
708
|
},
|
|
@@ -835,6 +836,31 @@ function createRadarClient(options = {}) {
|
|
|
835
836
|
}
|
|
836
837
|
);
|
|
837
838
|
},
|
|
839
|
+
async spawnList(request) {
|
|
840
|
+
const registrationId = processRegistrationId;
|
|
841
|
+
if (registrationId === void 0)
|
|
842
|
+
return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
|
|
843
|
+
return withContract(RADAR_SPAWN_LIST_CONTRACT, async () => {
|
|
844
|
+
const params = {
|
|
845
|
+
registration_id: registrationId,
|
|
846
|
+
...request.after !== void 0 ? { after: request.after } : {},
|
|
847
|
+
...request.limit !== void 0 ? { limit: request.limit } : {}
|
|
848
|
+
};
|
|
849
|
+
const answer = await call("spawn.list", { ...params });
|
|
850
|
+
if (answer.ok === false) return answer;
|
|
851
|
+
const spawns = recordAt(answer.value, "spawns");
|
|
852
|
+
const next = recordAt(answer.value, "next");
|
|
853
|
+
if (!Array.isArray(spawns) || next != null && !isUuid(next))
|
|
854
|
+
return protocol("spawn.list reply carries no valid page");
|
|
855
|
+
for (const spawn of spawns) {
|
|
856
|
+
const decoded = spawnTopology(spawn);
|
|
857
|
+
if (decoded.ok === false) return decoded;
|
|
858
|
+
if (typeof recordAt(spawn, "version") !== "number" || typeof recordAt(spawn, "recorded_at") !== "string" || !["unbound", "bound", "unresolved"].includes(String(recordAt(spawn, "state"))))
|
|
859
|
+
return protocol("spawn.list reply carries an incomplete topology");
|
|
860
|
+
}
|
|
861
|
+
return { ok: true, value: answer.value };
|
|
862
|
+
});
|
|
863
|
+
},
|
|
838
864
|
async spawnGet(requestId) {
|
|
839
865
|
if (!isUuid(requestId))
|
|
840
866
|
return {
|
|
@@ -1822,6 +1848,11 @@ function createPresenceReporter(input) {
|
|
|
1822
1848
|
await releasePending();
|
|
1823
1849
|
const registration = await input.registration();
|
|
1824
1850
|
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
|
+
}
|
|
1825
1856
|
const answer = await client.presence({
|
|
1826
1857
|
...registration,
|
|
1827
1858
|
process: registration.process,
|
|
@@ -1860,7 +1891,7 @@ function createPresenceReporter(input) {
|
|
|
1860
1891
|
releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
|
|
1861
1892
|
}
|
|
1862
1893
|
standing = void 0;
|
|
1863
|
-
state = { version: 2, session, claim_id: randomUUID2(), releases };
|
|
1894
|
+
state = { version: 2, session, claim_id: randomUUID2(), initial_load: state === void 0, releases };
|
|
1864
1895
|
save();
|
|
1865
1896
|
stopped = false;
|
|
1866
1897
|
void ensure();
|
|
@@ -4,10 +4,11 @@ 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 = 5;
|
|
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";
|
|
11
|
+
var RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
|
|
11
12
|
var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
|
|
12
13
|
var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
|
|
13
14
|
var RADAR_EVENTS_CONTRACT = "events.v1";
|
|
@@ -142,22 +143,20 @@ function handoffAt(value) {
|
|
|
142
143
|
const version = numberAt(value, "version");
|
|
143
144
|
const id = stringAt(value, "id");
|
|
144
145
|
const sender = recordAt(value, "sender");
|
|
145
|
-
const senderGeneration = numberAt(sender, "execution_generation");
|
|
146
146
|
const target = stringAt(value, "recipient_subject_id");
|
|
147
147
|
const summary = stringAt(value, "summary");
|
|
148
148
|
const content = stringAt(value, "content");
|
|
149
149
|
const published = numberAt(value, "published_at_unix_ms");
|
|
150
150
|
const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
|
|
151
|
-
if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "
|
|
151
|
+
if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "claim_id")) || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
|
|
152
152
|
return value;
|
|
153
153
|
}
|
|
154
154
|
function handoffReceiptAt(value) {
|
|
155
155
|
const version = numberAt(value, "version");
|
|
156
156
|
const handoffId = stringAt(value, "handoff_id");
|
|
157
157
|
const recipient = recordAt(value, "recipient");
|
|
158
|
-
const generation = numberAt(recipient, "execution_generation");
|
|
159
158
|
const received = numberAt(value, "received_at_unix_ms");
|
|
160
|
-
if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "
|
|
159
|
+
if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "claim_id")) || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
|
|
161
160
|
return value;
|
|
162
161
|
}
|
|
163
162
|
function writerBinding(value) {
|
|
@@ -679,6 +678,7 @@ function createRadarClient(options = {}) {
|
|
|
679
678
|
};
|
|
680
679
|
return answer;
|
|
681
680
|
};
|
|
681
|
+
let processRegistrationId;
|
|
682
682
|
let handshake;
|
|
683
683
|
const negotiated = async () => {
|
|
684
684
|
handshake ??= probe();
|
|
@@ -760,6 +760,7 @@ function createRadarClient(options = {}) {
|
|
|
760
760
|
const record = answer.value.registration;
|
|
761
761
|
const agentId = stringAt(record, "registration_id");
|
|
762
762
|
if (!isUuid(agentId)) return protocol("registration reply carries no registration_id");
|
|
763
|
+
processRegistrationId = agentId;
|
|
763
764
|
return { ok: true, value: { registration_id: agentId } };
|
|
764
765
|
});
|
|
765
766
|
},
|
|
@@ -893,6 +894,31 @@ function createRadarClient(options = {}) {
|
|
|
893
894
|
}
|
|
894
895
|
);
|
|
895
896
|
},
|
|
897
|
+
async spawnList(request) {
|
|
898
|
+
const registrationId = processRegistrationId;
|
|
899
|
+
if (registrationId === void 0)
|
|
900
|
+
return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
|
|
901
|
+
return withContract(RADAR_SPAWN_LIST_CONTRACT, async () => {
|
|
902
|
+
const params = {
|
|
903
|
+
registration_id: registrationId,
|
|
904
|
+
...request.after !== void 0 ? { after: request.after } : {},
|
|
905
|
+
...request.limit !== void 0 ? { limit: request.limit } : {}
|
|
906
|
+
};
|
|
907
|
+
const answer = await call("spawn.list", { ...params });
|
|
908
|
+
if (answer.ok === false) return answer;
|
|
909
|
+
const spawns = recordAt(answer.value, "spawns");
|
|
910
|
+
const next = recordAt(answer.value, "next");
|
|
911
|
+
if (!Array.isArray(spawns) || next != null && !isUuid(next))
|
|
912
|
+
return protocol("spawn.list reply carries no valid page");
|
|
913
|
+
for (const spawn of spawns) {
|
|
914
|
+
const decoded = spawnTopology(spawn);
|
|
915
|
+
if (decoded.ok === false) return decoded;
|
|
916
|
+
if (typeof recordAt(spawn, "version") !== "number" || typeof recordAt(spawn, "recorded_at") !== "string" || !["unbound", "bound", "unresolved"].includes(String(recordAt(spawn, "state"))))
|
|
917
|
+
return protocol("spawn.list reply carries an incomplete topology");
|
|
918
|
+
}
|
|
919
|
+
return { ok: true, value: answer.value };
|
|
920
|
+
});
|
|
921
|
+
},
|
|
896
922
|
async spawnGet(requestId) {
|
|
897
923
|
if (!isUuid(requestId))
|
|
898
924
|
return {
|
|
@@ -1250,6 +1276,7 @@ export {
|
|
|
1250
1276
|
RADAR_SPAWN_CLOSE_CONTRACT,
|
|
1251
1277
|
RADAR_SPAWN_CONTRACT,
|
|
1252
1278
|
RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT,
|
|
1279
|
+
RADAR_SPAWN_LIST_CONTRACT,
|
|
1253
1280
|
RADAR_SPAWN_TOKEN_ENV,
|
|
1254
1281
|
boundedRadarAssignment,
|
|
1255
1282
|
boundedRadarFacts,
|
|
@@ -702,6 +702,11 @@ function createPresenceReporter(input) {
|
|
|
702
702
|
await releasePending();
|
|
703
703
|
const registration = await input.registration();
|
|
704
704
|
if (!registration?.process || state !== claim || stopped) return void 0;
|
|
705
|
+
if (claim.initial_load) {
|
|
706
|
+
if (registration.spawn_token) claim.claim_id = registration.spawn_token;
|
|
707
|
+
claim.initial_load = false;
|
|
708
|
+
if (!save()) return void 0;
|
|
709
|
+
}
|
|
705
710
|
const answer = await client.presence({
|
|
706
711
|
...registration,
|
|
707
712
|
process: registration.process,
|
|
@@ -740,7 +745,7 @@ function createPresenceReporter(input) {
|
|
|
740
745
|
releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
|
|
741
746
|
}
|
|
742
747
|
standing = void 0;
|
|
743
|
-
state = { version: 2, session, claim_id: randomUUID(), releases };
|
|
748
|
+
state = { version: 2, session, claim_id: randomUUID(), initial_load: state === void 0, releases };
|
|
744
749
|
save();
|
|
745
750
|
stopped = false;
|
|
746
751
|
void ensure();
|
|
@@ -707,6 +707,11 @@ function createPresenceReporter(input) {
|
|
|
707
707
|
await releasePending();
|
|
708
708
|
const registration = await input.registration();
|
|
709
709
|
if (!registration?.process || state !== claim || stopped) return void 0;
|
|
710
|
+
if (claim.initial_load) {
|
|
711
|
+
if (registration.spawn_token) claim.claim_id = registration.spawn_token;
|
|
712
|
+
claim.initial_load = false;
|
|
713
|
+
if (!save()) return void 0;
|
|
714
|
+
}
|
|
710
715
|
const answer = await client.presence({
|
|
711
716
|
...registration,
|
|
712
717
|
process: registration.process,
|
|
@@ -745,7 +750,7 @@ function createPresenceReporter(input) {
|
|
|
745
750
|
releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
|
|
746
751
|
}
|
|
747
752
|
standing = void 0;
|
|
748
|
-
state = { version: 2, session, claim_id: randomUUID(), releases };
|
|
753
|
+
state = { version: 2, session, claim_id: randomUUID(), initial_load: state === void 0, releases };
|
|
749
754
|
save();
|
|
750
755
|
stopped = false;
|
|
751
756
|
void ensure();
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ __export(radar_client_exports, {
|
|
|
22
22
|
RADAR_SPAWN_CLOSE_CONTRACT: () => RADAR_SPAWN_CLOSE_CONTRACT,
|
|
23
23
|
RADAR_SPAWN_CONTRACT: () => RADAR_SPAWN_CONTRACT,
|
|
24
24
|
RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT: () => RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT,
|
|
25
|
+
RADAR_SPAWN_LIST_CONTRACT: () => RADAR_SPAWN_LIST_CONTRACT,
|
|
25
26
|
RADAR_SPAWN_TOKEN_ENV: () => RADAR_SPAWN_TOKEN_ENV,
|
|
26
27
|
boundedRadarAssignment: () => boundedRadarAssignment,
|
|
27
28
|
boundedRadarFacts: () => boundedRadarFacts,
|
|
@@ -39,10 +40,11 @@ import { lstatSync } from "node:fs";
|
|
|
39
40
|
import { createConnection } from "node:net";
|
|
40
41
|
import { dirname, join } from "node:path";
|
|
41
42
|
var RADAR_PROTOCOL_VERSION = 2;
|
|
42
|
-
var RADAR_MINIMUM_REVISION =
|
|
43
|
+
var RADAR_MINIMUM_REVISION = 5;
|
|
43
44
|
var RADAR_REGISTRY_CONTRACT = "registry.v1";
|
|
44
45
|
var RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
|
|
45
46
|
var RADAR_SPAWN_CONTRACT = "spawn.v1";
|
|
47
|
+
var RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
|
|
46
48
|
var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
|
|
47
49
|
var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
|
|
48
50
|
var RADAR_EVENTS_CONTRACT = "events.v1";
|
|
@@ -177,22 +179,20 @@ function handoffAt(value) {
|
|
|
177
179
|
const version = numberAt(value, "version");
|
|
178
180
|
const id = stringAt(value, "id");
|
|
179
181
|
const sender = recordAt(value, "sender");
|
|
180
|
-
const senderGeneration = numberAt(sender, "execution_generation");
|
|
181
182
|
const target = stringAt(value, "recipient_subject_id");
|
|
182
183
|
const summary = stringAt(value, "summary");
|
|
183
184
|
const content = stringAt(value, "content");
|
|
184
185
|
const published = numberAt(value, "published_at_unix_ms");
|
|
185
186
|
const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
|
|
186
|
-
if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "
|
|
187
|
+
if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "claim_id")) || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
|
|
187
188
|
return value;
|
|
188
189
|
}
|
|
189
190
|
function handoffReceiptAt(value) {
|
|
190
191
|
const version = numberAt(value, "version");
|
|
191
192
|
const handoffId = stringAt(value, "handoff_id");
|
|
192
193
|
const recipient = recordAt(value, "recipient");
|
|
193
|
-
const generation = numberAt(recipient, "execution_generation");
|
|
194
194
|
const received = numberAt(value, "received_at_unix_ms");
|
|
195
|
-
if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "
|
|
195
|
+
if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "claim_id")) || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
|
|
196
196
|
return value;
|
|
197
197
|
}
|
|
198
198
|
function writerBinding(value) {
|
|
@@ -714,6 +714,7 @@ function createRadarClient(options = {}) {
|
|
|
714
714
|
};
|
|
715
715
|
return answer;
|
|
716
716
|
};
|
|
717
|
+
let processRegistrationId;
|
|
717
718
|
let handshake;
|
|
718
719
|
const negotiated = async () => {
|
|
719
720
|
handshake ??= probe();
|
|
@@ -795,6 +796,7 @@ function createRadarClient(options = {}) {
|
|
|
795
796
|
const record = answer.value.registration;
|
|
796
797
|
const agentId = stringAt(record, "registration_id");
|
|
797
798
|
if (!isUuid(agentId)) return protocol("registration reply carries no registration_id");
|
|
799
|
+
processRegistrationId = agentId;
|
|
798
800
|
return { ok: true, value: { registration_id: agentId } };
|
|
799
801
|
});
|
|
800
802
|
},
|
|
@@ -928,6 +930,31 @@ function createRadarClient(options = {}) {
|
|
|
928
930
|
}
|
|
929
931
|
);
|
|
930
932
|
},
|
|
933
|
+
async spawnList(request) {
|
|
934
|
+
const registrationId = processRegistrationId;
|
|
935
|
+
if (registrationId === void 0)
|
|
936
|
+
return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
|
|
937
|
+
return withContract(RADAR_SPAWN_LIST_CONTRACT, async () => {
|
|
938
|
+
const params = {
|
|
939
|
+
registration_id: registrationId,
|
|
940
|
+
...request.after !== void 0 ? { after: request.after } : {},
|
|
941
|
+
...request.limit !== void 0 ? { limit: request.limit } : {}
|
|
942
|
+
};
|
|
943
|
+
const answer = await call("spawn.list", { ...params });
|
|
944
|
+
if (answer.ok === false) return answer;
|
|
945
|
+
const spawns = recordAt(answer.value, "spawns");
|
|
946
|
+
const next = recordAt(answer.value, "next");
|
|
947
|
+
if (!Array.isArray(spawns) || next != null && !isUuid(next))
|
|
948
|
+
return protocol("spawn.list reply carries no valid page");
|
|
949
|
+
for (const spawn of spawns) {
|
|
950
|
+
const decoded = spawnTopology(spawn);
|
|
951
|
+
if (decoded.ok === false) return decoded;
|
|
952
|
+
if (typeof recordAt(spawn, "version") !== "number" || typeof recordAt(spawn, "recorded_at") !== "string" || !["unbound", "bound", "unresolved"].includes(String(recordAt(spawn, "state"))))
|
|
953
|
+
return protocol("spawn.list reply carries an incomplete topology");
|
|
954
|
+
}
|
|
955
|
+
return { ok: true, value: answer.value };
|
|
956
|
+
});
|
|
957
|
+
},
|
|
931
958
|
async spawnGet(requestId) {
|
|
932
959
|
if (!isUuid(requestId))
|
|
933
960
|
return {
|
|
@@ -1944,6 +1971,11 @@ function createPresenceReporter(input) {
|
|
|
1944
1971
|
await releasePending();
|
|
1945
1972
|
const registration = await input.registration();
|
|
1946
1973
|
if (!registration?.process || state !== claim || stopped) return void 0;
|
|
1974
|
+
if (claim.initial_load) {
|
|
1975
|
+
if (registration.spawn_token) claim.claim_id = registration.spawn_token;
|
|
1976
|
+
claim.initial_load = false;
|
|
1977
|
+
if (!save()) return void 0;
|
|
1978
|
+
}
|
|
1947
1979
|
const answer = await client.presence({
|
|
1948
1980
|
...registration,
|
|
1949
1981
|
process: registration.process,
|
|
@@ -1982,7 +2014,7 @@ function createPresenceReporter(input) {
|
|
|
1982
2014
|
releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
|
|
1983
2015
|
}
|
|
1984
2016
|
standing = void 0;
|
|
1985
|
-
state = { version: 2, session, claim_id: randomUUID2(), releases };
|
|
2017
|
+
state = { version: 2, session, claim_id: randomUUID2(), initial_load: state === void 0, releases };
|
|
1986
2018
|
save();
|
|
1987
2019
|
stopped = false;
|
|
1988
2020
|
void ensure();
|
|
@@ -151,8 +151,7 @@ export interface HandoffSerialize {
|
|
|
151
151
|
* generation identify the exact execution that took the action.
|
|
152
152
|
*/
|
|
153
153
|
export interface HandoffIdentity {
|
|
154
|
-
|
|
155
|
-
execution_id: string;
|
|
154
|
+
claim_id: string;
|
|
156
155
|
registration_id: string;
|
|
157
156
|
session_id: string;
|
|
158
157
|
subject_id: string;
|
|
@@ -172,8 +171,7 @@ export interface HandoffReceiptSerialize {
|
|
|
172
171
|
* generation identify the exact execution that took the action.
|
|
173
172
|
*/
|
|
174
173
|
export interface HandoffIdentity {
|
|
175
|
-
|
|
176
|
-
execution_id: string;
|
|
174
|
+
claim_id: string;
|
|
177
175
|
registration_id: string;
|
|
178
176
|
session_id: string;
|
|
179
177
|
subject_id: string;
|
|
@@ -902,6 +900,7 @@ export interface RegistryLocation {
|
|
|
902
900
|
*/
|
|
903
901
|
export interface ManagedSubject {
|
|
904
902
|
child_close: ChildCloseTarget | null;
|
|
903
|
+
claim_id: string;
|
|
905
904
|
identity_unresolved: boolean;
|
|
906
905
|
metadata: ManagedMetadata;
|
|
907
906
|
process: ProcessIdentity;
|
|
@@ -938,10 +937,10 @@ export interface ManagedMetadata {
|
|
|
938
937
|
export interface SpawnBinding {
|
|
939
938
|
child_incarnation: string | null;
|
|
940
939
|
child_source: string | null;
|
|
940
|
+
claim_id: string | null;
|
|
941
941
|
parent: string;
|
|
942
942
|
request_id: string;
|
|
943
943
|
state: SpawnState;
|
|
944
|
-
subject_id: string | null;
|
|
945
944
|
}
|
|
946
945
|
/**
|
|
947
946
|
* What this machine knows about that process. Filled by the collector
|
|
@@ -1385,3 +1384,107 @@ export interface ObservationTooLarge {
|
|
|
1385
1384
|
limit_bytes: number;
|
|
1386
1385
|
revision: number;
|
|
1387
1386
|
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Input accepted by `spawn.list`.
|
|
1389
|
+
*
|
|
1390
|
+
* A caller presents itself; the parent whose launches it reads is derived from
|
|
1391
|
+
* the claim it holds. There is deliberately no parent parameter: naming another
|
|
1392
|
+
* subject would let any client read its launches.
|
|
1393
|
+
*/
|
|
1394
|
+
export interface SpawnListParamsDeserialize {
|
|
1395
|
+
after?: string | null;
|
|
1396
|
+
limit?: number | null;
|
|
1397
|
+
registration_id?: string | null;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* What a completed request did.
|
|
1401
|
+
*/
|
|
1402
|
+
export type SpawnListPageSerializeRequestOutcome = "completed" | "refused" | "unknown";
|
|
1403
|
+
/**
|
|
1404
|
+
* What a spawn topology read can say about a child's process exit.
|
|
1405
|
+
*/
|
|
1406
|
+
export type SpawnListPageSerializePublicSpawnExitObservation =
|
|
1407
|
+
| {
|
|
1408
|
+
state: "unknown";
|
|
1409
|
+
}
|
|
1410
|
+
| {
|
|
1411
|
+
code: number | null;
|
|
1412
|
+
observed_at: string;
|
|
1413
|
+
signal: number | null;
|
|
1414
|
+
state: "exited";
|
|
1415
|
+
};
|
|
1416
|
+
/**
|
|
1417
|
+
* Freshness is independent of process identity or completion.
|
|
1418
|
+
*/
|
|
1419
|
+
export type SpawnListPageSerializeFreshness = "fresh" | "stale";
|
|
1420
|
+
/**
|
|
1421
|
+
* The state of one edge in the recorded topology: the strongest statement a
|
|
1422
|
+
* read can make about it, and nothing beyond the records and this read's own
|
|
1423
|
+
* evidence.
|
|
1424
|
+
*/
|
|
1425
|
+
export type SpawnListPageSerializeSpawnState = "unbound" | "bound" | "unresolved";
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* One page of spawn edges. `next` names the request id the page ended on.
|
|
1429
|
+
*/
|
|
1430
|
+
export interface SpawnListPageSerialize {
|
|
1431
|
+
next?: string | null;
|
|
1432
|
+
spawns: SpawnListPageSerializePublicSpawnTopology[];
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* One edge as a topology read reports it: the stored facts, plus the two things
|
|
1436
|
+
* a read derives instead of inferring.
|
|
1437
|
+
*/
|
|
1438
|
+
export interface SpawnListPageSerializePublicSpawnTopology {
|
|
1439
|
+
bound?: SpawnListPageSerializeBoundChild | null;
|
|
1440
|
+
claim_id?: string | null;
|
|
1441
|
+
created: SpawnListPageSerializeRequestOutcome;
|
|
1442
|
+
exit_observation?: SpawnListPageSerializePublicSpawnExitObservation | null;
|
|
1443
|
+
/**
|
|
1444
|
+
* Freshness of the recorded location against this read's evidence, in the
|
|
1445
|
+
* registry's one freshness vocabulary: `fresh` when the backend reports the
|
|
1446
|
+
* location now, `stale` when it does not — which is never evidence that the
|
|
1447
|
+
* child stopped. Absent when the edge recorded no location, because there is
|
|
1448
|
+
* nothing to verify.
|
|
1449
|
+
*/
|
|
1450
|
+
freshness?: SpawnListPageSerializeFreshness | null;
|
|
1451
|
+
launched: SpawnListPageSerializeRequestOutcome;
|
|
1452
|
+
location?: SpawnListPageSerializeRegistryLocation | null;
|
|
1453
|
+
parent: string;
|
|
1454
|
+
parent_session_id?: string | null;
|
|
1455
|
+
parent_subject_id?: string | null;
|
|
1456
|
+
recorded_at: string;
|
|
1457
|
+
request_id: string;
|
|
1458
|
+
state: SpawnListPageSerializeSpawnState;
|
|
1459
|
+
version: number;
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* The child a token bound: the registering incarnation's own registry identity.
|
|
1463
|
+
*/
|
|
1464
|
+
export interface SpawnListPageSerializeBoundChild {
|
|
1465
|
+
/**
|
|
1466
|
+
* When the edge bound to this incarnation.
|
|
1467
|
+
*/
|
|
1468
|
+
bound_at: string;
|
|
1469
|
+
incarnation: string;
|
|
1470
|
+
/**
|
|
1471
|
+
* Exact registry identity returned to the registering child. Absent in legacy records.
|
|
1472
|
+
*/
|
|
1473
|
+
registration_id?: string | null;
|
|
1474
|
+
source: string;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Where an agent's incarnation lives on one backend, in that backend's own
|
|
1478
|
+
* coordinates.
|
|
1479
|
+
*
|
|
1480
|
+
* The backend name is free-form so a source Radar does not ship yet is
|
|
1481
|
+
* preserved rather than rejected; the rest are the coordinates as reported, and
|
|
1482
|
+
* each is optional because a backend need not name every level.
|
|
1483
|
+
*/
|
|
1484
|
+
export interface SpawnListPageSerializeRegistryLocation {
|
|
1485
|
+
backend: string;
|
|
1486
|
+
instance?: string | null;
|
|
1487
|
+
pane?: string | null;
|
|
1488
|
+
tab?: string | null;
|
|
1489
|
+
workspace?: string | null;
|
|
1490
|
+
}
|