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
|
@@ -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
|
|
|
8
8
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -55,7 +55,12 @@ import {
|
|
|
55
55
|
NodePiProcessFactory,
|
|
56
56
|
WorkerProcessSupervisor,
|
|
57
57
|
} from "../workflow/worker-process-supervisor.js";
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
WorkflowLeaseManager,
|
|
60
|
+
type ProcessIdentity,
|
|
61
|
+
type ProcessIdentityInspector,
|
|
62
|
+
type WorkflowLeaseOwnerV1,
|
|
63
|
+
} from "../workflow/workflow-lease.js";
|
|
59
64
|
import type {
|
|
60
65
|
RunProjectionV2,
|
|
61
66
|
ModelSpec,
|
|
@@ -94,6 +99,37 @@ const CAPABILITY_TOOLS: Readonly<Record<string, readonly string[]>> = {
|
|
|
94
99
|
message_worker: ["message_worker"],
|
|
95
100
|
};
|
|
96
101
|
|
|
102
|
+
const WORKER_RUNTIME_REGISTRY_SYMBOL = Symbol.for("pi-amq.worker-runtime-registry.v1");
|
|
103
|
+
|
|
104
|
+
interface WorkerRuntimeCoreV1 {
|
|
105
|
+
readonly schema: 1;
|
|
106
|
+
readonly key: string;
|
|
107
|
+
readonly workspacePath: string;
|
|
108
|
+
readonly workspaceId: string;
|
|
109
|
+
readonly namespaceDirectory: string;
|
|
110
|
+
readonly processIdentity: ProcessIdentity;
|
|
111
|
+
readonly supervisorGeneration: string;
|
|
112
|
+
readonly leaseManager: WorkflowLeaseManager;
|
|
113
|
+
readonly store: WorkflowRunStore;
|
|
114
|
+
readonly supervisor: WorkerProcessSupervisor;
|
|
115
|
+
readonly cleanup: WorkflowRunCleanup;
|
|
116
|
+
readonly doctor: WorkflowDoctor;
|
|
117
|
+
readonly artifactDirectory: string;
|
|
118
|
+
readonly requestKey: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface WorkerRuntimeRegistryV1 {
|
|
122
|
+
readonly schema: 1;
|
|
123
|
+
readonly entries: Map<string, unknown>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface WorkerRuntimeAttachment {
|
|
127
|
+
readonly core: WorkerRuntimeCoreV1;
|
|
128
|
+
readonly notifier: WorkerCompletionNotifier;
|
|
129
|
+
active: boolean;
|
|
130
|
+
unsubscribe: () => void;
|
|
131
|
+
}
|
|
132
|
+
|
|
97
133
|
export class PiWorkerRuntime implements WorkerToolProvider {
|
|
98
134
|
readonly #publishFleet: (records: readonly FleetAgentRecord[]) => void;
|
|
99
135
|
readonly #unlimitedDeclaration: () => unknown;
|
|
@@ -111,15 +147,7 @@ export class PiWorkerRuntime implements WorkerToolProvider {
|
|
|
111
147
|
#requestKey: string | undefined;
|
|
112
148
|
#error: string | undefined;
|
|
113
149
|
#guidance = DRIVER_DOCTRINE;
|
|
114
|
-
|
|
115
|
-
readonly store: WorkflowRunStore;
|
|
116
|
-
readonly supervisor: WorkerProcessSupervisor;
|
|
117
|
-
readonly cleanup: WorkflowRunCleanup;
|
|
118
|
-
readonly doctor: WorkflowDoctor;
|
|
119
|
-
readonly artifactDirectory: string;
|
|
120
|
-
readonly requestKey: string;
|
|
121
|
-
readonly unsubscribe: () => void;
|
|
122
|
-
}>();
|
|
150
|
+
#attachment: WorkerRuntimeAttachment | undefined;
|
|
123
151
|
|
|
124
152
|
constructor(
|
|
125
153
|
publishFleet: (records: readonly FleetAgentRecord[]) => void = () => {},
|
|
@@ -134,36 +162,44 @@ export class PiWorkerRuntime implements WorkerToolProvider {
|
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
async start(ctx: ExtensionContext): Promise<void> {
|
|
165
|
+
this.#detach();
|
|
137
166
|
await this.refreshGuidance(ctx);
|
|
138
|
-
const existing = this.#runtimes.get(ctx.cwd);
|
|
139
|
-
if (existing) {
|
|
140
|
-
this.#cwd = ctx.cwd;
|
|
141
|
-
this.#store = existing.store;
|
|
142
|
-
this.#supervisor = existing.supervisor;
|
|
143
|
-
this.#cleanup = existing.cleanup;
|
|
144
|
-
this.#doctor = existing.doctor;
|
|
145
|
-
this.#artifactDirectory = existing.artifactDirectory;
|
|
146
|
-
this.#requestKey = existing.requestKey;
|
|
147
|
-
existing.supervisor.updateLaunchAuthority(await launchAuthority(ctx));
|
|
148
|
-
this.#publish(await requiredRun(existing.store, existing.requestKey));
|
|
149
|
-
void this.#publishWithConversation(await requiredRun(existing.store, existing.requestKey));
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
167
|
try {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const namespaceDirectory = workflowNamespaceDirectory(defaultPiStateDirectory(),
|
|
168
|
+
const workspacePath = canonicalWorkerWorkspace(ctx.cwd);
|
|
169
|
+
const workspaceId = workflowWorkspaceId(workspacePath);
|
|
170
|
+
const namespaceDirectory = workflowNamespaceDirectory(defaultPiStateDirectory(), workspacePath);
|
|
171
|
+
const key = workerRuntimeRegistryKey(namespaceDirectory, workspaceId);
|
|
172
|
+
const registry = processWorkerRuntimeRegistry();
|
|
156
173
|
const inspector = new SystemProcessIdentityInspector();
|
|
157
174
|
const leaseManager = new WorkflowLeaseManager({
|
|
158
175
|
namespaceDirectory,
|
|
159
|
-
workspaceId
|
|
176
|
+
workspaceId,
|
|
160
177
|
inspector,
|
|
161
178
|
});
|
|
179
|
+
const registered = registry.entries.get(key);
|
|
180
|
+
if (registered !== undefined) {
|
|
181
|
+
const existing = parseWorkerRuntimeCore(registered, key);
|
|
182
|
+
await validateRuntimeAdoption(existing, {
|
|
183
|
+
workspacePath,
|
|
184
|
+
workspaceId,
|
|
185
|
+
namespaceDirectory,
|
|
186
|
+
inspector,
|
|
187
|
+
leaseManager,
|
|
188
|
+
});
|
|
189
|
+
existing.supervisor.updateLaunchAuthority(await launchAuthority(ctx));
|
|
190
|
+
const current = await requiredRun(existing.store, existing.requestKey);
|
|
191
|
+
this.#attach(existing, current);
|
|
192
|
+
this.#error = undefined;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const runBudget = await confirmedRunBudget(ctx, this.#unlimitedDeclaration());
|
|
197
|
+
const authority = await launchAuthority(ctx);
|
|
162
198
|
const acquired = await leaseManager.acquireSupervisor();
|
|
163
199
|
if (acquired.kind !== "acquired") {
|
|
164
200
|
throw new Error(
|
|
165
201
|
acquired.kind === "held"
|
|
166
|
-
? `worker supervisor is held by PID ${acquired.owner.pid}`
|
|
202
|
+
? `worker supervisor is held by PID ${acquired.owner.pid}; fully restart Pi without deleting the supervisor lease`
|
|
167
203
|
: acquired.diagnostic,
|
|
168
204
|
);
|
|
169
205
|
}
|
|
@@ -173,7 +209,7 @@ export class PiWorkerRuntime implements WorkerToolProvider {
|
|
|
173
209
|
supervisorGeneration: acquired.owner.generation,
|
|
174
210
|
});
|
|
175
211
|
const sessionIdentity = ctx.sessionManager.getSessionFile() ?? `process:${process.pid}`;
|
|
176
|
-
const requestKey = `pi-session:${hash(`${
|
|
212
|
+
const requestKey = `pi-session:${hash(`${workspacePath}\0${sessionIdentity}`)}`;
|
|
177
213
|
await store.startRun({
|
|
178
214
|
requestKey,
|
|
179
215
|
source: {
|
|
@@ -212,53 +248,37 @@ export class PiWorkerRuntime implements WorkerToolProvider {
|
|
|
212
248
|
leases: leaseManager,
|
|
213
249
|
inspector,
|
|
214
250
|
});
|
|
215
|
-
const initial = await requiredRun(store, requestKey);
|
|
216
|
-
this.#baselineLifecycle(initial);
|
|
217
|
-
const notifier = new WorkerCompletionNotifier({ publish: this.#publishNotice });
|
|
218
|
-
notifier.baseline(initial);
|
|
219
|
-
const unsubscribeStore = store.subscribe((projection) => {
|
|
220
|
-
this.#publish(projection);
|
|
221
|
-
void this.#publishWithConversation(projection);
|
|
222
|
-
void this.#publishLifecycleChanges(projection).catch(() => {});
|
|
223
|
-
notifier.observe(projection);
|
|
224
|
-
});
|
|
225
|
-
const unsubscribe = (): void => {
|
|
226
|
-
unsubscribeStore();
|
|
227
|
-
notifier.dispose();
|
|
228
|
-
};
|
|
229
251
|
const reconciled = await supervisor.reconcileRun(requestKey);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
const core: WorkerRuntimeCoreV1 = {
|
|
253
|
+
schema: 1,
|
|
254
|
+
key,
|
|
255
|
+
workspacePath,
|
|
256
|
+
workspaceId,
|
|
257
|
+
namespaceDirectory,
|
|
258
|
+
processIdentity: processIdentityFromOwner(acquired.owner),
|
|
259
|
+
supervisorGeneration: acquired.owner.generation,
|
|
260
|
+
leaseManager,
|
|
238
261
|
store,
|
|
239
262
|
supervisor,
|
|
240
263
|
cleanup,
|
|
241
264
|
doctor,
|
|
242
265
|
artifactDirectory,
|
|
243
266
|
requestKey,
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
};
|
|
268
|
+
registry.entries.set(key, core);
|
|
269
|
+
this.#attach(core, reconciled);
|
|
246
270
|
this.#error = undefined;
|
|
247
|
-
this.#publish(reconciled);
|
|
248
271
|
} catch (error) {
|
|
272
|
+
this.#detach();
|
|
249
273
|
this.#error = toError(error).message;
|
|
250
|
-
this.#store = undefined;
|
|
251
|
-
this.#supervisor = undefined;
|
|
252
|
-
this.#cleanup = undefined;
|
|
253
|
-
this.#doctor = undefined;
|
|
254
|
-
this.#artifactDirectory = undefined;
|
|
255
|
-
this.#requestKey = undefined;
|
|
256
274
|
}
|
|
257
275
|
}
|
|
258
276
|
|
|
259
277
|
async shutdown(): Promise<void> {
|
|
260
278
|
// Workers own a parent-liveness descriptor. Session changes do not imply cancellation,
|
|
261
|
-
// so
|
|
279
|
+
// so only this extension instance's advisory subscribers are detached. The process-global
|
|
280
|
+
// core and its supervisor lease remain intact for a replacement extension instance.
|
|
281
|
+
this.#detach();
|
|
262
282
|
}
|
|
263
283
|
|
|
264
284
|
async refreshGuidance(ctx: ExtensionContext): Promise<void> {
|
|
@@ -278,7 +298,7 @@ export class PiWorkerRuntime implements WorkerToolProvider {
|
|
|
278
298
|
this.#guidance = `${DRIVER_DOCTRINE}\n\n${guidance.text}${
|
|
279
299
|
guidance.notices.length > 0 ? `\n${guidance.notices.join("\n")}` : ""
|
|
280
300
|
}`;
|
|
281
|
-
if (this.#cwd === ctx.cwd) {
|
|
301
|
+
if (this.#cwd === canonicalWorkerWorkspace(ctx.cwd)) {
|
|
282
302
|
this.#supervisor?.updateLaunchAuthority(await launchAuthority(ctx));
|
|
283
303
|
}
|
|
284
304
|
} catch (error) {
|
|
@@ -704,11 +724,67 @@ export class PiWorkerRuntime implements WorkerToolProvider {
|
|
|
704
724
|
};
|
|
705
725
|
}
|
|
706
726
|
|
|
727
|
+
#attach(core: WorkerRuntimeCoreV1, projection: RunProjectionV2): void {
|
|
728
|
+
this.#detach();
|
|
729
|
+
this.#cwd = core.workspacePath;
|
|
730
|
+
this.#store = core.store;
|
|
731
|
+
this.#supervisor = core.supervisor;
|
|
732
|
+
this.#cleanup = core.cleanup;
|
|
733
|
+
this.#doctor = core.doctor;
|
|
734
|
+
this.#artifactDirectory = core.artifactDirectory;
|
|
735
|
+
this.#requestKey = core.requestKey;
|
|
736
|
+
this.#lifecycleStates.clear();
|
|
737
|
+
this.#baselineLifecycle(projection);
|
|
738
|
+
const notifier = new WorkerCompletionNotifier({ publish: this.#publishNotice });
|
|
739
|
+
notifier.baseline(projection);
|
|
740
|
+
const attachment: WorkerRuntimeAttachment = {
|
|
741
|
+
core,
|
|
742
|
+
notifier,
|
|
743
|
+
active: true,
|
|
744
|
+
unsubscribe: () => {},
|
|
745
|
+
};
|
|
746
|
+
this.#attachment = attachment;
|
|
747
|
+
attachment.unsubscribe = core.store.subscribe((run) => {
|
|
748
|
+
if (!this.#isAttached(attachment)) return;
|
|
749
|
+
this.#publish(run);
|
|
750
|
+
void this.#publishWithConversation(run, attachment);
|
|
751
|
+
void this.#publishLifecycleChanges(run, attachment).catch(() => {});
|
|
752
|
+
notifier.observe(run);
|
|
753
|
+
});
|
|
754
|
+
this.#publish(projection);
|
|
755
|
+
void this.#publishWithConversation(projection, attachment);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
#detach(): void {
|
|
759
|
+
const attachment = this.#attachment;
|
|
760
|
+
this.#attachment = undefined;
|
|
761
|
+
if (attachment) {
|
|
762
|
+
attachment.active = false;
|
|
763
|
+
attachment.unsubscribe();
|
|
764
|
+
attachment.notifier.dispose();
|
|
765
|
+
}
|
|
766
|
+
this.#lifecycleStates.clear();
|
|
767
|
+
this.#cwd = undefined;
|
|
768
|
+
this.#store = undefined;
|
|
769
|
+
this.#supervisor = undefined;
|
|
770
|
+
this.#cleanup = undefined;
|
|
771
|
+
this.#doctor = undefined;
|
|
772
|
+
this.#artifactDirectory = undefined;
|
|
773
|
+
this.#requestKey = undefined;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#isAttached(attachment: WorkerRuntimeAttachment): boolean {
|
|
777
|
+
return attachment.active && this.#attachment === attachment;
|
|
778
|
+
}
|
|
779
|
+
|
|
707
780
|
#publish(run: RunProjectionV2): void {
|
|
708
781
|
this.#publishFleet(projectFleetRecords(run));
|
|
709
782
|
}
|
|
710
783
|
|
|
711
|
-
async #publishWithConversation(
|
|
784
|
+
async #publishWithConversation(
|
|
785
|
+
run: RunProjectionV2,
|
|
786
|
+
attachment: WorkerRuntimeAttachment,
|
|
787
|
+
): Promise<void> {
|
|
712
788
|
const records = await Promise.all(Object.values(run.workers).map(async (worker): Promise<FleetAgentRecord> => {
|
|
713
789
|
const attempt = worker.attempts.at(-1);
|
|
714
790
|
const contract = effectiveWorkerContract(worker);
|
|
@@ -739,7 +815,9 @@ export class PiWorkerRuntime implements WorkerToolProvider {
|
|
|
739
815
|
],
|
|
740
816
|
};
|
|
741
817
|
}));
|
|
742
|
-
if (this.#requestKey === run.requestKey)
|
|
818
|
+
if (this.#isAttached(attachment) && attachment.core.requestKey === run.requestKey) {
|
|
819
|
+
this.#publishFleet(records);
|
|
820
|
+
}
|
|
743
821
|
}
|
|
744
822
|
|
|
745
823
|
#baselineLifecycle(run: RunProjectionV2): void {
|
|
@@ -748,16 +826,160 @@ export class PiWorkerRuntime implements WorkerToolProvider {
|
|
|
748
826
|
}
|
|
749
827
|
}
|
|
750
828
|
|
|
751
|
-
async #publishLifecycleChanges(
|
|
829
|
+
async #publishLifecycleChanges(
|
|
830
|
+
run: RunProjectionV2,
|
|
831
|
+
attachment: WorkerRuntimeAttachment,
|
|
832
|
+
): Promise<void> {
|
|
752
833
|
for (const worker of Object.values(run.workers)) {
|
|
834
|
+
if (!this.#isAttached(attachment)) return;
|
|
753
835
|
const key = `${run.requestKey}\0${worker.workerId}`;
|
|
754
836
|
if (this.#lifecycleStates.get(key) === worker.state) continue;
|
|
755
837
|
this.#lifecycleStates.set(key, worker.state);
|
|
838
|
+
if (!this.#isAttached(attachment)) return;
|
|
756
839
|
await this.#publishLifecycle(projectWorkerLifecycle(worker.workerId, worker.state)).catch(() => {});
|
|
757
840
|
}
|
|
758
841
|
}
|
|
759
842
|
}
|
|
760
843
|
|
|
844
|
+
function processWorkerRuntimeRegistry(): WorkerRuntimeRegistryV1 {
|
|
845
|
+
const existing = Reflect.get(globalThis, WORKER_RUNTIME_REGISTRY_SYMBOL) as unknown;
|
|
846
|
+
if (existing === undefined) {
|
|
847
|
+
const created: WorkerRuntimeRegistryV1 = { schema: 1, entries: new Map() };
|
|
848
|
+
Reflect.set(globalThis, WORKER_RUNTIME_REGISTRY_SYMBOL, created);
|
|
849
|
+
return created;
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
if (!isRecord(existing) || existing["schema"] !== 1 || !(existing["entries"] instanceof Map)) {
|
|
853
|
+
throw new Error("registry envelope is invalid");
|
|
854
|
+
}
|
|
855
|
+
const registry = existing as unknown as WorkerRuntimeRegistryV1;
|
|
856
|
+
for (const [key, value] of registry.entries) {
|
|
857
|
+
if (typeof key !== "string" || key.length === 0) throw new Error("registry key is invalid");
|
|
858
|
+
parseWorkerRuntimeCore(value, key);
|
|
859
|
+
}
|
|
860
|
+
return registry;
|
|
861
|
+
} catch (error) {
|
|
862
|
+
throw reloadAdoptionRefused(`process-global registry is corrupt: ${toError(error).message}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function parseWorkerRuntimeCore(value: unknown, expectedKey: string): WorkerRuntimeCoreV1 {
|
|
867
|
+
if (!isRecord(value) || value["schema"] !== 1) {
|
|
868
|
+
throw reloadAdoptionRefused("process-global registry entry is corrupt");
|
|
869
|
+
}
|
|
870
|
+
const core = value as unknown as WorkerRuntimeCoreV1;
|
|
871
|
+
if (core.key !== expectedKey
|
|
872
|
+
|| !nonEmptyString(core.workspacePath)
|
|
873
|
+
|| !nonEmptyString(core.workspaceId)
|
|
874
|
+
|| !nonEmptyString(core.namespaceDirectory)
|
|
875
|
+
|| !nonEmptyString(core.supervisorGeneration)
|
|
876
|
+
|| !nonEmptyString(core.artifactDirectory)
|
|
877
|
+
|| !nonEmptyString(core.requestKey)
|
|
878
|
+
|| !runtimeProcessIdentity(core.processIdentity)
|
|
879
|
+
|| !hasMethods(core.leaseManager, ["inspectSupervisor"])
|
|
880
|
+
|| !hasMethods(core.store, ["readRun", "subscribe"])
|
|
881
|
+
|| !hasMethods(core.supervisor, ["updateLaunchAuthority", "steerWorker", "cancelWorker"])
|
|
882
|
+
|| !hasMethods(core.cleanup, ["plan", "execute"])
|
|
883
|
+
|| !hasMethods(core.doctor, ["inspect"])) {
|
|
884
|
+
throw reloadAdoptionRefused("process-global registry entry is corrupt");
|
|
885
|
+
}
|
|
886
|
+
return core;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function validateRuntimeAdoption(
|
|
890
|
+
core: WorkerRuntimeCoreV1,
|
|
891
|
+
expected: {
|
|
892
|
+
readonly workspacePath: string;
|
|
893
|
+
readonly workspaceId: string;
|
|
894
|
+
readonly namespaceDirectory: string;
|
|
895
|
+
readonly inspector: ProcessIdentityInspector;
|
|
896
|
+
readonly leaseManager: WorkflowLeaseManager;
|
|
897
|
+
},
|
|
898
|
+
): Promise<void> {
|
|
899
|
+
if (core.workspacePath !== expected.workspacePath
|
|
900
|
+
|| core.workspaceId !== expected.workspaceId
|
|
901
|
+
|| core.namespaceDirectory !== expected.namespaceDirectory
|
|
902
|
+
|| core.key !== workerRuntimeRegistryKey(expected.namespaceDirectory, expected.workspaceId)) {
|
|
903
|
+
throw reloadAdoptionRefused("workspace or namespace identity changed");
|
|
904
|
+
}
|
|
905
|
+
const current = await expected.inspector.current();
|
|
906
|
+
if (!sameProcessIdentity(core.processIdentity, current)) {
|
|
907
|
+
throw reloadAdoptionRefused("process-start identity changed");
|
|
908
|
+
}
|
|
909
|
+
const lease = await expected.leaseManager.inspectSupervisor();
|
|
910
|
+
if (lease.kind !== "owned") {
|
|
911
|
+
throw reloadAdoptionRefused(
|
|
912
|
+
lease.kind === "corrupt" ? `supervisor lease is corrupt: ${lease.diagnostic}` : "supervisor lease is missing",
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
if (lease.owner.kind !== "supervisor"
|
|
916
|
+
|| lease.owner.workspaceId !== expected.workspaceId
|
|
917
|
+
|| lease.owner.generation !== core.supervisorGeneration) {
|
|
918
|
+
throw reloadAdoptionRefused("supervisor generation or workspace does not match the registry");
|
|
919
|
+
}
|
|
920
|
+
const leaseIdentity = processIdentityFromOwner(lease.owner);
|
|
921
|
+
if (!sameProcessIdentity(leaseIdentity, current)
|
|
922
|
+
|| !sameProcessIdentity(leaseIdentity, core.processIdentity)) {
|
|
923
|
+
throw reloadAdoptionRefused("supervisor lease process identity does not match the registry");
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function canonicalWorkerWorkspace(workspacePath: string): string {
|
|
928
|
+
if (!isAbsolute(workspacePath)) throw new TypeError("Worker workspace must be absolute");
|
|
929
|
+
return normalize(resolve(workspacePath));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function workerRuntimeRegistryKey(namespaceDirectory: string, workspaceId: string): string {
|
|
933
|
+
return `${normalize(resolve(namespaceDirectory))}\0${workspaceId}`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function processIdentityFromOwner(owner: WorkflowLeaseOwnerV1): ProcessIdentity {
|
|
937
|
+
return {
|
|
938
|
+
pid: owner.pid,
|
|
939
|
+
processStartIdentity: owner.processStartIdentity,
|
|
940
|
+
bootIdentity: owner.bootIdentity,
|
|
941
|
+
executable: owner.executable,
|
|
942
|
+
hostname: owner.hostname,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function sameProcessIdentity(left: ProcessIdentity, right: ProcessIdentity): boolean {
|
|
947
|
+
return left.pid === right.pid
|
|
948
|
+
&& left.processStartIdentity === right.processStartIdentity
|
|
949
|
+
&& left.bootIdentity === right.bootIdentity
|
|
950
|
+
&& normalize(left.executable) === normalize(right.executable)
|
|
951
|
+
&& left.hostname === right.hostname;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function runtimeProcessIdentity(value: unknown): value is ProcessIdentity {
|
|
955
|
+
return isRecord(value)
|
|
956
|
+
&& Number.isSafeInteger(value["pid"])
|
|
957
|
+
&& (value["pid"] as number) > 0
|
|
958
|
+
&& nonEmptyString(value["processStartIdentity"])
|
|
959
|
+
&& (value["bootIdentity"] === null || nonEmptyString(value["bootIdentity"]))
|
|
960
|
+
&& nonEmptyString(value["executable"])
|
|
961
|
+
&& isAbsolute(value["executable"] as string)
|
|
962
|
+
&& nonEmptyString(value["hostname"]);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function hasMethods(value: unknown, methods: readonly string[]): boolean {
|
|
966
|
+
return isRecord(value) && methods.every((method) => typeof value[method] === "function");
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function nonEmptyString(value: unknown): value is string {
|
|
970
|
+
return typeof value === "string" && value.length > 0;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
974
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function reloadAdoptionRefused(reason: string): Error {
|
|
978
|
+
return new Error(
|
|
979
|
+
`worker runtime reload adoption refused: ${reason}; fully restart Pi without deleting the supervisor lease`,
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
761
983
|
export function projectFleetRecords(run: RunProjectionV2): readonly FleetAgentRecord[] {
|
|
762
984
|
return Object.values(run.workers).map((worker): FleetAgentRecord => {
|
|
763
985
|
const attempt = worker.attempts.at(-1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-amq",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A daemonless AMQ bridge and governed worker runtime for pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"typecheck": "tsc --project tsconfig.json",
|
|
40
40
|
"test:compile": "tsc --project tsconfig.test.json",
|
|
41
41
|
"test": "npm run test:compile && node --test .test-dist/test/*.test.js",
|
|
42
|
+
"test:package": "npm run test:compile && node --test .test-dist/test/package/*.test.js",
|
|
42
43
|
"prepack": "npm run build"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|