refactor: dedupe agent error formatting

This commit is contained in:
Peter Steinberger
2026-04-07 00:52:30 +01:00
parent 3417dbabf4
commit a4253deb67
19 changed files with 62 additions and 46 deletions

View File

@@ -27,6 +27,7 @@ import {
emitAgentEvent,
registerAgentRunContext,
} from "../infra/agent-events.js";
import { formatErrorMessage } from "../infra/errors.js";
import { buildOutboundSessionContext } from "../infra/outbound/session-context.js";
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
@@ -480,7 +481,7 @@ async function agentCommandInternal(
});
} catch (error) {
log.warn(
`ACP transcript persistence failed for ${sessionKey}: ${error instanceof Error ? error.message : String(error)}`,
`ACP transcript persistence failed for ${sessionKey}: ${formatErrorMessage(error)}`,
);
}

View File

@@ -6,6 +6,7 @@ import {
} from "@mariozechner/pi-ai/oauth";
import { loadConfig, type OpenClawConfig } from "../../config/config.js";
import { coerceSecretRef } from "../../config/types.secrets.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { withFileLock } from "../../infra/file-lock.js";
import {
formatProviderAuthProfileApiKeyWithPlugin,
@@ -119,7 +120,7 @@ async function buildOAuthProfileResult(params: {
}
function extractErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
return formatErrorMessage(error);
}
function isRefreshTokenReusedError(error: unknown): boolean {
@@ -207,7 +208,7 @@ function adoptNewerMainOAuthCredential(params: {
// Best-effort: don't crash if main agent store is missing or unreadable.
log.debug("adoptNewerMainOAuthCredential failed", {
profileId: params.profileId,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
});
}
return null;
@@ -403,7 +404,7 @@ async function resolveProfileSecretString(params: {
log.debug(params.inlineFailureMessage, {
profileId: params.profileId,
provider: params.provider,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
});
}
}
@@ -421,7 +422,7 @@ async function resolveProfileSecretString(params: {
log.debug(params.refFailureMessage, {
profileId: params.profileId,
provider: params.provider,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
});
}
}

View File

@@ -2,6 +2,7 @@ import { execFileSync, execSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { formatErrorMessage } from "../infra/errors.js";
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveUserPath } from "../utils.js";
@@ -461,7 +462,7 @@ export function writeClaudeCliKeychainCredentials(
return true;
} catch (error) {
log.warn("failed to write credentials to claude cli keychain", {
error: error instanceof Error ? error.message : String(error),
error: formatErrorMessage(error),
});
return false;
}
@@ -503,7 +504,7 @@ export function writeClaudeCliFileCredentials(
return true;
} catch (error) {
log.warn("failed to write credentials to claude cli file", {
error: error instanceof Error ? error.message : String(error),
error: formatErrorMessage(error),
});
return false;
}
@@ -584,7 +585,7 @@ export function writeCodexCliKeychainCredentials(
return true;
} catch (error) {
log.warn("failed to write credentials to codex cli keychain", {
error: error instanceof Error ? error.message : String(error),
error: formatErrorMessage(error),
});
return false;
}
@@ -614,7 +615,7 @@ export function writeCodexCliFileCredentials(
return true;
} catch (error) {
log.warn("failed to write credentials to codex cli file", {
error: error instanceof Error ? error.message : String(error),
error: formatErrorMessage(error),
});
return false;
}

View File

@@ -1,6 +1,7 @@
import type { ImageContent } from "@mariozechner/pi-ai";
import type { ThinkLevel } from "../auto-reply/thinking.js";
import type { OpenClawConfig } from "../config/config.js";
import { formatErrorMessage } from "../infra/errors.js";
import { executePreparedCliRun } from "./cli-runner/execute.js";
import { prepareCliRunContext } from "./cli-runner/prepare.js";
import type { RunCliAgentParams } from "./cli-runner/types.js";
@@ -73,7 +74,7 @@ export async function runCliAgent(params: RunCliAgentParams): Promise<EmbeddedPi
}
throw err;
}
const message = err instanceof Error ? err.message : String(err);
const message = formatErrorMessage(err);
if (isFailoverErrorMessage(message, { provider: params.provider })) {
const reason = classifyFailoverReason(message, { provider: params.provider }) ?? "unknown";
const status = resolveFailoverStatus(reason);

View File

@@ -3,6 +3,7 @@ import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
import { formatErrorMessage } from "../infra/errors.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { sanitizeForLog } from "../terminal/ansi.js";
import { resolveAuthProfileOrder } from "./auth-profiles/order.js";
@@ -814,7 +815,7 @@ export async function runWithModelFallback<T>(params: {
// compaction/retry logic, not by model fallback. If one escapes as a
// throw, rethrow it immediately rather than trying a different model
// that may have a smaller context window and fail worse.
const errMessage = err instanceof Error ? err.message : String(err);
const errMessage = formatErrorMessage(err);
if (isLikelyContextOverflowError(errMessage)) {
throw err;
}
@@ -937,7 +938,7 @@ export async function runWithImageModelFallback<T>(params: {
attempts.push({
provider: candidate.provider,
model: candidate.model,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
});
await params.onError?.({
provider: candidate.provider,

View File

@@ -8,6 +8,7 @@ import {
type Tool,
} from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { formatErrorMessage } from "../infra/errors.js";
import { inferParamBFromIdOrName } from "../shared/model-param-b.js";
import { normalizeProviderId } from "./provider-id.js";
@@ -284,7 +285,7 @@ async function probeTool(
return {
ok: false,
latencyMs: Date.now() - startedAt,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
};
}
}
@@ -321,7 +322,7 @@ async function probeImage(
return {
ok: false,
latencyMs: Date.now() - startedAt,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
};
}
}

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../config/config.js";
import { formatErrorMessage } from "../infra/errors.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import {
groupPluginDiscoveryProvidersByOrder,
@@ -322,7 +323,7 @@ async function runProviderCatalogWithTimeout(
}),
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const message = formatErrorMessage(error);
if (message.includes("provider catalog timed out after")) {
log.warn(`${message}; skipping provider discovery`);
return undefined;

View File

@@ -1,3 +1,12 @@
import { randomUUID } from "node:crypto";
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type {
AssistantMessage,
AssistantMessageEvent,
AssistantMessageEventStream,
StopReason,
} from "@mariozechner/pi-ai";
import * as piAi from "@mariozechner/pi-ai";
/**
* OpenAI WebSocket StreamFn Integration
*
@@ -20,16 +29,7 @@
*
* @see src/agents/openai-ws-connection.ts for the connection manager
*/
import { randomUUID } from "node:crypto";
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type {
AssistantMessage,
AssistantMessageEvent,
AssistantMessageEventStream,
StopReason,
} from "@mariozechner/pi-ai";
import * as piAi from "@mariozechner/pi-ai";
import { formatErrorMessage } from "../infra/errors.js";
import {
resolveProviderTransportTurnStateWithPlugin,
resolveProviderWebSocketSessionPolicyWithPlugin,
@@ -550,7 +550,7 @@ function normalizeWsRunError(err: unknown): OpenAIWebSocketRuntimeError {
if (err instanceof OpenAIWebSocketRuntimeError) {
return err;
}
return new OpenAIWebSocketRuntimeError(err instanceof Error ? err.message : String(err), {
return new OpenAIWebSocketRuntimeError(formatErrorMessage(err), {
kind: "server",
retryable: false,
});
@@ -1177,7 +1177,7 @@ export function createOpenAIWebSocketStreamFn(
queueMicrotask(() =>
run().catch((err) => {
const errorMessage = err instanceof Error ? err.message : String(err);
const errorMessage = formatErrorMessage(err);
log.warn(`[ws-stream] session=${sessionId} run error: ${errorMessage}`);
eventStream.push({
type: "error",

View File

@@ -1,4 +1,5 @@
import { callGateway } from "../gateway/call.js";
import { formatErrorMessage } from "../infra/errors.js";
import { extractAssistantText, stripToolMessages } from "./tools/chat-history-text.js";
type GatewayCaller = typeof callGateway;
@@ -137,7 +138,7 @@ export async function waitForAgentRun(params: {
}
return normalizeAgentWaitResult("ok", wait);
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
const error = formatErrorMessage(err);
return {
status: error.includes("gateway timeout") ? "timeout" : "error",
error,

View File

@@ -1,5 +1,6 @@
import { complete, type Api, type Model } from "@mariozechner/pi-ai";
import type { OpenClawConfig } from "../config/config.js";
import { formatErrorMessage } from "../infra/errors.js";
import { resolveAgentDir, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
import { DEFAULT_PROVIDER } from "./defaults.js";
import {
@@ -152,7 +153,7 @@ export async function prepareSimpleCompletionModel(params: {
});
} catch (err) {
return {
error: `Auth lookup failed for provider "${resolved.model.provider}": ${err instanceof Error ? err.message : String(err)}`,
error: `Auth lookup failed for provider "${resolved.model.provider}": ${formatErrorMessage(err)}`,
};
}
const rawApiKey = auth.apiKey?.trim();

View File

@@ -10,6 +10,7 @@ import {
type ClawHubSkillDetail,
type ClawHubSkillSearchResult,
} from "../infra/clawhub.js";
import { formatErrorMessage } from "../infra/errors.js";
import { withExtractedArchiveRoot } from "../infra/install-flow.js";
import { installPackageDir } from "../infra/install-package-dir.js";
import { resolveSafeInstallDir } from "../infra/install-safe-path.js";
@@ -336,7 +337,7 @@ async function performClawHubSkillInstall(
} catch (err) {
return {
ok: false,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
};
}
}
@@ -352,7 +353,7 @@ async function installRequestedSkillFromClawHub(
} catch (err) {
return {
ok: false,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
};
}
}
@@ -368,7 +369,7 @@ async function installTrackedSkillFromClawHub(
} catch (err) {
return {
ok: false,
error: err instanceof Error ? err.message : String(err),
error: formatErrorMessage(err),
};
}
}

View File

@@ -5,6 +5,7 @@ import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import type { ReadableStream as NodeReadableStream } from "node:stream/web";
import { isWindowsDrivePath } from "../infra/archive-path.js";
import { formatErrorMessage } from "../infra/errors.js";
import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
import { assertCanonicalPathWithinBase } from "../infra/install-safe-path.js";
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
@@ -151,7 +152,7 @@ export async function installDownloadSpec(params: {
const targetRelativePath = path.relative(safeRoot, requestedTargetDir);
targetDir = path.join(canonicalSafeRoot, targetRelativePath);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const message = formatErrorMessage(err);
return { ok: false, message, stdout: "", stderr: message, code: null };
}
@@ -181,7 +182,7 @@ export async function installDownloadSpec(params: {
});
downloaded = result.bytes;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const message = formatErrorMessage(err);
return { ok: false, message, stdout: "", stderr: message, code: null };
}
@@ -214,7 +215,7 @@ export async function installDownloadSpec(params: {
boundaryLabel: "skill tools directory",
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const message = formatErrorMessage(err);
return { ok: false, message, stdout: "", stderr: message, code: null };
}

View File

@@ -7,6 +7,7 @@ import {
prepareArchiveDestinationDir,
withStagedArchiveDestination,
} from "../infra/archive.js";
import { formatErrorMessage } from "../infra/errors.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { parseTarVerboseMetadata } from "./skills-install-tar-verbose.js";
import { hasBinary } from "./skills.js";
@@ -227,7 +228,7 @@ export async function extractArchive(params: {
return { stdout: "", stderr: `unsupported archive type: ${archiveType}`, code: null };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const message = formatErrorMessage(err);
return { stdout: "", stderr: message, code: 1 };
}
}

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { resolveBrewExecutable } from "../infra/brew.js";
import { formatErrorMessage } from "../infra/errors.js";
import {
type InstallSafetyOverrides,
scanSkillInstallSource,
@@ -248,7 +249,7 @@ async function runCommandSafely(
return {
code: null,
stdout: "",
stderr: err instanceof Error ? err.message : String(err),
stderr: formatErrorMessage(err),
};
}
}

View File

@@ -11,6 +11,7 @@ import type { SessionEntry } from "../config/sessions.js";
import { loadSessionStore, resolveStorePath, updateSessionStore } from "../config/sessions.js";
import { callGateway } from "../gateway/call.js";
import { logVerbose } from "../globals.js";
import { formatErrorMessage } from "../infra/errors.js";
import { isSubagentSessionKey, parseAgentSessionKey } from "../routing/session-key.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
@@ -171,9 +172,7 @@ async function killSubagentRun(params: {
});
} catch (error) {
logVerbose(
`subagents control kill: failed to persist abortedLastRun for ${childSessionKey}: ${
error instanceof Error ? error.message : String(error)
}`,
`subagents control kill: failed to persist abortedLastRun for ${childSessionKey}: ${formatErrorMessage(error)}`,
);
}
}
@@ -557,7 +556,7 @@ export async function steerControlledSubagentRun(params: {
}
} catch (err) {
clearSubagentRunSteerRestart(params.entry.runId);
const error = err instanceof Error ? err.message : String(err);
const error = formatErrorMessage(err);
return {
status: "error",
runId,
@@ -681,7 +680,7 @@ export async function sendControlledSubagentMessage(params: {
}
return { status: "ok" as const, runId, replyText: result.replyText };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
const error = formatErrorMessage(err);
return { status: "error" as const, runId, error };
}
}

View File

@@ -5,6 +5,7 @@ import {
resolveLeastPrivilegeOperatorScopesForMethod,
type OperatorScope,
} from "../../gateway/method-scopes.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
import { readStringParam } from "./common.js";
@@ -32,7 +33,7 @@ function canonicalizeToolGatewayWsUrl(raw: string): { origin: string; key: strin
try {
url = new URL(input);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const message = formatErrorMessage(error);
throw new Error(`invalid gatewayUrl: ${input} (${message})`, { cause: error });
}

View File

@@ -1,5 +1,6 @@
import crypto from "node:crypto";
import { parseTimeoutMs } from "../../cli/parse-timeout.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { jsonResult, readStringParam } from "./common.js";
import type { GatewayCallOptions } from "./gateway.js";
import { callGatewayTool } from "./gateway.js";
@@ -138,7 +139,7 @@ export async function executeNodeCommandAction(params: {
try {
invokeParams = JSON.parse(invokeParamsJson);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const message = formatErrorMessage(err);
throw new Error(`invokeParamsJson must be valid JSON: ${message}`, {
cause: err,
});

View File

@@ -1,5 +1,6 @@
import type { OpenClawConfig } from "../../config/config.js";
import { callGateway } from "../../gateway/call.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { isAcpSessionKey, normalizeMainKey } from "../../routing/session-key.js";
import { looksLikeSessionId } from "../../sessions/session-id.js";
@@ -274,7 +275,7 @@ async function resolveSessionKeyFromSessionId(params: {
error: `Session not visible from this sandboxed agent session: ${params.sessionId}`,
};
}
const message = err instanceof Error ? err.message : String(err);
const message = formatErrorMessage(err);
return {
ok: false,
status: "error",

View File

@@ -2,6 +2,7 @@ import crypto from "node:crypto";
import { Type } from "@sinclair/typebox";
import type { OpenClawConfig } from "../../config/config.js";
import { callGateway } from "../../gateway/call.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { normalizeAgentId, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
import {
@@ -156,7 +157,7 @@ export function createSessionsSendTool(opts?: {
});
resolvedKey = typeof resolved?.key === "string" ? resolved.key.trim() : "";
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
const msg = formatErrorMessage(err);
if (restrictToSpawned) {
return jsonResult({
runId: crypto.randomUUID(),