stageflow 0.23.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 +24 -5
- package/dist/a2a/configDiscovery.d.ts +9 -0
- package/dist/a2a/configDiscovery.js +18 -0
- package/dist/a2a/contracts.d.ts +94 -0
- package/dist/a2a/contracts.js +164 -0
- package/dist/a2a/limits.d.ts +14 -0
- package/dist/a2a/limits.js +29 -0
- package/dist/a2a/registry.d.ts +46 -0
- package/dist/a2a/registry.js +178 -0
- package/dist/a2a/server.d.ts +36 -0
- package/dist/a2a/server.js +344 -0
- package/dist/a2a/service.d.ts +97 -0
- package/dist/a2a/service.js +545 -0
- package/dist/a2a/store.d.ts +109 -0
- package/dist/a2a/store.js +202 -0
- 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/a2aCommand.d.ts +8 -0
- package/dist/cli/a2aCommand.js +131 -0
- 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 +34 -0
- package/dist/cli/runStageCommand.js +426 -0
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +162 -8
- package/dist/cli.js +190 -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 +591 -76
- 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 +27 -0
- package/dist/runstore/createStore.js +75 -7
- 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 +119 -8
- 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 +37 -3
- package/dist/runstore/sqlite/SqliteRunStore.js +401 -227
- 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 +283 -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/submission.d.ts +11 -0
- package/dist/runstore/submission.js +7 -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 +38 -1
- package/dist/runtime/pipelineRunner.js +179 -15
- package/dist/runtime/pipelineScheduler.d.ts +15 -0
- package/dist/runtime/pipelineScheduler.js +167 -42
- package/dist/runtime/reloadRunCatalog.js +12 -1
- package/dist/runtime/repositoryMaterialize.d.ts +47 -0
- package/dist/runtime/repositoryMaterialize.js +235 -0
- package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
- package/dist/runtime/resolveEnvelopeRef.js +79 -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 +179 -4
- package/dist/runtime/runManager.js +1644 -83
- 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 +38 -2
- package/dist/server/bootstrap.js +219 -4
- 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 +109 -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 +6 -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,62 +1,277 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { RunSubmissionExistsError } from "../runstore/submission.js";
|
|
4
|
+
import { access, readFile, realpath } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { findProjectRoot } from "../project/findProjectRoot.js";
|
|
5
|
-
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";
|
|
6
11
|
import { loadRunContext } from "./resumeReconstruct.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
12
|
+
import { assertResumableStage, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
|
|
13
|
+
import { loadTaskFromYamlOutcome } from "../config/loadTask.js";
|
|
14
|
+
import { buildValidationResult, loadPipelineValidated, } from "../config/validateCatalog.js";
|
|
9
15
|
import { deriveStatusFromStages, findUnhandledFailedStage, } from "../runstore/port.js";
|
|
10
|
-
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";
|
|
11
20
|
import { hydrateScheduleFromStore, hydratedScheduleHasRunnableWork, resumeRun, runPipelineDag, } from "./pipelineScheduler.js";
|
|
12
21
|
import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
|
|
13
22
|
import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
|
|
14
23
|
import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
|
|
15
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";
|
|
16
37
|
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
|
|
17
38
|
import { StageHitlController, waitKey, } from "./stageHitl.js";
|
|
18
|
-
import { resolveAndValidateCheckout, } from "./stageRoots.js";
|
|
39
|
+
import { resolveAndValidateCheckout, resolveEffectiveGitIdentity, stageBindingEnvFromRun, } from "./stageRoots.js";
|
|
19
40
|
import { orchestrateAnswerResume } from "./answerResume.js";
|
|
20
41
|
import { reconstructAndContinue as resumeReconstructAndContinue } from "./resumeReconstruct.js";
|
|
21
|
-
import { resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
22
|
-
import { resolveStartTaskInput } from "./taskInput.js";
|
|
23
|
-
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";
|
|
24
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";
|
|
52
|
+
function existingSubmissionResult(existing, request) {
|
|
53
|
+
if (existing.requestHash !== request.requestHash) {
|
|
54
|
+
return { ok: false, status: 409, reason: "Submission key was already used for different input" };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, runId: existing.runId, reused: true };
|
|
57
|
+
}
|
|
58
|
+
export const FORCE_DELETE_CANCEL_REASON = "delete_run: force";
|
|
25
59
|
const DEFAULT_MAX_CONCURRENT = 3;
|
|
26
|
-
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";
|
|
27
66
|
const OPERATOR_ABANDON_REASON = "process_interrupted: operator abandoned stage";
|
|
67
|
+
const log = rootLogger.child({ component: "runtime" });
|
|
28
68
|
function parseMaxConcurrent(raw) {
|
|
29
69
|
if (raw === undefined || raw.trim() === "")
|
|
30
70
|
return DEFAULT_MAX_CONCURRENT;
|
|
31
71
|
const n = Number.parseInt(raw, 10);
|
|
32
|
-
if (!Number.isFinite(n) || n < 1)
|
|
33
|
-
|
|
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
|
+
}
|
|
34
75
|
return n;
|
|
35
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;
|
|
99
|
+
return n;
|
|
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
|
+
}
|
|
36
241
|
async function toCheckoutLeaseKey(absPath) {
|
|
37
242
|
try {
|
|
38
243
|
return await realpath(absPath);
|
|
39
244
|
}
|
|
40
245
|
catch {
|
|
41
246
|
const fallback = path.resolve(absPath);
|
|
42
|
-
|
|
247
|
+
log.error("checkout.realpath_failed", `invariant: checkout realpath failed for ${absPath}; using path.resolve fallback ${fallback}`, { abs_path: absPath, fallback });
|
|
43
248
|
return fallback;
|
|
44
249
|
}
|
|
45
250
|
}
|
|
46
251
|
export class RunManager {
|
|
47
252
|
options;
|
|
253
|
+
submissionsInFlight = new Map();
|
|
48
254
|
active = new Map();
|
|
255
|
+
schedulingHalts = new Map();
|
|
49
256
|
checkoutLeases = new Map();
|
|
50
257
|
provisionalIds = new Set();
|
|
258
|
+
admissionQueue = new AdmissionQueue();
|
|
259
|
+
pendingQueuedStarts = new Map();
|
|
260
|
+
queuedDone = new Map();
|
|
261
|
+
admissionDrainInFlight = false;
|
|
51
262
|
resumeInFlight = new Set();
|
|
52
263
|
retryInFlight = new Set();
|
|
53
264
|
retryStartOwner = new Map();
|
|
54
265
|
retryStartWaiters = new Map();
|
|
55
266
|
trackingGeneration = 0;
|
|
56
267
|
maxConcurrent;
|
|
268
|
+
maxQueued;
|
|
269
|
+
maxConcurrentPerProject;
|
|
270
|
+
callerQuotas;
|
|
57
271
|
maxActiveStagesPerRun;
|
|
58
272
|
executionMode;
|
|
59
273
|
stageProcessLauncher;
|
|
274
|
+
a2aStore;
|
|
60
275
|
hitl;
|
|
61
276
|
attachedWaiting = new Set();
|
|
62
277
|
retryCoordinator = new RunRetryCoordinator();
|
|
@@ -90,15 +305,24 @@ export class RunManager {
|
|
|
90
305
|
cwd;
|
|
91
306
|
projectRoot;
|
|
92
307
|
isGitProject;
|
|
308
|
+
acceptingWork = true;
|
|
93
309
|
constructor(options) {
|
|
94
310
|
this.options = options;
|
|
95
311
|
this.cwd = options.cwd ?? process.cwd();
|
|
96
312
|
this.projectRoot = options.projectRoot ?? this.cwd;
|
|
97
313
|
this.isGitProject = options.isGitProject ?? false;
|
|
314
|
+
this.a2aStore = options.a2aStore;
|
|
98
315
|
this.maxConcurrent =
|
|
99
316
|
options.maxConcurrent ??
|
|
100
317
|
readMaxConcurrentFromGlobal() ??
|
|
101
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
|
+
]));
|
|
102
326
|
this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
|
|
103
327
|
this.executionMode = readStageExecutionMode(process.env, options.executionMode);
|
|
104
328
|
this.stageProcessLauncher =
|
|
@@ -125,6 +349,57 @@ export class RunManager {
|
|
|
125
349
|
getActiveCount() {
|
|
126
350
|
return this.active.size;
|
|
127
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
|
+
}
|
|
128
403
|
getActiveRunIds() {
|
|
129
404
|
return [...this.active.keys()];
|
|
130
405
|
}
|
|
@@ -134,6 +409,7 @@ export class RunManager {
|
|
|
134
409
|
const maxConcurrent = this.getMaxConcurrent();
|
|
135
410
|
const maxActiveStageProcessesRaw = readMaxActiveStageProcesses(process.env);
|
|
136
411
|
const activeStageProcesses = this.stageProcessLauncher?.activeCount() ?? 0;
|
|
412
|
+
const limits = getContainerLimits();
|
|
137
413
|
return {
|
|
138
414
|
ok: true,
|
|
139
415
|
activeRunIds,
|
|
@@ -144,8 +420,83 @@ export class RunManager {
|
|
|
144
420
|
maxActiveStageProcesses: Number.isFinite(maxActiveStageProcessesRaw)
|
|
145
421
|
? maxActiveStageProcessesRaw
|
|
146
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() },
|
|
432
|
+
};
|
|
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
|
+
})),
|
|
147
447
|
};
|
|
148
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
|
+
}
|
|
149
500
|
getHitlController() {
|
|
150
501
|
return this.hitl;
|
|
151
502
|
}
|
|
@@ -180,16 +531,27 @@ export class RunManager {
|
|
|
180
531
|
}
|
|
181
532
|
let checkoutKey;
|
|
182
533
|
let durableCheckoutRoot;
|
|
534
|
+
let attachCallerId = null;
|
|
535
|
+
let attachProjectRoot;
|
|
183
536
|
try {
|
|
184
537
|
const meta = await this.options.store.readRunMeta(runId);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
}
|
|
189
549
|
}
|
|
190
550
|
}
|
|
191
551
|
catch (err) {
|
|
192
|
-
|
|
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)}`);
|
|
193
555
|
}
|
|
194
556
|
const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
|
|
195
557
|
if (conflictHolder !== undefined) {
|
|
@@ -203,6 +565,8 @@ export class RunManager {
|
|
|
203
565
|
this.active.set(runId, {
|
|
204
566
|
checkoutKey,
|
|
205
567
|
durableCheckoutRoot,
|
|
568
|
+
projectRoot: attachProjectRoot,
|
|
569
|
+
callerId: attachCallerId,
|
|
206
570
|
generation: ++this.trackingGeneration,
|
|
207
571
|
});
|
|
208
572
|
for (const stage of waitingStages) {
|
|
@@ -224,27 +588,74 @@ export class RunManager {
|
|
|
224
588
|
const reconciled = [];
|
|
225
589
|
const runs = await this.options.store.listRuns();
|
|
226
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;
|
|
227
603
|
let detail;
|
|
228
604
|
try {
|
|
229
605
|
detail = await this.options.store.readRun(summary.run_id);
|
|
230
606
|
}
|
|
231
607
|
catch (err) {
|
|
232
|
-
|
|
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)}`);
|
|
233
611
|
continue;
|
|
234
612
|
}
|
|
235
613
|
const runId = summary.run_id;
|
|
614
|
+
const cancelled = meta.status === "cancelled";
|
|
236
615
|
let runChanged = false;
|
|
237
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
|
+
}
|
|
238
648
|
if (stage.status !== "running")
|
|
239
649
|
continue;
|
|
240
650
|
if (this.hasActiveWorker(runId, stage.stage_id))
|
|
241
651
|
continue;
|
|
242
652
|
try {
|
|
243
|
-
await
|
|
653
|
+
await markStageInterrupted({
|
|
244
654
|
store: this.options.store,
|
|
245
655
|
runId,
|
|
246
656
|
stageId: stage.stage_id,
|
|
247
657
|
reason: STARTUP_RECONCILE_REASON,
|
|
658
|
+
status: "interrupted",
|
|
248
659
|
});
|
|
249
660
|
reconciled.push({
|
|
250
661
|
runId,
|
|
@@ -252,26 +663,106 @@ export class RunManager {
|
|
|
252
663
|
reason: STARTUP_RECONCILE_REASON,
|
|
253
664
|
});
|
|
254
665
|
runChanged = true;
|
|
255
|
-
|
|
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 });
|
|
256
669
|
}
|
|
257
670
|
catch (err) {
|
|
258
|
-
|
|
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)}`);
|
|
259
674
|
}
|
|
260
675
|
}
|
|
261
|
-
if (runChanged) {
|
|
676
|
+
if (runChanged && !cancelled) {
|
|
262
677
|
try {
|
|
263
678
|
await syncRunStatusFromStages(this.options.store, runId);
|
|
264
679
|
}
|
|
265
680
|
catch (err) {
|
|
266
|
-
|
|
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)}`);
|
|
267
684
|
}
|
|
268
685
|
}
|
|
269
686
|
}
|
|
270
687
|
if (reconciled.length > 0) {
|
|
271
|
-
|
|
688
|
+
log.error("reconcile.complete", `reconcileOrphanedStages: reconciled ${reconciled.length} orphaned stage(s)`, { count: reconciled.length });
|
|
272
689
|
}
|
|
273
690
|
return { reconciled };
|
|
274
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
|
+
}
|
|
275
766
|
async resumeStalledSchedules() {
|
|
276
767
|
const resumed = [];
|
|
277
768
|
const runs = await this.options.store.listRuns();
|
|
@@ -287,11 +778,37 @@ export class RunManager {
|
|
|
287
778
|
}
|
|
288
779
|
}
|
|
289
780
|
catch (err) {
|
|
290
|
-
|
|
781
|
+
log
|
|
782
|
+
.child({ run_id: runId })
|
|
783
|
+
.error("resume.stalled_failed", `resumeStalledSchedules: failed to resume ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
291
784
|
}
|
|
292
785
|
}
|
|
293
786
|
return resumed;
|
|
294
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
|
+
}
|
|
295
812
|
async abandonStage(runId, stageId) {
|
|
296
813
|
let detail;
|
|
297
814
|
try {
|
|
@@ -330,11 +847,12 @@ export class RunManager {
|
|
|
330
847
|
await this.stageProcessLauncher.cancelRun(runId);
|
|
331
848
|
}
|
|
332
849
|
}
|
|
333
|
-
await
|
|
850
|
+
await markStageInterrupted({
|
|
334
851
|
store: this.options.store,
|
|
335
852
|
runId,
|
|
336
853
|
stageId,
|
|
337
854
|
reason: OPERATOR_ABANDON_REASON,
|
|
855
|
+
status: "failed",
|
|
338
856
|
});
|
|
339
857
|
if (this.retryCoordinator.isActive(runId) &&
|
|
340
858
|
this.retryCoordinator.getActiveRoots(runId)?.has(stageId)) {
|
|
@@ -348,13 +866,250 @@ export class RunManager {
|
|
|
348
866
|
}
|
|
349
867
|
return { ok: true, runId, stageId };
|
|
350
868
|
}
|
|
351
|
-
async
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
:
|
|
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
|
+
}
|
|
1058
|
+
async startRunOnce(input, submission) {
|
|
1059
|
+
if (!submission.key.trim() || submission.key.length > 256 || !/^[a-f0-9]{64}$/.test(submission.requestHash)) {
|
|
1060
|
+
return { ok: false, status: 400, reason: "Submission requires a key and SHA-256 request hash" };
|
|
1061
|
+
}
|
|
1062
|
+
const pending = this.submissionsInFlight.get(submission.key);
|
|
1063
|
+
if (pending) {
|
|
1064
|
+
if (pending.requestHash !== submission.requestHash) {
|
|
1065
|
+
return { ok: false, status: 409, reason: "Submission key was already used for different input" };
|
|
1066
|
+
}
|
|
1067
|
+
const result = await pending.result;
|
|
1068
|
+
return result.ok ? { ...result, reused: true } : result;
|
|
1069
|
+
}
|
|
1070
|
+
const result = this.startSubmittedRun(input, submission);
|
|
1071
|
+
this.submissionsInFlight.set(submission.key, { requestHash: submission.requestHash, result });
|
|
1072
|
+
try {
|
|
1073
|
+
return await result;
|
|
1074
|
+
}
|
|
1075
|
+
finally {
|
|
1076
|
+
this.submissionsInFlight.delete(submission.key);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async startSubmittedRun(input, submission) {
|
|
1080
|
+
const existing = await this.options.store.getRunBySubmission(submission.key);
|
|
1081
|
+
if (existing)
|
|
1082
|
+
return existingSubmissionResult(existing, submission);
|
|
1083
|
+
try {
|
|
1084
|
+
const result = await this.startRun(input, submission);
|
|
1085
|
+
return result.ok ? { ok: true, runId: result.runId, reused: false, done: result.done } : result;
|
|
1086
|
+
}
|
|
1087
|
+
catch (error) {
|
|
1088
|
+
if (error instanceof RunSubmissionExistsError)
|
|
1089
|
+
return existingSubmissionResult(error.submission, submission);
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async startRun(input, submission) {
|
|
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;
|
|
358
1113
|
let resolved;
|
|
359
1114
|
try {
|
|
360
1115
|
resolved = resolveStartTaskInput(input, cwd);
|
|
@@ -389,24 +1144,57 @@ export class RunManager {
|
|
|
389
1144
|
gitSha: input.gitSha,
|
|
390
1145
|
ciPrUrl: input.ciPrUrl,
|
|
391
1146
|
ciJobUrl: input.ciJobUrl,
|
|
392
|
-
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined);
|
|
1147
|
+
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission, undefined, input.callerId, input.skills);
|
|
393
1148
|
}
|
|
394
|
-
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
|
+
}
|
|
395
1158
|
const cwd = this.options.cwd ?? process.cwd();
|
|
396
1159
|
let pipeline;
|
|
397
1160
|
let taskYaml;
|
|
398
1161
|
let rerunCwd = cwd;
|
|
399
1162
|
let rerunProjectRoot;
|
|
1163
|
+
let skipGates;
|
|
1164
|
+
let ciIdentity;
|
|
1165
|
+
let pinned;
|
|
400
1166
|
try {
|
|
401
1167
|
const meta = await this.options.store.readRunMeta(runId);
|
|
402
|
-
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 {
|
|
403
1192
|
return {
|
|
404
1193
|
ok: false,
|
|
405
|
-
reason: `Run ${runId}
|
|
1194
|
+
reason: `Run ${runId} has neither pipeline_path nor pipeline_body; cannot re-run.`,
|
|
406
1195
|
status: 400,
|
|
407
1196
|
};
|
|
408
1197
|
}
|
|
409
|
-
pipeline = normalizeCatalogPath(meta.pipeline_path);
|
|
410
1198
|
rerunCwd = meta.project_root
|
|
411
1199
|
? normalizeCatalogPath(meta.project_root)
|
|
412
1200
|
: cwd;
|
|
@@ -414,16 +1202,44 @@ export class RunManager {
|
|
|
414
1202
|
? normalizeCatalogPath(meta.project_root)
|
|
415
1203
|
: undefined;
|
|
416
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
|
+
}
|
|
417
1225
|
}
|
|
418
1226
|
catch {
|
|
419
1227
|
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
420
1228
|
}
|
|
421
|
-
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);
|
|
422
1230
|
}
|
|
423
1231
|
async retryStage(runId, stageId) {
|
|
424
1232
|
return this.retryStageInternal(runId, stageId, true);
|
|
425
1233
|
}
|
|
426
|
-
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
|
+
}
|
|
427
1243
|
const resumeKey = waitKey(runId, stageId);
|
|
428
1244
|
if (this.resumeInFlight.has(resumeKey) || this.retryInFlight.has(resumeKey)) {
|
|
429
1245
|
return {
|
|
@@ -446,13 +1262,16 @@ export class RunManager {
|
|
|
446
1262
|
this.resumeInFlight.delete(resumeKey);
|
|
447
1263
|
return { ok: false, reason: `Stage not found: ${stageId}`, status: 404 };
|
|
448
1264
|
}
|
|
449
|
-
const eligibility =
|
|
1265
|
+
const eligibility = assertResumableStage(stageSnap.status, stageSnap.events);
|
|
450
1266
|
if (!eligibility.ok) {
|
|
451
1267
|
this.resumeInFlight.delete(resumeKey);
|
|
452
1268
|
return eligibility;
|
|
453
1269
|
}
|
|
454
1270
|
const latest = await this.options.store.getLatestStageExecution(runId, stageId);
|
|
455
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
|
+
}
|
|
456
1275
|
const wasActive = this.active.has(runId);
|
|
457
1276
|
const tracked = await this.ensureResumeTracked(runId);
|
|
458
1277
|
if (!tracked.ok) {
|
|
@@ -570,14 +1389,23 @@ export class RunManager {
|
|
|
570
1389
|
finally {
|
|
571
1390
|
this.retryInFlight.delete(retryKey);
|
|
572
1391
|
}
|
|
573
|
-
}
|
|
574
|
-
async retryStageInternal(runId, stageId, awaitRoot, options) {
|
|
1392
|
+
}
|
|
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
|
+
}
|
|
575
1402
|
const retryKey = waitKey(runId, stageId);
|
|
576
1403
|
if (this.retryInFlight.has(retryKey)) {
|
|
577
1404
|
return {
|
|
578
1405
|
ok: false,
|
|
579
1406
|
reason: `Retry already in progress for run ${runId} stage ${stageId}`,
|
|
580
1407
|
status: 409,
|
|
1408
|
+
code: "retry_in_progress",
|
|
581
1409
|
};
|
|
582
1410
|
}
|
|
583
1411
|
this.retryInFlight.add(retryKey);
|
|
@@ -588,6 +1416,7 @@ export class RunManager {
|
|
|
588
1416
|
ok: false,
|
|
589
1417
|
reason: "Stage uses manual recovery; use the manual recovery action instead of retry",
|
|
590
1418
|
status: 409,
|
|
1419
|
+
code: "manual_recovery_required",
|
|
591
1420
|
};
|
|
592
1421
|
}
|
|
593
1422
|
try {
|
|
@@ -1016,6 +1845,7 @@ export class RunManager {
|
|
|
1016
1845
|
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
1017
1846
|
executionMode: this.executionMode,
|
|
1018
1847
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1848
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1019
1849
|
});
|
|
1020
1850
|
this.registerResumeUntrack(runId, done);
|
|
1021
1851
|
const rest = await done;
|
|
@@ -1060,6 +1890,13 @@ export class RunManager {
|
|
|
1060
1890
|
try {
|
|
1061
1891
|
const runMeta = await store.readRunMeta(runId);
|
|
1062
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
|
+
});
|
|
1063
1900
|
const launchResult = await launcher.launch({
|
|
1064
1901
|
runId,
|
|
1065
1902
|
stageId,
|
|
@@ -1068,6 +1905,8 @@ export class RunManager {
|
|
|
1068
1905
|
resumeAnswer: opaqueAnswer,
|
|
1069
1906
|
attempt,
|
|
1070
1907
|
sessionFilePath,
|
|
1908
|
+
env: stageBinding.env,
|
|
1909
|
+
bindingKind: stageBinding.kind,
|
|
1071
1910
|
...(this.options.operatorCatalog !== undefined
|
|
1072
1911
|
? { operatorCatalog: this.options.operatorCatalog }
|
|
1073
1912
|
: {}),
|
|
@@ -1090,7 +1929,6 @@ export class RunManager {
|
|
|
1090
1929
|
catch {
|
|
1091
1930
|
// downstream resume may read envelope from store
|
|
1092
1931
|
}
|
|
1093
|
-
const { meta, task, loaded } = await loadRunContext(store, runId, this.cwd);
|
|
1094
1932
|
const rest = await resumeRun({
|
|
1095
1933
|
prepared: {
|
|
1096
1934
|
task,
|
|
@@ -1109,6 +1947,7 @@ export class RunManager {
|
|
|
1109
1947
|
initialPrior,
|
|
1110
1948
|
executionMode: this.executionMode,
|
|
1111
1949
|
stageProcessLauncher: launcher,
|
|
1950
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1112
1951
|
});
|
|
1113
1952
|
if (rest.outcome === "failed") {
|
|
1114
1953
|
return { ok: false, reason: rest.reason };
|
|
@@ -1156,36 +1995,273 @@ export class RunManager {
|
|
|
1156
1995
|
this.attachedWaiting.delete(`${runId}\0${stageId}`);
|
|
1157
1996
|
}
|
|
1158
1997
|
}
|
|
1159
|
-
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath) {
|
|
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;
|
|
1160
2023
|
let checkoutKey;
|
|
2024
|
+
let pathCheckoutRoot;
|
|
2025
|
+
let binding;
|
|
2026
|
+
let pipelineId;
|
|
2027
|
+
let pipelineDag;
|
|
2028
|
+
let pipelinePath;
|
|
2029
|
+
let loadedPipeline;
|
|
2030
|
+
let toolchainChecks = [];
|
|
1161
2031
|
try {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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)
|
|
1167
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;
|
|
1168
2117
|
}
|
|
1169
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
|
+
}
|
|
1170
2129
|
return {
|
|
1171
2130
|
ok: false,
|
|
1172
2131
|
reason: err instanceof Error ? err.message : String(err),
|
|
1173
2132
|
status: 400,
|
|
1174
2133
|
};
|
|
1175
2134
|
}
|
|
1176
|
-
const
|
|
1177
|
-
if (
|
|
1178
|
-
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 () => { };
|
|
1179
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);
|
|
1180
2249
|
const started = await startPipeline({
|
|
2250
|
+
submission,
|
|
1181
2251
|
agent: this.options.agent,
|
|
1182
2252
|
store: this.options.store,
|
|
1183
2253
|
taskYaml,
|
|
1184
2254
|
taskPath,
|
|
1185
2255
|
pipeline,
|
|
1186
2256
|
cwd,
|
|
1187
|
-
projectRoot:
|
|
2257
|
+
projectRoot: resolvedProjectRoot,
|
|
1188
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,
|
|
1189
2265
|
gitSha: ciIdentity?.gitSha,
|
|
1190
2266
|
ciPrUrl: ciIdentity?.ciPrUrl,
|
|
1191
2267
|
ciJobUrl: ciIdentity?.ciJobUrl,
|
|
@@ -1195,15 +2271,44 @@ export class RunManager {
|
|
|
1195
2271
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1196
2272
|
operatorCatalog: this.options.operatorCatalog,
|
|
1197
2273
|
skipGates,
|
|
2274
|
+
schedulingHalt,
|
|
2275
|
+
callerId,
|
|
2276
|
+
...(hasSkills ? { skills: validatedSkills } : {}),
|
|
1198
2277
|
});
|
|
1199
|
-
this.track(
|
|
2278
|
+
this.track(admitted.provisionalId, started.runId, started.done);
|
|
1200
2279
|
return { ok: true, runId: started.runId, done: started.done };
|
|
1201
2280
|
}
|
|
1202
2281
|
catch (err) {
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
+
}
|
|
2300
|
+
if (err instanceof PipelineValidationError || err instanceof RunSubmissionExistsError) {
|
|
1205
2301
|
throw err;
|
|
1206
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
|
+
}
|
|
1207
2312
|
return {
|
|
1208
2313
|
ok: false,
|
|
1209
2314
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -1213,24 +2318,87 @@ export class RunManager {
|
|
|
1213
2318
|
}
|
|
1214
2319
|
busyFailure(code, extras) {
|
|
1215
2320
|
const activeRunIds = this.getActiveRunIds();
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
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"}`;
|
|
1219
2335
|
return {
|
|
1220
2336
|
ok: false,
|
|
1221
2337
|
reason,
|
|
1222
2338
|
status: 409,
|
|
1223
2339
|
code,
|
|
1224
|
-
activeCount
|
|
1225
|
-
maxConcurrent
|
|
2340
|
+
activeCount,
|
|
2341
|
+
maxConcurrent,
|
|
1226
2342
|
activeRunIds,
|
|
1227
|
-
|
|
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
|
+
: {}),
|
|
1228
2354
|
};
|
|
1229
2355
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
+
};
|
|
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;
|
|
1233
2399
|
}
|
|
2400
|
+
}
|
|
2401
|
+
tryAdmitOrEnqueue(checkoutKey, projectRoot = this.projectRoot, callerId) {
|
|
1234
2402
|
if (checkoutKey !== undefined) {
|
|
1235
2403
|
const holder = this.checkoutLeases.get(checkoutKey);
|
|
1236
2404
|
if (holder !== undefined) {
|
|
@@ -1238,7 +2406,7 @@ export class RunManager {
|
|
|
1238
2406
|
? undefined
|
|
1239
2407
|
: holder;
|
|
1240
2408
|
return {
|
|
1241
|
-
|
|
2409
|
+
action: "reject",
|
|
1242
2410
|
failure: this.busyFailure("busy_checkout", {
|
|
1243
2411
|
conflictingRunId,
|
|
1244
2412
|
conflictingCheckout: checkoutKey,
|
|
@@ -1246,16 +2414,378 @@ export class RunManager {
|
|
|
1246
2414
|
};
|
|
1247
2415
|
}
|
|
1248
2416
|
}
|
|
1249
|
-
const
|
|
1250
|
-
this.
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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;
|
|
1254
2503
|
});
|
|
1255
|
-
|
|
1256
|
-
|
|
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";
|
|
1257
2788
|
}
|
|
1258
|
-
return { ok: true, provisionalId };
|
|
1259
2789
|
}
|
|
1260
2790
|
clearReservation(provisionalId) {
|
|
1261
2791
|
this.removeActiveEntry(provisionalId, true);
|
|
@@ -1292,7 +2822,9 @@ export class RunManager {
|
|
|
1292
2822
|
return false;
|
|
1293
2823
|
const tracked = await this.ensureResumeTracked(runId);
|
|
1294
2824
|
if (!tracked.ok) {
|
|
1295
|
-
|
|
2825
|
+
log
|
|
2826
|
+
.child({ run_id: runId })
|
|
2827
|
+
.error("resume.track_failed", `resumeStalledSchedules: ${tracked.reason}`);
|
|
1296
2828
|
return false;
|
|
1297
2829
|
}
|
|
1298
2830
|
try {
|
|
@@ -1315,6 +2847,7 @@ export class RunManager {
|
|
|
1315
2847
|
executionMode: this.executionMode,
|
|
1316
2848
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1317
2849
|
initialSchedule: hydrated,
|
|
2850
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1318
2851
|
}).finally(() => {
|
|
1319
2852
|
void syncRunStatusFromStages(this.options.store, runId).catch(() => { });
|
|
1320
2853
|
});
|
|
@@ -1336,16 +2869,27 @@ export class RunManager {
|
|
|
1336
2869
|
}
|
|
1337
2870
|
let checkoutKey;
|
|
1338
2871
|
let durableCheckoutRoot;
|
|
2872
|
+
let resumeCallerId = null;
|
|
2873
|
+
let resumeProjectRoot;
|
|
1339
2874
|
try {
|
|
1340
2875
|
const meta = await this.options.store.readRunMeta(runId);
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
+
}
|
|
1345
2887
|
}
|
|
1346
2888
|
}
|
|
1347
2889
|
catch (err) {
|
|
1348
|
-
|
|
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)}`);
|
|
1349
2893
|
}
|
|
1350
2894
|
const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
|
|
1351
2895
|
if (conflictHolder !== undefined) {
|
|
@@ -1360,6 +2904,8 @@ export class RunManager {
|
|
|
1360
2904
|
this.active.set(runId, {
|
|
1361
2905
|
checkoutKey,
|
|
1362
2906
|
durableCheckoutRoot,
|
|
2907
|
+
projectRoot: resumeProjectRoot,
|
|
2908
|
+
callerId: resumeCallerId,
|
|
1363
2909
|
generation: ++this.trackingGeneration,
|
|
1364
2910
|
});
|
|
1365
2911
|
return { ok: true };
|
|
@@ -1416,7 +2962,7 @@ export class RunManager {
|
|
|
1416
2962
|
catch {
|
|
1417
2963
|
// ignore secondary failures — still fail-closed in memory
|
|
1418
2964
|
}
|
|
1419
|
-
|
|
2965
|
+
log.child({ run_id: runId }).error("attach.quarantined", reason);
|
|
1420
2966
|
}
|
|
1421
2967
|
untrackIfGeneration(runId, generation) {
|
|
1422
2968
|
const entry = this.active.get(runId);
|
|
@@ -1429,6 +2975,7 @@ export class RunManager {
|
|
|
1429
2975
|
if (!entry)
|
|
1430
2976
|
return;
|
|
1431
2977
|
this.active.delete(id);
|
|
2978
|
+
this.schedulingHalts.delete(id);
|
|
1432
2979
|
if (isProvisional) {
|
|
1433
2980
|
this.provisionalIds.delete(id);
|
|
1434
2981
|
}
|
|
@@ -1436,5 +2983,19 @@ export class RunManager {
|
|
|
1436
2983
|
this.checkoutLeases.get(entry.checkoutKey) === id) {
|
|
1437
2984
|
this.checkoutLeases.delete(entry.checkoutKey);
|
|
1438
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;
|
|
1439
3000
|
}
|
|
1440
3001
|
}
|