fix: keep completed actions visible after settled finalization (#119726)

* fix(agents): preserve settled finalization evidence

* fix(agents): retain cron denial after finalization
This commit is contained in:
Peter Steinberger
2026-08-05 14:04:05 -07:00
committed by GitHub
parent 2db3431f4a
commit e9e6ef3edb
2 changed files with 77 additions and 19 deletions
@@ -1088,27 +1088,56 @@ describe("runEmbeddedAgent incomplete-turn safety", () => {
});
it("continues once after settled side-effecting tools finish without a final answer", async () => {
const acceptedSessionSpawns = [
{ runId: "child-run", childSessionKey: "agent:main:subagent:child" },
];
const toolUseAssistant = {
role: "assistant",
stopReason: "toolUse",
provider: "openai",
model: "gpt-5.5",
content: [{ type: "toolCall", id: "tool_1", name: "write", arguments: { path: "note.txt" } }],
content: [
{ type: "toolCall", id: "tool_write", name: "write", arguments: { path: "note.txt" } },
{ type: "toolCall", id: "tool_cron", name: "cron", arguments: { action: "add" } },
{
type: "toolCall",
id: "tool_spawn",
name: "sessions_spawn",
arguments: { task: "follow up" },
},
],
} as unknown as NonNullable<EmbeddedRunAttemptResult["lastAssistant"]>;
const settledToolResults = [
toolUseAssistant,
{ role: "toolResult", toolCallId: "tool_1", toolName: "write", isError: false },
{ role: "toolResult", toolCallId: "tool_write", toolName: "write", isError: false },
{ role: "toolResult", toolCallId: "tool_cron", toolName: "cron", isError: false },
{
role: "toolResult",
toolCallId: "tool_spawn",
toolName: "sessions_spawn",
isError: false,
},
] as unknown as EmbeddedRunAttemptResult["messagesSnapshot"];
mockedClassifyFailoverReason.mockReturnValue(null);
mockedRunEmbeddedAttempt.mockImplementationOnce(async (attemptParams) => {
markUserMessagePersisted(attemptParams);
return makeAttemptResult({
assistantTexts: [],
toolMetas: [{ toolName: "write", meta: "path=note.txt" }],
itemLifecycle: { startedCount: 1, completedCount: 1, activeCount: 0 },
latestMcpAppChannelView: { viewId: "view-after-tools" },
toolMetas: [
{ toolName: "write", meta: "path=note.txt" },
{ toolName: "cron" },
{ toolName: "sessions_spawn" },
],
acceptedSessionSpawns,
successfulCronAdds: 1,
itemLifecycle: { startedCount: 3, completedCount: 3, activeCount: 0 },
messagesSnapshot: settledToolResults,
lastAssistant: toolUseAssistant,
currentAttemptAssistant: toolUseAssistant,
codeModeEngaged: true,
assistantTurns: 1,
bridgeCalls: { search: 1, describe: 2, call: 3 },
});
});
const finalAssistant = {
@@ -1139,6 +1168,19 @@ describe("runEmbeddedAgent incomplete-turn safety", () => {
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
expect(result.payloads?.[0]?.text).toBe("Write completed. Here is the final answer.");
expect(result.latestMcpAppChannelView).toEqual({ viewId: "view-after-tools" });
expect(result.successfulCronAdds).toBe(1);
expect(result.acceptedSessionSpawns).toEqual(acceptedSessionSpawns);
expect(result.meta.toolSummary).toEqual({
calls: 3,
tools: ["write", "cron", "sessions_spawn"],
failures: 0,
});
expect(result.meta.agentMeta).toMatchObject({
codeModeEngaged: true,
assistantTurns: 2,
bridgeCalls: { search: 1, describe: 2, call: 3 },
});
const secondCall = runAttemptCall(1);
expect(secondCall.prompt).toBe(SETTLED_TOOL_TERMINAL_CONTINUATION_INSTRUCTION);
expect(secondCall.disableTools).toBe(true);
@@ -1205,7 +1247,11 @@ describe("runEmbeddedAgent incomplete-turn safety", () => {
] as unknown as EmbeddedRunAttemptResult["messagesSnapshot"],
lastAssistant: toolUseAssistant,
currentAttemptAssistant: toolUseAssistant,
lastToolError: { toolName: "exec", error: "post-processing error" },
lastToolError: {
toolName: "exec",
error: "post-processing error",
errorCode: "SYSTEM_RUN_DENIED",
},
});
});
const finalAssistant = {
@@ -1244,6 +1290,18 @@ describe("runEmbeddedAgent incomplete-turn safety", () => {
expect(finalizationCall.prompt).toContain(
"If any tool failed, state that failure plainly and do not claim it succeeded.",
);
expect(result.meta.failureSignal).toEqual(
runPolicy.trigger === "cron"
? {
kind: "execution_denied",
source: "tool",
toolName: "exec",
code: "SYSTEM_RUN_DENIED",
message: "post-processing error",
fatalForCron: true,
}
: undefined,
);
});
it("preserves a structured visible failed-tool payload without finalizing (#118274)", async () => {
@@ -16,7 +16,10 @@ import {
type EmbeddedRunTerminalState,
} from "./terminal-outcome.js";
import { prepareEmbeddedRunTerminal } from "./terminal-preparation.js";
import { resolveSettledTurnFinalizationRequest } from "./terminal-resolution.js";
import {
copyAttemptDeliveryState,
resolveSettledTurnFinalizationRequest,
} from "./terminal-resolution.js";
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
type TerminalPreparationInput = Parameters<typeof prepareEmbeddedRunTerminal>[0];
@@ -88,6 +91,7 @@ export async function prepareTerminalWithSettledTurnFinalization(input: {
finalizationSucceeded: false,
};
}
const settledFailureSignal = prepared.failureSignal;
const runParams = input.terminalBase.runParams;
const errorContext = input.terminalBase.activeErrorContext;
@@ -114,7 +118,7 @@ export async function prepareTerminalWithSettledTurnFinalization(input: {
}),
signalOwnedInterruption: false,
};
prepared = prepareEmbeddedRunTerminal({
const finalizedPrepared = prepareEmbeddedRunTerminal({
...input.terminalBase,
attempt,
currentAttemptCompletedAssistant: attempt.currentAttemptCompletedAssistant,
@@ -123,6 +127,8 @@ export async function prepareTerminalWithSettledTurnFinalization(input: {
lastRunPromptUsage,
terminalState,
});
// A failure-honest final answer cannot turn a settled cron denial into success.
prepared = { ...finalizedPrepared, failureSignal: settledFailureSignal };
return {
attempt,
attemptAssistant: attempt.currentAttemptAssistant,
@@ -188,8 +194,8 @@ function buildSettledTurnFinalizationAttemptResult(input: {
}): EmbeddedRunAttemptResult {
const { result, settledAttempt } = input;
const text = resolveSettledTurnFinalizationText(result);
// Finalization bypasses ordinary attempt normalization. Rebuild only the
// terminal projection so settled side effects and retry state cannot leak in.
// Finalization replaces terminal ownership, not facts from already-settled tools.
// Keep those facts while replay, abort, and lifecycle state remain finalizer-local.
return {
terminal: { kind: "ok" },
sessionIdUsed: settledAttempt.sessionIdUsed,
@@ -199,6 +205,7 @@ function buildSettledTurnFinalizationAttemptResult(input: {
runtimeArtifact: settledAttempt.runtimeArtifact,
systemPromptReport: settledAttempt.systemPromptReport,
finalPromptText: input.prompt,
...copyAttemptDeliveryState(settledAttempt),
messagesSnapshot: [...settledAttempt.messagesSnapshot, result.assistant],
assistantTexts: [text],
assistantTranscriptOwned: result.assistantTranscriptOwned,
@@ -207,19 +214,12 @@ function buildSettledTurnFinalizationAttemptResult(input: {
lastAssistant: result.assistant,
currentAttemptAssistant: result.assistant,
currentAttemptCompletedAssistant: result.assistant,
toolMetas: [],
acceptedSessionSpawns: [],
didSendViaMessagingTool: false,
didDeliverSourceReplyViaMessageTool: false,
didSendDeterministicApprovalPrompt: false,
messagingToolSentTexts: [],
messagingToolSentMediaUrls: [],
messagingToolSentTargets: [],
messagingToolSourceReplyPayloads: [],
toolMetas: settledAttempt.toolMetas,
hasToolMediaBlockReply: false,
successfulCronAdds: 0,
cloudCodeAssistFormatError: false,
attemptUsage: result.usage,
codeModeEngaged: settledAttempt.codeModeEngaged,
assistantTurns: 1,
replayMetadata: { hadPotentialSideEffects: false, replaySafe: true },
currentAttemptReplayMetadata: { hadPotentialSideEffects: false, replaySafe: true },
itemLifecycle: { startedCount: 0, completedCount: 0, activeCount: 0 },