Skip to content

fix(retry): honor minute/hour and compound retry-after hints#1052

Open
ozpool wants to merge 2 commits into
Soju06:mainfrom
ozpool:fix/retry-after-minute-units
Open

fix(retry): honor minute/hour and compound retry-after hints#1052
ozpool wants to merge 2 commits into
Soju06:mainfrom
ozpool:fix/retry-after-minute-units

Conversation

@ozpool

@ozpool ozpool commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Closes #1051.

Problem

parse_retry_after only matched s / ms units:

_RETRY_PATTERN = re.compile(r"(?i)try again in\s*(\d+(?:\.\d+)?)\s*(s|ms|seconds?)")

Upstream rate-limit messages that express the cooldown in minutes or hours (Please try again in 6m0s., Try again in 20m, compound 1h2m3s) failed to match, so the function returned None. The sole caller handle_rate_limit then fell back to backoff_seconds(state.error_count) (~0.2s for a first error), re-selecting an account the upstream asked us to pause for minutes — amplifying 429s against a still-rate-limited account.

Fix

Parse the full contiguous run of <number><unit> components that follows "try again in" and sum them to total seconds. Handles h/m/s/ms and their word forms (hours, minutes, seconds, milliseconds), including compound Go-duration strings.

input before after
Try again in 1.2s 1.2 1.2
Try again in 500ms 0.5 0.5
Try again in 20m None 1200.0
Please try again in 6m0s. None 360.0
Try again in 1m30s None 90.0
Try again in 1h2m3s None 3723.0
no retry info None None

Tests

Added minute, compound minute+second, compound-hour, and word-unit cases to tests/unit/test_retry.py. Existing second/millisecond/missing cases unchanged. ruff check + ruff format --check clean.

parse_retry_after only matched s/ms units, so upstream rate-limit
messages expressed in minutes or hours (e.g. "try again in 6m0s",
"try again in 20m") failed to match and returned None. handle_rate_limit
then fell back to sub-second exponential backoff and re-selected the
still-rate-limited account almost immediately.

Parse the full contiguous run of <number><unit> components after the
"try again in" hint, summing hours/minutes/seconds/milliseconds (plus
their word forms) to total seconds.
@Soju06

Soju06 commented Jun 18, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

def parse_retry_after(message: str) -> float | None:

P1 Badge Add OpenSpec coverage for retry cooldown behavior

This commit changes the retry/cooldown contract by teaching parse_retry_after to honor minute/hour and compound upstream hints, but I checked the commit diff and it only modifies app/core/utils/retry.py plus tests with no openspec/changes/<slug>/ artifact. That violates /workspace/codex-lb/AGENTS.md lines 90-96: “OpenSpec is a hard gate for behavior... changes” and “Code/tests alone are not enough when OpenSpec is required,” so the PR is not ready until the behavior change is captured and validated in OpenSpec.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/core/utils/retry.py
Comment on lines +35 to +36
_RETRY_PATTERN = re.compile(rf"(?i)try again in\s*((?:\d+(?:\.\d+)?\s*(?:{_UNIT_ALTERNATION})\s*)+)")
_DURATION_TOKEN = re.compile(rf"(?i)(\d+(?:\.\d+)?)\s*({_UNIT_ALTERNATION})")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent longer units from being parsed as minutes

Because the unit alternation is not followed by a token boundary, unsupported longer units that begin with a supported compact unit are partially accepted; for example, parse_retry_after("Try again in 2 months") now matches the leading 2m and returns 120.0. If an upstream monthly/quota message uses month(s), handle_rate_limit will retry after minutes instead of respecting the larger hint or falling back, so add a boundary/negative lookahead around units while preserving compact forms like 1m30s.

Useful? React with 👍 / 👎.

@Soju06 Soju06 added the 🤖 codex: needs work [@codex review] raised an issue label Jun 18, 2026
The retry/cooldown contract change in this PR (parse_retry_after now honors
minute/hour and compound upstream hints) needs an OpenSpec change per
AGENTS.md. Adds openspec/changes/honor-upstream-retry-after-hint-units/ with
the proposal, tasks, and an ADDED account-routing requirement plus scenarios
covering compound, minute-only, and unparseable retry hints.
@ozpool

ozpool commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Good catch on the missing OpenSpec artifact. Added openspec/changes/honor-upstream-retry-after-hint-units/ with a proposal, tasks, and an ADDED requirement under account-routing (the capability that owns the cooldown gate handle_rate_limit feeds). The requirement is normative on the cooldown lasting the full hint duration, with GIVEN/WHEN/THEN scenarios for a compound 6m0s hint (360s), a minute-only 20m hint (1200s), and the unparseable-hint backoff fallback. Layout mirrors the existing change folders.

ruff check + ruff format --check clean, tests/unit/test_retry.py green.

@Soju06

Soju06 commented Jun 19, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5cd08c2f5d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/unit/test_retry.py
Comment on lines +26 to +28
def test_parse_retry_after_compound_minutes_seconds():
assert parse_retry_after("Please try again in 6m0s.") == 360.0
assert parse_retry_after("Try again in 1m30s") == 90.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add product-path coverage for cooldown handling

AGENTS.md's PR trapdoor requires bug fixes to add regression coverage at the externally failing product path, but this change only tests the parsing helper. Because the actual failure is handle_rate_limit setting cooldown_until and select_account skipping that account, a future wiring regression where the parsed minute/compound delay is ignored would still pass these tests; add a load-balancer-level test for a 6m0s/20m 429 that verifies the cooldown and non-reselection behavior.

Useful? React with 👍 / 👎.

Comment thread app/core/utils/retry.py
# Capture the contiguous run of ``<number><unit>`` components that immediately
# follows "try again in" so compound hints such as ``6m0s`` or ``1h2m3s`` are
# read in full instead of stopping at the first unit.
_RETRY_PATTERN = re.compile(rf"(?i)try again in\s*((?:\d+(?:\.\d+)?\s*(?:{_UNIT_ALTERNATION})\s*)+)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Delimit unit matches before accepting abbreviations

When an upstream hint uses an unsupported word that starts with a supported one-letter unit, such as try again in 1 month, this pattern accepts only the leading m and schedules a 60-second cooldown instead of treating the hint as unparseable. That bypasses the new fallback promised for hints with no recognizable unit token and can reselect an account far earlier than the upstream intended; require a token boundary/negative lookahead around the unit alternatives so abbreviations only match complete units.

Useful? React with 👍 / 👎.

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

Labels

🤖 codex: needs work [@codex review] raised an issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

parse_retry_after drops minute/hour retry hints, collapsing account cooldown to sub-second backoff

2 participants