@kici-dev/orchestrator 0.20.1 → 0.21.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/dist/server.js CHANGED
@@ -5,7 +5,7 @@ __cjs_dirname(__filename);
5
5
  import "node:module";
6
6
  import "@kici-dev/shared/load-service-env-file";
7
7
  import { AgentDeliveryMode, AgentPlatform, BaseColdStore, ChunkLru, ChunkRequestWaiter, CommandError, PgTableAdapterBase, RingBuffer, addLogsToArchive, applyProxyKeepAliveTimeouts, chunkBuffer, closeDatabase, computeBackoffDelay, computeMigrationsHash, createDb, createHealthRoutes, createLogger, createMeter, createMetricsRoutes, createPool, createS3Client, decrypt, decryptJson, depCacheKeyOf, deriveKey, encodeKeySegment, encrypt, enrichRequestContext, formatDuration, getPrometheusExporter, getReconnectDelay, getRequestContext, guardStartup, initTelemetry, isPgUniqueViolation, logger, parseDatabaseUrl, redactConfig, requestContext, retryOnBrokenConnection, serializeError, setServiceName, setupGracefulShutdown, shQuote, sha256, stderrTail, stopHttpServer, storeMigrationContentHash, toCommandError, toErrorMessage, trackOpenSockets, validateRequiredTools } from "@kici-dev/shared";
