pi-radar 0.1.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.
Files changed (35) hide show
  1. package/README.md +14 -6
  2. package/dist/extension/baseline.js +2740 -0
  3. package/dist/extension/radar-client.js +97 -2
  4. package/dist/extension/radar-communication.js +39 -0
  5. package/dist/extension/radar-execution.js +148 -16
  6. package/dist/extension/radar-launch.js +5 -0
  7. package/dist/extension/radar-location.js +2 -2
  8. package/dist/extension/radar-publication.js +67 -15
  9. package/dist/extension/radar-runtime.js +42 -0
  10. package/dist/extension/radar-session-marker.js +82 -0
  11. package/dist/index.js +340 -21
  12. package/extension/baseline.ts +39 -0
  13. package/extension/generated-contracts.ts +85 -0
  14. package/extension/radar-client.ts +140 -1
  15. package/extension/radar-communication.ts +66 -0
  16. package/extension/radar-execution.ts +56 -3
  17. package/extension/radar-launch.ts +8 -1
  18. package/extension/radar-location.ts +2 -2
  19. package/extension/radar-publication.ts +73 -12
  20. package/extension/radar-runtime.ts +97 -0
  21. package/extension/radar-session-marker.ts +146 -0
  22. package/index.ts +2 -0
  23. package/package.json +24 -5
  24. package/schema/public/v1/handoff-inbox.deserialize.schema.json +18 -0
  25. package/schema/public/v1/handoff-inbox.serialize.schema.json +18 -0
  26. package/schema/public/v1/handoff-publish-params.deserialize.schema.json +33 -0
  27. package/schema/public/v1/handoff-publish-params.serialize.schema.json +33 -0
  28. package/schema/public/v1/handoff-receipt.deserialize.schema.json +66 -0
  29. package/schema/public/v1/handoff-receipt.serialize.schema.json +66 -0
  30. package/schema/public/v1/handoff.deserialize.schema.json +84 -0
  31. package/schema/public/v1/handoff.serialize.schema.json +84 -0
  32. package/schema/public/v1/launch-identity-marker.deserialize.schema.json +32 -0
  33. package/schema/public/v1/launch-identity-marker.serialize.schema.json +32 -0
  34. package/schema/public/v1/managed-launch-metadata.deserialize.schema.json +39 -0
  35. package/schema/public/v1/managed-launch-metadata.serialize.schema.json +39 -0
@@ -19,6 +19,91 @@ export interface TaskLaneStatus {
19
19
  available: boolean;
20
20
  error: string | null;
21
21
  }
22
+ /**
23
+ * Inputs for publishing an immutable handoff. The wire request ID carries the
24
+ * stable communication identity; the daemon derives the sender execution.
25
+ */
26
+ export interface HandoffPublishParamsDeserialize {
27
+ content: string;
28
+ correlation_id?: string | null;
29
+ recipient_subject_id: string;
30
+ sender_agent_id: string;
31
+ summary: string;
32
+ }
33
+ /**
34
+ * A bounded inbox page by stable handoff ID. Payloads are fetched one at a
35
+ * time with `communication.get`, keeping each response within the wire bound.
36
+ */
37
+ export interface HandoffInboxSerialize {
38
+ handoff_ids: string[];
39
+ }
40
+ /**
41
+ * Immutable handoff content. The request id is the stable communication id.
42
+ */
43
+ export interface HandoffSerialize {
44
+ content: string;
45
+ correlation_id?: string | null;
46
+ id: string;
47
+ published_at_unix_ms: number;
48
+ recipient_subject_id: string;
49
+ sender: HandoffIdentity;
50
+ summary: string;
51
+ version: number;
52
+ }
53
+ /**
54
+ * The current managed Pi execution that published or received a handoff.
55
+ * `subject_id` is stable across verified successors; `execution_id` and
56
+ * generation identify the exact execution that took the action.
57
+ */
58
+ export interface HandoffIdentity {
59
+ agent_id: string;
60
+ execution_generation: number;
61
+ execution_id: string;
62
+ session_id: string;
63
+ subject_id: string;
64
+ }
65
+ /**
66
+ * Durable recipient-consumer receipt, stored separately from immutable content.
67
+ */
68
+ export interface HandoffReceiptSerialize {
69
+ handoff_id: string;
70
+ received_at_unix_ms: number;
71
+ recipient: HandoffIdentity;
72
+ version: number;
73
+ }
74
+ /**
75
+ * The current managed Pi execution that published or received a handoff.
76
+ * `subject_id` is stable across verified successors; `execution_id` and
77
+ * generation identify the exact execution that took the action.
78
+ */
79
+ export interface HandoffIdentity {
80
+ agent_id: string;
81
+ execution_generation: number;
82
+ execution_id: string;
83
+ session_id: string;
84
+ subject_id: string;
85
+ }
86
+ /**
87
+ * Managed owner/parent metadata accepted with a spawn intent. Domain validation
88
+ * and parent-binding checks remain in the daemon handler.
89
+ */
90
+ export interface ManagedLaunchMetadataDeserialize {
91
+ label?: string | null;
92
+ owner?: string | null;
93
+ parent_subject_id?: string | null;
94
+ role?: string | null;
95
+ run?: string | null;
96
+ }
97
+ /**
98
+ * A non-secret Pi-session breadcrumb issued by Radar. This is correlation
99
+ * evidence only and cannot establish lifecycle authority.
100
+ */
101
+ export interface LaunchIdentityMarkerSerialize {
102
+ execution_generation: number;
103
+ launch_id: string;
104
+ subject_id: string;
105
+ version: number;
106
+ }
22
107
  /**
23
108
  * The agent's own process facts. Self-authored by any Pi session, with no owner
24
109
  * or assignment, and kept separate from execution and assignment. Every field
@@ -4,7 +4,13 @@ import { createConnection } from "node:net";
4
4
  import { dirname, join } from "node:path";
5
5
  import type {
6
6
  DaemonPingSerialize,
7
+ LaunchIdentityMarkerSerialize,
7
8
  FactsSnapshotDeserialize,
9
+ HandoffInboxSerialize,
10
+ HandoffReceiptSerialize,
11
+ HandoffPublishParamsDeserialize,
12
+ HandoffSerialize,
13
+ ManagedLaunchMetadataDeserialize,
8
14
  SpawnExitObservationSerialize,
9
15
  } from "./generated-contracts.ts";
10
16
 
@@ -26,7 +32,7 @@ import type {
26
32
  */
27
33
  export const RADAR_PROTOCOL_VERSION = 2;
28
34
  /** The minimum daemon wire revision this client understands. */
29
- export const RADAR_MINIMUM_REVISION = 1;
35
+ export const RADAR_MINIMUM_REVISION = 3;
30
36
  /** Public daemon contracts this client may preflight. */
31
37
  export const RADAR_REGISTRY_CONTRACT = "registry.v1";
32
38
  export const RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
@@ -34,6 +40,7 @@ export const RADAR_SPAWN_CONTRACT = "spawn.v1";
34
40
  export const RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
35
41
  export const RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
36
42
  export const RADAR_EVENTS_CONTRACT = "events.v1";
43
+ export const RADAR_COMMUNICATION_CONTRACT = "communication.handoff.v1";
37
44
  /** The default audit identity; callers can supply their own requester. */
38
45
  export const RADAR_REQUESTER = "pi-radar";
39
46
  /** The daemon's own line bound; nothing longer is sent or accepted. */
@@ -160,6 +167,9 @@ export type RadarBackgroundTask = NonNullable<
160
167
  FactsSnapshotDeserialize["background_tasks"]
161
168
  >[number];
162
169
  export type RadarFactsSnapshot = FactsSnapshotDeserialize;
170
+ export type RadarHandoff = HandoffSerialize;
171
+ export type RadarHandoffInbox = HandoffInboxSerialize;
172
+ export type RadarHandoffReceipt = HandoffReceiptSerialize;
163
173
 
164
174
  /**
165
175
  * The assignment channel's interim fields. `request` is the owner's active
@@ -194,6 +204,7 @@ export type RadarWriterBinding = {
194
204
  */
195
205
  export type RadarContextValue = {
196
206
  session: string | null;
207
+ launch_identity?: LaunchIdentityMarkerSerialize;
197
208
  };
198
209
 
199
210
  /**
@@ -296,6 +307,10 @@ export type RadarSpawnEdge = {
296
307
  * no exit was observed.
297
308
  */
298
309
  export type RadarSpawnExitObservation = SpawnExitObservationSerialize;
310
+ export type RadarLaunchIdentityMarker = LaunchIdentityMarkerSerialize;
311
+
312
+ /** Durable Radar-owned metadata accepted with a managed launch intent. */
313
+ export type RadarManagedLaunchMetadata = ManagedLaunchMetadataDeserialize;
299
314
 
300
315
  /**
301
316
  * One edge with what *that read* learned about the location it recorded.
@@ -501,6 +516,46 @@ function numberAt(value: unknown, field: string): number | undefined {
501
516
  : undefined;
502
517
  }
503
518
 
519
+ function handoffAt(value: unknown): HandoffSerialize | undefined {
520
+ const version = numberAt(value, "version");
521
+ const id = stringAt(value, "id");
522
+ const sender = recordAt(value, "sender");
523
+ const senderGeneration = numberAt(sender, "execution_generation");
524
+ const target = stringAt(value, "recipient_subject_id");
525
+ const summary = stringAt(value, "summary");
526
+ const content = stringAt(value, "content");
527
+ const published = numberAt(value, "published_at_unix_ms");
528
+ const correlation = typeof value === "object" && value !== null
529
+ ? (value as Record<string, unknown>).correlation_id
530
+ : undefined;
531
+ if (
532
+ version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "agent_id")) ||
533
+ !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) ||
534
+ !isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) ||
535
+ (senderGeneration ?? 0) < 1 || !isUuid(target) || summary === undefined || content === undefined ||
536
+ published === undefined || !Number.isSafeInteger(published) || published < 0 ||
537
+ (correlation !== undefined && correlation !== null &&
538
+ (typeof correlation !== "string" || correlation.length === 0 ||
539
+ Buffer.byteLength(correlation, "utf8") > 256 || /\p{Cc}/u.test(correlation)))
540
+ ) return undefined;
541
+ return value as HandoffSerialize;
542
+ }
543
+
544
+ function handoffReceiptAt(value: unknown): HandoffReceiptSerialize | undefined {
545
+ const version = numberAt(value, "version");
546
+ const handoffId = stringAt(value, "handoff_id");
547
+ const recipient = recordAt(value, "recipient");
548
+ const generation = numberAt(recipient, "execution_generation");
549
+ const received = numberAt(value, "received_at_unix_ms");
550
+ if (
551
+ version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "agent_id")) ||
552
+ !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) ||
553
+ !isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) ||
554
+ (generation ?? 0) < 1 || received === undefined || !Number.isSafeInteger(received) || received < 0
555
+ ) return undefined;
556
+ return value as HandoffReceiptSerialize;
557
+ }
558
+
504
559
  /** A writer binding a channel or context reply hands back to its publisher. */
505
560
  function writerBinding(value: unknown): RadarWriterBinding | undefined {
506
561
  const handle = stringAt(value, "handle");
@@ -651,6 +706,12 @@ function unresolvedOperation(requestId: string, detail: string): RadarResult<nev
651
706
  }
652
707
 
653
708
  export const RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
709
+ const RADAR_MANAGED_IDENTITY_ENV = new Set([
710
+ "PI_RADAR_MANAGED_SUBJECT_ID",
711
+ "PI_RADAR_MANAGED_LAUNCH_ID",
712
+ "PI_RADAR_MANAGED_EXECUTION_GENERATION",
713
+ "PI_RADAR_MANAGED_PARENT_SUBJECT_ID",
714
+ ]);
654
715
 
655
716
  /**
656
717
  * A spawn this process may still have caused.
@@ -857,6 +918,7 @@ function spawnRequestShape(request: {
857
918
  argv: string[];
858
919
  cwd?: string;
859
920
  env?: Record<string, string>;
921
+ managed?: RadarManagedLaunchMetadata;
860
922
  }): string | undefined {
861
923
  if (!isUuid(request.request_id))
862
924
  return "spawn requires a canonical request UUID";
@@ -875,9 +937,26 @@ function spawnRequestShape(request: {
875
937
  for (const [name, value] of env) {
876
938
  if (name === RADAR_SPAWN_TOKEN_ENV)
877
939
  return `spawn env cannot set the reserved ${RADAR_SPAWN_TOKEN_ENV}`;
940
+ if (RADAR_MANAGED_IDENTITY_ENV.has(name))
941
+ return `spawn env cannot set Radar-issued identity variable ${name}`;
878
942
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || /\p{Cc}/u.test(value))
879
943
  return `spawn env contains an invalid name or value: ${name}`;
880
944
  }
945
+ if (request.managed !== undefined) {
946
+ if (typeof request.managed !== "object" || request.managed === null)
947
+ return "managed launch metadata must be an object";
948
+ for (const [name, value] of Object.entries(request.managed)) {
949
+ if (!["owner", "run", "label", "role", "parent_subject_id"].includes(name))
950
+ return `unknown managed launch metadata field: ${name}`;
951
+ if (value != null && (typeof value !== "string" || value.trim() === "" || new TextEncoder().encode(value).length > 256 || /\p{Cc}/u.test(value)))
952
+ return `managed launch ${name} must be non-empty, control-free, and at most 256 UTF-8 bytes`;
953
+ }
954
+ if (
955
+ request.managed.parent_subject_id != null &&
956
+ !isUuid(request.managed.parent_subject_id)
957
+ )
958
+ return "managed parent_subject_id must be a canonical UUID";
959
+ }
881
960
  return undefined;
882
961
  }
883
962
 
@@ -978,12 +1057,19 @@ export type RadarClient = {
978
1057
  argv: string[];
979
1058
  cwd?: string;
980
1059
  env?: Record<string, string>;
1060
+ managed?: RadarManagedLaunchMetadata;
981
1061
  }): Promise<RadarResult<RadarSpawnEdge>>;
982
1062
  spawnGet(request_id: string): Promise<RadarResult<RadarSpawnTopology>>;
983
1063
  /** The registered child's process verdict, read from its immutable claim. */
984
1064
  agentGet(agent_id: string): Promise<RadarResult<RadarAgentProcess>>;
985
1065
  observe(): Promise<RadarResult<RadarInventory>>;
986
1066
  /** Read the durable event journal after `after`, waiting at most `wait_ms`. */
1067
+ communicationPublish(request: HandoffPublishParamsDeserialize & {
1068
+ request_id: string;
1069
+ }): Promise<RadarResult<{ handoff: HandoffSerialize; created: boolean }>>;
1070
+ communicationInbox(request: { agent_id: string }): Promise<RadarResult<HandoffInboxSerialize>>;
1071
+ communicationGet(request: { agent_id: string; handoff_id: string }): Promise<RadarResult<{ handoff: HandoffSerialize; receipt: HandoffReceiptSerialize | null }>>;
1072
+ communicationAck(request: { agent_id: string; handoff_id: string }): Promise<RadarResult<{ receipt: HandoffReceiptSerialize; created: boolean }>>;
987
1073
  events(request: {
988
1074
  after: number;
989
1075
  epoch?: string;
@@ -1513,6 +1599,9 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1513
1599
  ...(carriesContext && request.env !== undefined
1514
1600
  ? { env: request.env }
1515
1601
  : {}),
1602
+ ...(request.managed !== undefined
1603
+ ? { managed: request.managed }
1604
+ : {}),
1516
1605
  },
1517
1606
  requester,
1518
1607
  },
@@ -1632,6 +1721,56 @@ export function createRadarClient(options: RadarClientOptions = {}): RadarClient
1632
1721
  return protocol("spawn.close reply carries no close outcome");
1633
1722
  });
1634
1723
  },
1724
+ async communicationPublish(request) {
1725
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1726
+ const answer = await call("communication.publish", {
1727
+ sender_agent_id: request.sender_agent_id,
1728
+ recipient_subject_id: request.recipient_subject_id,
1729
+ ...(request.correlation_id !== undefined ? { correlation_id: request.correlation_id } : {}),
1730
+ summary: request.summary,
1731
+ content: request.content,
1732
+ }, request.request_id);
1733
+ if (answer.ok === false) return answer;
1734
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
1735
+ const created = recordAt(answer.value, "created");
1736
+ if (handoff === undefined || typeof created !== "boolean")
1737
+ return protocol("communication.publish reply carries no handoff");
1738
+ return { ok: true, value: { handoff, created } };
1739
+ });
1740
+ },
1741
+ async communicationInbox(request) {
1742
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1743
+ const answer = await call("communication.inbox", request);
1744
+ if (answer.ok === false) return answer;
1745
+ const handoffIds = recordAt(answer.value, "handoff_ids");
1746
+ if (!Array.isArray(handoffIds) || handoffIds.some((id) => !isUuid(id)))
1747
+ return protocol("communication.inbox reply carries malformed handoff IDs");
1748
+ return { ok: true, value: answer.value as HandoffInboxSerialize };
1749
+ });
1750
+ },
1751
+ async communicationGet(request) {
1752
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1753
+ const answer = await call("communication.get", request);
1754
+ if (answer.ok === false) return answer;
1755
+ const handoff = handoffAt(recordAt(answer.value, "handoff"));
1756
+ const receiptValue = recordAt(answer.value, "receipt");
1757
+ const receipt = receiptValue === null ? null : handoffReceiptAt(receiptValue);
1758
+ if (handoff === undefined || (receiptValue !== null && receipt === undefined))
1759
+ return protocol("communication.get reply carries malformed content or receipt");
1760
+ return { ok: true, value: { handoff, receipt: receipt ?? null } };
1761
+ });
1762
+ },
1763
+ async communicationAck(request) {
1764
+ return withContract(RADAR_COMMUNICATION_CONTRACT, async () => {
1765
+ const answer = await call("communication.ack", request);
1766
+ if (answer.ok === false) return answer;
1767
+ const receipt = handoffReceiptAt(recordAt(answer.value, "receipt"));
1768
+ const created = recordAt(answer.value, "created");
1769
+ if (receipt === undefined || typeof created !== "boolean")
1770
+ return protocol("communication.ack reply carries no receipt");
1771
+ return { ok: true, value: { receipt, created } };
1772
+ });
1773
+ },
1635
1774
  async events(request) {
1636
1775
  if (
1637
1776
  !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.agent_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_agent_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({ agent_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({ agent_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({ agent_id: identity.value, handoff_id });
64
+ },
65
+ };
66
+ }
@@ -3,6 +3,7 @@ import {
3
3
  isUuid,
4
4
  type RadarScheduler,
5
5
  type RadarSnapshot,
6
+ type RadarLaunchIdentityMarker,
6
7
  } from "./radar-client.ts";
7
8
  import type { RadarLocationResolution } from "./radar-location.ts";
8
9
  import type { RadarFactsSnapshot } from "./radar-client.ts";
@@ -19,6 +20,11 @@ import {
19
20
  type RadarPublication,
20
21
  type RadarChannelWriter,
21
22
  } from "./radar-publication.ts";
23
+ import {
24
+ activeRadarLaunchMarker,
25
+ persistRadarLaunchMarker,
26
+ radarLaunchMarkerFromEnvironment,
27
+ } from "./radar-session-marker.ts";
22
28
 
23
29
  /**
24
30
  * The minimal Pi surface this adapter uses. It is declared structurally so the
@@ -26,6 +32,7 @@ import {
26
32
  * be lifted into its own extension without touching this module.
27
33
  */
28
34
  export type RadarExecutionEventApi = {
35
+ appendEntry?: (customType: string, data?: unknown) => void;
29
36
  on(name: string, handler: (event: any, context: any) => unknown): unknown;
30
37
  events?: {
31
38
  on(name: string, handler: (event: any) => unknown): unknown;
@@ -63,6 +70,10 @@ export type RadarExecutionOptions = {
63
70
  * where no such event exists.
64
71
  */
65
72
  questionnaireEvent?: string;
73
+ /** Best-effort diagnostic for durable session marker failures. */
74
+ diagnostic?: (message: string) => void;
75
+ /** Injectable only for tests; production reads the daemon-issued process env. */
76
+ markerEnvironment?: Record<string, string | undefined>;
66
77
  };
67
78
 
68
79
  export type RadarExecutionAdapter = {
@@ -258,11 +269,14 @@ export function createRadarExecutionAdapter(
258
269
  * canonical UUID is left unreported: absent context is unknown, and publishing
259
270
  * the explicit null would claim there is no session at all.
260
271
  */
261
- function reportSession(context: any): void {
272
+ function reportSession(
273
+ context: any,
274
+ launchIdentity?: RadarLaunchIdentityMarker,
275
+ ): void {
262
276
  const session = context?.sessionManager?.getSessionId?.();
263
277
  if (!isUuid(session)) return;
264
278
  try {
265
- publication.context.update(session);
279
+ publication.context.update(session, launchIdentity);
266
280
  } catch {
267
281
  // Optional publication must not interfere with Pi's lifecycle handler.
268
282
  }
@@ -346,10 +360,49 @@ export function createRadarExecutionAdapter(
346
360
  );
347
361
 
348
362
  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
+ }
349
402
  rootSession = true;
350
403
  // The session comes first: re-arming the publication must renew the session
351
404
  // this process is in now, never the one a previous session left behind.
352
- reportSession(context);
405
+ reportSession(context, launchIdentity);
353
406
  // Facts follow the root flag, so they are published only after it is set.
354
407
  publishFacts(context);
355
408
  // A process can replace or resume its session without a new process, so the
@@ -64,9 +64,11 @@ export type LaunchMailbox<State extends LaunchState> = {
64
64
  * different leaves the launch unresolved rather than placed.
65
65
  */
66
66
 
67
- /** The exact parent this launch may use, and nothing inferred about it. */
67
+ /** The exact managed parent this launch may use, and nothing inferred about it. */
68
68
  export type RadarLaunchParent = {
69
69
  agent_id: string;
70
+ /** Stable subject from this process's current Radar registration. */
71
+ subject_id: string;
70
72
  location?: RadarLocation;
71
73
  };
72
74
 
@@ -395,6 +397,10 @@ export async function launchRadarWorker<State extends LaunchState>(
395
397
  return refusedLaunch(
396
398
  "the launch has no exact parent registration to spawn under",
397
399
  );
400
+ if (!isUuid(launch.parent.subject_id))
401
+ return refusedLaunch(
402
+ "the launch has no managed Radar subject for its parent",
403
+ );
398
404
  // A spawn is created under the pane its parent registered, and this port
399
405
  // registers no location it was not given: without one there is nothing to
400
406
  // place the child beside, and a name or session lookup is not a substitute.
@@ -408,6 +414,7 @@ export async function launchRadarWorker<State extends LaunchState>(
408
414
  parent: launch.parent.agent_id,
409
415
  parent_session_id: launch.parent_session_id,
410
416
  executable: launch.executable,
417
+ managed: { parent_subject_id: launch.parent.subject_id },
411
418
  argv: launch.argv,
412
419
  ...(launch.cwd !== "" ? { cwd: launch.cwd } : {}),
413
420
  ...(Object.keys(launch.env).length > 0 ? { env: launch.env } : {}),
@@ -153,7 +153,8 @@ export async function resolveRadarProcessLocation(
153
153
  const backend = ready.value.backend;
154
154
  if (backend === undefined || backend === "none")
155
155
  return { state: "absent", reason: "the daemon serves no physical backend" };
156
- const own = qualifiedOwnPane(env, backend, ready.value.instance);
156
+ const instance = ready.value.instance ?? undefined;
157
+ const own = qualifiedOwnPane(env, backend, instance);
157
158
  if ("absent" in own) return { state: "absent", reason: own.absent };
158
159
  if ("pending" in own) return { state: "pending", reason: own.pending };
159
160
  const paneId = own.pane;
@@ -186,7 +187,6 @@ export async function resolveRadarProcessLocation(
186
187
  if ("ambiguous" in containment)
187
188
  return { state: "pending", reason: containment.ambiguous };
188
189
  const row = containment.pane;
189
- const instance = ready.value.instance;
190
190
  return {
191
191
  state: "resolved",
192
192
  location: {