stageflow 0.8.0 → 0.12.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 +24 -14
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/agentBackend.d.ts +32 -0
- package/dist/agent/agentBackend.js +38 -0
- package/dist/agent/claudeActivity.d.ts +15 -0
- package/dist/agent/claudeActivity.js +58 -0
- package/dist/agent/claudeAdapter.d.ts +5 -0
- package/dist/agent/claudeAdapter.js +301 -0
- package/dist/agent/claudeSession.d.ts +18 -0
- package/dist/agent/claudeSession.js +54 -0
- package/dist/agent/claudeTools.d.ts +89 -0
- package/dist/agent/claudeTools.js +135 -0
- package/dist/agent/fakeAgent.js +24 -4
- package/dist/agent/piAdapter.d.ts +6 -3
- package/dist/agent/piAdapter.js +129 -21
- package/dist/agent/port.d.ts +50 -1
- package/dist/agent/resolveAgentPort.d.ts +26 -0
- package/dist/agent/resolveAgentPort.js +39 -0
- package/dist/cli/runCommand.js +2 -2
- package/dist/cli/runsCommand.d.ts +22 -0
- package/dist/cli/runsCommand.js +773 -0
- package/dist/cli/validateOutput.d.ts +2 -2
- package/dist/cli/validateOutput.js +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +43 -5
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +2 -1
- package/dist/config/createPipeline.js +34 -9
- package/dist/config/createStage.js +14 -9
- package/dist/config/loadPipeline.js +10 -1
- package/dist/config/loadStage.js +138 -2
- package/dist/config/loadStageflowManifest.js +14 -1
- package/dist/config/mergePipelineIncludes.d.ts +1 -0
- package/dist/config/mergePipelineIncludes.js +14 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
- package/dist/config/normalizePipelineStageEntry.js +49 -6
- package/dist/config/parseCompletionContract.d.ts +8 -0
- package/dist/config/parseCompletionContract.js +218 -0
- package/dist/config/parsePreEmitChecks.d.ts +3 -0
- package/dist/config/parsePreEmitChecks.js +65 -0
- package/dist/config/pipelineNeeds.d.ts +19 -0
- package/dist/config/pipelineNeeds.js +151 -0
- package/dist/config/pipelineStageKeys.js +16 -1
- package/dist/config/resolveForkEmitContext.d.ts +6 -2
- package/dist/config/resolveForkEmitContext.js +9 -2
- package/dist/config/resolvePipelineDag.d.ts +2 -1
- package/dist/config/resolvePipelineDag.js +182 -39
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCompletionContract.d.ts +4 -0
- package/dist/config/validateCompletionContract.js +52 -0
- package/dist/envelope/check.d.ts +1 -1
- package/dist/envelope/check.js +86 -13
- package/dist/envelope/cloneForks.d.ts +1 -1
- package/dist/envelope/cloneForks.js +24 -9
- package/dist/envelope/feedbackLoop.d.ts +4 -0
- package/dist/envelope/feedbackLoop.js +31 -0
- package/dist/envelope/payloadSchema.d.ts +7 -1
- package/dist/envelope/payloadSchema.js +170 -13
- package/dist/envelope/preEmitChecks.d.ts +29 -0
- package/dist/envelope/preEmitChecks.js +57 -0
- package/dist/hitl/qaTrail.d.ts +25 -0
- package/dist/hitl/qaTrail.js +51 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/catalogTools.js +4 -2
- package/dist/mcp/controlTools.js +72 -0
- package/dist/mcp/waitingGates.d.ts +2 -1
- package/dist/mcp/waitingGates.js +17 -1
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/projection/projectRun.d.ts +4 -1
- package/dist/projection/projectRun.js +7 -0
- package/dist/prompt/feedbackLoopContext.d.ts +2 -0
- package/dist/prompt/feedbackLoopContext.js +17 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -2
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
- package/dist/runstore/pipelineDagSnapshot.js +51 -11
- package/dist/runstore/port.d.ts +185 -3
- package/dist/runstore/port.js +42 -3
- package/dist/runstore/runProjection.d.ts +7 -3
- package/dist/runstore/runProjection.js +30 -15
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +99 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -1
- package/dist/runstore/stageInstanceId.js +3 -2
- package/dist/runstore/trackProjection.js +59 -27
- package/dist/runstore/verificationHistory.d.ts +19 -0
- package/dist/runstore/verificationHistory.js +30 -0
- package/dist/runtime/cloneSchedule.d.ts +40 -4
- package/dist/runtime/cloneSchedule.js +212 -28
- package/dist/runtime/completionCheckRunner.d.ts +137 -0
- package/dist/runtime/completionCheckRunner.js +365 -0
- package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
- package/dist/runtime/completionCheckoutBaseline.js +44 -0
- package/dist/runtime/envelopeRouting.d.ts +10 -3
- package/dist/runtime/envelopeRouting.js +146 -5
- package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
- package/dist/runtime/feedbackLoopCoordinator.js +446 -0
- package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
- package/dist/runtime/feedbackLoopDecision.js +207 -0
- package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
- package/dist/runtime/feedbackLoopRoute.js +13 -0
- package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
- package/dist/runtime/feedbackLoopSchedule.js +72 -0
- package/dist/runtime/forkGeneration.d.ts +72 -0
- package/dist/runtime/forkGeneration.js +295 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
- package/dist/runtime/gitCheckoutCapability.js +97 -0
- package/dist/runtime/manualRecoveryState.d.ts +40 -0
- package/dist/runtime/manualRecoveryState.js +73 -0
- package/dist/runtime/pipelineScheduler.d.ts +13 -1
- package/dist/runtime/pipelineScheduler.js +328 -45
- package/dist/runtime/replayLifecycle.d.ts +107 -0
- package/dist/runtime/replayLifecycle.js +168 -0
- package/dist/runtime/runManager.d.ts +61 -1
- package/dist/runtime/runManager.js +501 -7
- package/dist/runtime/runRetryCoordinator.d.ts +8 -1
- package/dist/runtime/runRetryCoordinator.js +47 -10
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
- package/dist/runtime/stageAttemptBootstrap.js +79 -1
- package/dist/runtime/stageProcessLauncher.d.ts +1 -1
- package/dist/runtime/stageRecovery.js +1 -1
- package/dist/runtime/stageRunner.d.ts +4 -1
- package/dist/runtime/stageRunner.js +29 -3
- package/dist/runtime/stageWorker.js +69 -2
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
- package/dist/runtime/verifiedStageExecution.d.ts +29 -0
- package/dist/runtime/verifiedStageExecution.js +119 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +114 -0
- package/dist/server/operatorResults.js +2 -0
- package/dist/tools/askOperator.d.ts +161 -5
- package/dist/tools/askOperator.js +58 -25
- package/dist/tools/emitStageEnvelope.d.ts +12 -12
- package/dist/tools/emitStageEnvelope.js +112 -24
- package/dist/types/completion.d.ts +43 -0
- package/dist/types/completion.js +1 -0
- package/dist/types/envelope.d.ts +18 -0
- package/dist/types/forkChoice.d.ts +9 -4
- package/dist/types/forkChoice.js +1 -1
- package/dist/types/pipeline.d.ts +39 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +13 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-CXGz4nL7.js +118 -0
- package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/stageflow/references/control-surface.md +4 -2
- package/skills/stageflow-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-run/SKILL.md +56 -25
- package/skills/stageflow-run/references/mcp-call.md +2 -2
- package/skills/stageflow-run/references/native-question-ui.md +78 -0
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { normalizeForkChoice } from "../envelope/forkChoice.js";
|
|
2
2
|
import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnapshot.js";
|
|
3
3
|
import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
4
|
-
import { applyCloneForksFromEnvelopes, applyCloneForksToSchedule, cloneFailureContinuesSchedule, cloneFanoutConflict, cloneRetryDownstream, cloneFailFastSkipIds, cloneScheduleAllowsRun, isCloneInstance, protectedClonableChildIds, sequentialLaterCloneIds, } from "./cloneSchedule.js";
|
|
5
|
-
import { collectDownstreamStageIds } from "./dagTraversal.js";
|
|
4
|
+
import { applyCloneForksFromEnvelopes, applyCloneForksToSchedule, cloneFailureContinuesSchedule, cloneFanoutConflict, cloneRetryDownstream, cloneFailFastSkipIds, cloneScheduleAllowsRun, failureIsAccepted, isCloneInstance, protectedClonableChildIds, sequentialLaterCloneIds, skipRejectedNeedDependents, } from "./cloneSchedule.js";
|
|
6
5
|
import { buildCompletedEnvelopesFromRun, buildStageConfigById, } from "./envelopeRouting.js";
|
|
6
|
+
import { activeCohortFromCloneIds, forkChoicePreservesReplaySource, forkParentForNeedsStage, mintCohortForFanout, } from "./forkGeneration.js";
|
|
7
|
+
import { activeReplayId, activeSourceStageId, applySendBack, cohortOverrideMap, createReplaySchedule, hydrateFromStore, isHeld, launchAttemptFor, launchFor, noteActiveCohort, onContinued, onRouteStageFailed, onRouteStageRunning, onRouteStageSucceeded, rebindRouteAttempts, resolveDecision, } from "./replayLifecycle.js";
|
|
7
8
|
import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
|
|
8
9
|
import { runStage, isRunStageWaiting } from "./stageRunner.js";
|
|
9
|
-
import { attemptContext } from "./stageAttemptContext.js";
|
|
10
|
+
import { attemptContext, resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
11
|
+
const EMPTY_RETRY_DOWNSTREAM = new Set();
|
|
10
12
|
function asDagSnapshot(dag) {
|
|
11
13
|
const snapshot = dag;
|
|
12
14
|
if (Array.isArray(snapshot.stage_ids))
|
|
@@ -53,10 +55,16 @@ export async function hydrateScheduleFromStore(store, runId, dag, executionMode
|
|
|
53
55
|
for (const snap of runDetail.stages) {
|
|
54
56
|
const state = snapshotToScheduleState(snap, overrides, snap.stage_id, executionMode);
|
|
55
57
|
states.set(snap.stage_id, state);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
}
|
|
59
|
+
for (const [stageId, state] of states) {
|
|
60
|
+
if (state !== "failed")
|
|
61
|
+
continue;
|
|
62
|
+
if (cloneFailureContinuesSchedule(resolvedDag, stageId, completedEnvelopes)) {
|
|
63
|
+
continue;
|
|
59
64
|
}
|
|
65
|
+
if (failureIsAccepted(resolvedDag, stageId, states))
|
|
66
|
+
continue;
|
|
67
|
+
schedulingHalted = true;
|
|
60
68
|
}
|
|
61
69
|
return {
|
|
62
70
|
states,
|
|
@@ -136,19 +144,16 @@ export async function resumeRun(options) {
|
|
|
136
144
|
return state !== undefined && terminalState(state);
|
|
137
145
|
});
|
|
138
146
|
if (allTerminal) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
return s === "succeeded" || s === "skipped";
|
|
142
|
-
});
|
|
143
|
-
await prepared.store.updateRunStatus(prepared.run.runId, allSucceeded ? "succeeded" : "failed");
|
|
147
|
+
const unhandledFailure = scheduleHasUnhandledFailure(dag, hydrated.states);
|
|
148
|
+
await prepared.store.updateRunStatus(prepared.run.runId, unhandledFailure ? "failed" : "succeeded");
|
|
144
149
|
return {
|
|
145
|
-
ok:
|
|
146
|
-
outcome:
|
|
150
|
+
ok: !unhandledFailure,
|
|
151
|
+
outcome: unhandledFailure ? "failed" : "succeeded",
|
|
147
152
|
runDir: prepared.run.workspaceDir,
|
|
148
153
|
runId: prepared.run.runId,
|
|
149
|
-
...(
|
|
150
|
-
? {}
|
|
151
|
-
: {
|
|
154
|
+
...(unhandledFailure
|
|
155
|
+
? { reason: hydrated.firstFailureReason ?? "pipeline incomplete" }
|
|
156
|
+
: {}),
|
|
152
157
|
};
|
|
153
158
|
}
|
|
154
159
|
return runPipelineDag(options);
|
|
@@ -180,9 +185,7 @@ function applyForkChoiceToSchedule(dag, forkStageId, chosen, states, options) {
|
|
|
180
185
|
continue;
|
|
181
186
|
if (!chosen.has(childId)) {
|
|
182
187
|
skipPending(childId);
|
|
183
|
-
|
|
184
|
-
skipPending(desc);
|
|
185
|
-
}
|
|
188
|
+
skipRejectedNeedDependents(dag, childId, states, "skipped", skipPending);
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
}
|
|
@@ -197,7 +200,7 @@ export function applyForkSkipsFromEnvelopes(dag, states, completedEnvelopes) {
|
|
|
197
200
|
applyForkChoiceToSchedule(dag, node.id, chosen, states);
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
|
-
export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt) {
|
|
203
|
+
export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt, alreadyLaunchedIds) {
|
|
201
204
|
retryRoots.set(stageId, attempt);
|
|
202
205
|
const downstream = cloneRetryDownstream(dag, [stageId]);
|
|
203
206
|
const resetIds = new Set([stageId, ...downstream]);
|
|
@@ -206,7 +209,13 @@ export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots,
|
|
|
206
209
|
}
|
|
207
210
|
for (const id of resetIds) {
|
|
208
211
|
const state = states.get(id);
|
|
209
|
-
if (state === "active"
|
|
212
|
+
if (state === "active") {
|
|
213
|
+
completedEnvelopes.delete(id);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (id === stageId &&
|
|
217
|
+
state === "succeeded" &&
|
|
218
|
+
alreadyLaunchedIds?.has(id)) {
|
|
210
219
|
completedEnvelopes.delete(id);
|
|
211
220
|
continue;
|
|
212
221
|
}
|
|
@@ -218,7 +227,7 @@ export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots,
|
|
|
218
227
|
export function hasWorkOutsideBranch(dag, branchRootId, states, ctx) {
|
|
219
228
|
const branchIds = new Set([
|
|
220
229
|
branchRootId,
|
|
221
|
-
...
|
|
230
|
+
...cloneRetryDownstream(dag, [branchRootId]),
|
|
222
231
|
]);
|
|
223
232
|
for (const node of dag.nodes) {
|
|
224
233
|
if (branchIds.has(node.id))
|
|
@@ -253,17 +262,56 @@ export function hasWorkOutsideBranch(dag, branchRootId, states, ctx) {
|
|
|
253
262
|
function terminalState(status) {
|
|
254
263
|
return status === "succeeded" || status === "failed" || status === "skipped";
|
|
255
264
|
}
|
|
265
|
+
function scheduleHasUnhandledFailure(dag, states) {
|
|
266
|
+
for (const [stageId, state] of states) {
|
|
267
|
+
if (state !== "failed")
|
|
268
|
+
continue;
|
|
269
|
+
if (failureIsAccepted(dag, stageId, states))
|
|
270
|
+
continue;
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
256
275
|
function sortRunnableIds(dag, ids) {
|
|
257
276
|
const indexById = new Map(dag.nodes.map((node) => [node.id, node.stageIndex]));
|
|
258
277
|
return ids.slice().sort((a, b) => (indexById.get(a) ?? 0) - (indexById.get(b) ?? 0));
|
|
259
278
|
}
|
|
260
|
-
function isRunnable(dag, stageId, states, schedulingHalted, completedEnvelopes) {
|
|
279
|
+
function isRunnable(dag, stageId, states, schedulingHalted, completedEnvelopes, feedback) {
|
|
261
280
|
if (schedulingHalted)
|
|
262
281
|
return false;
|
|
282
|
+
if (feedback !== undefined && isHeld(feedback, stageId))
|
|
283
|
+
return false;
|
|
263
284
|
const state = states.get(stageId);
|
|
264
285
|
if (state !== "pending")
|
|
265
286
|
return false;
|
|
266
|
-
return cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes
|
|
287
|
+
return cloneScheduleAllowsRun(dag, stageId, states, completedEnvelopes, {
|
|
288
|
+
activeCohortForNeeds: (needsId) => {
|
|
289
|
+
const override = feedback !== undefined ? cohortOverrideMap(feedback) : undefined;
|
|
290
|
+
if (override === undefined || override.size === 0) {
|
|
291
|
+
return { kind: "untracked" };
|
|
292
|
+
}
|
|
293
|
+
const forkParentId = forkParentForNeedsStage(dag, needsId);
|
|
294
|
+
if (forkParentId === null)
|
|
295
|
+
return { kind: "untracked" };
|
|
296
|
+
const tracked = override.get(forkParentId);
|
|
297
|
+
if (tracked === undefined)
|
|
298
|
+
return { kind: "untracked" };
|
|
299
|
+
return activeCohortFromCloneIds(tracked);
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
export function hydratedScheduleHasRunnableWork(hydrated) {
|
|
304
|
+
if (hydrated.schedulingHalted)
|
|
305
|
+
return false;
|
|
306
|
+
const dag = hydrated.dag;
|
|
307
|
+
if (dag === undefined)
|
|
308
|
+
return false;
|
|
309
|
+
for (const node of dag.nodes) {
|
|
310
|
+
if (isRunnable(dag, node.id, hydrated.states, hydrated.schedulingHalted, hydrated.completedEnvelopes)) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
267
315
|
}
|
|
268
316
|
export async function runPipelineDag(options) {
|
|
269
317
|
const { prepared, maxActiveStagesPerRun } = options;
|
|
@@ -280,7 +328,34 @@ export async function runPipelineDag(options) {
|
|
|
280
328
|
}
|
|
281
329
|
const stageById = buildStageConfigById(loaded);
|
|
282
330
|
const retryContext = options.retryContext;
|
|
331
|
+
const feedbackSchedule = createReplaySchedule();
|
|
332
|
+
await hydrateFromStore(feedbackSchedule, store, run.runId, dag);
|
|
333
|
+
if (retryContext !== undefined &&
|
|
334
|
+
activeReplayId(feedbackSchedule) !== undefined) {
|
|
335
|
+
const routeIds = new Set(feedbackSchedule.feedback.contextsByStageId.keys());
|
|
336
|
+
const cascade = cloneRetryDownstream(dag, [
|
|
337
|
+
...retryContext.retryRoots.keys(),
|
|
338
|
+
]);
|
|
339
|
+
const overrides = new Map();
|
|
340
|
+
for (const [stageId, attempt] of retryContext.retryRoots) {
|
|
341
|
+
if (routeIds.has(stageId))
|
|
342
|
+
overrides.set(stageId, attempt);
|
|
343
|
+
}
|
|
344
|
+
for (const stageId of cascade) {
|
|
345
|
+
if (!routeIds.has(stageId))
|
|
346
|
+
continue;
|
|
347
|
+
if (overrides.has(stageId))
|
|
348
|
+
continue;
|
|
349
|
+
const exec = await store.createStageExecution(run.runId, stageId);
|
|
350
|
+
overrides.set(stageId, exec.attempt);
|
|
351
|
+
}
|
|
352
|
+
await rebindRouteAttempts(feedbackSchedule, store, run.runId, overrides);
|
|
353
|
+
}
|
|
283
354
|
const resolveLaunchAttempt = async (stageId) => {
|
|
355
|
+
const feedbackAttempt = launchAttemptFor(feedbackSchedule, stageId);
|
|
356
|
+
if (feedbackAttempt !== undefined) {
|
|
357
|
+
return feedbackAttempt;
|
|
358
|
+
}
|
|
284
359
|
if (retryContext) {
|
|
285
360
|
const preCreatedAttempt = retryContext.retryRoots.get(stageId);
|
|
286
361
|
if (preCreatedAttempt !== undefined) {
|
|
@@ -289,11 +364,10 @@ export async function runPipelineDag(options) {
|
|
|
289
364
|
const execution = await store.createStageExecution(run.runId, stageId);
|
|
290
365
|
return execution.attempt;
|
|
291
366
|
}
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return 1;
|
|
367
|
+
const latest = await store.getLatestStageExecution(run.runId, stageId);
|
|
368
|
+
if (latest !== null)
|
|
369
|
+
return latest.attempt;
|
|
370
|
+
return (await store.createStageExecution(run.runId, stageId)).attempt;
|
|
297
371
|
};
|
|
298
372
|
if (options.resumeFromStageId !== undefined) {
|
|
299
373
|
const resumedId = options.resumeFromStageId;
|
|
@@ -338,6 +412,7 @@ export async function runPipelineDag(options) {
|
|
|
338
412
|
}
|
|
339
413
|
let activeCount = 0;
|
|
340
414
|
const inFlight = new Set();
|
|
415
|
+
const launchedIds = new Set();
|
|
341
416
|
const notifyRetryRootTerminal = (stageId, outcome) => {
|
|
342
417
|
if (retryContext === undefined)
|
|
343
418
|
return;
|
|
@@ -346,12 +421,26 @@ export async function runPipelineDag(options) {
|
|
|
346
421
|
return;
|
|
347
422
|
options.onRetryRootTerminal?.(stageId, attempt, outcome);
|
|
348
423
|
};
|
|
424
|
+
const persistSkipRejected = async (parentId, observed) => {
|
|
425
|
+
const ids = [];
|
|
426
|
+
skipRejectedNeedDependents(dag, parentId, states, observed, (id) => {
|
|
427
|
+
if (states.get(id) !== "pending")
|
|
428
|
+
return;
|
|
429
|
+
states.set(id, "skipped");
|
|
430
|
+
ids.push(id);
|
|
431
|
+
});
|
|
432
|
+
for (const id of ids) {
|
|
433
|
+
notifyRetryRootTerminal(id, "skipped");
|
|
434
|
+
await store.appendStageEvent(run.runId, id, { event: "skipped" });
|
|
435
|
+
}
|
|
436
|
+
};
|
|
349
437
|
const persistSkipPending = async (id) => {
|
|
350
438
|
if (states.get(id) !== "pending")
|
|
351
439
|
return;
|
|
352
440
|
states.set(id, "skipped");
|
|
353
441
|
notifyRetryRootTerminal(id, "skipped");
|
|
354
442
|
await store.appendStageEvent(run.runId, id, { event: "skipped" });
|
|
443
|
+
await persistSkipRejected(id, "skipped");
|
|
355
444
|
};
|
|
356
445
|
for (const [stageId, state] of states) {
|
|
357
446
|
if (state !== "failed")
|
|
@@ -369,7 +458,7 @@ export async function runPipelineDag(options) {
|
|
|
369
458
|
}
|
|
370
459
|
};
|
|
371
460
|
const markBranchDownstreamSkipped = (stageId) => {
|
|
372
|
-
for (const id of
|
|
461
|
+
for (const id of cloneRetryDownstream(dag, [stageId])) {
|
|
373
462
|
if (states.get(id) === "pending") {
|
|
374
463
|
states.set(id, "skipped");
|
|
375
464
|
notifyRetryRootTerminal(id, "skipped");
|
|
@@ -387,7 +476,7 @@ export async function runPipelineDag(options) {
|
|
|
387
476
|
options.mutationQueue.drainPending((stageId, attempt) => {
|
|
388
477
|
applyRetryRootDelta(dag, states, completedEnvelopes, retryContext.retryRoots, stageId, attempt, () => {
|
|
389
478
|
schedulingHalted = false;
|
|
390
|
-
});
|
|
479
|
+
}, launchedIds);
|
|
391
480
|
});
|
|
392
481
|
};
|
|
393
482
|
const recordCloneFailureEnvelope = async (stageId, reason, envelope) => {
|
|
@@ -409,10 +498,42 @@ export async function runPipelineDag(options) {
|
|
|
409
498
|
}
|
|
410
499
|
}
|
|
411
500
|
};
|
|
501
|
+
const scheduleAutomaticRepair = async (stageId) => {
|
|
502
|
+
const recovery = dag.nodes.find((node) => node.id === stageId)?.recovery;
|
|
503
|
+
if (recovery?.mode !== "repair")
|
|
504
|
+
return false;
|
|
505
|
+
const latestAttempt = await store.getLatestStageExecution(run.runId, stageId);
|
|
506
|
+
if (latestAttempt?.verification_outcome !== "failed")
|
|
507
|
+
return false;
|
|
508
|
+
const attempts = await store.countStageAttempts(run.runId, stageId);
|
|
509
|
+
if (attempts >= recovery.max_attempts)
|
|
510
|
+
return false;
|
|
511
|
+
const nextAttempt = await store.createStageExecution(run.runId, stageId);
|
|
512
|
+
if (retryContext !== undefined) {
|
|
513
|
+
retryContext.retryRoots.set(stageId, nextAttempt.attempt);
|
|
514
|
+
}
|
|
515
|
+
if (activeReplayId(feedbackSchedule) !== undefined) {
|
|
516
|
+
await rebindRouteAttempts(feedbackSchedule, store, run.runId, new Map([[stageId, nextAttempt.attempt]]));
|
|
517
|
+
}
|
|
518
|
+
states.set(stageId, "pending");
|
|
519
|
+
completedEnvelopes.delete(stageId);
|
|
520
|
+
return true;
|
|
521
|
+
};
|
|
412
522
|
const onStageFailure = async (stageId, reason, envelope) => {
|
|
523
|
+
if (await scheduleAutomaticRepair(stageId))
|
|
524
|
+
return;
|
|
525
|
+
if (activeReplayId(feedbackSchedule) !== undefined) {
|
|
526
|
+
await onRouteStageFailed({
|
|
527
|
+
store,
|
|
528
|
+
runId: run.runId,
|
|
529
|
+
schedule: feedbackSchedule,
|
|
530
|
+
stageId,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
413
533
|
states.set(stageId, "failed");
|
|
414
534
|
notifyRetryRootTerminal(stageId, "failed");
|
|
415
535
|
await recordCloneFailureEnvelope(stageId, reason, envelope);
|
|
536
|
+
await persistSkipRejected(stageId, "failed");
|
|
416
537
|
if (cloneFailureContinuesSchedule(dag, stageId, completedEnvelopes)) {
|
|
417
538
|
if (firstFailureReason === undefined) {
|
|
418
539
|
firstFailureReason = reason;
|
|
@@ -426,23 +547,33 @@ export async function runPipelineDag(options) {
|
|
|
426
547
|
return;
|
|
427
548
|
}
|
|
428
549
|
if (retryContext) {
|
|
429
|
-
|
|
550
|
+
const accepted = failureIsAccepted(dag, stageId, states);
|
|
551
|
+
if (!accepted) {
|
|
552
|
+
markBranchDownstreamSkipped(stageId);
|
|
553
|
+
}
|
|
430
554
|
drainRetryMutations();
|
|
431
|
-
if (!
|
|
555
|
+
if (!accepted &&
|
|
556
|
+
!hasWorkOutsideBranch(dag, stageId, states, outsideWorkContext())) {
|
|
432
557
|
schedulingHalted = true;
|
|
433
558
|
}
|
|
434
|
-
if (firstFailureReason === undefined) {
|
|
559
|
+
if (!accepted && firstFailureReason === undefined) {
|
|
435
560
|
firstFailureReason = reason;
|
|
436
561
|
}
|
|
437
562
|
return;
|
|
438
563
|
}
|
|
439
|
-
if (!
|
|
440
|
-
schedulingHalted
|
|
441
|
-
|
|
564
|
+
if (!failureIsAccepted(dag, stageId, states)) {
|
|
565
|
+
if (!schedulingHalted) {
|
|
566
|
+
schedulingHalted = true;
|
|
567
|
+
firstFailureReason = reason;
|
|
568
|
+
}
|
|
442
569
|
}
|
|
443
570
|
};
|
|
444
571
|
const onStageSuccess = async (stageId, envelope) => {
|
|
445
|
-
const
|
|
572
|
+
const retryDownstreamIds = retryContext
|
|
573
|
+
? cloneRetryDownstream(dag, [...retryContext.retryRoots.keys()])
|
|
574
|
+
: EMPTY_RETRY_DOWNSTREAM;
|
|
575
|
+
const forceFreshCloneIds = activeReplayId(feedbackSchedule) !== undefined;
|
|
576
|
+
const conflict = cloneFanoutConflict(dag, envelope, states, retryDownstreamIds, { forceFreshCloneIds });
|
|
446
577
|
if (conflict !== undefined) {
|
|
447
578
|
await store.appendStageEvent(run.runId, stageId, {
|
|
448
579
|
event: "failed",
|
|
@@ -454,10 +585,119 @@ export async function runPipelineDag(options) {
|
|
|
454
585
|
states.set(stageId, "succeeded");
|
|
455
586
|
completedEnvelopes.set(stageId, envelope);
|
|
456
587
|
notifyRetryRootTerminal(stageId, "succeeded");
|
|
588
|
+
const feedbackAction = envelope.feedback_loop;
|
|
589
|
+
if (feedbackAction?.action === "send_back") {
|
|
590
|
+
const node = dag.nodes.find((n) => n.id === stageId);
|
|
591
|
+
if (node === undefined || node.feedback_loop === undefined) {
|
|
592
|
+
await handlePostSuccessError(stageId, `feedback_loop send_back from stage "${stageId}" without feedback_loop policy`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const latest = await store.getLatestStageExecution(run.runId, stageId);
|
|
596
|
+
const sourceAttempt = latest?.attempt ?? 1;
|
|
597
|
+
const applied = await applySendBack({
|
|
598
|
+
store,
|
|
599
|
+
runId: run.runId,
|
|
600
|
+
sourceNode: node,
|
|
601
|
+
sourceAttempt,
|
|
602
|
+
envelope,
|
|
603
|
+
schedule: feedbackSchedule,
|
|
604
|
+
projection: {
|
|
605
|
+
dag,
|
|
606
|
+
states,
|
|
607
|
+
completedEnvelopes,
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
if (applied.kind === "rejected") {
|
|
611
|
+
await handlePostSuccessError(stageId, applied.reason);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (applied.kind === "waiting_for_human") {
|
|
615
|
+
const waitAttempt = sourceAttempt;
|
|
616
|
+
await store.appendStageEvent(run.runId, stageId, { event: "waiting_for_input" }, { attempt: waitAttempt });
|
|
617
|
+
try {
|
|
618
|
+
await store.updateStageExecution(run.runId, stageId, waitAttempt, {
|
|
619
|
+
status: "waiting_for_input",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
// execution row may be absent in unusual harnesses
|
|
624
|
+
}
|
|
625
|
+
states.set(stageId, "waiting");
|
|
626
|
+
if (options.onFeedbackLoopWaiting !== undefined) {
|
|
627
|
+
let decisionResult;
|
|
628
|
+
await options.onFeedbackLoopWaiting({
|
|
629
|
+
loopId: applied.loop.loop_id,
|
|
630
|
+
sourceStageId: stageId,
|
|
631
|
+
resolve: async (decision) => {
|
|
632
|
+
decisionResult = await resolveDecision({
|
|
633
|
+
store,
|
|
634
|
+
runId: run.runId,
|
|
635
|
+
decision: decision.decision,
|
|
636
|
+
loopId: applied.loop.loop_id,
|
|
637
|
+
reason: decision.reason,
|
|
638
|
+
schedule: feedbackSchedule,
|
|
639
|
+
projection: {
|
|
640
|
+
dag,
|
|
641
|
+
states,
|
|
642
|
+
completedEnvelopes,
|
|
643
|
+
},
|
|
644
|
+
sourceNode: node,
|
|
645
|
+
});
|
|
646
|
+
return decisionResult;
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
if (decisionResult === undefined) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (!decisionResult.ok) {
|
|
653
|
+
await handlePostSuccessError(stageId, decisionResult.reason);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (decisionResult.effect === "abandoned") {
|
|
657
|
+
schedulingHalted = true;
|
|
658
|
+
if (firstFailureReason === undefined) {
|
|
659
|
+
firstFailureReason = decisionResult.reason;
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
for (const id of applied.retiredSkippedStageIds) {
|
|
668
|
+
await store.appendStageEvent(run.runId, id, { event: "skipped" });
|
|
669
|
+
}
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (feedbackAction?.action === "continue") {
|
|
673
|
+
await onContinued({
|
|
674
|
+
store,
|
|
675
|
+
runId: run.runId,
|
|
676
|
+
sourceStageId: stageId,
|
|
677
|
+
envelope,
|
|
678
|
+
schedule: feedbackSchedule,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
else if (activeReplayId(feedbackSchedule) !== undefined) {
|
|
682
|
+
await onRouteStageSucceeded({
|
|
683
|
+
store,
|
|
684
|
+
runId: run.runId,
|
|
685
|
+
schedule: feedbackSchedule,
|
|
686
|
+
stageId,
|
|
687
|
+
envelope,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
457
690
|
const node = dag.nodes.find((n) => n.id === stageId);
|
|
458
691
|
const protectedIds = protectedClonableChildIds(dag, stageId, envelope);
|
|
459
692
|
if (node?.fork) {
|
|
460
693
|
const chosen = normalizeForkChoice(envelope.fork_choice, "stored");
|
|
694
|
+
const sourceId = activeSourceStageId(feedbackSchedule);
|
|
695
|
+
if (activeReplayId(feedbackSchedule) !== undefined &&
|
|
696
|
+
sourceId !== undefined &&
|
|
697
|
+
!forkChoicePreservesReplaySource(dag, new Set(chosen), sourceId)) {
|
|
698
|
+
await handlePostSuccessError(stageId, `fork_choice during feedback replay must preserve a path to source "${sourceId}"`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
461
701
|
const skippedIds = [];
|
|
462
702
|
applyForkChoiceToSchedule(dag, stageId, chosen, states, {
|
|
463
703
|
protectedIds,
|
|
@@ -470,7 +710,9 @@ export async function runPipelineDag(options) {
|
|
|
470
710
|
await store.appendStageEvent(run.runId, id, { event: "skipped" });
|
|
471
711
|
}
|
|
472
712
|
}
|
|
473
|
-
const applied = applyCloneForksToSchedule(dag, stageId, envelope, states
|
|
713
|
+
const applied = applyCloneForksToSchedule(dag, stageId, envelope, states, {
|
|
714
|
+
forceFreshCloneIds,
|
|
715
|
+
});
|
|
474
716
|
const dagChanged = applied.dag !== dag;
|
|
475
717
|
dag = applied.dag;
|
|
476
718
|
if (dagChanged) {
|
|
@@ -480,6 +722,21 @@ export async function runPipelineDag(options) {
|
|
|
480
722
|
notifyRetryRootTerminal(id, "skipped");
|
|
481
723
|
await store.appendStageEvent(run.runId, id, { event: "skipped" });
|
|
482
724
|
}
|
|
725
|
+
if (applied.mintedBySuccessor !== undefined) {
|
|
726
|
+
for (const [, cloneStageIds] of applied.mintedBySuccessor) {
|
|
727
|
+
if (cloneStageIds.length === 0)
|
|
728
|
+
continue;
|
|
729
|
+
const replayId = activeReplayId(feedbackSchedule);
|
|
730
|
+
await mintCohortForFanout({
|
|
731
|
+
store,
|
|
732
|
+
runId: run.runId,
|
|
733
|
+
...(replayId !== undefined ? { replayId } : {}),
|
|
734
|
+
forkParent: stageId,
|
|
735
|
+
cloneStageIds,
|
|
736
|
+
});
|
|
737
|
+
noteActiveCohort(feedbackSchedule, stageId, cloneStageIds);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
483
740
|
};
|
|
484
741
|
const handlePostSuccessError = async (stageId, reason) => {
|
|
485
742
|
if (states.get(stageId) === "succeeded") {
|
|
@@ -500,6 +757,22 @@ export async function runPipelineDag(options) {
|
|
|
500
757
|
}
|
|
501
758
|
const attempt = await resolveLaunchAttempt(stageId);
|
|
502
759
|
const attemptCtx = attemptContext(attempt);
|
|
760
|
+
const prep = launchFor(feedbackSchedule, stageId);
|
|
761
|
+
const feedbackLoopContext = prep?.feedbackLoopContext;
|
|
762
|
+
const sessionMode = prep?.sessionMode;
|
|
763
|
+
const priorAttempt = prep?.priorAttempt;
|
|
764
|
+
const resumeToken = sessionMode === "feedback_resume" && priorAttempt !== undefined
|
|
765
|
+
? resumeSessionFilePath(run.workspaceDir, stageId, priorAttempt)
|
|
766
|
+
: undefined;
|
|
767
|
+
if (activeReplayId(feedbackSchedule) !== undefined &&
|
|
768
|
+
feedbackLoopContext !== undefined) {
|
|
769
|
+
await onRouteStageRunning({
|
|
770
|
+
store,
|
|
771
|
+
runId: run.runId,
|
|
772
|
+
schedule: feedbackSchedule,
|
|
773
|
+
stageId,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
503
776
|
if (executionMode === "process") {
|
|
504
777
|
const launcher = options.stageProcessLauncher;
|
|
505
778
|
if (!launcher) {
|
|
@@ -511,6 +784,16 @@ export async function runPipelineDag(options) {
|
|
|
511
784
|
stageId,
|
|
512
785
|
rootDir: factoryCwd,
|
|
513
786
|
attempt,
|
|
787
|
+
...(sessionMode !== undefined
|
|
788
|
+
? {
|
|
789
|
+
mode: sessionMode === "feedback_resume"
|
|
790
|
+
? "feedback_resume"
|
|
791
|
+
: sessionMode === "new_session"
|
|
792
|
+
? "new_session"
|
|
793
|
+
: "run",
|
|
794
|
+
}
|
|
795
|
+
: {}),
|
|
796
|
+
...(resumeToken !== undefined ? { sessionFilePath: resumeToken } : {}),
|
|
514
797
|
...(prepared.operatorCatalog !== undefined
|
|
515
798
|
? { operatorCatalog: prepared.operatorCatalog }
|
|
516
799
|
: {}),
|
|
@@ -550,6 +833,9 @@ export async function runPipelineDag(options) {
|
|
|
550
833
|
operatorCatalog: prepared.operatorCatalog,
|
|
551
834
|
completedEnvelopes,
|
|
552
835
|
skipGates: prepared.skipGates,
|
|
836
|
+
...(sessionMode !== undefined ? { sessionMode } : {}),
|
|
837
|
+
...(feedbackLoopContext !== undefined ? { feedbackLoopContext } : {}),
|
|
838
|
+
...(resumeToken !== undefined ? { resumeToken } : {}),
|
|
553
839
|
});
|
|
554
840
|
if (isRunStageWaiting(result)) {
|
|
555
841
|
await onStageFailure(stageId, WAIT_WITHOUT_WORKER_DISPATCH);
|
|
@@ -577,6 +863,7 @@ export async function runPipelineDag(options) {
|
|
|
577
863
|
if (states.get(stageId) !== "pending")
|
|
578
864
|
return;
|
|
579
865
|
states.set(stageId, "active");
|
|
866
|
+
launchedIds.add(stageId);
|
|
580
867
|
activeCount += 1;
|
|
581
868
|
const taskPromise = launchStage(stageId).finally(() => {
|
|
582
869
|
activeCount -= 1;
|
|
@@ -587,7 +874,7 @@ export async function runPipelineDag(options) {
|
|
|
587
874
|
const pickRunnable = () => {
|
|
588
875
|
const ids = [];
|
|
589
876
|
for (const node of dag.nodes) {
|
|
590
|
-
if (isRunnable(dag, node.id, states, schedulingHalted, completedEnvelopes)) {
|
|
877
|
+
if (isRunnable(dag, node.id, states, schedulingHalted, completedEnvelopes, feedbackSchedule)) {
|
|
591
878
|
ids.push(node.id);
|
|
592
879
|
}
|
|
593
880
|
}
|
|
@@ -660,11 +947,7 @@ export async function runPipelineDag(options) {
|
|
|
660
947
|
reason: firstFailureReason,
|
|
661
948
|
};
|
|
662
949
|
}
|
|
663
|
-
|
|
664
|
-
const s = states.get(node.id);
|
|
665
|
-
return s === "succeeded" || s === "skipped";
|
|
666
|
-
});
|
|
667
|
-
if (!allSucceeded) {
|
|
950
|
+
if (scheduleHasUnhandledFailure(dag, states) || !allTerminal()) {
|
|
668
951
|
if (retryContext === undefined) {
|
|
669
952
|
await store.updateRunStatus(run.runId, "failed");
|
|
670
953
|
}
|