pi-radar 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +10 -4
  2. package/dist/extension/baseline.js +333 -394
  3. package/dist/extension/radar-client.js +126 -11
  4. package/dist/extension/radar-communication.js +39 -0
  5. package/dist/extension/radar-execution.js +169 -382
  6. package/dist/extension/radar-launch.js +18 -13
  7. package/dist/extension/radar-publication.js +161 -298
  8. package/dist/extension/radar-runtime.js +1 -1
  9. package/dist/index.js +356 -407
  10. package/extension/baseline.ts +2 -0
  11. package/extension/generated-contracts.ts +1310 -10
  12. package/extension/radar-client.ts +191 -23
  13. package/extension/radar-communication.ts +66 -0
  14. package/extension/radar-execution.ts +11 -58
  15. package/extension/radar-launch.ts +29 -22
  16. package/extension/radar-publication.ts +211 -410
  17. package/extension/radar-runtime.ts +4 -1
  18. package/index.ts +1 -0
  19. package/package.json +9 -5
  20. package/schema/public/v1/handoff-inbox.deserialize.schema.json +18 -0
  21. package/schema/public/v1/handoff-inbox.serialize.schema.json +18 -0
  22. package/schema/public/v1/handoff-publish-params.deserialize.schema.json +33 -0
  23. package/schema/public/v1/handoff-publish-params.serialize.schema.json +33 -0
  24. package/schema/public/v1/handoff-receipt.deserialize.schema.json +66 -0
  25. package/schema/public/v1/handoff-receipt.serialize.schema.json +66 -0
  26. package/schema/public/v1/handoff.deserialize.schema.json +84 -0
  27. package/schema/public/v1/handoff.serialize.schema.json +84 -0
  28. package/schema/public/v1/observation-get-params.deserialize.schema.json +46 -0
  29. package/schema/public/v1/observation-get-params.serialize.schema.json +52 -0
  30. package/schema/public/v1/observation-get-response.deserialize.schema.json +2731 -0
  31. package/schema/public/v1/observation-get-response.serialize.schema.json +2862 -0
  32. package/schema/public/v1/observation-snapshot.deserialize.schema.json +2616 -0
  33. package/schema/public/v1/observation-snapshot.serialize.schema.json +2741 -0
  34. package/schema/public/v1/presence-answer.deserialize.schema.json +40 -0
  35. package/schema/public/v1/presence-answer.serialize.schema.json +40 -0
  36. package/schema/public/v1/presence-release.deserialize.schema.json +51 -0
  37. package/schema/public/v1/presence-release.serialize.schema.json +52 -0
  38. package/schema/public/v1/presence-report.deserialize.schema.json +149 -0
  39. package/schema/public/v1/presence-report.serialize.schema.json +157 -0
  40. package/extension/radar-session-marker.ts +0 -146
  41. package/schema/public/v1/launch-identity-marker.deserialize.schema.json +0 -32
  42. package/schema/public/v1/launch-identity-marker.serialize.schema.json +0 -32
@@ -4,8 +4,11 @@ import { createConnection } from "node:net";
4
4
  import { dirname, join } from "node:path";
5
5
  import type {
6
6
  DaemonPingSerialize,
7
- LaunchIdentityMarkerSerialize,
8
7
  FactsSnapshotDeserialize,
8
+ HandoffInboxSerialize,
9
+ HandoffReceiptSerialize,
10
+ HandoffPublishParamsDeserialize,
11
+ HandoffSerialize,
9
12
  ManagedLaunchMetadataDeserialize,
10
13
  SpawnExitObservationSerialize,
11
14
  } from "./generated-contracts.ts";
@@ -36,6 +39,7 @@ export const RADAR_SPAWN_CONTRACT = "spawn.v1";
36
39
  export const RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
37
40
  export const RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
38
41
  export const RADAR_EVENTS_CONTRACT = "events.v1";
42
+ export const RADAR_COMMUNICATION_CONTRACT = "communication.handoff.v1";
39
43
  /** The default audit identity; callers can supply their own requester. */
40
44
  export const RADAR_REQUESTER = "pi-radar";
41
45
  /** The daemon's own line bound; nothing longer is sent or accepted. */
