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
|
@@ -4,9 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
|
|
5
5
|
import { loadRunContext } from "./resumeReconstruct.js";
|
|
6
6
|
import { loadTaskFromYaml } from "../config/loadTask.js";
|
|
7
|
+
import { deriveStatusFromStages, findUnhandledFailedStage, } from "../runstore/port.js";
|
|
7
8
|
import { PipelineValidationError, startPipeline, } from "./pipelineRunner.js";
|
|
8
|
-
import { resumeRun } from "./pipelineScheduler.js";
|
|
9
|
-
import {
|
|
9
|
+
import { hydrateScheduleFromStore, hydratedScheduleHasRunnableWork, resumeRun, runPipelineDag, } from "./pipelineScheduler.js";
|
|
10
|
+
import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
|
|
11
|
+
import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
|
|
10
12
|
import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
|
|
11
13
|
import { StageProcessLauncher } from "./stageProcessLauncher.js";
|
|
12
14
|
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, projectSettingsContext, readMaxConcurrentFromContext, writeMaxConcurrentToContext, } from "./settingsFile.js";
|
|
@@ -17,6 +19,7 @@ import { reconstructAndContinue as resumeReconstructAndContinue } from "./resume
|
|
|
17
19
|
import { resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
18
20
|
import { resolveStartTaskInput } from "./taskInput.js";
|
|
19
21
|
import { failStageAsInterrupted, syncRunStatusFromStages, } from "./stageRecovery.js";
|
|
22
|
+
import { blocksGenericRetry, manualRecoveryEligibility, readManualRecoveryState, } from "./manualRecoveryState.js";
|
|
20
23
|
const DEFAULT_MAX_CONCURRENT = 3;
|
|
21
24
|
const STARTUP_RECONCILE_REASON = "process_interrupted: no active worker (server restart)";
|
|
22
25
|
const OPERATOR_ABANDON_REASON = "process_interrupted: operator abandoned stage";
|
|
@@ -45,6 +48,8 @@ export class RunManager {
|
|
|
45
48
|
provisionalIds = new Set();
|
|
46
49
|
resumeInFlight = new Set();
|
|
47
50
|
retryInFlight = new Set();
|
|
51
|
+
retryStartOwner = new Map();
|
|
52
|
+
retryStartWaiters = new Map();
|
|
48
53
|
trackingGeneration = 0;
|
|
49
54
|
maxConcurrent;
|
|
50
55
|
maxActiveStagesPerRun;
|
|
@@ -64,6 +69,7 @@ export class RunManager {
|
|
|
64
69
|
},
|
|
65
70
|
onOrchestrationStarted: (runId, promise) => {
|
|
66
71
|
this.registerResumeUntrack(runId, promise);
|
|
72
|
+
this.notifyRetryStartWaiters(runId);
|
|
67
73
|
},
|
|
68
74
|
rollbackStartTracking: async (runId, state) => {
|
|
69
75
|
if (state.insertedForResume) {
|
|
@@ -265,6 +271,26 @@ export class RunManager {
|
|
|
265
271
|
}
|
|
266
272
|
return { reconciled };
|
|
267
273
|
}
|
|
274
|
+
async resumeStalledSchedules() {
|
|
275
|
+
const resumed = [];
|
|
276
|
+
const runs = await this.options.store.listRuns();
|
|
277
|
+
for (const summary of runs) {
|
|
278
|
+
if (summary.status !== "running")
|
|
279
|
+
continue;
|
|
280
|
+
const runId = summary.run_id;
|
|
281
|
+
if (this.active.has(runId))
|
|
282
|
+
continue;
|
|
283
|
+
try {
|
|
284
|
+
if (await this.tryResumeStalledSchedule(runId)) {
|
|
285
|
+
resumed.push({ runId });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
console.error(`resumeStalledSchedules: failed to resume ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return resumed;
|
|
293
|
+
}
|
|
268
294
|
async abandonStage(runId, stageId) {
|
|
269
295
|
let detail;
|
|
270
296
|
try {
|
|
@@ -389,6 +415,81 @@ export class RunManager {
|
|
|
389
415
|
return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, undefined, undefined, rerunProjectRoot);
|
|
390
416
|
}
|
|
391
417
|
async retryStage(runId, stageId) {
|
|
418
|
+
return this.retryStageInternal(runId, stageId, true);
|
|
419
|
+
}
|
|
420
|
+
async recoverManualStage(runId, stageId, guidance) {
|
|
421
|
+
return this.recoverManualStageInternal(runId, stageId, guidance, true);
|
|
422
|
+
}
|
|
423
|
+
async recoverManualStageUntilStop(runId, stageId, guidance) {
|
|
424
|
+
const retryKey = waitKey(runId, stageId);
|
|
425
|
+
const retried = await this.recoverManualStageInternal(runId, stageId, guidance, false);
|
|
426
|
+
if (!retried.ok)
|
|
427
|
+
return retried;
|
|
428
|
+
try {
|
|
429
|
+
if (retried.done === undefined) {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
reason: `Recovery orchestration did not start for run ${runId} stage ${stageId}`,
|
|
433
|
+
status: 500,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return { ok: true, pipeline: await retried.done };
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
this.retryInFlight.delete(retryKey);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async recoverManualStageInternal(runId, stageId, guidance, awaitRoot) {
|
|
443
|
+
if (guidance !== undefined && guidance.trim().length > 4_000) {
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
reason: "Manual recovery guidance must be 4,000 characters or fewer",
|
|
447
|
+
status: 400,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const eligibility = await this.manualRecoveryEligibility(runId, stageId);
|
|
451
|
+
if (!eligibility.ok)
|
|
452
|
+
return eligibility;
|
|
453
|
+
return this.retryStageInternal(runId, stageId, awaitRoot, {
|
|
454
|
+
manualRecovery: true,
|
|
455
|
+
beforeAttemptStart: async (attempt) => {
|
|
456
|
+
await this.options.store.appendStageEvent(runId, stageId, {
|
|
457
|
+
event: "manual_recovery_requested",
|
|
458
|
+
...(guidance !== undefined && guidance.trim() !== ""
|
|
459
|
+
? { guidance: guidance.trim() }
|
|
460
|
+
: {}),
|
|
461
|
+
}, { attempt });
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
async stopManualRecovery(runId, stageId) {
|
|
466
|
+
const eligibility = await this.manualRecoveryEligibility(runId, stageId);
|
|
467
|
+
if (!eligibility.ok)
|
|
468
|
+
return eligibility;
|
|
469
|
+
await this.options.store.appendStageEvent(runId, stageId, { event: "manual_recovery_stopped" }, { attempt: eligibility.failedAttempt });
|
|
470
|
+
return { ok: true, runId, stageId };
|
|
471
|
+
}
|
|
472
|
+
async retryStageUntilStop(runId, stageId) {
|
|
473
|
+
const retryKey = waitKey(runId, stageId);
|
|
474
|
+
const retried = await this.retryStageInternal(runId, stageId, false);
|
|
475
|
+
if (!retried.ok)
|
|
476
|
+
return retried;
|
|
477
|
+
try {
|
|
478
|
+
if (retried.done === undefined) {
|
|
479
|
+
return {
|
|
480
|
+
ok: false,
|
|
481
|
+
reason: `Retry orchestration did not start for run ${runId} stage ${stageId}`,
|
|
482
|
+
status: 500,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const pipeline = await retried.done;
|
|
486
|
+
return { ok: true, pipeline };
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
this.retryInFlight.delete(retryKey);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async retryStageInternal(runId, stageId, awaitRoot, options) {
|
|
392
493
|
const retryKey = waitKey(runId, stageId);
|
|
393
494
|
if (this.retryInFlight.has(retryKey)) {
|
|
394
495
|
return {
|
|
@@ -398,10 +499,58 @@ export class RunManager {
|
|
|
398
499
|
};
|
|
399
500
|
}
|
|
400
501
|
this.retryInFlight.add(retryKey);
|
|
502
|
+
if (options?.manualRecovery !== true &&
|
|
503
|
+
await this.blocksGenericRetry(runId, stageId)) {
|
|
504
|
+
this.retryInFlight.delete(retryKey);
|
|
505
|
+
return {
|
|
506
|
+
ok: false,
|
|
507
|
+
reason: "Stage uses manual recovery; use the manual recovery action instead of retry",
|
|
508
|
+
status: 409,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
401
511
|
try {
|
|
402
|
-
|
|
512
|
+
let orchestrationConflict = this.active.has(runId) &&
|
|
403
513
|
!this.attachedWaiting.has(waitKey(runId, stageId));
|
|
404
|
-
|
|
514
|
+
if (!this.retryCoordinator.isActive(runId)) {
|
|
515
|
+
const entry = this.active.get(runId);
|
|
516
|
+
const pending = entry?.done;
|
|
517
|
+
const generation = entry?.generation;
|
|
518
|
+
if (orchestrationConflict &&
|
|
519
|
+
pending !== undefined &&
|
|
520
|
+
(await this.isStartRunWindingDown(runId))) {
|
|
521
|
+
if (this.claimRetryStart(runId, stageId)) {
|
|
522
|
+
await this.awaitStartRunWindDown(runId, pending, generation);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
await this.waitForRetryCoordinatorOrOwnerRelease(runId);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else if (!this.claimRetryStart(runId, stageId)) {
|
|
529
|
+
await this.waitForRetryCoordinatorOrOwnerRelease(runId);
|
|
530
|
+
}
|
|
531
|
+
let detail;
|
|
532
|
+
try {
|
|
533
|
+
detail = await this.options.store.readRun(runId);
|
|
534
|
+
await this.options.store.readRunMeta(runId);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
this.retryInFlight.delete(retryKey);
|
|
538
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
539
|
+
}
|
|
540
|
+
orchestrationConflict =
|
|
541
|
+
this.active.has(runId) &&
|
|
542
|
+
!this.attachedWaiting.has(waitKey(runId, stageId)) &&
|
|
543
|
+
!this.retryCoordinator.isActive(runId);
|
|
544
|
+
if (orchestrationConflict &&
|
|
545
|
+
!(await this.isStartRunWindingDown(runId))) {
|
|
546
|
+
const hasActiveStage = detail.stages.some((stage) => stage.status === "running" ||
|
|
547
|
+
stage.status === "waiting_for_input");
|
|
548
|
+
if (!hasActiveStage) {
|
|
549
|
+
orchestrationConflict = false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const result = await this.retryCoordinator.retryStage({
|
|
405
554
|
runId,
|
|
406
555
|
stageId,
|
|
407
556
|
store: this.options.store,
|
|
@@ -414,10 +563,153 @@ export class RunManager {
|
|
|
414
563
|
hitl: this.hitl,
|
|
415
564
|
orchestrationConflict,
|
|
416
565
|
tracking: this.retryTracking,
|
|
566
|
+
awaitRoot,
|
|
567
|
+
...(options?.beforeAttemptStart !== undefined
|
|
568
|
+
? { beforeAttemptStart: options.beforeAttemptStart }
|
|
569
|
+
: {}),
|
|
417
570
|
});
|
|
571
|
+
if (!result.ok || awaitRoot !== false) {
|
|
572
|
+
this.retryInFlight.delete(retryKey);
|
|
573
|
+
}
|
|
574
|
+
return result;
|
|
418
575
|
}
|
|
419
|
-
|
|
576
|
+
catch (err) {
|
|
420
577
|
this.retryInFlight.delete(retryKey);
|
|
578
|
+
throw err;
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
this.releaseRetryStart(runId, stageId);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async isStartRunWindingDown(runId) {
|
|
585
|
+
try {
|
|
586
|
+
const detail = await this.options.store.readRun(runId);
|
|
587
|
+
const meta = await this.options.store.readRunMeta(runId);
|
|
588
|
+
let hasPending = false;
|
|
589
|
+
for (const stage of detail.stages) {
|
|
590
|
+
if (stage.status === "running" ||
|
|
591
|
+
stage.status === "waiting_for_input") {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
if (stage.status === "pending")
|
|
595
|
+
hasPending = true;
|
|
596
|
+
}
|
|
597
|
+
return (findUnhandledFailedStage(detail.stages, meta.pipeline_dag) !==
|
|
598
|
+
undefined || !hasPending);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async awaitStartRunWindDown(runId, pending, generation) {
|
|
605
|
+
let pendingError;
|
|
606
|
+
const settled = pending.then(() => undefined, (err) => {
|
|
607
|
+
pendingError = err;
|
|
608
|
+
});
|
|
609
|
+
const timedOut = await this.raceRetryRootTimeout(settled);
|
|
610
|
+
const after = this.active.get(runId);
|
|
611
|
+
const leftGeneration = after === undefined || after.generation !== generation;
|
|
612
|
+
if (pendingError !== undefined && !leftGeneration) {
|
|
613
|
+
throw pendingError instanceof Error
|
|
614
|
+
? pendingError
|
|
615
|
+
: new Error(String(pendingError));
|
|
616
|
+
}
|
|
617
|
+
if (timedOut && !leftGeneration) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async raceRetryRootTimeout(promise) {
|
|
622
|
+
const ms = readRetryRootWaitTimeoutMs();
|
|
623
|
+
let timer;
|
|
624
|
+
try {
|
|
625
|
+
return await Promise.race([
|
|
626
|
+
promise.then(() => false),
|
|
627
|
+
new Promise((resolve) => {
|
|
628
|
+
timer = setTimeout(() => resolve(true), ms);
|
|
629
|
+
}),
|
|
630
|
+
]);
|
|
631
|
+
}
|
|
632
|
+
finally {
|
|
633
|
+
if (timer !== undefined)
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
claimRetryStart(runId, stageId) {
|
|
638
|
+
const owner = this.retryStartOwner.get(runId);
|
|
639
|
+
if (owner !== undefined && owner !== stageId)
|
|
640
|
+
return false;
|
|
641
|
+
this.retryStartOwner.set(runId, stageId);
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
releaseRetryStart(runId, stageId) {
|
|
645
|
+
if (this.retryStartOwner.get(runId) !== stageId)
|
|
646
|
+
return;
|
|
647
|
+
this.retryStartOwner.delete(runId);
|
|
648
|
+
this.notifyRetryStartWaiters(runId);
|
|
649
|
+
}
|
|
650
|
+
notifyRetryStartWaiters(runId) {
|
|
651
|
+
const waiters = this.retryStartWaiters.get(runId);
|
|
652
|
+
if (waiters === undefined)
|
|
653
|
+
return;
|
|
654
|
+
this.retryStartWaiters.delete(runId);
|
|
655
|
+
for (const resolve of waiters)
|
|
656
|
+
resolve();
|
|
657
|
+
}
|
|
658
|
+
async waitForRetryCoordinatorOrOwnerRelease(runId) {
|
|
659
|
+
if (this.retryCoordinator.isActive(runId) ||
|
|
660
|
+
!this.retryStartOwner.has(runId)) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
let onReady;
|
|
664
|
+
const watched = new Promise((resolve) => {
|
|
665
|
+
const list = this.retryStartWaiters.get(runId) ?? new Set();
|
|
666
|
+
onReady = () => resolve();
|
|
667
|
+
list.add(onReady);
|
|
668
|
+
this.retryStartWaiters.set(runId, list);
|
|
669
|
+
if (this.retryCoordinator.isActive(runId) ||
|
|
670
|
+
!this.retryStartOwner.has(runId)) {
|
|
671
|
+
resolve();
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
try {
|
|
675
|
+
await this.raceRetryRootTimeout(watched);
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
if (onReady !== undefined) {
|
|
679
|
+
const list = this.retryStartWaiters.get(runId);
|
|
680
|
+
if (list !== undefined) {
|
|
681
|
+
list.delete(onReady);
|
|
682
|
+
if (list.size === 0)
|
|
683
|
+
this.retryStartWaiters.delete(runId);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async manualRecoveryEligibility(runId, stageId) {
|
|
689
|
+
try {
|
|
690
|
+
const eligibility = manualRecoveryEligibility(await readManualRecoveryState(this.options.store, runId, stageId));
|
|
691
|
+
if (!eligibility.ok) {
|
|
692
|
+
return {
|
|
693
|
+
...eligibility,
|
|
694
|
+
reason: eligibility.reason === "Stage is not configured for manual recovery"
|
|
695
|
+
? `${eligibility.reason}: ${stageId}`
|
|
696
|
+
: eligibility.reason === "Stage not found"
|
|
697
|
+
? `${eligibility.reason}: ${stageId}`
|
|
698
|
+
: eligibility.reason,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
return eligibility;
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async blocksGenericRetry(runId, stageId) {
|
|
708
|
+
try {
|
|
709
|
+
return blocksGenericRetry(await readManualRecoveryState(this.options.store, runId, stageId));
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
return false;
|
|
421
713
|
}
|
|
422
714
|
}
|
|
423
715
|
/**
|
|
@@ -509,6 +801,158 @@ export class RunManager {
|
|
|
509
801
|
}
|
|
510
802
|
}
|
|
511
803
|
}
|
|
804
|
+
/**
|
|
805
|
+
* Resolve a feedback-loop wait_for_human decision after the scheduler has
|
|
806
|
+
* exited waiting (host-down / HTTP / CLI). Persist-only resolve, then
|
|
807
|
+
* resume orchestration for extend/continue.
|
|
808
|
+
*/
|
|
809
|
+
async decideFeedbackLoop(runId, stageId, input) {
|
|
810
|
+
const resumeKey = waitKey(runId, stageId);
|
|
811
|
+
if (this.resumeInFlight.has(resumeKey)) {
|
|
812
|
+
return {
|
|
813
|
+
ok: false,
|
|
814
|
+
reason: `Resume already in progress for run ${runId} stage ${stageId}`,
|
|
815
|
+
status: 409,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
this.resumeInFlight.add(resumeKey);
|
|
819
|
+
let detail;
|
|
820
|
+
try {
|
|
821
|
+
detail = await this.options.store.readRun(runId);
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
this.resumeInFlight.delete(resumeKey);
|
|
825
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
826
|
+
}
|
|
827
|
+
const stageSnap = detail.stages.find((s) => s.stage_id === stageId);
|
|
828
|
+
if (!stageSnap) {
|
|
829
|
+
this.resumeInFlight.delete(resumeKey);
|
|
830
|
+
return {
|
|
831
|
+
ok: false,
|
|
832
|
+
reason: `Stage not found: ${stageId}`,
|
|
833
|
+
status: 404,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
const active = detail.active_feedback_loop;
|
|
837
|
+
if (active === undefined ||
|
|
838
|
+
active.state !== "waiting_for_human") {
|
|
839
|
+
this.resumeInFlight.delete(resumeKey);
|
|
840
|
+
return {
|
|
841
|
+
ok: false,
|
|
842
|
+
reason: "no feedback loop is waiting_for_human",
|
|
843
|
+
status: 409,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
if (active.source_stage_id !== stageId) {
|
|
847
|
+
this.resumeInFlight.delete(resumeKey);
|
|
848
|
+
return {
|
|
849
|
+
ok: false,
|
|
850
|
+
reason: `stage "${stageId}" is not the feedback loop source (expected ${active.source_stage_id})`,
|
|
851
|
+
status: 409,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
if (input.loopId !== undefined &&
|
|
855
|
+
input.loopId !== active.loop_id) {
|
|
856
|
+
this.resumeInFlight.delete(resumeKey);
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
reason: `feedback loop not found: ${input.loopId}`,
|
|
860
|
+
status: 404,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (stageSnap.status !== "waiting_for_input") {
|
|
864
|
+
this.resumeInFlight.delete(resumeKey);
|
|
865
|
+
return {
|
|
866
|
+
ok: false,
|
|
867
|
+
reason: `Stage is not waiting for input (status=${stageSnap.status})`,
|
|
868
|
+
status: 409,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
const wasActive = this.active.has(runId);
|
|
872
|
+
const tracked = await this.ensureResumeTracked(runId);
|
|
873
|
+
if (!tracked.ok) {
|
|
874
|
+
this.resumeInFlight.delete(resumeKey);
|
|
875
|
+
return {
|
|
876
|
+
ok: false,
|
|
877
|
+
reason: tracked.reason,
|
|
878
|
+
status: 409,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const insertedForResume = !wasActive;
|
|
882
|
+
try {
|
|
883
|
+
const decided = await resolveFeedbackLoopDecision({
|
|
884
|
+
store: this.options.store,
|
|
885
|
+
runId,
|
|
886
|
+
decision: input.decision,
|
|
887
|
+
loopId: input.loopId ?? active.loop_id,
|
|
888
|
+
reason: input.reason,
|
|
889
|
+
});
|
|
890
|
+
if (!decided.ok) {
|
|
891
|
+
if (insertedForResume) {
|
|
892
|
+
this.removeActiveEntry(runId, false);
|
|
893
|
+
}
|
|
894
|
+
return { ok: false, reason: decided.reason, status: 409 };
|
|
895
|
+
}
|
|
896
|
+
if (decided.effect === "abandoned") {
|
|
897
|
+
if (insertedForResume) {
|
|
898
|
+
this.removeActiveEntry(runId, false);
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
ok: true,
|
|
902
|
+
effect: "abandoned",
|
|
903
|
+
loopId: decided.loop.loop_id,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const { meta, task, loaded } = await loadRunContext(this.options.store, runId, this.cwd);
|
|
907
|
+
const done = runPipelineDag({
|
|
908
|
+
prepared: {
|
|
909
|
+
task,
|
|
910
|
+
loaded,
|
|
911
|
+
run: {
|
|
912
|
+
runId,
|
|
913
|
+
workspaceDir: this.options.store.getWorkspaceDir(runId),
|
|
914
|
+
},
|
|
915
|
+
agent: this.options.agent,
|
|
916
|
+
store: this.options.store,
|
|
917
|
+
cwd: this.cwd,
|
|
918
|
+
projectRoot: this.projectRoot,
|
|
919
|
+
checkoutRoot: meta.checkout_root,
|
|
920
|
+
hitl: this.hitl,
|
|
921
|
+
operatorCatalog: this.options.operatorCatalog,
|
|
922
|
+
},
|
|
923
|
+
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
924
|
+
executionMode: this.executionMode,
|
|
925
|
+
stageProcessLauncher: this.stageProcessLauncher,
|
|
926
|
+
});
|
|
927
|
+
this.registerResumeUntrack(runId, done);
|
|
928
|
+
const rest = await done;
|
|
929
|
+
if (rest.outcome === "failed") {
|
|
930
|
+
return {
|
|
931
|
+
ok: false,
|
|
932
|
+
reason: rest.reason ?? "pipeline failed after feedback decision",
|
|
933
|
+
status: 500,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
ok: true,
|
|
938
|
+
effect: decided.effect === "extended" ? "extended" : "continued",
|
|
939
|
+
loopId: decided.loop.loop_id,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
catch (err) {
|
|
943
|
+
if (insertedForResume) {
|
|
944
|
+
this.removeActiveEntry(runId, false);
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
949
|
+
status: 500,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
finally {
|
|
953
|
+
this.resumeInFlight.delete(resumeKey);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
512
956
|
async resumeViaSubprocess(runId, stageId, opaqueAnswer) {
|
|
513
957
|
const store = this.options.store;
|
|
514
958
|
const launcher = this.stageProcessLauncher;
|
|
@@ -727,7 +1171,7 @@ export class RunManager {
|
|
|
727
1171
|
this.active.delete(provisionalId);
|
|
728
1172
|
this.provisionalIds.delete(provisionalId);
|
|
729
1173
|
const generation = ++this.trackingGeneration;
|
|
730
|
-
this.active.set(runId, { ...entry, generation });
|
|
1174
|
+
this.active.set(runId, { ...entry, generation, done });
|
|
731
1175
|
if (entry.checkoutKey !== undefined) {
|
|
732
1176
|
this.checkoutLeases.set(entry.checkoutKey, runId);
|
|
733
1177
|
}
|
|
@@ -735,6 +1179,56 @@ export class RunManager {
|
|
|
735
1179
|
this.untrackIfGeneration(runId, generation);
|
|
736
1180
|
});
|
|
737
1181
|
}
|
|
1182
|
+
async tryResumeStalledSchedule(runId) {
|
|
1183
|
+
const detail = await this.options.store.readRun(runId);
|
|
1184
|
+
const meta = await this.options.store.readRunMeta(runId);
|
|
1185
|
+
if (detail.stages.some((stage) => stage.status === "running" || stage.status === "waiting_for_input")) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
if (deriveStatusFromStages(detail.stages, meta.pipeline_dag) !== "running") {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
const dag = meta.pipeline_dag;
|
|
1192
|
+
if (dag === undefined)
|
|
1193
|
+
return false;
|
|
1194
|
+
const hydrated = await hydrateScheduleFromStore(this.options.store, runId, dag, this.executionMode);
|
|
1195
|
+
if (!hydratedScheduleHasRunnableWork(hydrated))
|
|
1196
|
+
return false;
|
|
1197
|
+
const tracked = await this.ensureResumeTracked(runId);
|
|
1198
|
+
if (!tracked.ok) {
|
|
1199
|
+
console.error(`resumeStalledSchedules: ${tracked.reason}`);
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
try {
|
|
1203
|
+
const { meta: loadedMeta, task, loaded, workspaceDir } = await loadRunContext(this.options.store, runId, this.cwd);
|
|
1204
|
+
const promise = runPipelineDag({
|
|
1205
|
+
prepared: {
|
|
1206
|
+
task,
|
|
1207
|
+
loaded,
|
|
1208
|
+
run: { runId, workspaceDir },
|
|
1209
|
+
agent: this.options.agent,
|
|
1210
|
+
store: this.options.store,
|
|
1211
|
+
cwd: this.cwd,
|
|
1212
|
+
projectRoot: this.projectRoot,
|
|
1213
|
+
checkoutRoot: loadedMeta.checkout_root,
|
|
1214
|
+
hitl: this.hitl,
|
|
1215
|
+
operatorCatalog: this.options.operatorCatalog,
|
|
1216
|
+
},
|
|
1217
|
+
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
1218
|
+
executionMode: this.executionMode,
|
|
1219
|
+
stageProcessLauncher: this.stageProcessLauncher,
|
|
1220
|
+
initialSchedule: hydrated,
|
|
1221
|
+
}).finally(() => {
|
|
1222
|
+
void syncRunStatusFromStages(this.options.store, runId).catch(() => { });
|
|
1223
|
+
});
|
|
1224
|
+
this.registerResumeUntrack(runId, promise);
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
catch (err) {
|
|
1228
|
+
this.removeActiveEntry(runId, false);
|
|
1229
|
+
throw err;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
738
1232
|
/**
|
|
739
1233
|
* Acquire active tracking + checkout lease for a restart resume before
|
|
740
1234
|
* reconstruct starts. Over-max is allowed. Lease held by another run → fail closed.
|
|
@@ -778,7 +1272,7 @@ export class RunManager {
|
|
|
778
1272
|
if (entry === undefined)
|
|
779
1273
|
return;
|
|
780
1274
|
const generation = ++this.trackingGeneration;
|
|
781
|
-
this.active.set(runId, { ...entry, generation });
|
|
1275
|
+
this.active.set(runId, { ...entry, generation, done });
|
|
782
1276
|
void done.finally(() => {
|
|
783
1277
|
this.untrackIfGeneration(runId, generation);
|
|
784
1278
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentPort } from "../agent/port.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type RunDetail, type RunPipelineDagSnapshot, type RunStatus, type RunStore, type StageSnapshot } from "../runstore/port.js";
|
|
3
3
|
import type { PipelineRunResult } from "./pipelineRunner.js";
|
|
4
4
|
import { type SchedulerPreparedPipeline } from "./pipelineScheduler.js";
|
|
5
5
|
import type { StageExecutionMode } from "./stageConcurrency.js";
|
|
@@ -29,6 +29,7 @@ export type RetryStageResult = {
|
|
|
29
29
|
runId: string;
|
|
30
30
|
stageId: string;
|
|
31
31
|
attemptIndex: number;
|
|
32
|
+
done?: Promise<PipelineRunResult>;
|
|
32
33
|
} | {
|
|
33
34
|
ok: false;
|
|
34
35
|
reason: string;
|
|
@@ -62,9 +63,14 @@ export type RetryStageRequest = {
|
|
|
62
63
|
hitl: StageHitlController;
|
|
63
64
|
orchestrationConflict: boolean;
|
|
64
65
|
tracking: RetryTrackingPort;
|
|
66
|
+
awaitRoot?: boolean;
|
|
67
|
+
beforeAttemptStart?: (attempt: number) => Promise<void>;
|
|
65
68
|
};
|
|
69
|
+
export declare function succeededStageRetryBlocker(stageSnap: StageSnapshot): string | undefined;
|
|
66
70
|
export declare function assertStageRetryEligible(detail: RunDetail, stageId: string, opts?: {
|
|
67
71
|
recoveryActive?: boolean;
|
|
72
|
+
persistedStatus?: RunStatus;
|
|
73
|
+
dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
|
|
68
74
|
}): {
|
|
69
75
|
ok: true;
|
|
70
76
|
} | {
|
|
@@ -72,6 +78,7 @@ export declare function assertStageRetryEligible(detail: RunDetail, stageId: str
|
|
|
72
78
|
reason: string;
|
|
73
79
|
status: number;
|
|
74
80
|
};
|
|
81
|
+
export declare function readRetryRootWaitTimeoutMs(): number;
|
|
75
82
|
export declare class RunRetryCoordinator {
|
|
76
83
|
private readonly sessions;
|
|
77
84
|
isActive(runId: string): boolean;
|