Skip to content

Commit d78e13f

Browse files
authored
fix(agent): clarify embedded transport errors (#51419)
Merged via squash. Prepared head SHA: cea32a4 Co-authored-by: scoootscooob <[email protected]> Co-authored-by: scoootscooob <[email protected]> Reviewed-by: @scoootscooob
1 parent 6b4c24c commit d78e13f

6 files changed

+99
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ Docs: https://docs.openclaw.ai
192192
- Web search: align onboarding, configure, and finalize with plugin-owned provider contracts, including disabled-provider recovery, config-aware credential hooks, and runtime-visible summaries. (#50935) Thanks @gumadeiras.
193193
- Agents/replay: sanitize malformed assistant tool-call replay blocks before provider replay so follow-up Anthropic requests do not inherit the downstream `replace` crash. (#50005) Thanks @jalehman.
194194
- Plugins/context engines: retry strict legacy `assemble()` calls without the new `prompt` field when older engines reject it, preserving prompt-aware retrieval compatibility for pre-prompt plugins. (#50848) thanks @danhdoan.
195+
- Agents/embedded transport errors: distinguish common network failures like connection refused, DNS lookup failure, and interrupted sockets from true timeouts in embedded-run user messaging and lifecycle diagnostics. (#51419) Thanks @scoootscooob.
195196

196197
### Breaking
197198

src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,27 @@ describe("formatAssistantErrorText", () => {
125125
const msg = makeAssistantError("request ended without sending any chunks");
126126
expect(formatAssistantErrorText(msg)).toBe("LLM request timed out.");
127127
});
128+
129+
it("returns a connection-refused message for ECONNREFUSED failures", () => {
130+
const msg = makeAssistantError("connect ECONNREFUSED 127.0.0.1:443 during upstream call");
131+
expect(formatAssistantErrorText(msg)).toBe(
132+
"LLM request failed: connection refused by the provider endpoint.",
133+
);
134+
});
135+
136+
it("returns a DNS-specific message for provider lookup failures", () => {
137+
const msg = makeAssistantError("dial tcp: lookup api.example.com: no such host (ENOTFOUND)");
138+
expect(formatAssistantErrorText(msg)).toBe(
139+
"LLM request failed: DNS lookup for the provider endpoint failed.",
140+
);
141+
});
142+
143+
it("returns an interrupted-connection message for socket hang ups", () => {
144+
const msg = makeAssistantError("socket hang up");
145+
expect(formatAssistantErrorText(msg)).toBe(
146+
"LLM request failed: network connection was interrupted.",
147+
);
148+
});
128149
});
129150

130151
describe("formatRawAssistantErrorForUi", () => {

src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ describe("sanitizeUserFacingText", () => {
8888
);
8989
});
9090

91+
it("returns a transport-specific message for prefixed ECONNREFUSED errors", () => {
92+
expect(
93+
sanitizeUserFacingText("Error: connect ECONNREFUSED 127.0.0.1:443", {
94+
errorContext: true,
95+
}),
96+
).toBe("LLM request failed: connection refused by the provider endpoint.");
97+
});
98+
9199
it.each([
92100
{
93101
input: "Hello there!\n\nHello there!",

src/agents/pi-embedded-helpers/errors.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,57 @@ function formatRateLimitOrOverloadedErrorCopy(raw: string): string | undefined {
6565
return undefined;
6666
}
6767

68+
function formatTransportErrorCopy(raw: string): string | undefined {
69+
if (!raw) {
70+
return undefined;
71+
}
72+
const lower = raw.toLowerCase();
73+
74+
if (
75+
/\beconnrefused\b/i.test(raw) ||
76+
lower.includes("connection refused") ||
77+
lower.includes("actively refused")
78+
) {
79+
return "LLM request failed: connection refused by the provider endpoint.";
80+
}
81+
82+
if (
83+
/\beconnreset\b|\beconnaborted\b|\benetreset\b|\bepipe\b/i.test(raw) ||
84+
lower.includes("socket hang up") ||
85+
lower.includes("connection reset") ||
86+
lower.includes("connection aborted")
87+
) {
88+
return "LLM request failed: network connection was interrupted.";
89+
}
90+
91+
if (
92+
/\benotfound\b|\beai_again\b/i.test(raw) ||
93+
lower.includes("getaddrinfo") ||
94+
lower.includes("no such host") ||
95+
lower.includes("dns")
96+
) {
97+
return "LLM request failed: DNS lookup for the provider endpoint failed.";
98+
}
99+
100+
if (
101+
/\benetunreach\b|\behostunreach\b|\behostdown\b/i.test(raw) ||
102+
lower.includes("network is unreachable") ||
103+
lower.includes("host is unreachable")
104+
) {
105+
return "LLM request failed: the provider endpoint is unreachable from this host.";
106+
}
107+
108+
if (
109+
lower.includes("fetch failed") ||
110+
lower.includes("connection error") ||
111+
lower.includes("network request failed")
112+
) {
113+
return "LLM request failed: network connection error.";
114+
}
115+
116+
return undefined;
117+
}
118+
68119
function isReasoningConstraintErrorMessage(raw: string): boolean {
69120
if (!raw) {
70121
return false;
@@ -566,6 +617,11 @@ export function formatAssistantErrorText(
566617
return transientCopy;
567618
}
568619

620+
const transportCopy = formatTransportErrorCopy(raw);
621+
if (transportCopy) {
622+
return transportCopy;
623+
}
624+
569625
if (isTimeoutErrorMessage(raw)) {
570626
return "LLM request timed out.";
571627
}
@@ -626,6 +682,10 @@ export function sanitizeUserFacingText(text: string, opts?: { errorContext?: boo
626682
if (prefixedCopy) {
627683
return prefixedCopy;
628684
}
685+
const transportCopy = formatTransportErrorCopy(trimmed);
686+
if (transportCopy) {
687+
return transportCopy;
688+
}
629689
if (isTimeoutErrorMessage(trimmed)) {
630690
return "LLM request timed out.";
631691
}

src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,16 @@ describe("handleAgentEnd", () => {
5858
expect(warn.mock.calls[0]?.[1]).toMatchObject({
5959
event: "embedded_run_agent_end",
6060
runId: "run-1",
61-
error: "connection refused",
61+
error: "LLM request failed: connection refused by the provider endpoint.",
6262
rawErrorPreview: "connection refused",
63+
consoleMessage:
64+
"embedded run agent end: runId=run-1 isError=true model=unknown provider=unknown error=LLM request failed: connection refused by the provider endpoint. rawError=connection refused",
6365
});
6466
expect(onAgentEvent).toHaveBeenCalledWith({
6567
stream: "lifecycle",
6668
data: {
6769
phase: "error",
68-
error: "connection refused",
70+
error: "LLM request failed: connection refused by the provider endpoint.",
6971
},
7072
});
7173
});
@@ -92,7 +94,7 @@ describe("handleAgentEnd", () => {
9294
failoverReason: "overloaded",
9395
providerErrorType: "overloaded_error",
9496
consoleMessage:
95-
"embedded run agent end: runId=run-1 isError=true model=claude-test provider=anthropic error=The AI service is temporarily overloaded. Please try again in a moment.",
97+
'embedded run agent end: runId=run-1 isError=true model=claude-test provider=anthropic error=The AI service is temporarily overloaded. Please try again in a moment. rawError={"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}',
9698
});
9799
});
98100

@@ -112,7 +114,7 @@ describe("handleAgentEnd", () => {
112114
const meta = warn.mock.calls[0]?.[1];
113115
expect(meta).toMatchObject({
114116
consoleMessage:
115-
"embedded run agent end: runId=run-1 isError=true model=claude sonnet 4 provider=anthropic]8;;https://evil.test error=connection refused",
117+
"embedded run agent end: runId=run-1 isError=true model=claude sonnet 4 provider=anthropic]8;;https://evil.test error=LLM request failed: connection refused by the provider endpoint. rawError=connection refused",
116118
});
117119
expect(meta?.consoleMessage).not.toContain("\n");
118120
expect(meta?.consoleMessage).not.toContain("\r");

src/agents/pi-embedded-subscribe.handlers.lifecycle.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
5050
const safeRunId = sanitizeForConsole(ctx.params.runId) ?? "-";
5151
const safeModel = sanitizeForConsole(lastAssistant.model) ?? "unknown";
5252
const safeProvider = sanitizeForConsole(lastAssistant.provider) ?? "unknown";
53+
const safeRawErrorPreview = sanitizeForConsole(observedError.rawErrorPreview);
54+
const rawErrorConsoleSuffix = safeRawErrorPreview ? ` rawError=${safeRawErrorPreview}` : "";
5355
ctx.log.warn("embedded run agent end", {
5456
event: "embedded_run_agent_end",
5557
tags: ["error_handling", "lifecycle", "agent_end", "assistant_error"],
@@ -60,7 +62,7 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
6062
model: lastAssistant.model,
6163
provider: lastAssistant.provider,
6264
...observedError,
63-
consoleMessage: `embedded run agent end: runId=${safeRunId} isError=true model=${safeModel} provider=${safeProvider} error=${safeErrorText}`,
65+
consoleMessage: `embedded run agent end: runId=${safeRunId} isError=true model=${safeModel} provider=${safeProvider} error=${safeErrorText}${rawErrorConsoleSuffix}`,
6466
});
6567
emitAgentEvent({
6668
runId: ctx.params.runId,

0 commit comments

Comments
 (0)