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.
Files changed (36) hide show
  1. package/README.md +4 -2
  2. package/dist/extension/baseline.js +227 -402
  3. package/dist/extension/radar-client.js +53 -13
  4. package/dist/extension/radar-communication.js +5 -5
  5. package/dist/extension/radar-execution.js +168 -383
  6. package/dist/extension/radar-launch.js +13 -13
  7. package/dist/extension/radar-publication.js +160 -299
  8. package/dist/index.js +239 -414
  9. package/extension/generated-contracts.ts +1249 -13
  10. package/extension/radar-client.ts +92 -29
  11. package/extension/radar-communication.ts +5 -5
  12. package/extension/radar-execution.ts +11 -58
  13. package/extension/radar-launch.ts +21 -21
  14. package/extension/radar-publication.ts +209 -412
  15. package/package.json +3 -4
  16. package/schema/public/v1/handoff-publish-params.deserialize.schema.json +2 -2
  17. package/schema/public/v1/handoff-publish-params.serialize.schema.json +2 -2
  18. package/schema/public/v1/handoff-receipt.deserialize.schema.json +4 -4
  19. package/schema/public/v1/handoff-receipt.serialize.schema.json +4 -4
  20. package/schema/public/v1/handoff.deserialize.schema.json +4 -4
  21. package/schema/public/v1/handoff.serialize.schema.json +4 -4
  22. package/schema/public/v1/observation-get-params.deserialize.schema.json +46 -0
  23. package/schema/public/v1/observation-get-params.serialize.schema.json +52 -0
  24. package/schema/public/v1/observation-get-response.deserialize.schema.json +2731 -0
  25. package/schema/public/v1/observation-get-response.serialize.schema.json +2862 -0
  26. package/schema/public/v1/observation-snapshot.deserialize.schema.json +2616 -0
  27. package/schema/public/v1/observation-snapshot.serialize.schema.json +2741 -0
  28. package/schema/public/v1/presence-answer.deserialize.schema.json +40 -0
  29. package/schema/public/v1/presence-answer.serialize.schema.json +40 -0
  30. package/schema/public/v1/presence-release.deserialize.schema.json +51 -0
  31. package/schema/public/v1/presence-release.serialize.schema.json +52 -0
  32. package/schema/public/v1/presence-report.deserialize.schema.json +149 -0
  33. package/schema/public/v1/presence-report.serialize.schema.json +157 -0
  34. package/extension/radar-session-marker.ts +0 -146
  35. package/schema/public/v1/launch-identity-marker.deserialize.schema.json +0 -32
  36. package/schema/public/v1/launch-identity-marker.serialize.schema.json +0 -32
@@ -15,27 +15,6 @@ import {
15
15
  } from "node:fs";
16
16
  import { dirname, join, parse, resolve, sep } from "node:path";
17
17
 
18
- // extension/radar-session-marker.ts
19
- var RADAR_LAUNCH_MARKER_VERSION = 1;
20
- var RADAR_MAX_EXECUTION_GENERATION = 4294967295;
21
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
22
- function decodeRadarLaunchMarker(value) {
23
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
24
- const record = value;
25
- if (Object.keys(record).sort().join(",") !== "execution_generation,launch_id,subject_id,version") {
26
- return void 0;
27
- }
28
- if (record.version !== RADAR_LAUNCH_MARKER_VERSION || typeof record.subject_id !== "string" || !UUID.test(record.subject_id) || typeof record.launch_id !== "string" || !UUID.test(record.launch_id) || !Number.isSafeInteger(record.execution_generation) || record.execution_generation < 1 || record.execution_generation > RADAR_MAX_EXECUTION_GENERATION) {
29
- return void 0;
30
- }
31
- return {
32
- version: RADAR_LAUNCH_MARKER_VERSION,
33
- subject_id: record.subject_id,
34
- launch_id: record.launch_id,
35
- execution_generation: record.execution_generation
36
- };
37
- }
38
-
39
18
  // extension/radar-client.ts
40
19
  var RADAR_MAX_LINE_BYTES = 1024 * 1024;
41
20
  var wallClock = { setTimeout, clearTimeout, setInterval, clearInterval };
@@ -53,9 +32,9 @@ function bounded(text, bytes) {
53
32
  end -= 1;
54
33
  return text.slice(0, end);
55
34
  }
56
- var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
35
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
57
36
  function isUuid(value) {
58
- return typeof value === "string" && UUID2.test(value);
37
+ return typeof value === "string" && UUID.test(value);
59
38
  }
60
39
  var RADAR_MAX_BACKGROUND_TASKS = 64;
61
40
  var RADAR_MAX_AWAITED = 64;
@@ -133,6 +112,7 @@ function boundedRadarSnapshot(snapshot) {
133
112
  }
134
113
 
135
114
  // extension/radar-publication.ts
115
+ var DUPLICATE_SESSION_DIAGNOSTIC = "Radar rejected this session claim as a duplicate; reload or resume the session before retrying";
136
116
  var RADAR_PUBLICATION_VERSION = 1;
137
117
  var RADAR_LEASE_MS = 3e4;
138
118
  var RADAR_HEARTBEAT_MS = 15e3;
@@ -157,7 +137,7 @@ function readProcessBirth() {
157
137
  return void 0;
158
138
  }
159
139
  }
160
- var RECORD_NAME = /^(?:subjects|channels|context|bindings)\/[a-z0-9-]{1,64}\.json$/;
140
+ var RECORD_NAME = /^(?:registrations|presence|subjects|channels|context|bindings)\/[a-z0-9-]{1,64}\.json$/;
161
141
  var MAX_RECORD_BYTES = 64 * 1024;
162
142
  var PRIVATE_MODE = 448;
163
143
  var RECORD_MODE = 384;
@@ -442,39 +422,6 @@ function pendingAt(value, keys = PENDING_KEYS, codec = executionCodec) {
442
422
  return void 0;
443
423
  return { sequence, snapshot, lease_ms: lease, observed_at: observed };
444
424
  }
