// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010403 API契约 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-010205 HWLAB接入 draft-2026-06-25-p0-session-warm-runner-contract; PJ2026-0102 Agent编排 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// Responsibility: Code Agent HTTP steer and trace resources.

import { createHash, randomUUID } from "node:crypto";

import { M3_IO_CONTROL_ROUTE, M3_STATUS_ROUTE, describeM3StatusLive, handleM3IoControl } from "./m3-io-control.ts";
import {
  agentRunSessionEvidence,
  cancelAgentRunChatTurn,
  codeAgentAgentRunAdapterEnabled,
  initialAgentRunChatResult,
  loadPersistedAgentRunResult,
  steerAgentRunChatTurn,
  submitAgentRunChatTurn
} from "./code-agent-agentrun-adapter.ts";
import { createCodeAgentErrorPayload, handleCodeAgentChat } from "./code-agent-chat.ts";
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts";
import { messageAuthorityTextValue } from "./code-agent-agentrun-prompt.ts";
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
import { codeAgentOtelTraceFields, emitCodeAgentOtelSpan } from "./otel-trace.ts";
import {
  firstHeaderValue,
  getHeader,
  parsePositiveInteger,
  positiveInteger,
  readBody,
  safeConversationId,
  safeOpaqueId,
  safeSessionId,
  safeTraceId,
  sendJson,
  truthyFlag
} from "./server-http-utils.ts";

import {
  normalizeCodeAgentProviderProfile,
  firstNonEmptyValue,
  recordCodeAgentSessionInputFact,
  stableCodeAgentInputId,
  codeAgentInputDelivery,
  codeAgentInputStatus,
  codeAgentResultTimingFields,
  firstTimestampIso,
  firstLatestTimestampIso,
  codeAgentChatShortConnectionRequested,
  preflightCodeAgentBilling,
  recordCodeAgentBillingUsage,
  finalizeCodeAgentBillingUsage,
  releaseCodeAgentBillingReservation,
  withCodeAgentBillingReservation,
  codeAgentBillingEnabled,
  codeAgentBillingMetadata,
  sanitizeBillingReservation,
  sanitizeBillingRecord,
  sanitizeBillingRelease,
  codeAgentUsedTokens,
  normalizeTurnStatus,
  recordCodeAgentConversationFact,
  recordCodeAgentSessionOwner,
  codeAgentOwnerStatusForResult,
  textValue,
  timestampIsoOrNow,
  timestampIsoOrNull,
  codeAgentSessionOwnerEvidence,
  codeAgentContinuationEvidence,
  codeAgentFreshContinuationFailure,
  normalizeCodeAgentFailureKind,
  codeAgentFailureRequiresFreshContinuation,
  codeAgentProviderProfileEvidence,
  codeAgentTraceResultEvidence,
  codeAgentPromptMetadata,
  codeAgentPromptMetadataFromText,
  codeAgentPromptMetadataFromParts,
  codeAgentPromptFields,
  codeAgentPromptId,
  clippedPromptText,
  sha256Text,
  compactCodeAgentObject,
  codeAgentBillingReservationEvidence,
  codeAgentConversationMessagesEvidence,
  boundedConversationMessageText,
  conversationText,
  conversationTextRaw,
  codeAgentMessageTraceSuffix,
  codeAgentTurnLifecycleFields,
  codeAgentLifecycleMessageId,
  safeTurnId,
  safeMessageId,
  codeAgentConversationAgentMessageStatus,
  codeAgentPayloadHasSealedFinalResponse,
  codeAgentConversationRunnerTraceEvidence,
  codeAgentFinalResponseEvidence,
  codeAgentFinalResponseText,
  codeAgentTraceSummaryEvidence,
  annotateOwner,
  canAccessOwnedResult,
  isCodeAgentResultCanceled,
  codeAgentCancelScopeMismatch,
  cancelExpectedValues,
  cancelScopeFieldMismatch,
  cancelBlockedPayload,
  isTraceCommandTerminalStatus,
  traceSnapshotWithTerminalEvidence,
  traceSnapshotWithAttachedTerminalEvidence,
  agentRunTerminalTraceEvidence,
  agentRunTerminalFinalResponse,
  traceRetentionSummary,
  numberOrNull,
  createCodeAgentChatResultStore,
  compactCodeAgentChatResultPayload,
  terminalEvidencePayload,
  compactRunnerTraceForResult,
  traceEventLabel,
  resultTraceEventLimit,
  createCodeAgentM3HwlabApiRequestJson,
  recordCodeAgentProjectionMetrics,
  recordCodeAgentTurnReadMetric,
  turnReadDegradedReason,
  responseBodyBytes,
  statusClassLabel,
  sourceLatestSeqFromResult,
  sourceLatestAtFromResult,
  terminalSourceAtFromResult,
  projectionReason,
  nowMs,
  uniqueStrings
} from "./server-code-agent-http-support.ts";

import {
  codeAgentCompatProjectionPayload,
  codeAgentRequestScopedOptions,
  measureCodeAgentHttpPhase,
  readCodeAgentCompatProjection,
  traceProjectMismatchSnapshot
} from "./server-code-agent-turn-http.ts";

const DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT = 120;
const DEFAULT_CODE_AGENT_TRACE_PAGE_LIMIT = 100;
const MAX_CODE_AGENT_TRACE_PAGE_LIMIT = 100;
const DEFAULT_CODE_AGENT_TURN_STATUS_REFRESH_TIMEOUT_MS = 2500;
const DEFAULT_CODE_AGENT_PROJECT_ID = "prj_hwpod_workbench";
const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek";
const CODE_AGENT_TERMINAL_STATUSES = new Set(["completed", "failed", "blocked", "timeout", "cancelled", "canceled"]);
const CODE_AGENT_PROVIDER_PROFILE_ALIASES = Object.freeze({ codex: "codex-api" });
const CODE_AGENT_PROVIDER_PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9.-]{0,63}$/u;
const CODE_AGENT_PROVIDER_PROFILE_LABELS = Object.freeze({
  "deepseek": "DeepSeek",
  "codex-api": "Codex API",
  "gpt.pika": "gpt.pika",
  "minimax-m3": "MiniMax-M3 via AgentRun",
  "runtime-default": "运行默认"
});
const DEFAULT_CODE_AGENT_CODEX_API_MODEL = "gpt-5.5";
const DEFAULT_CODE_AGENT_CODEX_API_BASE_URL = "http://127.0.0.1:49280/responses";
const DEFAULT_CODE_AGENT_DEEPSEEK_MODEL = "deepseek-chat";
const DEFAULT_CODE_AGENT_MINIMAX_M3_MODEL = "MiniMax-M3";

