stageflow 0.18.0 → 0.23.0
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 +2 -1
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/activityObserver.d.ts +3 -0
- package/dist/agent/activityObserver.js +6 -1
- package/dist/agent/claudeAdapter.js +38 -0
- package/dist/agent/fakeAgent.js +3 -1
- package/dist/agent/piAdapter.js +40 -4
- package/dist/agent/port.d.ts +3 -1
- package/dist/agent/providerInspect.d.ts +19 -0
- package/dist/agent/providerInspect.js +35 -0
- package/dist/agent/stageTimeout.d.ts +9 -0
- package/dist/agent/stageTimeout.js +24 -0
- package/dist/agent/streamLogRedact.d.ts +2 -0
- package/dist/agent/streamLogRedact.js +13 -0
- package/dist/cli/artifactCommand.js +2 -2
- package/dist/cli/envelopeCommand.js +2 -2
- package/dist/cli/exportRunCommand.js +2 -2
- package/dist/cli/graphCommand.d.ts +15 -0
- package/dist/cli/graphCommand.js +79 -0
- package/dist/cli/graphRender.d.ts +2 -0
- package/dist/cli/graphRender.js +85 -0
- package/dist/cli/hostClient.d.ts +43 -0
- package/dist/cli/hostClient.js +182 -0
- package/dist/cli/providersCommand.js +5 -3
- package/dist/cli/runCommand.d.ts +6 -5
- package/dist/cli/runCommand.js +23 -31
- package/dist/cli/runOutput.d.ts +1 -1
- package/dist/cli/runsCommand.d.ts +4 -8
- package/dist/cli/runsCommand.js +65 -89
- package/dist/cli.js +13 -5
- package/dist/config/describePipeline.d.ts +26 -0
- package/dist/config/describePipeline.js +37 -0
- package/dist/config/loadPipeline.d.ts +13 -1
- package/dist/config/loadPipeline.js +34 -4
- package/dist/config/mergePipelineIncludes.d.ts +11 -0
- package/dist/config/mergePipelineIncludes.js +75 -0
- package/dist/config/validateCatalog.d.ts +2 -2
- package/dist/config/validateCatalog.js +24 -17
- package/dist/mcp/catalogTools.js +146 -52
- package/dist/mcp/controlTools.js +94 -19
- package/dist/mcp/deps.d.ts +3 -0
- package/dist/mcp/projectMcpTools.d.ts +3 -0
- package/dist/mcp/projectMcpTools.js +25 -0
- package/dist/mcp/providerTools.d.ts +3 -0
- package/dist/mcp/providerTools.js +17 -0
- package/dist/mcp/readArtifact.d.ts +9 -0
- package/dist/mcp/readArtifact.js +11 -0
- package/dist/mcp/server.js +9 -1
- package/dist/mcp/tailStreamLog.d.ts +8 -0
- package/dist/mcp/tailStreamLog.js +28 -0
- package/dist/mcp/toolResults.d.ts +17 -0
- package/dist/mcp/toolResults.js +15 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/mcp/waitRun.js +4 -4
- package/dist/mcp/waitingGates.d.ts +1 -0
- package/dist/mcp/waitingGates.js +3 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +3 -0
- package/dist/projection/projectRun.js +7 -0
- package/dist/runstore/port.d.ts +3 -1
- package/dist/runstore/port.js +2 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +15 -1
- package/dist/runstore/workspaceLayout.d.ts +1 -0
- package/dist/runstore/workspaceLayout.js +3 -0
- package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
- package/dist/runtime/feedbackLoopCoordinator.js +50 -20
- package/dist/runtime/feedbackLoopDecision.js +33 -1
- package/dist/runtime/forkGeneration.d.ts +4 -0
- package/dist/runtime/forkGeneration.js +16 -0
- package/dist/runtime/pipelineRunner.d.ts +3 -3
- package/dist/runtime/pipelineRunner.js +3 -1
- package/dist/runtime/pipelineScheduler.js +23 -1
- package/dist/runtime/reopenSkipped.d.ts +13 -0
- package/dist/runtime/reopenSkipped.js +56 -0
- package/dist/runtime/resumeReconstruct.js +2 -2
- package/dist/runtime/resumeTimedOut.d.ts +33 -0
- package/dist/runtime/resumeTimedOut.js +165 -0
- package/dist/runtime/runManager.d.ts +3 -1
- package/dist/runtime/runManager.js +110 -14
- package/dist/runtime/settingsFile.d.ts +2 -0
- package/dist/runtime/settingsFile.js +19 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
- package/dist/runtime/stageAttemptBootstrap.js +8 -2
- package/dist/runtime/stageRecovery.js +2 -2
- package/dist/runtime/stageRunner.js +6 -2
- package/dist/runtime/stageStreamLog.d.ts +17 -0
- package/dist/runtime/stageStreamLog.js +89 -0
- package/dist/runtime/stageWorker.js +2 -1
- package/dist/server/bootstrap.js +9 -2
- package/dist/server/ensureGlobalService.d.ts +35 -0
- package/dist/server/ensureGlobalService.js +138 -0
- package/dist/server/http.d.ts +20 -1
- package/dist/server/http.js +452 -386
- package/dist/server/mcpHost.js +7 -8
- package/dist/server/operatorResults.d.ts +10 -0
- package/dist/server/operatorResults.js +24 -0
- package/dist/server/providerRoutes.d.ts +0 -6
- package/dist/server/providerRoutes.js +10 -18
- package/dist/tools/askOperator.d.ts +1 -0
- package/dist/tools/askOperator.js +14 -11
- package/dist/tools/emitStageEnvelope.d.ts +1 -0
- package/dist/tools/emitStageEnvelope.js +3 -1
- package/dist/tools/prepareToolArguments.d.ts +4 -0
- package/dist/tools/prepareToolArguments.js +79 -0
- package/dist/types/pipeline.d.ts +8 -0
- package/dist/ui/assets/{index-DsgVjLUS.css → index-CSsDued1.css} +1 -1
- package/dist/ui/assets/index-Da5EHU6M.js +119 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/skills/stageflow/references/control-surface.md +2 -2
- package/skills/stageflow-run/SKILL.md +27 -9
- package/skills/stageflow-run/references/mcp-call.md +1 -1
- package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
- package/dist/ui/assets/index-DwKGaQvQ.js +0 -119
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { normalizeForkChoice } from "../envelope/forkChoice.js";
|
|
2
|
+
import { forkCohortMapsFromStore } from "./forkGeneration.js";
|
|
3
|
+
import { joinAllowsRun } from "./joinReadiness.js";
|
|
4
|
+
export function isUnchosenForkChild(dag, stageId, envelopes) {
|
|
5
|
+
for (const node of dag.nodes) {
|
|
6
|
+
const envelope = envelopes.get(node.id);
|
|
7
|
+
if (envelope?.fork_choice === undefined || envelope.fork_choice === null) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const children = dag.childrenOf[node.id] ?? [];
|
|
11
|
+
if (!children.includes(stageId))
|
|
12
|
+
continue;
|
|
13
|
+
const chosen = normalizeForkChoice(envelope.fork_choice, "stored");
|
|
14
|
+
if (!chosen.has(stageId))
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
export function eventsShowStarted(events) {
|
|
20
|
+
return events.some((event) => event.event === "started" || event.event === "resumed");
|
|
21
|
+
}
|
|
22
|
+
export function shouldReopenSkippedStage(dag, stageId, states, envelopes, hasStarted, options) {
|
|
23
|
+
if (hasStarted)
|
|
24
|
+
return false;
|
|
25
|
+
if (options?.supersededCloneIds?.has(stageId))
|
|
26
|
+
return false;
|
|
27
|
+
if (states.get(stageId) !== "skipped")
|
|
28
|
+
return false;
|
|
29
|
+
if (isUnchosenForkChild(dag, stageId, envelopes))
|
|
30
|
+
return false;
|
|
31
|
+
const hypothetical = new Map(states);
|
|
32
|
+
hypothetical.set(stageId, "pending");
|
|
33
|
+
return joinAllowsRun(dag, stageId, hypothetical, envelopes);
|
|
34
|
+
}
|
|
35
|
+
export async function persistReopenedStages(store, runId, stageIds, states) {
|
|
36
|
+
for (const id of stageIds) {
|
|
37
|
+
if (states.get(id) !== "skipped")
|
|
38
|
+
continue;
|
|
39
|
+
await store.appendStageEvent(runId, id, { event: "reopened" });
|
|
40
|
+
states.set(id, "pending");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function reopenRunnableSkippedStages(store, runId, dag, states, envelopes) {
|
|
44
|
+
const { supersededCloneIds } = await forkCohortMapsFromStore(store, runId);
|
|
45
|
+
const ids = [];
|
|
46
|
+
for (const [id, state] of states) {
|
|
47
|
+
if (state !== "skipped")
|
|
48
|
+
continue;
|
|
49
|
+
const events = await store.listStageEvents(runId, id);
|
|
50
|
+
if (!shouldReopenSkippedStage(dag, id, states, envelopes, eventsShowStarted(events), { supersededCloneIds })) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
ids.push(id);
|
|
54
|
+
}
|
|
55
|
+
await persistReopenedStages(store, runId, ids, states);
|
|
56
|
+
}
|
|
@@ -33,12 +33,12 @@ export async function loadRunContext(store, runId, cwd) {
|
|
|
33
33
|
}
|
|
34
34
|
export async function reconstructAndContinue(ctx) {
|
|
35
35
|
const { runId, stageId, opaqueAnswer, agent, store, cwd } = ctx;
|
|
36
|
-
const factoryCwd = ctx.factoryCwd ?? cwd;
|
|
37
36
|
try {
|
|
38
37
|
const latestExecution = await store.getLatestStageExecution(runId, stageId);
|
|
39
38
|
const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
|
|
40
39
|
const attemptOpt = attemptCtx.eventOptions();
|
|
41
40
|
const { meta, task, loaded } = await loadRunContext(store, runId, cwd);
|
|
41
|
+
const factoryCwd = meta.project_root ?? ctx.factoryCwd ?? cwd;
|
|
42
42
|
const definitionId = definitionIdForInstance(meta.pipeline_dag, stageId);
|
|
43
43
|
const stageIndex = loaded.stages.findIndex((s) => s.id === definitionId);
|
|
44
44
|
if (stageIndex < 0) {
|
|
@@ -127,7 +127,7 @@ export async function reconstructAndContinue(ctx) {
|
|
|
127
127
|
run: { runId, workspaceDir },
|
|
128
128
|
agent,
|
|
129
129
|
store,
|
|
130
|
-
cwd,
|
|
130
|
+
cwd: factoryCwd,
|
|
131
131
|
projectRoot: factoryCwd,
|
|
132
132
|
checkoutRoot,
|
|
133
133
|
hitl: ctx.hitl,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentPort } from "../agent/port.js";
|
|
2
|
+
import type { RunStore } from "../runstore/port.js";
|
|
3
|
+
import type { StageExecutionMode } from "./stageConcurrency.js";
|
|
4
|
+
import type { StageProcessLauncher } from "./stageProcessLauncher.js";
|
|
5
|
+
import { StageHitlController } from "./stageHitl.js";
|
|
6
|
+
import { type OperatorCatalog } from "./stageAttemptBootstrap.js";
|
|
7
|
+
export type ResumeTimedOutContext = {
|
|
8
|
+
runId: string;
|
|
9
|
+
stageId: string;
|
|
10
|
+
agent: AgentPort;
|
|
11
|
+
store: RunStore;
|
|
12
|
+
hitl: StageHitlController;
|
|
13
|
+
executionMode: StageExecutionMode;
|
|
14
|
+
stageProcessLauncher?: StageProcessLauncher;
|
|
15
|
+
cwd: string;
|
|
16
|
+
factoryCwd?: string;
|
|
17
|
+
maxActiveStagesPerRun: number;
|
|
18
|
+
operatorCatalog?: OperatorCatalog;
|
|
19
|
+
};
|
|
20
|
+
export declare function assertTimedOutStageEligible(status: string, events: ReadonlyArray<{
|
|
21
|
+
event: string;
|
|
22
|
+
reason?: string;
|
|
23
|
+
}>): {
|
|
24
|
+
ok: true;
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
reason: string;
|
|
28
|
+
status: 409;
|
|
29
|
+
};
|
|
30
|
+
export declare function reconstructTimedOutAndContinue(ctx: ResumeTimedOutContext): Promise<{
|
|
31
|
+
ok: boolean;
|
|
32
|
+
reason?: string;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { fakeHitlResumePath } from "../agent/fakeAgent.js";
|
|
3
|
+
import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
4
|
+
import { attemptSessionPath } from "../runstore/workspaceLayout.js";
|
|
5
|
+
import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
|
|
6
|
+
import { resumeRun } from "./pipelineScheduler.js";
|
|
7
|
+
import { loadRunContext } from "./resumeReconstruct.js";
|
|
8
|
+
import { isRunStageWaiting, runStage } from "./stageRunner.js";
|
|
9
|
+
import { attemptContext } from "./stageAttemptContext.js";
|
|
10
|
+
import { openStageAttempt, } from "./stageAttemptBootstrap.js";
|
|
11
|
+
import { buildStageRoots, withResolvedAuthPath } from "./stageRoots.js";
|
|
12
|
+
import { isStageTimeoutReason, lastFailedReason, } from "../agent/stageTimeout.js";
|
|
13
|
+
import { deriveExecutionPatchFromEvent } from "../runstore/stageExecution.js";
|
|
14
|
+
export function assertTimedOutStageEligible(status, events) {
|
|
15
|
+
if (status !== "failed") {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: `Stage is not timed out (status=${status})`,
|
|
19
|
+
status: 409,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (!isStageTimeoutReason(lastFailedReason(events))) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
reason: "Stage did not fail due to timeout; use retry to start a new attempt",
|
|
26
|
+
status: 409,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { ok: true };
|
|
30
|
+
}
|
|
31
|
+
export async function reconstructTimedOutAndContinue(ctx) {
|
|
32
|
+
const { runId, stageId, agent, store, cwd } = ctx;
|
|
33
|
+
const factoryCwd = ctx.factoryCwd ?? cwd;
|
|
34
|
+
try {
|
|
35
|
+
const latestExecution = await store.getLatestStageExecution(runId, stageId);
|
|
36
|
+
const attempt = latestExecution?.attempt ?? 1;
|
|
37
|
+
const attemptCtx = attemptContext(attempt);
|
|
38
|
+
const attemptOpt = attemptCtx.eventOptions();
|
|
39
|
+
const { meta, task, loaded } = await loadRunContext(store, runId, cwd);
|
|
40
|
+
const definitionId = definitionIdForInstance(meta.pipeline_dag, stageId);
|
|
41
|
+
const stageIndex = loaded.stages.findIndex((s) => s.id === definitionId);
|
|
42
|
+
if (stageIndex < 0) {
|
|
43
|
+
const reason = `Stage ${stageId} not in pipeline ${meta.pipeline_id}`;
|
|
44
|
+
await store.appendStageEvent(runId, stageId, { event: "failed", reason }, attemptOpt);
|
|
45
|
+
await store.updateRunStatus(runId, "failed");
|
|
46
|
+
return { ok: false, reason };
|
|
47
|
+
}
|
|
48
|
+
const stage = loaded.stages[stageIndex];
|
|
49
|
+
const dag = meta.pipeline_dag ?? loaded.dag;
|
|
50
|
+
const workspaceDir = store.getWorkspaceDir(runId);
|
|
51
|
+
const checkoutRoot = meta.checkout_root;
|
|
52
|
+
const roots = withResolvedAuthPath(buildStageRoots(workspaceDir, stageId, checkoutRoot, attemptCtx), factoryCwd);
|
|
53
|
+
const sessionFile = attemptSessionPath(workspaceDir, stageId, attempt);
|
|
54
|
+
const fakeResume = fakeHitlResumePath(roots, stageId);
|
|
55
|
+
if (!existsSync(sessionFile) && !existsSync(fakeResume)) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
reason: "missing session to resume; use retry to start a new attempt",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const resumed = { event: "resumed" };
|
|
62
|
+
await store.appendStageEvent(runId, stageId, resumed, attemptOpt);
|
|
63
|
+
try {
|
|
64
|
+
const execution = await store.getStageExecution(runId, stageId, attempt);
|
|
65
|
+
const patch = deriveExecutionPatchFromEvent(resumed, execution);
|
|
66
|
+
if (Object.keys(patch).length > 0) {
|
|
67
|
+
await store.updateStageExecution(runId, stageId, attempt, patch);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// no execution row yet
|
|
72
|
+
}
|
|
73
|
+
await store.updateRunStatus(runId, "running");
|
|
74
|
+
const opened = await openStageAttempt({
|
|
75
|
+
agent,
|
|
76
|
+
store,
|
|
77
|
+
runId,
|
|
78
|
+
stage,
|
|
79
|
+
stageId,
|
|
80
|
+
task,
|
|
81
|
+
dag,
|
|
82
|
+
checkoutRoot,
|
|
83
|
+
workspaceDir,
|
|
84
|
+
factoryCwd,
|
|
85
|
+
attemptCtx,
|
|
86
|
+
operatorCatalog: ctx.operatorCatalog,
|
|
87
|
+
sessionMode: "timeout_resume",
|
|
88
|
+
resumeToken: sessionFile,
|
|
89
|
+
onActivity: (event) => {
|
|
90
|
+
void store.appendStageEvent(runId, stageId, event, attemptOpt);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (!opened.ok) {
|
|
94
|
+
await store.appendStageEvent(runId, stageId, { event: "failed", reason: opened.reason }, attemptOpt);
|
|
95
|
+
await store.updateRunStatus(runId, "failed");
|
|
96
|
+
return { ok: false, reason: opened.reason };
|
|
97
|
+
}
|
|
98
|
+
const result = await runStage({
|
|
99
|
+
agent,
|
|
100
|
+
store,
|
|
101
|
+
runId,
|
|
102
|
+
stage,
|
|
103
|
+
stageId,
|
|
104
|
+
task,
|
|
105
|
+
dag,
|
|
106
|
+
checkoutRoot,
|
|
107
|
+
workspaceDir,
|
|
108
|
+
hitl: ctx.hitl,
|
|
109
|
+
skipStarted: true,
|
|
110
|
+
existingHandle: opened.handle,
|
|
111
|
+
attemptCtx,
|
|
112
|
+
factoryCwd,
|
|
113
|
+
operatorCatalog: ctx.operatorCatalog,
|
|
114
|
+
sessionMode: "timeout_resume",
|
|
115
|
+
resumeToken: sessionFile,
|
|
116
|
+
});
|
|
117
|
+
if (isRunStageWaiting(result)) {
|
|
118
|
+
await store.updateRunStatus(runId, "failed");
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
reason: WAIT_WITHOUT_WORKER_DISPATCH,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!result.ok) {
|
|
125
|
+
await store.updateRunStatus(runId, "failed");
|
|
126
|
+
return { ok: false, reason: result.reason };
|
|
127
|
+
}
|
|
128
|
+
const rest = await resumeRun({
|
|
129
|
+
prepared: {
|
|
130
|
+
task,
|
|
131
|
+
loaded,
|
|
132
|
+
run: { runId, workspaceDir },
|
|
133
|
+
agent,
|
|
134
|
+
store,
|
|
135
|
+
cwd,
|
|
136
|
+
projectRoot: factoryCwd,
|
|
137
|
+
checkoutRoot,
|
|
138
|
+
hitl: ctx.hitl,
|
|
139
|
+
operatorCatalog: ctx.operatorCatalog,
|
|
140
|
+
},
|
|
141
|
+
maxActiveStagesPerRun: ctx.maxActiveStagesPerRun,
|
|
142
|
+
resumeFromStageId: stageId,
|
|
143
|
+
initialPrior: result.envelope ?? null,
|
|
144
|
+
executionMode: ctx.executionMode,
|
|
145
|
+
stageProcessLauncher: ctx.stageProcessLauncher,
|
|
146
|
+
});
|
|
147
|
+
if (rest.outcome === "failed") {
|
|
148
|
+
return { ok: false, reason: rest.reason };
|
|
149
|
+
}
|
|
150
|
+
return { ok: true };
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const reason = err instanceof Error ? err.message : `timeout resume failed: ${String(err)}`;
|
|
154
|
+
try {
|
|
155
|
+
const latestExecution = await store.getLatestStageExecution(runId, stageId);
|
|
156
|
+
const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
|
|
157
|
+
await store.appendStageEvent(runId, stageId, { event: "failed", reason }, attemptCtx.eventOptions());
|
|
158
|
+
await store.updateRunStatus(runId, "failed");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// ignore secondary failures
|
|
162
|
+
}
|
|
163
|
+
return { ok: false, reason };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentPort, OpaqueAnswer } from "../agent/port.js";
|
|
2
|
+
import type { InlinePipelineDefinition } from "../types/pipeline.js";
|
|
2
3
|
import { type RunStore } from "../runstore/port.js";
|
|
3
4
|
import type { TaskFile } from "../types/task.js";
|
|
4
5
|
import { type PipelineRunResult } from "./pipelineRunner.js";
|
|
@@ -125,7 +126,7 @@ export declare class RunManager {
|
|
|
125
126
|
}>>;
|
|
126
127
|
abandonStage(runId: string, stageId: string): Promise<AbandonStageResult>;
|
|
127
128
|
startRun(input: StartTaskInput & {
|
|
128
|
-
pipeline: string;
|
|
129
|
+
pipeline: string | InlinePipelineDefinition;
|
|
129
130
|
task?: string | TaskFile;
|
|
130
131
|
checkoutOverride?: string;
|
|
131
132
|
skipGates?: boolean;
|
|
@@ -135,6 +136,7 @@ export declare class RunManager {
|
|
|
135
136
|
}): Promise<StartRunResult>;
|
|
136
137
|
rerun(runId: string): Promise<StartRunResult>;
|
|
137
138
|
retryStage(runId: string, stageId: string): Promise<RetryStageResult>;
|
|
139
|
+
resumeTimedOutStage(runId: string, stageId: string): Promise<RetryStageResult>;
|
|
138
140
|
recoverManualStage(runId: string, stageId: string, guidance?: string): Promise<RetryStageResult>;
|
|
139
141
|
recoverManualStageUntilStop(runId: string, stageId: string, guidance?: string): Promise<{
|
|
140
142
|
ok: true;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile, realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { findProjectRoot } from "../project/findProjectRoot.js";
|
|
4
5
|
import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
|
|
5
6
|
import { loadRunContext } from "./resumeReconstruct.js";
|
|
7
|
+
import { assertTimedOutStageEligible, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
|
|
6
8
|
import { loadTaskFromYaml } from "../config/loadTask.js";
|
|
7
9
|
import { deriveStatusFromStages, findUnhandledFailedStage, } from "../runstore/port.js";
|
|
8
10
|
import { PipelineValidationError, startPipeline, } from "./pipelineRunner.js";
|
|
@@ -11,7 +13,7 @@ import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
|
|
|
11
13
|
import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
|
|
12
14
|
import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
|
|
13
15
|
import { StageProcessLauncher } from "./stageProcessLauncher.js";
|
|
14
|
-
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount,
|
|
16
|
+
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
|
|
15
17
|
import { StageHitlController, waitKey, } from "./stageHitl.js";
|
|
16
18
|
import { resolveAndValidateCheckout, } from "./stageRoots.js";
|
|
17
19
|
import { orchestrateAnswerResume } from "./answerResume.js";
|
|
@@ -93,10 +95,9 @@ export class RunManager {
|
|
|
93
95
|
this.cwd = options.cwd ?? process.cwd();
|
|
94
96
|
this.projectRoot = options.projectRoot ?? this.cwd;
|
|
95
97
|
this.isGitProject = options.isGitProject ?? false;
|
|
96
|
-
const settingsCtx = projectSettingsContext(this.cwd, this.projectRoot, this.isGitProject);
|
|
97
98
|
this.maxConcurrent =
|
|
98
99
|
options.maxConcurrent ??
|
|
99
|
-
|
|
100
|
+
readMaxConcurrentFromGlobal() ??
|
|
100
101
|
parseMaxConcurrent(process.env.STAGEFLOW_MAX_CONCURRENT_RUNS);
|
|
101
102
|
this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
|
|
102
103
|
this.executionMode = readStageExecutionMode(process.env, options.executionMode);
|
|
@@ -118,7 +119,7 @@ export class RunManager {
|
|
|
118
119
|
throw new Error(INVALID_SLOT_COUNT_MESSAGE);
|
|
119
120
|
}
|
|
120
121
|
this.maxConcurrent = parsed;
|
|
121
|
-
|
|
122
|
+
writeMaxConcurrentToGlobal(parsed);
|
|
122
123
|
return this.getHealth();
|
|
123
124
|
}
|
|
124
125
|
getActiveCount() {
|
|
@@ -349,6 +350,11 @@ export class RunManager {
|
|
|
349
350
|
}
|
|
350
351
|
async startRun(input) {
|
|
351
352
|
const cwd = this.options.cwd ?? process.cwd();
|
|
353
|
+
// An inline pipeline has no filesystem anchor to derive a project root from.
|
|
354
|
+
const derivedProjectRoot = typeof input.pipeline === "string"
|
|
355
|
+
? (findProjectRoot(path.dirname(path.resolve(cwd, input.pipeline))) ??
|
|
356
|
+
this.projectRoot)
|
|
357
|
+
: this.projectRoot;
|
|
352
358
|
let resolved;
|
|
353
359
|
try {
|
|
354
360
|
resolved = resolveStartTaskInput(input, cwd);
|
|
@@ -383,7 +389,7 @@ export class RunManager {
|
|
|
383
389
|
gitSha: input.gitSha,
|
|
384
390
|
ciPrUrl: input.ciPrUrl,
|
|
385
391
|
ciJobUrl: input.ciJobUrl,
|
|
386
|
-
},
|
|
392
|
+
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined);
|
|
387
393
|
}
|
|
388
394
|
async rerun(runId) {
|
|
389
395
|
const cwd = this.options.cwd ?? process.cwd();
|
|
@@ -417,6 +423,82 @@ export class RunManager {
|
|
|
417
423
|
async retryStage(runId, stageId) {
|
|
418
424
|
return this.retryStageInternal(runId, stageId, true);
|
|
419
425
|
}
|
|
426
|
+
async resumeTimedOutStage(runId, stageId) {
|
|
427
|
+
const resumeKey = waitKey(runId, stageId);
|
|
428
|
+
if (this.resumeInFlight.has(resumeKey) || this.retryInFlight.has(resumeKey)) {
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
reason: `Resume already in progress for run ${runId} stage ${stageId}`,
|
|
432
|
+
status: 409,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
this.resumeInFlight.add(resumeKey);
|
|
436
|
+
let detail;
|
|
437
|
+
try {
|
|
438
|
+
detail = await this.options.store.readRun(runId);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
this.resumeInFlight.delete(resumeKey);
|
|
442
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
443
|
+
}
|
|
444
|
+
const stageSnap = detail.stages.find((s) => s.stage_id === stageId);
|
|
445
|
+
if (!stageSnap) {
|
|
446
|
+
this.resumeInFlight.delete(resumeKey);
|
|
447
|
+
return { ok: false, reason: `Stage not found: ${stageId}`, status: 404 };
|
|
448
|
+
}
|
|
449
|
+
const eligibility = assertTimedOutStageEligible(stageSnap.status, stageSnap.events);
|
|
450
|
+
if (!eligibility.ok) {
|
|
451
|
+
this.resumeInFlight.delete(resumeKey);
|
|
452
|
+
return eligibility;
|
|
453
|
+
}
|
|
454
|
+
const latest = await this.options.store.getLatestStageExecution(runId, stageId);
|
|
455
|
+
const attemptIndex = latest?.attempt ?? 1;
|
|
456
|
+
const wasActive = this.active.has(runId);
|
|
457
|
+
const tracked = await this.ensureResumeTracked(runId);
|
|
458
|
+
if (!tracked.ok) {
|
|
459
|
+
this.resumeInFlight.delete(resumeKey);
|
|
460
|
+
return { ok: false, reason: tracked.reason, status: 409 };
|
|
461
|
+
}
|
|
462
|
+
const insertedForResume = !wasActive;
|
|
463
|
+
try {
|
|
464
|
+
const runProjectRoot = detail.project_root ?? this.projectRoot;
|
|
465
|
+
const done = reconstructTimedOutAndContinue({
|
|
466
|
+
runId,
|
|
467
|
+
stageId,
|
|
468
|
+
agent: this.options.agent,
|
|
469
|
+
store: this.options.store,
|
|
470
|
+
hitl: this.hitl,
|
|
471
|
+
executionMode: this.executionMode,
|
|
472
|
+
cwd: runProjectRoot,
|
|
473
|
+
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
474
|
+
factoryCwd: runProjectRoot,
|
|
475
|
+
...(this.stageProcessLauncher !== undefined
|
|
476
|
+
? { stageProcessLauncher: this.stageProcessLauncher }
|
|
477
|
+
: {}),
|
|
478
|
+
...(this.options.operatorCatalog !== undefined
|
|
479
|
+
? { operatorCatalog: this.options.operatorCatalog }
|
|
480
|
+
: {}),
|
|
481
|
+
});
|
|
482
|
+
this.registerResumeUntrack(runId, done);
|
|
483
|
+
const outcome = await done;
|
|
484
|
+
if (!outcome.ok) {
|
|
485
|
+
if (insertedForResume) {
|
|
486
|
+
this.removeActiveEntry(runId, false);
|
|
487
|
+
}
|
|
488
|
+
const missingSession = outcome.reason !== undefined &&
|
|
489
|
+
outcome.reason.startsWith("missing session to resume");
|
|
490
|
+
return {
|
|
491
|
+
ok: false,
|
|
492
|
+
reason: outcome.reason ?? "timeout resume failed",
|
|
493
|
+
status: missingSession ? 409 : 500,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return { ok: true, runId, stageId, attemptIndex };
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
this.resumeInFlight.delete(resumeKey);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
420
502
|
async recoverManualStage(runId, stageId, guidance) {
|
|
421
503
|
return this.recoverManualStageInternal(runId, stageId, guidance, true);
|
|
422
504
|
}
|
|
@@ -509,6 +591,7 @@ export class RunManager {
|
|
|
509
591
|
};
|
|
510
592
|
}
|
|
511
593
|
try {
|
|
594
|
+
let meta;
|
|
512
595
|
let orchestrationConflict = this.active.has(runId) &&
|
|
513
596
|
!this.attachedWaiting.has(waitKey(runId, stageId));
|
|
514
597
|
if (!this.retryCoordinator.isActive(runId)) {
|
|
@@ -531,7 +614,7 @@ export class RunManager {
|
|
|
531
614
|
let detail;
|
|
532
615
|
try {
|
|
533
616
|
detail = await this.options.store.readRun(runId);
|
|
534
|
-
await this.options.store.readRunMeta(runId);
|
|
617
|
+
meta = await this.options.store.readRunMeta(runId);
|
|
535
618
|
}
|
|
536
619
|
catch {
|
|
537
620
|
this.retryInFlight.delete(retryKey);
|
|
@@ -550,12 +633,21 @@ export class RunManager {
|
|
|
550
633
|
}
|
|
551
634
|
}
|
|
552
635
|
}
|
|
636
|
+
if (meta === undefined) {
|
|
637
|
+
try {
|
|
638
|
+
meta = await this.options.store.readRunMeta(runId);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
this.retryInFlight.delete(retryKey);
|
|
642
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
553
645
|
const result = await this.retryCoordinator.retryStage({
|
|
554
646
|
runId,
|
|
555
647
|
stageId,
|
|
556
648
|
store: this.options.store,
|
|
557
649
|
agent: this.options.agent,
|
|
558
|
-
cwd: this.
|
|
650
|
+
cwd: meta.project_root ?? this.projectRoot,
|
|
559
651
|
operatorCatalog: this.options.operatorCatalog,
|
|
560
652
|
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
561
653
|
executionMode: this.executionMode,
|
|
@@ -904,6 +996,7 @@ export class RunManager {
|
|
|
904
996
|
};
|
|
905
997
|
}
|
|
906
998
|
const { meta, task, loaded } = await loadRunContext(this.options.store, runId, this.cwd);
|
|
999
|
+
const runProjectRoot = meta.project_root ?? this.projectRoot;
|
|
907
1000
|
const done = runPipelineDag({
|
|
908
1001
|
prepared: {
|
|
909
1002
|
task,
|
|
@@ -914,8 +1007,8 @@ export class RunManager {
|
|
|
914
1007
|
},
|
|
915
1008
|
agent: this.options.agent,
|
|
916
1009
|
store: this.options.store,
|
|
917
|
-
cwd:
|
|
918
|
-
projectRoot:
|
|
1010
|
+
cwd: runProjectRoot,
|
|
1011
|
+
projectRoot: runProjectRoot,
|
|
919
1012
|
checkoutRoot: meta.checkout_root,
|
|
920
1013
|
hitl: this.hitl,
|
|
921
1014
|
operatorCatalog: this.options.operatorCatalog,
|
|
@@ -965,10 +1058,12 @@ export class RunManager {
|
|
|
965
1058
|
const sessionFilePath = resumeSessionFilePath(workspaceDir, stageId, attempt);
|
|
966
1059
|
const eventOptions = { attempt };
|
|
967
1060
|
try {
|
|
1061
|
+
const runMeta = await store.readRunMeta(runId);
|
|
1062
|
+
const runProjectRoot = runMeta.project_root ?? this.projectRoot;
|
|
968
1063
|
const launchResult = await launcher.launch({
|
|
969
1064
|
runId,
|
|
970
1065
|
stageId,
|
|
971
|
-
rootDir:
|
|
1066
|
+
rootDir: runProjectRoot,
|
|
972
1067
|
mode: "resume",
|
|
973
1068
|
resumeAnswer: opaqueAnswer,
|
|
974
1069
|
attempt,
|
|
@@ -1003,8 +1098,8 @@ export class RunManager {
|
|
|
1003
1098
|
run: { runId, workspaceDir: store.getWorkspaceDir(runId) },
|
|
1004
1099
|
agent: this.options.agent,
|
|
1005
1100
|
store,
|
|
1006
|
-
cwd: this.
|
|
1007
|
-
projectRoot: this.projectRoot,
|
|
1101
|
+
cwd: meta.project_root ?? this.projectRoot,
|
|
1102
|
+
projectRoot: meta.project_root ?? this.projectRoot,
|
|
1008
1103
|
checkoutRoot: meta.checkout_root,
|
|
1009
1104
|
hitl: this.hitl,
|
|
1010
1105
|
operatorCatalog: this.options.operatorCatalog,
|
|
@@ -1201,6 +1296,7 @@ export class RunManager {
|
|
|
1201
1296
|
return false;
|
|
1202
1297
|
}
|
|
1203
1298
|
try {
|
|
1299
|
+
const runProjectRoot = meta.project_root ?? this.projectRoot;
|
|
1204
1300
|
const { meta: loadedMeta, task, loaded, workspaceDir } = await loadRunContext(this.options.store, runId, this.cwd);
|
|
1205
1301
|
const promise = runPipelineDag({
|
|
1206
1302
|
prepared: {
|
|
@@ -1209,8 +1305,8 @@ export class RunManager {
|
|
|
1209
1305
|
run: { runId, workspaceDir },
|
|
1210
1306
|
agent: this.options.agent,
|
|
1211
1307
|
store: this.options.store,
|
|
1212
|
-
cwd:
|
|
1213
|
-
projectRoot:
|
|
1308
|
+
cwd: runProjectRoot,
|
|
1309
|
+
projectRoot: runProjectRoot,
|
|
1214
1310
|
checkoutRoot: loadedMeta.checkout_root,
|
|
1215
1311
|
hitl: this.hitl,
|
|
1216
1312
|
operatorCatalog: this.options.operatorCatalog,
|
|
@@ -23,6 +23,8 @@ export declare function readMaxConcurrentFromContext(ctx: ProjectContext): numbe
|
|
|
23
23
|
export declare function readMaxConcurrentFromFile(cwd: string): number | undefined;
|
|
24
24
|
export declare function writeMaxConcurrentToContext(ctx: ProjectContext, maxConcurrent: number): void;
|
|
25
25
|
export declare function writeMaxConcurrentToFile(cwd: string, maxConcurrent: number): void;
|
|
26
|
+
export declare function readMaxConcurrentFromGlobal(): number | undefined;
|
|
27
|
+
export declare function writeMaxConcurrentToGlobal(maxConcurrent: number): void;
|
|
26
28
|
export declare function readCredentialSourceFromFile(cwd: string): CredentialSource | undefined;
|
|
27
29
|
export declare function writeCredentialSourceToContext(ctx: ProjectContext, credentialSource: CredentialSource): void;
|
|
28
30
|
export declare function writeCredentialSourceToFile(cwd: string, credentialSource: CredentialSource): void;
|
|
@@ -150,6 +150,25 @@ export function writeMaxConcurrentToContext(ctx, maxConcurrent) {
|
|
|
150
150
|
export function writeMaxConcurrentToFile(cwd, maxConcurrent) {
|
|
151
151
|
writeFactorySettings(cwd, { maxConcurrent });
|
|
152
152
|
}
|
|
153
|
+
export function readMaxConcurrentFromGlobal() {
|
|
154
|
+
return parseFactorySettingsFromRaw(readRawSettingsFromFile(globalSettingsFilePath())).maxConcurrent;
|
|
155
|
+
}
|
|
156
|
+
export function writeMaxConcurrentToGlobal(maxConcurrent) {
|
|
157
|
+
if (parseSlotCount(maxConcurrent) === undefined) {
|
|
158
|
+
throw new Error(INVALID_SLOT_COUNT_MESSAGE);
|
|
159
|
+
}
|
|
160
|
+
const current = parseFactorySettingsFromRaw(readRawSettingsFromFile(globalSettingsFilePath()));
|
|
161
|
+
const next = { ...current, maxConcurrent };
|
|
162
|
+
const out = {};
|
|
163
|
+
if (next.maxConcurrent !== undefined) {
|
|
164
|
+
out.maxConcurrent = next.maxConcurrent;
|
|
165
|
+
}
|
|
166
|
+
if (next.credentialSource !== undefined) {
|
|
167
|
+
out.credentialSource = next.credentialSource;
|
|
168
|
+
}
|
|
169
|
+
mkdirSync(path.dirname(globalSettingsFilePath()), { recursive: true });
|
|
170
|
+
writeFileSync(globalSettingsFilePath(), `${JSON.stringify(out, null, 2)}\n`, "utf8");
|
|
171
|
+
}
|
|
153
172
|
export function readCredentialSourceFromFile(cwd) {
|
|
154
173
|
return readFactorySettings(cwd).credentialSource;
|
|
155
174
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunInput, StageSessionMode } from "../agent/port.js";
|
|
2
|
+
import { type StageStreamLogWriter } from "./stageStreamLog.js";
|
|
2
3
|
import type { RunStore } from "../runstore/port.js";
|
|
3
4
|
import type { StageEnvelope } from "../types/envelope.js";
|
|
4
5
|
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
@@ -23,6 +24,8 @@ export type StageAttemptOpenInput = {
|
|
|
23
24
|
attemptCtx?: StageAttemptContext;
|
|
24
25
|
operatorCatalog?: OperatorCatalog;
|
|
25
26
|
onActivity?: StageRunInput["onActivity"];
|
|
27
|
+
/** Test seam; defaults to the real createStageStreamLogWriter. */
|
|
28
|
+
streamLogWriterFactory?: (streamLogPath: string) => StageStreamLogWriter;
|
|
26
29
|
roots?: StageRoots;
|
|
27
30
|
resumeToken?: string;
|
|
28
31
|
sessionMode?: StageSessionMode;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveSkillByName } from "../config/listSkills.js";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { MCP_CATALOG_FILENAME, STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV, StageMcpError, resolveStageMcpServers, stampStagePromptArtifactsDir, } from "../config/resolveStageMcpServers.js";
|
|
4
|
-
import { attemptArtifactsDir } from "../runstore/workspaceLayout.js";
|
|
4
|
+
import { attemptArtifactsDir, attemptStreamLogPath } from "../runstore/workspaceLayout.js";
|
|
5
|
+
import { createStageStreamLogWriter, } from "./stageStreamLog.js";
|
|
5
6
|
import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
|
|
6
7
|
import { createAttemptQaTrailReader } from "../hitl/qaTrail.js";
|
|
7
8
|
import { assertPriorInputPayload, payloadInstanceMismatch, } from "../envelope/payloadSchema.js";
|
|
@@ -199,6 +200,7 @@ export async function openStageAttempt(input) {
|
|
|
199
200
|
return priorInput;
|
|
200
201
|
const roots = resolveAttemptRoots(input, stageId);
|
|
201
202
|
const attempt = input.attemptCtx?.attempt ?? 1;
|
|
203
|
+
const streamWriter = (input.streamLogWriterFactory ?? createStageStreamLogWriter)(attemptStreamLogPath(input.workspaceDir, stageId, attempt));
|
|
202
204
|
const resumeToken = input.resumeToken ??
|
|
203
205
|
resumeSessionFilePath(input.workspaceDir, stageId, attempt);
|
|
204
206
|
const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
|
|
@@ -231,7 +233,11 @@ export async function openStageAttempt(input) {
|
|
|
231
233
|
: {}),
|
|
232
234
|
resumeToken,
|
|
233
235
|
...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
|
|
234
|
-
onActivity:
|
|
236
|
+
onActivity: (event) => {
|
|
237
|
+
input.onActivity?.(event);
|
|
238
|
+
void streamWriter.flush();
|
|
239
|
+
},
|
|
240
|
+
onAssistantTextDelta: streamWriter.onDelta,
|
|
235
241
|
forkEmitContext,
|
|
236
242
|
cloneEmitContext,
|
|
237
243
|
...(feedbackLoopEmitContext !== undefined ? { feedbackLoopEmitContext } : {}),
|
|
@@ -35,10 +35,10 @@ export async function failStageAsInterrupted(options) {
|
|
|
35
35
|
}
|
|
36
36
|
export async function syncRunStatusFromStages(store, runId) {
|
|
37
37
|
const meta = await store.readRunMeta(runId);
|
|
38
|
-
if (meta.status === "succeeded")
|
|
39
|
-
return;
|
|
40
38
|
const run = await store.readRun(runId);
|
|
41
39
|
const derived = deriveStatusFromStages(run.stages, meta.pipeline_dag);
|
|
40
|
+
if (meta.status === "succeeded" && derived !== "running")
|
|
41
|
+
return;
|
|
42
42
|
if ((meta.status ?? "created") !== derived) {
|
|
43
43
|
await store.updateRunStatus(runId, derived);
|
|
44
44
|
}
|
|
@@ -3,6 +3,7 @@ import { buildStageRoots, withResolvedAuthPath, } from "./stageRoots.js";
|
|
|
3
3
|
import { createDefaultHitlQaHooks, recordWaitEnter, } from "./stageHitl.js";
|
|
4
4
|
import { openStageAttempt, } from "./stageAttemptBootstrap.js";
|
|
5
5
|
import { createVerifiedStageExecution } from "./verifiedStageExecution.js";
|
|
6
|
+
import { isStageTimeoutReason } from "../agent/stageTimeout.js";
|
|
6
7
|
export function isRunStageWaiting(outcome) {
|
|
7
8
|
return "waiting" in outcome && outcome.waiting === true;
|
|
8
9
|
}
|
|
@@ -220,7 +221,9 @@ export async function runStage(options) {
|
|
|
220
221
|
attemptCtx,
|
|
221
222
|
});
|
|
222
223
|
console.error(`Stage ${stageId} failed: ${reason}`);
|
|
223
|
-
await handle
|
|
224
|
+
await handle
|
|
225
|
+
.close(isStageTimeoutReason(reason) ? { park: true } : undefined)
|
|
226
|
+
.catch(() => undefined);
|
|
224
227
|
return { ok: false, reason };
|
|
225
228
|
}
|
|
226
229
|
if (isRunStageWaiting(outcome)) {
|
|
@@ -251,6 +254,7 @@ export async function runStage(options) {
|
|
|
251
254
|
});
|
|
252
255
|
}
|
|
253
256
|
finally {
|
|
254
|
-
|
|
257
|
+
const timedOut = isStageTimeoutReason(outcome.ok ? undefined : outcome.reason);
|
|
258
|
+
await handle.close(timedOut ? { park: true } : undefined).catch(() => undefined);
|
|
255
259
|
}
|
|
256
260
|
}
|