Skip to content

fix(core): cap gateway timeouts and log scheduled retries - #54544

Closed
FahadBinHussain wants to merge 2 commits into
anomalyco:devfrom
FahadBinHussain:timeout-retry-cap
Closed

FahadBinHussain wants to merge 2 commits into
anomalyco:devfrom
FahadBinHussain:timeout-retry-cap

Conversation

@FahadBinHussain

@FahadBinHussain FahadBinHussain commented Oct 11, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #54538

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

the timeout retry cap already existed (MAX_TIMEOUT_RETRIES = 3 in SessionRunnerRetry), but it only fired for local transport timeouts (Transport + code === "Timeout"). gateway-reported provider timeouts come back as ordinary failures instead: opencode zen answers a dead upstream in-band with Streaming response failed: [504] Provider timed out after 362397ms, which classifies as ProviderInternal, so those attempts took the full general allowance — ~10 retries of ~6 minutes each, about an hour of a session stuck busy while every api call returns SessionBusyError. that is exactly the outcome the cap's own comment says it exists to prevent.

this changes isTimeout to also match http statuses 408/504 and a timeout phrase in the message, restricted to the ProviderInternal/UnknownProvider shapes gateway failures arrive in, so the heuristic does not swallow websocket idle/connect timeouts or unrelated messages. fast failures like 502 and rate limits keep the general allowance, since retrying those costs milliseconds, while each 504 already burned minutes.

it also adds one warning log line per scheduled retry (attempt, delay, error type, message). previously a hang-then-recover loop left zero trace in the logs — only the final drain failure logs — which made the failure in #54538 take a day to pin down.

How did you verify your code works?

packages/core/test/session-runner-retry.test.ts covers the gateway 504 message, a 504 status without a timeout message, and the negative controls (502, plain internal, websocket idle timeout) keeping the full allowance. the new gateway cases fail against base and pass with the PR, reviewed by the automated retry-policy check on this PR. bun run check passes (36/36).

Screenshots / recordings

not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks sound. I checked it by running the retry policy tests against the PR and against base's retry.ts: the new gateway-timeout cases fail on base and pass with the PR. packages/core type-checks.

Public API is unchanged: the edit is internal retry policy plus a log line.

One small inline point on isTimeout: the Timeout tag check is unreachable, and the message check could be narrowed. Nothing blocking.

Fixes #54538
Related: #45514, #49030, #53747

// upstream in-band with `Streaming response failed: [504] Provider timed out after Nms`, and the
// status never reaches the reason. The status and message checks keep those attempts on the same
// cap as local transport timeouts instead of the full general allowance.
const isTimeout = (error: AIError) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small things here:

  • error.reason._tag === "Timeout" can never take effect. isRetryable returns false for Timeout, so input.retry is already false and the cap never applies. It can be dropped.
  • The message regex runs on every reason type, not just the in-band gateway failures this targets. Today it also catches e.g. the Session WebSocket idle-timeout / connect-timeout transport errors ("Timed out waiting for WebSocket data"), and any RateLimit or other message that happens to mention a timeout. Limiting the text match to ProviderInternal / UnknownProvider (the shapes the Zen 504 arrives in) would keep the heuristic to the case it's meant for.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both addressed in c40ef3f - dropped the Timeout tag check and restricted the status/message match to ProviderInternal/UnknownProvider, so websocket idle/connect timeouts and rate-limit messages stay on the general allowance. added a regression test for the websocket idle-timeout shape.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I checked c40ef3f: the unreachable Timeout tag check is gone, and the 408/504 status and message checks now apply only to ProviderInternal/UnknownProvider errors. Local Transport timeouts still count toward the cap. The new idle-timeout test is a useful guard, and session-runner-retry.test.ts passes (6/6) on the new head. That covers my inline point.

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

thread payload hangs every request ~325s -> [504] Provider timed out, retry loop wedges the session, no circuit breaker

1 participant