export async function handleCodeAgentSteerHttp(request, response, options) {
  const body = await readBody(request, options.bodyLimitBytes);
  let params = {};

  try {
    params = body ? JSON.parse(body) : {};
  } catch (error) {
    sendJson(response, 400, {
      ...createCodeAgentErrorPayload({
        code: "parse_error",
        message: "Invalid JSON body",
        reason: error.message,
        traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
        layer: "api",
        retryable: true,
        route: "/v1/agent/chat/steer"
      })
    });
    return;
  }

  if (!params || typeof params !== "object" || Array.isArray(params)) {
    sendJson(response, 400, {
      ...createCodeAgentErrorPayload({
        code: "invalid_params",
        message: "Code Agent steer body must be a JSON object",
        traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
        layer: "api",
        retryable: true,
        route: "/v1/agent/chat/steer"
      })
    });
    return;
  }

  const traceId = safeTraceId(getHeader(request, "x-trace-id") || params.traceId || params.targetTraceId);
  const message = firstNonEmptyValue(params.message, params.prompt, params.text);
  if (!traceId || !message) {
    sendJson(response, 400, {
      ...createCodeAgentErrorPayload({
        code: !traceId ? "steer_trace_missing" : "steer_message_missing",
        message: !traceId ? "traceId is required to steer the current Code Agent turn." : "message is required to steer the current Code Agent turn.",
        traceId: traceId || "trc_unassigned",
        layer: "api",
        retryable: true,
        route: "/v1/agent/chat/steer"
      })
    });
    return;
  }

  const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
  const durableResult = await loadPersistedAgentRunResult(traceId, options);
  const currentResult = durableResult ?? options.codeAgentChatResults?.get?.(traceId);
  const projectionContext = await readCodeAgentCompatProjection(traceId, options);
  if (!canAccessOwnedResult(currentResult, options.actor)) {
    sendJson(response, 403, {
      ok: false,
      status: "forbidden",
      traceId,
      error: { code: "code_agent_trace_forbidden", message: "Code Agent trace is not visible to the current actor" }
    });
    return;
  }
  const projectionStoreBlocker = codeAgentProjectionStoreBlocker(projectionContext);
  if (projectionStoreBlocker) {
    sendJson(response, 503, {
      ok: false,
      accepted: false,
      status: "blocked",
      traceId,
      route: "/v1/agent/chat/steer",
      error: {
        ...projectionStoreBlocker,
        code: "projection_store_unavailable",
        layer: projectionStoreBlocker.layer ?? "runtime-store",
        category: projectionStoreBlocker.category ?? "projection-store-unavailable",
        retryable: true,
        message: projectionStoreBlocker.message ?? "Workbench projection store is unavailable; steer is blocked until terminal authority can be checked.",
        traceId,
        route: "/v1/agent/chat/steer"
      },
      valuesPrinted: false
    });
    return;
  }
  if (!currentResult?.agentRun?.runId || !currentResult?.agentRun?.commandId) {
    sendJson(response, 404, {
      ok: false,
      status: "not_found",
      traceId,
      error: { code: "steer_trace_not_found", message: "No AgentRun-backed Code Agent turn was found for the requested traceId." },
      route: "/v1/agent/chat/steer",
      runnerTrace: traceStore.snapshot(traceId)
    });
    return;
  }
  const projectedTerminalStatus = normalizeTurnStatus(
    projectionContext.trace?.terminalEvidence?.status,
    projectionContext.trace?.status
  );
  if (isTraceCommandTerminalStatus(projectedTerminalStatus)) {
    sendJson(response, 409, {
      ok: false,
      accepted: false,
      status: "blocked",
      traceId,
      route: "/v1/agent/chat/steer",
      sessionId: safeSessionId(currentResult.sessionId ?? params.sessionId) || null,
      threadId: safeOpaqueId(currentResult.threadId ?? params.threadId) || null,
      agentRun: currentResult.agentRun,
      runnerTrace: traceStore.snapshot(traceId),
      error: {
        code: "steer_trace_terminal",
        layer: "api",
        category: "not_in_flight",
        retryable: false,
        message: `Trace ${traceId} is already terminal and cannot accept a steer command.`,
        traceId,
        route: "/v1/agent/chat/steer",
        toolName: "agentrun.command.steer"
      },
      valuesPrinted: false
    });
    return;
  }

  try {
    await recordCodeAgentSessionInputFact({
      params: {
        ...params,
        sessionId: currentResult.sessionId ?? currentResult.session?.sessionId ?? params.sessionId,
        ownerUserId: options.actor?.id,
        ownerRole: options.actor?.role
      },
      options,
      traceId,
      delivery: "steer",
      status: "promoted",
      commandId: currentResult.agentRun.commandId
    });
    const payload = await steerAgentRunChatTurn({
      traceId,
      currentResult,
      params: {
        ...params,
        message,
        ownerUserId: options.actor?.id ?? params.ownerUserId,
        ownerRole: options.actor?.role ?? params.ownerRole
      },
      options,
      traceStore
    });
    sendJson(response, 202, payload);
  } catch (error) {
    traceStore.append(traceId, {
      type: "backend",
      status: "failed",
      label: "agentrun:steer:failed",
      errorCode: error?.code ?? "agentrun_steer_failed",
      message: error?.message ?? "AgentRun steer command failed.",
      terminal: false,
      valuesPrinted: false
    });
    sendJson(response, error?.statusCode === 404 ? 404 : 409, {
      ok: false,
      accepted: false,
      status: "failed",
      traceId,
      route: "/v1/agent/chat/steer",
      sessionId: safeSessionId(currentResult.sessionId ?? params.sessionId) || null,
      threadId: safeOpaqueId(currentResult.threadId ?? params.threadId) || null,
      agentRun: currentResult.agentRun,
      runnerTrace: traceStore.snapshot(traceId),
      error: {
        code: error?.code ?? "agentrun_steer_failed",
        layer: "agentrun",
        category: "steer_failed",
        retryable: true,
        message: error?.message ?? "AgentRun steer command failed.",
        traceId,
        route: "/v1/agent/chat/steer",
        toolName: "agentrun.command.steer"
      }
    });
  }
}

