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
@@ -110,77 +110,7 @@ import {
110
110
  writeSync
111
111
  } from "node:fs";
112
112
  import { dirname, join, parse, resolve, sep } from "node:path";
113
-
114
- // extension/radar-session-marker.ts
115
- var RADAR_LAUNCH_MARKER_TYPE = "pi-radar.launch-identity";
116
- var RADAR_LAUNCH_MARKER_VERSION = 1;
117
- var RADAR_MAX_EXECUTION_GENERATION = 4294967295;
118
- var RADAR_MANAGED_SUBJECT_ENV = "PI_RADAR_MANAGED_SUBJECT_ID";
119
- var RADAR_MANAGED_LAUNCH_ENV = "PI_RADAR_MANAGED_LAUNCH_ID";
120
- var RADAR_MANAGED_GENERATION_ENV = "PI_RADAR_MANAGED_EXECUTION_GENERATION";
121
- var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
122
- function decodeRadarLaunchMarker(value) {
123
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
124
- const record = value;
125
- if (Object.keys(record).sort().join(",") !== "execution_generation,launch_id,subject_id,version") {
126
- return void 0;
127
- }
128
- if (record.version !== RADAR_LAUNCH_MARKER_VERSION || typeof record.subject_id !== "string" || !UUID2.test(record.subject_id) || typeof record.launch_id !== "string" || !UUID2.test(record.launch_id) || !Number.isSafeInteger(record.execution_generation) || record.execution_generation < 1 || record.execution_generation > RADAR_MAX_EXECUTION_GENERATION) {
129
- return void 0;
130
- }
131
- return {
132
- version: RADAR_LAUNCH_MARKER_VERSION,
133
- subject_id: record.subject_id,
134
- launch_id: record.launch_id,
135
- execution_generation: record.execution_generation
136
- };
137
- }
138
- function appendRadarLaunchMarker(appendEntry, marker) {
139
- const validated = decodeRadarLaunchMarker(marker);
140
- if (!validated) throw new Error("invalid Radar launch identity marker");
141
- appendEntry(RADAR_LAUNCH_MARKER_TYPE, validated);
142
- }
143
- function latestRadarLaunchMarker(branch) {
144
- let latest;
145
- for (const entry of branch) {
146
- if (entry.type === "custom" && entry.customType === RADAR_LAUNCH_MARKER_TYPE) {
147
- latest = entry;
148
- }
149
- }
150
- if (!latest) return { status: "absent" };
151
- const marker = decodeRadarLaunchMarker(latest.data);
152
- return marker ? { status: "valid", marker } : { status: "invalid" };
153
- }
154
- function radarLaunchMarkerFromEnvironment(env = process.env) {
155
- const rawGeneration = env[RADAR_MANAGED_GENERATION_ENV];
156
- if (env[RADAR_MANAGED_SUBJECT_ENV] === void 0 || env[RADAR_MANAGED_LAUNCH_ENV] === void 0 || rawGeneration === void 0 || !/^[1-9][0-9]*$/.test(rawGeneration)) {
157
- return void 0;
158
- }
159
- return decodeRadarLaunchMarker({
160
- version: RADAR_LAUNCH_MARKER_VERSION,
161
- subject_id: env[RADAR_MANAGED_SUBJECT_ENV],
162
- launch_id: env[RADAR_MANAGED_LAUNCH_ENV],
163
- execution_generation: Number(rawGeneration)
164
- });
165
- }
166
- function activeRadarLaunchMarker(host) {
167
- if (!host.sessionManager) return { status: "absent" };
168
- return latestRadarLaunchMarker(host.sessionManager.getBranch());
169
- }
170
- function persistRadarLaunchMarker(host, env = process.env) {
171
- const marker = radarLaunchMarkerFromEnvironment(env);
172
- if (!marker) return "absent";
173
- if (!host.appendEntry || !host.sessionManager) return "unsupported";
174
- const current = latestRadarLaunchMarker(host.sessionManager.getBranch());
175
- if (current.status === "valid" && sameMarker(current.marker, marker)) return "unchanged";
176
- appendRadarLaunchMarker(host.appendEntry, marker);
177
- return "written";
178
- }
179
- function sameMarker(a, b) {
180
- return a.version === b.version && a.subject_id === b.subject_id && a.launch_id === b.launch_id && a.execution_generation === b.execution_generation;
181
- }
182
-
183
- // extension/radar-publication.ts
113
+ var DUPLICATE_SESSION_DIAGNOSTIC = "Radar rejected this session claim as a duplicate; reload or resume the session before retrying";
184
114
  var RADAR_PUBLICATION_VERSION = 1;
185
115
  var RADAR_LEASE_MS = 3e4;
186
116
  var RADAR_HEARTBEAT_MS = 15e3;
@@ -205,7 +135,7 @@ function readProcessBirth() {
205
135
  return void 0;
206
136
  }
207
137
  }
208
- var RECORD_NAME = /^(?:subjects|channels|context|bindings)\/[a-z0-9-]{1,64}\.json$/;
138
+ var RECORD_NAME = /^(?:registrations|presence|subjects|channels|context|bindings)\/[a-z0-9-]{1,64}\.json$/;
209
139
  var MAX_RECORD_BYTES = 64 * 1024;
210
140
  var PRIVATE_MODE = 448;
211
141
  var RECORD_MODE = 384;
@@ -487,39 +417,6 @@ function pendingAt(value, keys = PENDING_KEYS, codec = executionCodec) {
487
417
  return void 0;
488
418
  return { sequence, snapshot, lease_ms: lease, observed_at: observed };
489
419
  }
490
- var CONTEXT_PENDING_KEYS = "lease_ms,observed_at,sequence,session";
491
- var CONTEXT_PENDING_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sequence,session";
492
- var CONTEXT_ACCEPTED_KEYS = "lease_ms,observed_at,sent_at,sequence,session";
493
- var CONTEXT_ACCEPTED_MARKED_KEYS = "launch_identity,lease_ms,observed_at,sent_at,sequence,session";
494
- function sessionAt(value) {
495
- if (typeof value !== "object" || value === null) return void 0;
496
- const found = value.session;
497
- if (found === null) return null;
498
- return typeof found === "string" ? found : void 0;
499
- }
500
- function sameLaunchIdentity(a, b) {
501
- 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;
502
- }
503
- function contextReportAt(value, keys, markedKeys) {
504
- const marked = exactKeys(value, markedKeys);
505
- if (!exactKeys(value, keys) && !marked) return void 0;
506
- const sequence = numberAt(value, "sequence");
507
- const lease = numberAt(value, "lease_ms");
508
- const observed = textAt(value, "observed_at");
509
- const session = sessionAt(value);
510
- if (sequence === void 0 || sequence < 1) return void 0;
511
- if (lease === void 0 || observed === void 0 || session === void 0)
512
- return void 0;
513
- const marker = marked ? decodeRadarLaunchMarker(value.launch_identity) : void 0;
514
- if (marked && marker === void 0) return void 0;
515
- return {
516
- sequence,
517
- session,
518
- lease_ms: lease,
519
- observed_at: observed,
520
- ...marker !== void 0 ? { launch_identity: marker } : {}
521
- };
522
- }
523
420
  function createChannelWriter(input) {
524
421
  const { store, client, scheduler, recordName, channel, publisher } = input;
525
422
  const codec = input.codec ?? executionCodec;
@@ -535,7 +432,7 @@ function createChannelWriter(input) {
535
432
  function blank(agentId) {
536
433
  return {
537
434
  version: RADAR_PUBLICATION_VERSION,
538
- agent_id: agentId,
435
+ registration_id: agentId,
539
436
  channel,
540
437
  publisher,
541
438
  sequence: 0
@@ -543,7 +440,7 @@ function createChannelWriter(input) {
543
440
  }
544
441
  function loadRecord() {
545
442
  const value = store.read(recordName);
546
- const agentId = textAt(value, "agent_id");
443
+ const agentId = textAt(value, "registration_id");
547
444
  const storedPublisher = recordAt(value, "publisher");
548
445
  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)
549
446
  return blank("");
@@ -558,7 +455,7 @@ function createChannelWriter(input) {
558
455
  const sentAt = numberAt(acceptedValue, "sent_at");
559
456
  return {
560
457
  version: RADAR_PUBLICATION_VERSION,
561
- agent_id: agentId,
458
+ registration_id: agentId,
562
459
  channel,
563
460
  publisher,
564
461
  ...handle !== void 0 && generation !== void 0 ? { writer: { handle, generation } } : {},
@@ -599,12 +496,12 @@ function createChannelWriter(input) {
599
496
  if (record.writer !== void 0) return true;
600
497
  const agentId = await input.subject(recovering);
601
498
  if (agentId === void 0) return false;
602
- if (record.agent_id !== agentId) {
499
+ if (record.registration_id !== agentId) {
603
500
  record = blank(agentId);
604
501
  save();
605
502
  }
606
503
  const answer = await client.acquire({
607
- agent_id: agentId,
504
+ registration_id: agentId,
608
505
  channel,
609
506
  publisher
610
507
  });
@@ -634,7 +531,7 @@ function createChannelWriter(input) {
634
531
  const writer = record.writer;
635
532
  if (writer === void 0) return;
636
533
  const answer = await client.publish({
637
- agent_id: record.agent_id,
534
+ registration_id: record.registration_id,
638
535
  channel,
639
536
  writer_handle: writer.handle,
640
537
  sequence: pending.sequence,
@@ -728,9 +625,9 @@ function createChannelWriter(input) {
728
625
  stopPump();
729
626
  const writer = record.writer;
730
627
  if (writer === void 0) return;
731
- const agentId = record.agent_id;
628
+ const agentId = record.registration_id;
732
629
  try {
733
- void client.retire({ agent_id: agentId, channel, writer_handle: writer.handle }).then((answer) => {
630
+ void client.retire({ registration_id: agentId, channel, writer_handle: writer.handle }).then((answer) => {
734
631
  if (answer.ok === true) {
735
632
  try {
736
633
  record.retired_at = new Date(scheduler.now()).toISOString();
@@ -753,214 +650,127 @@ function createChannelWriter(input) {
753
650
  diagnostic: () => record.diagnostic
754
651
  };
755
652
  }
756
- function createContextWriter(input) {
757
- const { store, client, scheduler, recordName, publisher } = input;
758
- let record = loadRecord();
759
- let desired;
760
- let desiredMarker;
761
- let running = false;
762
- let again = false;
763
- let recovering = false;
764
- let stopped = false;
765
- let fenced = false;
766
- let pump;
767
- function blank(agentId) {
768
- return {
769
- version: RADAR_PUBLICATION_VERSION,
770
- agent_id: agentId,
771
- publisher,
772
- sequence: 0
773
- };
774
- }
775
- function loadRecord() {
776
- const value = store.read(recordName);
777
- const agentId = textAt(value, "agent_id");
778
- const storedPublisher = recordAt(value, "publisher");
779
- 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)
780
- return blank("");
781
- const writer = recordAt(value, "writer");
782
- const handle = textAt(writer, "handle");
783
- const generation = numberAt(writer, "generation");
784
- if (writer !== void 0 && (!exactKeys(writer, WRITER_KEYS) || !isUuid(handle) || generation === void 0))
785
- return blank(agentId);
786
- const pending = contextReportAt(
787
- recordAt(value, "pending"),
788
- CONTEXT_PENDING_KEYS,
789
- CONTEXT_PENDING_MARKED_KEYS
790
- );
791
- const acceptedValue = recordAt(value, "accepted");
792
- const accepted = contextReportAt(
793
- acceptedValue,
794
- CONTEXT_ACCEPTED_KEYS,
795
- CONTEXT_ACCEPTED_MARKED_KEYS
796
- );
797
- const sentAt = numberAt(acceptedValue, "sent_at");
798
- return {
799
- version: RADAR_PUBLICATION_VERSION,
800
- agent_id: agentId,
801
- publisher,
802
- ...handle !== void 0 && generation !== void 0 ? { writer: { handle, generation } } : {},
803
- sequence: numberAt(value, "sequence") ?? 0,
804
- ...pending !== void 0 ? { pending } : {},
805
- ...accepted !== void 0 && sentAt !== void 0 ? { accepted: { ...accepted, sent_at: sentAt } } : {},
806
- ...textAt(value, "diagnostic") !== void 0 ? { diagnostic: textAt(value, "diagnostic") } : {}
807
- };
808
- }
809
- function note(message) {
810
- if (record.diagnostic !== void 0) return;
811
- record.diagnostic = message.slice(0, 256);
812
- save();
653
+ function createPresenceReporter(input) {
654
+ const { store, client, scheduler, recordName } = input;
655
+ let state;
656
+ let standing;
657
+ let diagnostic;
658
+ try {
659
+ const saved = store.read(recordName);
660
+ if (saved?.version === 2 && isUuid(saved.claim_id) && isUuid(saved.session) && Array.isArray(saved.releases))
661
+ state = saved;
662
+ } catch (error) {
663
+ diagnostic = `presence storage unavailable: ${String(error)}`;
813
664
  }
665
+ let timer;
666
+ let stopped = false;
667
+ let running;
814
668
  function save() {
815
- store.write(recordName, record);
816
- }
817
- function fence(message) {
818
- fenced = true;
819
- stopPump();
820
- note(`context stopped: ${message}`);
821
- }
822
- function startPump() {
823
- if (pump !== void 0 || stopped || fenced) return;
824
- pump = scheduler.setInterval(() => kick(), input.heartbeatMs);
825
- pump.unref?.();
826
- }
827
- function stopPump() {
828
- if (pump === void 0) return;
829
- scheduler.clearInterval(pump);
830
- pump = void 0;
669
+ try {
670
+ if (state) store.write(recordName, state);
671
+ return true;
672
+ } catch (error) {
673
+ diagnostic = `presence storage unavailable: ${String(error)}`;
674
+ return false;
675
+ }
831
676
  }
832
- async function ensureSubject() {
833
- if (record.writer !== void 0) return true;
834
- const agentId = await input.subject(recovering);
835
- if (agentId === void 0) return false;
836
- if (record.agent_id !== agentId) {
837
- record = blank(agentId);
677
+ async function releasePending() {
678
+ if (!state) return;
679
+ const registration = await input.registration();
680
+ if (!registration?.process) return;
681
+ for (const release of [...state.releases]) {
682
+ const answer = await client.releasePresence({ ...release, process: registration.process });
683
+ if (!answer.ok) return;
684
+ state.releases = state.releases.filter((r) => r.claim_id !== release.claim_id);
838
685
  save();
839
686
  }
840
- return true;
841
687
  }
842
- function heartbeatDue() {
843
- return record.accepted !== void 0 && scheduler.now() - record.accepted.sent_at >= input.heartbeatMs;
688
+ function schedule() {
689
+ if (stopped || timer) return;
690
+ timer = scheduler.setTimeout(() => {
691
+ timer = void 0;
692
+ void ensure();
693
+ }, input.heartbeatMs);
694
+ timer.unref?.();
844
695
  }
845
- async function send(pending) {
846
- const writer = record.writer;
847
- const answer = await client.context({
848
- agent_id: record.agent_id,
849
- publisher,
850
- // A first publish presents no handle: the daemon binds the writer the
851
- // publisher already is, which is the binding this writer then keeps.
852
- ...writer !== void 0 ? { writer_handle: writer.handle } : {},
853
- sequence: pending.sequence,
854
- lease_ms: pending.lease_ms,
855
- observed_at: pending.observed_at,
856
- context: {
857
- session: pending.session,
858
- ...pending.launch_identity !== void 0 ? { launch_identity: pending.launch_identity } : {}
696
+ async function ensure() {
697
+ if (running) return running;
698
+ if (stopped || !state) return void 0;
699
+ const claim = state;
700
+ if (!save()) return void 0;
701
+ running = (async () => {
702
+ await releasePending();
703
+ const registration = await input.registration();
704
+ if (!registration?.process || state !== claim || stopped) return void 0;
705
+ const answer = await client.presence({
706
+ ...registration,
707
+ process: registration.process,
708
+ session_id: claim.session,
709
+ claim_id: claim.claim_id,
710
+ ...claim.registration_id ? { registration_id: claim.registration_id } : {},
711
+ ...claim.subject_id ? { subject_id: claim.subject_id } : {}
712
+ });
713
+ if (state !== claim) return void 0;
714
+ if (!answer.ok) {
715
+ if (!["absent", "timeout", "transport"].includes(answer.code)) diagnostic = answer.message;
716
+ return void 0;
859
717
  }
860
- });
861
- if (answer.ok === true) {
862
- recovering = false;
863
- record.writer = {
864
- handle: answer.value.writer.handle,
865
- generation: answer.value.writer.generation
866
- };
867
- record.sequence = Math.max(record.sequence, answer.value.writer.sequence);
868
- record.accepted = {
869
- ...pending,
870
- sent_at: answer.value.warning === void 0 ? scheduler.now() : record.accepted?.sent_at ?? scheduler.now()
871
- };
872
- record.pending = void 0;
873
- save();
874
- return;
875
- }
876
- if (answer.code === "refused" || answer.code === "bad_params" || // A daemon that speaks the registry but does not serve this record would
877
- // refuse every retry the same way; one diagnostic beats a silent retry
878
- // loop once per heartbeat.
879
- answer.code === "unknown_method") {
880
- fence(answer.message);
881
- return;
882
- }
883
- if (answer.code === "not_found") {
884
- record = blank("");
885
- recovering = true;
718
+ claim.registration_id = answer.value.registration_id;
719
+ claim.subject_id = answer.value.standing === "granted" ? answer.value.subject_id ?? void 0 : void 0;
720
+ 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" };
721
+ diagnostic = answer.value.standing === "duplicate" ? DUPLICATE_SESSION_DIAGNOSTIC : void 0;
886
722
  save();
887
- return;
888
- }
889
- }
890
- async function stage(session, launchIdentity) {
891
- const pending = {
892
- sequence: record.sequence + 1,
893
- session,
894
- lease_ms: input.leaseMs,
895
- observed_at: new Date(scheduler.now()).toISOString(),
896
- ...launchIdentity !== void 0 ? { launch_identity: launchIdentity } : {}
897
- };
898
- record.pending = pending;
899
- save();
900
- await send(pending);
901
- }
902
- async function drainOnce() {
903
- if (fenced) return;
904
- if (record.pending !== void 0) {
905
- await send(record.pending);
906
- return;
907
- }
908
- if (desired === void 0) return;
909
- if (!await ensureSubject()) return;
910
- if (fenced) return;
911
- if (record.writer === void 0 || record.accepted === void 0 || record.accepted.session !== desired || !sameLaunchIdentity(record.accepted.launch_identity, desiredMarker)) {
912
- await stage(desired, desiredMarker);
913
- return;
914
- }
915
- if (heartbeatDue()) await stage(record.accepted.session, desiredMarker);
916
- }
917
- async function drain() {
918
- if (running || stopped || fenced) return;
919
- running = true;
723
+ return claim.registration_id;
724
+ })();
920
725
  try {
921
- await drainOnce();
726
+ return await running;
922
727
  } catch (error) {
923
- fenced = true;
924
- stopPump();
925
- try {
926
- note(`context storage failure: ${String(error).slice(0, 180)}`);
927
- } catch {
928
- }
728
+ diagnostic = `presence unavailable: ${String(error)}`;
729
+ return void 0;
929
730
  } finally {
930
- running = false;
931
- if (again && !stopped && !fenced) {
932
- again = false;
933
- kick();
934
- }
731
+ running = void 0;
732
+ if (!stopped && state !== claim) void ensure();
733
+ else schedule();
935
734
  }
936
735
  }
937
- function kick() {
938
- if (stopped || fenced) return;
939
- startPump();
940
- if (running) {
941
- again = true;
942
- return;
943
- }
944
- void drain().catch(() => void 0);
945
- }
946
736
  return {
947
- update(session, launchIdentity) {
948
- if (fenced || session === desired && sameLaunchIdentity(desiredMarker, launchIdentity))
949
- return;
950
- desired = session;
951
- desiredMarker = launchIdentity;
952
- if (stopped) return;
953
- kick();
737
+ report(session) {
738
+ const releases = [...state?.releases ?? []];
739
+ if (state?.registration_id && !releases.some((release) => release.claim_id === state.claim_id)) {
740
+ releases.push({ registration_id: state.registration_id, claim_id: state.claim_id });
741
+ }
742
+ standing = void 0;
743
+ state = { version: 2, session, claim_id: randomUUID(), releases };
744
+ save();
745
+ stopped = false;
746
+ void ensure();
747
+ },
748
+ ensure,
749
+ async release() {
750
+ const releasing = state;
751
+ standing = void 0;
752
+ stopped = true;
753
+ if (timer) scheduler.clearTimeout(timer);
754
+ timer = void 0;
755
+ if (running) await running;
756
+ if (state === releasing) standing = void 0;
757
+ if (releasing?.registration_id && !releasing.releases.some((r) => r.claim_id === releasing.claim_id)) {
758
+ releasing.releases.push({ registration_id: releasing.registration_id, claim_id: releasing.claim_id });
759
+ if (!save()) return;
760
+ }
761
+ await releasePending();
954
762
  },
955
763
  stop() {
956
764
  stopped = true;
957
- stopPump();
765
+ if (timer) scheduler.clearTimeout(timer);
766
+ timer = void 0;
958
767
  },
959
768
  reopen() {
960
769
  stopped = false;
961
- if (desired !== void 0 || record.pending !== void 0) kick();
770
+ void ensure();
962
771
  },
963
- diagnostic: () => record.diagnostic
772
+ management: () => standing,
773
+ diagnostic: () => diagnostic
964
774
  };
965
775
  }
966
776
  var STORE_IDENTITY = /* @__PURE__ */ new WeakMap();
@@ -989,10 +799,13 @@ function unavailablePublication(error) {
989
799
  };
990
800
  return {
991
801
  execution: writer,
992
- context: {
993
- update: () => void 0,
802
+ presence: {
803
+ report: () => void 0,
804
+ ensure: async () => void 0,
805
+ release: async () => void 0,
994
806
  stop: () => void 0,
995
807
  reopen: () => void 0,
808
+ management: () => void 0,
996
809
  diagnostic: () => diagnostic
997
810
  },
998
811
  facts: { ...writer, update: () => void 0 },
@@ -1019,33 +832,29 @@ function createRadarPublicationInner(options) {
1019
832
  cached.reopen();
1020
833
  return cached;
1021
834
  }
1022
- const subjectName = `subjects/${createHash("sha256").update(`${options.registration.source}\0${incarnation}`).digest("hex").slice(0, 32)}.json`;
835
+ const registrationName = `registrations/${createHash("sha256").update(`${options.registration.source}\0${incarnation}`).digest("hex").slice(0, 32)}.json`;
1023
836
  const channelRecord = (channel, target) => `channels/${createHash("sha256").update(`${channel}\0${incarnation}\0${target ?? ""}`).digest("hex").slice(0, 32)}.json`;
1024
- const contextRecord = `context/${createHash("sha256").update(`context\0${incarnation}`).digest("hex").slice(0, 32)}.json`;
1025
- function readSubject() {
1026
- const value = store.read(subjectName);
837
+ const presenceRecord = `presence/${createHash("sha256").update(`presence\0${incarnation}`).digest("hex").slice(0, 32)}.json`;
838
+ function readRegistration() {
839
+ const value = store.read(registrationName);
1027
840
  const registration = recordAt(value, "registration");
1028
841
  if (numberAt(value, "version") !== RADAR_PUBLICATION_VERSION || textAt(registration, "source") !== options.registration.source || textAt(registration, "incarnation") !== incarnation)
1029
842
  return void 0;
1030
- const storedAgentId = textAt(value, "agent_id");
843
+ const storedAgentId = textAt(value, "registration_id");
1031
844
  return {
1032
845
  version: RADAR_PUBLICATION_VERSION,
1033
846
  registration,
1034
- ...storedAgentId !== void 0 && isUuid(storedAgentId) ? { agent_id: storedAgentId } : {}
847
+ ...storedAgentId !== void 0 && isUuid(storedAgentId) ? { registration_id: storedAgentId } : {}
1035
848
  };
1036
849
  }
1037
- const stored = readSubject();
1038
- let subject = stored;
850
+ const stored = readRegistration();
851
+ let processRecord = stored;
1039
852
  let registrationDiagnostic;
1040
853
  let registrationRecord;
1041
854
  async function ensureRegistration() {
1042
- if (subject !== void 0) return subject;
855
+ if (processRecord !== void 0) return processRecord;
1043
856
  registrationRecord ??= (async () => {
1044
857
  const resolution = options.location ? await options.location() : { state: "absent", reason: "this process publishes no location" };
1045
- if (resolution.state === "pending") {
1046
- registrationDiagnostic = `location bootstrap pending: ${resolution.reason}`.slice(0, 256);
1047
- return void 0;
1048
- }
1049
858
  const built = {
1050
859
  version: RADAR_PUBLICATION_VERSION,
1051
860
  registration: {
@@ -1056,13 +865,13 @@ function createRadarPublicationInner(options) {
1056
865
  }
1057
866
  };
1058
867
  try {
1059
- store.write(subjectName, built);
868
+ store.write(registrationName, built);
1060
869
  } catch (error) {
1061
870
  registrationDiagnostic ??= `private storage unavailable: ${String(error)}`.slice(0, 256);
1062
871
  return void 0;
1063
872
  }
1064
873
  registrationDiagnostic = void 0;
1065
- subject = built;
874
+ processRecord = built;
1066
875
  return built;
1067
876
  })();
1068
877
  try {
@@ -1071,11 +880,10 @@ function createRadarPublicationInner(options) {
1071
880
  registrationRecord = void 0;
1072
881
  }
1073
882
  }
1074
- let agentId = stored?.agent_id;
883
+ let agentId = stored?.registration_id;
1075
884
  let subjectReady;
1076
885
  async function ensureSubject(recheck) {
1077
- if (recheck) agentId = void 0;
1078
- if (agentId !== void 0) return agentId;
886
+ if (!recheck && agentId !== void 0) return agentId;
1079
887
  subjectReady ??= (async () => {
1080
888
  const ready = await client.ping();
1081
889
  if (ready.ok === false) {
@@ -1085,18 +893,11 @@ function createRadarPublicationInner(options) {
1085
893
  }
1086
894
  const record = await ensureRegistration();
1087
895
  if (record === void 0) return void 0;
1088
- const answer = await client.register({
1089
- ...record.registration,
1090
- ...spawnToken !== void 0 ? { spawn_token: spawnToken } : {}
1091
- });
1092
- if (answer.ok === false) {
1093
- if (answer.code !== "absent" && answer.code !== "timeout" && answer.code !== "transport")
1094
- registrationDiagnostic ??= `subject registration refused: ${answer.message}`.slice(0, 256);
1095
- return void 0;
1096
- }
1097
- agentId = answer.value.agent_id;
1098
- subject = { ...record, agent_id: agentId };
1099
- store.write(subjectName, subject);
896
+ const registered = await presence.ensure();
897
+ if (registered === void 0) return void 0;
898
+ agentId = registered;
899
+ processRecord = { ...record, registration_id: agentId };
900
+ store.write(registrationName, processRecord);
1100
901
  if (options.binding !== void 0) writeBinding(options.binding);
1101
902
  return agentId;
1102
903
  })();
@@ -1110,7 +911,7 @@ function createRadarPublicationInner(options) {
1110
911
  if (agentId === void 0) return;
1111
912
  store.write(`bindings/${binding.key}.json`, {
1112
913
  version: RADAR_PUBLICATION_VERSION,
1113
- agent_id: agentId,
914
+ registration_id: agentId,
1114
915
  incarnation,
1115
916
  run: binding.run,
1116
917
  owner: binding.owner,
@@ -1134,14 +935,22 @@ function createRadarPublicationInner(options) {
1134
935
  leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
1135
936
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
1136
937
  });
1137
- const context = createContextWriter({
938
+ const presence = createPresenceReporter({
1138
939
  store,
1139
940
  client,
1140
941
  scheduler,
1141
- recordName: contextRecord,
1142
- publisher: publisher("herdsman-pi"),
1143
- subject: ensureSubject,
1144
- leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
942
+ recordName: presenceRecord,
943
+ registration: async () => {
944
+ const record = await ensureRegistration();
945
+ if (!record) return void 0;
946
+ const location = await options.location?.();
947
+ const { location: _initialLocation, ...identity } = record.registration;
948
+ return {
949
+ ...identity,
950
+ ...location?.state === "resolved" ? { location: location.location } : {},
951
+ ...spawnToken ? { spawn_token: spawnToken } : {}
952
+ };
953
+ },
1145
954
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
1146
955
  });
1147
956
  const facts = createChannelWriter({
@@ -1159,7 +968,7 @@ function createRadarPublicationInner(options) {
1159
968
  const assignmentWriters = /* @__PURE__ */ new Map();
1160
969
  const publication = {
1161
970
  execution,
1162
- context,
971
+ presence,
1163
972
  facts,
1164
973
  assignment(target, reportingOwner) {
1165
974
  const existing = assignmentWriters.get(target);
@@ -1174,7 +983,7 @@ function createRadarPublicationInner(options) {
1174
983
  publisher: publisher("herdsman-owner", reportingOwner),
1175
984
  codec: assignmentCodec,
1176
985
  // The owner publishes about a subject it did not register: the exact
1177
- // agent_id from the child's sidecar is the whole correlation.
986
+ // registration_id from the child's sidecar is the whole correlation.
1178
987
  subject: async () => target,
1179
988
  leaseMs: options.leaseMs ?? RADAR_LEASE_MS,
1180
989
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
@@ -1194,7 +1003,7 @@ function createRadarPublicationInner(options) {
1194
1003
  registrationDiagnostic ??= `Radar binding unavailable: ${String(error).slice(0, 180)}`;
1195
1004
  return void 0;
1196
1005
  }
1197
- const bound = textAt(value, "agent_id");
1006
+ const bound = textAt(value, "registration_id");
1198
1007
  const boundIncarnation = textAt(value, "incarnation");
1199
1008
  const run = textAt(value, "run");
1200
1009
  const owner = textAt(value, "owner");
@@ -1204,7 +1013,7 @@ function createRadarPublicationInner(options) {
1204
1013
  return void 0;
1205
1014
  return {
1206
1015
  version: RADAR_PUBLICATION_VERSION,
1207
- agent_id: bound,
1016
+ registration_id: bound,
1208
1017
  incarnation: boundIncarnation,
1209
1018
  run,
1210
1019
  owner,
@@ -1220,25 +1029,27 @@ function createRadarPublicationInner(options) {
1220
1029
  if (agentId2 === void 0) return void 0;
1221
1030
  const record = await ensureRegistration();
1222
1031
  if (record === void 0) return void 0;
1223
- const location = record.registration.location;
1224
- const subjectId = process.env.PI_RADAR_MANAGED_SUBJECT_ID;
1032
+ const currentLocation = await options.location?.();
1033
+ const location = currentLocation?.state === "resolved" ? currentLocation.location : void 0;
1034
+ const standing = presence.management();
1035
+ const subjectId = standing?.state === "managed" ? standing.subject_id : void 0;
1225
1036
  return {
1226
- agent_id: agentId2,
1037
+ registration_id: agentId2,
1227
1038
  ...subjectId !== void 0 ? { subject_id: subjectId } : {},
1228
1039
  ...location !== void 0 ? { location } : {}
1229
1040
  };
1230
1041
  },
1231
- diagnostic: () => registrationDiagnostic ?? execution.diagnostic() ?? context.diagnostic() ?? facts.diagnostic(),
1042
+ diagnostic: () => registrationDiagnostic ?? execution.diagnostic() ?? presence.diagnostic() ?? facts.diagnostic(),
1232
1043
  stop() {
1233
1044
  for (const writer of assignmentWriters.values()) writer.stop();
1234
1045
  execution.stop();
1235
- context.stop();
1046
+ presence.stop();
1236
1047
  facts.stop();
1237
1048
  },
1238
1049
  reopen() {
1239
1050
  for (const writer of assignmentWriters.values()) writer.reopen();
1240
1051
  execution.reopen();
1241
- context.reopen();
1052
+ presence.reopen();
1242
1053
  facts.reopen();
1243
1054
  }
1244
1055
  };
@@ -1276,10 +1087,13 @@ function unavailablePublication2() {
1276
1087
  return {
1277
1088
  execution: writer,
1278
1089
  facts: { ...writer, update: () => void 0 },
1279
- context: {
1280
- update: () => void 0,
1090
+ presence: {
1091
+ report: () => void 0,
1281
1092
  stop: () => void 0,
1282
1093
  reopen: () => void 0,
1094
+ management: () => void 0,
1095
+ ensure: async () => void 0,
1096
+ release: async () => void 0,
1283
1097
  diagnostic: () => "Radar private storage is unavailable"
1284
1098
  },
1285
1099
  assignment: () => writer,
@@ -1362,11 +1176,11 @@ function createRadarExecutionAdapter(pi, options) {
1362
1176
  ...lastOutcome !== void 0 ? { last_outcome: lastOutcome } : {}
1363
1177
  };
1364
1178
  }
1365
- function reportSession(context, launchIdentity) {
1179
+ function reportSession(context) {
1366
1180
  const session = context?.sessionManager?.getSessionId?.();
1367
1181
  if (!isUuid(session)) return;
1368
1182
  try {
1369
- publication.context.update(session, launchIdentity);
1183
+ publication.presence.report(session);
1370
1184
  } catch {
1371
1185
  }
1372
1186
  }
@@ -1430,38 +1244,8 @@ function createRadarExecutionAdapter(pi, options) {
1430
1244
  })
1431
1245
  );
1432
1246
  on("session_start", (_event, context) => {
1433
- const sessionHost = {
1434
- appendEntry: pi.appendEntry ? (customType, data) => pi.appendEntry?.(customType, data) : void 0,
1435
- sessionManager: context?.sessionManager
1436
- };
1437
- let launchIdentity;
1438
- try {
1439
- const result = persistRadarLaunchMarker(
1440
- sessionHost,
1441
- options.markerEnvironment
1442
- );
1443
- if (result === "written" || result === "unchanged") {
1444
- const expected = radarLaunchMarkerFromEnvironment(options.markerEnvironment);
1445
- const activeMarker = activeRadarLaunchMarker(sessionHost);
1446
- if (expected && activeMarker.status === "valid" && JSON.stringify(activeMarker.marker) === JSON.stringify(expected)) {
1447
- launchIdentity = activeMarker.marker;
1448
- } else {
1449
- options.diagnostic?.("Radar launch marker was not visible on the active Pi session branch");
1450
- }
1451
- } else if (result === "unsupported") {
1452
- options.diagnostic?.("Pi host does not support Radar launch identity session entries");
1453
- } else if (options.markerEnvironment && [
1454
- "PI_RADAR_MANAGED_SUBJECT_ID",
1455
- "PI_RADAR_MANAGED_LAUNCH_ID",
1456
- "PI_RADAR_MANAGED_EXECUTION_GENERATION"
1457
- ].some((key) => options.markerEnvironment?.[key] !== void 0)) {
1458
- options.diagnostic?.("incomplete or invalid Radar launch identity environment");
1459
- }
1460
- } catch (error) {
1461
- options.diagnostic?.(`could not persist Radar launch marker: ${String(error)}`);
1462
- }
1463
1247
  rootSession = true;
1464
- reportSession(context, launchIdentity);
1248
+ reportSession(context);
1465
1249
  publishFacts(context);
1466
1250
  publication.reopen();
1467
1251
  active = context?.isIdle?.() === false;
@@ -1492,8 +1276,9 @@ function createRadarExecutionAdapter(pi, options) {
1492
1276
  }
1493
1277
  emit();
1494
1278
  });
1495
- on("session_shutdown", () => {
1279
+ on("session_shutdown", async () => {
1496
1280
  suspend();
1281
+ await publication.presence.release();
1497
1282
  });
1498
1283
  function suspend() {
1499
1284
  if (stopped) return;