pi-radar 0.2.0 → 0.3.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 CHANGED
@@ -9,7 +9,7 @@ const radar = client.createRadarClient();
9
9
  const handshake = await radar.ping();
10
10
  ```
11
11
 
12
- Focused subpaths are also exported: `pi-radar/client`, `pi-radar/execution`, `pi-radar/publication`, `pi-radar/runtime`, `pi-radar/launch`, `pi-radar/location`, and `pi-radar/inspection`. The runtime subpath provides a versioned `pi.events` request/reply handoff so multiple extensions can consume one baseline publication without sharing bundled module singletons. `extension/radar-session-marker.ts` provides the versioned Pi custom-session-entry codec; it is correlation evidence only, not lifecycle authority. Existing extension deep imports under `pi-radar/extension/` remain exported.
12
+ Focused subpaths are also exported: `pi-radar/client`, `pi-radar/communication`, `pi-radar/execution`, `pi-radar/publication`, `pi-radar/runtime`, `pi-radar/launch`, `pi-radar/location`, and `pi-radar/inspection`. The shared `runtime` handle exposes `services.communication`, a handoff port bound to the baseline's existing client, this process's current registration, and the managed parent injected by Radar. Callers do not choose sender or recipient identity; `communication.createRadarHandoffPort` is the underlying SDK factory. The runtime subpath provides a versioned `pi.events` request/reply handoff so multiple extensions can consume one baseline publication without sharing bundled module singletons. `extension/radar-session-marker.ts` provides the versioned Pi custom-session-entry codec; it is correlation evidence only, not lifecycle authority. Existing extension deep imports under `pi-radar/extension/` remain exported.
13
13
 
14
14
  ## Pi baseline extension
15
15
 
@@ -17,15 +17,19 @@ Installing `pi-radar` directly as a Pi package loads `extension/baseline.ts`, wh
17
17
 
18
18
  ## Daemon contract
19
19
 
20
- The client speaks protocol 2 and requires wire revision 3 or newer. Revision 3 makes lifecycle `subject_id` a stable logical identity and exposes the exact current `execution_id` and `u32` generation separately; the generation remains exactly representable as a JSON number. 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.
20
+ The client speaks protocol 2 and requires wire revision 3 or newer for its existing lifecycle operations. Revision 3 made lifecycle `subject_id` a stable logical identity and exposed the exact current `execution_id` and `u32` generation separately. Handoff methods additionally require the independently negotiated `communication.handoff.v1` contract introduced at wire revision 4; 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.
21
21
 
22
- 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`, and event reads require `events.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.
22
+ 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.
23
23
 
24
24
  `RadarFactsSnapshot` matches the daemon facts channel: optional model/provider/thinking/session name, `context_window: { tokens, window }`, typed `background_tasks: { id, phase }[]`, and `background_started_unix_ms`. Context counts and the timestamp are JSON numbers bounded by JavaScript's safe-integer limit. The old text `context_usage` field is not accepted; Rust retains cross-field validation such as a positive window and tokens not exceeding it.
25
25
 
26
+ ## Durable handoffs
27
+
28
+ 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.
29
+
26
30
  ## Generated protocol contracts
27
31
 
28
- 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/`. Serialize and deserialize schemas are separate because Serde defaults, null handling and omission differ by direction. The SDK's ping, managed-launch-metadata input, facts-input and spawn-exit types are generated into `extension/generated-contracts.ts`; do not edit that file by hand.
32
+ 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/`. Serialize and deserialize schemas are separate because Serde defaults, null handling and omission differ by direction. The SDK's ping, handoff inbox, publication-parameter, immutable handoff, receipt, managed-launch-metadata input, facts-input and spawn-exit types are generated into `extension/generated-contracts.ts`; do not edit that file by hand.
29
33
 
30
34
  From the repository checkout, run `npm run generate:contracts` in this directory to regenerate schemas and TypeScript declarations. `npm test` checks the generated artifacts are current, typechecks the public SDK aliases, validates canonical wire fixtures and integer bounds against the schemas, and runs the package tests. Generation uses the locked Schemars crate plus pinned `json-schema-to-typescript` and TypeScript development dependencies; it adds no runtime package dependency.
31
35
 
@@ -15,6 +15,7 @@ var RADAR_SPAWN_CONTRACT = "spawn.v1";
15
15
  var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
16
16
  var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
17
17
  var RADAR_EVENTS_CONTRACT = "events.v1";
18
+ var RADAR_COMMUNICATION_CONTRACT = "communication.handoff.v1";
18
19
  var RADAR_REQUESTER = "pi-radar";
19
20
  var RADAR_MAX_LINE_BYTES = 1024 * 1024;
20
21
  var RADAR_CALL_TIMEOUT_MS = 2e3;
@@ -122,6 +123,28 @@ function numberAt(value, field) {
122
123
  const found = value[field];
123
124
  return typeof found === "number" && Number.isFinite(found) ? found : void 0;
124
125
  }
126
+ function handoffAt(value) {
127
+ const version = numberAt(value, "version");
128
+ const id = stringAt(value, "id");
129
+ const sender = recordAt(value, "sender");
130
+ const senderGeneration = numberAt(sender, "execution_generation");
131
+ const target = stringAt(value, "recipient_subject_id");
132
+ const summary = stringAt(value, "summary");
133
+ const content = stringAt(value, "content");
134
+ const published = numberAt(value, "published_at_unix_ms");
135
+ const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
136
+ if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "agent_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) || (senderGeneration ?? 0) < 1 || !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;
137
+ return value;
138
+ }
139
+ function handoffReceiptAt(value) {
140
+ const version = numberAt(value, "version");
141
+ const handoffId = stringAt(value, "handoff_id");
142
+ const recipient = recordAt(value, "recipient");
143
+ const generation = numberAt(recipient, "execution_generation");
144
+ const received = numberAt(value, "received_at_unix_ms");
145
+ if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "agent_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) || (generation ?? 0) < 1 || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
146
+ return value;
147
+ }
125
148
  function writerBinding(value) {
126
149
  const handle = stringAt(value, "handle");
127
150
  const source = stringAt(value, "source");
@@ -227,7 +250,8 @@ var RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
227
250
  var RADAR_MANAGED_IDENTITY_ENV = /* @__PURE__ */ new Set([
228
251
  "PI_RADAR_MANAGED_SUBJECT_ID",
229
252
  "PI_RADAR_MANAGED_LAUNCH_ID",
230
- "PI_RADAR_MANAGED_EXECUTION_GENERATION"
253
+ "PI_RADAR_MANAGED_EXECUTION_GENERATION",
254
+ "PI_RADAR_MANAGED_PARENT_SUBJECT_ID"
231
255
  ]);
232
256
  function unresolvedSpawn(requestId, detail) {
233
257
  return {
@@ -843,6 +867,56 @@ function createRadarClient(options = {}) {
843
867
  return protocol("spawn.close reply carries no close outcome");
844
868
  });
845
869
  },
870
+ async communicationPublish(request) {
871
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
872
+ const answer = await call("communication.publish", {
873
+ sender_agent_id: request.sender_agent_id,
874
+ recipient_subject_id: request.recipient_subject_id,
875
+ ...request.correlation_id !== void 0 ? { correlation_id: request.correlation_id } : {},
876
+ summary: request.summary,
877
+ content: request.content
878
+ }, request.request_id);
879
+ if (answer.ok === false) return answer;
880
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
881
+ const created = recordAt(answer.value, "created");
882
+ if (handoff === void 0 || typeof created !== "boolean")
883
+ return protocol("communication.publish reply carries no handoff");
884
+ return { ok: true, value: { handoff, created } };
885
+ });
886
+ },
887
+ async communicationInbox(request) {
888
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
889
+ const answer = await call("communication.inbox", request);
890
+ if (answer.ok === false) return answer;
891
+ const handoffIds = recordAt(answer.value, "handoff_ids");
892
+ if (!Array.isArray(handoffIds) || handoffIds.some((id) => !isUuid(id)))
893
+ return protocol("communication.inbox reply carries malformed handoff IDs");
894
+ return { ok: true, value: answer.value };
895
+ });
896
+ },
897
+ async communicationGet(request) {
898
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
899
+ const answer = await call("communication.get", request);
900
+ if (answer.ok === false) return answer;
901
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
902
+ const receiptValue = recordAt(answer.value, "receipt");
903
+ const receipt = receiptValue === null ? null : handoffReceiptAt(receiptValue);
904
+ if (handoff === void 0 || receiptValue !== null && receipt === void 0)
905
+ return protocol("communication.get reply carries malformed content or receipt");
906
+ return { ok: true, value: { handoff, receipt: receipt ?? null } };
907
+ });
908
+ },
909
+ async communicationAck(request) {
910
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
911
+ const answer = await call("communication.ack", request);
912
+ if (answer.ok === false) return answer;
913
+ const receipt = handoffReceiptAt(recordAt(answer.value, "receipt"));
914
+ const created = recordAt(answer.value, "created");
915
+ if (receipt === void 0 || typeof created !== "boolean")
916
+ return protocol("communication.ack reply carries no receipt");
917
+ return { ok: true, value: { receipt, created } };
918
+ });
919
+ },
846
920
  async events(request) {
847
921
  if (!Number.isSafeInteger(request.after) || request.after < 0 || !Number.isSafeInteger(request.wait_ms) || request.wait_ms < 0 || request.wait_ms > RADAR_EVENT_WAIT_MS)
848
922
  return {
@@ -1063,6 +1137,43 @@ function boundedRadarSnapshot(snapshot) {
1063
1137
  };
1064
1138
  }
1065
1139
 
1140
+ // extension/radar-communication.ts
1141
+ function createRadarHandoffPort(client, publication, parentSubjectId = () => process.env.PI_RADAR_MANAGED_PARENT_SUBJECT_ID) {
1142
+ async function agentId() {
1143
+ const subject = await publication.subject();
1144
+ return subject === void 0 ? { ok: false, code: "absent", message: "this process has no current Radar registration" } : { ok: true, value: subject.agent_id };
1145
+ }
1146
+ return {
1147
+ async publish(request) {
1148
+ const identity = await agentId();
1149
+ if (identity.ok === false) return identity;
1150
+ const parent = parentSubjectId();
1151
+ if (parent === void 0)
1152
+ return { ok: false, code: "absent", message: "this managed execution has no Radar parent subject" };
1153
+ return client.communicationPublish({
1154
+ ...request,
1155
+ sender_agent_id: identity.value,
1156
+ recipient_subject_id: parent
1157
+ });
1158
+ },
1159
+ async inbox() {
1160
+ const identity = await agentId();
1161
+ if (identity.ok === false) return identity;
1162
+ return client.communicationInbox({ agent_id: identity.value });
1163
+ },
1164
+ async get(handoff_id) {
1165
+ const identity = await agentId();
1166
+ if (identity.ok === false) return identity;
1167
+ return client.communicationGet({ agent_id: identity.value, handoff_id });
1168
+ },
1169
+ async ack(handoff_id) {
1170
+ const identity = await agentId();
1171
+ if (identity.ok === false) return identity;
1172
+ return client.communicationAck({ agent_id: identity.value, handoff_id });
1173
+ }
1174
+ };
1175
+ }
1176
+
1066
1177
  // extension/radar-publication.ts
1067
1178
  import { createHash, randomUUID as randomUUID2 } from "node:crypto";
1068
1179
  import {
@@ -2190,8 +2301,10 @@ function createRadarPublicationInner(options) {
2190
2301
  const record = await ensureRegistration();
2191
2302
  if (record === void 0) return void 0;
2192
2303
  const location = record.registration.location;
2304
+ const subjectId = process.env.PI_RADAR_MANAGED_SUBJECT_ID;
2193
2305
  return {
2194
2306
  agent_id: agentId2,
2307
+ ...subjectId !== void 0 ? { subject_id: subjectId } : {},
2195
2308
  ...location !== void 0 ? { location } : {}
2196
2309
  };
2197
2310
  },
@@ -2589,7 +2702,7 @@ async function resolveRadarProcessLocation(client, env = process.env) {
2589
2702
 
2590
2703
  // extension/radar-runtime.ts
2591
2704
  var RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
2592
- var RADAR_RUNTIME_VERSION = 1;
2705
+ var RADAR_RUNTIME_VERSION = 2;
2593
2706
  function lazyRadarRuntime(start) {
2594
2707
  let pending;
2595
2708
  return {
@@ -2617,6 +2730,7 @@ function registerPiRadar(pi) {
2617
2730
  });
2618
2731
  const runtime = lazyRadarRuntime(async () => ({
2619
2732
  publication: execution.publication,
2733
+ communication: createRadarHandoffPort(client, execution.publication),
2620
2734
  execution
2621
2735
  }));
2622
2736
  provideRadarRuntime(pi.events, runtime);
@@ -11,6 +11,7 @@ var RADAR_SPAWN_CONTRACT = "spawn.v1";
11
11
  var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
12
12
  var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
13
13
  var RADAR_EVENTS_CONTRACT = "events.v1";
14
+ var RADAR_COMMUNICATION_CONTRACT = "communication.handoff.v1";
14
15
  var RADAR_REQUESTER = "pi-radar";
15
16
  var RADAR_MAX_LINE_BYTES = 1024 * 1024;
16
17
  var RADAR_CALL_TIMEOUT_MS = 2e3;
@@ -118,6 +119,28 @@ function numberAt(value, field) {
118
119
  const found = value[field];
119
120
  return typeof found === "number" && Number.isFinite(found) ? found : void 0;
120
121
  }
122
+ function handoffAt(value) {
123
+ const version = numberAt(value, "version");
124
+ const id = stringAt(value, "id");
125
+ const sender = recordAt(value, "sender");
126
+ const senderGeneration = numberAt(sender, "execution_generation");
127
+ const target = stringAt(value, "recipient_subject_id");
128
+ const summary = stringAt(value, "summary");
129
+ const content = stringAt(value, "content");
130
+ const published = numberAt(value, "published_at_unix_ms");
131
+ const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
132
+ if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "agent_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) || (senderGeneration ?? 0) < 1 || !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;
133
+ return value;
134
+ }
135
+ function handoffReceiptAt(value) {
136
+ const version = numberAt(value, "version");
137
+ const handoffId = stringAt(value, "handoff_id");
138
+ const recipient = recordAt(value, "recipient");
139
+ const generation = numberAt(recipient, "execution_generation");
140
+ const received = numberAt(value, "received_at_unix_ms");
141
+ if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "agent_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) || (generation ?? 0) < 1 || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
142
+ return value;
143
+ }
121
144
  function writerBinding(value) {
122
145
  const handle = stringAt(value, "handle");
123
146
  const source = stringAt(value, "source");
@@ -227,7 +250,8 @@ var RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
227
250
  var RADAR_MANAGED_IDENTITY_ENV = /* @__PURE__ */ new Set([
228
251
  "PI_RADAR_MANAGED_SUBJECT_ID",
229
252
  "PI_RADAR_MANAGED_LAUNCH_ID",
230
- "PI_RADAR_MANAGED_EXECUTION_GENERATION"
253
+ "PI_RADAR_MANAGED_EXECUTION_GENERATION",
254
+ "PI_RADAR_MANAGED_PARENT_SUBJECT_ID"
231
255
  ]);
232
256
  function unresolvedSpawn(requestId, detail) {
233
257
  return {
@@ -901,6 +925,56 @@ function createRadarClient(options = {}) {
901
925
  return protocol("spawn.close reply carries no close outcome");
902
926
  });
903
927
  },
928
+ async communicationPublish(request) {
929
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
930
+ const answer = await call("communication.publish", {
931
+ sender_agent_id: request.sender_agent_id,
932
+ recipient_subject_id: request.recipient_subject_id,
933
+ ...request.correlation_id !== void 0 ? { correlation_id: request.correlation_id } : {},
934
+ summary: request.summary,
935
+ content: request.content
936
+ }, request.request_id);
937
+ if (answer.ok === false) return answer;
938
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
939
+ const created = recordAt(answer.value, "created");
940
+ if (handoff === void 0 || typeof created !== "boolean")
941
+ return protocol("communication.publish reply carries no handoff");
942
+ return { ok: true, value: { handoff, created } };
943
+ });
944
+ },
945
+ async communicationInbox(request) {
946
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
947
+ const answer = await call("communication.inbox", request);
948
+ if (answer.ok === false) return answer;
949
+ const handoffIds = recordAt(answer.value, "handoff_ids");
950
+ if (!Array.isArray(handoffIds) || handoffIds.some((id) => !isUuid(id)))
951
+ return protocol("communication.inbox reply carries malformed handoff IDs");
952
+ return { ok: true, value: answer.value };
953
+ });
954
+ },
955
+ async communicationGet(request) {
956
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
957
+ const answer = await call("communication.get", request);
958
+ if (answer.ok === false) return answer;
959
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
960
+ const receiptValue = recordAt(answer.value, "receipt");
961
+ const receipt = receiptValue === null ? null : handoffReceiptAt(receiptValue);
962
+ if (handoff === void 0 || receiptValue !== null && receipt === void 0)
963
+ return protocol("communication.get reply carries malformed content or receipt");
964
+ return { ok: true, value: { handoff, receipt: receipt ?? null } };
965
+ });
966
+ },
967
+ async communicationAck(request) {
968
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
969
+ const answer = await call("communication.ack", request);
970
+ if (answer.ok === false) return answer;
971
+ const receipt = handoffReceiptAt(recordAt(answer.value, "receipt"));
972
+ const created = recordAt(answer.value, "created");
973
+ if (receipt === void 0 || typeof created !== "boolean")
974
+ return protocol("communication.ack reply carries no receipt");
975
+ return { ok: true, value: { receipt, created } };
976
+ });
977
+ },
904
978
  async events(request) {
905
979
  if (!Number.isSafeInteger(request.after) || request.after < 0 || !Number.isSafeInteger(request.wait_ms) || request.wait_ms < 0 || request.wait_ms > RADAR_EVENT_WAIT_MS)
906
980
  return {
@@ -1122,6 +1196,7 @@ function boundedRadarSnapshot(snapshot) {
1122
1196
  }
1123
1197
  export {
1124
1198
  RADAR_CALL_TIMEOUT_MS,
1199
+ RADAR_COMMUNICATION_CONTRACT,
1125
1200
  RADAR_EVENTS_CONTRACT,
1126
1201
  RADAR_EVENT_RETRY_MS,
1127
1202
  RADAR_MAX_AWAITED,
@@ -0,0 +1,39 @@
1
+ // extension/radar-communication.ts
2
+ function createRadarHandoffPort(client, publication, parentSubjectId = () => process.env.PI_RADAR_MANAGED_PARENT_SUBJECT_ID) {
3
+ async function agentId() {
4
+ const subject = await publication.subject();
5
+ return subject === void 0 ? { ok: false, code: "absent", message: "this process has no current Radar registration" } : { ok: true, value: subject.agent_id };
6
+ }
7
+ return {
8
+ async publish(request) {
9
+ const identity = await agentId();
10
+ if (identity.ok === false) return identity;
11
+ const parent = parentSubjectId();
12
+ if (parent === void 0)
13
+ return { ok: false, code: "absent", message: "this managed execution has no Radar parent subject" };
14
+ return client.communicationPublish({
15
+ ...request,
16
+ sender_agent_id: identity.value,
17
+ recipient_subject_id: parent
18
+ });
19
+ },
20
+ async inbox() {
21
+ const identity = await agentId();
22
+ if (identity.ok === false) return identity;
23
+ return client.communicationInbox({ agent_id: identity.value });
24
+ },
25
+ async get(handoff_id) {
26
+ const identity = await agentId();
27
+ if (identity.ok === false) return identity;
28
+ return client.communicationGet({ agent_id: identity.value, handoff_id });
29
+ },
30
+ async ack(handoff_id) {
31
+ const identity = await agentId();
32
+ if (identity.ok === false) return identity;
33
+ return client.communicationAck({ agent_id: identity.value, handoff_id });
34
+ }
35
+ };
36
+ }
37
+ export {
38
+ createRadarHandoffPort
39
+ };
@@ -1221,8 +1221,10 @@ function createRadarPublicationInner(options) {
1221
1221
  const record = await ensureRegistration();
1222
1222
  if (record === void 0) return void 0;
1223
1223
  const location = record.registration.location;
1224
+ const subjectId = process.env.PI_RADAR_MANAGED_SUBJECT_ID;
1224
1225
  return {
1225
1226
  agent_id: agentId2,
1227
+ ...subjectId !== void 0 ? { subject_id: subjectId } : {},
1226
1228
  ...location !== void 0 ? { location } : {}
1227
1229
  };
1228
1230
  },
@@ -146,6 +146,10 @@ async function launchRadarWorker(options) {
146
146
  return refusedLaunch(
147
147
  "the launch has no exact parent registration to spawn under"
148
148
  );
149
+ if (!isUuid(launch.parent.subject_id))
150
+ return refusedLaunch(
151
+ "the launch has no managed Radar subject for its parent"
152
+ );
149
153
  if (pane === void 0 || pane === "")
150
154
  return refusedLaunch(
151
155
  "this process's Radar registration names no pane, so it cannot be a spawn parent"
@@ -155,6 +159,7 @@ async function launchRadarWorker(options) {
155
159
  parent: launch.parent.agent_id,
156
160
  parent_session_id: launch.parent_session_id,
157
161
  executable: launch.executable,
162
+ managed: { parent_subject_id: launch.parent.subject_id },
158
163
  argv: launch.argv,
159
164
  ...launch.cwd !== "" ? { cwd: launch.cwd } : {},
160
165
  ...Object.keys(launch.env).length > 0 ? { env: launch.env } : {}
@@ -1176,8 +1176,10 @@ function createRadarPublicationInner(options) {
1176
1176
  const record = await ensureRegistration();
1177
1177
  if (record === void 0) return void 0;
1178
1178
  const location = record.registration.location;
1179
+ const subjectId = process.env.PI_RADAR_MANAGED_SUBJECT_ID;
1179
1180
  return {
1180
1181
  agent_id: agentId2,
1182
+ ...subjectId !== void 0 ? { subject_id: subjectId } : {},
1181
1183
  ...location !== void 0 ? { location } : {}
1182
1184
  };
1183
1185
  },
@@ -1,6 +1,6 @@
1
1
  // extension/radar-runtime.ts
2
2
  var RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
3
- var RADAR_RUNTIME_VERSION = 1;
3
+ var RADAR_RUNTIME_VERSION = 2;
4
4
  function lazyRadarRuntime(start) {
5
5
  let pending;
6
6
  return {
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ var __export = (target, all) => {
8
8
  var radar_client_exports = {};
9
9
  __export(radar_client_exports, {
10
10
  RADAR_CALL_TIMEOUT_MS: () => RADAR_CALL_TIMEOUT_MS,
11
+ RADAR_COMMUNICATION_CONTRACT: () => RADAR_COMMUNICATION_CONTRACT,
11
12
  RADAR_EVENTS_CONTRACT: () => RADAR_EVENTS_CONTRACT,
12
13
  RADAR_EVENT_RETRY_MS: () => RADAR_EVENT_RETRY_MS,
13
14
  RADAR_MAX_AWAITED: () => RADAR_MAX_AWAITED,
@@ -45,6 +46,7 @@ var RADAR_SPAWN_CONTRACT = "spawn.v1";
45
46
  var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
46
47
  var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
47
48
  var RADAR_EVENTS_CONTRACT = "events.v1";
49
+ var RADAR_COMMUNICATION_CONTRACT = "communication.handoff.v1";
48
50
  var RADAR_REQUESTER = "pi-radar";
49
51
  var RADAR_MAX_LINE_BYTES = 1024 * 1024;
50
52
  var RADAR_CALL_TIMEOUT_MS = 2e3;
@@ -152,6 +154,28 @@ function numberAt(value, field) {
152
154
  const found = value[field];
153
155
  return typeof found === "number" && Number.isFinite(found) ? found : void 0;
154
156
  }
157
+ function handoffAt(value) {
158
+ const version = numberAt(value, "version");
159
+ const id = stringAt(value, "id");
160
+ const sender = recordAt(value, "sender");
161
+ const senderGeneration = numberAt(sender, "execution_generation");
162
+ const target = stringAt(value, "recipient_subject_id");
163
+ const summary = stringAt(value, "summary");
164
+ const content = stringAt(value, "content");
165
+ const published = numberAt(value, "published_at_unix_ms");
166
+ const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
167
+ if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "agent_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) || (senderGeneration ?? 0) < 1 || !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;
168
+ return value;
169
+ }
170
+ function handoffReceiptAt(value) {
171
+ const version = numberAt(value, "version");
172
+ const handoffId = stringAt(value, "handoff_id");
173
+ const recipient = recordAt(value, "recipient");
174
+ const generation = numberAt(recipient, "execution_generation");
175
+ const received = numberAt(value, "received_at_unix_ms");
176
+ if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "agent_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) || (generation ?? 0) < 1 || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
177
+ return value;
178
+ }
155
179
  function writerBinding(value) {
156
180
  const handle = stringAt(value, "handle");
157
181
  const source = stringAt(value, "source");
@@ -261,7 +285,8 @@ var RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
261
285
  var RADAR_MANAGED_IDENTITY_ENV = /* @__PURE__ */ new Set([
262
286
  "PI_RADAR_MANAGED_SUBJECT_ID",
263
287
  "PI_RADAR_MANAGED_LAUNCH_ID",
264
- "PI_RADAR_MANAGED_EXECUTION_GENERATION"
288
+ "PI_RADAR_MANAGED_EXECUTION_GENERATION",
289
+ "PI_RADAR_MANAGED_PARENT_SUBJECT_ID"
265
290
  ]);
266
291
  function unresolvedSpawn(requestId, detail) {
267
292
  return {
@@ -935,6 +960,56 @@ function createRadarClient(options = {}) {
935
960
  return protocol("spawn.close reply carries no close outcome");
936
961
  });
937
962
  },
963
+ async communicationPublish(request) {
964
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
965
+ const answer = await call("communication.publish", {
966
+ sender_agent_id: request.sender_agent_id,
967
+ recipient_subject_id: request.recipient_subject_id,
968
+ ...request.correlation_id !== void 0 ? { correlation_id: request.correlation_id } : {},
969
+ summary: request.summary,
970
+ content: request.content
971
+ }, request.request_id);
972
+ if (answer.ok === false) return answer;
973
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
974
+ const created = recordAt(answer.value, "created");
975
+ if (handoff === void 0 || typeof created !== "boolean")
976
+ return protocol("communication.publish reply carries no handoff");
977
+ return { ok: true, value: { handoff, created } };
978
+ });
979
+ },
980
+ async communicationInbox(request) {
981
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
982
+ const answer = await call("communication.inbox", request);
983
+ if (answer.ok === false) return answer;
984
+ const handoffIds = recordAt(answer.value, "handoff_ids");
985
+ if (!Array.isArray(handoffIds) || handoffIds.some((id) => !isUuid(id)))
986
+ return protocol("communication.inbox reply carries malformed handoff IDs");
987
+ return { ok: true, value: answer.value };
988
+ });
989
+ },
990
+ async communicationGet(request) {
991
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
992
+ const answer = await call("communication.get", request);
993
+ if (answer.ok === false) return answer;
994
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
995
+ const receiptValue = recordAt(answer.value, "receipt");
996
+ const receipt = receiptValue === null ? null : handoffReceiptAt(receiptValue);
997
+ if (handoff === void 0 || receiptValue !== null && receipt === void 0)
998
+ return protocol("communication.get reply carries malformed content or receipt");
999
+ return { ok: true, value: { handoff, receipt: receipt ?? null } };
1000
+ });
1001
+ },
1002
+ async communicationAck(request) {
1003
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1004
+ const answer = await call("communication.ack", request);
1005
+ if (answer.ok === false) return answer;
1006
+ const receipt = handoffReceiptAt(recordAt(answer.value, "receipt"));
1007
+ const created = recordAt(answer.value, "created");
1008
+ if (receipt === void 0 || typeof created !== "boolean")
1009
+ return protocol("communication.ack reply carries no receipt");
1010
+ return { ok: true, value: { receipt, created } };
1011
+ });
1012
+ },
938
1013
  async events(request) {
939
1014
  if (!Number.isSafeInteger(request.after) || request.after < 0 || !Number.isSafeInteger(request.wait_ms) || request.wait_ms < 0 || request.wait_ms > RADAR_EVENT_WAIT_MS)
940
1015
  return {
@@ -1155,6 +1230,47 @@ function boundedRadarSnapshot(snapshot) {
1155
1230
  };
1156
1231
  }
1157
1232
 
1233
+ // extension/radar-communication.ts
1234
+ var radar_communication_exports = {};
1235
+ __export(radar_communication_exports, {
1236
+ createRadarHandoffPort: () => createRadarHandoffPort
1237
+ });
1238
+ function createRadarHandoffPort(client, publication, parentSubjectId = () => process.env.PI_RADAR_MANAGED_PARENT_SUBJECT_ID) {
1239
+ async function agentId() {
1240
+ const subject = await publication.subject();
1241
+ return subject === void 0 ? { ok: false, code: "absent", message: "this process has no current Radar registration" } : { ok: true, value: subject.agent_id };
1242
+ }
1243
+ return {
1244
+ async publish(request) {
1245
+ const identity = await agentId();
1246
+ if (identity.ok === false) return identity;
1247
+ const parent = parentSubjectId();
1248
+ if (parent === void 0)
1249
+ return { ok: false, code: "absent", message: "this managed execution has no Radar parent subject" };
1250
+ return client.communicationPublish({
1251
+ ...request,
1252
+ sender_agent_id: identity.value,
1253
+ recipient_subject_id: parent
1254
+ });
1255
+ },
1256
+ async inbox() {
1257
+ const identity = await agentId();
1258
+ if (identity.ok === false) return identity;
1259
+ return client.communicationInbox({ agent_id: identity.value });
1260
+ },
1261
+ async get(handoff_id) {
1262
+ const identity = await agentId();
1263
+ if (identity.ok === false) return identity;
1264
+ return client.communicationGet({ agent_id: identity.value, handoff_id });
1265
+ },
1266
+ async ack(handoff_id) {
1267
+ const identity = await agentId();
1268
+ if (identity.ok === false) return identity;
1269
+ return client.communicationAck({ agent_id: identity.value, handoff_id });
1270
+ }
1271
+ };
1272
+ }
1273
+
1158
1274
  // extension/radar-execution.ts
1159
1275
  var radar_execution_exports = {};
1160
1276
  __export(radar_execution_exports, {
@@ -2307,8 +2423,10 @@ function createRadarPublicationInner(options) {
2307
2423
  const record = await ensureRegistration();
2308
2424
  if (record === void 0) return void 0;
2309
2425
  const location = record.registration.location;
2426
+ const subjectId = process.env.PI_RADAR_MANAGED_SUBJECT_ID;
2310
2427
  return {
2311
2428
  agent_id: agentId2,
2429
+ ...subjectId !== void 0 ? { subject_id: subjectId } : {},
2312
2430
  ...location !== void 0 ? { location } : {}
2313
2431
  };
2314
2432
  },
@@ -2910,6 +3028,10 @@ async function launchRadarWorker(options) {
2910
3028
  return refusedLaunch(
2911
3029
  "the launch has no exact parent registration to spawn under"
2912
3030
  );
3031
+ if (!isUuid(launch.parent.subject_id))
3032
+ return refusedLaunch(
3033
+ "the launch has no managed Radar subject for its parent"
3034
+ );
2913
3035
  if (pane === void 0 || pane === "")
2914
3036
  return refusedLaunch(
2915
3037
  "this process's Radar registration names no pane, so it cannot be a spawn parent"
@@ -2919,6 +3041,7 @@ async function launchRadarWorker(options) {
2919
3041
  parent: launch.parent.agent_id,
2920
3042
  parent_session_id: launch.parent_session_id,
2921
3043
  executable: launch.executable,
3044
+ managed: { parent_subject_id: launch.parent.subject_id },
2922
3045
  argv: launch.argv,
2923
3046
  ...launch.cwd !== "" ? { cwd: launch.cwd } : {},
2924
3047
  ...Object.keys(launch.env).length > 0 ? { env: launch.env } : {}
@@ -3056,7 +3179,7 @@ __export(radar_runtime_exports, {
3056
3179
  requestRadarRuntime: () => requestRadarRuntime
3057
3180
  });
3058
3181
  var RADAR_RUNTIME_REQUEST = "pi-radar/runtime-request";
3059
- var RADAR_RUNTIME_VERSION = 1;
3182
+ var RADAR_RUNTIME_VERSION = 2;
3060
3183
  function lazyRadarRuntime(start) {
3061
3184
  let pending;
3062
3185
  return {
@@ -3091,6 +3214,7 @@ function isRadarRuntimeHandle(value) {
3091
3214
  }
3092
3215
  export {
3093
3216
  radar_client_exports as client,
3217
+ radar_communication_exports as communication,
3094
3218
  radar_execution_exports as execution,
3095
3219
  radar_inspection_exports as inspection,
3096
3220
  radar_launch_exports as launch,
@@ -1,6 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { createRadarClient } from "./radar-client.ts";
4
+ import { createRadarHandoffPort } from "./radar-communication.ts";
4
5
  import {
5
6
  createRadarExecutionAdapter,
6
7
  type RadarExecutionEventApi,
@@ -31,6 +32,7 @@ export default function registerPiRadar(pi: PiRadarHost): void {
31
32
  });
32
33
  const runtime = lazyRadarRuntime(async () => ({
33
34
  publication: execution.publication,
35
+ communication: createRadarHandoffPort(client, execution.publication),
34
36
  execution,
35
37
  }));
36
38
  provideRadarRuntime(pi.events, runtime);