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 +42 -3
- package/dist/extension/worker-runtime.d.ts.map +1 -1
- package/dist/extension/worker-runtime.js +224 -59
- package/dist/extension/worker-runtime.js.map +1 -1
- package/docs/conformance.md +1 -0
- package/docs/contracts/deferred-findings.md +76 -0
- package/docs/contracts/operations.md +65 -0
- package/docs/contracts/transport.md +73 -0
- package/docs/contracts/worker-runtime.md +57 -0
- package/docs/design.md +4 -0
- package/docs/operator-guide.md +1 -1
- package/docs//346/250/241/345/236/213/345/274/272/345/272/246/344/270/216Harness/345/244/215/346/235/202/345/272/246.md +1011 -0
- package/extension/worker-runtime.ts +289 -67
- package/package.json +2 -1
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
|
-
|
|
89
|
-
|
|
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":"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;
|
|
1
|
+
{"version":3,"file":"worker-runtime.d.ts","sourceRoot":"","sources":["../../extension/worker-runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
-
#
|
|
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
|
|
79
|
-
const
|
|
80
|
-
const namespaceDirectory = workflowNamespaceDirectory(defaultPiStateDirectory(),
|
|
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
|
|
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(`${
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
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);
|