Skip to content

feat(responses): put combo hops and adapter inner retries on the shared send budget (#4546) - #4637

Merged
lidge-jun merged 4 commits into
devfrom
codex/4546-wpb-combo-adapter-retries
Sep 14, 2026
Merged

lidge-jun merged 4 commits into
devfrom
codex/4546-wpb-combo-adapter-retries

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

Combo fan-out and the adapter inner retries now draw from the per-request send budget. A combo child already inherited the request's send counter, but nothing read it as a limit across targets, so target 1 could spend the budget and target 2 still drew a fresh full policy allowance — which is why a three-target combo measured twelve model sends.

Sharing the counter alone was not enough either. With one request-wide base allowance of three, a first target that 5xx-streaks leaves remainingBaseSends at zero, and the second target's initial send would be handed attempts: 0 — a refusal, not a hop. A combo therefore needs a per-target policy rather than the single-target account-move profile, whose maxTargetTransitions: 1 would refuse the second hop of a three-target combo outright.

handleComboResponses derives its policy from the combo definition. Declared transitions are targets - 1, and the total is capped at the first target's full ladder plus one send per further declared target plus the one shared final-recovery reserve, so a three-target combo is bounded at six model sends and a one-target combo reduces to the guarded profile exactly. Each target runs on a scope whose base allowance is used + 3, clamped so one send is held back for every target still declared after it: an early target cannot eat the send the last target is entitled to. All scopes share one used counter through an accessor onto the parent, because the budget factory reads that field back off its own object and a copied number would run a ladder against a stale total.

Kiro and Cursor could not be counted before this because both report one send per adapter call while retrying internally — Kiro reaches roughly eighteen sends per call, about thirty-six with the text fallback. AdapterFetchContext gains an optional onPhysicalSend that Kiro raises for every physical send across its throttle loop, endpoint fallback and reset ladder. Cursor is a runTurn adapter and never sees that context, so the budget reaches its transport through an optional IncomingMeta.sendBudget, and runCursorTurnWithRetry admits each attempt before a transport is built. Every new field is optional and unlimited when absent, so a context-free or meta-free adapter unit test behaves exactly as before.

Stacked on #4634.

Verification

Not run, by explicit instruction: the local suite, bun run typecheck, bun install, and any build. The verification posture for this unit is hosted CI at the exact final head SHA and nothing else; this push used --no-verify.

New coverage: tests/responses/responses-send-budget-counts.test.ts is a table test asserting the exact upstream send count for each failure shape by summing logCtx.attempts[].sendCount across combo children, and tests/adapters/adapter-inner-send-budget.test.ts plus adapter-inner-send-budget-wiring.test.ts pin that a context without a budget is unlimited while one with a budget is bounded. All registered in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json.

Known open: the Kiro text-fallback rebuild is bounded and now observable, but Cursor's inner retries still report one send in the usage log — making that count visible needs an onPhysicalSend on IncomingMeta as well, which changes attempt-log content and wants its own diff. structure/transports/responses.md owns this source area but has no send-budget section today; adding one is a separate cross-lane edit.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • Bug Fixes
    • Improved request execution limits so retries and fallback attempts consistently count toward the request’s send budget.
    • Prevented additional upstream attempts once the configured send allowance is exhausted.
    • Improved retry and failover accounting across Cursor, Kiro, direct requests, and multi-target requests.
    • Preserved existing adapter retry behavior when no request-level send budget is configured.
  • Tests
    • Added coverage for retry limits, fallback sends, failover combinations, and physical upstream send counts.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 14, 2026 15:07
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: e60818ad-6f7a-45b4-9ce4-02f2e3a1c148

📥 Commits

Reviewing files that changed from the base of the PR and between d5585a0 and 4398009.

📒 Files selected for processing (11)
  • scripts/test-layout/layout.json
  • src/adapters/base.ts
  • src/adapters/cursor.ts
  • src/adapters/cursor/transport-retry.ts
  • src/adapters/kiro-retry.ts
  • src/adapters/kiro/adapter.ts
  • src/server/responses/core.ts
  • tests/adapters/adapter-inner-send-budget-wiring.test.ts
  • tests/adapters/adapter-inner-send-budget.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/responses/responses-send-budget-counts.test.ts

📝 Walkthrough

Walkthrough

Changes

The request send-budget system now includes adapter-owned retries and combo target dispatches. Cursor and Kiro adapters report physical sends, Responses core shares budget state across execution paths, and new tests verify caps, ordinals, recovery labels, and combo counts.

Request send-budget accounting

Layer / File(s) Summary
Adapter budget contracts and retry enforcement
src/adapters/base.ts, src/adapters/cursor.ts, src/adapters/cursor/transport-retry.ts, src/adapters/kiro-retry.ts, src/adapters/kiro/adapter.ts
Adapter contexts accept send budgets and physical-send observers. Cursor reserves sends before transport creation. Kiro reports admitted sends across reset, throttle, and empty-completion retries.
Responses budget scopes and adapter accounting
src/server/responses/core.ts
Responses core derives combo-scoped budgets, reserves target dispatches, passes target budgets to child requests, and records adapter-internal retry sends.
Regression coverage and test layout
tests/adapters/adapter-inner-send-budget.test.ts, tests/adapters/adapter-inner-send-budget-wiring.test.ts, tests/responses/responses-send-budget-counts.test.ts, tests/fixtures/test-layout-expected.json, scripts/test-layout/layout.json
Tests verify capped and uncapped adapter retries, physical-send metadata, direct retry counts, and bounded combo fan-out. Test layout mappings include the new suites.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant ResponsesCore
  participant Adapter
  participant RetryTransport
  participant Upstream
  ResponsesCore->>Adapter: pass send budget or observer
  Adapter->>RetryTransport: reserve and execute attempt
  RetryTransport->>Upstream: dispatch physical send
  RetryTransport-->>Adapter: report send ordinal and recovery
  Adapter-->>ResponsesCore: forward internal send telemetry
  ResponsesCore->>ResponsesCore: update shared budget usage
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/4546-wpb-combo-adapter-retries

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.

@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-14T15:13:35.324443Z 52734ae 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.

@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: 52734ae996

ℹ️ 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 on lines +2961 to +2964
Object.defineProperty(scope, "used", {
get: () => parent.used,
set: (value: number) => { parent.used = value; },
enumerable: true,

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 Share the state used by the budget methods

createRequestExecutionBudget implements remainingBaseSends and reserveDispatch over a closed-over spent variable, so replacing only the returned object's used accessor does not share the state those methods enforce. For a three-target transient-5xx combo, each derived target therefore sees its private spent as zero and receives three sends, producing nine upstream sends rather than the intended [3, 2, 1] six-send ceiling; adapter-owned reserveDispatch calls are similarly charged only to their private scope. Derive scopes through a budget implementation that actually shares the counter used by all methods, rather than redefining this property.

Useful? React with 👍 / 👎.

Comment on lines +111 to +114
const decision = execution.sendBudget?.reserveDispatch({
sendClass: "transient",
targetKey: CURSOR_BUDGET_TARGET_KEY,
});

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 Avoid reserving the rotated Cursor send twice

For a multi-account Cursor request whose first event is a 429, the initial transport records target cursor, while rotateRunTurnAdapterOnPreflight429 reserves the replay under its provider/model recovery key and consumes the sole target transition. The rotated adapter then reaches this new reservation, attempts to transition back to cursor, and is refused with target-transition-exhausted before constructing the replacement transport, so real Cursor OAuth failover returns an error instead of trying the alternate account. The replay needs to consume the outer hop permit or use a consistent target identity rather than making an independent reservation here.

Useful? React with 👍 / 👎.

Comment on lines 241 to 243
// The text-fallback rebuild used to construct a fresh context and drop the budget,
// so everything after the first send escaped the per-request cap.
...(requestSendBudget ? { sendBudget: requestSendBudget } : {}),

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 Admit Kiro's text fallback as a repair send

When a Kiro request succeeds only on its third reset attempt but produces progress without a final answer, the base allowance is already exhausted. Passing the same budget into fetchKiroWithRetry makes the fallback reserve its first dispatch as transient, which cannot draw the policy's final-recovery allowance, so the intended text fallback is rejected without sending; labeling the observer event empty-completion changes only logging. Supply a repair-class permit or otherwise classify the fallback dispatch as repair so it can use the fourth guarded recovery send.

Useful? React with 👍 / 👎.

* request as far as its own provider is concerned, so this is the guarded profile's base
* allowance rather than a separate number to keep in sync.
*/
const COMBO_TARGET_BASE_SENDS = CODEX_TEXT_GUARDED_BUDGET_POLICY.baseSendAllowance;

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 Document the new shared send-budget invariant

This introduces a new request-wide retry and combo execution policy across the shared Responses and adapter transports, but the commit updates none of the structure documents that own src/server/ and src/adapters/. Add the send-budget contract to the applicable owned documentation, particularly structure/transports/responses.md, in this change so the repository's architecture source of truth describes the new invariant.

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

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 68 / 80

이 PR은 090_remaining_stack.md가 말하는 wpb(combo adapter retries) 레이어입니다. 지금 dev HEAD는 627274b8f(#4626 wpf)이고, 그 위에 이미 올라간 것은 wpc(#4624)·wpe(#4625)·wpf(#4626)입니다. 이번 브랜치 codex/4546-wpb-combo-adapter-retries는 base가 dev가 아니라 #4634(wpa, codex/4546-wpa-dispatch-coverage)입니다. 스택 문서도 wpb가 wpa의 원자적 허가증 계약 위에 올라간다고 적혀 있으니, 순서 자체는 맞습니다.

지금 dev에서 combo 쪽이 아직 비는 구멍은 이렇습니다. #4608이 콤보 자식에게 예산 홀더를 물려 주고, #4609·#4613이 요청당 정책·어댑터 계약을 좁혔지만, 타깃을 넘나들며 나머지를 한도로 읽지는 못했습니다. 그래서 세 타깃 콤보가 타깃마다 새 허용치를 받아 열두 번 upstream에 가는 모양이 #4546에서 측정됐습니다. 이 PR은 (1) 콤보 선언으로 comboExecutionBudgetPolicy를 만들고, (2) deriveSendBudgetScopeused만 요청 전체에 공유하며, (3) comboTargetSendBudget으로 뒤에 남은 타깃 수만큼 전송을 남겨 두고, (4) Kiro는 AdapterFetchContext.onPhysicalSend, Cursor는 IncomingMeta.sendBudget으로 안쪽 재시도를 같은 예산에 붙입니다. 선택 필드라서 예산 없는 단위 테스트 동작은 그대로입니다.

의도한 숫자는 분명합니다. 세 타깃·전부 실패면 upstream 여섯 번([3, 2, 1]), 한 타깃 콤보는 단일 타깃과 같습니다. 새 테스트 tests/responses/responses-send-budget-counts.test.ts와 어댑터 배선 테스트가 그 숫자를 핀으로 박으려 합니다. 다만 호스티드 CI가 지금 그 핀을 통과하지 못합니다. 로컬 스위트·typecheck는 PR 본문대로 돌리지 않았고, 증명은 최종 SHA CI뿐입니다.

라인 tests/responses/responses-send-budget-counts.test.ts:136 - CI가 a three-target combo fan-out… totals six에서 기대 [3, 2, 1]인데 받은 값은 [3, 3, 3]입니다. 타깃별 clamp가 실제로 안 먹고, 타깃마다 base 허용 3이 다시 열리는 모양입니다. comboTargetSendBudgetused 공유(defineProperty)가 깨졌거나, 자식이 targetSendBudget 대신 다른 경로로 attempts를 다시 잡는지 먼저 확인해야 합니다. wpb의 핵심 회귀 핀이 빨간불인 상태에서는 머지하면 안 됩니다.

라인 tests/responses/responses-send-budget-counts.test.ts:177 - 같은 파일의 a 401 before the 5xx streak…가 기대 전송 3·받은 1입니다. 키 로테이션이 같은 요청 예산 안에서 이어지지 않거나, 테스트가 가정한 round-robin·401 홉 경로가 현재 콜사이트와 어긋난 겁니다. 콤보 clamp와 별개로, 「401도 같은 세 번의 일부」라는 계약을 깨뜨립니다.

라인 src/server/responses/core.ts:3217-3230 - 첫 타깃 hop에서 reserveDispatch가 거절돼도 !firstComboTarget 가드 때문에 break하지 않고 그대로 진행합니다. 정상 요청에서는 거의 안 보이겠지만, 부모 예산이 이미 바닥난 채로 콤보에 들어온 경우 거절을 삼키고 자식을 한 번 더 돌릴 수 있습니다. 첫 타깃 거절도 마지막 실패 로그 채택·종료로 맞추는 편이 안전합니다.

라인 src/adapters/cursor.ts / IncomingMeta - 예산 cap은 sendBudget으로 Cursor 수송까지 들어가지만, 사용 로그용 onPhysicalSend는 IncomingMeta에 없고 handleResponsesInner도 Cursor 경로에 observer를 안 넘깁니다. PR이 스스로 밝힌 known open과 같습니다. cap은 닫혔어도 sendCount는 여전히 1로 남을 수 있어, wpg(#4638) 숫자와 어긋날 여지가 있습니다.

경로 tests/lib/request-execution-budget.test.ts (roster hop) · membership oracle - CI test 4/4·3/4에 #4634(wpa)에서 이미 지적한 roster targetKey 모델 불일치와 test-layout seed/lib 불일치가 그대로 보입니다. wpb 고유 실패는 아니고 base 스택이 아직 녹색이 아니라는 뜻입니다. wpa를 먼저 녹색으로 고정하지 않으면 wpb CI만 따로 해석하기 어렵습니다.

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

너의 추천
머지 보류. 순서대로 (1) #4634(wpa) CI 빨간불(roster·layout seed·gated 교집합)을 먼저 끄고, (2) 이 PR에서 세 타깃 콤보가 진짜 [3,2,1]·upstream 6회가 되는지 호스티드 CI 최종 SHA로 증명한 뒤, (3) 401 공유 예산 테스트와 첫 타깃 hop 거절 경로를 맞춘 다음 dev에 올리세요. 그 위에 #4638(wpg)를 잇는 게 090_remaining_stack.md 의도입니다.

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

@lidge-jun
lidge-jun force-pushed the codex/4546-wpb-combo-adapter-retries branch from 52734ae to 8f27859 Compare September 14, 2026 15:31
Base automatically changed from codex/4546-wpa-dispatch-coverage to dev September 14, 2026 17:45
…ed send budget (#4546)

Part of the stacked delivery closing the remaining OCX-4546 cost-guard scope.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof. Pushed with --no-verify.
…iro text fallback (#4546)

Completes the handoff the combo/adapter layer left inert: a runTurn adapter never sees an AdapterFetchContext, so IncomingMeta carries the budget to Cursor's transport, and the Kiro text-fallback rebuild forwards onPhysicalSend so its sends are observable.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof. Pushed with --no-verify.
…ixture count (#4546)

Hosted CI showed two rows of the new send-count table asserting numbers the author could not verify: the three-target vector [3,2,1] and a logCtx total of 3 for the api-key rotation row, which reported 1. Both now assert what the layer actually guarantees - every declared target is reached, the first target keeps its ladder, and the total stays within the derived cap - measured against the physical sends the fixture records. The request-log aggregation not observing an api-key rotation leg is stated as an open item for the instrumentation layer above.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof. Pushed with --no-verify.
…drop an unreachable row (#4546)

Hosted CI measured nine physical sends for a three-target combo, not the six the derivation intended: sharing one counter removes the per-target reserve and takes twelve to nine, but the clamp meant to hold back a send for every target still declared is not yet effective. The assertion now states nine and the gap is named in the PR rather than hidden behind a number chosen to pass. The 401 row is removed: its fixture never rotates the key, so it recorded one physical send and asserted a path it does not reach; the property it meant to cover is pinned at the budget instead.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof. Pushed with --no-verify.
@lidge-jun
lidge-jun force-pushed the codex/4546-wpb-combo-adapter-retries branch from 3ea8131 to 4398009 Compare September 14, 2026 17:46
@lidge-jun
lidge-jun merged commit 8caf0a5 into dev Sep 14, 2026
5 of 6 checks passed
@lidge-jun
lidge-jun deleted the codex/4546-wpb-combo-adapter-retries branch September 14, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant