pi-radar 0.5.1 → 0.7.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 (27) hide show
  1. package/README.md +76 -2
  2. package/dist/extension/baseline.js +45 -13
  3. package/dist/extension/radar-client.js +37 -10
  4. package/dist/extension/radar-execution.js +10 -4
  5. package/dist/extension/radar-publication.js +9 -3
  6. package/dist/index.js +47 -14
  7. package/extension/generated-contracts.ts +115 -8
  8. package/extension/radar-client.ts +42 -15
  9. package/extension/radar-execution.ts +5 -5
  10. package/extension/radar-publication.ts +11 -4
  11. package/package.json +1 -1
  12. package/schema/public/v1/handoff-receipt.deserialize.schema.json +2 -9
  13. package/schema/public/v1/handoff-receipt.serialize.schema.json +2 -9
  14. package/schema/public/v1/handoff.deserialize.schema.json +2 -9
  15. package/schema/public/v1/handoff.serialize.schema.json +2 -9
  16. package/schema/public/v1/observation-get-response.deserialize.schema.json +18 -6
  17. package/schema/public/v1/observation-get-response.serialize.schema.json +19 -7
  18. package/schema/public/v1/observation-snapshot.deserialize.schema.json +18 -6
  19. package/schema/public/v1/observation-snapshot.serialize.schema.json +19 -7
  20. package/schema/public/v1/presence-answer.deserialize.schema.json +1 -0
  21. package/schema/public/v1/presence-answer.serialize.schema.json +1 -0
  22. package/schema/public/v1/presence-report.deserialize.schema.json +0 -7
  23. package/schema/public/v1/presence-report.serialize.schema.json +0 -8
  24. package/schema/public/v1/spawn-list-page.deserialize.schema.json +291 -0
  25. package/schema/public/v1/spawn-list-page.serialize.schema.json +296 -0
  26. package/schema/public/v1/spawn-list-params.deserialize.schema.json +33 -0
  27. package/schema/public/v1/spawn-list-params.serialize.schema.json +38 -0
package/README.md CHANGED
@@ -15,11 +15,17 @@ Focused subpaths are also exported: `pi-radar/client`, `pi-radar/communication`,
15
15
 
16
16
  Installing `pi-radar` directly as a Pi package loads `extension/baseline.ts`, which creates one baseline publisher and exposes its lazy runtime handle through Pi's event bus. Herdsman must request that handle rather than starting another `RadarExecutionAdapter`. Do not load the baseline extension with an older Herdsman integration that still creates its own publisher; first migrate Herdsman to the runtime handle.
17
17
 
18
- The baseline reports session presence through one flat request. A fresh `session_start` creates a claim; heartbeat/reconnect retries reuse it. `publication.presence.management()` reports `managed`, `duplicate`, `pending` or `unmanaged`; `session_shutdown` awaits release of the exact claim. A duplicate remains rejected until a fresh Pi session load. `subject()` returns Radar's current subject only after a grant; no session marker is written to the Pi transcript.
18
+ The baseline reports session presence through one flat request. The first managed `session_start` uses its pre-minted launch claim; subsequent session loads create fresh claims. Heartbeat/reconnect retries reuse the same claim. `publication.presence.management()` reports `managed`, `duplicate`, `pending` or `unmanaged`; `session_shutdown` awaits release of the exact claim. A duplicate remains rejected until a fresh Pi session load. `subject()` returns Radar's current subject only after a grant; no session marker is written to the Pi transcript.
19
19
 
20
20
  ## Daemon contract
21
21
 
22
- The client speaks protocol 2 and requires wire revision 4. Revision 3 made lifecycle `subject_id` a stable logical identity and exposed the exact current `execution_id` and `u32` generation separately; revision 4 added Pi presence (`agent.presence`/`agent.release`), which is how a session acquires its subject, so a revision-3 daemon would accept the handshake and then leave every Pi session unmanaged. Handoff methods additionally require the independently negotiated `communication.handoff.v1` contract; its timestamps remain exactly representable as JSON numbers. Clients that control managed executions must not target the legacy per-execution ID semantics. It reads `revision` and the public `contracts` list from `daemon.ping`, preserves unfamiliar contract names, and preflights only contracts needed for a multi-step or versioned operation. Backend `mux.*` primitives are private to the daemon; direct mux methods are dispatched and return the daemon's backend refusal when unavailable.
22
+ **Managed execution:** a launch Radar admits arrives with `PI_RADAR_REGISTRATION_ID` in its environment. The baseline captures it, removes it from this process's environment before any child starts, and reports it as `registration_id`, so a managed launch binds the registration Radar already allocated instead of registering itself. A session claim is still fresh per load, and a reload or `/resume` takes a new one under the same registration; nothing about the process identity is presented or reused as a claim. `launch.admit` is the method `radar run` uses and is not part of the client surface. `agent.list` entries and observation agents carry `scoped`: `true` for a process Radar launched into a scope, `false` for an observed process it did not.
23
+
24
+ **0.7.0 upgrade:** update Radar (0.3.0) and pi-radar together; the floor is wire revision 6. A managed launch now carries `PI_RADAR_REGISTRATION_ID` instead of a claim id, and pi-radar always presents its own fresh claim. Nothing in your code changes unless you read the launch environment directly.
25
+
26
+ **0.6.0 upgrade:** update Radar and pi-radar together. Lifecycle controls require both `subject_id` and `claim_id`; handoff identities and spawn edges use `claim_id`, with no execution generation. Do not pass an agent's subject id where an attempt is required. Existing private launch records remain readable.
27
+
28
+ pi-radar 0.7.0 speaks protocol 2 and requires wire revision 6. Revision 6 gives a managed launch its process identity up front: Radar mints the `registration_id`, passes it as `PI_RADAR_REGISTRATION_ID`, and holds a `reserved` claim that grants nothing until the first verified report replaces it with the session's own claim, so a 0.6.0 client, which spends the spawn token as its first claim, cannot bind. Revision 4 added Pi presence (`agent.presence`/`agent.release`), which is how a session acquires its subject. Revision 5 names the agent `subject_id` and one attempt `claim_id`, drops the execution-generation ordinal, and requires lifecycle controls to name the claim they were issued against, so a revision-4 client would read those fields with the old meanings. Handoff methods additionally require the independently negotiated `communication.handoff.v1` contract; its timestamps remain exactly representable as JSON numbers. Clients that control managed executions must not target the legacy per-execution ID semantics. It reads `revision` and the public `contracts` list from `daemon.ping`, preserves unfamiliar contract names, and preflights only contracts needed for a multi-step or versioned operation. Backend `mux.*` primitives are private to the daemon; direct mux methods are dispatched and return the daemon's backend refusal when unavailable.
23
29
 
