@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 0.1.4-dev.bfa11ebe98c6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -1
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +9 -1
- package/dist/bridge/bridge.service.d.ts +129 -3
- package/dist/bridge/bridge.service.js +2426 -70
- package/dist/bridge/bridge.types.d.ts +218 -2
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +14 -2
- package/dist/cleanup/cleanup.service.js +496 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +55 -7
- package/dist/cli/cli.service.js +429 -219
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
- package/dist/runtime/attempt-process-supervisor.js +397 -0
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +20 -0
- package/dist/runtime/generated-output-reset.js +157 -0
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +191 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +51 -0
- package/dist/runtime/host-listener-inspector.js +385 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +58 -0
- package/dist/runtime/host-runtime-contract.js +754 -0
- package/dist/runtime/host-runtime-lease.d.ts +215 -0
- package/dist/runtime/host-runtime-lease.js +2449 -0
- package/dist/runtime/host-runtime-promotion.d.ts +32 -0
- package/dist/runtime/host-runtime-promotion.js +206 -0
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +285 -0
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +161 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +6 -0
- package/dist/sandbox/repository-lock.js +128 -0
- package/dist/sandbox/runtime.d.ts +302 -0
- package/dist/sandbox/runtime.js +2367 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +74 -0
- package/dist/session/managed-attempt-tools.d.ts +28 -0
- package/dist/session/managed-attempt-tools.js +1011 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +27 -0
- package/dist/session/phase-completion.artifacts.js +829 -0
- package/dist/session/phase-completion.d.ts +53 -42
- package/dist/session/phase-completion.helpers.d.ts +117 -0
- package/dist/session/phase-completion.helpers.js +707 -0
- package/dist/session/phase-completion.js +125 -3007
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +233 -0
- package/dist/session/phase-completion.runtime.d.ts +49 -0
- package/dist/session/phase-completion.runtime.js +833 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +859 -0
- package/dist/session/phase-completion.validation.d.ts +38 -0
- package/dist/session/phase-completion.validation.js +918 -0
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +657 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
- package/dist/session/provider-cli/provider-cli.process.js +874 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +78 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
- package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
- package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
- package/dist/session/provider-cli/provider-output-state.js +817 -0
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +448 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +586 -59
- package/dist/session/session.types.d.ts +324 -3
- package/dist/session/session.types.js +939 -10
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +21 -1
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +3 -0
- package/dist/shared/process/process.js +13 -7
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +4 -0
- package/dist/shared/provider-capabilities.js +88 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +47 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +82 -10
- package/dist/worktree/dependency-warmer.d.ts +53 -1
- package/dist/worktree/dependency-warmer.js +619 -11
- package/dist/worktree/index.d.ts +2 -1
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +16 -3
- package/dist/worktree/worktree.service.js +334 -54
- package/dist/worktree/worktree.types.d.ts +9 -1
- package/package.json +12 -7
- package/runtime/windows-job-supervisor.ps1 +722 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -1,22 +1,166 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { redactRunnerDispatchEvent, redactRunOnceResultPacket } from "./session-redaction.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { RoutingProviderCliService } from "./provider-cli/provider-cli.service.js";
|
|
4
|
+
import { ForgeFailureError, waitKindForEvent, waitWakeCondition } from "./session.types.js";
|
|
3
5
|
import { GitWorktreeService } from "../worktree/worktree.service.js";
|
|
6
|
+
import { SandboxWorkspaceService } from "../sandbox/workspace.service.js";
|
|
7
|
+
import { HostRuntimeAdmissionExpiredError, hostRuntimeEventResources, } from "../runtime/host-runtime-lease.js";
|
|
8
|
+
import { configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible, } from "../runtime/host-runtime-promotion.js";
|
|
9
|
+
const minimumReadinessProbeStartupBudgetMs = 150_000;
|
|
4
10
|
export class BridgeSessionService {
|
|
5
11
|
worktrees;
|
|
6
12
|
providerCli;
|
|
7
|
-
|
|
13
|
+
hostRuntime;
|
|
14
|
+
constructor(worktrees = process.env.FORGE_SANDBOX_ID
|
|
15
|
+
? new SandboxWorkspaceService()
|
|
16
|
+
: new GitWorktreeService(), providerCli = new RoutingProviderCliService(), hostRuntime) {
|
|
8
17
|
this.worktrees = worktrees;
|
|
9
18
|
this.providerCli = providerCli;
|
|
19
|
+
this.hostRuntime = hostRuntime;
|
|
20
|
+
}
|
|
21
|
+
async updateLease(dispatchAttemptId, fencingToken, expiresAt) {
|
|
22
|
+
await this.hostRuntime?.updateLease(dispatchAttemptId, fencingToken, expiresAt);
|
|
23
|
+
}
|
|
24
|
+
async reapHostRuntime(options) {
|
|
25
|
+
return await this.hostRuntime?.reap?.(options) ?? null;
|
|
26
|
+
}
|
|
27
|
+
runtimeCapacity() {
|
|
28
|
+
return this.hostRuntime?.snapshot?.() ?? null;
|
|
29
|
+
}
|
|
30
|
+
async probeHostRuntime(request, signal) {
|
|
31
|
+
if (!this.hostRuntime) {
|
|
32
|
+
return failedProbe("host_runtime_unavailable", "This bridge does not provide managed host runtime isolation.");
|
|
33
|
+
}
|
|
34
|
+
const dispatch = readinessProbeDispatch(request);
|
|
35
|
+
let worktree = null;
|
|
36
|
+
let admissionAcquired = false;
|
|
37
|
+
let runtimeStarted = false;
|
|
38
|
+
let result;
|
|
39
|
+
let cleanupFailure = null;
|
|
40
|
+
try {
|
|
41
|
+
throwIfAborted(signal);
|
|
42
|
+
await this.hostRuntime.acquire(dispatch, signal);
|
|
43
|
+
admissionAcquired = true;
|
|
44
|
+
// Readiness probes acquire background-exclusive project admission. Only
|
|
45
|
+
// invalidate the prior proof after that fence is held so a user lane can
|
|
46
|
+
// never observe the proof disappear underneath shared admission.
|
|
47
|
+
await this.hostRuntime.invalidatePromotion?.(request.repoId, "discovery");
|
|
48
|
+
assertReadinessProbeStartupBudget(request.expiresAt);
|
|
49
|
+
worktree = await this.worktrees.prepare({
|
|
50
|
+
repo: dispatch.repo,
|
|
51
|
+
workItem: dispatch.workItem,
|
|
52
|
+
baseBranch: request.revision,
|
|
53
|
+
worktreeKey: dispatch.lease.worktreeKey,
|
|
54
|
+
ownerType: "project",
|
|
55
|
+
ownerId: request.projectId,
|
|
56
|
+
lane: "runtime_readiness",
|
|
57
|
+
phase: "readiness_probe",
|
|
58
|
+
allowedRepoRoot: request.repoRoot,
|
|
59
|
+
// Readiness probes are disposable infrastructure checks, not agent lanes.
|
|
60
|
+
// Always remove their worktree even when the bridge is configured to retain
|
|
61
|
+
// failed agent worktrees for inspection.
|
|
62
|
+
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
63
|
+
artifacts: [],
|
|
64
|
+
});
|
|
65
|
+
assertReadinessProbeStartupBudget(request.expiresAt);
|
|
66
|
+
let started = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal);
|
|
67
|
+
runtimeStarted = true;
|
|
68
|
+
const discovery = started.runtimeContract.discovery;
|
|
69
|
+
if (!hostRuntimeParallelEligible(started.runtimeContract)) {
|
|
70
|
+
result = {
|
|
71
|
+
status: "serialized",
|
|
72
|
+
runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
|
|
73
|
+
runtimeContractSource: started.runtimeContract.source,
|
|
74
|
+
reasonCode: discovery?.reasonCode ?? "host_runtime_contract_serialized",
|
|
75
|
+
reasonMessage: discovery?.summary ?? "The detected runtime requires serialized host execution.",
|
|
76
|
+
checkedAt: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
assertReadinessProbeStartupBudget(request.expiresAt);
|
|
81
|
+
started = await this.hostRuntime.startConfiguredProject(request.requestId, request.fencingToken, signal);
|
|
82
|
+
const requiredPorts = configuredLaunchPortNames(started.runtimeContract);
|
|
83
|
+
const verifiedPorts = started.lease.resources.ports.map(port => port.name);
|
|
84
|
+
const verified = requiredPorts.length > 0 && requiredPorts.every(name => verifiedPorts.includes(name));
|
|
85
|
+
result = verified
|
|
86
|
+
? {
|
|
87
|
+
status: "parallel_ready",
|
|
88
|
+
runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
|
|
89
|
+
runtimeContractSource: started.runtimeContract.source,
|
|
90
|
+
reasonCode: null,
|
|
91
|
+
reasonMessage: null,
|
|
92
|
+
qaAppLaunchTarget: managedQaLaunchTarget(started.runtimeContract),
|
|
93
|
+
checkedAt: new Date().toISOString(),
|
|
94
|
+
}
|
|
95
|
+
: failedProbe("host_runtime_probe_unverified", "The application did not prove ownership and readiness on every Forge-assigned port.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
result = failedProbe(probeFailureCode(error), probeFailureMessage(error));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (runtimeStarted || admissionAcquired) {
|
|
103
|
+
try {
|
|
104
|
+
const released = await this.hostRuntime.release(request.requestId, request.fencingToken);
|
|
105
|
+
if (runtimeStarted && !released)
|
|
106
|
+
cleanupFailure = new Error("The runtime lease was no longer current during cleanup.");
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
cleanupFailure = error instanceof Error ? error : new Error(String(error));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (worktree) {
|
|
113
|
+
try {
|
|
114
|
+
await this.worktrees.release(worktree);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
cleanupFailure ??= error instanceof Error ? error : new Error(String(error));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (cleanupFailure) {
|
|
122
|
+
await this.hostRuntime.invalidatePromotion?.(request.repoId).catch(() => undefined);
|
|
123
|
+
return failedProbe("host_runtime_cleanup_degraded", `Forge could not prove exact readiness cleanup: ${cleanupFailure.message}`);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
10
126
|
}
|
|
11
127
|
async runOnce(dispatch, sink, signal) {
|
|
12
128
|
validateApiKeySecretGrant(dispatch);
|
|
13
129
|
const redactionSecrets = exactSecrets(dispatch);
|
|
14
130
|
const events = [];
|
|
131
|
+
let waitObservationVersion = 0;
|
|
132
|
+
const waitFirstObservedAt = new Map();
|
|
133
|
+
const waitObservation = (eventType, queuePosition) => {
|
|
134
|
+
const occurredAt = new Date().toISOString();
|
|
135
|
+
const kind = waitKindForEvent(eventType);
|
|
136
|
+
const firstObservedAt = waitFirstObservedAt.get(kind) ?? occurredAt;
|
|
137
|
+
waitFirstObservedAt.set(kind, firstObservedAt);
|
|
138
|
+
waitObservationVersion += 1;
|
|
139
|
+
const wait = {
|
|
140
|
+
schemaVersion: 2,
|
|
141
|
+
kind,
|
|
142
|
+
scope: "project",
|
|
143
|
+
resourceKeys: [`project-runtime:${dispatch.repo.id}`],
|
|
144
|
+
...(queuePosition === undefined ? {} : { queuePosition }),
|
|
145
|
+
wakeCondition: waitWakeCondition(kind),
|
|
146
|
+
firstObservedAt,
|
|
147
|
+
observationVersion: waitObservationVersion,
|
|
148
|
+
contractOrigin: "bridge_v2",
|
|
149
|
+
};
|
|
150
|
+
return { occurredAt, wait };
|
|
151
|
+
};
|
|
15
152
|
const emit = async (event) => {
|
|
16
153
|
if (signal?.aborted) {
|
|
17
154
|
return;
|
|
18
155
|
}
|
|
19
|
-
const redacted =
|
|
156
|
+
const redacted = redactRunnerDispatchEvent({ ...event, eventId: event.eventId ?? randomUUID() }, redactionSecrets);
|
|
157
|
+
if (redacted.eventType !== "provider_tool_heartbeat") {
|
|
158
|
+
events.push(redacted);
|
|
159
|
+
}
|
|
160
|
+
await sink.emit(redacted);
|
|
161
|
+
};
|
|
162
|
+
const emitLifecycle = async (event) => {
|
|
163
|
+
const redacted = redactRunnerDispatchEvent({ ...event, eventId: event.eventId ?? randomUUID() }, redactionSecrets);
|
|
20
164
|
events.push(redacted);
|
|
21
165
|
await sink.emit(redacted);
|
|
22
166
|
};
|
|
@@ -27,75 +171,234 @@ export class BridgeSessionService {
|
|
|
27
171
|
content: "forge-bridge accepted run-once dispatch.",
|
|
28
172
|
eventType: "dispatch_accepted"
|
|
29
173
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
emit,
|
|
34
|
-
signal
|
|
35
|
-
});
|
|
36
|
-
throwIfAborted(signal);
|
|
37
|
-
return redactSecrets({
|
|
38
|
-
schemaVersion: 1,
|
|
39
|
-
exitCode: 0,
|
|
40
|
-
summary: run.summary,
|
|
41
|
-
events,
|
|
42
|
-
spec: run.spec ?? null,
|
|
43
|
-
artifact: run.artifact ?? null,
|
|
44
|
-
artifacts: run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : []
|
|
45
|
-
}, redactionSecrets);
|
|
46
|
-
}
|
|
47
|
-
const worktree = await this.worktrees.prepare({
|
|
48
|
-
repo: dispatch.repo,
|
|
49
|
-
workItem: dispatch.workItem,
|
|
50
|
-
baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
|
|
51
|
-
worktreeKey: dispatch.lease.worktreeKey,
|
|
52
|
-
ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
|
|
53
|
-
ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
|
|
54
|
-
lane: dispatch.execution.lane,
|
|
55
|
-
phase: dispatch.cycleRun.phase,
|
|
56
|
-
allowedRepoRoot: dispatch.serverLocal.repoRoot,
|
|
57
|
-
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
58
|
-
artifacts: dispatch.artifacts,
|
|
59
|
-
qaRework: dispatch.qaRework,
|
|
60
|
-
warmPathOverrides: dispatch.warmPathOverrides
|
|
61
|
-
});
|
|
62
|
-
const providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
|
|
63
|
-
throwIfAborted(signal);
|
|
64
|
-
await emit({
|
|
65
|
-
role: "runner",
|
|
66
|
-
kind: "checkpoint",
|
|
67
|
-
content: `Prepared worktree ${worktree.branchName}.`,
|
|
68
|
-
eventType: "worktree_prepared"
|
|
69
|
-
});
|
|
70
|
-
if (worktree.dependencyWarm) {
|
|
71
|
-
await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
|
|
72
|
-
}
|
|
174
|
+
let worktree = null;
|
|
175
|
+
let hostSingletonKeys = null;
|
|
176
|
+
let hostLease = null;
|
|
73
177
|
try {
|
|
178
|
+
if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
|
|
179
|
+
const capacity = this.hostRuntime.snapshot?.() ?? null;
|
|
180
|
+
const projectCapacity = capacity?.projects.find(project => project.repoId === dispatch.repo.id) ?? null;
|
|
181
|
+
const admissionWait = waitObservation("host_runtime_admission_waiting", capacity ? capacity.waitingAttempts + 1 : undefined);
|
|
182
|
+
await emitLifecycle({
|
|
183
|
+
role: "runner",
|
|
184
|
+
kind: "checkpoint",
|
|
185
|
+
content: capacity
|
|
186
|
+
? `Waiting for host-runtime admission before preparing the worktree (queue position ${capacity.waitingAttempts + 1}; runner ${capacity.activeAttempts} active, ${capacity.reservedAttempts} reserved; project ${projectCapacity?.activeAttempts ?? 0} active, ${projectCapacity?.waitingAttempts ?? 0} already waiting). This wait can be caused by the project serialization policy, readiness proof, or resource headroom; Forge will continue automatically when admission is available.`
|
|
187
|
+
: "Waiting for host-runtime admission before preparing the worktree. Forge will continue automatically when admission is available.",
|
|
188
|
+
eventType: "host_runtime_admission_waiting",
|
|
189
|
+
...admissionWait,
|
|
190
|
+
data: capacity ? JSON.stringify({
|
|
191
|
+
queue_position: capacity.waitingAttempts + 1,
|
|
192
|
+
runner_active_attempts: capacity.activeAttempts,
|
|
193
|
+
runner_reserved_attempts: capacity.reservedAttempts,
|
|
194
|
+
runner_waiting_attempts: capacity.waitingAttempts,
|
|
195
|
+
project_active_attempts: projectCapacity?.activeAttempts ?? 0,
|
|
196
|
+
project_reserved_attempts: projectCapacity?.reservedAttempts ?? 0,
|
|
197
|
+
project_waiting_attempts: projectCapacity?.waitingAttempts ?? 0,
|
|
198
|
+
project_safety_class: projectCapacity?.safetyClass ?? "unknown",
|
|
199
|
+
}) : undefined,
|
|
200
|
+
});
|
|
201
|
+
try {
|
|
202
|
+
hostSingletonKeys = await (this.hostRuntime.acquirePreparation?.(dispatch, signal)
|
|
203
|
+
?? this.hostRuntime.acquire(dispatch, signal));
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof HostRuntimeAdmissionExpiredError) {
|
|
207
|
+
await emitLifecycle({
|
|
208
|
+
role: "runner",
|
|
209
|
+
kind: "error",
|
|
210
|
+
content: "Managed host admission expired while the project singleton remained busy; no worktree or runtime resources were created.",
|
|
211
|
+
eventType: "host_runtime_admission_expired",
|
|
212
|
+
}).catch(() => undefined);
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
throwIfAborted(signal);
|
|
217
|
+
const admissionAcquired = waitObservation("host_runtime_admission_acquired");
|
|
218
|
+
await emitLifecycle({
|
|
219
|
+
role: "runner",
|
|
220
|
+
kind: "checkpoint",
|
|
221
|
+
content: this.hostRuntime.acquirePreparation
|
|
222
|
+
? "Acquired worktree-preparation capacity. Prepared runtime verification must finish before agent admission."
|
|
223
|
+
: hostSingletonKeys.length > 0
|
|
224
|
+
? `Acquired managed host singleton ${hostSingletonKeys.join(", ")}.`
|
|
225
|
+
: "Observed assigned-port evidence permits provisional parallel host admission; the prepared runtime contract must still match before launch.",
|
|
226
|
+
eventType: "host_runtime_admission_acquired",
|
|
227
|
+
...admissionAcquired,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
worktree = await this.worktrees.prepare({
|
|
232
|
+
repo: dispatch.repo,
|
|
233
|
+
workItem: dispatch.workItem,
|
|
234
|
+
baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
|
|
235
|
+
worktreeKey: dispatch.lease.worktreeKey,
|
|
236
|
+
ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
|
|
237
|
+
ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
|
|
238
|
+
lane: dispatch.execution.lane,
|
|
239
|
+
phase: dispatch.cycleRun.phase,
|
|
240
|
+
allowedRepoRoot: dispatch.serverLocal.repoRoot,
|
|
241
|
+
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
242
|
+
artifacts: dispatch.artifacts,
|
|
243
|
+
priorPhaseArtifacts: dispatch.priorPhaseArtifacts,
|
|
244
|
+
storyQa: dispatch.storyQa,
|
|
245
|
+
referenceRunId: dispatch.sandbox?.dispatchAttemptId ?? dispatch.cycleRun.id,
|
|
246
|
+
referenceCycleRunId: dispatch.cycleRun.id,
|
|
247
|
+
effectivePlanAmendmentHash: dispatch.cycleRun.effectivePlanAmendmentHash,
|
|
248
|
+
qaRework: dispatch.qaRework,
|
|
249
|
+
warmPathOverrides: dispatch.warmPathOverrides,
|
|
250
|
+
applicationRuntimeRequired: dispatch.runtimeIntent.applicationRuntimeRequired,
|
|
251
|
+
preserveExistingState: Boolean(dispatch.session.resumedFromSessionId)
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
throw new ForgeFailureError("worktree_prepare_failed", `worktree_prepare_failed: ${failureMessage(error)}`, "bridge", { cause: error });
|
|
256
|
+
}
|
|
257
|
+
let providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
|
|
258
|
+
throwIfAborted(signal);
|
|
259
|
+
await emit({
|
|
260
|
+
role: "runner",
|
|
261
|
+
kind: "checkpoint",
|
|
262
|
+
content: dispatch.session.resumedFromSessionId
|
|
263
|
+
? `Reattached to worktree ${worktree.branchName} without resetting source or clearing attempt artifacts.`
|
|
264
|
+
: `Prepared worktree ${worktree.branchName}.`,
|
|
265
|
+
eventType: dispatch.session.resumedFromSessionId ? "worktree_reattached" : "worktree_prepared"
|
|
266
|
+
});
|
|
267
|
+
if ((worktree.hydratedPriorPhaseArtifactCount ?? 0) > 0) {
|
|
268
|
+
await emit({
|
|
269
|
+
role: "runner",
|
|
270
|
+
kind: "checkpoint",
|
|
271
|
+
content: `Hydrated ${worktree.hydratedPriorPhaseArtifactCount} persisted prior-phase artifact(s) into the prepared worktree.`,
|
|
272
|
+
eventType: "prior_phase_artifacts_hydrated"
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (worktree.dependencyWarm) {
|
|
276
|
+
await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
|
|
277
|
+
}
|
|
278
|
+
if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
|
|
279
|
+
hostLease = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal, async (checkpoint) => {
|
|
280
|
+
const observation = waitObservation(checkpoint.code);
|
|
281
|
+
await emitLifecycle({
|
|
282
|
+
role: "runner",
|
|
283
|
+
kind: "checkpoint",
|
|
284
|
+
content: checkpoint.message,
|
|
285
|
+
eventType: checkpoint.code,
|
|
286
|
+
...observation,
|
|
287
|
+
data: JSON.stringify({ expires_at: checkpoint.expiresAt }),
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
await emitLifecycle({
|
|
291
|
+
role: "runner",
|
|
292
|
+
kind: "checkpoint",
|
|
293
|
+
content: `Host runtime lease ${hostLease.lease.leaseId} is active for this worktree.`,
|
|
294
|
+
eventType: "sandbox_started",
|
|
295
|
+
occurredAt: hostLease.lease.stateChangedAt,
|
|
296
|
+
data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
|
|
297
|
+
});
|
|
298
|
+
await emitLifecycle({
|
|
299
|
+
role: "runner",
|
|
300
|
+
kind: "checkpoint",
|
|
301
|
+
content: "Managed host fallback isolates the worktree, attempt paths, assigned loopback ports, and supervised process trees, but not the host filesystem, network, credentials, or kernel. Serialized projects also hold a runner-local singleton. Enable Run in Docker in Project Settings when stronger isolation is required.",
|
|
302
|
+
eventType: "host_runtime_isolation_notice",
|
|
303
|
+
data: JSON.stringify({
|
|
304
|
+
isolationStrength: "managed_host_process",
|
|
305
|
+
remediation: "Enable Run in Docker in Project Settings.",
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
const unconfiguredPortCount = hostLease.lease.resources.ports.length;
|
|
309
|
+
hostLease = await this.hostRuntime.startConfiguredProject(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken, signal, async (checkpoint) => {
|
|
310
|
+
await emitLifecycle({
|
|
311
|
+
role: "runner",
|
|
312
|
+
kind: "checkpoint",
|
|
313
|
+
content: checkpoint.message,
|
|
314
|
+
eventType: checkpoint.code,
|
|
315
|
+
data: checkpoint.data ? JSON.stringify(checkpoint.data) : undefined,
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
if (hostLease.lease.resources.ports.length > unconfiguredPortCount) {
|
|
319
|
+
const assigned = hostLease.lease.resources.ports.at(-1);
|
|
320
|
+
await emitLifecycle({
|
|
321
|
+
role: "runner",
|
|
322
|
+
kind: "checkpoint",
|
|
323
|
+
content: `Verified supervised project runtime ${assigned.name} on ${assigned.hostIp}:${assigned.hostPort}.`,
|
|
324
|
+
eventType: "host_runtime_project_ready",
|
|
325
|
+
data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
providerDispatch = withMaterializedManagedQaTarget(providerDispatch, hostLease);
|
|
330
|
+
if (dispatch.draft) {
|
|
331
|
+
throwIfAborted(signal);
|
|
332
|
+
const processRunner = hostLease && dispatch.sandbox
|
|
333
|
+
? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
|
|
334
|
+
: undefined;
|
|
335
|
+
const run = await this.providerCli.runDraft({
|
|
336
|
+
dispatch: providerDispatch,
|
|
337
|
+
worktree,
|
|
338
|
+
runtimeEnvironment: hostLease?.environment,
|
|
339
|
+
processRunner,
|
|
340
|
+
emit,
|
|
341
|
+
signal
|
|
342
|
+
});
|
|
343
|
+
throwIfAborted(signal);
|
|
344
|
+
const draftArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
345
|
+
return redactRunOnceResultPacket({
|
|
346
|
+
schemaVersion: 2,
|
|
347
|
+
exitCode: 0,
|
|
348
|
+
summary: run.summary,
|
|
349
|
+
events,
|
|
350
|
+
spec: run.spec ?? null,
|
|
351
|
+
artifact: run.artifact ?? null,
|
|
352
|
+
artifacts: draftArtifacts
|
|
353
|
+
}, redactionSecrets);
|
|
354
|
+
}
|
|
74
355
|
throwIfAborted(signal);
|
|
75
|
-
const lane = dispatch.execution.lane.toLowerCase();
|
|
76
|
-
const role = dispatch.execution.role.toLowerCase();
|
|
356
|
+
const lane = dispatch.execution.lane.trim().toLowerCase();
|
|
357
|
+
const role = dispatch.execution.role.trim().toLowerCase();
|
|
358
|
+
const cycleKind = dispatch.cycleRun.kind.trim().toLowerCase();
|
|
77
359
|
const isQa = role === "qa" || lane === "qa";
|
|
78
|
-
const isArchitect = role === "architect" || lane === "architect" ||
|
|
360
|
+
const isArchitect = role === "architect" || lane === "architect" || cycleKind === "architect";
|
|
361
|
+
const isCiRepair = role === "ci_repair" || lane === "ci_repair" || cycleKind === "pr_checks";
|
|
79
362
|
const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
|
|
80
363
|
const context = {
|
|
81
364
|
dispatch: providerDispatch,
|
|
82
365
|
worktree,
|
|
366
|
+
runtimeEnvironment: hostLease?.environment,
|
|
367
|
+
processRunner: hostLease && dispatch.sandbox
|
|
368
|
+
? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
|
|
369
|
+
: undefined,
|
|
83
370
|
emit,
|
|
84
371
|
signal
|
|
85
372
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
? await this.providerCli.
|
|
90
|
-
:
|
|
91
|
-
? await this.providerCli.
|
|
92
|
-
:
|
|
373
|
+
let run;
|
|
374
|
+
try {
|
|
375
|
+
run = isQa
|
|
376
|
+
? await this.providerCli.runQa(context)
|
|
377
|
+
: isArchitect
|
|
378
|
+
? await this.providerCli.runArchitect(context)
|
|
379
|
+
: isCiRepair
|
|
380
|
+
? await this.providerCli.runCiRepair(context)
|
|
381
|
+
: isConflictResolve
|
|
382
|
+
? await this.providerCli.runConflictResolve(context)
|
|
383
|
+
: await this.providerCli.runImplementation(context);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
const message = failureMessage(error);
|
|
387
|
+
if (error instanceof ForgeFailureError
|
|
388
|
+
&& (error.failureCode === "attempt_process_supervisor_start_failed"
|
|
389
|
+
|| error.failureCode === "attempt_process_supervisor_start_timeout")) {
|
|
390
|
+
throw new ForgeFailureError("provider_start_unavailable", `provider_start_unavailable: ${message}`, "bridge", { cause: error });
|
|
391
|
+
}
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
93
394
|
throwIfAborted(signal);
|
|
94
395
|
const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
95
|
-
return
|
|
96
|
-
schemaVersion:
|
|
396
|
+
return redactRunOnceResultPacket({
|
|
397
|
+
schemaVersion: 2,
|
|
97
398
|
exitCode: 0,
|
|
98
399
|
summary: run.summary,
|
|
400
|
+
resultCommitSha: run.resultCommitSha ?? null,
|
|
401
|
+
publishedCommitSha: run.publishedCommitSha ?? null,
|
|
99
402
|
events,
|
|
100
403
|
spec: run.spec ?? null,
|
|
101
404
|
artifact: run.artifact ?? null,
|
|
@@ -103,10 +406,217 @@ export class BridgeSessionService {
|
|
|
103
406
|
}, redactionSecrets);
|
|
104
407
|
}
|
|
105
408
|
finally {
|
|
106
|
-
|
|
409
|
+
if (hostLease && dispatch.sandbox) {
|
|
410
|
+
const providerMemory = this.hostRuntime?.providerMemoryTelemetry?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
|
|
411
|
+
if (providerMemory) {
|
|
412
|
+
await emitLifecycle({
|
|
413
|
+
role: "runner",
|
|
414
|
+
kind: "checkpoint",
|
|
415
|
+
content: `Observed managed-host provider process-tree peak memory ${formatMemoryBytes(providerMemory.peakMemoryBytes)} from ${providerMemory.sampleCount} sample${providerMemory.sampleCount === 1 ? "" : "s"}.`,
|
|
416
|
+
eventType: "sandbox_peak_memory_observed",
|
|
417
|
+
occurredAt: providerMemory.observedAt,
|
|
418
|
+
data: JSON.stringify({
|
|
419
|
+
peakMemoryBytes: providerMemory.peakMemoryBytes,
|
|
420
|
+
sampleCount: providerMemory.sampleCount,
|
|
421
|
+
source: providerMemory.source,
|
|
422
|
+
containers: [{
|
|
423
|
+
role: "agent",
|
|
424
|
+
peakMemoryBytes: providerMemory.peakMemoryBytes,
|
|
425
|
+
}],
|
|
426
|
+
}),
|
|
427
|
+
}).catch(() => undefined);
|
|
428
|
+
}
|
|
429
|
+
const teardownAt = new Date().toISOString();
|
|
430
|
+
await emitLifecycle({
|
|
431
|
+
role: "runner",
|
|
432
|
+
kind: "checkpoint",
|
|
433
|
+
content: `Releasing host runtime lease ${hostLease.lease.leaseId}.`,
|
|
434
|
+
eventType: "sandbox_teardown_started",
|
|
435
|
+
occurredAt: teardownAt,
|
|
436
|
+
}).catch(() => undefined);
|
|
437
|
+
try {
|
|
438
|
+
const released = await this.hostRuntime?.release(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
|
|
439
|
+
if (!released) {
|
|
440
|
+
await emitLifecycle({
|
|
441
|
+
role: "runner",
|
|
442
|
+
kind: "error",
|
|
443
|
+
content: "Host runtime cleanup requires reconciliation: the runtime lease was not current during release.",
|
|
444
|
+
eventType: "teardown_failed",
|
|
445
|
+
occurredAt: new Date().toISOString(),
|
|
446
|
+
}).catch(() => undefined);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
await emitLifecycle({
|
|
450
|
+
role: "runner",
|
|
451
|
+
kind: "checkpoint",
|
|
452
|
+
content: `Released host runtime lease ${released.leaseId}. Diagnostics remain runner-local.`,
|
|
453
|
+
eventType: "sandbox_teardown_completed",
|
|
454
|
+
occurredAt: released.releasedAt,
|
|
455
|
+
}).catch(() => undefined);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
await emitLifecycle({
|
|
460
|
+
role: "runner",
|
|
461
|
+
kind: "error",
|
|
462
|
+
content: `Host runtime cleanup requires reconciliation: ${error instanceof Error ? error.message : String(error)}`,
|
|
463
|
+
eventType: "teardown_failed",
|
|
464
|
+
occurredAt: new Date().toISOString(),
|
|
465
|
+
}).catch(() => undefined);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!hostLease && hostSingletonKeys && dispatch.sandbox) {
|
|
469
|
+
await this.hostRuntime?.release(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken).catch(() => undefined);
|
|
470
|
+
}
|
|
471
|
+
if (worktree && !isPauseCancellation(signal))
|
|
472
|
+
await this.worktrees.release(worktree);
|
|
107
473
|
}
|
|
108
474
|
}
|
|
109
475
|
}
|
|
476
|
+
function failureMessage(error) {
|
|
477
|
+
return error instanceof Error ? error.message : String(error);
|
|
478
|
+
}
|
|
479
|
+
function formatMemoryBytes(bytes) {
|
|
480
|
+
return `${(bytes / (1024 ** 2)).toFixed(bytes >= 1024 ** 3 ? 0 : 1)} MiB`;
|
|
481
|
+
}
|
|
482
|
+
function isPauseCancellation(signal) {
|
|
483
|
+
return signal?.aborted === true
|
|
484
|
+
&& String(signal.reason ?? "").trim().toLowerCase() === "session pause requested";
|
|
485
|
+
}
|
|
486
|
+
function readinessProbeDispatch(request) {
|
|
487
|
+
return {
|
|
488
|
+
schemaVersion: 2,
|
|
489
|
+
userId: request.userId,
|
|
490
|
+
workItem: {
|
|
491
|
+
id: request.projectId,
|
|
492
|
+
kind: "runtime_readiness",
|
|
493
|
+
title: "Automatic host-isolation readiness check",
|
|
494
|
+
description: "Internal readiness proof without a provider agent.",
|
|
495
|
+
acceptanceCriteria: "",
|
|
496
|
+
reproSteps: "",
|
|
497
|
+
systemInfo: "",
|
|
498
|
+
},
|
|
499
|
+
repo: {
|
|
500
|
+
id: request.repoId,
|
|
501
|
+
name: request.repoName,
|
|
502
|
+
remoteUrl: request.remoteUrl,
|
|
503
|
+
defaultBranch: request.defaultBranch,
|
|
504
|
+
cloneOnDemand: request.cloneOnDemand,
|
|
505
|
+
serverLocalPath: request.runnerRepoLocalPath,
|
|
506
|
+
},
|
|
507
|
+
cycleRun: {
|
|
508
|
+
id: request.requestId,
|
|
509
|
+
ownerType: "project",
|
|
510
|
+
ownerId: request.projectId,
|
|
511
|
+
kind: "runtime_readiness",
|
|
512
|
+
phase: "readiness_probe",
|
|
513
|
+
requestedBase: request.revision,
|
|
514
|
+
baseBranch: request.revision,
|
|
515
|
+
attempt: request.attempt,
|
|
516
|
+
model: "none",
|
|
517
|
+
},
|
|
518
|
+
session: { id: request.requestId, model: "none", lane: "runtime_readiness" },
|
|
519
|
+
lease: {
|
|
520
|
+
id: request.requestId,
|
|
521
|
+
ownerType: "project",
|
|
522
|
+
ownerId: request.projectId,
|
|
523
|
+
worktreeKey: `runtime-readiness-${request.requestId}`,
|
|
524
|
+
lane: "runtime_readiness",
|
|
525
|
+
fencingToken: request.fencingToken,
|
|
526
|
+
expiresAt: request.expiresAt,
|
|
527
|
+
},
|
|
528
|
+
execution: {
|
|
529
|
+
provider: "none",
|
|
530
|
+
model: "none",
|
|
531
|
+
authKind: "none",
|
|
532
|
+
location: "bridge",
|
|
533
|
+
runnerId: request.runnerId,
|
|
534
|
+
repoId: request.repoId,
|
|
535
|
+
role: "runtime_readiness",
|
|
536
|
+
lane: "runtime_readiness",
|
|
537
|
+
requiresApiKeySecretGrant: false,
|
|
538
|
+
runnerRepoLocalPath: request.runnerRepoLocalPath,
|
|
539
|
+
},
|
|
540
|
+
runtimeIntent: { executionClass: "managed_runtime", applicationRuntimeRequired: true },
|
|
541
|
+
serverLocal: { repoRoot: request.repoRoot, cleanupWorktrees: true },
|
|
542
|
+
sandbox: {
|
|
543
|
+
dispatchAttemptId: request.requestId,
|
|
544
|
+
executionLeaseId: request.requestId,
|
|
545
|
+
runnerInstanceId: request.runnerInstanceId,
|
|
546
|
+
executionMode: "host",
|
|
547
|
+
executionPolicy: "host_allowed",
|
|
548
|
+
networkPolicy: { mode: "offline", allowedHosts: [] },
|
|
549
|
+
requestedResources: {
|
|
550
|
+
sandboxSlots: 1,
|
|
551
|
+
cpuMillis: 2000,
|
|
552
|
+
workerMemoryLimitBytes: 1024 * 1024 * 1024,
|
|
553
|
+
serviceMemoryLimitBytes: 1024 * 1024 * 1024,
|
|
554
|
+
runtimeOverheadBytes: 256 * 1024 * 1024,
|
|
555
|
+
memoryBytes: 2 * 1024 * 1024 * 1024,
|
|
556
|
+
workspaceBytes: 10 * 1024 * 1024 * 1024,
|
|
557
|
+
pids: 512,
|
|
558
|
+
buildSlots: 1,
|
|
559
|
+
},
|
|
560
|
+
publicationMode: "shared_filesystem",
|
|
561
|
+
},
|
|
562
|
+
artifacts: [],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function failedProbe(reasonCode, reasonMessage) {
|
|
566
|
+
return { status: "failed", reasonCode, reasonMessage, checkedAt: new Date().toISOString() };
|
|
567
|
+
}
|
|
568
|
+
function assertReadinessProbeStartupBudget(expiresAt) {
|
|
569
|
+
const remainingMs = Date.parse(expiresAt) - Date.now();
|
|
570
|
+
if (!Number.isFinite(remainingMs) || remainingMs < minimumReadinessProbeStartupBudgetMs) {
|
|
571
|
+
throw new Error("host_runtime_probe_deadline_insufficient: readiness admission completed too close to the probe deadline; Forge released the reservation before launching the application.");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function managedQaLaunchTarget(runtimeContract) {
|
|
575
|
+
const playwright = runtimeContract.contract.runtime.playwright;
|
|
576
|
+
if (!playwright)
|
|
577
|
+
return null;
|
|
578
|
+
const port = runtimeContract.contract.runtime.ports.find(candidate => candidate.name === playwright.baseUrlPort);
|
|
579
|
+
return port?.launch && port.baseUrlEnv && port.format
|
|
580
|
+
? { managedRuntimePort: port.name, routes: ["/"] }
|
|
581
|
+
: null;
|
|
582
|
+
}
|
|
583
|
+
function withMaterializedManagedQaTarget(dispatch, hostLease) {
|
|
584
|
+
const target = dispatch.qaBrowser?.launchTarget;
|
|
585
|
+
const portName = target?.managed_runtime_port?.trim().toLowerCase();
|
|
586
|
+
if (!target || !portName)
|
|
587
|
+
return dispatch;
|
|
588
|
+
if (!hostLease) {
|
|
589
|
+
throw new Error("qa_managed_runtime_unavailable: Browser QA requires the verified managed host runtime selected during readiness.");
|
|
590
|
+
}
|
|
591
|
+
const normalizedName = portName.toUpperCase().replaceAll(/[^A-Z0-9]/gu, "_");
|
|
592
|
+
const url = hostLease.environment[`FORGE_${normalizedName}_URL`];
|
|
593
|
+
if (!url) {
|
|
594
|
+
throw new Error(`qa_managed_runtime_unavailable: verified runtime port '${portName}' was not launched for this attempt.`);
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
...dispatch,
|
|
598
|
+
qaBrowser: dispatch.qaBrowser ? {
|
|
599
|
+
...dispatch.qaBrowser,
|
|
600
|
+
launchTarget: {
|
|
601
|
+
...target,
|
|
602
|
+
url: target.url ?? url,
|
|
603
|
+
ready_url: target.ready_url ?? url,
|
|
604
|
+
notes: [target.notes, "Forge launched and verified this URL inside the current fenced attempt."]
|
|
605
|
+
.filter(Boolean)
|
|
606
|
+
.join(" "),
|
|
607
|
+
},
|
|
608
|
+
} : dispatch.qaBrowser,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function probeFailureCode(error) {
|
|
612
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
613
|
+
const candidate = message.split(":", 1)[0]?.trim().toLowerCase() ?? "";
|
|
614
|
+
return /^[a-z][a-z0-9_.-]{2,159}$/u.test(candidate) ? candidate : "host_runtime_probe_failed";
|
|
615
|
+
}
|
|
616
|
+
function probeFailureMessage(error) {
|
|
617
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
618
|
+
return message.length > 1000 ? `${message.slice(0, 997)}...` : message;
|
|
619
|
+
}
|
|
110
620
|
function throwIfAborted(signal) {
|
|
111
621
|
if (signal?.aborted) {
|
|
112
622
|
throw new Error(String(signal.reason ?? "Dispatch cancelled."));
|
|
@@ -147,6 +657,23 @@ function withPreparedQaReworkPacket(dispatch, packetPath) {
|
|
|
147
657
|
};
|
|
148
658
|
}
|
|
149
659
|
async function emitDependencyWarmEvents(warm, emit) {
|
|
660
|
+
for (const receipt of warm.receipts) {
|
|
661
|
+
await emit({
|
|
662
|
+
role: "runner",
|
|
663
|
+
kind: "checkpoint",
|
|
664
|
+
content: receipt.cacheHit
|
|
665
|
+
? `Reused verified ${receipt.ecosystem} dependency preparation receipt ${receipt.fingerprint} in ${receipt.packageRoot}.`
|
|
666
|
+
: `Published ${receipt.ecosystem} dependency preparation receipt ${receipt.fingerprint} in ${receipt.packageRoot}.`,
|
|
667
|
+
eventType: receipt.cacheHit ? "dependencies_preparation_cached" : "dependencies_preparation_receipt_published",
|
|
668
|
+
data: JSON.stringify({
|
|
669
|
+
ecosystem: receipt.ecosystem,
|
|
670
|
+
package_root: receipt.packageRoot,
|
|
671
|
+
cache_hit: receipt.cacheHit,
|
|
672
|
+
fingerprint: receipt.fingerprint,
|
|
673
|
+
receipt_path: receipt.receiptPath,
|
|
674
|
+
}),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
150
677
|
if (warm.linked.length > 0) {
|
|
151
678
|
await emit({
|
|
152
679
|
role: "runner",
|