445
- var CONTEXT_PENDING_KEYS = "lease_ms,observed_at,sequence,session";
446
- var CONTEXT_PENDING_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sequence,session";
447
- var CONTEXT_ACCEPTED_KEYS = "lease_ms,observed_at,sent_at,sequence,session";
448
- var CONTEXT_ACCEPTED_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sent_at,sequence,session";
449
- function sessionAt(value) {
450
- if (typeof value !== "object" || value === null) return void 0;
451
- const found = value.session;
452
- if (found === null) return null;
453
- return typeof found === "string" ? found : void 0;
454
- }
455
- function sameLaunchIdentity(a, b) {
456
- return a === b || a !== void 0 && b !== void 0 && a.version === b.version && a.subject_id === b.subject_id && a.launch_id === b.launch_id && a.execution_generation === b.execution_generation;
457
- }
458
- function contextReportAt(value, keys, markedKeys) {
459
- const marked = exactKeys(value, markedKeys);
460
- if (!exactKeys(value, keys) && !marked) return void 0;
461
- const sequence = numberAt(value, "sequence");
462
- const lease = numberAt(value, "lease_ms");
463
- const observed = textAt(value, "observed_at");
464
- const session = sessionAt(value);
465
- if (sequence === void 0 || sequence < 1) return void 0;
466
- if (lease === void 0 || observed === void 0 || session === void 0)
467
- return void 0;
468
- const marker = marked ? decodeRadarLaunchMarker(value.launch_identity) : void 0;
469
- if (marked && marker === void 0) return void 0;
470
- return {
471
- sequence,
472
- session,
473
- lease_ms: lease,
474
- observed_at: observed,
475
- ...marker !== void 0 ? { launch_identity: marker } : {}
476
- };
477
- }
478
425
  function createChannelWriter(input) {
479
426
  const { store, client, scheduler, recordName, channel, publisher } = input;
480
427
  const codec = input.codec ?? executionCodec;
@@ -490,7 +437,7 @@ function createChannelWriter(input) {
490
437
  function blank(agentId) {
491
438
  return {
492
439
  version: RADAR_PUBLICATION_VERSION,
493
- agent_id: agentId,
440
+ registration_id: agentId,
494
441
  channel,
495
442
  publisher,
496
443
  sequence: 0
@@ -498,7 +445,7 @@ function createChannelWriter(input) {
498
445
  }
499
446
  function loadRecord() {
500
447
  const value = store.read(recordName);
501
- const agentId = textAt(value, "agent_id");
448
+ const agentId = textAt(value, "registration_id");
502
449
  const storedPublisher = recordAt(value, "publisher");
503
450
  if (numberAt(value, "version") !== RADAR_PUBLICATION_VERSION || agentId === void 0 || !isUuid(agentId) || textAt(value, "channel") !== channel || textAt(storedPublisher, "source") !== publisher.source || textAt(storedPublisher, "incarnation") !== publisher.incarnation || textAt(storedPublisher, "reporting_owner") !== publisher.reporting_owner)
504
451
  return blank("");
@@ -513,7 +460,7 @@ function createChannelWriter(input) {
513
460
  const sentAt = numberAt(acceptedValue, "sent_at");
514
461
  return {
515
462
  version: RADAR_PUBLICATION_VERSION,
516
- agent_id: agentId,
463
+ registration_id: agentId,
517
464
  channel,
518
465
  publisher,
519
466
  ...handle !== void 0 && generation !== void 0 ? { writer: { handle, generation } } : {},
@@ -554,12 +501,12 @@ function createChannelWriter(input) {
554
501
  if (record.writer !== void 0) return true;
555
502
  const agentId = await input.subject(recovering);
556
503
  if (agentId === void 0) return false;
557
- if (record.agent_id !== agentId) {
504
+ if (record.registration_id !== agentId) {
558
505
  record = blank(agentId);
559
506
  save();
560
507
  }
561
508
  const answer = await client.acquire({
562
- agent_id: agentId,
509
+ registration_id: agentId,
563
510
  channel,
564
511
  publisher
565
512
  });
@@ -589,7 +536,7 @@ function createChannelWriter(input) {
589
536
  const writer = record.writer;
590
537
  if (writer === void 0) return;
591
538
  const answer = await client.publish({
592
- agent_id: record.agent_id,
539
+ registration_id: record.registration_id,
593
540
  channel,
594
541
  writer_handle: writer.handle,
595
542
  sequence: pending.sequence,
@@ -683,9 +630,9 @@ function createChannelWriter(input) {
683
630
  stopPump();
684
631
  const writer = record.writer;
685
632
  if (writer === void 0) return;
686
- const agentId = record.agent_id;
633
+ const agentId = record.registration_id;
687
634
  try {
688
- void client.retire({ agent_id: agentId, channel, writer_handle: writer.handle }).then((answer) => {
635
+ void client.retire({ registration_id: agentId, channel, writer_handle: writer.handle }).then((answer) => {
689
636
  if (answer.ok === true) {
690
637
  try {
691
638
  record.retired_at = new Date(scheduler.now()).toISOString();
@@ -708,214 +655,127 @@ function createChannelWriter(input) {
708
655
  diagnostic: () => record.diagnostic
709
656
  };
710
657
  }
711
- function createContextWriter(input) {
712
- const { store, client, scheduler, recordName, publisher } = input;
713
- let record = loadRecord();
714
- let desired;
715
- let desiredMarker;
716
- let running = false;
717
- let again = false;
718
- let recovering = false;
719
- let stopped = false;
720
- let fenced = false;
721
- let pump;
722
- function blank(agentId) {
723
- return {
724
- version: RADAR_PUBLICATION_VERSION,
725
- agent_id: agentId,
726
- publisher,
727
- sequence: 0
728
- };
729
- }
730
- function loadRecord() {
731
- const value = store.read(recordName);
732
- const agentId = textAt(value, "agent_id");
733
- const storedPublisher = recordAt(value, "publisher");
734
- if (numberAt(value, "version") !== RADAR_PUBLICATION_VERSION || agentId === void 0 || !isUuid(agentId) && agentId !== "" || textAt(storedPublisher, "source") !== publisher.source || textAt(storedPublisher, "incarnation") !== publisher.incarnation || textAt(storedPublisher, "reporting_owner") !== publisher.reporting_owner)
735
- return blank("");
736
- const writer = recordAt(value, "writer");
737
- const handle = textAt(writer, "handle");
738
- const generation = numberAt(writer, "generation");
739
- if (writer !== void 0 && (!exactKeys(writer, WRITER_KEYS) || !isUuid(handle) || generation === void 0))
740
- return blank(agentId);
741
- const pending = contextReportAt(
742
- recordAt(value, "pending"),
743
- CONTEXT_PENDING_KEYS,
744
- CONTEXT_PENDING_MARKED_KEYS
745
- );
746
- const acceptedValue = recordAt(value, "accepted");
747
- const accepted = contextReportAt(
748
- acceptedValue,
749
- CONTEXT_ACCEPTED_KEYS,
750
- CONTEXT_ACCEPTED_MARKED_KEYS
751
- );
752
- const sentAt = numberAt(acceptedValue, "sent_at");
753
- return {
754
- version: RADAR_PUBLICATION_VERSION,
755
- agent_id: agentId,
756
- publisher,
757
- ...handle !== void 0 && generation !== void 0 ? { writer: { handle, generation } } : {},
758
- sequence: numberAt(value, "sequence") ?? 0,
759
- ...pending !== void 0 ? { pending } : {},
760
- ...accepted !== void 0 && sentAt !== void 0 ? { accepted: { ...accepted, sent_at: sentAt } } : {},
761
- ...textAt(value, "diagnostic") !== void 0 ? { diagnostic: textAt(value, "diagnostic") } : {}
762
- };
763
- }
764
- function note(message) {
765
- if (record.diagnostic !== void 0) return;
766
- record.diagnostic = message.slice(0, 256);
767
- save();
658
+ function createPresenceReporter(input) {
659
+ const { store, client, scheduler, recordName } = input;
660
+ let state;
661
+ let standing;
662
+ let diagnostic;
663
+ try {
664
+ const saved = store.read(recordName);
665
+ if (saved?.version === 2 && isUuid(saved.claim_id) && isUuid(saved.session) && Array.isArray(saved.releases))
666
+ state = saved;
667
+ } catch (error) {
668
+ diagnostic = `presence storage unavailable: ${String(error)}`;
768
669
  }
670
+ let timer;
671
+ let stopped = false;
672
+ let running;
769
673
  function save() {
770
- store.write(recordName, record);
771
- }
772
- function fence(message) {
773
- fenced = true;
774
- stopPump();
775
- note(`context stopped: ${message}`);
776
- }
777
- function startPump() {
778
- if (pump !== void 0 || stopped || fenced) return;
779
- pump = scheduler.setInterval(() => kick(), input.heartbeatMs);
780
- pump.unref?.();
781
- }
782
- function stopPump() {
783
- if (pump === void 0) return;
784
- scheduler.clearInterval(pump);
785
- pump = void 0;
674
+ try {
675
+ if (state) store.write(recordName, state);
676
+ return true;
677
+ } catch (error) {
678
+ diagnostic = `presence storage unavailable: ${String(error)}`;
679
+ return false;
680
+ }
786
681
  }
787
- async function ensureSubject() {
788
- if (record.writer !== void 0) return true;
789
- const agentId = await input.subject(recovering);
790
- if (agentId === void 0) return false;
791
- if (record.agent_id !== agentId) {
792
- record = blank(agentId);
682
+ async function releasePending() {
683
+ if (!state) return;
684
+ const registration = await input.registration();
685
+ if (!registration?.process) return;
686
+ for (const release of [...state.releases]) {
687
+ const answer = await client.releasePresence({ ...release, process: registration.process });
688
+ if (!answer.ok) return;
689
+ state.releases = state.releases.filter((r) => r.claim_id !== release.claim_id);
793
690
  save();
794
691
  }
795
- return true;
796
692
  }
797
- function heartbeatDue() {
798
- return record.accepted !== void 0 && scheduler.now() - record.accepted.sent_at >= input.heartbeatMs;
693
+ function schedule() {
694
+ if (stopped || timer) return;
695
+ timer = scheduler.setTimeout(() => {
696
+ timer = void 0;
697
+ void ensure();
698
+ }, input.heartbeatMs);
699
+ timer.unref?.();
799
700
  }
800
- async function send(pending) {
801
- const writer = record.writer;
802
- const answer = await client.context({
803
- agent_id: record.agent_id,
804
- publisher,
805
- // A first publish presents no handle: the daemon binds the writer the
806
- // publisher already is, which is the binding this writer then keeps.
807
- ...writer !== void 0 ? { writer_handle: writer.handle } : {},
808
- sequence: pending.sequence,
809
- lease_ms: pending.lease_ms,
810
- observed_at: pending.observed_at,
811
- context: {
812
- session: pending.session,
813
- ...pending.launch_identity !== void 0 ? { launch_identity: pending.launch_identity } : {}
701
+ async function ensure() {
702
+ if (running) return running;
703
+ if (stopped || !state) return void 0;
704
+ const claim = state;
705
+ if (!save()) return void 0;
706
+ running = (async () => {
707
+ await releasePending();
708
+ const registration = await input.registration();
709
+ if (!registration?.process || state !== claim || stopped) return void 0;
710
+ const answer = await client.presence({
711
+ ...registration,
712
+ process: registration.process,
713
+ session_id: claim.session,
714
+ claim_id: claim.claim_id,
715
+ ...claim.registration_id ? { registration_id: claim.registration_id } : {},
716
+ ...claim.subject_id ? { subject_id: claim.subject_id } : {}
717
+ });
718
+ if (state !== claim) return void 0;
719
+ if (!answer.ok) {
720
+ if (!["absent", "timeout", "transport"].includes(answer.code)) diagnostic = answer.message;
721
+ return void 0;
814
722
  }
815
- });
816
- if (answer.ok === true) {
817
- recovering = false;
818
- record.writer = {
819
- handle: answer.value.writer.handle,
820
- generation: answer.value.writer.generation
821
- };
822
- record.sequence = Math.max(record.sequence, answer.value.writer.sequence);
823
- record.accepted = {
824
- ...pending,
825
- sent_at: answer.value.warning === void 0 ? scheduler.now() : record.accepted?.sent_at ?? scheduler.now()
826
- };
827
- record.pending = void 0;
828
- save();
829
- return;
830
- }
831
- if (answer.code === "refused" || answer.code === "bad_params" || // A daemon that speaks the registry but does not serve this record would
832
- // refuse every retry the same way; one diagnostic beats a silent retry
833
- // loop once per heartbeat.
834
- answer.code === "unknown_method") {
835
- fence(answer.message);
836
- return;
837
- }
838
- if (answer.code === "not_found") {
839
- record = blank("");
840
- recovering = true;
723
+ claim.registration_id = answer.value.registration_id;
724
+ claim.subject_id = answer.value.standing === "granted" ? answer.value.subject_id ?? void 0 : void 0;
725
+ standing = answer.value.standing === "granted" ? { state: "managed", subject_id: claim.subject_id } : answer.value.standing === "duplicate" ? { state: "duplicate" } : answer.value.standing === "pending" ? { state: "pending" } : { state: "unmanaged" };
726
+ diagnostic = answer.value.standing === "duplicate" ? DUPLICATE_SESSION_DIAGNOSTIC : void 0;
841
727
  save();
842
- return;
843
- }
844
- }
845
- async function stage(session, launchIdentity) {
846
- const pending = {
847
- sequence: record.sequence + 1,
848
- session,
849
- lease_ms: input.leaseMs,
850
- observed_at: new Date(scheduler.now()).toISOString(),
851
- ...launchIdentity !== void 0 ? { launch_identity: launchIdentity } : {}
852
- };
853
- record.pending = pending;
854
- save();
855
- await send(pending);
856
- }
857
- async function drainOnce() {
858
- if (fenced) return;
859
- if (record.pending !== void 0) {
860
- await send(record.pending);
861
- return;
862
- }
863
- if (desired === void 0) return;
864
- if (!await ensureSubject()) return;
865
- if (fenced) return;
866
- if (record.writer === void 0 || record.accepted === void 0 || record.accepted.session !== desired || !sameLaunchIdentity(record.accepted.launch_identity, desiredMarker)) {
867
- await stage(desired, desiredMarker);
868
- return;
869
- }
870
- if (heartbeatDue()) await stage(record.accepted.session, desiredMarker);
871
- }
872
- async function drain() {
873
- if (running || stopped || fenced) return;
874
- running = true;
728
+ return claim.registration_id;
729
+ })();
875
730
  try {
876
- await drainOnce();
731
+ return await running;
877
732
  } catch (error) {
878
- fenced = true;
879
- stopPump();
880
- try {
881
- note(`context storage failure: ${String(error).slice(0, 180)}`);
882
- } catch {
883
- }
733
+ diagnostic = `presence unavailable: ${String(error)}`;
734
+ return void 0;
884
735
  } finally {
885
- running = false;
886
- if (again && !stopped && !fenced) {
887
- again = false;
888
- kick();
889
- }
736
+ running = void 0;
737
+ if (!stopped && state !== claim) void ensure();
738
+ else schedule();
890
739
  }
891
740
  }
892
- function kick() {
893
- if (stopped || fenced) return;
894
- startPump();
895
- if (running) {
896
- again = true;
897
- return;
898
- }
899
- void drain().catch(() => void 0);
900
- }
901
741
  return {
902
- update(session, launchIdentity) {
903
- if (fenced || session === desired && sameLaunchIdentity(desiredMarker, launchIdentity))
904
- return;
905
- desired = session;
906
- desiredMarker = launchIdentity;
907
- if (stopped) return;
908
- kick();
742
+ report(session) {
743
+ const releases = [...state?.releases ?? []];
744
+ if (state?.registration_id && !releases.some((release) => release.claim_id === state.claim_id)) {
745
+ releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
746
+ }
747
+ standing = void 0;
748
+ state = { version: 2, session, claim_id: randomUUID(), releases };
749
+ save();
750
+ stopped = false;
751
+ void ensure();
752
+ },
753
+ ensure,
754
+ async release() {
755
+ const releasing = state;
756
+ standing = void 0;
757
+ stopped = true;
758
+ if (timer) scheduler.clearTimeout(timer);
759
+ timer = void 0;
760
+ if (running) await running;
761
+ if (state === releasing) standing = void 0;
762
+ if (releasing?.registration_id && !releasing.releases.some((r) => r.claim_id === releasing.claim_id)) {
763
+ releasing.releases.push({ registration_id: releasing.registration_id, claim_id: releasing.claim_id });
764
+ if (!save()) return;
765
+ }
766
+ await releasePending();
909
767
  },
910
768
  stop() {
911
769
  stopped = true;
912
- stopPump();
770
+ if (timer) scheduler.clearTimeout(timer);
771
+ timer = void 0;
913
772
  },
914
773
  reopen() {
915
774
  stopped = false;
916
- if (desired !== void 0 || record.pending !== void 0) kick();
775
+ void ensure();
917
776
  },
918
- diagnostic: () => record.diagnostic
777
+ management: () => standing,
778
+ diagnostic: () => diagnostic
919
779
  };
920
780
  }
921
781
  var STORE_IDENTITY = /* @__PURE__ */ new WeakMap();
@@ -944,10 +804,13 @@ function unavailablePublication(error) {
944
804
  };
945
805
  return {
946
806
  execution: writer,
947
- context: {
948
- update: () => void 0,
807
+ presence: {
808
+ report: () => void 0,
809
+ ensure: async () => void 0,
810
+ release: async () => void 0,
949
811
  stop: () => void 0,
950
812
  reopen: () => void 0,
813
+ management: () => void 0,
951
814
  diagnostic: () => diagnostic
952
815
  },
953
816
  facts: { ...writer, update: () => void 0 },
@@ -974,33 +837,29 @@ function createRadarPublicationInner(options) {
974
837
  cached.reopen();
975
838
  return cached;
976
839
  }
977
- const subjectName = `subjects/${createHash("sha256").update(`${options.registration.source}\0${incarnation}`).digest("hex").slice(0, 32)}.json`;
840
+ const registrationName = `registrations/${createHash("sha256").update(`${options.registration.source}\0${incarnation}`).digest("hex").slice(0, 32)}.json`;
978
841
  const channelRecord = (channel, target) => `channels/${createHash("sha256").update(`${channel}\0${incarnation}\0${target ?? ""}`).digest("hex").slice(0, 32)}.json`;
979
- const contextRecord = `context/${createHash("sha256").update(`context\0${incarnation}`).digest("hex").slice(0, 32)}.json`;
980
- function readSubject() {
981
- const value = store.read(subjectName);
842
+ const presenceRecord = `presence/${createHash("sha256").update(`presence\0${incarnation}`).digest("hex").slice(0, 32)}.json`;
843
+ function readRegistration() {
844
+ const value = store.read(registrationName);
982
845
  const registration = recordAt(value, "registration");
983
846
  if (numberAt(value, "version") !== RADAR_PUBLICATION_VERSION || textAt(registration, "source") !== options.registration.source || textAt(registration, "incarnation") !== incarnation)
984
847
  return void 0;
985
- const storedAgentId = textAt(value, "agent_id");
848
+ const storedAgentId = textAt(value, "registration_id");
986
849
  return {
987
850
  version: RADAR_PUBLICATION_VERSION,
988
851
  registration,
989
- ...storedAgentId !== void 0 && isUuid(storedAgentId) ? { agent_id: storedAgentId } : {}
852
+ ...storedAgentId !== void 0 && isUuid(storedAgentId) ? { registration_id: storedAgentId } : {}
990
853
  };
991
854
  }
992
- const stored = readSubject();
993
- let subject = stored;
855
+ const stored = readRegistration();
856
+ let processRecord = stored;
994
857
  let registrationDiagnostic;
995
858
  let registrationRecord;
996
859
  async function ensureRegistration() {
997
- if (subject !== void 0) return subject;
860
+ if (processRecord !== void 0) return processRecord;
998
861
  registrationRecord ??= (async () => {
999
862
  const resolution = options.location ? await options.location() : { state: "absent", reason: "this process publishes no location" };
1000
- if (resolution.state === "pending") {
1001
- registrationDiagnostic = `location bootstrap pending: ${resolution.reason}`.slice(0, 256);
1002
- return void 0;
1003
- }
1004
863
  const built = {
1005
864
  version: RADAR_PUBLICATION_VERSION,
1006
865
  registration: {
@@ -1011,13 +870,13 @@ function createRadarPublicationInner(options) {
1011
870
  }
1012
871
  };
1013
872
  try {
1014
- store.write(subjectName, built);
873
+ store.write(registrationName, built);
1015
874
  } catch (error) {
1016
875
  registrationDiagnostic ??= `private storage unavailable: ${String(error)}`.slice(0, 256);
1017
876
  return void 0;
1018
877
  }
1019
878
  registrationDiagnostic = void 0;
1020
- subject = built;
879
+ processRecord = built;
1021
880
  return built;
1022
881
  })();
1023
882
  try {
@@ -1026,11 +885,10 @@ function createRadarPublicationInner(options) {
1026
885
  registrationRecord = void 0;
1027
886
  }
1028
887
  }
1029
- let agentId = stored?.agent_id;
888
+ let agentId = stored?.registration_id;
1030
889
  let subjectReady;
1031
890
  async function ensureSubject(recheck) {
1032
- if (recheck) agentId = void 0;
1033
- if (agentId !== void 0) return agentId;
891
+ if (!recheck && agentId !== void 0) return agentId;
1034
892
  subjectReady ??= (async () => {
1035
893
  const ready = await client.ping();
1036
894
  if (ready.ok === false) {
@@ -1040,18 +898,11 @@ function createRadarPublicationInner(options) {
1040
898
  }
1041
899
  const record = await ensureRegistration();
1042
900
  if (record === void 0) return void 0;
1043
- const answer = await client.register({
1044
- ...record.registration,
1045
- ...spawnToken !== void 0 ? { spawn_token: spawnToken } : {}
1046
- });
1047
- if (answer.ok === false) {
1048
- if (answer.code !== "absent" && answer.code !== "timeout" && answer.code !== "transport")
1049
- registrationDiagnostic ??= `subject registration refused: ${answer.message}`.slice(0, 256);
1050
- return void 0;
1051
- }
1052
- agentId = answer.value.agent_id;
1053
- subject = { ...record, agent_id: agentId };
1054
- store.write(subjectName, subject);
901
+ const registered = await presence.ensure();
902
+ if (registered === void 0) return void 0;
903
+ agentId = registered;
904
+ processRecord = { ...record, registration_id: agentId };
905
+ store.write(registrationName, processRecord);
1055
906
  if (options.binding !== void 0) writeBinding(options.binding);
1056
907
  return agentId;
1057
908
  })();
@@ -1065,7 +916,7 @@ function createRadarPublicationInner(options) {
1065
916
  if (agentId === void 0) return;
1066
917
  store.write(`bindings/${binding.key}.json`, {
1067
918
  version: RADAR_PUBLICATION_VERSION,
1068
- agent_id: agentId,
919
+ registration_id: agentId,
1069
920
  incarnation,
1070
921
  run: binding.run,
1071
922
  owner: binding.owner,
@@ -1089,14 +940,22 @@ function createRadarPublicationInner(options) {
1089
940
  leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
1090
941
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
1091
942
  });
1092
- const context = createContextWriter({
943
+ const presence = createPresenceReporter({
1093
944
  store,
1094
945
  client,
1095
946
  scheduler,
1096
- recordName: contextRecord,
1097
- publisher: publisher("herdsman-pi"),
1098
- subject: ensureSubject,
1099
- leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
947
+ recordName: presenceRecord,
948
+ registration: async () => {
949
+ const record = await ensureRegistration();
950
+ if (!record) return void 0;
951
+ const location = await options.location?.();
952
+ const { location: _initialLocation, ...identity } = record.registration;
953
+ return {
954
+ ...identity,
955
+ ...location?.state === "resolved" ? { location: location.location } : {},
956
+ ...spawnToken ? { spawn_token: spawnToken } : {}
957
+ };
958
+ },
1100
959
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
1101
960
  });
1102
961
  const facts = createChannelWriter({
@@ -1114,7 +973,7 @@ function createRadarPublicationInner(options) {
1114
973
  const assignmentWriters = /* @__PURE__ */ new Map();
1115
974
  const publication = {
1116
975
  execution,
1117
- context,
976
+ presence,
1118
977
  facts,
1119
978
  assignment(target, reportingOwner) {
1120
979
  const existing = assignmentWriters.get(target);
@@ -1129,7 +988,7 @@ function createRadarPublicationInner(options) {
1129
988
  publisher: publisher("herdsman-owner", reportingOwner),
1130
989
  codec: assignmentCodec,
1131
990
  // The owner publishes about a subject it did not register: the exact
1132
- // agent_id from the child's sidecar is the whole correlation.
991
+ // registration_id from the child's sidecar is the whole correlation.
1133
992
  subject: async () => target,
1134
993
  leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
1135
994
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
@@ -1149,7 +1008,7 @@ function createRadarPublicationInner(options) {
1149
1008
  registrationDiagnostic ??= `Radar binding unavailable: ${String(error).slice(0, 180)}`;
1150
1009
  return void 0;
1151
1010
  }
1152
- const bound = textAt(value, "agent_id");
1011
+ const bound = textAt(value, "registration_id");
1153
1012
  const boundIncarnation = textAt(value, "incarnation");
1154
1013
  const run = textAt(value, "run");
1155
1014
  const owner = textAt(value, "owner");
@@ -1159,7 +1018,7 @@ function createRadarPublicationInner(options) {
1159
1018
  return void 0;
1160
1019
  return {
1161
1020
  version: RADAR_PUBLICATION_VERSION,
1162
- agent_id: bound,
1021
+ registration_id: bound,
1163
1022
  incarnation: boundIncarnation,
1164
1023
  run,
1165
1024
  owner,
@@ -1175,25 +1034,27 @@ function createRadarPublicationInner(options) {
1175
1034
  if (agentId2 === void 0) return void 0;
1176
1035
  const record = await ensureRegistration();
1177
1036
  if (record === void 0) return void 0;
1178
- const location = record.registration.location;
1179
- const subjectId = process.env.PI_RADAR_MANAGED_SUBJECT_ID;
1037
+ const currentLocation = await options.location?.();
1038
+ const location = currentLocation?.state === "resolved" ? currentLocation.location : void 0;
1039
+ const standing = presence.management();
1040
+ const subjectId = standing?.state === "managed" ? standing.subject_id : void 0;
1180
1041
  return {
1181
- agent_id: agentId2,
1042
+ registration_id: agentId2,
1182
1043
  ...subjectId !== void 0 ? { subject_id: subjectId } : {},
1183
1044
  ...location !== void 0 ? { location } : {}
1184
1045
  };
1185
1046
  },
1186
- diagnostic: () => registrationDiagnostic ?? execution.diagnostic() ?? context.diagnostic() ?? facts.diagnostic(),
1047
+ diagnostic: () => registrationDiagnostic ?? execution.diagnostic() ?? presence.diagnostic() ?? facts.diagnostic(),
1187
1048
  stop() {
1188
1049
  for (const writer of assignmentWriters.values()) writer.stop();
1189
1050
  execution.stop();
1190
- context.stop();
1051
+ presence.stop();
1191
1052
  facts.stop();
1192
1053
  },
1193
1054
  reopen() {
1194
1055
  for (const writer of assignmentWriters.values()) writer.reopen();
1195
1056
  execution.reopen();
1196
- context.reopen();
1057
+ presence.reopen();
1197
1058
  facts.reopen();
1198
1059
  }
1199
1060
  };