From 5ff705c23c3240bdd73501b81886b6f0da3b6909 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:44:40 +0800 Subject: [PATCH] fix(workbench): block steer after unsealed terminal errors (#2050) --- internal/cloud/code-agent-agentrun-adapter.ts | 4 +- internal/cloud/server-code-agent-http.ts | 120 +++++++++++++++++- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 49d2193b..f093a32c 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -836,8 +836,8 @@ function agentRunTerminalStatusFromEvents(events = []) { event?.type === "terminal_status" ? payload.terminalStatus ?? event?.terminalStatus ?? payload.status ?? event?.status : null, phase === "command-terminal" ? payload.terminalStatus ?? event?.terminalStatus ?? payload.status ?? event?.status : null, phase === "turn:completed" || phase === "turn/steer:completed" ? "completed" : null, - event?.terminal === true || event?.final === true || event?.replyAuthority === true ? payload.terminalStatus ?? event?.terminalStatus ?? payload.status ?? event?.status ?? "completed" : null, - payload.terminal === true || payload.final === true || payload.replyAuthority === true ? payload.terminalStatus ?? event?.terminalStatus ?? payload.status ?? event?.status ?? "completed" : null + event?.terminal === true ? payload.terminalStatus ?? event?.terminalStatus ?? payload.status ?? event?.status ?? "completed" : null, + payload.terminal === true ? payload.terminalStatus ?? event?.terminalStatus ?? payload.status ?? event?.status ?? "completed" : null ]; for (const value of candidates) { const status = normalizeAgentRunStatus(value); diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index 546d020f..43083d84 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -1804,7 +1804,7 @@ function codeAgentObservedTerminalStatus(result, runnerTrace) { event?.type === "terminal_status" ? payload.terminalStatus : null, payload.phase === "command-terminal" ? payload.terminalStatus : null, payload.phase === "turn:completed" || payload.phase === "turn/steer:completed" ? "completed" : null, - event?.terminal === true || event?.final === true || event?.replyAuthority === true ? payload.terminalStatus ?? payload.status ?? event.status ?? "completed" : null, + event?.terminal === true ? payload.terminalStatus ?? payload.status ?? event.status ?? "completed" : null, payload.terminal === true ? payload.terminalStatus ?? payload.status ?? "completed" : null ]) { const terminalStatus = normalizeCodeAgentTerminalStatus(value); @@ -2386,7 +2386,7 @@ function codeAgentSnapshotHasTerminalAuthority(snapshot = null) { 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 || event?.final === true || event?.replyAuthority === true); + return events.some((event) => event?.terminal === true); } function codeAgentResultHasTerminalAuthority(result = null, traceId = null) { @@ -2963,6 +2963,72 @@ export async function handleCodeAgentSteerHttp(request, response, options) { }); return; } + const unsealedTerminalError = traceSnapshotTerminalFailureDiagnostic(traceStore.snapshot(traceId)); + if (unsealedTerminalError) { + const terminalSync = await syncAgentRunTerminalBeforeSteer({ traceId, currentResult, params, options, traceStore }); + const syncedResult = terminalSync?.payload ?? currentResult; + if (terminalSync?.terminal === true) { + sendJson(response, 409, { + ok: false, + accepted: false, + status: "blocked", + traceId, + route: "/v1/agent/chat/steer", + sessionId: safeSessionId(syncedResult.sessionId ?? params.sessionId) || null, + threadId: safeOpaqueId(syncedResult.threadId ?? params.threadId) || null, + agentRun: syncedResult.agentRun ?? currentResult.agentRun, + runnerTrace: terminalSync.runnerTrace ?? traceStore.snapshot(traceId), + terminalDiagnostic: unsealedTerminalError, + 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; + } + traceStore.append(traceId, { + type: "backend", + status: "blocked", + label: "agentrun:steer:blocked-unsealed-terminal-error", + errorCode: terminalSync?.error?.code ?? unsealedTerminalError.failureKind ?? "steer_trace_unsealed_terminal_error", + message: terminalSync?.error?.message ?? "Trace has an unsealed terminal error diagnostic; HWLAB refused to steer a new prompt into the old trace.", + waitingFor: "terminal-projection-reconciliation", + terminalDiagnostic: unsealedTerminalError, + terminal: false, + valuesPrinted: false + }); + sendJson(response, terminalSync?.error ? 503 : 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), + terminalDiagnostic: unsealedTerminalError, + error: { + code: terminalSync?.error?.code ?? "steer_trace_unsealed_terminal_error", + layer: terminalSync?.error?.layer ?? "api", + category: "not_in_flight", + retryable: false, + message: terminalSync?.error?.message ?? "Trace has an unsealed terminal error diagnostic; start a new turn instead of steering into the old trace.", + traceId, + route: "/v1/agent/chat/steer", + toolName: "agentrun.command.steer" + }, + valuesPrinted: false + }); + return; + } if (isTraceCommandTerminalStatus(currentResult.status)) { sendJson(response, 409, { ok: false, @@ -3040,6 +3106,56 @@ export async function handleCodeAgentSteerHttp(request, response, options) { } } +async function syncAgentRunTerminalBeforeSteer({ traceId, currentResult = null, params = {}, options = {}, traceStore = defaultCodeAgentTraceStore } = {}) { + try { + const synced = await syncAgentRunChatResult({ + traceId, + currentResult, + options: { ...options, deferAgentRunResultSync: false }, + traceStore, + forceResultSync: true, + refreshEvents: true + }); + const payload = codeAgentPayloadWithObservedTerminalStatus(synced?.result ?? null, synced?.runnerTrace ?? traceStore.snapshot(traceId)) ?? synced?.result ?? currentResult; + if (isTraceCommandTerminalStatus(payload?.status)) { + await recordCodeAgentTerminalTurnStatusEffects({ + payload, + params: { ...params, traceId, ownerUserId: options.actor?.id ?? params.ownerUserId, ownerRole: options.actor?.role ?? params.ownerRole }, + options, + preserveLastTraceId: true + }); + return { terminal: true, payload, runnerTrace: synced?.runnerTrace ?? payload?.runnerTrace ?? traceStore.snapshot(traceId), valuesPrinted: false }; + } + return { terminal: false, payload, runnerTrace: synced?.runnerTrace ?? payload?.runnerTrace ?? traceStore.snapshot(traceId), valuesPrinted: false }; + } catch (error) { + return { terminal: false, error, runnerTrace: traceStore.snapshot(traceId), valuesPrinted: false }; + } +} + +function traceSnapshotTerminalFailureDiagnostic(snapshot = null) { + const events = Array.isArray(snapshot?.events) ? snapshot.events : []; + for (const event of [...events].reverse()) { + const payload = event?.payload && typeof event.payload === "object" ? event.payload : {}; + const label = textValue(event?.label ?? event?.type); + const status = normalizeTurnStatus(event?.status ?? payload.status); + const failureKind = textValue(event?.failureKind ?? event?.errorCode ?? payload.failureKind ?? payload.errorCode); + const retrying = event?.willRetry === true || payload.willRetry === true; + const retryExhausted = event?.retryExhausted === true || payload.retryExhausted === true; + const explicitlyNonRetryable = event?.retryable === false || payload.retryable === false; + const hardFailure = retryExhausted || explicitlyNonRetryable || failureKind === "backend-timeout" || /^agentrun:error:/u.test(label); + if (retrying || !hardFailure || (status !== "failed" && !failureKind)) continue; + return { + label: label || null, + status: status || null, + failureKind: failureKind || null, + retryExhausted, + message: textValue(event?.message ?? payload.message) || null, + valuesPrinted: false + }; + } + return null; +} + async function recordCodeAgentSessionOwner({ payload = {}, params = {}, options = {}, status = "active", preserveLastTraceId = false } = {}) { const ownerUserId = options.actor?.id ?? params.ownerUserId; if (!ownerUserId || !options.accessController?.recordAgentSessionOwner) return null;