diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6d156cdd0..a60dc0321 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Bedrock requests that fail with `The pending stream has been canceled` after a stalled HTTP/2 connection not being retried automatically ([#10379](https://github.com/earendil-works/pi/issues/10379)) + ## [1.0.3] - 2026-10-05 ### Breaking Changes diff --git a/packages/ai/src/utils/retry.ts b/packages/ai/src/utils/retry.ts index 880dd0a4b..0a148485f 100644 --- a/packages/ai/src/utils/retry.ts +++ b/packages/ai/src/utils/retry.ts @@ -81,6 +81,9 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([ "stream ended before message_stop", "stream ended before a terminal response event", "http2 request did not get a response", + // Node ERR_HTTP2_STREAM_CANCEL: the HTTP/2 session died before the request was + // sent, e.g. after the Bedrock SDK's 5-minute session timeout (#10379). + "pending stream has been canceled", // Provider-requested retry delay cap failures should flow through the outer // retry policy so callers can surface/abort the backoff (#1123). diff --git a/packages/ai/test/retry.test.ts b/packages/ai/test/retry.test.ts index faf4a3295..a6ae9d610 100644 --- a/packages/ai/test/retry.test.ts +++ b/packages/ai/test/retry.test.ts @@ -62,6 +62,14 @@ describe("provider retry classification", () => { expect(isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage }))).toBe(true); }); + it.each(["The pending stream has been canceled", "The pending stream has been canceled (caused by: socket closed)"])( + "matches HTTP/2 pending stream cancellation: %s", + (errorMessage) => { + // Regression for #10379. + expect(isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage }))).toBe(true); + }, + ); + it("matches OpenAI Responses streams that end before terminal events", () => { expect( isRetryableAssistantError(