@@ -109,6 +113,10 @@ export type RadarLocation = {
109
113
  pane?: string;
110
114
  };
111
115
 
116
+ export type RadarPresenceReport = import("./generated-contracts.ts").PresenceReportDeserialize;
117
+ export type RadarPresenceAnswer = import("./generated-contracts.ts").PresenceAnswerSerialize;
118
+ export type RadarPresenceRelease = import("./generated-contracts.ts").PresenceReleaseDeserialize;
119
+
112
120
  export type RadarProcessClaim = {
113
121
  boot_id: string;
114
122
  pid: number;
@@ -162,6 +170,9 @@ export type RadarBackgroundTask = NonNullable<
162
170
  FactsSnapshotDeserialize["background_tasks"]
163
171
  >[number];
164
172
  export type RadarFactsSnapshot = FactsSnapshotDeserialize;
173
+ export type RadarHandoff = HandoffSerialize;
174
+ export type RadarHandoffInbox = HandoffInboxSerialize;
175
+ export type RadarHandoffReceipt = HandoffReceiptSerialize;
165
176
 
166
177
  /**
167
178
  * The assignment channel's interim fields. `request` is the owner's active
@@ -196,7 +207,6 @@ export type RadarWriterBinding = {
196
207
  */
197
208
  export type RadarContextValue = {
198
209
  session: string | null;
199
- launch_identity?: LaunchIdentityMarkerSerialize;
200
210
  };
201
211
 
202
212
  /**
@@ -278,7 +288,7 @@ export type RadarEffectOutcome = "completed" | "refused" | "unknown";
278
288
  /**
279
289
  * One durable spawn edge as the daemon publishes it (`spawn.get`).
280
290
  *
281
- * `bound.agent_id` is the exact registry identity the child obtained with the
291
+ * `bound.registration_id` is the exact registry identity the child obtained with the
282
292
  * launch's private token, which is the only thing that says *this* child belongs
283
293
  * to *this* spawn: a label, a pane or a session UUID would each be a guess.
284
294
  */
@@ -288,7 +298,7 @@ export type RadarSpawnEdge = {
288
298
  created: RadarEffectOutcome;
289
299
  launched: RadarEffectOutcome;
290
300
  location?: RadarLocation;
291
- bound?: { source: string; incarnation: string; agent_id?: string };
301
+ bound?: { source: string; incarnation: string; registration_id?: string };
292
302
  exit_observation?: RadarSpawnExitObservation;
293
303
  };
294
304
 
@@ -299,7 +309,6 @@ export type RadarSpawnEdge = {
299
309
  * no exit was observed.
300
310
  */
301
311
  export type RadarSpawnExitObservation = SpawnExitObservationSerialize;
302
- export type RadarLaunchIdentityMarker = LaunchIdentityMarkerSerialize;
303
312
 
304
313
  /** Durable Radar-owned metadata accepted with a managed launch intent. */
305
314
  export type RadarManagedLaunchMetadata = ManagedLaunchMetadataDeserialize;
@@ -494,6 +503,43 @@ export function isUuid(value: unknown): value is string {
494
503
  return typeof value === "string" && UUID.test(value);
495
504
  }
496
505
 
506
+ function decodePresenceAnswer(value: unknown): RadarResult<RadarPresenceAnswer> {
507
+ const registrationId = recordAt(value, "registration_id");
508
+ const claimId = recordAt(value, "claim_id");
509
+ const standing = recordAt(value, "standing");
510
+ const subjectId = recordAt(value, "subject_id");
511
+ if (
512
+ !isUuid(registrationId) ||
513
+ !isUuid(claimId) ||
514
+ !["pending", "granted", "duplicate", "released"].includes(String(standing)) ||
515
+ (subjectId !== undefined && !isUuid(subjectId)) ||
516
+ (standing === "granted" && !isUuid(subjectId))
517
+ )
518
+ return protocol("presence reply carries no valid claim standing");
519
+ return { ok: true, value: value as RadarPresenceAnswer };
520
+ }
521
+
522
+ /**
523
+ * How Radar stands on a publisher's current session: adopted as a managed Pi
524
+ * session, rejected as a duplicate, still pending evidence/recovery, or not managed (yet).
525
+ */
526
+ export type RadarManagement =
527
+ | { state: "managed"; subject_id: string }
528
+ | { state: "duplicate" }
529
+ | { state: "pending" }
530
+ | { state: "unmanaged" };
531
+
532
+ function managementAt(value: unknown): RadarManagement | undefined {
533
+ const field = recordAt(value, "management");
534
+ const state = stringAt(field, "state");
535
+ if (state === "managed") {
536
+ const subjectId = stringAt(field, "subject_id");
537
+ return subjectId === undefined ? undefined : { state, subject_id: subjectId };
538
+ }
539
+ if (state === "duplicate" || state === "pending" || state === "unmanaged") return { state };
540
+ return undefined;
541
+ }
542
+
497
543
  function stringAt(value: unknown, field: string): string | undefined {
498
544
  if (typeof value !== "object" || value === null) return undefined;
499
545
  const found = (value as Record<string, unknown>)[field];
@@ -508,6 +554,46 @@ function numberAt(value: unknown, field: string): number | undefined {
508
554
  : undefined;
509
555
  }
510
556
 
557
+ function handoffAt(value: unknown): HandoffSerialize | undefined {
558
+ const version = numberAt(value, "version");
559
+ const id = stringAt(value, "id");
560
+ const sender = recordAt(value, "sender");
561
+ const senderGeneration = numberAt(sender, "execution_generation");
562
+ const target = stringAt(value, "recipient_subject_id");
563
+ const summary = stringAt(value, "summary");
564
+ const content = stringAt(value, "content");
565
+ const published = numberAt(value, "published_at_unix_ms");
566
+ const correlation = typeof value === "object" && value !== null
567
+ ? (value as Record<string, unknown>).correlation_id
568
+ : undefined;
569
+ if (
570
+ version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) ||
571
+ !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) ||
572
+ !isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) ||
573
+ (senderGeneration ?? 0) < 1 || !isUuid(target) || summary === undefined || content === undefined ||
574
+ published === undefined || !Number.isSafeInteger(published) || published < 0 ||
575
+ (correlation !== undefined && correlation !== null &&
576
+ (typeof correlation !== "string" || correlation.length === 0 ||
577
+ Buffer.byteLength(correlation, "utf8") > 256 || /\p{Cc}/u.test(correlation)))
578
+ ) return undefined;
579
+ return value as HandoffSerialize;
580
+ }
581
+
582
+ function handoffReceiptAt(value: unknown): HandoffReceiptSerialize | undefined {
583
+ const version = numberAt(value, "version");
584
+ const handoffId = stringAt(value, "handoff_id");
585
+ const recipient = recordAt(value, "recipient");
586
+ const generation = numberAt(recipient, "execution_generation");
587
+ const received = numberAt(value, "received_at_unix_ms");
588
+ if (
589
+ version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) ||
590
+ !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) ||
591
+ !isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) ||
592
+ (generation ?? 0) < 1 || received === undefined || !Number.isSafeInteger(received) || received < 0
593
+ ) return undefined;
594
+ return value as HandoffReceiptSerialize;
595
+ }
596
+
511
597
  /** A writer binding a channel or context reply hands back to its publisher. */
