mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
2db3431f4a
commit
e9e6ef3edb
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user