stageflow 0.25.0 → 0.27.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 +10 -3
- package/dist/a2a/contracts.d.ts +1 -0
- package/dist/a2a/contracts.js +1 -0
- package/dist/a2a/registry.d.ts +2 -0
- package/dist/a2a/registry.js +20 -1
- package/dist/a2a/server.d.ts +7 -0
- package/dist/a2a/server.js +5 -2
- package/dist/a2a/service.d.ts +1 -1
- package/dist/a2a/service.js +57 -8
- package/dist/a2a/store.d.ts +8 -5
- package/dist/a2a/store.js +24 -67
- package/dist/agent/activity.d.ts +3 -0
- package/dist/agent/bootProviderConfig.d.ts +21 -0
- package/dist/agent/bootProviderConfig.js +74 -0
- package/dist/agent/piAdapter.d.ts +9 -1
- package/dist/agent/piAdapter.js +58 -3
- package/dist/agent/piIsolatedMcp.d.ts +2 -0
- package/dist/agent/piIsolatedMcp.js +10 -2
- package/dist/agent/port.d.ts +6 -0
- package/dist/agent/streamLogRedact.d.ts +1 -1
- package/dist/agent/streamLogRedact.js +4 -11
- package/dist/cli/artifactCommand.js +9 -16
- package/dist/cli/backupCommand.d.ts +9 -0
- package/dist/cli/backupCommand.js +111 -0
- package/dist/cli/debugRunCommand.d.ts +10 -0
- package/dist/cli/debugRunCommand.js +109 -0
- package/dist/cli/doctorCommand.d.ts +32 -0
- package/dist/cli/doctorCommand.js +453 -0
- package/dist/cli/envelopeCommand.js +9 -2
- package/dist/cli/exportAllCommand.d.ts +55 -0
- package/dist/cli/exportAllCommand.js +218 -0
- package/dist/cli/exportRunCommand.js +23 -21
- package/dist/cli/hostClient.d.ts +21 -3
- package/dist/cli/hostClient.js +93 -12
- package/dist/cli/initTemplates.d.ts +1 -1
- package/dist/cli/initTemplates.js +8 -1
- package/dist/cli/resolveSafeOutPath.d.ts +2 -0
- package/dist/cli/resolveSafeOutPath.js +40 -0
- package/dist/cli/restoreCommand.d.ts +10 -0
- package/dist/cli/restoreCommand.js +90 -0
- package/dist/cli/runCommand.d.ts +3 -2
- package/dist/cli/runCommand.js +93 -11
- package/dist/cli/runOutput.d.ts +4 -0
- package/dist/cli/runOutput.js +33 -7
- package/dist/cli/runStageCommand.d.ts +1 -0
- package/dist/cli/runStageCommand.js +44 -25
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +162 -8
- package/dist/cli.js +171 -30
- package/dist/config/catalogRelativePath.d.ts +62 -0
- package/dist/config/catalogRelativePath.js +118 -0
- package/dist/config/configOrigin.d.ts +27 -0
- package/dist/config/configOrigin.js +59 -0
- package/dist/config/hostConfig.d.ts +55 -0
- package/dist/config/hostConfig.js +333 -0
- package/dist/config/listSkills.d.ts +26 -0
- package/dist/config/listSkills.js +190 -3
- package/dist/config/loadOutcome.d.ts +1 -1
- package/dist/config/loadPipeline.js +23 -2
- package/dist/config/loadStage.js +18 -0
- package/dist/config/loadTask.d.ts +2 -0
- package/dist/config/loadTask.js +57 -18
- package/dist/config/materializeStageModels.js +5 -1
- package/dist/config/mergePipelineIncludes.d.ts +3 -0
- package/dist/config/mergePipelineIncludes.js +19 -0
- package/dist/config/migrateYaml.js +6 -11
- package/dist/config/multiProjectCatalog.d.ts +30 -0
- package/dist/config/multiProjectCatalog.js +142 -0
- package/dist/config/normalizePipelineStageEntry.js +20 -1
- package/dist/config/pipelineStageKeys.js +2 -0
- package/dist/config/resolveCatalogRoots.d.ts +27 -0
- package/dist/config/resolveCatalogRoots.js +76 -0
- package/dist/config/resolveModel.d.ts +7 -1
- package/dist/config/resolveModel.js +11 -1
- package/dist/config/resolveStageMcpServers.d.ts +9 -1
- package/dist/config/resolveStageMcpServers.js +36 -9
- package/dist/config/secretFromEnvOrFile.d.ts +9 -0
- package/dist/config/secretFromEnvOrFile.js +34 -0
- package/dist/config/seededCatalog.d.ts +11 -0
- package/dist/config/seededCatalog.js +29 -0
- package/dist/config/toolRequires.d.ts +24 -0
- package/dist/config/toolRequires.js +115 -0
- package/dist/config/validateCatalog.d.ts +5 -1
- package/dist/config/validateCatalog.js +140 -1
- package/dist/diagnostics/checks.d.ts +39 -0
- package/dist/diagnostics/checks.js +135 -0
- package/dist/errors/codes.d.ts +14 -0
- package/dist/errors/codes.js +59 -0
- package/dist/git/cache.d.ts +14 -0
- package/dist/git/cache.js +150 -0
- package/dist/git/credentials.d.ts +9 -0
- package/dist/git/credentials.js +88 -0
- package/dist/git/errors.d.ts +20 -0
- package/dist/git/errors.js +94 -0
- package/dist/git/exec.d.ts +16 -0
- package/dist/git/exec.js +135 -0
- package/dist/git/operations.d.ts +29 -0
- package/dist/git/operations.js +153 -0
- package/dist/git/version.d.ts +1 -0
- package/dist/git/version.js +10 -0
- package/dist/logging/audit.d.ts +16 -0
- package/dist/logging/audit.js +19 -0
- package/dist/logging/commonWords.d.ts +2 -0
- package/dist/logging/commonWords.js +13 -0
- package/dist/logging/logger.d.ts +49 -0
- package/dist/logging/logger.js +193 -0
- package/dist/logging/namedSecrets.d.ts +6 -0
- package/dist/logging/namedSecrets.js +58 -0
- package/dist/logging/redact.d.ts +13 -0
- package/dist/logging/redact.js +88 -0
- package/dist/mcp/catalogTools.js +456 -80
- package/dist/mcp/checkoutTools.d.ts +52 -0
- package/dist/mcp/checkoutTools.js +332 -0
- package/dist/mcp/controlTools.js +179 -5
- package/dist/mcp/deps.d.ts +1 -0
- package/dist/mcp/getStartedTool.d.ts +3 -0
- package/dist/mcp/getStartedTool.js +62 -0
- package/dist/mcp/readArtifact.d.ts +11 -0
- package/dist/mcp/readArtifact.js +46 -30
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/net/proxy.d.ts +14 -0
- package/dist/net/proxy.js +78 -0
- package/dist/package-meta.d.ts +2 -1
- package/dist/package-meta.js +2 -1
- package/dist/preflight/bash.d.ts +8 -0
- package/dist/preflight/bash.js +34 -0
- package/dist/preflight/claudeRoot.d.ts +11 -0
- package/dist/preflight/claudeRoot.js +23 -0
- package/dist/preflight/mcpCommands.d.ts +8 -0
- package/dist/preflight/mcpCommands.js +36 -0
- package/dist/preflight/pipelinePreflight.d.ts +40 -0
- package/dist/preflight/pipelinePreflight.js +192 -0
- package/dist/preflight/tls.d.ts +6 -0
- package/dist/preflight/tls.js +18 -0
- package/dist/preflight/toolchain.d.ts +45 -0
- package/dist/preflight/toolchain.js +219 -0
- package/dist/project/findProjectRoot.js +3 -4
- package/dist/project/globalHome.d.ts +7 -0
- package/dist/project/globalHome.js +63 -2
- package/dist/project/resolveProjectContext.js +2 -2
- package/dist/project/resolveStageflowContext.js +2 -2
- package/dist/projection/projectRun.d.ts +2 -1
- package/dist/projection/projectRun.js +1 -0
- package/dist/runstore/assertTmpdir.d.ts +1 -0
- package/dist/runstore/assertTmpdir.js +21 -0
- package/dist/runstore/backup.d.ts +52 -0
- package/dist/runstore/backup.js +292 -0
- package/dist/runstore/createStore.d.ts +18 -1
- package/dist/runstore/createStore.js +64 -5
- package/dist/runstore/debugBundle.d.ts +60 -0
- package/dist/runstore/debugBundle.js +143 -0
- package/dist/runstore/diskUsage.d.ts +50 -0
- package/dist/runstore/diskUsage.js +214 -0
- package/dist/runstore/exportRunPayload.d.ts +8 -0
- package/dist/runstore/exportRunPayload.js +23 -0
- package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
- package/dist/runstore/normalizeCatalogPath.js +10 -0
- package/dist/runstore/paths.d.ts +5 -0
- package/dist/runstore/paths.js +145 -1
- package/dist/runstore/port.d.ts +111 -13
- package/dist/runstore/port.js +8 -1
- package/dist/runstore/restore.d.ts +81 -0
- package/dist/runstore/restore.js +355 -0
- package/dist/runstore/retention.d.ts +27 -0
- package/dist/runstore/retention.js +74 -0
- package/dist/runstore/runManifest.d.ts +146 -0
- package/dist/runstore/runManifest.js +288 -0
- package/dist/runstore/runProjection.d.ts +3 -1
- package/dist/runstore/runProjection.js +63 -1
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +28 -3
- package/dist/runstore/sqlite/SqliteRunStore.js +352 -219
- package/dist/runstore/sqlite/applyStorePragmas.d.ts +39 -0
- package/dist/runstore/sqlite/applyStorePragmas.js +145 -0
- package/dist/runstore/sqlite/migrations/001-baseline.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/001-baseline.js +10 -0
- package/dist/runstore/sqlite/migrations/002-repository-binding.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/002-repository-binding.js +29 -0
- package/dist/runstore/sqlite/migrations/003-run-lifecycle.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/003-run-lifecycle.js +28 -0
- package/dist/runstore/sqlite/migrations/004-auto-resume-count.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/004-auto-resume-count.js +19 -0
- package/dist/runstore/sqlite/migrations/005-config-origins.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/005-config-origins.js +19 -0
- package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.js +28 -0
- package/dist/runstore/sqlite/migrations/007-projects-registry.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/007-projects-registry.js +31 -0
- package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
- package/dist/runstore/sqlite/migrations/index.js +72 -0
- package/dist/runstore/sqlite/schema.d.ts +20 -1
- package/dist/runstore/sqlite/schema.js +277 -1
- package/dist/runstore/sqlite/storeOpenError.d.ts +6 -0
- package/dist/runstore/sqlite/storeOpenError.js +11 -0
- package/dist/runstore/sqlite/storeSchemaError.d.ts +5 -0
- package/dist/runstore/sqlite/storeSchemaError.js +8 -0
- package/dist/runstore/stageExecution.js +3 -0
- package/dist/runstore/storeFilesystem.d.ts +32 -0
- package/dist/runstore/storeFilesystem.js +148 -0
- package/dist/runstore/trackProjection.js +2 -0
- package/dist/runstore/workspaceLayout.d.ts +13 -0
- package/dist/runstore/workspaceLayout.js +76 -0
- package/dist/runtime/completionCheckRunner.d.ts +3 -0
- package/dist/runtime/completionCheckRunner.js +45 -7
- package/dist/runtime/containerLimits.d.ts +16 -0
- package/dist/runtime/containerLimits.js +84 -0
- package/dist/runtime/credentialMaterialisation.d.ts +28 -0
- package/dist/runtime/credentialMaterialisation.js +128 -0
- package/dist/runtime/gitCheckoutCapability.js +7 -13
- package/dist/runtime/pipelineRunner.d.ts +36 -1
- package/dist/runtime/pipelineRunner.js +178 -17
- package/dist/runtime/pipelineScheduler.d.ts +15 -0
- package/dist/runtime/pipelineScheduler.js +135 -27
- package/dist/runtime/reloadRunCatalog.js +6 -1
- package/dist/runtime/repositoryMaterialize.d.ts +47 -0
- package/dist/runtime/repositoryMaterialize.js +235 -0
- package/dist/runtime/resumeTimedOut.d.ts +1 -1
- package/dist/runtime/resumeTimedOut.js +5 -2
- package/dist/runtime/runChangeBus.js +9 -0
- package/dist/runtime/runDeletion.d.ts +7 -0
- package/dist/runtime/runDeletion.js +37 -0
- package/dist/runtime/runManager.d.ts +158 -4
- package/dist/runtime/runManager.js +1596 -79
- package/dist/runtime/runRetentionSweep.d.ts +31 -0
- package/dist/runtime/runRetentionSweep.js +328 -0
- package/dist/runtime/runRetryCoordinator.d.ts +2 -0
- package/dist/runtime/runRetryCoordinator.js +19 -2
- package/dist/runtime/runSkills.d.ts +40 -0
- package/dist/runtime/runSkills.js +173 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
- package/dist/runtime/stageAttemptBootstrap.js +195 -24
- package/dist/runtime/stageCacheEnv.d.ts +6 -0
- package/dist/runtime/stageCacheEnv.js +34 -0
- package/dist/runtime/stageConcurrency.d.ts +2 -1
- package/dist/runtime/stageConcurrency.js +4 -2
- package/dist/runtime/stageEnvironment.d.ts +38 -0
- package/dist/runtime/stageEnvironment.js +193 -0
- package/dist/runtime/stageProcessLauncher.d.ts +18 -0
- package/dist/runtime/stageProcessLauncher.js +194 -38
- package/dist/runtime/stageRecovery.d.ts +3 -1
- package/dist/runtime/stageRecovery.js +19 -4
- package/dist/runtime/stageRoots.d.ts +38 -1
- package/dist/runtime/stageRoots.js +118 -0
- package/dist/runtime/stageRunner.d.ts +1 -0
- package/dist/runtime/stageRunner.js +3 -1
- package/dist/runtime/stageSecretDecl.d.ts +9 -0
- package/dist/runtime/stageSecretDecl.js +102 -0
- package/dist/runtime/stageSecrets.d.ts +40 -0
- package/dist/runtime/stageSecrets.js +181 -0
- package/dist/runtime/stageWorker.d.ts +3 -2
- package/dist/runtime/stageWorker.js +58 -10
- package/dist/runtime/stageWorkerProtocol.d.ts +5 -0
- package/dist/runtime/stageWorkerProtocol.js +8 -0
- package/dist/runtime/startPayload.d.ts +32 -0
- package/dist/runtime/startPayload.js +59 -0
- package/dist/runtime/taskInput.js +14 -5
- package/dist/runtime/verifiedStageExecution.d.ts +1 -0
- package/dist/runtime/verifiedStageExecution.js +2 -1
- package/dist/runtime/workspaceBinding.d.ts +20 -0
- package/dist/runtime/workspaceBinding.js +60 -0
- package/dist/server/allowedHosts.d.ts +19 -0
- package/dist/server/allowedHosts.js +179 -0
- package/dist/server/bootstrap.d.ts +36 -2
- package/dist/server/bootstrap.js +209 -5
- package/dist/server/controlToken.d.ts +47 -0
- package/dist/server/controlToken.js +241 -0
- package/dist/server/createHttpHost.d.ts +11 -2
- package/dist/server/createHttpHost.js +103 -10
- package/dist/server/ensureGlobalService.d.ts +5 -1
- package/dist/server/ensureGlobalService.js +35 -1
- package/dist/server/healthSurfaces.d.ts +6 -0
- package/dist/server/healthSurfaces.js +98 -0
- package/dist/server/http.d.ts +12 -1
- package/dist/server/http.js +1085 -485
- package/dist/server/listenHost.d.ts +5 -0
- package/dist/server/listenHost.js +56 -0
- package/dist/server/mcpHost.d.ts +10 -1
- package/dist/server/mcpHost.js +36 -6
- package/dist/server/operatorResults.d.ts +2 -2
- package/dist/server/operatorResults.js +40 -29
- package/dist/server/requestAuthContext.d.ts +12 -0
- package/dist/server/requestAuthContext.js +21 -0
- package/dist/server/shutdown.d.ts +69 -0
- package/dist/server/shutdown.js +219 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/dist/types/stage.d.ts +8 -0
- package/dist/types/task.d.ts +8 -0
- package/dist/ui/assets/index-B6hlSXXn.js +120 -0
- package/dist/ui/index.html +1 -1
- package/examples/README.md +98 -0
- package/examples/archify-on-pr/README.md +213 -0
- package/examples/archify-on-pr/archify-on-pr.pipeline.yaml +19 -0
- package/examples/archify-on-pr/archify-on-pr.task.yaml +19 -0
- package/examples/archify-on-pr/author-diagrams.yaml +106 -0
- package/examples/archify-on-pr/detect-changes.yaml +89 -0
- package/examples/ci-validate/README.md +35 -0
- package/examples/ci-validate/check.yaml +13 -0
- package/examples/ci-validate/ci-demo.pipeline.yaml +4 -0
- package/examples/ci-validate/validate.sh +5 -0
- package/examples/compliance-review-demo/README.md +125 -0
- package/examples/compliance-review-demo/compliance-review-demo.pipeline.yaml +29 -0
- package/examples/compliance-review-demo/connect-account.yaml +47 -0
- package/examples/compliance-review-demo/map-to-control.yaml +123 -0
- package/examples/compliance-review-demo/poll-bucket-config.yaml +103 -0
- package/examples/compliance-review-demo/publish-report.yaml +79 -0
- package/examples/compliance-review-demo/review-verdict.yaml +122 -0
- package/examples/compliance-review-demo/run-compliance-check.yaml +96 -0
- package/examples/compliance-review-demo/store-evidence.yaml +143 -0
- package/examples/conditional-fork/README.md +89 -0
- package/examples/conditional-fork/branch-a.yaml +13 -0
- package/examples/conditional-fork/branch-b.yaml +13 -0
- package/examples/conditional-fork/decide.yaml +35 -0
- package/examples/conditional-fork/fork-demo.pipeline.yaml +20 -0
- package/examples/conditional-fork/fork-demo.task.yaml +8 -0
- package/examples/context7-mcp/README.md +42 -0
- package/examples/context7-mcp/context7-mcp.pipeline.yaml +15 -0
- package/examples/context7-mcp/context7-mcp.task.yaml +4 -0
- package/examples/context7-mcp/fetch-docs.yaml +52 -0
- package/examples/context7-mcp/resolve-library.yaml +31 -0
- package/examples/context7-mcp/write-brief.yaml +52 -0
- package/examples/feature-loop/README.md +104 -0
- package/examples/feature-loop/address-feedback.yaml +148 -0
- package/examples/feature-loop/align.yaml +116 -0
- package/examples/feature-loop/decompose.yaml +86 -0
- package/examples/feature-loop/default-model-hierarchy.task.yaml +58 -0
- package/examples/feature-loop/feature-loop.pipeline.yaml +140 -0
- package/examples/feature-loop/feature-loop.task.yaml +25 -0
- package/examples/feature-loop/implement.yaml +128 -0
- package/examples/feature-loop/plan.yaml +111 -0
- package/examples/feature-loop/publish.yaml +167 -0
- package/examples/feature-loop/review.yaml +146 -0
- package/examples/feature-loop/verify.yaml +106 -0
- package/examples/feedback-loop/README.md +72 -0
- package/examples/feedback-loop/VERIFY.md +208 -0
- package/examples/feedback-loop/continue-only.task.yaml +4 -0
- package/examples/feedback-loop/exhaust-require-continue.task.yaml +6 -0
- package/examples/feedback-loop/exhaust-wait-human.task.yaml +11 -0
- package/examples/feedback-loop/feedback-loop-wait-human.pipeline.yaml +23 -0
- package/examples/feedback-loop/feedback-loop.pipeline.yaml +23 -0
- package/examples/feedback-loop/feedback-loop.task.yaml +4 -0
- package/examples/feedback-loop/implement.yaml +14 -0
- package/examples/feedback-loop/plan.yaml +14 -0
- package/examples/feedback-loop/review.yaml +15 -0
- package/examples/feedback-loop/send-back-once.task.yaml +6 -0
- package/examples/feedback-loop/submit.yaml +12 -0
- package/examples/generic-fan-in/README.md +151 -0
- package/examples/generic-fan-in/clarify.yaml +18 -0
- package/examples/generic-fan-in/generic-fan-in-accepted.pipeline.yaml +19 -0
- package/examples/generic-fan-in/generic-fan-in-accepted.task.yaml +8 -0
- package/examples/generic-fan-in/generic-fan-in.pipeline.yaml +18 -0
- package/examples/generic-fan-in/generic-fan-in.task.yaml +9 -0
- package/examples/generic-fan-in/research.yaml +45 -0
- package/examples/generic-fan-in/synthesize.yaml +66 -0
- package/examples/generic-fan-in/validation.yaml +45 -0
- package/examples/github-release/README.md +78 -0
- package/examples/github-release/draft-release-notes.yaml +64 -0
- package/examples/github-release/github-release.pipeline.yaml +11 -0
- package/examples/github-release/github-release.task.yaml +13 -0
- package/examples/github-release/publish-github-release.yaml +64 -0
- package/examples/hello-world/README.md +28 -0
- package/examples/hello-world/hello.pipeline.yaml +4 -0
- package/examples/hello-world/my-task.task.yaml +6 -0
- package/examples/hello-world/research.yaml +18 -0
- package/examples/mcp-hitl-tour/README.md +113 -0
- package/examples/mcp-hitl-tour/draft-angle.yaml +33 -0
- package/examples/mcp-hitl-tour/draft-short.yaml +32 -0
- package/examples/mcp-hitl-tour/emit-angles.yaml +45 -0
- package/examples/mcp-hitl-tour/gather.yaml +31 -0
- package/examples/mcp-hitl-tour/intake.yaml +62 -0
- package/examples/mcp-hitl-tour/mcp-hitl-tour-exhaust.task.yaml +13 -0
- package/examples/mcp-hitl-tour/mcp-hitl-tour.pipeline.yaml +54 -0
- package/examples/mcp-hitl-tour/mcp-hitl-tour.task.yaml +16 -0
- package/examples/mcp-hitl-tour/polish.yaml +33 -0
- package/examples/mcp-hitl-tour/review.yaml +17 -0
- package/examples/mcp-hitl-tour/ship.yaml +25 -0
- package/examples/oss-issue-contribution/README.md +218 -0
- package/examples/oss-issue-contribution/oss-address-review-feedback.yaml +117 -0
- package/examples/oss-issue-contribution/oss-approve-contribution.yaml +181 -0
- package/examples/oss-issue-contribution/oss-approve-plan.yaml +111 -0
- package/examples/oss-issue-contribution/oss-explain-fix.yaml +108 -0
- package/examples/oss-issue-contribution/oss-explain-issue.yaml +114 -0
- package/examples/oss-issue-contribution/oss-implement-source-fix.yaml +171 -0
- package/examples/oss-issue-contribution/oss-investigate-area.yaml +116 -0
- package/examples/oss-issue-contribution/oss-issue-contribution.pipeline.yaml +125 -0
- package/examples/oss-issue-contribution/oss-issue-intake.yaml +106 -0
- package/examples/oss-issue-contribution/oss-plan-investigation.yaml +112 -0
- package/examples/oss-issue-contribution/oss-plan-review.yaml +109 -0
- package/examples/oss-issue-contribution/oss-reproduce-issue.yaml +140 -0
- package/examples/oss-issue-contribution/oss-review-change.yaml +107 -0
- package/examples/oss-issue-contribution/oss-verify-fix.yaml +130 -0
- package/examples/oss-issue-contribution/oss-write-regression-test.yaml +135 -0
- package/examples/oss-issue-contribution/pi-7629.task.yaml +79 -0
- package/examples/oss-issue-contribution/pi-8919.task.yaml +75 -0
- package/examples/oss-issue-contribution/pydantic-ai-7971.task.yaml +53 -0
- package/examples/oss-quick-fix/README.md +65 -0
- package/examples/oss-quick-fix/implement-fix.yaml +84 -0
- package/examples/oss-quick-fix/oss-quick-fix.pipeline.yaml +35 -0
- package/examples/oss-quick-fix/oss-quick-fix.task.yaml +19 -0
- package/examples/oss-quick-fix/ship-fix.yaml +81 -0
- package/examples/oss-quick-fix/understand-issue.yaml +67 -0
- package/examples/oss-quick-fix/verify-fix.yaml +73 -0
- package/examples/plan-review/README.md +37 -0
- package/examples/plan-review/plan-review-followup.yaml +13 -0
- package/examples/plan-review/plan-review.pipeline.yaml +9 -0
- package/examples/plan-review/plan-review.task.yaml +9 -0
- package/examples/plan-review/plan-review.yaml +25 -0
- package/examples/playwright-mcp/README.md +39 -0
- package/examples/playwright-mcp/playwright-mcp.pipeline.yaml +5 -0
- package/examples/playwright-mcp/playwright-mcp.task.yaml +4 -0
- package/examples/playwright-mcp/screenshot.yaml +40 -0
- package/examples/retry-tour/README.md +75 -0
- package/examples/retry-tour/retry-tour.pipeline.yaml +122 -0
- package/examples/retry-tour/retry-tour.task.yaml +9 -0
- package/examples/route-if-tour/README.md +202 -0
- package/examples/route-if-tour/route-if-tour.pipeline.yaml +950 -0
- package/examples/route-if-tour/route-if-tour.task.yaml +4 -0
- package/examples/route-wiring-smoke-test/01-core-routing.pipeline.yaml +168 -0
- package/examples/route-wiring-smoke-test/02-on-gating.pipeline.yaml +44 -0
- package/examples/route-wiring-smoke-test/03-two-entry-points.pipeline.yaml +28 -0
- package/examples/route-wiring-smoke-test/04-entry-false-equivalent-to-omitted.pipeline.yaml +27 -0
- package/examples/route-wiring-smoke-test/05-fork-choice.pipeline.yaml +110 -0
- package/examples/route-wiring-smoke-test/06-fan-out-fan-in.pipeline.yaml +82 -0
- package/examples/route-wiring-smoke-test/07-loop-human-decision.pipeline.yaml +82 -0
- package/examples/route-wiring-smoke-test/08-skip-fallback-and-multi-on-gate.pipeline.yaml +77 -0
- package/examples/route-wiring-smoke-test/09-multi-loop-targets.pipeline.yaml +102 -0
- package/examples/route-wiring-smoke-test/10-uses-dialect-fork.pipeline.yaml +23 -0
- package/examples/route-wiring-smoke-test/11-sequential-io-handoff.pipeline.yaml +37 -0
- package/examples/route-wiring-smoke-test/12-complex-io-schemas.pipeline.yaml +138 -0
- package/examples/route-wiring-smoke-test/13-ref-io-handoff.pipeline.yaml +84 -0
- package/examples/route-wiring-smoke-test/14-if-eq-gating.pipeline.yaml +50 -0
- package/examples/route-wiring-smoke-test/15-if-all-gated.pipeline.yaml +37 -0
- package/examples/route-wiring-smoke-test/16-if-composition.pipeline.yaml +65 -0
- package/examples/route-wiring-smoke-test/17-if-join.pipeline.yaml +80 -0
- package/examples/route-wiring-smoke-test/README.md +377 -0
- package/examples/route-wiring-smoke-test/rejected/01-reject-legacy-needs.pipeline.yaml +26 -0
- package/examples/route-wiring-smoke-test/rejected/02-reject-legacy-fork.pipeline.yaml +25 -0
- package/examples/route-wiring-smoke-test/rejected/03-reject-legacy-feedback-loop.pipeline.yaml +30 -0
- package/examples/route-wiring-smoke-test/rejected/04-reject-missing-entry.pipeline.yaml +24 -0
- package/examples/route-wiring-smoke-test/rejected/05-reject-unreachable.pipeline.yaml +35 -0
- package/examples/route-wiring-smoke-test/rejected/06-reject-allow-none-without-route-select.pipeline.yaml +37 -0
- package/examples/route-wiring-smoke-test/rejected/07-reject-cycle.pipeline.yaml +27 -0
- package/examples/route-wiring-smoke-test/rejected/08-reject-loop-non-ancestor.pipeline.yaml +42 -0
- package/examples/route-wiring-smoke-test/rejected/09-reject-route-select-leaf.pipeline.yaml +26 -0
- package/examples/route-wiring-smoke-test/rejected/12-reject-loop-replay-unsafe.pipeline.yaml +55 -0
- package/examples/route-wiring-smoke-test/rejected/13-reject-multiple-loop-entries.pipeline.yaml +51 -0
- package/examples/route-wiring-smoke-test/rejected/14-reject-duplicate-stage-id.pipeline.yaml +35 -0
- package/examples/route-wiring-smoke-test/rejected/15-reject-unknown-route-target.pipeline.yaml +15 -0
- package/examples/route-wiring-smoke-test/rejected/16-reject-malformed-route-item.pipeline.yaml +15 -0
- package/examples/route-wiring-smoke-test/rejected/17-reject-io-incompatible.pipeline.yaml +33 -0
- package/examples/route-wiring-smoke-test/rejected/18-reject-nested-io.pipeline.yaml +51 -0
- package/examples/route-wiring-smoke-test/rejected/19-reject-array-item-io.pipeline.yaml +55 -0
- package/examples/route-wiring-smoke-test/rejected/20-reject-closed-io.pipeline.yaml +36 -0
- package/examples/route-wiring-smoke-test/rejected/21-reject-ref-io.pipeline.yaml +42 -0
- package/examples/route-wiring-smoke-test/rejected/22-reject-if-unknown-field.pipeline.yaml +33 -0
- package/examples/route-wiring-smoke-test/rejected/23-reject-if-optional-field.pipeline.yaml +32 -0
- package/examples/route-wiring-smoke-test/rejected/24-reject-if-empty-all.pipeline.yaml +31 -0
- package/examples/route-wiring-smoke-test/rejected/25-reject-if-gt-on-string.pipeline.yaml +33 -0
- package/examples/route-wiring-smoke-test/rejected/26-reject-if-optional-nested.pipeline.yaml +36 -0
- package/examples/route-wiring-smoke-test/rejected/27-reject-if-on-loop.pipeline.yaml +63 -0
- package/examples/route-wiring-smoke-test/rejected/29-reject-if-on-failed.pipeline.yaml +46 -0
- package/examples/route-wiring-smoke-test/smoke-test.task.yaml +4 -0
- package/examples/route-wiring-smoke-test/stages/branch-a.yaml +12 -0
- package/examples/route-wiring-smoke-test/stages/branch-b.yaml +12 -0
- package/examples/route-wiring-smoke-test/stages/decide.yaml +12 -0
- package/examples/route-wiring-smoke-test/stages/finish.yaml +12 -0
- package/examples/ship-feature/README.md +190 -0
- package/examples/ship-feature/feat-address-feedback.yaml +119 -0
- package/examples/ship-feature/feat-implement.yaml +180 -0
- package/examples/ship-feature/feat-plan.yaml +120 -0
- package/examples/ship-feature/feat-review.yaml +109 -0
- package/examples/ship-feature/feat-ship.yaml +203 -0
- package/examples/ship-feature/ship-feature.pipeline.yaml +44 -0
- package/examples/ship-feature/ship-feature.task.yaml +78 -0
- package/examples/stage-mcp/README.md +144 -0
- package/examples/stage-mcp/echo-mcp.mjs +20 -0
- package/examples/stage-mcp/stage-mcp-dead.pipeline.yaml +5 -0
- package/examples/stage-mcp/stage-mcp-dead.task.yaml +3 -0
- package/examples/stage-mcp/stage-mcp-missing-var.pipeline.yaml +5 -0
- package/examples/stage-mcp/stage-mcp-missing-var.task.yaml +3 -0
- package/examples/stage-mcp/stage-mcp.pipeline.yaml +5 -0
- package/examples/stage-mcp/stage-mcp.task.yaml +4 -0
- package/examples/stage-mcp/use-echo.yaml +20 -0
- package/examples/verify-tour/README.md +80 -0
- package/examples/verify-tour/verify-tour.pipeline.yaml +162 -0
- package/examples/verify-tour/verify-tour.task.yaml +11 -0
- package/package.json +5 -1
- package/skills/stageflow/SKILL.md +6 -6
- package/skills/stageflow/references/control-surface.md +11 -5
- package/skills/stageflow/scripts/detect-host.mjs +14 -1
- package/skills/stageflow/scripts/detect-host.test.mjs +29 -1
- package/skills/stageflow-author/SKILL.md +4 -3
- package/skills/stageflow-author/references/catalog-mapping.md +16 -0
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-run/SKILL.md +20 -12
- package/skills/stageflow-run/references/mcp-call.md +9 -1
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +16 -5
- package/skills/stageflow-run/scripts/mcp-call.mjs +46 -6
- package/skills/stageflow-run/scripts/mcp-call.test.mjs +64 -0
- package/skills/stageflow-session-capture/references/catalog-authoring.md +2 -0
- package/skills/stageflow-setup/SKILL.md +40 -8
- package/dist/ui/assets/index-Da5EHU6M.js +0 -119
|
@@ -1,52 +1,250 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
2
3
|
import { RunSubmissionExistsError } from "../runstore/submission.js";
|
|
3
|
-
import { readFile, realpath } from "node:fs/promises";
|
|
4
|
+
import { access, readFile, realpath } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { findProjectRoot } from "../project/findProjectRoot.js";
|
|
6
|
-
import {
|
|
7
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
8
|
+
import { normalizeCatalogPath, normalizeProjectRoot } from "../runstore/normalizeCatalogPath.js";
|
|
9
|
+
import { durableRootDiskBreakdown, readFilesystemSize, refreshRunDiskUsage, resolveMinFreeDiskFloor, } from "../runstore/diskUsage.js";
|
|
10
|
+
import { newRunId } from "../runstore/paths.js";
|
|
7
11
|
import { loadRunContext } from "./resumeReconstruct.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
12
|
+
import { assertResumableStage, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
|
|
13
|
+
import { loadTaskFromYamlOutcome } from "../config/loadTask.js";
|
|
14
|
+
import { buildValidationResult, loadPipelineValidated, } from "../config/validateCatalog.js";
|
|
10
15
|
import { deriveStatusFromStages, findUnhandledFailedStage, } from "../runstore/port.js";
|
|
11
|
-
import {
|
|
16
|
+
import { buildPipelineDagSnapshotFromLoaded } from "../runstore/pipelineDagSnapshot.js";
|
|
17
|
+
import { InlinePipelineTooLargeError, PipelinePreflightError, PipelineValidationError, QueuedRunActivationAborted, startPipeline, } from "./pipelineRunner.js";
|
|
18
|
+
import { INLINE_PIPELINE_TOO_LARGE, pipelineBodyBytes, pipelinePersistenceForStart, } from "./startPayload.js";
|
|
19
|
+
import { materializeRunSkills, validateSkillsPayload, } from "./runSkills.js";
|
|
12
20
|
import { hydrateScheduleFromStore, hydratedScheduleHasRunnableWork, resumeRun, runPipelineDag, } from "./pipelineScheduler.js";
|
|
13
21
|
import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
|
|
14
22
|
import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
|
|
15
23
|
import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
|
|
16
24
|
import { StageProcessLauncher } from "./stageProcessLauncher.js";
|
|
25
|
+
import { logger as rootLogger } from "../logging/logger.js";
|
|
26
|
+
import { registerNamedSecrets } from "../logging/namedSecrets.js";
|
|
27
|
+
import { shouldRegisterValue } from "../logging/redact.js";
|
|
28
|
+
import { bindingFromFields, buildInitialRunManifest, collectDeclaredSecretValues, finaliseStoredRunManifest, } from "../runstore/runManifest.js";
|
|
29
|
+
import { getRequestAuth } from "../server/requestAuthContext.js";
|
|
30
|
+
import { STAGE_ENV_PASSTHROUGH } from "./stageEnvironment.js";
|
|
31
|
+
import { proxyHealthFields } from "../net/proxy.js";
|
|
32
|
+
import { getContainerLimits } from "./containerLimits.js";
|
|
33
|
+
import { stageflowCacheRoot } from "./stageCacheEnv.js";
|
|
34
|
+
import { assertClaudeNotRoot, ClaudeRootError } from "../preflight/claudeRoot.js";
|
|
35
|
+
import { preflightFailureCode, runPipelinePreflight, } from "../preflight/pipelinePreflight.js";
|
|
36
|
+
import { asAgentBackendId } from "../agent/agentBackend.js";
|
|
17
37
|
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
|
|
18
38
|
import { StageHitlController, waitKey, } from "./stageHitl.js";
|
|
19
|
-
import { resolveAndValidateCheckout, } from "./stageRoots.js";
|
|
39
|
+
import { resolveAndValidateCheckout, resolveEffectiveGitIdentity, stageBindingEnvFromRun, } from "./stageRoots.js";
|
|
20
40
|
import { orchestrateAnswerResume } from "./answerResume.js";
|
|
21
41
|
import { reconstructAndContinue as resumeReconstructAndContinue } from "./resumeReconstruct.js";
|
|
22
|
-
import { resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
23
|
-
import { resolveStartTaskInput } from "./taskInput.js";
|
|
24
|
-
import {
|
|
42
|
+
import { attemptContext, resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
43
|
+
import { checkTaskEntryInput, resolveStartTaskInput } from "./taskInput.js";
|
|
44
|
+
import { markStageInterrupted, OPERATOR_CANCEL_REASON, syncRunStatusFromStages, } from "./stageRecovery.js";
|
|
45
|
+
import { deleteRunEverywhere } from "./runDeletion.js";
|
|
46
|
+
import { runRetentionSweep, } from "./runRetentionSweep.js";
|
|
25
47
|
import { blocksGenericRetry, manualRecoveryEligibility, readManualRecoveryState, } from "./manualRecoveryState.js";
|
|
48
|
+
import { resolveWorkspaceBinding } from "./workspaceBinding.js";
|
|
49
|
+
import { derivedBindingKindFromMeta, materializeWorkspaceBinding, StartLinkError, } from "./repositoryMaterialize.js";
|
|
50
|
+
export const PROJECT_ROOT_UNAVAILABLE_REASON = "project_root_unavailable";
|
|
51
|
+
export const INSUFFICIENT_DISK_CANCEL_REASON = "insufficient_disk";
|
|
26
52
|
function existingSubmissionResult(existing, request) {
|
|
27
53
|
if (existing.requestHash !== request.requestHash) {
|
|
28
54
|
return { ok: false, status: 409, reason: "Submission key was already used for different input" };
|
|
29
55
|
}
|
|
30
56
|
return { ok: true, runId: existing.runId, reused: true };
|
|
31
57
|
}
|
|
58
|
+
export const FORCE_DELETE_CANCEL_REASON = "delete_run: force";
|
|
32
59
|
const DEFAULT_MAX_CONCURRENT = 3;
|
|
33
|
-
const
|
|
60
|
+
const DEFAULT_MAX_QUEUED = 32;
|
|
61
|
+
const DEFAULT_MAX_AUTO_RESUMES = 3;
|
|
62
|
+
const STARTUP_RECONCILE_REASON = "orphaned_no_worker";
|
|
63
|
+
const AUTO_RESUME_CAPPED_REASON = "auto_resume_capped";
|
|
64
|
+
export const STAGEFLOW_AUTO_RESUME_INTERRUPTED = "STAGEFLOW_AUTO_RESUME_INTERRUPTED";
|
|
65
|
+
export const STAGEFLOW_MAX_AUTO_RESUMES = "STAGEFLOW_MAX_AUTO_RESUMES";
|
|
34
66
|
const OPERATOR_ABANDON_REASON = "process_interrupted: operator abandoned stage";
|
|
67
|
+
const log = rootLogger.child({ component: "runtime" });
|
|
35
68
|
function parseMaxConcurrent(raw) {
|
|
36
69
|
if (raw === undefined || raw.trim() === "")
|
|
37
70
|
return DEFAULT_MAX_CONCURRENT;
|
|
38
71
|
const n = Number.parseInt(raw, 10);
|
|
39
|
-
if (!Number.isFinite(n) || n < 1)
|
|
40
|
-
|
|
72
|
+
if (!Number.isFinite(n) || n < 1 || String(n) !== raw.trim()) {
|
|
73
|
+
throw new Error(`Invalid value for STAGEFLOW_MAX_CONCURRENT_RUNS: ${JSON.stringify(raw)}`);
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
function parseMaxQueued(raw) {
|
|
78
|
+
if (raw === undefined || raw.trim() === "")
|
|
79
|
+
return DEFAULT_MAX_QUEUED;
|
|
80
|
+
const n = Number.parseInt(raw, 10);
|
|
81
|
+
if (!Number.isFinite(n) || n < 0)
|
|
82
|
+
return DEFAULT_MAX_QUEUED;
|
|
83
|
+
return n;
|
|
84
|
+
}
|
|
85
|
+
export function isAutoResumeInterruptedEnabled(env = process.env) {
|
|
86
|
+
const raw = env[STAGEFLOW_AUTO_RESUME_INTERRUPTED];
|
|
87
|
+
if (raw === undefined || raw.trim() === "")
|
|
88
|
+
return false;
|
|
89
|
+
const normalized = raw.trim().toLowerCase();
|
|
90
|
+
return normalized !== "0" && normalized !== "false";
|
|
91
|
+
}
|
|
92
|
+
export function parseMaxAutoResumes(env = process.env) {
|
|
93
|
+
const raw = env[STAGEFLOW_MAX_AUTO_RESUMES];
|
|
94
|
+
if (raw === undefined || raw.trim() === "")
|
|
95
|
+
return DEFAULT_MAX_AUTO_RESUMES;
|
|
96
|
+
const n = Number.parseInt(raw, 10);
|
|
97
|
+
if (!Number.isFinite(n) || n < 0)
|
|
98
|
+
return DEFAULT_MAX_AUTO_RESUMES;
|
|
41
99
|
return n;
|
|
42
100
|
}
|
|
101
|
+
/** Private FIFO-per-project_root + round-robin dequeue (KTD3). */
|
|
102
|
+
class AdmissionQueue {
|
|
103
|
+
byRoot = new Map();
|
|
104
|
+
rrOrder = [];
|
|
105
|
+
rrIndex = 0;
|
|
106
|
+
get size() {
|
|
107
|
+
let n = 0;
|
|
108
|
+
for (const list of this.byRoot.values())
|
|
109
|
+
n += list.length;
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
clear() {
|
|
113
|
+
this.byRoot.clear();
|
|
114
|
+
this.rrOrder = [];
|
|
115
|
+
this.rrIndex = 0;
|
|
116
|
+
}
|
|
117
|
+
enqueue(projectRoot, entry) {
|
|
118
|
+
let list = this.byRoot.get(projectRoot);
|
|
119
|
+
if (list === undefined) {
|
|
120
|
+
list = [];
|
|
121
|
+
this.byRoot.set(projectRoot, list);
|
|
122
|
+
this.rrOrder.push(projectRoot);
|
|
123
|
+
}
|
|
124
|
+
list.push(entry);
|
|
125
|
+
return this.size;
|
|
126
|
+
}
|
|
127
|
+
requeueFront(projectRoot, entry) {
|
|
128
|
+
let list = this.byRoot.get(projectRoot);
|
|
129
|
+
if (list === undefined) {
|
|
130
|
+
list = [];
|
|
131
|
+
this.byRoot.set(projectRoot, list);
|
|
132
|
+
this.rrOrder.push(projectRoot);
|
|
133
|
+
}
|
|
134
|
+
list.unshift(entry);
|
|
135
|
+
}
|
|
136
|
+
remove(runId) {
|
|
137
|
+
for (const [root, list] of this.byRoot) {
|
|
138
|
+
const idx = list.findIndex((e) => e.runId === runId);
|
|
139
|
+
if (idx < 0)
|
|
140
|
+
continue;
|
|
141
|
+
list.splice(idx, 1);
|
|
142
|
+
if (list.length === 0) {
|
|
143
|
+
this.byRoot.delete(root);
|
|
144
|
+
const orderIdx = this.rrOrder.indexOf(root);
|
|
145
|
+
if (orderIdx >= 0) {
|
|
146
|
+
this.rrOrder.splice(orderIdx, 1);
|
|
147
|
+
if (this.rrOrder.length === 0) {
|
|
148
|
+
this.rrIndex = 0;
|
|
149
|
+
}
|
|
150
|
+
else if (orderIdx < this.rrIndex) {
|
|
151
|
+
this.rrIndex -= 1;
|
|
152
|
+
}
|
|
153
|
+
else if (this.rrIndex >= this.rrOrder.length) {
|
|
154
|
+
this.rrIndex = 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
positionOf(runId) {
|
|
163
|
+
let position = 0;
|
|
164
|
+
const rootCount = this.rrOrder.length;
|
|
165
|
+
if (rootCount === 0)
|
|
166
|
+
return undefined;
|
|
167
|
+
const heads = this.rrOrder.map((root) => ({
|
|
168
|
+
root,
|
|
169
|
+
list: this.byRoot.get(root) ?? [],
|
|
170
|
+
i: 0,
|
|
171
|
+
}));
|
|
172
|
+
let rr = this.rrIndex % rootCount;
|
|
173
|
+
let remaining = this.size;
|
|
174
|
+
while (remaining > 0) {
|
|
175
|
+
let advanced = false;
|
|
176
|
+
for (let step = 0; step < rootCount; step++) {
|
|
177
|
+
const slot = heads[(rr + step) % rootCount];
|
|
178
|
+
if (slot === undefined || slot.i >= slot.list.length)
|
|
179
|
+
continue;
|
|
180
|
+
const entry = slot.list[slot.i];
|
|
181
|
+
slot.i += 1;
|
|
182
|
+
remaining -= 1;
|
|
183
|
+
position += 1;
|
|
184
|
+
advanced = true;
|
|
185
|
+
if (entry?.runId === runId)
|
|
186
|
+
return position;
|
|
187
|
+
rr = (rr + step + 1) % rootCount;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
if (!advanced)
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
dequeueNext(skipRoots, skipCallers) {
|
|
196
|
+
if (this.rrOrder.length === 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
const start = this.rrIndex % this.rrOrder.length;
|
|
199
|
+
for (let step = 0; step < this.rrOrder.length; step++) {
|
|
200
|
+
const idx = (start + step) % this.rrOrder.length;
|
|
201
|
+
const root = this.rrOrder[idx];
|
|
202
|
+
if (root === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
if (skipRoots?.has(root))
|
|
205
|
+
continue;
|
|
206
|
+
const list = this.byRoot.get(root);
|
|
207
|
+
if (list === undefined || list.length === 0)
|
|
208
|
+
continue;
|
|
209
|
+
let pickIdx = 0;
|
|
210
|
+
if (skipCallers !== undefined && skipCallers.size > 0) {
|
|
211
|
+
pickIdx = list.findIndex((e) => {
|
|
212
|
+
const c = e.callerId;
|
|
213
|
+
if (c === undefined || c === null || c === "")
|
|
214
|
+
return true;
|
|
215
|
+
return !skipCallers.has(c.toLowerCase());
|
|
216
|
+
});
|
|
217
|
+
if (pickIdx < 0)
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const [entry] = list.splice(pickIdx, 1);
|
|
221
|
+
if (entry === undefined)
|
|
222
|
+
continue;
|
|
223
|
+
if (list.length === 0) {
|
|
224
|
+
this.byRoot.delete(root);
|
|
225
|
+
this.rrOrder.splice(idx, 1);
|
|
226
|
+
if (this.rrOrder.length === 0) {
|
|
227
|
+
this.rrIndex = 0;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
this.rrIndex = idx % this.rrOrder.length;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
this.rrIndex = (idx + 1) % this.rrOrder.length;
|
|
235
|
+
}
|
|
236
|
+
return { projectRoot: root, entry };
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
43
241
|
async function toCheckoutLeaseKey(absPath) {
|
|
44
242
|
try {
|
|
45
243
|
return await realpath(absPath);
|
|
46
244
|
}
|
|
47
245
|
catch {
|
|
48
246
|
const fallback = path.resolve(absPath);
|
|
49
|
-
|
|
247
|
+
log.error("checkout.realpath_failed", `invariant: checkout realpath failed for ${absPath}; using path.resolve fallback ${fallback}`, { abs_path: absPath, fallback });
|
|
50
248
|
return fallback;
|
|
51
249
|
}
|
|
52
250
|
}
|
|
@@ -54,17 +252,26 @@ export class RunManager {
|
|
|
54
252
|
options;
|
|
55
253
|
submissionsInFlight = new Map();
|
|
56
254
|
active = new Map();
|
|
255
|
+
schedulingHalts = new Map();
|
|
57
256
|
checkoutLeases = new Map();
|
|
58
257
|
provisionalIds = new Set();
|
|
258
|
+
admissionQueue = new AdmissionQueue();
|
|
259
|
+
pendingQueuedStarts = new Map();
|
|
260
|
+
queuedDone = new Map();
|
|
261
|
+
admissionDrainInFlight = false;
|
|
59
262
|
resumeInFlight = new Set();
|
|
60
263
|
retryInFlight = new Set();
|
|
61
264
|
retryStartOwner = new Map();
|
|
62
265
|
retryStartWaiters = new Map();
|
|
63
266
|
trackingGeneration = 0;
|
|
64
267
|
maxConcurrent;
|
|
268
|
+
maxQueued;
|
|
269
|
+
maxConcurrentPerProject;
|
|
270
|
+
callerQuotas;
|
|
65
271
|
maxActiveStagesPerRun;
|
|
66
272
|
executionMode;
|
|
67
273
|
stageProcessLauncher;
|
|
274
|
+
a2aStore;
|
|
68
275
|
hitl;
|
|
69
276
|
attachedWaiting = new Set();
|
|
70
277
|
retryCoordinator = new RunRetryCoordinator();
|
|
@@ -98,15 +305,24 @@ export class RunManager {
|
|
|
98
305
|
cwd;
|
|
99
306
|
projectRoot;
|
|
100
307
|
isGitProject;
|
|
308
|
+
acceptingWork = true;
|
|
101
309
|
constructor(options) {
|
|
102
310
|
this.options = options;
|
|
103
311
|
this.cwd = options.cwd ?? process.cwd();
|
|
104
312
|
this.projectRoot = options.projectRoot ?? this.cwd;
|
|
105
313
|
this.isGitProject = options.isGitProject ?? false;
|
|
314
|
+
this.a2aStore = options.a2aStore;
|
|
106
315
|
this.maxConcurrent =
|
|
107
316
|
options.maxConcurrent ??
|
|
108
317
|
readMaxConcurrentFromGlobal() ??
|
|
109
318
|
parseMaxConcurrent(process.env.STAGEFLOW_MAX_CONCURRENT_RUNS);
|
|
319
|
+
this.maxQueued =
|
|
320
|
+
options.maxQueued ?? parseMaxQueued(process.env.STAGEFLOW_MAX_QUEUED);
|
|
321
|
+
this.maxConcurrentPerProject = options.maxConcurrentPerProject;
|
|
322
|
+
this.callerQuotas = new Map(Object.entries(options.callerQuotas ?? {}).map(([id, n]) => [
|
|
323
|
+
id.toLowerCase(),
|
|
324
|
+
n,
|
|
325
|
+
]));
|
|
110
326
|
this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
|
|
111
327
|
this.executionMode = readStageExecutionMode(process.env, options.executionMode);
|
|
112
328
|
this.stageProcessLauncher =
|
|
@@ -133,6 +349,57 @@ export class RunManager {
|
|
|
133
349
|
getActiveCount() {
|
|
134
350
|
return this.active.size;
|
|
135
351
|
}
|
|
352
|
+
stopAcceptingWork() {
|
|
353
|
+
this.acceptingWork = false;
|
|
354
|
+
for (const halt of this.schedulingHalts.values()) {
|
|
355
|
+
halt.halted = true;
|
|
356
|
+
halt.hostShutdown = true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
isAcceptingWork() {
|
|
360
|
+
return this.acceptingWork;
|
|
361
|
+
}
|
|
362
|
+
async drainActiveStages(options) {
|
|
363
|
+
for (const halt of this.schedulingHalts.values()) {
|
|
364
|
+
halt.halted = true;
|
|
365
|
+
halt.hostShutdown = true;
|
|
366
|
+
}
|
|
367
|
+
const launcher = this.stageProcessLauncher;
|
|
368
|
+
if (launcher === undefined || launcher.activeCount() === 0) {
|
|
369
|
+
return { forced: false };
|
|
370
|
+
}
|
|
371
|
+
const previouslyActive = launcher.getActiveStageProcesses();
|
|
372
|
+
const seenRuns = new Set();
|
|
373
|
+
for (const { runId, stageId } of previouslyActive) {
|
|
374
|
+
await markStageInterrupted({
|
|
375
|
+
store: this.options.store,
|
|
376
|
+
runId,
|
|
377
|
+
stageId,
|
|
378
|
+
reason: "host_shutdown",
|
|
379
|
+
status: "interrupted",
|
|
380
|
+
});
|
|
381
|
+
seenRuns.add(runId);
|
|
382
|
+
}
|
|
383
|
+
for (const runId of seenRuns) {
|
|
384
|
+
await syncRunStatusFromStages(this.options.store, runId).catch(() => undefined);
|
|
385
|
+
}
|
|
386
|
+
launcher.signalAllActive("SIGTERM");
|
|
387
|
+
while (launcher.activeCount() > 0 &&
|
|
388
|
+
Date.now() < options.deadlineMs &&
|
|
389
|
+
!(options.isEscalated?.() ?? false)) {
|
|
390
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
391
|
+
}
|
|
392
|
+
const remaining = launcher.getActiveStageProcesses();
|
|
393
|
+
if (remaining.length === 0) {
|
|
394
|
+
return { forced: false };
|
|
395
|
+
}
|
|
396
|
+
launcher.signalAllActive("SIGKILL");
|
|
397
|
+
const killWaitUntil = Date.now() + 500;
|
|
398
|
+
while (launcher.activeCount() > 0 && Date.now() < killWaitUntil) {
|
|
399
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
400
|
+
}
|
|
401
|
+
return { forced: true };
|
|
402
|
+
}
|
|
136
403
|
getActiveRunIds() {
|
|
137
404
|
return [...this.active.keys()];
|
|
138
405
|
}
|
|
@@ -142,6 +409,7 @@ export class RunManager {
|
|
|
142
409
|
const maxConcurrent = this.getMaxConcurrent();
|
|
143
410
|
const maxActiveStageProcessesRaw = readMaxActiveStageProcesses(process.env);
|
|
144
411
|
const activeStageProcesses = this.stageProcessLauncher?.activeCount() ?? 0;
|
|
412
|
+
const limits = getContainerLimits();
|
|
145
413
|
return {
|
|
146
414
|
ok: true,
|
|
147
415
|
activeRunIds,
|
|
@@ -152,8 +420,83 @@ export class RunManager {
|
|
|
152
420
|
maxActiveStageProcesses: Number.isFinite(maxActiveStageProcessesRaw)
|
|
153
421
|
? maxActiveStageProcessesRaw
|
|
154
422
|
: null,
|
|
423
|
+
stage_env_passthrough: process.env[STAGE_ENV_PASSTHROUGH]?.trim() === "all",
|
|
424
|
+
proxy: proxyHealthFields(process.env),
|
|
425
|
+
container: {
|
|
426
|
+
max_old_space_size_mb: limits.maxOldSpaceSizeMb,
|
|
427
|
+
max_active_stage_processes: limits.maxActiveStageProcesses,
|
|
428
|
+
memory_limit_bytes: limits.memoryLimitBytes ?? null,
|
|
429
|
+
source: limits.source,
|
|
430
|
+
},
|
|
431
|
+
cache: { root: stageflowCacheRoot() },
|
|
155
432
|
};
|
|
156
433
|
}
|
|
434
|
+
getPerProjectCapacity() {
|
|
435
|
+
const byRoot = new Map();
|
|
436
|
+
for (const entry of this.active.values()) {
|
|
437
|
+
const root = entry.projectRoot ?? this.projectRoot;
|
|
438
|
+
byRoot.set(root, (byRoot.get(root) ?? 0) + 1);
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
maxConcurrent: this.maxConcurrentPerProject,
|
|
442
|
+
projects: [...byRoot.entries()].map(([project_root, activeCount]) => ({
|
|
443
|
+
project_root,
|
|
444
|
+
activeCount,
|
|
445
|
+
maxConcurrent: this.maxConcurrentPerProject,
|
|
446
|
+
})),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
countActiveForProject(projectRoot) {
|
|
450
|
+
const normalized = normalizeCatalogPath(projectRoot);
|
|
451
|
+
let n = 0;
|
|
452
|
+
for (const entry of this.active.values()) {
|
|
453
|
+
const root = normalizeCatalogPath(entry.projectRoot ?? this.projectRoot);
|
|
454
|
+
if (root === normalized)
|
|
455
|
+
n += 1;
|
|
456
|
+
}
|
|
457
|
+
return n;
|
|
458
|
+
}
|
|
459
|
+
countActiveForCaller(callerId) {
|
|
460
|
+
const normalized = callerId.toLowerCase();
|
|
461
|
+
let n = 0;
|
|
462
|
+
for (const entry of this.active.values()) {
|
|
463
|
+
if (entry.callerId?.toLowerCase() === normalized)
|
|
464
|
+
n += 1;
|
|
465
|
+
}
|
|
466
|
+
return n;
|
|
467
|
+
}
|
|
468
|
+
callerQuotaLimit(callerId) {
|
|
469
|
+
if (callerId === null || callerId === undefined || callerId === "") {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
return this.callerQuotas.get(callerId.toLowerCase());
|
|
473
|
+
}
|
|
474
|
+
/** Capacity plus on-demand durable-root disk breakdown (KTD16). */
|
|
475
|
+
async getHealthWithDisk() {
|
|
476
|
+
const base = this.getHealth();
|
|
477
|
+
try {
|
|
478
|
+
const disk = await durableRootDiskBreakdown(globalStageflowHome());
|
|
479
|
+
return { ...base, disk };
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return {
|
|
483
|
+
...base,
|
|
484
|
+
disk: {
|
|
485
|
+
runs_bytes: 0,
|
|
486
|
+
worktrees_bytes: 0,
|
|
487
|
+
repos_bytes: 0,
|
|
488
|
+
state_db_bytes: 0,
|
|
489
|
+
a2a_artifacts_bytes: 0,
|
|
490
|
+
cache_bytes: 0,
|
|
491
|
+
free_bytes: 0,
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** Refresh cached `disk_bytes` for one run after a terminal transition. */
|
|
497
|
+
async refreshRunDiskBytes(runId) {
|
|
498
|
+
await refreshRunDiskUsage(this.options.store, runId);
|
|
499
|
+
}
|
|
157
500
|
getHitlController() {
|
|
158
501
|
return this.hitl;
|
|
159
502
|
}
|
|
@@ -188,16 +531,27 @@ export class RunManager {
|
|
|
188
531
|
}
|
|
189
532
|
let checkoutKey;
|
|
190
533
|
let durableCheckoutRoot;
|
|
534
|
+
let attachCallerId = null;
|
|
535
|
+
let attachProjectRoot;
|
|
191
536
|
try {
|
|
192
537
|
const meta = await this.options.store.readRunMeta(runId);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
538
|
+
attachCallerId = meta.caller_id ?? null;
|
|
539
|
+
attachProjectRoot = meta.project_root
|
|
540
|
+
? normalizeCatalogPath(meta.project_root)
|
|
541
|
+
: undefined;
|
|
542
|
+
const kind = derivedBindingKindFromMeta(meta);
|
|
543
|
+
if (kind === "checkout") {
|
|
544
|
+
const checkoutRoot = meta.checkout_root;
|
|
545
|
+
if (checkoutRoot !== undefined && checkoutRoot !== "") {
|
|
546
|
+
durableCheckoutRoot = checkoutRoot;
|
|
547
|
+
checkoutKey = await toCheckoutLeaseKey(checkoutRoot);
|
|
548
|
+
}
|
|
197
549
|
}
|
|
198
550
|
}
|
|
199
551
|
catch (err) {
|
|
200
|
-
|
|
552
|
+
log
|
|
553
|
+
.child({ run_id: runId })
|
|
554
|
+
.error("attach.checkout_root_failed", `invariant: attach failed reading checkout_root for run ${runId}; tracking without checkout lease: ${err instanceof Error ? err.message : String(err)}`);
|
|
201
555
|
}
|
|
202
556
|
const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
|
|
203
557
|
if (conflictHolder !== undefined) {
|
|
@@ -211,6 +565,8 @@ export class RunManager {
|
|
|
211
565
|
this.active.set(runId, {
|
|
212
566
|
checkoutKey,
|
|
213
567
|
durableCheckoutRoot,
|
|
568
|
+
projectRoot: attachProjectRoot,
|
|
569
|
+
callerId: attachCallerId,
|
|
214
570
|
generation: ++this.trackingGeneration,
|
|
215
571
|
});
|
|
216
572
|
for (const stage of waitingStages) {
|
|
@@ -232,27 +588,74 @@ export class RunManager {
|
|
|
232
588
|
const reconciled = [];
|
|
233
589
|
const runs = await this.options.store.listRuns();
|
|
234
590
|
for (const summary of runs) {
|
|
591
|
+
let meta;
|
|
592
|
+
try {
|
|
593
|
+
meta = await this.options.store.readRunMeta(summary.run_id);
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
log
|
|
597
|
+
.child({ run_id: summary.run_id })
|
|
598
|
+
.error("reconcile.read_meta_failed", `reconcileOrphanedStages: failed to read run meta ${summary.run_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (meta.status === "queued")
|
|
602
|
+
continue;
|
|
235
603
|
let detail;
|
|
236
604
|
try {
|
|
237
605
|
detail = await this.options.store.readRun(summary.run_id);
|
|
238
606
|
}
|
|
239
607
|
catch (err) {
|
|
240
|
-
|
|
608
|
+
log
|
|
609
|
+
.child({ run_id: summary.run_id })
|
|
610
|
+
.error("reconcile.read_run_failed", `reconcileOrphanedStages: failed to read run ${summary.run_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
241
611
|
continue;
|
|
242
612
|
}
|
|
243
613
|
const runId = summary.run_id;
|
|
614
|
+
const cancelled = meta.status === "cancelled";
|
|
244
615
|
let runChanged = false;
|
|
245
616
|
for (const stage of detail.stages) {
|
|
617
|
+
if (cancelled) {
|
|
618
|
+
if (stage.status !== "pending" &&
|
|
619
|
+
stage.status !== "running" &&
|
|
620
|
+
stage.status !== "waiting_for_input") {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
await markStageInterrupted({
|
|
625
|
+
store: this.options.store,
|
|
626
|
+
runId,
|
|
627
|
+
stageId: stage.stage_id,
|
|
628
|
+
reason: OPERATOR_CANCEL_REASON,
|
|
629
|
+
status: "failed",
|
|
630
|
+
});
|
|
631
|
+
reconciled.push({
|
|
632
|
+
runId,
|
|
633
|
+
stageId: stage.stage_id,
|
|
634
|
+
reason: OPERATOR_CANCEL_REASON,
|
|
635
|
+
});
|
|
636
|
+
runChanged = true;
|
|
637
|
+
log
|
|
638
|
+
.child({ run_id: runId, stage_id: stage.stage_id })
|
|
639
|
+
.error("reconcile.orphaned_stage", `reconcileOrphanedStages: failed orphaned stage ${runId}/${stage.stage_id}: ${OPERATOR_CANCEL_REASON}`, { reason: OPERATOR_CANCEL_REASON });
|
|
640
|
+
}
|
|
641
|
+
catch (err) {
|
|
642
|
+
log
|
|
643
|
+
.child({ run_id: runId, stage_id: stage.stage_id })
|
|
644
|
+
.error("reconcile.stage_failed", `reconcileOrphanedStages: failed to reconcile ${runId}/${stage.stage_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
645
|
+
}
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
246
648
|
if (stage.status !== "running")
|
|
247
649
|
continue;
|
|
248
650
|
if (this.hasActiveWorker(runId, stage.stage_id))
|
|
249
651
|
continue;
|
|
250
652
|
try {
|
|
251
|
-
await
|
|
653
|
+
await markStageInterrupted({
|
|
252
654
|
store: this.options.store,
|
|
253
655
|
runId,
|
|
254
656
|
stageId: stage.stage_id,
|
|
255
657
|
reason: STARTUP_RECONCILE_REASON,
|
|
658
|
+
status: "interrupted",
|
|
256
659
|
});
|
|
257
660
|
reconciled.push({
|
|
258
661
|
runId,
|
|
@@ -260,26 +663,106 @@ export class RunManager {
|
|
|
260
663
|
reason: STARTUP_RECONCILE_REASON,
|
|
261
664
|
});
|
|
262
665
|
runChanged = true;
|
|
263
|
-
|
|
666
|
+
log
|
|
667
|
+
.child({ run_id: runId, stage_id: stage.stage_id })
|
|
668
|
+
.error("reconcile.orphaned_stage", `reconcileOrphanedStages: interrupted orphaned stage ${runId}/${stage.stage_id}: ${STARTUP_RECONCILE_REASON}`, { reason: STARTUP_RECONCILE_REASON });
|
|
264
669
|
}
|
|
265
670
|
catch (err) {
|
|
266
|
-
|
|
671
|
+
log
|
|
672
|
+
.child({ run_id: runId, stage_id: stage.stage_id })
|
|
673
|
+
.error("reconcile.stage_failed", `reconcileOrphanedStages: failed to reconcile ${runId}/${stage.stage_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
267
674
|
}
|
|
268
675
|
}
|
|
269
|
-
if (runChanged) {
|
|
676
|
+
if (runChanged && !cancelled) {
|
|
270
677
|
try {
|
|
271
678
|
await syncRunStatusFromStages(this.options.store, runId);
|
|
272
679
|
}
|
|
273
680
|
catch (err) {
|
|
274
|
-
|
|
681
|
+
log
|
|
682
|
+
.child({ run_id: runId })
|
|
683
|
+
.error("reconcile.sync_status_failed", `reconcileOrphanedStages: failed to sync run status for ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
275
684
|
}
|
|
276
685
|
}
|
|
277
686
|
}
|
|
278
687
|
if (reconciled.length > 0) {
|
|
279
|
-
|
|
688
|
+
log.error("reconcile.complete", `reconcileOrphanedStages: reconciled ${reconciled.length} orphaned stage(s)`, { count: reconciled.length });
|
|
280
689
|
}
|
|
281
690
|
return { reconciled };
|
|
282
691
|
}
|
|
692
|
+
async autoResumeInterruptedStages() {
|
|
693
|
+
const resumed = [];
|
|
694
|
+
const capped = [];
|
|
695
|
+
const skipped = [];
|
|
696
|
+
if (!isAutoResumeInterruptedEnabled()) {
|
|
697
|
+
return { resumed, capped, skipped };
|
|
698
|
+
}
|
|
699
|
+
const maxAutoResumes = parseMaxAutoResumes();
|
|
700
|
+
const interrupted = await this.options.store.listInterruptedStageExecutions();
|
|
701
|
+
for (const execution of interrupted) {
|
|
702
|
+
const runId = execution.run_id;
|
|
703
|
+
const stageId = execution.stage_id;
|
|
704
|
+
const attempt = execution.attempt;
|
|
705
|
+
const latest = await this.options.store.getLatestStageExecution(runId, stageId);
|
|
706
|
+
if (latest === null || latest.attempt !== attempt) {
|
|
707
|
+
skipped.push({
|
|
708
|
+
runId,
|
|
709
|
+
stageId,
|
|
710
|
+
reason: "interrupted attempt is not the latest",
|
|
711
|
+
});
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (execution.auto_resume_count >= maxAutoResumes) {
|
|
715
|
+
try {
|
|
716
|
+
await markStageInterrupted({
|
|
717
|
+
store: this.options.store,
|
|
718
|
+
runId,
|
|
719
|
+
stageId,
|
|
720
|
+
reason: AUTO_RESUME_CAPPED_REASON,
|
|
721
|
+
status: "interrupted",
|
|
722
|
+
attemptCtx: attemptContext(attempt),
|
|
723
|
+
});
|
|
724
|
+
capped.push({ runId, stageId });
|
|
725
|
+
log
|
|
726
|
+
.child({ run_id: runId, stage_id: stageId, attempt })
|
|
727
|
+
.info("auto_resume.capped", `autoResumeInterruptedStages: capped ${runId}/${stageId} at ${execution.auto_resume_count}`, { auto_resume_count: execution.auto_resume_count });
|
|
728
|
+
}
|
|
729
|
+
catch (err) {
|
|
730
|
+
skipped.push({
|
|
731
|
+
runId,
|
|
732
|
+
stageId,
|
|
733
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
await this.options.store.updateStageExecution(runId, stageId, attempt, {
|
|
740
|
+
auto_resume_count: execution.auto_resume_count + 1,
|
|
741
|
+
});
|
|
742
|
+
const result = await this.resumeTimedOutStage(runId, stageId, {
|
|
743
|
+
source: "auto",
|
|
744
|
+
});
|
|
745
|
+
if (result.ok) {
|
|
746
|
+
resumed.push({ runId, stageId });
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
skipped.push({
|
|
750
|
+
runId,
|
|
751
|
+
stageId,
|
|
752
|
+
reason: result.reason ?? "auto resume failed",
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
catch (err) {
|
|
757
|
+
skipped.push({
|
|
758
|
+
runId,
|
|
759
|
+
stageId,
|
|
760
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return { resumed, capped, skipped };
|
|
765
|
+
}
|
|
283
766
|
async resumeStalledSchedules() {
|
|
284
767
|
const resumed = [];
|
|
285
768
|
const runs = await this.options.store.listRuns();
|
|
@@ -295,11 +778,37 @@ export class RunManager {
|
|
|
295
778
|
}
|
|
296
779
|
}
|
|
297
780
|
catch (err) {
|
|
298
|
-
|
|
781
|
+
log
|
|
782
|
+
.child({ run_id: runId })
|
|
783
|
+
.error("resume.stalled_failed", `resumeStalledSchedules: failed to resume ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
299
784
|
}
|
|
300
785
|
}
|
|
301
786
|
return resumed;
|
|
302
787
|
}
|
|
788
|
+
/** Rebuild in-memory admission queue from persisted `queued` rows (R27). */
|
|
789
|
+
async reenqueuePersistedQueuedRuns() {
|
|
790
|
+
const queued = await this.options.store.listRuns({ status: "queued" });
|
|
791
|
+
const ordered = [...queued].sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
792
|
+
this.admissionQueue.clear();
|
|
793
|
+
for (const row of ordered) {
|
|
794
|
+
const root = normalizeCatalogPath(row.project_root ?? this.projectRoot);
|
|
795
|
+
let callerId = null;
|
|
796
|
+
try {
|
|
797
|
+
const meta = await this.options.store.readRunMeta(row.run_id);
|
|
798
|
+
callerId = meta.caller_id ?? null;
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
/* leave null; drain stamps from meta on quota miss */
|
|
802
|
+
}
|
|
803
|
+
this.admissionQueue.enqueue(root, {
|
|
804
|
+
runId: row.run_id,
|
|
805
|
+
createdAt: row.created_at,
|
|
806
|
+
callerId,
|
|
807
|
+
});
|
|
808
|
+
this.ensureQueuedDone(row.run_id);
|
|
809
|
+
}
|
|
810
|
+
await this.drainAdmissionQueue();
|
|
811
|
+
}
|
|
303
812
|
async abandonStage(runId, stageId) {
|
|
304
813
|
let detail;
|
|
305
814
|
try {
|
|
@@ -338,11 +847,12 @@ export class RunManager {
|
|
|
338
847
|
await this.stageProcessLauncher.cancelRun(runId);
|
|
339
848
|
}
|
|
340
849
|
}
|
|
341
|
-
await
|
|
850
|
+
await markStageInterrupted({
|
|
342
851
|
store: this.options.store,
|
|
343
852
|
runId,
|
|
344
853
|
stageId,
|
|
345
854
|
reason: OPERATOR_ABANDON_REASON,
|
|
855
|
+
status: "failed",
|
|
346
856
|
});
|
|
347
857
|
if (this.retryCoordinator.isActive(runId) &&
|
|
348
858
|
this.retryCoordinator.getActiveRoots(runId)?.has(stageId)) {
|
|
@@ -356,6 +866,195 @@ export class RunManager {
|
|
|
356
866
|
}
|
|
357
867
|
return { ok: true, runId, stageId };
|
|
358
868
|
}
|
|
869
|
+
async cancelRun(runId, reason) {
|
|
870
|
+
let meta;
|
|
871
|
+
try {
|
|
872
|
+
meta = await this.options.store.readRunMeta(runId);
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
876
|
+
}
|
|
877
|
+
if (meta.status === "cancelled") {
|
|
878
|
+
return { ok: true, runId };
|
|
879
|
+
}
|
|
880
|
+
if (meta.status === "succeeded" || meta.status === "failed") {
|
|
881
|
+
return {
|
|
882
|
+
ok: false,
|
|
883
|
+
reason: `Run is ${meta.status} and cannot be cancelled`,
|
|
884
|
+
status: 409,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const trimmedReason = reason.trim();
|
|
888
|
+
if (trimmedReason.length === 0) {
|
|
889
|
+
return {
|
|
890
|
+
ok: false,
|
|
891
|
+
reason: "Cancel reason is required",
|
|
892
|
+
status: 400,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
await this.options.store.updateRunStatus(runId, "cancelled");
|
|
896
|
+
await this.options.store.setCancelReason(runId, trimmedReason);
|
|
897
|
+
await finaliseStoredRunManifest(this.options.store, runId).catch(() => undefined);
|
|
898
|
+
await this.refreshRunDiskBytes(runId).catch(() => undefined);
|
|
899
|
+
const halt = this.schedulingHalts.get(runId);
|
|
900
|
+
if (halt !== undefined) {
|
|
901
|
+
halt.halted = true;
|
|
902
|
+
}
|
|
903
|
+
this.admissionQueue.remove(runId);
|
|
904
|
+
this.pendingQueuedStarts.delete(runId);
|
|
905
|
+
this.resolveQueuedDone(runId, {
|
|
906
|
+
ok: false,
|
|
907
|
+
outcome: "cancelled",
|
|
908
|
+
runDir: this.options.store.getWorkspaceDir(runId),
|
|
909
|
+
runId,
|
|
910
|
+
reason: trimmedReason,
|
|
911
|
+
});
|
|
912
|
+
if (this.stageProcessLauncher !== undefined) {
|
|
913
|
+
await this.stageProcessLauncher.cancelRun(runId);
|
|
914
|
+
}
|
|
915
|
+
const detail = await this.options.store.readRun(runId);
|
|
916
|
+
for (const stage of detail.stages) {
|
|
917
|
+
if (stage.status !== "pending" &&
|
|
918
|
+
stage.status !== "running" &&
|
|
919
|
+
stage.status !== "waiting_for_input") {
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const wasWaiting = stage.status === "waiting_for_input";
|
|
923
|
+
await markStageInterrupted({
|
|
924
|
+
store: this.options.store,
|
|
925
|
+
runId,
|
|
926
|
+
stageId: stage.stage_id,
|
|
927
|
+
reason: OPERATOR_CANCEL_REASON,
|
|
928
|
+
status: "failed",
|
|
929
|
+
});
|
|
930
|
+
if (wasWaiting) {
|
|
931
|
+
this.hitl.clearLiveWait(runId, stage.stage_id);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (this.active.has(runId)) {
|
|
935
|
+
this.removeActiveEntry(runId, false);
|
|
936
|
+
}
|
|
937
|
+
return { ok: true, runId };
|
|
938
|
+
}
|
|
939
|
+
setA2aStore(store) {
|
|
940
|
+
this.a2aStore = store;
|
|
941
|
+
}
|
|
942
|
+
async deleteRun(runId, options = {
|
|
943
|
+
channel: "rest",
|
|
944
|
+
}) {
|
|
945
|
+
const force = options.force === true;
|
|
946
|
+
const channel = options.channel;
|
|
947
|
+
let meta;
|
|
948
|
+
try {
|
|
949
|
+
meta = await this.options.store.readRunMeta(runId);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
953
|
+
}
|
|
954
|
+
const activeStatus = meta.status === "created" ||
|
|
955
|
+
meta.status === "queued" ||
|
|
956
|
+
meta.status === "running";
|
|
957
|
+
if (activeStatus && !force) {
|
|
958
|
+
return {
|
|
959
|
+
ok: false,
|
|
960
|
+
reason: `Run is ${meta.status} and cannot be deleted without force`,
|
|
961
|
+
status: 409,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (activeStatus && force) {
|
|
965
|
+
const settle = this.active.get(runId)?.done;
|
|
966
|
+
const cancelled = await this.cancelRun(runId, FORCE_DELETE_CANCEL_REASON);
|
|
967
|
+
if (!cancelled.ok && cancelled.status !== 404) {
|
|
968
|
+
return cancelled;
|
|
969
|
+
}
|
|
970
|
+
if (settle !== undefined) {
|
|
971
|
+
try {
|
|
972
|
+
await this.waitForPromise(settle, 30_000);
|
|
973
|
+
}
|
|
974
|
+
catch (err) {
|
|
975
|
+
return {
|
|
976
|
+
ok: false,
|
|
977
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
978
|
+
status: 500,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const a2aStore = this.a2aStore;
|
|
984
|
+
if (a2aStore === undefined) {
|
|
985
|
+
return {
|
|
986
|
+
ok: false,
|
|
987
|
+
reason: "A2A store is not available for delete_run",
|
|
988
|
+
status: 500,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
await deleteRunEverywhere(this.options.store, a2aStore, runId);
|
|
993
|
+
}
|
|
994
|
+
catch (err) {
|
|
995
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
996
|
+
if (message.startsWith("Run not found:")) {
|
|
997
|
+
return { ok: false, reason: message, status: 404 };
|
|
998
|
+
}
|
|
999
|
+
return { ok: false, reason: message, status: 500 };
|
|
1000
|
+
}
|
|
1001
|
+
log.child({ run_id: runId }).info("delete_run", "run deleted", {
|
|
1002
|
+
force,
|
|
1003
|
+
channel,
|
|
1004
|
+
});
|
|
1005
|
+
return { ok: true, runId };
|
|
1006
|
+
}
|
|
1007
|
+
async gcRuns(options = {}) {
|
|
1008
|
+
const execute = options.execute === true;
|
|
1009
|
+
const channel = options.channel ?? "rest";
|
|
1010
|
+
let report;
|
|
1011
|
+
try {
|
|
1012
|
+
report = await runRetentionSweep(this.options.store, this.a2aStore, {
|
|
1013
|
+
execute,
|
|
1014
|
+
now: options.now,
|
|
1015
|
+
windows: options.windows,
|
|
1016
|
+
env: options.env,
|
|
1017
|
+
artifactMaxBytes: options.artifactMaxBytes,
|
|
1018
|
+
bareCacheTtlMs: options.bareCacheTtlMs,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
catch (err) {
|
|
1022
|
+
return {
|
|
1023
|
+
ok: false,
|
|
1024
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1025
|
+
status: 500,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
if (execute) {
|
|
1029
|
+
for (const runId of report.slimmed) {
|
|
1030
|
+
await this.refreshRunDiskBytes(runId).catch(() => undefined);
|
|
1031
|
+
}
|
|
1032
|
+
log.info("run_retention_sweep", "retention sweep executed", {
|
|
1033
|
+
channel,
|
|
1034
|
+
slimmed: report.slimmed,
|
|
1035
|
+
purged: report.purged,
|
|
1036
|
+
bareCachesEvicted: report.bareCachesEvicted,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
return { ok: true, ...report };
|
|
1040
|
+
}
|
|
1041
|
+
async waitForPromise(promise, timeoutMs) {
|
|
1042
|
+
let timer;
|
|
1043
|
+
try {
|
|
1044
|
+
await Promise.race([
|
|
1045
|
+
promise.then(() => undefined, () => undefined),
|
|
1046
|
+
new Promise((_, reject) => {
|
|
1047
|
+
timer = setTimeout(() => {
|
|
1048
|
+
reject(new Error(`Timed out waiting for run to settle after force cancel`));
|
|
1049
|
+
}, timeoutMs);
|
|
1050
|
+
}),
|
|
1051
|
+
]);
|
|
1052
|
+
}
|
|
1053
|
+
finally {
|
|
1054
|
+
if (timer !== undefined)
|
|
1055
|
+
clearTimeout(timer);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
359
1058
|
async startRunOnce(input, submission) {
|
|
360
1059
|
if (!submission.key.trim() || submission.key.length > 256 || !/^[a-f0-9]{64}$/.test(submission.requestHash)) {
|
|
361
1060
|
return { ok: false, status: 400, reason: "Submission requires a key and SHA-256 request hash" };
|
|
@@ -392,12 +1091,25 @@ export class RunManager {
|
|
|
392
1091
|
}
|
|
393
1092
|
}
|
|
394
1093
|
async startRun(input, submission) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1094
|
+
if (!this.acceptingWork) {
|
|
1095
|
+
return {
|
|
1096
|
+
ok: false,
|
|
1097
|
+
reason: "Host is shutting down",
|
|
1098
|
+
status: 503,
|
|
1099
|
+
code: "shutting_down",
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const hostCwd = this.options.cwd ?? process.cwd();
|
|
1103
|
+
const wireRoot = typeof input.projectRoot === "string" && input.projectRoot.trim().length > 0
|
|
1104
|
+
? normalizeProjectRoot(input.projectRoot)
|
|
1105
|
+
: undefined;
|
|
1106
|
+
// Wire root wins. Otherwise path pipelines may derive via findProjectRoot;
|
|
1107
|
+
// inline-only without projectRoot keeps Host projectRoot.
|
|
1108
|
+
const derivedProjectRoot = wireRoot ??
|
|
1109
|
+
(typeof input.pipeline === "string"
|
|
1110
|
+
? (findProjectRoot(path.dirname(path.resolve(hostCwd, input.pipeline))) ?? this.projectRoot)
|
|
1111
|
+
: this.projectRoot);
|
|
1112
|
+
const cwd = wireRoot ?? hostCwd;
|
|
401
1113
|
let resolved;
|
|
402
1114
|
try {
|
|
403
1115
|
resolved = resolveStartTaskInput(input, cwd);
|
|
@@ -432,24 +1144,57 @@ export class RunManager {
|
|
|
432
1144
|
gitSha: input.gitSha,
|
|
433
1145
|
ciPrUrl: input.ciPrUrl,
|
|
434
1146
|
ciJobUrl: input.ciJobUrl,
|
|
435
|
-
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission);
|
|
1147
|
+
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission, undefined, input.callerId, input.skills);
|
|
436
1148
|
}
|
|
437
|
-
async rerun(runId) {
|
|
1149
|
+
async rerun(runId, options) {
|
|
1150
|
+
if (!this.acceptingWork) {
|
|
1151
|
+
return {
|
|
1152
|
+
ok: false,
|
|
1153
|
+
reason: "Host is shutting down",
|
|
1154
|
+
status: 503,
|
|
1155
|
+
code: "shutting_down",
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
438
1158
|
const cwd = this.options.cwd ?? process.cwd();
|
|
439
1159
|
let pipeline;
|
|
440
1160
|
let taskYaml;
|
|
441
1161
|
let rerunCwd = cwd;
|
|
442
1162
|
let rerunProjectRoot;
|
|
1163
|
+
let skipGates;
|
|
1164
|
+
let ciIdentity;
|
|
1165
|
+
let pinned;
|
|
443
1166
|
try {
|
|
444
1167
|
const meta = await this.options.store.readRunMeta(runId);
|
|
445
|
-
if (
|
|
1168
|
+
if (meta.pipeline_source === "inline") {
|
|
1169
|
+
const body = await this.options.store.readPipelineBody(runId);
|
|
1170
|
+
if (body === null || body === "") {
|
|
1171
|
+
return {
|
|
1172
|
+
ok: false,
|
|
1173
|
+
reason: `Run ${runId} is missing pipeline_body; re-run of an inline pipeline requires the stored body.`,
|
|
1174
|
+
status: 400,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
try {
|
|
1178
|
+
pipeline = JSON.parse(body);
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
return {
|
|
1182
|
+
ok: false,
|
|
1183
|
+
reason: `Run ${runId} has invalid pipeline_body JSON`,
|
|
1184
|
+
status: 400,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
else if (meta.pipeline_path) {
|
|
1189
|
+
pipeline = normalizeCatalogPath(meta.pipeline_path);
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
446
1192
|
return {
|
|
447
1193
|
ok: false,
|
|
448
|
-
reason: `Run ${runId}
|
|
1194
|
+
reason: `Run ${runId} has neither pipeline_path nor pipeline_body; cannot re-run.`,
|
|
449
1195
|
status: 400,
|
|
450
1196
|
};
|
|
451
1197
|
}
|
|
452
|
-
pipeline = normalizeCatalogPath(meta.pipeline_path);
|
|
453
1198
|
rerunCwd = meta.project_root
|
|
454
1199
|
? normalizeCatalogPath(meta.project_root)
|
|
455
1200
|
: cwd;
|
|
@@ -457,16 +1202,44 @@ export class RunManager {
|
|
|
457
1202
|
? normalizeCatalogPath(meta.project_root)
|
|
458
1203
|
: undefined;
|
|
459
1204
|
taskYaml = await this.options.store.readTaskYaml(runId);
|
|
1205
|
+
skipGates = meta.skip_gates;
|
|
1206
|
+
ciIdentity = {
|
|
1207
|
+
...(meta.git_sha !== undefined ? { gitSha: meta.git_sha } : {}),
|
|
1208
|
+
...(meta.ci_pr_url !== undefined ? { ciPrUrl: meta.ci_pr_url } : {}),
|
|
1209
|
+
...(meta.ci_job_url !== undefined ? { ciJobUrl: meta.ci_job_url } : {}),
|
|
1210
|
+
};
|
|
1211
|
+
if (options?.pinned) {
|
|
1212
|
+
if (meta.resolved_sha === undefined ||
|
|
1213
|
+
meta.resolved_sha === "" ||
|
|
1214
|
+
meta.ref === undefined ||
|
|
1215
|
+
meta.ref === "") {
|
|
1216
|
+
return {
|
|
1217
|
+
ok: false,
|
|
1218
|
+
reason: `Run ${runId} has no resolved_sha/ref to pin`,
|
|
1219
|
+
status: 400,
|
|
1220
|
+
code: "pinned_sha_unavailable",
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
pinned = { ref: meta.ref, resolvedSha: meta.resolved_sha };
|
|
1224
|
+
}
|
|
460
1225
|
}
|
|
461
1226
|
catch {
|
|
462
1227
|
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
463
1228
|
}
|
|
464
|
-
return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, undefined, undefined,
|
|
1229
|
+
return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, skipGates, ciIdentity, rerunProjectRoot, undefined, undefined, pinned, options?.callerId);
|
|
465
1230
|
}
|
|
466
1231
|
async retryStage(runId, stageId) {
|
|
467
1232
|
return this.retryStageInternal(runId, stageId, true);
|
|
468
1233
|
}
|
|
469
|
-
async resumeTimedOutStage(runId, stageId) {
|
|
1234
|
+
async resumeTimedOutStage(runId, stageId, options) {
|
|
1235
|
+
if (!this.acceptingWork) {
|
|
1236
|
+
return {
|
|
1237
|
+
ok: false,
|
|
1238
|
+
reason: "Host is shutting down",
|
|
1239
|
+
status: 503,
|
|
1240
|
+
code: "shutting_down",
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
470
1243
|
const resumeKey = waitKey(runId, stageId);
|
|
471
1244
|
if (this.resumeInFlight.has(resumeKey) || this.retryInFlight.has(resumeKey)) {
|
|
472
1245
|
return {
|
|
@@ -489,13 +1262,16 @@ export class RunManager {
|
|
|
489
1262
|
this.resumeInFlight.delete(resumeKey);
|
|
490
1263
|
return { ok: false, reason: `Stage not found: ${stageId}`, status: 404 };
|
|
491
1264
|
}
|
|
492
|
-
const eligibility =
|
|
1265
|
+
const eligibility = assertResumableStage(stageSnap.status, stageSnap.events);
|
|
493
1266
|
if (!eligibility.ok) {
|
|
494
1267
|
this.resumeInFlight.delete(resumeKey);
|
|
495
1268
|
return eligibility;
|
|
496
1269
|
}
|
|
497
1270
|
const latest = await this.options.store.getLatestStageExecution(runId, stageId);
|
|
498
1271
|
const attemptIndex = latest?.attempt ?? 1;
|
|
1272
|
+
if ((options?.source ?? "explicit") === "explicit" && latest !== null) {
|
|
1273
|
+
await this.options.store.updateStageExecution(runId, stageId, attemptIndex, { auto_resume_count: 0 });
|
|
1274
|
+
}
|
|
499
1275
|
const wasActive = this.active.has(runId);
|
|
500
1276
|
const tracked = await this.ensureResumeTracked(runId);
|
|
501
1277
|
if (!tracked.ok) {
|
|
@@ -615,12 +1391,21 @@ export class RunManager {
|
|
|
615
1391
|
}
|
|
616
1392
|
}
|
|
617
1393
|
async retryStageInternal(runId, stageId, awaitRoot, options) {
|
|
1394
|
+
if (!this.acceptingWork) {
|
|
1395
|
+
return {
|
|
1396
|
+
ok: false,
|
|
1397
|
+
reason: "Host is shutting down",
|
|
1398
|
+
status: 503,
|
|
1399
|
+
code: "shutting_down",
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
618
1402
|
const retryKey = waitKey(runId, stageId);
|
|
619
1403
|
if (this.retryInFlight.has(retryKey)) {
|
|
620
1404
|
return {
|
|
621
1405
|
ok: false,
|
|
622
1406
|
reason: `Retry already in progress for run ${runId} stage ${stageId}`,
|
|
623
1407
|
status: 409,
|
|
1408
|
+
code: "retry_in_progress",
|
|
624
1409
|
};
|
|
625
1410
|
}
|
|
626
1411
|
this.retryInFlight.add(retryKey);
|
|
@@ -631,6 +1416,7 @@ export class RunManager {
|
|
|
631
1416
|
ok: false,
|
|
632
1417
|
reason: "Stage uses manual recovery; use the manual recovery action instead of retry",
|
|
633
1418
|
status: 409,
|
|
1419
|
+
code: "manual_recovery_required",
|
|
634
1420
|
};
|
|
635
1421
|
}
|
|
636
1422
|
try {
|
|
@@ -1059,6 +1845,7 @@ export class RunManager {
|
|
|
1059
1845
|
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
1060
1846
|
executionMode: this.executionMode,
|
|
1061
1847
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1848
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1062
1849
|
});
|
|
1063
1850
|
this.registerResumeUntrack(runId, done);
|
|
1064
1851
|
const rest = await done;
|
|
@@ -1103,6 +1890,13 @@ export class RunManager {
|
|
|
1103
1890
|
try {
|
|
1104
1891
|
const runMeta = await store.readRunMeta(runId);
|
|
1105
1892
|
const runProjectRoot = runMeta.project_root ?? this.projectRoot;
|
|
1893
|
+
const { meta, task, loaded } = await loadRunContext(store, runId, runProjectRoot);
|
|
1894
|
+
const stageBinding = stageBindingEnvFromRun({
|
|
1895
|
+
meta,
|
|
1896
|
+
task,
|
|
1897
|
+
runWorkspaceDir: workspaceDir,
|
|
1898
|
+
hostEnv: process.env,
|
|
1899
|
+
});
|
|
1106
1900
|
const launchResult = await launcher.launch({
|
|
1107
1901
|
runId,
|
|
1108
1902
|
stageId,
|
|
@@ -1111,6 +1905,8 @@ export class RunManager {
|
|
|
1111
1905
|
resumeAnswer: opaqueAnswer,
|
|
1112
1906
|
attempt,
|
|
1113
1907
|
sessionFilePath,
|
|
1908
|
+
env: stageBinding.env,
|
|
1909
|
+
bindingKind: stageBinding.kind,
|
|
1114
1910
|
...(this.options.operatorCatalog !== undefined
|
|
1115
1911
|
? { operatorCatalog: this.options.operatorCatalog }
|
|
1116
1912
|
: {}),
|
|
@@ -1133,7 +1929,6 @@ export class RunManager {
|
|
|
1133
1929
|
catch {
|
|
1134
1930
|
// downstream resume may read envelope from store
|
|
1135
1931
|
}
|
|
1136
|
-
const { meta, task, loaded } = await loadRunContext(store, runId, this.cwd);
|
|
1137
1932
|
const rest = await resumeRun({
|
|
1138
1933
|
prepared: {
|
|
1139
1934
|
task,
|
|
@@ -1152,6 +1947,7 @@ export class RunManager {
|
|
|
1152
1947
|
initialPrior,
|
|
1153
1948
|
executionMode: this.executionMode,
|
|
1154
1949
|
stageProcessLauncher: launcher,
|
|
1950
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1155
1951
|
});
|
|
1156
1952
|
if (rest.outcome === "failed") {
|
|
1157
1953
|
return { ok: false, reason: rest.reason };
|
|
@@ -1199,27 +1995,257 @@ export class RunManager {
|
|
|
1199
1995
|
this.attachedWaiting.delete(`${runId}\0${stageId}`);
|
|
1200
1996
|
}
|
|
1201
1997
|
}
|
|
1202
|
-
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission) {
|
|
1998
|
+
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission, pinned, callerId, skills) {
|
|
1999
|
+
const persistence = pipelinePersistenceForStart(pipeline);
|
|
2000
|
+
if (!persistence.ok) {
|
|
2001
|
+
return {
|
|
2002
|
+
ok: false,
|
|
2003
|
+
reason: `Inline pipeline body is ${persistence.bytes} bytes; max is ${persistence.maxBytes}`,
|
|
2004
|
+
status: 400,
|
|
2005
|
+
code: INLINE_PIPELINE_TOO_LARGE,
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
const skillsCheck = validateSkillsPayload(skills, {
|
|
2009
|
+
pipelineBytes: pipelineBodyBytes(persistence.fields),
|
|
2010
|
+
});
|
|
2011
|
+
if (!skillsCheck.ok) {
|
|
2012
|
+
return {
|
|
2013
|
+
ok: false,
|
|
2014
|
+
reason: skillsCheck.reason,
|
|
2015
|
+
status: 400,
|
|
2016
|
+
code: skillsCheck.code,
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
const validatedSkills = skillsCheck.skills;
|
|
2020
|
+
const hasSkills = Object.keys(validatedSkills).length > 0;
|
|
2021
|
+
const resolvedProjectRoot = normalizeCatalogPath(projectRoot ?? this.projectRoot);
|
|
2022
|
+
let task;
|
|
1203
2023
|
let checkoutKey;
|
|
2024
|
+
let pathCheckoutRoot;
|
|
2025
|
+
let binding;
|
|
2026
|
+
let pipelineId;
|
|
2027
|
+
let pipelineDag;
|
|
2028
|
+
let pipelinePath;
|
|
2029
|
+
let loadedPipeline;
|
|
2030
|
+
let toolchainChecks = [];
|
|
1204
2031
|
try {
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
2032
|
+
const loadedTask = loadTaskFromYamlOutcome(taskYaml, taskLabel);
|
|
2033
|
+
if (!loadedTask.ok) {
|
|
2034
|
+
const issue = loadedTask.issues[0];
|
|
2035
|
+
return {
|
|
2036
|
+
ok: false,
|
|
2037
|
+
reason: issue?.message ?? "Invalid task",
|
|
2038
|
+
status: 400,
|
|
2039
|
+
...(issue?.code !== undefined
|
|
2040
|
+
? { code: issue.code }
|
|
2041
|
+
: {}),
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
task = loadedTask.value;
|
|
2045
|
+
const bindingOutcome = resolveWorkspaceBinding(task, { checkoutOverride });
|
|
2046
|
+
if (!bindingOutcome.ok) {
|
|
2047
|
+
const issue = bindingOutcome.issues[0];
|
|
2048
|
+
return {
|
|
2049
|
+
ok: false,
|
|
2050
|
+
reason: issue?.message ?? "Invalid workspace binding",
|
|
2051
|
+
status: 400,
|
|
2052
|
+
...(issue?.code !== undefined
|
|
2053
|
+
? { code: issue.code }
|
|
2054
|
+
: {}),
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
binding = bindingOutcome.value;
|
|
2058
|
+
if (binding.kind === "checkout") {
|
|
2059
|
+
pathCheckoutRoot = await resolveAndValidateCheckout(task, checkoutOverride, cwd);
|
|
2060
|
+
checkoutKey =
|
|
2061
|
+
pathCheckoutRoot !== undefined
|
|
2062
|
+
? await toCheckoutLeaseKey(pathCheckoutRoot)
|
|
2063
|
+
: undefined;
|
|
2064
|
+
}
|
|
2065
|
+
const loadResult = await loadPipelineValidated(pipeline, {
|
|
2066
|
+
cwd,
|
|
2067
|
+
projectRoot: resolvedProjectRoot,
|
|
2068
|
+
validateStages: true,
|
|
2069
|
+
});
|
|
2070
|
+
if (!loadResult.ok) {
|
|
2071
|
+
throw new PipelineValidationError(buildValidationResult("pipeline", loadResult.findings, false));
|
|
2072
|
+
}
|
|
2073
|
+
loadedPipeline = loadResult.loaded;
|
|
2074
|
+
const pairing = checkTaskEntryInput(task, loadResult.loaded, {
|
|
2075
|
+
cwd,
|
|
2076
|
+
taskPath,
|
|
2077
|
+
});
|
|
2078
|
+
if (pairing.some((finding) => finding.severity === "error")) {
|
|
2079
|
+
throw new PipelineValidationError(buildValidationResult("pipeline", pairing, false));
|
|
2080
|
+
}
|
|
2081
|
+
pipelineId = loadResult.loaded.pipeline.id;
|
|
2082
|
+
pipelineDag = buildPipelineDagSnapshotFromLoaded(loadResult.loaded);
|
|
2083
|
+
pipelinePath =
|
|
2084
|
+
typeof pipeline === "string"
|
|
2085
|
+
? normalizeCatalogPath(loadResult.loaded.pipelinePath)
|
|
1210
2086
|
: undefined;
|
|
2087
|
+
try {
|
|
2088
|
+
const pipelineAgent = asAgentBackendId(loadResult.loaded.pipeline.agent);
|
|
2089
|
+
assertClaudeNotRoot({ backendId: pipelineAgent });
|
|
2090
|
+
for (const stage of loadResult.loaded.stages) {
|
|
2091
|
+
assertClaudeNotRoot({
|
|
2092
|
+
backendId: asAgentBackendId(stage.agent) ?? pipelineAgent,
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
catch (err) {
|
|
2097
|
+
if (err instanceof ClaudeRootError) {
|
|
2098
|
+
return { ok: false, reason: err.message, status: 400, code: err.code };
|
|
2099
|
+
}
|
|
2100
|
+
throw err;
|
|
2101
|
+
}
|
|
2102
|
+
const preflight = await runPipelinePreflight(loadResult.loaded, {
|
|
2103
|
+
projectRoot: resolvedProjectRoot,
|
|
2104
|
+
forStart: true,
|
|
2105
|
+
});
|
|
2106
|
+
if (!preflight.ok) {
|
|
2107
|
+
const code = preflightFailureCode(preflight, { forStart: true }) ??
|
|
2108
|
+
"missing_tool";
|
|
2109
|
+
return {
|
|
2110
|
+
ok: false,
|
|
2111
|
+
reason: `Pipeline preflight failed: ${code}`,
|
|
2112
|
+
status: 400,
|
|
2113
|
+
code,
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
toolchainChecks = preflight.toolchain.checks;
|
|
1211
2117
|
}
|
|
1212
2118
|
catch (err) {
|
|
2119
|
+
if (err instanceof PipelineValidationError)
|
|
2120
|
+
throw err;
|
|
2121
|
+
if (err instanceof PipelinePreflightError) {
|
|
2122
|
+
return {
|
|
2123
|
+
ok: false,
|
|
2124
|
+
reason: err.message,
|
|
2125
|
+
status: 400,
|
|
2126
|
+
code: err.code,
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
1213
2129
|
return {
|
|
1214
2130
|
ok: false,
|
|
1215
2131
|
reason: err instanceof Error ? err.message : String(err),
|
|
1216
2132
|
status: 400,
|
|
1217
2133
|
};
|
|
1218
2134
|
}
|
|
1219
|
-
const
|
|
1220
|
-
if (
|
|
1221
|
-
return
|
|
2135
|
+
const diskGate = await this.checkDiskFloorAdmission();
|
|
2136
|
+
if (diskGate !== undefined)
|
|
2137
|
+
return diskGate;
|
|
2138
|
+
const admitted = this.tryAdmitOrEnqueue(checkoutKey, resolvedProjectRoot, callerId);
|
|
2139
|
+
if (admitted.action === "reject")
|
|
2140
|
+
return admitted.failure;
|
|
2141
|
+
if (admitted.action === "enqueue") {
|
|
2142
|
+
const runId = newRunId();
|
|
2143
|
+
const gitIdentity = resolveEffectiveGitIdentity(process.env, task.git_identity);
|
|
2144
|
+
const createdAt = new Date().toISOString();
|
|
2145
|
+
const auth = getRequestAuth();
|
|
2146
|
+
const surface = auth?.surface ?? "cli";
|
|
2147
|
+
const secretNames = loadedPipeline.stages.flatMap((s) => (s.secrets ?? []).map((d) => d.name));
|
|
2148
|
+
const namedSecrets = collectDeclaredSecretValues(secretNames).filter((s) => shouldRegisterValue(s.value));
|
|
2149
|
+
if (namedSecrets.length > 0) {
|
|
2150
|
+
registerNamedSecrets(namedSecrets);
|
|
2151
|
+
}
|
|
2152
|
+
const runManifest = buildInitialRunManifest({
|
|
2153
|
+
runId,
|
|
2154
|
+
createdAt,
|
|
2155
|
+
callerId: callerId ?? null,
|
|
2156
|
+
surface,
|
|
2157
|
+
binding: bindingFromFields({}),
|
|
2158
|
+
pipelineSource: persistence.fields.pipelineSource,
|
|
2159
|
+
pipelinePath,
|
|
2160
|
+
pipelineBody: persistence.fields.pipelineBody ?? null,
|
|
2161
|
+
taskYaml,
|
|
2162
|
+
taskPath: taskPath
|
|
2163
|
+
? normalizeCatalogPath(path.resolve(cwd, taskPath))
|
|
2164
|
+
: undefined,
|
|
2165
|
+
skills: hasSkills ? validatedSkills : undefined,
|
|
2166
|
+
stages: loadedPipeline.stages,
|
|
2167
|
+
toolchain: toolchainChecks,
|
|
2168
|
+
namedSecrets,
|
|
2169
|
+
});
|
|
2170
|
+
const created = await this.options.store.createRun({
|
|
2171
|
+
submission,
|
|
2172
|
+
runId,
|
|
2173
|
+
pipelineId,
|
|
2174
|
+
taskYaml,
|
|
2175
|
+
taskId: task.id,
|
|
2176
|
+
pipelineDag,
|
|
2177
|
+
pipelinePath,
|
|
2178
|
+
taskPath: taskPath
|
|
2179
|
+
? normalizeCatalogPath(path.resolve(cwd, taskPath))
|
|
2180
|
+
: undefined,
|
|
2181
|
+
projectRoot: resolvedProjectRoot,
|
|
2182
|
+
gitSha: ciIdentity?.gitSha,
|
|
2183
|
+
ciPrUrl: ciIdentity?.ciPrUrl,
|
|
2184
|
+
ciJobUrl: ciIdentity?.ciJobUrl,
|
|
2185
|
+
gitAuthorName: gitIdentity.name,
|
|
2186
|
+
gitAuthorEmail: gitIdentity.email,
|
|
2187
|
+
status: "queued",
|
|
2188
|
+
pipelineSource: persistence.fields.pipelineSource,
|
|
2189
|
+
...(persistence.fields.pipelineBody !== undefined
|
|
2190
|
+
? { pipelineBody: persistence.fields.pipelineBody }
|
|
2191
|
+
: {}),
|
|
2192
|
+
...(callerId !== undefined && callerId !== null
|
|
2193
|
+
? { callerId }
|
|
2194
|
+
: {}),
|
|
2195
|
+
runManifest,
|
|
2196
|
+
skipGates,
|
|
2197
|
+
});
|
|
2198
|
+
if (hasSkills) {
|
|
2199
|
+
await materializeRunSkills(created.workspaceDir, validatedSkills);
|
|
2200
|
+
}
|
|
2201
|
+
const meta = await this.options.store.readRunMeta(created.runId);
|
|
2202
|
+
const queuePosition = this.admissionQueue.enqueue(resolvedProjectRoot, {
|
|
2203
|
+
runId: created.runId,
|
|
2204
|
+
createdAt: meta.created_at,
|
|
2205
|
+
callerId: callerId ?? meta.caller_id ?? null,
|
|
2206
|
+
});
|
|
2207
|
+
this.pendingQueuedStarts.set(created.runId, {
|
|
2208
|
+
taskYaml,
|
|
2209
|
+
pipeline,
|
|
2210
|
+
taskLabel,
|
|
2211
|
+
cwd,
|
|
2212
|
+
projectRoot: resolvedProjectRoot,
|
|
2213
|
+
checkoutOverride,
|
|
2214
|
+
skipGates,
|
|
2215
|
+
ciIdentity,
|
|
2216
|
+
taskPath,
|
|
2217
|
+
submission,
|
|
2218
|
+
pinned,
|
|
2219
|
+
pathCheckoutRoot,
|
|
2220
|
+
binding,
|
|
2221
|
+
checkoutKey,
|
|
2222
|
+
callerId,
|
|
2223
|
+
...(hasSkills ? { skills: validatedSkills } : {}),
|
|
2224
|
+
});
|
|
2225
|
+
const done = this.ensureQueuedDone(created.runId);
|
|
2226
|
+
return {
|
|
2227
|
+
ok: true,
|
|
2228
|
+
runId: created.runId,
|
|
2229
|
+
done,
|
|
2230
|
+
queued: true,
|
|
2231
|
+
queuePosition,
|
|
2232
|
+
...(admitted.reason === "caller_quota"
|
|
2233
|
+
? { queuedCode: "busy_caller_quota" }
|
|
2234
|
+
: {}),
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
const runId = newRunId();
|
|
2238
|
+
let rollback = async () => { };
|
|
1222
2239
|
try {
|
|
2240
|
+
const { materialized, rollback: linkRollback } = await materializeWorkspaceBinding({
|
|
2241
|
+
runId,
|
|
2242
|
+
task,
|
|
2243
|
+
binding,
|
|
2244
|
+
checkoutRoot: pathCheckoutRoot,
|
|
2245
|
+
pinned,
|
|
2246
|
+
});
|
|
2247
|
+
rollback = linkRollback;
|
|
2248
|
+
const schedulingHalt = this.ensureSchedulingHalt(materialized.runId);
|
|
1223
2249
|
const started = await startPipeline({
|
|
1224
2250
|
submission,
|
|
1225
2251
|
agent: this.options.agent,
|
|
@@ -1228,8 +2254,14 @@ export class RunManager {
|
|
|
1228
2254
|
taskPath,
|
|
1229
2255
|
pipeline,
|
|
1230
2256
|
cwd,
|
|
1231
|
-
projectRoot:
|
|
2257
|
+
projectRoot: resolvedProjectRoot,
|
|
1232
2258
|
checkoutOverride,
|
|
2259
|
+
runId: materialized.runId,
|
|
2260
|
+
checkoutRoot: materialized.checkoutRoot,
|
|
2261
|
+
repository: materialized.repository,
|
|
2262
|
+
ref: materialized.ref,
|
|
2263
|
+
resolvedSha: materialized.resolvedSha,
|
|
2264
|
+
runBranch: materialized.runBranch,
|
|
1233
2265
|
gitSha: ciIdentity?.gitSha,
|
|
1234
2266
|
ciPrUrl: ciIdentity?.ciPrUrl,
|
|
1235
2267
|
ciJobUrl: ciIdentity?.ciJobUrl,
|
|
@@ -1239,15 +2271,44 @@ export class RunManager {
|
|
|
1239
2271
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1240
2272
|
operatorCatalog: this.options.operatorCatalog,
|
|
1241
2273
|
skipGates,
|
|
2274
|
+
schedulingHalt,
|
|
2275
|
+
callerId,
|
|
2276
|
+
...(hasSkills ? { skills: validatedSkills } : {}),
|
|
1242
2277
|
});
|
|
1243
|
-
this.track(
|
|
2278
|
+
this.track(admitted.provisionalId, started.runId, started.done);
|
|
1244
2279
|
return { ok: true, runId: started.runId, done: started.done };
|
|
1245
2280
|
}
|
|
1246
2281
|
catch (err) {
|
|
1247
|
-
|
|
2282
|
+
await rollback().catch(() => undefined);
|
|
2283
|
+
this.clearReservation(admitted.provisionalId);
|
|
2284
|
+
if (err instanceof InlinePipelineTooLargeError) {
|
|
2285
|
+
return {
|
|
2286
|
+
ok: false,
|
|
2287
|
+
reason: err.message,
|
|
2288
|
+
status: 400,
|
|
2289
|
+
code: err.code,
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
if (err instanceof PipelinePreflightError) {
|
|
2293
|
+
return {
|
|
2294
|
+
ok: false,
|
|
2295
|
+
reason: err.message,
|
|
2296
|
+
status: 400,
|
|
2297
|
+
code: err.code,
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
1248
2300
|
if (err instanceof PipelineValidationError || err instanceof RunSubmissionExistsError) {
|
|
1249
2301
|
throw err;
|
|
1250
2302
|
}
|
|
2303
|
+
if (err instanceof StartLinkError) {
|
|
2304
|
+
return {
|
|
2305
|
+
ok: false,
|
|
2306
|
+
reason: err.message,
|
|
2307
|
+
status: err.status,
|
|
2308
|
+
code: err.code,
|
|
2309
|
+
...(err.stderr !== undefined ? { stderr: err.stderr } : {}),
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
1251
2312
|
return {
|
|
1252
2313
|
ok: false,
|
|
1253
2314
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -1257,24 +2318,87 @@ export class RunManager {
|
|
|
1257
2318
|
}
|
|
1258
2319
|
busyFailure(code, extras) {
|
|
1259
2320
|
const activeRunIds = this.getActiveRunIds();
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
2321
|
+
const scope = extras?.scope ?? (code === "busy_caller_quota" ? "caller" : "global");
|
|
2322
|
+
const activeCount = extras?.activeCount ?? this.active.size;
|
|
2323
|
+
const maxConcurrent = extras?.maxConcurrent ?? this.maxConcurrent;
|
|
2324
|
+
const reason = code === "busy_caller_quota"
|
|
2325
|
+
? `Caller quota full: ${activeCount}/${maxConcurrent} active runs for caller ${extras?.caller_id ?? "unknown"}`
|
|
2326
|
+
: code === "busy_capacity"
|
|
2327
|
+
? scope === "project"
|
|
2328
|
+
? `Project capacity full: ${activeCount}/${maxConcurrent} active runs for ${extras?.project_root ?? "project"}`
|
|
2329
|
+
: this.active.size >= this.maxConcurrent &&
|
|
2330
|
+
this.admissionQueue.size >= this.maxQueued &&
|
|
2331
|
+
this.maxQueued > 0
|
|
2332
|
+
? `Admission queue full: ${this.admissionQueue.size}/${this.maxQueued} queued runs`
|
|
2333
|
+
: `Capacity full: ${this.active.size}/${this.maxConcurrent} active runs`
|
|
2334
|
+
: `Checkout in use by run ${extras?.conflictingRunId ?? "unknown"}`;
|
|
1263
2335
|
return {
|
|
1264
2336
|
ok: false,
|
|
1265
2337
|
reason,
|
|
1266
2338
|
status: 409,
|
|
1267
2339
|
code,
|
|
1268
|
-
activeCount
|
|
1269
|
-
maxConcurrent
|
|
2340
|
+
activeCount,
|
|
2341
|
+
maxConcurrent,
|
|
1270
2342
|
activeRunIds,
|
|
1271
|
-
|
|
2343
|
+
scope,
|
|
2344
|
+
...(extras?.project_root !== undefined
|
|
2345
|
+
? { project_root: extras.project_root }
|
|
2346
|
+
: {}),
|
|
2347
|
+
...(extras?.caller_id !== undefined ? { caller_id: extras.caller_id } : {}),
|
|
2348
|
+
...(extras?.conflictingRunId !== undefined
|
|
2349
|
+
? { conflictingRunId: extras.conflictingRunId }
|
|
2350
|
+
: {}),
|
|
2351
|
+
...(extras?.conflictingCheckout !== undefined
|
|
2352
|
+
? { conflictingCheckout: extras.conflictingCheckout }
|
|
2353
|
+
: {}),
|
|
1272
2354
|
};
|
|
1273
2355
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
2356
|
+
insufficientDiskFailure(freeBytes, minFreeBytes) {
|
|
2357
|
+
return {
|
|
2358
|
+
ok: false,
|
|
2359
|
+
reason: `Insufficient free disk: ${freeBytes} bytes free, floor ${minFreeBytes} bytes`,
|
|
2360
|
+
status: 409,
|
|
2361
|
+
code: "insufficient_disk",
|
|
2362
|
+
freeBytes,
|
|
2363
|
+
minFreeBytes,
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
async checkDiskFloorAdmission() {
|
|
2367
|
+
try {
|
|
2368
|
+
const readFree = this.options.freeSpaceReader ?? readFilesystemSize;
|
|
2369
|
+
const size = await readFree(globalStageflowHome());
|
|
2370
|
+
const minFreeBytes = resolveMinFreeDiskFloor(process.env.STAGEFLOW_MIN_FREE_DISK_BYTES, size.totalBytes);
|
|
2371
|
+
if (size.freeBytes < minFreeBytes) {
|
|
2372
|
+
return this.insufficientDiskFailure(size.freeBytes, minFreeBytes);
|
|
2373
|
+
}
|
|
2374
|
+
return undefined;
|
|
2375
|
+
}
|
|
2376
|
+
catch (err) {
|
|
2377
|
+
// Default floor is always active via resolveMinFreeDiskFloor — fail closed.
|
|
2378
|
+
return {
|
|
2379
|
+
ok: false,
|
|
2380
|
+
reason: `Disk free-space check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2381
|
+
status: 503,
|
|
2382
|
+
code: "disk_check_failed",
|
|
2383
|
+
};
|
|
1277
2384
|
}
|
|
2385
|
+
}
|
|
2386
|
+
async isProjectRootAvailable(projectRoot) {
|
|
2387
|
+
const provider = this.options.knownWritableProjectRoots;
|
|
2388
|
+
if (provider !== undefined) {
|
|
2389
|
+
const roots = [...(await provider())].map((r) => normalizeCatalogPath(r));
|
|
2390
|
+
const normalized = normalizeCatalogPath(projectRoot);
|
|
2391
|
+
return roots.includes(normalized);
|
|
2392
|
+
}
|
|
2393
|
+
try {
|
|
2394
|
+
await access(projectRoot, constants.W_OK);
|
|
2395
|
+
return true;
|
|
2396
|
+
}
|
|
2397
|
+
catch {
|
|
2398
|
+
return false;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
tryAdmitOrEnqueue(checkoutKey, projectRoot = this.projectRoot, callerId) {
|
|
1278
2402
|
if (checkoutKey !== undefined) {
|
|
1279
2403
|
const holder = this.checkoutLeases.get(checkoutKey);
|
|
1280
2404
|
if (holder !== undefined) {
|
|
@@ -1282,7 +2406,7 @@ export class RunManager {
|
|
|
1282
2406
|
? undefined
|
|
1283
2407
|
: holder;
|
|
1284
2408
|
return {
|
|
1285
|
-
|
|
2409
|
+
action: "reject",
|
|
1286
2410
|
failure: this.busyFailure("busy_checkout", {
|
|
1287
2411
|
conflictingRunId,
|
|
1288
2412
|
conflictingCheckout: checkoutKey,
|
|
@@ -1290,16 +2414,378 @@ export class RunManager {
|
|
|
1290
2414
|
};
|
|
1291
2415
|
}
|
|
1292
2416
|
}
|
|
1293
|
-
const
|
|
1294
|
-
this.
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
2417
|
+
const normalizedRoot = normalizeCatalogPath(projectRoot);
|
|
2418
|
+
if (this.maxConcurrentPerProject !== undefined) {
|
|
2419
|
+
const projectActive = this.countActiveForProject(normalizedRoot);
|
|
2420
|
+
if (projectActive >= this.maxConcurrentPerProject) {
|
|
2421
|
+
return {
|
|
2422
|
+
action: "reject",
|
|
2423
|
+
failure: this.busyFailure("busy_capacity", {
|
|
2424
|
+
scope: "project",
|
|
2425
|
+
project_root: normalizedRoot,
|
|
2426
|
+
activeCount: projectActive,
|
|
2427
|
+
maxConcurrent: this.maxConcurrentPerProject,
|
|
2428
|
+
}),
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (this.active.size < this.maxConcurrent) {
|
|
2433
|
+
const quota = this.callerQuotaLimit(callerId);
|
|
2434
|
+
if (quota !== undefined &&
|
|
2435
|
+
callerId !== undefined &&
|
|
2436
|
+
callerId !== null &&
|
|
2437
|
+
this.countActiveForCaller(callerId) >= quota) {
|
|
2438
|
+
if (this.admissionQueue.size < this.maxQueued) {
|
|
2439
|
+
return { action: "enqueue", reason: "caller_quota" };
|
|
2440
|
+
}
|
|
2441
|
+
return {
|
|
2442
|
+
action: "reject",
|
|
2443
|
+
failure: this.busyFailure("busy_caller_quota", {
|
|
2444
|
+
scope: "caller",
|
|
2445
|
+
caller_id: callerId,
|
|
2446
|
+
activeCount: this.countActiveForCaller(callerId),
|
|
2447
|
+
maxConcurrent: quota,
|
|
2448
|
+
}),
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
const provisionalId = randomUUID();
|
|
2452
|
+
this.provisionalIds.add(provisionalId);
|
|
2453
|
+
this.active.set(provisionalId, {
|
|
2454
|
+
checkoutKey,
|
|
2455
|
+
projectRoot: normalizedRoot,
|
|
2456
|
+
callerId: callerId ?? null,
|
|
2457
|
+
generation: ++this.trackingGeneration,
|
|
2458
|
+
});
|
|
2459
|
+
if (checkoutKey !== undefined) {
|
|
2460
|
+
this.checkoutLeases.set(checkoutKey, provisionalId);
|
|
2461
|
+
}
|
|
2462
|
+
return { action: "reserve", provisionalId };
|
|
2463
|
+
}
|
|
2464
|
+
if (this.admissionQueue.size < this.maxQueued) {
|
|
2465
|
+
return { action: "enqueue", reason: "capacity" };
|
|
2466
|
+
}
|
|
2467
|
+
return {
|
|
2468
|
+
action: "reject",
|
|
2469
|
+
failure: this.busyFailure("busy_capacity", { scope: "global" }),
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
tryReserve(checkoutKey, projectRoot = this.projectRoot, callerId) {
|
|
2473
|
+
const admitted = this.tryAdmitOrEnqueue(checkoutKey, projectRoot, callerId);
|
|
2474
|
+
if (admitted.action === "reserve") {
|
|
2475
|
+
return { ok: true, provisionalId: admitted.provisionalId };
|
|
2476
|
+
}
|
|
2477
|
+
if (admitted.action === "enqueue") {
|
|
2478
|
+
if (admitted.reason === "caller_quota") {
|
|
2479
|
+
const quota = this.callerQuotaLimit(callerId) ?? 0;
|
|
2480
|
+
return {
|
|
2481
|
+
ok: false,
|
|
2482
|
+
failure: this.busyFailure("busy_caller_quota", {
|
|
2483
|
+
scope: "caller",
|
|
2484
|
+
caller_id: callerId ?? undefined,
|
|
2485
|
+
activeCount: callerId !== undefined && callerId !== null
|
|
2486
|
+
? this.countActiveForCaller(callerId)
|
|
2487
|
+
: 0,
|
|
2488
|
+
maxConcurrent: quota,
|
|
2489
|
+
}),
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
return { ok: false, failure: this.busyFailure("busy_capacity") };
|
|
2493
|
+
}
|
|
2494
|
+
return { ok: false, failure: admitted.failure };
|
|
2495
|
+
}
|
|
2496
|
+
ensureQueuedDone(runId) {
|
|
2497
|
+
const existing = this.queuedDone.get(runId);
|
|
2498
|
+
if (existing !== undefined)
|
|
2499
|
+
return existing.promise;
|
|
2500
|
+
let resolve;
|
|
2501
|
+
const promise = new Promise((res) => {
|
|
2502
|
+
resolve = res;
|
|
1298
2503
|
});
|
|
1299
|
-
|
|
1300
|
-
|
|
2504
|
+
this.queuedDone.set(runId, { promise, resolve });
|
|
2505
|
+
return promise;
|
|
2506
|
+
}
|
|
2507
|
+
resolveQueuedDone(runId, result) {
|
|
2508
|
+
const deferred = this.queuedDone.get(runId);
|
|
2509
|
+
if (deferred === undefined)
|
|
2510
|
+
return;
|
|
2511
|
+
this.queuedDone.delete(runId);
|
|
2512
|
+
deferred.resolve(result);
|
|
2513
|
+
}
|
|
2514
|
+
attachQueuedDone(runId, done) {
|
|
2515
|
+
void done.then((result) => this.resolveQueuedDone(runId, result), (err) => this.resolveQueuedDone(runId, {
|
|
2516
|
+
ok: false,
|
|
2517
|
+
outcome: "failed",
|
|
2518
|
+
runDir: this.options.store.getWorkspaceDir(runId),
|
|
2519
|
+
runId,
|
|
2520
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
2521
|
+
}));
|
|
2522
|
+
}
|
|
2523
|
+
async drainAdmissionQueue() {
|
|
2524
|
+
if (!this.acceptingWork)
|
|
2525
|
+
return;
|
|
2526
|
+
if (this.admissionDrainInFlight)
|
|
2527
|
+
return;
|
|
2528
|
+
this.admissionDrainInFlight = true;
|
|
2529
|
+
try {
|
|
2530
|
+
const blockedRoots = new Set();
|
|
2531
|
+
const blockedCallers = new Set();
|
|
2532
|
+
while (this.acceptingWork && this.active.size < this.maxConcurrent) {
|
|
2533
|
+
const next = this.admissionQueue.dequeueNext(blockedRoots, blockedCallers);
|
|
2534
|
+
if (next === undefined)
|
|
2535
|
+
break;
|
|
2536
|
+
const outcome = await this.startDequeuedAdmission(next, blockedCallers);
|
|
2537
|
+
if (outcome === "checkout_busy") {
|
|
2538
|
+
this.admissionQueue.requeueFront(next.projectRoot, next.entry);
|
|
2539
|
+
blockedRoots.add(next.projectRoot);
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
if (outcome === "project_capacity_busy") {
|
|
2543
|
+
blockedRoots.add(next.projectRoot);
|
|
2544
|
+
continue;
|
|
2545
|
+
}
|
|
2546
|
+
if (outcome === "caller_quota_busy") {
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
if (outcome === "capacity_busy") {
|
|
2550
|
+
break;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
finally {
|
|
2555
|
+
this.admissionDrainInFlight = false;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
async startDequeuedAdmission(next, blockedCallers) {
|
|
2559
|
+
if (!this.acceptingWork) {
|
|
2560
|
+
this.admissionQueue.requeueFront(next.projectRoot, next.entry);
|
|
2561
|
+
return "capacity_busy";
|
|
2562
|
+
}
|
|
2563
|
+
const { runId } = next.entry;
|
|
2564
|
+
let meta;
|
|
2565
|
+
try {
|
|
2566
|
+
meta = await this.options.store.readRunMeta(runId);
|
|
2567
|
+
}
|
|
2568
|
+
catch {
|
|
2569
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2570
|
+
return "cancelled";
|
|
2571
|
+
}
|
|
2572
|
+
if (meta.status !== "queued") {
|
|
2573
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2574
|
+
return "cancelled";
|
|
2575
|
+
}
|
|
2576
|
+
if (!(await this.isProjectRootAvailable(next.projectRoot))) {
|
|
2577
|
+
await this.cancelRun(runId, PROJECT_ROOT_UNAVAILABLE_REASON);
|
|
2578
|
+
return "cancelled";
|
|
2579
|
+
}
|
|
2580
|
+
const diskGate = await this.checkDiskFloorAdmission();
|
|
2581
|
+
if (diskGate !== undefined) {
|
|
2582
|
+
await this.cancelRun(runId, INSUFFICIENT_DISK_CANCEL_REASON);
|
|
2583
|
+
return "cancelled";
|
|
2584
|
+
}
|
|
2585
|
+
const pending = this.pendingQueuedStarts.get(runId);
|
|
2586
|
+
let taskYaml;
|
|
2587
|
+
let task;
|
|
2588
|
+
let binding;
|
|
2589
|
+
let pathCheckoutRoot;
|
|
2590
|
+
let checkoutKey;
|
|
2591
|
+
let pipeline;
|
|
2592
|
+
let cwd;
|
|
2593
|
+
let projectRoot;
|
|
2594
|
+
let checkoutOverride;
|
|
2595
|
+
let skipGates;
|
|
2596
|
+
let ciIdentity;
|
|
2597
|
+
let taskPath;
|
|
2598
|
+
let submission;
|
|
2599
|
+
let pinned;
|
|
2600
|
+
let callerId = pending?.callerId ?? meta.caller_id ?? null;
|
|
2601
|
+
let skills = pending?.skills;
|
|
2602
|
+
if (callerId !== undefined &&
|
|
2603
|
+
callerId !== null &&
|
|
2604
|
+
blockedCallers.has(callerId.toLowerCase())) {
|
|
2605
|
+
next.entry.callerId = next.entry.callerId ?? callerId;
|
|
2606
|
+
this.admissionQueue.requeueFront(next.projectRoot, next.entry);
|
|
2607
|
+
return "caller_quota_busy";
|
|
2608
|
+
}
|
|
2609
|
+
try {
|
|
2610
|
+
if (pending !== undefined) {
|
|
2611
|
+
taskYaml = pending.taskYaml;
|
|
2612
|
+
binding = pending.binding;
|
|
2613
|
+
pathCheckoutRoot = pending.pathCheckoutRoot;
|
|
2614
|
+
checkoutKey = pending.checkoutKey;
|
|
2615
|
+
pipeline = pending.pipeline;
|
|
2616
|
+
cwd = pending.cwd;
|
|
2617
|
+
projectRoot = pending.projectRoot;
|
|
2618
|
+
checkoutOverride = pending.checkoutOverride;
|
|
2619
|
+
skipGates = pending.skipGates;
|
|
2620
|
+
ciIdentity = pending.ciIdentity;
|
|
2621
|
+
taskPath = pending.taskPath;
|
|
2622
|
+
submission = pending.submission;
|
|
2623
|
+
pinned = pending.pinned;
|
|
2624
|
+
callerId = pending.callerId ?? meta.caller_id ?? null;
|
|
2625
|
+
const loadedTask = loadTaskFromYamlOutcome(taskYaml, pending.taskLabel);
|
|
2626
|
+
if (!loadedTask.ok) {
|
|
2627
|
+
await this.cancelRun(runId, loadedTask.issues[0]?.message ?? "Invalid task");
|
|
2628
|
+
return "cancelled";
|
|
2629
|
+
}
|
|
2630
|
+
task = loadedTask.value;
|
|
2631
|
+
}
|
|
2632
|
+
else {
|
|
2633
|
+
taskYaml = await this.options.store.readTaskYaml(runId);
|
|
2634
|
+
const loadedTask = loadTaskFromYamlOutcome(taskYaml, `run ${runId} task`);
|
|
2635
|
+
if (!loadedTask.ok) {
|
|
2636
|
+
await this.cancelRun(runId, loadedTask.issues[0]?.message ?? "Invalid task");
|
|
2637
|
+
return "cancelled";
|
|
2638
|
+
}
|
|
2639
|
+
task = loadedTask.value;
|
|
2640
|
+
const bindingOutcome = resolveWorkspaceBinding(task, {});
|
|
2641
|
+
if (!bindingOutcome.ok) {
|
|
2642
|
+
await this.cancelRun(runId, bindingOutcome.issues[0]?.message ?? "Invalid workspace binding");
|
|
2643
|
+
return "cancelled";
|
|
2644
|
+
}
|
|
2645
|
+
binding = bindingOutcome.value;
|
|
2646
|
+
cwd = meta.project_root ?? this.cwd;
|
|
2647
|
+
projectRoot = normalizeCatalogPath(meta.project_root ?? this.projectRoot);
|
|
2648
|
+
if (binding.kind === "checkout") {
|
|
2649
|
+
pathCheckoutRoot = await resolveAndValidateCheckout(task, undefined, cwd);
|
|
2650
|
+
checkoutKey =
|
|
2651
|
+
pathCheckoutRoot !== undefined
|
|
2652
|
+
? await toCheckoutLeaseKey(pathCheckoutRoot)
|
|
2653
|
+
: undefined;
|
|
2654
|
+
}
|
|
2655
|
+
if (meta.pipeline_source === "inline") {
|
|
2656
|
+
const body = await this.options.store.readPipelineBody(runId);
|
|
2657
|
+
if (body === null || body === "") {
|
|
2658
|
+
await this.cancelRun(runId, "pipeline_body unavailable after restart");
|
|
2659
|
+
return "cancelled";
|
|
2660
|
+
}
|
|
2661
|
+
try {
|
|
2662
|
+
pipeline = JSON.parse(body);
|
|
2663
|
+
}
|
|
2664
|
+
catch {
|
|
2665
|
+
await this.cancelRun(runId, "pipeline_body invalid after restart");
|
|
2666
|
+
return "cancelled";
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
else if (meta.pipeline_path) {
|
|
2670
|
+
pipeline = normalizeCatalogPath(meta.pipeline_path);
|
|
2671
|
+
}
|
|
2672
|
+
else {
|
|
2673
|
+
await this.cancelRun(runId, "pipeline_path unavailable after restart");
|
|
2674
|
+
return "cancelled";
|
|
2675
|
+
}
|
|
2676
|
+
taskPath = meta.task_path;
|
|
2677
|
+
skipGates = meta.skip_gates;
|
|
2678
|
+
ciIdentity = {
|
|
2679
|
+
gitSha: meta.git_sha,
|
|
2680
|
+
ciPrUrl: meta.ci_pr_url,
|
|
2681
|
+
ciJobUrl: meta.ci_job_url,
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
catch (err) {
|
|
2686
|
+
await this.cancelRun(runId, err instanceof Error ? err.message : String(err));
|
|
2687
|
+
return "cancelled";
|
|
2688
|
+
}
|
|
2689
|
+
const reserved = this.tryReserve(checkoutKey, projectRoot, callerId);
|
|
2690
|
+
if (!reserved.ok) {
|
|
2691
|
+
if (reserved.failure.code === "busy_checkout") {
|
|
2692
|
+
return "checkout_busy";
|
|
2693
|
+
}
|
|
2694
|
+
this.admissionQueue.requeueFront(next.projectRoot, next.entry);
|
|
2695
|
+
if (reserved.failure.code === "busy_capacity" &&
|
|
2696
|
+
reserved.failure.scope === "project") {
|
|
2697
|
+
return "project_capacity_busy";
|
|
2698
|
+
}
|
|
2699
|
+
if (reserved.failure.code === "busy_caller_quota") {
|
|
2700
|
+
if (callerId !== undefined && callerId !== null) {
|
|
2701
|
+
blockedCallers.add(callerId.toLowerCase());
|
|
2702
|
+
next.entry.callerId = next.entry.callerId ?? callerId;
|
|
2703
|
+
}
|
|
2704
|
+
return "caller_quota_busy";
|
|
2705
|
+
}
|
|
2706
|
+
return "capacity_busy";
|
|
2707
|
+
}
|
|
2708
|
+
let rollback = async () => { };
|
|
2709
|
+
try {
|
|
2710
|
+
if (this.options.onBeforeQueuedMaterialize !== undefined) {
|
|
2711
|
+
await this.options.onBeforeQueuedMaterialize(runId);
|
|
2712
|
+
}
|
|
2713
|
+
const { materialized, rollback: linkRollback } = await materializeWorkspaceBinding({
|
|
2714
|
+
runId,
|
|
2715
|
+
task,
|
|
2716
|
+
binding,
|
|
2717
|
+
checkoutRoot: pathCheckoutRoot,
|
|
2718
|
+
pinned,
|
|
2719
|
+
});
|
|
2720
|
+
rollback = linkRollback;
|
|
2721
|
+
let metaAfterMaterialize;
|
|
2722
|
+
try {
|
|
2723
|
+
metaAfterMaterialize = await this.options.store.readRunMeta(runId);
|
|
2724
|
+
}
|
|
2725
|
+
catch {
|
|
2726
|
+
await rollback().catch(() => undefined);
|
|
2727
|
+
this.clearReservation(reserved.provisionalId);
|
|
2728
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2729
|
+
return "cancelled";
|
|
2730
|
+
}
|
|
2731
|
+
if (metaAfterMaterialize.status !== "queued") {
|
|
2732
|
+
await rollback().catch(() => undefined);
|
|
2733
|
+
this.clearReservation(reserved.provisionalId);
|
|
2734
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2735
|
+
this.schedulingHalts.delete(runId);
|
|
2736
|
+
return "cancelled";
|
|
2737
|
+
}
|
|
2738
|
+
const schedulingHalt = this.ensureSchedulingHalt(runId);
|
|
2739
|
+
const started = await startPipeline({
|
|
2740
|
+
submission,
|
|
2741
|
+
agent: this.options.agent,
|
|
2742
|
+
store: this.options.store,
|
|
2743
|
+
taskYaml,
|
|
2744
|
+
taskPath,
|
|
2745
|
+
pipeline,
|
|
2746
|
+
cwd,
|
|
2747
|
+
projectRoot,
|
|
2748
|
+
checkoutOverride,
|
|
2749
|
+
runId: materialized.runId,
|
|
2750
|
+
reuseExistingRun: true,
|
|
2751
|
+
checkoutRoot: materialized.checkoutRoot,
|
|
2752
|
+
repository: materialized.repository,
|
|
2753
|
+
ref: materialized.ref,
|
|
2754
|
+
resolvedSha: materialized.resolvedSha,
|
|
2755
|
+
runBranch: materialized.runBranch,
|
|
2756
|
+
gitSha: ciIdentity?.gitSha,
|
|
2757
|
+
ciPrUrl: ciIdentity?.ciPrUrl,
|
|
2758
|
+
ciJobUrl: ciIdentity?.ciJobUrl,
|
|
2759
|
+
hitl: this.hitl,
|
|
2760
|
+
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
2761
|
+
executionMode: this.executionMode,
|
|
2762
|
+
stageProcessLauncher: this.stageProcessLauncher,
|
|
2763
|
+
operatorCatalog: this.options.operatorCatalog,
|
|
2764
|
+
skipGates,
|
|
2765
|
+
schedulingHalt,
|
|
2766
|
+
...(skills !== undefined ? { skills } : {}),
|
|
2767
|
+
});
|
|
2768
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2769
|
+
this.track(reserved.provisionalId, started.runId, started.done);
|
|
2770
|
+
this.attachQueuedDone(started.runId, started.done);
|
|
2771
|
+
return "started";
|
|
2772
|
+
}
|
|
2773
|
+
catch (err) {
|
|
2774
|
+
await rollback().catch(() => undefined);
|
|
2775
|
+
this.clearReservation(reserved.provisionalId);
|
|
2776
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2777
|
+
this.schedulingHalts.delete(runId);
|
|
2778
|
+
if (err instanceof QueuedRunActivationAborted) {
|
|
2779
|
+
return "cancelled";
|
|
2780
|
+
}
|
|
2781
|
+
const reason = err instanceof StartLinkError
|
|
2782
|
+
? err.code
|
|
2783
|
+
: err instanceof Error
|
|
2784
|
+
? err.message
|
|
2785
|
+
: String(err);
|
|
2786
|
+
await this.cancelRun(runId, reason);
|
|
2787
|
+
return "cancelled";
|
|
1301
2788
|
}
|
|
1302
|
-
return { ok: true, provisionalId };
|
|
1303
2789
|
}
|
|
1304
2790
|
clearReservation(provisionalId) {
|
|
1305
2791
|
this.removeActiveEntry(provisionalId, true);
|
|
@@ -1336,7 +2822,9 @@ export class RunManager {
|
|
|
1336
2822
|
return false;
|
|
1337
2823
|
const tracked = await this.ensureResumeTracked(runId);
|
|
1338
2824
|
if (!tracked.ok) {
|
|
1339
|
-
|
|
2825
|
+
log
|
|
2826
|
+
.child({ run_id: runId })
|
|
2827
|
+
.error("resume.track_failed", `resumeStalledSchedules: ${tracked.reason}`);
|
|
1340
2828
|
return false;
|
|
1341
2829
|
}
|
|
1342
2830
|
try {
|
|
@@ -1359,6 +2847,7 @@ export class RunManager {
|
|
|
1359
2847
|
executionMode: this.executionMode,
|
|
1360
2848
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1361
2849
|
initialSchedule: hydrated,
|
|
2850
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1362
2851
|
}).finally(() => {
|
|
1363
2852
|
void syncRunStatusFromStages(this.options.store, runId).catch(() => { });
|
|
1364
2853
|
});
|
|
@@ -1380,16 +2869,27 @@ export class RunManager {
|
|
|
1380
2869
|
}
|
|
1381
2870
|
let checkoutKey;
|
|
1382
2871
|
let durableCheckoutRoot;
|
|
2872
|
+
let resumeCallerId = null;
|
|
2873
|
+
let resumeProjectRoot;
|
|
1383
2874
|
try {
|
|
1384
2875
|
const meta = await this.options.store.readRunMeta(runId);
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
2876
|
+
resumeCallerId = meta.caller_id ?? null;
|
|
2877
|
+
resumeProjectRoot = meta.project_root
|
|
2878
|
+
? normalizeCatalogPath(meta.project_root)
|
|
2879
|
+
: undefined;
|
|
2880
|
+
const kind = derivedBindingKindFromMeta(meta);
|
|
2881
|
+
if (kind === "checkout") {
|
|
2882
|
+
const checkoutRoot = meta.checkout_root;
|
|
2883
|
+
if (checkoutRoot !== undefined && checkoutRoot !== "") {
|
|
2884
|
+
durableCheckoutRoot = checkoutRoot;
|
|
2885
|
+
checkoutKey = await toCheckoutLeaseKey(checkoutRoot);
|
|
2886
|
+
}
|
|
1389
2887
|
}
|
|
1390
2888
|
}
|
|
1391
2889
|
catch (err) {
|
|
1392
|
-
|
|
2890
|
+
log
|
|
2891
|
+
.child({ run_id: runId })
|
|
2892
|
+
.error("resume.checkout_root_failed", `invariant: trackResume failed reading checkout_root for run ${runId}; continuing without checkout lease: ${err instanceof Error ? err.message : String(err)}`);
|
|
1393
2893
|
}
|
|
1394
2894
|
const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
|
|
1395
2895
|
if (conflictHolder !== undefined) {
|
|
@@ -1404,6 +2904,8 @@ export class RunManager {
|
|
|
1404
2904
|
this.active.set(runId, {
|
|
1405
2905
|
checkoutKey,
|
|
1406
2906
|
durableCheckoutRoot,
|
|
2907
|
+
projectRoot: resumeProjectRoot,
|
|
2908
|
+
callerId: resumeCallerId,
|
|
1407
2909
|
generation: ++this.trackingGeneration,
|
|
1408
2910
|
});
|
|
1409
2911
|
return { ok: true };
|
|
@@ -1460,7 +2962,7 @@ export class RunManager {
|
|
|
1460
2962
|
catch {
|
|
1461
2963
|
// ignore secondary failures — still fail-closed in memory
|
|
1462
2964
|
}
|
|
1463
|
-
|
|
2965
|
+
log.child({ run_id: runId }).error("attach.quarantined", reason);
|
|
1464
2966
|
}
|
|
1465
2967
|
untrackIfGeneration(runId, generation) {
|
|
1466
2968
|
const entry = this.active.get(runId);
|
|
@@ -1473,6 +2975,7 @@ export class RunManager {
|
|
|
1473
2975
|
if (!entry)
|
|
1474
2976
|
return;
|
|
1475
2977
|
this.active.delete(id);
|
|
2978
|
+
this.schedulingHalts.delete(id);
|
|
1476
2979
|
if (isProvisional) {
|
|
1477
2980
|
this.provisionalIds.delete(id);
|
|
1478
2981
|
}
|
|
@@ -1480,5 +2983,19 @@ export class RunManager {
|
|
|
1480
2983
|
this.checkoutLeases.get(entry.checkoutKey) === id) {
|
|
1481
2984
|
this.checkoutLeases.delete(entry.checkoutKey);
|
|
1482
2985
|
}
|
|
2986
|
+
if (!isProvisional) {
|
|
2987
|
+
void this.drainAdmissionQueue();
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
ensureSchedulingHalt(runId) {
|
|
2991
|
+
let halt = this.schedulingHalts.get(runId);
|
|
2992
|
+
if (halt === undefined) {
|
|
2993
|
+
halt = {
|
|
2994
|
+
halted: !this.acceptingWork,
|
|
2995
|
+
hostShutdown: !this.acceptingWork,
|
|
2996
|
+
};
|
|
2997
|
+
this.schedulingHalts.set(runId, halt);
|
|
2998
|
+
}
|
|
2999
|
+
return halt;
|
|
1483
3000
|
}
|
|
1484
3001
|
}
|