openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
372 lines (371 loc) • 15.3 kB
JavaScript
import { y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js";
import { c as readErrorName } from "./errors-BXgSefBE.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { r as isSessionWriteLockAcquireError } from "./session-write-lock-error-CYOzPsPk.js";
import { y as isTimeoutErrorMessage } from "./sanitize-user-facing-text-DDpUeAo_.js";
import { S as isUnclassifiedNoBodyHttpSignal, c as inferSignalStatus, r as classifyFailoverSignal } from "./errors-B5Da2wE-.js";
//#region src/agents/failover-error.ts
/**
* Provider/model failover error classification.
* Converts nested provider, transport, timeout, auth, and local coordination
* failures into structured failover reasons and remediation metadata.
*/
const ABORT_TIMEOUT_RE = /request was aborted|request aborted/i;
const MAX_FAILOVER_CAUSE_DEPTH = 25;
/** Structured error used to carry model fallback/failover metadata across layers. */
var FailoverError = class extends Error {
constructor(message, params) {
super(message, { cause: params.cause });
this.name = "FailoverError";
this.reason = params.reason;
this.provider = params.provider;
this.model = params.model;
this.profileId = params.profileId;
this.status = params.status;
this.code = params.code;
this.rawError = params.rawError;
this.authProfileFailure = params.authProfileFailure;
this.sessionId = params.sessionId;
this.lane = params.lane;
this.suspend = params.suspend;
}
};
/** Return true for native or serialized failover errors. */
function isFailoverError(err) {
if (err instanceof FailoverError) return true;
return Boolean(err && typeof err === "object" && err.name === "FailoverError" && typeof err.reason === "string");
}
/** Map a failover reason to the closest HTTP-like status code. */
function resolveFailoverStatus(reason) {
switch (reason) {
case "billing": return 402;
case "server_error": return 500;
case "rate_limit": return 429;
case "overloaded": return 503;
case "auth": return 401;
case "auth_permanent": return 403;
case "timeout": return 408;
case "format": return 400;
case "model_not_found": return 404;
case "session_expired": return 410;
default: return;
}
}
function findErrorProperty(err, reader, seen = /* @__PURE__ */ new Set()) {
const direct = reader(err);
if (direct !== void 0) return direct;
if (!err || typeof err !== "object") return;
if (seen.has(err)) return;
seen.add(err);
const candidate = err;
return findErrorProperty(candidate.error, reader, seen) ?? findErrorProperty(candidate.cause, reader, seen);
}
function readDirectStatusCode(err) {
if (!err || typeof err !== "object") return;
const candidate = err.status ?? err.statusCode;
if (typeof candidate === "number") return candidate;
if (typeof candidate === "string") return parseStrictNonNegativeInteger(candidate);
}
function getStatusCode(err) {
return findErrorProperty(err, readDirectStatusCode);
}
function readDirectErrorCode(err) {
if (!err || typeof err !== "object") return;
const directCode = err.code;
if (typeof directCode === "string") {
const trimmed = directCode.trim();
return trimmed ? trimmed : void 0;
}
const detailCode = err.detail?.code;
if (typeof detailCode === "string") {
const trimmed = detailCode.trim();
return trimmed ? trimmed : void 0;
}
const status = err.status;
if (typeof status !== "string" || /^\d+$/.test(status)) return;
const trimmed = status.trim();
return trimmed ? trimmed : void 0;
}
function getErrorCode(err) {
return findErrorProperty(err, readDirectErrorCode);
}
function isStableProviderErrorType(value) {
if (/^(?:api|authentication|invalid_request|not_found|overloaded|permission|rate_limit|server)_error$/i.test(value)) return false;
return /^[A-Z][A-Z0-9_:-]*$/.test(value);
}
function readDirectErrorType(err) {
if (!err || typeof err !== "object") return;
const directType = err.errorType;
if (typeof directType === "string") {
const trimmed = directType.trim();
return trimmed && isStableProviderErrorType(trimmed) ? trimmed : void 0;
}
const detailType = err.detail?.type;
if (typeof detailType === "string") {
const trimmed = detailType.trim();
return trimmed && isStableProviderErrorType(trimmed) ? trimmed : void 0;
}
const type = err.type;
if (typeof type === "string") {
const trimmed = type.trim();
if (!trimmed || /^(?:error|exception)$/i.test(trimmed)) return;
return isStableProviderErrorType(trimmed) ? trimmed : void 0;
}
}
function getErrorType(err) {
return findErrorProperty(err, readDirectErrorType);
}
function readDirectProvider(err) {
if (!err || typeof err !== "object") return;
const provider = err.provider;
if (typeof provider !== "string") return;
return provider.trim() || void 0;
}
function getProvider(err) {
return findErrorProperty(err, readDirectProvider);
}
function readDirectErrorMessage(err) {
if (err instanceof Error) return err.message || void 0;
if (typeof err === "string") return err || void 0;
if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") return String(err);
if (typeof err === "symbol") return err.description ?? void 0;
if (err && typeof err === "object") {
const message = err.message;
if (typeof message === "string") return message || void 0;
}
}
function getErrorMessage(err) {
return findErrorProperty(err, readDirectErrorMessage) ?? "";
}
function normalizeDirectErrorSignal(err) {
const message = readDirectErrorMessage(err);
return {
status: readDirectStatusCode(err),
code: readDirectErrorCode(err),
errorType: readDirectErrorType(err),
message: message || void 0,
provider: readDirectProvider(err)
};
}
function hasSessionWriteLockContention(err, seen = /* @__PURE__ */ new Set()) {
if (isSessionWriteLockAcquireError(err)) return true;
if (!err || typeof err !== "object") return false;
if (seen.has(err)) return false;
seen.add(err);
const candidate = err;
return hasSessionWriteLockContention(candidate.error, seen) || hasSessionWriteLockContention(candidate.cause, seen) || hasSessionWriteLockContention(candidate.reason, seen);
}
function isEmbeddedAttemptSessionTakeover(err) {
return Boolean(err && typeof err === "object" && readErrorName(err) === "EmbeddedAttemptSessionTakeoverError");
}
function hasEmbeddedAttemptSessionTakeover(err, seen = /* @__PURE__ */ new Set()) {
if (isEmbeddedAttemptSessionTakeover(err)) return true;
if (!err || typeof err !== "object") return false;
if (seen.has(err)) return false;
seen.add(err);
const candidate = err;
return hasEmbeddedAttemptSessionTakeover(candidate.error, seen) || hasEmbeddedAttemptSessionTakeover(candidate.cause, seen) || hasEmbeddedAttemptSessionTakeover(candidate.reason, seen);
}
/**
* True when the error is a local runtime coordination error (session write-lock
* timeout or embedded attempt session takeover) rather than a provider/model
* failure. The model fallback chain must abort on these instead of consuming
* candidate slots — retrying any model would hit the same local condition.
* See #83510.
*/
function isNonProviderRuntimeCoordinationError(err) {
if (!hasSessionWriteLockContention(err) && !hasEmbeddedAttemptSessionTakeover(err)) return false;
if (isFailoverError(err)) return false;
if (isEmbeddedAttemptSessionTakeover(err)) return true;
return resolveFailoverClassificationFromError(err) === null;
}
function hasTimeoutHint(err) {
if (!err) return false;
if (hasSessionWriteLockContention(err)) return false;
if (readErrorName(err) === "TimeoutError") return true;
const message = getErrorMessage(err);
return Boolean(message && isTimeoutErrorMessage(message));
}
/** Return true when an unknown error shape represents a timeout. */
function isTimeoutError(err) {
if (hasTimeoutHint(err)) return true;
if (!err || typeof err !== "object") return false;
if (readErrorName(err) !== "AbortError") return false;
if (hasSessionWriteLockContention(err)) return false;
const message = getErrorMessage(err);
if (message && ABORT_TIMEOUT_RE.test(message)) return true;
const cause = "cause" in err ? err.cause : void 0;
const reason = "reason" in err ? err.reason : void 0;
return hasTimeoutHint(cause) || hasTimeoutHint(reason);
}
function failoverReasonFromClassification(classification) {
return classification?.kind === "reason" ? classification.reason : null;
}
function normalizeErrorSignal(err, providerHint) {
const message = getErrorMessage(err);
return {
status: getStatusCode(err),
code: getErrorCode(err),
errorType: getErrorType(err),
message: message || void 0,
provider: getProvider(err) ?? providerHint
};
}
function getNestedErrorCandidates(err) {
if (!err || typeof err !== "object") return [];
const candidate = err;
return [candidate.error, candidate.cause].filter((value) => value !== void 0 && value !== err);
}
function isFormatClassification(classification) {
return classification?.kind === "reason" && classification.reason === "format";
}
function decideNestedFormatOverride(candidate, inheritedStatus, seen, depth) {
if (depth > MAX_FAILOVER_CAUSE_DEPTH) return null;
if (candidate && typeof candidate === "object") {
if (seen.has(candidate)) return null;
seen.add(candidate);
}
const directSignal = normalizeDirectErrorSignal(candidate);
const nestedCandidates = getNestedErrorCandidates(candidate);
const nestedStatus = directSignal.status ?? inheritedStatus;
const hasDirectMessage = Boolean(directSignal.message?.trim());
if (hasDirectMessage && isUnclassifiedNoBodyHttpSignal({
...directSignal,
status: nestedStatus
})) return true;
if (hasDirectMessage && (nestedCandidates.length === 0 || classifyFailoverSignal(directSignal))) return false;
for (const nestedCandidate of nestedCandidates) {
const decision = decideNestedFormatOverride(nestedCandidate, nestedStatus, seen, depth + 1);
if (decision !== null) return decision;
}
return null;
}
function resolveFailoverClassificationFromErrorInternal(err, seen, depth, providerHint) {
if (depth > MAX_FAILOVER_CAUSE_DEPTH) return null;
if (err && typeof err === "object") {
if (seen.has(err)) return null;
seen.add(err);
}
if (isFailoverError(err)) return {
kind: "reason",
reason: err.reason
};
const signal = normalizeErrorSignal(err, providerHint);
const codeReason = signal.code ? failoverReasonFromClassification(classifyFailoverSignal({ code: signal.code })) : null;
const hasExplicitFailoverMetadata = typeof inferSignalStatus(signal) === "number" || codeReason !== null && codeReason !== "timeout";
const hasSessionLock = hasSessionWriteLockContention(err);
const classification = classifyFailoverSignal(signal);
const nestedCandidates = getNestedErrorCandidates(err);
if (!classification || classification.kind === "context_overflow") for (const candidate of nestedCandidates) {
const nestedClassification = resolveFailoverClassificationFromErrorInternal(candidate, seen, depth + 1, providerHint);
if (nestedClassification) {
if (hasSessionLock && !hasExplicitFailoverMetadata) return null;
return nestedClassification;
}
}
if (isFormatClassification(classification)) for (const candidate of nestedCandidates) {
const shouldClearFormat = decideNestedFormatOverride(candidate, signal.status, seen, depth + 1);
if (shouldClearFormat === true) return null;
if (shouldClearFormat === false) break;
}
if (classification) {
if (hasSessionLock && !hasExplicitFailoverMetadata) return null;
return classification;
}
if (hasSessionLock) return null;
if (isTimeoutError(err)) return {
kind: "reason",
reason: "timeout"
};
return null;
}
function resolveFailoverClassificationFromError(err, providerHint) {
return resolveFailoverClassificationFromErrorInternal(err, /* @__PURE__ */ new Set(), 0, providerHint);
}
/** Resolve the failover reason represented by an unknown provider/runtime error. */
function resolveFailoverReasonFromError(err, providerHint) {
return failoverReasonFromClassification(resolveFailoverClassificationFromError(err, providerHint));
}
/**
* Build an actionable remediation hint for a failover error when the failure
* reason is `auth` / `auth_permanent` and we have enough provider attribution
* to suggest a re-authentication command. Returns `undefined` for any other
* failure shape so callers can opportunistically append the hint without
* branching on every reason themselves.
*
* Keep the string short and copy-pasteable — operators see it in fallback
* summary errors and TUI status lines.
*/
function buildFailoverRemediationHint(err) {
if (!isFailoverError(err)) return;
if (err.reason !== "auth" && err.reason !== "auth_permanent") return;
const provider = err.provider?.trim();
if (!provider) return;
const command = buildProviderReauthCommand(provider);
return command ? `Re-authenticate with: ${command}` : void 0;
}
function quotePosixShellArg(value) {
return `'${value.replaceAll("'", "'\\''")}'`;
}
/** Build the operator command for reauthenticating one provider. */
function buildProviderReauthCommand(provider, env = process.env) {
const trimmed = provider.trim();
if (!trimmed || hasControlCharacter(trimmed)) return;
return formatCliCommand(`openclaw models auth login --provider ${quotePosixShellArg(trimmed)} --force`, env);
}
function hasControlCharacter(value) {
for (let i = 0; i < value.length; i += 1) {
const code = value.charCodeAt(i);
if (code < 32 || code === 127) return true;
}
return false;
}
/** Convert a failover or raw error into structured fields for logs/UI. */
function describeFailoverError(err) {
if (isFailoverError(err)) return {
message: err.message,
rawError: err.rawError,
reason: err.reason,
status: err.status,
code: err.code,
provider: err.provider,
model: err.model,
profileId: err.profileId,
sessionId: err.sessionId,
lane: err.lane
};
const signal = normalizeErrorSignal(err);
return {
message: signal.message ?? String(err),
reason: resolveFailoverReasonFromError(err) ?? void 0,
status: signal.status,
code: signal.code,
provider: signal.provider
};
}
/** Convert a classified raw error into a FailoverError with optional request context. */
function coerceToFailoverError(err, context) {
if (isFailoverError(err)) return err;
const reason = resolveFailoverReasonFromError(err, context?.provider);
if (!reason) return null;
const signal = normalizeErrorSignal(err);
const message = signal.message ?? String(err);
const status = signal.status ?? resolveFailoverStatus(reason);
const code = signal.code;
const shouldSuspend = Boolean(context?.sessionId) && (reason === "rate_limit" || reason === "billing");
return new FailoverError(message, {
reason,
provider: context?.provider ?? signal.provider,
model: context?.model,
profileId: context?.profileId,
sessionId: context?.sessionId,
lane: context?.lane,
status,
code,
rawError: message,
cause: err instanceof Error ? err : void 0,
suspend: shouldSuspend
});
}
//#endregion
export { describeFailoverError as a, isTimeoutError as c, coerceToFailoverError as i, resolveFailoverReasonFromError as l, buildFailoverRemediationHint as n, isFailoverError as o, buildProviderReauthCommand as r, isNonProviderRuntimeCoordinationError as s, FailoverError as t, resolveFailoverStatus as u };