8
- import { ACTOR_AGENT_SUFFIX, AGENT_API_REQUEST_TIMEOUT_MS, AGENT_FEATURE_LABELS, ALLOWED_SYSTEM_VARS, ARTIFACT_NAME_MAX_LENGTH, AccessLogAction, AccessLogOutcome, AccessLogSource, AccessLogTargetType, ActorType, AgentFailureCategory, ApprovalDecision, ArtifactCompleteAckOutcome, ArtifactDownloadOutcome, ArtifactUploadOutcome, AttestationOrigin, BREAKING_FLOOR, CANONICAL_STATUSES, CLUSTER_MEMBERSHIP_MAX_WORKERS, CONTAINER_BUILD_RUNTIME_LABEL, CacheRefScope, CheckMode, CheckRunConclusion, CheckStepOutcome, CiTrustLevel, ConcurrencyStrategy, ContextDeleteErrorCode, ContextGateRejectReason, ContextType, DASHBOARD_REQUEST_TYPE_SET, DEFAULT_APPROVAL_EXPIRY_SECONDS, DEFAULT_CONCURRENCY_STRATEGY, DEFAULT_HOLD_EXPIRY_SECONDS, DEFERRED_PATHS_SUMMARY_SUFFIX, DashboardResponseErrorCode, DeploymentContainerRuntimeSchema, DeploymentModeSchema, EVENT_LOG_PAYLOAD_CHUNK_BYTES, EventLogPayloadStreamError, EventLogSource, EventLogStatus, ExecutionJobStatus, ExecutionRunStatus, ExecutionStepStatus, FanoutCause, FanoutError, FleetHostWriteRefusal, ForkPolicy, GLOBAL_EVAL_SKIPS_RESULT_AWARE_LABEL, HeldRunQueueType, HeldRunStatus, HoldScope, HoldType, ID_TOKEN_CLAIM_NAMES, INIT_LABEL, INIT_RUNNER_ROLE_LABEL, INSTALL_JOB_ID_PREFIX, INVALID_JOIN_REQUEST_MESSAGE, InitFailureCategory, InventorySelectorSchema, JOB_IMAGE_RUNTIME_LABEL, JOIN_PROTOCOL_UPGRADE_TOKEN, JOIN_PROTOCOL_V1_REMOVED_MESSAGE, JobRejectReason, JoinErrorCode, JoinRequestKind, KICI_AGENT_ENV_PREFIX, KICI_EVENT_NAME_PREFIX, KNOWN_ROLES, LabelMatcher, LockFileParseError, LogStream, MAX_APPROVAL_EXPIRY_SECONDS, MIN_APPROVAL_EXPIRY_SECONDS, MIN_PROTOCOL_VERSION, MinimumTrustSchema, NackCode, ORCH_AGENT_CAPABILITIES, ORCH_CAPABILITIES, OWN_INGRESS_MODES, OrchLogPhase, OrchRole, OrchestratorMode, PEER_MUTUAL_AUTH_REQUIRED_REASON, PLATFORM_CONNECTED_MODES, PLATFORM_TO_ORCH_RECOGNIZED_TYPES, PRIVILEGED_ROOT_LABEL, PROTOCOL_VERSION, PayloadOmittedReason, PeerAuthMode, PeerAuthScheme, PeerForgetOutcome, RELAY_INGRESS_MODES, RegisterableTriggerType, RunFailureClass, RuntimeFact, SCALER_EVENT_NAMES, SCHEMA_VERSION, SECURITY_HOLD_JOB_IDS, SSH_TRANSPORT_CAPABILITY, ScaleDownReason, ScaleDownReason as ScaleDownReason$1, ScalerBackendType, ScalerEventType, ScalerEventType as ScalerEventType$1, ScalerOrphansAction, ScalerReloadOutcome, ScalerScaleDownPayload, ScalerScaleUpPayload, ScalerVmStatus, ScalerVmStopOutcome, ScalerVmTracker, SetupStepType, SourceOrigin, SourceSubtype, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, TEST_RUN_LOG_CURSOR_START, TRIGGER_EVENT_TYPES, TimeoutReason, TraceCheck, TriggerSource, TrustTierSchema, UnavailablePathsTrace, VariantKind, WEBHOOK_RELAY_MAX_BODY_BYTES, WS_CLOSE_AGENT_AUTH_FAILED, WS_CLOSE_AUTH_TIMEOUT, WS_CLOSE_DISPATCH_ACK_TIMEOUT, WS_CLOSE_GOING_AWAY, WS_CLOSE_HEARTBEAT_TIMEOUT, WS_CLOSE_INTERNAL_ERROR, WS_CLOSE_INVALID_MESSAGE, WS_CLOSE_PLAN_LIMIT, WS_CLOSE_PROTOCOL_ERROR, WS_CLOSE_SUPERSEDED_BY_RECONNECT, WS_CLOSE_UNAUTHORIZED, WS_MAX_PAYLOAD_BYTES, WebhookRelayResult, WsRateLimiter, accessLogWarmSqlCase, agentAuthRequestSchema, agentLabelOf, agentToOrchestratorMessageSchema, agentTypeLabel, appendChecks, approvalTimeoutSecondsSchema, artifactInvalidNameError, assertValidScopeName, assertValidSecretKey, attestationVerifyStatusSchema, buildEventClaims, buildIdTokenSubject, buildJoinRefusal, buildTrustedPassthroughEnv, buildUnsupportedMessageNack, canonicalizeCapability, canonicalizeLabel, canonicalizeLabelSet, canonicalizeLabels, canonicalizeMatcher, checkArtifactName, clampInitFailure, clampStatusFreeText, classifyJoinRequest, coerceDispatchInputs, collectDiscriminatorTypes, createContentRequirementsTraceEntry, createDispatchFailureTraceEntry, createGlobalFilterTraceEntry, createWorkflowDecision, dashboardArtifactsListResponseSchema, dashboardAttestationGetResponseSchema, dashboardAttestationsListAllResponseSchema, dashboardAttestationsListResponseSchema, dashboardRunDetailApiResponseSchema, dashboardStepLogsApiResponseSchema, deriveOsArchLabels, diffRangeKindSchema, executionStatusSchema, fanoutEnvelopeFields, flattenActor, formatLabelMatcher, getAccessLogColdDays, getRepoGlobMatcher, getSecretAuditLogColdDays, githubIngressPath, githubWebhookPath, globalEvalRoundResultSchema, hostLabel, hostSatisfiesTarget, hostToScalerPlatform, installGateJobId, invalidFramePaths, isFailureStatus, isJoinRequestFrame, isKnownCapability, isLockDynamicJobFn, isLockParallelStep, isLockStaticJob, isPullRequestFamilyTriggerEvent, isSecurityHoldJobId, isSelfReportedLabel, jobStatusForwardSchema, joinRequestSchema, logPullPlatformToOrchSchema, matchAllWorkflows, matchScopePattern, matchWorkflowsForEvent, matcherSatisfiedBy, materializeFanout, materializeResolvedHosts, materializeResolvedMatrix, matrixEnvelopeFields, mergeOrderedMaps, minAccessLogWarmDays, minSecretAuditLogWarmDays, partitionMatchers, peerAuthRequestSchema, peerFromPeerMessageSchema, peerHelloResponseSchema, peerHelloSchema, platformToOrchestratorMessageSchema, platformToOsArchLabels, platformToTaints, reservedEventNamePrefix, resolveDiffRange, resolveRoleLabels, resolveScheduleInputs, resolveSecretsWithProvenance, resourceRequestNestedSchema, runtimeLabel, scalerAgentLabels, scalerLabel, scalerPlatformSchema, scheduleTriggerKey, secretAuditLogWarmSqlCase, shouldRecordAccess, shouldRecordSecretResolve, sleep, stateReplaySchema, stepStatusForwardSchema, stringifyActor, testRelayTriggerRequestSchema, testRelayUploadsInitRequestSchema, toIsoString, truncateDecisionsToByteBudget, truncateReasonText, trustPolicyUpdateSchema, trustedContributorHoldReason, validateScopeName, validateSecretKey, wrapUntrusted } from "@kici-dev/engine";
8
+ import { ACTOR_AGENT_SUFFIX, AGENT_API_REQUEST_TIMEOUT_MS, AGENT_FEATURE_LABELS, ALLOWED_SYSTEM_VARS, ARTIFACT_NAME_MAX_LENGTH, AccessLogAction, AccessLogOutcome, AccessLogSource, AccessLogTargetType, ActorType, AgentFailureCategory, ApprovalDecision, ArtifactCompleteAckOutcome, ArtifactDownloadOutcome, ArtifactUploadOutcome, AttestationOrigin, BREAKING_FLOOR, CANONICAL_STATUSES, CLUSTER_MEMBERSHIP_MAX_WORKERS, CONTAINER_BUILD_RUNTIME_LABEL, CacheRefScope, CheckMode, CheckRunConclusion, CheckStepOutcome, CiTrustLevel, ConcurrencyStrategy, ContextDeleteErrorCode, ContextGateRejectReason, ContextType, DASHBOARD_REQUEST_TYPE_SET, DEFAULT_APPROVAL_EXPIRY_SECONDS, DEFAULT_CONCURRENCY_STRATEGY, DEFAULT_HOLD_EXPIRY_SECONDS, DEFERRED_PATHS_SUMMARY_SUFFIX, DashboardResponseErrorCode, DeploymentContainerRuntimeSchema, DeploymentModeSchema, EVENT_LOG_PAYLOAD_CHUNK_BYTES, EventLogPayloadStreamError, EventLogSource, EventLogStatus, ExecutionJobStatus, ExecutionRunStatus, ExecutionStepStatus, FanoutCause, FanoutError, FleetHostWriteRefusal, ForkPolicy, GLOBAL_EVAL_SKIPS_RESULT_AWARE_LABEL, HeldRunQueueType, HeldRunStatus, HoldScope, HoldType, ID_TOKEN_CLAIM_NAMES, INIT_LABEL, INIT_RUNNER_ROLE_LABEL, INIT_STEP_TYPE_PREFIX, INSTALL_JOB_ID_PREFIX, INVALID_JOIN_REQUEST_MESSAGE, InitFailureCategory, InventorySelectorSchema, JOB_IMAGE_RUNTIME_LABEL, JOIN_PROTOCOL_UPGRADE_TOKEN, JOIN_PROTOCOL_V1_REMOVED_MESSAGE, JobRejectReason, JoinErrorCode, JoinRequestKind, KICI_AGENT_ENV_PREFIX, KICI_EVENT_NAME_PREFIX, KNOWN_ROLES, LabelMatcher, LockFileParseError, LogStream, MAX_APPROVAL_EXPIRY_SECONDS, MIN_APPROVAL_EXPIRY_SECONDS, MIN_PROTOCOL_VERSION, MinimumTrustSchema, NackCode, ORCH_AGENT_CAPABILITIES, ORCH_CAPABILITIES, OWN_INGRESS_MODES, OrchLogPhase, OrchRole, OrchestratorMode, PEER_MUTUAL_AUTH_REQUIRED_REASON, PLATFORM_CONNECTED_MODES, PLATFORM_TO_ORCH_RECOGNIZED_TYPES, PRIVILEGED_ROOT_LABEL, PROTOCOL_VERSION, PayloadOmittedReason, PeerAuthMode, PeerAuthScheme, PeerForgetOutcome, RELAY_INGRESS_MODES, RegisterableTriggerType, RunFailureClass, RuntimeFact, SCALER_EVENT_NAMES, SCHEMA_VERSION, SECURITY_HOLD_JOB_IDS, SSH_TRANSPORT_CAPABILITY, ScaleDownReason, ScaleDownReason as ScaleDownReason$1, ScalerBackendType, ScalerEventType, ScalerEventType as ScalerEventType$1, ScalerOrphansAction, ScalerReloadOutcome, ScalerScaleDownPayload, ScalerScaleUpPayload, ScalerVmStatus, ScalerVmStopOutcome, ScalerVmTracker, SetupStepType, SourceOrigin, SourceSubtype, StepPhase, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, TEST_RUN_LOG_CURSOR_START, TRIGGER_EVENT_TYPES, TimeoutReason, TraceCheck, TriggerSource, TrustTierSchema, UnavailablePathsTrace, VariantKind, WEBHOOK_RELAY_MAX_BODY_BYTES, WS_CLOSE_AGENT_AUTH_FAILED, WS_CLOSE_AUTH_TIMEOUT, WS_CLOSE_DISPATCH_ACK_TIMEOUT, WS_CLOSE_GOING_AWAY, WS_CLOSE_HEARTBEAT_TIMEOUT, WS_CLOSE_INTERNAL_ERROR, WS_CLOSE_INVALID_MESSAGE, WS_CLOSE_PLAN_LIMIT, WS_CLOSE_PROTOCOL_ERROR, WS_CLOSE_SUPERSEDED_BY_RECONNECT, WS_CLOSE_UNAUTHORIZED, WS_MAX_PAYLOAD_BYTES, WebhookRelayResult, WsRateLimiter, accessLogWarmSqlCase, agentAuthRequestSchema, agentLabelOf, agentToOrchestratorMessageSchema, agentTypeLabel, appendChecks, approvalTimeoutSecondsSchema, artifactInvalidNameError, assertValidScopeName, assertValidSecretKey, attestationVerifyStatusSchema, buildEventClaims, buildIdTokenSubject, buildJoinRefusal, buildTrustedPassthroughEnv, buildUnsupportedMessageNack, canonicalizeCapability, canonicalizeLabel, canonicalizeLabelSet, canonicalizeLabels, canonicalizeMatcher, checkArtifactName, clampInitFailure, clampStatusFreeText, classifyJoinRequest, coerceDispatchInputs, collectDiscriminatorTypes, compareStepsByPhase, createContentRequirementsTraceEntry, createDispatchFailureTraceEntry, createGlobalFilterTraceEntry, createWorkflowDecision, dashboardArtifactsListResponseSchema, dashboardAttestationGetResponseSchema, dashboardAttestationsListAllResponseSchema, dashboardAttestationsListResponseSchema, dashboardRunDetailApiResponseSchema, dashboardStepLogsApiResponseSchema, deriveOsArchLabels, diffRangeKindSchema, executionStatusSchema, fanoutEnvelopeFields, flattenActor, formatLabelMatcher, getAccessLogColdDays, getRepoGlobMatcher, getSecretAuditLogColdDays, githubIngressPath, githubWebhookPath, globalEvalRoundResultSchema, hostLabel, hostSatisfiesTarget, hostToScalerPlatform, initStepIndex, installGateJobId, invalidFramePaths, isFailureStatus, isJoinRequestFrame, isKnownCapability, isLockDynamicJobFn, isLockParallelStep, isLockStaticJob, isPullRequestFamilyTriggerEvent, isRunId, isSecurityHoldJobId, isSelfReportedLabel, jobStatusForwardSchema, joinRequestSchema, logPullPlatformToOrchSchema, matchAllWorkflows, matchScopePattern, matchWorkflowsForEvent, matcherSatisfiedBy, materializeFanout, materializeResolvedHosts, materializeResolvedMatrix, matrixEnvelopeFields, mergeOrderedMaps, minAccessLogWarmDays, minSecretAuditLogWarmDays, parseRunIdRef, partitionMatchers, peerAuthRequestSchema, peerFromPeerMessageSchema, peerHelloResponseSchema, peerHelloSchema, platformToOrchestratorMessageSchema, platformToOsArchLabels, platformToTaints, reservedEventNamePrefix, resolveDiffRange, resolveRoleLabels, resolveScheduleInputs, resolveSecretsWithProvenance, resourceRequestNestedSchema, runIdPrefixRange, runtimeLabel, scalerAgentLabels, scalerLabel, scalerPlatformSchema, scheduleTriggerKey, secretAuditLogWarmSqlCase, shouldRecordAccess, shouldRecordSecretResolve, sleep, stateReplaySchema, stepPhase, stepStatusForwardSchema, stepTypeOf, stringifyActor, testRelayTriggerRequestSchema, testRelayUploadsInitRequestSchema, toIsoString, truncateDecisionsToByteBudget, truncateReasonText, trustPolicyUpdateSchema, trustedContributorHoldReason, validateScopeName, validateSecretKey, wrapUntrusted } from "@kici-dev/engine";
9
9
  import * as fs$2 from "node:fs";
10
10
  import fs, { chmodSync, closeSync, createReadStream, createWriteStream, fsyncSync, mkdirSync, mkdtempSync, openSync, promises, readFileSync, renameSync, rmSync, statSync, unwatchFile, watchFile, writeFileSync, writeSync } from "node:fs";
11
11
  import { z } from "zod";
@@ -8056,7 +8056,7 @@ var init_execution_tracker = __esmMin((() => {
8056
8056
  const run = this.runs.get(runId);
8057
8057
  const prefix = `needs-pending-${jobName}-`;
8058
8058
  if (run) {
8059
- for (const key of run.jobs.keys()) if (key.startsWith(prefix)) return key;
8059
+ for (const [key, job] of run.jobs) if (key.startsWith(prefix) && job.name === jobName) return key;
8060
8060
  }
8061
8061
  return (await this.db.selectFrom("execution_jobs").select("job_id").where("run_id", "=", runId).where("job_name", "=", jobName).where("job_id", "like", `${prefix}%`).executeTakeFirst())?.job_id;
8062
8062
  }
@@ -9130,6 +9130,10 @@ var init_execution_tracker = __esmMin((() => {
9130
9130
  async recordInitFailureRun(args) {
9131
9131
  const now = /* @__PURE__ */ new Date();
9132
9132
  const customerId = await this.resolveCustomerId(args.routingKey);
9133
+ const testRunColumns = args.testRun ? {
9134
+ is_test_run: true,
9135
+ fixture_id: args.testRun.fixtureId
9136
+ } : {};
9133
9137
  const crossRepoWorkflowRepo = crossRepoWorkflowRepoOf(args);
9134
9138
  if (!guardedWriteApplied(await this.db.insertInto("execution_runs").values({
9135
9139
  run_id: args.runId,
@@ -9151,7 +9155,8 @@ var init_execution_tracker = __esmMin((() => {
9151
9155
  init_failure: JSON.stringify(args.initFailure),
9152
9156
  ...inheritedChainDepth(args.chainDepth),
9153
9157
  ...rerunLineageColumns(args.rerunLineage),
9154
- ...crossRepoWorkflowRepo && { workflow_repo_identifier: crossRepoWorkflowRepo }
9158
+ ...crossRepoWorkflowRepo && { workflow_repo_identifier: crossRepoWorkflowRepo },
9159
+ ...testRunColumns
9155
9160
  }).onConflict((oc) => oc.column("run_id").doUpdateSet({
9156
9161
  completed_at: now,
9157
9162
  duration_ms: sql`extract(epoch from (${now} - execution_runs.started_at)) * 1000`,
@@ -9160,7 +9165,8 @@ var init_execution_tracker = __esmMin((() => {
9160
9165
  failure_class: RunFailureClass.enum.never_started,
9161
9166
  init_failure: JSON.stringify(args.initFailure),
9162
9167
  ...inheritedChainDepth(args.chainDepth),
9163
- ...crossRepoWorkflowRepo && { workflow_repo_identifier: crossRepoWorkflowRepo }
9168
+ ...crossRepoWorkflowRepo && { workflow_repo_identifier: crossRepoWorkflowRepo },
9169
+ ...testRunColumns
9164
9170
  }).where("execution_runs.status", "not in", [...TERMINAL_RUN_STATES])).executeTakeFirst())) {
9165
9171
  logger$171.debug("Init-failure record skipped: run already terminal in DB", { runId: args.runId });
9166
9172
  return;
@@ -9265,6 +9271,10 @@ var init_execution_tracker = __esmMin((() => {
9265
9271
  */
9266
9272
  async recordRunHeld(args) {
9267
9273
  const now = /* @__PURE__ */ new Date();
9274
+ const testRunColumns = args.testRun ? {
9275
+ is_test_run: true,
9276
+ fixture_id: args.testRun.fixtureId
9277
+ } : {};
9268
9278
  const customerId = await this.resolveCustomerId(args.routingKey);
9269
9279
  const crossRepoProvenance = crossRepoProvenanceOf(args.repoIdentifier, {
9270
9280
  identifier: args.workflowRepoIdentifier,
@@ -9292,11 +9302,13 @@ var init_execution_tracker = __esmMin((() => {
9292
9302
  ...rerunLineageColumns(args.rerunLineage),
9293
9303
  ...crossRepoProvenance && workflowProvenanceColumns(crossRepoProvenance),
9294
9304
  ...args.isGlobalEvalRound === true && { is_global_eval_round: true },
9295
- ...args.dispatchRoutingKey != null && args.dispatchRoutingKey !== args.routingKey && { dispatch_routing_key: args.dispatchRoutingKey }
9305
+ ...args.dispatchRoutingKey != null && args.dispatchRoutingKey !== args.routingKey && { dispatch_routing_key: args.dispatchRoutingKey },
9306
+ ...testRunColumns
9296
9307
  }).onConflict((oc) => oc.column("run_id").doUpdateSet({
9297
9308
  status: ExecutionRunStatus.enum.held,
9298
9309
  ...args.contextName && { context: args.contextName },
9299
9310
  ...args.prNumber != null && { pr_number: args.prNumber },
9311
+ ...testRunColumns,
9300
9312
  ...inheritedChainDepth(args.chainDepth),
9301
9313
  ...crossRepoProvenance && {
9302
9314
  workflow_repo_identifier: crossRepoProvenance.identifier,
@@ -9562,7 +9574,8 @@ var init_execution_tracker = __esmMin((() => {
9562
9574
  log_path: logPath,
9563
9575
  routing_key: this.runs.get(runId)?.routingKey ?? null
9564
9576
  };
9565
- if (data?.stepType && typeof data.stepType === "string") values.step_type = data.stepType;
9577
+ const stepType = stepTypeOf(data);
9578
+ if (stepType) values.step_type = stepType;
9566
9579
  if (state === ExecutionStepStatus.enum.running) values.started_at = now;
9567
9580
  if (TERMINAL_JOB_STATES.has(state)) {
9568
9581
  values.completed_at = now;
@@ -14920,6 +14933,17 @@ var init_reduced_privilege_note = __esmMin((() => {
14920
14933
  * - Size safety: 65535 byte limit with progressive truncation
14921
14934
  */
14922
14935
  /**
14936
+ * Record one step's progress: replace the entry for its index, or add it in display
14937
+ * order. Pseudo-steps have high indices (init at 1_000_000 + n), so the list is keyed
14938
+ * by index rather than position; a positional list would pad every gap up to it.
14939
+ */
14940
+ function applyStepProgress(steps, entry) {
14941
+ return [...steps.map((s, position) => ({
14942
+ ...s,
14943
+ stepIndex: s.stepIndex ?? position
14944
+ })).filter((s) => s.stepIndex !== entry.stepIndex), entry].sort(compareStepsByPhase);
14945
+ }
14946
+ /**
14923
14947
  * Cut `summary` to fit the API's cap, whatever a caller wrapped around it. The last line of
14924
14948
  * defence: `reservedBytes` keeps a summary readable by dropping log tiers first.
14925
14949
  */
@@ -15904,27 +15928,13 @@ var init_check_run_reporter = __esmMin((() => {
15904
15928
  if (!checkRunId) return;
15905
15929
  if (this.terminalSent.has(key)) return;
15906
15930
  this.trackRunKey(opts.runId, key);
15907
- let steps = this.stepProgress.get(key);
15908
- if (!steps) {
15909
- steps = [];
15910
- this.stepProgress.set(key, steps);
15911
- }
15912
- if (opts.stepIndex < steps.length) steps[opts.stepIndex] = {
15931
+ const steps = applyStepProgress(this.stepProgress.get(key) ?? [], {
15913
15932
  name: opts.stepName,
15914
15933
  status: opts.state,
15915
- durationMs: opts.durationMs
15916
- };
15917
- else {
15918
- while (steps.length < opts.stepIndex) steps.push({
15919
- name: `Step ${steps.length}`,
15920
- status: "pending"
15921
- });
15922
- steps.push({
15923
- name: opts.stepName,
15924
- status: opts.state,
15925
- durationMs: opts.durationMs
15926
- });
15927
- }
15934
+ durationMs: opts.durationMs,
15935
+ stepIndex: opts.stepIndex,
15936
+ ...opts.stepType && { stepType: opts.stepType }
15937
+ });
15928
15938
  await this.persistStepProgress(key, steps, opts.runId);
15929
15939
  if (opts.state === ExecutionStepStatus.enum.running && !this.inProgressSent.get(key)) {
15930
15940
  await this.persistInProgressSent(key, opts.runId);
@@ -48284,7 +48294,9 @@ function createAdminQueueExecutionRoutes(deps) {
48284
48294
  });
48285
48295
  app.get("/executions/:runId", async (c) => {
48286
48296
  deps.rbac.requirePermission(c.get("role"), "secret.read");
48287
- const runId = c.req.param("runId");
48297
+ const rawRunId = c.req.param("runId");
48298
+ if (!isRunId(rawRunId)) return c.json({ error: `Invalid run id '${rawRunId}': a run id is a full UUID.` }, 400);
48299
+ const runId = rawRunId.toLowerCase();
48288
48300
  const runRow = await sql`
48289
48301
  SELECT id, run_id, workflow_name, status, provider, repo_identifier,
48290
48302
  ref, sha, routing_key, context, trust_tier, created_at,
@@ -49220,6 +49232,10 @@ var init_decorating_secret_resolver = __esmMin((() => {
49220
49232
  * decorating secret resolver), and the fixture's `secrets` context mapping
49221
49233
  * resolves into namespaced secrets;
49222
49234
  * - direct workflow execution (bypass trigger matching) is supported.
49235
+ *
49236
+ * Like a webhook, it creates one run per matched workflow. A caller that
49237
+ * follows a single run (no `acceptsMultipleRuns`) gets one shared run instead,
49238
+ * and only when the workflows' job names cannot collide in it.
49223
49239
  */
49224
49240
  /**
49225
49241
  * Repo identity for an inline-lock (local working tree) run. Derived from the
@@ -49438,9 +49454,90 @@ function buildTestDispatchContext(shared, workflow, decision, runId) {
49438
49454
  ...Object.keys(shared.runWideFlatSecrets).length > 0 && { runWideFlatSecrets: shared.runWideFlatSecrets }
49439
49455
  };
49440
49456
  }
49457
+ /** The static job names a workflow defines; null when it has a dynamic job, whose names are unknown until it runs. */
49458
+ function staticJobNames(workflow) {
49459
+ const names = [];
49460
+ for (const job of workflow.jobs) {
49461
+ if (job._type !== "static") return null;
49462
+ names.push(job.name);
49463
+ }
49464
+ return names;
49465
+ }
49466
+ /**
49467
+ * The job names two or more matched workflows define, sorted. Null when a
49468
+ * matched workflow has a dynamic job, so the names cannot be compared before
49469
+ * the run.
49470
+ */
49471
+ function sharedJobNames(fullLockFile, matchedDecisions) {
49472
+ const owners = /* @__PURE__ */ new Map();
49473
+ for (const decision of matchedDecisions) {
49474
+ const workflow = fullLockFile.workflows.find((w) => w.name === decision.workflowName);
49475
+ if (!workflow) continue;
49476
+ const names = staticJobNames(workflow);
49477
+ if (names === null) return null;
49478
+ for (const name of new Set(names)) owners.set(name, (owners.get(name) ?? 0) + 1);
49479
+ }
49480
+ return [...owners.entries()].filter(([, count]) => count > 1).map(([name]) => name).sort();
49481
+ }
49482
+ /**
49483
+ * Split the matched workflows into runs: one run per workflow, as every other
49484
+ * run producer does.
49485
+ *
49486
+ * A caller that does not set `acceptsMultipleRuns` follows only `runId`. For
49487
+ * it, several matched workflows share one run when their job names cannot
49488
+ * collide (a run keys its needs edges, pending contexts and logs by job name),
49489
+ * and the trigger is refused when they can.
49490
+ */
49491
+ function planTestRuns(input, fullLockFile, matchedDecisions, firstRunId) {
49492
+ if (input.acceptsMultipleRuns || matchedDecisions.length === 1) return matchedDecisions.map((decision, i) => ({
49493
+ runId: i === 0 ? firstRunId : randomUUID(),
49494
+ decisions: [decision]
49495
+ }));
49496
+ const shared = sharedJobNames(fullLockFile, matchedDecisions);
49497
+ if (shared === null || shared.length > 0) return { rejected: `This fixture matches several workflows (${matchedDecisions.map((d) => d.workflowName).join(", ")}) and ${shared === null ? "one of them defines dynamic jobs, whose names cannot be checked before the run" : `they share job names (${shared.join(", ")})`}. This kici version follows a single run, so upgrade kici to run each workflow as its own run, or pass --workflow <name> to run one workflow.` };
49498
+ return [{
49499
+ runId: firstRunId,
49500
+ decisions: matchedDecisions
49501
+ }];
49502
+ }
49503
+ /**
49504
+ * Dispatch one planned run. A run of several workflows (the shared run of a
49505
+ * caller without `acceptsMultipleRuns`) is held open from its first
49506
+ * registration until its last workflow has registered: a workflow whose jobs
49507
+ * all end before the next one registers would otherwise complete the run, and
49508
+ * the next workflow would start it over.
49509
+ */
49510
+ async function dispatchPlannedRun(shared, deps, planned) {
49511
+ const { runId, decisions } = planned;
49512
+ const out = {
49513
+ jobIds: [],
49514
+ deferredJobCount: 0,
49515
+ warnings: []
49516
+ };
49517
+ let runHeld = false;
49518
+ const holdRunOpen = () => {
49519
+ if (!runHeld && decisions.length > 1) runHeld = deps.executionTracker?.holdRunForPendingJobs(runId) ?? false;
49520
+ };
49521
+ try {
49522
+ for (const decision of decisions) {
49523
+ const workflow = shared.fullLockFile.workflows.find((w) => w.name === decision.workflowName);
49524
+ if (!workflow) continue;
49525
+ const ctx = buildTestDispatchContext(shared, workflow, decision, runId);
49526
+ ctx.onRunRegistered = holdRunOpen;
49527
+ const result = await dispatchMatchedWorkflow(ctx);
49528
+ out.jobIds.push(...result.dispatchedJobIds);
49529
+ out.deferredJobCount += result.deferredJobCount ?? 0;
49530
+ if (result.envWarnings?.length) out.warnings.push(...result.envWarnings);
49531
+ }
49532
+ } finally {
49533
+ if (runHeld) await deps.executionTracker?.releasePendingJobsHold(runId);
49534
+ }
49535
+ return out;
49536
+ }
49441
49537
  /**
49442
49538
  * Process a test trigger through the shared dispatch core: one
49443
- * `WorkflowDispatchContext` per matched workflow, into `dispatchMatchedWorkflow`.
49539
+ * `WorkflowDispatchContext` per matched workflow, into `dispatchMatchedWorkflow`,
49540
+ * each under its own run id.
49444
49541
  */
49445
49542
  async function processTestTrigger(input, deps) {
49446
49543
  const runId = randomUUID();
@@ -49476,6 +49573,13 @@ async function processTestTrigger(input, deps) {
49476
49573
  jobIds: []
49477
49574
  };
49478
49575
  const matchedDecisions = matchResult;
49576
+ const plan = planTestRuns(input, fullLockFile, matchedDecisions, runId);
49577
+ if (!Array.isArray(plan)) return {
49578
+ runId,
49579
+ status: "rejected",
49580
+ reason: plan.rejected,
49581
+ jobIds: []
49582
+ };
49479
49583
  const resolvedOrgId = deps.db ? await resolveOrgId(deps.db, input.routingKey) : "__default__";
49480
49584
  const dispatchInputsResult = resolveDispatchInputs(input, fullLockFile, matchedDecisions);
49481
49585
  if ("rejected" in dispatchInputsResult) return {
@@ -49491,13 +49595,13 @@ async function processTestTrigger(input, deps) {
49491
49595
  reason: namespacedResult.rejected,
49492
49596
  jobIds: []
49493
49597
  };
49494
- await storeFixturePayload(input, deps, runId);
49598
+ for (const planned of plan) await storeFixturePayload(input, deps, planned.runId);
49495
49599
  const cliSecrets = decryptCliSecrets(input);
49496
49600
  const testDeps = deps.secretResolver ? {
49497
49601
  ...deps,
49498
49602
  secretResolver: new DecoratingSecretResolver(deps.secretResolver, cliSecrets)
49499
49603
  } : deps;
49500
- deps.executionTracker?.markTestRun(runId);
49604
+ for (const planned of plan) deps.executionTracker?.markTestRun(planned.runId);
49501
49605
  const provider = inlineProvider ?? input.routingKey.split(":")[0] ?? "local";
49502
49606
  const shared = {
49503
49607
  input,
@@ -49523,34 +49627,31 @@ async function processTestTrigger(input, deps) {
49523
49627
  const jobIds = [];
49524
49628
  const warnings = [];
49525
49629
  let deferredJobCount = 0;
49526
- let runHeld = false;
49527
- const holdRunOpen = () => {
49528
- if (!runHeld && matchedDecisions.length > 1) runHeld = deps.executionTracker?.holdRunForPendingJobs(runId) ?? false;
49529
- };
49530
- try {
49531
- for (const decision of matchedDecisions) {
49532
- const workflow = fullLockFile.workflows.find((w) => w.name === decision.workflowName);
49533
- if (!workflow) continue;
49534
- const ctx = buildTestDispatchContext(shared, workflow, decision, runId);
49535
- ctx.onRunRegistered = holdRunOpen;
49536
- const result = await dispatchMatchedWorkflow(ctx);
49537
- jobIds.push(...result.dispatchedJobIds);
49538
- deferredJobCount += result.deferredJobCount ?? 0;
49539
- if (result.envWarnings?.length) warnings.push(...result.envWarnings);
49540
- }
49541
- } finally {
49542
- if (runHeld) await deps.executionTracker?.releasePendingJobsHold(runId);
49543
- }
49630
+ const runs = [];
49631
+ for (const planned of plan) {
49632
+ const dispatched = await dispatchPlannedRun(shared, deps, planned);
49633
+ jobIds.push(...dispatched.jobIds);
49634
+ deferredJobCount += dispatched.deferredJobCount;
49635
+ warnings.push(...dispatched.warnings);
49636
+ if (planned.decisions.length === 1) runs.push({
49637
+ runId: planned.runId,
49638
+ workflowName: planned.decisions[0].workflowName,
49639
+ jobIds: dispatched.jobIds
49640
+ });
49641
+ }
49642
+ const runsField = runs.length === plan.length ? { runs } : {};
49544
49643
  if (jobIds.length === 0 && deferredJobCount === 0) return {
49545
49644
  runId,
49546
49645
  status: "rejected",
49547
49646
  reason: "No jobs dispatched (all matched workflows had no static jobs or dispatch was rejected)",
49548
- jobIds: []
49647
+ jobIds: [],
49648
+ ...runsField
49549
49649
  };
49550
49650
  return {
49551
49651
  runId,
49552
49652
  status: "accepted",
49553
49653
  jobIds,
49654
+ ...runsField,
49554
49655
  ...warnings.length > 0 && { warnings }
49555
49656
  };
49556
49657
  }
@@ -49632,17 +49733,19 @@ async function handleTestTrigger(msg, deps) {
49632
49733
  checkMode: msg.checkMode,
49633
49734
  target: msg.target,
49634
49735
  dispatchInputs: msg.dispatchInputs,
49736
+ ...msg.acceptsMultipleRuns && { acceptsMultipleRuns: true },
49635
49737
  requestId: msg.requestId,
49636
49738
  actor: msg.actor
49637
49739
  }, deps);
49638
- runDetached(logger$93, "Access log write", () => deps.accessLog?.record({
49740
+ const runIds = result.runs?.length ? result.runs.map((r) => r.runId) : [result.runId];
49741
+ for (const runId of runIds) runDetached(logger$93, "Access log write", () => deps.accessLog?.record({
49639
49742
  orgId: deps.orgId ?? null,
49640
49743
  routingKey: deps.routingKey ?? msg.routingKey,
49641
49744
  actor: msg.actor,
49642
49745
  action: "run.trigger",
49643
49746
  target: {
49644
49747
  type: "run",
49645
- id: result.runId
49748
+ id: runId
49646
49749
  },
49647
49750
  requestId: msg.requestId,
49648
49751
  source: deps.accessLogSource ?? AccessLogSource.enum.platform_proxy,
@@ -49650,10 +49753,11 @@ async function handleTestTrigger(msg, deps) {
49650
49753
  ...result.reason ? { errorMessage: result.reason } : {}
49651
49754
  }), {
49652
49755
  requestId: msg.requestId,
49653
- runId: result.runId
49756
+ runId
49654
49757
  });
49655
49758
  return {
49656
49759
  runId: result.runId,
49760
+ ...result.runs && { runs: result.runs },
49657
49761
  status: result.status,
49658
49762
  ...result.reason ? { reason: result.reason } : {},
49659
49763
  jobIds: result.jobIds,
@@ -49688,7 +49792,8 @@ async function handleTestRunStatus(msg, deps) {
49688
49792
  const run = await deps.db.selectFrom("execution_runs").select([
49689
49793
  "run_id",
49690
49794
  "status",
49691
- "is_test_run"
49795
+ "is_test_run",
49796
+ "failure_reason"
49692
49797
  ]).where("run_id", "=", msg.runId).executeTakeFirst();
49693
49798
  if (!run || !run.is_test_run) return { error: "Run not found" };
49694
49799
  const jobs = await deps.db.selectFrom("execution_jobs").select([
@@ -49708,6 +49813,7 @@ async function handleTestRunStatus(msg, deps) {
49708
49813
  errorMessage: j.error_message ?? null,
49709
49814
  durationMs: j.duration_ms ?? null
49710
49815
  })),
49816
+ failureReason: run.failure_reason ?? null,
49711
49817
  done: TERMINAL_RUN_STATES.has(run.status)
49712
49818
  };
49713
49819
  }
@@ -49784,14 +49890,18 @@ async function collectRunLogLines(runId, logStorage) {
49784
49890
  async function readFromFileOffsets(runId, logStorage, offsets, terminal) {
49785
49891
  const next = { ...offsets };
49786
49892
  const lines = [];
49893
+ const lineJobNames = [];
49787
49894
  if (logStorage) for (const file of await listRunLogFiles(runId, logStorage)) {
49788
49895
  const fileLines = splitLogLines((await logStorage.read(file.path)).data, terminal);
49789
49896
  const from = Math.min(offsets[file.key] ?? 0, fileLines.length);
49790
- lines.push(...fileLines.slice(from));
49897
+ const fresh = fileLines.slice(from);
49898
+ lines.push(...fresh);
49899
+ for (let i = 0; i < fresh.length; i++) lineJobNames.push(file.jobName);
49791
49900
  next[file.key] = Math.max(offsets[file.key] ?? 0, fileLines.length);
49792
49901
  }
49793
49902
  return {
49794
49903
  lines,
49904
+ lineJobNames,
49795
49905
  nextCursor: Object.values(next).reduce((sum, n) => sum + n, 0),
49796
49906
  done: terminal,
49797
49907
  nextLogCursor: encodeRunLogCursor(next)
@@ -52270,26 +52380,47 @@ var init_admin_registrations = __esmMin((() => {
52270
52380
  *
52271
52381
  * Sorting `execution_steps` by `step_index` alone puts a pseudo-step where its index landed,
52272
52382
  * not where it ran. That suits the cache pseudo-steps (indices above the real steps, mostly
52273
- * bracketing the work), but a job's image build runs strictly FIRST.
52383
+ * bracketing the work), but a job's image build and its init commands run strictly FIRST.
52274
52384
  *
52275
52385
  * The index cannot express this: `step.status` rejects a negative index (an agent sending one
52276
52386
  * to an older orchestrator is disconnected mid-job), and the check-run reporter
52277
52387
  * (`steps[stepIndex]`) and the dashboard (log fetching gated on `stepIndex >= 0`) treat a
52278
- * negative index as absent. So the ordering is expressed by TYPE, here, once.
52388
+ * negative index as absent. So the ordering is expressed by TYPE, as the engine's
52389
+ * `StepPhase`, here, once.
52279
52390
  */
52280
52391
  /**
52281
- * Sort key placing setup pseudo-steps (run before the job's real steps) first. Apply BEFORE
52282
- * the `step_index` sort:
52392
+ * Sort key placing steps by `StepPhase`: setup pseudo-steps, then init commands, then the
52393
+ * job's steps. Apply BEFORE the `step_index` sort:
52283
52394
  *
52284
52395
  * ```ts
52285
52396
  * qb.orderBy(setupStepsFirst()).orderBy('step_index', 'asc')
52286
52397
  * ```
52287
52398
  *
52288
52399
  * Untyped steps and every real step share one key, so `step_index` alone orders each group.
52400
+ * The init pattern matches `initStepIndex`: the prefix followed by digits only.
52289
52401
  */
52290
52402
  function setupStepsFirst() {
52291
52403
  const setupTypes = SetupStepType.options;
52292
- return sql`case when step_type in (${sql.join(setupTypes.map((t) => sql.lit(t)))}) then 0 else 1 end`;
52404
+ const initPattern = `^${INIT_STEP_TYPE_PREFIX}[0-9]+$`;
52405
+ return sql`case when step_type in (${sql.join(setupTypes.map((t) => sql.lit(t)))}) then ${sql.lit(StepPhase.options.indexOf(StepPhase.enum.setup))} when step_type ~ ${sql.lit(initPattern)} then ${sql.lit(StepPhase.options.indexOf(StepPhase.enum.init))} else ${sql.lit(StepPhase.options.indexOf(StepPhase.enum.steps))} end`;
52406
+ }
52407
+ /** In-memory equivalent over DB rows, kept beside the SQL so the two agree on the phases. */
52408
+ function compareStepsForDisplay(a, b) {
52409
+ return compareStepsByPhase({
52410
+ stepType: a.step_type,
52411
+ stepIndex: a.step_index
52412
+ }, {
52413
+ stepType: b.step_type,
52414
+ stepIndex: b.step_index
52415
+ });
52416
+ }
52417
+ /** {@link compareStepsForDisplay} over untyped rows, such as cold-store rows read back from S3. */
52418
+ function compareStepRowsForDisplay(a, b) {
52419
+ const typed = (r) => ({
52420
+ step_type: typeof r.step_type === "string" ? r.step_type : null,
52421
+ step_index: Number(r.step_index ?? 0)
52422
+ });
52423
+ return compareStepsForDisplay(typed(a), typed(b));
52293
52424
  }
52294
52425
  var init_step_display_order = __esmMin((() => {}));
52295
52426
  //#endregion
@@ -52792,6 +52923,19 @@ function parseCountFlag(raw) {
52792
52923
  function spansWorkflow(workflowName) {
52793
52924
  return (eb) => eb.or([eb("execution_runs.workflow_name", "=", workflowName), eb.exists(eb.selectFrom("execution_jobs").select("execution_jobs.job_id").whereRef("execution_jobs.run_id", "=", "execution_runs.run_id").where("execution_jobs.workflow_name", "=", workflowName))]);
52794
52925
  }
52926
+ /**
52927
+ * `?runIdPrefix=`: the run ids that start with the given characters, as a UUID range
52928
+ * the primary-key index answers. A full run id is its own one-id range.
52929
+ */
52930
+ function parseRunIdPrefixQuery(raw) {
52931
+ if (raw === void 0 || raw === "") return void 0;
52932
+ const ref = parseRunIdRef(raw);
52933
+ if (ref.kind === "invalid") return { error: `Invalid "runIdPrefix" '${raw}': ${ref.reason}` };
52934
+ return ref.kind === "uuid" ? {
52935
+ low: ref.runId,
52936
+ high: ref.runId
52937
+ } : runIdPrefixRange(ref.hex);
52938
+ }
52795
52939
  /** Run routes, mounted at /api/v1/admin/runs. */
52796
52940
  function createAdminRunRoutes(deps) {
52797
52941
  const app = createAdminApp(logger$85);
@@ -52801,12 +52945,16 @@ function createAdminRunRoutes(deps) {
52801
52945
  });
52802
52946
  app.use("/api/v1/admin/runs", authMiddleware);
52803
52947
  app.use("/api/v1/admin/runs/*", authMiddleware);
52948
+ app.use("/api/v1/admin/runs/:runId", requireFullRunId);
52949
+ app.use("/api/v1/admin/runs/:runId/*", requireFullRunId);
52804
52950
  app.get("/api/v1/admin/runs", async (c) => {
52805
52951
  deps.rbac.requirePermission(c.get("role"), "run.read");
52806
52952
  const statuses = parseStatus(c.req.query("status"));
52807
52953
  const workflowName = c.req.query("workflowName");
52808
52954
  const repo = c.req.query("repo");
52809
52955
  const deliveryId = c.req.query("deliveryId");
52956
+ const runIdPrefix = parseRunIdPrefixQuery(c.req.query("runIdPrefix"));
52957
+ if (runIdPrefix && "error" in runIdPrefix) return c.json({ error: runIdPrefix.error }, 400);
52810
52958
  const sinceParsed = parseSince(c.req.query("since"));
52811
52959
  if (!sinceParsed.ok) return c.json({ error: "Invalid \"since\" query parameter: expected ISO-8601 timestamp" }, 400);
52812
52960
  const since = sinceParsed.value;
@@ -52823,6 +52971,7 @@ function createAdminRunRoutes(deps) {
52823
52971
  if (workflowName) countQuery = countQuery.where(spansWorkflow(workflowName));
52824
52972
  if (repo) countQuery = countQuery.where("repo_identifier", "=", repo);
52825
52973
  if (deliveryId) countQuery = countQuery.where("delivery_id", "=", deliveryId);
52974
+ if (runIdPrefix) countQuery = countQuery.where("run_id", ">=", runIdPrefix.low).where("run_id", "<=", runIdPrefix.high);
52826
52975
  if (since) countQuery = countQuery.where("created_at", ">", since);
52827
52976
  if (tokenRoutingKey) countQuery = countQuery.where("routing_key", "=", tokenRoutingKey);
52828
52977
  if (countOnly) {
@@ -52858,6 +53007,7 @@ function createAdminRunRoutes(deps) {
52858
53007
  if (workflowName) query = query.where(spansWorkflow(workflowName));
52859
53008
  if (repo) query = query.where("repo_identifier", "=", repo);
52860
53009
  if (deliveryId) query = query.where("delivery_id", "=", deliveryId);
53010
+ if (runIdPrefix) query = query.where("run_id", ">=", runIdPrefix.low).where("run_id", "<=", runIdPrefix.high);
52861
53011
  if (since) query = query.where("created_at", ">", since);
52862
53012
  if (tokenRoutingKey) query = query.where("routing_key", "=", tokenRoutingKey);
52863
53013
  const [runs, countResult] = await Promise.all([query.orderBy("created_at", "desc").limit(limit).offset(offset).execute(), countQuery.executeTakeFirstOrThrow()]);
@@ -52889,7 +53039,7 @@ function createAdminRunRoutes(deps) {
52889
53039
  });
52890
53040
  app.get("/api/v1/admin/runs/:runId", async (c) => {
52891
53041
  deps.rbac.requirePermission(c.get("role"), "run.read");
52892
- const runId = c.req.param("runId");
53042
+ const runId = c.req.param("runId").toLowerCase();
52893
53043
  const run = await deps.db.selectFrom("execution_runs").select([
52894
53044
  "run_id",
52895
53045
  "workflow_name",
@@ -52953,7 +53103,7 @@ function createAdminRunRoutes(deps) {
52953
53103
  });
52954
53104
  app.get("/api/v1/admin/runs/:runId/jobs", async (c) => {
52955
53105
  deps.rbac.requirePermission(c.get("role"), "run.read");
52956
- const runId = c.req.param("runId");
53106
+ const runId = c.req.param("runId").toLowerCase();
52957
53107
  const includeSteps = parseCountFlag(c.req.query("includeSteps"));
52958
53108
  const runExists = await deps.db.selectFrom("execution_runs").select([
52959
53109
  "run_id",
@@ -53005,6 +53155,7 @@ function createAdminRunRoutes(deps) {
53005
53155
  jobSteps = [];
53006
53156
  stepsByJob.set(step.job_id, jobSteps);
53007
53157
  }
53158
+ const initIndex = initStepIndex(step.step_type);
53008
53159
  jobSteps.push({
53009
53160
  stepIndex: step.step_index,
53010
53161
  stepName: step.step_name,
@@ -53014,7 +53165,9 @@ function createAdminRunRoutes(deps) {
53014
53165
  durationMs: step.duration_ms,
53015
53166
  exitCode: step.exit_code,
53016
53167
  errorMessage: step.error_message,
53017
- stepType: step.step_type
53168
+ stepType: step.step_type,
53169
+ phase: stepPhase(step.step_type),
53170
+ ...initIndex !== void 0 && { initIndex }
53018
53171
  });
53019
53172
  }
53020
53173
  }
@@ -53041,7 +53194,7 @@ function createAdminRunRoutes(deps) {
53041
53194
  });
53042
53195
  app.get("/api/v1/admin/runs/:runId/structured", async (c) => {
53043
53196
  deps.rbac.requirePermission(c.get("role"), "run.read");
53044
- const runId = c.req.param("runId");
53197
+ const runId = c.req.param("runId").toLowerCase();
53045
53198
  const detail = await aggregateRunDetail(deps.db, runId);
53046
53199
  if (!detail) return c.json({ error: `Run ${runId} not found` }, 404);
53047
53200
  const denied = enforceRoutingKeyScope(c, detail.routingKey);
@@ -53050,7 +53203,7 @@ function createAdminRunRoutes(deps) {
53050
53203
  });
53051
53204
  app.get("/api/v1/admin/runs/:runId/jobs/:jobId/steps/:stepIndex/logs", async (c) => {
53052
53205
  deps.rbac.requirePermission(c.get("role"), "run.read");
53053
- const runId = c.req.param("runId");
53206
+ const runId = c.req.param("runId").toLowerCase();
53054
53207
  const jobId = c.req.param("jobId");
53055
53208
  const stepIndex = parseInt(c.req.param("stepIndex"), 10);
53056
53209
  if (Number.isNaN(stepIndex)) return c.json({ error: "Invalid stepIndex" }, 400);
@@ -53076,7 +53229,7 @@ function createAdminRunRoutes(deps) {
53076
53229
  });
53077
53230
  app.get("/api/v1/admin/runs/:runId/ephemeral-key", async (c) => {
53078
53231
  deps.rbac.requirePermission(c.get("role"), "run.read");
53079
- const runId = c.req.param("runId");
53232
+ const runId = c.req.param("runId").toLowerCase();
53080
53233
  const runExists = await deps.db.selectFrom("execution_runs").select(["run_id", "routing_key"]).where("run_id", "=", runId).executeTakeFirst();
53081
53234
  if (!runExists) return c.json({ error: `Run ${runId} not found` }, 404);
53082
53235
  const denied = enforceRoutingKeyScope(c, runExists.routing_key);
@@ -53094,7 +53247,7 @@ function createAdminRunRoutes(deps) {
53094
53247
  app.get("/api/v1/admin/runs/:runId/secret-outputs", async (c) => {
53095
53248
  const role = c.get("role");
53096
53249
  deps.rbac.requirePermission(role, "run.read");
53097
- const runId = c.req.param("runId");
53250
+ const runId = c.req.param("runId").toLowerCase();
53098
53251
  const outputKeyFilter = c.req.query("outputKey");
53099
53252
  const reveal = parseCountFlag(c.req.query("reveal"));
53100
53253
  if (reveal) {
@@ -53165,7 +53318,7 @@ function createAdminRunRoutes(deps) {
53165
53318
  });
53166
53319
  return app;
53167
53320
  }
53168
- var logger$85, RUN_STATUSES;
53321
+ var logger$85, RUN_STATUSES, requireFullRunId;
53169
53322
  var init_admin_runs = __esmMin((() => {
53170
53323
  init_step_display_order();
53171
53324
  init_secret_output_crypto();
@@ -53179,6 +53332,11 @@ var init_admin_runs = __esmMin((() => {
53179
53332
  init_admin_env();
53180
53333
  logger$85 = createLogger({ prefix: "admin-runs" });
53181
53334
  RUN_STATUSES = new Set(CANONICAL_STATUSES);
53335
+ requireFullRunId = async (c, next) => {
53336
+ const runId = c.req.param("runId") ?? "";
53337
+ if (!isRunId(runId)) return c.json({ error: `Invalid run id '${runId}': a run id is a full UUID. List runs (or filter with ?runIdPrefix=) to find it.` }, 400);
53338
+ return next();
53339
+ };
53182
53340
  }));
53183
53341
  //#endregion
53184
53342
  //#region src/cold-store/load-event-log-range.ts
@@ -54276,14 +54434,14 @@ function createHealthRoutes$1(deps = {}) {
54276
54434
  return createHealthRoutes({
54277
54435
  livenessInfo: () => {
54278
54436
  return {
54279
- version: "0.20.1",
54280
- buildDate: "2026-10-17T14:01:06.224Z",
54281
- sdkVersion: "0.20.1",
54437
+ version: "0.21.0",
54438
+ buildDate: "2026-10-18T02:27:04.078Z",
54439
+ sdkVersion: "0.21.0",
54282
54440
  sdkBundleHash: "b012bd8bace3df9e574252bf290c652bc39766da617acf51729a08586756266d",
54283
- sharedVersion: "0.20.1",
54441
+ sharedVersion: "0.21.0",
54284
54442
  sharedBundleHash: "23378ace59973f47203be3ff171bb7d5e66702eaa335927b390f94b9771a1f8b",
54285
- engineVersion: "0.20.1",
54286
- engineBundleHash: "406208ee50766cd0d4ce3ab1fd5dec4501d5117a74acd294b25736d9bd6e89bd"
54443
+ engineVersion: "0.21.0",
54444
+ engineBundleHash: "d9dfe7c806a7fdbd64da7ef48455a139c1d8db8510964ab79e233efdc3f171ba"
54287
54445
  };
54288
54446
  },
54289
54447
  readinessCheck: deps.db ? async () => {
@@ -54315,7 +54473,7 @@ function createCapabilitiesRoutes() {
54315
54473
  const app = new Hono();
54316
54474
  app.get("/api/v1/capabilities", (c) => {
54317
54475
  const manifest = {
54318
- orchestratorVersion: "0.20.1",
54476
+ orchestratorVersion: "0.21.0",
54319
54477
  protocolVersion: PROTOCOL_VERSION,
54320
54478
  minProtocolVersion: MIN_PROTOCOL_VERSION
54321
54479
  };
@@ -59081,6 +59239,7 @@ function createApp(deps) {
59081
59239
  jobName: deps.executionTracker.getJobName(msg.runId, msg.jobId) ?? msg.jobId,
59082
59240
  stepIndex: msg.stepIndex,
59083
59241
  stepName: msg.stepName,
59242
+ stepType: stepTypeOf(msg.data),
59084
59243
  state: msg.state,
59085
59244
  durationMs: msg.data?.durationMs ?? void 0,
59086
59245
  installationId: execContext.installationId,
@@ -59303,7 +59462,7 @@ function createApp(deps) {
59303
59462
  app.use("*", (c, next) => c.req.path.startsWith("/api/v1/cache/blob/") ? next() : webhookBodyLimit(c, next));
59304
59463
  if (deps.fsCache) registerBlobRoutes(app, deps.fsCache);
59305
59464
  app.post("/api/v1/admin/runs/:runId/cancel", async (c) => {
59306
- const runId = c.req.param("runId");
59465
+ const runId = c.req.param("runId").toLowerCase();
59307
59466
  if (!runId) return c.json({ error: "Missing runId" }, 400);
59308
59467
  if (!deps.adminDeps) return c.json({ error: "Admin API not available" }, 503);
59309
59468
  const auth = await resolveBearerAuth(c, {
@@ -59338,6 +59497,11 @@ function createApp(deps) {
59338
59497
  recordAccess("denied", `role "${tokenInfo.role}" lacks run.cancel`);
59339
59498
  return c.json({ error: "Permission denied: run.cancel required" }, 403);
59340
59499
  }
59500
+ if (!isRunId(runId)) {
59501
+ const error = `Invalid run id '${runId}': a run id is a full UUID.`;
59502
+ recordAccess("allowed", error);
59503
+ return c.json({ error }, 400);
59504
+ }
59341
59505
  try {
59342
59506
  let force = false;
59343
59507
  try {
@@ -59811,7 +59975,7 @@ var init_app = __esmMin((() => {
59811
59975
  init_agent_metrics_aggregator();
59812
59976
  init_run_detached();
59813
59977
  logger$66 = createLogger({ prefix: "app" });
59814
- ORCHESTRATOR_VERSION$2 = "0.20.1";
59978
+ ORCHESTRATOR_VERSION$2 = "0.21.0";
59815
59979
  SourceLocationStore = class {
59816
59980
  cache = /* @__PURE__ */ new Map();
59817
59981
  key(workflowName, jobName) {
@@ -68902,7 +69066,7 @@ var init_peer_client = __esmMin((() => {
68902
69066
  PeerDialOrigin["Discovered"] = "discovered";
68903
69067
  return PeerDialOrigin;
68904
69068
  }({});
68905
- SOFTWARE_VERSION$1 = "0.20.1";
69069
+ SOFTWARE_VERSION$1 = "0.21.0";
68906
69070
  PeerClient$1 = class {
68907
69071
  ws = null;
68908
69072
  _state = "disconnected";
@@ -71373,7 +71537,7 @@ var init_peer_handler = __esmMin((() => {
71373
71537
  init_join_token();
71374
71538
  init_run_detached();
71375
71539
  logger$46 = createLogger({ prefix: "peer-handler" });
71376
- SOFTWARE_VERSION = "0.20.1";
71540
+ SOFTWARE_VERSION = "0.21.0";
71377
71541
  ADVERTISED_PEER_AUTH_SCHEMES = [PeerAuthScheme.enum["mutual-v2"]];
71378
71542
  MAX_TOKEN_CANDIDATES = 8;
71379
71543
  RATE_LIMIT_MAX = 5;
@@ -87804,7 +87968,8 @@ async function recordInitFailureFromSkip(args) {
87804
87968
  },
87805
87969
  triggerEvent: dispatchTriggerEvent(ctx),
87806
87970
  commitMessage: extractCommitMessage(setup.info.event, setup.info.payload),
87807
- ...preDispatchRunProvenance(ctx)
87971
+ ...preDispatchRunProvenance(ctx),
87972
+ ...ctx.testRun && { testRun: ctx.testRun }
87808
87973
  });
87809
87974
  await completeChecksForUndispatchedRun({
87810
87975
  ctx,
@@ -87855,7 +88020,8 @@ async function holdWorkflowForInstallGate(args) {
87855
88020
  commitMessage: extractCommitMessage(setup.info.event, setup.info.payload),
87856
88021
  workflowSha: ctx.global?.workflowSha ?? null,
87857
88022
  workflowBranch: ctx.global?.workflowBranch ?? null,
87858
- ...preDispatchRunProvenance(ctx)
88023
+ ...preDispatchRunProvenance(ctx),
88024
+ ...ctx.testRun && { testRun: ctx.testRun }
87859
88025
  });
87860
88026
  if (deps.heldRunStore) await deps.heldRunStore.createHold(ctx.resolvedOrgId, {
87861
88027
  runId,
@@ -87931,7 +88097,8 @@ async function holdRunForSecurityPolicy(args) {
87931
88097
  prNumber: event.prNumber ?? null,
87932
88098
  workflowSha: ctx.global?.workflowSha ?? null,
87933
88099
  workflowBranch: ctx.global?.workflowBranch ?? null,
87934
- ...preDispatchRunProvenance(ctx)
88100
+ ...preDispatchRunProvenance(ctx),
88101
+ ...ctx.testRun && { testRun: ctx.testRun }
87935
88102
  });
87936
88103
  let heldRow;
87937
88104
  if (deps.heldRunStore) {
@@ -95022,7 +95189,7 @@ var init_handler = __esmMin((() => {
95022
95189
  outputs: typeof r.outputs === "string" ? r.outputs : null,
95023
95190
  init_failure: r.init_failure ?? null
95024
95191
  }));
95025
- stepRows.sort((a, b) => Number(a.step_index ?? 0) - Number(b.step_index ?? 0));
95192
+ stepRows.sort(compareStepRowsForDisplay);
95026
95193
  out.steps = stepRows.map((r) => ({
95027
95194
  job_id: String(r.job_id ?? ""),
95028
95195
  step_index: Number(r.step_index ?? 0),
@@ -100577,13 +100744,13 @@ var init_worker_core = __esmMin((() => {
100577
100744
  init_app_on_error();
100578
100745
  init_run_detached();
100579
100746
  init_peer_job_cancel();
100580
- ORCHESTRATOR_VERSION$1 = "0.20.1";
100581
- WORKER_SDK_VERSION = "0.20.1";
100747
+ ORCHESTRATOR_VERSION$1 = "0.21.0";
100748
+ WORKER_SDK_VERSION = "0.21.0";
100582
100749
  WORKER_SDK_BUNDLE_HASH = "b012bd8bace3df9e574252bf290c652bc39766da617acf51729a08586756266d";
100583
- WORKER_SHARED_VERSION = "0.20.1";
100750
+ WORKER_SHARED_VERSION = "0.21.0";
100584
100751
  WORKER_SHARED_BUNDLE_HASH = "23378ace59973f47203be3ff171bb7d5e66702eaa335927b390f94b9771a1f8b";
100585
- WORKER_ENGINE_VERSION = "0.20.1";
100586
- WORKER_ENGINE_BUNDLE_HASH = "406208ee50766cd0d4ce3ab1fd5dec4501d5117a74acd294b25736d9bd6e89bd";
100752
+ WORKER_ENGINE_VERSION = "0.21.0";
100753
+ WORKER_ENGINE_BUNDLE_HASH = "d9dfe7c806a7fdbd64da7ef48455a139c1d8db8510964ab79e233efdc3f171ba";
100587
100754
  logger$2 = createLogger({ prefix: "worker" });
100588
100755
  DRAIN_TIMEOUT_MS = 3e5;
100589
100756
  }));
@@ -100610,13 +100777,13 @@ init_run_detached();
100610
100777
  init_dashboard_access();
100611
100778
  init_peer_job_cancel();
100612
100779
  init_orphan_requests();
100613
- const ORCHESTRATOR_VERSION = "0.20.1";
100614
- const SDK_VERSION = "0.20.1";
100780
+ const ORCHESTRATOR_VERSION = "0.21.0";
100781
+ const SDK_VERSION = "0.21.0";
100615
100782
  const SDK_BUNDLE_HASH = "b012bd8bace3df9e574252bf290c652bc39766da617acf51729a08586756266d";
100616
- const SHARED_VERSION = "0.20.1";
100783
+ const SHARED_VERSION = "0.21.0";
100617
100784
  const SHARED_BUNDLE_HASH = "23378ace59973f47203be3ff171bb7d5e66702eaa335927b390f94b9771a1f8b";
100618
- const ENGINE_VERSION = "0.20.1";
100619
- const ENGINE_BUNDLE_HASH = "406208ee50766cd0d4ce3ab1fd5dec4501d5117a74acd294b25736d9bd6e89bd";
100785
+ const ENGINE_VERSION = "0.21.0";
100786
+ const ENGINE_BUNDLE_HASH = "d9dfe7c806a7fdbd64da7ef48455a139c1d8db8510964ab79e233efdc3f171ba";
100620
100787
  const otelSdk = initTelemetry({
100621
100788
  serviceName: "kici-orchestrator",
100622
100789
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT