Skip to content

[WRONG BRANCH] fix(responses): report a spent send budget as this proxy refusing (#4708) - #4758

Merged
lidge-jun merged 17 commits into
codex/bl2-durable-ledger-wiringfrom
codex/bl3-budget-refusal-semantics
Sep 16, 2026
Merged

lidge-jun merged 17 commits into
codex/bl2-durable-ledger-wiringfrom
codex/bl3-budget-refusal-semantics

Conversation

@lidge-jun

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

Copy link
Copy Markdown
Owner

Summary

When a request exhausts its shared send budget, the three dispatch paths disagreed about what the client was told. Passthrough returned 429 and explicitly declined to blame the provider. The adapter paths did not special-case SendBudgetExhaustedError: they fell through describeUpstreamConnectFailure and answered 502 upstream_error — "Provider unreachable" — for a refusal this process made itself. The runTurn path pushed an unstructured error event, which is inferred back to 502 and delivered under HTTP 200.

The status is the load-bearing half, and the 502 is worse than a mislabel. The Codex client retries 5xx and does not retry a direct 429 (retry_429 is false in its provider policy), so telling it the provider broke makes it send the whole turn again — the amplification this budget exists to stop. Reporting the refusal as a quota code would stop the client for a reason that is not true, and the retryable streaming rate-limit codes (rate_limit_exceeded, slow_down) would restart the stream, so neither is available. 429 with a distinct code is the only option that both halts the client and stays honest.

Both adapter catch sites now answer 429 before describeUpstreamConnectFailure can launder the refusal, and runTurn emits it as a structured terminal event carrying its status, type and code on the event itself, because an unstructured message is inferred back to 502.

classifyError keeps the distinct code by matching the supplied type rather than the status, so an upstream 429 still classifies as rate_limit_exceeded and only this proxy's own refusal carries request_send_budget_exhausted. Before this, the passthrough path asked for that code and the classifier overwrote it, so even the one path that got the status right could not be told apart afterwards.

A local 429 must also not look like a provider one to our own routing. rotateRunTurnAdapterOnPreflight429 returns early on the code, before it reads the status, so a refusal cannot rotate a credential or write a cooldown against an account that rate-limited nothing — a fake quota signal that outlives the request and misroutes later ones.

The terminal-guard continuation loop never consulted sendBudgetExhausted() while the main recovery loop did, so a spent budget could still same-key 429-replay on a live stream. It is now checked before the wait cancels the upstream body, so a refusal keeps the real 429 with its Retry-After and quota evidence intact.

Out of scope, deliberately: classifying an upstream 429 as org or project spend exhaustion is a separate contract with a separate owner; nothing in src/codex/quota-rejection.ts is touched here.

Closes #4708

Verification

No local suite, focused test, typecheck, install, or build step was run. The repository owner prohibits local suite execution in this lane after a past local run deleted real user home data. Verification is static reading plus hosted CI.

Static checks performed:

  • Read the upstream Codex client to establish the retry asymmetry this change depends on: its provider retry policy sets retry_429: false while retrying 5xx and transport failures, and a non-quota HTTP 429 maps to a terminal RetryLimit that is explicitly non-retryable. That is why 502 caused a client resend and 429 does not.
  • Confirmed classifyError's ordering: the new branch is keyed on the supplied type and placed before the status-429 branch, so an upstream 429 arriving with any other type is unaffected. Verified no test or source asserted rate_limit_exceeded for the passthrough budget refusal, so nothing depended on the old collapsed code.
  • Traced adapterFailureFromEvent: event.code overrides the classified code and event.errorType overrides the type, so the structured runTurn event produces exactly 429 / rate_limit_error / request_send_budget_exhausted.
  • Confirmed the passthrough path already excluded budget refusals from transport-failure classification, host-circuit failure recording and Codex account outcome recording, and that the two adapter catch sites call none of those — so the fix here is the wire and request-log classification, and the routing guard belongs on the runTurn preflight, which is the one place a local 429 could have reached a rotation arm.
  • Checked that the continuation loop's new condition precedes prepareSameTarget429Wait, which is what cancels the retained upstream body.
  • Registered the new test in both scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json, and updated structure/transports/responses.md.

Regression coverage added — tests/responses/responses-send-budget-errors.test.ts:

  • the distinct code survives serialization, while a provider 429 still classifies as rate_limit_exceeded;
  • the error class and the classifier name the same identity;
  • a structured terminal event yields 429 with the distinct code, with a negative control showing the same message unstructured still lands on 502 — the behaviour that made the client retry;
  • both adapter catch sites guard before every describeUpstreamConnectFailure call;
  • the runTurn rotation guard runs before the status is read;
  • the continuation loop consults the remainder before the body is cancelled.

Hosted CI: non-tip layer of a stacked lane, carrying [skip ci] under the maintainer-approved DEV-STACK-08 tip-only CI policy. The lane's CI gate runs on the tip branch.

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.

) [skip ci]

When a request exhausts its shared send budget the three dispatch paths
disagreed about what the client was told. Passthrough returned 429 and
explicitly declined to blame the provider. The adapter paths did not special
case SendBudgetExhaustedError: they fell through describeUpstreamConnectFailure
and answered 502 "Provider unreachable" for a refusal this process made itself.
The runTurn path pushed an unstructured error event, which is inferred back to
502 and delivered under HTTP 200.

The status is the load-bearing half, and it is worse than a mislabel. The Codex
client retries 5xx and does not retry a direct 429, so telling it the provider
broke makes it send the whole turn again -- the amplification this budget exists
to stop. Reporting the refusal as a quota code would stop the client for a
reason that is not true, and the retryable streaming rate-limit codes would
restart the stream, so neither is available.

Both adapter catch sites now answer 429 before describeUpstreamConnectFailure
can launder the refusal, and runTurn emits it as a structured terminal event
with its status, type and code on the event itself, because an unstructured
message is inferred back to 502.

classifyError keeps the distinct code by matching the supplied type rather than
the status. An upstream 429 still classifies as rate_limit_exceeded; only this
proxy's own refusal carries request_send_budget_exhausted. Before this the
passthrough path asked for that code and the classifier overwrote it, so even
the one path that got the status right could not be told apart afterwards.

A local 429 must also not look like a provider one to our own routing.
rotateRunTurnAdapterOnPreflight429 returns early on the code, before it reads
the status, so a refusal cannot rotate a credential or write a cooldown against
an account that rate-limited nothing -- a fake quota signal that outlives the
request and misroutes later ones.

The terminal-guard continuation loop never consulted sendBudgetExhausted() while
the main recovery loop did, so a spent budget could still same-key 429-replay on
a live stream. It is checked before the wait cancels the upstream body, so a
refusal keeps the real 429 with its Retry-After and quota evidence intact.

Upstream classification of a provider 429 as org or project spend exhaustion is
a separate contract and is not touched here.

Closes #4708
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 16, 2026 02:22
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: a03666eb-4b7c-4177-a780-9fc3aeee5c48

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 16, 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-16T02:26:12.199259Z 01a2b1f 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 github-actions Bot added the bug Something isn't working label Sep 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 76 / 80

이 PR은 이슈 #4708을 고친다. 요청의 공유 send budget이 바닥났을 때, 세 갈래 디스패치가 클라이언트에게 서로 다른 답을 주고 있었다. 현재 dev HEAD는 3070d64d8822c6d8c62989665f82ab665e4d164c (package 2.57.0)이다. 그 tip에서 패스스루(passthrough-dispatch.ts 약 698–702행)만 SendBudgetExhaustedError를 잡아 HTTP 429와 코드 request_send_budget_exhausted로 돌려준다. 어댑터 쪽(adapter-dispatch.ts)은 같은 예외를 describeUpstreamConnectFailure로 흘려 보내고 502 upstream_error / "Provider unreachable"로 포장한다. runTurn(run-turn-execution.ts)은 구조 없는 error 이벤트만 밀어 넣어, 추론 결과가 다시 502가 되고 HTTP 200 SSE 아래로 전달된다. 상태 코드가 핵심이다. Codex 클라이언트는 5xx를 재시도하고 직접 429는 재시도하지 않는다(retry_429: false). 프록시가 스스로 거절한 전송을 502로 말하면 클라이언트가 턴 전체를 다시 보내고, budget가 막으려던 증폭이 그대로 난다. 쿼터 코드로 위장하면 사실이 틀리고, 스트리밍용 재시도 가능 rate-limit 코드는 스트림을 다시 켜므로 쓸 수 없다. 그래서 정직한 답은 429 + 구별 코드뿐이다.

