Files
openclaw/packages/normalization-core/src/error-coercion.ts
Peter Steinberger 2aa4ff0825 refactor: consolidate duplicate helper exports (#106016)
* refactor(normalization): consolidate string helpers

* refactor(normalization): consolidate agent ids

* refactor(paths): consolidate user path resolution

* refactor(gateway): preserve transcript helper facade

* fix(normalization): align helper build aliases

* fix(memory): keep helper import line neutral

* chore(plugin-sdk): align consolidated helper surface

* refactor(normalization): reuse lowercase string helper

* refactor(normalization): reuse record guard

* refactor(normalization): share surrogate boundary helper

* refactor(agents): share token estimate constant

* refactor(memory): distinguish standalone helper contracts

* refactor(normalization): share error cause formatter

* refactor(config): distinguish eligibility predicates

* refactor(plugins): share registry state symbol

* refactor(cli): reuse outbound dependency adapter

* refactor(cron): distinguish positive duration parser

* fix(gateway): keep transcript helper private

* fix(paths): preserve public helper status

* refactor(channels): reuse registry normalizer

* refactor(agents): reuse agent core runtime facade

* refactor(auth): reuse locked profile upsert

* refactor(records): distinguish trap-safe guard

* refactor(migrations): distinguish sync directory helper

* test(plugins): drop stale duration alias expectations

* fix(channels): remove stale registry import

* chore(plugin-sdk): refresh helper API baseline

* fix(memory): keep renamed helpers private

* fix(plugins): keep registry state key private

* fix(plugin-sdk): tighten wildcard surface budget

* chore(plugin-sdk): refresh rebased helper API baseline
2026-07-13 03:22:00 -07:00

143 lines
4.2 KiB
TypeScript

export type FormatErrorMessageOptions = {
redact: (text: string) => string;
};
function readProperty(value: object, key: "cause" | "code" | "status"): unknown {
try {
return (value as Record<string, unknown>)[key];
} catch {
return undefined;
}
}
function formatStatusAndCode(value: unknown): string | undefined {
if ((typeof value !== "object" || value === null) && typeof value !== "function") {
return undefined;
}
try {
if (Object.keys(value).some((key) => key !== "status" && key !== "code")) {
return undefined;
}
} catch {
// Proxy enumeration can fail; retain the safe status/code fallback below.
}
const statusValue = readProperty(value, "status");
const codeValue = readProperty(value, "code");
if (statusValue === undefined && codeValue === undefined) {
return undefined;
}
const statusText =
typeof statusValue === "string" || typeof statusValue === "number"
? String(statusValue)
: "unknown";
const codeText =
typeof codeValue === "string" || typeof codeValue === "number" ? String(codeValue) : "unknown";
return `status=${statusText} code=${codeText}`;
}
function stringifyUnknown(value: unknown): string {
if (value === null) {
return "null";
}
if (value === undefined) {
return "undefined";
}
if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ||
typeof value === "bigint" ||
typeof value === "symbol"
) {
return String(value);
}
try {
const json = JSON.stringify(value);
if (json !== undefined) {
return json;
}
} catch {
// Fall through to the stable object tag below.
}
try {
return Object.prototype.toString.call(value);
} catch {
return "Unknown error";
}
}
/** Formats unknown errors with cause details, structured codes, and secret redaction. */
export function formatErrorMessage(value: unknown, options: FormatErrorMessageOptions): string {
let formatted: string;
if (value instanceof Error) {
formatted = value.message || value.name || "Error";
let cause = readProperty(value, "cause");
const seen = new Set<unknown>([value]);
const seenMessages = new Set<string>([formatted]);
const appendCauseMessage = (message: string | undefined): void => {
if (!message || seenMessages.has(message)) {
return;
}
formatted += ` | ${message}`;
seenMessages.add(message);
};
while (cause && !seen.has(cause)) {
seen.add(cause);
if (cause instanceof Error) {
appendCauseMessage(cause.message);
const code = readProperty(cause, "code");
if (typeof code === "string" || typeof code === "number") {
appendCauseMessage(String(code));
}
cause = readProperty(cause, "cause");
} else if (typeof cause === "string") {
appendCauseMessage(cause);
break;
} else {
appendCauseMessage(formatStatusAndCode(cause));
break;
}
}
} else {
formatted = formatStatusAndCode(value) ?? stringifyUnknown(value);
}
return options.redact(formatted);
}
/**
* Normalizes an unknown thrown value into an Error. Non-Error objects become
* the `cause` and have their enumerable fields copied so structured details
* (codes, statuses) survive the coercion.
*/
export function toErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}
/** Renders a non-Error cause as useful text without throwing. */
export function stringifyNonErrorCause(value: unknown): string {
if (value === null) {
return "null";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return String(value);
}
try {
return JSON.stringify(value) ?? Object.prototype.toString.call(value);
} catch {
return Object.prototype.toString.call(value);
}
}