fix(workbench): block steer after unsealed terminal errors (#2050)
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user