pi-amq 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,7 +32,14 @@ version-matched excerpts covering `overview`, `tools`, `contracts`, `gate`,
32
32
 
33
33
  Design: [docs/design.md](docs/design.md) — the authoritative contract for
34
34
  implementation. Every debated decision carries a `(debate: ...)` provenance
35
- note.
35
+ note. [docs/contracts/](docs/contracts) maps that contract to owners,
36
+ implementation files, and fault-sensitive tests
37
+ ([transport](docs/contracts/transport.md),
38
+ [worker runtime](docs/contracts/worker-runtime.md),
39
+ [operations](docs/contracts/operations.md)); the maps add no rules, and any
40
+ conflict resolves to `docs/design.md`. Findings that were investigated and
41
+ deliberately not acted on, with their reopen conditions, are recorded in
42
+ [docs/contracts/deferred-findings.md](docs/contracts/deferred-findings.md).
36
43
 
37
44
  ## Principle
38
45
 
@@ -85,8 +92,40 @@ from both the in-memory session and disk leaves the request pending with
85
92
  visible `/amq ingest` guidance instead; see
86
93
  [§17.1](docs/design.md#171-fresh-session-automation-compatibility-amendment).
87
94
 
88
- Run `npm test` for the fault-sensitive suite. The reproducible live AMQ flow is
89
- in [`scripts/demo-two-terminal.sh`](scripts/demo-two-terminal.sh), with a
95
+ ## Verification
96
+
97
+ Two suites, one boundary.
98
+
99
+ - `npm test` is the **repository suite**. It compiles `test/**/*.ts` and runs
100
+ `.test-dist/test/*.test.js` against the source tree in this checkout.
101
+ - `npm run test:package` is the **consumer-artifact gate**. It packs the real
102
+ npm tarball, extracts it into a staged `node_modules/pi-amq` tree, and loads
103
+ that extracted copy through the stock pi `--extension` seam, asserting the
104
+ registered tool surface, that `guide` resolves `docs/operator-guide.md` from
105
+ the extracted package root, and that the packaged manifest ships the documents
106
+ it promises.
107
+
108
+ The pi-amq extension under test is loaded only from the extracted tarball, so a
109
+ path missing from `package.json#files` fails the gate. The repository still
110
+ supplies the test-only probe extension and the already-resolved dependency tree
111
+ the staged package links against. Staging extracts the tarball; it does not run
112
+ npm's installer, so npm's own resolution, lifecycle scripts, and peer-dependency
113
+ handling are out of scope. Every probe runs with `HOME` and
114
+ `PI_CODING_AGENT_DIR` inside its staged workspace, so the gate never touches the
115
+ caller's `~/.pi` runtime state.
116
+
117
+ The gate is fail-closed: it needs a stock `pi` binary on `PATH` or at
118
+ `PI_AMQ_TEST_PI_BINARY`, and without one it **fails** with a missing-binary
119
+ diagnostic instead of reporting success on skipped cases. The manifest case does
120
+ not need `pi` and stays independently runnable.
121
+
122
+ Neither suite ships. `package.json#files` omits `test` and `tsconfig.test.json`
123
+ on purpose: a consumer installs the runtime surface, not the harness that
124
+ verifies it. `test:package` is the check that keeps that omission honest,
125
+ because it only ever sees what a consumer receives.
126
+
127
+ The reproducible live AMQ flow is in
128
+ [`scripts/demo-two-terminal.sh`](scripts/demo-two-terminal.sh), with a
90
129
  normalized transcript in [`docs/demo-transcript.md`](docs/demo-transcript.md).
91
130
  The fault-sensitive zero-AMQ worker-team demonstration is case 37 in
92
131
  [`test/workflow-run-store.test.ts`](test/workflow-run-store.test.ts).
@@ -1 +1 @@
1
- {"version":3,"file":"worker-runtime.d.ts","sourceRoot":"","sources":["../../extension/worker-runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"worker-runtime.d.ts","sourceRoot":"","sources":["../../extension/worker-runtime.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAKrE,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,8CAA8C,CAAC;AAmCtD,OAAO,EAA0B,KAAK,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAW/G,OAAO,KAAK,EACV,eAAe,EAIf,gBAAgB,EAEjB,MAAM,+BAA+B,CAAC;AAMvC,OAAO,KAAK,EACV,0BAA0B,EAC1B,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,EACzB,4BAA4B,EAC5B,yBAAyB,EACzB,yBAAyB,EACzB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AA+C3B,qBAAa,eAAgB,YAAW,kBAAkB;;gBAoBtD,YAAY,GAAE,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,KAAK,IAAe,EACvE,oBAAoB,GAAE,MAAM,OAAyB,EACrD,gBAAgB,GAAE,CAAC,UAAU,EAAE,2BAA2B,KAAK,OAAO,CAAC,IAAI,CAAkB,EAC7F,aAAa,GAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAe;IAQpD,KAAK,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiH3C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAOzB,eAAe,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB3D,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAItC,KAAK,CACT,MAAM,EAAE,yBAAyB,EACjC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,OAAO,CAAC;IAwEb,IAAI,CAAC,MAAM,EAAE,yBAAyB,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBjF,OAAO,CACX,MAAM,EAAE,2BAA2B,EACnC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,OAAO,CAAC;IAyCb,MAAM,CACV,MAAM,EAAE,0BAA0B,EAClC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,OAAO,CAAC;IAUb,KAAK,CACT,MAAM,EAAE,yBAAyB,EACjC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,OAAO,CAAC;IAWb,cAAc,CAClB,MAAM,EAAE,4BAA4B,EACpC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,OAAO,CAAC;IAoBb,WAAW,CACf,MAAM,EAAE,yBAAyB,EACjC,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,GAAG,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,GACnD,OAAO,CAAC,OAAO,CAAC;IAkBb,cAAc,CAClB,OAAO,EAAE,WAAW,GAAG,SAAS,EAChC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,GACnC,OAAO,CAAC,OAAO,CAAC;IAIb,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC;IAUrC,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBpF,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CA2R3E;AA6ID,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,eAAe,GAAG,SAAS,gBAAgB,EAAE,CA6BrF;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAUjE;AAiCD,QAAA,MAAM,qBAAqB,oGAEjB,CAAC;AACX,KAAK,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,kBAAkB,EAAE,CAa5F;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,gBAAgB,EACrB,WAAW,EAAE,OAAO,GACnB,OAAO,CAAC,OAAO,+BAA+B,EAAE,SAAS,CAAC,CAyB5D;AA8KD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAyCvE"}
@@ -2,7 +2,7 @@ import { spawn as spawnChild } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { access, readFile } from "node:fs/promises";
4
4
  import { constants } from "node:fs";
5
- import { delimiter, isAbsolute, join } from "node:path";
5
+ import { delimiter, isAbsolute, join, normalize, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { openLaunchPreview, } from "../integration/launch-preview.js";
8
8
  import { WorkerCompletionNotifier, } from "../integration/worker-completion-notifier.js";
@@ -20,7 +20,7 @@ import { WorkerRolePresetCatalog, workerRoleContract, } from "../workflow/worker
20
20
  import { LaunchPreviewCoordinator, loadConfirmSpawnsPolicy, } from "../workflow/spawn-confirmation.js";
21
21
  import { projectWorkerLifecycle } from "../workflow/lifecycle-projection.js";
22
22
  import { NodePiProcessFactory, WorkerProcessSupervisor, } from "../workflow/worker-process-supervisor.js";
23
- import { WorkflowLeaseManager } from "../workflow/workflow-lease.js";
23
+ import { WorkflowLeaseManager, } from "../workflow/workflow-lease.js";
24
24
  import { defaultPiStateDirectory, workflowNamespaceDirectory, workflowWorkspaceId, } from "../workflow/workspace-paths.js";
25
25
  const DRIVER_DOCTRINE = [
26
26
  "## Supervised Workers (pi-amq)",
@@ -34,6 +34,7 @@ const CAPABILITY_TOOLS = {
34
34
  shell: ["bash"],
35
35
  message_worker: ["message_worker"],
36
36
  };
37
+ const WORKER_RUNTIME_REGISTRY_SYMBOL = Symbol.for("pi-amq.worker-runtime-registry.v1");
37
38
  export class PiWorkerRuntime {
38
39
  #publishFleet;
39
40
  #unlimitedDeclaration;
@@ -51,7 +52,7 @@ export class PiWorkerRuntime {
51
52
  #requestKey;
52
53
  #error;
53
54
  #guidance = DRIVER_DOCTRINE;
54
- #runtimes = new Map();
55
+ #attachment;
55
56
  constructor(publishFleet = () => { }, unlimitedDeclaration = () => undefined, publishLifecycle = async () => { }, publishNotice = () => { }) {
56
57
  this.#publishFleet = publishFleet;
57
58
  this.#unlimitedDeclaration = unlimitedDeclaration;
@@ -59,35 +60,42 @@ export class PiWorkerRuntime {
59
60
  this.#publishNotice = publishNotice;
60
61
  }
61
62
  async start(ctx) {
63
+ this.#detach();
62
64
  await this.refreshGuidance(ctx);
63
- const existing = this.#runtimes.get(ctx.cwd);
64
- if (existing) {
65
- this.#cwd = ctx.cwd;
66
- this.#store = existing.store;
67
- this.#supervisor = existing.supervisor;
68
- this.#cleanup = existing.cleanup;
69
- this.#doctor = existing.doctor;
70
- this.#artifactDirectory = existing.artifactDirectory;
71
- this.#requestKey = existing.requestKey;
72
- existing.supervisor.updateLaunchAuthority(await launchAuthority(ctx));
73
- this.#publish(await requiredRun(existing.store, existing.requestKey));
74
- void this.#publishWithConversation(await requiredRun(existing.store, existing.requestKey));
75
- return;
76
- }
77
65
  try {
78
- const runBudget = await confirmedRunBudget(ctx, this.#unlimitedDeclaration());
79
- const authority = await launchAuthority(ctx);
80
- const namespaceDirectory = workflowNamespaceDirectory(defaultPiStateDirectory(), ctx.cwd);
66
+ const workspacePath = canonicalWorkerWorkspace(ctx.cwd);
67
+ const workspaceId = workflowWorkspaceId(workspacePath);
68
+ const namespaceDirectory = workflowNamespaceDirectory(defaultPiStateDirectory(), workspacePath);
69
+ const key = workerRuntimeRegistryKey(namespaceDirectory, workspaceId);
70
+ const registry = processWorkerRuntimeRegistry();
81
71
  const inspector = new SystemProcessIdentityInspector();
82
72
  const leaseManager = new WorkflowLeaseManager({
83
73
  namespaceDirectory,
84
- workspaceId: workflowWorkspaceId(ctx.cwd),
74
+ workspaceId,
85
75
  inspector,
86
76
  });
77
+ const registered = registry.entries.get(key);
78
+ if (registered !== undefined) {
79
+ const existing = parseWorkerRuntimeCore(registered, key);
80
+ await validateRuntimeAdoption(existing, {
81
+ workspacePath,
82
+ workspaceId,
83
+ namespaceDirectory,
84
+ inspector,
85
+ leaseManager,
86
+ });
87
+ existing.supervisor.updateLaunchAuthority(await launchAuthority(ctx));
88
+ const current = await requiredRun(existing.store, existing.requestKey);
89
+ this.#attach(existing, current);
90
+ this.#error = undefined;
91
+ return;
92
+ }
93
+ const runBudget = await confirmedRunBudget(ctx, this.#unlimitedDeclaration());
94
+ const authority = await launchAuthority(ctx);
87
95
  const acquired = await leaseManager.acquireSupervisor();
88
96
  if (acquired.kind !== "acquired") {
89
97
  throw new Error(acquired.kind === "held"
90
- ? `worker supervisor is held by PID ${acquired.owner.pid}`
98
+ ? `worker supervisor is held by PID ${acquired.owner.pid}; fully restart Pi without deleting the supervisor lease`
91
99
  : acquired.diagnostic);
92
100
  }
93
101
  const store = new WorkflowRunStore({
@@ -96,7 +104,7 @@ export class PiWorkerRuntime {
96
104
  supervisorGeneration: acquired.owner.generation,
97
105
  });
98
106
  const sessionIdentity = ctx.sessionManager.getSessionFile() ?? `process:${process.pid}`;
99
- const requestKey = `pi-session:${hash(`${ctx.cwd}\0${sessionIdentity}`)}`;
107
+ const requestKey = `pi-session:${hash(`${workspacePath}\0${sessionIdentity}`)}`;
100
108
  await store.startRun({
101
109
  requestKey,
102
110
  source: {
@@ -135,53 +143,37 @@ export class PiWorkerRuntime {
135
143
  leases: leaseManager,
136
144
  inspector,
137
145
  });
138
- const initial = await requiredRun(store, requestKey);
139
- this.#baselineLifecycle(initial);
140
- const notifier = new WorkerCompletionNotifier({ publish: this.#publishNotice });
141
- notifier.baseline(initial);
142
- const unsubscribeStore = store.subscribe((projection) => {
143
- this.#publish(projection);
144
- void this.#publishWithConversation(projection);
145
- void this.#publishLifecycleChanges(projection).catch(() => { });
146
- notifier.observe(projection);
147
- });
148
- const unsubscribe = () => {
149
- unsubscribeStore();
150
- notifier.dispose();
151
- };
152
146
  const reconciled = await supervisor.reconcileRun(requestKey);
153
- this.#cwd = ctx.cwd;
154
- this.#store = store;
155
- this.#supervisor = supervisor;
156
- this.#cleanup = cleanup;
157
- this.#doctor = doctor;
158
- this.#artifactDirectory = artifactDirectory;
159
- this.#requestKey = requestKey;
160
- this.#runtimes.set(ctx.cwd, {
147
+ const core = {
148
+ schema: 1,
149
+ key,
150
+ workspacePath,
151
+ workspaceId,
152
+ namespaceDirectory,
153
+ processIdentity: processIdentityFromOwner(acquired.owner),
154
+ supervisorGeneration: acquired.owner.generation,
155
+ leaseManager,
161
156
  store,
162
157
  supervisor,
163
158
  cleanup,
164
159
  doctor,
165
160
  artifactDirectory,
166
161
  requestKey,
167
- unsubscribe,
168
- });
162
+ };
163
+ registry.entries.set(key, core);
164
+ this.#attach(core, reconciled);
169
165
  this.#error = undefined;
170
- this.#publish(reconciled);
171
166
  }
172
167
  catch (error) {
168
+ this.#detach();
173
169
  this.#error = toError(error).message;
174
- this.#store = undefined;
175
- this.#supervisor = undefined;
176
- this.#cleanup = undefined;
177
- this.#doctor = undefined;
178
- this.#artifactDirectory = undefined;
179
- this.#requestKey = undefined;
180
170
  }
181
171
  }
182
172
  async shutdown() {
183
173
  // Workers own a parent-liveness descriptor. Session changes do not imply cancellation,
184
- // so the runtime intentionally retains the supervisor instead of inventing a stop policy.
174
+ // so only this extension instance's advisory subscribers are detached. The process-global
175
+ // core and its supervisor lease remain intact for a replacement extension instance.
176
+ this.#detach();
185
177
  }
186
178
  async refreshGuidance(ctx) {
187
179
  try {
@@ -198,7 +190,7 @@ export class PiWorkerRuntime {
198
190
  projectTrusted: ctx.isProjectTrusted(),
199
191
  });
200
192
  this.#guidance = `${DRIVER_DOCTRINE}\n\n${guidance.text}${guidance.notices.length > 0 ? `\n${guidance.notices.join("\n")}` : ""}`;
201
- if (this.#cwd === ctx.cwd) {
193
+ if (this.#cwd === canonicalWorkerWorkspace(ctx.cwd)) {
202
194
  this.#supervisor?.updateLaunchAuthority(await launchAuthority(ctx));
203
195
  }
204
196
  }
@@ -563,10 +555,61 @@ export class PiWorkerRuntime {
563
555
  requestKey: this.#requestKey,
564
556
  };
565
557
  }
558
+ #attach(core, projection) {
559
+ this.#detach();
560
+ this.#cwd = core.workspacePath;
561
+ this.#store = core.store;
562
+ this.#supervisor = core.supervisor;
563
+ this.#cleanup = core.cleanup;
564
+ this.#doctor = core.doctor;
565
+ this.#artifactDirectory = core.artifactDirectory;
566
+ this.#requestKey = core.requestKey;
567
+ this.#lifecycleStates.clear();
568
+ this.#baselineLifecycle(projection);
569
+ const notifier = new WorkerCompletionNotifier({ publish: this.#publishNotice });
570
+ notifier.baseline(projection);
571
+ const attachment = {
572
+ core,
573
+ notifier,
574
+ active: true,
575
+ unsubscribe: () => { },
576
+ };
577
+ this.#attachment = attachment;
578
+ attachment.unsubscribe = core.store.subscribe((run) => {
579
+ if (!this.#isAttached(attachment))
580
+ return;
581
+ this.#publish(run);
582
+ void this.#publishWithConversation(run, attachment);
583
+ void this.#publishLifecycleChanges(run, attachment).catch(() => { });
584
+ notifier.observe(run);
585
+ });
586
+ this.#publish(projection);
587
+ void this.#publishWithConversation(projection, attachment);
588
+ }
589
+ #detach() {
590
+ const attachment = this.#attachment;
591
+ this.#attachment = undefined;
592
+ if (attachment) {
593
+ attachment.active = false;
594
+ attachment.unsubscribe();
595
+ attachment.notifier.dispose();
596
+ }
597
+ this.#lifecycleStates.clear();
598
+ this.#cwd = undefined;
599
+ this.#store = undefined;
600
+ this.#supervisor = undefined;
601
+ this.#cleanup = undefined;
602
+ this.#doctor = undefined;
603
+ this.#artifactDirectory = undefined;
604
+ this.#requestKey = undefined;
605
+ }
606
+ #isAttached(attachment) {
607
+ return attachment.active && this.#attachment === attachment;
608
+ }
566
609
  #publish(run) {
567
610
  this.#publishFleet(projectFleetRecords(run));
568
611
  }
569
- async #publishWithConversation(run) {
612
+ async #publishWithConversation(run, attachment) {
570
613
  const records = await Promise.all(Object.values(run.workers).map(async (worker) => {
571
614
  const attempt = worker.attempts.at(-1);
572
615
  const contract = effectiveWorkerContract(worker);
@@ -597,24 +640,146 @@ export class PiWorkerRuntime {
597
640
  ],
598
641
  };
599
642
  }));
600
- if (this.#requestKey === run.requestKey)
643
+ if (this.#isAttached(attachment) && attachment.core.requestKey === run.requestKey) {
601
644
  this.#publishFleet(records);
645
+ }
602
646
  }
603
647
  #baselineLifecycle(run) {
604
648
  for (const worker of Object.values(run.workers)) {
605
649
  this.#lifecycleStates.set(`${run.requestKey}\0${worker.workerId}`, worker.state);
606
650
  }
607
651
  }
608
- async #publishLifecycleChanges(run) {
652
+ async #publishLifecycleChanges(run, attachment) {
609
653
  for (const worker of Object.values(run.workers)) {
654
+ if (!this.#isAttached(attachment))
655
+ return;
610
656
  const key = `${run.requestKey}\0${worker.workerId}`;
611
657
  if (this.#lifecycleStates.get(key) === worker.state)
612
658
  continue;
613
659
  this.#lifecycleStates.set(key, worker.state);
660
+ if (!this.#isAttached(attachment))
661
+ return;
614
662
  await this.#publishLifecycle(projectWorkerLifecycle(worker.workerId, worker.state)).catch(() => { });
615
663
  }
616
664
  }
617
665
  }
666
+ function processWorkerRuntimeRegistry() {
667
+ const existing = Reflect.get(globalThis, WORKER_RUNTIME_REGISTRY_SYMBOL);
668
+ if (existing === undefined) {
669
+ const created = { schema: 1, entries: new Map() };
670
+ Reflect.set(globalThis, WORKER_RUNTIME_REGISTRY_SYMBOL, created);
671
+ return created;
672
+ }
673
+ try {
674
+ if (!isRecord(existing) || existing["schema"] !== 1 || !(existing["entries"] instanceof Map)) {
675
+ throw new Error("registry envelope is invalid");
676
+ }
677
+ const registry = existing;
678
+ for (const [key, value] of registry.entries) {
679
+ if (typeof key !== "string" || key.length === 0)
680
+ throw new Error("registry key is invalid");
681
+ parseWorkerRuntimeCore(value, key);
682
+ }
683
+ return registry;
684
+ }
685
+ catch (error) {
686
+ throw reloadAdoptionRefused(`process-global registry is corrupt: ${toError(error).message}`);
687
+ }
688
+ }
689
+ function parseWorkerRuntimeCore(value, expectedKey) {
690
+ if (!isRecord(value) || value["schema"] !== 1) {
691
+ throw reloadAdoptionRefused("process-global registry entry is corrupt");
692
+ }
693
+ const core = value;
694
+ if (core.key !== expectedKey
695
+ || !nonEmptyString(core.workspacePath)
696
+ || !nonEmptyString(core.workspaceId)
697
+ || !nonEmptyString(core.namespaceDirectory)
698
+ || !nonEmptyString(core.supervisorGeneration)
699
+ || !nonEmptyString(core.artifactDirectory)
700
+ || !nonEmptyString(core.requestKey)
701
+ || !runtimeProcessIdentity(core.processIdentity)
702
+ || !hasMethods(core.leaseManager, ["inspectSupervisor"])
703
+ || !hasMethods(core.store, ["readRun", "subscribe"])
704
+ || !hasMethods(core.supervisor, ["updateLaunchAuthority", "steerWorker", "cancelWorker"])
705
+ || !hasMethods(core.cleanup, ["plan", "execute"])
706
+ || !hasMethods(core.doctor, ["inspect"])) {
707
+ throw reloadAdoptionRefused("process-global registry entry is corrupt");
708
+ }
709
+ return core;
710
+ }
711
+ async function validateRuntimeAdoption(core, expected) {
712
+ if (core.workspacePath !== expected.workspacePath
713
+ || core.workspaceId !== expected.workspaceId
714
+ || core.namespaceDirectory !== expected.namespaceDirectory
715
+ || core.key !== workerRuntimeRegistryKey(expected.namespaceDirectory, expected.workspaceId)) {
716
+ throw reloadAdoptionRefused("workspace or namespace identity changed");
717
+ }
718
+ const current = await expected.inspector.current();
719
+ if (!sameProcessIdentity(core.processIdentity, current)) {
720
+ throw reloadAdoptionRefused("process-start identity changed");
721
+ }
722
+ const lease = await expected.leaseManager.inspectSupervisor();
723
+ if (lease.kind !== "owned") {
724
+ throw reloadAdoptionRefused(lease.kind === "corrupt" ? `supervisor lease is corrupt: ${lease.diagnostic}` : "supervisor lease is missing");
725
+ }
726
+ if (lease.owner.kind !== "supervisor"
727
+ || lease.owner.workspaceId !== expected.workspaceId
728
+ || lease.owner.generation !== core.supervisorGeneration) {
729
+ throw reloadAdoptionRefused("supervisor generation or workspace does not match the registry");
730
+ }
731
+ const leaseIdentity = processIdentityFromOwner(lease.owner);
732
+ if (!sameProcessIdentity(leaseIdentity, current)
733
+ || !sameProcessIdentity(leaseIdentity, core.processIdentity)) {
734
+ throw reloadAdoptionRefused("supervisor lease process identity does not match the registry");
735
+ }
736
+ }
737
+ function canonicalWorkerWorkspace(workspacePath) {
738
+ if (!isAbsolute(workspacePath))
739
+ throw new TypeError("Worker workspace must be absolute");
740
+ return normalize(resolve(workspacePath));
741
+ }
742
+ function workerRuntimeRegistryKey(namespaceDirectory, workspaceId) {
743
+ return `${normalize(resolve(namespaceDirectory))}\0${workspaceId}`;
744
+ }
745
+ function processIdentityFromOwner(owner) {
746
+ return {
747
+ pid: owner.pid,
748
+ processStartIdentity: owner.processStartIdentity,
749
+ bootIdentity: owner.bootIdentity,
750
+ executable: owner.executable,
751
+ hostname: owner.hostname,
752
+ };
753
+ }
754
+ function sameProcessIdentity(left, right) {
755
+ return left.pid === right.pid
756
+ && left.processStartIdentity === right.processStartIdentity
757
+ && left.bootIdentity === right.bootIdentity
758
+ && normalize(left.executable) === normalize(right.executable)
759
+ && left.hostname === right.hostname;
760
+ }
761
+ function runtimeProcessIdentity(value) {
762
+ return isRecord(value)
763
+ && Number.isSafeInteger(value["pid"])
764
+ && value["pid"] > 0
765
+ && nonEmptyString(value["processStartIdentity"])
766
+ && (value["bootIdentity"] === null || nonEmptyString(value["bootIdentity"]))
767
+ && nonEmptyString(value["executable"])
768
+ && isAbsolute(value["executable"])
769
+ && nonEmptyString(value["hostname"]);
770
+ }
771
+ function hasMethods(value, methods) {
772
+ return isRecord(value) && methods.every((method) => typeof value[method] === "function");
773
+ }
774
+ function nonEmptyString(value) {
775
+ return typeof value === "string" && value.length > 0;
776
+ }
777
+ function isRecord(value) {
778
+ return typeof value === "object" && value !== null && !Array.isArray(value);
779
+ }
780
+ function reloadAdoptionRefused(reason) {
781
+ return new Error(`worker runtime reload adoption refused: ${reason}; fully restart Pi without deleting the supervisor lease`);
782
+ }
618
783
  export function projectFleetRecords(run) {
619
784
  return Object.values(run.workers).map((worker) => {
620
785
  const attempt = worker.attempts.at(-1);