512
598
  function writerBinding(value: unknown): RadarWriterBinding | undefined {
513
599
  const handle = stringAt(value, "handle");
@@ -662,6 +748,7 @@ const RADAR_MANAGED_IDENTITY_ENV = new Set([
662
748
  "PI_RADAR_MANAGED_SUBJECT_ID",
663
749
  "PI_RADAR_MANAGED_LAUNCH_ID",
664
750
  "PI_RADAR_MANAGED_EXECUTION_GENERATION",
751
+ "PI_RADAR_MANAGED_PARENT_SUBJECT_ID",
665
752
  ]);
666
753
 
667
754
  /**
@@ -715,7 +802,7 @@ function locationAt(value: unknown): RadarLocation | undefined {
715
802
  }
716
803
 
717
804
  /**
718
- * One durable edge, decoded strictly. The bound child's `agent_id` is kept only
805
+ * One durable edge, decoded strictly. The bound child's `registration_id` is kept only
719
806
  * when it is a canonical UUID: a legacy binding has none, and an unreadable one
720
807
  * is not an identity to correlate with.
721
808
  */
@@ -735,7 +822,7 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
735
822
  const bound = recordAt(value, "bound");
736
823
  const source = stringAt(bound, "source");
737
824
  const incarnation = stringAt(bound, "incarnation");
738
- const agentId = stringAt(bound, "agent_id");
825
+ const agentId = stringAt(bound, "registration_id");
739
826
  const exit = exitAt(recordAt(value, "exit_observation"));
740
827
  return {
741
828
  ok: true,
@@ -750,7 +837,7 @@ function spawnEdge(value: unknown): RadarResult<RadarSpawnEdge> {
750
837
  bound: {
751
838
  source,
752
839
  incarnation,
753
- ...(isUuid(agentId) ? { agent_id: agentId } : {}),
840
+ ...(isUuid(agentId) ? { registration_id: agentId } : {}),
754
841
  },
755
842
  }
756
843
  : {}),
@@ -952,15 +1039,15 @@ export type RadarClient = {
952
1039
  ping(): Promise<RadarResult<RadarHandshake>>;
953
1040
  register(
954
1041
  registration: RadarRegistration,
955
- ): Promise<RadarResult<{ agent_id: string }>>;
1042
+ ): Promise<RadarResult<{ registration_id: string }>>;
956
1043
  acquire(request: {
957
- agent_id: string;
1044
+ registration_id: string;
958
1045
  channel: RadarChannel;
959
1046
  publisher: RadarPublisherIdentity;
960
1047
  replace?: { generation: number; handle: string };
961
1048
  }): Promise<RadarResult<RadarWriterBinding>>;
962
1049
  publish(request: {
963
- agent_id: string;
1050
+ registration_id: string;
964
1051
  channel: RadarChannel;
965
1052
  writer_handle: string;
966
1053
  sequence: number;
@@ -968,8 +1055,10 @@ export type RadarClient = {
968
1055
  observed_at?: string;
969
1056
  snapshot: RadarChannelSnapshot;
970
1057
  }): Promise<RadarResult<{ sequence: number }>>;
1058
+ presence(request: RadarPresenceReport): Promise<RadarResult<RadarPresenceAnswer>>;
1059
+ releasePresence(request: RadarPresenceRelease): Promise<RadarResult<RadarPresenceAnswer>>;
971
1060
  context(request: {
972
- agent_id: string;
1061
+ registration_id: string;
973
1062
  publisher: RadarPublisherIdentity;
974
1063
  writer_handle?: string;
975
1064
  replace?: { generation: number; handle: string };
@@ -977,9 +1066,9 @@ export type RadarClient = {
977
1066
  lease_ms?: number;
978
1067
  observed_at?: string;
979
1068
  context: RadarContextValue;
980
- }): Promise<RadarResult<{ writer: RadarWriterBinding; warning?: string }>>;
1069
+ }): Promise<RadarResult<{ writer: RadarWriterBinding; warning?: string; management?: RadarManagement }>>;
981
1070
  retire(request: {
982
- agent_id: string;
1071
+ registration_id: string;
983
1072
  channel: RadarChannel;
984
1073
  writer_handle: string;
985
1074
  }): Promise<RadarResult<void>>;
@@ -1012,9 +1101,15 @@ export type RadarClient = {
1012
1101
  }): Promise<RadarResult<RadarSpawnEdge>>;
