pi-radar 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/extension/baseline.js +333 -394
- package/dist/extension/radar-client.js +126 -11
- package/dist/extension/radar-communication.js +39 -0
- package/dist/extension/radar-execution.js +169 -382
- package/dist/extension/radar-launch.js +18 -13
- package/dist/extension/radar-publication.js +161 -298
- package/dist/extension/radar-runtime.js +1 -1
- package/dist/index.js +356 -407
- package/extension/baseline.ts +2 -0
- package/extension/generated-contracts.ts +1310 -10
- package/extension/radar-client.ts +191 -23
- package/extension/radar-communication.ts +66 -0
- package/extension/radar-execution.ts +11 -58
- package/extension/radar-launch.ts +29 -22
- package/extension/radar-publication.ts +211 -410
- package/extension/radar-runtime.ts +4 -1
- package/index.ts +1 -0
- package/package.json +9 -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/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,7 +1126,9 @@ 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
|
-
|
|
1129
|
+
registration_id: string;
|
|
1130
|
+
/** Stable managed logical subject: a Radar-issued launch, or an adopted Pi session. */
|
|
1131
|
+
subject_id?: string;
|
|
1321
1132
|
location?: RadarLocation;
|
|
1322
1133
|
};
|
|
1323
1134
|
|
|
@@ -1333,32 +1144,23 @@ export type RadarPublicationOptions = {
|
|
|
1333
1144
|
scheduler?: RadarScheduler;
|
|
1334
1145
|
/** `undefined` omits the process claim rather than guessing one. */
|
|
1335
1146
|
birth?: ProcessBirth;
|
|
1336
|
-
/**
|
|
1337
|
-
*
|
|
1338
|
-
* record is first built. It is awaited rather than required up front, because
|
|
1339
|
-
* the daemon may not have answered yet - and a gap in that evidence is not a
|
|
1340
|
-
* fact to freeze: only a settled answer writes the immutable record, and a
|
|
1341
|
-
* record that already exists is never rebuilt and never acquires a location
|
|
1342
|
-
* 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.
|
|
1343
1149
|
*/
|
|
1344
1150
|
location?: () => Promise<RadarLocationResolution>;
|
|
1345
1151
|
/** Set on a managed child: the sidecar its owner correlates the subject with. */
|
|
1346
1152
|
binding?: { key: string; run: string; owner: string; label: string };
|
|
1347
|
-
/**
|
|
1348
|
-
* A daemon-spawned child's one-use launch token. It is sent with this
|
|
1349
|
-
* registration only, and never written to the subject record: a private
|
|
1350
|
-
* credential is not part of the identity that record holds.
|
|
1351
|
-
*/
|
|
1153
|
+
/** A daemon-spawned child's one-use launch token; sent privately, never stored in the process record. */
|
|
1352
1154
|
spawnToken?: string;
|
|
1353
1155
|
leaseMs?: number;
|
|
1354
1156
|
heartbeatMs?: number;
|
|
1355
1157
|
};
|
|
1356
1158
|
|
|
1357
|
-
/** The
|
|
1358
|
-
type
|
|
1159
|
+
/** The process registration and its daemon-issued registration ID. */
|
|
1160
|
+
type RegistrationRecord = {
|
|
1359
1161
|
version: typeof RADAR_PUBLICATION_VERSION;
|
|
1360
1162
|
registration: RadarRegistration;
|
|
1361
|
-
|
|
1163
|
+
registration_id?: string;
|
|
1362
1164
|
};
|
|
1363
1165
|
|
|
1364
1166
|
/**
|
|
@@ -1397,10 +1199,13 @@ function unavailablePublication(error?: unknown): RadarPublication {
|
|
|
1397
1199
|
};
|
|
1398
1200
|
return {
|
|
1399
1201
|
execution: writer,
|
|
1400
|
-
|
|
1401
|
-
|
|
1202
|
+
presence: {
|
|
1203
|
+
report: () => undefined,
|
|
1204
|
+
ensure: async () => undefined,
|
|
1205
|
+
release: async () => undefined,
|
|
1402
1206
|
stop: () => undefined,
|
|
1403
1207
|
reopen: () => undefined,
|
|
1208
|
+
management: () => undefined,
|
|
1404
1209
|
diagnostic: () => diagnostic,
|
|
1405
1210
|
},
|
|
1406
1211
|
facts: { ...writer, update: () => undefined },
|
|
@@ -1436,7 +1241,7 @@ function createRadarPublicationInner(
|
|
|
1436
1241
|
return cached;
|
|
1437
1242
|
}
|
|
1438
1243
|
|
|
1439
|
-
const
|
|
1244
|
+
const registrationName = `registrations/${createHash("sha256")
|
|
1440
1245
|
.update(`${options.registration.source}\0${incarnation}`)
|
|
1441
1246
|
.digest("hex")
|
|
1442
1247
|
.slice(0, 32)}.json`;
|
|
@@ -1454,8 +1259,8 @@ function createRadarPublicationInner(
|
|
|
1454
1259
|
.slice(0, 32)}.json`;
|
|
1455
1260
|
|
|
1456
1261
|
/** One current-session record per publisher, for the same reason as channels. */
|
|
1457
|
-
const
|
|
1458
|
-
.update(`
|
|
1262
|
+
const presenceRecord = `presence/${createHash("sha256")
|
|
1263
|
+
.update(`presence\0${incarnation}`)
|
|
1459
1264
|
.digest("hex")
|
|
1460
1265
|
.slice(0, 32)}.json`;
|
|
1461
1266
|
|
|
@@ -1464,8 +1269,8 @@ function createRadarPublicationInner(
|
|
|
1464
1269
|
* stored content wins over a later attempt to build it again: a claim read
|
|
1465
1270
|
* successfully once may not change what this incarnation is registered as.
|
|
1466
1271
|
*/
|
|
1467
|
-
function
|
|
1468
|
-
const value = store.read(
|
|
1272
|
+
function readRegistration(): RegistrationRecord | undefined {
|
|
1273
|
+
const value = store.read(registrationName);
|
|
1469
1274
|
const registration = recordAt(value, "registration");
|
|
1470
1275
|
if (
|
|
1471
1276
|
numberAt(value, "version") !== RADAR_PUBLICATION_VERSION ||
|
|
@@ -1473,20 +1278,20 @@ function createRadarPublicationInner(
|
|
|
1473
1278
|
textAt(registration, "incarnation") !== incarnation
|
|
1474
1279
|
)
|
|
1475
1280
|
return undefined;
|
|
1476
|
-
const storedAgentId = textAt(value, "
|
|
1281
|
+
const storedAgentId = textAt(value, "registration_id");
|
|
1477
1282
|
return {
|
|
1478
1283
|
version: RADAR_PUBLICATION_VERSION,
|
|
1479
1284
|
registration: registration as RadarRegistration,
|
|
1480
1285
|
...(storedAgentId !== undefined && isUuid(storedAgentId)
|
|
1481
|
-
? {
|
|
1286
|
+
? { registration_id: storedAgentId }
|
|
1482
1287
|
: {}),
|
|
1483
1288
|
};
|
|
1484
1289
|
}
|
|
1485
1290
|
|
|
1486
|
-
const stored =
|
|
1487
|
-
let
|
|
1291
|
+
const stored = readRegistration();
|
|
1292
|
+
let processRecord: RegistrationRecord | undefined = stored;
|
|
1488
1293
|
let registrationDiagnostic: string | undefined;
|
|
1489
|
-
let registrationRecord: Promise<
|
|
1294
|
+
let registrationRecord: Promise<RegistrationRecord | undefined> | undefined;
|
|
1490
1295
|
/**
|
|
1491
1296
|
* The immutable registration is written before it is ever sent, and the stored
|
|
1492
1297
|
* content wins over a later attempt to build it again: a claim read
|
|
@@ -1501,18 +1306,13 @@ function createRadarPublicationInner(
|
|
|
1501
1306
|
* never be repaired without replacing the identity it belongs to; the next
|
|
1502
1307
|
* attempt asks the daemon again instead.
|
|
1503
1308
|
*/
|
|
1504
|
-
async function ensureRegistration(): Promise<
|
|
1505
|
-
if (
|
|
1309
|
+
async function ensureRegistration(): Promise<RegistrationRecord | undefined> {
|
|
1310
|
+
if (processRecord !== undefined) return processRecord;
|
|
1506
1311
|
registrationRecord ??= (async () => {
|
|
1507
1312
|
const resolution = options.location
|
|
1508
1313
|
? await options.location()
|
|
1509
1314
|
: ({ state: "absent", reason: "this process publishes no location" } as const);
|
|
1510
|
-
|
|
1511
|
-
registrationDiagnostic =
|
|
1512
|
-
`location bootstrap pending: ${resolution.reason}`.slice(0, 256);
|
|
1513
|
-
return undefined;
|
|
1514
|
-
}
|
|
1515
|
-
const built: SubjectRecord = {
|
|
1315
|
+
const built: RegistrationRecord = {
|
|
1516
1316
|
version: RADAR_PUBLICATION_VERSION,
|
|
1517
1317
|
registration: {
|
|
1518
1318
|
...options.registration,
|
|
@@ -1524,14 +1324,14 @@ function createRadarPublicationInner(
|
|
|
1524
1324
|
},
|
|
1525
1325
|
};
|
|
1526
1326
|
try {
|
|
1527
|
-
store.write(
|
|
1327
|
+
store.write(registrationName, built);
|
|
1528
1328
|
} catch (error) {
|
|
1529
1329
|
registrationDiagnostic ??=
|
|
1530
1330
|
`private storage unavailable: ${String(error)}`.slice(0, 256);
|
|
1531
1331
|
return undefined;
|
|
1532
1332
|
}
|
|
1533
1333
|
registrationDiagnostic = undefined;
|
|
1534
|
-
|
|
1334
|
+
processRecord = built;
|
|
1535
1335
|
return built;
|
|
1536
1336
|
})();
|
|
1537
1337
|
try {
|
|
@@ -1540,11 +1340,10 @@ function createRadarPublicationInner(
|
|
|
1540
1340
|
registrationRecord = undefined;
|
|
1541
1341
|
}
|
|
1542
1342
|
}
|
|
1543
|
-
let agentId = stored?.
|
|
1343
|
+
let agentId = stored?.registration_id;
|
|
1544
1344
|
let subjectReady: Promise<string | undefined> | undefined;
|
|
1545
1345
|
async function ensureSubject(recheck: boolean): Promise<string | undefined> {
|
|
1546
|
-
if (recheck
|
|
1547
|
-
if (agentId !== undefined) return agentId;
|
|
1346
|
+
if (!recheck && agentId !== undefined) return agentId;
|
|
1548
1347
|
subjectReady ??= (async () => {
|
|
1549
1348
|
const ready = await client.ping();
|
|
1550
1349
|
if (ready.ok === false) {
|
|
@@ -1554,18 +1353,11 @@ function createRadarPublicationInner(
|
|
|
1554
1353
|
}
|
|
1555
1354
|
const record = await ensureRegistration();
|
|
1556
1355
|
if (record === undefined) return undefined;
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
if (answer.code !== "absent" && answer.code !== "timeout" && answer.code !== "transport")
|
|
1563
|
-
registrationDiagnostic ??= `subject registration refused: ${answer.message}`.slice(0, 256);
|
|
1564
|
-
return undefined;
|
|
1565
|
-
}
|
|
1566
|
-
agentId = answer.value.agent_id;
|
|
1567
|
-
subject = { ...record, agent_id: agentId };
|
|
1568
|
-
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);
|
|
1569
1361
|
if (options.binding !== undefined) writeBinding(options.binding);
|
|
1570
1362
|
return agentId;
|
|
1571
1363
|
})();
|
|
@@ -1585,7 +1377,7 @@ function createRadarPublicationInner(
|
|
|
1585
1377
|
if (agentId === undefined) return;
|
|
1586
1378
|
store.write(`bindings/${binding.key}.json`, {
|
|
1587
1379
|
version: RADAR_PUBLICATION_VERSION,
|
|
1588
|
-
|
|
1380
|
+
registration_id: agentId,
|
|
1589
1381
|
incarnation,
|
|
1590
1382
|
run: binding.run,
|
|
1591
1383
|
owner: binding.owner,
|
|
@@ -1615,14 +1407,18 @@ function createRadarPublicationInner(
|
|
|
1615
1407
|
heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS,
|
|
1616
1408
|
});
|
|
1617
1409
|
|
|
1618
|
-
const
|
|
1619
|
-
store,
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
+
},
|
|
1626
1422
|
heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS,
|
|
1627
1423
|
});
|
|
1628
1424
|
|
|
@@ -1643,7 +1439,7 @@ function createRadarPublicationInner(
|
|
|
1643
1439
|
|
|
1644
1440
|
const publication: RadarPublication = {
|
|
1645
1441
|
execution,
|
|
1646
|
-
|
|
1442
|
+
presence,
|
|
1647
1443
|
facts,
|
|
1648
1444
|
assignment(target, reportingOwner: string) {
|
|
1649
1445
|
const existing = assignmentWriters.get(target);
|
|
@@ -1658,7 +1454,7 @@ function createRadarPublicationInner(
|
|
|
1658
1454
|
publisher: publisher("herdsman-owner", reportingOwner),
|
|
1659
1455
|
codec: assignmentCodec,
|
|
1660
1456
|
// The owner publishes about a subject it did not register: the exact
|
|
1661
|
-
//
|
|
1457
|
+
// registration_id from the child's sidecar is the whole correlation.
|
|
1662
1458
|
subject: async () => target,
|
|
1663
1459
|
leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
|
|
1664
1460
|
heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS,
|
|
@@ -1678,7 +1474,7 @@ function createRadarPublicationInner(
|
|
|
1678
1474
|
registrationDiagnostic ??= `Radar binding unavailable: ${String(error).slice(0, 180)}`;
|
|
1679
1475
|
return undefined;
|
|
1680
1476
|
}
|
|
1681
|
-
const bound = textAt(value, "
|
|
1477
|
+
const bound = textAt(value, "registration_id");
|
|
1682
1478
|
const boundIncarnation = textAt(value, "incarnation");
|
|
1683
1479
|
const run = textAt(value, "run");
|
|
1684
1480
|
const owner = textAt(value, "owner");
|
|
@@ -1697,7 +1493,7 @@ function createRadarPublicationInner(
|
|
|
1697
1493
|
return undefined;
|
|
1698
1494
|
return {
|
|
1699
1495
|
version: RADAR_PUBLICATION_VERSION,
|
|
1700
|
-
|
|
1496
|
+
registration_id: bound,
|
|
1701
1497
|
incarnation: boundIncarnation,
|
|
1702
1498
|
run,
|
|
1703
1499
|
owner,
|
|
@@ -1715,27 +1511,32 @@ function createRadarPublicationInner(
|
|
|
1715
1511
|
if (agentId === undefined) return undefined;
|
|
1716
1512
|
const record = await ensureRegistration();
|
|
1717
1513
|
if (record === undefined) return undefined;
|
|
1718
|
-
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;
|
|
1719
1519
|
return {
|
|
1720
|
-
|
|
1520
|
+
registration_id: agentId,
|
|
1521
|
+
...(subjectId !== undefined ? { subject_id: subjectId } : {}),
|
|
1721
1522
|
...(location !== undefined ? { location } : {}),
|
|
1722
1523
|
};
|
|
1723
1524
|
},
|
|
1724
1525
|
diagnostic: () =>
|
|
1725
1526
|
registrationDiagnostic ??
|
|
1726
1527
|
execution.diagnostic() ??
|
|
1727
|
-
|
|
1528
|
+
presence.diagnostic() ??
|
|
1728
1529
|
facts.diagnostic(),
|
|
1729
1530
|
stop() {
|
|
1730
1531
|
for (const writer of assignmentWriters.values()) writer.stop();
|
|
1731
1532
|
execution.stop();
|
|
1732
|
-
|
|
1533
|
+
presence.stop();
|
|
1733
1534
|
facts.stop();
|
|
1734
1535
|
},
|
|
1735
1536
|
reopen() {
|
|
1736
1537
|
for (const writer of assignmentWriters.values()) writer.reopen();
|
|
1737
1538
|
execution.reopen();
|
|
1738
|
-
|
|
1539
|
+
presence.reopen();
|
|
1739
1540
|
facts.reopen();
|
|
1740
1541
|
},
|
|
1741
1542
|
};
|