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";
import {
  codeAgentAgentRunAdapterEnabled,
  describeAgentRunAdapterAvailability
} from "./code-agent-agentrun-adapter.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,
        threadId: params.threadId,
        traceId,
        env: options.env ?? process.env,
        now: options.now,
        workspace: options.workspace,
        timeoutMs: options.timeoutMs,
        hardTimeoutMs: options.hardTimeoutMs,
        model: providerPlan.model,
        codexStdioManager: options.codexStdioManager,
        skillsDirs: options.skillsDirs,
        skillsDirsExact: options.skillsDirsExact,
        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?.diagnosis) payload.diagnosis = payload.error.diagnosis;
    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;
    const factPayload = shouldRecordErrorConversationFact(payload) ? attachConversationFacts(decorateChatSessionLifecycle(payload), sessionRegistry, { now: options.now }) : null;
    if (factPayload?.conversationFacts) {
      payload.conversationFacts = factPayload.conversationFacts;
    } else 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 shouldRecordErrorConversationFact(payload = {}) {
  if (!payload || typeof payload !== "object") return false;
  if (payload.status === "running" || payload.status === "completed") return false;
  if (payload.partialContext) return true;
  if (payload.runnerTrace?.partialContext) return true;
  return Boolean(payload.runnerTrace?.traceId && payload.sessionId && payload.conversationId);
}

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,
    degraded: summary.degraded,
    degradationReason: summary.reason,
    degradationReasonCode: summary.reasonCode
  };
}

export async function describeCodeAgentAvailability(env = process.env, options = {}) {
  if (codeAgentAgentRunAdapterEnabled(env)) {
    return describeAgentRunAdapterAvailability(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, threadId, traceId, env, now, workspace, timeoutMs, hardTimeoutMs, model, codexStdioManager, skillsDirs, skillsDirsExact, traceRecorder, conversationFacts, externalNetworkIntent = null }) {
  const manager = resolveCodexStdioSessionManager({ codexStdioManager });
  try {
    return await manager.chat({
      message,
      conversationId,
      sessionId,
      threadId,
      traceId,
      env,
      now,
      workspace,
      timeoutMs,
      hardTimeoutMs,
      model,
      skillsDirs,
      skillsDirsExact,
      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,
      diagnosis: error.diagnosis,
      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 diagnosis = normalizeDiagnosis(error.diagnosis ?? context.diagnosis ?? error.blocker?.diagnosis ?? null);
  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,
    diagnosis
  });
  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,
    diagnosis,
    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: "runner_blocked",
      retryable: true,
      userMessage: "Code Agent 当前 turn 已失败，session/thread 已保留，可继续发送下一条消息。"
    },
    session_storage_evicted: {
      layer: "session",
      category: "session_blocked",
      retryable: true,
      userMessage: "Code Agent session 存储已失效（PVC 被回收或 TTL 到期），HWLAB 已为你开新 sessionId，可继续发送下一条消息。"
    },
    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 当前 turn 已超时，输入、trace、session/thread 已保留，可继续发送下一条消息。"
    },
    session_error: {
      layer: "session",
      category: "runner_blocked",
      retryable: true,
      userMessage: "Code Agent 当前 turn 返回错误，输入、trace、session/thread 已保留，可继续发送下一条消息。"
    },
    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, diagnosis = 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,
    diagnosis,
    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,
      currentTraceId: blocker.currentTraceId,
      currentTraceStatus: blocker.currentTraceStatus,
      currentTraceLastEvent: blocker.currentTraceLastEvent,
      currentTraceUpdatedAt: safeIsoLike(blocker.currentTraceUpdatedAt),
      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,
      diagnosis: normalizeDiagnosis(blocker.diagnosis ?? 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 normalizeDiagnosis(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  return {
    code: value.code ? redactText(value.code) : null,
    layer: value.layer ? redactText(value.layer) : null,
    kind: value.kind ? redactText(value.kind) : null,
    summary: value.summary ? redactText(value.summary) : null,
    waitingFor: value.waitingFor ? redactText(value.waitingFor) : null,
    lastActivityAt: safeIsoLike(value.lastActivityAt),
    idleMs: numberErrorField(value.idleMs),
    threadId: value.threadId ? redactText(value.threadId) : null,
    turnId: value.turnId ? redactText(value.turnId) : null,
    lastRoutedEvent: value.lastRoutedEvent && typeof value.lastRoutedEvent === "object" ? {
      type: value.lastRoutedEvent.type ?? null,
      status: value.lastRoutedEvent.status ?? null,
      label: value.lastRoutedEvent.label ?? null,
      stage: value.lastRoutedEvent.stage ?? null,
      toolName: value.lastRoutedEvent.toolName ?? null,
      itemId: value.lastRoutedEvent.itemId ?? null,
      waitingFor: value.lastRoutedEvent.waitingFor ?? null,
      exitCode: numberErrorField(value.lastRoutedEvent.exitCode),
      durationMs: numberErrorField(value.lastRoutedEvent.durationMs)
    } : null
  };
}

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, "//***@");
  }
}
