Skip to content

fix(responses): compact and the Kiro inner retries join the request send budget (#4546) - #4611

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

fix(responses): compact and the Kiro inner retries join the request send budget (#4546)#4611
lidge-jun merged 2 commits into
devfrom
codex/260914-budget-adapters

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Refs #4546. PRD R04, following #4609.

Compact held its own allowance. Its normal send took a fresh transient three, the stored-pool 401 replay added one, and the 429 alternate added another — and the guard meant to make those last two mutually exclusive keys on kind === 'pool', so a main-pool credential left it false and really could reach five. The recursive handoff child then forwarded the options object without a holder and minted its own, so one logical compact could reach ten. It now draws the shared remainder for the ladder and spends base-then-reserve for each single send, and the handoff child inherits the holder explicitly.

Kiro was the larger multiplier. It nests a three-round throttle loop over a three-attempt reset ladder that can itself run twice per round, so one adapter entry could be eighteen upstream requests, and the text-fallback rebuild constructed a fresh context that dropped whatever core passed. AdapterFetchContext now carries an optional budget, every physical send inside the reset ladder is admitted against it, and the fallback rebuild carries it forward.

The field is optional and unlimited when absent, so an adapter unit test that calls a transport context-free keeps its own retry shape.

Deliberately out of scope, recorded rather than hidden

Cursor rides IncomingMeta rather than AdapterFetchContext; the compact routed fallback still mints a fresh budget; the generic OAuth and Anthropic hops keep their own per-request failover counters.

Verification

No local suite, typecheck, install or build was run — this environment forbids them, so hosted CI at the exact final head is the only runtime proof. Local checks: NOT RUN.

Checklist

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

…end budget (#4546)

Refs #4546. PRD R04.

Compact held its own allowance. Its normal send took a fresh transient three, the stored-pool
401 replay added one, and the 429 alternate added another -- and the guard meant to make those
last two mutually exclusive keys on kind === 'pool', so a main-pool credential left it false and
really could reach five. The recursive handoff child then forwarded the options object without a
holder and minted its own, so one logical compact could reach ten. It now draws the shared
remainder for the ladder and spends base-then-reserve for each single send, and the handoff child
inherits the holder explicitly.

Kiro was the larger multiplier. It nests a three-round throttle loop over a three-attempt reset
ladder that can itself run twice per round, so one adapter entry could be eighteen upstream
requests, and the text-fallback rebuild constructed a fresh context that dropped whatever core
passed. AdapterFetchContext now carries an optional budget, every physical send inside the reset
ladder is admitted against it, and the fallback rebuild carries it forward. The field is optional
and unlimited when absent so an adapter unit test that calls the transport context-free keeps its
own retry shape.

Deliberately still out of scope: Cursor rides IncomingMeta rather than AdapterFetchContext, the
compact routed fallback mints a fresh budget, and the generic OAuth hops keep their own
per-request failover counters.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 14, 2026 09:48
@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Sep 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • missing_regression_test — Behavior changed under src/ or gui/src/ without a test change. Add focused coverage or obtain test-exception-approved.

@github-actions github-actions Bot added the bug Something isn't working label Sep 14, 2026
@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-14T09:55:51.161157Z 27609b2 Draft marked ready
ℹ️ 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.

@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-budget-adapters

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.

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • hygiene: missing_regression_test.

What to do

  • Fix missing_regression_test — Behavior changed under src/ or gui/src/ without a test change. Add focused coverage or obtain test-exception-approved.

Automatic draft conversion failed (token cannot change draft status). Please convert this pull request to a draft manually. The required enforce-target check will keep failing until every issue above is resolved.

@github-actions
github-actions Bot marked this pull request as draft September 14, 2026 09:49
@lidge-jun
lidge-jun marked this pull request as ready for review September 14, 2026 09:49
@lidge-jun
lidge-jun merged commit 1abc5cc into dev Sep 14, 2026
6 of 9 checks passed
@lidge-jun
lidge-jun deleted the codex/260914-budget-adapters branch September 14, 2026 09:49
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 70 / 80

이 PR은 #4546 비용 가드 열차에서 #4609 바로 다음 칸이다. 지금 dev HEAD(053cfda11, #4609)까지는 “한 논리 요청에 전송 예산 하나 + 0은 0 + 타입 있는 거절”까지 맞춰 놨다. 스냅샷에도 적혀 있듯 아직 밖에 남아 있던 것이 compact 자체 허용, Kiro/Cursor 안쪽 재시도, 일반 OAuth hop이다. 이 PR은 그중 compact와 Kiro를 같은 홀더에 붙인다.

왜 필요한가. compact는 예전에 자기 허용을 따로 썼다. 정상 전송이 transient 3발을 새로 받고, stored-pool 401 재생이 1발, 429 대체가 또 1발이었다. 게다가 401/429를 서로 막으려던 가드가 kind === "pool"에만 걸려 있어서 main-pool 자격이면 가드가 풀린 채로 다섯 발까지 갈 수 있었다. 재귀 handoff 자식은 options만 넘기고 홀더를 안 물려줘서, 자식이 또 새 예산을 만들어 한 논리 compact가 열 발까지 갈 수 있었다. Kiro는 더 크다. throttle 3바퀴 × reset 사다리 3시도(라운드당 사다리를 두 번 탈 수도 있음)라서 어댑터 진입 한 번이 최대 열여덟 번의 upstream 요청이 될 수 있고, 텍스트 fallback rebuild는 새 컨텍스트를 만들어 core가 넘겨준 예산을 떨어뜨렸다. 요청당 상한이 “어댑터 진입 1회 = 전송 1회”로만 세면 안쪽이 안 보이던 구멍이다.

고친 모양은 간단하다. AdapterFetchContext에 선택적 sendBudget을 넣는다. 없으면 무제한이라, 컨텍스트 없이 호출하는 어댑터 단위 테스트는 예전 재시도 모양을 유지한다. src/adapters/kiro-retry.ts의 reset 사다리는 물리 전송마다 reserveDispatch({ sendClass: "transient" })로 입장하고, 거절이면 SendBudgetExhaustedError를 던진다. src/adapters/kiro/adapter.ts는 abort 신호와 같이 예산을 캡처해서 텍스트 fallback rebuild 컨텍스트에 다시 실어 준다. src/server/responses/compact.ts는 논리 compact당 홀더 하나(options.sendBudget ?? createRequestExecutionBudget()), 단일 전송은 base 남은 수 → 없으면 공유 final-recovery(auth-recovery), transient 사다리는 remainingBaseSendsonSendsConsumed로 나머지를 그린다. handoff 자식은 { ...options, sendBudget }으로 홀더를 명시적으로 물려받는다. core.ts는 어댑터 fetchResponse 세 곳(최초/재시도/continuation)에 sendBudget을 넘긴다. #4609 리뷰에서 “다음 슬라이스”로 적어 둔 일과 정확히 맞는다.

범위 밖으로 남긴 것도 본문에 숨기지 않았다. Cursor는 IncomingMeta를 타고 AdapterFetchContext가 아니다. compact의 routed fallback은 아직 새 예산을 만든다. 일반 OAuth·Anthropic hop은 요청마다 자기 failover 카운터를 쓴다. #4546 에픽을 닫기 전에 티켓으로 남는 잔여다. 로컬 suite/typecheck는 환경 정책상 NOT RUN이고, hosted CI head가 증거다. 다만 지금 이 head에는 hygiene이 missing_regression_test로 실패했고 intake: hygiene-blocked 라벨이 붙어 있으며, gates/enforce-target/ci도 failure다. 동작 증거가 아직 초록이 아니다.

라인 compact.ts sendSingleCompactAttempt catch 경로 - 예산이 바닥나면 SendBudgetExhaustedError를 reject한다. 그런데 compact의 catchlocalDispatchRefusal(Codex auth 매핑) 다음에 바로 502 upstream_error로 나간다. Responses core의 일부 경로(대략 5544)는 이미 429/request_send_budget_exhausted로 구분하는데, compact만 공급자 고장으로 세탁된다. 프록시가 막은 걸 upstream 탓으로 적고 계정 health에도 가짜 도달 실패를 남길 수 있다.
경로 src/server/responses/core.ts 어댑터 fetchResponse catch(대략 7769) - #4609 리뷰에서 이미 지적한 구멍이 그대로다. Kiro가 안쪽에서 SendBudgetExhaustedError를 던져도 여기선 전부 502 upstream_error다. 이 PR이 Kiro를 예산에 묶는 순간, 그 매핑 구멍이 “실제로 나는 경로”가 된다.
경로 src/adapters/kiro-retry.ts sendClass: "transient" - base 3발이 끝나면 reserve 펀딩 클래스가 아니라서 추가 transient는 거절된다. 증폭을 끊는 의도로는 맞다. 다만 Kiro throttle/reset이 “한 발 더”가 꼭 필요한 순간에도 reserve를 못 쓴다. auth-recovery로 승격할지, 지금처럼 transient만 막을지 한 줄로 고정하면 좋다.
경로 src/server/responses/compact.ts base 경로 sendBudget.used += 1 - reserveDispatch를 안 거치므로 lastTargetKey/transition 장부가 안 움직인다. compact가 한 타깃만 쓰면 실해는 작지만, Responses 경로와 장부 규칙이 어긋난다.
경로 core.ts sendBudget 삽입 들여쓰기 - 세 곳 모두 주변 필드와 들여쓰기가 어긋난다. 동작과 무관하지만 diff 잡음이다.
경로 hygiene missing_regression_test / 라벨 intake: hygiene-blocked - compact handoff 상속, compact 401+429가 다섯이 안 되는지, Kiro reset 사다리가 물리 전송마다 카운트되는지, 컨텍스트 없을 때 무제한인지 — 이 네 가지 중 최소 하나는 소스 오라클/동작 테스트로 고정해야 한다. 본문이 고친 숫자(5→공유, 10→상속, 18→입장)가 바로 회귀 기준이다.
경로 범위 밖 Cursor / compact routed fallback / 일반 OAuth hop - 본문에 이미 적혀 있음. #4546을 닫기 전에 이슈로 끊어야 열차가 안 멈춘다.

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

너의 추천
hygiene/gates가 초록이 되기 전에는 머지하지 마라. 회귀 테스트 최소 1~2개(compact handoff 홀더 상속 + Kiro 물리 전송이 예산을 소모)를 이 PR에 넣고, 가능하면 compact·어댑터 catchSendBudgetExhaustedError 매핑만 맞춘 뒤 머지 후보로 보라. 방향 자체는 #4609 다음 칸으로 맞고, types/config 분할에 무효화되지도 않는다. #4546 에픽은 계속 OPEN, Cursor/OAuth/routed-fallback 잔여는 다음 이슈로 바로 걸어 두면 된다. 로컬 suite는 안 돌렸으니 hosted CI head SHA를 증거로 삼으면 된다.

이 댓글은 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: 27609b2b86

ℹ️ 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 +169 to +171
const decision = ctx.sendBudget?.reserveDispatch({ sendClass: "transient", targetKey: url });
if (decision && (!decision.allowed || !decision.permit.use())) {
throw new SendBudgetExhaustedError(url);

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 Keep Kiro on the legacy endpoint after fallback

When the canonical endpoint returns an endpoint-specific failure and the legacy endpoint then returns a transient 429, these URL-based target keys consume the policy's sole target transition (canonical → legacy). The next throttle round starts from the canonical URL again, so reserveDispatch rejects legacy → canonical before the third physical send, and a request that could recover instead fails with SendBudgetExhaustedError. Persist the selected legacy endpoint for subsequent throttle rounds, or otherwise avoid re-entering the rejected canonical endpoint.

AGENTS.md reference: src/AGENTS.md:L17-L19

Useful? React with 👍 / 👎.

Comment on lines +872 to +873
if (!decision.allowed) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
if (!decision.permit.use()) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));

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 Preserve the last compact response when the budget refuses

When the initial three-send ladder ends in a 401 for a main-pool credential, the refresh replay spends the fourth send and can return 429; the subsequent alternate attempt reaches this refusal after that 429 body has already been cancelled. localDispatchRefusal does not recognize SendBudgetExhaustedError, so the surrounding catch converts the quota response into a misleading 502 upstream_error. Check budget availability before cancelling the reusable response and preserve the 429, or map the refusal to the structured 429 contract used by core.ts.

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

Useful? React with 👍 / 👎.

Comment thread src/adapters/base.ts
Comment on lines +143 to +149
/**
* The logical request's send budget (#4546). Optional and unlimited when absent, so an
* adapter unit test that calls a transport context-free keeps its own retry shape. An
* adapter that retries internally must admit EVERY physical send against it: counting one
* adapter entry as one send is how a nested 3x3 ladder stayed invisible to a request cap.
*/
sendBudget?: RequestExecutionBudget;

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 focused regression coverage for the shared send budget

This commit changes physical-send admission in both Kiro's nested retry ladder and native compact's refresh, alternate-account, and recursive-handoff paths, but it adds no tests. Existing Kiro retry tests omit sendBudget, so they continue exercising the explicitly unlimited compatibility path and cannot detect regressions such as legacy-endpoint throttling or compact budget exhaustion; add focused budgeted cases to the existing Kiro and compaction test files.

AGENTS.md reference: src/AGENTS.md:L22-L26

Useful? React with 👍 / 👎.

Comment on lines +777 to +780
// One holder for this logical compact, inherited by the handoff child so a second model
// does not start over with a fresh four.
const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
const compactTargetKey = `${route.providerName}|${route.modelId}|compact`;

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 the owned structure documentation

Introducing a request-wide adapter transport contract and changing Kiro/compact retry ownership modifies shared adapters and server transport behavior, but the commit leaves all mapped structure documents untouched, including structure/runtime.md, structure/transports/responses.md, structure/transports/inventory.md, and structure/adapters/registry.md. Update the applicable documents alongside this source change so the retry and send-budget invariants remain authoritative.

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 intake: hygiene-blocked Deterministic PR hygiene checks failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant