refactor(session): effectify session processor (#19485)
This commit is contained in:
@@ -1,28 +1,18 @@
|
||||
import type { NamedError } from "@opencode-ai/util/error"
|
||||
import { Cause, Clock, Duration, Effect, Schedule } from "effect"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
export namespace SessionRetry {
|
||||
export type Err = ReturnType<NamedError["toObject"]>
|
||||
|
||||
export const RETRY_INITIAL_DELAY = 2000
|
||||
export const RETRY_BACKOFF_FACTOR = 2
|
||||
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
|
||||
export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
|
||||
|
||||
export async function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const abortHandler = () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new DOMException("Aborted", "AbortError"))
|
||||
}
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
signal.removeEventListener("abort", abortHandler)
|
||||
resolve()
|
||||
},
|
||||
Math.min(ms, RETRY_MAX_DELAY),
|
||||
)
|
||||
signal.addEventListener("abort", abortHandler, { once: true })
|
||||
})
|
||||
function cap(ms: number) {
|
||||
return Math.min(ms, RETRY_MAX_DELAY)
|
||||
}
|
||||
|
||||
export function delay(attempt: number, error?: MessageV2.APIError) {
|
||||
@@ -33,7 +23,7 @@ export namespace SessionRetry {
|
||||
if (retryAfterMs) {
|
||||
const parsedMs = Number.parseFloat(retryAfterMs)
|
||||
if (!Number.isNaN(parsedMs)) {
|
||||
return parsedMs
|
||||
return cap(parsedMs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,23 +32,23 @@ export namespace SessionRetry {
|
||||
const parsedSeconds = Number.parseFloat(retryAfter)
|
||||
if (!Number.isNaN(parsedSeconds)) {
|
||||
// convert seconds to milliseconds
|
||||
return Math.ceil(parsedSeconds * 1000)
|
||||
return cap(Math.ceil(parsedSeconds * 1000))
|
||||
}
|
||||
// Try parsing as HTTP date format
|
||||
const parsed = Date.parse(retryAfter) - Date.now()
|
||||
if (!Number.isNaN(parsed) && parsed > 0) {
|
||||
return Math.ceil(parsed)
|
||||
return cap(Math.ceil(parsed))
|
||||
}
|
||||
}
|
||||
|
||||
return RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)
|
||||
return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1))
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS)
|
||||
return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS))
|
||||
}
|
||||
|
||||
export function retryable(error: ReturnType<NamedError["toObject"]>) {
|
||||
export function retryable(error: Err) {
|
||||
// context overflow errors should not be retried
|
||||
if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
|
||||
if (MessageV2.APIError.isInstance(error)) {
|
||||
@@ -80,22 +70,37 @@ export namespace SessionRetry {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
try {
|
||||
if (!json || typeof json !== "object") return undefined
|
||||
const code = typeof json.code === "string" ? json.code : ""
|
||||
if (!json || typeof json !== "object") return undefined
|
||||
const code = typeof json.code === "string" ? json.code : ""
|
||||
|
||||
if (json.type === "error" && json.error?.type === "too_many_requests") {
|
||||
return "Too Many Requests"
|
||||
}
|
||||
if (code.includes("exhausted") || code.includes("unavailable")) {
|
||||
return "Provider is overloaded"
|
||||
}
|
||||
if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
|
||||
return "Rate Limited"
|
||||
}
|
||||
return JSON.stringify(json)
|
||||
} catch {
|
||||
return undefined
|
||||
if (json.type === "error" && json.error?.type === "too_many_requests") {
|
||||
return "Too Many Requests"
|
||||
}
|
||||
if (code.includes("exhausted") || code.includes("unavailable")) {
|
||||
return "Provider is overloaded"
|
||||
}
|
||||
if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
|
||||
return "Rate Limited"
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function policy(opts: {
|
||||
parse: (error: unknown) => Err
|
||||
set: (input: { attempt: number; message: string; next: number }) => Effect.Effect<void>
|
||||
}) {
|
||||
return Schedule.fromStepWithMetadata(
|
||||
Effect.succeed((meta: Schedule.InputMetadata<unknown>) => {
|
||||
const error = opts.parse(meta.input)
|
||||
const message = retryable(error)
|
||||
if (!message) return Cause.done(meta.attempt)
|
||||
return Effect.gen(function* () {
|
||||
const wait = delay(meta.attempt, MessageV2.APIError.isInstance(error) ? error : undefined)
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
yield* opts.set({ attempt: meta.attempt, message, next: now + wait })
|
||||
return [meta.attempt, Duration.millis(wait)] as [number, Duration.Duration]
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user