24
30
  The client uses `registry.v1` for registry operations and additionally requires `registry.facts.v1` for the facts channel and assignment extensions. Managed children require `spawn.v1`, child-pane removal requires `spawn.close.v1`, event reads require `events.v1`, and handoff operations require `communication.handoff.v1`. The expected wire examples are maintained in this repository's `docs/control-plane.fixture.jsonl` and `docs/agent-registration.fixture.jsonl` and are exercised by the package tests.
25
31
 
@@ -41,6 +47,74 @@ Lifecycle execution generations are bounded to `u32` and remain exactly represen
41
47
 
42
48
  Run `npm pack` to build the bundled runtime entrypoints before creating the tarball.
43
49
 
50
+ ## Testing against this SDK
51
+
52
+ Fake the port, not the daemon. Every consumer-facing class here depends on a
53
+ narrow `Pick` of the client, so a test double implements a handful of methods and
54
+ no socket, framing, IDs, error envelope or durable state:
55
+
56
+ | Port | Methods | Where |
57
+ | --- | --- | --- |
58
+ | `RadarPublicationClient` | `ping`, `presence`, `releasePresence`, `acquire`, `publish`, `retire` | `pi-radar/publication` |
59
+ | `RadarHandoffPort` | `publish`, `inbox`, `get`, `ack` | `pi-radar/communication` |
60
+ | anything else | `Pick<RadarClient, ...>` of the reads you call | `pi-radar/client` |
61
+
62
+ ```ts
63
+ import type { RadarPublicationClient } from "pi-radar/publication";
64
+ import { createRadarPublication } from "pi-radar/publication";
65
+
66
+ const client: RadarPublicationClient = {
67
+ ping: async () => ({
68
+ ok: true,
69
+ value: {
70
+ protocol: 2, backend: "tmux", revision: 4, instance: null,
71
+ contracts: ["registry.v1"], tasks: { available: false, error: null },
72
+ },
73
+ }),
74
+ presence: async (report) => ({
75
+ ok: true,
76
+ value: {
77
+ registration_id: REGISTRATION,
78
+ claim_id: report.claim_id,
79
+ standing: "granted",
80
+ subject_id: SUBJECT,
81
+ },
82
+ }),
83
+ releasePresence: async (release) => ({
84
+ ok: true,
85
+ value: {
86
+ registration_id: release.registration_id,
87
+ claim_id: release.claim_id,
88
+ standing: "released",
89
+ },
90
+ }),
91
+ acquire: async () => ({
92
+ ok: true,
93
+ value: {
94
+ handle: "writer", source: "consumer", incarnation: INCARNATION,
95
+ generation: 1, sequence: 0,
96
+ },
97
+ }),
98
+ publish: async ({ sequence }) => ({ ok: true, value: { sequence } }),
99
+ retire: async () => ({ ok: true, value: undefined }),
100
+ };
101
+
102
+ const publication = createRadarPublication({
103
+ dataRoot, client, registration: { source: "consumer", label: "lead" },
104
+ });
105
+ ```
106
+
107
+ A socket-level fake daemon is a second implementation of Radar's protocol
108
+ maintained by someone who does not own it. It drifts silently: it keeps passing
109
+ while the real daemon refuses the same call, which is worse than having no fake
110
+ at all. That is not hypothetical — a consumer's fake still answered the retired
111
+ registration methods after the daemon had moved to presence, and its suite stayed
112
+ green through the whole contract change.
113
+
114
+ Wire-level proof belongs to one integration test against the real daemon (a
115
+ temporary socket path and state root are enough; `tests/agent_publisher.py` in
116
+ the Radar repository shows the shape), not to a fake every consumer re-implements.
117
+
44
118
  ## Releasing
45
119
 
46
120
  Publishing is one tag push and it runs in CI; no npm token is stored anywhere.
@@ -8,10 +8,11 @@ import { lstatSync } from "node:fs";
8
8
  import { createConnection } from "node:net";
9
9
  import { dirname, join } from "node:path";
10
10
  var RADAR_PROTOCOL_VERSION = 2;
11
- var RADAR_MINIMUM_REVISION = 4;
11
+ var RADAR_MINIMUM_REVISION = 6;
12
12
  var RADAR_REGISTRY_CONTRACT = "registry.v1";
13
13
  var RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
14
14
  var RADAR_SPAWN_CONTRACT = "spawn.v1";
15
+ var RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
15
16
  var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
16
17
  var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
17
18
  var RADAR_EVENTS_CONTRACT = "events.v1";
@@ -118,7 +119,7 @@ function decodePresenceAnswer(value) {
118
119
  const claimId = recordAt(value, "claim_id");
119
120
  const standing = recordAt(value, "standing");
120
121
  const subjectId = recordAt(value, "subject_id");
121
- if (!isUuid(registrationId) || !isUuid(claimId) || !["pending", "granted", "duplicate", "released"].includes(String(standing)) || subjectId !== void 0 && !isUuid(subjectId) || standing === "granted" && !isUuid(subjectId))
122
+ if (!isUuid(registrationId) || !isUuid(claimId) || !["reserved", "pending", "granted", "duplicate", "released"].includes(String(standing)) || subjectId !== void 0 && !isUuid(subjectId) || standing === "granted" && !isUuid(subjectId))
122
123
  return protocol("presence reply carries no valid claim standing");
123
124
  return { ok: true, value };
124
125
  }
@@ -146,22 +147,20 @@ function handoffAt(value) {
146
147
  const version = numberAt(value, "version");
147
148
  const id = stringAt(value, "id");
148
149
  const sender = recordAt(value, "sender");
149
- const senderGeneration = numberAt(sender, "execution_generation");
150
150
  const target = stringAt(value, "recipient_subject_id");
151
151
  const summary = stringAt(value, "summary");
152
152
  const content = stringAt(value, "content");
153
153
  const published = numberAt(value, "published_at_unix_ms");
154
154
  const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
155
- if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) || (senderGeneration ?? 0) < 1 || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
155
+ if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "claim_id")) || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
156
156
  return value;
157
157
  }
158
158
  function handoffReceiptAt(value) {
159
159
  const version = numberAt(value, "version");
160
160
  const handoffId = stringAt(value, "handoff_id");
161
161
  const recipient = recordAt(value, "recipient");
162
- const generation = numberAt(recipient, "execution_generation");
163
162
  const received = numberAt(value, "received_at_unix_ms");
164
- if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) || (generation ?? 0) < 1 || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
163
+ if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "claim_id")) || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
165
164
  return value;
166
165
  }
167
166
  function writerBinding(value) {
@@ -265,7 +264,7 @@ function unresolvedOperation(requestId, detail) {
265
264
  message: `focus ${requestId} is unresolved (${detail})`
266
265
  };
267
266
  }
268
- var RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
267
+ var RADAR_REGISTRATION_ENV = "PI_RADAR_REGISTRATION_ID";
269
268
  var RADAR_MANAGED_IDENTITY_ENV = /* @__PURE__ */ new Set([
270
269
  "PI_RADAR_MANAGED_SUBJECT_ID",
271
270
  "PI_RADAR_MANAGED_LAUNCH_ID",
@@ -414,8 +413,8 @@ function spawnRequestShape(request) {
414
413
  const env = Object.entries(request.env ?? {});
415
414
  if (env.length > 256) return "spawn env names more than 256 entries";
416
415
  for (const [name, value] of env) {
417
- if (name === RADAR_SPAWN_TOKEN_ENV)
418
- return `spawn env cannot set the reserved ${RADAR_SPAWN_TOKEN_ENV}`;
416
+ if (name === RADAR_REGISTRATION_ENV)
417
+ return `spawn env cannot set the reserved ${RADAR_REGISTRATION_ENV}`;
419
418
  if (RADAR_MANAGED_IDENTITY_ENV.has(name))
420
419
  return `spawn env cannot set Radar-issued identity variable ${name}`;
421
420
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || new RegExp("\\p{Cc}", "u").test(value))
@@ -621,6 +620,7 @@ function createRadarClient(options = {}) {
621
620
  };
622
621
  return answer;
623
622
  };
623
+ let processRegistrationId;
624
624
  let handshake;
625
625
  const negotiated = async () => {
626
626
  handshake ??= probe();
@@ -702,6 +702,7 @@ function createRadarClient(options = {}) {
702
702
  const record = answer.value.registration;
703
703
  const agentId = stringAt(record, "registration_id");
704
704
  if (!isUuid(agentId)) return protocol("registration reply carries no registration_id");
705
+ processRegistrationId = agentId;
705
706
  return { ok: true, value: { registration_id: agentId } };
706
707
  });
707
708
  },
@@ -835,6 +836,31 @@ function createRadarClient(options = {}) {
835
836
  }
836
837
  );
837
838
  },
839
+ async spawnList(request) {
840
+ const registrationId = processRegistrationId;
841
+ if (registrationId === void 0)
842
+ return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
843
+ return withContract(RADAR_SPAWN_LIST_CONTRACT, async () => {
844
+ const params = {
845
+ registration_id: registrationId,
846
+ ...request.after !== void 0 ? { after: request.after } : {},
847
+ ...request.limit !== void 0 ? { limit: request.limit } : {}
848
+ };
849
+ const answer = await call("spawn.list", { ...params });
850
+ if (answer.ok === false) return answer;
851
+ const spawns = recordAt(answer.value, "spawns");
852
+ const next = recordAt(answer.value, "next");
853
+ if (!Array.isArray(spawns) || next != null && !isUuid(next))
854
+ return protocol("spawn.list reply carries no valid page");
855
+ for (const spawn of spawns) {
856
+ const decoded = spawnTopology(spawn);
857
+ if (decoded.ok === false) return decoded;
858
+ if (typeof recordAt(spawn, "version") !== "number" || typeof recordAt(spawn, "recorded_at") !== "string" || !["unbound", "bound", "unresolved"].includes(String(recordAt(spawn, "state"))))
859
+ return protocol("spawn.list reply carries an incomplete topology");
860
+ }
861
+ return { ok: true, value: answer.value };
862
+ });
863
+ },
838
864
  async spawnGet(requestId) {
839
865
  if (!isUuid(requestId))
840
866
  return {
@@ -1392,7 +1418,13 @@ function createRadarProcessSlot() {
1392
1418
  }
1393
1419
  function radarProcessSlot() {
1394
1420
  const host = globalThis;
1395
- return host[PROCESS_SLOT] ??= createRadarProcessSlot();
1421
+ if (!host[PROCESS_SLOT]) {
1422
+ const slot = createRadarProcessSlot();
1423
+ slot.launchRegistrationId = process.env.PI_RADAR_REGISTRATION_ID;
1424
+ delete process.env.PI_RADAR_REGISTRATION_ID;
1425
+ host[PROCESS_SLOT] = slot;
1426
+ }
1427
+ return host[PROCESS_SLOT];
1396
1428
  }
1397
1429
  function radarProcessIncarnation(slot = radarProcessSlot()) {
1398
1430
  return slot.incarnation ??= randomUUID2();
@@ -1943,8 +1975,8 @@ function createRadarPublicationInner(options) {
1943
1975
  const store = options.store ?? createPublicationStore(dataRoot);
1944
1976
  const scheduler = options.scheduler ?? systemScheduler;
1945
1977
  const client = options.client;
1946
- const spawnToken = options.spawnToken;
1947
1978
  const slot = options.slot ?? radarProcessSlot();
1979
+ const launchRegistrationId = options.launchRegistrationId ?? slot.launchRegistrationId;
1948
1980
  const incarnation = options.incarnation ?? radarProcessIncarnation(slot);
1949
1981
  const cacheKey = `${options.store === void 0 ? dataRoot : storeIdentity(options.store)}\0${incarnation}`;
1950
1982
  const cached = slot.publications.get(cacheKey);
@@ -2068,7 +2100,7 @@ function createRadarPublicationInner(options) {
2068
2100
  return {
2069
2101
  ...identity,
2070
2102
  ...location?.state === "resolved" ? { location: location.location } : {},
2071
- ...spawnToken ? { spawn_token: spawnToken } : {}
2103
+ ...launchRegistrationId ? { registration_id: launchRegistrationId } : {}
2072
2104
  };
2073
2105
  },
2074
2106
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
@@ -2255,7 +2287,7 @@ function createRadarExecutionAdapter(pi, options) {
2255
2287
  },
2256
2288
  ...options.location !== void 0 ? { location: options.location } : {},
2257
2289
  ...managed !== void 0 ? { binding: managed } : {},
2258
- ...options.spawnToken !== void 0 ? { spawnToken: options.spawnToken } : {}
2290
+ ...options.launchRegistrationId !== void 0 ? { launchRegistrationId: options.launchRegistrationId } : {}
2259
2291
  });
2260
2292
  } catch {
2261
2293
  publication = unavailablePublication2();
@@ -4,10 +4,11 @@ import { lstatSync } from "node:fs";
4
4
  import { createConnection } from "node:net";
5
5
  import { dirname, join } from "node:path";
6
6
  var RADAR_PROTOCOL_VERSION = 2;
7
- var RADAR_MINIMUM_REVISION = 4;
7
+ var RADAR_MINIMUM_REVISION = 6;
8
8
  var RADAR_REGISTRY_CONTRACT = "registry.v1";
9
9
  var RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
10
10
  var RADAR_SPAWN_CONTRACT = "spawn.v1";
11
+ var RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
11
12
  var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
12
13
  var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
13
14
  var RADAR_EVENTS_CONTRACT = "events.v1";
@@ -114,7 +115,7 @@ function decodePresenceAnswer(value) {
114
115
  const claimId = recordAt(value, "claim_id");
115
116
  const standing = recordAt(value, "standing");
116
117
  const subjectId = recordAt(value, "subject_id");
117
- if (!isUuid(registrationId) || !isUuid(claimId) || !["pending", "granted", "duplicate", "released"].includes(String(standing)) || subjectId !== void 0 && !isUuid(subjectId) || standing === "granted" && !isUuid(subjectId))
118
+ if (!isUuid(registrationId) || !isUuid(claimId) || !["reserved", "pending", "granted", "duplicate", "released"].includes(String(standing)) || subjectId !== void 0 && !isUuid(subjectId) || standing === "granted" && !isUuid(subjectId))
118
119
  return protocol("presence reply carries no valid claim standing");
119
120
  return { ok: true, value };
120
121
  }
@@ -142,22 +143,20 @@ function handoffAt(value) {
142
143
  const version = numberAt(value, "version");
143
144
  const id = stringAt(value, "id");
144
145
  const sender = recordAt(value, "sender");
145
- const senderGeneration = numberAt(sender, "execution_generation");
146
146
  const target = stringAt(value, "recipient_subject_id");
147
147
  const summary = stringAt(value, "summary");
148
148
  const content = stringAt(value, "content");
149
149
  const published = numberAt(value, "published_at_unix_ms");
150
150
  const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
151
- if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) || (senderGeneration ?? 0) < 1 || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
151
+ if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "claim_id")) || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
152
152
  return value;
153
153
  }
154
154
  function handoffReceiptAt(value) {
155
155
  const version = numberAt(value, "version");
156
156
  const handoffId = stringAt(value, "handoff_id");
157
157
  const recipient = recordAt(value, "recipient");
158
- const generation = numberAt(recipient, "execution_generation");
159
158
  const received = numberAt(value, "received_at_unix_ms");
160
- if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) || (generation ?? 0) < 1 || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
159
+ if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "claim_id")) || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
161
160
  return value;
162
161
  }
163
162
  function writerBinding(value) {
@@ -265,7 +264,7 @@ function unresolvedOperation(requestId, detail) {
265
264
  message: `focus ${requestId} is unresolved (${detail})`
266
265
  };
267
266
  }
268
- var RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
267
+ var RADAR_REGISTRATION_ENV = "PI_RADAR_REGISTRATION_ID";
269
268
  var RADAR_MANAGED_IDENTITY_ENV = /* @__PURE__ */ new Set([
270
269
  "PI_RADAR_MANAGED_SUBJECT_ID",
271
270
  "PI_RADAR_MANAGED_LAUNCH_ID",
@@ -414,8 +413,8 @@ function spawnRequestShape(request) {
414
413
  const env = Object.entries(request.env ?? {});
415
414
  if (env.length > 256) return "spawn env names more than 256 entries";
416
415
  for (const [name, value] of env) {
417
- if (name === RADAR_SPAWN_TOKEN_ENV)
418
- return `spawn env cannot set the reserved ${RADAR_SPAWN_TOKEN_ENV}`;
416
+ if (name === RADAR_REGISTRATION_ENV)
417
+ return `spawn env cannot set the reserved ${RADAR_REGISTRATION_ENV}`;
419
418
  if (RADAR_MANAGED_IDENTITY_ENV.has(name))
420
419
  return `spawn env cannot set Radar-issued identity variable ${name}`;
421
420
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || new RegExp("\\p{Cc}", "u").test(value))
@@ -679,6 +678,7 @@ function createRadarClient(options = {}) {
679
678
  };
680
679
  return answer;
681
680
  };
681
+ let processRegistrationId;
682
682
  let handshake;
683
683
  const negotiated = async () => {
684
684
  handshake ??= probe();
@@ -760,6 +760,7 @@ function createRadarClient(options = {}) {
760
760
  const record = answer.value.registration;
761
761
  const agentId = stringAt(record, "registration_id");
762
762
  if (!isUuid(agentId)) return protocol("registration reply carries no registration_id");
763
+ processRegistrationId = agentId;
763
764
  return { ok: true, value: { registration_id: agentId } };
764
765
  });
765
766
  },
@@ -893,6 +894,31 @@ function createRadarClient(options = {}) {
893
894
  }
894
895
  );
895
896
  },
