stageflow 0.9.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 +1 -0
- 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 +1 -1
- package/dist/cli/runsCommand.js +184 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +11 -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/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 -1
- package/dist/package-meta.js +1 -1
- 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 +54 -1
- package/dist/runtime/runManager.js +471 -6
- package/dist/runtime/runRetryCoordinator.d.ts +6 -1
- package/dist/runtime/runRetryCoordinator.js +43 -9
- 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-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- 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 {
|
|
@@ -391,6 +417,58 @@ export class RunManager {
|
|
|
391
417
|
async retryStage(runId, stageId) {
|
|
392
418
|
return this.retryStageInternal(runId, stageId, true);
|
|
393
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
|
+
}
|
|
394
472
|
async retryStageUntilStop(runId, stageId) {
|
|
395
473
|
const retryKey = waitKey(runId, stageId);
|
|
396
474
|
const retried = await this.retryStageInternal(runId, stageId, false);
|
|
@@ -411,7 +489,7 @@ export class RunManager {
|
|
|
411
489
|
this.retryInFlight.delete(retryKey);
|
|
412
490
|
}
|
|
413
491
|
}
|
|
414
|
-
async retryStageInternal(runId, stageId, awaitRoot) {
|
|
492
|
+
async retryStageInternal(runId, stageId, awaitRoot, options) {
|
|
415
493
|
const retryKey = waitKey(runId, stageId);
|
|
416
494
|
if (this.retryInFlight.has(retryKey)) {
|
|
417
495
|
return {
|
|
@@ -421,9 +499,57 @@ export class RunManager {
|
|
|
421
499
|
};
|
|
422
500
|
}
|
|
423
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
|
+
}
|
|
424
511
|
try {
|
|
425
|
-
|
|
512
|
+
let orchestrationConflict = this.active.has(runId) &&
|
|
426
513
|
!this.attachedWaiting.has(waitKey(runId, stageId));
|
|
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
|
+
}
|
|
427
553
|
const result = await this.retryCoordinator.retryStage({
|
|
428
554
|
runId,
|
|
429
555
|
stageId,
|
|
@@ -438,6 +564,9 @@ export class RunManager {
|
|
|
438
564
|
orchestrationConflict,
|
|
439
565
|
tracking: this.retryTracking,
|
|
440
566
|
awaitRoot,
|
|
567
|
+
...(options?.beforeAttemptStart !== undefined
|
|
568
|
+
? { beforeAttemptStart: options.beforeAttemptStart }
|
|
569
|
+
: {}),
|
|
441
570
|
});
|
|
442
571
|
if (!result.ok || awaitRoot !== false) {
|
|
443
572
|
this.retryInFlight.delete(retryKey);
|
|
@@ -448,6 +577,140 @@ export class RunManager {
|
|
|
448
577
|
this.retryInFlight.delete(retryKey);
|
|
449
578
|
throw err;
|
|
450
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;
|
|
713
|
+
}
|
|
451
714
|
}
|
|
452
715
|
/**
|
|
453
716
|
* Deliver an operator answer for `(runId, stageId)` (KTD2 / KTD6).
|
|
@@ -538,6 +801,158 @@ export class RunManager {
|
|
|
538
801
|
}
|
|
539
802
|
}
|
|
540
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
|
+
}
|
|
541
956
|
async resumeViaSubprocess(runId, stageId, opaqueAnswer) {
|
|
542
957
|
const store = this.options.store;
|
|
543
958
|
const launcher = this.stageProcessLauncher;
|
|
@@ -756,7 +1171,7 @@ export class RunManager {
|
|
|
756
1171
|
this.active.delete(provisionalId);
|
|
757
1172
|
this.provisionalIds.delete(provisionalId);
|
|
758
1173
|
const generation = ++this.trackingGeneration;
|
|
759
|
-
this.active.set(runId, { ...entry, generation });
|
|
1174
|
+
this.active.set(runId, { ...entry, generation, done });
|
|
760
1175
|
if (entry.checkoutKey !== undefined) {
|
|
761
1176
|
this.checkoutLeases.set(entry.checkoutKey, runId);
|
|
762
1177
|
}
|
|
@@ -764,6 +1179,56 @@ export class RunManager {
|
|
|
764
1179
|
this.untrackIfGeneration(runId, generation);
|
|
765
1180
|
});
|
|
766
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
|
+
}
|
|
767
1232
|
/**
|
|
768
1233
|
* Acquire active tracking + checkout lease for a restart resume before
|
|
769
1234
|
* reconstruct starts. Over-max is allowed. Lease held by another run → fail closed.
|
|
@@ -807,7 +1272,7 @@ export class RunManager {
|
|
|
807
1272
|
if (entry === undefined)
|
|
808
1273
|
return;
|
|
809
1274
|
const generation = ++this.trackingGeneration;
|
|
810
|
-
this.active.set(runId, { ...entry, generation });
|
|
1275
|
+
this.active.set(runId, { ...entry, generation, done });
|
|
811
1276
|
void done.finally(() => {
|
|
812
1277
|
this.untrackIfGeneration(runId, generation);
|
|
813
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";
|
|
@@ -64,9 +64,13 @@ export type RetryStageRequest = {
|
|
|
64
64
|
orchestrationConflict: boolean;
|
|
65
65
|
tracking: RetryTrackingPort;
|
|
66
66
|
awaitRoot?: boolean;
|
|
67
|
+
beforeAttemptStart?: (attempt: number) => Promise<void>;
|
|
67
68
|
};
|
|
69
|
+
export declare function succeededStageRetryBlocker(stageSnap: StageSnapshot): string | undefined;
|
|
68
70
|
export declare function assertStageRetryEligible(detail: RunDetail, stageId: string, opts?: {
|
|
69
71
|
recoveryActive?: boolean;
|
|
72
|
+
persistedStatus?: RunStatus;
|
|
73
|
+
dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
|
|
70
74
|
}): {
|
|
71
75
|
ok: true;
|
|
72
76
|
} | {
|
|
@@ -74,6 +78,7 @@ export declare function assertStageRetryEligible(detail: RunDetail, stageId: str
|
|
|
74
78
|
reason: string;
|
|
75
79
|
status: number;
|
|
76
80
|
};
|
|
81
|
+
export declare function readRetryRootWaitTimeoutMs(): number;
|
|
77
82
|
export declare class RunRetryCoordinator {
|
|
78
83
|
private readonly sessions;
|
|
79
84
|
isActive(runId: string): boolean;
|