Skip to content

Commit 6b3408f

Browse files
authored
Merge pull request #141 from browser-use/retry-budget-naming
refactor(opencode): name the retry cap for its cumulative scope
2 parents a08812e + 76a7a6c commit 6b3408f

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

‎packages/opencode/src/session/retry.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ export type RetryReason = "free_tier_limit" | "account_rate_limit" | (string & {
2323

2424
export 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

7983
export 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)

‎packages/opencode/test/session/retry.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,30 @@ describe("session.retry.delay", () => {
139139
expect(Exit.isFailure(third)).toBe(true)
140140
}),
141141
)
142+
143+
it.effect("earlier retries of another reason spend the output-length budget", () =>
144+
Effect.gen(function* () {
145+
// maxTotalAttempts is measured against one per-request counter, so a
146+
// request that already retried twice for rate limits gets no resample.
147+
let current: SessionRetry.Err = new SessionV1.APIError({
148+
message: "boom",
149+
isRetryable: true,
150+
responseHeaders: { "retry-after-ms": "0" },
151+
}).toObject()
152+
const step = yield* Schedule.toStep(
153+
SessionRetry.policy({
154+
provider: "test",
155+
parse: () => current,
156+
set: () => Effect.void,
157+
}),
158+
)
159+
160+
yield* step(0, current)
161+
yield* step(0, current)
162+
current = new SessionV1.OutputLengthError({}).toObject()
163+
expect(Exit.isFailure(yield* step(0, current).pipe(Effect.exit))).toBe(true)
164+
}),
165+
)
142166
})
143167

144168
describe("session.retry.retryable", () => {

0 commit comments

Comments
 (0)