export async function handleCodeAgentTraceHttp(request, response, url, options) {
  const startedAt = nowMs();
  const parts = url.pathname.split("/").filter(Boolean);
  const traceId = decodeURIComponent(parts[3] ?? "");
  const streamSegment = parts[4];
  const pageOptions = codeAgentTracePageOptions(url);
  if (!/^trc_[A-Za-z0-9_.:-]+$/u.test(traceId)) {
    sendJson(response, 400, {
      error: {
        code: "invalid_trace_id",
        message: "traceId must start with trc_ and contain only safe identifier characters"
      }
    });
    return;
  }
  const requestOptions = codeAgentRequestScopedOptions(options);
  const projectMismatch = await measureCodeAgentHttpPhase(requestOptions, "trace_project_check", () => traceProjectMismatchSnapshot(traceId, url, requestOptions, "trace"));
  if (projectMismatch) {
    sendJson(response, projectMismatch.statusCode, projectMismatch.body);
    return;
  }
  const context = await measureCodeAgentHttpPhase(requestOptions, "trace_projection_read", () => readCodeAgentCompatProjection(traceId, requestOptions));
  if (context.statusCode) {
    sendJson(response, context.statusCode, context.body);
    return;
  }

  if (streamSegment === "stream") {
    sendJson(response, 410, {
      ok: false,
      status: "removed",
      traceId,
      error: {
        code: "trace_store_sse_removed",
        message: "Trace-store SSE was removed; use the committed Workbench projection event stream.",
        workbenchEventsUrl: `/v1/workbench/events?traceId=${encodeURIComponent(traceId)}`
      },
      valuesRedacted: true
    });
    return;
  }

  const diagnosticTrace = (requestOptions.traceStore ?? defaultCodeAgentTraceStore).snapshot(traceId);
  const trace = mergeCodeAgentDiagnosticTrace(context.trace, diagnosticTrace, traceId);
  const diagnosticFound = trace.events.length > 0;
  const body = await measureCodeAgentHttpPhase(requestOptions, "trace_paginate", () => {
    return paginateTraceSnapshot(trace, pageOptions);
  });
  const statusCode = context.found || diagnosticFound ? 200 : 404;
  void emitCodeAgentOtelSpan("trace_events_read", traceId, requestOptions.env ?? process.env, {
    startTimeMs: startedAt,
    attributes: {
      "http.method": "GET",
      "http.route": "/v1/agent/traces/:traceId",
      "http.status_code": statusCode,
      found: Boolean(context.found),
      returnedEvents: Array.isArray(body?.events) ? body.events.length : 0,
      sinceSeq: pageOptions.sinceSeq,
      limit: pageOptions.limit,
      fromSeq: body?.range?.fromSeq ?? null,
      toSeq: body?.range?.toSeq ?? null,
      totalEvents: body?.range?.total ?? body?.eventCount ?? null,
      hasMore: Boolean(body?.hasMore),
      fullTraceLoaded: Boolean(body?.fullTraceLoaded)
    }
  });
  sendJson(response, statusCode, codeAgentCompatProjectionPayload(body, context));
}

function mergeCodeAgentDiagnosticTrace(projectionTrace, diagnosticTrace, traceId) {
  const projected = projectionTrace && typeof projectionTrace === "object" ? projectionTrace : {};
  const diagnostics = diagnosticTrace && typeof diagnosticTrace === "object" ? diagnosticTrace : {};
  const merged = new Map();
  for (const [source, events] of [["diagnostic", diagnostics.events], ["projection", projected.events]]) {
    for (const [index, event] of (Array.isArray(events) ? events : []).entries()) {
      const originalSeq = traceEventSequence(event, index);
      const key = textValue(event?.id ?? event?.sourceEventId) || `${originalSeq}:${textValue(event?.label ?? event?.type)}:${textValue(event?.createdAt ?? event?.occurredAt)}`;
      const normalized = source === "projection"
        ? {
            ...event,
            projectedSeq: Number.isFinite(Number(event?.projectedSeq)) ? Number(event.projectedSeq) : originalSeq,
            authority: "workbench-projection"
          }
        : {
            ...event,
            sourceSeq: Number.isFinite(Number(event?.sourceSeq)) ? Number(event.sourceSeq) : originalSeq,
            terminal: false,
            sealed: false,
            diagnosticOnly: true,
            authority: "trace-store-diagnostics"
          };
      merged.set(key, { event: normalized, source, originalSeq, sourceIndex: index, key });
    }
  }
  const events = [...merged.values()]
    .sort((left, right) => left.originalSeq - right.originalSeq
      || (left.source === right.source ? 0 : left.source === "projection" ? -1 : 1)
      || left.sourceIndex - right.sourceIndex
      || left.key.localeCompare(right.key))
    .map(({ event }, index) => ({ ...event, seq: index + 1, paginationSeq: index + 1 }));
  const projectionFound = projected.status && projected.status !== "missing";
  const projectionEvents = events.filter((event) => event.authority === "workbench-projection");
  const projectionTerminal = projected.terminal === true || projectionEvents.some((event) => event.terminal === true);
  const projectionSealed = projected.sealed === true || projectionEvents.some((event) => event.sealed === true);
  return {
    ...projected,
    traceId,
    status: projectionFound ? projected.status : events.length > 0 ? "running" : "missing",
    terminal: projectionTerminal,
    sealed: projectionSealed,
    events,
    eventCount: events.length,
    diagnosticEventCount: events.filter((event) => event.diagnosticOnly === true).length,
    terminalAuthority: "workbench-projection",
    valuesRedacted: true
  };
}

function codeAgentProjectionStoreBlocker(context = {}) {
  const candidates = [
    context?.projection?.blocker,
    context?.trace?.projection?.blocker,
    context?.trace?.blocker,
    context?.result?.projection?.blocker,
    context?.result?.blocker
  ];
  return candidates.find((blocker) => blocker?.code === "projection_store_unavailable") ?? null;
}

function codeAgentTracePageOptions(url) {
  const sinceSeq = nonNegativeInteger(url.searchParams.get("sinceSeq") ?? url.searchParams.get("since"), 0);
  const requestedLimit = parsePositiveInteger(url.searchParams.get("limit"), DEFAULT_CODE_AGENT_TRACE_PAGE_LIMIT);
  return {
    sinceSeq,
    limit: Math.min(requestedLimit, MAX_CODE_AGENT_TRACE_PAGE_LIMIT)
  };
}

function paginateTraceSnapshot(snapshot, { sinceSeq, limit }) {
  const sourceEvents = Array.isArray(snapshot?.events) ? snapshot.events : [];
  const indexedEvents = sourceEvents.map((event, index) => ({ event, seq: traceEventSequence(event, index) }));
  const firstIndex = indexedEvents.findIndex((item) => item.seq > sinceSeq);
  const startIndex = firstIndex >= 0 ? firstIndex : indexedEvents.length;
  const pageItems = indexedEvents.slice(startIndex, startIndex + limit);
  const events = pageItems.map((item) => item.event);
  const fromSeq = pageItems.length > 0 ? pageItems[0].seq : null;
  const toSeq = pageItems.length > 0 ? pageItems[pageItems.length - 1].seq : null;
  const hasMore = startIndex + pageItems.length < indexedEvents.length;
  const totalEvents = snapshot?.eventCount ?? indexedEvents.length;
  return {
    ...snapshot,
    events,
    eventCount: totalEvents,
    range: {
      sinceSeq,
      fromSeq,
      toSeq,
      limit,
      returned: events.length,
      total: totalEvents
    },
    truncated: hasMore,
    hasMore,
    nextSinceSeq: pageItems.length > 0 ? toSeq : sinceSeq,
    fullTraceLoaded: !hasMore
  };
}

function traceEventSequence(event, index) {
  const seq = Number(event?.seq);
  return Number.isFinite(seq) && seq > 0 ? Math.trunc(seq) : index + 1;
}

function nonNegativeInteger(value, fallback) {
  const parsed = Number.parseInt(value ?? "", 10);
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
}
