// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-07-12-p0-admission-failure-identity. // Responsibility: Normalize typed AgentRun dispatch failures before durable admission. // rollout 仅接受上游类型化 failureKind 作为权威;HTTP 状态、耗时和展示文案都不能推断 rollout。 const FAILURE_REASON_RULES = Object.freeze([ rule("rollout-in-progress", ["rollout-in-progress", "rollout-progressing", "deployment-in-progress", "runtime-starting", "service-starting"]), rule("manager-service-unavailable", ["agentrun-connect-failed", "agentrun-manager-fetch-failed", "agentrun-request-failed", "agentrun-timeout", "agentrun-unreachable", "manager-unavailable", "service-unavailable", "upstream-unavailable", "http-502", "http-503", "http-504", "fetch-failed", "network-error", "timeout"]), rule("admission-policy-denied", ["admission-denied", "policy-denied", "forbidden", "permission-denied", "rbac-denied", "unauthorized"]), rule("runner-capacity-exhausted", ["runner-capacity", "runner-capacity-exhausted", "capacity-exhausted", "no-runner-capacity", "runner-unavailable", "runner-quota-exceeded"]), rule("provider-profile-unconfigured", ["provider-profile-unconfigured", "provider-profile-missing", "provider-not-configured", "profile-not-configured", "provider-unconfigured"]), rule("secret-unavailable", ["secret-unavailable", "secret-missing", "secret-not-found", "credential-unavailable", "credential-missing"]), rule("image-unavailable", ["image-unavailable", "image-not-found", "image-pull-failed", "image-pull-backoff", "invalid-image"]), rule("workspace-materialization-failed", ["workspace-materialization-failed", "resource-bundle-materialization-failed", "workspace-unavailable", "gitbundle-unavailable", "workspace-checkout-failed"]), rule("lease-idempotency-conflict", ["lease-conflict", "lease-lost", "idempotency-conflict", "command-conflict", "dispatch-intent-conflict", "already-claimed"]), rule("dispatcher-runtime-misconfigured", ["schema-invalid", "config-invalid", "configuration-invalid", "dispatcher-misconfigured", "runtime-misconfigured", "agentrun-internal-url-required", "invalid-dispatch-contract"]) ]); const FAILURE_PRESENTATIONS = Object.freeze({ "rollout-in-progress": { userMessage: "AgentRun 正在滚动更新,本次请求尚未接纳;可稍后重试。", recoveryAction: "wait-for-rollout" }, "manager-service-unavailable": { userMessage: "AgentRun 管理服务暂时不可用,本次请求尚未接纳;请等待服务恢复后重试。", recoveryAction: "retry-when-agentrun-service-ready" }, "admission-policy-denied": { userMessage: "AgentRun 接纳策略拒绝了本次请求;请检查身份、租户与执行策略。", recoveryAction: "review-admission-policy" }, "runner-capacity-exhausted": { userMessage: "AgentRun 当前没有可用 Runner 容量,本次请求尚未接纳;请释放或扩容后重试。", recoveryAction: "release-or-add-runner-capacity" }, "provider-profile-unconfigured": { userMessage: "AgentRun Provider/Profile 未配置,本次请求无法接纳;请先完成对应 Profile 配置。", recoveryAction: "configure-provider-profile" }, "secret-unavailable": { userMessage: "AgentRun 所需 Secret 不可用,本次请求无法接纳;请恢复 YAML 引用的 Secret。", recoveryAction: "restore-referenced-secret" }, "image-unavailable": { userMessage: "AgentRun Runner 镜像不可用,本次请求无法接纳;请发布或修正目标镜像。", recoveryAction: "publish-or-correct-runner-image" }, "workspace-materialization-failed": { userMessage: "AgentRun Workspace/Resource Bundle 物化失败,本次请求无法继续;请检查仓库与资源包引用。", recoveryAction: "repair-workspace-materialization" }, "lease-idempotency-conflict": { userMessage: "AgentRun Lease/Idempotency 发生冲突;请核对已有 Run/Command 后再重试。", recoveryAction: "inspect-existing-lease-or-idempotent-command" }, "dispatcher-runtime-misconfigured": { userMessage: "AgentRun 调度契约或运行面配置不匹配,本次请求未进入实时事件流;请展开诊断修复配置。", recoveryAction: "repair-agentrun-dispatch-contract" }, "unknown-dispatch-failure": { userMessage: "AgentRun 返回了未识别的调度失败,本次请求尚未接纳;请展开诊断定位。", recoveryAction: "inspect-agentrun-diagnostic" } }); export function classifyCodeAgentDispatchFailure(error = {}) { const upstream = record(error?.agentRunError); const nested = record(upstream?.error); const failureKind = normalizedFailureKind(firstText(error?.failureKind, upstream?.failureKind, nested?.failureKind, error?.code, upstream?.code, nested?.code, "agentrun-dispatch-error")); const statusCode = firstInteger(error?.statusCode, error?.status, upstream?.statusCode, upstream?.status, nested?.statusCode); const reason = dispatchFailureReason(failureKind, statusCode); const presentation = FAILURE_PRESENTATIONS[reason] ?? FAILURE_PRESENTATIONS["unknown-dispatch-failure"]; const retryable = firstBoolean(error?.retryable, upstream?.retryable, nested?.retryable, defaultRetryable(reason, statusCode)); return { failureKind, reason, retryable, userMessage: presentation.userMessage, recoveryAction: presentation.recoveryAction, message: firstText(error?.message, upstream?.message, nested?.message, "AgentRun dispatch failed before durable admission."), statusCode, dispatchStage: firstText(error?.dispatchStage, error?.stage, upstream?.dispatchStage, upstream?.stage), upstreamTraceId: firstText(upstream?.traceId, nested?.traceId), runId: firstText(error?.runId, upstream?.runId, upstream?.run?.id, nested?.runId), commandId: firstText(error?.commandId, upstream?.commandId, upstream?.command?.id, nested?.commandId), dispatchIntentId: firstText(error?.dispatchIntentId, upstream?.dispatchIntentId, upstream?.dispatchIntent?.id, nested?.dispatchIntentId), valuesPrinted: false }; } export function dispatchFailureReason(failureKind, statusCode = null) { const normalized = normalizedFailureKind(failureKind); const matched = FAILURE_REASON_RULES.find((candidate) => candidate.kinds.has(normalized)); if (matched) return matched.reason; if (statusCode === 401 || statusCode === 403) return "admission-policy-denied"; if (statusCode === 409) return "lease-idempotency-conflict"; if ([502, 503, 504].includes(statusCode)) return "manager-service-unavailable"; return "unknown-dispatch-failure"; } function rule(reason, kinds) { return Object.freeze({ reason, kinds: new Set(kinds) }); } function defaultRetryable(reason, statusCode) { if (["rollout-in-progress", "manager-service-unavailable", "runner-capacity-exhausted", "lease-idempotency-conflict"].includes(reason)) return true; if (["admission-policy-denied", "provider-profile-unconfigured", "secret-unavailable", "image-unavailable", "workspace-materialization-failed", "dispatcher-runtime-misconfigured"].includes(reason)) return false; return Number.isInteger(statusCode) && statusCode >= 500; } function normalizedFailureKind(value) { const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-").replace(/[^a-z0-9.-]+/gu, "-").replace(/^-+|-+$/gu, ""); return text || "agentrun-dispatch-error"; } function record(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function firstText(...values) { for (const value of values) { if (typeof value === "string" && value.trim()) return value.trim(); } return null; } function firstInteger(...values) { for (const value of values) { const number = Number(value); if (Number.isInteger(number) && number > 0) return number; } return null; } function firstBoolean(...values) { for (const value of values) if (typeof value === "boolean") return value; return false; }