@@ -62,18 +62,29 @@ export function delay(attempt: number, error?: MessageV2.APIError) {
6262 return cap ( Math . min ( RETRY_INITIAL_DELAY * Math . pow ( RETRY_BACKOFF_FACTOR , attempt - 1 ) , RETRY_MAX_DELAY_NO_HEADERS ) )
6363}
6464
65- export function retryable ( error : Err ) {
65+ // Branch order matches legacy retryable(): ContextOverflow -> APIError -> plain-text
66+ // rate-limit -> transport -> JSON. An error message like "rate limit exceeded
67+ // (ETIMEDOUT during retry)" must stay classified as rate-limit (not transport)
68+ // for message semantics, but we still honor TRANSPORT_RETRY_CAP via isTransport
69+ // when the message also matches a transport pattern.
70+ export function classify ( error : Err ) {
6671 // context overflow errors should not be retried
6772 if ( MessageV2 . ContextOverflowError . isInstance ( error ) ) return undefined
6873 if ( MessageV2 . APIError . isInstance ( error ) ) {
6974 const status = error . data . statusCode
7075 // 5xx errors are transient server failures and should always be retried,
7176 // even when the provider SDK doesn't explicitly mark them as retryable.
7277 if ( ! error . data . isRetryable && ! ( status !== undefined && status >= 500 ) ) return undefined
73- if ( error . data . responseBody ?. includes ( "FreeUsageLimitError" ) ) return GO_UPSELL_MESSAGE
74- return error . data . message . includes ( "Overloaded" ) ? "Provider is overloaded" : error . data . message
78+ if ( error . data . responseBody ?. includes ( "FreeUsageLimitError" ) ) {
79+ return { message : GO_UPSELL_MESSAGE }
80+ }
81+ return {
82+ message : error . data . message . includes ( "Overloaded" ) ? "Provider is overloaded" : error . data . message ,
83+ }
7584 }
7685
86+ const transport = transportMessage ( error )
87+
7788 // Check for rate limit patterns in plain text error messages
7889 const msg = error . data ?. message
7990 if ( typeof msg === "string" ) {
@@ -83,12 +94,12 @@ export function retryable(error: Err) {
8394 lower . includes ( "rate limit" ) ||
8495 lower . includes ( "too many requests" )
8596 ) {
86- return msg
97+ if ( transport ) return { message : msg , isTransport : true as const }
98+ return { message : msg }
8799 }
88100 }
89101
90- const transport = transportMessage ( error )
91- if ( transport ) return transport
102+ if ( transport ) return { message : transport , isTransport : true as const }
92103
93104 const json = iife ( ( ) => {
94105 try {
@@ -106,34 +117,38 @@ export function retryable(error: Err) {
106117 const code = typeof json . code === "string" ? json . code : ""
107118
108119 if ( json . type === "error" && json . error ?. type === "too_many_requests" ) {
109- return "Too Many Requests"
120+ return { message : "Too Many Requests" }
110121 }
111122 if ( code . includes ( "exhausted" ) || code . includes ( "unavailable" ) ) {
112- return "Provider is overloaded"
123+ return { message : "Provider is overloaded" }
113124 }
114125 if ( json . type === "error" && typeof json . error ?. code === "string" && json . error . code . includes ( "rate_limit" ) ) {
115- return "Rate Limited"
126+ return { message : "Rate Limited" }
116127 }
117128 return undefined
118129}
119130
131+ // Kept to avoid churning the existing retry.test.ts suite. Prefer classify() in new code.
132+ export function retryable ( error : Err ) {
133+ return classify ( error ) ?. message
134+ }
135+
120136export function policy ( opts : {
121137 parse : ( error : unknown ) => Err
122138 set : ( input : { attempt : number ; message : string ; next : number } ) => Effect . Effect < void >
123139} ) {
124140 return Schedule . fromStepWithMetadata (
125141 Effect . succeed ( ( meta : Schedule . InputMetadata < unknown > ) => {
126142 const error = opts . parse ( meta . input )
127- const message = retryable ( error )
128- const transport = transportMessage ( error )
129- if ( ! message ) return Cause . done ( meta . attempt )
130- if ( transport && ! MessageV2 . APIError . isInstance ( error ) && meta . attempt > TRANSPORT_RETRY_CAP ) {
143+ const c = classify ( error )
144+ if ( ! c ) return Cause . done ( meta . attempt )
145+ if ( c . isTransport && ! MessageV2 . APIError . isInstance ( error ) && meta . attempt > TRANSPORT_RETRY_CAP ) {
131146 return Cause . done ( meta . attempt )
132147 }
133148 return Effect . gen ( function * ( ) {
134149 const wait = delay ( meta . attempt , MessageV2 . APIError . isInstance ( error ) ? error : undefined )
135150 const now = yield * Clock . currentTimeMillis
136- yield * opts . set ( { attempt : meta . attempt , message, next : now + wait } )
151+ yield * opts . set ( { attempt : meta . attempt , message : c . message , next : now + wait } )
137152 return [ meta . attempt , Duration . millis ( wait ) ] as [ number , Duration . Duration ]
138153 } )
139154 } ) ,
0 commit comments