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.
- package/README.md +14 -6
- package/dist/extension/baseline.js +2740 -0
- package/dist/extension/radar-client.js +97 -2
- package/dist/extension/radar-communication.js +39 -0
- package/dist/extension/radar-execution.js +148 -16
- package/dist/extension/radar-launch.js +5 -0
- package/dist/extension/radar-location.js +2 -2
- package/dist/extension/radar-publication.js +67 -15
- package/dist/extension/radar-runtime.js +42 -0
- package/dist/extension/radar-session-marker.js +82 -0
- package/dist/index.js +340 -21
- package/extension/baseline.ts +39 -0
- package/extension/generated-contracts.ts +85 -0
- package/extension/radar-client.ts +140 -1
- package/extension/radar-communication.ts +66 -0
- package/extension/radar-execution.ts +56 -3
- package/extension/radar-launch.ts +8 -1
- package/extension/radar-location.ts +2 -2
- package/extension/radar-publication.ts +73 -12
- package/extension/radar-runtime.ts +97 -0
- package/extension/radar-session-marker.ts +146 -0
- package/index.ts +2 -0
- package/package.json +24 -5
- package/schema/public/v1/handoff-inbox.deserialize.schema.json +18 -0
- package/schema/public/v1/handoff-inbox.serialize.schema.json +18 -0
- package/schema/public/v1/handoff-publish-params.deserialize.schema.json +33 -0
- package/schema/public/v1/handoff-publish-params.serialize.schema.json +33 -0
- package/schema/public/v1/handoff-receipt.deserialize.schema.json +66 -0
- package/schema/public/v1/handoff-receipt.serialize.schema.json +66 -0
- package/schema/public/v1/handoff.deserialize.schema.json +84 -0
- package/schema/public/v1/handoff.serialize.schema.json +84 -0
- package/schema/public/v1/launch-identity-marker.deserialize.schema.json +32 -0
- package/schema/public/v1/launch-identity-marker.serialize.schema.json +32 -0
- package/schema/public/v1/managed-launch-metadata.deserialize.schema.json +39 -0
- 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 =
|
|
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(
|
|
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
|
|
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: {
|