고치는 축은 네 곳이다. (1) src/lib/errors.tsSEND_BUDGET_EXHAUSTED_CODE 상수를 두고, classifyError가 공급된 type이 그 상수일 때 status-429 일반 분기에 먹히기 전에 rate_limit_error / request_send_budget_exhausted를 유지한다. 예전에는 패스스루가 코드를 넣어도 분류기가 rate_limit_exceeded로 덮어써서, 상태만 맞고 로그에서 구분이 안 됐다. 업스트림 429는 다른 type으로 오면 그대로 일반 rate-limit이다. (2) adapter-dispatch.ts의 두 catch(첫 교환·복구 다리)에서 SendBudgetExhaustedErrordescribeUpstreamConnectFailure보다 먼저 보고 formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, …)를 반환한다. (3) runTurn은 구조화된 단말 이벤트(status 429, errorType, code, message)를 밀어 넣는다. SSE가 이미 열린 뒤에는 이벤트에 실어야만 클라이언트에 429 의미가 남고, 메시지 문자열만 있으면 다시 502로 추론된다. (4) rotateRunTurnAdapterOnPreflight429는 코드가 거절 코드면 status를 읽기 전에 false를 돌려, 로컬 429가 자격 증명 로테이션·쿨다운을 쓰지 않게 한다. 가짜 쿼터 신호는 요청 밖으로 살아남아 다음 라우팅을 망가뜨린다. 이어 adapter-continuation.ts 단말-가드 429 루프에 !sendBudgetExhausted()를 넣어, 본문 취소(prepareSameTarget429Wait) 전에 잔여 budget를 본다. 메인 복구 루프·패스스루와 맞춘다. 업스트림 429를 org/project spend로 분류하는 일은 quota-rejection.ts 쪽 별 계약이라 손대지 않았다.

검증 쪽은 로컬 full suite/typecheck/build를 돌리지 말라는 레인 규칙을 지켰다. 대신 tests/responses/responses-send-budget-errors.test.ts가 분류기·에러 클래스 동일 신원, 구조화 단말 vs 비구조화→502 음성 대조, 어댑터 가드 순서(소스 문자열), 로테이션 가드가 status 읽기 전인지, continuation이 wait 전에 remainder를 보는지까지 단언한다. layout.json / test-layout-expected.json 등록과 structure/transports/responses.md ‘What a spent budget tells the client’ 절도 맞춰 두었다. types.ts/config.ts 분할·pre-split monolith 재편집과는 무관하다 — close-don't-rebase 대상이 아니다. Preview deploy는 계획에 없다. Closes #4708.

베이스는 dev가 아니라 codex/bl2-durable-ledger-wiring (#4756, durable spend ledger 생산 배선 / #4707)이다. 스택은 #4745(bl1 hop settle, base dev) → #4756(bl2) → 이 PR(bl3). 지금은 bl3 위에 열린 PR이 없어 이 브랜치가 레인 tip이다, 커밋 메시지에는 [skip ci]가 붙어 ‘비팁’이라고 적혀 있다. DEV-STACK-08 tip-only라면 tip SHA에서 unit·typecheck가 돌아야 하는데, head 01a2b1f0cdbfa05a270d6d22e9560f8fc972f3af rollup은 resolve-pr / hygiene / label / enforce-target·CodeRabbit만 있고 전체 suite는 없다. mergeable은 MERGEABLE, mergeStateStatus는 UNSTABLE이다.

라인 src/lib/upstream-retry.ts SendBudgetExhaustedError.code - 클래스는 여전히 문자열 리터럴 request_send_budget_exhausted를 들고, 새 상수는 errors.ts에만 있다. 테스트가 둘의 동치를 검사하지만 소스가 둘이다. 한쪽으로 import해 묶으면 드리프트가 줄어든다.
라인 src/server/responses/passthrough-dispatch.ts formatErrorResponse(429, "request_send_budget_exhausted", …) - 패스스루는 아직 리터럴이다. 동작은 같고, 상수로 바꾸면 세 경로가 한 이름을 쓴다. 이 PR 범위 밖이어도 후속 한 줄 정리 후보다.
경로 스택 tip / [skip ci] - 본문은 비팁·팁 CI 의존이라고 하지만, base가 bl2이고 bl3를 base로 하는 열린 PR이 없다. tip인데 skip이면 레인 게이트가 어디 head에서 도는지 불명확하다. skip을 빼거나 tip 레이어를 하나 더 두는지 정할 것.
경로 베이스 - #4745 → #4756이 dev에 오른 뒤(또는 retarget)에야 이 PR을 안전하게 합칠 수 있다. #4756 mergeStateStatus는 CLEAN이었고 #4745는 BLOCKED였다(같은 날 관측). 순서·게이트를 확인할 것.
경로 adapterFailureFromEvent - runTurn 구조화 이벤트는 classifyError에 errorType rate_limit_error를 넘긴 뒤 event.code로 덮어쓴다. 테스트·본문이 그 경로를 기대하므로 동작은 맞다. classifyError의 type===상수 분기는 주로 formatErrorResponse(패스스루·어댑터 JSON) 경로용이다.
경로 로컬 suite - 레인 금지 준수. 정적·소스 가드 테스트에 기대고 tip hosted CI로 확인하는 전제다.

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

너의 추천
KEEP. #4708의 세 경로 불일치·502 증폭·로테이션 오염·continuation remainder 누락을 상태+구별 코드+가드 순서로 정확히 맞춘다. 테스트·structure·layout 등록도 있다. types/config 분할 close-don't-rebase 대상 아님. 추천: 스택 앞단(#4745, #4756)이 dev에 오른 것을 확인한 뒤 머지하고 #4708을 닫는다. 그 전에 tip/[skip ci] 모순을 정리해 이 head(또는 진짜 tip)에서 hosted CI 초록을 확보하라. 상수 단일화는 머지 직후 한 줄 후속이어도 된다.

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

…4546) [skip ci]

Carried from PR #4717. The Command Code reasoning-effort retry commit is left
behind: that retry stays on the same selected key, so it is not needed to tell
account A's usage from account B's, and keeping it out keeps this layer
reviewable. The injected-executor fix it depended on is already here.

Adds the consumer-side assertion the attribution depends on. A row carries BOTH
the per-attempt records and the request total, and a reader that added them
would report 600 input tokens for 300 that were actually spent. usageAttributions
takes the attempts when a row has them and the entry row only when it has none,
so the parent total is a fallback for rows written before attempts existed rather
than another column to sum.

That arithmetic is also why hidden attempts must not be folded into the response
the client sees: the Codex client treats response.completed.usage as the exact
usage for that response and adds it to its durable turn and thread totals, so a
proxy-side sum would corrupt accounting it owns.

Closes #4717

Co-authored-by: thisisjun786 <259586770+thisisjun786@users.noreply.github.com>

@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: 01a2b1f0cd

ℹ️ 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 +189 to +191
status: 429,
errorType: "rate_limit_error",
code: SEND_BUDGET_EXHAUSTED_CODE,

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 Preserve the local 429 in combo runTurn preflight

When a combo target uses a runTurn adapter and its send budget throws here, both comboAttempt branches later in this function discard the new status, errorType, and code: lines 337–343 and 433–439 still return formatErrorResponse(502, "upstream_error", ...). Since core-combo.ts invokes every child with comboAttempt: true, a spent budget can therefore still be reported as the retryable provider failure this change is intended to eliminate. Convert the preflight event through adapterFailureFromEvent or otherwise preserve its structured 429 fields, and cover an actual combo/runTurn refusal rather than only testing the event helper.

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

Useful? React with 👍 / 👎.

// is inferred back to 502, which the Codex client retries.
? {
type: "error",
status: 429,

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 Apply the event status to buffered runTurn responses

For a non-combo stream: false request handled by a runTurn adapter, this 429 is only stored on the queued event. The buffered path passes the event through buildResponseJSON, but then constructs the HTTP response with new Response(...) and no status override, so the client still receives HTTP 200 rather than the documented direct 429. Derive the buffered response status from the terminal error event, using the same adapterFailureFromEvent mapping, so the newly added status affects the wire response.

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

Useful? React with 👍 / 👎.

…later (#4546) [skip ci]

The transient-hold resolver and the pool-wide recovery limiter added in #4626
still have no production caller, so #4701 is not closed here. Wiring them turned
up a defect in the thing being wired, and that has to be fixed first.

A withheld dispatch promises the caller a retry time. It was computed from the
probe pacing alone. When the RATIO limiter is what refused, the account usually
has no probe state at all -- nothing was ever granted for it -- so nextProbeAt
returned now, and the refusal told the caller to try again immediately. A
withheld dispatch that busy-loops puts the same load on an already-failing pool
as the dispatch it refused, which is the opposite of what the limiter is for.
It also violates the Retry-After half of #4701's completion criteria directly.

The limiter is the only thing that knows when its own window moves, so it now
says: nextRecoveryAt returns now while the allowance is unspent, and otherwise
the moment the oldest bucket still inside the window falls out. Every such
bucket started after now - windowMs, so the answer is always strictly in the
future, and it is a real change point rather than a guessed delay. The withheld
result takes the later of that and the probe pacing.

The existing zero-allowance test asserted only that the result was withheld,
which is why the defect survived the unit suite that was written to cover this
module. It now asserts the time as well.

Refs #4701
…#4546)

Each layer of this lane closes one seam of the #4546 amplification: the
credential hop that was charged twice, the spend ledger with no caller, the
refusal reported as a provider fault, the usage attributed to the wrong key, the
withheld recovery that said "retry now". What none of them checks is whether the
seams agree with each other.

This composes the real primitives -- the request execution budget, the durable
spend ledger with its request-scoped caller, the pool recovery limiter -- and
asserts that the numbers describe the same events: physical sends, budget
consumption, ledger reservation and settlement, and the refusal the caller is
given.

The scenarios are the incident's own: a request whose every layer tries to
recover, concurrent requests contending for one process-wide recovery
allowance, a caller that keeps its detour instead of adding a second trial to a
failing account, a fan-out child spending the parent's allowance rather than a
fresh one, and a restart that must neither reset a ceiling nor settle the same
send twice.

A fixture that only counted sends would have passed throughout the incident,
which is why every case ties a send count to the spend the ledger recorded for
it.
The account-change scenario the incident needs, written against current
behaviour because the #4710 refusal is owned by another lane and is not in this
stack yet.

What it pins now: continuation state is dropped and the turn continues, an
uploaded file reference is classified non-portable and is NOT removed by the
scrub, and the carriers must be read directly because the portability verdict
reports only the first reason it finds -- a body carrying both a response id and
a file reports the response id.

What it documents: once the refusal lands, that body must be declined before
dispatch and the refusal must win over the response id. The two properties above
are what the change has to preserve, so they are asserted now.

Also pins the accounting invariant that refusal owes: a decision made before
dispatch spends no send and books no ledger entry. A refusal counted as a send
would appear as provider load that never happened and would push a healthy
account toward a cooldown.
…fusing sends (#4546)

Four fixes, batched into one push so the queue only pays once.

1. src/server/responses/core.ts was 214 lines against a 210-line cap in
   tests/fixtures/file-size-baseline.json. The spend-observer wiring added four
   lines of comment and continuation. The comment is now one line and the
   expression one line, and the file is back at its cap. The ratchet only ever
   lowers caps, so growing past one is a hard failure rather than a nudge.

2. The spend tracker refused a dispatch on ANY ledger denial. Only an operator's
   configured ceiling should: capacity, durability and a journal this process
   could not prove complete all mean the ledger cannot ACCOUNT for the send,
   which is not a reason to refuse one. An unconfigured install keeps the count
   caps it already had and is not newly refused, and a degraded ledger must not
   become an outage.

3. The shared ledger is now resolved on the first charge rather than when the
   request is built. It opens a journal under the OpenCodex home, and a request
   that never dispatches has no business creating one; this also means the home
   in effect at dispatch is the one written to, instead of whichever home was
   current when the first request of the process happened to be constructed.

4. Three assertions in the new tests claimed states the code never reaches.
   The concurrent-probe case asserted a limiter refusal, but the second caller
   short-circuits on the lease before it reaches the limiter and costs no
   allowance; the shared bound is now proved by asking the limiter directly.
   The exhausted-ceiling case asserted final-recovery-spent where the total
   ceiling refuses first, so it asserts total-exhausted and checks reserveSpent
   separately for the point it was making. The unstructured-error control
   asserted an exact 502 where the property that matters is that the identity is
   gone, so it asserts that instead.

Tests are not typechecked -- tsconfig includes only src -- so a test that
asserts the opposite of what it claims passes silently. These were found by
reading, not by running.
Two source-of-truth failures from the previous tip run, both mine.

tests/lib/transient-budget-scope-source.test.ts pinned the exact core.ts line
that mints the request's send budget, and bl2 changed it to install the spend
observer. The oracle now matches the new shape and additionally asserts the
observer is attached at the same place, which is the property that actually
matters: a combo child inherits the parent's holder and must not open a second
set of ledger entries for the same physical sends.

tests/lib/spend-reservation-ledger.test.ts caught a real defect in the replay
reconciliation, not a stale expectation. An exhausted scope must still be
exhausted after a restart -- that is the whole reason the ledger is on disk --
and abandoning a replayed undispatched reservation handed its tokens back and
reset the ceiling.

The distinction I drew was wrong. "Open" does not prove nothing was sent: the
torn-tail rule immediately above says the journal may be missing its last
record, so a send can dispatch and die before its dispatch record lands.
Both live states now resolve to unresolved spend, which is the conservative
answer and the one that preserves the ceiling.

The bl2 wiring test asserted the old split and is updated to the new figures,
along with the structure contract and the tracker's own comment.
…gression

test(responses): pin the #4546 incident as one system, not five fixes (#4546)
…spatch

fix(routing): give a withheld recovery a retry time that is actually later (#4546)
fix(usage): attribute each attempt to the account that dispatched it (#4717)
@lidge-jun

Copy link
Copy Markdown
Owner Author

Cascading downward. Internal budget exhaustion no longer leaves as 502 upstream_error; a passthrough that already asked for the right code was being overwritten by classifyError.

Evidence at the verified tip d9e5b28 (tree b69974e3f2c5da4e1cda8e302943a5ea6b107474), from run 35055864527:

  • test 1/4, 2/4, 3/4 and 4/4 all completed with conclusion success, confirmed through the check-runs API rather than the check rollup, so the heavy jobs actually executed and were not path-filtered.
  • macos 1/2 and 2/2, gates and the aggregate ci check all completed with conclusion success.
  • The same commit also carries a ci failure and a gates cancellation from run 35055864360. Its annotation reads needed job(s) did not pass: changes=cancelled: that run was superseded by workflow concurrency when the six branches were pushed together. It is a cancellation, not a test failure.
  • The lane absorbed dev at 5e3029e from the bottom layer upward, so each pull request keeps its own layer diff (8 / 11 / 8 / 51 / 2 / 10 files) and the tip stays reviewable. Four merge conflicts were resolved by keeping both sides: the documentation sections that dev and this lane each added, and all four test imports. No test was deleted and no dev-only content was lost.
  • git merge-tree --write-tree origin/dev <tip> reports a clean merge.
  • Ancestry verified so each layer closes as MERGED: bl1, bl2, bl3, bl4 and bl5 are all ancestors of this tip.

Chained-child stacks merge top-down, so this lands in the parent branch and cascades to dev. CI evidence transfers by tree identity at each step.

Maintainer integration decision under MAINTAINERS.md / AGENTS.md: a maintainer with maintain or admin access may integrate into dev without a second maintainer approval, recording the decision and exact-head CI evidence.

@lidge-jun
lidge-jun merged commit 6e254f3 into codex/bl2-durable-ledger-wiring Sep 16, 2026
7 checks passed
@lidge-jun
lidge-jun deleted the codex/bl3-budget-refusal-semantics branch September 16, 2026 04:49
@github-actions github-actions Bot changed the title fix(responses): report a spent send budget as this proxy refusing (#4708) [WRONG BRANCH] fix(responses): report a spent send budget as this proxy refusing (#4708) Sep 16, 2026
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • wrong target branch (codex/bl2-durable-ledger-wiring); retarget to dev.

What to do

  • Retarget this PR to dev — all contributions go to dev.

Its title has been prefixed with [WRONG BRANCH].
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.

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.

2 participants