897
+ async spawnList(request) {
898
+ const registrationId = processRegistrationId;
899
+ if (registrationId === void 0)
900
+ return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
901
+ return withContract(RADAR_SPAWN_LIST_CONTRACT, async () => {
902
+ const params = {
903
+ registration_id: registrationId,
904
+ ...request.after !== void 0 ? { after: request.after } : {},
905
+ ...request.limit !== void 0 ? { limit: request.limit } : {}
906
+ };
907
+ const answer = await call("spawn.list", { ...params });
908
+ if (answer.ok === false) return answer;
909
+ const spawns = recordAt(answer.value, "spawns");
910
+ const next = recordAt(answer.value, "next");
911
+ if (!Array.isArray(spawns) || next != null && !isUuid(next))
912
+ return protocol("spawn.list reply carries no valid page");
913
+ for (const spawn of spawns) {
914
+ const decoded = spawnTopology(spawn);
915
+ if (decoded.ok === false) return decoded;
916
+ if (typeof recordAt(spawn, "version") !== "number" || typeof recordAt(spawn, "recorded_at") !== "string" || !["unbound", "bound", "unresolved"].includes(String(recordAt(spawn, "state"))))
917
+ return protocol("spawn.list reply carries an incomplete topology");
918
+ }
919
+ return { ok: true, value: answer.value };
920
+ });
921
+ },
896
922
  async spawnGet(requestId) {
897
923
  if (!isUuid(requestId))
898
924
  return {
@@ -1244,13 +1270,14 @@ export {
1244
1270
  RADAR_MAX_LINE_BYTES,
1245
1271
  RADAR_MINIMUM_REVISION,
1246
1272
  RADAR_PROTOCOL_VERSION,
1273
+ RADAR_REGISTRATION_ENV,
1247
1274
  RADAR_REGISTRY_CONTRACT,
1248
1275
  RADAR_REGISTRY_FACTS_CONTRACT,
1249
1276
  RADAR_REQUESTER,
1250
1277
  RADAR_SPAWN_CLOSE_CONTRACT,
1251
1278
  RADAR_SPAWN_CONTRACT,
1252
1279
  RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT,
1253
- RADAR_SPAWN_TOKEN_ENV,
1280
+ RADAR_SPAWN_LIST_CONTRACT,
1254
1281
  boundedRadarAssignment,
1255
1282
  boundedRadarFacts,
1256
1283
  boundedRadarSnapshot,
@@ -272,7 +272,13 @@ function createRadarProcessSlot() {
272
272
  }
273
273
  function radarProcessSlot() {
274
274
  const host = globalThis;
275
- return host[PROCESS_SLOT] ??= createRadarProcessSlot();
275
+ if (!host[PROCESS_SLOT]) {
276
+ const slot = createRadarProcessSlot();
277
+ slot.launchRegistrationId = process.env.PI_RADAR_REGISTRATION_ID;
278
+ delete process.env.PI_RADAR_REGISTRATION_ID;
279
+ host[PROCESS_SLOT] = slot;
280
+ }
281
+ return host[PROCESS_SLOT];
276
282
  }
277
283
  function radarProcessIncarnation(slot = radarProcessSlot()) {
278
284
  return slot.incarnation ??= randomUUID();
@@ -823,8 +829,8 @@ function createRadarPublicationInner(options) {
823
829
  const store = options.store ?? createPublicationStore(dataRoot);
824
830
  const scheduler = options.scheduler ?? systemScheduler;
825
831
  const client = options.client;
826
- const spawnToken = options.spawnToken;
827
832
  const slot = options.slot ?? radarProcessSlot();
833
+ const launchRegistrationId = options.launchRegistrationId ?? slot.launchRegistrationId;
828
834
  const incarnation = options.incarnation ?? radarProcessIncarnation(slot);
829
835
  const cacheKey = `${options.store === void 0 ? dataRoot : storeIdentity(options.store)}\0${incarnation}`;
830
836
  const cached = slot.publications.get(cacheKey);
@@ -948,7 +954,7 @@ function createRadarPublicationInner(options) {
948
954
  return {
949
955
  ...identity,
950
956
  ...location?.state === "resolved" ? { location: location.location } : {},
951
- ...spawnToken ? { spawn_token: spawnToken } : {}
957
+ ...launchRegistrationId ? { registration_id: launchRegistrationId } : {}
952
958
  };
953
959
  },
954
960
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
@@ -1141,7 +1147,7 @@ function createRadarExecutionAdapter(pi, options) {
1141
1147
  },
1142
1148
  ...options.location !== void 0 ? { location: options.location } : {},
1143
1149
  ...managed !== void 0 ? { binding: managed } : {},
1144
- ...options.spawnToken !== void 0 ? { spawnToken: options.spawnToken } : {}
1150
+ ...options.launchRegistrationId !== void 0 ? { launchRegistrationId: options.launchRegistrationId } : {}
1145
1151
  });
1146
1152
  } catch {
1147
1153
  publication = unavailablePublication2();
@@ -274,7 +274,13 @@ function createRadarProcessSlot() {
274
274
  }
275
275
  function radarProcessSlot() {
276
276
  const host = globalThis;
277
- return host[PROCESS_SLOT] ??= createRadarProcessSlot();
277
+ if (!host[PROCESS_SLOT]) {
278
+ const slot = createRadarProcessSlot();
279
+ slot.launchRegistrationId = process.env.PI_RADAR_REGISTRATION_ID;
280
+ delete process.env.PI_RADAR_REGISTRATION_ID;
281
+ host[PROCESS_SLOT] = slot;
282
+ }
283
+ return host[PROCESS_SLOT];
278
284
  }
279
285
  function radarProcessIncarnation(slot = radarProcessSlot()) {
280
286
  return slot.incarnation ??= randomUUID();
@@ -828,8 +834,8 @@ function createRadarPublicationInner(options) {
828
834
  const store = options.store ?? createPublicationStore(dataRoot);
829
835
  const scheduler = options.scheduler ?? systemScheduler;
830
836
  const client = options.client;
831
- const spawnToken = options.spawnToken;
832
837
  const slot = options.slot ?? radarProcessSlot();
838
+ const launchRegistrationId = options.launchRegistrationId ?? slot.launchRegistrationId;
833
839
  const incarnation = options.incarnation ?? radarProcessIncarnation(slot);
834
840
  const cacheKey = `${options.store === void 0 ? dataRoot : storeIdentity(options.store)}\0${incarnation}`;
835
841
  const cached = slot.publications.get(cacheKey);
@@ -953,7 +959,7 @@ function createRadarPublicationInner(options) {
953
959
  return {
954
960
  ...identity,
955
961
  ...location?.state === "resolved" ? { location: location.location } : {},
956
- ...spawnToken ? { spawn_token: spawnToken } : {}
962
+ ...launchRegistrationId ? { registration_id: launchRegistrationId } : {}
957
963
  };
958
964
  },
959
965
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
package/dist/index.js CHANGED
@@ -16,13 +16,14 @@ __export(radar_client_exports, {
16
16
  RADAR_MAX_LINE_BYTES: () => RADAR_MAX_LINE_BYTES,
17
17
  RADAR_MINIMUM_REVISION: () => RADAR_MINIMUM_REVISION,
18
18
  RADAR_PROTOCOL_VERSION: () => RADAR_PROTOCOL_VERSION,
19
+ RADAR_REGISTRATION_ENV: () => RADAR_REGISTRATION_ENV,
19
20
  RADAR_REGISTRY_CONTRACT: () => RADAR_REGISTRY_CONTRACT,
20
21
  RADAR_REGISTRY_FACTS_CONTRACT: () => RADAR_REGISTRY_FACTS_CONTRACT,
21
22
  RADAR_REQUESTER: () => RADAR_REQUESTER,
22
23
  RADAR_SPAWN_CLOSE_CONTRACT: () => RADAR_SPAWN_CLOSE_CONTRACT,
23
24
  RADAR_SPAWN_CONTRACT: () => RADAR_SPAWN_CONTRACT,
24
25
  RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT: () => RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT,
25
- RADAR_SPAWN_TOKEN_ENV: () => RADAR_SPAWN_TOKEN_ENV,
26
+ RADAR_SPAWN_LIST_CONTRACT: () => RADAR_SPAWN_LIST_CONTRACT,
26
27
  boundedRadarAssignment: () => boundedRadarAssignment,
27
28
  boundedRadarFacts: () => boundedRadarFacts,
28
29
  boundedRadarSnapshot: () => boundedRadarSnapshot,
@@ -39,10 +40,11 @@ import { lstatSync } from "node:fs";
39
40
  import { createConnection } from "node:net";
40
41
  import { dirname, join } from "node:path";
41
42
  var RADAR_PROTOCOL_VERSION = 2;
42
- var RADAR_MINIMUM_REVISION = 4;
43
+ var RADAR_MINIMUM_REVISION = 6;
43
44
  var RADAR_REGISTRY_CONTRACT = "registry.v1";
44
45
  var RADAR_REGISTRY_FACTS_CONTRACT = "registry.facts.v1";
45
46
  var RADAR_SPAWN_CONTRACT = "spawn.v1";
47
+ var RADAR_SPAWN_LIST_CONTRACT = "spawn.list.v1";
46
48
  var RADAR_SPAWN_CLOSE_CONTRACT = "spawn.close.v1";
47
49
  var RADAR_SPAWN_LAUNCH_CONTEXT_CONTRACT = "spawn.launch_context.v1";
48
50
  var RADAR_EVENTS_CONTRACT = "events.v1";
@@ -149,7 +151,7 @@ function decodePresenceAnswer(value) {
149
151
  const claimId = recordAt(value, "claim_id");
150
152
  const standing = recordAt(value, "standing");
151
153
  const subjectId = recordAt(value, "subject_id");
152
- if (!isUuid(registrationId) || !isUuid(claimId) || !["pending", "granted", "duplicate", "released"].includes(String(standing)) || subjectId !== void 0 && !isUuid(subjectId) || standing === "granted" && !isUuid(subjectId))
154
+ if (!isUuid(registrationId) || !isUuid(claimId) || !["reserved", "pending", "granted", "duplicate", "released"].includes(String(standing)) || subjectId !== void 0 && !isUuid(subjectId) || standing === "granted" && !isUuid(subjectId))
153
155
  return protocol("presence reply carries no valid claim standing");
154
156
  return { ok: true, value };
155
157
  }
@@ -177,22 +179,20 @@ function handoffAt(value) {
177
179
  const version = numberAt(value, "version");
178
180
  const id = stringAt(value, "id");
179
181
  const sender = recordAt(value, "sender");
180
- const senderGeneration = numberAt(sender, "execution_generation");
181
182
  const target = stringAt(value, "recipient_subject_id");
182
183
  const summary = stringAt(value, "summary");
183
184
  const content = stringAt(value, "content");
184
185
  const published = numberAt(value, "published_at_unix_ms");
185
186
  const correlation = typeof value === "object" && value !== null ? value.correlation_id : void 0;
186
- if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "execution_id")) || !Number.isSafeInteger(senderGeneration) || (senderGeneration ?? 0) < 1 || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
187
+ if (version !== 1 || !isUuid(id) || !isUuid(stringAt(sender, "registration_id")) || !isUuid(stringAt(sender, "session_id")) || !isUuid(stringAt(sender, "subject_id")) || !isUuid(stringAt(sender, "claim_id")) || !isUuid(target) || summary === void 0 || content === void 0 || published === void 0 || !Number.isSafeInteger(published) || published < 0 || correlation !== void 0 && correlation !== null && (typeof correlation !== "string" || correlation.length === 0 || Buffer.byteLength(correlation, "utf8") > 256 || new RegExp("\\p{Cc}", "u").test(correlation))) return void 0;
187
188
  return value;
188
189
  }
189
190
  function handoffReceiptAt(value) {
190
191
  const version = numberAt(value, "version");
191
192
  const handoffId = stringAt(value, "handoff_id");
192
193
  const recipient = recordAt(value, "recipient");
193
- const generation = numberAt(recipient, "execution_generation");
194
194
  const received = numberAt(value, "received_at_unix_ms");
195
- if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "execution_id")) || !Number.isSafeInteger(generation) || (generation ?? 0) < 1 || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
195
+ if (version !== 1 || !isUuid(handoffId) || !isUuid(stringAt(recipient, "registration_id")) || !isUuid(stringAt(recipient, "session_id")) || !isUuid(stringAt(recipient, "subject_id")) || !isUuid(stringAt(recipient, "claim_id")) || received === void 0 || !Number.isSafeInteger(received) || received < 0) return void 0;
196
196
  return value;
197
197
  }
198
198
  function writerBinding(value) {
@@ -300,7 +300,7 @@ function unresolvedOperation(requestId, detail) {
300
300
  message: `focus ${requestId} is unresolved (${detail})`
301
301
  };
302
302
  }
303
- var RADAR_SPAWN_TOKEN_ENV = "PI_RADAR_SPAWN_TOKEN";
303
+ var RADAR_REGISTRATION_ENV = "PI_RADAR_REGISTRATION_ID";
304
304
  var RADAR_MANAGED_IDENTITY_ENV = /* @__PURE__ */ new Set([
305
305
  "PI_RADAR_MANAGED_SUBJECT_ID",
306
306
  "PI_RADAR_MANAGED_LAUNCH_ID",
@@ -449,8 +449,8 @@ function spawnRequestShape(request) {
449
449
  const env = Object.entries(request.env ?? {});
450
450
  if (env.length > 256) return "spawn env names more than 256 entries";
451
451
  for (const [name, value] of env) {
452
- if (name === RADAR_SPAWN_TOKEN_ENV)
453
- return `spawn env cannot set the reserved ${RADAR_SPAWN_TOKEN_ENV}`;
452
+ if (name === RADAR_REGISTRATION_ENV)
453
+ return `spawn env cannot set the reserved ${RADAR_REGISTRATION_ENV}`;
454
454
  if (RADAR_MANAGED_IDENTITY_ENV.has(name))
455
455
  return `spawn env cannot set Radar-issued identity variable ${name}`;
456
456
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || new RegExp("\\p{Cc}", "u").test(value))
@@ -714,6 +714,7 @@ function createRadarClient(options = {}) {
714
714
  };
715
715
  return answer;
716
716
  };
717
+ let processRegistrationId;
717
718
  let handshake;
718
719
  const negotiated = async () => {
719
720
  handshake ??= probe();
@@ -795,6 +796,7 @@ function createRadarClient(options = {}) {
795
796
  const record = answer.value.registration;
796
797
  const agentId = stringAt(record, "registration_id");
797
798
  if (!isUuid(agentId)) return protocol("registration reply carries no registration_id");
799
+ processRegistrationId = agentId;
798
800
  return { ok: true, value: { registration_id: agentId } };
799
801
  });
800
802
  },
@@ -928,6 +930,31 @@ function createRadarClient(options = {}) {
928
930
  }
929
931
  );
930
932
  },
933
+ async spawnList(request) {
934
+ const registrationId = processRegistrationId;
935
+ if (registrationId === void 0)
936
+ return { ok: false, code: "refused", message: "spawn.list requires this client's process registration" };
937
+ return withContract(RADAR_SPAWN_LIST_CONTRACT, async () => {
938
+ const params = {
939
+ registration_id: registrationId,
940
+ ...request.after !== void 0 ? { after: request.after } : {},
941
+ ...request.limit !== void 0 ? { limit: request.limit } : {}
942
+ };
943
+ const answer = await call("spawn.list", { ...params });
944
+ if (answer.ok === false) return answer;
945
+ const spawns = recordAt(answer.value, "spawns");
946
+ const next = recordAt(answer.value, "next");
947
+ if (!Array.isArray(spawns) || next != null && !isUuid(next))
948
+ return protocol("spawn.list reply carries no valid page");
949
+ for (const spawn of spawns) {
950
+ const decoded = spawnTopology(spawn);
951
+ if (decoded.ok === false) return decoded;
952
+ if (typeof recordAt(spawn, "version") !== "number" || typeof recordAt(spawn, "recorded_at") !== "string" || !["unbound", "bound", "unresolved"].includes(String(recordAt(spawn, "state"))))
953
+ return protocol("spawn.list reply carries an incomplete topology");
954
+ }
955
+ return { ok: true, value: answer.value };
956
+ });
957
+ },
931
958
  async spawnGet(requestId) {
932
959
  if (!isUuid(requestId))
933
960
  return {
@@ -1511,7 +1538,13 @@ function createRadarProcessSlot() {
1511
1538
  }
1512
1539
  function radarProcessSlot() {
1513
1540
  const host = globalThis;
1514
- return host[PROCESS_SLOT] ??= createRadarProcessSlot();
1541
+ if (!host[PROCESS_SLOT]) {
1542
+ const slot = createRadarProcessSlot();
1543
+ slot.launchRegistrationId = process.env.PI_RADAR_REGISTRATION_ID;
1544
+ delete process.env.PI_RADAR_REGISTRATION_ID;
1545
+ host[PROCESS_SLOT] = slot;
1546
+ }
1547
+ return host[PROCESS_SLOT];
1515
1548
  }
1516
1549
  function radarProcessIncarnation(slot = radarProcessSlot()) {
1517
1550
  return slot.incarnation ??= randomUUID2();
@@ -2065,8 +2098,8 @@ function createRadarPublicationInner(options) {
2065
2098
  const store = options.store ?? createPublicationStore(dataRoot);
2066
2099
  const scheduler = options.scheduler ?? systemScheduler;
2067
2100
  const client = options.client;
2068
- const spawnToken = options.spawnToken;
2069
2101
  const slot = options.slot ?? radarProcessSlot();
2102
+ const launchRegistrationId = options.launchRegistrationId ?? slot.launchRegistrationId;
2070
2103
  const incarnation = options.incarnation ?? radarProcessIncarnation(slot);
2071
2104
  const cacheKey = `${options.store === void 0 ? dataRoot : storeIdentity(options.store)}\0${incarnation}`;
2072
2105
  const cached = slot.publications.get(cacheKey);
@@ -2190,7 +2223,7 @@ function createRadarPublicationInner(options) {
2190
2223
  return {
2191
2224
  ...identity,
2192
2225
  ...location?.state === "resolved" ? { location: location.location } : {},
2193
- ...spawnToken ? { spawn_token: spawnToken } : {}
2226
+ ...launchRegistrationId ? { registration_id: launchRegistrationId } : {}
2194
2227
  };
2195
2228
  },
2196
2229
  heartbeatMs: options.heartbeatMs ?? RADAR_HEARTBEAT_MS
@@ -2383,7 +2416,7 @@ function createRadarExecutionAdapter(pi, options) {
2383
2416
  },
2384
2417
  ...options.location !== void 0 ? { location: options.location } : {},
2385
2418
  ...managed !== void 0 ? { binding: managed } : {},
2386
- ...options.spawnToken !== void 0 ? { spawnToken: options.spawnToken } : {}
2419
+ ...options.launchRegistrationId !== void 0 ? { launchRegistrationId: options.launchRegistrationId } : {}
2387
2420
  });
2388
2421
  } catch {
2389
2422
  publication = unavailablePublication2();