stageflow 0.25.0 → 0.30.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 +14 -3
- package/builtin-skills/browser/SKILL.md +42 -0
- package/dist/a2a/contracts.d.ts +1 -0
- package/dist/a2a/contracts.js +1 -0
- package/dist/a2a/registry.d.ts +2 -0
- package/dist/a2a/registry.js +20 -1
- package/dist/a2a/server.d.ts +7 -0
- package/dist/a2a/server.js +5 -2
- package/dist/a2a/service.d.ts +1 -1
- package/dist/a2a/service.js +57 -8
- package/dist/a2a/store.d.ts +8 -5
- package/dist/a2a/store.js +24 -67
- package/dist/agent/activity.d.ts +3 -0
- package/dist/agent/bootProviderConfig.d.ts +21 -0
- package/dist/agent/bootProviderConfig.js +74 -0
- package/dist/agent/claudeAdapter.js +2 -2
- package/dist/agent/cursorProvider.d.ts +2 -0
- package/dist/agent/cursorProvider.js +30 -5
- package/dist/agent/fakeAgent.d.ts +1 -0
- package/dist/agent/fakeAgent.js +13 -0
- package/dist/agent/piAdapter.d.ts +11 -24
- package/dist/agent/piAdapter.js +107 -50
- package/dist/agent/piIsolatedMcp.d.ts +2 -0
- package/dist/agent/piIsolatedMcp.js +10 -2
- package/dist/agent/piSessionFactory.d.ts +38 -0
- package/dist/agent/piSessionFactory.js +60 -0
- package/dist/agent/port.d.ts +8 -0
- package/dist/agent/streamLogRedact.d.ts +7 -1
- package/dist/agent/streamLogRedact.js +26 -11
- package/dist/browser/anchor.d.ts +27 -0
- package/dist/browser/anchor.js +127 -0
- package/dist/browser/auditSink.d.ts +38 -0
- package/dist/browser/auditSink.js +34 -0
- package/dist/browser/browserHost.d.ts +86 -0
- package/dist/browser/browserHost.js +7 -0
- package/dist/browser/browserSweep.d.ts +14 -0
- package/dist/browser/browserSweep.js +54 -0
- package/dist/browser/browserTeardown.d.ts +56 -0
- package/dist/browser/browserTeardown.js +235 -0
- package/dist/browser/fakeRemoteBrowserHost.d.ts +2 -0
- package/dist/browser/fakeRemoteBrowserHost.js +34 -0
- package/dist/browser/gateHandoff.d.ts +25 -0
- package/dist/browser/gateHandoff.js +83 -0
- package/dist/browser/humanLogin.d.ts +18 -0
- package/dist/browser/humanLogin.js +34 -0
- package/dist/browser/keyProvider.d.ts +10 -0
- package/dist/browser/keyProvider.js +39 -0
- package/dist/browser/localBrowserHost.d.ts +10 -0
- package/dist/browser/localBrowserHost.js +152 -0
- package/dist/browser/localProfileLock.d.ts +2 -0
- package/dist/browser/localProfileLock.js +218 -0
- package/dist/browser/localProfileStore.d.ts +5 -0
- package/dist/browser/localProfileStore.js +91 -0
- package/dist/browser/loginCheck.d.ts +53 -0
- package/dist/browser/loginCheck.js +147 -0
- package/dist/browser/memoryProfileLock.d.ts +2 -0
- package/dist/browser/memoryProfileLock.js +53 -0
- package/dist/browser/memoryProfileStore.d.ts +2 -0
- package/dist/browser/memoryProfileStore.js +31 -0
- package/dist/browser/navigationAudit.d.ts +16 -0
- package/dist/browser/navigationAudit.js +53 -0
- package/dist/browser/persistedEnv.d.ts +3 -0
- package/dist/browser/persistedEnv.js +15 -0
- package/dist/browser/profileLock.d.ts +40 -0
- package/dist/browser/profileLock.js +10 -0
- package/dist/browser/profileStore.d.ts +23 -0
- package/dist/browser/profileStore.js +39 -0
- package/dist/browser/runLiveness.d.ts +13 -0
- package/dist/browser/runLiveness.js +39 -0
- package/dist/browser/sitePolicy.d.ts +10 -0
- package/dist/browser/sitePolicy.js +61 -0
- package/dist/browser/stageBrowserEnv.d.ts +22 -0
- package/dist/browser/stageBrowserEnv.js +147 -0
- package/dist/browser/stageProfileLock.d.ts +17 -0
- package/dist/browser/stageProfileLock.js +39 -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/browserCommand.d.ts +44 -0
- package/dist/cli/browserCommand.js +472 -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 +41 -3
- package/dist/cli/hostClient.js +129 -14
- package/dist/cli/initCommand.d.ts +1 -0
- package/dist/cli/initCommand.js +23 -7
- package/dist/cli/initTemplates.d.ts +5 -2
- package/dist/cli/initTemplates.js +32 -2
- package/dist/cli/resolveSafeOutPath.d.ts +2 -0
- package/dist/cli/resolveSafeOutPath.js +40 -0
- package/dist/cli/restoreCommand.d.ts +10 -0
- package/dist/cli/restoreCommand.js +90 -0
- package/dist/cli/runCommand.d.ts +3 -2
- package/dist/cli/runCommand.js +93 -11
- package/dist/cli/runOutput.d.ts +4 -0
- package/dist/cli/runOutput.js +33 -7
- package/dist/cli/runStageCommand.d.ts +1 -0
- package/dist/cli/runStageCommand.js +44 -25
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +168 -10
- package/dist/cli/triggerCommand.d.ts +12 -0
- package/dist/cli/triggerCommand.js +183 -0
- package/dist/cli/validateCommand.d.ts +1 -1
- package/dist/cli/validateCommand.js +23 -6
- package/dist/cli.js +202 -32
- package/dist/config/browseCatalog.d.ts +1 -0
- package/dist/config/browseCatalog.js +5 -4
- package/dist/config/builtinSkills.d.ts +4 -0
- package/dist/config/builtinSkills.js +23 -0
- package/dist/config/catalogRelativePath.d.ts +63 -0
- package/dist/config/catalogRelativePath.js +126 -0
- package/dist/config/configOrigin.d.ts +27 -0
- package/dist/config/configOrigin.js +59 -0
- package/dist/config/createTrigger.d.ts +27 -0
- package/dist/config/createTrigger.js +232 -0
- package/dist/config/draftPackage.d.ts +117 -0
- package/dist/config/draftPackage.js +550 -0
- package/dist/config/ensureCatalogScanRoot.d.ts +16 -0
- package/dist/config/ensureCatalogScanRoot.js +73 -0
- package/dist/config/hostConfig.d.ts +57 -0
- package/dist/config/hostConfig.js +360 -0
- package/dist/config/listSkills.d.ts +26 -0
- package/dist/config/listSkills.js +206 -5
- package/dist/config/loadOutcome.d.ts +7 -1
- package/dist/config/loadPipeline.js +39 -2
- package/dist/config/loadStage.js +25 -0
- package/dist/config/loadStageflowManifest.js +25 -0
- package/dist/config/loadTask.d.ts +2 -0
- package/dist/config/loadTask.js +57 -18
- package/dist/config/loadTrigger.d.ts +9 -0
- package/dist/config/loadTrigger.js +120 -0
- 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 +32 -0
- package/dist/config/multiProjectCatalog.js +150 -0
- package/dist/config/normalizePipelineStageEntry.js +27 -1
- package/dist/config/parseCompletionContract.js +6 -0
- package/dist/config/pipelineStageKeys.js +3 -0
- package/dist/config/publishDraftPackage.d.ts +7 -0
- package/dist/config/publishDraftPackage.js +25 -0
- package/dist/config/resolveCatalogContext.js +1 -1
- 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/resolvePipelineDag.js +1 -1
- package/dist/config/resolveStageMcpServers.d.ts +9 -1
- package/dist/config/resolveStageMcpServers.js +36 -9
- package/dist/config/scanCatalogPaths.d.ts +1 -1
- package/dist/config/scanCatalogPaths.js +6 -2
- 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/stageBrowser.d.ts +12 -0
- package/dist/config/stageBrowser.js +118 -0
- package/dist/config/toolRequires.d.ts +24 -0
- package/dist/config/toolRequires.js +115 -0
- package/dist/config/triggerCatalog.d.ts +10 -0
- package/dist/config/triggerCatalog.js +37 -0
- package/dist/config/validateCatalog.d.ts +17 -3
- package/dist/config/validateCatalog.js +223 -3
- package/dist/config/validateCompletionContract.js +8 -0
- package/dist/diagnostics/checks.d.ts +39 -0
- package/dist/diagnostics/checks.js +135 -0
- package/dist/envelope/preEmitChecks.d.ts +1 -1
- package/dist/envelope/preEmitChecks.js +8 -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 +17 -0
- package/dist/logging/redact.js +105 -0
- package/dist/mcp/catalogTools.js +492 -81
- package/dist/mcp/checkoutTools.d.ts +52 -0
- package/dist/mcp/checkoutTools.js +332 -0
- package/dist/mcp/controlTools.js +182 -6
- package/dist/mcp/deps.d.ts +3 -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 +2 -0
- package/dist/mcp/tools.js +6 -0
- package/dist/mcp/triggerTools.d.ts +3 -0
- package/dist/mcp/triggerTools.js +176 -0
- package/dist/net/proxy.d.ts +14 -0
- package/dist/net/proxy.js +78 -0
- package/dist/operatorAgent/docsRetrieval.d.ts +40 -0
- package/dist/operatorAgent/docsRetrieval.js +269 -0
- package/dist/operatorAgent/draftContext.d.ts +16 -0
- package/dist/operatorAgent/draftContext.js +68 -0
- package/dist/operatorAgent/fakeHost.d.ts +39 -0
- package/dist/operatorAgent/fakeHost.js +92 -0
- package/dist/operatorAgent/host.d.ts +40 -0
- package/dist/operatorAgent/host.js +179 -0
- package/dist/operatorAgent/index.d.ts +9 -0
- package/dist/operatorAgent/index.js +8 -0
- package/dist/operatorAgent/piModel.d.ts +80 -0
- package/dist/operatorAgent/piModel.js +528 -0
- package/dist/operatorAgent/profiles/workshopAuthor.d.ts +23 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +338 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +6 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +103 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +57 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +623 -0
- package/dist/operatorAgent/proposals.d.ts +32 -0
- package/dist/operatorAgent/proposals.js +144 -0
- package/dist/operatorAgent/types.d.ts +128 -0
- package/dist/operatorAgent/types.js +1 -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/findManifestRoot.d.ts +3 -0
- package/dist/project/findManifestRoot.js +35 -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.d.ts +7 -0
- package/dist/project/resolveStageflowContext.js +46 -30
- 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 +25 -0
- package/dist/runstore/backup.d.ts +52 -0
- package/dist/runstore/backup.js +292 -0
- package/dist/runstore/createStore.d.ts +18 -1
- package/dist/runstore/createStore.js +64 -5
- package/dist/runstore/debugBundle.d.ts +60 -0
- package/dist/runstore/debugBundle.js +143 -0
- package/dist/runstore/diskUsage.d.ts +50 -0
- package/dist/runstore/diskUsage.js +214 -0
- package/dist/runstore/exportRunPayload.d.ts +8 -0
- package/dist/runstore/exportRunPayload.js +23 -0
- package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
- package/dist/runstore/normalizeCatalogPath.js +10 -0
- package/dist/runstore/paths.d.ts +5 -0
- package/dist/runstore/paths.js +145 -1
- package/dist/runstore/port.d.ts +139 -13
- package/dist/runstore/port.js +8 -1
- package/dist/runstore/restore.d.ts +81 -0
- package/dist/runstore/restore.js +355 -0
- package/dist/runstore/retention.d.ts +27 -0
- package/dist/runstore/retention.js +74 -0
- package/dist/runstore/runManifest.d.ts +146 -0
- package/dist/runstore/runManifest.js +288 -0
- package/dist/runstore/runProjection.d.ts +3 -1
- package/dist/runstore/runProjection.js +63 -1
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +35 -3
- package/dist/runstore/sqlite/SqliteRunStore.js +442 -219
- package/dist/runstore/sqlite/applyStorePragmas.d.ts +39 -0
- package/dist/runstore/sqlite/applyStorePragmas.js +145 -0
- package/dist/runstore/sqlite/migrations/001-baseline.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/001-baseline.js +10 -0
- package/dist/runstore/sqlite/migrations/002-repository-binding.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/002-repository-binding.js +29 -0
- package/dist/runstore/sqlite/migrations/003-run-lifecycle.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/003-run-lifecycle.js +28 -0
- package/dist/runstore/sqlite/migrations/004-auto-resume-count.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/004-auto-resume-count.js +19 -0
- package/dist/runstore/sqlite/migrations/005-config-origins.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/005-config-origins.js +19 -0
- package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/006-pipeline-body-and-caller.js +28 -0
- package/dist/runstore/sqlite/migrations/007-projects-registry.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/007-projects-registry.js +31 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/008-triggers-table.js +19 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/009-trigger-next-run.js +19 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.d.ts +7 -0
- package/dist/runstore/sqlite/migrations/010-trigger-adapter-state.js +17 -0
- package/dist/runstore/sqlite/migrations/index.d.ts +21 -0
- package/dist/runstore/sqlite/migrations/index.js +78 -0
- package/dist/runstore/sqlite/schema.d.ts +20 -1
- package/dist/runstore/sqlite/schema.js +277 -1
- package/dist/runstore/sqlite/storeOpenError.d.ts +6 -0
- package/dist/runstore/sqlite/storeOpenError.js +11 -0
- package/dist/runstore/sqlite/storeSchemaError.d.ts +5 -0
- package/dist/runstore/sqlite/storeSchemaError.js +8 -0
- package/dist/runstore/stageExecution.js +3 -0
- package/dist/runstore/storeFilesystem.d.ts +32 -0
- package/dist/runstore/storeFilesystem.js +148 -0
- package/dist/runstore/trackProjection.js +2 -0
- package/dist/runstore/workspaceLayout.d.ts +13 -0
- package/dist/runstore/workspaceLayout.js +76 -0
- package/dist/runtime/answerResume.d.ts +1 -0
- package/dist/runtime/answerResume.js +3 -1
- package/dist/runtime/completionCheckRunner.d.ts +15 -1
- package/dist/runtime/completionCheckRunner.js +67 -7
- package/dist/runtime/containerLimits.d.ts +16 -0
- package/dist/runtime/containerLimits.js +84 -0
- package/dist/runtime/credentialBinding.d.ts +2 -0
- package/dist/runtime/credentialBinding.js +3 -0
- package/dist/runtime/credentialMaterialisation.d.ts +28 -0
- package/dist/runtime/credentialMaterialisation.js +128 -0
- package/dist/runtime/emailSource.d.ts +72 -0
- package/dist/runtime/emailSource.js +304 -0
- package/dist/runtime/gitCheckoutCapability.js +7 -13
- package/dist/runtime/githubPollSource.d.ts +86 -0
- package/dist/runtime/githubPollSource.js +284 -0
- package/dist/runtime/pipelineRunner.d.ts +40 -1
- package/dist/runtime/pipelineRunner.js +181 -17
- package/dist/runtime/pipelineScheduler.d.ts +17 -0
- package/dist/runtime/pipelineScheduler.js +220 -28
- package/dist/runtime/reloadRunCatalog.js +6 -1
- package/dist/runtime/repositoryMaterialize.d.ts +47 -0
- package/dist/runtime/repositoryMaterialize.js +235 -0
- package/dist/runtime/resumeTimedOut.d.ts +1 -1
- package/dist/runtime/resumeTimedOut.js +5 -2
- package/dist/runtime/runChangeBus.js +9 -0
- package/dist/runtime/runDeletion.d.ts +7 -0
- package/dist/runtime/runDeletion.js +37 -0
- package/dist/runtime/runManager.d.ts +169 -4
- package/dist/runtime/runManager.js +1748 -83
- package/dist/runtime/runRetentionSweep.d.ts +31 -0
- package/dist/runtime/runRetentionSweep.js +328 -0
- package/dist/runtime/runRetryCoordinator.d.ts +4 -0
- package/dist/runtime/runRetryCoordinator.js +20 -2
- package/dist/runtime/runSkills.d.ts +40 -0
- package/dist/runtime/runSkills.js +173 -0
- package/dist/runtime/scheduleSource.d.ts +38 -0
- package/dist/runtime/scheduleSource.js +119 -0
- package/dist/runtime/settingsFile.d.ts +5 -0
- package/dist/runtime/settingsFile.js +61 -54
- package/dist/runtime/stageAttemptBootstrap.d.ts +5 -1
- package/dist/runtime/stageAttemptBootstrap.js +250 -27
- 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 +194 -0
- package/dist/runtime/stageHitl.d.ts +1 -0
- package/dist/runtime/stageHitl.js +5 -1
- package/dist/runtime/stageProcessLauncher.d.ts +20 -0
- package/dist/runtime/stageProcessLauncher.js +236 -41
- 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 +6 -0
- package/dist/runtime/stageRunner.js +32 -6
- 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/stageStreamLog.d.ts +1 -0
- package/dist/runtime/stageStreamLog.js +2 -1
- 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/triggerCatalog.d.ts +6 -0
- package/dist/runtime/triggerCatalog.js +24 -0
- package/dist/runtime/triggerEventMatch.d.ts +2 -0
- package/dist/runtime/triggerEventMatch.js +6 -0
- package/dist/runtime/triggerPort.d.ts +14 -0
- package/dist/runtime/triggerPort.js +1 -0
- package/dist/runtime/triggerRunner.d.ts +17 -0
- package/dist/runtime/triggerRunner.js +89 -0
- package/dist/runtime/verifiedStageExecution.d.ts +8 -0
- package/dist/runtime/verifiedStageExecution.js +16 -1
- package/dist/runtime/webhookSignature.d.ts +1 -0
- package/dist/runtime/webhookSignature.js +12 -0
- 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 +42 -2
- package/dist/server/bootstrap.js +273 -5
- package/dist/server/controlToken.d.ts +47 -0
- package/dist/server/controlToken.js +254 -0
- package/dist/server/createHttpHost.d.ts +11 -2
- package/dist/server/createHttpHost.js +106 -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 +123 -0
- package/dist/server/http.d.ts +27 -1
- package/dist/server/http.js +2254 -477
- 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/server/triggerRoutes.d.ts +15 -0
- package/dist/server/triggerRoutes.js +268 -0
- package/dist/tools/askOperator.d.ts +3 -2
- package/dist/types/completion.d.ts +3 -0
- package/dist/types/pipeline.d.ts +10 -1
- package/dist/types/preEmitCheck.d.ts +7 -0
- package/dist/types/stage.d.ts +22 -0
- package/dist/types/stageflowManifest.d.ts +3 -0
- package/dist/types/task.d.ts +8 -0
- package/dist/types/trigger.d.ts +18 -0
- package/dist/types/trigger.js +1 -0
- package/dist/ui/assets/{index-CSsDued1.css → index-Bv4p1aPK.css} +1 -1
- package/dist/ui/assets/index-CntB4bZR.js +133 -0
- package/dist/ui/index.html +2 -2
- package/dist/workshop/autosave.d.ts +58 -0
- package/dist/workshop/autosave.js +222 -0
- package/dist/workshop/buildStore.d.ts +79 -0
- package/dist/workshop/buildStore.js +326 -0
- package/dist/workshop/chatTurn.d.ts +232 -0
- package/dist/workshop/chatTurn.js +475 -0
- package/dist/workshop/modelSettings.d.ts +18 -0
- package/dist/workshop/modelSettings.js +44 -0
- package/dist/workshop/sessionStore.d.ts +61 -0
- package/dist/workshop/sessionStore.js +254 -0
- package/dist/workshop/workshopPin.d.ts +72 -0
- package/dist/workshop/workshopPin.js +193 -0
- package/examples/README.md +99 -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/browser-session/README.md +77 -0
- package/examples/browser-session/browser-session.pipeline.yaml +50 -0
- package/examples/browser-session/browser-session.task.yaml +6 -0
- package/examples/browser-session/check-login.yaml +22 -0
- package/examples/browser-session/fixture-server.mjs +64 -0
- package/examples/browser-session/human-login.yaml +31 -0
- package/examples/browser-session/merge.yaml +36 -0
- package/examples/browser-session/work-a.yaml +41 -0
- package/examples/browser-session/work-b.yaml +41 -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 +145 -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/stageflow.yaml +51 -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 +9 -1
- package/skills/stageflow/SKILL.md +6 -6
- package/skills/stageflow/references/control-surface.md +13 -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-author/references/stage-prompt-template.md +4 -4
- package/skills/stageflow-run/SKILL.md +22 -12
- package/skills/stageflow-run/references/mcp-call.md +9 -1
- package/skills/stageflow-run/references/task-and-pipeline-selection.md +19 -6
- 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 +51 -8
- package/dist/ui/assets/index-Da5EHU6M.js +0 -119
|
@@ -1,52 +1,257 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
2
3
|
import { RunSubmissionExistsError } from "../runstore/submission.js";
|
|
3
|
-
import { readFile, realpath } from "node:fs/promises";
|
|
4
|
+
import { access, readFile, realpath } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { findProjectRoot } from "../project/findProjectRoot.js";
|
|
6
|
-
import {
|
|
7
|
+
import { globalStageflowHome } from "../project/globalHome.js";
|
|
8
|
+
import { normalizeCatalogPath, normalizeProjectRoot } from "../runstore/normalizeCatalogPath.js";
|
|
9
|
+
import { durableRootDiskBreakdown, readFilesystemSize, refreshRunDiskUsage, resolveMinFreeDiskFloor, } from "../runstore/diskUsage.js";
|
|
10
|
+
import { newRunId } from "../runstore/paths.js";
|
|
7
11
|
import { loadRunContext } from "./resumeReconstruct.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
12
|
+
import { assertResumableStage, reconstructTimedOutAndContinue, } from "./resumeTimedOut.js";
|
|
13
|
+
import { loadTaskFromYamlOutcome } from "../config/loadTask.js";
|
|
14
|
+
import { buildValidationResult, loadPipelineValidated, } from "../config/validateCatalog.js";
|
|
10
15
|
import { deriveStatusFromStages, findUnhandledFailedStage, } from "../runstore/port.js";
|
|
11
|
-
import {
|
|
12
|
-
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";
|
|
20
|
+
import { hydrateScheduleFromStore, hydratedScheduleHasRunnableWork, resumeRun, retryRun, runPipelineDag, } from "./pipelineScheduler.js";
|
|
13
21
|
import { resolveFeedbackLoopDecision, } from "./feedbackLoopDecision.js";
|
|
14
22
|
import { RunRetryCoordinator, readRetryRootWaitTimeoutMs, } from "./runRetryCoordinator.js";
|
|
15
23
|
import { readMaxActiveStageProcesses, readMaxActiveStagesPerRun, readStageExecutionMode, } from "./stageConcurrency.js";
|
|
24
|
+
import { isHumanLoginStage } from "../browser/humanLogin.js";
|
|
25
|
+
import { defaultStageBrowserSupport, resolveStageBrowserEnv, } from "../browser/stageBrowserEnv.js";
|
|
26
|
+
import { teardownRunBrowsers, teardownStageBrowser, } from "../browser/browserTeardown.js";
|
|
27
|
+
import { createRunLiveness } from "../browser/runLiveness.js";
|
|
28
|
+
import { acquireStageProfile, stageProfileLock, } from "../browser/stageProfileLock.js";
|
|
29
|
+
import { sweepOrphanBrowserSessions } from "../browser/browserSweep.js";
|
|
30
|
+
import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
16
31
|
import { StageProcessLauncher } from "./stageProcessLauncher.js";
|
|
32
|
+
import { logger as rootLogger } from "../logging/logger.js";
|
|
33
|
+
import { registerNamedSecrets } from "../logging/namedSecrets.js";
|
|
34
|
+
import { shouldRegisterValue } from "../logging/redact.js";
|
|
35
|
+
import { bindingFromFields, buildInitialRunManifest, collectDeclaredSecretValues, finaliseStoredRunManifest, } from "../runstore/runManifest.js";
|
|
36
|
+
import { getRequestAuth } from "../server/requestAuthContext.js";
|
|
37
|
+
import { STAGE_ENV_PASSTHROUGH } from "./stageEnvironment.js";
|
|
38
|
+
import { proxyHealthFields } from "../net/proxy.js";
|
|
39
|
+
import { getContainerLimits } from "./containerLimits.js";
|
|
40
|
+
import { stageflowCacheRoot } from "./stageCacheEnv.js";
|
|
41
|
+
import { assertClaudeNotRoot, ClaudeRootError } from "../preflight/claudeRoot.js";
|
|
42
|
+
import { preflightFailureCode, runPipelinePreflight, } from "../preflight/pipelinePreflight.js";
|
|
43
|
+
import { asAgentBackendId } from "../agent/agentBackend.js";
|
|
17
44
|
import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, readMaxConcurrentFromGlobal, writeMaxConcurrentToGlobal, } from "./settingsFile.js";
|
|
18
45
|
import { StageHitlController, waitKey, } from "./stageHitl.js";
|
|
19
|
-
import { resolveAndValidateCheckout, } from "./stageRoots.js";
|
|
46
|
+
import { resolveAndValidateCheckout, resolveEffectiveGitIdentity, stageBindingEnvFromRun, } from "./stageRoots.js";
|
|
20
47
|
import { orchestrateAnswerResume } from "./answerResume.js";
|
|
21
48
|
import { reconstructAndContinue as resumeReconstructAndContinue } from "./resumeReconstruct.js";
|
|
22
|
-
import { resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
23
|
-
import { resolveStartTaskInput } from "./taskInput.js";
|
|
24
|
-
import {
|
|
49
|
+
import { attemptContext, resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
50
|
+
import { checkTaskEntryInput, resolveStartTaskInput } from "./taskInput.js";
|
|
51
|
+
import { markStageInterrupted, OPERATOR_CANCEL_REASON, syncRunStatusFromStages, } from "./stageRecovery.js";
|
|
52
|
+
import { deleteRunEverywhere } from "./runDeletion.js";
|
|
53
|
+
import { runRetentionSweep, } from "./runRetentionSweep.js";
|
|
25
54
|
import { blocksGenericRetry, manualRecoveryEligibility, readManualRecoveryState, } from "./manualRecoveryState.js";
|
|
55
|
+
import { resolveWorkspaceBinding } from "./workspaceBinding.js";
|
|
56
|
+
import { derivedBindingKindFromMeta, materializeWorkspaceBinding, StartLinkError, } from "./repositoryMaterialize.js";
|
|
57
|
+
export const PROJECT_ROOT_UNAVAILABLE_REASON = "project_root_unavailable";
|
|
58
|
+
export const INSUFFICIENT_DISK_CANCEL_REASON = "insufficient_disk";
|
|
26
59
|
function existingSubmissionResult(existing, request) {
|
|
27
60
|
if (existing.requestHash !== request.requestHash) {
|
|
28
61
|
return { ok: false, status: 409, reason: "Submission key was already used for different input" };
|
|
29
62
|
}
|
|
30
63
|
return { ok: true, runId: existing.runId, reused: true };
|
|
31
64
|
}
|
|
65
|
+
export const FORCE_DELETE_CANCEL_REASON = "delete_run: force";
|
|
32
66
|
const DEFAULT_MAX_CONCURRENT = 3;
|
|
33
|
-
const
|
|
67
|
+
const DEFAULT_MAX_QUEUED = 32;
|
|
68
|
+
const DEFAULT_MAX_AUTO_RESUMES = 3;
|
|
69
|
+
const STARTUP_RECONCILE_REASON = "orphaned_no_worker";
|
|
70
|
+
const AUTO_RESUME_CAPPED_REASON = "auto_resume_capped";
|
|
71
|
+
export const STAGEFLOW_AUTO_RESUME_INTERRUPTED = "STAGEFLOW_AUTO_RESUME_INTERRUPTED";
|
|
72
|
+
export const STAGEFLOW_MAX_AUTO_RESUMES = "STAGEFLOW_MAX_AUTO_RESUMES";
|
|
34
73
|
const OPERATOR_ABANDON_REASON = "process_interrupted: operator abandoned stage";
|
|
74
|
+
const log = rootLogger.child({ component: "runtime" });
|
|
35
75
|
function parseMaxConcurrent(raw) {
|
|
36
76
|
if (raw === undefined || raw.trim() === "")
|
|
37
77
|
return DEFAULT_MAX_CONCURRENT;
|
|
38
78
|
const n = Number.parseInt(raw, 10);
|
|
39
|
-
if (!Number.isFinite(n) || n < 1)
|
|
40
|
-
|
|
79
|
+
if (!Number.isFinite(n) || n < 1 || String(n) !== raw.trim()) {
|
|
80
|
+
throw new Error(`Invalid value for STAGEFLOW_MAX_CONCURRENT_RUNS: ${JSON.stringify(raw)}`);
|
|
81
|
+
}
|
|
82
|
+
return n;
|
|
83
|
+
}
|
|
84
|
+
function parseMaxQueued(raw) {
|
|
85
|
+
if (raw === undefined || raw.trim() === "")
|
|
86
|
+
return DEFAULT_MAX_QUEUED;
|
|
87
|
+
const n = Number.parseInt(raw, 10);
|
|
88
|
+
if (!Number.isFinite(n) || n < 0)
|
|
89
|
+
return DEFAULT_MAX_QUEUED;
|
|
90
|
+
return n;
|
|
91
|
+
}
|
|
92
|
+
export function isAutoResumeInterruptedEnabled(env = process.env) {
|
|
93
|
+
const raw = env[STAGEFLOW_AUTO_RESUME_INTERRUPTED];
|
|
94
|
+
if (raw === undefined || raw.trim() === "")
|
|
95
|
+
return false;
|
|
96
|
+
const normalized = raw.trim().toLowerCase();
|
|
97
|
+
return normalized !== "0" && normalized !== "false";
|
|
98
|
+
}
|
|
99
|
+
export function parseMaxAutoResumes(env = process.env) {
|
|
100
|
+
const raw = env[STAGEFLOW_MAX_AUTO_RESUMES];
|
|
101
|
+
if (raw === undefined || raw.trim() === "")
|
|
102
|
+
return DEFAULT_MAX_AUTO_RESUMES;
|
|
103
|
+
const n = Number.parseInt(raw, 10);
|
|
104
|
+
if (!Number.isFinite(n) || n < 0)
|
|
105
|
+
return DEFAULT_MAX_AUTO_RESUMES;
|
|
41
106
|
return n;
|
|
42
107
|
}
|
|
108
|
+
/** Private FIFO-per-project_root + round-robin dequeue (KTD3). */
|
|
109
|
+
class AdmissionQueue {
|
|
110
|
+
byRoot = new Map();
|
|
111
|
+
rrOrder = [];
|
|
112
|
+
rrIndex = 0;
|
|
113
|
+
get size() {
|
|
114
|
+
let n = 0;
|
|
115
|
+
for (const list of this.byRoot.values())
|
|
116
|
+
n += list.length;
|
|
117
|
+
return n;
|
|
118
|
+
}
|
|
119
|
+
clear() {
|
|
120
|
+
this.byRoot.clear();
|
|
121
|
+
this.rrOrder = [];
|
|
122
|
+
this.rrIndex = 0;
|
|
123
|
+
}
|
|
124
|
+
enqueue(projectRoot, entry) {
|
|
125
|
+
let list = this.byRoot.get(projectRoot);
|
|
126
|
+
if (list === undefined) {
|
|
127
|
+
list = [];
|
|
128
|
+
this.byRoot.set(projectRoot, list);
|
|
129
|
+
this.rrOrder.push(projectRoot);
|
|
130
|
+
}
|
|
131
|
+
list.push(entry);
|
|
132
|
+
return this.size;
|
|
133
|
+
}
|
|
134
|
+
requeueFront(projectRoot, entry) {
|
|
135
|
+
let list = this.byRoot.get(projectRoot);
|
|
136
|
+
if (list === undefined) {
|
|
137
|
+
list = [];
|
|
138
|
+
this.byRoot.set(projectRoot, list);
|
|
139
|
+
this.rrOrder.push(projectRoot);
|
|
140
|
+
}
|
|
141
|
+
list.unshift(entry);
|
|
142
|
+
}
|
|
143
|
+
remove(runId) {
|
|
144
|
+
for (const [root, list] of this.byRoot) {
|
|
145
|
+
const idx = list.findIndex((e) => e.runId === runId);
|
|
146
|
+
if (idx < 0)
|
|
147
|
+
continue;
|
|
148
|
+
list.splice(idx, 1);
|
|
149
|
+
if (list.length === 0) {
|
|
150
|
+
this.byRoot.delete(root);
|
|
151
|
+
const orderIdx = this.rrOrder.indexOf(root);
|
|
152
|
+
if (orderIdx >= 0) {
|
|
153
|
+
this.rrOrder.splice(orderIdx, 1);
|
|
154
|
+
if (this.rrOrder.length === 0) {
|
|
155
|
+
this.rrIndex = 0;
|
|
156
|
+
}
|
|
157
|
+
else if (orderIdx < this.rrIndex) {
|
|
158
|
+
this.rrIndex -= 1;
|
|
159
|
+
}
|
|
160
|
+
else if (this.rrIndex >= this.rrOrder.length) {
|
|
161
|
+
this.rrIndex = 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
positionOf(runId) {
|
|
170
|
+
let position = 0;
|
|
171
|
+
const rootCount = this.rrOrder.length;
|
|
172
|
+
if (rootCount === 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
const heads = this.rrOrder.map((root) => ({
|
|
175
|
+
root,
|
|
176
|
+
list: this.byRoot.get(root) ?? [],
|
|
177
|
+
i: 0,
|
|
178
|
+
}));
|
|
179
|
+
let rr = this.rrIndex % rootCount;
|
|
180
|
+
let remaining = this.size;
|
|
181
|
+
while (remaining > 0) {
|
|
182
|
+
let advanced = false;
|
|
183
|
+
for (let step = 0; step < rootCount; step++) {
|
|
184
|
+
const slot = heads[(rr + step) % rootCount];
|
|
185
|
+
if (slot === undefined || slot.i >= slot.list.length)
|
|
186
|
+
continue;
|
|
187
|
+
const entry = slot.list[slot.i];
|
|
188
|
+
slot.i += 1;
|
|
189
|
+
remaining -= 1;
|
|
190
|
+
position += 1;
|
|
191
|
+
advanced = true;
|
|
192
|
+
if (entry?.runId === runId)
|
|
193
|
+
return position;
|
|
194
|
+
rr = (rr + step + 1) % rootCount;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (!advanced)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
dequeueNext(skipRoots, skipCallers) {
|
|
203
|
+
if (this.rrOrder.length === 0)
|
|
204
|
+
return undefined;
|
|
205
|
+
const start = this.rrIndex % this.rrOrder.length;
|
|
206
|
+
for (let step = 0; step < this.rrOrder.length; step++) {
|
|
207
|
+
const idx = (start + step) % this.rrOrder.length;
|
|
208
|
+
const root = this.rrOrder[idx];
|
|
209
|
+
if (root === undefined)
|
|
210
|
+
continue;
|
|
211
|
+
if (skipRoots?.has(root))
|
|
212
|
+
continue;
|
|
213
|
+
const list = this.byRoot.get(root);
|
|
214
|
+
if (list === undefined || list.length === 0)
|
|
215
|
+
continue;
|
|
216
|
+
let pickIdx = 0;
|
|
217
|
+
if (skipCallers !== undefined && skipCallers.size > 0) {
|
|
218
|
+
pickIdx = list.findIndex((e) => {
|
|
219
|
+
const c = e.callerId;
|
|
220
|
+
if (c === undefined || c === null || c === "")
|
|
221
|
+
return true;
|
|
222
|
+
return !skipCallers.has(c.toLowerCase());
|
|
223
|
+
});
|
|
224
|
+
if (pickIdx < 0)
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const [entry] = list.splice(pickIdx, 1);
|
|
228
|
+
if (entry === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
if (list.length === 0) {
|
|
231
|
+
this.byRoot.delete(root);
|
|
232
|
+
this.rrOrder.splice(idx, 1);
|
|
233
|
+
if (this.rrOrder.length === 0) {
|
|
234
|
+
this.rrIndex = 0;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
this.rrIndex = idx % this.rrOrder.length;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
this.rrIndex = (idx + 1) % this.rrOrder.length;
|
|
242
|
+
}
|
|
243
|
+
return { projectRoot: root, entry };
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
43
248
|
async function toCheckoutLeaseKey(absPath) {
|
|
44
249
|
try {
|
|
45
250
|
return await realpath(absPath);
|
|
46
251
|
}
|
|
47
252
|
catch {
|
|
48
253
|
const fallback = path.resolve(absPath);
|
|
49
|
-
|
|
254
|
+
log.error("checkout.realpath_failed", `invariant: checkout realpath failed for ${absPath}; using path.resolve fallback ${fallback}`, { abs_path: absPath, fallback });
|
|
50
255
|
return fallback;
|
|
51
256
|
}
|
|
52
257
|
}
|
|
@@ -54,17 +259,26 @@ export class RunManager {
|
|
|
54
259
|
options;
|
|
55
260
|
submissionsInFlight = new Map();
|
|
56
261
|
active = new Map();
|
|
262
|
+
schedulingHalts = new Map();
|
|
57
263
|
checkoutLeases = new Map();
|
|
58
264
|
provisionalIds = new Set();
|
|
265
|
+
admissionQueue = new AdmissionQueue();
|
|
266
|
+
pendingQueuedStarts = new Map();
|
|
267
|
+
queuedDone = new Map();
|
|
268
|
+
admissionDrainInFlight = false;
|
|
59
269
|
resumeInFlight = new Set();
|
|
60
270
|
retryInFlight = new Set();
|
|
61
271
|
retryStartOwner = new Map();
|
|
62
272
|
retryStartWaiters = new Map();
|
|
63
273
|
trackingGeneration = 0;
|
|
64
274
|
maxConcurrent;
|
|
275
|
+
maxQueued;
|
|
276
|
+
maxConcurrentPerProject;
|
|
277
|
+
callerQuotas;
|
|
65
278
|
maxActiveStagesPerRun;
|
|
66
279
|
executionMode;
|
|
67
280
|
stageProcessLauncher;
|
|
281
|
+
a2aStore;
|
|
68
282
|
hitl;
|
|
69
283
|
attachedWaiting = new Set();
|
|
70
284
|
retryCoordinator = new RunRetryCoordinator();
|
|
@@ -98,15 +312,24 @@ export class RunManager {
|
|
|
98
312
|
cwd;
|
|
99
313
|
projectRoot;
|
|
100
314
|
isGitProject;
|
|
315
|
+
acceptingWork = true;
|
|
101
316
|
constructor(options) {
|
|
102
317
|
this.options = options;
|
|
103
318
|
this.cwd = options.cwd ?? process.cwd();
|
|
104
319
|
this.projectRoot = options.projectRoot ?? this.cwd;
|
|
105
320
|
this.isGitProject = options.isGitProject ?? false;
|
|
321
|
+
this.a2aStore = options.a2aStore;
|
|
106
322
|
this.maxConcurrent =
|
|
107
323
|
options.maxConcurrent ??
|
|
108
324
|
readMaxConcurrentFromGlobal() ??
|
|
109
325
|
parseMaxConcurrent(process.env.STAGEFLOW_MAX_CONCURRENT_RUNS);
|
|
326
|
+
this.maxQueued =
|
|
327
|
+
options.maxQueued ?? parseMaxQueued(process.env.STAGEFLOW_MAX_QUEUED);
|
|
328
|
+
this.maxConcurrentPerProject = options.maxConcurrentPerProject;
|
|
329
|
+
this.callerQuotas = new Map(Object.entries(options.callerQuotas ?? {}).map(([id, n]) => [
|
|
330
|
+
id.toLowerCase(),
|
|
331
|
+
n,
|
|
332
|
+
]));
|
|
110
333
|
this.maxActiveStagesPerRun = readMaxActiveStagesPerRun(process.env, options.maxActiveStagesPerRun);
|
|
111
334
|
this.executionMode = readStageExecutionMode(process.env, options.executionMode);
|
|
112
335
|
this.stageProcessLauncher =
|
|
@@ -133,6 +356,57 @@ export class RunManager {
|
|
|
133
356
|
getActiveCount() {
|
|
134
357
|
return this.active.size;
|
|
135
358
|
}
|
|
359
|
+
stopAcceptingWork() {
|
|
360
|
+
this.acceptingWork = false;
|
|
361
|
+
for (const halt of this.schedulingHalts.values()) {
|
|
362
|
+
halt.halted = true;
|
|
363
|
+
halt.hostShutdown = true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
isAcceptingWork() {
|
|
367
|
+
return this.acceptingWork;
|
|
368
|
+
}
|
|
369
|
+
async drainActiveStages(options) {
|
|
370
|
+
for (const halt of this.schedulingHalts.values()) {
|
|
371
|
+
halt.halted = true;
|
|
372
|
+
halt.hostShutdown = true;
|
|
373
|
+
}
|
|
374
|
+
const launcher = this.stageProcessLauncher;
|
|
375
|
+
if (launcher === undefined || launcher.activeCount() === 0) {
|
|
376
|
+
return { forced: false };
|
|
377
|
+
}
|
|
378
|
+
const previouslyActive = launcher.getActiveStageProcesses();
|
|
379
|
+
const seenRuns = new Set();
|
|
380
|
+
for (const { runId, stageId } of previouslyActive) {
|
|
381
|
+
await markStageInterrupted({
|
|
382
|
+
store: this.options.store,
|
|
383
|
+
runId,
|
|
384
|
+
stageId,
|
|
385
|
+
reason: "host_shutdown",
|
|
386
|
+
status: "interrupted",
|
|
387
|
+
});
|
|
388
|
+
seenRuns.add(runId);
|
|
389
|
+
}
|
|
390
|
+
for (const runId of seenRuns) {
|
|
391
|
+
await syncRunStatusFromStages(this.options.store, runId).catch(() => undefined);
|
|
392
|
+
}
|
|
393
|
+
launcher.signalAllActive("SIGTERM");
|
|
394
|
+
while (launcher.activeCount() > 0 &&
|
|
395
|
+
Date.now() < options.deadlineMs &&
|
|
396
|
+
!(options.isEscalated?.() ?? false)) {
|
|
397
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
398
|
+
}
|
|
399
|
+
const remaining = launcher.getActiveStageProcesses();
|
|
400
|
+
if (remaining.length === 0) {
|
|
401
|
+
return { forced: false };
|
|
402
|
+
}
|
|
403
|
+
launcher.signalAllActive("SIGKILL");
|
|
404
|
+
const killWaitUntil = Date.now() + 500;
|
|
405
|
+
while (launcher.activeCount() > 0 && Date.now() < killWaitUntil) {
|
|
406
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
407
|
+
}
|
|
408
|
+
return { forced: true };
|
|
409
|
+
}
|
|
136
410
|
getActiveRunIds() {
|
|
137
411
|
return [...this.active.keys()];
|
|
138
412
|
}
|
|
@@ -142,6 +416,7 @@ export class RunManager {
|
|
|
142
416
|
const maxConcurrent = this.getMaxConcurrent();
|
|
143
417
|
const maxActiveStageProcessesRaw = readMaxActiveStageProcesses(process.env);
|
|
144
418
|
const activeStageProcesses = this.stageProcessLauncher?.activeCount() ?? 0;
|
|
419
|
+
const limits = getContainerLimits();
|
|
145
420
|
return {
|
|
146
421
|
ok: true,
|
|
147
422
|
activeRunIds,
|
|
@@ -152,8 +427,83 @@ export class RunManager {
|
|
|
152
427
|
maxActiveStageProcesses: Number.isFinite(maxActiveStageProcessesRaw)
|
|
153
428
|
? maxActiveStageProcessesRaw
|
|
154
429
|
: null,
|
|
430
|
+
stage_env_passthrough: process.env[STAGE_ENV_PASSTHROUGH]?.trim() === "all",
|
|
431
|
+
proxy: proxyHealthFields(process.env),
|
|
432
|
+
container: {
|
|
433
|
+
max_old_space_size_mb: limits.maxOldSpaceSizeMb,
|
|
434
|
+
max_active_stage_processes: limits.maxActiveStageProcesses,
|
|
435
|
+
memory_limit_bytes: limits.memoryLimitBytes ?? null,
|
|
436
|
+
source: limits.source,
|
|
437
|
+
},
|
|
438
|
+
cache: { root: stageflowCacheRoot() },
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
getPerProjectCapacity() {
|
|
442
|
+
const byRoot = new Map();
|
|
443
|
+
for (const entry of this.active.values()) {
|
|
444
|
+
const root = entry.projectRoot ?? this.projectRoot;
|
|
445
|
+
byRoot.set(root, (byRoot.get(root) ?? 0) + 1);
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
maxConcurrent: this.maxConcurrentPerProject,
|
|
449
|
+
projects: [...byRoot.entries()].map(([project_root, activeCount]) => ({
|
|
450
|
+
project_root,
|
|
451
|
+
activeCount,
|
|
452
|
+
maxConcurrent: this.maxConcurrentPerProject,
|
|
453
|
+
})),
|
|
155
454
|
};
|
|
156
455
|
}
|
|
456
|
+
countActiveForProject(projectRoot) {
|
|
457
|
+
const normalized = normalizeCatalogPath(projectRoot);
|
|
458
|
+
let n = 0;
|
|
459
|
+
for (const entry of this.active.values()) {
|
|
460
|
+
const root = normalizeCatalogPath(entry.projectRoot ?? this.projectRoot);
|
|
461
|
+
if (root === normalized)
|
|
462
|
+
n += 1;
|
|
463
|
+
}
|
|
464
|
+
return n;
|
|
465
|
+
}
|
|
466
|
+
countActiveForCaller(callerId) {
|
|
467
|
+
const normalized = callerId.toLowerCase();
|
|
468
|
+
let n = 0;
|
|
469
|
+
for (const entry of this.active.values()) {
|
|
470
|
+
if (entry.callerId?.toLowerCase() === normalized)
|
|
471
|
+
n += 1;
|
|
472
|
+
}
|
|
473
|
+
return n;
|
|
474
|
+
}
|
|
475
|
+
callerQuotaLimit(callerId) {
|
|
476
|
+
if (callerId === null || callerId === undefined || callerId === "") {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
return this.callerQuotas.get(callerId.toLowerCase());
|
|
480
|
+
}
|
|
481
|
+
/** Capacity plus on-demand durable-root disk breakdown (KTD16). */
|
|
482
|
+
async getHealthWithDisk() {
|
|
483
|
+
const base = this.getHealth();
|
|
484
|
+
try {
|
|
485
|
+
const disk = await durableRootDiskBreakdown(globalStageflowHome());
|
|
486
|
+
return { ...base, disk };
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return {
|
|
490
|
+
...base,
|
|
491
|
+
disk: {
|
|
492
|
+
runs_bytes: 0,
|
|
493
|
+
worktrees_bytes: 0,
|
|
494
|
+
repos_bytes: 0,
|
|
495
|
+
state_db_bytes: 0,
|
|
496
|
+
a2a_artifacts_bytes: 0,
|
|
497
|
+
cache_bytes: 0,
|
|
498
|
+
free_bytes: 0,
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/** Refresh cached `disk_bytes` for one run after a terminal transition. */
|
|
504
|
+
async refreshRunDiskBytes(runId) {
|
|
505
|
+
await refreshRunDiskUsage(this.options.store, runId);
|
|
506
|
+
}
|
|
157
507
|
getHitlController() {
|
|
158
508
|
return this.hitl;
|
|
159
509
|
}
|
|
@@ -188,16 +538,27 @@ export class RunManager {
|
|
|
188
538
|
}
|
|
189
539
|
let checkoutKey;
|
|
190
540
|
let durableCheckoutRoot;
|
|
541
|
+
let attachCallerId = null;
|
|
542
|
+
let attachProjectRoot;
|
|
191
543
|
try {
|
|
192
544
|
const meta = await this.options.store.readRunMeta(runId);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
545
|
+
attachCallerId = meta.caller_id ?? null;
|
|
546
|
+
attachProjectRoot = meta.project_root
|
|
547
|
+
? normalizeCatalogPath(meta.project_root)
|
|
548
|
+
: undefined;
|
|
549
|
+
const kind = derivedBindingKindFromMeta(meta);
|
|
550
|
+
if (kind === "checkout") {
|
|
551
|
+
const checkoutRoot = meta.checkout_root;
|
|
552
|
+
if (checkoutRoot !== undefined && checkoutRoot !== "") {
|
|
553
|
+
durableCheckoutRoot = checkoutRoot;
|
|
554
|
+
checkoutKey = await toCheckoutLeaseKey(checkoutRoot);
|
|
555
|
+
}
|
|
197
556
|
}
|
|
198
557
|
}
|
|
199
558
|
catch (err) {
|
|
200
|
-
|
|
559
|
+
log
|
|
560
|
+
.child({ run_id: runId })
|
|
561
|
+
.error("attach.checkout_root_failed", `invariant: attach failed reading checkout_root for run ${runId}; tracking without checkout lease: ${err instanceof Error ? err.message : String(err)}`);
|
|
201
562
|
}
|
|
202
563
|
const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
|
|
203
564
|
if (conflictHolder !== undefined) {
|
|
@@ -211,6 +572,8 @@ export class RunManager {
|
|
|
211
572
|
this.active.set(runId, {
|
|
212
573
|
checkoutKey,
|
|
213
574
|
durableCheckoutRoot,
|
|
575
|
+
projectRoot: attachProjectRoot,
|
|
576
|
+
callerId: attachCallerId,
|
|
214
577
|
generation: ++this.trackingGeneration,
|
|
215
578
|
});
|
|
216
579
|
for (const stage of waitingStages) {
|
|
@@ -228,31 +591,95 @@ export class RunManager {
|
|
|
228
591
|
.getActiveStageProcesses()
|
|
229
592
|
.some((entry) => entry.runId === runId && entry.stageId === stageId);
|
|
230
593
|
}
|
|
594
|
+
async sweepBrowserSessions() {
|
|
595
|
+
const support = this.options.browser ?? defaultStageBrowserSupport();
|
|
596
|
+
const isRunLive = createRunLiveness(this.options.store);
|
|
597
|
+
await stageProfileLock(support)
|
|
598
|
+
.reclaimStale(isRunLive)
|
|
599
|
+
.catch(() => 0);
|
|
600
|
+
return sweepOrphanBrowserSessions({
|
|
601
|
+
isRunLive,
|
|
602
|
+
...(support.runner !== undefined ? { runner: support.runner } : {}),
|
|
603
|
+
...(support.closeWaitMs !== undefined
|
|
604
|
+
? { closeWaitMs: support.closeWaitMs }
|
|
605
|
+
: {}),
|
|
606
|
+
...(support.socketRoot !== undefined
|
|
607
|
+
? { socketRoot: support.socketRoot }
|
|
608
|
+
: {}),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
231
611
|
async reconcileOrphanedStages() {
|
|
232
612
|
const reconciled = [];
|
|
233
613
|
const runs = await this.options.store.listRuns();
|
|
234
614
|
for (const summary of runs) {
|
|
615
|
+
let meta;
|
|
616
|
+
try {
|
|
617
|
+
meta = await this.options.store.readRunMeta(summary.run_id);
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
log
|
|
621
|
+
.child({ run_id: summary.run_id })
|
|
622
|
+
.error("reconcile.read_meta_failed", `reconcileOrphanedStages: failed to read run meta ${summary.run_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (meta.status === "queued")
|
|
626
|
+
continue;
|
|
235
627
|
let detail;
|
|
236
628
|
try {
|
|
237
629
|
detail = await this.options.store.readRun(summary.run_id);
|
|
238
630
|
}
|
|
239
631
|
catch (err) {
|
|
240
|
-
|
|
632
|
+
log
|
|
633
|
+
.child({ run_id: summary.run_id })
|
|
634
|
+
.error("reconcile.read_run_failed", `reconcileOrphanedStages: failed to read run ${summary.run_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
241
635
|
continue;
|
|
242
636
|
}
|
|
243
637
|
const runId = summary.run_id;
|
|
638
|
+
const cancelled = meta.status === "cancelled";
|
|
244
639
|
let runChanged = false;
|
|
245
640
|
for (const stage of detail.stages) {
|
|
641
|
+
if (cancelled) {
|
|
642
|
+
if (stage.status !== "pending" &&
|
|
643
|
+
stage.status !== "running" &&
|
|
644
|
+
stage.status !== "waiting_for_input") {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
await markStageInterrupted({
|
|
649
|
+
store: this.options.store,
|
|
650
|
+
runId,
|
|
651
|
+
stageId: stage.stage_id,
|
|
652
|
+
reason: OPERATOR_CANCEL_REASON,
|
|
653
|
+
status: "failed",
|
|
654
|
+
});
|
|
655
|
+
reconciled.push({
|
|
656
|
+
runId,
|
|
657
|
+
stageId: stage.stage_id,
|
|
658
|
+
reason: OPERATOR_CANCEL_REASON,
|
|
659
|
+
});
|
|
660
|
+
runChanged = true;
|
|
661
|
+
log
|
|
662
|
+
.child({ run_id: runId, stage_id: stage.stage_id })
|
|
663
|
+
.error("reconcile.orphaned_stage", `reconcileOrphanedStages: failed orphaned stage ${runId}/${stage.stage_id}: ${OPERATOR_CANCEL_REASON}`, { reason: OPERATOR_CANCEL_REASON });
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
log
|
|
667
|
+
.child({ run_id: runId, stage_id: stage.stage_id })
|
|
668
|
+
.error("reconcile.stage_failed", `reconcileOrphanedStages: failed to reconcile ${runId}/${stage.stage_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
669
|
+
}
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
246
672
|
if (stage.status !== "running")
|
|
247
673
|
continue;
|
|
248
674
|
if (this.hasActiveWorker(runId, stage.stage_id))
|
|
249
675
|
continue;
|
|
250
676
|
try {
|
|
251
|
-
await
|
|
677
|
+
await markStageInterrupted({
|
|
252
678
|
store: this.options.store,
|
|
253
679
|
runId,
|
|
254
680
|
stageId: stage.stage_id,
|
|
255
681
|
reason: STARTUP_RECONCILE_REASON,
|
|
682
|
+
status: "interrupted",
|
|
256
683
|
});
|
|
257
684
|
reconciled.push({
|
|
258
685
|
runId,
|
|
@@ -260,26 +687,106 @@ export class RunManager {
|
|
|
260
687
|
reason: STARTUP_RECONCILE_REASON,
|
|
261
688
|
});
|
|
262
689
|
runChanged = true;
|
|
263
|
-
|
|
690
|
+
log
|
|
691
|
+
.child({ run_id: runId, stage_id: stage.stage_id })
|
|
692
|
+
.error("reconcile.orphaned_stage", `reconcileOrphanedStages: interrupted orphaned stage ${runId}/${stage.stage_id}: ${STARTUP_RECONCILE_REASON}`, { reason: STARTUP_RECONCILE_REASON });
|
|
264
693
|
}
|
|
265
694
|
catch (err) {
|
|
266
|
-
|
|
695
|
+
log
|
|
696
|
+
.child({ run_id: runId, stage_id: stage.stage_id })
|
|
697
|
+
.error("reconcile.stage_failed", `reconcileOrphanedStages: failed to reconcile ${runId}/${stage.stage_id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
267
698
|
}
|
|
268
699
|
}
|
|
269
|
-
if (runChanged) {
|
|
700
|
+
if (runChanged && !cancelled) {
|
|
270
701
|
try {
|
|
271
702
|
await syncRunStatusFromStages(this.options.store, runId);
|
|
272
703
|
}
|
|
273
704
|
catch (err) {
|
|
274
|
-
|
|
705
|
+
log
|
|
706
|
+
.child({ run_id: runId })
|
|
707
|
+
.error("reconcile.sync_status_failed", `reconcileOrphanedStages: failed to sync run status for ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
275
708
|
}
|
|
276
709
|
}
|
|
277
710
|
}
|
|
278
711
|
if (reconciled.length > 0) {
|
|
279
|
-
|
|
712
|
+
log.error("reconcile.complete", `reconcileOrphanedStages: reconciled ${reconciled.length} orphaned stage(s)`, { count: reconciled.length });
|
|
280
713
|
}
|
|
281
714
|
return { reconciled };
|
|
282
715
|
}
|
|
716
|
+
async autoResumeInterruptedStages() {
|
|
717
|
+
const resumed = [];
|
|
718
|
+
const capped = [];
|
|
719
|
+
const skipped = [];
|
|
720
|
+
if (!isAutoResumeInterruptedEnabled()) {
|
|
721
|
+
return { resumed, capped, skipped };
|
|
722
|
+
}
|
|
723
|
+
const maxAutoResumes = parseMaxAutoResumes();
|
|
724
|
+
const interrupted = await this.options.store.listInterruptedStageExecutions();
|
|
725
|
+
for (const execution of interrupted) {
|
|
726
|
+
const runId = execution.run_id;
|
|
727
|
+
const stageId = execution.stage_id;
|
|
728
|
+
const attempt = execution.attempt;
|
|
729
|
+
const latest = await this.options.store.getLatestStageExecution(runId, stageId);
|
|
730
|
+
if (latest === null || latest.attempt !== attempt) {
|
|
731
|
+
skipped.push({
|
|
732
|
+
runId,
|
|
733
|
+
stageId,
|
|
734
|
+
reason: "interrupted attempt is not the latest",
|
|
735
|
+
});
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (execution.auto_resume_count >= maxAutoResumes) {
|
|
739
|
+
try {
|
|
740
|
+
await markStageInterrupted({
|
|
741
|
+
store: this.options.store,
|
|
742
|
+
runId,
|
|
743
|
+
stageId,
|
|
744
|
+
reason: AUTO_RESUME_CAPPED_REASON,
|
|
745
|
+
status: "interrupted",
|
|
746
|
+
attemptCtx: attemptContext(attempt),
|
|
747
|
+
});
|
|
748
|
+
capped.push({ runId, stageId });
|
|
749
|
+
log
|
|
750
|
+
.child({ run_id: runId, stage_id: stageId, attempt })
|
|
751
|
+
.info("auto_resume.capped", `autoResumeInterruptedStages: capped ${runId}/${stageId} at ${execution.auto_resume_count}`, { auto_resume_count: execution.auto_resume_count });
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
skipped.push({
|
|
755
|
+
runId,
|
|
756
|
+
stageId,
|
|
757
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
await this.options.store.updateStageExecution(runId, stageId, attempt, {
|
|
764
|
+
auto_resume_count: execution.auto_resume_count + 1,
|
|
765
|
+
});
|
|
766
|
+
const result = await this.resumeTimedOutStage(runId, stageId, {
|
|
767
|
+
source: "auto",
|
|
768
|
+
});
|
|
769
|
+
if (result.ok) {
|
|
770
|
+
resumed.push({ runId, stageId });
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
skipped.push({
|
|
774
|
+
runId,
|
|
775
|
+
stageId,
|
|
776
|
+
reason: result.reason ?? "auto resume failed",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
skipped.push({
|
|
782
|
+
runId,
|
|
783
|
+
stageId,
|
|
784
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return { resumed, capped, skipped };
|
|
789
|
+
}
|
|
283
790
|
async resumeStalledSchedules() {
|
|
284
791
|
const resumed = [];
|
|
285
792
|
const runs = await this.options.store.listRuns();
|
|
@@ -295,11 +802,37 @@ export class RunManager {
|
|
|
295
802
|
}
|
|
296
803
|
}
|
|
297
804
|
catch (err) {
|
|
298
|
-
|
|
805
|
+
log
|
|
806
|
+
.child({ run_id: runId })
|
|
807
|
+
.error("resume.stalled_failed", `resumeStalledSchedules: failed to resume ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
299
808
|
}
|
|
300
809
|
}
|
|
301
810
|
return resumed;
|
|
302
811
|
}
|
|
812
|
+
/** Rebuild in-memory admission queue from persisted `queued` rows (R27). */
|
|
813
|
+
async reenqueuePersistedQueuedRuns() {
|
|
814
|
+
const queued = await this.options.store.listRuns({ status: "queued" });
|
|
815
|
+
const ordered = [...queued].sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
816
|
+
this.admissionQueue.clear();
|
|
817
|
+
for (const row of ordered) {
|
|
818
|
+
const root = normalizeCatalogPath(row.project_root ?? this.projectRoot);
|
|
819
|
+
let callerId = null;
|
|
820
|
+
try {
|
|
821
|
+
const meta = await this.options.store.readRunMeta(row.run_id);
|
|
822
|
+
callerId = meta.caller_id ?? null;
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
/* leave null; drain stamps from meta on quota miss */
|
|
826
|
+
}
|
|
827
|
+
this.admissionQueue.enqueue(root, {
|
|
828
|
+
runId: row.run_id,
|
|
829
|
+
createdAt: row.created_at,
|
|
830
|
+
callerId,
|
|
831
|
+
});
|
|
832
|
+
this.ensureQueuedDone(row.run_id);
|
|
833
|
+
}
|
|
834
|
+
await this.drainAdmissionQueue();
|
|
835
|
+
}
|
|
303
836
|
async abandonStage(runId, stageId) {
|
|
304
837
|
let detail;
|
|
305
838
|
try {
|
|
@@ -338,11 +871,18 @@ export class RunManager {
|
|
|
338
871
|
await this.stageProcessLauncher.cancelRun(runId);
|
|
339
872
|
}
|
|
340
873
|
}
|
|
341
|
-
await
|
|
874
|
+
await teardownStageBrowser(this.options.browser ?? defaultStageBrowserSupport(), {
|
|
875
|
+
runId,
|
|
876
|
+
runDir: this.options.store.getWorkspaceDir(runId),
|
|
877
|
+
stageId,
|
|
878
|
+
events: () => this.options.store.listStageEvents(runId, stageId),
|
|
879
|
+
}).catch(() => undefined);
|
|
880
|
+
await markStageInterrupted({
|
|
342
881
|
store: this.options.store,
|
|
343
882
|
runId,
|
|
344
883
|
stageId,
|
|
345
884
|
reason: OPERATOR_ABANDON_REASON,
|
|
885
|
+
status: "failed",
|
|
346
886
|
});
|
|
347
887
|
if (this.retryCoordinator.isActive(runId) &&
|
|
348
888
|
this.retryCoordinator.getActiveRoots(runId)?.has(stageId)) {
|
|
@@ -351,11 +891,213 @@ export class RunManager {
|
|
|
351
891
|
await syncRunStatusFromStages(this.options.store, runId);
|
|
352
892
|
const after = await this.options.store.readRun(runId);
|
|
353
893
|
const hasWaiting = after.stages.some((s) => s.status === "waiting_for_input");
|
|
894
|
+
if (!hasWaiting &&
|
|
895
|
+
!(await createRunLiveness(this.options.store)(runId).catch(() => true))) {
|
|
896
|
+
await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
|
|
897
|
+
runId,
|
|
898
|
+
runDir: this.options.store.getWorkspaceDir(runId),
|
|
899
|
+
events: (stageId) => this.options.store.listStageEvents(runId, stageId),
|
|
900
|
+
}).catch(() => undefined);
|
|
901
|
+
}
|
|
354
902
|
if (!hasWaiting && this.active.has(runId)) {
|
|
355
903
|
this.removeActiveEntry(runId, false);
|
|
356
904
|
}
|
|
357
905
|
return { ok: true, runId, stageId };
|
|
358
906
|
}
|
|
907
|
+
async cancelRun(runId, reason) {
|
|
908
|
+
let meta;
|
|
909
|
+
try {
|
|
910
|
+
meta = await this.options.store.readRunMeta(runId);
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
914
|
+
}
|
|
915
|
+
if (meta.status === "cancelled") {
|
|
916
|
+
return { ok: true, runId };
|
|
917
|
+
}
|
|
918
|
+
if (meta.status === "succeeded" || meta.status === "failed") {
|
|
919
|
+
return {
|
|
920
|
+
ok: false,
|
|
921
|
+
reason: `Run is ${meta.status} and cannot be cancelled`,
|
|
922
|
+
status: 409,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
const trimmedReason = reason.trim();
|
|
926
|
+
if (trimmedReason.length === 0) {
|
|
927
|
+
return {
|
|
928
|
+
ok: false,
|
|
929
|
+
reason: "Cancel reason is required",
|
|
930
|
+
status: 400,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
await this.options.store.updateRunStatus(runId, "cancelled");
|
|
934
|
+
await this.options.store.setCancelReason(runId, trimmedReason);
|
|
935
|
+
await finaliseStoredRunManifest(this.options.store, runId).catch(() => undefined);
|
|
936
|
+
await this.refreshRunDiskBytes(runId).catch(() => undefined);
|
|
937
|
+
const halt = this.schedulingHalts.get(runId);
|
|
938
|
+
if (halt !== undefined) {
|
|
939
|
+
halt.halted = true;
|
|
940
|
+
}
|
|
941
|
+
this.admissionQueue.remove(runId);
|
|
942
|
+
this.pendingQueuedStarts.delete(runId);
|
|
943
|
+
this.resolveQueuedDone(runId, {
|
|
944
|
+
ok: false,
|
|
945
|
+
outcome: "cancelled",
|
|
946
|
+
runDir: this.options.store.getWorkspaceDir(runId),
|
|
947
|
+
runId,
|
|
948
|
+
reason: trimmedReason,
|
|
949
|
+
});
|
|
950
|
+
if (this.stageProcessLauncher !== undefined) {
|
|
951
|
+
await this.stageProcessLauncher.cancelRun(runId);
|
|
952
|
+
}
|
|
953
|
+
await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
|
|
954
|
+
runId,
|
|
955
|
+
runDir: this.options.store.getWorkspaceDir(runId),
|
|
956
|
+
events: (stageId) => this.options.store.listStageEvents(runId, stageId),
|
|
957
|
+
});
|
|
958
|
+
const detail = await this.options.store.readRun(runId);
|
|
959
|
+
for (const stage of detail.stages) {
|
|
960
|
+
if (stage.status !== "pending" &&
|
|
961
|
+
stage.status !== "running" &&
|
|
962
|
+
stage.status !== "waiting_for_input") {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
const wasWaiting = stage.status === "waiting_for_input";
|
|
966
|
+
await markStageInterrupted({
|
|
967
|
+
store: this.options.store,
|
|
968
|
+
runId,
|
|
969
|
+
stageId: stage.stage_id,
|
|
970
|
+
reason: OPERATOR_CANCEL_REASON,
|
|
971
|
+
status: "failed",
|
|
972
|
+
});
|
|
973
|
+
if (wasWaiting) {
|
|
974
|
+
this.hitl.clearLiveWait(runId, stage.stage_id);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (this.active.has(runId)) {
|
|
978
|
+
this.removeActiveEntry(runId, false);
|
|
979
|
+
}
|
|
980
|
+
return { ok: true, runId };
|
|
981
|
+
}
|
|
982
|
+
setA2aStore(store) {
|
|
983
|
+
this.a2aStore = store;
|
|
984
|
+
}
|
|
985
|
+
async deleteRun(runId, options = {
|
|
986
|
+
channel: "rest",
|
|
987
|
+
}) {
|
|
988
|
+
const force = options.force === true;
|
|
989
|
+
const channel = options.channel;
|
|
990
|
+
let meta;
|
|
991
|
+
try {
|
|
992
|
+
meta = await this.options.store.readRunMeta(runId);
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
996
|
+
}
|
|
997
|
+
const activeStatus = meta.status === "created" ||
|
|
998
|
+
meta.status === "queued" ||
|
|
999
|
+
meta.status === "running";
|
|
1000
|
+
if (activeStatus && !force) {
|
|
1001
|
+
return {
|
|
1002
|
+
ok: false,
|
|
1003
|
+
reason: `Run is ${meta.status} and cannot be deleted without force`,
|
|
1004
|
+
status: 409,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
if (activeStatus && force) {
|
|
1008
|
+
const settle = this.active.get(runId)?.done;
|
|
1009
|
+
const cancelled = await this.cancelRun(runId, FORCE_DELETE_CANCEL_REASON);
|
|
1010
|
+
if (!cancelled.ok && cancelled.status !== 404) {
|
|
1011
|
+
return cancelled;
|
|
1012
|
+
}
|
|
1013
|
+
if (settle !== undefined) {
|
|
1014
|
+
try {
|
|
1015
|
+
await this.waitForPromise(settle, 30_000);
|
|
1016
|
+
}
|
|
1017
|
+
catch (err) {
|
|
1018
|
+
return {
|
|
1019
|
+
ok: false,
|
|
1020
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1021
|
+
status: 500,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const a2aStore = this.a2aStore;
|
|
1027
|
+
if (a2aStore === undefined) {
|
|
1028
|
+
return {
|
|
1029
|
+
ok: false,
|
|
1030
|
+
reason: "A2A store is not available for delete_run",
|
|
1031
|
+
status: 500,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
try {
|
|
1035
|
+
await deleteRunEverywhere(this.options.store, a2aStore, runId);
|
|
1036
|
+
}
|
|
1037
|
+
catch (err) {
|
|
1038
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1039
|
+
if (message.startsWith("Run not found:")) {
|
|
1040
|
+
return { ok: false, reason: message, status: 404 };
|
|
1041
|
+
}
|
|
1042
|
+
return { ok: false, reason: message, status: 500 };
|
|
1043
|
+
}
|
|
1044
|
+
log.child({ run_id: runId }).info("delete_run", "run deleted", {
|
|
1045
|
+
force,
|
|
1046
|
+
channel,
|
|
1047
|
+
});
|
|
1048
|
+
return { ok: true, runId };
|
|
1049
|
+
}
|
|
1050
|
+
async gcRuns(options = {}) {
|
|
1051
|
+
const execute = options.execute === true;
|
|
1052
|
+
const channel = options.channel ?? "rest";
|
|
1053
|
+
let report;
|
|
1054
|
+
try {
|
|
1055
|
+
report = await runRetentionSweep(this.options.store, this.a2aStore, {
|
|
1056
|
+
execute,
|
|
1057
|
+
now: options.now,
|
|
1058
|
+
windows: options.windows,
|
|
1059
|
+
env: options.env,
|
|
1060
|
+
artifactMaxBytes: options.artifactMaxBytes,
|
|
1061
|
+
bareCacheTtlMs: options.bareCacheTtlMs,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
catch (err) {
|
|
1065
|
+
return {
|
|
1066
|
+
ok: false,
|
|
1067
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1068
|
+
status: 500,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
if (execute) {
|
|
1072
|
+
for (const runId of report.slimmed) {
|
|
1073
|
+
await this.refreshRunDiskBytes(runId).catch(() => undefined);
|
|
1074
|
+
}
|
|
1075
|
+
log.info("run_retention_sweep", "retention sweep executed", {
|
|
1076
|
+
channel,
|
|
1077
|
+
slimmed: report.slimmed,
|
|
1078
|
+
purged: report.purged,
|
|
1079
|
+
bareCachesEvicted: report.bareCachesEvicted,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
return { ok: true, ...report };
|
|
1083
|
+
}
|
|
1084
|
+
async waitForPromise(promise, timeoutMs) {
|
|
1085
|
+
let timer;
|
|
1086
|
+
try {
|
|
1087
|
+
await Promise.race([
|
|
1088
|
+
promise.then(() => undefined, () => undefined),
|
|
1089
|
+
new Promise((_, reject) => {
|
|
1090
|
+
timer = setTimeout(() => {
|
|
1091
|
+
reject(new Error(`Timed out waiting for run to settle after force cancel`));
|
|
1092
|
+
}, timeoutMs);
|
|
1093
|
+
}),
|
|
1094
|
+
]);
|
|
1095
|
+
}
|
|
1096
|
+
finally {
|
|
1097
|
+
if (timer !== undefined)
|
|
1098
|
+
clearTimeout(timer);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
359
1101
|
async startRunOnce(input, submission) {
|
|
360
1102
|
if (!submission.key.trim() || submission.key.length > 256 || !/^[a-f0-9]{64}$/.test(submission.requestHash)) {
|
|
361
1103
|
return { ok: false, status: 400, reason: "Submission requires a key and SHA-256 request hash" };
|
|
@@ -392,12 +1134,25 @@ export class RunManager {
|
|
|
392
1134
|
}
|
|
393
1135
|
}
|
|
394
1136
|
async startRun(input, submission) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1137
|
+
if (!this.acceptingWork) {
|
|
1138
|
+
return {
|
|
1139
|
+
ok: false,
|
|
1140
|
+
reason: "Host is shutting down",
|
|
1141
|
+
status: 503,
|
|
1142
|
+
code: "shutting_down",
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const hostCwd = this.options.cwd ?? process.cwd();
|
|
1146
|
+
const wireRoot = typeof input.projectRoot === "string" && input.projectRoot.trim().length > 0
|
|
1147
|
+
? normalizeProjectRoot(input.projectRoot)
|
|
1148
|
+
: undefined;
|
|
1149
|
+
// Wire root wins. Otherwise path pipelines may derive via findProjectRoot;
|
|
1150
|
+
// inline-only without projectRoot keeps Host projectRoot.
|
|
1151
|
+
const derivedProjectRoot = wireRoot ??
|
|
1152
|
+
(typeof input.pipeline === "string"
|
|
1153
|
+
? (findProjectRoot(path.dirname(path.resolve(hostCwd, input.pipeline))) ?? this.projectRoot)
|
|
1154
|
+
: this.projectRoot);
|
|
1155
|
+
const cwd = wireRoot ?? hostCwd;
|
|
401
1156
|
let resolved;
|
|
402
1157
|
try {
|
|
403
1158
|
resolved = resolveStartTaskInput(input, cwd);
|
|
@@ -432,24 +1187,57 @@ export class RunManager {
|
|
|
432
1187
|
gitSha: input.gitSha,
|
|
433
1188
|
ciPrUrl: input.ciPrUrl,
|
|
434
1189
|
ciJobUrl: input.ciJobUrl,
|
|
435
|
-
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission);
|
|
1190
|
+
}, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission, undefined, input.callerId, input.skills);
|
|
436
1191
|
}
|
|
437
|
-
async rerun(runId) {
|
|
1192
|
+
async rerun(runId, options) {
|
|
1193
|
+
if (!this.acceptingWork) {
|
|
1194
|
+
return {
|
|
1195
|
+
ok: false,
|
|
1196
|
+
reason: "Host is shutting down",
|
|
1197
|
+
status: 503,
|
|
1198
|
+
code: "shutting_down",
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
438
1201
|
const cwd = this.options.cwd ?? process.cwd();
|
|
439
1202
|
let pipeline;
|
|
440
1203
|
let taskYaml;
|
|
441
1204
|
let rerunCwd = cwd;
|
|
442
1205
|
let rerunProjectRoot;
|
|
1206
|
+
let skipGates;
|
|
1207
|
+
let ciIdentity;
|
|
1208
|
+
let pinned;
|
|
443
1209
|
try {
|
|
444
1210
|
const meta = await this.options.store.readRunMeta(runId);
|
|
445
|
-
if (
|
|
1211
|
+
if (meta.pipeline_source === "inline") {
|
|
1212
|
+
const body = await this.options.store.readPipelineBody(runId);
|
|
1213
|
+
if (body === null || body === "") {
|
|
1214
|
+
return {
|
|
1215
|
+
ok: false,
|
|
1216
|
+
reason: `Run ${runId} is missing pipeline_body; re-run of an inline pipeline requires the stored body.`,
|
|
1217
|
+
status: 400,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
try {
|
|
1221
|
+
pipeline = JSON.parse(body);
|
|
1222
|
+
}
|
|
1223
|
+
catch {
|
|
1224
|
+
return {
|
|
1225
|
+
ok: false,
|
|
1226
|
+
reason: `Run ${runId} has invalid pipeline_body JSON`,
|
|
1227
|
+
status: 400,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
else if (meta.pipeline_path) {
|
|
1232
|
+
pipeline = normalizeCatalogPath(meta.pipeline_path);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
446
1235
|
return {
|
|
447
1236
|
ok: false,
|
|
448
|
-
reason: `Run ${runId}
|
|
1237
|
+
reason: `Run ${runId} has neither pipeline_path nor pipeline_body; cannot re-run.`,
|
|
449
1238
|
status: 400,
|
|
450
1239
|
};
|
|
451
1240
|
}
|
|
452
|
-
pipeline = normalizeCatalogPath(meta.pipeline_path);
|
|
453
1241
|
rerunCwd = meta.project_root
|
|
454
1242
|
? normalizeCatalogPath(meta.project_root)
|
|
455
1243
|
: cwd;
|
|
@@ -457,16 +1245,44 @@ export class RunManager {
|
|
|
457
1245
|
? normalizeCatalogPath(meta.project_root)
|
|
458
1246
|
: undefined;
|
|
459
1247
|
taskYaml = await this.options.store.readTaskYaml(runId);
|
|
1248
|
+
skipGates = meta.skip_gates;
|
|
1249
|
+
ciIdentity = {
|
|
1250
|
+
...(meta.git_sha !== undefined ? { gitSha: meta.git_sha } : {}),
|
|
1251
|
+
...(meta.ci_pr_url !== undefined ? { ciPrUrl: meta.ci_pr_url } : {}),
|
|
1252
|
+
...(meta.ci_job_url !== undefined ? { ciJobUrl: meta.ci_job_url } : {}),
|
|
1253
|
+
};
|
|
1254
|
+
if (options?.pinned) {
|
|
1255
|
+
if (meta.resolved_sha === undefined ||
|
|
1256
|
+
meta.resolved_sha === "" ||
|
|
1257
|
+
meta.ref === undefined ||
|
|
1258
|
+
meta.ref === "") {
|
|
1259
|
+
return {
|
|
1260
|
+
ok: false,
|
|
1261
|
+
reason: `Run ${runId} has no resolved_sha/ref to pin`,
|
|
1262
|
+
status: 400,
|
|
1263
|
+
code: "pinned_sha_unavailable",
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
pinned = { ref: meta.ref, resolvedSha: meta.resolved_sha };
|
|
1267
|
+
}
|
|
460
1268
|
}
|
|
461
1269
|
catch {
|
|
462
1270
|
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
463
1271
|
}
|
|
464
|
-
return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, undefined, undefined,
|
|
1272
|
+
return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, skipGates, ciIdentity, rerunProjectRoot, undefined, undefined, pinned, options?.callerId);
|
|
465
1273
|
}
|
|
466
1274
|
async retryStage(runId, stageId) {
|
|
467
1275
|
return this.retryStageInternal(runId, stageId, true);
|
|
468
1276
|
}
|
|
469
|
-
async resumeTimedOutStage(runId, stageId) {
|
|
1277
|
+
async resumeTimedOutStage(runId, stageId, options) {
|
|
1278
|
+
if (!this.acceptingWork) {
|
|
1279
|
+
return {
|
|
1280
|
+
ok: false,
|
|
1281
|
+
reason: "Host is shutting down",
|
|
1282
|
+
status: 503,
|
|
1283
|
+
code: "shutting_down",
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
470
1286
|
const resumeKey = waitKey(runId, stageId);
|
|
471
1287
|
if (this.resumeInFlight.has(resumeKey) || this.retryInFlight.has(resumeKey)) {
|
|
472
1288
|
return {
|
|
@@ -489,13 +1305,16 @@ export class RunManager {
|
|
|
489
1305
|
this.resumeInFlight.delete(resumeKey);
|
|
490
1306
|
return { ok: false, reason: `Stage not found: ${stageId}`, status: 404 };
|
|
491
1307
|
}
|
|
492
|
-
const eligibility =
|
|
1308
|
+
const eligibility = assertResumableStage(stageSnap.status, stageSnap.events);
|
|
493
1309
|
if (!eligibility.ok) {
|
|
494
1310
|
this.resumeInFlight.delete(resumeKey);
|
|
495
1311
|
return eligibility;
|
|
496
1312
|
}
|
|
497
1313
|
const latest = await this.options.store.getLatestStageExecution(runId, stageId);
|
|
498
1314
|
const attemptIndex = latest?.attempt ?? 1;
|
|
1315
|
+
if ((options?.source ?? "explicit") === "explicit" && latest !== null) {
|
|
1316
|
+
await this.options.store.updateStageExecution(runId, stageId, attemptIndex, { auto_resume_count: 0 });
|
|
1317
|
+
}
|
|
499
1318
|
const wasActive = this.active.has(runId);
|
|
500
1319
|
const tracked = await this.ensureResumeTracked(runId);
|
|
501
1320
|
if (!tracked.ok) {
|
|
@@ -615,12 +1434,21 @@ export class RunManager {
|
|
|
615
1434
|
}
|
|
616
1435
|
}
|
|
617
1436
|
async retryStageInternal(runId, stageId, awaitRoot, options) {
|
|
618
|
-
|
|
619
|
-
if (this.retryInFlight.has(retryKey)) {
|
|
1437
|
+
if (!this.acceptingWork) {
|
|
620
1438
|
return {
|
|
621
1439
|
ok: false,
|
|
622
|
-
reason:
|
|
1440
|
+
reason: "Host is shutting down",
|
|
1441
|
+
status: 503,
|
|
1442
|
+
code: "shutting_down",
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
const retryKey = waitKey(runId, stageId);
|
|
1446
|
+
if (this.retryInFlight.has(retryKey)) {
|
|
1447
|
+
return {
|
|
1448
|
+
ok: false,
|
|
1449
|
+
reason: `Retry already in progress for run ${runId} stage ${stageId}`,
|
|
623
1450
|
status: 409,
|
|
1451
|
+
code: "retry_in_progress",
|
|
624
1452
|
};
|
|
625
1453
|
}
|
|
626
1454
|
this.retryInFlight.add(retryKey);
|
|
@@ -631,6 +1459,7 @@ export class RunManager {
|
|
|
631
1459
|
ok: false,
|
|
632
1460
|
reason: "Stage uses manual recovery; use the manual recovery action instead of retry",
|
|
633
1461
|
status: 409,
|
|
1462
|
+
code: "manual_recovery_required",
|
|
634
1463
|
};
|
|
635
1464
|
}
|
|
636
1465
|
try {
|
|
@@ -692,6 +1521,9 @@ export class RunManager {
|
|
|
692
1521
|
agent: this.options.agent,
|
|
693
1522
|
cwd: meta.project_root ?? this.projectRoot,
|
|
694
1523
|
operatorCatalog: this.options.operatorCatalog,
|
|
1524
|
+
...(this.options.browser !== undefined
|
|
1525
|
+
? { browser: this.options.browser }
|
|
1526
|
+
: {}),
|
|
695
1527
|
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
696
1528
|
executionMode: this.executionMode,
|
|
697
1529
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
@@ -1055,10 +1887,14 @@ export class RunManager {
|
|
|
1055
1887
|
checkoutRoot: meta.checkout_root,
|
|
1056
1888
|
hitl: this.hitl,
|
|
1057
1889
|
operatorCatalog: this.options.operatorCatalog,
|
|
1890
|
+
...(this.options.browser !== undefined
|
|
1891
|
+
? { browser: this.options.browser }
|
|
1892
|
+
: {}),
|
|
1058
1893
|
},
|
|
1059
1894
|
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
1060
1895
|
executionMode: this.executionMode,
|
|
1061
1896
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1897
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1062
1898
|
});
|
|
1063
1899
|
this.registerResumeUntrack(runId, done);
|
|
1064
1900
|
const rest = await done;
|
|
@@ -1103,14 +1939,46 @@ export class RunManager {
|
|
|
1103
1939
|
try {
|
|
1104
1940
|
const runMeta = await store.readRunMeta(runId);
|
|
1105
1941
|
const runProjectRoot = runMeta.project_root ?? this.projectRoot;
|
|
1942
|
+
const { meta, task, loaded } = await loadRunContext(store, runId, runProjectRoot);
|
|
1943
|
+
const stageBinding = stageBindingEnvFromRun({
|
|
1944
|
+
meta,
|
|
1945
|
+
task,
|
|
1946
|
+
runWorkspaceDir: workspaceDir,
|
|
1947
|
+
hostEnv: process.env,
|
|
1948
|
+
});
|
|
1949
|
+
const resumedStage = loaded.stages.find((s) => s.id === definitionIdForInstance(meta.pipeline_dag, stageId));
|
|
1950
|
+
if (resumedStage?.browser?.profile !== undefined) {
|
|
1951
|
+
await acquireStageProfile(this.options.browser ?? defaultStageBrowserSupport(), {
|
|
1952
|
+
profile: resumedStage.browser.profile,
|
|
1953
|
+
owner: { runId, stageId },
|
|
1954
|
+
isRunLive: createRunLiveness(store),
|
|
1955
|
+
halted: () => true,
|
|
1956
|
+
}).then((outcome) => {
|
|
1957
|
+
if (outcome === "halted") {
|
|
1958
|
+
throw new Error(`browser profile "${resumedStage.browser?.profile}" is held by another run`);
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
const browserEnv = await resolveStageBrowserEnv(this.options.browser ?? defaultStageBrowserSupport(), {
|
|
1963
|
+
runId,
|
|
1964
|
+
stageId,
|
|
1965
|
+
runDir: workspaceDir,
|
|
1966
|
+
browser: resumedStage?.browser,
|
|
1967
|
+
attempt,
|
|
1968
|
+
humanLogin: isHumanLoginStage(meta.pipeline_dag, definitionIdForInstance(meta.pipeline_dag, stageId), resumedStage?.browser),
|
|
1969
|
+
resuming: true,
|
|
1970
|
+
});
|
|
1106
1971
|
const launchResult = await launcher.launch({
|
|
1107
1972
|
runId,
|
|
1108
1973
|
stageId,
|
|
1109
1974
|
rootDir: runProjectRoot,
|
|
1975
|
+
...(browserEnv !== undefined ? { browserEnv } : {}),
|
|
1110
1976
|
mode: "resume",
|
|
1111
1977
|
resumeAnswer: opaqueAnswer,
|
|
1112
1978
|
attempt,
|
|
1113
1979
|
sessionFilePath,
|
|
1980
|
+
env: stageBinding.env,
|
|
1981
|
+
bindingKind: stageBinding.kind,
|
|
1114
1982
|
...(this.options.operatorCatalog !== undefined
|
|
1115
1983
|
? { operatorCatalog: this.options.operatorCatalog }
|
|
1116
1984
|
: {}),
|
|
@@ -1118,12 +1986,53 @@ export class RunManager {
|
|
|
1118
1986
|
if (launchResult.type === "waiting") {
|
|
1119
1987
|
return { ok: true };
|
|
1120
1988
|
}
|
|
1989
|
+
await teardownStageBrowser(this.options.browser ?? defaultStageBrowserSupport(), {
|
|
1990
|
+
runId,
|
|
1991
|
+
runDir: workspaceDir,
|
|
1992
|
+
stageId,
|
|
1993
|
+
events: () => store.listStageEvents(runId, stageId),
|
|
1994
|
+
}).catch(() => undefined);
|
|
1121
1995
|
if (launchResult.type === "failed") {
|
|
1996
|
+
const nextRepairAttempt = await this.scheduleRepairAfterVerifyFailure(runId, stageId, meta.pipeline_dag);
|
|
1997
|
+
if (nextRepairAttempt !== undefined) {
|
|
1998
|
+
await store.updateRunStatus(runId, "running");
|
|
1999
|
+
const repaired = await retryRun({
|
|
2000
|
+
prepared: {
|
|
2001
|
+
task,
|
|
2002
|
+
loaded,
|
|
2003
|
+
run: { runId, workspaceDir: store.getWorkspaceDir(runId) },
|
|
2004
|
+
agent: this.options.agent,
|
|
2005
|
+
store,
|
|
2006
|
+
cwd: meta.project_root ?? this.projectRoot,
|
|
2007
|
+
projectRoot: meta.project_root ?? this.projectRoot,
|
|
2008
|
+
checkoutRoot: meta.checkout_root,
|
|
2009
|
+
hitl: this.hitl,
|
|
2010
|
+
operatorCatalog: this.options.operatorCatalog,
|
|
2011
|
+
...(this.options.browser !== undefined
|
|
2012
|
+
? { browser: this.options.browser }
|
|
2013
|
+
: {}),
|
|
2014
|
+
},
|
|
2015
|
+
retryRoots: new Map([[stageId, nextRepairAttempt]]),
|
|
2016
|
+
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
2017
|
+
executionMode: this.executionMode,
|
|
2018
|
+
stageProcessLauncher: launcher,
|
|
2019
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
2020
|
+
});
|
|
2021
|
+
if (repaired.outcome === "failed") {
|
|
2022
|
+
return { ok: false, reason: repaired.reason };
|
|
2023
|
+
}
|
|
2024
|
+
return { ok: true, verification: "failed_retrying" };
|
|
2025
|
+
}
|
|
1122
2026
|
await store.appendStageEvent(runId, stageId, {
|
|
1123
2027
|
event: "failed",
|
|
1124
2028
|
reason: launchResult.reason,
|
|
1125
2029
|
}, eventOptions);
|
|
1126
2030
|
await store.updateRunStatus(runId, "failed");
|
|
2031
|
+
await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
|
|
2032
|
+
runId,
|
|
2033
|
+
runDir: workspaceDir,
|
|
2034
|
+
events: (stageId) => store.listStageEvents(runId, stageId),
|
|
2035
|
+
}).catch(() => undefined);
|
|
1127
2036
|
return { ok: false, reason: launchResult.reason };
|
|
1128
2037
|
}
|
|
1129
2038
|
let initialPrior = null;
|
|
@@ -1133,7 +2042,6 @@ export class RunManager {
|
|
|
1133
2042
|
catch {
|
|
1134
2043
|
// downstream resume may read envelope from store
|
|
1135
2044
|
}
|
|
1136
|
-
const { meta, task, loaded } = await loadRunContext(store, runId, this.cwd);
|
|
1137
2045
|
const rest = await resumeRun({
|
|
1138
2046
|
prepared: {
|
|
1139
2047
|
task,
|
|
@@ -1146,12 +2054,16 @@ export class RunManager {
|
|
|
1146
2054
|
checkoutRoot: meta.checkout_root,
|
|
1147
2055
|
hitl: this.hitl,
|
|
1148
2056
|
operatorCatalog: this.options.operatorCatalog,
|
|
2057
|
+
...(this.options.browser !== undefined
|
|
2058
|
+
? { browser: this.options.browser }
|
|
2059
|
+
: {}),
|
|
1149
2060
|
},
|
|
1150
2061
|
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
1151
2062
|
resumeFromStageId: stageId,
|
|
1152
2063
|
initialPrior,
|
|
1153
2064
|
executionMode: this.executionMode,
|
|
1154
2065
|
stageProcessLauncher: launcher,
|
|
2066
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1155
2067
|
});
|
|
1156
2068
|
if (rest.outcome === "failed") {
|
|
1157
2069
|
return { ok: false, reason: rest.reason };
|
|
@@ -1168,6 +2080,11 @@ export class RunManager {
|
|
|
1168
2080
|
reason,
|
|
1169
2081
|
}, eventOptions);
|
|
1170
2082
|
await store.updateRunStatus(runId, "failed");
|
|
2083
|
+
await teardownRunBrowsers(this.options.browser ?? defaultStageBrowserSupport(), {
|
|
2084
|
+
runId,
|
|
2085
|
+
runDir: store.getWorkspaceDir(runId),
|
|
2086
|
+
events: (stageId) => store.listStageEvents(runId, stageId),
|
|
2087
|
+
}).catch(() => undefined);
|
|
1171
2088
|
}
|
|
1172
2089
|
catch {
|
|
1173
2090
|
// ignore secondary failures
|
|
@@ -1178,6 +2095,24 @@ export class RunManager {
|
|
|
1178
2095
|
this.attachedWaiting.delete(`${runId}\0${stageId}`);
|
|
1179
2096
|
}
|
|
1180
2097
|
}
|
|
2098
|
+
/**
|
|
2099
|
+
* A resumed stage whose after-phase verification failed is repaired by the
|
|
2100
|
+
* same policy the scheduler applies to a first-launch failure. Returns the
|
|
2101
|
+
* freshly created attempt, or undefined when the failure is final.
|
|
2102
|
+
*/
|
|
2103
|
+
async scheduleRepairAfterVerifyFailure(runId, stageId, dag) {
|
|
2104
|
+
const store = this.options.store;
|
|
2105
|
+
const recovery = dag?.nodes.find((node) => node.id === stageId)?.recovery;
|
|
2106
|
+
if (recovery?.mode !== "repair")
|
|
2107
|
+
return undefined;
|
|
2108
|
+
const latest = await store.getLatestStageExecution(runId, stageId);
|
|
2109
|
+
if (latest?.verification_outcome !== "failed")
|
|
2110
|
+
return undefined;
|
|
2111
|
+
const attempts = await store.countStageAttempts(runId, stageId);
|
|
2112
|
+
if (attempts >= recovery.max_attempts)
|
|
2113
|
+
return undefined;
|
|
2114
|
+
return (await store.createStageExecution(runId, stageId)).attempt;
|
|
2115
|
+
}
|
|
1181
2116
|
async reconstructAndContinue(runId, stageId, opaqueAnswer) {
|
|
1182
2117
|
try {
|
|
1183
2118
|
return await resumeReconstructAndContinue({
|
|
@@ -1199,37 +2134,276 @@ export class RunManager {
|
|
|
1199
2134
|
this.attachedWaiting.delete(`${runId}\0${stageId}`);
|
|
1200
2135
|
}
|
|
1201
2136
|
}
|
|
1202
|
-
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission) {
|
|
2137
|
+
async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission, pinned, callerId, skills) {
|
|
2138
|
+
const persistence = pipelinePersistenceForStart(pipeline);
|
|
2139
|
+
if (!persistence.ok) {
|
|
2140
|
+
return {
|
|
2141
|
+
ok: false,
|
|
2142
|
+
reason: `Inline pipeline body is ${persistence.bytes} bytes; max is ${persistence.maxBytes}`,
|
|
2143
|
+
status: 400,
|
|
2144
|
+
code: INLINE_PIPELINE_TOO_LARGE,
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
const skillsCheck = validateSkillsPayload(skills, {
|
|
2148
|
+
pipelineBytes: pipelineBodyBytes(persistence.fields),
|
|
2149
|
+
});
|
|
2150
|
+
if (!skillsCheck.ok) {
|
|
2151
|
+
return {
|
|
2152
|
+
ok: false,
|
|
2153
|
+
reason: skillsCheck.reason,
|
|
2154
|
+
status: 400,
|
|
2155
|
+
code: skillsCheck.code,
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
const validatedSkills = skillsCheck.skills;
|
|
2159
|
+
const hasSkills = Object.keys(validatedSkills).length > 0;
|
|
2160
|
+
const resolvedProjectRoot = normalizeCatalogPath(projectRoot ?? this.projectRoot);
|
|
2161
|
+
let task;
|
|
1203
2162
|
let checkoutKey;
|
|
2163
|
+
let pathCheckoutRoot;
|
|
2164
|
+
let binding;
|
|
2165
|
+
let pipelineId;
|
|
2166
|
+
let pipelineDag;
|
|
2167
|
+
let pipelinePath;
|
|
2168
|
+
let loadedPipeline;
|
|
2169
|
+
let toolchainChecks = [];
|
|
1204
2170
|
try {
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
2171
|
+
const loadedTask = loadTaskFromYamlOutcome(taskYaml, taskLabel);
|
|
2172
|
+
if (!loadedTask.ok) {
|
|
2173
|
+
const issue = loadedTask.issues[0];
|
|
2174
|
+
return {
|
|
2175
|
+
ok: false,
|
|
2176
|
+
reason: issue?.message ?? "Invalid task",
|
|
2177
|
+
status: 400,
|
|
2178
|
+
...(issue?.code !== undefined
|
|
2179
|
+
? { code: issue.code }
|
|
2180
|
+
: {}),
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
task = loadedTask.value;
|
|
2184
|
+
const bindingOutcome = resolveWorkspaceBinding(task, { checkoutOverride });
|
|
2185
|
+
if (!bindingOutcome.ok) {
|
|
2186
|
+
const issue = bindingOutcome.issues[0];
|
|
2187
|
+
return {
|
|
2188
|
+
ok: false,
|
|
2189
|
+
reason: issue?.message ?? "Invalid workspace binding",
|
|
2190
|
+
status: 400,
|
|
2191
|
+
...(issue?.code !== undefined
|
|
2192
|
+
? { code: issue.code }
|
|
2193
|
+
: {}),
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
binding = bindingOutcome.value;
|
|
2197
|
+
if (binding.kind === "checkout") {
|
|
2198
|
+
pathCheckoutRoot = await resolveAndValidateCheckout(task, checkoutOverride, cwd);
|
|
2199
|
+
checkoutKey =
|
|
2200
|
+
pathCheckoutRoot !== undefined
|
|
2201
|
+
? await toCheckoutLeaseKey(pathCheckoutRoot)
|
|
2202
|
+
: undefined;
|
|
2203
|
+
}
|
|
2204
|
+
const loadResult = await loadPipelineValidated(pipeline, {
|
|
2205
|
+
cwd,
|
|
2206
|
+
projectRoot: resolvedProjectRoot,
|
|
2207
|
+
validateStages: true,
|
|
2208
|
+
});
|
|
2209
|
+
if (!loadResult.ok) {
|
|
2210
|
+
throw new PipelineValidationError(buildValidationResult("pipeline", loadResult.findings, false));
|
|
2211
|
+
}
|
|
2212
|
+
loadedPipeline = loadResult.loaded;
|
|
2213
|
+
const pairing = checkTaskEntryInput(task, loadResult.loaded, {
|
|
2214
|
+
cwd,
|
|
2215
|
+
taskPath,
|
|
2216
|
+
});
|
|
2217
|
+
if (pairing.some((finding) => finding.severity === "error")) {
|
|
2218
|
+
throw new PipelineValidationError(buildValidationResult("pipeline", pairing, false));
|
|
2219
|
+
}
|
|
2220
|
+
pipelineId = loadResult.loaded.pipeline.id;
|
|
2221
|
+
pipelineDag = buildPipelineDagSnapshotFromLoaded(loadResult.loaded);
|
|
2222
|
+
pipelinePath =
|
|
2223
|
+
typeof pipeline === "string"
|
|
2224
|
+
? normalizeCatalogPath(loadResult.loaded.pipelinePath)
|
|
1210
2225
|
: undefined;
|
|
2226
|
+
try {
|
|
2227
|
+
const pipelineAgent = asAgentBackendId(loadResult.loaded.pipeline.agent);
|
|
2228
|
+
assertClaudeNotRoot({ backendId: pipelineAgent });
|
|
2229
|
+
for (const stage of loadResult.loaded.stages) {
|
|
2230
|
+
assertClaudeNotRoot({
|
|
2231
|
+
backendId: asAgentBackendId(stage.agent) ?? pipelineAgent,
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
catch (err) {
|
|
2236
|
+
if (err instanceof ClaudeRootError) {
|
|
2237
|
+
return { ok: false, reason: err.message, status: 400, code: err.code };
|
|
2238
|
+
}
|
|
2239
|
+
throw err;
|
|
2240
|
+
}
|
|
2241
|
+
const preflight = await runPipelinePreflight(loadResult.loaded, {
|
|
2242
|
+
projectRoot: resolvedProjectRoot,
|
|
2243
|
+
forStart: true,
|
|
2244
|
+
});
|
|
2245
|
+
if (!preflight.ok) {
|
|
2246
|
+
const code = preflightFailureCode(preflight, { forStart: true }) ??
|
|
2247
|
+
"missing_tool";
|
|
2248
|
+
return {
|
|
2249
|
+
ok: false,
|
|
2250
|
+
reason: `Pipeline preflight failed: ${code}`,
|
|
2251
|
+
status: 400,
|
|
2252
|
+
code,
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
toolchainChecks = preflight.toolchain.checks;
|
|
1211
2256
|
}
|
|
1212
2257
|
catch (err) {
|
|
2258
|
+
if (err instanceof PipelineValidationError)
|
|
2259
|
+
throw err;
|
|
2260
|
+
if (err instanceof PipelinePreflightError) {
|
|
2261
|
+
return {
|
|
2262
|
+
ok: false,
|
|
2263
|
+
reason: err.message,
|
|
2264
|
+
status: 400,
|
|
2265
|
+
code: err.code,
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
1213
2268
|
return {
|
|
1214
2269
|
ok: false,
|
|
1215
2270
|
reason: err instanceof Error ? err.message : String(err),
|
|
1216
2271
|
status: 400,
|
|
1217
2272
|
};
|
|
1218
2273
|
}
|
|
1219
|
-
const
|
|
1220
|
-
if (
|
|
1221
|
-
return
|
|
2274
|
+
const diskGate = await this.checkDiskFloorAdmission();
|
|
2275
|
+
if (diskGate !== undefined)
|
|
2276
|
+
return diskGate;
|
|
2277
|
+
const admitted = this.tryAdmitOrEnqueue(checkoutKey, resolvedProjectRoot, callerId);
|
|
2278
|
+
if (admitted.action === "reject")
|
|
2279
|
+
return admitted.failure;
|
|
2280
|
+
if (admitted.action === "enqueue") {
|
|
2281
|
+
const runId = newRunId();
|
|
2282
|
+
const gitIdentity = resolveEffectiveGitIdentity(process.env, task.git_identity);
|
|
2283
|
+
const createdAt = new Date().toISOString();
|
|
2284
|
+
const auth = getRequestAuth();
|
|
2285
|
+
const surface = auth?.surface ?? "cli";
|
|
2286
|
+
const secretNames = loadedPipeline.stages.flatMap((s) => (s.secrets ?? []).map((d) => d.name));
|
|
2287
|
+
const namedSecrets = collectDeclaredSecretValues(secretNames).filter((s) => shouldRegisterValue(s.value));
|
|
2288
|
+
if (namedSecrets.length > 0) {
|
|
2289
|
+
registerNamedSecrets(namedSecrets);
|
|
2290
|
+
}
|
|
2291
|
+
const runManifest = buildInitialRunManifest({
|
|
2292
|
+
runId,
|
|
2293
|
+
createdAt,
|
|
2294
|
+
callerId: callerId ?? null,
|
|
2295
|
+
surface,
|
|
2296
|
+
binding: bindingFromFields({}),
|
|
2297
|
+
pipelineSource: persistence.fields.pipelineSource,
|
|
2298
|
+
pipelinePath,
|
|
2299
|
+
pipelineBody: persistence.fields.pipelineBody ?? null,
|
|
2300
|
+
taskYaml,
|
|
2301
|
+
taskPath: taskPath
|
|
2302
|
+
? normalizeCatalogPath(path.resolve(cwd, taskPath))
|
|
2303
|
+
: undefined,
|
|
2304
|
+
skills: hasSkills ? validatedSkills : undefined,
|
|
2305
|
+
stages: loadedPipeline.stages,
|
|
2306
|
+
toolchain: toolchainChecks,
|
|
2307
|
+
namedSecrets,
|
|
2308
|
+
});
|
|
2309
|
+
const created = await this.options.store.createRun({
|
|
2310
|
+
submission,
|
|
2311
|
+
runId,
|
|
2312
|
+
pipelineId,
|
|
2313
|
+
taskYaml,
|
|
2314
|
+
taskId: task.id,
|
|
2315
|
+
pipelineDag,
|
|
2316
|
+
pipelinePath,
|
|
2317
|
+
taskPath: taskPath
|
|
2318
|
+
? normalizeCatalogPath(path.resolve(cwd, taskPath))
|
|
2319
|
+
: undefined,
|
|
2320
|
+
projectRoot: resolvedProjectRoot,
|
|
2321
|
+
gitSha: ciIdentity?.gitSha,
|
|
2322
|
+
ciPrUrl: ciIdentity?.ciPrUrl,
|
|
2323
|
+
ciJobUrl: ciIdentity?.ciJobUrl,
|
|
2324
|
+
gitAuthorName: gitIdentity.name,
|
|
2325
|
+
gitAuthorEmail: gitIdentity.email,
|
|
2326
|
+
status: "queued",
|
|
2327
|
+
pipelineSource: persistence.fields.pipelineSource,
|
|
2328
|
+
...(persistence.fields.pipelineBody !== undefined
|
|
2329
|
+
? { pipelineBody: persistence.fields.pipelineBody }
|
|
2330
|
+
: {}),
|
|
2331
|
+
...(callerId !== undefined && callerId !== null
|
|
2332
|
+
? { callerId }
|
|
2333
|
+
: {}),
|
|
2334
|
+
runManifest,
|
|
2335
|
+
skipGates,
|
|
2336
|
+
});
|
|
2337
|
+
if (hasSkills) {
|
|
2338
|
+
await materializeRunSkills(created.workspaceDir, validatedSkills);
|
|
2339
|
+
}
|
|
2340
|
+
const meta = await this.options.store.readRunMeta(created.runId);
|
|
2341
|
+
const queuePosition = this.admissionQueue.enqueue(resolvedProjectRoot, {
|
|
2342
|
+
runId: created.runId,
|
|
2343
|
+
createdAt: meta.created_at,
|
|
2344
|
+
callerId: callerId ?? meta.caller_id ?? null,
|
|
2345
|
+
});
|
|
2346
|
+
this.pendingQueuedStarts.set(created.runId, {
|
|
2347
|
+
taskYaml,
|
|
2348
|
+
pipeline,
|
|
2349
|
+
taskLabel,
|
|
2350
|
+
cwd,
|
|
2351
|
+
projectRoot: resolvedProjectRoot,
|
|
2352
|
+
checkoutOverride,
|
|
2353
|
+
skipGates,
|
|
2354
|
+
ciIdentity,
|
|
2355
|
+
taskPath,
|
|
2356
|
+
submission,
|
|
2357
|
+
pinned,
|
|
2358
|
+
pathCheckoutRoot,
|
|
2359
|
+
binding,
|
|
2360
|
+
checkoutKey,
|
|
2361
|
+
callerId,
|
|
2362
|
+
...(hasSkills ? { skills: validatedSkills } : {}),
|
|
2363
|
+
});
|
|
2364
|
+
const done = this.ensureQueuedDone(created.runId);
|
|
2365
|
+
return {
|
|
2366
|
+
ok: true,
|
|
2367
|
+
runId: created.runId,
|
|
2368
|
+
done,
|
|
2369
|
+
queued: true,
|
|
2370
|
+
queuePosition,
|
|
2371
|
+
...(admitted.reason === "caller_quota"
|
|
2372
|
+
? { queuedCode: "busy_caller_quota" }
|
|
2373
|
+
: {}),
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
const runId = newRunId();
|
|
2377
|
+
let rollback = async () => { };
|
|
1222
2378
|
try {
|
|
2379
|
+
const { materialized, rollback: linkRollback } = await materializeWorkspaceBinding({
|
|
2380
|
+
runId,
|
|
2381
|
+
task,
|
|
2382
|
+
binding,
|
|
2383
|
+
checkoutRoot: pathCheckoutRoot,
|
|
2384
|
+
pinned,
|
|
2385
|
+
});
|
|
2386
|
+
rollback = linkRollback;
|
|
2387
|
+
const schedulingHalt = this.ensureSchedulingHalt(materialized.runId);
|
|
1223
2388
|
const started = await startPipeline({
|
|
1224
2389
|
submission,
|
|
2390
|
+
...(this.options.browser !== undefined
|
|
2391
|
+
? { browser: this.options.browser }
|
|
2392
|
+
: {}),
|
|
1225
2393
|
agent: this.options.agent,
|
|
1226
2394
|
store: this.options.store,
|
|
1227
2395
|
taskYaml,
|
|
1228
2396
|
taskPath,
|
|
1229
2397
|
pipeline,
|
|
1230
2398
|
cwd,
|
|
1231
|
-
projectRoot:
|
|
2399
|
+
projectRoot: resolvedProjectRoot,
|
|
1232
2400
|
checkoutOverride,
|
|
2401
|
+
runId: materialized.runId,
|
|
2402
|
+
checkoutRoot: materialized.checkoutRoot,
|
|
2403
|
+
repository: materialized.repository,
|
|
2404
|
+
ref: materialized.ref,
|
|
2405
|
+
resolvedSha: materialized.resolvedSha,
|
|
2406
|
+
runBranch: materialized.runBranch,
|
|
1233
2407
|
gitSha: ciIdentity?.gitSha,
|
|
1234
2408
|
ciPrUrl: ciIdentity?.ciPrUrl,
|
|
1235
2409
|
ciJobUrl: ciIdentity?.ciJobUrl,
|
|
@@ -1239,15 +2413,44 @@ export class RunManager {
|
|
|
1239
2413
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1240
2414
|
operatorCatalog: this.options.operatorCatalog,
|
|
1241
2415
|
skipGates,
|
|
2416
|
+
schedulingHalt,
|
|
2417
|
+
callerId,
|
|
2418
|
+
...(hasSkills ? { skills: validatedSkills } : {}),
|
|
1242
2419
|
});
|
|
1243
|
-
this.track(
|
|
2420
|
+
this.track(admitted.provisionalId, started.runId, started.done);
|
|
1244
2421
|
return { ok: true, runId: started.runId, done: started.done };
|
|
1245
2422
|
}
|
|
1246
2423
|
catch (err) {
|
|
1247
|
-
|
|
2424
|
+
await rollback().catch(() => undefined);
|
|
2425
|
+
this.clearReservation(admitted.provisionalId);
|
|
2426
|
+
if (err instanceof InlinePipelineTooLargeError) {
|
|
2427
|
+
return {
|
|
2428
|
+
ok: false,
|
|
2429
|
+
reason: err.message,
|
|
2430
|
+
status: 400,
|
|
2431
|
+
code: err.code,
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
if (err instanceof PipelinePreflightError) {
|
|
2435
|
+
return {
|
|
2436
|
+
ok: false,
|
|
2437
|
+
reason: err.message,
|
|
2438
|
+
status: 400,
|
|
2439
|
+
code: err.code,
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
1248
2442
|
if (err instanceof PipelineValidationError || err instanceof RunSubmissionExistsError) {
|
|
1249
2443
|
throw err;
|
|
1250
2444
|
}
|
|
2445
|
+
if (err instanceof StartLinkError) {
|
|
2446
|
+
return {
|
|
2447
|
+
ok: false,
|
|
2448
|
+
reason: err.message,
|
|
2449
|
+
status: err.status,
|
|
2450
|
+
code: err.code,
|
|
2451
|
+
...(err.stderr !== undefined ? { stderr: err.stderr } : {}),
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
1251
2454
|
return {
|
|
1252
2455
|
ok: false,
|
|
1253
2456
|
reason: err instanceof Error ? err.message : String(err),
|
|
@@ -1257,24 +2460,87 @@ export class RunManager {
|
|
|
1257
2460
|
}
|
|
1258
2461
|
busyFailure(code, extras) {
|
|
1259
2462
|
const activeRunIds = this.getActiveRunIds();
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
2463
|
+
const scope = extras?.scope ?? (code === "busy_caller_quota" ? "caller" : "global");
|
|
2464
|
+
const activeCount = extras?.activeCount ?? this.active.size;
|
|
2465
|
+
const maxConcurrent = extras?.maxConcurrent ?? this.maxConcurrent;
|
|
2466
|
+
const reason = code === "busy_caller_quota"
|
|
2467
|
+
? `Caller quota full: ${activeCount}/${maxConcurrent} active runs for caller ${extras?.caller_id ?? "unknown"}`
|
|
2468
|
+
: code === "busy_capacity"
|
|
2469
|
+
? scope === "project"
|
|
2470
|
+
? `Project capacity full: ${activeCount}/${maxConcurrent} active runs for ${extras?.project_root ?? "project"}`
|
|
2471
|
+
: this.active.size >= this.maxConcurrent &&
|
|
2472
|
+
this.admissionQueue.size >= this.maxQueued &&
|
|
2473
|
+
this.maxQueued > 0
|
|
2474
|
+
? `Admission queue full: ${this.admissionQueue.size}/${this.maxQueued} queued runs`
|
|
2475
|
+
: `Capacity full: ${this.active.size}/${this.maxConcurrent} active runs`
|
|
2476
|
+
: `Checkout in use by run ${extras?.conflictingRunId ?? "unknown"}`;
|
|
1263
2477
|
return {
|
|
1264
2478
|
ok: false,
|
|
1265
2479
|
reason,
|
|
1266
2480
|
status: 409,
|
|
1267
2481
|
code,
|
|
1268
|
-
activeCount
|
|
1269
|
-
maxConcurrent
|
|
2482
|
+
activeCount,
|
|
2483
|
+
maxConcurrent,
|
|
1270
2484
|
activeRunIds,
|
|
1271
|
-
|
|
2485
|
+
scope,
|
|
2486
|
+
...(extras?.project_root !== undefined
|
|
2487
|
+
? { project_root: extras.project_root }
|
|
2488
|
+
: {}),
|
|
2489
|
+
...(extras?.caller_id !== undefined ? { caller_id: extras.caller_id } : {}),
|
|
2490
|
+
...(extras?.conflictingRunId !== undefined
|
|
2491
|
+
? { conflictingRunId: extras.conflictingRunId }
|
|
2492
|
+
: {}),
|
|
2493
|
+
...(extras?.conflictingCheckout !== undefined
|
|
2494
|
+
? { conflictingCheckout: extras.conflictingCheckout }
|
|
2495
|
+
: {}),
|
|
1272
2496
|
};
|
|
1273
2497
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
2498
|
+
insufficientDiskFailure(freeBytes, minFreeBytes) {
|
|
2499
|
+
return {
|
|
2500
|
+
ok: false,
|
|
2501
|
+
reason: `Insufficient free disk: ${freeBytes} bytes free, floor ${minFreeBytes} bytes`,
|
|
2502
|
+
status: 409,
|
|
2503
|
+
code: "insufficient_disk",
|
|
2504
|
+
freeBytes,
|
|
2505
|
+
minFreeBytes,
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
async checkDiskFloorAdmission() {
|
|
2509
|
+
try {
|
|
2510
|
+
const readFree = this.options.freeSpaceReader ?? readFilesystemSize;
|
|
2511
|
+
const size = await readFree(globalStageflowHome());
|
|
2512
|
+
const minFreeBytes = resolveMinFreeDiskFloor(process.env.STAGEFLOW_MIN_FREE_DISK_BYTES, size.totalBytes);
|
|
2513
|
+
if (size.freeBytes < minFreeBytes) {
|
|
2514
|
+
return this.insufficientDiskFailure(size.freeBytes, minFreeBytes);
|
|
2515
|
+
}
|
|
2516
|
+
return undefined;
|
|
2517
|
+
}
|
|
2518
|
+
catch (err) {
|
|
2519
|
+
// Default floor is always active via resolveMinFreeDiskFloor — fail closed.
|
|
2520
|
+
return {
|
|
2521
|
+
ok: false,
|
|
2522
|
+
reason: `Disk free-space check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2523
|
+
status: 503,
|
|
2524
|
+
code: "disk_check_failed",
|
|
2525
|
+
};
|
|
1277
2526
|
}
|
|
2527
|
+
}
|
|
2528
|
+
async isProjectRootAvailable(projectRoot) {
|
|
2529
|
+
const provider = this.options.knownWritableProjectRoots;
|
|
2530
|
+
if (provider !== undefined) {
|
|
2531
|
+
const roots = [...(await provider())].map((r) => normalizeCatalogPath(r));
|
|
2532
|
+
const normalized = normalizeCatalogPath(projectRoot);
|
|
2533
|
+
return roots.includes(normalized);
|
|
2534
|
+
}
|
|
2535
|
+
try {
|
|
2536
|
+
await access(projectRoot, constants.W_OK);
|
|
2537
|
+
return true;
|
|
2538
|
+
}
|
|
2539
|
+
catch {
|
|
2540
|
+
return false;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
tryAdmitOrEnqueue(checkoutKey, projectRoot = this.projectRoot, callerId) {
|
|
1278
2544
|
if (checkoutKey !== undefined) {
|
|
1279
2545
|
const holder = this.checkoutLeases.get(checkoutKey);
|
|
1280
2546
|
if (holder !== undefined) {
|
|
@@ -1282,7 +2548,7 @@ export class RunManager {
|
|
|
1282
2548
|
? undefined
|
|
1283
2549
|
: holder;
|
|
1284
2550
|
return {
|
|
1285
|
-
|
|
2551
|
+
action: "reject",
|
|
1286
2552
|
failure: this.busyFailure("busy_checkout", {
|
|
1287
2553
|
conflictingRunId,
|
|
1288
2554
|
conflictingCheckout: checkoutKey,
|
|
@@ -1290,16 +2556,381 @@ export class RunManager {
|
|
|
1290
2556
|
};
|
|
1291
2557
|
}
|
|
1292
2558
|
}
|
|
1293
|
-
const
|
|
1294
|
-
this.
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
2559
|
+
const normalizedRoot = normalizeCatalogPath(projectRoot);
|
|
2560
|
+
if (this.maxConcurrentPerProject !== undefined) {
|
|
2561
|
+
const projectActive = this.countActiveForProject(normalizedRoot);
|
|
2562
|
+
if (projectActive >= this.maxConcurrentPerProject) {
|
|
2563
|
+
return {
|
|
2564
|
+
action: "reject",
|
|
2565
|
+
failure: this.busyFailure("busy_capacity", {
|
|
2566
|
+
scope: "project",
|
|
2567
|
+
project_root: normalizedRoot,
|
|
2568
|
+
activeCount: projectActive,
|
|
2569
|
+
maxConcurrent: this.maxConcurrentPerProject,
|
|
2570
|
+
}),
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
if (this.active.size < this.maxConcurrent) {
|
|
2575
|
+
const quota = this.callerQuotaLimit(callerId);
|
|
2576
|
+
if (quota !== undefined &&
|
|
2577
|
+
callerId !== undefined &&
|
|
2578
|
+
callerId !== null &&
|
|
2579
|
+
this.countActiveForCaller(callerId) >= quota) {
|
|
2580
|
+
if (this.admissionQueue.size < this.maxQueued) {
|
|
2581
|
+
return { action: "enqueue", reason: "caller_quota" };
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
action: "reject",
|
|
2585
|
+
failure: this.busyFailure("busy_caller_quota", {
|
|
2586
|
+
scope: "caller",
|
|
2587
|
+
caller_id: callerId,
|
|
2588
|
+
activeCount: this.countActiveForCaller(callerId),
|
|
2589
|
+
maxConcurrent: quota,
|
|
2590
|
+
}),
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
const provisionalId = randomUUID();
|
|
2594
|
+
this.provisionalIds.add(provisionalId);
|
|
2595
|
+
this.active.set(provisionalId, {
|
|
2596
|
+
checkoutKey,
|
|
2597
|
+
projectRoot: normalizedRoot,
|
|
2598
|
+
callerId: callerId ?? null,
|
|
2599
|
+
generation: ++this.trackingGeneration,
|
|
2600
|
+
});
|
|
2601
|
+
if (checkoutKey !== undefined) {
|
|
2602
|
+
this.checkoutLeases.set(checkoutKey, provisionalId);
|
|
2603
|
+
}
|
|
2604
|
+
return { action: "reserve", provisionalId };
|
|
2605
|
+
}
|
|
2606
|
+
if (this.admissionQueue.size < this.maxQueued) {
|
|
2607
|
+
return { action: "enqueue", reason: "capacity" };
|
|
2608
|
+
}
|
|
2609
|
+
return {
|
|
2610
|
+
action: "reject",
|
|
2611
|
+
failure: this.busyFailure("busy_capacity", { scope: "global" }),
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
tryReserve(checkoutKey, projectRoot = this.projectRoot, callerId) {
|
|
2615
|
+
const admitted = this.tryAdmitOrEnqueue(checkoutKey, projectRoot, callerId);
|
|
2616
|
+
if (admitted.action === "reserve") {
|
|
2617
|
+
return { ok: true, provisionalId: admitted.provisionalId };
|
|
2618
|
+
}
|
|
2619
|
+
if (admitted.action === "enqueue") {
|
|
2620
|
+
if (admitted.reason === "caller_quota") {
|
|
2621
|
+
const quota = this.callerQuotaLimit(callerId) ?? 0;
|
|
2622
|
+
return {
|
|
2623
|
+
ok: false,
|
|
2624
|
+
failure: this.busyFailure("busy_caller_quota", {
|
|
2625
|
+
scope: "caller",
|
|
2626
|
+
caller_id: callerId ?? undefined,
|
|
2627
|
+
activeCount: callerId !== undefined && callerId !== null
|
|
2628
|
+
? this.countActiveForCaller(callerId)
|
|
2629
|
+
: 0,
|
|
2630
|
+
maxConcurrent: quota,
|
|
2631
|
+
}),
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
return { ok: false, failure: this.busyFailure("busy_capacity") };
|
|
2635
|
+
}
|
|
2636
|
+
return { ok: false, failure: admitted.failure };
|
|
2637
|
+
}
|
|
2638
|
+
ensureQueuedDone(runId) {
|
|
2639
|
+
const existing = this.queuedDone.get(runId);
|
|
2640
|
+
if (existing !== undefined)
|
|
2641
|
+
return existing.promise;
|
|
2642
|
+
let resolve;
|
|
2643
|
+
const promise = new Promise((res) => {
|
|
2644
|
+
resolve = res;
|
|
1298
2645
|
});
|
|
1299
|
-
|
|
1300
|
-
|
|
2646
|
+
this.queuedDone.set(runId, { promise, resolve });
|
|
2647
|
+
return promise;
|
|
2648
|
+
}
|
|
2649
|
+
resolveQueuedDone(runId, result) {
|
|
2650
|
+
const deferred = this.queuedDone.get(runId);
|
|
2651
|
+
if (deferred === undefined)
|
|
2652
|
+
return;
|
|
2653
|
+
this.queuedDone.delete(runId);
|
|
2654
|
+
deferred.resolve(result);
|
|
2655
|
+
}
|
|
2656
|
+
attachQueuedDone(runId, done) {
|
|
2657
|
+
void done.then((result) => this.resolveQueuedDone(runId, result), (err) => this.resolveQueuedDone(runId, {
|
|
2658
|
+
ok: false,
|
|
2659
|
+
outcome: "failed",
|
|
2660
|
+
runDir: this.options.store.getWorkspaceDir(runId),
|
|
2661
|
+
runId,
|
|
2662
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
2663
|
+
}));
|
|
2664
|
+
}
|
|
2665
|
+
async drainAdmissionQueue() {
|
|
2666
|
+
if (!this.acceptingWork)
|
|
2667
|
+
return;
|
|
2668
|
+
if (this.admissionDrainInFlight)
|
|
2669
|
+
return;
|
|
2670
|
+
this.admissionDrainInFlight = true;
|
|
2671
|
+
try {
|
|
2672
|
+
const blockedRoots = new Set();
|
|
2673
|
+
const blockedCallers = new Set();
|
|
2674
|
+
while (this.acceptingWork && this.active.size < this.maxConcurrent) {
|
|
2675
|
+
const next = this.admissionQueue.dequeueNext(blockedRoots, blockedCallers);
|
|
2676
|
+
if (next === undefined)
|
|
2677
|
+
break;
|
|
2678
|
+
const outcome = await this.startDequeuedAdmission(next, blockedCallers);
|
|
2679
|
+
if (outcome === "checkout_busy") {
|
|
2680
|
+
this.admissionQueue.requeueFront(next.projectRoot, next.entry);
|
|
2681
|
+
blockedRoots.add(next.projectRoot);
|
|
2682
|
+
continue;
|
|
2683
|
+
}
|
|
2684
|
+
if (outcome === "project_capacity_busy") {
|
|
2685
|
+
blockedRoots.add(next.projectRoot);
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
if (outcome === "caller_quota_busy") {
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
if (outcome === "capacity_busy") {
|
|
2692
|
+
break;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
finally {
|
|
2697
|
+
this.admissionDrainInFlight = false;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
async startDequeuedAdmission(next, blockedCallers) {
|
|
2701
|
+
if (!this.acceptingWork) {
|
|
2702
|
+
this.admissionQueue.requeueFront(next.projectRoot, next.entry);
|
|
2703
|
+
return "capacity_busy";
|
|
2704
|
+
}
|
|
2705
|
+
const { runId } = next.entry;
|
|
2706
|
+
let meta;
|
|
2707
|
+
try {
|
|
2708
|
+
meta = await this.options.store.readRunMeta(runId);
|
|
2709
|
+
}
|
|
2710
|
+
catch {
|
|
2711
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2712
|
+
return "cancelled";
|
|
2713
|
+
}
|
|
2714
|
+
if (meta.status !== "queued") {
|
|
2715
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2716
|
+
return "cancelled";
|
|
2717
|
+
}
|
|
2718
|
+
if (!(await this.isProjectRootAvailable(next.projectRoot))) {
|
|
2719
|
+
await this.cancelRun(runId, PROJECT_ROOT_UNAVAILABLE_REASON);
|
|
2720
|
+
return "cancelled";
|
|
2721
|
+
}
|
|
2722
|
+
const diskGate = await this.checkDiskFloorAdmission();
|
|
2723
|
+
if (diskGate !== undefined) {
|
|
2724
|
+
await this.cancelRun(runId, INSUFFICIENT_DISK_CANCEL_REASON);
|
|
2725
|
+
return "cancelled";
|
|
2726
|
+
}
|
|
2727
|
+
const pending = this.pendingQueuedStarts.get(runId);
|
|
2728
|
+
let taskYaml;
|
|
2729
|
+
let task;
|
|
2730
|
+
let binding;
|
|
2731
|
+
let pathCheckoutRoot;
|
|
2732
|
+
let checkoutKey;
|
|
2733
|
+
let pipeline;
|
|
2734
|
+
let cwd;
|
|
2735
|
+
let projectRoot;
|
|
2736
|
+
let checkoutOverride;
|
|
2737
|
+
let skipGates;
|
|
2738
|
+
let ciIdentity;
|
|
2739
|
+
let taskPath;
|
|
2740
|
+
let submission;
|
|
2741
|
+
let pinned;
|
|
2742
|
+
let callerId = pending?.callerId ?? meta.caller_id ?? null;
|
|
2743
|
+
let skills = pending?.skills;
|
|
2744
|
+
if (callerId !== undefined &&
|
|
2745
|
+
callerId !== null &&
|
|
2746
|
+
blockedCallers.has(callerId.toLowerCase())) {
|
|
2747
|
+
next.entry.callerId = next.entry.callerId ?? callerId;
|
|
2748
|
+
this.admissionQueue.requeueFront(next.projectRoot, next.entry);
|
|
2749
|
+
return "caller_quota_busy";
|
|
2750
|
+
}
|
|
2751
|
+
try {
|
|
2752
|
+
if (pending !== undefined) {
|
|
2753
|
+
taskYaml = pending.taskYaml;
|
|
2754
|
+
binding = pending.binding;
|
|
2755
|
+
pathCheckoutRoot = pending.pathCheckoutRoot;
|
|
2756
|
+
checkoutKey = pending.checkoutKey;
|
|
2757
|
+
pipeline = pending.pipeline;
|
|
2758
|
+
cwd = pending.cwd;
|
|
2759
|
+
projectRoot = pending.projectRoot;
|
|
2760
|
+
checkoutOverride = pending.checkoutOverride;
|
|
2761
|
+
skipGates = pending.skipGates;
|
|
2762
|
+
ciIdentity = pending.ciIdentity;
|
|
2763
|
+
taskPath = pending.taskPath;
|
|
2764
|
+
submission = pending.submission;
|
|
2765
|
+
pinned = pending.pinned;
|
|
2766
|
+
callerId = pending.callerId ?? meta.caller_id ?? null;
|
|
2767
|
+
const loadedTask = loadTaskFromYamlOutcome(taskYaml, pending.taskLabel);
|
|
2768
|
+
if (!loadedTask.ok) {
|
|
2769
|
+
await this.cancelRun(runId, loadedTask.issues[0]?.message ?? "Invalid task");
|
|
2770
|
+
return "cancelled";
|
|
2771
|
+
}
|
|
2772
|
+
task = loadedTask.value;
|
|
2773
|
+
}
|
|
2774
|
+
else {
|
|
2775
|
+
taskYaml = await this.options.store.readTaskYaml(runId);
|
|
2776
|
+
const loadedTask = loadTaskFromYamlOutcome(taskYaml, `run ${runId} task`);
|
|
2777
|
+
if (!loadedTask.ok) {
|
|
2778
|
+
await this.cancelRun(runId, loadedTask.issues[0]?.message ?? "Invalid task");
|
|
2779
|
+
return "cancelled";
|
|
2780
|
+
}
|
|
2781
|
+
task = loadedTask.value;
|
|
2782
|
+
const bindingOutcome = resolveWorkspaceBinding(task, {});
|
|
2783
|
+
if (!bindingOutcome.ok) {
|
|
2784
|
+
await this.cancelRun(runId, bindingOutcome.issues[0]?.message ?? "Invalid workspace binding");
|
|
2785
|
+
return "cancelled";
|
|
2786
|
+
}
|
|
2787
|
+
binding = bindingOutcome.value;
|
|
2788
|
+
cwd = meta.project_root ?? this.cwd;
|
|
2789
|
+
projectRoot = normalizeCatalogPath(meta.project_root ?? this.projectRoot);
|
|
2790
|
+
if (binding.kind === "checkout") {
|
|
2791
|
+
pathCheckoutRoot = await resolveAndValidateCheckout(task, undefined, cwd);
|
|
2792
|
+
checkoutKey =
|
|
2793
|
+
pathCheckoutRoot !== undefined
|
|
2794
|
+
? await toCheckoutLeaseKey(pathCheckoutRoot)
|
|
2795
|
+
: undefined;
|
|
2796
|
+
}
|
|
2797
|
+
if (meta.pipeline_source === "inline") {
|
|
2798
|
+
const body = await this.options.store.readPipelineBody(runId);
|
|
2799
|
+
if (body === null || body === "") {
|
|
2800
|
+
await this.cancelRun(runId, "pipeline_body unavailable after restart");
|
|
2801
|
+
return "cancelled";
|
|
2802
|
+
}
|
|
2803
|
+
try {
|
|
2804
|
+
pipeline = JSON.parse(body);
|
|
2805
|
+
}
|
|
2806
|
+
catch {
|
|
2807
|
+
await this.cancelRun(runId, "pipeline_body invalid after restart");
|
|
2808
|
+
return "cancelled";
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
else if (meta.pipeline_path) {
|
|
2812
|
+
pipeline = normalizeCatalogPath(meta.pipeline_path);
|
|
2813
|
+
}
|
|
2814
|
+
else {
|
|
2815
|
+
await this.cancelRun(runId, "pipeline_path unavailable after restart");
|
|
2816
|
+
return "cancelled";
|
|
2817
|
+
}
|
|
2818
|
+
taskPath = meta.task_path;
|
|
2819
|
+
skipGates = meta.skip_gates;
|
|
2820
|
+
ciIdentity = {
|
|
2821
|
+
gitSha: meta.git_sha,
|
|
2822
|
+
ciPrUrl: meta.ci_pr_url,
|
|
2823
|
+
ciJobUrl: meta.ci_job_url,
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
catch (err) {
|
|
2828
|
+
await this.cancelRun(runId, err instanceof Error ? err.message : String(err));
|
|
2829
|
+
return "cancelled";
|
|
2830
|
+
}
|
|
2831
|
+
const reserved = this.tryReserve(checkoutKey, projectRoot, callerId);
|
|
2832
|
+
if (!reserved.ok) {
|
|
2833
|
+
if (reserved.failure.code === "busy_checkout") {
|
|
2834
|
+
return "checkout_busy";
|
|
2835
|
+
}
|
|
2836
|
+
this.admissionQueue.requeueFront(next.projectRoot, next.entry);
|
|
2837
|
+
if (reserved.failure.code === "busy_capacity" &&
|
|
2838
|
+
reserved.failure.scope === "project") {
|
|
2839
|
+
return "project_capacity_busy";
|
|
2840
|
+
}
|
|
2841
|
+
if (reserved.failure.code === "busy_caller_quota") {
|
|
2842
|
+
if (callerId !== undefined && callerId !== null) {
|
|
2843
|
+
blockedCallers.add(callerId.toLowerCase());
|
|
2844
|
+
next.entry.callerId = next.entry.callerId ?? callerId;
|
|
2845
|
+
}
|
|
2846
|
+
return "caller_quota_busy";
|
|
2847
|
+
}
|
|
2848
|
+
return "capacity_busy";
|
|
2849
|
+
}
|
|
2850
|
+
let rollback = async () => { };
|
|
2851
|
+
try {
|
|
2852
|
+
if (this.options.onBeforeQueuedMaterialize !== undefined) {
|
|
2853
|
+
await this.options.onBeforeQueuedMaterialize(runId);
|
|
2854
|
+
}
|
|
2855
|
+
const { materialized, rollback: linkRollback } = await materializeWorkspaceBinding({
|
|
2856
|
+
runId,
|
|
2857
|
+
task,
|
|
2858
|
+
binding,
|
|
2859
|
+
checkoutRoot: pathCheckoutRoot,
|
|
2860
|
+
pinned,
|
|
2861
|
+
});
|
|
2862
|
+
rollback = linkRollback;
|
|
2863
|
+
let metaAfterMaterialize;
|
|
2864
|
+
try {
|
|
2865
|
+
metaAfterMaterialize = await this.options.store.readRunMeta(runId);
|
|
2866
|
+
}
|
|
2867
|
+
catch {
|
|
2868
|
+
await rollback().catch(() => undefined);
|
|
2869
|
+
this.clearReservation(reserved.provisionalId);
|
|
2870
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2871
|
+
return "cancelled";
|
|
2872
|
+
}
|
|
2873
|
+
if (metaAfterMaterialize.status !== "queued") {
|
|
2874
|
+
await rollback().catch(() => undefined);
|
|
2875
|
+
this.clearReservation(reserved.provisionalId);
|
|
2876
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2877
|
+
this.schedulingHalts.delete(runId);
|
|
2878
|
+
return "cancelled";
|
|
2879
|
+
}
|
|
2880
|
+
const schedulingHalt = this.ensureSchedulingHalt(runId);
|
|
2881
|
+
const started = await startPipeline({
|
|
2882
|
+
submission,
|
|
2883
|
+
...(this.options.browser !== undefined
|
|
2884
|
+
? { browser: this.options.browser }
|
|
2885
|
+
: {}),
|
|
2886
|
+
agent: this.options.agent,
|
|
2887
|
+
store: this.options.store,
|
|
2888
|
+
taskYaml,
|
|
2889
|
+
taskPath,
|
|
2890
|
+
pipeline,
|
|
2891
|
+
cwd,
|
|
2892
|
+
projectRoot,
|
|
2893
|
+
checkoutOverride,
|
|
2894
|
+
runId: materialized.runId,
|
|
2895
|
+
reuseExistingRun: true,
|
|
2896
|
+
checkoutRoot: materialized.checkoutRoot,
|
|
2897
|
+
repository: materialized.repository,
|
|
2898
|
+
ref: materialized.ref,
|
|
2899
|
+
resolvedSha: materialized.resolvedSha,
|
|
2900
|
+
runBranch: materialized.runBranch,
|
|
2901
|
+
gitSha: ciIdentity?.gitSha,
|
|
2902
|
+
ciPrUrl: ciIdentity?.ciPrUrl,
|
|
2903
|
+
ciJobUrl: ciIdentity?.ciJobUrl,
|
|
2904
|
+
hitl: this.hitl,
|
|
2905
|
+
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
2906
|
+
executionMode: this.executionMode,
|
|
2907
|
+
stageProcessLauncher: this.stageProcessLauncher,
|
|
2908
|
+
operatorCatalog: this.options.operatorCatalog,
|
|
2909
|
+
skipGates,
|
|
2910
|
+
schedulingHalt,
|
|
2911
|
+
...(skills !== undefined ? { skills } : {}),
|
|
2912
|
+
});
|
|
2913
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2914
|
+
this.track(reserved.provisionalId, started.runId, started.done);
|
|
2915
|
+
this.attachQueuedDone(started.runId, started.done);
|
|
2916
|
+
return "started";
|
|
2917
|
+
}
|
|
2918
|
+
catch (err) {
|
|
2919
|
+
await rollback().catch(() => undefined);
|
|
2920
|
+
this.clearReservation(reserved.provisionalId);
|
|
2921
|
+
this.pendingQueuedStarts.delete(runId);
|
|
2922
|
+
this.schedulingHalts.delete(runId);
|
|
2923
|
+
if (err instanceof QueuedRunActivationAborted) {
|
|
2924
|
+
return "cancelled";
|
|
2925
|
+
}
|
|
2926
|
+
const reason = err instanceof StartLinkError
|
|
2927
|
+
? err.code
|
|
2928
|
+
: err instanceof Error
|
|
2929
|
+
? err.message
|
|
2930
|
+
: String(err);
|
|
2931
|
+
await this.cancelRun(runId, reason);
|
|
2932
|
+
return "cancelled";
|
|
1301
2933
|
}
|
|
1302
|
-
return { ok: true, provisionalId };
|
|
1303
2934
|
}
|
|
1304
2935
|
clearReservation(provisionalId) {
|
|
1305
2936
|
this.removeActiveEntry(provisionalId, true);
|
|
@@ -1336,7 +2967,9 @@ export class RunManager {
|
|
|
1336
2967
|
return false;
|
|
1337
2968
|
const tracked = await this.ensureResumeTracked(runId);
|
|
1338
2969
|
if (!tracked.ok) {
|
|
1339
|
-
|
|
2970
|
+
log
|
|
2971
|
+
.child({ run_id: runId })
|
|
2972
|
+
.error("resume.track_failed", `resumeStalledSchedules: ${tracked.reason}`);
|
|
1340
2973
|
return false;
|
|
1341
2974
|
}
|
|
1342
2975
|
try {
|
|
@@ -1354,11 +2987,15 @@ export class RunManager {
|
|
|
1354
2987
|
checkoutRoot: loadedMeta.checkout_root,
|
|
1355
2988
|
hitl: this.hitl,
|
|
1356
2989
|
operatorCatalog: this.options.operatorCatalog,
|
|
2990
|
+
...(this.options.browser !== undefined
|
|
2991
|
+
? { browser: this.options.browser }
|
|
2992
|
+
: {}),
|
|
1357
2993
|
},
|
|
1358
2994
|
maxActiveStagesPerRun: this.maxActiveStagesPerRun,
|
|
1359
2995
|
executionMode: this.executionMode,
|
|
1360
2996
|
stageProcessLauncher: this.stageProcessLauncher,
|
|
1361
2997
|
initialSchedule: hydrated,
|
|
2998
|
+
schedulingHalt: this.ensureSchedulingHalt(runId),
|
|
1362
2999
|
}).finally(() => {
|
|
1363
3000
|
void syncRunStatusFromStages(this.options.store, runId).catch(() => { });
|
|
1364
3001
|
});
|
|
@@ -1380,16 +3017,27 @@ export class RunManager {
|
|
|
1380
3017
|
}
|
|
1381
3018
|
let checkoutKey;
|
|
1382
3019
|
let durableCheckoutRoot;
|
|
3020
|
+
let resumeCallerId = null;
|
|
3021
|
+
let resumeProjectRoot;
|
|
1383
3022
|
try {
|
|
1384
3023
|
const meta = await this.options.store.readRunMeta(runId);
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
3024
|
+
resumeCallerId = meta.caller_id ?? null;
|
|
3025
|
+
resumeProjectRoot = meta.project_root
|
|
3026
|
+
? normalizeCatalogPath(meta.project_root)
|
|
3027
|
+
: undefined;
|
|
3028
|
+
const kind = derivedBindingKindFromMeta(meta);
|
|
3029
|
+
if (kind === "checkout") {
|
|
3030
|
+
const checkoutRoot = meta.checkout_root;
|
|
3031
|
+
if (checkoutRoot !== undefined && checkoutRoot !== "") {
|
|
3032
|
+
durableCheckoutRoot = checkoutRoot;
|
|
3033
|
+
checkoutKey = await toCheckoutLeaseKey(checkoutRoot);
|
|
3034
|
+
}
|
|
1389
3035
|
}
|
|
1390
3036
|
}
|
|
1391
3037
|
catch (err) {
|
|
1392
|
-
|
|
3038
|
+
log
|
|
3039
|
+
.child({ run_id: runId })
|
|
3040
|
+
.error("resume.checkout_root_failed", `invariant: trackResume failed reading checkout_root for run ${runId}; continuing without checkout lease: ${err instanceof Error ? err.message : String(err)}`);
|
|
1393
3041
|
}
|
|
1394
3042
|
const conflictHolder = this.findActiveCheckoutConflict(runId, checkoutKey, durableCheckoutRoot);
|
|
1395
3043
|
if (conflictHolder !== undefined) {
|
|
@@ -1404,6 +3052,8 @@ export class RunManager {
|
|
|
1404
3052
|
this.active.set(runId, {
|
|
1405
3053
|
checkoutKey,
|
|
1406
3054
|
durableCheckoutRoot,
|
|
3055
|
+
projectRoot: resumeProjectRoot,
|
|
3056
|
+
callerId: resumeCallerId,
|
|
1407
3057
|
generation: ++this.trackingGeneration,
|
|
1408
3058
|
});
|
|
1409
3059
|
return { ok: true };
|
|
@@ -1460,7 +3110,7 @@ export class RunManager {
|
|
|
1460
3110
|
catch {
|
|
1461
3111
|
// ignore secondary failures — still fail-closed in memory
|
|
1462
3112
|
}
|
|
1463
|
-
|
|
3113
|
+
log.child({ run_id: runId }).error("attach.quarantined", reason);
|
|
1464
3114
|
}
|
|
1465
3115
|
untrackIfGeneration(runId, generation) {
|
|
1466
3116
|
const entry = this.active.get(runId);
|
|
@@ -1473,6 +3123,7 @@ export class RunManager {
|
|
|
1473
3123
|
if (!entry)
|
|
1474
3124
|
return;
|
|
1475
3125
|
this.active.delete(id);
|
|
3126
|
+
this.schedulingHalts.delete(id);
|
|
1476
3127
|
if (isProvisional) {
|
|
1477
3128
|
this.provisionalIds.delete(id);
|
|
1478
3129
|
}
|
|
@@ -1480,5 +3131,19 @@ export class RunManager {
|
|
|
1480
3131
|
this.checkoutLeases.get(entry.checkoutKey) === id) {
|
|
1481
3132
|
this.checkoutLeases.delete(entry.checkoutKey);
|
|
1482
3133
|
}
|
|
3134
|
+
if (!isProvisional) {
|
|
3135
|
+
void this.drainAdmissionQueue();
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
ensureSchedulingHalt(runId) {
|
|
3139
|
+
let halt = this.schedulingHalts.get(runId);
|
|
3140
|
+
if (halt === undefined) {
|
|
3141
|
+
halt = {
|
|
3142
|
+
halted: !this.acceptingWork,
|
|
3143
|
+
hostShutdown: !this.acceptingWork,
|
|
3144
|
+
};
|
|
3145
|
+
this.schedulingHalts.set(runId, halt);
|
|
3146
|
+
}
|
|
3147
|
+
return halt;
|
|
1483
3148
|
}
|
|
1484
3149
|
}
|