fix: preserve bootstrap paths and expose failed mutations (#16131)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 385dcbd8a9
Co-authored-by: Swader <1430603+Swader@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Bruno Škvorc
2026-02-14 23:01:16 +01:00
committed by GitHub
parent bc299ae17e
commit dbdcbe03e7
14 changed files with 718 additions and 34 deletions

View File

@@ -18,6 +18,7 @@ import {
extractAssistantThinking,
formatReasoningMessage,
} from "../../pi-embedded-utils.js";
import { isLikelyMutatingToolName } from "../../tool-mutation.js";
type ToolMetaEntry = { toolName: string; meta?: string };
@@ -25,7 +26,13 @@ export function buildEmbeddedRunPayloads(params: {
assistantTexts: string[];
toolMetas: ToolMetaEntry[];
lastAssistant: AssistantMessage | undefined;
lastToolError?: { toolName: string; meta?: string; error?: string };
lastToolError?: {
toolName: string;
meta?: string;
error?: string;
mutatingAction?: boolean;
actionFingerprint?: string;
};
config?: OpenClawConfig;
sessionKey: string;
provider?: string;
@@ -223,22 +230,37 @@ export function buildEmbeddedRunPayloads(params: {
errorLower.includes("must have") ||
errorLower.includes("needs") ||
errorLower.includes("requires");
const isMutatingToolError =
params.lastToolError.mutatingAction ??
isLikelyMutatingToolName(params.lastToolError.toolName);
const shouldShowToolError = isMutatingToolError || (!hasUserFacingReply && !isRecoverableError);
// Show tool errors only when:
// 1. There's no user-facing reply AND the error is not recoverable
// Recoverable errors (validation, missing params) are already in the model's context
// and shouldn't be surfaced to users since the model should retry.
if (!hasUserFacingReply && !isRecoverableError) {
// Always surface mutating tool failures so we do not silently confirm actions that did not happen.
// Otherwise, keep the previous behavior and only surface non-recoverable failures when no reply exists.
if (shouldShowToolError) {
const toolSummary = formatToolAggregate(
params.lastToolError.toolName,
params.lastToolError.meta ? [params.lastToolError.meta] : undefined,
{ markdown: useMarkdown },
);
const errorSuffix = params.lastToolError.error ? `: ${params.lastToolError.error}` : "";
replyItems.push({
text: `⚠️ ${toolSummary} failed${errorSuffix}`,
isError: true,
});
const warningText = `⚠️ ${toolSummary} failed${errorSuffix}`;
const normalizedWarning = normalizeTextForComparison(warningText);
const duplicateWarning = normalizedWarning
? replyItems.some((item) => {
if (!item.text) {
return false;
}
const normalizedExisting = normalizeTextForComparison(item.text);
return normalizedExisting.length > 0 && normalizedExisting === normalizedWarning;
})
: false;
if (!duplicateWarning) {
replyItems.push({
text: warningText,
isError: true,
});
}
}
}