@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53
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/dist/bridge/bridge.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +8 -0
- package/dist/bridge/bridge.service.js +184 -12
- package/dist/bridge/bridge.types.d.ts +37 -0
- package/dist/cli/cli.help.js +1 -1
- package/dist/cli/cli.service.js +1 -1
- package/dist/cli/local-config.js +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
- package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
- package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
- package/dist/runtime/attempt-process-supervisor.js +75 -15
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +16 -0
- package/dist/runtime/host-listener-inspector.js +143 -4
- package/dist/runtime/host-runtime-contract.d.ts +9 -1
- package/dist/runtime/host-runtime-contract.js +411 -6
- package/dist/runtime/host-runtime-lease.d.ts +12 -1
- package/dist/runtime/host-runtime-lease.js +271 -50
- package/dist/runtime/host-runtime-promotion.d.ts +2 -0
- package/dist/runtime/host-runtime-promotion.js +35 -4
- package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
- package/dist/runtime/node-host-runtime-adapter.js +71 -8
- package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
- package/dist/sandbox/preflight.d.ts +9 -0
- package/dist/sandbox/preflight.js +39 -4
- package/dist/sandbox/workspace.service.js +7 -2
- package/dist/session/managed-attempt-tools.d.ts +17 -0
- package/dist/session/managed-attempt-tools.js +712 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +11 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +45 -9
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +21 -6
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +183 -8
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +144 -18
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.process.js +176 -18
- package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
- package/dist/session/provider-cli/provider-cli.service.js +390 -83
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +30 -2
- package/dist/session/session.base.d.ts +2 -1
- package/dist/session/session.service.d.ts +2 -1
- package/dist/session/session.service.js +303 -30
- package/dist/session/session.types.d.ts +38 -0
- package/dist/session/session.types.js +17 -0
- package/dist/shared/process/host-process-supervisor.d.ts +18 -1
- package/dist/shared/process/host-process-supervisor.js +94 -7
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +12 -1
- package/package.json +1 -1
- package/runtime/windows-job-supervisor.ps1 +89 -20
|
@@ -11,6 +11,7 @@ import { HostListenerInspector } from "./host-listener-inspector.js";
|
|
|
11
11
|
import { PlaywrightHostRuntimeAdapter } from "./playwright-host-runtime-adapter.js";
|
|
12
12
|
import { HostParallelPromotionStore, configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible, } from "./host-runtime-promotion.js";
|
|
13
13
|
import { ExactHostProcessRecovery, } from "../shared/process/host-process-recovery.js";
|
|
14
|
+
import { resetGeneratedOutputs } from "./generated-output-reset.js";
|
|
14
15
|
export class HostRuntimeLeaseManager {
|
|
15
16
|
stateRoot;
|
|
16
17
|
processSupervisors;
|
|
@@ -27,6 +28,7 @@ export class HostRuntimeLeaseManager {
|
|
|
27
28
|
parallelProjectOwners = new Map();
|
|
28
29
|
parallelProjectWaiters = [];
|
|
29
30
|
degraded = new Map();
|
|
31
|
+
knownPromotions = new Map();
|
|
30
32
|
portBroker;
|
|
31
33
|
nodeAdapter;
|
|
32
34
|
constructor(stateRoot, portBroker = new HostPortBroker(), nodeAdapter, processSupervisors = new AttemptProcessSupervisorRegistry(), processRunner = new StreamingProviderProcessRunner(), listenerInspector = new HostListenerInspector(), promotionStore = new HostParallelPromotionStore(stateRoot), playwrightAdapter = new PlaywrightHostRuntimeAdapter(), processRecovery = new ExactHostProcessRecovery()) {
|
|
@@ -41,6 +43,7 @@ export class HostRuntimeLeaseManager {
|
|
|
41
43
|
this.nodeAdapter = nodeAdapter ?? new NodeHostRuntimeAdapter(portBroker);
|
|
42
44
|
}
|
|
43
45
|
async reap(options) {
|
|
46
|
+
await this.refreshKnownPromotions();
|
|
44
47
|
const report = {
|
|
45
48
|
examined: 0,
|
|
46
49
|
reaped: 0,
|
|
@@ -50,8 +53,25 @@ export class HostRuntimeLeaseManager {
|
|
|
50
53
|
};
|
|
51
54
|
for (const ledger of await this.readLedgers()) {
|
|
52
55
|
const lease = ledger.runtimeLease;
|
|
53
|
-
if (lease.runnerId !== options.runnerId
|
|
56
|
+
if (lease.runnerId !== options.runnerId)
|
|
54
57
|
continue;
|
|
58
|
+
const active = this.active.get(lease.dispatchAttemptId);
|
|
59
|
+
if (active) {
|
|
60
|
+
if (active.lease.state !== "degraded")
|
|
61
|
+
continue;
|
|
62
|
+
report.examined += 1;
|
|
63
|
+
try {
|
|
64
|
+
const released = await this.release(lease.dispatchAttemptId, lease.fencingToken);
|
|
65
|
+
if (released?.state === "released")
|
|
66
|
+
report.reaped += 1;
|
|
67
|
+
else
|
|
68
|
+
report.degraded += 1;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
report.degraded += 1;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
55
75
|
const staleInstance = lease.runnerInstanceId !== options.runnerInstanceId;
|
|
56
76
|
const expired = Date.parse(lease.expiresAt) <= (options.nowMs ?? Date.now());
|
|
57
77
|
if (!staleInstance && !expired && lease.state !== "released" && lease.state !== "degraded")
|
|
@@ -91,6 +111,13 @@ export class HostRuntimeLeaseManager {
|
|
|
91
111
|
}
|
|
92
112
|
snapshot() {
|
|
93
113
|
const projects = new Map();
|
|
114
|
+
for (const promotion of this.knownPromotions.values()) {
|
|
115
|
+
const project = capacityProject(projects, promotion.repoId);
|
|
116
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_parallel_safe");
|
|
117
|
+
project.runtimeContractFingerprint = promotion.runtimeContractFingerprint;
|
|
118
|
+
project.runtimeContractSource = promotion.runtimeContractSource ?? null;
|
|
119
|
+
project.promotedAt = promotion.observedAt;
|
|
120
|
+
}
|
|
94
121
|
for (const [dispatchAttemptId, admission] of this.admissions) {
|
|
95
122
|
const active = this.active.get(dispatchAttemptId);
|
|
96
123
|
const project = capacityProject(projects, admission.repoId);
|
|
@@ -101,17 +128,21 @@ export class HostRuntimeLeaseManager {
|
|
|
101
128
|
}
|
|
102
129
|
else {
|
|
103
130
|
project.activeAttempts += 1;
|
|
104
|
-
const effectiveConcurrency = effectiveHostConcurrency(active.lease, active.runtimeContract);
|
|
131
|
+
const effectiveConcurrency = effectiveHostConcurrency(active.lease, active.runtimeContract, active.executionClass);
|
|
105
132
|
project.safetyClass = conservativeSafetyClass(project.safetyClass, effectiveConcurrency === "host_parallel_safe"
|
|
106
133
|
? effectiveConcurrency
|
|
107
|
-
: admission.
|
|
108
|
-
? "
|
|
109
|
-
:
|
|
134
|
+
: admission.executionClass === "analysis_only"
|
|
135
|
+
? "host_parallel_safe"
|
|
136
|
+
: admission.parallelProjectKey && admission.provisionalPromotionFingerprint
|
|
137
|
+
? "host_parallel_candidate"
|
|
138
|
+
: "host_serial_only");
|
|
110
139
|
}
|
|
111
140
|
}
|
|
112
141
|
else {
|
|
113
142
|
project.reservedAttempts += 1;
|
|
114
|
-
project.safetyClass = conservativeSafetyClass(project.safetyClass, admission.
|
|
143
|
+
project.safetyClass = conservativeSafetyClass(project.safetyClass, admission.executionClass === "analysis_only"
|
|
144
|
+
? "host_parallel_safe"
|
|
145
|
+
: admission.provisionalPromotionFingerprint ? "host_parallel_candidate" : "host_serial_only");
|
|
115
146
|
}
|
|
116
147
|
}
|
|
117
148
|
for (const [dispatchAttemptId, degraded] of this.degraded) {
|
|
@@ -179,37 +210,49 @@ export class HostRuntimeLeaseManager {
|
|
|
179
210
|
}
|
|
180
211
|
return existing.keys;
|
|
181
212
|
}
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
213
|
+
const executionClass = runtimeExecutionClass(dispatch);
|
|
214
|
+
const promotion = executionClass === "managed_runtime"
|
|
215
|
+
? await this.promotionStore.load(dispatch.repo.id)
|
|
216
|
+
: null;
|
|
217
|
+
if (promotion)
|
|
218
|
+
this.knownPromotions.set(promotion.repoId, promotion);
|
|
219
|
+
const projectKey = executionClass === "analysis_only"
|
|
220
|
+
? projectHostAnalysisKey(dispatch.repo.id)
|
|
221
|
+
: projectHostRuntimeSingletonKey(dispatch.repo.id);
|
|
222
|
+
let keys = promotion || executionClass === "analysis_only" ? [] : [projectKey];
|
|
185
223
|
const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
|
|
186
224
|
? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
|
|
187
225
|
: dispatch.lease.expiresAt;
|
|
188
226
|
try {
|
|
189
|
-
if (promotion) {
|
|
227
|
+
if (promotion || executionClass === "analysis_only") {
|
|
190
228
|
await this.reserveParallelProject(projectKey, owner, expiresAt, signal);
|
|
191
229
|
}
|
|
192
230
|
else {
|
|
193
|
-
await this.reserveSingletonKeys(keys, owner, expiresAt, signal);
|
|
231
|
+
keys = await this.reserveSingletonKeys(keys, owner, expiresAt, signal);
|
|
194
232
|
}
|
|
195
233
|
}
|
|
196
234
|
catch (error) {
|
|
197
235
|
this.clearPendingRenewal(owner);
|
|
198
236
|
throw error;
|
|
199
237
|
}
|
|
238
|
+
const admittedPromotion = promotion
|
|
239
|
+
?? (executionClass === "managed_runtime" && keys.length === 0
|
|
240
|
+
? this.knownPromotions.get(dispatch.repo.id) ?? null
|
|
241
|
+
: null);
|
|
200
242
|
this.admissions.set(sandbox.dispatchAttemptId, {
|
|
201
243
|
repoId: dispatch.repo.id,
|
|
202
244
|
fencingToken: owner.fencingToken,
|
|
203
|
-
|
|
245
|
+
executionClass,
|
|
246
|
+
provisionalPromotionFingerprint: admittedPromotion?.runtimeContractFingerprint ?? null,
|
|
204
247
|
keys,
|
|
205
|
-
parallelProjectKey:
|
|
248
|
+
parallelProjectKey: admittedPromotion || executionClass === "analysis_only" ? projectKey : null,
|
|
206
249
|
});
|
|
207
250
|
return keys;
|
|
208
251
|
}
|
|
209
252
|
async reserveSingletonKeys(keys, owner, expiresAt, signal) {
|
|
210
253
|
const owned = keys.every(key => sameSingletonOwner(this.singletonOwners.get(key), owner));
|
|
211
254
|
if (owned)
|
|
212
|
-
return;
|
|
255
|
+
return keys;
|
|
213
256
|
const staleFence = [...this.singletonOwners.values()].find(existing => existing.dispatchAttemptId === owner.dispatchAttemptId
|
|
214
257
|
&& existing.fencingToken !== owner.fencingToken);
|
|
215
258
|
if (staleFence) {
|
|
@@ -220,13 +263,13 @@ export class HostRuntimeLeaseManager {
|
|
|
220
263
|
throwIfAdmissionExpired(expiresAt);
|
|
221
264
|
if (this.singletonKeysAvailable(keys)) {
|
|
222
265
|
this.assignSingletonKeys(keys, owner);
|
|
223
|
-
return;
|
|
266
|
+
return keys;
|
|
224
267
|
}
|
|
225
|
-
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
226
269
|
const waiter = {
|
|
227
270
|
owner,
|
|
228
271
|
keys,
|
|
229
|
-
resolve
|
|
272
|
+
resolve,
|
|
230
273
|
reject,
|
|
231
274
|
signal,
|
|
232
275
|
expiresAt,
|
|
@@ -322,8 +365,9 @@ export class HostRuntimeLeaseManager {
|
|
|
322
365
|
}
|
|
323
366
|
return {
|
|
324
367
|
lease: existing.lease,
|
|
325
|
-
environment: hostRuntimeEnvironment(dispatch.session.id, existing.lease, existing.runtimeContract),
|
|
368
|
+
environment: hostRuntimeEnvironment(dispatch.session.id, existing.lease, existing.runtimeContract, existing.executionClass),
|
|
326
369
|
runtimeContract: existing.runtimeContract,
|
|
370
|
+
executionClass: existing.executionClass,
|
|
327
371
|
};
|
|
328
372
|
}
|
|
329
373
|
const admission = this.admissions.get(sandbox.dispatchAttemptId);
|
|
@@ -352,8 +396,8 @@ export class HostRuntimeLeaseManager {
|
|
|
352
396
|
runtimeContract = await loadHostRuntimeContract(normalizedWorktreePath);
|
|
353
397
|
}
|
|
354
398
|
catch (error) {
|
|
355
|
-
if (admission.provisionalPromotionFingerprint) {
|
|
356
|
-
await this.
|
|
399
|
+
if (admission.executionClass === "managed_runtime" && admission.provisionalPromotionFingerprint) {
|
|
400
|
+
await this.invalidatePromotion(dispatch.repo.id).catch(() => undefined);
|
|
357
401
|
}
|
|
358
402
|
throw error;
|
|
359
403
|
}
|
|
@@ -361,11 +405,13 @@ export class HostRuntimeLeaseManager {
|
|
|
361
405
|
throw new Error("host_runtime_disabled: project runtime configuration requires Docker execution.");
|
|
362
406
|
}
|
|
363
407
|
const runtimeContractFingerprint = hostRuntimeContractFingerprint(runtimeContract);
|
|
364
|
-
const promotion =
|
|
408
|
+
const promotion = admission.executionClass === "managed_runtime"
|
|
409
|
+
? await this.promotionStore.load(dispatch.repo.id)
|
|
410
|
+
: null;
|
|
365
411
|
const promotionMatches = hostRuntimeParallelEligible(runtimeContract)
|
|
366
412
|
&& promotion?.runtimeContractFingerprint === runtimeContractFingerprint;
|
|
367
|
-
if (admission.keys.length === 0 && !promotionMatches) {
|
|
368
|
-
await this.
|
|
413
|
+
if (admission.executionClass === "managed_runtime" && admission.keys.length === 0 && !promotionMatches) {
|
|
414
|
+
await this.invalidatePromotion(dispatch.repo.id);
|
|
369
415
|
const projectKeys = hostRuntimeSingletonKeys(dispatch);
|
|
370
416
|
if (admission.parallelProjectKey) {
|
|
371
417
|
this.releaseParallelProjectNoDrain(admission.parallelProjectKey, owner);
|
|
@@ -374,8 +420,33 @@ export class HostRuntimeLeaseManager {
|
|
|
374
420
|
const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
|
|
375
421
|
? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
|
|
376
422
|
: dispatch.lease.expiresAt;
|
|
377
|
-
await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal);
|
|
378
|
-
admission.keys
|
|
423
|
+
admission.keys = await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal);
|
|
424
|
+
while (admission.keys.length === 0) {
|
|
425
|
+
const promoted = this.knownPromotions.get(dispatch.repo.id);
|
|
426
|
+
if (!promoted) {
|
|
427
|
+
throw new Error("host_runtime_parallel_promotion_missing: shared admission requires matching promotion evidence.");
|
|
428
|
+
}
|
|
429
|
+
if (promoted.runtimeContractFingerprint !== runtimeContractFingerprint) {
|
|
430
|
+
this.releaseParallelProjectNoDrain(projectKeys[0], owner);
|
|
431
|
+
await this.invalidatePromotion(dispatch.repo.id);
|
|
432
|
+
admission.keys = await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
admission.parallelProjectKey = projectKeys[0] ?? null;
|
|
436
|
+
admission.provisionalPromotionFingerprint = promoted.runtimeContractFingerprint;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const declaredSingletonKeys = runtimeContract.contract.runtime.singletonResources
|
|
440
|
+
.map(hostRuntimeDeclaredSingletonKey);
|
|
441
|
+
const missingSingletonKeys = declaredSingletonKeys
|
|
442
|
+
.filter(key => !admission.keys.includes(key));
|
|
443
|
+
if (missingSingletonKeys.length > 0) {
|
|
444
|
+
const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
|
|
445
|
+
? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
|
|
446
|
+
: dispatch.lease.expiresAt;
|
|
447
|
+
const reservedSingletonKeys = await this.reserveSingletonKeys(missingSingletonKeys, owner, expiresAt, signal);
|
|
448
|
+
admission.keys = [...admission.keys, ...reservedSingletonKeys]
|
|
449
|
+
.sort((left, right) => left.localeCompare(right));
|
|
379
450
|
}
|
|
380
451
|
const singletonKeys = admission.keys;
|
|
381
452
|
if (!singletonKeys.every(key => sameSingletonOwner(this.singletonOwners.get(key), owner))) {
|
|
@@ -391,6 +462,7 @@ export class HostRuntimeLeaseManager {
|
|
|
391
462
|
ensurePrivateDirectory(browserProfilePath),
|
|
392
463
|
ensurePrivateDirectory(testOutputPath),
|
|
393
464
|
]);
|
|
465
|
+
await resetGeneratedOutputs(normalizedWorktreePath, testOutputPath);
|
|
394
466
|
let lease = createRuntimeIsolationLease({
|
|
395
467
|
leaseId: sandbox.executionLeaseId,
|
|
396
468
|
dispatchAttemptId: sandbox.dispatchAttemptId,
|
|
@@ -440,6 +512,7 @@ export class HostRuntimeLeaseManager {
|
|
|
440
512
|
attemptRoot,
|
|
441
513
|
runtimeContract,
|
|
442
514
|
runtimeContractFingerprint,
|
|
515
|
+
executionClass: admission.executionClass,
|
|
443
516
|
parallelPromotionEligible: hostRuntimeParallelEligible(runtimeContract),
|
|
444
517
|
processSupervisor,
|
|
445
518
|
adapterCleanups: [],
|
|
@@ -449,8 +522,9 @@ export class HostRuntimeLeaseManager {
|
|
|
449
522
|
this.pendingRenewals.delete(sandbox.dispatchAttemptId);
|
|
450
523
|
return {
|
|
451
524
|
lease,
|
|
452
|
-
environment: hostRuntimeEnvironment(dispatch.session.id, lease, runtimeContract),
|
|
525
|
+
environment: hostRuntimeEnvironment(dispatch.session.id, lease, runtimeContract, admission.executionClass),
|
|
453
526
|
runtimeContract,
|
|
527
|
+
executionClass: admission.executionClass,
|
|
454
528
|
};
|
|
455
529
|
}
|
|
456
530
|
catch (error) {
|
|
@@ -499,34 +573,49 @@ export class HostRuntimeLeaseManager {
|
|
|
499
573
|
if (!active || active.lease.fencingToken !== fencingToken || active.lease.state !== "active" || active.releasing) {
|
|
500
574
|
throw new Error("host_runtime_project_unavailable: an active matching host runtime lease is required.");
|
|
501
575
|
}
|
|
502
|
-
|
|
503
|
-
|
|
576
|
+
if (active.executionClass === "analysis_only") {
|
|
577
|
+
return {
|
|
578
|
+
lease: active.lease,
|
|
579
|
+
environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
|
|
580
|
+
runtimeContract: active.runtimeContract,
|
|
581
|
+
executionClass: active.executionClass,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const configuredPorts = active.runtimeContract.contract.runtime.ports.filter((port) => port.launch !== null);
|
|
585
|
+
if (configuredPorts.length === 0) {
|
|
504
586
|
return {
|
|
505
587
|
lease: active.lease,
|
|
506
|
-
environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract),
|
|
588
|
+
environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
|
|
507
589
|
runtimeContract: active.runtimeContract,
|
|
590
|
+
executionClass: active.executionClass,
|
|
508
591
|
};
|
|
509
592
|
}
|
|
510
|
-
const workingDirectory = await containedWorkingDirectory(active.lease.worktreePath, configuredPort.launch.workingDirectory);
|
|
511
593
|
const owner = hostPortOwner(active.lease);
|
|
512
594
|
const logDirectory = active.lease.resources.paths.find(entry => entry.role === "logs")?.path;
|
|
513
595
|
if (!logDirectory)
|
|
514
596
|
throw new Error("host_runtime_project_invalid: attempt log directory is unavailable.");
|
|
515
597
|
const launcher = new AttemptNodeRuntimeLauncher(active.processSupervisor, owner, logDirectory, this.listenerInspector);
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
598
|
+
for (const configuredPort of configuredPorts) {
|
|
599
|
+
const workingDirectory = await containedWorkingDirectory(active.lease.worktreePath, configuredPort.launch.workingDirectory);
|
|
600
|
+
await this.launchNodeRuntime(dispatchAttemptId, fencingToken, configuredPort.name, {
|
|
601
|
+
command: configuredPort.launch.command,
|
|
602
|
+
args: configuredPort.launch.args,
|
|
603
|
+
cwd: workingDirectory,
|
|
604
|
+
environment: {
|
|
605
|
+
...configuredPort.launch.environment,
|
|
606
|
+
...hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass),
|
|
607
|
+
},
|
|
608
|
+
}, launcher, signal);
|
|
609
|
+
}
|
|
610
|
+
const promotedLease = await this.promoteVerifiedRuntime(active, active.lease);
|
|
611
|
+
const runtimeEnvironment = hostRuntimeEnvironment(active.sessionId, promotedLease, active.runtimeContract, active.executionClass);
|
|
524
612
|
const playwright = active.runtimeContract.contract.runtime.playwright;
|
|
525
613
|
if (!playwright) {
|
|
526
614
|
return {
|
|
527
615
|
lease: promotedLease,
|
|
528
616
|
environment: runtimeEnvironment,
|
|
529
617
|
runtimeContract: active.runtimeContract,
|
|
618
|
+
executionClass: active.executionClass,
|
|
530
619
|
};
|
|
531
620
|
}
|
|
532
621
|
const playwrightPort = promotedLease.resources.ports.find(port => port.name === playwright.baseUrlPort);
|
|
@@ -553,8 +642,15 @@ export class HostRuntimeLeaseManager {
|
|
|
553
642
|
lease: promotedLease,
|
|
554
643
|
environment: { ...runtimeEnvironment, ...preparedPlaywright.environment },
|
|
555
644
|
runtimeContract: active.runtimeContract,
|
|
645
|
+
executionClass: active.executionClass,
|
|
556
646
|
};
|
|
557
647
|
}
|
|
648
|
+
providerProcessRunner(dispatchAttemptId, fencingToken) {
|
|
649
|
+
const active = this.active.get(dispatchAttemptId);
|
|
650
|
+
if (!active || active.lease.fencingToken !== fencingToken || active.releasing)
|
|
651
|
+
return null;
|
|
652
|
+
return new AttemptSupervisedProviderProcessRunner(active.processSupervisor, hostPortOwner(active.lease), this.listenerInspector, active.executionClass === "analysis_only");
|
|
653
|
+
}
|
|
558
654
|
async promoteVerifiedRuntime(active, runtimeLease) {
|
|
559
655
|
if (!active.parallelPromotionEligible)
|
|
560
656
|
return runtimeLease;
|
|
@@ -573,10 +669,11 @@ export class HostRuntimeLeaseManager {
|
|
|
573
669
|
active.lease = promotedLease;
|
|
574
670
|
try {
|
|
575
671
|
const promotion = await this.promotionStore.observe(active.repoId, active.runtimeContract, runtimeLease.dispatchAttemptId, verifiedPortNames);
|
|
672
|
+
this.knownPromotions.set(promotion.repoId, promotion);
|
|
576
673
|
this.convertSingletonToParallelProject(projectKey, {
|
|
577
674
|
dispatchAttemptId: runtimeLease.dispatchAttemptId,
|
|
578
675
|
fencingToken: runtimeLease.fencingToken,
|
|
579
|
-
});
|
|
676
|
+
}, promotion);
|
|
580
677
|
const admission = this.admissions.get(runtimeLease.dispatchAttemptId);
|
|
581
678
|
if (admission?.fencingToken === runtimeLease.fencingToken) {
|
|
582
679
|
this.admissions.set(runtimeLease.dispatchAttemptId, {
|
|
@@ -589,7 +686,7 @@ export class HostRuntimeLeaseManager {
|
|
|
589
686
|
return promotedLease;
|
|
590
687
|
}
|
|
591
688
|
catch {
|
|
592
|
-
await this.
|
|
689
|
+
await this.invalidatePromotion(active.repoId).catch(() => undefined);
|
|
593
690
|
active.lease = runtimeLease;
|
|
594
691
|
await this.writeLedger(runtimeLease);
|
|
595
692
|
return runtimeLease;
|
|
@@ -831,12 +928,25 @@ export class HostRuntimeLeaseManager {
|
|
|
831
928
|
}
|
|
832
929
|
this.drainAdmissionWaiters();
|
|
833
930
|
}
|
|
834
|
-
convertSingletonToParallelProject(key, owner) {
|
|
931
|
+
convertSingletonToParallelProject(key, owner, promotion) {
|
|
835
932
|
if (!sameSingletonOwner(this.singletonOwners.get(key), owner)) {
|
|
836
933
|
throw new Error("host_runtime_parallel_promotion_stale: matching exclusive project admission is required.");
|
|
837
934
|
}
|
|
838
935
|
this.assignParallelProject(key, owner);
|
|
839
936
|
this.singletonOwners.delete(key);
|
|
937
|
+
for (let index = 0; index < this.singletonWaiters.length;) {
|
|
938
|
+
const waiter = this.singletonWaiters[index];
|
|
939
|
+
if (waiter.keys.length !== 1 || waiter.keys[0] !== key) {
|
|
940
|
+
index += 1;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
this.singletonWaiters.splice(index, 1);
|
|
944
|
+
clearAdmissionDeadline(waiter);
|
|
945
|
+
waiter.signal?.removeEventListener("abort", waiter.abort);
|
|
946
|
+
this.assignParallelProject(key, waiter.owner);
|
|
947
|
+
this.knownPromotions.set(promotion.repoId, promotion);
|
|
948
|
+
waiter.resolve([]);
|
|
949
|
+
}
|
|
840
950
|
this.drainAdmissionWaiters();
|
|
841
951
|
}
|
|
842
952
|
parallelProjectOwned(key, owner) {
|
|
@@ -961,11 +1071,91 @@ export class HostRuntimeLeaseManager {
|
|
|
961
1071
|
waiter.resolve();
|
|
962
1072
|
}
|
|
963
1073
|
}
|
|
1074
|
+
async refreshKnownPromotions() {
|
|
1075
|
+
const promotions = await this.promotionStore.list();
|
|
1076
|
+
this.knownPromotions.clear();
|
|
1077
|
+
for (const promotion of promotions)
|
|
1078
|
+
this.knownPromotions.set(promotion.repoId, promotion);
|
|
1079
|
+
}
|
|
1080
|
+
async invalidatePromotion(repoId) {
|
|
1081
|
+
await this.promotionStore.invalidate(repoId);
|
|
1082
|
+
this.knownPromotions.delete(repoId.trim().toLowerCase());
|
|
1083
|
+
}
|
|
964
1084
|
drainAdmissionWaiters() {
|
|
965
1085
|
this.drainSingletonWaiters();
|
|
966
1086
|
this.drainParallelProjectWaiters();
|
|
967
1087
|
}
|
|
968
1088
|
}
|
|
1089
|
+
class AttemptSupervisedProviderProcessRunner {
|
|
1090
|
+
supervisor;
|
|
1091
|
+
owner;
|
|
1092
|
+
listenerInspector;
|
|
1093
|
+
enforceNoListeners;
|
|
1094
|
+
constructor(supervisor, owner, listenerInspector, enforceNoListeners) {
|
|
1095
|
+
this.supervisor = supervisor;
|
|
1096
|
+
this.owner = owner;
|
|
1097
|
+
this.listenerInspector = listenerInspector;
|
|
1098
|
+
this.enforceNoListeners = enforceNoListeners;
|
|
1099
|
+
}
|
|
1100
|
+
async run(request, handlers = {}, signal) {
|
|
1101
|
+
const child = await this.supervisor.startChildReady("provider", request, handlers, signal, { maxStartAttempts: request.stdin === undefined ? 1 : 3, retryDelayMs: 100 });
|
|
1102
|
+
if (!this.enforceNoListeners)
|
|
1103
|
+
return await child.result;
|
|
1104
|
+
let settled = false;
|
|
1105
|
+
let inspectionFailure;
|
|
1106
|
+
const monitor = (async () => {
|
|
1107
|
+
while (!settled) {
|
|
1108
|
+
try {
|
|
1109
|
+
const ports = await this.listenerInspector.inspectOwnedLoopbackListeners(child, this.owner);
|
|
1110
|
+
if (ports.length > 0) {
|
|
1111
|
+
inspectionFailure = new Error(`host_analysis_listener_violation: analysis-only provider opened undeclared loopback listener(s): ${ports.join(", ")}.`);
|
|
1112
|
+
await child.stop(this.owner.fencingToken).catch(() => undefined);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch (error) {
|
|
1117
|
+
inspectionFailure = error;
|
|
1118
|
+
await child.stop(this.owner.fencingToken).catch(() => undefined);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
await waitForAnalysisInspection(250, signal).catch(error => {
|
|
1122
|
+
if (!settled && !signal?.aborted)
|
|
1123
|
+
inspectionFailure = error;
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
})();
|
|
1127
|
+
try {
|
|
1128
|
+
const result = await child.result;
|
|
1129
|
+
settled = true;
|
|
1130
|
+
await monitor;
|
|
1131
|
+
if (inspectionFailure)
|
|
1132
|
+
throw inspectionFailure;
|
|
1133
|
+
return result;
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
settled = true;
|
|
1137
|
+
await monitor.catch(() => undefined);
|
|
1138
|
+
if (inspectionFailure)
|
|
1139
|
+
throw inspectionFailure;
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function waitForAnalysisInspection(milliseconds, signal) {
|
|
1145
|
+
if (signal?.aborted)
|
|
1146
|
+
return Promise.reject(cancellationError(signal));
|
|
1147
|
+
return new Promise((resolve, reject) => {
|
|
1148
|
+
const timer = setTimeout(() => {
|
|
1149
|
+
signal?.removeEventListener("abort", abort);
|
|
1150
|
+
resolve();
|
|
1151
|
+
}, milliseconds);
|
|
1152
|
+
const abort = () => {
|
|
1153
|
+
clearTimeout(timer);
|
|
1154
|
+
reject(cancellationError(signal));
|
|
1155
|
+
};
|
|
1156
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
969
1159
|
export class HostRuntimeLeaseCleanupError extends Error {
|
|
970
1160
|
lease;
|
|
971
1161
|
constructor(lease, cause) {
|
|
@@ -980,11 +1170,11 @@ export class HostRuntimeAdmissionExpiredError extends Error {
|
|
|
980
1170
|
this.name = "HostRuntimeAdmissionExpiredError";
|
|
981
1171
|
}
|
|
982
1172
|
}
|
|
983
|
-
export function hostRuntimeEventResources(lease, runtimeContract) {
|
|
1173
|
+
export function hostRuntimeEventResources(lease, runtimeContract, executionClass = "managed_runtime") {
|
|
984
1174
|
const requestedHostConcurrency = runtimeContract?.contract.runtime.hostConcurrency ?? "serial";
|
|
985
1175
|
const configuredLaunches = runtimeContract?.contract.runtime.ports.filter(port => port.launch !== null) ?? [];
|
|
986
1176
|
const verifiedLaunches = configuredLaunches.filter(configured => lease.resources.ports.some(port => port.name === configured.name));
|
|
987
|
-
const effectiveConcurrency = effectiveHostConcurrency(lease, runtimeContract);
|
|
1177
|
+
const effectiveConcurrency = effectiveHostConcurrency(lease, runtimeContract, executionClass);
|
|
988
1178
|
return {
|
|
989
1179
|
agentContainer: null,
|
|
990
1180
|
sidecarContainers: [],
|
|
@@ -994,7 +1184,7 @@ export function hostRuntimeEventResources(lease, runtimeContract) {
|
|
|
994
1184
|
hostConcurrency: effectiveConcurrency,
|
|
995
1185
|
requestedHostConcurrency,
|
|
996
1186
|
hostParallelPromotion: effectiveConcurrency === "host_parallel_safe"
|
|
997
|
-
? "observed_probe_passed"
|
|
1187
|
+
? executionClass === "analysis_only" ? "analysis_profile_enforced" : "observed_probe_passed"
|
|
998
1188
|
: requestedHostConcurrency !== "parallel"
|
|
999
1189
|
? "not_requested"
|
|
1000
1190
|
: configuredLaunches.length > 0 && verifiedLaunches.length === configuredLaunches.length
|
|
@@ -1002,6 +1192,7 @@ export function hostRuntimeEventResources(lease, runtimeContract) {
|
|
|
1002
1192
|
: "pending_adapter_probe",
|
|
1003
1193
|
verifiedRuntimePorts: verifiedLaunches.map(port => port.name),
|
|
1004
1194
|
runtimeContractSource: runtimeContract?.source ?? "default",
|
|
1195
|
+
executionClass,
|
|
1005
1196
|
requestedPorts: runtimeContract?.contract.runtime.ports.map(port => port.name) ?? [],
|
|
1006
1197
|
singletonKeys: lease.resources.singletonKeys,
|
|
1007
1198
|
processSupervisorId: lease.resources.processSupervisorId,
|
|
@@ -1011,7 +1202,7 @@ export function hostRuntimeEventResources(lease, runtimeContract) {
|
|
|
1011
1202
|
export function hostRuntimeSingletonKeys(dispatch) {
|
|
1012
1203
|
return [projectHostRuntimeSingletonKey(dispatch.repo.id)];
|
|
1013
1204
|
}
|
|
1014
|
-
function hostRuntimeEnvironment(sessionId, lease, runtimeContract) {
|
|
1205
|
+
function hostRuntimeEnvironment(sessionId, lease, runtimeContract, executionClass) {
|
|
1015
1206
|
const pathFor = (role) => lease.resources.paths.find(entry => entry.role === role)?.path ?? "";
|
|
1016
1207
|
const environment = {
|
|
1017
1208
|
FORGE_SESSION_ID: sessionId,
|
|
@@ -1027,10 +1218,13 @@ function hostRuntimeEnvironment(sessionId, lease, runtimeContract) {
|
|
|
1027
1218
|
FORGE_CACHE_DIR: pathFor("cache"),
|
|
1028
1219
|
FORGE_BROWSER_PROFILE_DIR: pathFor("browser_profile"),
|
|
1029
1220
|
FORGE_TEST_OUTPUT_DIR: pathFor("test_output"),
|
|
1221
|
+
FORGE_ATTEMPT_DATABASE_SCHEMA: `forge_attempt_${uuidHex(lease.dispatchAttemptId, "dispatch attempt")}`,
|
|
1222
|
+
FORGE_ATTEMPT_DATABASE_READINESS_PATH: path.join(pathFor("logs"), "attempt-database-readiness.json"),
|
|
1030
1223
|
FORGE_PROCESS_SUPERVISOR_ID: lease.resources.processSupervisorId ?? "",
|
|
1031
1224
|
FORGE_PROCESS_SUPERVISOR_METADATA_PATH: pathFor("process_supervisor_metadata"),
|
|
1032
|
-
FORGE_HOST_CONCURRENCY: effectiveHostConcurrency(lease, runtimeContract),
|
|
1225
|
+
FORGE_HOST_CONCURRENCY: effectiveHostConcurrency(lease, runtimeContract, executionClass),
|
|
1033
1226
|
FORGE_HOST_CONCURRENCY_REQUESTED: runtimeContract.contract.runtime.hostConcurrency,
|
|
1227
|
+
FORGE_RUNTIME_EXECUTION_CLASS: executionClass,
|
|
1034
1228
|
FORGE_HOST_RUNTIME_CONTRACT_SOURCE: runtimeContract.source,
|
|
1035
1229
|
FORGE_SINGLETON_KEYS: lease.resources.singletonKeys.join(","),
|
|
1036
1230
|
TMPDIR: pathFor("temporary"),
|
|
@@ -1057,11 +1251,19 @@ function hostRuntimeEnvironment(sessionId, lease, runtimeContract) {
|
|
|
1057
1251
|
}
|
|
1058
1252
|
}
|
|
1059
1253
|
}
|
|
1254
|
+
if (environment.FORGE_API_URL)
|
|
1255
|
+
environment.FORGE_E2E_API_URL = environment.FORGE_API_URL;
|
|
1256
|
+
if (environment.FORGE_WEB_URL)
|
|
1257
|
+
environment.FORGE_E2E_WEB_URL = environment.FORGE_WEB_URL;
|
|
1060
1258
|
if (resolvedBaseUrls.length === 1)
|
|
1061
1259
|
environment.FORGE_BASE_URL = resolvedBaseUrls[0];
|
|
1062
1260
|
return environment;
|
|
1063
1261
|
}
|
|
1064
|
-
function effectiveHostConcurrency(lease, runtimeContract) {
|
|
1262
|
+
function effectiveHostConcurrency(lease, runtimeContract, executionClass = "managed_runtime") {
|
|
1263
|
+
if (executionClass === "analysis_only") {
|
|
1264
|
+
const analysisSingletonHeld = lease.resources.singletonKeys.length > 0;
|
|
1265
|
+
return analysisSingletonHeld ? "host_serial_only" : "host_parallel_safe";
|
|
1266
|
+
}
|
|
1065
1267
|
if (!runtimeContract || !hostRuntimeParallelEligible(runtimeContract))
|
|
1066
1268
|
return "host_serial_only";
|
|
1067
1269
|
const requiredPorts = configuredLaunchPortNames(runtimeContract);
|
|
@@ -1073,6 +1275,18 @@ function effectiveHostConcurrency(lease, runtimeContract) {
|
|
|
1073
1275
|
function projectHostRuntimeSingletonKey(repoId) {
|
|
1074
1276
|
return `project:${repoId.toLowerCase()}:host-runtime`;
|
|
1075
1277
|
}
|
|
1278
|
+
function projectHostAnalysisKey(repoId) {
|
|
1279
|
+
return `project:${repoId.toLowerCase()}:host-analysis`;
|
|
1280
|
+
}
|
|
1281
|
+
function hostRuntimeDeclaredSingletonKey(resource) {
|
|
1282
|
+
return `runtime:${resource}`;
|
|
1283
|
+
}
|
|
1284
|
+
function runtimeExecutionClass(dispatch) {
|
|
1285
|
+
return dispatch.runtimeIntent?.executionClass === "analysis_only"
|
|
1286
|
+
&& dispatch.runtimeIntent.applicationRuntimeRequired === false
|
|
1287
|
+
? "analysis_only"
|
|
1288
|
+
: "managed_runtime";
|
|
1289
|
+
}
|
|
1076
1290
|
function capacityProject(projects, repoId) {
|
|
1077
1291
|
const normalized = repoId.trim().toLowerCase();
|
|
1078
1292
|
const existing = projects.get(normalized);
|
|
@@ -1085,6 +1299,9 @@ function capacityProject(projects, repoId) {
|
|
|
1085
1299
|
reservedAttempts: 0,
|
|
1086
1300
|
waitingAttempts: 0,
|
|
1087
1301
|
degradedAttempts: 0,
|
|
1302
|
+
runtimeContractFingerprint: null,
|
|
1303
|
+
runtimeContractSource: null,
|
|
1304
|
+
promotedAt: null,
|
|
1088
1305
|
};
|
|
1089
1306
|
projects.set(normalized, created);
|
|
1090
1307
|
return created;
|
|
@@ -1104,8 +1321,8 @@ function repoIdFromProjectSingletonKeys(keys) {
|
|
|
1104
1321
|
}
|
|
1105
1322
|
function repoIdFromProjectSingletonKey(key) {
|
|
1106
1323
|
const prefix = "project:";
|
|
1107
|
-
const suffix = ":host-runtime";
|
|
1108
|
-
return key.startsWith(prefix) &&
|
|
1324
|
+
const suffix = [":host-runtime", ":host-analysis"].find(candidate => key.endsWith(candidate));
|
|
1325
|
+
return key.startsWith(prefix) && suffix
|
|
1109
1326
|
? key.slice(prefix.length, -suffix.length)
|
|
1110
1327
|
: null;
|
|
1111
1328
|
}
|
|
@@ -1185,13 +1402,17 @@ function validHostProcessSupervisorMetadata(value, lease) {
|
|
|
1185
1402
|
&& (metadata.rootIdentity == null
|
|
1186
1403
|
|| (typeof metadata.rootIdentity === "string" && metadata.rootIdentity.length <= 256))
|
|
1187
1404
|
&& /^[0-9a-f]{64}$/u.test(metadata.commandDigest)
|
|
1188
|
-
&&
|
|
1405
|
+
&& containedMetadataWorktreePath(lease.worktreePath, metadata.worktreePath)
|
|
1189
1406
|
&& metadata.runtimeLeaseId === lease.executionLeaseId
|
|
1190
1407
|
&& metadata.dispatchAttemptId === lease.dispatchAttemptId
|
|
1191
1408
|
&& metadata.fencingToken === lease.fencingToken
|
|
1192
1409
|
&& Number.isSafeInteger(metadata.bridgePid)
|
|
1193
1410
|
&& metadata.bridgePid > 0;
|
|
1194
1411
|
}
|
|
1412
|
+
function containedMetadataWorktreePath(worktreePath, metadataWorktreePath) {
|
|
1413
|
+
const relative = path.relative(path.resolve(worktreePath), path.resolve(metadataWorktreePath));
|
|
1414
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
1415
|
+
}
|
|
1195
1416
|
function transitionHostLeaseForCleanup(lease, state) {
|
|
1196
1417
|
if (lease.state === state || lease.state === "released")
|
|
1197
1418
|
return lease;
|
|
@@ -3,6 +3,7 @@ export interface HostParallelPromotionRecord {
|
|
|
3
3
|
readonly schemaVersion: 1;
|
|
4
4
|
readonly repoId: string;
|
|
5
5
|
readonly runtimeContractFingerprint: string;
|
|
6
|
+
readonly runtimeContractSource?: "project" | "derived";
|
|
6
7
|
readonly observedAt: string;
|
|
7
8
|
readonly observedDispatchAttemptId: string;
|
|
8
9
|
readonly verifiedPortNames: readonly string[];
|
|
@@ -12,6 +13,7 @@ export declare class HostParallelPromotionStore {
|
|
|
12
13
|
private readonly stateRoot;
|
|
13
14
|
constructor(stateRoot: string);
|
|
14
15
|
load(repoId: string): Promise<HostParallelPromotionRecord | null>;
|
|
16
|
+
list(): Promise<readonly HostParallelPromotionRecord[]>;
|
|
15
17
|
observe(repoId: string, runtimeContract: LoadedHostRuntimeContract, dispatchAttemptId: string, verifiedPortNames: readonly string[]): Promise<HostParallelPromotionRecord>;
|
|
16
18
|
invalidate(repoId: string): Promise<void>;
|
|
17
19
|
private recordPath;
|