Files
openclaw/src/agents/tool-error-summary.ts
Jason 95ebb3dc1e fix(tui): surface safe tool-validation abort diagnostics (#91002)
* fix(tui): show last tool-error summary on aborted runs

When a run ends while looping on tool-call validation errors the TUI showed
only "run aborted". Carry the last tool failure on the terminal lifecycle
metadata and render a sanitized one-line summary on the abort line, e.g.
"run aborted: edit tool validation failed: edits: must have required properties edits".
Argument dumps are stripped; with no summary it stays "run aborted".

Refs #90982

* fix(tui): secure abort validation diagnostics

* docs(changelog): move TUI fix to unreleased

* docs(changelog): aggregate TUI fixes

* fix(tui): scope abort validation diagnostics

* docs(changelog): restore unreleased spacing

* fix(tui): scope abort validation diagnostics

* fix(tui): scope abort validation diagnostics

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-05 08:30:20 -07:00

74 lines
2.3 KiB
TypeScript

/**
* Compact tool error summary types.
*
* Stores failure metadata used by transcripts, retry behavior, and mutation recovery logic.
*/
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import type { FileTarget } from "./tool-mutation.js";
export type ToolErrorSummary = {
toolName: string;
meta?: string;
errorCode?: string;
error?: string;
validationErrorSummary?: string;
timedOut?: boolean;
middlewareError?: boolean;
mutatingAction?: boolean;
actionFingerprint?: string;
fileTarget?: FileTarget;
};
const EXEC_LIKE_TOOL_NAMES = new Set(["exec", "bash"]);
/** Detects shell-execution tools that share retry and mutation semantics. */
export function isExecLikeToolName(toolName: string): boolean {
return EXEC_LIKE_TOOL_NAMES.has(normalizeOptionalLowercaseString(toolName) ?? "");
}
const MAX_ABORT_SUMMARY_LENGTH = 160;
function hasUnsafeSummaryCharacter(value: string): boolean {
for (const char of value) {
const code = char.charCodeAt(0);
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
return true;
}
}
return false;
}
/** Accepts only the compact single-line diagnostic produced below. */
export function readToolValidationErrorSummary(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const summary = value.trim();
if (!summary || summary.length > MAX_ABORT_SUMMARY_LENGTH || hasUnsafeSummaryCharacter(summary)) {
return undefined;
}
return summary;
}
/** Builds a static diagnostic from typed pre-execution validation provenance. */
export function createToolValidationErrorSummary(toolName: string): string | undefined {
if (hasUnsafeSummaryCharacter(toolName)) {
return undefined;
}
const normalizedToolName = toolName.replace(/\s+/g, " ").trim();
if (!normalizedToolName) {
return undefined;
}
return readToolValidationErrorSummary(
`${normalizedToolName} tool validation failed: invalid arguments`,
);
}
/**
* Returns only a boundary-prepared validation summary. Raw validator messages
* stay private because paths and custom messages can contain model input.
*/
export function summarizeToolValidationError(summary: ToolErrorSummary): string | undefined {
return readToolValidationErrorSummary(summary.validationErrorSummary);
}