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