diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 21149a2cf389..66ef3970a0c9 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -66,7 +66,8 @@ function message(providerID: ProviderV2.ID, e: APICallError) { return msg } - return `${msg}: ${e.responseBody}` + const body = e.responseBody.length > 200 ? e.responseBody.slice(0, 200) + "..." : e.responseBody + return `${msg}: ${body}` }).trim() } diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 1590e0890372..9c1d0cd50594 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -611,6 +611,17 @@ export function latest(msgs: WithParts[]) { return { user, assistant, finished, tasks } } +const TRANSIENT_CODES = new Set(["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "ENETUNREACH", "EPIPE"]) +const TRANSIENT_PATTERNS = ["failed to fetch", "network connection was lost", "network request failed", "socket hang up", "unexpected end of data", "premature close", "response closed without sending", "load failed", "fetch failed", "body is unusable"] + +function isTransientNetworkError(e: unknown): e is Error { + if (!(e instanceof Error)) return false + const code = (e as Error & { code?: string }).code + if (code && TRANSIENT_CODES.has(code)) return true + const msg = e.message.toLowerCase() + return TRANSIENT_PATTERNS.some((p) => msg.includes(p)) +} + export function fromError( e: unknown, ctx: { providerID: ProviderV2.ID; aborted?: boolean }, @@ -710,6 +721,18 @@ export function fromError( }, { cause: e }, ).toObject() + case isTransientNetworkError(e): + return new APIError( + { + message: e.message, + isRetryable: true, + metadata: { + type: "network_error", + code: e.name, + }, + }, + { cause: e }, + ).toObject() case e instanceof Error: return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject() default: