@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.1ff37038366e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +165 -1
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +9 -1
  5. package/dist/bridge/bridge.service.d.ts +129 -3
  6. package/dist/bridge/bridge.service.js +2427 -70
  7. package/dist/bridge/bridge.types.d.ts +218 -2
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +14 -2
  21. package/dist/cleanup/cleanup.service.js +496 -3
  22. package/dist/cleanup/cleanup.types.d.ts +10 -0
  23. package/dist/cleanup/index.d.ts +1 -1
  24. package/dist/cleanup/index.js +1 -1
  25. package/dist/cleanup/internals.d.ts +2 -2
  26. package/dist/cleanup/internals.js +1 -1
  27. package/dist/cli/cli.help.d.ts +2 -0
  28. package/dist/cli/cli.help.js +217 -0
  29. package/dist/cli/cli.service.d.ts +55 -7
  30. package/dist/cli/cli.service.js +429 -219
  31. package/dist/cli/local-config.d.ts +7 -0
  32. package/dist/cli/local-config.js +51 -3
  33. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  34. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  35. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  36. package/dist/runtime/attempt-database-broker.js +390 -0
  37. package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
  38. package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
  39. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  40. package/dist/runtime/attempt-path-operation.js +94 -0
  41. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  42. package/dist/runtime/attempt-private-directory.js +54 -0
  43. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  44. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  45. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  46. package/dist/runtime/attempt-process-supervisor.js +397 -0
  47. package/dist/runtime/canonical-source-file.d.ts +3 -0
  48. package/dist/runtime/canonical-source-file.js +54 -0
  49. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  50. package/dist/runtime/docker-engine-probe.js +333 -0
  51. package/dist/runtime/generated-output-reset.d.ts +20 -0
  52. package/dist/runtime/generated-output-reset.js +157 -0
  53. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  54. package/dist/runtime/host-database-capability-boundary.js +62 -0
  55. package/dist/runtime/host-diagnostics.d.ts +11 -0
  56. package/dist/runtime/host-diagnostics.js +191 -0
  57. package/dist/runtime/host-execution-root.d.ts +4 -0
  58. package/dist/runtime/host-execution-root.js +39 -0
  59. package/dist/runtime/host-listener-inspector.d.ts +51 -0
  60. package/dist/runtime/host-listener-inspector.js +385 -0
  61. package/dist/runtime/host-port-broker.d.ts +76 -0
  62. package/dist/runtime/host-port-broker.js +333 -0
  63. package/dist/runtime/host-runtime-contract.d.ts +58 -0
  64. package/dist/runtime/host-runtime-contract.js +813 -0
  65. package/dist/runtime/host-runtime-lease.d.ts +217 -0
  66. package/dist/runtime/host-runtime-lease.js +2487 -0
  67. package/dist/runtime/host-runtime-promotion.d.ts +32 -0
  68. package/dist/runtime/host-runtime-promotion.js +206 -0
  69. package/dist/runtime/memory-capacity.d.ts +70 -0
  70. package/dist/runtime/memory-capacity.js +123 -0
  71. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  72. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  73. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  74. package/dist/runtime/node-host-runtime-adapter.js +285 -0
  75. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  76. package/dist/runtime/pinned-directory-process.js +61 -0
  77. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  78. package/dist/runtime/pinned-source-directory.js +94 -0
  79. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  80. package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
  81. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  82. package/dist/runtime/prepared-runtime-identity.js +218 -0
  83. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  84. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  85. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  86. package/dist/runtime/runtime-isolation-lease.js +161 -0
  87. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  88. package/dist/runtime/workspace-resource-estimator.js +174 -0
  89. package/dist/sandbox/command-runner.d.ts +4 -0
  90. package/dist/sandbox/command-runner.js +47 -0
  91. package/dist/sandbox/contract.d.ts +5 -0
  92. package/dist/sandbox/contract.js +276 -0
  93. package/dist/sandbox/event-budget.d.ts +11 -0
  94. package/dist/sandbox/event-budget.js +27 -0
  95. package/dist/sandbox/image-policy.d.ts +35 -0
  96. package/dist/sandbox/image-policy.js +247 -0
  97. package/dist/sandbox/index.d.ts +5 -0
  98. package/dist/sandbox/index.js +5 -0
  99. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  100. package/dist/sandbox/lease-watchdog.js +67 -0
  101. package/dist/sandbox/preflight.d.ts +84 -0
  102. package/dist/sandbox/preflight.js +521 -0
  103. package/dist/sandbox/process.d.ts +20 -0
  104. package/dist/sandbox/process.js +209 -0
  105. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  106. package/dist/sandbox/provider-auth/codex.js +212 -0
  107. package/dist/sandbox/repository-lock.d.ts +6 -0
  108. package/dist/sandbox/repository-lock.js +128 -0
  109. package/dist/sandbox/runtime.d.ts +302 -0
  110. package/dist/sandbox/runtime.js +2367 -0
  111. package/dist/sandbox/sandbox.types.d.ts +93 -0
  112. package/dist/sandbox/sandbox.types.js +1 -0
  113. package/dist/sandbox/smoke.d.ts +76 -0
  114. package/dist/sandbox/smoke.js +380 -0
  115. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  116. package/dist/sandbox/worker-supervisor.js +36 -0
  117. package/dist/sandbox/worker.d.ts +1 -0
  118. package/dist/sandbox/worker.js +225 -0
  119. package/dist/sandbox/workspace.service.d.ts +13 -0
  120. package/dist/sandbox/workspace.service.js +74 -0
  121. package/dist/session/managed-attempt-tools.d.ts +28 -0
  122. package/dist/session/managed-attempt-tools.js +1015 -0
  123. package/dist/session/managed-build-coordinator.d.ts +5 -0
  124. package/dist/session/managed-build-coordinator.js +105 -0
  125. package/dist/session/managed-build-receipt.d.ts +21 -0
  126. package/dist/session/managed-build-receipt.js +337 -0
  127. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  128. package/dist/session/managed-verification-timeouts.js +17 -0
  129. package/dist/session/phase-completion.artifacts.d.ts +27 -0
  130. package/dist/session/phase-completion.artifacts.js +829 -0
  131. package/dist/session/phase-completion.d.ts +53 -42
  132. package/dist/session/phase-completion.helpers.d.ts +117 -0
  133. package/dist/session/phase-completion.helpers.js +707 -0
  134. package/dist/session/phase-completion.js +125 -3007
  135. package/dist/session/phase-completion.prompts.d.ts +14 -0
  136. package/dist/session/phase-completion.prompts.js +233 -0
  137. package/dist/session/phase-completion.runtime.d.ts +49 -0
  138. package/dist/session/phase-completion.runtime.js +840 -0
  139. package/dist/session/phase-completion.schemas.d.ts +3 -0
  140. package/dist/session/phase-completion.schemas.js +859 -0
  141. package/dist/session/phase-completion.validation.d.ts +38 -0
  142. package/dist/session/phase-completion.validation.js +918 -0
  143. package/dist/session/pre-publication-validation.d.ts +75 -0
  144. package/dist/session/pre-publication-validation.js +661 -0
  145. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  146. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  147. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  148. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  149. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
  150. package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
  151. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  152. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  153. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  154. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  155. package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
  156. package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
  157. package/dist/session/provider-cli/provider-cli.process.js +874 -0
  158. package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
  159. package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
  160. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  161. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
  162. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  163. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  164. package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
  165. package/dist/session/provider-cli/provider-cli.runtime.js +81 -0
  166. package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
  167. package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
  168. package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
  169. package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
  170. package/dist/session/provider-cli/provider-output-state.js +817 -0
  171. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  172. package/dist/session/provider-cli/provider-usage.js +28 -5
  173. package/dist/session/session-redaction.d.ts +3 -0
  174. package/dist/session/session-redaction.js +448 -0
  175. package/dist/session/session.base.d.ts +6 -1
  176. package/dist/session/session.service.d.ts +8 -2
  177. package/dist/session/session.service.js +586 -59
  178. package/dist/session/session.types.d.ts +324 -3
  179. package/dist/session/session.types.js +939 -10
  180. package/dist/session/unreferenced-exports.d.ts +12 -0
  181. package/dist/session/unreferenced-exports.js +59 -0
  182. package/dist/shared/format/format-bytes.d.ts +1 -0
  183. package/dist/shared/format/format-bytes.js +9 -0
  184. package/dist/shared/git/git.js +2 -1
  185. package/dist/shared/path/path.d.ts +1 -0
  186. package/dist/shared/path/path.js +21 -1
  187. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  188. package/dist/shared/process/dotnet-toolchain.js +307 -0
  189. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  190. package/dist/shared/process/host-process-recovery.js +78 -0
  191. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  192. package/dist/shared/process/host-process-supervisor.js +330 -0
  193. package/dist/shared/process/process.d.ts +3 -0
  194. package/dist/shared/process/process.js +13 -7
  195. package/dist/shared/process/windows-launch.d.ts +8 -0
  196. package/dist/shared/process/windows-launch.js +41 -0
  197. package/dist/shared/provider-capabilities.d.ts +4 -0
  198. package/dist/shared/provider-capabilities.js +88 -0
  199. package/dist/shared/redaction/redaction.d.ts +1 -0
  200. package/dist/shared/redaction/redaction.js +47 -4
  201. package/dist/version.d.ts +1 -1
  202. package/dist/version.js +1 -1
  203. package/dist/worktree/artifact-injector.d.ts +3 -2
  204. package/dist/worktree/artifact-injector.js +82 -10
  205. package/dist/worktree/dependency-warmer.d.ts +53 -1
  206. package/dist/worktree/dependency-warmer.js +619 -11
  207. package/dist/worktree/index.d.ts +2 -1
  208. package/dist/worktree/index.js +1 -0
  209. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  210. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  211. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  212. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  213. package/dist/worktree/worktree-paths.d.ts +4 -0
  214. package/dist/worktree/worktree-paths.js +30 -0
  215. package/dist/worktree/worktree.base.d.ts +3 -0
  216. package/dist/worktree/worktree.service.d.ts +16 -3
  217. package/dist/worktree/worktree.service.js +334 -54
  218. package/dist/worktree/worktree.types.d.ts +9 -1
  219. package/package.json +12 -7
  220. package/runtime/windows-job-supervisor.ps1 +722 -0
  221. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,2487 @@
