anomalyco / anomalyco/opencode
Configurable retry policy: expose maxRetries / initialDelay / backoffFactor / maxDelay via config
@nexxeln is already working on this.
Since Aug 20, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Configurable retry policy: expose maxRetries / initialDelay / backoffFactor / maxDelay via config
Summary
RETRY_MAX_RETRIES = 5 (introduced in #41939 / c789868) is hardcoded. For providers with long quota windows, 5 retries (~68s total) abort the turn while the error is still transient — the quota window simply hasn't reset yet. Please expose the retry constants via config so users can tune or uncap them.
Background
Before c789868 (v1.18.x, 2026-08-12), retryable errors retried indefinitely with exponential backoff. After it, packages/opencode/src/session/retry.ts caps at 5 retries:
export const RETRY_MAX_RETRIES = 5
...
if (meta.attempt > RETRY_MAX_RETRIES) return Cause.done(meta.attempt)
This is a reasonable default. But it breaks a class of providers whose "transient" errors last much longer than 68 seconds.
Real-world case
Zhipu GLM coding plan (zhipuai-coding-plan) enforces a 5-hour usage window. When exhausted, the API returns a 429-class error:
AI_APICallError: 已达到 5 小时的使用上限。您的限额将在 2026-08-19 02:14:39 重置。
("5-hour usage limit reached. Your quota resets at 2026-08-19 02:14:39.")
Observed timeline (opencode 1.18.18, from ~/.local/share/opencode/log/opencode.log):
17:00:02 attempt 1 → 429 limit error
17:00:04 attempt 2 (+2.1s)
17:00:09 attempt 3 (+4.5s)
17:00:17 attempt 4 (+8.7s)
17:00:34 attempt 5 (+17.1s)
17:01:10 attempt 6 (+35.8s)
17:01:10 process error → turn aborted
Six attempts in 68 seconds, then the session aborts. The error itself says the quota resets hours later. Before the cap, opencode kept backing off (2s → 4s → ... → tens of minutes per attempt) and automatically resumed once the window reset — which is exactly the desired behavior for this provider class. Several coding-plan providers (Zhipu, and similar subscription-window plans) share this pattern.
Note the delay machinery already handles this well: delay() honors retry-after/retry-after-ms headers, and the exponential backoff caps at RETRY_MAX_DELAY (max int32). The binding constraint is the attempt count, not the delay.
Proposal
Expose the existing constants via config, defaults unchanged:
{
"experimental": {
"retry": {
"maxRetries": 5, // default unchanged; -1 or "unlimited" = no cap
"initialDelayMs": 2000,
"backoffFactor": 2, // e.g. 1.35 for a gentler curve
"jitterFactor": 0.25,
"maxDelayMs": 2147483647,
"maxDelayNoHeadersMs": 30000
}
}
}
- Zero behavior change unless configured.
- A per-provider override (
provider.<id>.retry) would be a nice follow-up, but a global knob solves the immediate problem. - Alternatively/additionally: treat a retryable error whose stated reset time (or
retry-after) exceeds the total backoff budget as "keep waiting" rather than "give up".
Why not just bump the default?
5 is a sensible default for genuinely transient errors (network blips, brief 429 spikes). The problem is heterogeneity: one user's "transient" is 30 seconds, another's is 5 hours. A config knob is the minimal fix that serves both without regressing the #41939 motivation.
Environment
- opencode 1.18.18 (npm, linux-x64)
- provider:
zhipuai-coding-plan, model glm-5.3 - log excerpt above
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.