Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 5ecb99a

Browse files
authored
fix(agent): classify mid-response stream deaths as transient upstream failures
Claude Code surfaces an SSE stream that dies after content started as "API Error: Connection closed mid-response. The response above may be incomplete." classifyAgentError did not recognize that string (nor the transport-level "socket connection was closed" variants), so these transient transport failures fell through to agent_error and hard-failed the whole task run with a raw error message instead of being treated as retriable upstream failures. Generated-By: PostHog Code Task-Id: a04c4d02-9070-4e5d-9748-bfceb251e3fa
1 parent 59f538b commit 5ecb99a

4 files changed

Lines changed: 54 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from "vitest";
2+
import { classifyAgentError } from "./error-classification";
3+
4+
describe("classifyAgentError", () => {
5+
it.each([
6+
["API Error: terminated", "upstream_stream_terminated"],
7+
[
8+
"API Error: Connection closed mid-response. The response above may be incomplete.",
9+
"upstream_stream_terminated",
10+
],
11+
[
12+
"API Error: The socket connection was closed unexpectedly.",
13+
"upstream_stream_terminated",
14+
],
15+
[
16+
"The socket connection was closed unexpectedly. For more information, pass `verbose: true`",
17+
"upstream_stream_terminated",
18+
],
19+
["socket connection closed", "upstream_stream_terminated"],
20+
["API Error: Connection error.", "upstream_connection_error"],
21+
["API Error: Request timed out.", "upstream_timeout"],
22+
["API Error: 429 rate limited", "upstream_provider_failure"],
23+
["API Error: 529 overloaded", "upstream_provider_failure"],
24+
["API Error: 400 invalid request", "agent_error"],
25+
[
26+
"Connection closed mid-response without the API Error prefix",
27+
"agent_error",
28+
],
29+
["some unrelated failure", "agent_error"],
30+
[undefined, "agent_error"],
31+
] as const)("classifies %j as %s", (message, expected) => {
32+
expect(classifyAgentError(message)).toBe(expected);
33+
});
34+
});

‎packages/agent/src/adapters/error-classification.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ export function classifyAgentError(
2323
if (/API Error:\s*terminated\b/i.test(text)) {
2424
return "upstream_stream_terminated";
2525
}
26+
// Claude Code surfaces an SSE stream that dies after content started
27+
// (no message_stop) as "Connection closed mid-response". Seen when a
28+
// gateway pod is replaced mid-stream or an intermediary cuts the socket
29+
// during a long silent stretch.
30+
if (/API Error:.*Connection closed mid-response/i.test(text)) {
31+
return "upstream_stream_terminated";
32+
}
33+
// Transport-level socket deaths reported by fetch implementations
34+
// (Bun/undici wording varies) — same failure mode as above. These are raw
35+
// transport errors, so they don't always carry the "API Error:" prefix.
36+
if (/socket connection (?:was )?closed/i.test(text)) {
37+
return "upstream_stream_terminated";
38+
}
2639
if (/API Error:\s*Connection error\b/i.test(text)) {
2740
return "upstream_connection_error";
2841
}

‎packages/shared/src/errors.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ describe("isTransientUpstreamError", () => {
133133
"API Error: 500 internal server error",
134134
"API Error: 529 overloaded_error",
135135
"Internal error: API Error: request timed out",
136+
"Internal error: API Error: Connection closed mid-response. The response above may be incomplete.",
137+
"The socket connection was closed unexpectedly.",
138+
"socket connection closed",
136139
])("recognises %j", (message) => {
137140
expect(isTransientUpstreamError(message)).toBe(true);
138141
});

‎packages/shared/src/errors.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ const FATAL_SESSION_ERROR_PATTERNS = [
9494
const UPSTREAM_TRANSIENT_ERROR_REGEXES = [
9595
/API Error:\s*terminated\b/i,
9696
/API Error:\s*Connection error\b/i,
97+
/API Error:.*Connection closed mid-response/i,
98+
// Raw transport-level socket death — wording varies by fetch
99+
// implementation and doesn't always carry the "API Error:" prefix.
100+
/socket connection (?:was )?closed/i,
97101
/API Error:.*\b(?:timed out|timeout)\b/i,
98102
/API Error:\s*(?:429|5\d\d)\b/i,
99103
] as const;

0 commit comments

Comments
 (0)