1
+ import fs from "node:fs/promises";
2
+ import { statfsSync } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { assertHostAttemptRootIdentity, retainHostDiagnostics } from "./host-diagnostics.js";
7
+ import { assertHostExecutionRoot, compactHostAttemptId } from "./host-execution-root.js";
8
+ import { PreparedRuntimeReadiness } from "./prepared-runtime-readiness.js";
9
+ import { preparedRuntimeIdentity } from "./prepared-runtime-identity.js";
10
+ import { resolveExistingDirectoryPath } from "../shared/path/path.js";
11
+ import { formatBytes } from "../shared/format/format-bytes.js";
12
+ import { redactText } from "../shared/redaction/redaction.js";
13
+ import { StreamingProviderProcessRunner, } from "../session/provider-cli/provider-cli.process.js";
14
+ import { createRuntimeIsolationLease, renewRuntimeIsolationLease, transitionRuntimeIsolationLease, validRuntimeIsolationLease, withRuntimeIsolationResources, } from "./runtime-isolation-lease.js";
15
+ import { hostPortEnvironment, loadHostRuntimeContract, } from "./host-runtime-contract.js";
16
+ import { HostPortBroker, } from "./host-port-broker.js";
17
+ import { NodeHostRuntimeAdapter, } from "./node-host-runtime-adapter.js";
18
+ import { AttemptProcessSupervisorRegistry } from "./attempt-process-supervisor-registry.js";
19
+ import { attemptDatabaseCapabilityEnvironment, attemptDatabaseCapabilitySecrets, } from "./attempt-database-broker.js";
20
+ import { AttemptNodeRuntimeLauncher } from "./attempt-node-runtime-launcher.js";
21
+ import { HostListenerInspector } from "./host-listener-inspector.js";
22
+ import { RecoveryStabilizedMemory, buildMemoryAdmissionDiagnostics, resolveAdaptiveMemoryReserveBytes, } from "./memory-capacity.js";
23
+ import { PlaywrightHostRuntimeAdapter } from "./playwright-host-runtime-adapter.js";
24
+ import { HostParallelPromotionStore, configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible, } from "./host-runtime-promotion.js";
25
+ import { ExactHostProcessRecovery, } from "../shared/process/host-process-recovery.js";
26
+ import { resetGeneratedOutputs } from "./generated-output-reset.js";
27
+ import { assertCanonicalDirectory, createCanonicalDirectory } from "./attempt-private-directory.js";
28
+ import { readCanonicalSourceFile } from "./canonical-source-file.js";
29
+ import { readPinnedSourceDirectory } from "./pinned-source-directory.js";
30
+ import { architectMinimumWorkspaceEstimateBytes } from "./workspace-resource-estimator.js";
31
+ import { createAspNetRuntimePrebuildPlan, ensureAspNetRuntimePrebuilt, resolveWorktreeCommitSha, } from "./aspnet-runtime-prebuild.js";
32
+ const hostCapacityWaiterScanLimit = 64;
33
+ const maximumProcessRecoveryMetadataBytes = 1024 * 1024;
34
+ export class HostRuntimeLeaseManager {
35
+ stateRoot;
36
+ processSupervisors;
37
+ processRunner;
38
+ listenerInspector;
39
+ promotionStore;
40
+ playwrightAdapter;
41
+ processRecovery;
42
+ capacityResourceSample;
43
+ databaseBroker;
44
+ preparedReadiness;
45
+ active = new Map();
46
+ pendingRenewals = new Map();
47
+ acquiringAdmissions = new Set();
48
+ admissions = new Map();
49
+ singletonOwners = new Map();
50
+ singletonWaiters = [];
51
+ parallelProjectOwners = new Map();
52
+ parallelProjectWaiters = [];
53
+ capacityReservations = new Map();
54
+ capacityWaiters = [];
55
+ capacityWaiterScanCursor = 0;
56
+ backgroundAdmissionWaiters = [];
57
+ degraded = new Map();
58
+ knownPromotions = new Map();
59
+ ledgerWriteTail = Promise.resolve();
60
+ workspaceAvailableBytes = Number.MAX_SAFE_INTEGER;
61
+ portBroker;
62
+ nodeAdapter;
63
+ capacityConfiguration;
64
+ memoryAvailability;
65
+ executionRoot;
66
+ 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(), capacityResourceSample, executionRoot, databaseBroker = null) {
67
+ this.stateRoot = stateRoot;
68
+ this.processSupervisors = processSupervisors;
69
+ this.processRunner = processRunner;
70
+ this.listenerInspector = listenerInspector;
71
+ this.promotionStore = promotionStore;
72
+ this.playwrightAdapter = playwrightAdapter;
73
+ this.processRecovery = processRecovery;
74
+ this.capacityResourceSample = capacityResourceSample;
75
+ this.databaseBroker = databaseBroker;
76
+ this.executionRoot = path.resolve(executionRoot ?? stateRoot);
77
+ if (executionRoot !== undefined)
78
+ assertHostExecutionRoot(this.executionRoot);
79
+ this.preparedReadiness = new PreparedRuntimeReadiness(this, promotionStore, (id, fence) => this.release(id, fence, true), () => new HostRuntimeAdmissionExpiredError());
80
+ this.portBroker = portBroker;
81
+ this.nodeAdapter = nodeAdapter ?? new NodeHostRuntimeAdapter(portBroker);
82
+ this.capacityConfiguration = loadHostCapacityConfiguration(capacityResourceSample);
83
+ this.memoryAvailability = new RecoveryStabilizedMemory(this.capacityConfiguration.memoryRecoverySamples, this.capacityConfiguration.memoryRecoverySampleIntervalMs);
84
+ }
85
+ async reap(options) {
86
+ await this.refreshKnownPromotions();
87
+ const recoveredAttempts = [];
88
+ const report = {
89
+ examined: 0,
90
+ reaped: 0,
91
+ processesTerminated: 0,
92
+ reusedPidsProtected: 0,
93
+ degraded: 0,
94
+ recoveredAttempts,
95
+ };
96
+ for (const ledger of await this.readLedgers()) {
97
+ const lease = ledger.runtimeLease;
98
+ if (lease.runnerId !== options.runnerId)
99
+ continue;
100
+ // A released ledger with its original attempt root is intentionally retained
101
+ // evidence that exceeded the diagnostics archive budget. It owns no runtime
102
+ // resources, but still requires process-identity recovery below on restart.
103
+ if (lease.state === "released") {
104
+ const attemptRoot = this.attemptRoot(lease.dispatchAttemptId);
105
+ const rootExists = await fs.stat(attemptRoot).then(() => true, error => {
106
+ if (error.code === "ENOENT")
107
+ return false;
108
+ throw error;
109
+ });
110
+ if (!rootExists) {
111
+ await fs.rm(this.ledgerPath(lease.dispatchAttemptId), { force: true });
112
+ continue;
113
+ }
114
+ }
115
+ const active = this.active.get(lease.dispatchAttemptId);
116
+ if (active) {
117
+ if (active.lease.state !== "degraded")
118
+ continue;
119
+ report.examined += 1;
120
+ try {
121
+ const released = await this.release(lease.dispatchAttemptId, lease.fencingToken);
122
+ if (released?.state === "released")
123
+ report.reaped += 1;
124
+ else
125
+ report.degraded += 1;
126
+ }
127
+ catch {
128
+ report.degraded += 1;
129
+ }
130
+ continue;
131
+ }
132
+ const staleInstance = lease.runnerInstanceId !== options.runnerInstanceId;
133
+ const expired = Date.parse(lease.expiresAt) <= (options.nowMs ?? Date.now());
134
+ if (!staleInstance && !expired && lease.state !== "released" && lease.state !== "degraded")
135
+ continue;
136
+ report.examined += 1;
137
+ const attemptRoot = this.attemptRoot(lease.dispatchAttemptId);
138
+ const attemptRootIdentity = deserializeHostAttemptRootIdentity(ledger.attemptRootIdentity);
139
+ let outcomes;
140
+ try {
141
+ if (!attemptRootIdentity) {
142
+ throw new Error("host_runtime_reaper_invalid: original attempt-root identity is unavailable.");
143
+ }
144
+ outcomes = await this.recoverPersistedProcesses(lease, attemptRoot, attemptRootIdentity);
145
+ }
146
+ catch {
147
+ await this.markLedgerDegraded(ledger);
148
+ report.degraded += 1;
149
+ continue;
150
+ }
151
+ report.processesTerminated += outcomes.filter(outcome => outcome === "terminated").length;
152
+ report.reusedPidsProtected += outcomes.filter(outcome => outcome === "pid_reused").length;
153
+ if (outcomes.includes("unverifiable")) {
154
+ await this.markLedgerDegraded(ledger);
155
+ report.degraded += 1;
156
+ continue;
157
+ }
158
+ // The runtime has already been released. Keep the over-budget original
159
+ // evidence without re-copying it or reacquiring a capacity claim.
160
+ if (lease.state === "released")
161
+ continue;
162
+ // Every supervised tree of this previous-instance attempt is proved gone,
163
+ // so no provider can still act for it. Diagnostics handling below never
164
+ // revives a process, so the proof stands even if cleanup degrades. An
165
+ // attempt with no ownership record at all may have been mid-launch, so
166
+ // it is not reported and keeps its unknown outcome.
167
+ if (staleInstance && outcomes.length > 0) {
168
+ recoveredAttempts.push({
169
+ dispatchAttemptId: lease.dispatchAttemptId,
170
+ fencingToken: lease.fencingToken,
171
+ runnerInstanceId: lease.runnerInstanceId,
172
+ });
173
+ }
174
+ try {
175
+ const releasing = transitionHostLeaseForCleanup(lease, "releasing");
176
+ await this.writeLedger(releasing, ledger.repoId, attemptRootIdentity);
177
+ let evidenceRetainedAtAttemptRoot = false;
178
+ try {
179
+ await retainHostDiagnostics(this.stateRoot, attemptRoot, lease.dispatchAttemptId, attemptRootIdentity, this.executionRoot);
180
+ }
181
+ catch {
182
+ // Exact process recovery above already proved that the runtime is gone.
183
+ // Diagnostics retention can independently refuse a changed inventory,
184
+ // an unsafe archive path, or an over-budget source. Preserve the
185
+ // original attempt root and a released ledger for reconciliation, but
186
+ // never turn evidence-only work back into a capacity-owning runtime.
187
+ evidenceRetainedAtAttemptRoot = true;
188
+ }
189
+ if (evidenceRetainedAtAttemptRoot) {
190
+ const released = transitionRuntimeIsolationLease(releasing, "released");
191
+ await this.writeLedger(released, ledger.repoId, attemptRootIdentity);
192
+ this.forgetDegraded(lease.dispatchAttemptId, lease.fencingToken);
193
+ report.reaped += 1;
194
+ continue;
195
+ }
196
+ await removeHostAttemptRoot(attemptRoot);
197
+ await fs.rm(this.ledgerPath(lease.dispatchAttemptId), { force: true });
198
+ this.forgetDegraded(lease.dispatchAttemptId, lease.fencingToken);
199
+ report.reaped += 1;
200
+ }
201
+ catch {
202
+ await this.markLedgerDegraded(ledger);
203
+ report.degraded += 1;
204
+ }
205
+ }
206
+ // Databases issued to attempts that ended without a clean release (a Bridge crash, or a
207
+ // drop that met a lingering session) are removed once no live lease holds them.
208
+ // A degraded lease may still own running processes after a restart, so its database is kept.
209
+ await this.databaseBroker?.reapOrphans(id => this.active.has(id) || this.degraded.has(id)).catch(() => undefined);
210
+ return report;
211
+ }
212
+ snapshot() {
213
+ this.reconcileReservationOwners();
214
+ const projects = new Map();
215
+ for (const promotion of this.knownPromotions.values()) {
216
+ const project = capacityProject(projects, promotion.repoId);
217
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_parallel_safe");
218
+ project.runtimeContractFingerprint = promotion.runtimeContractFingerprint;
219
+ project.runtimeContractSource = promotion.runtimeContractSource ?? null;
220
+ project.promotedAt = promotion.observedAt;
221
+ }
222
+ for (const [dispatchAttemptId, admission] of this.admissions) {
223
+ const active = this.active.get(dispatchAttemptId);
224
+ const project = capacityProject(projects, admission.repoId);
225
+ if (active) {
226
+ if (active.lease.state === "degraded") {
227
+ project.degradedAttempts += 1;
228
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_serial_only");
229
+ }
230
+ else {
231
+ project.activeAttempts += 1;
232
+ const effectiveConcurrency = effectiveHostConcurrency(active.lease, active.runtimeContract, active.executionClass);
233
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, effectiveConcurrency === "host_parallel_safe"
234
+ ? effectiveConcurrency
235
+ : admission.executionClass === "analysis_only"
236
+ ? "host_parallel_safe"
237
+ : admission.parallelProjectKey && admission.provisionalPromotionFingerprint
238
+ ? "host_parallel_candidate"
239
+ : "host_serial_only");
240
+ }
241
+ }
242
+ else {
243
+ project.reservedAttempts += 1;
244
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, admission.executionClass === "analysis_only"
245
+ ? "host_parallel_safe"
246
+ : admission.provisionalPromotionFingerprint ? "host_parallel_candidate" : "host_serial_only");
247
+ }
248
+ }
249
+ for (const [dispatchAttemptId, degraded] of this.degraded) {
250
+ if (this.active.has(dispatchAttemptId) || !degraded.repoId)
251
+ continue;
252
+ const project = capacityProject(projects, degraded.repoId);
253
+ project.degradedAttempts += 1;
254
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_serial_only");
255
+ }
256
+ for (const waiter of this.singletonWaiters) {
257
+ const repoId = repoIdFromProjectSingletonKeys(waiter.keys);
258
+ if (!repoId)
259
+ continue;
260
+ const project = capacityProject(projects, repoId);
261
+ project.waitingAttempts += 1;
262
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_serial_only");
263
+ }
264
+ for (const waiter of this.parallelProjectWaiters) {
265
+ const repoId = repoIdFromProjectSingletonKey(waiter.key);
266
+ if (!repoId)
267
+ continue;
268
+ const project = capacityProject(projects, repoId);
269
+ project.waitingAttempts += 1;
270
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_parallel_candidate");
271
+ }
272
+ for (const waiter of this.backgroundAdmissionWaiters) {
273
+ const repoId = repoIdFromProjectSingletonKeys(waiter.keys);
274
+ if (!repoId)
275
+ continue;
276
+ const project = capacityProject(projects, repoId);
277
+ project.waitingAttempts += 1;
278
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_serial_only");
279
+ }
280
+ // A prepared-runtime verification remains foreground demand after its
281
+ // preparation admission is released and before the internal probe has
282
+ // acquired a normal admission. Report that barrier as waiting capacity so
283
+ // the server cannot mistake the transient gap for an idle host and dispatch
284
+ // readiness probes in a tight loop.
285
+ const unrepresentedPrepared = this.preparedReadiness.pendingSnapshots()
286
+ .filter(pending => !this.hasAttemptRepresentation(pending.probeDispatchAttemptId ?? pending.dispatchAttemptId, pending.probeFencingToken ?? pending.fencingToken));
287
+ for (const pending of unrepresentedPrepared) {
288
+ const project = capacityProject(projects, pending.repoId);
289
+ project.waitingAttempts += 1;
290
+ project.safetyClass = conservativeSafetyClass(project.safetyClass, "host_parallel_candidate");
291
+ }
292
+ const activeAttempts = [...this.active.values()]
293
+ .filter(active => active.lease.state !== "degraded").length;
294
+ const degradedAttempts = new Set([
295
+ ...this.degraded.keys(),
296
+ ...[...this.active.entries()]
297
+ .filter(([, active]) => active.lease.state === "degraded")
298
+ .map(([dispatchAttemptId]) => dispatchAttemptId),
299
+ ]).size;
300
+ const hostResources = this.hostResourceSnapshot();
301
+ return {
302
+ activeAttempts,
303
+ preparedWorktreeReadiness: true,
304
+ reservedAttempts: Math.max(0, this.admissions.size - this.active.size),
305
+ waitingAttempts: this.singletonWaiters.length
306
+ + this.parallelProjectWaiters.length
307
+ + this.capacityWaiters.length
308
+ + this.backgroundAdmissionWaiters.length
309
+ + unrepresentedPrepared.length,
310
+ degradedAttempts,
311
+ exclusiveReservations: this.singletonOwners.size,
312
+ sharedReservations: [...this.parallelProjectOwners.values()]
313
+ .reduce((count, owners) => count + owners.size, 0),
314
+ ...hostResources,
315
+ projects: [...projects.values()]
316
+ .map(project => ({
317
+ ...project,
318
+ safetyClass: project.safetyClass ?? "host_serial_only",
319
+ }))
320
+ .sort((left, right) => left.repoId.localeCompare(right.repoId)),
321
+ };
322
+ }
323
+ hasForegroundDemand() {
324
+ return this.preparedReadiness.hasPending()
325
+ || [...this.admissions.values()].some(admission => admission.priority === "foreground")
326
+ || this.singletonWaiters.some(waiter => waiter.priority === "foreground")
327
+ || this.parallelProjectWaiters.some(waiter => waiter.priority === "foreground")
328
+ || this.capacityWaiters.some(waiter => waiter.priority === "foreground");
329
+ }
330
+ async acquirePreparation(dispatch, signal) {
331
+ // Discovery probes already own their verification lifecycle. Analysis-only
332
+ // work never launches the application and must not wait for a runtime wave.
333
+ if (runtimeExecutionClass(dispatch) === "analysis_only" || runtimeAdmissionPriority(dispatch) === "background") {
334
+ return await this.acquire(dispatch, signal);
335
+ }
336
+ return await this.preparedReadiness.acquire(dispatch, signal);
337
+ }
338
+ async acquire(dispatch, signal) {
339
+ const sandbox = dispatch.sandbox;
340
+ if (!sandbox || sandbox.executionMode !== "host") {
341
+ throw new Error("host_runtime_lease_invalid: managed host sandbox context is required.");
342
+ }
343
+ uuidHex(sandbox.dispatchAttemptId, "dispatch attempt");
344
+ uuidHex(dispatch.lease.fencingToken, "fencing token");
345
+ const owner = {
346
+ dispatchAttemptId: sandbox.dispatchAttemptId,
347
+ fencingToken: dispatch.lease.fencingToken,
348
+ };
349
+ const existing = this.admissions.get(sandbox.dispatchAttemptId);
350
+ if (existing) {
351
+ if (existing.fencingToken !== owner.fencingToken) {
352
+ throw new Error("host_runtime_singleton_conflict: dispatch attempt already holds admission under a different fence.");
353
+ }
354
+ return existing.keys;
355
+ }
356
+ const executionClass = runtimeExecutionClass(dispatch);
357
+ const priority = runtimeAdmissionPriority(dispatch);
358
+ const promotion = executionClass === "managed_runtime"
359
+ ? await this.promotionStore.load(dispatch.repo.id)
360
+ : null;
361
+ if (promotion)
362
+ this.knownPromotions.set(promotion.repoId, promotion);
363
+ const projectKey = executionClass === "analysis_only"
364
+ ? projectHostAnalysisKey(dispatch.repo.id)
365
+ : projectHostRuntimeSingletonKey(dispatch.repo.id);
366
+ const useParallelAdmission = priority === "foreground"
367
+ && dispatch.cycleRun.phase !== "prepared_readiness"
368
+ && (promotion !== null || executionClass === "analysis_only");
369
+ let keys = useParallelAdmission ? [] : [projectKey];
370
+ const resources = normalizedHostResourceRequest(sandbox.requestedResources);
371
+ const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
372
+ ? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
373
+ : dispatch.lease.expiresAt;
374
+ const acquisitionKey = hostRuntimeOwnerKey(owner);
375
+ this.acquiringAdmissions.add(acquisitionKey);
376
+ try {
377
+ try {
378
+ if (priority === "background") {
379
+ await this.refreshWorkspaceCapacity();
380
+ keys = await this.reserveBackgroundAdmission(keys, owner, resources, expiresAt, signal);
381
+ }
382
+ else if (useParallelAdmission) {
383
+ await this.reserveParallelProject(projectKey, owner, expiresAt, signal, priority);
384
+ }
385
+ else {
386
+ keys = await this.reserveSingletonKeys(keys, owner, expiresAt, signal, priority);
387
+ }
388
+ if (priority === "foreground") {
389
+ await this.refreshWorkspaceCapacity();
390
+ const capacityExpiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
391
+ ? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
392
+ : expiresAt;
393
+ await this.reserveHostCapacity(owner, resources, capacityExpiresAt, signal, priority);
394
+ }
395
+ }
396
+ catch (error) {
397
+ this.releaseHostCapacity(owner.dispatchAttemptId, owner.fencingToken);
398
+ this.releaseSingletonKeys(owner.dispatchAttemptId, owner.fencingToken);
399
+ this.releaseParallelProjects(owner.dispatchAttemptId, owner.fencingToken);
400
+ this.clearPendingRenewal(owner);
401
+ throw error;
402
+ }
403
+ const admittedPromotion = useParallelAdmission
404
+ ? promotion ?? (executionClass === "managed_runtime" && keys.length === 0
405
+ ? this.knownPromotions.get(dispatch.repo.id) ?? null
406
+ : null)
407
+ : null;
408
+ this.admissions.set(sandbox.dispatchAttemptId, {
409
+ repoId: dispatch.repo.id,
410
+ fencingToken: owner.fencingToken,
411
+ executionClass,
412
+ priority,
413
+ provisionalPromotionFingerprint: admittedPromotion?.runtimeContractFingerprint ?? null,
414
+ keys,
415
+ parallelProjectKey: useParallelAdmission ? projectKey : null,
416
+ });
417
+ return keys;
418
+ }
419
+ finally {
420
+ this.acquiringAdmissions.delete(acquisitionKey);
421
+ }
422
+ }
423
+ async reserveHostCapacity(owner, resources, expiresAt, signal, priority = "foreground") {
424
+ const existing = this.capacityReservations.get(owner.dispatchAttemptId);
425
+ if (existing) {
426
+ if (!sameSingletonOwner(existing.owner, owner)) {
427
+ throw new Error("host_runtime_capacity_conflict: dispatch attempt already holds capacity under a different fence.");
428
+ }
429
+ return;
430
+ }
431
+ if (signal?.aborted)
432
+ throw cancellationError(signal);
433
+ throwIfAdmissionExpired(expiresAt);
434
+ const impossibleReason = this.hostCapacityImpossibleReason(resources);
435
+ if (impossibleReason)
436
+ throw new HostRuntimeCapacityRequestImpossibleError(impossibleReason);
437
+ if (this.capacityWaiters.length === 0 && this.hostCapacityAvailable(resources)) {
438
+ this.capacityReservations.set(owner.dispatchAttemptId, { owner, resources, state: "pending" });
439
+ return;
440
+ }
441
+ return new Promise((resolve, reject) => {
442
+ const waiter = {
443
+ owner,
444
+ resources,
445
+ priority,
446
+ resolve,
447
+ reject,
448
+ signal,
449
+ expiresAt,
450
+ deadlineTimer: null,
451
+ capacityRecheckTimer: null,
452
+ abort: () => {
453
+ if (!this.detachCapacityWaiter(waiter))
454
+ return;
455
+ this.clearPendingRenewal(owner);
456
+ reject(cancellationError(signal));
457
+ this.drainAdmissionWaiters();
458
+ },
459
+ expire: () => {
460
+ if (!this.detachCapacityWaiter(waiter))
461
+ return;
462
+ this.clearPendingRenewal(owner);
463
+ reject(new HostRuntimeAdmissionExpiredError());
464
+ this.drainAdmissionWaiters();
465
+ },
466
+ };
467
+ waiter.deadlineTimer = scheduleAdmissionDeadline(expiresAt, waiter.expire);
468
+ // Memory, CPU and filesystem capacity can recover without one of our leases
469
+ // being released (for example when an unrelated host process exits). Recheck
470
+ // ambient capacity while queued so readiness probes and attempts do not remain
471
+ // stranded behind a stale low-resource sample.
472
+ waiter.capacityRecheckTimer = setInterval(() => this.drainCapacityWaiters(), 1_000);
473
+ waiter.capacityRecheckTimer.unref?.();
474
+ signal?.addEventListener("abort", waiter.abort, { once: true });
475
+ enqueueAdmissionWaiter(this.capacityWaiters, waiter);
476
+ this.drainCapacityWaiters();
477
+ });
478
+ }
479
+ async reserveBackgroundAdmission(keys, owner, resources, expiresAt, signal) {
480
+ if (signal?.aborted)
481
+ throw cancellationError(signal);
482
+ throwIfAdmissionExpired(expiresAt);
483
+ return await new Promise((resolve, reject) => {
484
+ const waiter = {
485
+ owner,
486
+ keys,
487
+ resources,
488
+ priority: "background",
489
+ resolve,
490
+ reject,
491
+ signal,
492
+ expiresAt,
493
+ deadlineTimer: null,
494
+ capacityRecheckTimer: null,
495
+ abort: () => {
496
+ removeWaiter(this.backgroundAdmissionWaiters, waiter);
497
+ clearAdmissionDeadline(waiter);
498
+ clearCapacityRecheck(waiter);
499
+ this.clearPendingRenewal(owner);
500
+ reject(cancellationError(signal));
501
+ this.drainAdmissionWaiters();
502
+ },
503
+ expire: () => {
504
+ removeWaiter(this.backgroundAdmissionWaiters, waiter);
505
+ clearCapacityRecheck(waiter);
506
+ signal?.removeEventListener("abort", waiter.abort);
507
+ this.clearPendingRenewal(owner);
508
+ reject(new HostRuntimeAdmissionExpiredError());
509
+ this.drainAdmissionWaiters();
510
+ },
511
+ };
512
+ waiter.deadlineTimer = scheduleAdmissionDeadline(expiresAt, waiter.expire);
513
+ waiter.capacityRecheckTimer = setInterval(() => this.drainAdmissionWaiters(), 1_000);
514
+ waiter.capacityRecheckTimer.unref?.();
515
+ signal?.addEventListener("abort", waiter.abort, { once: true });
516
+ this.backgroundAdmissionWaiters.push(waiter);
517
+ this.drainAdmissionWaiters();
518
+ });
519
+ }
520
+ hostCapacityAvailable(resources) {
521
+ const capacity = this.hostResourceSnapshot();
522
+ return capacity.availableAttempts >= resources.sandboxSlots
523
+ && capacity.availableMemoryBytes >= resources.memoryBytes
524
+ && capacity.availableCpuMillis >= resources.cpuMillis
525
+ && capacity.availableWorkspaceBytes >= resources.workspaceBytes;
526
+ }
527
+ hostCapacityImpossibleReason(resources) {
528
+ const { hardMaxAttempts, idleHostReserveBytes, cpuReserveMillis } = this.capacityConfiguration;
529
+ if (resources.sandboxSlots > hardMaxAttempts) {
530
+ return `requested ${resources.sandboxSlots} sandbox slots exceed the host maximum of ${hardMaxAttempts}.`;
531
+ }
532
+ const totalMemoryBytes = this.capacityResourceSample?.totalMemoryBytes ?? os.totalmem();
533
+ const maximumMemoryBytes = Math.max(0, totalMemoryBytes - idleHostReserveBytes);
534
+ if (resources.memoryBytes > maximumMemoryBytes) {
535
+ return `requested memory ${resources.memoryBytes} exceeds the host maximum of ${maximumMemoryBytes} bytes after reserve.`;
536
+ }
537
+ const totalCpuMillis = this.capacityResourceSample?.totalCpuMillis ?? os.cpus().length * 1_000;
538
+ const maximumCpuMillis = Math.max(0, totalCpuMillis - cpuReserveMillis);
539
+ if (resources.cpuMillis > maximumCpuMillis) {
540
+ return `requested CPU ${resources.cpuMillis} millis exceeds the host maximum of ${maximumCpuMillis} millis after reserve.`;
541
+ }
542
+ return null;
543
+ }
544
+ hostResourceSnapshot() {
545
+ if (!this.capacityResourceSample) {
546
+ try {
547
+ const stats = statfsSync(this.stateRoot);
548
+ this.workspaceAvailableBytes = Math.max(0, stats.bavail * stats.bsize);
549
+ }
550
+ catch {
551
+ // State root may not exist during the first capability heartbeat. The first
552
+ // admission creates it and refreshes the sample before reserving resources.
553
+ }
554
+ }
555
+ const { hardMaxAttempts, cpuReserveMillis, workspaceReserveBytes } = this.capacityConfiguration;
556
+ const hostReserveBytes = this.capacityReservations.size === 0
557
+ ? this.capacityConfiguration.idleHostReserveBytes
558
+ : this.capacityConfiguration.hostReserveBytes;
559
+ const activeReservationMemoryBytes = [...this.capacityReservations.values()]
560
+ .reduce((total, reservation) => total + reservation.resources.memoryBytes, 0);
561
+ const pendingReservationMemoryBytes = [...this.capacityReservations.values()]
562
+ .filter(reservation => reservation.state === "pending")
563
+ .reduce((total, reservation) => total + reservation.resources.memoryBytes, 0);
564
+ const activeReservationCpuMillis = [...this.capacityReservations.values()]
565
+ .reduce((total, reservation) => total + reservation.resources.cpuMillis, 0);
566
+ const activeReservationWorkspaceBytes = [...this.capacityReservations.values()]
567
+ .reduce((total, reservation) => total + reservation.resources.workspaceBytes, 0);
568
+ const sampledAt = new Date().toISOString();
569
+ const totalMemoryBytes = this.capacityResourceSample?.totalMemoryBytes ?? os.totalmem();
570
+ const rawAvailableMemoryBytes = this.capacityResourceSample?.availableMemoryBytes ?? os.freemem();
571
+ const sampledAvailableMemoryBytes = this.memoryAvailability.observe(rawAvailableMemoryBytes);
572
+ // raw/stabilized available memory already includes every resident host and
573
+ // Forge process. Keep the independent total ceiling scoped to Forge's own
574
+ // reservations; adding ambient use here charges it a second time and can
575
+ // strand work even while more than the requested memory is physically free.
576
+ const committedMemoryBytes = Math.min(totalMemoryBytes, activeReservationMemoryBytes);
577
+ const totalCpuMillis = this.capacityResourceSample?.totalCpuMillis ?? os.cpus().length * 1000;
578
+ const sampledWorkspaceBytes = this.capacityResourceSample?.availableWorkspaceBytes ?? this.workspaceAvailableBytes;
579
+ const memoryAdmission = buildMemoryAdmissionDiagnostics({
580
+ sampledAt,
581
+ totalMemoryBytes,
582
+ rawAvailableMemoryBytes,
583
+ stabilizedAvailableMemoryBytes: sampledAvailableMemoryBytes,
584
+ controlPlaneMemoryBytes: process.memoryUsage().rss,
585
+ reserveMemoryBytes: hostReserveBytes,
586
+ committedMemoryBytes,
587
+ pendingCommitmentMemoryBytes: pendingReservationMemoryBytes,
588
+ });
589
+ const availableMemoryBytes = memoryAdmission.availableForAdmissionBytes;
590
+ const availableCpuMillis = Math.max(0, totalCpuMillis - cpuReserveMillis - activeReservationCpuMillis);
591
+ const availableWorkspaceBytes = Math.max(0, sampledWorkspaceBytes - workspaceReserveBytes - activeReservationWorkspaceBytes);
592
+ const slotAvailability = Math.max(0, hardMaxAttempts - this.capacityReservations.size);
593
+ // Capability snapshots advertise whether any supported attempt can fit.
594
+ // The queue/admission path still checks the exact project-derived request.
595
+ const minimumRequest = minimumSupportedHostResourceRequest();
596
+ const resourceAvailability = Math.min(resourceSlots(availableMemoryBytes, minimumRequest.memoryBytes), resourceSlots(availableCpuMillis, minimumRequest.cpuMillis), resourceSlots(availableWorkspaceBytes, minimumRequest.workspaceBytes));
597
+ const availableAttempts = Math.max(0, Math.min(slotAvailability, resourceAvailability));
598
+ const unavailableReason = slotAvailability <= 0
599
+ ? `Host attempt capacity is full (${this.capacityReservations.size}/${hardMaxAttempts}).`
600
+ : availableMemoryBytes < minimumRequest.memoryBytes
601
+ ? "Host memory is below the admission reserve."
602
+ : availableCpuMillis < minimumRequest.cpuMillis
603
+ ? "Host CPU capacity is below the admission reserve."
604
+ : availableWorkspaceBytes < minimumRequest.workspaceBytes
605
+ ? "Host workspace capacity is below the admission reserve."
606
+ : null;
607
+ return {
608
+ hardMaxAttempts,
609
+ effectiveMaxAttempts: this.capacityReservations.size + availableAttempts,
610
+ availableAttempts,
611
+ totalMemoryBytes,
612
+ availableMemoryBytes,
613
+ hostReserveBytes,
614
+ availableCpuMillis,
615
+ availableWorkspaceBytes,
616
+ activeReservationMemoryBytes,
617
+ memoryAdmission,
618
+ unavailableCode: unavailableReason ? "capacity_unavailable" : null,
619
+ unavailableReason,
620
+ };
621
+ }
622
+ async refreshWorkspaceCapacity() {
623
+ if (this.capacityResourceSample)
624
+ return;
625
+ try {
626
+ await fs.mkdir(this.stateRoot, { recursive: true });
627
+ const stats = await fs.statfs(this.stateRoot);
628
+ this.workspaceAvailableBytes = Math.max(0, stats.bavail * stats.bsize);
629
+ }
630
+ catch {
631
+ // Keep the last trustworthy sample. Memory/CPU and the hard ceiling still
632
+ // protect admission when the filesystem cannot be sampled temporarily.
633
+ }
634
+ }
635
+ releaseHostCapacity(dispatchAttemptId, fencingToken) {
636
+ const reservation = this.capacityReservations.get(dispatchAttemptId);
637
+ if (!reservation || reservation.owner.fencingToken !== fencingToken)
638
+ return;
639
+ this.capacityReservations.delete(dispatchAttemptId);
640
+ this.drainAdmissionWaiters();
641
+ }
642
+ drainCapacityWaiters() {
643
+ for (const waiter of this.capacityWaitersForScan()) {
644
+ if (!this.capacityWaiters.includes(waiter))
645
+ continue;
646
+ if (waiter.signal?.aborted) {
647
+ this.detachCapacityWaiter(waiter);
648
+ this.clearPendingRenewal(waiter.owner);
649
+ waiter.reject(cancellationError(waiter.signal));
650
+ continue;
651
+ }
652
+ if (admissionExpired(waiter.expiresAt)) {
653
+ this.detachCapacityWaiter(waiter);
654
+ this.clearPendingRenewal(waiter.owner);
655
+ waiter.reject(new HostRuntimeAdmissionExpiredError());
656
+ continue;
657
+ }
658
+ const impossibleReason = this.hostCapacityImpossibleReason(waiter.resources);
659
+ if (impossibleReason) {
660
+ this.detachCapacityWaiter(waiter);
661
+ this.clearPendingRenewal(waiter.owner);
662
+ waiter.reject(new HostRuntimeCapacityRequestImpossibleError(impossibleReason));
663
+ continue;
664
+ }
665
+ if (!this.hostCapacityAvailable(waiter.resources))
666
+ continue;
667
+ this.detachCapacityWaiter(waiter);
668
+ this.capacityReservations.set(waiter.owner.dispatchAttemptId, {
669
+ owner: waiter.owner,
670
+ resources: waiter.resources,
671
+ state: "pending",
672
+ });
673
+ waiter.resolve();
674
+ }
675
+ }
676
+ capacityWaitersForScan() {
677
+ const head = this.capacityWaiters[0];
678
+ if (!head) {
679
+ this.capacityWaiterScanCursor = 0;
680
+ return [];
681
+ }
682
+ const scanCount = Math.min(hostCapacityWaiterScanLimit, this.capacityWaiters.length);
683
+ if (scanCount === this.capacityWaiters.length) {
684
+ this.capacityWaiterScanCursor = 0;
685
+ return this.capacityWaiters.slice();
686
+ }
687
+ // The head always gets the first opportunity, while a rotating bounded
688
+ // window prevents an unfit renewed head (or a very deep queue) from
689
+ // indefinitely hiding a later request that fits current capacity.
690
+ const laterCount = this.capacityWaiters.length - 1;
691
+ const laterScanCount = scanCount - 1;
692
+ const start = this.capacityWaiterScanCursor % laterCount;
693
+ const result = [head];
694
+ for (let offset = 0; offset < laterScanCount; offset += 1) {
695
+ result.push(this.capacityWaiters[1 + ((start + offset) % laterCount)]);
696
+ }
697
+ this.capacityWaiterScanCursor = (start + laterScanCount) % laterCount;
698
+ return result;
699
+ }
700
+ detachCapacityWaiter(waiter) {
701
+ const index = this.capacityWaiters.indexOf(waiter);
702
+ if (index < 0)
703
+ return false;
704
+ this.capacityWaiters.splice(index, 1);
705
+ clearAdmissionDeadline(waiter);
706
+ clearCapacityRecheck(waiter);
707
+ waiter.signal?.removeEventListener("abort", waiter.abort);
708
+ if (this.capacityWaiters.length === 0)
709
+ this.capacityWaiterScanCursor = 0;
710
+ return true;
711
+ }
712
+ async reserveSingletonKeys(keys, owner, expiresAt, signal, priority = "foreground") {
713
+ const owned = keys.every(key => sameSingletonOwner(this.singletonOwners.get(key), owner));
714
+ if (owned)
715
+ return keys;
716
+ const staleFence = [...this.singletonOwners.values()].find(existing => existing.dispatchAttemptId === owner.dispatchAttemptId
717
+ && existing.fencingToken !== owner.fencingToken);
718
+ if (staleFence) {
719
+ throw new Error("host_runtime_singleton_conflict: dispatch attempt already holds the singleton under a different fence.");
720
+ }
721
+ if (signal?.aborted)
722
+ throw cancellationError(signal);
723
+ throwIfAdmissionExpired(expiresAt);
724
+ if (this.singletonKeysAvailable(keys)) {
725
+ this.assignSingletonKeys(keys, owner);
726
+ return keys;
727
+ }
728
+ return new Promise((resolve, reject) => {
729
+ const waiter = {
730
+ owner,
731
+ keys,
732
+ priority,
733
+ resolve,
734
+ reject,
735
+ signal,
736
+ expiresAt,
737
+ deadlineTimer: null,
738
+ abort: () => {
739
+ const index = this.singletonWaiters.indexOf(waiter);
740
+ if (index >= 0)
741
+ this.singletonWaiters.splice(index, 1);
742
+ clearAdmissionDeadline(waiter);
743
+ this.clearPendingRenewal(owner);
744
+ reject(cancellationError(signal));
745
+ this.drainAdmissionWaiters();
746
+ },
747
+ expire: () => {
748
+ const index = this.singletonWaiters.indexOf(waiter);
749
+ if (index < 0)
750
+ return;
751
+ this.singletonWaiters.splice(index, 1);
752
+ clearAdmissionDeadline(waiter);
753
+ waiter.signal?.removeEventListener("abort", waiter.abort);
754
+ this.clearPendingRenewal(owner);
755
+ reject(new HostRuntimeAdmissionExpiredError());
756
+ this.drainAdmissionWaiters();
757
+ },
758
+ };
759
+ enqueueAdmissionWaiter(this.singletonWaiters, waiter);
760
+ signal?.addEventListener("abort", waiter.abort, { once: true });
761
+ waiter.deadlineTimer = scheduleAdmissionDeadline(waiter.expiresAt, waiter.expire);
762
+ this.drainAdmissionWaiters();
763
+ });
764
+ }
765
+ async reserveParallelProject(key, owner, expiresAt, signal, priority = "foreground") {
766
+ if (this.parallelProjectOwned(key, owner))
767
+ return;
768
+ const staleFence = [...(this.parallelProjectOwners.get(key)?.values() ?? [])]
769
+ .find(existing => existing.dispatchAttemptId === owner.dispatchAttemptId
770
+ && existing.fencingToken !== owner.fencingToken);
771
+ if (staleFence) {
772
+ throw new Error("host_runtime_parallel_conflict: dispatch attempt already holds shared admission under a different fence.");
773
+ }
774
+ if (signal?.aborted)
775
+ throw cancellationError(signal);
776
+ throwIfAdmissionExpired(expiresAt);
777
+ if (this.parallelProjectAvailable(key, priority)) {
778
+ this.assignParallelProject(key, owner);
779
+ return;
780
+ }
781
+ await new Promise((resolve, reject) => {
782
+ const waiter = {
783
+ owner,
784
+ key,
785
+ priority,
786
+ resolve,
787
+ reject,
788
+ signal,
789
+ expiresAt,
790
+ deadlineTimer: null,
791
+ abort: () => {
792
+ const index = this.parallelProjectWaiters.indexOf(waiter);
793
+ if (index >= 0)
794
+ this.parallelProjectWaiters.splice(index, 1);
795
+ clearAdmissionDeadline(waiter);
796
+ this.clearPendingRenewal(owner);
797
+ reject(cancellationError(signal));
798
+ this.drainAdmissionWaiters();
799
+ },
800
+ expire: () => {
801
+ const index = this.parallelProjectWaiters.indexOf(waiter);
802
+ if (index < 0)
803
+ return;
804
+ this.parallelProjectWaiters.splice(index, 1);
805
+ clearAdmissionDeadline(waiter);
806
+ waiter.signal?.removeEventListener("abort", waiter.abort);
807
+ this.clearPendingRenewal(owner);
808
+ reject(new HostRuntimeAdmissionExpiredError());
809
+ this.drainAdmissionWaiters();
810
+ },
811
+ };
812
+ enqueueAdmissionWaiter(this.parallelProjectWaiters, waiter);
813
+ signal?.addEventListener("abort", waiter.abort, { once: true });
814
+ waiter.deadlineTimer = scheduleAdmissionDeadline(waiter.expiresAt, waiter.expire);
815
+ this.drainAdmissionWaiters();
816
+ });
817
+ }
818
+ async upgradeParallelProjectToSingleton(keys, parallelProjectKey, owner, expiresAt, signal, priority = "foreground") {
819
+ if (!this.parallelProjectOwned(parallelProjectKey, owner)) {
820
+ throw new Error("host_runtime_parallel_unreserved: matching shared project admission is required before serialization.");
821
+ }
822
+ if (signal?.aborted)
823
+ throw cancellationError(signal);
824
+ throwIfAdmissionExpired(expiresAt);
825
+ return await new Promise((resolve, reject) => {
826
+ const waiter = {
827
+ owner,
828
+ keys,
829
+ priority,
830
+ resolve,
831
+ reject,
832
+ signal,
833
+ expiresAt,
834
+ deadlineTimer: null,
835
+ abort: () => {
836
+ const index = this.singletonWaiters.indexOf(waiter);
837
+ if (index >= 0)
838
+ this.singletonWaiters.splice(index, 1);
839
+ clearAdmissionDeadline(waiter);
840
+ this.clearPendingRenewal(owner);
841
+ reject(cancellationError(signal));
842
+ this.drainAdmissionWaiters();
843
+ },
844
+ expire: () => {
845
+ const index = this.singletonWaiters.indexOf(waiter);
846
+ if (index < 0)
847
+ return;
848
+ this.singletonWaiters.splice(index, 1);
849
+ clearAdmissionDeadline(waiter);
850
+ waiter.signal?.removeEventListener("abort", waiter.abort);
851
+ this.clearPendingRenewal(owner);
852
+ reject(new HostRuntimeAdmissionExpiredError());
853
+ this.drainAdmissionWaiters();
854
+ },
855
+ };
856
+ // Queue the exclusive claim before relinquishing the shared claim. This
857
+ // makes the promotion downgrade atomic from the broker's perspective and
858
+ // prevents a background readiness probe from jumping ahead in between.
859
+ enqueueAdmissionWaiter(this.singletonWaiters, waiter);
860
+ signal?.addEventListener("abort", waiter.abort, { once: true });
861
+ waiter.deadlineTimer = scheduleAdmissionDeadline(waiter.expiresAt, waiter.expire);
862
+ this.releaseParallelProjectNoDrain(parallelProjectKey, owner);
863
+ this.drainAdmissionWaiters();
864
+ });
865
+ }
866
+ async start(dispatch, worktreePath, signal, onAdmissionCheckpoint) {
867
+ const sandbox = dispatch.sandbox;
868
+ if (!sandbox || sandbox.executionMode !== "host") {
869
+ throw new Error("host_runtime_lease_invalid: managed host sandbox context is required.");
870
+ }
871
+ const wasPrepared = this.preparedReadiness.has(sandbox.dispatchAttemptId);
872
+ const preparedProof = wasPrepared
873
+ ? await this.preparedReadiness.verify(dispatch, worktreePath, onAdmissionCheckpoint)
874
+ : undefined;
875
+ if (wasPrepared) {
876
+ signal?.throwIfAborted();
877
+ const admission = this.hostResourceSnapshot().memoryAdmission;
878
+ const requestedMemoryBytes = normalizedHostResourceRequest(sandbox.requestedResources).memoryBytes;
879
+ await onAdmissionCheckpoint?.({
880
+ code: "host_runtime_prepared_admission_waiting",
881
+ message: `Prepared runtime verification passed. Waiting for host admission before agent startup: ${formatBytes(requestedMemoryBytes)} requested, ${formatBytes(admission.availableForAdmissionBytes)} currently available under the host reserve.`,
882
+ expiresAt: dispatch.lease.expiresAt,
883
+ });
884
+ await this.acquire(dispatch, signal);
885
+ await onAdmissionCheckpoint?.({
886
+ code: "host_runtime_prepared_admission_acquired",
887
+ message: "Prepared runtime acquired host admission; agent startup is continuing.",
888
+ expiresAt: dispatch.lease.expiresAt,
889
+ });
890
+ }
891
+ const existing = this.active.get(sandbox.dispatchAttemptId);
892
+ if (existing) {
893
+ if (existing.lease.fencingToken !== dispatch.lease.fencingToken) {
894
+ throw new Error("host_runtime_lease_conflict: an active host lease has a different fence.");
895
+ }
896
+ return {
897
+ lease: existing.lease,
898
+ environment: hostRuntimeEnvironment(dispatch.session.id, existing.lease, existing.runtimeContract, existing.executionClass, existing.databaseCapability),
899
+ runtimeContract: existing.runtimeContract,
900
+ executionClass: existing.executionClass,
901
+ };
902
+ }
903
+ const admission = this.admissions.get(sandbox.dispatchAttemptId);
904
+ const owner = { dispatchAttemptId: sandbox.dispatchAttemptId, fencingToken: dispatch.lease.fencingToken };
905
+ if (!admission || admission.fencingToken !== owner.fencingToken) {
906
+ throw new Error("host_runtime_singleton_unreserved: managed host admission is required before start.");
907
+ }
908
+ const attemptHex = uuidHex(sandbox.dispatchAttemptId, "dispatch attempt");
909
+ const compactAttemptHex = compactHostAttemptId(sandbox.dispatchAttemptId);
910
+ const normalizedWorktreePath = await resolveExistingDirectoryPath(worktreePath);
911
+ const attemptRoot = path.join(this.executionRoot, "a", compactAttemptHex);
912
+ const temporaryPath = path.join(attemptRoot, "t");
913
+ const logPath = path.join(attemptRoot, "logs");
914
+ const cachePath = path.join(attemptRoot, "c");
915
+ const browserProfilePath = path.join(attemptRoot, "b");
916
+ const testOutputPath = path.join(attemptRoot, "test-output");
917
+ const processSupervisorMetadataPath = path.join(logPath, "attempt-process-supervisor.json");
918
+ const processSupervisorId = `host-${attemptHex}`;
919
+ const renewedExpiry = this.pendingRenewals.get(sandbox.dispatchAttemptId);
920
+ const expiresAt = renewedExpiry?.fencingToken === dispatch.lease.fencingToken
921
+ ? renewedExpiry.expiresAt
922
+ : dispatch.lease.expiresAt;
923
+ let processSupervisor = null;
924
+ let attemptRootIdentity = null;
925
+ try {
926
+ const runtimeContract = await loadHostRuntimeContract(normalizedWorktreePath);
927
+ if (runtimeContract.contract.runtime.hostConcurrency === "disabled") {
928
+ throw new Error("host_runtime_disabled: project runtime configuration requires Docker execution.");
929
+ }
930
+ if (wasPrepared && !preparedProof && admission.executionClass === "managed_runtime"
931
+ && hostRuntimeParallelEligible(runtimeContract)) {
932
+ throw new Error("host_runtime_prepared_source_changed: runtime became parallel-eligible after preparation; a new foreground proof is required.");
933
+ }
934
+ const runtimeContractFingerprint = hostRuntimeContractFingerprint(runtimeContract);
935
+ const inputCommitSha = preparedProof?.inputCommitSha
936
+ ?? await resolveWorktreeCommitSha(normalizedWorktreePath, sandbox.inputCommitSha);
937
+ if (preparedProof && (runtimeContractFingerprint !== preparedProof.runtimeContractFingerprint
938
+ || JSON.stringify(preparedProof) !== JSON.stringify(await preparedRuntimeIdentity(normalizedWorktreePath, signal)))) {
939
+ throw new Error("host_runtime_prepared_source_changed: source changed after its foreground proof; agent was not started.");
940
+ }
941
+ const promotion = admission.executionClass === "managed_runtime"
942
+ ? await this.promotionStore.load(dispatch.repo.id, preparedProof ?? { runtimeContractFingerprint, inputCommitSha })
943
+ : null;
944
+ const promotionMatches = hostRuntimeParallelEligible(runtimeContract)
945
+ && promotion?.runtimeContractFingerprint === runtimeContractFingerprint
946
+ && promotion.inputCommitSha === inputCommitSha;
947
+ if (admission.executionClass === "managed_runtime" && admission.keys.length === 0 && !promotionMatches) {
948
+ const projectKeys = hostRuntimeSingletonKeys(dispatch);
949
+ const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
950
+ ? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
951
+ : dispatch.lease.expiresAt;
952
+ const parallelProjectKey = admission.parallelProjectKey;
953
+ admission.parallelProjectKey = null;
954
+ const capacityReservation = this.capacityReservations.get(owner.dispatchAttemptId);
955
+ if (capacityReservation && !sameSingletonOwner(capacityReservation.owner, owner)) {
956
+ throw new Error("host_runtime_capacity_conflict: dispatch attempt holds capacity under a different fence.");
957
+ }
958
+ const serializedAdmission = parallelProjectKey
959
+ ? this.upgradeParallelProjectToSingleton(projectKeys, parallelProjectKey, owner, expiresAt, signal, admission.priority)
960
+ : this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal, admission.priority);
961
+ // A sibling can already hold the same provisional shared project claim
962
+ // while waiting for this attempt's host-capacity slot. Retaining capacity
963
+ // while waiting for that sibling to release its shared claim creates a
964
+ // cycle: exclusive waits on shared, shared waits on capacity. Relinquish
965
+ // the capacity slot only after the exclusive waiter is queued, then
966
+ // reacquire it after serialization is granted.
967
+ if (capacityReservation) {
968
+ this.releaseHostCapacity(owner.dispatchAttemptId, owner.fencingToken);
969
+ }
970
+ // The checkpoint is allowed to do I/O. Observe an early admission
971
+ // failure now so a slow acknowledgement cannot create an unhandled
972
+ // rejection before the operational promise is awaited below.
973
+ void serializedAdmission.catch(() => undefined);
974
+ // Begin the operational downgrade before publishing telemetry. A lost
975
+ // callback acknowledgement must not leave the attempt holding its old
976
+ // shared admission while every recovery probe waits behind it.
977
+ await onAdmissionCheckpoint?.({
978
+ code: "host_runtime_serial_admission_waiting",
979
+ message: "The prepared runtime contract no longer matches the parallel-readiness proof. Waiting for foreground-priority serialized admission before launch.",
980
+ expiresAt,
981
+ });
982
+ admission.keys = await serializedAdmission;
983
+ while (admission.keys.length === 0) {
984
+ const promoted = this.knownPromotions.get(dispatch.repo.id);
985
+ if (!promoted) {
986
+ throw new Error("host_runtime_parallel_promotion_missing: shared admission requires matching promotion evidence.");
987
+ }
988
+ const matching = await this.promotionStore.load(dispatch.repo.id, preparedProof ?? { runtimeContractFingerprint, inputCommitSha });
989
+ if (!matching) {
990
+ // Drop obsolete proof before asking for exclusive admission; otherwise
991
+ // the queue can grant the same stale shared promotion indefinitely.
992
+ this.releaseParallelProjectNoDrain(projectKeys[0], owner);
993
+ admission.keys = await this.reserveSingletonKeys(projectKeys, owner, expiresAt, signal, admission.priority);
994
+ continue;
995
+ }
996
+ admission.parallelProjectKey = projectKeys[0] ?? null;
997
+ admission.provisionalPromotionFingerprint = promoted.runtimeContractFingerprint;
998
+ break;
999
+ }
1000
+ // Do not hold a capacity slot while a stale shared re-grant is still
1001
+ // being converted to exclusive admission.
1002
+ if (capacityReservation) {
1003
+ await this.reserveHostCapacity(owner, capacityReservation.resources, expiresAt, signal, admission.priority);
1004
+ }
1005
+ }
1006
+ const declaredSingletonKeys = runtimeContract.contract.runtime.singletonResources
1007
+ .map(hostRuntimeDeclaredSingletonKey);
1008
+ const missingSingletonKeys = declaredSingletonKeys
1009
+ .filter(key => !admission.keys.includes(key));
1010
+ if (missingSingletonKeys.length > 0) {
1011
+ const expiresAt = this.pendingRenewals.get(sandbox.dispatchAttemptId)?.fencingToken === owner.fencingToken
1012
+ ? this.pendingRenewals.get(sandbox.dispatchAttemptId).expiresAt
1013
+ : dispatch.lease.expiresAt;
1014
+ const reservedSingletonKeys = await this.reserveSingletonKeys(missingSingletonKeys, owner, expiresAt, signal, admission.priority);
1015
+ admission.keys = [...admission.keys, ...reservedSingletonKeys]
1016
+ .sort((left, right) => left.localeCompare(right));
1017
+ }
1018
+ const singletonKeys = admission.keys;
1019
+ if (!singletonKeys.every(key => sameSingletonOwner(this.singletonOwners.get(key), owner))) {
1020
+ throw new Error("host_runtime_singleton_unreserved: managed host singleton ownership is required before start.");
1021
+ }
1022
+ if (admission.parallelProjectKey && !this.parallelProjectOwned(admission.parallelProjectKey, owner)) {
1023
+ throw new Error("host_runtime_parallel_unreserved: matching shared project admission is required before start.");
1024
+ }
1025
+ await createCanonicalDirectory(attemptRoot);
1026
+ attemptRootIdentity = (await assertCanonicalDirectory(attemptRoot));
1027
+ await Promise.all([
1028
+ createCanonicalDirectory(temporaryPath),
1029
+ createCanonicalDirectory(logPath),
1030
+ createCanonicalDirectory(cachePath),
1031
+ createCanonicalDirectory(browserProfilePath),
1032
+ createCanonicalDirectory(testOutputPath),
1033
+ ]);
1034
+ await assertHostAttemptRootIdentity(attemptRoot, attemptRootIdentity);
1035
+ await resetGeneratedOutputs(normalizedWorktreePath, testOutputPath);
1036
+ let lease = createRuntimeIsolationLease({
1037
+ leaseId: sandbox.executionLeaseId,
1038
+ dispatchAttemptId: sandbox.dispatchAttemptId,
1039
+ executionLeaseId: sandbox.executionLeaseId,
1040
+ fencingToken: dispatch.lease.fencingToken,
1041
+ runnerId: dispatch.execution.runnerId ?? "",
1042
+ runnerInstanceId: sandbox.runnerInstanceId,
1043
+ worktreeKey: dispatch.lease.worktreeKey,
1044
+ worktreePath: normalizedWorktreePath,
1045
+ mode: "host",
1046
+ expiresAt,
1047
+ resources: {
1048
+ memoryReservationBytes: 0,
1049
+ cpuMillis: 0,
1050
+ pids: 0,
1051
+ ports: [],
1052
+ paths: [
1053
+ { path: normalizedWorktreePath, role: "worktree" },
1054
+ { path: temporaryPath, role: "temporary" },
1055
+ { path: logPath, role: "logs" },
1056
+ { path: cachePath, role: "cache" },
1057
+ { path: browserProfilePath, role: "browser_profile" },
1058
+ { path: testOutputPath, role: "test_output" },
1059
+ { path: processSupervisorMetadataPath, role: "process_supervisor_metadata" },
1060
+ ],
1061
+ processSupervisorId,
1062
+ dockerResources: [],
1063
+ singletonKeys,
1064
+ },
1065
+ });
1066
+ await this.writeLedger(lease, undefined, attemptRootIdentity);
1067
+ lease = transitionRuntimeIsolationLease(lease, "starting");
1068
+ await this.writeLedger(lease, undefined, attemptRootIdentity);
1069
+ lease = transitionRuntimeIsolationLease(lease, "active");
1070
+ await this.writeLedger(lease, undefined, attemptRootIdentity);
1071
+ processSupervisor = this.processSupervisors.register({
1072
+ processSupervisorId,
1073
+ dispatchAttemptId: lease.dispatchAttemptId,
1074
+ executionLeaseId: lease.executionLeaseId,
1075
+ fencingToken: lease.fencingToken,
1076
+ logDirectory: logPath,
1077
+ }, this.processRunner, { signal });
1078
+ this.active.set(sandbox.dispatchAttemptId, {
1079
+ lease,
1080
+ repoId: dispatch.repo.id,
1081
+ sessionId: dispatch.session.id,
1082
+ attemptRoot,
1083
+ attemptRootIdentity,
1084
+ runtimeContract,
1085
+ runtimeContractFingerprint,
1086
+ inputCommitSha,
1087
+ executionClass: admission.executionClass,
1088
+ parallelPromotionEligible: hostRuntimeParallelEligible(runtimeContract) && dispatch.cycleRun.phase !== "prepared_readiness",
1089
+ processSupervisor,
1090
+ adapterCleanups: [],
1091
+ pendingAdapterLaunches: new Map(),
1092
+ runtimeResourcesReleased: false,
1093
+ releasing: false,
1094
+ });
1095
+ this.pendingRenewals.delete(sandbox.dispatchAttemptId);
1096
+ return {
1097
+ lease,
1098
+ environment: hostRuntimeEnvironment(dispatch.session.id, lease, runtimeContract, admission.executionClass),
1099
+ runtimeContract,
1100
+ executionClass: admission.executionClass,
1101
+ };
1102
+ }
1103
+ catch (error) {
1104
+ this.pendingRenewals.delete(sandbox.dispatchAttemptId);
1105
+ if (processSupervisor) {
1106
+ await processSupervisor.stopAll(dispatch.lease.fencingToken, "Host runtime start rolled back.").catch(() => undefined);
1107
+ this.processSupervisors.unregister(sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
1108
+ }
1109
+ const attemptRemoved = await retainHostDiagnostics(this.stateRoot, attemptRoot, sandbox.dispatchAttemptId, attemptRootIdentity, this.executionRoot).then(async () => {
1110
+ await removeHostAttemptRoot(attemptRoot);
1111
+ return true;
1112
+ }).catch(() => false); // A retention failure preserves the original evidence and ownership ledger.
1113
+ if (attemptRemoved) {
1114
+ await fs.rm(this.ledgerPath(sandbox.dispatchAttemptId), { force: true }).catch(() => undefined);
1115
+ }
1116
+ this.releaseSingletonKeys(sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
1117
+ this.releaseParallelProjects(sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
1118
+ this.deleteAdmission(sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
1119
+ throw error;
1120
+ }
1121
+ }
1122
+ async updateLease(dispatchAttemptId, fencingToken, expiresAt) {
1123
+ try {
1124
+ uuidHex(dispatchAttemptId, "dispatch attempt");
1125
+ uuidHex(fencingToken, "fencing token");
1126
+ }
1127
+ catch {
1128
+ return;
1129
+ }
1130
+ await this.preparedReadiness.renew(dispatchAttemptId, fencingToken, expiresAt);
1131
+ const active = this.active.get(dispatchAttemptId);
1132
+ if (!active) {
1133
+ if (Number.isFinite(Date.parse(expiresAt))) {
1134
+ const pending = this.pendingRenewals.get(dispatchAttemptId);
1135
+ if (!pending
1136
+ || pending.fencingToken !== fencingToken
1137
+ || Date.parse(expiresAt) > Date.parse(pending.expiresAt)) {
1138
+ this.pendingRenewals.set(dispatchAttemptId, { fencingToken, expiresAt });
1139
+ }
1140
+ this.renewAdmissionWaiterDeadline(dispatchAttemptId, fencingToken, expiresAt);
1141
+ }
1142
+ return;
1143
+ }
1144
+ if (active.releasing || active.lease.state === "releasing" || active.lease.state === "released")
1145
+ return;
1146
+ const renewed = renewRuntimeIsolationLease(active.lease, fencingToken, expiresAt);
1147
+ if (!renewed)
1148
+ return;
1149
+ active.lease = renewed;
1150
+ await this.writeLedger(renewed);
1151
+ }
1152
+ async startConfiguredProject(dispatchAttemptId, fencingToken, signal, onProjectCheckpoint) {
1153
+ const active = this.active.get(dispatchAttemptId);
1154
+ if (!active || active.lease.fencingToken !== fencingToken || active.lease.state !== "active" || active.releasing) {
1155
+ throw new Error("host_runtime_project_unavailable: an active matching host runtime lease is required.");
1156
+ }
1157
+ if (active.executionClass === "analysis_only") {
1158
+ return {
1159
+ lease: active.lease,
1160
+ environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass, active.databaseCapability),
1161
+ runtimeContract: active.runtimeContract,
1162
+ executionClass: active.executionClass,
1163
+ };
1164
+ }
1165
+ const configuredPorts = active.runtimeContract.contract.runtime.ports.filter((port) => port.launch !== null);
1166
+ if (configuredPorts.length === 0) {
1167
+ return {
1168
+ lease: active.lease,
1169
+ environment: hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass, active.databaseCapability),
1170
+ runtimeContract: active.runtimeContract,
1171
+ executionClass: active.executionClass,
1172
+ };
1173
+ }
1174
+ const owner = hostPortOwner(active.lease);
1175
+ const logDirectory = active.lease.resources.paths.find(entry => entry.role === "logs")?.path;
1176
+ if (!logDirectory)
1177
+ throw new Error("host_runtime_project_invalid: attempt log directory is unavailable.");
1178
+ if (this.databaseBroker && !active.databaseCapability
1179
+ && configuredPorts.some(port => /^dotnet(?:\.exe)?$/iu.test(path.basename(port.launch.command)))) {
1180
+ // An ASP.NET runtime may require an isolated database (Forge.Api refuses to start in a
1181
+ // managed host attempt without one). Issue it before the first launch. Release waits for
1182
+ // an in-flight provision, so a database issued during release is still dropped.
1183
+ const provision = this.databaseBroker.provision(dispatchAttemptId, fencingToken, signal)
1184
+ .then(capability => {
1185
+ active.databaseCapability = capability;
1186
+ return capability;
1187
+ });
1188
+ active.databaseProvision = provision;
1189
+ await provision;
1190
+ if (active.releasing) {
1191
+ throw new Error("host_runtime_project_unavailable: the host runtime lease was released during database provisioning.");
1192
+ }
1193
+ await onProjectCheckpoint?.({
1194
+ code: "host_runtime_database_provisioned",
1195
+ message: `Provisioned isolated attempt database ${active.databaseCapability.databaseName} on the native test cluster.`,
1196
+ data: { database: active.databaseCapability.databaseName },
1197
+ });
1198
+ }
1199
+ // Runtime output is persisted to attempt logs; the database login it may print is redacted.
1200
+ const capabilitySecrets = attemptDatabaseCapabilitySecrets(active.databaseCapability?.connectionString);
1201
+ const launcher = new AttemptNodeRuntimeLauncher(active.processSupervisor, owner, logDirectory, this.listenerInspector, capabilitySecrets);
1202
+ for (const configuredPort of configuredPorts) {
1203
+ const workingDirectory = await containedWorkingDirectory(active.lease.worktreePath, configuredPort.launch.workingDirectory);
1204
+ const buildEnvironment = {
1205
+ ...configuredPort.launch.environment,
1206
+ ...hostRuntimeEnvironment(active.sessionId, active.lease, active.runtimeContract, active.executionClass, active.databaseCapability),
1207
+ };
1208
+ const prebuild = createAspNetRuntimePrebuildPlan({
1209
+ command: configuredPort.launch.command,
1210
+ args: configuredPort.launch.args,
1211
+ cwd: workingDirectory,
1212
+ worktreePath: active.lease.worktreePath,
1213
+ environment: buildEnvironment,
1214
+ inputCommitSha: active.inputCommitSha,
1215
+ runtimeContractFingerprint: active.runtimeContractFingerprint,
1216
+ });
1217
+ if (prebuild) {
1218
+ await onProjectCheckpoint?.({
1219
+ code: "host_runtime_build_started",
1220
+ message: `Preparing the ASP.NET runtime for commit ${active.inputCommitSha.slice(0, 12)} before binding its assigned port.`,
1221
+ data: { input_commit_sha: active.inputCommitSha, project_path: prebuild.projectPath },
1222
+ });
1223
+ const build = await ensureAspNetRuntimePrebuilt(prebuild, async (plan, buildSignal) => await executeAspNetRuntimeBuild(active.processSupervisor, logDirectory, plan, buildSignal, capabilitySecrets), signal);
1224
+ await onProjectCheckpoint?.({
1225
+ code: build.cacheHit ? "host_runtime_build_cached" : "host_runtime_build_completed",
1226
+ message: build.cacheHit
1227
+ ? `Reused the verified ASP.NET build for commit ${active.inputCommitSha.slice(0, 12)}.`
1228
+ : `ASP.NET runtime build completed for commit ${active.inputCommitSha.slice(0, 12)}; launching without another build.`,
1229
+ data: {
1230
+ input_commit_sha: active.inputCommitSha,
1231
+ build_fingerprint: build.fingerprint,
1232
+ receipt_path: build.receiptPath,
1233
+ },
1234
+ });
1235
+ }
1236
+ await this.launchNodeRuntime(dispatchAttemptId, fencingToken, configuredPort.name, {
1237
+ command: configuredPort.launch.command,
1238
+ args: configuredPort.launch.args,
1239
+ cwd: workingDirectory,
1240
+ environment: {
1241
+ ...buildEnvironment,
1242
+ },
1243
+ }, launcher, signal);
1244
+ }
1245
+ const promotedLease = await this.promoteVerifiedRuntime(active, active.lease);
1246
+ const runtimeEnvironment = hostRuntimeEnvironment(active.sessionId, promotedLease, active.runtimeContract, active.executionClass, active.databaseCapability);
1247
+ const playwright = active.runtimeContract.contract.runtime.playwright;
1248
+ if (!playwright) {
1249
+ return {
1250
+ lease: promotedLease,
1251
+ environment: runtimeEnvironment,
1252
+ runtimeContract: active.runtimeContract,
1253
+ executionClass: active.executionClass,
1254
+ };
1255
+ }
1256
+ const playwrightPort = promotedLease.resources.ports.find(port => port.name === playwright.baseUrlPort);
1257
+ const playwrightPortConfig = active.runtimeContract.contract.runtime.ports
1258
+ .find(port => port.name === playwright.baseUrlPort);
1259
+ if (!playwrightPort || !playwrightPortConfig?.baseUrlEnv) {
1260
+ throw new Error("host_runtime_playwright_unavailable: the configured verified base URL is unavailable.");
1261
+ }
1262
+ if (playwrightPort.hostIp !== "127.0.0.1") {
1263
+ throw new Error("host_runtime_playwright_unavailable: Playwright requires an IPv4 loopback listener.");
1264
+ }
1265
+ const baseUrl = hostPortEnvironment(playwrightPortConfig, playwrightPort.hostIp, playwrightPort.hostPort)[playwrightPortConfig.baseUrlEnv];
1266
+ if (!baseUrl)
1267
+ throw new Error("host_runtime_playwright_unavailable: the configured base URL could not be rendered.");
1268
+ const pathFor = (role) => promotedLease.resources.paths.find(entry => entry.role === role)?.path ?? "";
1269
+ const preparedPlaywright = await this.playwrightAdapter.prepare(playwright, {
1270
+ worktreePath: promotedLease.worktreePath,
1271
+ temporaryDirectory: pathFor("temporary"),
1272
+ testOutputDirectory: pathFor("test_output"),
1273
+ browserProfileDirectory: pathFor("browser_profile"),
1274
+ baseUrl,
1275
+ ownershipToken: uuidHex(promotedLease.dispatchAttemptId, "dispatch attempt"),
1276
+ });
1277
+ return {
1278
+ lease: promotedLease,
1279
+ environment: { ...runtimeEnvironment, ...preparedPlaywright.environment },
1280
+ runtimeContract: active.runtimeContract,
1281
+ executionClass: active.executionClass,
1282
+ };
1283
+ }
1284
+ providerProcessRunner(dispatchAttemptId, fencingToken) {
1285
+ const active = this.active.get(dispatchAttemptId);
1286
+ if (!active || active.lease.fencingToken !== fencingToken || active.releasing)
1287
+ return null;
1288
+ return new AttemptSupervisedProviderProcessRunner(active.processSupervisor, hostPortOwner(active.lease), this.listenerInspector, active.executionClass === "analysis_only", () => this.markCapacityReservationRunning(dispatchAttemptId, fencingToken));
1289
+ }
1290
+ markCapacityReservationRunning(dispatchAttemptId, fencingToken) {
1291
+ const reservation = this.capacityReservations.get(dispatchAttemptId);
1292
+ if (reservation?.owner.fencingToken === fencingToken)
1293
+ reservation.state = "running";
1294
+ }
1295
+ providerMemoryTelemetry(dispatchAttemptId, fencingToken) {
1296
+ const active = this.active.get(dispatchAttemptId);
1297
+ if (!active || active.lease.fencingToken !== fencingToken || active.releasing)
1298
+ return null;
1299
+ return active.processSupervisor.providerMemoryTelemetry();
1300
+ }
1301
+ async promoteVerifiedRuntime(active, runtimeLease) {
1302
+ if (!active.parallelPromotionEligible)
1303
+ return runtimeLease;
1304
+ const verifiedPortNames = runtimeLease.resources.ports.map(port => port.name).sort((left, right) => left.localeCompare(right));
1305
+ const requiredPortNames = configuredLaunchPortNames(active.runtimeContract);
1306
+ if (requiredPortNames.length !== verifiedPortNames.length
1307
+ || requiredPortNames.some((name, index) => name !== verifiedPortNames[index])) {
1308
+ return runtimeLease;
1309
+ }
1310
+ const projectKey = projectHostRuntimeSingletonKey(active.repoId);
1311
+ const promotedLease = withRuntimeIsolationResources(runtimeLease, {
1312
+ ...runtimeLease.resources,
1313
+ singletonKeys: runtimeLease.resources.singletonKeys.filter(key => key !== projectKey),
1314
+ });
1315
+ await this.writeLedger(promotedLease);
1316
+ active.lease = promotedLease;
1317
+ try {
1318
+ const promotion = await this.promotionStore.observe(active.repoId, active.runtimeContract, runtimeLease.dispatchAttemptId, verifiedPortNames, active.inputCommitSha);
1319
+ this.knownPromotions.set(promotion.repoId, promotion);
1320
+ const owner = {
1321
+ dispatchAttemptId: runtimeLease.dispatchAttemptId,
1322
+ fencingToken: runtimeLease.fencingToken,
1323
+ };
1324
+ if (!this.parallelProjectOwned(projectKey, owner)) {
1325
+ this.convertSingletonToParallelProject(projectKey, owner, promotion);
1326
+ }
1327
+ const admission = this.admissions.get(runtimeLease.dispatchAttemptId);
1328
+ if (admission?.fencingToken === runtimeLease.fencingToken) {
1329
+ this.admissions.set(runtimeLease.dispatchAttemptId, {
1330
+ ...admission,
1331
+ keys: promotedLease.resources.singletonKeys,
1332
+ provisionalPromotionFingerprint: promotion.runtimeContractFingerprint,
1333
+ parallelProjectKey: projectKey,
1334
+ });
1335
+ }
1336
+ return promotedLease;
1337
+ }
1338
+ catch {
1339
+ await this.invalidatePromotion(active.repoId).catch(() => undefined);
1340
+ active.lease = runtimeLease;
1341
+ await this.writeLedger(runtimeLease);
1342
+ return runtimeLease;
1343
+ }
1344
+ }
1345
+ async launchNodeRuntime(dispatchAttemptId, fencingToken, portName, command, launcher, signal) {
1346
+ const active = this.active.get(dispatchAttemptId);
1347
+ if (!active || active.lease.fencingToken !== fencingToken || active.lease.state !== "active" || active.releasing) {
1348
+ throw new Error("host_runtime_adapter_unavailable: an active matching host runtime lease is required.");
1349
+ }
1350
+ const portConfig = active.runtimeContract.contract.runtime.ports.find(port => port.name === portName);
1351
+ if (!portConfig)
1352
+ throw new Error(`host_runtime_adapter_invalid: configured port '${portName}' was not found.`);
1353
+ if (active.lease.resources.ports.some(port => port.name === portName) || active.pendingAdapterLaunches.has(portName)) {
1354
+ throw new Error(`host_runtime_adapter_conflict: configured port '${portName}' is already active.`);
1355
+ }
1356
+ const controller = linkedAbortController(signal);
1357
+ let retainedByActiveRuntime = false;
1358
+ const operation = (async () => {
1359
+ const owner = hostPortOwner(active.lease);
1360
+ const launched = await this.nodeAdapter.launch(owner, portConfig, command, launcher, controller.signal);
1361
+ let cleaned = false;
1362
+ const cleanup = async () => {
1363
+ if (cleaned)
1364
+ return;
1365
+ await launcher.stop(launched.value);
1366
+ await this.portBroker.release(launched.reservation.reservationId, owner);
1367
+ cleaned = true;
1368
+ unlinkAbortController(controller, signal);
1369
+ };
1370
+ active.adapterCleanups.push(cleanup);
1371
+ try {
1372
+ if (active.releasing)
1373
+ throw new Error("host_runtime_adapter_cancelled: host runtime release started during launch.");
1374
+ const runtimeLease = withRuntimeIsolationResources(active.lease, {
1375
+ ...active.lease.resources,
1376
+ ports: [...active.lease.resources.ports, {
1377
+ name: launched.reservation.name,
1378
+ protocol: launched.reservation.protocol,
1379
+ hostIp: launched.reservation.hostIp,
1380
+ hostPort: launched.reservation.hostPort,
1381
+ targetPort: launched.reservation.targetPort,
1382
+ }],
1383
+ });
1384
+ await this.writeLedger(runtimeLease);
1385
+ active.lease = runtimeLease;
1386
+ retainedByActiveRuntime = true;
1387
+ return { ...launched, runtimeLease };
1388
+ }
1389
+ catch (error) {
1390
+ try {
1391
+ await cleanup();
1392
+ removeCleanup(active.adapterCleanups, cleanup);
1393
+ }
1394
+ catch {
1395
+ // Preserve the cleanup and broker claim for the fenced release retry.
1396
+ }
1397
+ throw error;
1398
+ }
1399
+ })();
1400
+ active.pendingAdapterLaunches.set(portName, { controller, promise: operation });
1401
+ try {
1402
+ return await operation;
1403
+ }
1404
+ finally {
1405
+ active.pendingAdapterLaunches.delete(portName);
1406
+ if (!retainedByActiveRuntime)
1407
+ unlinkAbortController(controller, signal);
1408
+ }
1409
+ }
1410
+ async release(dispatchAttemptId, fencingToken, preservePreparation = false) {
1411
+ if (!preservePreparation)
1412
+ this.preparedReadiness.cancel(dispatchAttemptId, fencingToken);
1413
+ const active = this.active.get(dispatchAttemptId);
1414
+ if (!preservePreparation)
1415
+ this.pendingRenewals.delete(dispatchAttemptId);
1416
+ if (!active) {
1417
+ this.cancelAdmissionWaiters(dispatchAttemptId, fencingToken, "Managed host admission was released before acquisition.");
1418
+ this.releaseSingletonKeys(dispatchAttemptId, fencingToken);
1419
+ this.releaseParallelProjects(dispatchAttemptId, fencingToken);
1420
+ this.deleteAdmission(dispatchAttemptId, fencingToken);
1421
+ return null;
1422
+ }
1423
+ if (active.lease.fencingToken !== fencingToken)
1424
+ return null;
1425
+ active.releasing = true;
1426
+ for (const pending of active.pendingAdapterLaunches.values()) {
1427
+ pending.controller.abort("Host runtime release cancelled the pending adapter launch.");
1428
+ }
1429
+ await Promise.allSettled([...active.pendingAdapterLaunches.values()].map(pending => pending.promise));
1430
+ await active.databaseProvision?.catch(() => undefined);
1431
+ let releasing = transitionRuntimeIsolationLease(active.lease, "releasing");
1432
+ active.lease = releasing;
1433
+ await this.writeLedger(releasing);
1434
+ try {
1435
+ if (!active.runtimeResourcesReleased) {
1436
+ await active.processSupervisor.stopAll(fencingToken, "Host runtime lease released.");
1437
+ await releaseAdapterResources(active.adapterCleanups);
1438
+ if (active.databaseCapability && this.databaseBroker) {
1439
+ // A database the broker cannot drop now is reaped later; it must not hold host capacity.
1440
+ await this.databaseBroker.release(dispatchAttemptId, fencingToken).catch(() => "retained");
1441
+ }
1442
+ active.runtimeResourcesReleased = true;
1443
+ // Runtime admission protects live processes, listeners, ports, and their
1444
+ // memory commitment. Once exact process and adapter cleanup succeeds,
1445
+ // diagnostics retention remains retryable evidence work and must not keep
1446
+ // consuming host capacity or blocking the next project dispatch.
1447
+ this.processSupervisors.unregister(dispatchAttemptId, fencingToken);
1448
+ this.releaseSingletonKeys(dispatchAttemptId, fencingToken);
1449
+ this.releaseParallelProjects(dispatchAttemptId, fencingToken);
1450
+ this.deleteAdmission(dispatchAttemptId, fencingToken);
1451
+ }
1452
+ let evidenceRetainedAtAttemptRoot = false;
1453
+ try {
1454
+ const diagnosticsPath = await retainHostDiagnostics(this.stateRoot, active.attemptRoot, dispatchAttemptId, active.attemptRootIdentity, this.executionRoot);
1455
+ releasing = withRuntimeIsolationResources(releasing, {
1456
+ ...releasing.resources,
1457
+ paths: [...releasing.resources.paths.filter(entry => entry.role !== "diagnostics_archive"),
1458
+ { role: "diagnostics_archive", path: diagnosticsPath }],
1459
+ });
1460
+ }
1461
+ catch {
1462
+ // Runtime teardown is already proven above. Diagnostics archival is
1463
+ // deliberately non-destructive and may refuse a changed or unsupported
1464
+ // attempt-root inventory. Preserve that root and its released ledger for
1465
+ // operator inspection/reconciliation without converting completed
1466
+ // teardown into a capacity-owning degraded runtime.
1467
+ evidenceRetainedAtAttemptRoot = true;
1468
+ }
1469
+ if (!evidenceRetainedAtAttemptRoot)
1470
+ await removeHostAttemptRoot(active.attemptRoot);
1471
+ const released = transitionRuntimeIsolationLease(releasing, "released");
1472
+ // Publish the terminal in-memory state before its serialized write. Any writer
1473
+ // that already captured this active owner must observe the terminal fence and
1474
+ // cannot recreate a nonterminal ledger after release completes.
1475
+ active.lease = released;
1476
+ await this.writeLedger(released);
1477
+ if (!evidenceRetainedAtAttemptRoot) {
1478
+ await fs.rm(this.ledgerPath(dispatchAttemptId), { force: true });
1479
+ }
1480
+ releasing = released;
1481
+ this.active.delete(dispatchAttemptId);
1482
+ this.processSupervisors.unregister(dispatchAttemptId, fencingToken);
1483
+ this.releaseSingletonKeys(dispatchAttemptId, fencingToken);
1484
+ this.releaseParallelProjects(dispatchAttemptId, fencingToken);
1485
+ this.deleteAdmission(dispatchAttemptId, fencingToken);
1486
+ return releasing;
1487
+ }
1488
+ catch (error) {
1489
+ const degraded = transitionRuntimeIsolationLease(releasing, "degraded");
1490
+ active.lease = degraded;
1491
+ await this.writeLedger(degraded).catch(() => undefined);
1492
+ throw new HostRuntimeLeaseCleanupError(degraded, error);
1493
+ }
1494
+ }
1495
+ async readLedgers() {
1496
+ const root = path.join(this.stateRoot, "runtime-ledger", "host");
1497
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(error => {
1498
+ if (error.code === "ENOENT")
1499
+ return [];
1500
+ throw error;
1501
+ });
1502
+ const ledgers = [];
1503
+ for (const entry of entries) {
1504
+ if (!entry.isFile() || !/^[0-9a-f]{32}\.json$/u.test(entry.name))
1505
+ continue;
1506
+ try {
1507
+ const value = JSON.parse(await fs.readFile(path.join(root, entry.name), "utf8"));
1508
+ if (!validHostRuntimeLedger(value))
1509
+ continue;
1510
+ const expectedName = `${uuidHex(value.runtimeLease.dispatchAttemptId, "dispatch attempt")}.json`;
1511
+ if (entry.name !== expectedName)
1512
+ continue;
1513
+ ledgers.push(value);
1514
+ }
1515
+ catch {
1516
+ // An invalid ledger has no trustworthy ownership identity and is never reaped.
1517
+ }
1518
+ }
1519
+ return ledgers;
1520
+ }
1521
+ async recoverPersistedProcesses(lease, attemptRoot, attemptRootIdentity) {
1522
+ const logsRoot = path.join(attemptRoot, "logs");
1523
+ const expectedAttemptRoot = path.resolve(this.attemptRoot(lease.dispatchAttemptId));
1524
+ if (path.resolve(attemptRoot) !== expectedAttemptRoot) {
1525
+ throw new Error("host_runtime_reaper_invalid: attempt path is outside the fenced attempt root.");
1526
+ }
1527
+ await assertHostAttemptRootIdentity(attemptRoot, attemptRootIdentity);
1528
+ const rootEntries = await readPinnedSourceDirectory(attemptRoot, attemptRootIdentity);
1529
+ const logsEntry = rootEntries.find(entry => entry.name === "logs");
1530
+ if (!logsEntry)
1531
+ return [];
1532
+ if (logsEntry.kind !== "directory") {
1533
+ throw new Error("host_runtime_reaper_invalid: process ownership log path is not an ordinary directory.");
1534
+ }
1535
+ const entries = await readPinnedSourceDirectory(logsRoot, logsEntry.identity);
1536
+ const outcomes = [];
1537
+ for (const entry of entries) {
1538
+ if (entry.kind !== "file" || !/^process-supervisor\.[a-z][a-z0-9_-]{0,31}\.\d+\.json$/u.test(entry.name))
1539
+ continue;
1540
+ const metadataPath = path.join(logsRoot, entry.name);
1541
+ const value = JSON.parse((await readCanonicalSourceFile(metadataPath, maximumProcessRecoveryMetadataBytes, entry.identity)).toString("utf8"));
1542
+ if (!validHostProcessSupervisorMetadata(value, lease)) {
1543
+ throw new Error("host_runtime_reaper_invalid: process ownership metadata did not match the runtime lease.");
1544
+ }
1545
+ outcomes.push(await this.processRecovery.recover(value));
1546
+ }
1547
+ return outcomes;
1548
+ }
1549
+ async markLedgerDegraded(ledger) {
1550
+ const lease = ledger.runtimeLease;
1551
+ if (lease.state === "released")
1552
+ return;
1553
+ const degraded = transitionHostLeaseForCleanup(lease, "degraded");
1554
+ const repoId = ledger.repoId ?? repoIdFromProjectSingletonKeys(lease.resources.singletonKeys);
1555
+ this.rememberDegraded(degraded, repoId);
1556
+ await this.writeLedger(degraded, repoId ?? undefined, deserializeHostAttemptRootIdentity(ledger.attemptRootIdentity)).catch(() => undefined);
1557
+ }
1558
+ rememberDegraded(lease, repoId) {
1559
+ this.degraded.set(lease.dispatchAttemptId, { lease, repoId });
1560
+ if (!repoId)
1561
+ return;
1562
+ const owner = { dispatchAttemptId: lease.dispatchAttemptId, fencingToken: lease.fencingToken };
1563
+ const projectKey = projectHostRuntimeSingletonKey(repoId);
1564
+ const existing = this.singletonOwners.get(projectKey);
1565
+ if (!existing || sameSingletonOwner(existing, owner))
1566
+ this.singletonOwners.set(projectKey, owner);
1567
+ }
1568
+ forgetDegraded(dispatchAttemptId, fencingToken) {
1569
+ if (this.degraded.get(dispatchAttemptId)?.lease.fencingToken !== fencingToken)
1570
+ return;
1571
+ this.degraded.delete(dispatchAttemptId);
1572
+ this.releaseSingletonKeys(dispatchAttemptId, fencingToken);
1573
+ this.releaseParallelProjects(dispatchAttemptId, fencingToken);
1574
+ }
1575
+ attemptRoot(dispatchAttemptId) {
1576
+ return path.join(this.executionRoot, "a", compactHostAttemptId(dispatchAttemptId));
1577
+ }
1578
+ writeLedger(runtimeLease, explicitRepoId, explicitAttemptRootIdentity) {
1579
+ const activeOwner = this.active.get(runtimeLease.dispatchAttemptId);
1580
+ const write = this.ledgerWriteTail.then(() => {
1581
+ if (activeOwner?.lease.fencingToken === runtimeLease.fencingToken
1582
+ && activeOwner.lease.state === "released"
1583
+ && runtimeLease.state !== "released") {
1584
+ return;
1585
+ }
1586
+ return this.writeLedgerAtomic(runtimeLease, explicitRepoId, explicitAttemptRootIdentity);
1587
+ });
1588
+ this.ledgerWriteTail = write.catch(() => undefined);
1589
+ return write;
1590
+ }
1591
+ async writeLedgerAtomic(runtimeLease, explicitRepoId, explicitAttemptRootIdentity) {
1592
+ const root = path.join(this.stateRoot, "runtime-ledger", "host");
1593
+ await ensurePrivateDirectory(root);
1594
+ const target = this.ledgerPath(runtimeLease.dispatchAttemptId);
1595
+ const temporary = `${target}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
1596
+ try {
1597
+ const handle = await fs.open(temporary, "wx", 0o600);
1598
+ try {
1599
+ const repoId = explicitRepoId
1600
+ ?? this.active.get(runtimeLease.dispatchAttemptId)?.repoId
1601
+ ?? this.admissions.get(runtimeLease.dispatchAttemptId)?.repoId
1602
+ ?? this.degraded.get(runtimeLease.dispatchAttemptId)?.repoId
1603
+ ?? undefined;
1604
+ const attemptRootIdentity = explicitAttemptRootIdentity
1605
+ ?? this.active.get(runtimeLease.dispatchAttemptId)?.attemptRootIdentity
1606
+ ?? undefined;
1607
+ const ledger = {
1608
+ schemaVersion: 1,
1609
+ ...(repoId ? { repoId } : {}),
1610
+ ...(attemptRootIdentity
1611
+ ? { attemptRootIdentity: serializeHostAttemptRootIdentity(attemptRootIdentity) }
1612
+ : {}),
1613
+ runtimeLease,
1614
+ };
1615
+ await handle.writeFile(`${JSON.stringify(ledger)}\n`);
1616
+ await handle.sync();
1617
+ }
1618
+ finally {
1619
+ await handle.close();
1620
+ }
1621
+ await fs.rename(temporary, target);
1622
+ }
1623
+ catch (error) {
1624
+ await fs.rm(temporary, { force: true }).catch(() => undefined);
1625
+ throw new Error(`host_runtime_ledger_write_failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1626
+ }
1627
+ }
1628
+ ledgerPath(dispatchAttemptId) {
1629
+ const attemptHex = uuidHex(dispatchAttemptId, "dispatch attempt");
1630
+ return path.join(this.stateRoot, "runtime-ledger", "host", `${attemptHex}.json`);
1631
+ }
1632
+ singletonKeysAvailable(keys) {
1633
+ return keys.every(key => !this.singletonOwners.has(key) && !this.parallelProjectOwners.has(key));
1634
+ }
1635
+ assignSingletonKeys(keys, owner) {
1636
+ for (const key of keys)
1637
+ this.singletonOwners.set(key, owner);
1638
+ }
1639
+ releaseSingletonKeys(dispatchAttemptId, fencingToken) {
1640
+ for (const [key, owner] of this.singletonOwners) {
1641
+ if (owner.dispatchAttemptId === dispatchAttemptId && owner.fencingToken === fencingToken) {
1642
+ this.singletonOwners.delete(key);
1643
+ }
1644
+ }
1645
+ this.drainAdmissionWaiters();
1646
+ }
1647
+ convertSingletonToParallelProject(key, owner, promotion) {
1648
+ if (!sameSingletonOwner(this.singletonOwners.get(key), owner)) {
1649
+ throw new Error("host_runtime_parallel_promotion_stale: matching exclusive project admission is required.");
1650
+ }
1651
+ this.assignParallelProject(key, owner);
1652
+ this.singletonOwners.delete(key);
1653
+ for (let index = 0; index < this.singletonWaiters.length;) {
1654
+ const waiter = this.singletonWaiters[index];
1655
+ if (waiter.priority !== "foreground" || waiter.keys.length !== 1 || waiter.keys[0] !== key) {
1656
+ index += 1;
1657
+ continue;
1658
+ }
1659
+ this.singletonWaiters.splice(index, 1);
1660
+ clearAdmissionDeadline(waiter);
1661
+ waiter.signal?.removeEventListener("abort", waiter.abort);
1662
+ this.assignParallelProject(key, waiter.owner);
1663
+ this.knownPromotions.set(promotion.repoId, promotion);
1664
+ waiter.resolve([]);
1665
+ }
1666
+ this.drainAdmissionWaiters();
1667
+ }
1668
+ parallelProjectOwned(key, owner) {
1669
+ return sameSingletonOwner(this.parallelProjectOwners.get(key)?.get(owner.dispatchAttemptId), owner);
1670
+ }
1671
+ parallelProjectAvailable(key, priority = "foreground") {
1672
+ return !this.singletonOwners.has(key)
1673
+ && !this.singletonWaiters.some(waiter => waiter.keys.includes(key)
1674
+ && (priority === "background" || waiter.priority === "foreground"));
1675
+ }
1676
+ assignParallelProject(key, owner) {
1677
+ const owners = this.parallelProjectOwners.get(key) ?? new Map();
1678
+ owners.set(owner.dispatchAttemptId, owner);
1679
+ this.parallelProjectOwners.set(key, owners);
1680
+ }
1681
+ releaseParallelProjectNoDrain(key, owner) {
1682
+ const owners = this.parallelProjectOwners.get(key);
1683
+ if (!sameSingletonOwner(owners?.get(owner.dispatchAttemptId), owner))
1684
+ return;
1685
+ owners.delete(owner.dispatchAttemptId);
1686
+ if (owners.size === 0)
1687
+ this.parallelProjectOwners.delete(key);
1688
+ }
1689
+ releaseParallelProjects(dispatchAttemptId, fencingToken) {
1690
+ const owner = { dispatchAttemptId, fencingToken };
1691
+ for (const key of [...this.parallelProjectOwners.keys()]) {
1692
+ this.releaseParallelProjectNoDrain(key, owner);
1693
+ }
1694
+ this.drainAdmissionWaiters();
1695
+ }
1696
+ deleteAdmission(dispatchAttemptId, fencingToken) {
1697
+ if (this.admissions.get(dispatchAttemptId)?.fencingToken === fencingToken) {
1698
+ this.admissions.delete(dispatchAttemptId);
1699
+ }
1700
+ this.releaseHostCapacity(dispatchAttemptId, fencingToken);
1701
+ }
1702
+ renewAdmissionWaiterDeadline(dispatchAttemptId, fencingToken, expiresAt) {
1703
+ for (const waiter of [
1704
+ ...this.singletonWaiters,
1705
+ ...this.parallelProjectWaiters,
1706
+ ...this.capacityWaiters,
1707
+ ...this.backgroundAdmissionWaiters,
1708
+ ]) {
1709
+ if (!sameSingletonOwner(waiter.owner, { dispatchAttemptId, fencingToken })
1710
+ || Date.parse(expiresAt) <= Date.parse(waiter.expiresAt))
1711
+ continue;
1712
+ waiter.expiresAt = expiresAt;
1713
+ clearAdmissionDeadline(waiter);
1714
+ waiter.deadlineTimer = scheduleAdmissionDeadline(waiter.expiresAt, waiter.expire);
1715
+ }
1716
+ }
1717
+ clearPendingRenewal(owner) {
1718
+ if (this.pendingRenewals.get(owner.dispatchAttemptId)?.fencingToken === owner.fencingToken) {
1719
+ this.pendingRenewals.delete(owner.dispatchAttemptId);
1720
+ }
1721
+ }
1722
+ cancelAdmissionWaiters(dispatchAttemptId, fencingToken, reason) {
1723
+ const owner = { dispatchAttemptId, fencingToken };
1724
+ for (const waiters of [
1725
+ this.singletonWaiters,
1726
+ this.parallelProjectWaiters,
1727
+ this.capacityWaiters,
1728
+ this.backgroundAdmissionWaiters,
1729
+ ]) {
1730
+ for (let index = waiters.length - 1; index >= 0; index -= 1) {
1731
+ const waiter = waiters[index];
1732
+ if (!sameSingletonOwner(waiter.owner, owner))
1733
+ continue;
1734
+ waiters.splice(index, 1);
1735
+ clearAdmissionDeadline(waiter);
1736
+ if ("capacityRecheckTimer" in waiter)
1737
+ clearCapacityRecheck(waiter);
1738
+ waiter.signal?.removeEventListener("abort", waiter.abort);
1739
+ waiter.reject(new Error(`host_runtime_admission_cancelled: ${reason}`));
1740
+ }
1741
+ }
1742
+ this.drainAdmissionWaiters();
1743
+ }
1744
+ drainSingletonWaiters() {
1745
+ for (let index = 0; index < this.singletonWaiters.length;) {
1746
+ const waiter = this.singletonWaiters[index];
1747
+ if (waiter.signal?.aborted) {
1748
+ this.singletonWaiters.splice(index, 1);
1749
+ clearAdmissionDeadline(waiter);
1750
+ waiter.signal.removeEventListener("abort", waiter.abort);
1751
+ this.clearPendingRenewal(waiter.owner);
1752
+ waiter.reject(cancellationError(waiter.signal));
1753
+ continue;
1754
+ }
1755
+ if (admissionExpired(waiter.expiresAt)) {
1756
+ this.singletonWaiters.splice(index, 1);
1757
+ clearAdmissionDeadline(waiter);
1758
+ waiter.signal?.removeEventListener("abort", waiter.abort);
1759
+ this.clearPendingRenewal(waiter.owner);
1760
+ waiter.reject(new HostRuntimeAdmissionExpiredError());
1761
+ continue;
1762
+ }
1763
+ if (!this.singletonKeysAvailable(waiter.keys)) {
1764
+ index += 1;
1765
+ continue;
1766
+ }
1767
+ this.singletonWaiters.splice(index, 1);
1768
+ clearAdmissionDeadline(waiter);
1769
+ waiter.signal?.removeEventListener("abort", waiter.abort);
1770
+ this.assignSingletonKeys(waiter.keys, waiter.owner);
1771
+ waiter.resolve(waiter.keys);
1772
+ }
1773
+ }
1774
+ drainParallelProjectWaiters() {
1775
+ for (let index = 0; index < this.parallelProjectWaiters.length;) {
1776
+ const waiter = this.parallelProjectWaiters[index];
1777
+ if (waiter.signal?.aborted) {
1778
+ this.parallelProjectWaiters.splice(index, 1);
1779
+ clearAdmissionDeadline(waiter);
1780
+ waiter.signal.removeEventListener("abort", waiter.abort);
1781
+ this.clearPendingRenewal(waiter.owner);
1782
+ waiter.reject(cancellationError(waiter.signal));
1783
+ continue;
1784
+ }
1785
+ if (admissionExpired(waiter.expiresAt)) {
1786
+ this.parallelProjectWaiters.splice(index, 1);
1787
+ clearAdmissionDeadline(waiter);
1788
+ waiter.signal?.removeEventListener("abort", waiter.abort);
1789
+ this.clearPendingRenewal(waiter.owner);
1790
+ waiter.reject(new HostRuntimeAdmissionExpiredError());
1791
+ continue;
1792
+ }
1793
+ if (!this.parallelProjectAvailable(waiter.key, waiter.priority)) {
1794
+ index += 1;
1795
+ continue;
1796
+ }
1797
+ this.parallelProjectWaiters.splice(index, 1);
1798
+ clearAdmissionDeadline(waiter);
1799
+ waiter.signal?.removeEventListener("abort", waiter.abort);
1800
+ this.assignParallelProject(waiter.key, waiter.owner);
1801
+ waiter.resolve();
1802
+ }
1803
+ }
1804
+ drainBackgroundAdmissionWaiters() {
1805
+ if (this.hasForegroundDemand())
1806
+ return;
1807
+ for (let index = 0; index < this.backgroundAdmissionWaiters.length;) {
1808
+ const waiter = this.backgroundAdmissionWaiters[index];
1809
+ if (waiter.signal?.aborted) {
1810
+ this.backgroundAdmissionWaiters.splice(index, 1);
1811
+ clearAdmissionDeadline(waiter);
1812
+ clearCapacityRecheck(waiter);
1813
+ waiter.signal.removeEventListener("abort", waiter.abort);
1814
+ this.clearPendingRenewal(waiter.owner);
1815
+ waiter.reject(cancellationError(waiter.signal));
1816
+ continue;
1817
+ }
1818
+ if (admissionExpired(waiter.expiresAt)) {
1819
+ this.backgroundAdmissionWaiters.splice(index, 1);
1820
+ clearAdmissionDeadline(waiter);
1821
+ clearCapacityRecheck(waiter);
1822
+ waiter.signal?.removeEventListener("abort", waiter.abort);
1823
+ this.clearPendingRenewal(waiter.owner);
1824
+ waiter.reject(new HostRuntimeAdmissionExpiredError());
1825
+ continue;
1826
+ }
1827
+ if (!this.singletonKeysAvailable(waiter.keys) || !this.hostCapacityAvailable(waiter.resources)) {
1828
+ index += 1;
1829
+ continue;
1830
+ }
1831
+ this.backgroundAdmissionWaiters.splice(index, 1);
1832
+ clearAdmissionDeadline(waiter);
1833
+ clearCapacityRecheck(waiter);
1834
+ waiter.signal?.removeEventListener("abort", waiter.abort);
1835
+ this.assignSingletonKeys(waiter.keys, waiter.owner);
1836
+ this.capacityReservations.set(waiter.owner.dispatchAttemptId, {
1837
+ owner: waiter.owner,
1838
+ resources: waiter.resources,
1839
+ state: "pending",
1840
+ });
1841
+ waiter.resolve(waiter.keys);
1842
+ }
1843
+ }
1844
+ async refreshKnownPromotions() {
1845
+ const promotions = await this.promotionStore.list();
1846
+ this.knownPromotions.clear();
1847
+ for (const promotion of promotions)
1848
+ this.knownPromotions.set(promotion.repoId, promotion);
1849
+ }
1850
+ async invalidatePromotion(repoId, scope = "all") {
1851
+ await this.promotionStore.invalidate(repoId, scope);
1852
+ this.knownPromotions.delete(repoId.trim().toLowerCase());
1853
+ }
1854
+ drainAdmissionWaiters() {
1855
+ this.reconcileReservationOwners();
1856
+ this.drainSingletonWaiters();
1857
+ this.drainParallelProjectWaiters();
1858
+ this.drainCapacityWaiters();
1859
+ this.drainBackgroundAdmissionWaiters();
1860
+ }
1861
+ reconcileReservationOwners() {
1862
+ for (const [key, owner] of this.singletonOwners) {
1863
+ if (!this.reservationOwnerIsLive(owner))
1864
+ this.singletonOwners.delete(key);
1865
+ }
1866
+ for (const [key, owners] of this.parallelProjectOwners) {
1867
+ for (const [dispatchAttemptId, owner] of owners) {
1868
+ const awaitingExclusiveUpgrade = this.singletonWaiters.some(waiter => waiter.keys.includes(key) && sameSingletonOwner(waiter.owner, owner));
1869
+ // A parallel owner cannot legitimately keep the shared claim after the
1870
+ // same fenced attempt has queued its exclusive downgrade. Reconcile the
1871
+ // half-upgraded state before testing singleton availability so one missed
1872
+ // release cannot make an attempt wait behind itself until lease expiry.
1873
+ if (awaitingExclusiveUpgrade || !this.reservationOwnerIsLive(owner)) {
1874
+ owners.delete(dispatchAttemptId);
1875
+ }
1876
+ }
1877
+ if (owners.size === 0)
1878
+ this.parallelProjectOwners.delete(key);
1879
+ }
1880
+ }
1881
+ reservationOwnerIsLive(owner) {
1882
+ if (this.acquiringAdmissions.has(hostRuntimeOwnerKey(owner)))
1883
+ return true;
1884
+ if (this.admissions.get(owner.dispatchAttemptId)?.fencingToken === owner.fencingToken)
1885
+ return true;
1886
+ if (this.active.get(owner.dispatchAttemptId)?.lease.fencingToken === owner.fencingToken)
1887
+ return true;
1888
+ if (this.degraded.get(owner.dispatchAttemptId)?.lease.fencingToken === owner.fencingToken)
1889
+ return true;
1890
+ if (sameSingletonOwner(this.capacityReservations.get(owner.dispatchAttemptId)?.owner, owner))
1891
+ return true;
1892
+ return [
1893
+ ...this.singletonWaiters,
1894
+ ...this.parallelProjectWaiters,
1895
+ ...this.capacityWaiters,
1896
+ ...this.backgroundAdmissionWaiters,
1897
+ ].some(waiter => sameSingletonOwner(waiter.owner, owner));
1898
+ }
1899
+ hasAttemptRepresentation(dispatchAttemptId, fencingToken) {
1900
+ return this.reservationOwnerIsLive({ dispatchAttemptId, fencingToken });
1901
+ }
1902
+ }
1903
+ class AttemptSupervisedProviderProcessRunner {
1904
+ supervisor;
1905
+ owner;
1906
+ listenerInspector;
1907
+ enforceNoListeners;
1908
+ onProcessTreeStarted;
1909
+ constructor(supervisor, owner, listenerInspector, enforceNoListeners, onProcessTreeStarted) {
1910
+ this.supervisor = supervisor;
1911
+ this.owner = owner;
1912
+ this.listenerInspector = listenerInspector;
1913
+ this.enforceNoListeners = enforceNoListeners;
1914
+ this.onProcessTreeStarted = onProcessTreeStarted;
1915
+ }
1916
+ async run(request, handlers = {}, signal) {
1917
+ const baselineListeners = this.enforceNoListeners
1918
+ ? new Set(await this.listenerInspector.inspectLoopbackListeners())
1919
+ : new Set();
1920
+ const child = await this.supervisor.startChildReady("provider", request, handlers, signal, { maxStartAttempts: request.stdin === undefined ? 1 : 3, retryDelayMs: 100 });
1921
+ this.onProcessTreeStarted();
1922
+ if (!this.enforceNoListeners)
1923
+ return await child.result;
1924
+ let settled = false;
1925
+ let inspectionFailure;
1926
+ const monitor = (async () => {
1927
+ while (!settled) {
1928
+ try {
1929
+ const ports = (await this.listenerInspector.inspectOwnedLoopbackListeners(child, this.owner))
1930
+ .filter(port => !baselineListeners.has(port));
1931
+ if (ports.length > 0) {
1932
+ inspectionFailure = new Error(`host_analysis_listener_violation: analysis-only provider opened undeclared loopback listener(s): ${ports.join(", ")}.`);
1933
+ await child.stop(this.owner.fencingToken).catch(() => undefined);
1934
+ return;
1935
+ }
1936
+ }
1937
+ catch (error) {
1938
+ inspectionFailure = error;
1939
+ await child.stop(this.owner.fencingToken).catch(() => undefined);
1940
+ return;
1941
+ }
1942
+ await waitForAnalysisInspection(250, signal).catch(error => {
1943
+ if (!settled && !signal?.aborted)
1944
+ inspectionFailure = error;
1945
+ });
1946
+ }
1947
+ })();
1948
+ try {
1949
+ const result = await child.result;
1950
+ settled = true;
1951
+ await monitor;
1952
+ if (inspectionFailure)
1953
+ throw inspectionFailure;
1954
+ return result;
1955
+ }
1956
+ catch (error) {
1957
+ settled = true;
1958
+ await monitor.catch(() => undefined);
1959
+ if (inspectionFailure)
1960
+ throw inspectionFailure;
1961
+ throw error;
1962
+ }
1963
+ }
1964
+ }
1965
+ function waitForAnalysisInspection(milliseconds, signal) {
1966
+ if (signal?.aborted)
1967
+ return Promise.reject(cancellationError(signal));
1968
+ return new Promise((resolve, reject) => {
1969
+ const timer = setTimeout(() => {
1970
+ signal?.removeEventListener("abort", abort);
1971
+ resolve();
1972
+ }, milliseconds);
1973
+ const abort = () => {
1974
+ clearTimeout(timer);
1975
+ reject(cancellationError(signal));
1976
+ };
1977
+ signal?.addEventListener("abort", abort, { once: true });
1978
+ });
1979
+ }
1980
+ export class HostRuntimeLeaseCleanupError extends Error {
1981
+ lease;
1982
+ constructor(lease, cause) {
1983
+ super(`host_runtime_cleanup_degraded: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
1984
+ this.lease = lease;
1985
+ this.name = "HostRuntimeLeaseCleanupError";
1986
+ }
1987
+ }
1988
+ export class HostRuntimeAdmissionExpiredError extends Error {
1989
+ constructor() {
1990
+ super("host_runtime_admission_expired: dispatch lease expired while waiting for managed host admission.");
1991
+ this.name = "HostRuntimeAdmissionExpiredError";
1992
+ }
1993
+ }
1994
+ export class HostRuntimeCapacityRequestImpossibleError extends Error {
1995
+ constructor(reason) {
1996
+ super(`host_runtime_capacity_request_impossible: ${reason}`);
1997
+ this.name = "HostRuntimeCapacityRequestImpossibleError";
1998
+ }
1999
+ }
2000
+ export function hostRuntimeEventResources(lease, runtimeContract, executionClass = "managed_runtime") {
2001
+ const requestedHostConcurrency = runtimeContract?.contract.runtime.hostConcurrency ?? "serial";
2002
+ const configuredLaunches = runtimeContract?.contract.runtime.ports.filter(port => port.launch !== null) ?? [];
2003
+ const verifiedLaunches = configuredLaunches.filter(configured => lease.resources.ports.some(port => port.name === configured.name));
2004
+ const effectiveConcurrency = effectiveHostConcurrency(lease, runtimeContract, executionClass);
2005
+ return {
2006
+ agentContainer: null,
2007
+ sidecarContainers: [],
2008
+ networks: [],
2009
+ volumes: [],
2010
+ publishedPorts: lease.resources.ports,
2011
+ hostConcurrency: effectiveConcurrency,
2012
+ requestedHostConcurrency,
2013
+ hostParallelPromotion: effectiveConcurrency === "host_parallel_safe"
2014
+ ? executionClass === "analysis_only" ? "analysis_profile_enforced" : "observed_probe_passed"
2015
+ : requestedHostConcurrency !== "parallel"
2016
+ ? "not_requested"
2017
+ : configuredLaunches.length > 0 && verifiedLaunches.length === configuredLaunches.length
2018
+ ? "pending_live_parallel_proof"
2019
+ : "pending_adapter_probe",
2020
+ verifiedRuntimePorts: verifiedLaunches.map(port => port.name),
2021
+ runtimeContractSource: runtimeContract?.source ?? "default",
2022
+ executionClass,
2023
+ requestedPorts: runtimeContract?.contract.runtime.ports.map(port => port.name) ?? [],
2024
+ singletonKeys: lease.resources.singletonKeys,
2025
+ processSupervisorId: lease.resources.processSupervisorId,
2026
+ runtimeLease: lease,
2027
+ };
2028
+ }
2029
+ export function hostRuntimeSingletonKeys(dispatch) {
2030
+ return [projectHostRuntimeSingletonKey(dispatch.repo.id)];
2031
+ }
2032
+ function hostRuntimeEnvironment(sessionId, lease, runtimeContract, executionClass, databaseCapability) {
2033
+ const pathFor = (role) => lease.resources.paths.find(entry => entry.role === role)?.path ?? "";
2034
+ const environment = {
2035
+ FORGE_SESSION_ID: sessionId,
2036
+ FORGE_RUNTIME_MODE: "host",
2037
+ FORGE_RESOURCE_LEASE_ID: lease.leaseId,
2038
+ FORGE_DISPATCH_ATTEMPT_ID: lease.dispatchAttemptId,
2039
+ FORGE_E2E_OWNERSHIP_MODE: "attempt",
2040
+ FORGE_E2E_OWNERSHIP_TOKEN: uuidHex(lease.dispatchAttemptId, "dispatch attempt"),
2041
+ FORGE_EXECUTION_LEASE_ID: lease.executionLeaseId,
2042
+ FORGE_FENCING_TOKEN: lease.fencingToken,
2043
+ FORGE_WORKSPACE: lease.worktreePath,
2044
+ FORGE_WORKTREE_PATH: lease.worktreePath,
2045
+ FORGE_TMPDIR: pathFor("temporary"),
2046
+ FORGE_LOG_DIR: pathFor("logs"),
2047
+ FORGE_CACHE_DIR: pathFor("cache"),
2048
+ FORGE_BROWSER_PROFILE_DIR: pathFor("browser_profile"),
2049
+ FORGE_TEST_OUTPUT_DIR: pathFor("test_output"),
2050
+ // Project-authored ASP.NET code must not fall back to the durable Forge
2051
+ // database when no brokered attempt database exists. The API policy treats
2052
+ // this marker as a fail-closed isolation requirement.
2053
+ FORGE_ATTEMPT_DATABASE_POLICY: "isolated_only",
2054
+ FORGE_ATTEMPT_DATABASE_SCHEMA: `forge_attempt_${uuidHex(lease.dispatchAttemptId, "dispatch attempt")}`,
2055
+ FORGE_ATTEMPT_DATABASE_READINESS_PATH: path.join(pathFor("logs"), "attempt-database-readiness.json"),
2056
+ FORGE_PROCESS_SUPERVISOR_ID: lease.resources.processSupervisorId ?? "",
2057
+ FORGE_PROCESS_SUPERVISOR_METADATA_PATH: pathFor("process_supervisor_metadata"),
2058
+ FORGE_HOST_CONCURRENCY: effectiveHostConcurrency(lease, runtimeContract, executionClass),
2059
+ FORGE_HOST_CONCURRENCY_REQUESTED: runtimeContract.contract.runtime.hostConcurrency,
2060
+ FORGE_RUNTIME_EXECUTION_CLASS: executionClass,
2061
+ FORGE_HOST_RUNTIME_CONTRACT_SOURCE: runtimeContract.source,
2062
+ FORGE_SINGLETON_KEYS: lease.resources.singletonKeys.join(","),
2063
+ TMPDIR: pathFor("temporary"),
2064
+ TMP: pathFor("temporary"),
2065
+ TEMP: pathFor("temporary"),
2066
+ XDG_CACHE_HOME: pathFor("cache"),
2067
+ NPM_CONFIG_CACHE: path.join(pathFor("cache"), "npm"),
2068
+ };
2069
+ if (lease.resources.singletonKeys.length === 0)
2070
+ delete environment.FORGE_SINGLETON_KEYS;
2071
+ if (databaseCapability)
2072
+ environment[attemptDatabaseCapabilityEnvironment] = databaseCapability.connectionString;
2073
+ const resolvedBaseUrls = [];
2074
+ for (const assigned of lease.resources.ports) {
2075
+ const configured = runtimeContract.contract.runtime.ports.find(port => port.name === assigned.name);
2076
+ if (!configured || assigned.hostIp !== "127.0.0.1")
2077
+ continue;
2078
+ Object.assign(environment, hostPortEnvironment(configured, assigned.hostIp, assigned.hostPort));
2079
+ const normalizedName = assigned.name.toUpperCase().replaceAll(/[^A-Z0-9]/gu, "_");
2080
+ environment[`FORGE_${normalizedName}_PORT`] = String(assigned.hostPort);
2081
+ if (configured.baseUrlEnv && configured.format) {
2082
+ const baseUrl = hostPortEnvironment(configured, assigned.hostIp, assigned.hostPort)[configured.baseUrlEnv];
2083
+ if (baseUrl) {
2084
+ environment[`FORGE_${normalizedName}_URL`] = baseUrl;
2085
+ resolvedBaseUrls.push(baseUrl);
2086
+ }
2087
+ }
2088
+ }
2089
+ if (environment.FORGE_API_URL)
2090
+ environment.FORGE_E2E_API_URL = environment.FORGE_API_URL;
2091
+ if (environment.FORGE_WEB_URL)
2092
+ environment.FORGE_E2E_WEB_URL = environment.FORGE_WEB_URL;
2093
+ if (resolvedBaseUrls.length === 1)
2094
+ environment.FORGE_BASE_URL = resolvedBaseUrls[0];
2095
+ return environment;
2096
+ }
2097
+ async function executeAspNetRuntimeBuild(supervisor, logDirectory, plan, signal, exactSecrets = []) {
2098
+ const childName = `runtime-build-${plan.fingerprint.slice(-12)}`;
2099
+ const stdoutLogPath = path.join(logDirectory, `${childName}.stdout.log`);
2100
+ const stderrLogPath = path.join(logDirectory, `${childName}.stderr.log`);
2101
+ let stdoutTail = "";
2102
+ let stderrTail = "";
2103
+ const append = async (target, line) => {
2104
+ await fs.appendFile(target, `${redactText(line, exactSecrets)}\n`, { encoding: "utf8", mode: 0o600 });
2105
+ };
2106
+ const child = await supervisor.startChildReady(childName, {
2107
+ command: plan.command,
2108
+ args: plan.args,
2109
+ cwd: plan.cwd,
2110
+ env: plan.environment,
2111
+ captureOutput: false,
2112
+ }, {
2113
+ onStdoutLine: async (line) => {
2114
+ stdoutTail = appendRuntimeBuildTail(stdoutTail, redactText(line, exactSecrets));
2115
+ await append(stdoutLogPath, line);
2116
+ },
2117
+ onStderrLine: async (line) => {
2118
+ stderrTail = appendRuntimeBuildTail(stderrTail, redactText(line, exactSecrets));
2119
+ await append(stderrLogPath, line);
2120
+ },
2121
+ }, signal, { maxStartAttempts: 2, retryDelayMs: 250 });
2122
+ const result = await child.result;
2123
+ return {
2124
+ exitCode: result.exitCode,
2125
+ stdoutTail: appendRuntimeBuildTail(stdoutTail, redactText(result.stdout, exactSecrets)),
2126
+ stderrTail: appendRuntimeBuildTail(stderrTail, redactText(result.stderr, exactSecrets)),
2127
+ stdoutLogPath,
2128
+ stderrLogPath,
2129
+ };
2130
+ }
2131
+ function appendRuntimeBuildTail(current, value) {
2132
+ if (!value)
2133
+ return current;
2134
+ return `${current}${current ? "\n" : ""}${value}`.slice(-4_000);
2135
+ }
2136
+ function effectiveHostConcurrency(lease, runtimeContract, executionClass = "managed_runtime") {
2137
+ if (executionClass === "analysis_only") {
2138
+ const analysisSingletonHeld = lease.resources.singletonKeys.length > 0;
2139
+ return analysisSingletonHeld ? "host_serial_only" : "host_parallel_safe";
2140
+ }
2141
+ if (!runtimeContract || !hostRuntimeParallelEligible(runtimeContract))
2142
+ return "host_serial_only";
2143
+ const requiredPorts = configuredLaunchPortNames(runtimeContract);
2144
+ const everyPortVerified = requiredPorts.length > 0
2145
+ && requiredPorts.every(name => lease.resources.ports.some(port => port.name === name));
2146
+ const projectSingletonHeld = lease.resources.singletonKeys.some(key => key.endsWith(":host-runtime"));
2147
+ return everyPortVerified && !projectSingletonHeld ? "host_parallel_safe" : "host_serial_only";
2148
+ }
2149
+ function projectHostRuntimeSingletonKey(repoId) {
2150
+ return `project:${repoId.toLowerCase()}:host-runtime`;
2151
+ }
2152
+ function projectHostAnalysisKey(repoId) {
2153
+ return `project:${repoId.toLowerCase()}:host-analysis`;
2154
+ }
2155
+ function hostRuntimeDeclaredSingletonKey(resource) {
2156
+ return `runtime:${resource}`;
2157
+ }
2158
+ function runtimeExecutionClass(dispatch) {
2159
+ return dispatch.runtimeIntent?.executionClass === "analysis_only"
2160
+ && dispatch.runtimeIntent.applicationRuntimeRequired === false
2161
+ ? "analysis_only"
2162
+ : "managed_runtime";
2163
+ }
2164
+ function runtimeAdmissionPriority(dispatch) {
2165
+ return dispatch.execution.lane.trim().toLowerCase() === "runtime_readiness"
2166
+ || dispatch.cycleRun.phase.trim().toLowerCase() === "readiness_probe"
2167
+ ? "background"
2168
+ : "foreground";
2169
+ }
2170
+ function enqueueAdmissionWaiter(waiters, waiter) {
2171
+ if (waiter.priority === "background") {
2172
+ waiters.push(waiter);
2173
+ return;
2174
+ }
2175
+ const backgroundIndex = waiters.findIndex(candidate => candidate.priority === "background");
2176
+ if (backgroundIndex < 0)
2177
+ waiters.push(waiter);
2178
+ else
2179
+ waiters.splice(backgroundIndex, 0, waiter);
2180
+ }
2181
+ function defaultHostResourceRequest() {
2182
+ const workerMemoryLimitBytes = configuredInteger("FORGE_HOST_DEFAULT_WORKER_MEMORY_LIMIT_BYTES", 2 * 1024 ** 3, 128 * 1024 ** 2, 512 * 1024 ** 3);
2183
+ const runtimeOverheadBytes = 128 * 1024 ** 2;
2184
+ return {
2185
+ sandboxSlots: 1,
2186
+ cpuMillis: 2000,
2187
+ workerMemoryLimitBytes,
2188
+ serviceMemoryLimitBytes: 0,
2189
+ runtimeOverheadBytes,
2190
+ memoryBytes: workerMemoryLimitBytes + runtimeOverheadBytes,
2191
+ workspaceBytes: 10 * 1024 ** 3,
2192
+ pids: 512,
2193
+ buildSlots: 1,
2194
+ };
2195
+ }
2196
+ function minimumSupportedHostResourceRequest() {
2197
+ return {
2198
+ sandboxSlots: 1,
2199
+ cpuMillis: 1000,
2200
+ workerMemoryLimitBytes: 1024 ** 3,
2201
+ serviceMemoryLimitBytes: 0,
2202
+ runtimeOverheadBytes: 128 * 1024 ** 2,
2203
+ memoryBytes: 1152 * 1024 ** 2,
2204
+ workspaceBytes: architectMinimumWorkspaceEstimateBytes,
2205
+ pids: 256,
2206
+ buildSlots: 0,
2207
+ };
2208
+ }
2209
+ function normalizedHostResourceRequest(resources) {
2210
+ const defaults = defaultHostResourceRequest();
2211
+ const workerMemoryLimitBytes = positiveResourceValue(resources?.workerMemoryLimitBytes, defaults.workerMemoryLimitBytes);
2212
+ const serviceMemoryLimitBytes = nonNegativeResourceValue(resources?.serviceMemoryLimitBytes, defaults.serviceMemoryLimitBytes);
2213
+ const runtimeOverheadBytes = nonNegativeResourceValue(resources?.runtimeOverheadBytes, defaults.runtimeOverheadBytes);
2214
+ return {
2215
+ sandboxSlots: positiveResourceValue(resources?.sandboxSlots, defaults.sandboxSlots),
2216
+ cpuMillis: positiveResourceValue(resources?.cpuMillis, defaults.cpuMillis),
2217
+ workerMemoryLimitBytes,
2218
+ serviceMemoryLimitBytes,
2219
+ runtimeOverheadBytes,
2220
+ memoryBytes: workerMemoryLimitBytes + serviceMemoryLimitBytes + runtimeOverheadBytes,
2221
+ workspaceBytes: positiveResourceValue(resources?.workspaceBytes, defaults.workspaceBytes),
2222
+ pids: positiveResourceValue(resources?.pids, defaults.pids),
2223
+ buildSlots: nonNegativeResourceValue(resources?.buildSlots, defaults.buildSlots),
2224
+ };
2225
+ }
2226
+ function positiveResourceValue(value, fallback) {
2227
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
2228
+ }
2229
+ function nonNegativeResourceValue(value, fallback) {
2230
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
2231
+ }
2232
+ function configuredInteger(name, fallback, minimum, maximum) {
2233
+ const parsed = Number.parseInt(process.env[name] ?? "", 10);
2234
+ return Number.isFinite(parsed) ? Math.max(minimum, Math.min(maximum, parsed)) : fallback;
2235
+ }
2236
+ function loadHostCapacityConfiguration(capacityResourceSample) {
2237
+ const sampledCpuCount = capacityResourceSample
2238
+ ? Math.max(1, Math.floor(capacityResourceSample.totalCpuMillis / 1_000))
2239
+ : os.cpus().length;
2240
+ const totalMemoryBytes = capacityResourceSample?.totalMemoryBytes ?? os.totalmem();
2241
+ const reserveFloorBytes = configuredInteger("FORGE_HOST_MEMORY_RESERVE_FLOOR_BYTES", 1024 ** 3, 512 * 1024 ** 2, 128 * 1024 ** 3);
2242
+ const reserveCeilingBytes = configuredInteger("FORGE_HOST_MEMORY_RESERVE_CEILING_BYTES", 4 * 1024 ** 3, reserveFloorBytes, 128 * 1024 ** 3);
2243
+ const hostReserveBytes = resolveAdaptiveMemoryReserveBytes(totalMemoryBytes, {
2244
+ explicitBytes: configuredOptionalInteger("FORGE_HOST_MEMORY_RESERVE_BYTES", 512 * 1024 ** 2, 128 * 1024 ** 3),
2245
+ floorBytes: reserveFloorBytes,
2246
+ ceilingBytes: reserveCeilingBytes,
2247
+ percent: configuredNumber("FORGE_HOST_MEMORY_RESERVE_PERCENT", 12.5, 1, 50),
2248
+ });
2249
+ const minimumIdleReserveBytes = Math.min(hostReserveBytes, reserveFloorBytes);
2250
+ return {
2251
+ hardMaxAttempts: configuredInteger("FORGE_HOST_MAX_CONCURRENT_ATTEMPTS", Math.max(1, Math.min(8, Math.floor(sampledCpuCount / 2))), 1, 32),
2252
+ hostReserveBytes,
2253
+ idleHostReserveBytes: configuredInteger("FORGE_HOST_IDLE_MEMORY_RESERVE_BYTES", minimumIdleReserveBytes, 512 * 1024 ** 2, hostReserveBytes),
2254
+ cpuReserveMillis: configuredInteger("FORGE_HOST_CPU_RESERVE_MILLIS", 1000, 0, 32_000),
2255
+ workspaceReserveBytes: configuredInteger("FORGE_HOST_WORKSPACE_RESERVE_BYTES", 10 * 1024 ** 3, 1024 ** 3, Number.MAX_SAFE_INTEGER),
2256
+ memoryRecoverySamples: configuredInteger("FORGE_HOST_MEMORY_RECOVERY_SAMPLES", 2, 1, 10),
2257
+ memoryRecoverySampleIntervalMs: configuredInteger("FORGE_HOST_MEMORY_RECOVERY_SAMPLE_INTERVAL_MS", 1_000, 100, 30_000),
2258
+ };
2259
+ }
2260
+ function configuredOptionalInteger(name, minimum, maximum) {
2261
+ const raw = process.env[name];
2262
+ if (raw === undefined || raw.trim().length === 0)
2263
+ return null;
2264
+ const parsed = Number.parseInt(raw, 10);
2265
+ return Number.isFinite(parsed) ? Math.max(minimum, Math.min(maximum, parsed)) : null;
2266
+ }
2267
+ function configuredNumber(name, fallback, minimum, maximum) {
2268
+ const parsed = Number(process.env[name] ?? "");
2269
+ return Number.isFinite(parsed) && parsed > 0
2270
+ ? Math.max(minimum, Math.min(maximum, parsed))
2271
+ : fallback;
2272
+ }
2273
+ function resourceSlots(available, requested) {
2274
+ if (requested <= 0)
2275
+ return Number.MAX_SAFE_INTEGER;
2276
+ return Math.max(0, Math.floor(available / requested));
2277
+ }
2278
+ function removeWaiter(waiters, waiter) {
2279
+ const index = waiters.indexOf(waiter);
2280
+ if (index >= 0)
2281
+ waiters.splice(index, 1);
2282
+ }
2283
+ function capacityProject(projects, repoId) {
2284
+ const normalized = repoId.trim().toLowerCase();
2285
+ const existing = projects.get(normalized);
2286
+ if (existing)
2287
+ return existing;
2288
+ const created = {
2289
+ repoId: normalized,
2290
+ safetyClass: null,
2291
+ activeAttempts: 0,
2292
+ reservedAttempts: 0,
2293
+ waitingAttempts: 0,
2294
+ degradedAttempts: 0,
2295
+ runtimeContractFingerprint: null,
2296
+ runtimeContractSource: null,
2297
+ promotedAt: null,
2298
+ };
2299
+ projects.set(normalized, created);
2300
+ return created;
2301
+ }
2302
+ function conservativeSafetyClass(left, right) {
2303
+ if (!left)
2304
+ return right;
2305
+ const rank = {
2306
+ host_serial_only: 0,
2307
+ host_parallel_candidate: 1,
2308
+ host_parallel_safe: 2,
2309
+ };
2310
+ return rank[right] < rank[left] ? right : left;
2311
+ }
2312
+ function repoIdFromProjectSingletonKeys(keys) {
2313
+ return keys.map(repoIdFromProjectSingletonKey).find((value) => value !== null) ?? null;
2314
+ }
2315
+ function repoIdFromProjectSingletonKey(key) {
2316
+ const prefix = "project:";
2317
+ const suffix = [":host-runtime", ":host-analysis"].find(candidate => key.endsWith(candidate));
2318
+ return key.startsWith(prefix) && suffix
2319
+ ? key.slice(prefix.length, -suffix.length)
2320
+ : null;
2321
+ }
2322
+ function sameSingletonOwner(left, right) {
2323
+ return left?.dispatchAttemptId === right.dispatchAttemptId && left.fencingToken === right.fencingToken;
2324
+ }
2325
+ function hostRuntimeOwnerKey(owner) {
2326
+ return `${owner.dispatchAttemptId}:${owner.fencingToken}`;
2327
+ }
2328
+ function hostPortOwner(lease) {
2329
+ const processSupervisorId = lease.resources.processSupervisorId;
2330
+ if (!processSupervisorId) {
2331
+ throw new Error("host_runtime_adapter_invalid: process supervisor identity is required.");
2332
+ }
2333
+ return {
2334
+ dispatchAttemptId: lease.dispatchAttemptId,
2335
+ executionLeaseId: lease.executionLeaseId,
2336
+ fencingToken: lease.fencingToken,
2337
+ processSupervisorId,
2338
+ };
2339
+ }
2340
+ function linkedAbortController(signal) {
2341
+ const controller = new AbortController();
2342
+ if (signal?.aborted) {
2343
+ controller.abort(signal.reason);
2344
+ }
2345
+ else {
2346
+ signal?.addEventListener("abort", () => controller.abort(signal.reason), { once: true, signal: controller.signal });
2347
+ }
2348
+ return controller;
2349
+ }
2350
+ function unlinkAbortController(controller, _signal) {
2351
+ if (!controller.signal.aborted)
2352
+ controller.abort("Host adapter launch settled.");
2353
+ }
2354
+ function removeCleanup(cleanups, cleanup) {
2355
+ const index = cleanups.indexOf(cleanup);
2356
+ if (index >= 0)
2357
+ cleanups.splice(index, 1);
2358
+ }
2359
+ async function releaseAdapterResources(cleanups) {
2360
+ const pending = cleanups.splice(0);
2361
+ const failed = [];
2362
+ let firstError;
2363
+ for (const cleanup of pending.reverse()) {
2364
+ try {
2365
+ await cleanup();
2366
+ }
2367
+ catch (error) {
2368
+ firstError ??= error;
2369
+ failed.unshift(cleanup);
2370
+ }
2371
+ }
2372
+ cleanups.push(...failed);
2373
+ if (firstError !== undefined)
2374
+ throw firstError;
2375
+ }
2376
+ async function removeHostAttemptRoot(attemptRoot) {
2377
+ await fs.rm(attemptRoot, {
2378
+ recursive: true,
2379
+ force: true,
2380
+ maxRetries: 10,
2381
+ retryDelay: 100,
2382
+ });
2383
+ }
2384
+ function cancellationError(signal) {
2385
+ return new Error(String(signal?.reason ?? "Dispatch cancelled while waiting for host runtime singleton ownership."));
2386
+ }
2387
+ function validHostRuntimeLedger(value) {
2388
+ if (typeof value !== "object" || value === null)
2389
+ return false;
2390
+ const ledger = value;
2391
+ return ledger.schemaVersion === 1
2392
+ && (ledger.repoId === undefined || /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(ledger.repoId))
2393
+ && (ledger.attemptRootIdentity === undefined
2394
+ || validSerializedHostAttemptRootIdentity(ledger.attemptRootIdentity))
2395
+ && validRuntimeIsolationLease(ledger.runtimeLease)
2396
+ && ledger.runtimeLease.mode === "host";
2397
+ }
2398
+ function validSerializedHostAttemptRootIdentity(value) {
2399
+ if (typeof value !== "object" || value === null)
2400
+ return false;
2401
+ const identity = value;
2402
+ return /^-?\d{1,64}$/u.test(identity.dev) && /^-?\d{1,64}$/u.test(identity.ino);
2403
+ }
2404
+ function serializeHostAttemptRootIdentity(identity) {
2405
+ return { dev: identity.dev.toString(), ino: identity.ino.toString() };
2406
+ }
2407
+ function deserializeHostAttemptRootIdentity(identity) {
2408
+ return identity && validSerializedHostAttemptRootIdentity(identity)
2409
+ ? { dev: BigInt(identity.dev), ino: BigInt(identity.ino) }
2410
+ : null;
2411
+ }
2412
+ function validHostProcessSupervisorMetadata(value, lease) {
2413
+ if (typeof value !== "object" || value === null)
2414
+ return false;
2415
+ const metadata = value;
2416
+ return metadata.schemaVersion === 1
2417
+ && /^host-[0-9a-f]{32}$/u.test(metadata.supervisorId)
2418
+ && (metadata.strategy === "windows_job_object" || metadata.strategy === "unix_process_group")
2419
+ && Number.isSafeInteger(metadata.rootPid)
2420
+ && metadata.rootPid > 0
2421
+ && Number.isFinite(Date.parse(metadata.rootCreatedAt))
2422
+ && (metadata.rootIdentity == null
2423
+ || (typeof metadata.rootIdentity === "string" && metadata.rootIdentity.length <= 256))
2424
+ && /^[0-9a-f]{64}$/u.test(metadata.commandDigest)
2425
+ && containedMetadataWorktreePath(lease.worktreePath, metadata.worktreePath)
2426
+ && metadata.runtimeLeaseId === lease.executionLeaseId
2427
+ && metadata.dispatchAttemptId === lease.dispatchAttemptId
2428
+ && metadata.fencingToken === lease.fencingToken
2429
+ && Number.isSafeInteger(metadata.bridgePid)
2430
+ && metadata.bridgePid > 0;
2431
+ }
2432
+ function containedMetadataWorktreePath(worktreePath, metadataWorktreePath) {
2433
+ const relative = path.relative(path.resolve(worktreePath), path.resolve(metadataWorktreePath));
2434
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
2435
+ }
2436
+ function transitionHostLeaseForCleanup(lease, state) {
2437
+ if (lease.state === state || lease.state === "released")
2438
+ return lease;
2439
+ return transitionRuntimeIsolationLease(lease, state);
2440
+ }
2441
+ function throwIfAdmissionExpired(expiresAt) {
2442
+ if (admissionExpired(expiresAt))
2443
+ throw new HostRuntimeAdmissionExpiredError();
2444
+ }
2445
+ function admissionExpired(expiresAt) {
2446
+ const deadline = Date.parse(expiresAt);
2447
+ return !Number.isFinite(deadline) || deadline <= Date.now();
2448
+ }
2449
+ function scheduleAdmissionDeadline(expiresAt, expire) {
2450
+ const delay = Math.max(0, Date.parse(expiresAt) - Date.now());
2451
+ const timer = setTimeout(expire, Math.min(delay, 2_147_483_647));
2452
+ timer.unref?.();
2453
+ return timer;
2454
+ }
2455
+ function clearAdmissionDeadline(waiter) {
2456
+ if (waiter.deadlineTimer)
2457
+ clearTimeout(waiter.deadlineTimer);
2458
+ waiter.deadlineTimer = null;
2459
+ }
2460
+ function clearCapacityRecheck(waiter) {
2461
+ if (waiter.capacityRecheckTimer)
2462
+ clearInterval(waiter.capacityRecheckTimer);
2463
+ waiter.capacityRecheckTimer = null;
2464
+ }
2465
+ async function ensurePrivateDirectory(directory) {
2466
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
2467
+ if (process.platform !== "win32")
2468
+ await fs.chmod(directory, 0o700);
2469
+ }
2470
+ async function containedWorkingDirectory(worktreePath, configuredDirectory) {
2471
+ const [realWorktree, realDirectory] = await Promise.all([
2472
+ fs.realpath(worktreePath),
2473
+ fs.realpath(path.resolve(worktreePath, configuredDirectory)),
2474
+ ]);
2475
+ const relative = path.relative(realWorktree, realDirectory);
2476
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
2477
+ throw new Error("host_runtime_project_invalid: launch working directory must remain inside the prepared worktree.");
2478
+ }
2479
+ return realDirectory;
2480
+ }
2481
+ function uuidHex(value, label) {
2482
+ const normalized = value.replaceAll("-", "").toLowerCase();
2483
+ if (!/^[0-9a-f]{32}$/u.test(normalized)) {
2484
+ throw new Error(`host_runtime_lease_invalid: ${label} id must be a full UUID.`);
2485
+ }
2486
+ return normalized;
2487
+ }