{"repo": "agil-p-varghese/opencode-resume-agent", "files": {"packages/opencode/src/session/retry.ts": {"62a5f331bc0c9423065d21255e34324c47203bb9": {"t": 1785596382.699154, "n": 7152, "h": "0c7fcace557156bd", "c": "import type { NamedError } from \"@opencode-ai/core/util/error\"\nimport { SessionV1 } from \"@opencode-ai/core/v1/session\"\nimport { Cause, Clock, Duration, Effect, Schedule } from \"effect\"\nimport { MessageV2 } from \"./message-v2\"\nimport { iife } from \"@/util/iife\"\nimport { isRecord } from \"@/util/record\"\n\nexport type Err = ReturnType\n\nexport const GO_UPSELL_MESSAGE = \"Free usage exceeded, subscribe to Go\"\nexport const GO_UPSELL_URL = \"https://opencode.ai/go\"\nexport type RetryReason = \"free_tier_limit\" | \"account_rate_limit\" | (string & {})\n\nexport type Retryable = {\n message: string\n action?: {\n reason: RetryReason\n provider: string\n title: string\n message: string\n label: string\n link?: string\n }\n}\n\nexport const RETRY_INITIAL_DELAY = 2000\nexport const RETRY_BACKOFF_FACTOR = 2\nexport const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds\nexport const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout\n\nfunction cap(ms: number) {\n return Math.min(ms, RETRY_MAX_DELAY)\n}\n\nexport function delay(attempt: number, error?: SessionV1.APIError) {\n if (error) {\n const headers = error.data.responseHeaders\n if (headers) {\n const retryAfterMs = headers[\"retry-after-ms\"]\n if (retryAfterMs) {\n const parsedMs = Number.parseFloat(retryAfterMs)\n if (!Number.isNaN(parsedMs)) {\n return cap(parsedMs)\n }\n }\n\n const retryAfter = headers[\"retry-after\"]\n if (retryAfter) {\n const parsedSeconds = Number.parseFloat(retryAfter)\n if (!Number.isNaN(parsedSeconds)) {\n // convert seconds to milliseconds\n return cap(Math.ceil(parsedSeconds * 1000))\n }\n // Try parsing as HTTP date format\n const parsed = Date.parse(retryAfter) - Date.now()\n if (!Number.isNaN(parsed) && parsed > 0) {\n return cap(Math.ceil(parsed))\n }\n }\n\n return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1))\n }\n }\n\n return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS))\n}\n\nexport function retryable(error: Err, provider: string) {\n // context overflow errors should not be retried\n if (SessionV1.ContextOverflowError.isInstance(error)) return undefined\n if (SessionV1.APIError.isInstance(error)) {\n const status = error.data.statusCode\n // 5xx errors are transient server failures and should always be retried,\n // even when the provider SDK doesn't explicitly mark them as retryable.\n if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined\n if (error.data.responseBody?.includes(\"FreeUsageLimitError\")) {\n return {\n message: GO_UPSELL_MESSAGE,\n action: {\n reason: \"free_tier_limit\",\n provider,\n title: \"Free limit reached\",\n message: \"Subscribe to OpenCode Go for reliable access to the best open-source models, starting at $5/month.\",\n label: \"subscribe\",\n link: GO_UPSELL_URL,\n },\n }\n }\n if (error.data.responseBody?.includes(\"GoUsageLimitError\")) {\n const body = parseJSON(error.data.responseBody)\n const workspace = str(body?.metadata?.workspace)\n const limitName = str(body?.metadata?.limitName)\n const retryAfter = num(error.data.responseHeaders?.[\"retry-after\"])\n const resetIn = iife(() => {\n if (retryAfter === undefined) return \"\"\n const seconds = Math.max(0, Math.ceil(retryAfter))\n const days = Math.floor(seconds / 86_400)\n const hours = Math.floor((seconds % 86_400) / 3_600)\n const minutes = Math.ceil((seconds % 3_600) / 60)\n const unit = (value: number, name: string) => `${value} ${name}${value === 1 ? \"\" : \"s\"}`\n\n if (days > 0) return hours > 0 ? `${unit(days, \"day\")} ${unit(hours, \"hour\")}` : unit(days, \"day\")\n if (hours > 0) return minutes > 0 ? `${unit(hours, \"hour\")} ${unit(minutes, \"minute\")}` : unit(hours, \"hour\")\n return minutes > 0 ? unit(minutes, \"minute\") : \"less than a minute\"\n })\n\n const message = `${limitName ? `${limitName} usage limit` : \"Usage limit\"} reached. It will reset in ${resetIn}. To continue using this model now, enable usage from your available balance`\n\n const link = `https://opencode.ai/workspace/${workspace}/go`\n return {\n message: `${message} - ${link}`,\n action: {\n reason: \"account_rate_limit\",\n provider,\n title: \"Go limit reached\",\n message,\n label: \"open settings\",\n link,\n },\n }\n }\n return { message: error.data.message.includes(\"Overloaded\") ? \"Provider is overloaded\" : error.data.message }\n }\n\n // Check for rate limit patterns in plain text error messages\n const msg = isRecord(error.data) ? error.data.message : undefined\n if (typeof msg === \"string\") {\n const lower = msg.toLowerCase()\n if (\n lower.includes(\"rate increased too quickly\") ||\n lower.includes(\"rate limit\") ||\n lower.includes(\"too many requests\")\n ) {\n return { message: msg }\n }\n }\n\n const json = parseJSON(msg)\n if (!json || typeof json !== \"object\") return undefined\n const code = typeof json.code === \"string\" ? json.code : \"\"\n\n if (json.type === \"error\" && json.error?.type === \"too_many_requests\") {\n return { message: \"Too Many Requests\" }\n }\n if (code.includes(\"exhausted\") || code.includes(\"unavailable\")) {\n return { message: \"Provider is overloaded\" }\n }\n if (json.type === \"error\" && typeof json.error?.code === \"string\" && json.error.code.includes(\"rate_limit\")) {\n return { message: \"Rate Limited\" }\n }\n return undefined\n}\n\nfunction str(value: unknown) {\n if (value === undefined || value === null) return \"\"\n return String(value)\n}\n\nfunction num(value: unknown) {\n const parsed = Number.parseFloat(str(value))\n if (Number.isNaN(parsed)) return undefined\n return parsed\n}\n\nfunction parseJSON(value: unknown) {\n return iife(() => {\n try {\n if (typeof value !== \"string\") return undefined\n return JSON.parse(value)\n } catch {\n return undefined\n }\n })\n}\n\nexport function policy(opts: {\n provider: string\n parse: (error: unknown) => Err\n set: (input: { attempt: number; message: string; action?: Retryable[\"action\"]; next: number }) => Effect.Effect\n}) {\n return Schedule.fromStepWithMetadata(\n Effect.succeed((meta: Schedule.InputMetadata) => {\n const error = opts.parse(meta.input)\n const retry = retryable(error, opts.provider)\n if (!retry) return Cause.done(meta.attempt)\n return Effect.gen(function* () {\n const wait = delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined)\n const now = yield* Clock.currentTimeMillis\n yield* opts.set({\n attempt: meta.attempt,\n message: retry.message,\n action: retry.action,\n next: now + wait,\n })\n return [meta.attempt, Duration.millis(wait)] as [number, Duration.Duration]\n })\n }),\n )\n}\n\nexport * as SessionRetry from \"./retry\"\n"}}}}