Skip to content

fix(codex): promote a healthy detour instead of releasing it, and honour Retry-After (#4546) - #4616

Merged
lidge-jun merged 2 commits into
devfrom
codex/260914-success-recovery
Sep 14, 2026
Merged

fix(codex): promote a healthy detour instead of releasing it, and honour Retry-After (#4546)#4616
lidge-jun merged 2 commits into
devfrom
codex/260914-success-recovery

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Refs #4546. PRD R07.

When a transient hold outlived its window, routing deleted the whole affinity entry — including the detour account that had actually been serving the thread — and re-picked cold. The timer expiring restores the right to re-decide; it is not itself a recovery, and treating it as one threw away the single piece of evidence the request had. A still-healthy detour is now promoted to the binding, recorded as rebound / transient_hold_expired so the reason stays visible. A detour that has itself gone unhealthy still falls through to the cold path, and generation or quota refusals still release as before.

Retry-After becomes a lower bound on the transient path. The local maximum delay bounds our own exponential backoff and has no business shortening a wait the provider stated — sending early is a request we already know will be refused, which is the storm the header exists to prevent. It is opt-in per caller so the change lands on the transient path first rather than silently lengthening every adapter's backoff, and an honoured wait is ceilinged at one minute so an hour-long Retry-After cannot park a request.

Still out of scope

The transient half-open probe lease is not in this PR: a soft-avoided account still receives no traffic, so the two-consecutive-success clearing rule is met through the held fallback rather than a single leased probe.

Verification

Three new cases in tests/lib/upstream-retry.test.ts pin the opt-in lower bound, the ceiling, and that opting in never shortens below local backoff. The existing cap test does not opt in and keeps its behaviour. Local suite NOT RUN by policy; hosted CI at the exact head is the evidence.

Checklist

  • Targets dev
  • Behaviour change carries focused coverage
  • No request bodies, API keys or account identifiers are logged
  • Local CI — NOT RUN by policy

…our Retry-After (#4546)

Refs #4546. PRD R07.

When a transient hold outlived its window, routing deleted the whole affinity entry -- including
the detour account that had actually been serving the thread -- and re-picked cold. The timer
expiring restores the right to re-decide; it is not itself a recovery, and treating it as one
threw away the single piece of evidence the request had. A still-healthy detour is now promoted
to the binding instead, with the move recorded as rebound/transient_hold_expired so the reason is
visible. A detour that has itself gone unhealthy still falls through to the cold path.

Retry-After is a lower bound on the transient path. The local maximum delay bounds our own
exponential backoff and has no business shortening a wait the provider stated: sending early is a
request we already know will be refused, which is the storm the header exists to prevent. It is
opt-in per caller so the change lands on the transient path first rather than silently lengthening
every adapter's backoff, and an honoured wait is ceilinged at one minute so an hour-long
Retry-After cannot park a request.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 14, 2026 10:20
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T10:24:55.440604Z f5e878a PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 14, 2026
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/260914-success-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lidge-jun
lidge-jun merged commit 62f0222 into dev Sep 14, 2026
4 checks passed
@lidge-jun
lidge-jun deleted the codex/260914-success-recovery branch September 14, 2026 10:21
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 70 / 80

설명

이 PR(#4616)은 #4546 cost-guard 열차의 R07 조각이다. 지금 dev HEAD는 이미 62f02223a 이고, 이 PR이 그 커밋으로 들어와 있다. 즉 리뷰 시점에 merge가 끝난 상태이지만, 무엇을 고쳤는지와 남은 구멍은 그대로 기록해 둔다.

문제의 핵심은 아주 단순한 오해였다. 스레드가 잠깐 다른 계정(detour)으로 우회하는 동안 affinity 엔트리에는 transientHoldSincetransientDetourAccountId 가 남는다. 홀드 타이머가 끝나면 ‘다시 고를 권리’만 돌아오는 것인데, 예전 코드는 엔트리 전체를 지우고 처음부터 다시 골랐다. 그래서 실제로 몇 분 동안 잘 받던 detour 계정 B의 증거까지 같이 버려지고, 차가운 접두사(cold prefix)를 다시 사서 #4546이 막으려던 토큰 불길이 다시 붙을 수 있었다.

고친 뒤의 동작은 src/codex/routing.tsresolveCodexAccountForThreadDetailed 안에 있다. 홀드가 만료됐고, generation이 살아 있고, quota 거부가 아니며, 만료된 detour 계정이 아직 선택 가능하고 soft-avoid / failover / unrecovered quota 가 아니면 그 detour를 바인딩으로 승격한다. affinity move는 rebound, reason은 transient_hold_expired 로 남긴다. generation 무효나 quota 거부는 그대로 release 경로로 떨어진다. 독립 quota scope가 아니면 promoteActiveCodexAccount 도 함께 호출한다.

두 번째 축은 src/lib/upstream-retry.tsRetry-After 처리다. 예전에는 제공자가 ‘30초 뒤에 다시 와’라고 말해도 Math.min(retryAfter, maxDelayMs) 때문에 로컬 최대(예: 5초)로 잘려서, 이미 거절될 요청을 더 빨리 다시 보냈다. 이제는 호출자가 retryAfterIsLowerBound: true 를 켠 경우에만 Retry-After를 하한으로 쓰고, RETRY_AFTER_CEILING_MS(기본 60초)로 상한을 걸어 한 시간짜리 헤더에 요청이 묶이지 않게 한다. fetchWithTransientRetry 만 먼저 opt-in 했고, 다른 호출자는 예전 기본값을 유지한다.

현재 dev 맥락에서는 #4614(워커 fan-out에서 interactive 용량 예약) 바로 위에 앉는 조각이다. 패키지는 여전히 2.55.0이고, #4546 에픽은 아직 OPEN이다. PR 본문이 말한 대로 half-open probe lease(soft-avoid 계정에 한 스레드만 시험 트래픽)는 이 PR 밖이다. types/config 분할 캠페인에 무효화되는 형태도 아니다. 중복 PR로 보일 만한 열린 짝도 이 조각 기준으로는 없다.

테스트는 tests/lib/upstream-retry.test.ts 에 Retry-After 하한·천장·로컬 backoff 미만으로 줄이지 않음 세 케이스가 추가됐다. 라우팅 승격 분기 자체를 고정하는 단위 테스트는 이 변경 집합에 없다. 로컬 스위트는 policy상 NOT RUN이고 hosted CI가 증거다.

라인 2946-2973 / src/codex/routing.ts - 홀드 만료 시 healthy detour 승격 분기가 들어갔고 의도는 맞다. 다만 같은 동작을 고정하는 routing 단위 테스트가 없어, 이후 affinity 리팩터에서 조용히 깨질 위험이 있다.
라인 2956-2965 / src/codex/routing.ts - 승격 조건이 generationLive·!quotaRefused·selectable·!softAvoided 등 여러 가드로 묶여 있어 안전하지만, soft-avoided detour는 여전히 cold release로 떨어져 half-open probe 없는 한계가 그대로다.
경로 src/lib/upstream-retry.ts retryBackoffDelayMs - retryAfterIsLowerBound 기본 false라 기존 어댑터 동작은 유지된다. 의도적 opt-in이지만, transient 경로만 켜 두면 다른 5xx/429 경로가 여전히 Retry-After를 잘라 먹을 수 있다.
경로 tests/lib/upstream-retry.test.ts - Retry-After 세 케이스는 하한·천장·제로 헤더를 잘 고정한다. routing promote 경로는 커버하지 않는다.
경로 devlog/_plan/.../020_backoff_preserves_binding.md - R07 outcome 기록이 코드 의도와 일치한다. half-open probe가 다음 조각임을 명시해 두어 후속 범위가 분명하다.
심볼 bindThreadAffinity / promoteActiveCodexAccount - 독립 quota scope에서는 promote를 건너뛴다. 설계상 맞아 보이지만, scope 조합에서 ‘바인딩만 바뀌고 active가 남는’ 관찰 경로가 있는지 운영에서 한 번 볼 만하다.

메인테이너의 판단이 필요한 지점

너의 추천
이미 merge된 #4616은 그대로 두고, 후속으로 (1) transient_hold_expired 승격/거절 케이스의 routing 단위 테스트 추가, (2) soft-avoid half-open probe lease를 다음 #4546 조각으로 잡는다. types/config 분할이나 중복 close 대상은 아니다. CI만 green이면 이 조각은 열차에 남긴다.

이 댓글은 grok-bot이 작성했습니다

@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: f5e878a534

ℹ️ 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 src/codex/routing.ts
Comment on lines +2967 to +2972
if (!isIndependentCodexQuotaScope(quotaScope)) promoteActiveCodexAccount(config, expiredDetour);
bindThreadAffinity(threadId, expiredDetour, now, quotaScope);
return {
status: "selected",
accountId: expiredDetour,
affinity: { move: "rebound", reason: "transient_hold_expired" },

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 Mirror expired-detour promotion in preview

When an ordinary binding's hold has expired and its recorded detour is healthy, this branch now resolves to the detour, but previewReusableAffinityAccount still returns null for the expired hold and lets previewCodexAccountForRequest make a fresh strategy pick. In the subagent fallback path, preview can therefore score a different account—and potentially choose a different model—than final authentication uses. Apply the same side-effect-free expired-detour selection in preview and add an expiry case asserting preview/resolve equality.

AGENTS.md reference: src/AGENTS.md:L24-L24

Useful? React with 👍 / 👎.

Comment thread src/codex/routing.ts
Comment on lines +2955 to +2958
const expiredDetour = entry.transientDetourAccountId;
if (
isTransientHoldExpired(entry, now)
&& generationLive

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 Promote expired detours in the model-affinity lane

This promotion is reachable only for the ordinary affinity entry. If a model-scoped affinity account fails and detours to a healthy model-eligible sibling, the earlier detourEntry branch still deletes that model affinity when the hold expires before execution can reach this block. With another eligible sibling, the subsequent cold strategy selection may move the conversation away from the account that served throughout the hold, recreating the cache-loss behavior this change fixes. Apply equivalent promotion to model-detour affinities and cover that expiry path.

AGENTS.md reference: src/AGENTS.md:L24-L24

Useful? React with 👍 / 👎.

Comment thread src/lib/upstream-retry.ts
Comment on lines +305 to +311
// A provider that names a wait is stating when it will serve again; sending earlier is a
// request we already know will be refused, and refusing it twice is the retry storm the
// header exists to prevent. The local maximum bounds our OWN exponential backoff and has no
// business shortening someone else's instruction. The ceiling is separate: it stops an
// hour-long Retry-After from parking a request forever.
const ceiling = opts.retryAfterCeilingMs ?? RETRY_AFTER_CEILING_MS;
return Math.min(Math.max(retryAfter, jittered), ceiling);

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 Update every owned structure contract

The commit changes shared account routing under src/codex/ and retry transport behavior under src/lib/, but it changes no structure/ file. The source instructions require the same change to update every structure document mapped to each affected source area; at minimum, the existing affinity contract in structure/providers/openai-tiers.md and retry contract in structure/transports/responses.md now omit the new promotion and Retry-After semantics. Synchronize all owners listed by structure/INDEX.md.

AGENTS.md reference: src/AGENTS.md:L10-L11

Useful? React with 👍 / 👎.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant