Skip to content

fix(session): SessionRetry.policy() retries forever with no max attempt count #21960

Description

@dangeReis

Bug

SessionRetry.policy() creates an Effect.Schedule that retries retryable errors (429, 529, overloaded) indefinitely with no maximum attempt count and no maximum total retry duration.

File: packages/opencode/src/session/retry.ts

Root Cause

The policy() function has exactly two branches:

  1. Stop: Cause.done(meta.attempt) — only when retryable() returns undefined (non-retryable error)
  2. Continue: [meta.attempt, Duration.millis(wait)] — whenever the error IS retryable

There is no check for meta.attempt >= MAX_RETRIES. Once an error is classified as retryable, the schedule will retry forever.

// retry.ts — policy()
const message = retryable(error)
if (!message) return Cause.done(meta.attempt)     // STOP (only for non-retryable)
return Effect.gen(function* () {
  const wait = delay(meta.attempt, ...)
  // ... set status ...
  return [meta.attempt, Duration.millis(wait)]    // CONTINUE (forever)
})

Impact

When a provider returns persistent 429/overloaded errors:

  1. OpenCode retries every 30s (backoff caps at RETRY_MAX_DELAY_NO_HEADERS = 30_000ms)
  2. Session status stays "retry"session.error and session.idle are never emitted
  3. Plugin consumers (oh-my-openagent) that rely on these events for task lifecycle management are stuck waiting indefinitely
  4. Tasks appear "running" for 30-45 minutes until a stale timeout fires (if one exists)

Observed behavior: 1,691 failed API calls to openai/gpt-5.3-codex-spark (unavailable model) in a single day, all returning errors, all retried forever with zero tokens generated.

Backoff Sequence

Without response headers: 2s → 4s → 8s → 16s → 30s → 30s → 30s → ... (caps at 30s per attempt, never stops)

With retry-after-ms header: individual delays up to RETRY_MAX_DELAY = 2,147,483,647ms (~24.8 days) per attempt.

Suggested Fix

Add a configurable max retry count to policy():

export const DEFAULT_MAX_RETRIES = 20

// At the top of the schedule step function:
if (meta.attempt >= (opts.maxRetries ?? DEFAULT_MAX_RETRIES)) {
  return Cause.done(meta.attempt)
}

This would allow the schedule to eventually terminate, halt() to run, and session.error to be emitted — giving consumers a clean error signal instead of an infinite wait.

Related Issues

Environment

  • OpenCode version: latest
  • Plugin: oh-my-openagent (background task manager)
  • Observed with: openai/gpt-5.3-codex-spark (unavailable), anthropic/claude-opus-4-6 (rate-limited)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions