npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

opencode-retry-forever

v0.1.0

Published

Keeps long-running opencode sessions alive by retrying transient provider failures (503, DNS, connection resets) forever, with no backoff.

Readme

opencode-retry-forever

Keeps long-running opencode sessions alive across transient provider failures.

If you leave a fleet of agents running for hours, a single upstream hiccup kills a whole session. opencode's built-in retry gives up after a handful of attempts, and then SessionRunner.drain throws:

level=ERROR message="Failed to drain Session" cause="AI.Error: RequestExecutor.execute: Provider request failed with HTTP 503"
level=ERROR message="Failed to drain Session" cause="AI.Error: RequestExecutor.request: getaddrinfo ENOTFOUND opencode.ai"
level=ERROR error.error="AI_RetryError: Failed after 3 attempts. Last error: Service Unavailable"

This plugin wraps globalThis.fetch — which is what opencode's request executor ultimately calls (effect/http/FetchHttpClient/Fetch defaults to () => globalThis.fetch) — so retries happen below the session runner. The session never learns that anything went wrong.

Install

mkdir -p ~/.config/opencode/plugin
curl -fsSL https://raw.githubusercontent.com/aryanranderiya/opencode-retry-forever/main/index.ts \
  -o ~/.config/opencode/plugin/retry-forever.ts

Or, once published to npm:

opencode plugin opencode-retry-forever -g

Per-project instead of global: drop the same file in .opencode/plugin/ inside the project. It loads on the next opencode start; opencode run --pure bypasses it.

Configuration

All optional, read from the environment at startup.

| Variable | Default | Meaning | | --- | --- | --- | | OPENCODE_RETRY_DELAY_MS | 250 | Fixed wait between attempts. No backoff, no growth. Set 0 for an immediate hot retry. | | OPENCODE_RETRY_MAX_ATTEMPTS | 0 | 0 means unlimited — retry until it gets through. | | OPENCODE_RETRY_HONOR_RETRY_AFTER | 1 | Respect a Retry-After header when the server sends one. Turning this off is a good way to get rate limited harder. | | OPENCODE_RETRY_MAX_RETRY_AFTER_MS | 60000 | Ceiling on how long a Retry-After may park you. | | OPENCODE_RETRY_VERBOSE | 1 | Log every retry. Keep this on — an unlimited retry loop that logs nothing is indistinguishable from a hang. |

What gets retried

  • HTTP status: 408, 409, 425, 429, 500, 502, 503, 504, 520–525, 527, 529, 530.
  • Thrown network errors, in both vocabularies that matter: Node/undici POSIX codes (ENOTFOUND, ECONNRESET, ETIMEDOUT, …) and Bun's own codes (ConnectionRefused, FailedToOpenSocket, DNSResolutionFailed, ConnectionClosed, ConnectionTimeout, IdleTimeout). opencode runs on Bun, and matching only the POSIX set silently misses every connect failure — the same blind spot that makes opencode's own retry give up on an unreachable provider.

Aborts are never retried: hitting Esc stops the loop immediately, including mid-wait. That guard is load-bearing, because a cancelled in-flight request can surface as ECONNRESET or terminated, both of which otherwise look retryable.

Request bodies are buffered once and replayed verbatim on every attempt, so Request objects and ReadableStream bodies survive a retry instead of failing as already-consumed.

Auto-resume: turns that die with an unknown finish reason

A transport retry cannot save a turn the provider ended mid-flight — the request succeeded, the stream opened, then stopped with finish: "unknown":

⚠ Retry attempt 2 scheduled: The provider response ended with an unknown finish reason.
⚠ Retry attempt 3 scheduled: The provider response ended with an unknown finish reason.
Error: Provider request failed with HTTP 503

opencode schedules a couple of retries and then gives up, taking the session with it. This plugin watches for that specific ending and re-sends the user's own prompt, indefinitely.

It only fires on a genuine drop. A turn that produced output and then stopped without a reason is a drop; a zero-token unknown finish is a deliberate abort — an interrupt, a denied permission, a compaction — and resending there would fight you.

| Variable | Default | Meaning | | --- | --- | --- | | OPENCODE_RESUME_PROVIDER | opencode-go | Provider to watch. * matches every provider. | | OPENCODE_RESUME_MODEL | ox-alpha-free | Model prefix to watch. * matches every model. | | OPENCODE_RESUME_DELAY_MS | 3000 | Wait before re-sending. | | OPENCODE_RESUME_MAX_ATTEMPTS | 0 | 0 means resend until the turn completes. |

What does not get retried

  • Failures mid-response-stream are handled by auto-resume above, not by the fetch retry: once the provider returns 200 and starts streaming, fetch has already resolved.
  • Non-transient statuses (400, 401, 403, 404, …) pass straight through.

opencode 1.x vs 2.x

The plugin loads on both — the default export carries id, server (1.x) and setup (2.x). What runs differs sharply, because opencode 2.x never calls server at all, only setup:

| | 1.x | 2.x beta | | --- | --- | --- | | Fetch-level retry (503, DNS, resets) | works | inert — v2 keeps provider traffic away from the plugin's globalThis.fetch | | Language-model retry | n/a | only for providers that register aisdk.hook("language") | | Auto-resume | via the event hook | via event.subscribe() + session.prompt() |

On 2.x the transport is simply not reachable from a plugin: session.hook("http.request") can mutate a request but never sees its response. Recovery therefore happens one level up, on session.execution.failed — the terminal event, raised once opencode's own retries are spent. A turn you stop yourself raises session.execution.interrupted instead, so resuming cannot fight an interrupt.

Verified against a provider that opens a stream, emits text, then dies with no finish chunk — 20 consecutive drops, which is well past opencode's own budget of about six turns:

| | resumer disabled | enabled | | --- | --- | --- | | provider turns | 6 | 22 | | outcome | session dead | recovered on turn 21 |

Under 2.x, opencode runs a long-lived background service that loads plugins once at startup, so a plugin change is not picked up until opencode2 service restart.

License

MIT