Skip to content

[Bug]: the generic-OAuth hop permit is not threaded to the adapter, so one replay can be charged twice #4709

Closed

Description

@lidge-jun

Client or integration

Other

Area

Authentication and account pool

Summary

The generic-OAuth 429 ladder reserves a hop from the shared request budget, and the reservation is the charge (src/server/responses/adapter-dispatch.ts:719). The same budget is then handed to the adapter for the replay (:420), and an adapter that reserves for itself charges again — Kiro reserves immediately before its physical send at src/adapters/kiro-retry.ts:171. The non-adapter transient helper has the same shape: the hop is not supplied through pendingHopPermit, so its onSendsConsumed reporter adds a second charge.

One physical replay can therefore be counted twice, or refused as a second target transition. The passthrough ladder already solves this by reserving with countedExternally: true and handing the permit down through pendingHopPermit (src/server/responses/passthrough-dispatch.ts:1115).

This predates 2.56.0 and was found while fixing a related permit leak in that release; it is the remaining half of the same accounting story.

Reproduction

  1. Configure a generic-OAuth provider with two or more accounts and an adapter that reserves its own sends (Kiro), or any provider with a transient retry policy.
  2. Make the first send return 429 so the roster rotates.
  3. Watch the request's send accounting: the single replay is charged by the hop reservation and again by the adapter or by onSendsConsumed.

Suggested fix: mirror the passthrough shape — reserve the hop with countedExternally: true and pass it through pendingHopPermit so the layer that actually dispatches settles it.

Version

2.56.0 (e4a8539)

Operating system

macOS 15.5

Provider and model

kiro / any generic-OAuth provider with two or more accounts

Logs or error output

(accounting defect; visible as an early request_send_budget refusal or a doubled send count)

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. lidge-jun commented on Sep 15, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 74 / 80

    이 이슈는 generic-OAuth 429 홉 예약이 어댑터/transient 쪽에 전달되지 않아 한 번의 물리 재전송이 두 번 청구될 수 있는 회계 버그다. 현재 tip 51d577c3f에서 src/server/responses/passthrough-dispatch.ts는 reserveCredentialHop(..., true)로 홉을 잡고 sendBudgetState.pendingHopPermit = hop.permit로 내려준 뒤 rebuild가 그 예약을 쓴다(약 1115–1144행대). 반면 src/server/responses/adapter-dispatch.ts의 generic-OAuth 429 사다리(약 719행대)는 reserveCredentialHop만 하고 pendingHopPermit 필드 자체가 이 파일에 없다. 같은 예산이 어댑터로 넘어가면 Kiro는 src/adapters/kiro-retry.ts 약 171행에서 물리 send 직전에 다시 reserveDispatch한다. transient helper의 onSendsConsumed도 홉 permit을 못 받으면 두 번째 청구를 남긴다.

    증상은 early request_send_budget 거절이거나 send count가 실제 wire보다 크게 보이는 형태다. 2.56.0 회귀 감사에서 관련 permit 누수를 고친 뒤 남은 반쪽이라는 설명과, passthrough가 이미 정답 모양을 갖고 있다는 점이 tip 코드와 일치한다. #4546 send-budget 스택의 핵심 불변(물리 send당 정확히 한 번)을 어댑터 경로에서 깨므로 우선순위가 높다.

    고치는 방법은 이슈 제안 그대로다. 홉을 countedExternally: true(또는 passthrough와 같은 세 번째 인자 true)로 예약하고, 실제 dispatch 층이 settle하도록 pendingHopPermit을 어댑터/transient 경로에 관통시킨다. Kiro·Cursor처럼 내부 ladder가 있는 어댑터로 회귀 테스트를 걸어야 한다.

    경로 src/server/responses/adapter-dispatch.ts generic-OAuth 429 - 홉을 예약하지만 permit을 아래로 넘기지 않는다.
    경로 src/server/responses/passthrough-dispatch.ts - pendingHopPermit로 같은 문제를 이미 해결한 참고 구현.
    라인 src/adapters/kiro-retry.ts ~171 - 어댑터가 자체 reserve하여 이중 청구 가능.
    경로 transient onSendsConsumed - 홉 permit 미전달 시 reporter가 한 번 더 센다.

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

    • adapter-dispatch에 passthrough와 동일한 pendingHopPermit 상태를 도입할지, 공통 helper로 뺄지.
    • 이중 청구 회귀를 Kiro만으로 고정할지 Cursor transient도 넣을지.
    • 2.57 필수 후속으로 올릴지.

    너의 추천
    받아라. passthrough 모양을 adapter-dispatch에 그대로 이식하고, Kiro 429 로스터 회전 통합 테스트로 ‘물리 1회 = 청구 1회’를 고정하라. types/config 스플릿과 무관, close-don't-rebase 대상 아님.

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

  2. added a commit that references this issue on Sep 16, 2026
  3. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    OwnerAuthor

    Landed via #4745 at cbc4c10.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workinglanded-via-maintainerOriginal PR closed after landing via a maintainer merge train

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions