// 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 result, turn, inspect, and cancel 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"; 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 handleCodeAgentChatResultHttp(request, response, url, options) { const parts = url.pathname.split("/").filter(Boolean); const traceId = decodeURIComponent(parts[4] ?? ""); if (!safeTraceId(traceId)) { sendJson(response, 400, { error: { code: "invalid_trace_id", message: "traceId must start with trc_ and contain only safe identifier characters" } }); return; } const context = await measureCodeAgentHttpPhase(options, "result_projection_read", () => readCodeAgentCompatProjection(traceId, options)); if (context.statusCode) { sendJson(response, context.statusCode, context.body); return; } const result = context.result; if (result && result.status !== "running" && !codeAgentResultUsesProjectionTerminalAuthority(result)) { sendJson(response, 200, codeAgentCompatProjectionPayload(compactCodeAgentChatResultPayload(result, options), context)); return; } if (context.found) { const body = codeAgentTurnStatusPayload({ traceId, result, snapshot: context.trace, resultPollError: null, refreshError: context.refreshError ?? null, options }); const resultPollTerminalReady = codeAgentResultPollTerminalReady(body); sendJson(response, resultPollTerminalReady ? 200 : 202, codeAgentCompatProjectionPayload({ accepted: body.ok, shortConnection: true, ...body, waitingFor: resultPollTerminalReady ? body.waitingFor ?? context.trace?.waitingFor ?? "workbench-projection" : "agentrun-result", ...(body.terminal === true && !resultPollTerminalReady ? { status: "running", running: true, terminal: false } : {}) }, context)); return; } sendJson(response, 404, { ...codeAgentCompatProjectionPayload({ traceId, status: "unknown" }, context), error: { code: "code_agent_result_not_found", message: `No Code Agent result is registered for ${traceId}` } }); } export async function handleCodeAgentTurnHttp(request, response, url, options) { const startedAt = nowMs(); const parts = url.pathname.split("/").filter(Boolean); const traceId = decodeURIComponent(parts[3] ?? ""); if (!safeTraceId(traceId)) { const body = { ok: false, status: "unknown", running: false, terminal: false, error: { code: "invalid_trace_id", message: "traceId must start with trc_ and contain only safe identifier characters" } }; recordCodeAgentTurnReadMetric(options, url, 400, body, startedAt); sendJson(response, 400, body); return; } const requestOptions = codeAgentRequestScopedOptions(options); let completed = false; const emitTurnStatusReadSpan = (name, attributes = {}, error = null) => { void emitCodeAgentOtelSpan(name, traceId, requestOptions.env ?? process.env, { startTimeMs: startedAt, status: error ? "error" : undefined, error, attributes: { "http.method": "GET", "http.route": "/v1/agent/turns/:traceId", durationMs: nowMs() - startedAt, ...attributes } }); }; response.once?.("close", () => { if (completed) return; emitTurnStatusReadSpan("turn_status_read.client_closed", { status: "client_closed", phase: "response_close", terminal: false }, new Error("Code Agent turn status client connection closed before response completed.")); }); try { const projectMismatch = await measureCodeAgentHttpPhase(requestOptions, "turn_project_check", () => traceProjectMismatchSnapshot(traceId, url, requestOptions, "turn")); if (projectMismatch) { completed = true; recordCodeAgentTurnReadMetric(options, url, projectMismatch.statusCode, projectMismatch.body, startedAt); emitTurnStatusReadSpan("turn_status_read", { "http.status_code": projectMismatch.statusCode, status: projectMismatch.body?.status ?? null, terminal: projectMismatch.body?.terminal ?? null, phase: "project_check" }); sendJson(response, projectMismatch.statusCode, projectMismatch.body); return; } const resolved = await measureCodeAgentHttpPhase(requestOptions, "turn_resolve_status", () => resolveCodeAgentTurnStatusSnapshot(traceId, requestOptions)); completed = true; recordCodeAgentTurnReadMetric(options, url, resolved.statusCode, resolved.body, startedAt); emitTurnStatusReadSpan("turn_status_read", { "http.status_code": resolved.statusCode, status: resolved.body?.status ?? null, terminal: resolved.body?.terminal ?? null, phase: "resolved" }); sendJson(response, resolved.statusCode, resolved.body); } catch (error) { completed = true; const body = { ok: false, status: "unknown", running: false, terminal: false, traceId, error: { code: error?.code ?? "turn_status_read_failed", message: error?.message ?? "Code Agent turn status read failed." }, valuesRedacted: true }; recordCodeAgentTurnReadMetric(options, url, 500, body, startedAt); emitTurnStatusReadSpan("turn_status_read", { "http.status_code": 500, status: body.status, terminal: false, phase: "failed", errorCode: body.error.code }, error); sendJson(response, 500, body); } } async function resolveCodeAgentTurnStatusSnapshot(traceId, options) { const context = await readCodeAgentCompatProjection(traceId, options); if (context.statusCode) return context; const body = await measureCodeAgentHttpPhase(options, "turn_payload", () => codeAgentTurnStatusPayload({ traceId, result: context.result, snapshot: context.trace, resultPollError: null, refreshError: context.refreshError ?? null, options })); return { statusCode: body.ok ? 200 : 404, body: codeAgentCompatProjectionPayload(body, context) }; } function codeAgentRequestScopedOptions(options = {}) { return { ...options, codeAgentTraceSessionCache: options.codeAgentTraceSessionCache instanceof Map ? options.codeAgentTraceSessionCache : new Map() }; } async function measureCodeAgentHttpPhase(options, phase, callback) { const perf = options?.backendPerformance; return perf && typeof perf.measure === "function" ? await perf.measure(phase, callback) : await callback(); } function forbiddenTurnSnapshot(traceId) { return { statusCode: 403, body: { ok: false, status: "unknown", running: false, terminal: false, traceId, error: { code: "agent_session_owner_required", message: "Only the session owner or admin can read this Code Agent turn status" } } }; } async function readCodeAgentCompatProjection(traceId, options = {}) { const readModel = createWorkbenchReadModel(options, options.actor ?? null); const session = await readModel.getSessionByTraceId(traceId); let result = readModel.resultForTrace(traceId) ?? projectedSessionTraceResult(session, traceId); if (result && !canAccessOwnedResult(result, options.actor)) return forbiddenTurnSnapshot(traceId); if (result?.ownerUserId && !readModel.canReadOwner(result.ownerUserId)) return forbiddenTurnSnapshot(traceId); const projectionTrace = await readModel.traceSnapshot(traceId); let trace = traceSnapshotWithTerminalEvidence(projectionTrace, result, traceId, null); const projection = readModel.projectionDiagnostics({ traceId, result, trace, refreshError: null }); recordCodeAgentProjectionMetrics(options, { result, trace, projection, refreshError: null }); const found = Boolean(result || session || (trace && trace.status !== "missing") || trace?.persisted === true); return { result, session, trace, projection, found, refreshError: null }; } function projectedSessionTraceResult(session, traceId) { const safeId = safeTraceId(traceId); if (!session || !safeId) return null; const snapshot = session.session && typeof session.session === "object" ? session.session : null; const traceResults = snapshot?.traceResults && typeof snapshot.traceResults === "object" ? snapshot.traceResults : null; const stored = traceResults?.[safeId] && typeof traceResults[safeId] === "object" ? traceResults[safeId] : null; if (!stored) return null; return { ...stored, traceId: safeId, conversationId: safeConversationId(stored.conversationId ?? session.conversationId) || null, sessionId: safeSessionId(stored.sessionId ?? session.id) || null, threadId: safeOpaqueId(stored.threadId ?? session.threadId) || null, ownerUserId: session.ownerUserId ?? stored.ownerUserId ?? null, ownerRole: session.ownerRole ?? stored.ownerRole ?? null, status: stored.status ?? session.status ?? null, valuesRedacted: true }; } function codeAgentCompatProjectionPayload(payload = {}, context = {}) { const traceId = safeTraceId(payload.traceId ?? context.trace?.traceId) ?? null; const projection = context.projection && typeof context.projection === "object" ? context.projection : {}; return { ...payload, projection, projectionStatus: projection.projectionStatus ?? null, projectionHealth: projection.projectionHealth ?? null, lastProjectedSeq: projection.lastProjectedSeq ?? null, sourceRunId: projection.sourceRunId ?? null, sourceCommandId: projection.sourceCommandId ?? null, staleMs: projection.staleMs ?? null, blocker: projection.blocker ?? null, workbench: traceId ? { detailOnly: true, turnDetailUrl: `/v1/workbench/turns/${encodeURIComponent(traceId)}`, traceEventsDetailUrl: `/v1/workbench/traces/${encodeURIComponent(traceId)}/events` } : null, valuesRedacted: true, secretMaterialStored: false }; } function codeAgentResultPollTerminalReady(body = {}) { if (body?.terminal !== true) return false; return Boolean(messageAuthorityTextValue(body.finalResponse ?? body.reply ?? body.terminalEvidence?.finalResponse)); } async function traceProjectMismatchSnapshot(traceId, url, options, resourceKind) { const requestedProjectId = textValue(url.searchParams.get("projectId")); if (!requestedProjectId || !options.accessController?.getAgentSessionByTraceId) return null; const session = await getCodeAgentSessionByTraceId(traceId, options); if (!session) return null; if (session.ownerUserId && options.actor?.role !== "admin" && session.ownerUserId !== options.actor?.id) return forbiddenTurnSnapshot(traceId); const actualProjectId = textValue(session.projectId); if (!actualProjectId || actualProjectId === requestedProjectId) return null; return { statusCode: 404, body: { ok: false, status: "not_found", running: false, terminal: false, traceId, error: { code: "trace_project_mismatch", message: `${resourceKind === "turn" ? "Agent turn" : "Agent trace"} is not visible in the requested project` } } }; } async function getCodeAgentSessionByTraceId(traceId, options = {}) { const safeId = safeTraceId(traceId); if (!safeId || typeof options.accessController?.getAgentSessionByTraceId !== "function") return null; const cache = options.codeAgentTraceSessionCache; if (cache instanceof Map) { if (cache.has(safeId)) return cache.get(safeId); const session = await options.accessController.getAgentSessionByTraceId(safeId); cache.set(safeId, session ?? null); return session ?? null; } return await options.accessController.getAgentSessionByTraceId(safeId); } export function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError, refreshError, options }) { const resultObject = result && typeof result === "object" ? result : null; const snapshotObject = snapshot && typeof snapshot === "object" ? snapshot : null; const projectionTerminalAuthority = codeAgentResultUsesProjectionTerminalAuthority(resultObject); const lifecycle = codeAgentTurnLifecycleFields(traceId, resultObject ?? snapshotObject ?? {}); const events = Array.isArray(snapshotObject?.events) ? snapshotObject.events : Array.isArray(resultObject?.runnerTrace?.events) ? resultObject.runnerTrace.events : []; const lastEvent = events.at(-1) ?? null; const finalResponse = projectionTerminalAuthority ? snapshotObject?.finalResponse ?? snapshotObject?.terminalEvidence?.finalResponse ?? codeAgentFinalResponseEvidence(snapshotObject ?? {}, traceId) : resultObject?.finalResponse ?? snapshotObject?.finalResponse ?? snapshotObject?.terminalEvidence?.finalResponse ?? codeAgentFinalResponseEvidence(resultObject ?? snapshotObject ?? {}, traceId); const terminalStatus = projectionTerminalAuthority ? codeAgentProjectionTerminalStatus(snapshotObject) : codeAgentAuthoritativeTerminalStatus(resultObject, snapshotObject, traceId); const terminalSealBlocked = Boolean(terminalStatus && isTurnTerminalStatus(terminalStatus) && !codeAgentTerminalFinalText(finalResponse, projectionTerminalAuthority ? null : resultObject, snapshotObject)); const rawStatus = projectionTerminalAuthority ? normalizeTurnStatus( terminalStatus, codeAgentRunningStatus(snapshotObject?.status), codeAgentRunningStatus(snapshotObject?.traceStatus), codeAgentRunningStatus(snapshotObject?.runnerTrace?.status), codeAgentRunningStatus(resultObject?.status), "running" ) : normalizeTurnStatus( terminalStatus, resultObject?.agentRun?.commandState, resultObject?.agentRun?.status, resultObject?.agentRun?.runStatus, codeAgentRunningStatus(snapshotObject?.status), codeAgentRunningStatus(snapshotObject?.traceStatus), codeAgentRunningStatus(snapshotObject?.runnerTrace?.status), codeAgentRunningStatus(resultObject?.status), resultObject?.agentRun?.terminalStatus, snapshotObject?.terminalEvidence?.traceSummary?.terminalStatus ); const status = terminalSealBlocked ? "running" : rawStatus; const found = Boolean(resultObject || (snapshotObject && snapshotObject.status !== "missing") || snapshotObject?.persisted === true); const running = isTurnRunningStatus(status); const terminal = isTurnTerminalStatus(status); const runnerTrace = snapshotObject && snapshotObject.status !== "missing" ? snapshotObject : resultObject?.runnerTrace ?? null; const timingFields = codeAgentResultTimingFields(resultObject, snapshotObject, runnerTrace); return { ok: found, action: "code-agent.turn.status", contractVersion: "code-agent-turn-status-v1", status: found ? status ?? "unknown" : "unknown", running: found ? running : false, terminal: found ? terminal : false, traceId, ...lifecycle, conversationId: safeConversationId(resultObject?.conversationId ?? snapshotObject?.conversationId) || null, sessionId: safeSessionId(resultObject?.sessionId ?? resultObject?.session?.sessionId ?? snapshotObject?.sessionId) || null, threadId: safeOpaqueId(resultObject?.threadId ?? resultObject?.session?.threadId ?? snapshotObject?.threadId) || null, updatedAt: textValue(resultObject?.updatedAt ?? resultObject?.agentRun?.updatedAt ?? snapshotObject?.updatedAt ?? timingFields.lastEventAt ?? timingFields.startedAt) || null, ...timingFields, lastEventLabel: textValue(snapshotObject?.lastEventLabel ?? runnerTrace?.lastEventLabel ?? lastEvent?.label ?? lastEvent?.type) || null, waitingFor: terminalSealBlocked ? "final_response" : textValue(snapshotObject?.waitingFor ?? runnerTrace?.waitingFor) || null, terminalObserved: Boolean(terminalStatus), terminalObservedStatus: terminalStatus ?? null, terminalSealBlocked, resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`, traceUrl: `/v1/agent/traces/${encodeURIComponent(traceId)}`, turnUrl: `/v1/agent/turns/${encodeURIComponent(traceId)}`, runnerTrace: runnerTrace ? compactRunnerTraceForResult(runnerTrace, resultTraceEventLimit(options)) : null, agentRun: resultObject?.agentRun ?? snapshotObject?.agentRun ?? null, terminalEvidence: snapshotObject?.terminalEvidence ?? null, finalResponse, traceSummary: resultObject?.traceSummary ?? snapshotObject?.traceSummary ?? snapshotObject?.terminalEvidence?.traceSummary ?? null, retention: snapshotObject?.retention ?? null, eventCount: numberOrNull(snapshotObject?.eventCount ?? runnerTrace?.eventCount ?? events.length), error: resultPollError || refreshError ? codeAgentRefreshErrorPayload(resultPollError ?? refreshError, traceId, resultObject?.agentRun ?? snapshotObject?.agentRun, "turn_status_degraded") : resultObject?.error ?? snapshotObject?.error ?? null, valuesRedacted: true, secretMaterialStored: false }; } function codeAgentResultUsesProjectionTerminalAuthority(result = null) { const agentRun = result?.agentRun && typeof result.agentRun === "object" ? result.agentRun : null; if (!agentRun || agentRun.adapter !== "agentrun-v01") return false; return agentRun.durableDispatch === true || Boolean(textValue(agentRun.dispatchIntentId ?? agentRun.commandId)); } function codeAgentProjectionTerminalStatus(snapshotObject = null) { const status = normalizeTurnStatus( snapshotObject?.terminalEvidence?.status, snapshotObject?.terminalEvidence?.traceSummary?.terminalStatus, snapshotObject?.status ); return status && isTurnTerminalStatus(status) && codeAgentSnapshotHasTerminalAuthority(snapshotObject) ? status : null; } function codeAgentTerminalFinalText(finalResponse = null, resultObject = null, snapshotObject = null) { const candidates = [ finalResponse, resultObject?.finalResponse, snapshotObject?.finalResponse, snapshotObject?.terminalEvidence?.finalResponse, resultObject?.terminalEvidence?.finalResponse ]; for (const value of candidates) { const text = conversationText(value); if (text) return text; } return codeAgentFinalResponseText(resultObject ?? {}) || codeAgentFinalResponseText(snapshotObject ?? {}); } function codeAgentAuthoritativeTerminalStatus(resultObject, snapshotObject, traceId) { const sealedPayload = codeAgentPayloadHasSealedFinalResponse(resultObject ?? {}) ? resultObject : codeAgentPayloadHasSealedFinalResponse(snapshotObject ?? {}) ? snapshotObject : null; if (sealedPayload) { const sealedStatus = normalizeTurnStatus(sealedPayload?.finalResponse?.status, sealedPayload?.terminalEvidence?.finalResponse?.status, sealedPayload?.agentRun?.terminalStatus, sealedPayload?.agentRun?.status, sealedPayload?.status); return sealedStatus && isTurnTerminalStatus(sealedStatus) ? sealedStatus : "completed"; } const snapshotStatus = normalizeTurnStatus(snapshotObject?.terminalEvidence?.traceSummary?.terminalStatus, snapshotObject?.terminalEvidence?.agentRun?.terminalStatus, snapshotObject?.terminalEvidence?.status, snapshotObject?.status); if (snapshotStatus && isTurnTerminalStatus(snapshotStatus) && codeAgentSnapshotHasTerminalAuthority(snapshotObject)) return snapshotStatus; const agentRunStatus = normalizeTurnStatus(resultObject?.agentRun?.terminalStatus, resultObject?.agentRun?.commandState, resultObject?.agentRun?.status, resultObject?.agentRun?.runStatus); if (agentRunStatus && isTurnTerminalStatus(agentRunStatus)) return agentRunStatus; const resultStatus = normalizeTurnStatus(resultObject?.status); if (resultStatus && isTurnTerminalStatus(resultStatus) && codeAgentResultHasTerminalAuthority(resultObject, traceId)) return resultStatus; return null; } function codeAgentSnapshotHasTerminalAuthority(snapshot = null) { if (!snapshot || typeof snapshot !== "object") return false; if (snapshot.terminal === true || snapshot.sealed === true) return true; if (snapshot.terminalEvidence?.available === true || snapshot.terminalEvidence?.source) return true; const events = Array.isArray(snapshot.events) ? snapshot.events : []; return events.some((event) => event?.terminal === true); } function codeAgentResultHasTerminalAuthority(result = null, traceId = null) { if (!result || typeof result !== "object") return false; if (result.terminal === true || result.sealed === true) return true; if (result.error || result.blocker) return true; if (codeAgentPayloadHasSealedFinalResponse(result)) return true; if (agentRunTerminalTraceEvidence(result, traceId)) return true; return Boolean(textValue(result.finishedAt ?? result.completedAt ?? result.endedAt)); } function codeAgentRefreshErrorPayload(error, traceId, agentRun, fallbackCode) { return { code: error?.code ?? fallbackCode, layer: error?.layer ?? "agentrun", category: error?.category ?? (error?.code === "agentrun_timeout" ? "upstream-timeout" : "upstream-refresh-failed"), retryable: error?.retryable !== false, message: error?.message ?? "Code Agent turn status refresh degraded", traceId, runId: agentRun?.runId ?? error?.runId ?? null, commandId: agentRun?.commandId ?? error?.commandId ?? null, method: error?.method ?? null, route: error?.route ?? error?.path ?? null, timeoutMs: numberOrNull(error?.timeoutMs), timeoutStage: error?.timeoutStage ?? null, upstreamStatus: numberOrNull(error?.statusCode), managerHost: error?.managerHost ?? null, valuesPrinted: false }; } function isTurnRunningStatus(status) { return status === "running"; } function codeAgentRunningStatus(value) { const status = normalizeTurnStatus(value); return isTurnRunningStatus(status) ? status : null; } function isTurnTerminalStatus(status) { return CODE_AGENT_TERMINAL_STATUSES.has(String(status ?? "").trim().toLowerCase().replace(/_/gu, "-")); } export async function handleCodeAgentInspectHttp(request, response, url, options) { const query = { conversationId: safeConversationId(url.searchParams.get("conversationId")), sessionId: safeSessionId(url.searchParams.get("sessionId")), threadId: safeOpaqueId(url.searchParams.get("threadId")), traceId: safeTraceId(url.searchParams.get("traceId")) }; const sessionRegistry = options.sessionRegistry; const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const manager = options.codexStdioManager; const managerDescription = manager && typeof manager.describe === "function" ? manager.describe() : null; const managerRecentSessions = Array.isArray(managerDescription?.recentSessions) ? managerDescription.recentSessions : []; const directManagerSession = query.sessionId && manager && typeof manager.get === "function" ? manager.get(query.sessionId, { conversationId: query.conversationId }) : null; const matchedManagerSession = directManagerSession ?? managerRecentSessions.find((session) => sessionMatchesInspectQuery(session, query)) ?? null; const registryInspect = sessionRegistry && typeof sessionRegistry.inspect === "function" ? sessionRegistry.inspect(query) : { ok: false, status: "unavailable", traceIds: [], session: null, conversationFacts: null }; const resultEvidence = await codeAgentInspectResultEvidence(query.traceId, options); const session = matchedManagerSession ?? registryInspect.session ?? resultEvidence.session ?? null; const conversationFacts = registryInspect.conversationFacts ?? resultEvidence.conversationFacts ?? null; const requestedRunnerTrace = query.traceId ? traceStore.snapshot(query.traceId) : null; const requestedTraceFound = Boolean(requestedRunnerTrace && requestedRunnerTrace.status !== "missing"); const traceIds = uniqueStrings([ requestedTraceFound ? query.traceId : null, session?.currentTraceId, session?.lastTraceId, conversationFacts?.latestTraceId, resultEvidence.latestTraceId, ...(Array.isArray(conversationFacts?.traceIds) ? conversationFacts.traceIds : []), ...(Array.isArray(registryInspect.traceIds) ? registryInspect.traceIds : []) ]); const latestTraceId = traceIds[0] ?? null; const runnerTrace = latestTraceId ? latestTraceId === query.traceId && requestedRunnerTrace ? requestedRunnerTrace : traceStore.snapshot(latestTraceId) : null; const found = Boolean(registryInspect.ok || session || latestTraceId); sendJson(response, found ? 200 : 404, { ok: found, action: "code-agent.chat.inspect", status: found ? "found" : "not_found", query, session, conversationFacts, traceIds, latestTraceId, traceUrl: latestTraceId ? `/v1/agent/traces/${encodeURIComponent(latestTraceId)}` : null, resultUrl: latestTraceId ? `/v1/agent/chat/result/${encodeURIComponent(latestTraceId)}` : null, runnerTrace, valuesRedacted: true, secretMaterialStored: false }); } async function codeAgentInspectResultEvidence(traceId, options = {}) { if (!safeTraceId(traceId)) return { session: null, conversationFacts: null, latestTraceId: null }; const cached = options.codeAgentChatResults?.get?.(traceId) ?? null; const persisted = cached ? null : await loadPersistedAgentRunResult(traceId, options); const result = cached ?? persisted ?? null; if (!result || typeof result !== "object") return { session: null, conversationFacts: null, latestTraceId: null }; const agentRun = result.agentRun && typeof result.agentRun === "object" ? result.agentRun : null; const resultSession = result.session && typeof result.session === "object" ? result.session : null; const sessionReuse = result.sessionReuse && typeof result.sessionReuse === "object" ? result.sessionReuse : null; const providerTrace = result.providerTrace && typeof result.providerTrace === "object" ? result.providerTrace : null; const sessionId = safeSessionId(result.sessionId ?? resultSession?.sessionId ?? sessionReuse?.sessionId ?? agentRun?.sessionId) || null; const conversationId = safeConversationId(result.conversationId ?? resultSession?.conversationId ?? sessionReuse?.conversationId ?? agentRun?.conversationId) || null; const threadId = safeOpaqueId(result.threadId ?? resultSession?.threadId ?? sessionReuse?.threadId ?? providerTrace?.threadId ?? agentRun?.threadId) || null; const status = textValue(resultSession?.status ?? result.status ?? agentRun?.status) || null; const updatedAt = textValue(result.updatedAt ?? agentRun?.updatedAt ?? resultSession?.updatedAt) || null; const session = sessionId || conversationId || threadId ? { sessionId, conversationId, threadId, status, lastTraceId: traceId, source: "code-agent-result", agentRun: agentRun ? agentRunSessionEvidence(result).agentRun : null, valuesRedacted: true, secretMaterialStored: false } : null; const conversationFacts = conversationId ? { conversationId, sessionId, threadId, latestTraceId: traceId, traceIds: [traceId], turnCount: 1, source: "code-agent-result", latestStatus: status, updatedAt, valuesRedacted: true, secretMaterialStored: false } : null; return { session, conversationFacts, latestTraceId: traceId }; } function sessionMatchesInspectQuery(session, query) { if (!session || typeof session !== "object") return false; if (query.sessionId && session.sessionId === query.sessionId) return true; if (query.threadId && session.threadId === query.threadId) return true; if (query.conversationId && session.conversationId === query.conversationId) return true; if (query.traceId && (session.currentTraceId === query.traceId || session.lastTraceId === query.traceId)) return true; return false; } export async function handleCodeAgentCancelHttp(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 }) }); return; } if (!params || typeof params !== "object" || Array.isArray(params)) { sendJson(response, 400, { ...createCodeAgentErrorPayload({ code: "invalid_params", message: "Code Agent cancel body must be a JSON object", traceId: getHeader(request, "x-trace-id") || "trc_unassigned", layer: "api", retryable: true }) }); return; } const traceId = safeTraceId(getHeader(request, "x-trace-id") || params.traceId); const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const snapshot = traceId ? traceStore.snapshot(traceId) : null; const durableResult = traceId ? await loadPersistedAgentRunResult(traceId, options) : null; const currentResult = durableResult ?? (traceId ? options.codeAgentChatResults?.get(traceId) : null); if (currentResult?.agentRun?.commandId) { const mismatch = codeAgentCancelScopeMismatch(params, currentResult); if (mismatch) { traceStore.append(traceId, { type: "cancel", status: "blocked", label: "agentrun:cancel:scope_mismatch", errorCode: "cancel_scope_mismatch", message: `Cancel request ${mismatch.field} does not match the trace owner; AgentRun cancel was not forwarded.`, requested: mismatch.requested, expected: mismatch.expected, waitingFor: "cancel-scope", valuesPrinted: false }); sendJson(response, 409, cancelBlockedPayload({ code: "cancel_scope_mismatch", message: `取消请求的 ${mismatch.field} 与 trace 归属不一致,已拒绝转发 AgentRun cancel,避免误取消其他 session。`, traceId, conversationId: safeConversationId(params.conversationId) || safeConversationId(currentResult.conversationId) || null, sessionId: safeSessionId(params.sessionId) || safeSessionId(currentResult.sessionId) || null, runnerTrace: traceStore.snapshot(traceId), status: "blocked" })); return; } await recordCodeAgentSessionInputFact({ params: { ...params, sessionId: currentResult.sessionId ?? currentResult.session?.sessionId ?? params.sessionId, messageId: codeAgentTurnLifecycleFields(traceId, currentResult).userMessageId, ownerUserId: options.actor?.id, ownerRole: options.actor?.role }, options, traceId, delivery: "cancel", status: "promoted", commandId: currentResult.agentRun.commandId }); const payload = await cancelAgentRunChatTurn({ traceId, currentResult, options, traceStore }); if (payload) { sendJson(response, 200, payload); return; } } const sessionId = safeSessionId(params.sessionId) || safeSessionId(snapshot?.sessionId); const conversationId = safeConversationId(params.conversationId); const manager = options.codexStdioManager; if (!traceId) { sendJson(response, 400, cancelBlockedPayload({ code: "cancel_trace_missing", message: "traceId is required to cancel the current Code Agent request.", traceId: "trc_unassigned", conversationId, sessionId })); return; } if (!manager || typeof manager.get !== "function" || typeof manager.cancel !== "function") { traceStore.append(traceId, { type: "cancel", status: "unsupported", label: "cancel:unsupported", errorCode: "cancel_unsupported", message: "Codex stdio cancel/interrupt backend is not available on this runtime.", waitingFor: "session-control" }); const runnerTrace = traceStore.snapshot(traceId); sendJson(response, 501, cancelBlockedPayload({ code: "cancel_unsupported", message: "当前后端不支持 Code Agent interrupt/cancel;本次按 unsupported/degraded 返回,不会静默假装已中断。", traceId, conversationId, sessionId, runnerTrace, status: "degraded", unsupported: true, degraded: true })); return; } if (!sessionId) { traceStore.append(traceId, { type: "cancel", status: "blocked", label: "cancel:not_cancelable", errorCode: "cancel_session_missing", message: "Cancel request did not include a bound Codex stdio sessionId.", waitingFor: "session-binding" }); sendJson(response, 409, cancelBlockedPayload({ code: "cancel_session_missing", message: "当前请求尚未暴露可取消的 Codex stdio sessionId;页面不能只隐藏 UI,已保留输入和 trace。", traceId, conversationId, sessionId: null, runnerTrace: traceStore.snapshot(traceId) })); return; } const currentSession = manager.get(sessionId, { conversationId }) ?? null; if (!currentSession || !["busy", "creating"].includes(currentSession.status)) { traceStore.append(traceId, { type: "cancel", status: "blocked", label: "cancel:not_in_flight", errorCode: "cancel_not_in_flight", message: `Session ${sessionId} is not an in-flight Codex stdio request.`, sessionId, sessionStatus: currentSession?.status ?? "missing" }); sendJson(response, 409, cancelBlockedPayload({ code: currentSession ? "cancel_not_in_flight" : "cancel_session_not_found", message: currentSession ? `当前 session 状态为 ${currentSession.status},没有可取消的 in-flight Codex stdio 请求。` : `没有找到 sessionId=${sessionId} 的 Codex stdio session。`, traceId, conversationId, sessionId, session: currentSession, runnerTrace: traceStore.snapshot(traceId) })); return; } const canceledSession = manager.cancel(sessionId, { traceId, conversationId, reason: "user_cancel" }); traceStore.append(traceId, { type: "cancel", status: "canceled", label: "cancel:canceled", message: "User canceled the current Codex stdio request.", sessionId, sessionStatus: canceledSession?.status ?? "canceled", sessionLifecycleStatus: codeAgentSessionLifecycleSummary({ session: canceledSession, status: "canceled" }).status, waitingFor: "user-retry", terminal: true }); const runnerTraceSnapshot = traceStore.snapshot(traceId); const sessionSummary = codeAgentSessionLifecycleSummary({ session: canceledSession, runnerTrace: runnerTraceSnapshot, status: "canceled" }); const runnerTrace = { ...runnerTraceSnapshot, sessionLifecycleStatus: sessionSummary.status }; const payload = { accepted: true, canceled: true, status: "canceled", conversationId: conversationId ?? canceledSession?.conversationId ?? null, sessionId, traceId, session: canceledSession, sessionLifecycleStatus: sessionSummary.status, sessionLifecycle: sessionSummary, sessionSummary, runnerTrace, lastTraceEvent: runnerTrace.lastEvent, retryable: true, error: { code: "codex_stdio_canceled", layer: "session", category: "canceled", retryable: true, message: "user canceled current Code Agent request", userMessage: "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。", traceId, route: "/v1/agent/chat/cancel", toolName: "codex-stdio.cancel" }, userMessage: "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。", updatedAt: new Date().toISOString() }; await recordCodeAgentSessionOwner({ payload, params: { ...params, traceId, ownerUserId: options.actor?.id, ownerRole: options.actor?.role }, options, status: "canceled" }); recordCodeAgentConversationFact(payload, options); options.codeAgentChatResults?.set(traceId, annotateOwner(payload, { ownerUserId: options.actor?.id, ownerRole: options.actor?.role })); sendJson(response, 200, payload); } export { codeAgentCompatProjectionPayload, codeAgentRequestScopedOptions, measureCodeAgentHttpPhase, readCodeAgentCompatProjection, traceProjectMismatchSnapshot };