fix(workbench): block steer after unsealed terminal errors (#2050)

This commit is contained in:
Lyon
2026-06-24 16:44:40 +08:00
committed by GitHub
parent d1bb34c360
commit 5ff705c23c
2 changed files with 120 additions and 4 deletions
+118 -2
View File
@@ -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;