@@ -23,7 +23,11 @@ export type RetryReason = "free_tier_limit" | "account_rate_limit" | (string & {
2323
2424export type Retryable = {
2525 message : string
26- maxAttempts ?: number
26+ // Give up once the schedule's CUMULATIVE attempt count reaches this. One
27+ // counter covers the whole request, so earlier retries of ANY reason spend
28+ // this budget: two rate-limit retries followed by an output-limit error
29+ // leaves the output-limit path none. Reasons that omit it are unbounded.
30+ maxTotalAttempts ?: number
2731 action ?: {
2832 reason : RetryReason
2933 provider : string
@@ -78,7 +82,7 @@ export function delay(attempt: number, error?: SessionV1.APIError) {
7882
7983export function retryable ( error : Err , provider : string ) {
8084 if ( SessionV1 . OutputLengthError . isInstance ( error ) ) {
81- return { message : "Model hit its output limit" , maxAttempts : 3 }
85+ return { message : "Model hit its output limit" , maxTotalAttempts : 3 }
8286 }
8387 // context overflow errors should not be retried
8488 if ( SessionV1 . ContextOverflowError . isInstance ( error ) ) return undefined
@@ -198,7 +202,10 @@ export function policy(opts: {
198202 const error = opts . parse ( meta . input )
199203 const retry = retryable ( error , opts . provider )
200204 if ( ! retry ) return Cause . done ( meta . attempt )
201- if ( retry . maxAttempts !== undefined && meta . attempt >= retry . maxAttempts ) return Cause . done ( meta . attempt )
205+ // `meta.attempt` is 1-based and shared by every reason (Schedule keeps one
206+ // counter per request), which is why the cap reads as a total.
207+ if ( retry . maxTotalAttempts !== undefined && meta . attempt >= retry . maxTotalAttempts )
208+ return Cause . done ( meta . attempt )
202209 return Effect . gen ( function * ( ) {
203210 if ( opts . onRetry ) yield * opts . onRetry ( error )
204211 const wait = delay ( meta . attempt , SessionV1 . APIError . isInstance ( error ) ? error : undefined )
0 commit comments