import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { DEV_CODE_AGENT_PROVIDER_CONTRACT, codeAgentSecretRefPlaceholder, inspectCodeAgentProviderEnv } from "./code-agent-contract.ts"; import { CODEX_STDIO_BACKEND, CODEX_STDIO_CAPABILITY_LEVEL, CODEX_STDIO_IMPLEMENTATION_TYPE, CODEX_STDIO_PROVIDER, CODEX_STDIO_RUNNER_KIND, CODEX_STDIO_SESSION_MODE, createCodexStdioSessionManager, longLivedSessionGate as codexLongLivedSessionGate } from "./codex-stdio-session.ts"; import { createCodeAgentTraceRecorder, defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts"; import { DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS, createCodeAgentSessionRegistry } from "./code-agent-session-registry.ts"; import { codeAgentSessionLifecycleSummary, decorateCodeAgentSession } from "./code-agent-session-lifecycle.ts"; const DEFAULT_MODEL = DEV_CODE_AGENT_PROVIDER_CONTRACT.model; const DEFAULT_PROJECT_ID = "prj_hwlab-cloud-workbench"; const READONLY_RUNNER_PROVIDER = "codex-readonly-runner"; const READONLY_RUNNER_BACKEND = "hwlab-cloud-api/codex-readonly-runner"; const READONLY_RUNNER_MODEL = "read-only-tools"; const READONLY_RUNNER_KIND = "hwlab-readonly-runner"; const READONLY_RUNNER_SANDBOX = "read-only"; const READONLY_SESSION_MODE = "controlled-readonly-session-registry"; const READONLY_IMPLEMENTATION_TYPE = "controlled-readonly-session-registry"; const READONLY_SESSION_CAPABILITY_LEVEL = "read-only-session-tools"; const HARDWARE_INVOKE_SHELL_METHOD = "hardware.invoke.shell"; const LEGACY_CODE_AGENT_HARDWARE_PROVIDER = "hwlab-hardware-capability"; const LEGACY_CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED = "blocked"; const OPENAI_FALLBACK_RUNNER_KIND = "openai-responses-fallback"; const READONLY_TOOL_OUTPUT_LIMIT = 4000; const MAX_READONLY_SESSIONS = 200; const CODE_AGENT_PROVIDER_SECRET_REF = codeAgentSecretRefPlaceholder().replace("secretRef:", ""); const CODEX_STDIO_SOURCE_ISSUE = "pikasTech/HWLAB#275"; const READONLY_LIMITATION_FLAGS = Object.freeze([ "not-codex-stdio", "not-write-capable", "process-local-session-registry" ]); const CODEX_STDIO_LIMITATION_FLAGS = Object.freeze([ "secret-values-redacted" ]); const CODE_AGENT_SYSTEM_PROMPT = [ "你是 HWLAB 云工作台的 Code Agent。", "请用中文直接回答用户的工作台问题。", "当前最小版本可以帮助用户整理任务、查看云工作台资源和说明下一步;不要声称已经执行硬件变更。", "不要声称已通过 M3、M4 或 M5 验收。", "不要声称 OpenAI Responses 文本 fallback 具备 Codex runner、workspace、tools 或 skills 能力。", "不要输出 secret、token、kubeconfig 或环境变量原文。" ].join("\n"); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const defaultCodeAgentSessionRegistry = createCodeAgentSessionRegistry({ idleTimeoutMs: DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS, maxSessions: MAX_READONLY_SESSIONS }); const defaultCodexStdioSessionManager = createCodexStdioSessionManager(); export async function handleCodeAgentChat(params = {}, options = {}) { const timestamp = nowIso(options.now); const { conversationId, sessionId, requestedSessionId } = resolveConversationSessionIds(params); const messageId = `msg_${randomUUID()}`; const traceId = cleanProtocolId(params.traceId, "trc") || `trc_${randomUUID()}`; const providerPlan = resolveProviderPlan(options.env ?? process.env, options); const sessionRegistry = resolveCodeAgentSessionRegistry(options); const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const traceRecorder = createCodeAgentTraceRecorder({ traceStore, traceId, now: options.now, runnerKind: CODEX_STDIO_RUNNER_KIND, workspace: options.workspace, sandbox: "workspace-write", sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE }); const base = { conversationId, sessionId, messageId, status: "running", createdAt: timestamp, updatedAt: timestamp, traceId, provider: providerPlan.provider, model: providerPlan.model, backend: providerPlan.backend, projectId: cleanProjectId(params.projectId) || DEFAULT_PROJECT_ID }; try { const message = normalizeUserMessage(params.message); const runnerIntent = { kind: "none", toolName: "codex-stdio.session" }; traceRecorder.append({ type: "request", status: "accepted", label: "request:accepted", promptSummary: message.length > 160 ? `${message.slice(0, 157)}...` : message, waitingFor: "codex-stdio-readiness" }); const codexStdioAvailability = await inspectCodexStdioFeasibility(options.env ?? process.env, options); if (codexStdioLongLivedReady(codexStdioAvailability)) { const stdioResult = await callCodexStdioRunner({ message, conversationId, sessionId: requestedSessionId, traceId, env: options.env ?? process.env, now: options.now, workspace: options.workspace, timeoutMs: options.timeoutMs, hardTimeoutMs: options.hardTimeoutMs, model: providerPlan.model, codexStdioManager: options.codexStdioManager, traceRecorder, conversationFacts: conversationFactsForPrompt(sessionRegistry, conversationId), externalNetworkIntent: null }); return completedRunnerPayload({ base, runnerResult: stdioResult, messageId, now: options.now, sessionRegistry }); } const primaryBlocker = codexStdioAvailability.blockers?.[0] ?? null; traceRecorder.append({ type: "error", status: "blocked", label: "codex-stdio:blocked", errorCode: primaryBlocker?.code ?? "codex_stdio_blocked", message: primaryBlocker?.summary ?? "Codex stdio is not available; no local fallback will be used.", waitingFor: "codex-stdio-readiness", terminal: true }); throw runnerError(primaryBlocker?.code ?? "codex_stdio_blocked", "Codex stdio long-lived session is unavailable; no controlled-readonly, skills, shell, or text fallback will answer this request.", { provider: CODEX_STDIO_PROVIDER, model: providerPlan.model, backend: CODEX_STDIO_BACKEND, workspace: codexStdioAvailability.workspace ?? options.workspace ?? repoRoot, sandbox: codexStdioAvailability.sandbox ?? "workspace-write", session: null, toolCalls: [], skills: notRequestedSkills(), runner: codexStdioBlockedRunnerDescriptor({ workspace: codexStdioAvailability.workspace ?? options.workspace, session: null }), runnerTrace: traceRecorder.runnerTrace({ runnerKind: CODEX_STDIO_RUNNER_KIND, workspace: codexStdioAvailability.workspace ?? options.workspace ?? repoRoot, sandbox: codexStdioAvailability.sandbox ?? "workspace-write", sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, limitations: ["no-fallback", "codex-stdio-required"], outputTruncated: false }), capabilityLevel: "blocked", sessionMode: CODEX_STDIO_SESSION_MODE, sessionReuse: null, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"], codexStdioFeasibility: codexStdioAvailability, longLivedSessionGate: longLivedSessionGate({ provider: CODEX_STDIO_PROVIDER, runnerKind: CODEX_STDIO_RUNNER_KIND, session: null, sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdioFeasibility: codexStdioAvailability }), blockers: codexStdioAvailability.blockers?.length ? codexStdioAvailability.blockers : [{ code: "codex_stdio_blocked", sourceIssue: "pikasTech/HWLAB#275", summary: "Codex stdio readiness did not pass." }], route: "/v1/agent/chat", toolName: runnerIntent.toolName ?? "codex-stdio.session", availability: await describeCodeAgentAvailability(options.env ?? process.env, options) }); } catch (error) { const failedAt = nowIso(options.now); const payload = { ...base, status: chatStatusForError(error), updatedAt: failedAt, error: normalizeChatError(error, { traceId, provider: error.provider ?? base.provider, model: error.model ?? base.model, backend: error.backend ?? base.backend, runner: error.runner, capabilityLevel: error.capabilityLevel ?? "blocked", route: error.route, toolName: error.toolName }) }; if (payload.error?.blocker) payload.blocker = payload.error.blocker; if (payload.error?.blockers) payload.blockers = payload.error.blockers; if (error.provider) payload.provider = error.provider; if (error.model) payload.model = error.model; if (error.backend) payload.backend = error.backend; if (error.workspace !== undefined) payload.workspace = error.workspace; if (error.sandbox !== undefined) payload.sandbox = error.sandbox; if (error.session !== undefined) payload.session = error.session; if (error.toolCalls !== undefined) payload.toolCalls = error.toolCalls; if (error.skills !== undefined) payload.skills = error.skills; if (error.runner !== undefined) payload.runner = error.runner; if (error.runnerTrace !== undefined) payload.runnerTrace = error.runnerTrace; if (error.providerTrace !== undefined) payload.providerTrace = error.providerTrace; if (payload.runnerTrace === undefined) { payload.runnerTrace = traceRecorder.runnerTrace({ runnerKind: CODEX_STDIO_RUNNER_KIND, workspace: error.workspace ?? options.workspace ?? repoRoot, sandbox: error.sandbox ?? "workspace-write", sessionMode: error.sessionMode ?? CODEX_STDIO_SESSION_MODE, implementationType: error.implementationType ?? CODEX_STDIO_IMPLEMENTATION_TYPE, limitations: error.runnerLimitations ?? ["codex-stdio-required"] }); } if (error.capabilityLevel !== undefined) payload.capabilityLevel = error.capabilityLevel; if (error.sessionMode !== undefined) payload.sessionMode = error.sessionMode; if (error.sessionReuse !== undefined) payload.sessionReuse = error.sessionReuse; if (error.implementationType !== undefined) payload.implementationType = error.implementationType; if (error.runnerLimitations !== undefined) payload.runnerLimitations = error.runnerLimitations; if (error.codexStdioFeasibility !== undefined) payload.codexStdioFeasibility = error.codexStdioFeasibility; if (error.longLivedSessionGate !== undefined) payload.longLivedSessionGate = error.longLivedSessionGate; if (error.conversationFacts !== undefined) { payload.conversationFacts = error.conversationFacts; } else if (typeof sessionRegistry.getConversationFacts === "function") { payload.conversationFacts = sessionRegistry.getConversationFacts(conversationId); } if (payload.conversationFacts && payload.error?.blocker) payload.error.blocker.conversationFacts = payload.conversationFacts; if (payload.conversationFacts && payload.blocker) payload.blocker.conversationFacts = payload.conversationFacts; if (error.availability !== undefined) { payload.availability = error.availability; } else if (["provider_unavailable", "provider_timeout", "codex_cli_binary_missing"].includes(error.code)) { payload.availability = await describeCodeAgentAvailability(options.env ?? process.env, options); } else if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "codex_stdio_blocked", "codex_stdio_failed", "codex_stdio_protocol_blocked", "codex_stdio_empty_response", "codex_stdio_command_probe_failed"].includes(error.code)) { payload.availability = await describeCodeAgentAvailability(options.env ?? process.env, options); } return decorateChatSessionLifecycle(payload); } } function completedRunnerPayload({ base, runnerResult, messageId, now, sessionRegistry }) { const completedAt = nowIso(now); const blocker = structuredCompletionBlocker(runnerResult, { traceId: base.traceId, provider: runnerResult.provider, backend: runnerResult.backend, runner: runnerResult.runner, capabilityLevel: runnerResult.capabilityLevel }); const payload = finalizeCodeAgentChatPayload(decorateChatSessionLifecycle({ ...base, sessionId: runnerResult.session?.sessionId ?? base.sessionId, status: "completed", updatedAt: completedAt, provider: runnerResult.provider, model: runnerResult.model, backend: runnerResult.backend, workspace: runnerResult.workspace, sandbox: runnerResult.sandbox, session: runnerResult.session, sessionMode: runnerResult.sessionMode, sessionReuse: runnerResult.sessionReuse, implementationType: runnerResult.implementationType, runnerLimitations: runnerResult.runnerLimitations, codexStdioFeasibility: runnerResult.codexStdioFeasibility, longLivedSessionGate: runnerResult.longLivedSessionGate, toolCalls: runnerResult.toolCalls, skills: runnerResult.skills, runner: runnerResult.runner, runnerTrace: runnerResult.runnerTrace, capabilityLevel: runnerResult.capabilityLevel, ...(runnerResult.responseType ? { responseType: runnerResult.responseType } : {}), ...(runnerResult.m3Io ? { m3Io: runnerResult.m3Io } : {}), ...(runnerResult.hardware ? { hardware: runnerResult.hardware } : {}), ...(runnerResult.operationId !== undefined ? { operationId: runnerResult.operationId } : {}), ...(runnerResult.dispatchStatus !== undefined ? { dispatchStatus: runnerResult.dispatchStatus } : {}), ...(runnerResult.exitCode !== undefined ? { exitCode: runnerResult.exitCode } : {}), ...(runnerResult.stdoutSummary !== undefined ? { stdoutSummary: runnerResult.stdoutSummary } : {}), ...(runnerResult.stderrSummary !== undefined ? { stderrSummary: runnerResult.stderrSummary } : {}), ...(runnerResult.route !== undefined ? { route: runnerResult.route } : {}), ...(runnerResult.toolName !== undefined ? { toolName: runnerResult.toolName } : {}), reply: { messageId, role: "assistant", content: runnerResult.content, createdAt: completedAt }, ...(blocker ? { blocker, blockers: [blocker, ...(runnerResult.blockers ?? [])].filter(Boolean) } : {}), usage: null, providerTrace: runnerResult.providerTrace })); return decorateChatSessionLifecycle(attachConversationFacts(payload, sessionRegistry, { now })); } export function validateCodeAgentChatSchema(payload) { const required = [ "conversationId", "sessionId", "messageId", "status", "createdAt", "updatedAt", "traceId", "provider", "model", "backend" ]; for (const field of required) { if (!payload || typeof payload !== "object" || !Object.hasOwn(payload, field)) { throw new Error(`code agent chat response missing ${field}`); } } if (!["running", "completed", "failed", "timeout", "error", "canceled"].includes(payload.status)) { throw new Error(`code agent chat response has invalid status ${JSON.stringify(payload.status)}`); } if (Number.isNaN(Date.parse(payload.createdAt)) || Number.isNaN(Date.parse(payload.updatedAt))) { throw new Error("code agent chat response timestamps must be RFC 3339 strings"); } if (["failed", "timeout", "error", "canceled"].includes(payload.status) && typeof payload.error?.message !== "string") { throw new Error("non-completed code agent chat response must include error.message"); } if (payload.status === "completed" && (typeof payload.reply?.content !== "string" || !payload.reply.content.trim())) { throw new Error("completed code agent chat response must include non-empty reply.content"); } validateCodeAgentToolCallContract(payload); } function validateCodeAgentToolCallContract(payload) { if (!Array.isArray(payload.toolCalls)) return; for (const [index, toolCall] of payload.toolCalls.entries()) { if (!toolCall || typeof toolCall !== "object") { throw new Error(`code agent toolCalls[${index}] must be an object`); } } } function finalizeCodeAgentChatPayload(payload) { validateCodeAgentChatSchema(payload); return payload; } function attachConversationFacts(payload, sessionRegistry, { now } = {}) { if (!sessionRegistry || typeof sessionRegistry.recordFact !== "function") return payload; const conversationFacts = sessionRegistry.recordFact(payload.conversationId, { kind: payload.toolCalls?.some((toolCall) => toolCall?.name === "session.context") ? "session_context" : "runner_turn", conversationId: payload.conversationId, sessionId: payload.sessionId, traceId: payload.traceId, provider: payload.provider, backend: payload.backend, workspace: payload.workspace, sandbox: payload.sandbox, session: payload.session, sessionMode: payload.sessionMode, sessionReuse: payload.sessionReuse, implementationType: payload.implementationType, runner: payload.runner, runnerTrace: payload.runnerTrace, capabilityLevel: payload.capabilityLevel, toolCalls: payload.toolCalls, skills: payload.skills }, { now }); return { ...payload, conversationFacts }; } function decorateChatSessionLifecycle(payload) { if (!payload || typeof payload !== "object") return payload; const decoratedSession = payload.session && typeof payload.session === "object" ? decorateCodeAgentSession(payload.session, { reused: payload.sessionReuse?.reused ?? payload.session?.reused, runnerTrace: payload.runnerTrace, sessionReuse: payload.sessionReuse, status: payload.status, provider: payload.provider, runner: payload.runner, capabilityLevel: payload.capabilityLevel, implementationType: payload.implementationType, longLivedSessionGate: payload.longLivedSessionGate, error: payload.error, blocker: payload.blocker }) : payload.session; const runnerTrace = payload.runnerTrace && typeof payload.runnerTrace === "object" ? { ...payload.runnerTrace, sessionLifecycleStatus: payload.runnerTrace.sessionLifecycleStatus ?? codeAgentSessionLifecycleSummary({ session: decoratedSession, sessionStatus: payload.runnerTrace.sessionStatus, status: payload.status, provider: payload.provider, runner: payload.runner, capabilityLevel: payload.capabilityLevel, implementationType: payload.implementationType, error: payload.error, blocker: payload.blocker }).status } : payload.runnerTrace; const summary = codeAgentSessionLifecycleSummary({ ...payload, session: decoratedSession, runnerTrace }); return { ...payload, session: decoratedSession, runnerTrace, sessionStatus: decoratedSession?.status ?? payload.sessionStatus ?? runnerTrace?.sessionStatus ?? null, sessionLifecycleStatus: summary.status, sessionLifecycle: summary, sessionSummary: summary, sessionReused: summary.reused, isNewSession: summary.newSession, fallbackUsed: summary.fallback, degraded: summary.degraded, degradationReason: summary.reason, degradationReasonCode: summary.reasonCode }; } export async function describeCodeAgentAvailability(env = process.env, options = {}) { const providerPlan = resolveProviderPlan(env, options); const providerContract = inspectCodeAgentProviderEnv(env); const missingEnv = providerPlan.mode === "openai" ? providerContract.missingEnv : []; if (providerPlan.mode === "codex-cli" && !env.OPENAI_API_KEY) { missingEnv.push("OPENAI_API_KEY"); } const sessionRegistry = resolveCodeAgentSessionRegistry(options); const runnerAvailability = await inspectReadOnlyRunnerAvailability(env, { ...options, sessionRegistry }); const codexStdio = await inspectCodexStdioFeasibility(env, options); const codexStdioReady = codexStdioLongLivedReady(codexStdio); const codexStdioActive = providerPlan.mode !== "openai" && codexStdioReady; const activeRunnerAvailability = codexStdioActive ? codexStdioRunnerAvailability(codexStdio) : codexStdioBlockedRunnerAvailability(codexStdio); const blocked = providerPlan.mode === "openai" ? !providerContract.ready : providerPlan.mode === "codex-stdio" ? !codexStdioReady : missingEnv.length > 0; const fallbackAvailability = { provider: providerPlan.provider, model: providerPlan.model, backend: providerPlan.backend, mode: providerPlan.mode, status: blocked ? "blocked" : "available", ready: !blocked, capabilityLevel: blocked ? "blocked" : "text-chat-only", blocker: blocked ? fallbackBlockerForPlan({ providerPlan, providerContract, codexStdio }) : null, reason: blocked ? "provider_unavailable" : null, missingEnv, secretRefs: blocked ? providerContract.secretRefs : [], egress: providerContract.egress, safety: providerContract.safety }; const longLivedGate = longLivedSessionGate({ provider: codexStdioActive ? CODEX_STDIO_PROVIDER : runnerAvailability.provider, runnerKind: codexStdioActive ? CODEX_STDIO_RUNNER_KIND : runnerAvailability.kind, sessionMode: codexStdioActive ? CODEX_STDIO_SESSION_MODE : READONLY_SESSION_MODE, implementationType: codexStdioActive ? CODEX_STDIO_IMPLEMENTATION_TYPE : READONLY_IMPLEMENTATION_TYPE, codexStdioFeasibility: codexStdio }); const agentKind = codexStdioActive ? "codex-stdio-feasible" : "codex-stdio-blocked"; const capabilityStatus = codexStdioActive ? "codex-stdio-feasible" : "blocked"; const codeAgentReady = codexStdioActive; const status = codexStdioActive ? "codex-stdio-feasible" : "blocked"; const capabilityLevel = codexStdioActive ? "long-lived-codex-stdio-session" : "blocked"; return { endpoint: "POST /v1/agent/chat", provider: providerPlan.provider, model: providerPlan.model, backend: providerPlan.backend, mode: providerPlan.mode, schema: [ "conversationId", "sessionId", "messageId", "status", "createdAt", "updatedAt", "traceId", "provider", "model", "backend", "runner", "workspace", "sandbox", "toolCalls", "skills", "runnerTrace", "capabilityLevel", "session", "sessionMode", "sessionReuse", "implementationType", "runnerLimitations", "codexStdioFeasibility", "longLivedSessionGate", "error.message", "error.code", "error.missingEnv", "availability.status" ], runner: activeRunnerAvailability, fallback: fallbackAvailability, codexStdio, status, readinessStatus: status, agentKind, capabilityStatus, blocker: codeAgentReady ? null : primaryCodeAgentBlocker({ blocked, providerContract, providerPlan, codexStdio, runnerAvailability }), reason: codeAgentReady ? null : primaryCodeAgentReason({ blocked, codexStdio, runnerAvailability }), summary: codeAgentAvailabilitySummary({ blocked, codexStdio, runnerAvailability }), m3IoSkill: runnerAvailability.m3IoSkill, missingEnv, secretRefs: blocked ? providerContract.secretRefs : [], egress: providerContract.egress, safety: providerContract.safety, capabilityLevel, workspace: activeRunnerAvailability.workspace, sandbox: activeRunnerAvailability.sandbox, session: activeRunnerAvailability.sessionRegistry?.recentSessions?.[0] ?? null, sessionStatus: activeRunnerAvailability.sessionRegistry?.recentSessions?.[0]?.status ?? null, sessionRegistry: activeRunnerAvailability.sessionRegistry ?? runnerAvailability.sessionRegistry, sessionMode: activeRunnerAvailability.sessionMode, implementationType: activeRunnerAvailability.implementationType, runnerLimitations: activeRunnerAvailability.runnerLimitations, codexStdioFeasibility: codexStdio, longLivedSessionGate: longLivedGate, blockers: [ ...codexStdio.blockers, ...(blocked && providerPlan.mode !== "codex-stdio" ? [{ code: "openai_fallback_blocked", sourceIssue: "pikasTech/HWLAB#143", summary: providerPlan.mode === "openai" ? providerContract.blocker : "Provider fallback credential/config is incomplete." }] : []) ], blockerCodes: [ ...codexStdio.blockerCodes, ...(blocked && providerPlan.mode !== "codex-stdio" ? ["openai_fallback_blocked"] : []) ], ready: codeAgentReady, partialReady: false }; } function fallbackBlockerForPlan({ providerPlan, providerContract, codexStdio }) { if (providerPlan.mode === "openai") return providerContract.blocker; if (providerPlan.mode === "codex-stdio") { return codexStdio.blockers?.[0]?.summary ?? "Codex stdio long-lived session is blocked."; } return "凭证缺口"; } function codexStdioLongLivedReady(codexStdio = {}) { return Boolean( (codexStdio.ready === true || codexStdio.canStartLongLivedCodexStdio === true) && codexStdio.commandProbe?.ready === true ); } function codexStdioRunnerAvailability(codexStdio = {}) { const workspace = codexStdio.workspace ?? repoRoot; const sandbox = codexStdio.sandbox ?? "workspace-write"; return { kind: CODEX_STDIO_RUNNER_KIND, backend: CODEX_STDIO_BACKEND, provider: CODEX_STDIO_PROVIDER, workspace, sandbox, mode: CODEX_STDIO_SESSION_MODE, sessionMode: CODEX_STDIO_SESSION_MODE, session: CODEX_STDIO_SESSION_MODE, status: "available", ready: true, capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, longLivedSession: true, durableSession: true, durable: true, codexStdio: true, writeCapable: true, readOnly: false, runnerLimitations: [...CODEX_STDIO_LIMITATION_FLAGS], codexStdioFeasibility: codexStdio, longLivedSessionGate: longLivedSessionGate({ provider: CODEX_STDIO_PROVIDER, runnerKind: CODEX_STDIO_RUNNER_KIND, sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdioFeasibility: codexStdio }), sessionRegistry: codexStdioSessionRegistrySummary(codexStdio), safety: { secretsRead: false, secretValuesPrinted: false, kubeconfigRead: false, hardwareControlViaCloudApiOnly: false, directGatewayCallsAllowed: true, directBoxSimuCallsAllowed: true, directPatchPanelCallsAllowed: true, m3m4m5AcceptanceClaimsAllowed: false } }; } function codexStdioBlockedRunnerAvailability(codexStdio = {}) { const workspace = codexStdio.workspace ?? repoRoot; const sandbox = codexStdio.sandbox ?? "workspace-write"; return { kind: CODEX_STDIO_RUNNER_KIND, backend: CODEX_STDIO_BACKEND, provider: CODEX_STDIO_PROVIDER, workspace, sandbox, mode: CODEX_STDIO_SESSION_MODE, sessionMode: CODEX_STDIO_SESSION_MODE, session: CODEX_STDIO_SESSION_MODE, status: "blocked", ready: false, capabilityLevel: "blocked", implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, longLivedSession: false, durableSession: false, durable: false, codexStdio: true, writeCapable: false, readOnly: false, runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"], codexStdioFeasibility: codexStdio, longLivedSessionGate: longLivedSessionGate({ provider: CODEX_STDIO_PROVIDER, runnerKind: CODEX_STDIO_RUNNER_KIND, sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdioFeasibility: codexStdio }), sessionRegistry: codexStdioSessionRegistrySummary(codexStdio), safety: { secretsRead: false, secretValuesPrinted: false, kubeconfigRead: false, hardwareControlViaCloudApiOnly: false, directGatewayCallsAllowed: true, directBoxSimuCallsAllowed: true, directPatchPanelCallsAllowed: true, m3m4m5AcceptanceClaimsAllowed: false } }; } function codexStdioSessionRegistrySummary(codexStdio = {}) { return { kind: "codex-stdio-session-registry", implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, status: "available", sessionCount: codexStdio.sessionLifecycle?.activeSessions ?? codexStdio.lifecycleSupervisor?.activeSessions ?? null, maxSessions: codexStdio.sessionLifecycle?.maxSessions ?? codexStdio.lifecycleSupervisor?.maxSessions ?? null, idleTimeoutMs: codexStdio.sessionLifecycle?.idleTimeoutMs ?? codexStdio.lifecycleSupervisor?.idleTimeoutMs ?? null, statuses: ["creating", "ready", "busy", "idle", "timeout", "error", "canceled", "interrupted", "expired", "failed"], statusCounts: codexStdio.statusCounts ?? null, sessionMode: CODEX_STDIO_SESSION_MODE, recentSessions: Array.isArray(codexStdio.recentSessions) ? codexStdio.recentSessions : [], longLivedCodexStdio: true, longLivedSession: true, durable: true, codexStdio: true, secretMaterialStored: false, valuesRedacted: true }; } function primaryCodeAgentBlocker({ blocked, providerContract, providerPlan, codexStdio, runnerAvailability }) { if (codexStdio.blockers?.length > 0) return codexStdio.blockers[0].summary; if (blocked && !runnerAvailability.ready) return providerPlan.mode === "openai" ? providerContract.blocker : "凭证缺口"; if (runnerAvailability.ready) return "Codex stdio is blocked; controlled read-only runner fallback is disabled for /v1/agent/chat."; return null; } function primaryCodeAgentReason({ blocked, codexStdio, runnerAvailability }) { if (codexStdio.blockers?.length > 0) return codexStdio.blockers[0].code; if (blocked && !runnerAvailability.ready) return "provider_unavailable"; if (runnerAvailability.ready) return "codex_stdio_blocked_fallback_disabled"; return null; } function codeAgentAvailabilitySummary({ blocked, codexStdio, runnerAvailability }) { if (codexStdio.ready) { return "Codex stdio long-lived session adapter is feasible; /v1/agent/chat sends natural-language requests to the persistent Codex session with full workspace and tool access."; } if (runnerAvailability.ready) return "Codex stdio long-lived session is blocked; /v1/agent/chat will not use controlled-readonly-session-registry, local skills discovery, shell/file shortcuts, or text fallback."; return blocked ? "OpenAI fallback 和 Codex stdio long-lived session 都 blocked;不返回伪完成。" : "Only text-chat fallback is available; it does not satisfy the Codex stdio session gate."; } function resolveProviderPlan(env, options = {}) { const provider = String(env.HWLAB_CODE_AGENT_PROVIDER || "auto").trim().toLowerCase(); const model = firstNonEmpty( options.model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, env.CODE_QUEUE_DEFAULT_MODEL, DEFAULT_MODEL ); if (provider === "openai") { return { provider: "openai-responses", model, backend: "hwlab-cloud-api/openai-responses", mode: "openai" }; } if (provider === "codex-cli" || provider === "codex") { return { provider: "codex-cli", model, backend: "hwlab-cloud-api/codex-cli", mode: "codex-cli" }; } if (provider === "codex-stdio") { return { provider: CODEX_STDIO_PROVIDER, model, backend: CODEX_STDIO_BACKEND, mode: "codex-stdio" }; } return { provider: env.OPENAI_API_KEY ? "openai-responses" : "codex-cli", model, backend: env.OPENAI_API_KEY ? "hwlab-cloud-api/openai-responses" : "hwlab-cloud-api/codex-cli", mode: env.OPENAI_API_KEY ? "openai" : "codex-cli", auto: true }; } async function inspectReadOnlyRunnerAvailability(env, options = {}) { const workspace = resolveRunnerWorkspace(env, options); const workspaceReady = Boolean(workspace && existsSync(workspace)); const skillsDirs = resolveSkillDirs(env, options); const skillsDirsPresent = skillsDirs.filter((dir) => existsSync(dir)); const codexStdioFeasibility = await inspectCodexStdioFeasibility(env, options); const sessionRegistry = resolveCodeAgentSessionRegistry(options).describe(); return { kind: READONLY_RUNNER_KIND, backend: READONLY_RUNNER_BACKEND, provider: READONLY_RUNNER_PROVIDER, workspace, sandbox: READONLY_RUNNER_SANDBOX, mode: READONLY_SESSION_MODE, sessionMode: READONLY_SESSION_MODE, session: READONLY_SESSION_MODE, status: workspaceReady ? "available" : "blocked", ready: workspaceReady, capabilityLevel: workspaceReady ? READONLY_SESSION_CAPABILITY_LEVEL : "blocked", implementationType: READONLY_IMPLEMENTATION_TYPE, longLivedSession: true, durableSession: false, codexStdio: false, writeCapable: false, runnerLimitations: [...READONLY_LIMITATION_FLAGS], codexStdioFeasibility, longLivedSessionGate: longLivedSessionGate({ provider: READONLY_RUNNER_PROVIDER, runnerKind: READONLY_RUNNER_KIND, sessionMode: READONLY_SESSION_MODE, implementationType: READONLY_IMPLEMENTATION_TYPE, codexStdioFeasibility }), sessionRegistry, skillsDirs, skillsDirsPresent, safety: runnerSafetyContract() }; } function structuredCompletionBlocker(result, context = {}) { if (!result || typeof result !== "object") return null; if (result.provider === OPENAI_FALLBACK_RUNNER_KIND || result.runner?.kind === OPENAI_FALLBACK_RUNNER_KIND || result.capabilityLevel === "text-chat-only") { return structuredBlocker({ code: "text_chat_only_fallback", layer: "provider", message: "OpenAI Responses fallback is text chat only and cannot satisfy Code Agent runner/session/tool capability.", userMessage: "当前仍是文本 fallback,只能回答普通问题,不能当作真实 Code Agent runner/session/tool 能力。", retryable: false, traceId: context.traceId, provider: result.provider ?? context.provider, backend: result.backend ?? context.backend, runner: result.runner ?? context.runner, capabilityLevel: result.capabilityLevel ?? context.capabilityLevel, blockers: result.longLivedSessionGate?.blockers }); } return null; } function notRequestedSkills() { return { status: "not_requested", items: [], count: 0, blockers: [] }; } async function callCodexStdioRunner({ message, conversationId, sessionId, traceId, env, now, workspace, timeoutMs, hardTimeoutMs, model, codexStdioManager, traceRecorder, conversationFacts, externalNetworkIntent = null }) { const manager = resolveCodexStdioSessionManager({ codexStdioManager }); try { return await manager.chat({ message, conversationId, sessionId, traceId, env, now, workspace, timeoutMs, hardTimeoutMs, model, traceRecorder, conversationFacts, externalNetworkIntent }); } catch (error) { const availability = error.availability ?? manager.describe({ env, workspace }); const session = error.session ?? null; const trace = error.runnerTrace ?? { traceId, runnerKind: CODEX_STDIO_RUNNER_KIND, workspace: workspace ?? availability.workspace ?? repoRoot, sandbox: availability.sandbox ?? "workspace-write", sessionMode: CODEX_STDIO_SESSION_MODE, events: [`blocked:${error.code ?? "codex_stdio_failed"}`], valuesPrinted: false }; throw runnerError(error.code ?? "codex_stdio_failed", error.message, { provider: CODEX_STDIO_PROVIDER, model, backend: CODEX_STDIO_BACKEND, workspace: workspace ?? availability.workspace ?? null, sandbox: availability.sandbox ?? "workspace-write", session, toolCalls: Array.isArray(error.toolCalls) ? error.toolCalls : [], skills: error.skills ?? notRequestedSkills(), runner: { kind: CODEX_STDIO_RUNNER_KIND, provider: CODEX_STDIO_PROVIDER, backend: CODEX_STDIO_BACKEND, workspace: workspace ?? availability.workspace ?? repoRoot, sandbox: availability.sandbox ?? "workspace-write", session: CODEX_STDIO_SESSION_MODE, sessionMode: CODEX_STDIO_SESSION_MODE, sessionId: session?.sessionId ?? null, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdio: true, longLivedSession: true, durableSession: true, writeCapable: true, readOnly: false, capabilityLevel: "blocked" }, runnerTrace: trace, capabilityLevel: "blocked", sessionMode: CODEX_STDIO_SESSION_MODE, sessionReuse: session ? sessionReuseEvidence(session) : null, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, runnerLimitations: error.runnerLimitations ?? ["codex-stdio-blocked"], codexStdioFeasibility: availability, longLivedSessionGate: longLivedSessionGate({ provider: CODEX_STDIO_PROVIDER, runnerKind: CODEX_STDIO_RUNNER_KIND, session, sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdioFeasibility: availability }), blockers: error.blockers ?? availability.blockers, route: error.route ?? null, toolName: error.toolName ?? externalNetworkIntent?.toolName ?? (error.missingTools?.length ? "codex-stdio.required-tools" : "codex-stdio.session"), conversationId, targetUrl: error.targetUrl, timeoutMs: error.timeoutMs, hardTimeoutMs: error.hardTimeoutMs, idleMs: error.idleMs, lastActivityAt: error.lastActivityAt, waitingFor: error.waitingFor, providerTrace: error.providerTrace, stage: error.stage, missingTools: error.missingTools, availability: await describeCodeAgentAvailability(env, { codexStdioManager: manager, workspace }) }); } } function sessionReuseEvidence(session) { return { conversationId: session.conversationId, sessionId: session.sessionId, mapped: true, reused: session.reused, turn: session.turn, previousTurns: Math.max(0, session.turn - 1), workspace: session.workspace, createdAt: session.createdAt, updatedAt: session.updatedAt, idleTimeoutMs: session.idleTimeoutMs, expiresAt: session.expiresAt, lastTraceId: session.lastTraceId, status: session.status }; } function releaseReadOnlySession(registry, session, { now, traceId, conversationId } = {}) { return registry.release(session.sessionId, { now, traceId, conversationId, reused: session.reused, status: "idle" }) ?? session; } function resolveCodeAgentSessionRegistry(options = {}) { return options.sessionRegistry && typeof options.sessionRegistry.acquire === "function" && typeof options.sessionRegistry.release === "function" && typeof options.sessionRegistry.describe === "function" ? options.sessionRegistry : defaultCodeAgentSessionRegistry; } function resolveCodexStdioSessionManager(options = {}) { return options.codexStdioManager && typeof options.codexStdioManager.describe === "function" && typeof options.codexStdioManager.chat === "function" && typeof options.codexStdioManager.cancel === "function" && typeof options.codexStdioManager.reapIdle === "function" ? options.codexStdioManager : defaultCodexStdioSessionManager; } function runnerSafetyContract() { return { secretsRead: false, secretValuesPrinted: false, kubeconfigRead: false, hardwareWritesAllowed: true, directGatewayCallsAllowed: true, directBoxSimuCallsAllowed: true, directPatchPanelCallsAllowed: true, openAiHardwareFallbackAllowed: false, m3m4m5AcceptanceClaimsAllowed: false, outputLimitBytes: READONLY_TOOL_OUTPUT_LIMIT }; } async function inspectCodexStdioFeasibility(env = process.env, options = {}) { const manager = resolveCodexStdioSessionManager(options); const descriptor = { env, workspace: options.workspace, command: options.command, sandbox: options.sandbox }; const feasibility = typeof manager.probe === "function" ? await manager.probe(descriptor) : manager.describe(descriptor); return { ...feasibility, checked: true, implementationRequired: "codex-stdio-or-equivalent-long-lived-runner", currentImplementation: feasibility.ready ? CODEX_STDIO_IMPLEMENTATION_TYPE : READONLY_IMPLEMENTATION_TYPE, sourceIssue: CODEX_STDIO_SOURCE_ISSUE }; } function longLivedSessionGate({ provider, runnerKind, session, sessionMode, implementationType, codexStdioFeasibility } = {}) { const normalizedProvider = String(provider ?? "").trim(); const normalizedRunnerKind = String(runnerKind ?? "").trim(); const normalizedSessionMode = String(sessionMode ?? "").trim(); const normalizedImplementation = String(implementationType ?? "").trim(); return codexLongLivedSessionGate({ provider: normalizedProvider, runnerKind: normalizedRunnerKind, session, sessionMode: normalizedSessionMode, implementationType: normalizedImplementation, codexStdioFeasibility }); } function codexStdioBlockedRunnerDescriptor({ workspace, session = null } = {}) { return { kind: CODEX_STDIO_RUNNER_KIND, provider: CODEX_STDIO_PROVIDER, backend: CODEX_STDIO_BACKEND, workspace: workspace ?? repoRoot, sandbox: "workspace-write", session: CODEX_STDIO_SESSION_MODE, sessionMode: CODEX_STDIO_SESSION_MODE, sessionId: session?.sessionId ?? null, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdio: true, longLivedSession: false, durableSession: false, writeCapable: false, readOnly: false, capabilityLevel: "blocked", toolPolicy: { allowed: ["codex-app-server.thread/start", "codex-app-server.thread/resume", "codex-app-server.turn/start"], blocked: ["controlled-readonly-session-registry", "skills.discover-local-fallback", "pwd-local-fallback", "ls-local-fallback", "rg-local-fallback", "cat-local-fallback", "openai-text-fallback"] }, runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"], safety: runnerSafetyContract() }; } function runnerError(code, message, details = {}) { const error = new Error(message); error.code = code; Object.assign(error, { provider: READONLY_RUNNER_PROVIDER, model: READONLY_RUNNER_MODEL, backend: READONLY_RUNNER_BACKEND, sandbox: READONLY_RUNNER_SANDBOX, ...details }); return error; } function boundToolOutput(value, maxLength = READONLY_TOOL_OUTPUT_LIMIT) { const text = String(value ?? ""); if (text.length <= maxLength) return { text, truncated: false }; return { text: `${text.slice(0, maxLength)}\n[output truncated at ${maxLength} bytes]`, truncated: true }; } function normalizeUserMessage(value) { if (typeof value !== "string") { throw badRequest("message must be a string"); } const message = value.trim(); if (!message) { throw badRequest("message is required"); } if (message.length > 4000) { throw badRequest("message exceeds 4000 characters"); } return message; } function chatStatusForError(error) { const code = String(error?.code ?? ""); if (code === "codex_stdio_canceled" || code === "session_canceled") return "canceled"; if (code === "codex_stdio_timeout" || code === "provider_timeout" || code === "session_timeout") return "timeout"; if (code === "session_error") return "error"; if (/timeout|timed out|超时/iu.test(String(error?.message ?? ""))) return "timeout"; return "failed"; } function conversationFactsForPrompt(sessionRegistry, conversationId) { if (!sessionRegistry || typeof sessionRegistry.getConversationFacts !== "function") return null; try { return sessionRegistry.getConversationFacts(conversationId); } catch { return null; } } export function createCodeAgentErrorPayload({ code = "code_agent_failed", message = "Code Agent request failed", reason, traceId = "trc_unassigned", conversationId = "cnv_unassigned", sessionId = "cnv_unassigned", messageId = "msg_unassigned", provider = "unassigned", model = "unknown", backend = "hwlab-cloud-api/code-agent-chat", layer, retryable, capabilityLevel = "blocked", route = "/v1/agent/chat", toolName = null, now } = {}) { const timestamp = nowIso(now); const error = new Error(message); Object.assign(error, { code, reason, layer, retryable, provider, model, backend, capabilityLevel, route, toolName }); const normalized = normalizeChatError(error, { traceId, provider, model, backend, capabilityLevel, route, toolName }); return decorateChatSessionLifecycle({ conversationId, sessionId, messageId, status: "failed", createdAt: timestamp, updatedAt: timestamp, traceId, provider, model, backend, capabilityLevel, error: normalized, blocker: normalized.blocker, blockers: normalized.blockers }); } function normalizeChatError(error, context = {}) { const code = String(error.code || "code_agent_failed"); const taxonomy = errorTaxonomy(code, error); const provider = error.provider ?? context.provider ?? READONLY_RUNNER_PROVIDER; const model = error.model ?? context.model ?? READONLY_RUNNER_MODEL; const backend = error.backend ?? context.backend ?? READONLY_RUNNER_BACKEND; const runnerKind = error.runner?.kind ?? error.runnerKind ?? context.runner?.kind ?? null; const traceId = error.traceId ?? context.traceId ?? null; const capabilityLevel = error.capabilityLevel ?? context.capabilityLevel ?? "blocked"; const route = error.route ?? context.route ?? routeForError(code, error); const toolName = error.toolName ?? context.toolName ?? toolNameForError(code, error); const message = redactText(error.message || taxonomy.message || "Code Agent request failed"); const lastEvent = error.runnerTrace?.lastEvent ?? context.runnerTrace?.lastEvent ?? error.blocker?.lastEvent ?? null; const elapsedMs = error.runnerTrace?.elapsedMs ?? context.runnerTrace?.elapsedMs ?? error.blocker?.elapsedMs ?? null; const sessionId = error.session?.sessionId ?? error.sessionId ?? error.blocker?.sessionId ?? context.sessionId ?? null; const conversationId = error.conversationId ?? error.blocker?.conversationId ?? error.session?.conversationId ?? context.conversationId ?? null; const stage = error.stage ?? error.blocker?.stage ?? lastEvent?.stage ?? taxonomy.layer; const blockers = normalizeBlockers(error.blockers, { fallbackCode: code, layer: taxonomy.layer, message, userMessage: taxonomy.userMessage, retryable: taxonomy.retryable }); const timeoutMs = numberErrorField(error.timeoutMs ?? context.timeoutMs); const hardTimeoutMs = numberErrorField(error.hardTimeoutMs ?? context.hardTimeoutMs); const idleMs = numberErrorField(error.idleMs ?? context.idleMs); const lastActivityAt = safeIsoLike(error.lastActivityAt ?? context.lastActivityAt); const waitingFor = redactText(error.waitingFor ?? context.waitingFor ?? ""); const blocker = structuredBlocker({ code, layer: taxonomy.layer, message, userMessage: error.userMessage ?? taxonomy.userMessage, retryable: error.retryable ?? taxonomy.retryable, traceId, provider, backend, runner: error.runner ?? context.runner, capabilityLevel, route, toolName, category: taxonomy.category, blockers, sessionId, conversationId, stage, lastEvent, elapsedMs, timeoutMs, hardTimeoutMs, idleMs, lastActivityAt, waitingFor }); const normalized = { code, layer: taxonomy.layer, category: taxonomy.category, blocker, retryable: blocker.retryable, userMessage: blocker.userMessage, message, traceId, provider, model, backend, runner: runnerKind, runnerKind, capabilityLevel, route, toolName, timeoutMs, hardTimeoutMs, idleMs, lastActivityAt, waitingFor, missingConfig: safeMissingConfig(error), blockers }; for (const key of [ "missingEnv", "missingCommands", "command", "exitCode", "providerStatus", "stderrSummary", "missingTools", "nextEvidence", "reason" ]) { if (error[key] !== undefined) { normalized[key] = sanitizeErrorField(key, error[key]); } } return normalized; } function errorTaxonomy(code, error = {}) { const catalog = { invalid_params: { layer: "api", category: "api_error", retryable: true, userMessage: "请求参数不完整或格式不正确,请修正后重试。" }, parse_error: { layer: "api", category: "api_error", retryable: true, userMessage: "请求 JSON 无法解析,请修正后重试。" }, provider_unavailable: { layer: error.missingEnv?.length || error.missingCommands?.length ? "provider-config" : "provider", category: error.missingEnv?.length || error.missingCommands?.length ? "needs_config" : "provider", retryable: !(error.missingEnv?.length || error.missingCommands?.length), userMessage: error.missingEnv?.length || error.missingCommands?.length ? "Code Agent provider 配置缺失,需要维护者补齐后才能使用。" : "Code Agent provider 暂不可用,可稍后重试。" }, provider_timeout: { layer: "provider", category: "timeout", retryable: true, userMessage: "Code Agent provider 响应超时,输入已保留,可稍后重试。" }, codex_cli_binary_missing: { layer: "runner", category: "needs_config", retryable: false, userMessage: "Codex CLI binary 未在运行时 PATH 中找到;当前不会安装临时二进制,也不会冒充真实 Codex。" }, session_busy: { layer: "session", category: "runner_busy", retryable: true, userMessage: "Code Agent session 正在处理上一轮请求,请稍后重试。" }, session_expired: { layer: "session", category: "session_blocked", retryable: true, userMessage: "Code Agent session 已过期,可重新发送建立新的 session。" }, session_reuse_conflict: { layer: "session", category: "session_blocked", retryable: true, userMessage: "当前 conversation 与 session 绑定不一致,可新建会话后重试。" }, session_failed: { layer: "session", category: "session_blocked", retryable: true, userMessage: "Code Agent session 已失败,可重新发送建立新的 session。" }, session_interrupted: { layer: "session", category: "session_blocked", retryable: true, userMessage: "Code Agent session 已中断,可重新发送建立新的 session。" }, session_canceled: { layer: "session", category: "session_blocked", retryable: true, userMessage: "Code Agent session 已取消,输入和 trace 已保留,可重试上一条消息。" }, session_timeout: { layer: "session", category: "timeout", retryable: true, userMessage: "Code Agent session 已超时,输入和 trace 已保留,可重试上一条消息。" }, session_error: { layer: "session", category: "session_blocked", retryable: true, userMessage: "Code Agent session 返回错误,输入和 trace 已保留,可重试上一条消息。" }, runner_unavailable: { layer: "runner", category: "runner_blocked", retryable: true, userMessage: "Code Agent runner 暂不可用,可稍后重试或等待工作区挂载恢复。" }, tool_unavailable: { layer: "tool", category: "capability_unavailable", retryable: false, userMessage: "当前 Code Agent 未开放该工具能力。" }, skills_unavailable: { layer: "skill-cli", category: "needs_config", retryable: false, userMessage: "Code Agent Skill 清单未挂载或不可读,需要补齐运行时配置。" }, text_chat_only_fallback: { layer: "provider", category: "fallback", retryable: false, userMessage: "当前仍是文本 fallback,不具备 runner/session/tool/HWLAB API 控制能力。" }, codex_stdio_blocked: { layer: "runner", category: "capability_unavailable", retryable: false, userMessage: "Codex stdio 长会话能力仍受阻,不能作为完整 Code Agent。" }, codex_stdio_protocol_blocked: { layer: "runner", category: "capability_unavailable", retryable: false, userMessage: "Codex stdio 协议工具不完整,不能作为完整 Code Agent。" }, codex_stdio_empty_response: { layer: "runner", category: "runner_blocked", retryable: true, userMessage: "Codex stdio 未返回有效回复,可稍后重试。" }, codex_stdio_timeout: { layer: "runner", category: "timeout", retryable: true, userMessage: "Codex stdio runner 响应超时,输入、sessionId 和 traceId 已保留,可重试。" }, codex_stdio_idle_timeout: { layer: "runner", category: "timeout", retryable: true, userMessage: "Codex stdio runner 超过配置时间无新事件,输入、sessionId 和 traceId 已保留,可重试。" }, codex_stdio_hard_timeout: { layer: "runner", category: "timeout", retryable: true, userMessage: "Codex stdio runner 达到硬性总时长上限,输入、sessionId 和 traceId 已保留,可重试。" }, codex_stdio_canceled: { layer: "runner", category: "canceled", retryable: true, userMessage: "本次 Codex stdio 请求已取消;输入、sessionId 和 traceId 已保留,可重试上一条消息。" }, codex_stdio_command_probe_failed: { layer: "runner", category: "runner_blocked", retryable: true, userMessage: "Codex stdio 基础命令探针失败,readiness 不会标为完整 ready;输入已保留,可稍后重试。" }, codex_stdio_failed: { layer: "runner", category: "runner_blocked", retryable: true, userMessage: "Codex stdio runner 执行失败;若 trace 显示 transport closed after partial assistant output,请新建 session 重试,并结合 providerTrace、runnerTrace 与 pod restart/rollout 事件判断是否为滚动中断。" }, codex_stdio_network_failed: { layer: "runner", category: "runner_blocked", retryable: true, userMessage: "Codex stdio runner 的网络操作失败;输入已保留,可稍后重试。" } }; return catalog[code] ?? { layer: "api", category: "api_error", retryable: true, userMessage: "Code Agent 请求失败,输入已保留,可稍后重试。" }; } function structuredBlocker({ code, layer, message, userMessage, retryable, traceId, provider, backend, runner, capabilityLevel, route, toolName, category, blockers, sessionId = null, conversationId = null, stage = null, lastEvent = null, elapsedMs = null, timeoutMs = null, hardTimeoutMs = null, idleMs = null, lastActivityAt = null, waitingFor = null }) { return { code, layer, category: category ?? layer, retryable: Boolean(retryable), summary: redactText(message), userMessage, traceId: traceId ?? null, provider: provider ?? null, backend: backend ?? null, runner: runner?.kind ?? runner ?? null, capabilityLevel: capabilityLevel ?? null, route: route ?? null, toolName: toolName ?? null, sessionId, conversationId, stage, lastEvent, elapsedMs, timeoutMs, hardTimeoutMs, idleMs, lastActivityAt, waitingFor, blockerCodes: Array.isArray(blockers) ? blockers.map((blocker) => blocker.code).filter(Boolean) : [] }; } function normalizeBlockers(blockers, fallback) { const items = Array.isArray(blockers) ? blockers : []; const normalized = items .filter((blocker) => blocker && typeof blocker === "object") .map((blocker) => ({ code: String(blocker.code ?? fallback.fallbackCode), layer: blocker.layer ?? fallback.layer, category: blocker.category ?? fallback.layer, retryable: Boolean(blocker.retryable ?? fallback.retryable), summary: redactText(blocker.summary ?? blocker.message ?? fallback.message), userMessage: blocker.userMessage ?? blocker.zh ?? fallback.userMessage, sourceIssue: blocker.sourceIssue, route: blocker.route, toolName: blocker.toolName, sessionId: blocker.sessionId, conversationId: blocker.conversationId, stage: blocker.stage, lastEvent: blocker.lastEvent, elapsedMs: blocker.elapsedMs, timeoutMs: numberErrorField(blocker.timeoutMs), hardTimeoutMs: numberErrorField(blocker.hardTimeoutMs), idleMs: numberErrorField(blocker.idleMs), lastActivityAt: safeIsoLike(blocker.lastActivityAt), waitingFor: blocker.waitingFor ? redactText(blocker.waitingFor) : null })); if (normalized.length > 0) return normalized; return [{ code: fallback.fallbackCode, layer: fallback.layer, category: fallback.layer, retryable: Boolean(fallback.retryable), summary: redactText(fallback.message), userMessage: fallback.userMessage }]; } function safeMissingConfig(error) { return [ ...(Array.isArray(error.missingEnv) ? error.missingEnv : []), ...(Array.isArray(error.missingCommands) ? error.missingCommands.map((command) => `command:${command}`) : []), ...(Array.isArray(error.missingTools) ? error.missingTools.map((tool) => `tool:${tool}`) : []) ].filter((item) => typeof item === "string" && /^[A-Za-z0-9_./:-]+$/u.test(item)); } function numberErrorField(value) { return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : null; } function safeIsoLike(value) { const text = typeof value === "string" ? value.trim() : ""; if (!text || text.length > 80) return null; return /^[0-9T:Z.+-]+$/u.test(text) ? text : null; } function resolveRunnerWorkspace(env = process.env, options = {}) { const configured = firstNonEmpty( options.workspace, env.HWLAB_CODE_AGENT_WORKSPACE, env.HWLAB_RUNNER_WORKSPACE, env.WORKSPACE ); return path.resolve(configured || repoRoot); } function resolveSkillDirs(env = process.env, options = {}) { const configured = Array.isArray(options.skillsDirs) ? options.skillsDirs : String(firstNonEmpty(env.HWLAB_CODE_AGENT_SKILLS_DIRS, env.UNIDESK_SKILLS_PATH, "")) .split(/[,;]/u) .flatMap((part) => part.split(path.delimiter)); const strict = options.skillsDirsExact === true || env.HWLAB_CODE_AGENT_SKILLS_STRICT === "1"; if (strict) { return [...new Set(configured .filter((dir) => typeof dir === "string" && dir.trim()) .map((dir) => path.resolve(dir.trim())))]; } return [path.resolve("/app/skills")]; } function sanitizeErrorField(key, value) { if (key === "missingEnv" || key === "missingCommands" || key === "missingTools") { return Array.isArray(value) ? value.filter((item) => typeof item === "string").map((item) => redactText(item)) : []; } if (key === "providerStatus" || key === "exitCode") return value; return redactText(value); } function routeForError(code, error = {}) { if (error.route !== undefined) return error.route; if (String(code).startsWith("hardware_")) return HARDWARE_INVOKE_SHELL_METHOD; return null; } function toolNameForError(code, error = {}) { if (error.toolName !== undefined) return error.toolName; if (String(code).startsWith("hardware_")) return HARDWARE_INVOKE_SHELL_METHOD; return null; } function badRequest(message) { const error = new Error(message); error.code = "invalid_params"; return error; } function cleanProtocolId(value, prefix) { if (typeof value !== "string") return null; const trimmed = value.trim(); if (!trimmed) return null; if (/^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/u.test(trimmed)) return trimmed; return `${prefix}_${trimmed.replace(/[^A-Za-z0-9._:-]/gu, "-").slice(0, 80)}`; } function cleanProjectId(value) { return cleanProtocolId(value, "prj"); } function resolveConversationSessionIds(params = {}) { const requestedConversationId = cleanProtocolId(params.conversationId, "cnv"); const requestedSessionId = cleanProtocolId(params.sessionId, "ses"); if (requestedConversationId && requestedSessionId) { return { conversationId: requestedConversationId, sessionId: requestedSessionId, requestedSessionId }; } if (requestedConversationId) { return { conversationId: requestedConversationId, sessionId: requestedConversationId, requestedSessionId: null }; } if (requestedSessionId) { const conversationId = `cnv_${requestedSessionId.replace(/^[a-z][a-z0-9]*_/u, "")}`; return { conversationId, sessionId: requestedSessionId, requestedSessionId }; } const conversationId = `cnv_${randomUUID()}`; return { conversationId, sessionId: conversationId, requestedSessionId: null }; } function nowIso(now) { return typeof now === "function" ? now() : new Date().toISOString(); } function firstDefined(...values) { return values.find((value) => value !== undefined && value !== null); } function formatM3Value(value) { if (value === undefined || value === null) return "未产生/不可证明"; return String(value); } function firstNonEmpty(...values) { for (const value of values) { if (typeof value === "string" && value.trim()) return value.trim(); } return ""; } function redactText(value) { return String(value ?? "") .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gu, "Bearer ***") .replace(/sk-[A-Za-z0-9._-]+/gu, "sk-***") .replace(/([A-Za-z0-9_]*TOKEN[A-Za-z0-9_]*=)[^\s]+/giu, "$1***") .replace(/([A-Za-z0-9_]*KEY[A-Za-z0-9_]*=)[^\s]+/giu, "$1***"); } function redactUrl(value) { try { const url = new URL(value); url.username = ""; url.password = ""; return url.toString(); } catch { return redactText(String(value ?? "")).replace(/\/\/[^/@]+@/u, "//***@"); } }