pi-radar 0.3.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.
- package/README.md +4 -2
- package/dist/extension/baseline.js +227 -402
- package/dist/extension/radar-client.js +53 -13
- package/dist/extension/radar-communication.js +5 -5
- package/dist/extension/radar-execution.js +168 -383
- package/dist/extension/radar-launch.js +13 -13
- package/dist/extension/radar-publication.js +160 -299
- package/dist/index.js +239 -414
- package/extension/generated-contracts.ts +1249 -13
- package/extension/radar-client.ts +92 -29
- package/extension/radar-communication.ts +5 -5
- package/extension/radar-execution.ts +11 -58
- package/extension/radar-launch.ts +21 -21
- package/extension/radar-publication.ts +209 -412
- package/package.json +3 -4
- package/schema/public/v1/handoff-publish-params.deserialize.schema.json +2 -2
- package/schema/public/v1/handoff-publish-params.serialize.schema.json +2 -2
- package/schema/public/v1/handoff-receipt.deserialize.schema.json +4 -4
- package/schema/public/v1/handoff-receipt.serialize.schema.json +4 -4
- package/schema/public/v1/handoff.deserialize.schema.json +4 -4
- package/schema/public/v1/handoff.serialize.schema.json +4 -4
- package/schema/public/v1/observation-get-params.deserialize.schema.json +46 -0
- package/schema/public/v1/observation-get-params.serialize.schema.json +52 -0
- package/schema/public/v1/observation-get-response.deserialize.schema.json +2731 -0
- package/schema/public/v1/observation-get-response.serialize.schema.json +2862 -0
- package/schema/public/v1/observation-snapshot.deserialize.schema.json +2616 -0
- package/schema/public/v1/observation-snapshot.serialize.schema.json +2741 -0
- package/schema/public/v1/presence-answer.deserialize.schema.json +40 -0
- package/schema/public/v1/presence-answer.serialize.schema.json +40 -0
- package/schema/public/v1/presence-release.deserialize.schema.json +51 -0
- package/schema/public/v1/presence-release.serialize.schema.json +52 -0
- package/schema/public/v1/presence-report.deserialize.schema.json +149 -0
- package/schema/public/v1/presence-report.serialize.schema.json +157 -0
- package/extension/radar-session-marker.ts +0 -146
- package/schema/public/v1/launch-identity-marker.deserialize.schema.json +0 -32
- package/schema/public/v1/launch-identity-marker.serialize.schema.json +0 -32
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { dirname, isAbsolute, join, parse, resolve, sep } from "node:path";
|
|
16
16
|
import type { RadarLocationResolution } from "./radar-location.ts";
|
|
17
|
-
import { decodeRadarLaunchMarker } from "./radar-session-marker.ts";
|
|
18
17
|
import {
|
|
19
18
|
boundedRadarAssignment,
|
|
20
19
|
boundedRadarFacts,
|
|
@@ -28,7 +27,7 @@ import {
|
|
|
28
27
|
type RadarFactsSnapshot,
|
|
29
28
|
type RadarClient,
|
|
30
29
|
type RadarLocation,
|
|
31
|
-
type
|
|
30
|
+
type RadarManagement,
|
|
32
31
|
type RadarPublisherIdentity,
|
|
33
32
|
type RadarRegistration,
|
|
34
33
|
type RadarScheduler,
|
|
@@ -37,16 +36,20 @@ import {
|
|
|
37
36
|
systemScheduler,
|
|
38
37
|
} from "./radar-client.ts";
|
|
39
38
|
|
|
39
|
+
/** A rejected session claim requires a fresh Pi session load before reconsideration. */
|
|
40
|
+
const DUPLICATE_SESSION_DIAGNOSTIC =
|
|
41
|
+
"Radar rejected this session claim as a duplicate; reload or resume the session before retrying";
|
|
42
|
+
|
|
40
43
|
/** The client members publication calls; consumers narrow to this rather than the whole client. */
|
|
41
44
|
export type RadarPublicationClient = Pick<
|
|
42
45
|
RadarClient,
|
|
43
|
-
"ping" | "
|
|
46
|
+
"ping" | "presence" | "releasePresence" | "acquire" | "publish" | "retire"
|
|
44
47
|
>;
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* Durable publication state for Radar's registry (`agent-radar`
|
|
48
51
|
* `docs/agent-registration.md`, contract version 1): this process's immutable
|
|
49
|
-
*
|
|
52
|
+
* registration record, the acquired writer binding per channel, the accepted
|
|
50
53
|
* sequence and the exact unresolved request.
|
|
51
54
|
*
|
|
52
55
|
* Everything here is best-effort and asynchronous. An absent daemon, a lost
|
|
@@ -100,7 +103,7 @@ export function readProcessBirth(): ProcessBirth | undefined {
|
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
const RECORD_NAME = /^(?:subjects|channels|context|bindings)\/[a-z0-9-]{1,64}\.json$/;
|
|
106
|
+
const RECORD_NAME = /^(?:registrations|presence|subjects|channels|context|bindings)\/[a-z0-9-]{1,64}\.json$/;
|
|
104
107
|
const MAX_RECORD_BYTES = 64 * 1024;
|
|
105
108
|
const PRIVATE_MODE = 0o700;
|
|
106
109
|
const RECORD_MODE = 0o600;
|
|
@@ -308,7 +311,7 @@ type PendingPublish = {
|
|
|
308
311
|
|
|
309
312
|
type ChannelRecord = {
|
|
310
313
|
version: typeof RADAR_PUBLICATION_VERSION;
|
|
311
|
-
|
|
314
|
+
registration_id: string;
|
|
312
315
|
channel: RadarChannel;
|
|
313
316
|
publisher: RadarPublisherIdentity;
|
|
314
317
|
writer?: { handle: string; generation: number };
|
|
@@ -325,7 +328,6 @@ type ContextReport = {
|
|
|
325
328
|
session: string | null;
|
|
326
329
|
lease_ms: number;
|
|
327
330
|
observed_at: string;
|
|
328
|
-
launch_identity?: RadarLaunchIdentityMarker;
|
|
329
331
|
};
|
|
330
332
|
|
|
331
333
|
/**
|
|
@@ -335,7 +337,7 @@ type ContextReport = {
|
|
|
335
337
|
*/
|
|
336
338
|
type ContextRecord = {
|
|
337
339
|
version: typeof RADAR_PUBLICATION_VERSION;
|
|
338
|
-
|
|
340
|
+
registration_id: string;
|
|
339
341
|
publisher: RadarPublisherIdentity;
|
|
340
342
|
writer?: { handle: string; generation: number };
|
|
341
343
|
sequence: number;
|
|
@@ -347,7 +349,7 @@ type ContextRecord = {
|
|
|
347
349
|
/** What a managed child tells its owner: which exact subject to publish about. */
|
|
348
350
|
export type RadarChildBinding = {
|
|
349
351
|
version: typeof RADAR_PUBLICATION_VERSION;
|
|
350
|
-
|
|
352
|
+
registration_id: string;
|
|
351
353
|
incarnation: string;
|
|
352
354
|
run: string;
|
|
353
355
|
owner: string;
|
|
@@ -563,9 +565,11 @@ function pendingAt(
|
|
|
563
565
|
}
|
|
564
566
|
|
|
565
567
|
const CONTEXT_PENDING_KEYS = "lease_ms,observed_at,sequence,session";
|
|
566
|
-
const CONTEXT_PENDING_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sequence,session";
|
|
567
568
|
const CONTEXT_ACCEPTED_KEYS = "lease_ms,observed_at,sent_at,sequence,session";
|
|
568
|
-
|
|
569
|
+
// Records written before session-bound management also carry a launch-identity
|
|
570
|
+
// marker. It is read past and never used: the attached subject is the identity.
|
|
571
|
+
const CONTEXT_PENDING_LEGACY_KEYS = "launch_identity,lease_ms,observed_at,sequence,session";
|
|
572
|
+
const CONTEXT_ACCEPTED_LEGACY_KEYS = "launch_identity,lease_ms,observed_at,sent_at,sequence,session";
|
|
569
573
|
|
|
570
574
|
/** The reported session, or `undefined` when the field is not one of the two. */
|
|
571
575
|
function sessionAt(value: unknown): string | null | undefined {
|
|
@@ -575,28 +579,12 @@ function sessionAt(value: unknown): string | null | undefined {
|
|
|
575
579
|
return typeof found === "string" ? found : undefined;
|
|
576
580
|
}
|
|
577
581
|
|
|
578
|
-
function sameLaunchIdentity(
|
|
579
|
-
a: RadarLaunchIdentityMarker | undefined,
|
|
580
|
-
b: RadarLaunchIdentityMarker | undefined,
|
|
581
|
-
): boolean {
|
|
582
|
-
return (
|
|
583
|
-
a === b ||
|
|
584
|
-
(a !== undefined &&
|
|
585
|
-
b !== undefined &&
|
|
586
|
-
a.version === b.version &&
|
|
587
|
-
a.subject_id === b.subject_id &&
|
|
588
|
-
a.launch_id === b.launch_id &&
|
|
589
|
-
a.execution_generation === b.execution_generation)
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
582
|
function contextReportAt(
|
|
594
583
|
value: unknown,
|
|
595
584
|
keys: string,
|
|
596
|
-
|
|
585
|
+
legacyKeys: string,
|
|
597
586
|
): ContextReport | undefined {
|
|
598
|
-
|
|
599
|
-
if (!exactKeys(value, keys) && !marked) return undefined;
|
|
587
|
+
if (!exactKeys(value, keys) && !exactKeys(value, legacyKeys)) return undefined;
|
|
600
588
|
const sequence = numberAt(value, "sequence");
|
|
601
589
|
const lease = numberAt(value, "lease_ms");
|
|
602
590
|
const observed = textAt(value, "observed_at");
|
|
@@ -604,17 +592,7 @@ function contextReportAt(
|
|
|
604
592
|
if (sequence === undefined || sequence < 1) return undefined;
|
|
605
593
|
if (lease === undefined || observed === undefined || session === undefined)
|
|
606
594
|
return undefined;
|
|
607
|
-
|
|
608
|
-
? decodeRadarLaunchMarker((value as Record<string, unknown>).launch_identity)
|
|
609
|
-
: undefined;
|
|
610
|
-
if (marked && marker === undefined) return undefined;
|
|
611
|
-
return {
|
|
612
|
-
sequence,
|
|
613
|
-
session,
|
|
614
|
-
lease_ms: lease,
|
|
615
|
-
observed_at: observed,
|
|
616
|
-
...(marker !== undefined ? { launch_identity: marker } : {}),
|
|
617
|
-
};
|
|
595
|
+
return { sequence, session, lease_ms: lease, observed_at: observed };
|
|
618
596
|
}
|
|
619
597
|
|
|
620
598
|
export type RadarFactsWriter = Omit<RadarChannelWriter, "update"> & {
|
|
@@ -667,7 +645,7 @@ function createChannelWriter(input: ChannelWriterInput): RadarChannelWriter {
|
|
|
667
645
|
function blank(agentId: string): ChannelRecord {
|
|
668
646
|
return {
|
|
669
647
|
version: RADAR_PUBLICATION_VERSION,
|
|
670
|
-
|
|
648
|
+
registration_id: agentId,
|
|
671
649
|
channel,
|
|
672
650
|
publisher,
|
|
673
651
|
sequence: 0,
|
|
@@ -676,7 +654,7 @@ function createChannelWriter(input: ChannelWriterInput): RadarChannelWriter {
|
|
|
676
654
|
|
|
677
655
|
function loadRecord(): ChannelRecord {
|
|
678
656
|
const value = store.read(recordName);
|
|
679
|
-
const agentId = textAt(value, "
|
|
657
|
+
const agentId = textAt(value, "registration_id");
|
|
680
658
|
const storedPublisher = recordAt(value, "publisher");
|
|
681
659
|
// A record written for another channel or publisher is not this writer's
|
|
682
660
|
// binding: adopting it would publish under someone else's identity.
|
|
@@ -704,7 +682,7 @@ function createChannelWriter(input: ChannelWriterInput): RadarChannelWriter {
|
|
|
704
682
|
const sentAt = numberAt(acceptedValue, "sent_at");
|
|
705
683
|
return {
|
|
706
684
|
version: RADAR_PUBLICATION_VERSION,
|
|
707
|
-
|
|
685
|
+
registration_id: agentId,
|
|
708
686
|
channel,
|
|
709
687
|
publisher,
|
|
710
688
|
...(handle !== undefined && generation !== undefined
|
|
@@ -772,7 +750,7 @@ function createChannelWriter(input: ChannelWriterInput): RadarChannelWriter {
|
|
|
772
750
|
// identical immutable content that was persisted with it.
|
|
773
751
|
const agentId = await input.subject(recovering);
|
|
774
752
|
if (agentId === undefined) return false;
|
|
775
|
-
if (record.
|
|
753
|
+
if (record.registration_id !== agentId) {
|
|
776
754
|
// A different subject incarnation is a different channel: the previous
|
|
777
755
|
// binding, sequence and unresolved request do not apply to it, so the
|
|
778
756
|
// next drain publishes a complete snapshot instead of replaying one.
|
|
@@ -780,7 +758,7 @@ function createChannelWriter(input: ChannelWriterInput): RadarChannelWriter {
|
|
|
780
758
|
save();
|
|
781
759
|
}
|
|
782
760
|
const answer = await client.acquire({
|
|
783
|
-
|
|
761
|
+
registration_id: agentId,
|
|
784
762
|
channel,
|
|
785
763
|
publisher,
|
|
786
764
|
});
|
|
@@ -821,7 +799,7 @@ function createChannelWriter(input: ChannelWriterInput): RadarChannelWriter {
|
|
|
821
799
|
const writer = record.writer;
|
|
822
800
|
if (writer === undefined) return;
|
|
823
801
|
const answer = await client.publish({
|
|
824
|
-
|
|
802
|
+
registration_id: record.registration_id,
|
|
825
803
|
channel,
|
|
826
804
|
writer_handle: writer.handle,
|
|
827
805
|
sequence: pending.sequence,
|
|
@@ -934,10 +912,10 @@ function createChannelWriter(input: ChannelWriterInput): RadarChannelWriter {
|
|
|
934
912
|
stopPump();
|
|
935
913
|
const writer = record.writer;
|
|
936
914
|
if (writer === undefined) return;
|
|
937
|
-
const agentId = record.
|
|
915
|
+
const agentId = record.registration_id;
|
|
938
916
|
try {
|
|
939
917
|
void client
|
|
940
|
-
.retire({
|
|
918
|
+
.retire({ registration_id: agentId, channel, writer_handle: writer.handle })
|
|
941
919
|
.then((answer) => {
|
|
942
920
|
if (answer.ok === true) {
|
|
943
921
|
try {
|
|
@@ -965,335 +943,166 @@ function createChannelWriter(input: ChannelWriterInput): RadarChannelWriter {
|
|
|
965
943
|
};
|
|
966
944
|
}
|
|
967
945
|
|
|
968
|
-
export type
|
|
969
|
-
/**
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
* does not know its session has nothing to assert.
|
|
973
|
-
*/
|
|
974
|
-
update(session: string | null, launchIdentity?: RadarLaunchIdentityMarker): void;
|
|
975
|
-
/** Release this writer's timers. The accepted report stays until it goes stale. */
|
|
946
|
+
export type RadarPresenceReporter = {
|
|
947
|
+
/** Start a fresh session-load claim. Retries and heartbeats keep its claim ID. */
|
|
948
|
+
report(session: string): void;
|
|
949
|
+
/** Release this exact session claim on graceful shutdown. */
|
|
976
950
|
stop(): void;
|
|
977
951
|
/** Re-arm a stopped writer after an extension reload. */
|
|
978
952
|
reopen(): void;
|
|
953
|
+
/** Radar's standing on the session last published, once the daemon answered. */
|
|
954
|
+
management(): RadarManagement | undefined;
|
|
955
|
+
ensure(): Promise<string | undefined>;
|
|
956
|
+
release(): Promise<void>;
|
|
979
957
|
diagnostic(): string | undefined;
|
|
980
958
|
};
|
|
981
959
|
|
|
982
|
-
type
|
|
960
|
+
type PresenceReporterInput = {
|
|
983
961
|
store: PublicationStore;
|
|
984
962
|
client: RadarPublicationClient;
|
|
985
963
|
scheduler: RadarScheduler;
|
|
986
964
|
recordName: string;
|
|
987
|
-
|
|
988
|
-
/**
|
|
989
|
-
* The exact subject this process registered. `recheck` forgets a cached
|
|
990
|
-
* subject so the identical immutable content is registered again, which is
|
|
991
|
-
* what a daemon that lost its state root needs.
|
|
992
|
-
*/
|
|
993
|
-
subject(recheck: boolean): Promise<string | undefined>;
|
|
994
|
-
leaseMs: number;
|
|
965
|
+
registration(): Promise<RadarRegistration | undefined>;
|
|
995
966
|
heartbeatMs: number;
|
|
996
967
|
};
|
|
997
968
|
|
|
998
969
|
/**
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
* the daemon issues; a later session is a newer sequence under that same
|
|
1002
|
-
* binding, and the lease is renewed from the same cadence as the process's
|
|
1003
|
-
* facts rather than from a second heartbeat.
|
|
1004
|
-
*
|
|
1005
|
-
* A refusal is a real disagreement about ownership, so it stops this writer
|
|
1006
|
-
* with one diagnostic instead of taking over a binding it did not observe.
|
|
970
|
+
* Owns this process's durable claim retry state; Radar owns the subject grant.
|
|
971
|
+
* Session-load boundaries create claim IDs; heartbeats/reconnects reuse them.
|
|
1007
972
|
*/
|
|
1008
|
-
function
|
|
1009
|
-
const { store, client, scheduler, recordName
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
let
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
agent_id: agentId,
|
|
1024
|
-
publisher,
|
|
1025
|
-
sequence: 0,
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function loadRecord(): ContextRecord {
|
|
1030
|
-
const value = store.read(recordName);
|
|
1031
|
-
const agentId = textAt(value, "agent_id");
|
|
1032
|
-
const storedPublisher = recordAt(value, "publisher");
|
|
1033
|
-
// A record written for another publisher is not this writer's binding:
|
|
1034
|
-
// adopting it would publish under someone else's identity.
|
|
1035
|
-
if (
|
|
1036
|
-
numberAt(value, "version") !== RADAR_PUBLICATION_VERSION ||
|
|
1037
|
-
agentId === undefined ||
|
|
1038
|
-
(!isUuid(agentId) && agentId !== "") ||
|
|
1039
|
-
textAt(storedPublisher, "source") !== publisher.source ||
|
|
1040
|
-
textAt(storedPublisher, "incarnation") !== publisher.incarnation ||
|
|
1041
|
-
textAt(storedPublisher, "reporting_owner") !== publisher.reporting_owner
|
|
1042
|
-
)
|
|
1043
|
-
return blank("");
|
|
1044
|
-
const writer = recordAt(value, "writer");
|
|
1045
|
-
const handle = textAt(writer, "handle");
|
|
1046
|
-
const generation = numberAt(writer, "generation");
|
|
973
|
+
function createPresenceReporter(input: PresenceReporterInput): RadarPresenceReporter {
|
|
974
|
+
const { store, client, scheduler, recordName } = input;
|
|
975
|
+
type State = {
|
|
976
|
+
version: 2;
|
|
977
|
+
claim_id: string;
|
|
978
|
+
session: string;
|
|
979
|
+
registration_id?: string;
|
|
980
|
+
subject_id?: string;
|
|
981
|
+
releases: Array<{ registration_id: string; claim_id: string }>;
|
|
982
|
+
};
|
|
983
|
+
let state: State | undefined;
|
|
984
|
+
let standing: RadarManagement | undefined;
|
|
985
|
+
let diagnostic: string | undefined;
|
|
986
|
+
try {
|
|
987
|
+
const saved = store.read(recordName) as State | undefined;
|
|
1047
988
|
if (
|
|
1048
|
-
|
|
1049
|
-
|
|
989
|
+
saved?.version === 2 &&
|
|
990
|
+
isUuid(saved.claim_id) &&
|
|
991
|
+
isUuid(saved.session) &&
|
|
992
|
+
Array.isArray(saved.releases)
|
|
1050
993
|
)
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
CONTEXT_PENDING_KEYS,
|
|
1055
|
-
CONTEXT_PENDING_MARKED_KEYS,
|
|
1056
|
-
);
|
|
1057
|
-
const acceptedValue = recordAt(value, "accepted");
|
|
1058
|
-
const accepted = contextReportAt(
|
|
1059
|
-
acceptedValue,
|
|
1060
|
-
CONTEXT_ACCEPTED_KEYS,
|
|
1061
|
-
CONTEXT_ACCEPTED_MARKED_KEYS,
|
|
1062
|
-
);
|
|
1063
|
-
const sentAt = numberAt(acceptedValue, "sent_at");
|
|
1064
|
-
return {
|
|
1065
|
-
version: RADAR_PUBLICATION_VERSION,
|
|
1066
|
-
agent_id: agentId,
|
|
1067
|
-
publisher,
|
|
1068
|
-
...(handle !== undefined && generation !== undefined
|
|
1069
|
-
? { writer: { handle, generation } }
|
|
1070
|
-
: {}),
|
|
1071
|
-
sequence: numberAt(value, "sequence") ?? 0,
|
|
1072
|
-
...(pending !== undefined ? { pending } : {}),
|
|
1073
|
-
...(accepted !== undefined && sentAt !== undefined
|
|
1074
|
-
? { accepted: { ...accepted, sent_at: sentAt } }
|
|
1075
|
-
: {}),
|
|
1076
|
-
...(textAt(value, "diagnostic") !== undefined
|
|
1077
|
-
? { diagnostic: textAt(value, "diagnostic") }
|
|
1078
|
-
: {}),
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
/** One bounded diagnostic: the first cause is the one a reader reconciles. */
|
|
1083
|
-
function note(message: string): void {
|
|
1084
|
-
if (record.diagnostic !== undefined) return;
|
|
1085
|
-
record.diagnostic = message.slice(0, 256);
|
|
1086
|
-
save();
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function save(): void {
|
|
1090
|
-
store.write(recordName, record);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
function fence(message: string): void {
|
|
1094
|
-
fenced = true;
|
|
1095
|
-
stopPump();
|
|
1096
|
-
note(`context stopped: ${message}`);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function startPump(): void {
|
|
1100
|
-
if (pump !== undefined || stopped || fenced) return;
|
|
1101
|
-
pump = scheduler.setInterval(() => kick(), input.heartbeatMs);
|
|
1102
|
-
pump.unref?.();
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
function stopPump(): void {
|
|
1106
|
-
if (pump === undefined) return;
|
|
1107
|
-
scheduler.clearInterval(pump);
|
|
1108
|
-
pump = undefined;
|
|
994
|
+
state = saved;
|
|
995
|
+
} catch (error) {
|
|
996
|
+
diagnostic = `presence storage unavailable: ${String(error)}`;
|
|
1109
997
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
record = blank(agentId);
|
|
1121
|
-
save();
|
|
998
|
+
let timer: RadarTimer | undefined;
|
|
999
|
+
let stopped = false;
|
|
1000
|
+
let running: Promise<string | undefined> | undefined;
|
|
1001
|
+
function save(): boolean {
|
|
1002
|
+
try {
|
|
1003
|
+
if (state) store.write(recordName, state);
|
|
1004
|
+
return true;
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
diagnostic = `presence storage unavailable: ${String(error)}`;
|
|
1007
|
+
return false;
|
|
1122
1008
|
}
|
|
1123
|
-
return true;
|
|
1124
1009
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
async function send(pending: ContextReport): Promise<void> {
|
|
1134
|
-
const writer = record.writer;
|
|
1135
|
-
const answer = await client.context({
|
|
1136
|
-
agent_id: record.agent_id,
|
|
1137
|
-
publisher,
|
|
1138
|
-
// A first publish presents no handle: the daemon binds the writer the
|
|
1139
|
-
// publisher already is, which is the binding this writer then keeps.
|
|
1140
|
-
...(writer !== undefined ? { writer_handle: writer.handle } : {}),
|
|
1141
|
-
sequence: pending.sequence,
|
|
1142
|
-
lease_ms: pending.lease_ms,
|
|
1143
|
-
observed_at: pending.observed_at,
|
|
1144
|
-
context: {
|
|
1145
|
-
session: pending.session,
|
|
1146
|
-
...(pending.launch_identity !== undefined
|
|
1147
|
-
? { launch_identity: pending.launch_identity }
|
|
1148
|
-
: {}),
|
|
1149
|
-
},
|
|
1150
|
-
});
|
|
1151
|
-
if (answer.ok === true) {
|
|
1152
|
-
recovering = false;
|
|
1153
|
-
record.writer = {
|
|
1154
|
-
handle: answer.value.writer.handle,
|
|
1155
|
-
generation: answer.value.writer.generation,
|
|
1156
|
-
};
|
|
1157
|
-
record.sequence = Math.max(record.sequence, answer.value.writer.sequence);
|
|
1158
|
-
record.accepted = {
|
|
1159
|
-
...pending,
|
|
1160
|
-
sent_at: answer.value.warning === undefined
|
|
1161
|
-
? scheduler.now()
|
|
1162
|
-
: record.accepted?.sent_at ?? scheduler.now(),
|
|
1163
|
-
};
|
|
1164
|
-
record.pending = undefined;
|
|
1165
|
-
save();
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
if (
|
|
1169
|
-
answer.code === "refused" ||
|
|
1170
|
-
answer.code === "bad_params" ||
|
|
1171
|
-
// A daemon that speaks the registry but does not serve this record would
|
|
1172
|
-
// refuse every retry the same way; one diagnostic beats a silent retry
|
|
1173
|
-
// loop once per heartbeat.
|
|
1174
|
-
answer.code === "unknown_method"
|
|
1175
|
-
) {
|
|
1176
|
-
fence(answer.message);
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
if (answer.code === "not_found") {
|
|
1180
|
-
// The subject is gone. Nothing of the old subject can be republished
|
|
1181
|
-
// against, so the whole record is dropped: the next heartbeat registers
|
|
1182
|
-
// the identical content again and binds a fresh writer to it.
|
|
1183
|
-
record = blank("");
|
|
1184
|
-
recovering = true;
|
|
1010
|
+
async function releasePending() {
|
|
1011
|
+
if (!state) return;
|
|
1012
|
+
const registration = await input.registration();
|
|
1013
|
+
if (!registration?.process) return;
|
|
1014
|
+
for (const release of [...state.releases]) {
|
|
1015
|
+
const answer = await client.releasePresence({ ...release, process: registration.process });
|
|
1016
|
+
if (!answer.ok) return;
|
|
1017
|
+
state.releases = state.releases.filter(r => r.claim_id !== release.claim_id);
|
|
1185
1018
|
save();
|
|
1186
|
-
return;
|
|
1187
1019
|
}
|
|
1188
|
-
// A timeout, a closed connection or a lost reply leaves the exact request
|
|
1189
|
-
// pending, so the retry replays it byte for byte and the lease does not
|
|
1190
|
-
// silently advance.
|
|
1191
1020
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
): Promise<void> {
|
|
1197
|
-
const pending: ContextReport = {
|
|
1198
|
-
sequence: record.sequence + 1,
|
|
1199
|
-
session,
|
|
1200
|
-
lease_ms: input.leaseMs,
|
|
1201
|
-
observed_at: new Date(scheduler.now()).toISOString(),
|
|
1202
|
-
...(launchIdentity !== undefined ? { launch_identity: launchIdentity } : {}),
|
|
1203
|
-
};
|
|
1204
|
-
// Persisted before it is sent, so a lost reply replays this exact request.
|
|
1205
|
-
record.pending = pending;
|
|
1206
|
-
save();
|
|
1207
|
-
await send(pending);
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
async function drainOnce(): Promise<void> {
|
|
1211
|
-
if (fenced) return;
|
|
1212
|
-
if (record.pending !== undefined) {
|
|
1213
|
-
// An unresolved request is never overwritten by newer content.
|
|
1214
|
-
await send(record.pending);
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
if (desired === undefined) return;
|
|
1218
|
-
if (!(await ensureSubject())) return;
|
|
1219
|
-
if (fenced) return;
|
|
1220
|
-
if (
|
|
1221
|
-
record.writer === undefined ||
|
|
1222
|
-
record.accepted === undefined ||
|
|
1223
|
-
record.accepted.session !== desired ||
|
|
1224
|
-
!sameLaunchIdentity(record.accepted.launch_identity, desiredMarker)
|
|
1225
|
-
) {
|
|
1226
|
-
await stage(desired, desiredMarker);
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
if (heartbeatDue()) await stage(record.accepted.session, desiredMarker);
|
|
1021
|
+
function schedule() {
|
|
1022
|
+
if (stopped || timer) return;
|
|
1023
|
+
timer = scheduler.setTimeout(() => { timer = undefined; void ensure(); }, input.heartbeatMs);
|
|
1024
|
+
timer.unref?.();
|
|
1230
1025
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
if (
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
if (
|
|
1248
|
-
|
|
1249
|
-
|
|
1026
|
+
async function ensure(): Promise<string | undefined> {
|
|
1027
|
+
if (running) return running;
|
|
1028
|
+
if (stopped || !state) return undefined;
|
|
1029
|
+
const claim = state;
|
|
1030
|
+
if (!save()) return undefined;
|
|
1031
|
+
running = (async () => {
|
|
1032
|
+
await releasePending();
|
|
1033
|
+
const registration = await input.registration();
|
|
1034
|
+
if (!registration?.process || state !== claim || stopped) return undefined;
|
|
1035
|
+
const answer = await client.presence({
|
|
1036
|
+
...registration, process: registration.process,
|
|
1037
|
+
session_id: claim.session, claim_id: claim.claim_id,
|
|
1038
|
+
...(claim.registration_id ? { registration_id: claim.registration_id } : {}),
|
|
1039
|
+
...(claim.subject_id ? { subject_id: claim.subject_id } : {}),
|
|
1040
|
+
});
|
|
1041
|
+
if (state !== claim) return undefined;
|
|
1042
|
+
if (!answer.ok) {
|
|
1043
|
+
if (!["absent", "timeout", "transport"].includes(answer.code)) diagnostic = answer.message;
|
|
1044
|
+
return undefined;
|
|
1250
1045
|
}
|
|
1046
|
+
claim.registration_id = answer.value.registration_id;
|
|
1047
|
+
claim.subject_id = answer.value.standing === "granted" ? answer.value.subject_id ?? undefined : undefined;
|
|
1048
|
+
standing = answer.value.standing === "granted"
|
|
1049
|
+
? { state: "managed", subject_id: claim.subject_id! }
|
|
1050
|
+
: answer.value.standing === "duplicate"
|
|
1051
|
+
? { state: "duplicate" }
|
|
1052
|
+
: answer.value.standing === "pending"
|
|
1053
|
+
? { state: "pending" }
|
|
1054
|
+
: { state: "unmanaged" };
|
|
1055
|
+
diagnostic = answer.value.standing === "duplicate" ? DUPLICATE_SESSION_DIAGNOSTIC : undefined;
|
|
1056
|
+
save();
|
|
1057
|
+
return claim.registration_id;
|
|
1058
|
+
})();
|
|
1059
|
+
try { return await running; }
|
|
1060
|
+
catch (error) { diagnostic = `presence unavailable: ${String(error)}`; return undefined; }
|
|
1061
|
+
finally {
|
|
1062
|
+
running = undefined;
|
|
1063
|
+
if (!stopped && state !== claim) void ensure();
|
|
1064
|
+
else schedule();
|
|
1251
1065
|
}
|
|
1252
1066
|
}
|
|
1253
|
-
|
|
1254
|
-
function kick(): void {
|
|
1255
|
-
if (stopped || fenced) return;
|
|
1256
|
-
startPump();
|
|
1257
|
-
if (running) {
|
|
1258
|
-
again = true;
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
void drain().catch(() => undefined);
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
1067
|
return {
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
(
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
// session the process is in now rather than the one it left.
|
|
1275
|
-
desired = session;
|
|
1276
|
-
desiredMarker = launchIdentity;
|
|
1277
|
-
if (stopped) return;
|
|
1278
|
-
kick();
|
|
1068
|
+
report(session) {
|
|
1069
|
+
const releases = [...(state?.releases ?? [])];
|
|
1070
|
+
if (state?.registration_id && !releases.some(release => release.claim_id === state!.claim_id)) {
|
|
1071
|
+
releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
|
|
1072
|
+
}
|
|
1073
|
+
standing = undefined;
|
|
1074
|
+
state = { version: 2, session, claim_id: randomUUID(), releases };
|
|
1075
|
+
save(); stopped = false;
|
|
1076
|
+
void ensure();
|
|
1279
1077
|
},
|
|
1280
|
-
|
|
1078
|
+
ensure,
|
|
1079
|
+
async release() {
|
|
1080
|
+
const releasing = state;
|
|
1081
|
+
standing = undefined;
|
|
1281
1082
|
stopped = true;
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if (
|
|
1083
|
+
if (timer) scheduler.clearTimeout(timer);
|
|
1084
|
+
timer = undefined;
|
|
1085
|
+
if (running) await running;
|
|
1086
|
+
if (state === releasing) standing = undefined;
|
|
1087
|
+
if (releasing?.registration_id && !releasing.releases.some(r => r.claim_id === releasing.claim_id)) {
|
|
1088
|
+
releasing.releases.push({ registration_id: releasing.registration_id, claim_id: releasing.claim_id });
|
|
1089
|
+
if (!save()) return;
|
|
1090
|
+
}
|
|
1091
|
+
await releasePending();
|
|
1287
1092
|
},
|
|
1288
|
-
|
|
1093
|
+
stop() { stopped = true; if (timer) scheduler.clearTimeout(timer); timer = undefined; },
|
|
1094
|
+
reopen() { stopped = false; void ensure(); },
|
|
1095
|
+
management: () => standing,
|
|
1096
|
+
diagnostic: () => diagnostic,
|
|
1289
1097
|
};
|
|
1290
1098
|
}
|
|
1291
1099
|
|
|
1100
|
+
|
|
1292
1101
|
export type RadarPublication = {
|
|
1293
1102
|
/** This process's own execution channel. */
|
|
1294
1103
|
execution: RadarChannelWriter;
|
|
1295
1104
|
/** This process's own current session. */
|
|
1296
|
-
|
|
1105
|
+
presence: RadarPresenceReporter;
|
|
1297
1106
|
/** This process's own facts, a complete replacement on every update. */
|
|
1298
1107
|
facts: RadarFactsWriter;
|
|
1299
1108
|
/** The assignment channel of one owned child subject. */
|
|
@@ -1317,8 +1126,8 @@ export type RadarPublication = {
|
|
|
1317
1126
|
|
|
1318
1127
|
/** What a process's own registration resolved to, and where it says it is. */
|
|
1319
1128
|
export type RadarSubject = {
|
|
1320
|
-
|
|
1321
|
-
/** Stable managed logical subject
|
|
1129
|
+
registration_id: string;
|
|
1130
|
+
/** Stable managed logical subject: a Radar-issued launch, or an adopted Pi session. */
|
|
1322
1131
|
subject_id?: string;
|
|
1323
1132
|
location?: RadarLocation;
|
|
1324
1133
|
};
|
|
@@ -1335,32 +1144,23 @@ export type RadarPublicationOptions = {
|
|
|
1335
1144
|
scheduler?: RadarScheduler;
|
|
1336
1145
|
/** `undefined` omits the process claim rather than guessing one. */
|
|
1337
1146
|
birth?: ProcessBirth;
|
|
1338
|
-
/**
|
|
1339
|
-
*
|
|
1340
|
-
* record is first built. It is awaited rather than required up front, because
|
|
1341
|
-
* the daemon may not have answered yet - and a gap in that evidence is not a
|
|
1342
|
-
* fact to freeze: only a settled answer writes the immutable record, and a
|
|
1343
|
-
* record that already exists is never rebuilt and never acquires a location
|
|
1344
|
-
* later.
|
|
1147
|
+
/** Current pane observation, refreshed for every presence report. Missing location
|
|
1148
|
+
* leaves process identity reportable but pending subject grant; no pane is guessed.
|
|
1345
1149
|
*/
|
|
1346
1150
|
location?: () => Promise<RadarLocationResolution>;
|
|
1347
1151
|
/** Set on a managed child: the sidecar its owner correlates the subject with. */
|
|
1348
1152
|
binding?: { key: string; run: string; owner: string; label: string };
|
|
1349
|
-
/**
|
|
1350
|
-
* A daemon-spawned child's one-use launch token. It is sent with this
|
|
1351
|
-
* registration only, and never written to the subject record: a private
|
|
1352
|
-
* credential is not part of the identity that record holds.
|
|
1353
|
-
*/
|
|
1153
|
+
/** A daemon-spawned child's one-use launch token; sent privately, never stored in the process record. */
|
|
1354
1154
|
spawnToken?: string;
|
|
1355
1155
|
leaseMs?: number;
|
|
1356
1156
|
heartbeatMs?: number;
|
|
1357
1157
|
};
|
|
1358
1158
|
|
|
1359
|
-
/** The
|
|
1360
|
-
type
|
|
1159
|
+
/** The process registration and its daemon-issued registration ID. */
|
|
1160
|
+
type RegistrationRecord = {
|
|
1361
1161
|
version: typeof RADAR_PUBLICATION_VERSION;
|
|
1362
1162
|
registration: RadarRegistration;
|
|
1363
|
-
|
|
1163
|
+
registration_id?: string;
|
|
1364
1164
|
};
|
|
1365
1165
|
|
|
1366
1166
|
/**
|
|
@@ -1399,10 +1199,13 @@ function unavailablePublication(error?: unknown): RadarPublication {
|
|
|
1399
1199
|
};
|
|
1400
1200
|
return {
|
|
1401
1201
|
execution: writer,
|
|
1402
|
-
|
|
1403
|
-
|
|
1202
|
+
presence: {
|
|
1203
|
+
report: () => undefined,
|
|
1204
|
+
ensure: async () => undefined,
|
|
1205
|
+
release: async () => undefined,
|
|
1404
1206
|
stop: () => undefined,
|
|
1405
1207
|
reopen: () => undefined,
|
|
1208
|
+
management: () => undefined,
|
|
1406
1209
|
diagnostic: () => diagnostic,
|
|
1407
1210
|
},
|
|
1408
1211
|
facts: { ...writer, update: () => undefined },
|
|
@@ -1438,7 +1241,7 @@ function createRadarPublicationInner(
|
|
|
1438
1241
|
return cached;
|
|
1439
1242
|
}
|
|
1440
1243
|
|
|
1441
|
-
const
|
|
1244
|
+
const registrationName = `registrations/${createHash("sha256")
|
|
1442
1245
|
.update(`${options.registration.source}\0${incarnation}`)
|
|
1443
1246
|
.digest("hex")
|
|
1444
1247
|
.slice(0, 32)}.json`;
|
|
@@ -1456,8 +1259,8 @@ function createRadarPublicationInner(
|
|
|
1456
1259
|
.slice(0, 32)}.json`;
|
|
1457
1260
|
|
|
1458
1261
|
/** One current-session record per publisher, for the same reason as channels. */
|
|
1459
|
-
const
|
|
1460
|
-
.update(`
|
|
1262
|
+
const presenceRecord = `presence/${createHash("sha256")
|
|
1263
|
+
.update(`presence\0${incarnation}`)
|
|
1461
1264
|
.digest("hex")
|
|
1462
1265
|
.slice(0, 32)}.json`;
|
|
1463
1266
|
|
|
@@ -1466,8 +1269,8 @@ function createRadarPublicationInner(
|
|
|
1466
1269
|
* stored content wins over a later attempt to build it again: a claim read
|
|
1467
1270
|
* successfully once may not change what this incarnation is registered as.
|
|
1468
1271
|
*/
|
|
1469
|
-
function
|
|
1470
|
-
const value = store.read(
|
|
1272
|
+
function readRegistration(): RegistrationRecord | undefined {
|
|
1273
|
+
const value = store.read(registrationName);
|
|
1471
1274
|
const registration = recordAt(value, "registration");
|
|
1472
1275
|
if (
|
|
1473
1276
|
numberAt(value, "version") !== RADAR_PUBLICATION_VERSION ||
|
|
@@ -1475,20 +1278,20 @@ function createRadarPublicationInner(
|
|
|
1475
1278
|
textAt(registration, "incarnation") !== incarnation
|
|
1476
1279
|
)
|
|
1477
1280
|
return undefined;
|
|
1478
|
-
const storedAgentId = textAt(value, "
|
|
1281
|
+
const storedAgentId = textAt(value, "registration_id");
|
|
1479
1282
|
return {
|
|
1480
1283
|
version: RADAR_PUBLICATION_VERSION,
|
|
1481
1284
|
registration: registration as RadarRegistration,
|
|
1482
1285
|
...(storedAgentId !== undefined && isUuid(storedAgentId)
|
|
1483
|
-
? {
|
|
1286
|
+
? { registration_id: storedAgentId }
|
|
1484
1287
|
: {}),
|
|
1485
1288
|
};
|
|
1486
1289
|
}
|
|
1487
1290
|
|
|
1488
|
-
const stored =
|
|
1489
|
-
let
|
|
1291
|
+
const stored = readRegistration();
|
|
1292
|
+
let processRecord: RegistrationRecord | undefined = stored;
|
|
1490
1293
|
let registrationDiagnostic: string | undefined;
|
|
1491
|
-
let registrationRecord: Promise<
|
|
1294
|
+
let registrationRecord: Promise<RegistrationRecord | undefined> | undefined;
|
|
1492
1295
|
/**
|
|
1493
1296
|
* The immutable registration is written before it is ever sent, and the stored
|
|
1494
1297
|
* content wins over a later attempt to build it again: a claim read
|
|
@@ -1503,18 +1306,13 @@ function createRadarPublicationInner(
|
|
|
1503
1306
|
* never be repaired without replacing the identity it belongs to; the next
|
|
1504
1307
|
* attempt asks the daemon again instead.
|
|
1505
1308
|
*/
|
|
1506
|
-
async function ensureRegistration(): Promise<
|
|
1507
|
-
if (
|
|
1309
|
+
async function ensureRegistration(): Promise<RegistrationRecord | undefined> {
|
|
1310
|
+
if (processRecord !== undefined) return processRecord;
|
|
1508
1311
|
registrationRecord ??= (async () => {
|
|
1509
1312
|
const resolution = options.location
|
|
1510
1313
|
? await options.location()
|
|
1511
1314
|
: ({ state: "absent", reason: "this process publishes no location" } as const);
|
|
1512
|
-
|
|
1513
|
-
registrationDiagnostic =
|
|
1514
|
-
`location bootstrap pending: ${resolution.reason}`.slice(0, 256);
|
|
1515
|
-
return undefined;
|
|
1516
|
-
}
|
|
1517
|
-
const built: SubjectRecord = {
|
|
1315
|
+
const built: RegistrationRecord = {
|
|
1518
1316
|
version: RADAR_PUBLICATION_VERSION,
|
|
1519
1317
|
registration: {
|
|
1520
1318
|
...options.registration,
|
|
@@ -1526,14 +1324,14 @@ function createRadarPublicationInner(
|
|
|
1526
1324
|
},
|
|
1527
1325
|
};
|
|
1528
1326
|
try {
|
|
1529
|
-
store.write(
|
|
1327
|
+
store.write(registrationName, built);
|
|
1530
1328
|
} catch (error) {
|
|
1531
1329
|
registrationDiagnostic ??=
|
|
1532
1330
|
`private storage unavailable: ${String(error)}`.slice(0, 256);
|
|
1533
1331
|
return undefined;
|
|
1534
1332
|
}
|
|
1535
1333
|
registrationDiagnostic = undefined;
|
|
1536
|
-
|
|
1334
|
+
processRecord = built;
|
|
1537
1335
|
return built;
|
|
1538
1336
|
})();
|
|
1539
1337
|
try {
|
|
@@ -1542,11 +1340,10 @@ function createRadarPublicationInner(
|
|
|
1542
1340
|
registrationRecord = undefined;
|
|
1543
1341
|
}
|
|
1544
1342
|
}
|
|
1545
|
-
let agentId = stored?.
|
|
1343
|
+
let agentId = stored?.registration_id;
|
|
1546
1344
|
let subjectReady: Promise<string | undefined> | undefined;
|
|
1547
1345
|
async function ensureSubject(recheck: boolean): Promise<string | undefined> {
|
|
1548
|
-
if (recheck
|
|
1549
|
-
if (agentId !== undefined) return agentId;
|
|
1346
|
+
if (!recheck && agentId !== undefined) return agentId;
|
|
1550
1347
|
subjectReady ??= (async () => {
|
|
1551
1348
|
const ready = await client.ping();
|
|
1552
1349
|
if (ready.ok === false) {
|
|
@@ -1556,18 +1353,11 @@ function createRadarPublicationInner(
|
|
|
1556
1353
|
}
|
|
1557
1354
|
const record = await ensureRegistration();
|
|
1558
1355
|
if (record === undefined) return undefined;
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
if (answer.code !== "absent" && answer.code !== "timeout" && answer.code !== "transport")
|
|
1565
|
-
registrationDiagnostic ??= `subject registration refused: ${answer.message}`.slice(0, 256);
|
|
1566
|
-
return undefined;
|
|
1567
|
-
}
|
|
1568
|
-
agentId = answer.value.agent_id;
|
|
1569
|
-
subject = { ...record, agent_id: agentId };
|
|
1570
|
-
store.write(subjectName, subject);
|
|
1356
|
+
const registered = await presence.ensure();
|
|
1357
|
+
if (registered === undefined) return undefined;
|
|
1358
|
+
agentId = registered;
|
|
1359
|
+
processRecord = { ...record, registration_id: agentId };
|
|
1360
|
+
store.write(registrationName, processRecord);
|
|
1571
1361
|
if (options.binding !== undefined) writeBinding(options.binding);
|
|
1572
1362
|
return agentId;
|
|
1573
1363
|
})();
|
|
@@ -1587,7 +1377,7 @@ function createRadarPublicationInner(
|
|
|
1587
1377
|
if (agentId === undefined) return;
|
|
1588
1378
|
store.write(`bindings/${binding.key}.json`, {
|
|
1589
1379
|
version: RADAR_PUBLICATION_VERSION,
|
|
1590
|
-
|
|
1380
|
+
registration_id: agentId,
|
|
1591
1381
|
incarnation,
|
|
1592
1382
|
run: binding.run,
|
|
1593
1383
|
owner: binding.owner,
|
|
@@ -1617,14 +1407,18 @@ function createRadarPublicationInner(
|
|
|
1617
1407
|
heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS,
|
|
1618
1408
|
});
|
|
1619
1409
|
|
|
1620
|
-
const
|
|
1621
|
-
store,
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1410
|
+
const presence = createPresenceReporter({
|
|
1411
|
+
store, client, scheduler, recordName: presenceRecord,
|
|
1412
|
+
registration: async () => {
|
|
1413
|
+
const record = await ensureRegistration();
|
|
1414
|
+
if (!record) return undefined;
|
|
1415
|
+
const location = await options.location?.();
|
|
1416
|
+
const { location: _initialLocation, ...identity } = record.registration;
|
|
1417
|
+
return { ...identity,
|
|
1418
|
+
...(location?.state === "resolved" ? { location: location.location } : {}),
|
|
1419
|
+
...(spawnToken ? { spawn_token: spawnToken } : {}),
|
|
1420
|
+
};
|
|
1421
|
+
},
|
|
1628
1422
|
heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS,
|
|
1629
1423
|
});
|
|
1630
1424
|
|
|
@@ -1645,7 +1439,7 @@ function createRadarPublicationInner(
|
|
|
1645
1439
|
|
|
1646
1440
|
const publication: RadarPublication = {
|
|
1647
1441
|
execution,
|
|
1648
|
-
|
|
1442
|
+
presence,
|
|
1649
1443
|
facts,
|
|
1650
1444
|
assignment(target, reportingOwner: string) {
|
|
1651
1445
|
const existing = assignmentWriters.get(target);
|
|
@@ -1660,7 +1454,7 @@ function createRadarPublicationInner(
|
|
|
1660
1454
|
publisher: publisher("herdsman-owner", reportingOwner),
|
|
1661
1455
|
codec: assignmentCodec,
|
|
1662
1456
|
// The owner publishes about a subject it did not register: the exact
|
|
1663
|
-
//
|
|
1457
|
+
// registration_id from the child's sidecar is the whole correlation.
|
|
1664
1458
|
subject: async () => target,
|
|
1665
1459
|
leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
|
|
1666
1460
|
heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS,
|
|
@@ -1680,7 +1474,7 @@ function createRadarPublicationInner(
|
|
|
1680
1474
|
registrationDiagnostic ??= `Radar binding unavailable: ${String(error).slice(0, 180)}`;
|
|
1681
1475
|
return undefined;
|
|
1682
1476
|
}
|
|
1683
|
-
const bound = textAt(value, "
|
|
1477
|
+
const bound = textAt(value, "registration_id");
|
|
1684
1478
|
const boundIncarnation = textAt(value, "incarnation");
|
|
1685
1479
|
const run = textAt(value, "run");
|
|
1686
1480
|
const owner = textAt(value, "owner");
|
|
@@ -1699,7 +1493,7 @@ function createRadarPublicationInner(
|
|
|
1699
1493
|
return undefined;
|
|
1700
1494
|
return {
|
|
1701
1495
|
version: RADAR_PUBLICATION_VERSION,
|
|
1702
|
-
|
|
1496
|
+
registration_id: bound,
|
|
1703
1497
|
incarnation: boundIncarnation,
|
|
1704
1498
|
run,
|
|
1705
1499
|
owner,
|
|
@@ -1717,10 +1511,13 @@ function createRadarPublicationInner(
|
|
|
1717
1511
|
if (agentId === undefined) return undefined;
|
|
1718
1512
|
const record = await ensureRegistration();
|
|
1719
1513
|
if (record === undefined) return undefined;
|
|
1720
|
-
const
|
|
1721
|
-
const
|
|
1514
|
+
const currentLocation = await options.location?.();
|
|
1515
|
+
const location = currentLocation?.state === "resolved" ? currentLocation.location : undefined;
|
|
1516
|
+
// Radar's answer to this process's current session names its subject.
|
|
1517
|
+
const standing = presence.management();
|
|
1518
|
+
const subjectId = standing?.state === "managed" ? standing.subject_id : undefined;
|
|
1722
1519
|
return {
|
|
1723
|
-
|
|
1520
|
+
registration_id: agentId,
|
|
1724
1521
|
...(subjectId !== undefined ? { subject_id: subjectId } : {}),
|
|
1725
1522
|
...(location !== undefined ? { location } : {}),
|
|
1726
1523
|
};
|
|
@@ -1728,18 +1525,18 @@ function createRadarPublicationInner(
|
|
|
1728
1525
|
diagnostic: () =>
|
|
1729
1526
|
registrationDiagnostic ??
|
|
1730
1527
|
execution.diagnostic() ??
|
|
1731
|
-
|
|
1528
|
+
presence.diagnostic() ??
|
|
1732
1529
|
facts.diagnostic(),
|
|
1733
1530
|
stop() {
|
|
1734
1531
|
for (const writer of assignmentWriters.values()) writer.stop();
|
|
1735
1532
|
execution.stop();
|
|
1736
|
-
|
|
1533
|
+
presence.stop();
|
|
1737
1534
|
facts.stop();
|
|
1738
1535
|
},
|
|
1739
1536
|
reopen() {
|
|
1740
1537
|
for (const writer of assignmentWriters.values()) writer.reopen();
|
|
1741
1538
|
execution.reopen();
|
|
1742
|
-
|
|
1539
|
+
presence.reopen();
|
|
1743
1540
|
facts.reopen();
|
|
1744
1541
|
},
|
|
1745
1542
|
};
|