@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge/bridge.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +8 -0
- package/dist/bridge/bridge.service.js +184 -12
- package/dist/bridge/bridge.types.d.ts +37 -0
- package/dist/cli/cli.help.js +1 -1
- package/dist/cli/cli.service.js +1 -1
- package/dist/cli/local-config.js +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
- package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
- package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
- package/dist/runtime/attempt-process-supervisor.js +75 -15
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +16 -0
- package/dist/runtime/host-listener-inspector.js +143 -4
- package/dist/runtime/host-runtime-contract.d.ts +9 -1
- package/dist/runtime/host-runtime-contract.js +411 -6
- package/dist/runtime/host-runtime-lease.d.ts +12 -1
- package/dist/runtime/host-runtime-lease.js +271 -50
- package/dist/runtime/host-runtime-promotion.d.ts +2 -0
- package/dist/runtime/host-runtime-promotion.js +35 -4
- package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
- package/dist/runtime/node-host-runtime-adapter.js +71 -8
- package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
- package/dist/sandbox/preflight.d.ts +9 -0
- package/dist/sandbox/preflight.js +39 -4
- package/dist/sandbox/workspace.service.js +7 -2
- package/dist/session/managed-attempt-tools.d.ts +17 -0
- package/dist/session/managed-attempt-tools.js +712 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +11 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +45 -9
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +21 -6
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +183 -8
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +144 -18
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.process.js +176 -18
- package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
- package/dist/session/provider-cli/provider-cli.service.js +390 -83
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +30 -2
- package/dist/session/session.base.d.ts +2 -1
- package/dist/session/session.service.d.ts +2 -1
- package/dist/session/session.service.js +303 -30
- package/dist/session/session.types.d.ts +38 -0
- package/dist/session/session.types.js +17 -0
- package/dist/shared/process/host-process-supervisor.d.ts +18 -1
- package/dist/shared/process/host-process-supervisor.js +94 -7
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +3 -2
- package/dist/worktree/artifact-injector.js +43 -4
- package/dist/worktree/dependency-warmer.d.ts +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +12 -1
- package/package.json +1 -1
- package/runtime/windows-job-supervisor.ps1 +89 -20
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { RunOnceDispatchPacket, RunnerDispatchArtifact, RunnerDispatchEvent, RunnerDispatchSpec } from "../session.types.js";
|
|
2
2
|
import type { WorktreeHandle } from "../../worktree/worktree.types.js";
|
|
3
|
+
import type { ProviderProcessRunner } from "./provider-cli.process.js";
|
|
3
4
|
export interface ProviderCliRunContext {
|
|
4
5
|
readonly dispatch: RunOnceDispatchPacket;
|
|
5
6
|
readonly worktree: WorktreeHandle;
|
|
6
7
|
readonly runtimeEnvironment?: Readonly<Record<string, string>>;
|
|
8
|
+
readonly processRunner?: ProviderProcessRunner;
|
|
7
9
|
readonly signal?: AbortSignal;
|
|
8
10
|
emit(event: RunnerDispatchEvent): Promise<void>;
|
|
9
11
|
}
|
|
@@ -11,6 +13,7 @@ export interface ProviderCliDraftContext {
|
|
|
11
13
|
readonly dispatch: RunOnceDispatchPacket;
|
|
12
14
|
readonly worktree: WorktreeHandle;
|
|
13
15
|
readonly runtimeEnvironment?: Readonly<Record<string, string>>;
|
|
16
|
+
readonly processRunner?: ProviderProcessRunner;
|
|
14
17
|
readonly signal?: AbortSignal;
|
|
15
18
|
emit(event: RunnerDispatchEvent): Promise<void>;
|
|
16
19
|
}
|
|
@@ -5,6 +5,8 @@ export declare function clip(value: string, maxLength: number): string;
|
|
|
5
5
|
export interface ProviderOutputState {
|
|
6
6
|
readonly sessionLabel: string | null;
|
|
7
7
|
finalMessage: string | null;
|
|
8
|
+
/** True only while the provider has reported a tool/command start without a matching completion. */
|
|
9
|
+
readonly hasActiveToolCall: boolean;
|
|
8
10
|
/** Accumulated provider-reported token usage for this run, or null when none was reported. */
|
|
9
11
|
readonly usage: ProviderUsage | null;
|
|
10
12
|
consume(line: string): readonly RunnerDispatchEvent[];
|
|
@@ -25,12 +25,16 @@ class CursorStreamJsonState {
|
|
|
25
25
|
finalMessage = null;
|
|
26
26
|
emittedSession = false;
|
|
27
27
|
accumulatedUsage = null;
|
|
28
|
+
activeToolCallCount = 0;
|
|
28
29
|
get sessionLabel() {
|
|
29
30
|
return this.sessionId ? `session ${this.sessionId}` : null;
|
|
30
31
|
}
|
|
31
32
|
get usage() {
|
|
32
33
|
return this.accumulatedUsage;
|
|
33
34
|
}
|
|
35
|
+
get hasActiveToolCall() {
|
|
36
|
+
return this.activeToolCallCount > 0;
|
|
37
|
+
}
|
|
34
38
|
consume(line) {
|
|
35
39
|
const event = parseJsonLine(line);
|
|
36
40
|
if (!event) {
|
|
@@ -65,6 +69,7 @@ class CursorStreamJsonState {
|
|
|
65
69
|
const subtype = stringField(event, "subtype");
|
|
66
70
|
const toolCall = objectField(event, "tool_call");
|
|
67
71
|
if (subtype === "started") {
|
|
72
|
+
this.activeToolCallCount += 1;
|
|
68
73
|
const summary = formatCursorToolCallStarted(toolCall);
|
|
69
74
|
if (summary) {
|
|
70
75
|
return [{
|
|
@@ -76,6 +81,7 @@ class CursorStreamJsonState {
|
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
if (subtype === "completed") {
|
|
84
|
+
this.activeToolCallCount = Math.max(0, this.activeToolCallCount - 1);
|
|
79
85
|
const completed = formatCursorToolCallCompleted(toolCall);
|
|
80
86
|
if (completed) {
|
|
81
87
|
return [{
|
|
@@ -224,18 +230,23 @@ class CodexJsonState {
|
|
|
224
230
|
finalMessage = null;
|
|
225
231
|
emittedThread = false;
|
|
226
232
|
accumulatedUsage = null;
|
|
233
|
+
activeToolCallCount = 0;
|
|
227
234
|
get sessionLabel() {
|
|
228
235
|
return this.threadId ? `thread ${this.threadId}` : null;
|
|
229
236
|
}
|
|
230
237
|
get usage() {
|
|
231
238
|
return this.accumulatedUsage;
|
|
232
239
|
}
|
|
240
|
+
get hasActiveToolCall() {
|
|
241
|
+
return this.activeToolCallCount > 0;
|
|
242
|
+
}
|
|
233
243
|
consume(line) {
|
|
234
244
|
const event = parseJsonLine(line);
|
|
235
245
|
if (!event) {
|
|
236
246
|
return [];
|
|
237
247
|
}
|
|
238
248
|
const type = stringField(event, "type") ?? "";
|
|
249
|
+
this.trackToolActivity(type, event);
|
|
239
250
|
if (type === "thread.started") {
|
|
240
251
|
this.threadId = stringField(event, "thread_id");
|
|
241
252
|
if (this.threadId && !this.emittedThread) {
|
|
@@ -265,7 +276,7 @@ class CodexJsonState {
|
|
|
265
276
|
eventType: "provider_cli_error"
|
|
266
277
|
}];
|
|
267
278
|
}
|
|
268
|
-
if (type === "item.completed" || type === "item.updated") {
|
|
279
|
+
if (type === "item.started" || type === "item.completed" || type === "item.updated") {
|
|
269
280
|
const item = objectField(event, "item");
|
|
270
281
|
const itemType = stringField(item, "type");
|
|
271
282
|
if (itemType === "agent_message") {
|
|
@@ -287,7 +298,7 @@ class CodexJsonState {
|
|
|
287
298
|
return [{
|
|
288
299
|
role: "worker",
|
|
289
300
|
kind: "checkpoint",
|
|
290
|
-
content: `Codex command ${status || "completed"}: ${clip(command, 500)}`,
|
|
301
|
+
content: `Codex command ${status || (type === "item.started" ? "started" : "completed")}: ${clip(command, 500)}`,
|
|
291
302
|
eventType: "provider_command"
|
|
292
303
|
}];
|
|
293
304
|
}
|
|
@@ -299,6 +310,23 @@ class CodexJsonState {
|
|
|
299
310
|
}
|
|
300
311
|
return [];
|
|
301
312
|
}
|
|
313
|
+
trackToolActivity(type, event) {
|
|
314
|
+
if (type !== "item.started" && type !== "item.completed") {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const item = objectField(event, "item");
|
|
318
|
+
const itemType = (stringField(item, "type") ?? "").toLowerCase();
|
|
319
|
+
const toolName = mcpToolName(item);
|
|
320
|
+
const isTool = itemType === "command_execution"
|
|
321
|
+
|| itemType.includes("mcp")
|
|
322
|
+
|| Boolean(toolName);
|
|
323
|
+
if (!isTool) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.activeToolCallCount = type === "item.started"
|
|
327
|
+
? this.activeToolCallCount + 1
|
|
328
|
+
: Math.max(0, this.activeToolCallCount - 1);
|
|
329
|
+
}
|
|
302
330
|
accumulateUsage(event) {
|
|
303
331
|
const usage = objectField(event, "usage");
|
|
304
332
|
if (Object.keys(usage).length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchEvent } from "./session.types.js";
|
|
1
|
+
import type { HostRuntimeReadinessProbeRequest, HostRuntimeReadinessProbeResult, RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchEvent } from "./session.types.js";
|
|
2
2
|
import type { HostRuntimeCapacitySnapshot, HostRuntimeReapOptions, HostRuntimeReapReport } from "../runtime/host-runtime-lease.js";
|
|
3
3
|
export interface SessionEventSink {
|
|
4
4
|
emit(event: RunnerDispatchEvent): Promise<void>;
|
|
@@ -8,4 +8,5 @@ export interface SessionService {
|
|
|
8
8
|
updateLease?(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
|
|
9
9
|
reapHostRuntime?(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport | null>;
|
|
10
10
|
runtimeCapacity?(): HostRuntimeCapacitySnapshot | null;
|
|
11
|
+
probeHostRuntime?(request: HostRuntimeReadinessProbeRequest, signal?: AbortSignal): Promise<HostRuntimeReadinessProbeResult>;
|
|
11
12
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ProviderCliService } from "./provider-cli/provider-cli.base.js";
|
|
2
2
|
import type { SessionEventSink, SessionService } from "./session.base.js";
|
|
3
|
-
import type { RunOnceDispatchPacket, RunOnceResultPacket } from "./session.types.js";
|
|
3
|
+
import type { HostRuntimeReadinessProbeRequest, HostRuntimeReadinessProbeResult, RunOnceDispatchPacket, RunOnceResultPacket } from "./session.types.js";
|
|
4
4
|
import type { WorktreeService } from "../worktree/worktree.base.js";
|
|
5
5
|
import { type HostRuntimeCapacitySnapshot, type HostRuntimeLeaseRuntime, type HostRuntimeReapOptions, type HostRuntimeReapReport } from "../runtime/host-runtime-lease.js";
|
|
6
6
|
export declare class BridgeSessionService implements SessionService {
|
|
@@ -11,5 +11,6 @@ export declare class BridgeSessionService implements SessionService {
|
|
|
11
11
|
updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
|
|
12
12
|
reapHostRuntime(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport | null>;
|
|
13
13
|
runtimeCapacity(): HostRuntimeCapacitySnapshot | null;
|
|
14
|
+
probeHostRuntime(request: HostRuntimeReadinessProbeRequest, signal?: AbortSignal): Promise<HostRuntimeReadinessProbeResult>;
|
|
14
15
|
runOnce(dispatch: RunOnceDispatchPacket, sink: SessionEventSink, signal?: AbortSignal): Promise<RunOnceResultPacket>;
|
|
15
16
|
}
|
|
@@ -3,6 +3,8 @@ import { RoutingProviderCliService } from "./provider-cli/provider-cli.service.j
|
|
|
3
3
|
import { GitWorktreeService } from "../worktree/worktree.service.js";
|
|
4
4
|
import { SandboxWorkspaceService } from "../sandbox/workspace.service.js";
|
|
5
5
|
import { HostRuntimeAdmissionExpiredError, hostRuntimeEventResources, } from "../runtime/host-runtime-lease.js";
|
|
6
|
+
import { configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible, } from "../runtime/host-runtime-promotion.js";
|
|
7
|
+
const minimumReadinessProbeStartupBudgetMs = 150_000;
|
|
6
8
|
export class BridgeSessionService {
|
|
7
9
|
worktrees;
|
|
8
10
|
providerCli;
|
|
@@ -23,6 +25,100 @@ export class BridgeSessionService {
|
|
|
23
25
|
runtimeCapacity() {
|
|
24
26
|
return this.hostRuntime?.snapshot?.() ?? null;
|
|
25
27
|
}
|
|
28
|
+
async probeHostRuntime(request, signal) {
|
|
29
|
+
if (!this.hostRuntime) {
|
|
30
|
+
return failedProbe("host_runtime_unavailable", "This bridge does not provide managed host runtime isolation.");
|
|
31
|
+
}
|
|
32
|
+
const dispatch = readinessProbeDispatch(request);
|
|
33
|
+
let worktree = null;
|
|
34
|
+
let admissionAcquired = false;
|
|
35
|
+
let runtimeStarted = false;
|
|
36
|
+
let result;
|
|
37
|
+
let cleanupFailure = null;
|
|
38
|
+
try {
|
|
39
|
+
throwIfAborted(signal);
|
|
40
|
+
await this.hostRuntime.invalidatePromotion?.(request.repoId);
|
|
41
|
+
await this.hostRuntime.acquire(dispatch, signal);
|
|
42
|
+
admissionAcquired = true;
|
|
43
|
+
assertReadinessProbeStartupBudget(request.expiresAt);
|
|
44
|
+
worktree = await this.worktrees.prepare({
|
|
45
|
+
repo: dispatch.repo,
|
|
46
|
+
workItem: dispatch.workItem,
|
|
47
|
+
baseBranch: request.revision,
|
|
48
|
+
worktreeKey: dispatch.lease.worktreeKey,
|
|
49
|
+
ownerType: "project",
|
|
50
|
+
ownerId: request.projectId,
|
|
51
|
+
lane: "runtime_readiness",
|
|
52
|
+
phase: "readiness_probe",
|
|
53
|
+
allowedRepoRoot: request.repoRoot,
|
|
54
|
+
// Readiness probes are disposable infrastructure checks, not agent lanes.
|
|
55
|
+
// Always remove their worktree even when the bridge is configured to retain
|
|
56
|
+
// failed agent worktrees for inspection.
|
|
57
|
+
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
58
|
+
artifacts: [],
|
|
59
|
+
});
|
|
60
|
+
assertReadinessProbeStartupBudget(request.expiresAt);
|
|
61
|
+
let started = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal);
|
|
62
|
+
runtimeStarted = true;
|
|
63
|
+
const discovery = started.runtimeContract.discovery;
|
|
64
|
+
if (!hostRuntimeParallelEligible(started.runtimeContract)) {
|
|
65
|
+
result = {
|
|
66
|
+
status: "serialized",
|
|
67
|
+
runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
|
|
68
|
+
runtimeContractSource: started.runtimeContract.source,
|
|
69
|
+
reasonCode: discovery?.reasonCode ?? "host_runtime_contract_serialized",
|
|
70
|
+
reasonMessage: discovery?.summary ?? "The detected runtime requires serialized host execution.",
|
|
71
|
+
checkedAt: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
assertReadinessProbeStartupBudget(request.expiresAt);
|
|
76
|
+
started = await this.hostRuntime.startConfiguredProject(request.requestId, request.fencingToken, signal);
|
|
77
|
+
const requiredPorts = configuredLaunchPortNames(started.runtimeContract);
|
|
78
|
+
const verifiedPorts = started.lease.resources.ports.map(port => port.name);
|
|
79
|
+
const verified = requiredPorts.length > 0 && requiredPorts.every(name => verifiedPorts.includes(name));
|
|
80
|
+
result = verified
|
|
81
|
+
? {
|
|
82
|
+
status: "parallel_ready",
|
|
83
|
+
runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
|
|
84
|
+
runtimeContractSource: started.runtimeContract.source,
|
|
85
|
+
reasonCode: null,
|
|
86
|
+
reasonMessage: null,
|
|
87
|
+
qaAppLaunchTarget: managedQaLaunchTarget(started.runtimeContract),
|
|
88
|
+
checkedAt: new Date().toISOString(),
|
|
89
|
+
}
|
|
90
|
+
: failedProbe("host_runtime_probe_unverified", "The application did not prove ownership and readiness on every Forge-assigned port.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
result = failedProbe(probeFailureCode(error), probeFailureMessage(error));
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
if (runtimeStarted || admissionAcquired) {
|
|
98
|
+
try {
|
|
99
|
+
const released = await this.hostRuntime.release(request.requestId, request.fencingToken);
|
|
100
|
+
if (runtimeStarted && !released)
|
|
101
|
+
cleanupFailure = new Error("The runtime lease was no longer current during cleanup.");
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
cleanupFailure = error instanceof Error ? error : new Error(String(error));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (worktree) {
|
|
108
|
+
try {
|
|
109
|
+
await this.worktrees.release(worktree);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
cleanupFailure ??= error instanceof Error ? error : new Error(String(error));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (cleanupFailure) {
|
|
117
|
+
await this.hostRuntime.invalidatePromotion?.(request.repoId).catch(() => undefined);
|
|
118
|
+
return failedProbe("host_runtime_cleanup_degraded", `Forge could not prove exact readiness cleanup: ${cleanupFailure.message}`);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
26
122
|
async runOnce(dispatch, sink, signal) {
|
|
27
123
|
validateApiKeySecretGrant(dispatch);
|
|
28
124
|
const redactionSecrets = exactSecrets(dispatch);
|
|
@@ -32,7 +128,9 @@ export class BridgeSessionService {
|
|
|
32
128
|
return;
|
|
33
129
|
}
|
|
34
130
|
const redacted = redactRunnerDispatchEvent(event, redactionSecrets);
|
|
35
|
-
|
|
131
|
+
if (redacted.eventType !== "provider_tool_heartbeat") {
|
|
132
|
+
events.push(redacted);
|
|
133
|
+
}
|
|
36
134
|
await sink.emit(redacted);
|
|
37
135
|
};
|
|
38
136
|
const emitLifecycle = async (event) => {
|
|
@@ -52,11 +150,25 @@ export class BridgeSessionService {
|
|
|
52
150
|
let hostLease = null;
|
|
53
151
|
try {
|
|
54
152
|
if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
|
|
153
|
+
const capacity = this.hostRuntime.snapshot?.() ?? null;
|
|
154
|
+
const projectCapacity = capacity?.projects.find(project => project.repoId === dispatch.repo.id) ?? null;
|
|
55
155
|
await emitLifecycle({
|
|
56
156
|
role: "runner",
|
|
57
157
|
kind: "checkpoint",
|
|
58
|
-
content:
|
|
158
|
+
content: capacity
|
|
159
|
+
? `Waiting for runner capacity 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). Forge will continue automatically when admission is available.`
|
|
160
|
+
: "Waiting for runner capacity before preparing the worktree. Forge will continue automatically when admission is available.",
|
|
59
161
|
eventType: "host_runtime_admission_waiting",
|
|
162
|
+
data: capacity ? JSON.stringify({
|
|
163
|
+
queue_position: capacity.waitingAttempts + 1,
|
|
164
|
+
runner_active_attempts: capacity.activeAttempts,
|
|
165
|
+
runner_reserved_attempts: capacity.reservedAttempts,
|
|
166
|
+
runner_waiting_attempts: capacity.waitingAttempts,
|
|
167
|
+
project_active_attempts: projectCapacity?.activeAttempts ?? 0,
|
|
168
|
+
project_reserved_attempts: projectCapacity?.reservedAttempts ?? 0,
|
|
169
|
+
project_waiting_attempts: projectCapacity?.waitingAttempts ?? 0,
|
|
170
|
+
project_safety_class: projectCapacity?.safetyClass ?? "unknown",
|
|
171
|
+
}) : undefined,
|
|
60
172
|
});
|
|
61
173
|
try {
|
|
62
174
|
hostSingletonKeys = await this.hostRuntime.acquire(dispatch, signal);
|
|
@@ -82,23 +194,28 @@ export class BridgeSessionService {
|
|
|
82
194
|
eventType: "host_runtime_admission_acquired",
|
|
83
195
|
});
|
|
84
196
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
197
|
+
try {
|
|
198
|
+
worktree = await this.worktrees.prepare({
|
|
199
|
+
repo: dispatch.repo,
|
|
200
|
+
workItem: dispatch.workItem,
|
|
201
|
+
baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
|
|
202
|
+
worktreeKey: dispatch.lease.worktreeKey,
|
|
203
|
+
ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
|
|
204
|
+
ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
|
|
205
|
+
lane: dispatch.execution.lane,
|
|
206
|
+
phase: dispatch.cycleRun.phase,
|
|
207
|
+
allowedRepoRoot: dispatch.serverLocal.repoRoot,
|
|
208
|
+
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
209
|
+
artifacts: dispatch.artifacts,
|
|
210
|
+
priorPhaseArtifacts: dispatch.priorPhaseArtifacts,
|
|
211
|
+
qaRework: dispatch.qaRework,
|
|
212
|
+
warmPathOverrides: dispatch.warmPathOverrides
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw new Error(`worktree_prepare_failed: ${failureMessage(error)}`, { cause: error });
|
|
217
|
+
}
|
|
218
|
+
let providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
|
|
102
219
|
throwIfAborted(signal);
|
|
103
220
|
await emit({
|
|
104
221
|
role: "runner",
|
|
@@ -125,7 +242,7 @@ export class BridgeSessionService {
|
|
|
125
242
|
content: `Host runtime lease ${hostLease.lease.leaseId} is active for this worktree.`,
|
|
126
243
|
eventType: "sandbox_started",
|
|
127
244
|
occurredAt: hostLease.lease.stateChangedAt,
|
|
128
|
-
data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract)),
|
|
245
|
+
data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
|
|
129
246
|
});
|
|
130
247
|
await emitLifecycle({
|
|
131
248
|
role: "runner",
|
|
@@ -146,16 +263,21 @@ export class BridgeSessionService {
|
|
|
146
263
|
kind: "checkpoint",
|
|
147
264
|
content: `Verified supervised project runtime ${assigned.name} on ${assigned.hostIp}:${assigned.hostPort}.`,
|
|
148
265
|
eventType: "host_runtime_project_ready",
|
|
149
|
-
data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract)),
|
|
266
|
+
data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
|
|
150
267
|
});
|
|
151
268
|
}
|
|
152
269
|
}
|
|
270
|
+
providerDispatch = withMaterializedManagedQaTarget(providerDispatch, hostLease);
|
|
153
271
|
if (dispatch.draft) {
|
|
154
272
|
throwIfAborted(signal);
|
|
273
|
+
const processRunner = hostLease && dispatch.sandbox
|
|
274
|
+
? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
|
|
275
|
+
: undefined;
|
|
155
276
|
const run = await this.providerCli.runDraft({
|
|
156
277
|
dispatch: providerDispatch,
|
|
157
278
|
worktree,
|
|
158
279
|
runtimeEnvironment: hostLease?.environment,
|
|
280
|
+
processRunner,
|
|
159
281
|
emit,
|
|
160
282
|
signal
|
|
161
283
|
});
|
|
@@ -183,18 +305,32 @@ export class BridgeSessionService {
|
|
|
183
305
|
dispatch: providerDispatch,
|
|
184
306
|
worktree,
|
|
185
307
|
runtimeEnvironment: hostLease?.environment,
|
|
308
|
+
processRunner: hostLease && dispatch.sandbox
|
|
309
|
+
? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
|
|
310
|
+
: undefined,
|
|
186
311
|
emit,
|
|
187
312
|
signal
|
|
188
313
|
};
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
? await this.providerCli.
|
|
193
|
-
:
|
|
194
|
-
? await this.providerCli.
|
|
195
|
-
:
|
|
196
|
-
? await this.providerCli.
|
|
197
|
-
:
|
|
314
|
+
let run;
|
|
315
|
+
try {
|
|
316
|
+
run = isQa
|
|
317
|
+
? await this.providerCli.runQa(context)
|
|
318
|
+
: isArchitect
|
|
319
|
+
? await this.providerCli.runArchitect(context)
|
|
320
|
+
: isCiRepair
|
|
321
|
+
? await this.providerCli.runCiRepair(context)
|
|
322
|
+
: isConflictResolve
|
|
323
|
+
? await this.providerCli.runConflictResolve(context)
|
|
324
|
+
: await this.providerCli.runImplementation(context);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
const message = failureMessage(error);
|
|
328
|
+
if (message.includes("attempt_process_supervisor_start_")
|
|
329
|
+
|| message.includes("host_process_supervisor_start_")) {
|
|
330
|
+
throw new Error(`provider_start_unavailable: ${message}`, { cause: error });
|
|
331
|
+
}
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
198
334
|
throwIfAborted(signal);
|
|
199
335
|
const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
200
336
|
return redactRunOnceResultPacket({
|
|
@@ -256,6 +392,143 @@ export class BridgeSessionService {
|
|
|
256
392
|
}
|
|
257
393
|
}
|
|
258
394
|
}
|
|
395
|
+
function failureMessage(error) {
|
|
396
|
+
return error instanceof Error ? error.message : String(error);
|
|
397
|
+
}
|
|
398
|
+
function readinessProbeDispatch(request) {
|
|
399
|
+
return {
|
|
400
|
+
schemaVersion: 2,
|
|
401
|
+
userId: request.userId,
|
|
402
|
+
workItem: {
|
|
403
|
+
id: request.projectId,
|
|
404
|
+
kind: "runtime_readiness",
|
|
405
|
+
title: "Automatic host-isolation readiness check",
|
|
406
|
+
description: "Internal readiness proof without a provider agent.",
|
|
407
|
+
acceptanceCriteria: "",
|
|
408
|
+
reproSteps: "",
|
|
409
|
+
systemInfo: "",
|
|
410
|
+
},
|
|
411
|
+
repo: {
|
|
412
|
+
id: request.repoId,
|
|
413
|
+
name: request.repoName,
|
|
414
|
+
remoteUrl: request.remoteUrl,
|
|
415
|
+
defaultBranch: request.defaultBranch,
|
|
416
|
+
cloneOnDemand: request.cloneOnDemand,
|
|
417
|
+
serverLocalPath: request.runnerRepoLocalPath,
|
|
418
|
+
},
|
|
419
|
+
cycleRun: {
|
|
420
|
+
id: request.requestId,
|
|
421
|
+
ownerType: "project",
|
|
422
|
+
ownerId: request.projectId,
|
|
423
|
+
kind: "runtime_readiness",
|
|
424
|
+
phase: "readiness_probe",
|
|
425
|
+
requestedBase: request.revision,
|
|
426
|
+
baseBranch: request.revision,
|
|
427
|
+
attempt: request.attempt,
|
|
428
|
+
model: "none",
|
|
429
|
+
},
|
|
430
|
+
session: { id: request.requestId, model: "none", lane: "runtime_readiness" },
|
|
431
|
+
lease: {
|
|
432
|
+
id: request.requestId,
|
|
433
|
+
ownerType: "project",
|
|
434
|
+
ownerId: request.projectId,
|
|
435
|
+
worktreeKey: `runtime-readiness-${request.requestId}`,
|
|
436
|
+
lane: "runtime_readiness",
|
|
437
|
+
fencingToken: request.fencingToken,
|
|
438
|
+
expiresAt: request.expiresAt,
|
|
439
|
+
},
|
|
440
|
+
execution: {
|
|
441
|
+
provider: "none",
|
|
442
|
+
model: "none",
|
|
443
|
+
authKind: "none",
|
|
444
|
+
location: "bridge",
|
|
445
|
+
runnerId: request.runnerId,
|
|
446
|
+
repoId: request.repoId,
|
|
447
|
+
role: "runtime_readiness",
|
|
448
|
+
lane: "runtime_readiness",
|
|
449
|
+
requiresApiKeySecretGrant: false,
|
|
450
|
+
runnerRepoLocalPath: request.runnerRepoLocalPath,
|
|
451
|
+
},
|
|
452
|
+
runtimeIntent: { executionClass: "managed_runtime", applicationRuntimeRequired: true },
|
|
453
|
+
serverLocal: { repoRoot: request.repoRoot, cleanupWorktrees: true },
|
|
454
|
+
sandbox: {
|
|
455
|
+
dispatchAttemptId: request.requestId,
|
|
456
|
+
executionLeaseId: request.requestId,
|
|
457
|
+
runnerInstanceId: request.runnerInstanceId,
|
|
458
|
+
executionMode: "host",
|
|
459
|
+
executionPolicy: "host_allowed",
|
|
460
|
+
networkPolicy: { mode: "offline", allowedHosts: [] },
|
|
461
|
+
requestedResources: {
|
|
462
|
+
sandboxSlots: 1,
|
|
463
|
+
cpuMillis: 2000,
|
|
464
|
+
workerMemoryLimitBytes: 1024 * 1024 * 1024,
|
|
465
|
+
serviceMemoryLimitBytes: 1024 * 1024 * 1024,
|
|
466
|
+
runtimeOverheadBytes: 256 * 1024 * 1024,
|
|
467
|
+
memoryBytes: 2 * 1024 * 1024 * 1024,
|
|
468
|
+
workspaceBytes: 10 * 1024 * 1024 * 1024,
|
|
469
|
+
pids: 512,
|
|
470
|
+
buildSlots: 1,
|
|
471
|
+
},
|
|
472
|
+
publicationMode: "shared_filesystem",
|
|
473
|
+
},
|
|
474
|
+
artifacts: [],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function failedProbe(reasonCode, reasonMessage) {
|
|
478
|
+
return { status: "failed", reasonCode, reasonMessage, checkedAt: new Date().toISOString() };
|
|
479
|
+
}
|
|
480
|
+
function assertReadinessProbeStartupBudget(expiresAt) {
|
|
481
|
+
const remainingMs = Date.parse(expiresAt) - Date.now();
|
|
482
|
+
if (!Number.isFinite(remainingMs) || remainingMs < minimumReadinessProbeStartupBudgetMs) {
|
|
483
|
+
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.");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function managedQaLaunchTarget(runtimeContract) {
|
|
487
|
+
const playwright = runtimeContract.contract.runtime.playwright;
|
|
488
|
+
if (!playwright)
|
|
489
|
+
return null;
|
|
490
|
+
const port = runtimeContract.contract.runtime.ports.find(candidate => candidate.name === playwright.baseUrlPort);
|
|
491
|
+
return port?.launch && port.baseUrlEnv && port.format
|
|
492
|
+
? { managedRuntimePort: port.name, routes: ["/"] }
|
|
493
|
+
: null;
|
|
494
|
+
}
|
|
495
|
+
function withMaterializedManagedQaTarget(dispatch, hostLease) {
|
|
496
|
+
const target = dispatch.qaBrowser?.launchTarget;
|
|
497
|
+
const portName = target?.managed_runtime_port?.trim().toLowerCase();
|
|
498
|
+
if (!target || !portName)
|
|
499
|
+
return dispatch;
|
|
500
|
+
if (!hostLease) {
|
|
501
|
+
throw new Error("qa_managed_runtime_unavailable: Browser QA requires the verified managed host runtime selected during readiness.");
|
|
502
|
+
}
|
|
503
|
+
const normalizedName = portName.toUpperCase().replaceAll(/[^A-Z0-9]/gu, "_");
|
|
504
|
+
const url = hostLease.environment[`FORGE_${normalizedName}_URL`];
|
|
505
|
+
if (!url) {
|
|
506
|
+
throw new Error(`qa_managed_runtime_unavailable: verified runtime port '${portName}' was not launched for this attempt.`);
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
...dispatch,
|
|
510
|
+
qaBrowser: dispatch.qaBrowser ? {
|
|
511
|
+
...dispatch.qaBrowser,
|
|
512
|
+
launchTarget: {
|
|
513
|
+
...target,
|
|
514
|
+
url: target.url ?? url,
|
|
515
|
+
ready_url: target.ready_url ?? url,
|
|
516
|
+
notes: [target.notes, "Forge launched and verified this URL inside the current fenced attempt."]
|
|
517
|
+
.filter(Boolean)
|
|
518
|
+
.join(" "),
|
|
519
|
+
},
|
|
520
|
+
} : dispatch.qaBrowser,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function probeFailureCode(error) {
|
|
524
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
525
|
+
const candidate = message.split(":", 1)[0]?.trim().toLowerCase() ?? "";
|
|
526
|
+
return /^[a-z][a-z0-9_.-]{2,159}$/u.test(candidate) ? candidate : "host_runtime_probe_failed";
|
|
527
|
+
}
|
|
528
|
+
function probeFailureMessage(error) {
|
|
529
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
530
|
+
return message.length > 1000 ? `${message.slice(0, 997)}...` : message;
|
|
531
|
+
}
|
|
259
532
|
function throwIfAborted(signal) {
|
|
260
533
|
if (signal?.aborted) {
|
|
261
534
|
throw new Error(String(signal.reason ?? "Dispatch cancelled."));
|
|
@@ -7,6 +7,7 @@ export interface RunOnceDispatchPacket {
|
|
|
7
7
|
readonly session: DispatchSession;
|
|
8
8
|
readonly lease: DispatchLease;
|
|
9
9
|
readonly execution: DispatchExecution;
|
|
10
|
+
readonly runtimeIntent: DispatchRuntimeIntent;
|
|
10
11
|
readonly serverLocal: DispatchServerLocalConfig;
|
|
11
12
|
readonly sandbox?: DispatchSandboxContext | null;
|
|
12
13
|
readonly pullRequest?: DispatchPullRequestContext | null;
|
|
@@ -25,6 +26,10 @@ export interface RunOnceDispatchPacket {
|
|
|
25
26
|
readonly designerRefs?: DispatchDesignerRefHandoffContext | null;
|
|
26
27
|
readonly draft?: DispatchDraftPrompt | null;
|
|
27
28
|
}
|
|
29
|
+
export interface DispatchRuntimeIntent {
|
|
30
|
+
readonly executionClass: "analysis_only" | "managed_runtime";
|
|
31
|
+
readonly applicationRuntimeRequired: boolean;
|
|
32
|
+
}
|
|
28
33
|
export interface DispatchSandboxContext {
|
|
29
34
|
readonly dispatchAttemptId: string;
|
|
30
35
|
readonly executionLeaseId: string;
|
|
@@ -105,6 +110,7 @@ export interface DispatchQaAppLaunchTarget {
|
|
|
105
110
|
readonly command?: string | null;
|
|
106
111
|
readonly working_directory?: string | null;
|
|
107
112
|
readonly ready_url?: string | null;
|
|
113
|
+
readonly managed_runtime_port?: string | null;
|
|
108
114
|
readonly routes: readonly string[];
|
|
109
115
|
readonly notes?: string | null;
|
|
110
116
|
}
|
|
@@ -385,4 +391,36 @@ export interface RunnerEventLine {
|
|
|
385
391
|
readonly event: RunnerDispatchEvent;
|
|
386
392
|
}
|
|
387
393
|
export declare function parseRunOnceDispatchPacket(value: unknown): RunOnceDispatchPacket;
|
|
394
|
+
export interface HostRuntimeReadinessProbeRequest {
|
|
395
|
+
readonly requestId: string;
|
|
396
|
+
readonly fencingToken: string;
|
|
397
|
+
readonly userId: string;
|
|
398
|
+
readonly projectId: string;
|
|
399
|
+
readonly repoId: string;
|
|
400
|
+
readonly runnerId: string;
|
|
401
|
+
readonly runnerInstanceId: string;
|
|
402
|
+
readonly attempt: number;
|
|
403
|
+
readonly repoName: string;
|
|
404
|
+
readonly remoteUrl: string;
|
|
405
|
+
readonly defaultBranch: string;
|
|
406
|
+
readonly cloneOnDemand: boolean;
|
|
407
|
+
readonly runnerRepoLocalPath: string;
|
|
408
|
+
readonly revision: string;
|
|
409
|
+
readonly expiresAt: string;
|
|
410
|
+
readonly repoRoot: string;
|
|
411
|
+
readonly cleanupWorktrees: boolean;
|
|
412
|
+
}
|
|
413
|
+
export interface HostRuntimeReadinessProbeResult {
|
|
414
|
+
readonly status: "parallel_ready" | "serialized" | "failed";
|
|
415
|
+
readonly runtimeContractFingerprint?: string | null;
|
|
416
|
+
readonly runtimeContractSource?: string | null;
|
|
417
|
+
readonly reasonCode?: string | null;
|
|
418
|
+
readonly reasonMessage?: string | null;
|
|
419
|
+
readonly qaAppLaunchTarget?: HostRuntimeReadinessQaLaunchTarget | null;
|
|
420
|
+
readonly checkedAt: string;
|
|
421
|
+
}
|
|
422
|
+
export interface HostRuntimeReadinessQaLaunchTarget {
|
|
423
|
+
readonly managedRuntimePort: string;
|
|
424
|
+
readonly routes: readonly string[];
|
|
425
|
+
}
|
|
388
426
|
export declare function failureResult(summary: string, events?: readonly RunnerDispatchEvent[]): RunOnceResultPacket;
|
|
@@ -14,6 +14,7 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
14
14
|
session: parseSession(packet.session),
|
|
15
15
|
lease: parseLease(packet.lease),
|
|
16
16
|
execution: parseExecution(packet.execution),
|
|
17
|
+
runtimeIntent: parseRuntimeIntent(packet.runtimeIntent),
|
|
17
18
|
serverLocal: parseServerLocal(packet.serverLocal),
|
|
18
19
|
sandbox: parseSandbox(packet.sandbox, schemaVersion),
|
|
19
20
|
pullRequest: parsePullRequest(packet.pullRequest),
|
|
@@ -33,6 +34,21 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
33
34
|
draft: parseDraftPrompt(packet.draft)
|
|
34
35
|
};
|
|
35
36
|
}
|
|
37
|
+
function parseRuntimeIntent(value) {
|
|
38
|
+
if (value === undefined || value === null) {
|
|
39
|
+
return { executionClass: "managed_runtime", applicationRuntimeRequired: true };
|
|
40
|
+
}
|
|
41
|
+
const intent = asObject(value, "runtimeIntent");
|
|
42
|
+
const executionClass = requiredString(intent, "executionClass").trim().toLowerCase();
|
|
43
|
+
if (executionClass !== "analysis_only" && executionClass !== "managed_runtime") {
|
|
44
|
+
throw new Error("runtimeIntent.executionClass must be analysis_only or managed_runtime.");
|
|
45
|
+
}
|
|
46
|
+
const applicationRuntimeRequired = requiredBoolean(intent, "applicationRuntimeRequired");
|
|
47
|
+
if ((executionClass === "analysis_only") === applicationRuntimeRequired) {
|
|
48
|
+
throw new Error("runtimeIntent execution class and application runtime requirement are inconsistent.");
|
|
49
|
+
}
|
|
50
|
+
return { executionClass, applicationRuntimeRequired };
|
|
51
|
+
}
|
|
36
52
|
function parseSandbox(value, schemaVersion) {
|
|
37
53
|
if (value === undefined || value === null) {
|
|
38
54
|
if (schemaVersion === 2) {
|
|
@@ -406,6 +422,7 @@ function parseQaAppLaunchTarget(value) {
|
|
|
406
422
|
command: optionalString(launchTarget, "command"),
|
|
407
423
|
working_directory: optionalString(launchTarget, "working_directory"),
|
|
408
424
|
ready_url: optionalString(launchTarget, "ready_url"),
|
|
425
|
+
managed_runtime_port: optionalString(launchTarget, "managed_runtime_port"),
|
|
409
426
|
routes: Array.isArray(routes)
|
|
410
427
|
? routes.map((entry, index) => {
|
|
411
428
|
if (typeof entry !== "string") {
|