1013
1102
  spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
1014
1103
  /** The registered child's process verdict, read from its immutable claim. */
1015
- agentGet(agent_id: string): Promise<RadarResult<RadarAgentProcess>>;
1104
+ agentGet(registration_id: string): Promise<RadarResult<RadarAgentProcess>>;
1016
1105
  observe(): Promise<RadarResult<RadarInventory>>;
1017
1106
  /** Read the durable event journal after `after`, waiting at most `wait_ms`. */
1107
+ communicationPublish(request: HandoffPublishParamsDeserialize & {
1108
+ request_id: string;
1109
+ }): Promise<RadarResult<{ handoff: HandoffSerialize; created: boolean }>>;
1110
+ communicationInbox(request: { registration_id: string }): Promise<RadarResult<HandoffInboxSerialize>>;
1111
+ communicationGet(request: { registration_id: string; handoff_id: string }): Promise<RadarResult<{ handoff: HandoffSerialize; receipt: HandoffReceiptSerialize | null }>>;
1112
+ communicationAck(request: { registration_id: string; handoff_id: string }): Promise<RadarResult<{ receipt: HandoffReceiptSerialize; created: boolean }>>;
1018
1113
  events(request: {
1019
1114
  after: number;
1020
1115
  epoch?: string;
@@ -1413,6 +1508,24 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1413
1508
  return {
1414
1509
  socketPath,
1415
1510
  ping,
1511
+ async presence(request) {
1512
+ return withRegistry(async () => {
1513
+ const answer = await call("agent.presence", { ...request });
1514
+ if (!answer.ok) return answer;
1515
+ return decodePresenceAnswer(answer.value);
1516
+ });
1517
+ },
1518
+ async releasePresence(request) {
1519
+ return withRegistry(async () => {
1520
+ const answer = await call("agent.release", { ...request });
1521
+ if (!answer.ok) return answer;
1522
+ const decoded = decodePresenceAnswer(answer.value);
1523
+ if (!decoded.ok) return decoded;
1524
+ return decoded.value.standing === "released"
1525
+ ? decoded
1526
+ : protocol("release reply did not confirm the claim release");
1527
+ });
1528
+ },
1416
1529
  async register(registration) {
1417
1530
  return withRegistry(async () => {
1418
1531
  const answer = await call("agent.register", {
@@ -1432,15 +1545,15 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1432
1545
  });
1433
1546
  if (answer.ok === false) return answer;
1434
1547
  const record = (answer.value as { registration?: unknown }).registration;
1435
- const agentId = stringAt(record, "agent_id");
1436
- if (!isUuid(agentId)) return protocol("registration reply carries no agent_id");
1437
- return { ok: true, value: { agent_id: agentId } };
1548
+ const agentId = stringAt(record, "registration_id");
1549
+ if (!isUuid(agentId)) return protocol("registration reply carries no registration_id");
1550
+ return { ok: true, value: { registration_id: agentId } };
1438
1551
  });
1439
1552
  },
1440
1553
  async acquire(request) {
1441
1554
  return withRegistryChannel(request.channel, async () => {
1442
1555
  const answer = await call("agent.acquire", {
1443
- agent_id: request.agent_id,
1556
+ registration_id: request.registration_id,
1444
1557
  channel: request.channel,
1445
1558
  publisher: request.publisher,
1446
1559
  ...(request.replace !== undefined ? { replace: request.replace } : {}),
@@ -1469,7 +1582,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1469
1582
  }
1470
1583
  return withRegistryChannel(request.channel, async () => {
1471
1584
  const answer = await call("agent.publish", {
1472
- agent_id: request.agent_id,
1585
+ registration_id: request.registration_id,
1473
1586
  channel: request.channel,
1474
1587
  writer_handle: request.writer_handle,
1475
1588
  sequence: request.sequence,
@@ -1488,7 +1601,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1488
1601
  async context(request) {
1489
1602
  return withRegistry(async () => {
1490
1603
  const answer = await call("agent.context", {
1491
- agent_id: request.agent_id,
1604
+ registration_id: request.registration_id,
1492
1605
  publisher: request.publisher,
1493
1606
  ...(request.writer_handle !== undefined
1494
1607
  ? { writer_handle: request.writer_handle }
@@ -1508,9 +1621,14 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1508
1621
  if (writer === undefined)
1509
1622
  return protocol("context reply carries no writer binding");
1510
1623
  const warning = stringAt(answer.value, "warning");
1624
+ const management = managementAt(answer.value);
1511
1625
  return {
1512
1626
  ok: true,
1513
- value: { writer, ...(warning !== undefined ? { warning } : {}) },
1627
+ value: {
1628
+ writer,
1629
+ ...(warning !== undefined ? { warning } : {}),
1630
+ ...(management !== undefined ? { management } : {}),
1631
+ },
1514
1632
  };
1515
1633
  });
1516
1634
  },
@@ -1603,7 +1721,7 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1603
1721
  message: "agent.get requires a canonical agent UUID",
1604
1722
  };
1605
1723
  return withRegistry(async () => {
1606
- const answer = await call("agent.get", { agent_id: agentId });
1724
+ const answer = await call("agent.get", { registration_id: agentId });
1607
1725
  if (answer.ok === false) return answer;
1608
1726
  return agentProcessAt(recordAt(answer.value, "agent"));
1609
1727
  });
@@ -1666,6 +1784,56 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1666
1784
  return protocol("spawn.close reply carries no close outcome");
1667
1785
  });
1668
1786
  },
1787
+ async communicationPublish(request) {
1788
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1789
+ const answer = await call("communication.publish", {
1790
+ sender_registration_id: request.sender_registration_id,
1791
+ recipient_subject_id: request.recipient_subject_id,
1792
+ ...(request.correlation_id !== undefined ? { correlation_id: request.correlation_id } : {}),
1793
+ summary: request.summary,
1794
+ content: request.content,
1795
+ }, request.request_id);
1796
+ if (answer.ok === false) return answer;
1797
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
1798
+ const created = recordAt(answer.value, "created");
1799
+ if (handoff === undefined || typeof created !== "boolean")
1800
+ return protocol("communication.publish reply carries no handoff");
1801
+ return { ok: true, value: { handoff, created } };
1802
+ });
1803
+ },
1804
+ async communicationInbox(request) {
1805
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1806
+ const answer = await call("communication.inbox", request);
1807
+ if (answer.ok === false) return answer;
1808
+ const handoffIds = recordAt(answer.value, "handoff_ids");
1809
+ if (!Array.isArray(handoffIds) || handoffIds.some((id) => !isUuid(id)))
1810
+ return protocol("communication.inbox reply carries malformed handoff IDs");
1811
+ return { ok: true, value: answer.value as HandoffInboxSerialize };
1812
+ });
1813
+ },
1814
+ async communicationGet(request) {
1815
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1816
+ const answer = await call("communication.get", request);
1817
+ if (answer.ok === false) return answer;
1818
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
1819
+ const receiptValue = recordAt(answer.value, "receipt");
1820
+ const receipt = receiptValue === null ? null : handoffReceiptAt(receiptValue);
1821
+ if (handoff === undefined || (receiptValue !== null && receipt === undefined))
1822
+ return protocol("communication.get reply carries malformed content or receipt");
1823
+ return { ok: true, value: { handoff, receipt: receipt ?? null } };
1824
+ });
1825
+ },
1826
+ async communicationAck(request) {
1827
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1828
+ const answer = await call("communication.ack", request);
1829
+ if (answer.ok === false) return answer;
1830
+ const receipt = handoffReceiptAt(recordAt(answer.value, "receipt"));
1831
+ const created = recordAt(answer.value, "created");
1832
+ if (receipt === undefined || typeof created !== "boolean")
1833
+ return protocol("communication.ack reply carries no receipt");
1834
+ return { ok: true, value: { receipt, created } };
1835
+ });
1836
+ },
1669
1837
  async events(request) {
1670
1838
  if (
1671
1839
  !Number.isSafeInteger(request.after) ||
@@ -0,0 +1,66 @@
1
+ import type { HandoffInboxSerialize, HandoffSerialize, HandoffReceiptSerialize } from "./generated-contracts.ts";
2
+ import type { RadarClient, RadarResult } from "./radar-client.ts";
3
+ import type { RadarPublication } from "./radar-publication.ts";
4
+
5
+ export type RadarHandoffPort = {
6
+ /** Publish to this managed execution's Radar-recorded parent. Retry an uncertain result with the same ID and bytes. */
7
+ publish(request: {
8
+ request_id: string;
9
+ correlation_id?: string;
10
+ summary: string;
11
+ content: string;
12
+ }): Promise<RadarResult<{ handoff: HandoffSerialize; created: boolean }>>;
13
+ /** Durable inbox scan for this process's managed subject. Notifications are hints; callers can always rescan. */
14
+ inbox(): Promise<RadarResult<HandoffInboxSerialize>>;
15
+ /** Retrieve the full immutable content even after it has been acknowledged. */
16
+ get(handoff_id: string): Promise<RadarResult<{ handoff: HandoffSerialize; receipt: HandoffReceiptSerialize | null }>>;
17
+ /** Call only after the consumer durably recorded the retrieved payload. */
18
+ ack(handoff_id: string): Promise<RadarResult<{ receipt: HandoffReceiptSerialize; created: boolean }>>;
19
+ };
20
+
21
+ /**
22
+ * Bind handoff operations to this Pi process's Radar registration and the
23
+ * parent subject injected from its accepted managed-launch metadata.
24
+ */
25
+ export function createRadarHandoffPort(
26
+ client: RadarClient,
27
+ publication: Pick<RadarPublication, "subject">,
28
+ parentSubjectId: () => string | undefined = () => process.env.PI_RADAR_MANAGED_PARENT_SUBJECT_ID,
29
+ ): RadarHandoffPort {
30
+ async function agentId(): Promise<RadarResult<string>> {
31
+ const subject = await publication.subject();
32
+ return subject === undefined
33
+ ? { ok: false, code: "absent", message: "this process has no current Radar registration" }
34
+ : { ok: true, value: subject.registration_id };
35
+ }
36
+
37
+ return {
38
+ async publish(request) {
39
+ const identity = await agentId();
40
+ if (identity.ok === false) return identity;
41
+ const parent = parentSubjectId();
42
+ if (parent === undefined)
43
+ return { ok: false, code: "absent", message: "this managed execution has no Radar parent subject" };
44
+ return client.communicationPublish({
45
+ ...request,
46
+ sender_registration_id: identity.value,
47
+ recipient_subject_id: parent,
48
+ });
49
+ },
50
+ async inbox() {
51
+ const identity = await agentId();
52
+ if (identity.ok === false) return identity;
53
+ return client.communicationInbox({ registration_id: identity.value });
54
+ },
55
+ async get(handoff_id) {
56
+ const identity = await agentId();
57
+ if (identity.ok === false) return identity;
58
+ return client.communicationGet({ registration_id: identity.value, handoff_id });
59
+ },
60
+ async ack(handoff_id) {
61
+ const identity = await agentId();
62
+ if (identity.ok === false) return identity;
63
+ return client.communicationAck({ registration_id: identity.value, handoff_id });
64
+ },
65
+ };
66
+ }
@@ -3,7 +3,6 @@ import {
3
3
  isUuid,
4
4
  type RadarScheduler,
5
5
  type RadarSnapshot,
6
- type RadarLaunchIdentityMarker,
7
6
  } from "./radar-client.ts";
8
7
  import type { RadarLocationResolution } from "./radar-location.ts";
9
8
  import type { RadarFactsSnapshot } from "./radar-client.ts";
@@ -20,11 +19,6 @@ import {
20
19
  type RadarPublication,
21
20
  type RadarChannelWriter,
22
21
  } from "./radar-publication.ts";
23
- import {
24
- activeRadarLaunchMarker,
25
- persistRadarLaunchMarker,
26
- radarLaunchMarkerFromEnvironment,
27
- } from "./radar-session-marker.ts";
28
22
 
29
23
  /**
30
24
  * The minimal Pi surface this adapter uses. It is declared structurally so the
@@ -32,7 +26,6 @@ import {
32
26
  * be lifted into its own extension without touching this module.
33
27
  */
34
28
  export type RadarExecutionEventApi = {
35
- appendEntry?: (customType: string, data?: unknown) => void;
36
29
  on(name: string, handler: (event: any, context: any) => unknown): unknown;
37
30
  events?: {
38
31
  on(name: string, handler: (event: any) => unknown): unknown;
@@ -70,10 +63,8 @@ export type RadarExecutionOptions = {
70
63
  * where no such event exists.
71
64
  */
72
65
  questionnaireEvent?: string;
73
- /** Best-effort diagnostic for durable session marker failures. */
66
+ /** Best-effort diagnostic for context publication failures. */
74
67
  diagnostic?: (message: string) => void;
75
- /** Injectable only for tests; production reads the daemon-issued process env. */
76
- markerEnvironment?: Record<string, string | undefined>;
77
68
  };
78
69
 
79
70
  export type RadarExecutionAdapter = {
@@ -138,10 +129,13 @@ function unavailablePublication(): RadarPublication {
138
129
  return {
139
130
  execution: writer,
140
131
  facts: { ...writer, update: () => undefined },
141
- context: {
142
- update: () => undefined,
132
+ presence: {
133
+ report: () => undefined,
143
134
  stop: () => undefined,
144
135
  reopen: () => undefined,
136
+ management: () => undefined,
137
+ ensure: async () => undefined,
138
+ release: async () => undefined,
145
139
  diagnostic: () => "Radar private storage is unavailable",
146
140
  },
147
141
  assignment: () => writer,
@@ -269,14 +263,11 @@ export function createRadarExecutionAdapter(
269
263
  * canonical UUID is left unreported: absent context is unknown, and publishing
270
264
  * the explicit null would claim there is no session at all.
271
265
  */
272
- function reportSession(
273
- context: any,
274
- launchIdentity?: RadarLaunchIdentityMarker,
275
- ): void {
266
+ function reportSession(context: any): void {
276
267
  const session = context?.sessionManager?.getSessionId?.();
277
268
  if (!isUuid(session)) return;
278
269
  try {
279
- publication.context.update(session, launchIdentity);
270
+ publication.presence.report(session);
280
271
  } catch {
281
272
  // Optional publication must not interfere with Pi's lifecycle handler.
282
273
  }
@@ -360,49 +351,10 @@ export function createRadarExecutionAdapter(
360
351
  );
361
352
 
362
353
  on("session_start", (_event, context) => {
363
- const sessionHost = {
364
- appendEntry: pi.appendEntry
365
- ? (customType: string, data?: unknown) => pi.appendEntry?.(customType, data)
366
- : undefined,
367
- sessionManager: context?.sessionManager,
368
- };
369
- let launchIdentity: RadarLaunchIdentityMarker | undefined;
370
- try {
371
- const result = persistRadarLaunchMarker(
372
- sessionHost,
373
- options.markerEnvironment,
374
- );
375
- if (result === "written" || result === "unchanged") {
376
- const expected = radarLaunchMarkerFromEnvironment(options.markerEnvironment);
377
- const activeMarker = activeRadarLaunchMarker(sessionHost);
378
- if (
379
- expected &&
380
- activeMarker.status === "valid" &&
381
- JSON.stringify(activeMarker.marker) === JSON.stringify(expected)
382
- ) {
383
- launchIdentity = activeMarker.marker;
384
- } else {
385
- options.diagnostic?.("Radar launch marker was not visible on the active Pi session branch");
386
- }
387
- } else if (result === "unsupported") {
388
- options.diagnostic?.("Pi host does not support Radar launch identity session entries");
389
- } else if (
390
- options.markerEnvironment &&
391
- [
392
- "PI_RADAR_MANAGED_SUBJECT_ID",
393
- "PI_RADAR_MANAGED_LAUNCH_ID",
394
- "PI_RADAR_MANAGED_EXECUTION_GENERATION",
395
- ].some((key) => options.markerEnvironment?.[key] !== undefined)
396
- ) {
397
- options.diagnostic?.("incomplete or invalid Radar launch identity environment");
398
- }
399
- } catch (error) {
400
- options.diagnostic?.(`could not persist Radar launch marker: ${String(error)}`);
401
- }
402
354
  rootSession = true;
403
355
  // The session comes first: re-arming the publication must renew the session
404
356
  // this process is in now, never the one a previous session left behind.
405
- reportSession(context, launchIdentity);
357
+ reportSession(context);
406
358
  // Facts follow the root flag, so they are published only after it is set.
407
359
  publishFacts(context);
408
360
  // A process can replace or resume its session without a new process, so the
@@ -450,12 +402,13 @@ export function createRadarExecutionAdapter(
450
402
  emit();
451
403
  });
452
404
 
453
- on("session_shutdown", () => {
405
+ on("session_shutdown", async () => {
454
406
  // A session can end or be replaced without ending this process, and Pi keeps
455
407
  // the same extension instance across that: the listeners stay registered so
456
408
  // the next session_start re-arms the same process publication. Only the
457
409
  // timers and this session's facts are released.
458
410
  suspend();
411
+ await publication.presence.release();
459
412
  });
460
413
 
461
414
  function suspend(): void {