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.
Files changed (159) hide show
  1. package/README.md +24 -14
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/agentBackend.d.ts +32 -0
  4. package/dist/agent/agentBackend.js +38 -0
  5. package/dist/agent/claudeActivity.d.ts +15 -0
  6. package/dist/agent/claudeActivity.js +58 -0
  7. package/dist/agent/claudeAdapter.d.ts +5 -0
  8. package/dist/agent/claudeAdapter.js +301 -0
  9. package/dist/agent/claudeSession.d.ts +18 -0
  10. package/dist/agent/claudeSession.js +54 -0
  11. package/dist/agent/claudeTools.d.ts +89 -0
  12. package/dist/agent/claudeTools.js +135 -0
  13. package/dist/agent/fakeAgent.js +24 -4
  14. package/dist/agent/piAdapter.d.ts +6 -3
  15. package/dist/agent/piAdapter.js +129 -21
  16. package/dist/agent/port.d.ts +50 -1
  17. package/dist/agent/resolveAgentPort.d.ts +26 -0
  18. package/dist/agent/resolveAgentPort.js +39 -0
  19. package/dist/cli/runCommand.js +2 -2
  20. package/dist/cli/runsCommand.d.ts +22 -0
  21. package/dist/cli/runsCommand.js +773 -0
  22. package/dist/cli/validateOutput.d.ts +2 -2
  23. package/dist/cli/validateOutput.js +2 -2
  24. package/dist/cli.d.ts +1 -1
  25. package/dist/cli.js +43 -5
  26. package/dist/config/browseCatalog.js +1 -1
  27. package/dist/config/createPipeline.d.ts +2 -1
  28. package/dist/config/createPipeline.js +34 -9
  29. package/dist/config/createStage.js +14 -9
  30. package/dist/config/loadPipeline.js +10 -1
  31. package/dist/config/loadStage.js +138 -2
  32. package/dist/config/loadStageflowManifest.js +14 -1
  33. package/dist/config/mergePipelineIncludes.d.ts +1 -0
  34. package/dist/config/mergePipelineIncludes.js +14 -1
  35. package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
  36. package/dist/config/normalizePipelineStageEntry.js +49 -6
  37. package/dist/config/parseCompletionContract.d.ts +8 -0
  38. package/dist/config/parseCompletionContract.js +218 -0
  39. package/dist/config/parsePreEmitChecks.d.ts +3 -0
  40. package/dist/config/parsePreEmitChecks.js +65 -0
  41. package/dist/config/pipelineNeeds.d.ts +19 -0
  42. package/dist/config/pipelineNeeds.js +151 -0
  43. package/dist/config/pipelineStageKeys.js +16 -1
  44. package/dist/config/resolveForkEmitContext.d.ts +6 -2
  45. package/dist/config/resolveForkEmitContext.js +9 -2
  46. package/dist/config/resolvePipelineDag.d.ts +2 -1
  47. package/dist/config/resolvePipelineDag.js +182 -39
  48. package/dist/config/validateCatalog.d.ts +1 -1
  49. package/dist/config/validateCompletionContract.d.ts +4 -0
  50. package/dist/config/validateCompletionContract.js +52 -0
  51. package/dist/envelope/check.d.ts +1 -1
  52. package/dist/envelope/check.js +86 -13
  53. package/dist/envelope/cloneForks.d.ts +1 -1
  54. package/dist/envelope/cloneForks.js +24 -9
  55. package/dist/envelope/feedbackLoop.d.ts +4 -0
  56. package/dist/envelope/feedbackLoop.js +31 -0
  57. package/dist/envelope/payloadSchema.d.ts +7 -1
  58. package/dist/envelope/payloadSchema.js +170 -13
  59. package/dist/envelope/preEmitChecks.d.ts +29 -0
  60. package/dist/envelope/preEmitChecks.js +57 -0
  61. package/dist/hitl/qaTrail.d.ts +25 -0
  62. package/dist/hitl/qaTrail.js +51 -0
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.js +1 -1
  65. package/dist/mcp/catalogTools.js +4 -2
  66. package/dist/mcp/controlTools.js +72 -0
  67. package/dist/mcp/waitingGates.d.ts +2 -1
  68. package/dist/mcp/waitingGates.js +17 -1
  69. package/dist/package-meta.d.ts +1 -0
  70. package/dist/package-meta.js +1 -0
  71. package/dist/projection/projectRun.d.ts +4 -1
  72. package/dist/projection/projectRun.js +7 -0
  73. package/dist/prompt/feedbackLoopContext.d.ts +2 -0
  74. package/dist/prompt/feedbackLoopContext.js +17 -0
  75. package/dist/prompt/priorEnvelope.d.ts +2 -2
  76. package/dist/prompt/priorEnvelope.js +4 -1
  77. package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
  78. package/dist/runstore/pipelineDagSnapshot.js +51 -11
  79. package/dist/runstore/port.d.ts +185 -3
  80. package/dist/runstore/port.js +42 -3
  81. package/dist/runstore/runProjection.d.ts +7 -3
  82. package/dist/runstore/runProjection.js +30 -15
  83. package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
  84. package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
  85. package/dist/runstore/sqlite/schema.d.ts +1 -1
  86. package/dist/runstore/sqlite/schema.js +99 -0
  87. package/dist/runstore/stageInstanceId.d.ts +1 -1
  88. package/dist/runstore/stageInstanceId.js +3 -2
  89. package/dist/runstore/trackProjection.js +59 -27
  90. package/dist/runstore/verificationHistory.d.ts +19 -0
  91. package/dist/runstore/verificationHistory.js +30 -0
  92. package/dist/runtime/cloneSchedule.d.ts +40 -4
  93. package/dist/runtime/cloneSchedule.js +212 -28
  94. package/dist/runtime/completionCheckRunner.d.ts +137 -0
  95. package/dist/runtime/completionCheckRunner.js +365 -0
  96. package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
  97. package/dist/runtime/completionCheckoutBaseline.js +44 -0
  98. package/dist/runtime/envelopeRouting.d.ts +10 -3
  99. package/dist/runtime/envelopeRouting.js +146 -5
  100. package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
  101. package/dist/runtime/feedbackLoopCoordinator.js +446 -0
  102. package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
  103. package/dist/runtime/feedbackLoopDecision.js +207 -0
  104. package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
  105. package/dist/runtime/feedbackLoopRoute.js +13 -0
  106. package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
  107. package/dist/runtime/feedbackLoopSchedule.js +72 -0
  108. package/dist/runtime/forkGeneration.d.ts +72 -0
  109. package/dist/runtime/forkGeneration.js +295 -0
  110. package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
  111. package/dist/runtime/gitCheckoutCapability.js +97 -0
  112. package/dist/runtime/manualRecoveryState.d.ts +40 -0
  113. package/dist/runtime/manualRecoveryState.js +73 -0
  114. package/dist/runtime/pipelineScheduler.d.ts +13 -1
  115. package/dist/runtime/pipelineScheduler.js +328 -45
  116. package/dist/runtime/replayLifecycle.d.ts +107 -0
  117. package/dist/runtime/replayLifecycle.js +168 -0
  118. package/dist/runtime/runManager.d.ts +61 -1
  119. package/dist/runtime/runManager.js +501 -7
  120. package/dist/runtime/runRetryCoordinator.d.ts +8 -1
  121. package/dist/runtime/runRetryCoordinator.js +47 -10
  122. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
  123. package/dist/runtime/stageAttemptBootstrap.js +79 -1
  124. package/dist/runtime/stageProcessLauncher.d.ts +1 -1
  125. package/dist/runtime/stageRecovery.js +1 -1
  126. package/dist/runtime/stageRunner.d.ts +4 -1
  127. package/dist/runtime/stageRunner.js +29 -3
  128. package/dist/runtime/stageWorker.js +69 -2
  129. package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
  130. package/dist/runtime/verifiedStageExecution.d.ts +29 -0
  131. package/dist/runtime/verifiedStageExecution.js +119 -0
  132. package/dist/server/bootstrap.js +1 -0
  133. package/dist/server/http.js +114 -0
  134. package/dist/server/operatorResults.js +2 -0
  135. package/dist/tools/askOperator.d.ts +161 -5
  136. package/dist/tools/askOperator.js +58 -25
  137. package/dist/tools/emitStageEnvelope.d.ts +12 -12
  138. package/dist/tools/emitStageEnvelope.js +112 -24
  139. package/dist/types/completion.d.ts +43 -0
  140. package/dist/types/completion.js +1 -0
  141. package/dist/types/envelope.d.ts +18 -0
  142. package/dist/types/forkChoice.d.ts +9 -4
  143. package/dist/types/forkChoice.js +1 -1
  144. package/dist/types/pipeline.d.ts +39 -2
  145. package/dist/types/preEmitCheck.d.ts +18 -0
  146. package/dist/types/preEmitCheck.js +1 -0
  147. package/dist/types/stage.d.ts +13 -0
  148. package/dist/types/stageflowManifest.d.ts +2 -0
  149. package/dist/ui/assets/index-CXGz4nL7.js +118 -0
  150. package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
  151. package/dist/ui/index.html +2 -2
  152. package/package.json +2 -1
  153. package/skills/stageflow/references/control-surface.md +4 -2
  154. package/skills/stageflow-author/references/catalog-mapping.md +26 -2
  155. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  156. package/skills/stageflow-run/SKILL.md +56 -25
  157. package/skills/stageflow-run/references/mcp-call.md +2 -2
  158. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  159. 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 { RunRetryCoordinator, } from "./runRetryCoordinator.js";
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
- const orchestrationConflict = this.active.has(runId) &&
512
+ let orchestrationConflict = this.active.has(runId) &&
403
513
  !this.attachedWaiting.has(waitKey(runId, stageId));
404
- return await this.retryCoordinator.retryStage({
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
- finally {
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 { RunDetail, RunStatus, RunStore } from "../runstore/port.js";
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;