Skip to content

docs(devlog): record why the root workflow budget expires a long session (#4546) - #4653

Merged
lidge-jun merged 2 commits into
devfrom
codex/4546-wf-budget-window
Sep 14, 2026
Merged

lidge-jun merged 2 commits into
devfrom
codex/4546-wf-budget-window

Conversation

@lidge-jun

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

Copy link
Copy Markdown
Owner

Summary

A Codex session spent hours dispatching subagents and every dispatch failed, across three unrelated providers, with a 429 that reads as a provider rate limit. It was not the model, the account or the upstream — the refusal came from this proxy, and this unit records why and what to do about it.

workflowSendCeilingReached compares a per-root send count against maxPhysicalSends, and that count only ever grows. state.children is a Set that is only ever added to, its own doc comment reading "Distinct children one root may ever create". The root id is x-codex-parent-thread-id, which for Codex is the session. So for a long-lived session the cap is not a fan-out guard, it is an expiry: a session that made 256 sends in its first hour is refused for the rest of the process even if it sends nothing for a day.

The justification in the code says a per-request cap "cannot bound a fan-out that sends once per child seven hundred times". That is a burst concern, and a burst is bounded by a rate. A lifetime total cannot tell seven hundred sends in a minute from two hundred and fifty-six spread over four hours, and it refuses both.

Two things made this expensive rather than merely annoying. The refusal is a 429 from a proxy that also forwards provider 429s, with nothing on the record saying which one it was — the same defect #4639 fixed for the synthetic 503. And the only way out is restarting the proxy: resetWorkflowBudgetsForTest is named for its audience and workflowBudgetSnapshot has no caller outside the module, so the state that decided the refusal cannot be read or cleared from outside the process.

The unit plans two layers: 010 makes the send and distinct-child ceilings windowed so a rate is bounded rather than a lifetime, and 020 makes the refusal name the ceiling that fired, marks it as proxy-origin on the record, and gives an operator a bounded, recorded way to read and clear one root. It also records the two-probe reproduction, which separates a proxy refusal from a provider one in a single step.

Docs only. No source behaviour changes in this pull request.

Verification

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

Nothing in the build, typecheck or test path reads from devlog/, so this change cannot affect them; privacy:scan does read it and the unit contains no credentials, account identifiers or unfixed-defect reproduction beyond a rate ceiling that is already public in the source.

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

  • Documentation
    • Added planning documentation describing workflow budget enforcement, including current lifetime-based limits and proposed rolling-window tracking.
    • Documented expected handling for budget refusals, including clearer ceiling-specific messages and distinction from upstream rate limits.
    • Outlined proposed management API capabilities for viewing and clearing an individual workflow budget.
    • Recorded acceptance criteria, operational constraints, and verification plans for the planned changes.

…ion (#4546)

A Codex session dispatching subagents was refused across three unrelated providers with a 429 that reads as a provider rate limit. The refusal was this proxy: workflowSendCeilingReached compares a per-root send count that only ever grows, keyed on x-codex-parent-thread-id, so for Codex the cap is a session expiry rather than a fan-out guard. A probe carrying the session id was refused while a probe carrying a fresh root id was served, and restarting the proxy served both.

The unit records the diagnosis and plans two layers: windowed ceilings so a rate is bounded rather than a lifetime, and a refusal an operator can read, name and clear without restarting.

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.
…efusal (#4546)

One body, one upstream, two answers separated only by the claimed root id. That single check rules out the provider, the account and the model, and it is what the next person should run before spending hours on a status page. Also records that a restart erases the evidence, which is why the obvious remedy hides the cause.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 14, 2026 19:14
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 14, 2026
@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: 9908c726-70ce-449f-81f5-9cd1d30a7562

📥 Commits

Reviewing files that changed from the base of the PR and between ce0ac61 and 095f45b.

📒 Files selected for processing (3)
  • devlog/_plan/260915_workflow_budget_window/000_unit.md
  • devlog/_plan/260915_workflow_budget_window/010_windowed_ceilings.md
  • devlog/_plan/260915_workflow_budget_window/020_legible_refusal.md

📝 Walkthrough

Walkthrough

The PR adds three planning documents. They describe a per-root lifetime budget defect, a timestamped windowing plan, and a management API plan for legible, operator-controlled refusals. No implementation code changes are included.

Changes

Workflow budget controls

Layer / File(s) Summary
Incident diagnosis and work scope
devlog/_plan/260915_workflow_budget_window/000_unit.md
Records the per-root lifetime send ceiling, reproduction steps, restart behavior, planned write scope, verification posture, and failure condition.
Windowed ceiling plan
devlog/_plan/260915_workflow_budget_window/010_windowed_ceilings.md
Defines timestamped send and child tracking, a ten-minute default window, unchanged concurrency and eviction rules, invariants, and acceptance criteria.
Legible refusal and management plan
devlog/_plan/260915_workflow_budget_window/020_legible_refusal.md
Defines denial-type reporting, proxy-origin request logging, management reads, and bounded operator clears for individual roots.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Change: Other · Severity of issue fixed: Medium

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/4546-wf-budget-window

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 f2dd9dd into dev Sep 14, 2026
18 of 19 checks passed
@lidge-jun
lidge-jun deleted the codex/4546-wf-budget-window branch September 14, 2026 19:14
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 71 / 80

설명

이 PR은 #4546 비용 가드 스택에서 소스 코드는 건드리지 않고, 운영 중에 실제로 겪은 「긴 Codex 세션이 갑자기 전 제공자 429로 죽는」 현상의 원인을 기록하고, 다음 두 층(wfb 창 단위 천장, wfc 읽기 쉬운 거절)의 로드맵만 devlog/_plan/260915_workflow_budget_window/에 남깁니다. 지금 dev HEAD는 ce0ac617d(#4651 wpj: 제네릭 OAuth 홉 두 자리가 공유 예산을 예약)이고, 그 앞에서 #4612 workflow-budget.ts가 루트당 물리 전송·구분 자식 상한을 이미 심어 둔 상태입니다. 이 단위는 그 상한이 버스트 가드가 아니라 세션 만료처럼 동작한다는 점을 문서로 고정합니다.

진단은 현재 checkout과 맞습니다. src/lib/workflow-budget.tsDEFAULT_WORKFLOW_BUDGET_POLICYmaxPhysicalSends: 256, maxDistinctChildren: 64이고, 주석도 「루트 수명 전체의 물리 전송」「루트가 평생 만들 수 있는 구분 자식」이라고 적혀 있습니다. workflowSendCeilingReachedstate.sends가 정책 이상인지만 보고, state.sendsstate.children Set은 줄어들지 않습니다. 루트 키는 x-codex-parent-thread-id이고(src/server/index.ts, src/server/responses/core.ts), Codex에서는 그게 세션이라서, 첫 한 시간에 256번 보낸 세션은 그 뒤 하루를 쉬어도 프로세스 재시작 전까지 거절됩니다. 재현 절차(같은 body, 오래된 세션 id vs 새 root id, 재시작 후 둘 다 200)도 이 메모리 천장 이야기와 정확히 맞습니다.

비싸게 만든 두 점도 코드와 일치합니다. 거절은 workflow_budget_exhausted 계열 429로 나가고(#4639가 고친 「로컬 합성 거절을 업스트림처럼 보이게 하는」 계열과 같은 냄새), workflowBudgetSnapshot은 모듈·테스트에만 있고 관리 API 호출자가 없으며, resetWorkflowBudgetsForTest 이름 그대로 테스트용입니다. 그래서 운영자가 원인 레코드를 읽거나 한 루트만 비울 길이 없고, 「프록시 재시작」이 유일한 출구이자 동시에 증거를 지우는 출구가 됩니다. docs-only(+199/−0), base dev, MERGEABLE, hygiene/ci 녹색이라 types.ts/config.ts 분할 무효화·중복 close 대상도 아닙니다.

방향(평생 합계 → 창 안 개수, 거절에 천장 이름·프록시 출처, 관리면으로 한 루트 읽기/지우기)은 #4546이 「예산을 제안이 아니라 경계로」 지키려던 것과 같은 축입니다. 숫자만 키우는 실패 모드를 명시적으로 금지한 것도 맞습니다. 다만 이 PR 자체는 구현이 아니라 계획서이므로, 머지 가치는 「다음 구현 PR이 이 단위를 기준으로 갈라지게」 만드는 쪽에 있습니다.

경로 devlog/_plan/260915_workflow_budget_window/000_unit.md - 원인·재현·실패 조건(숫자만 키우기 금지)이 초등학생도 따라갈 수 있게 적혀 있고, 현재 workflow-budget.ts / index.ts / responses/core.ts 경로와도 맞습니다. 재현 curl의 모델 id gpt-5.6-terra는 테스트·카탈로그에 실제로 쓰이는 이름입니다.
경로 010_windowed_ceilings.md - 「창 안 개수는 평생 개수보다 크지 않으니 기본 설치에서 거절이 늘지 않는다」는 안전 논증이 분명합니다. 다만 기본 창으로 적힌 「십 분」은 아직 주장이지 측정값이 아닙니다. 256 sends / 10분 ≈ 초당 0.4회 이상이면 거절인데, 이 숫자가 Codex 서브에이전트 팬아웃의 「학대」와 「정상 작업」 경계를 실제로 나누는지는 메인테이너가 한 번 골라야 합니다.
경로 010_windowed_ceilings.md (eviction) - 창이 지나면 더 이상 exhausted가 아니라서 다시 축출 가능해진다는 말은 #4546 축출 규칙과 맞습니다. 그런데 「창이 지나 여유를 되찾은 루트가, 곧바로 다시 버스트해도 같은 창 규칙으로만 막힌다」는 수용 조건은 이미 있고, 버킷 저장이 루트당 유계라는 4번 조건이 구현 시 가장 쉽게 빠질 부분입니다(타임스탬프 리스트가 영원히 늘면 창 도입이 새 누수가 됩니다).
경로 020_legible_refusal.md - #4639식 레코드 provenance를 429에도 적용하고, denial 타입(workflow-sends-exhausted vs workflow-children-exhausted)을 본문·로그에 살리고, 관리 API로 한 루트만 읽고 지운다는 범위는 맞습니다. Write scope에 src/server/management/가 들어가 있는데, 이 문서에는 아직 구체 경로(어느 GET/DELETE)가 없습니다. 구현 PR에서 새 라우트를 만들지 기존 logs/usage 면에 붙일지 미리 한 줄만 박아 두면 후속 PR이 덜 흔들립니다.
경로 단위 전체 vs spend-reservation-ledger - 헤더 주석대로 토큰 상한은 재시작을 살아남고 카운트 상한은 프로세스 메모리입니다. 이번 단위는 카운트 쪽만 창으로 바꿉니다. 재시작 뒤에도 「같은 세션이 토큰 장부로 막히는지」와 「전송 창이 비는지」가 서로 다른 축이라는 점을 000에 한 문장 더 적으면, 다음에 토큰 창을 건드릴 때 헷갈리지 않습니다.

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

  • 기본 창 길이를 10분으로 확정할지, 세션 친화적으로 더 길게(예: 1시간) 갈지, 아니면 설정 가능 기본값만 두고 숫자는 운영 관찰 뒤에 정할지
  • wfb(창)와 wfc(읽기 쉬운 거절·관리 clear)를 한 구현 PR에 묶을지, 문서 순서대로 wfb 먼저·wfc 나중으로 나눌지
  • 관리 clear API를 새 management 라우트로 둘지, 기존 대시보드/로그 면에 붙일지
  • epic [Bug]: Account pool routing destroys prompt cache and triggers 10x-50x token burn death-spiral above 80% usage threshold #4546 / 기존 090·wp 스택에 wfb/wfc를 공식 층 이름으로 올릴지(문서만 머지해도 이슈 보드에 층을 박을지)

너의 추천
docs-only이고 진단·재현·실패 모드가 현재 dev 코드와 일치하니 이 PR은 그대로 dev에 머지하세요. 구현은 분리해서, 먼저 010 창 카운터(+버킷 유계 테스트)를 넣고, 이어서 020으로 거절 본문·레코드 provenance·관리면 읽기/한 루트 clear를 붙이는 순서가 안전합니다. 숫자만 키우는 PR이 오면 이 단위의 「What would make this fail」을 근거로 close하세요. types/config 분할·중복 close 대상 아닙니다.

이 댓글은 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: 095f45b2e5

ℹ️ 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 +83 to +86
# any root the process has not seen: served
curl -s -o /dev/null -w '%{http_code}\n' -N -X POST http://127.0.0.1:10100/v1/responses \
-H 'Content-Type: application/json' -H 'Accept: text/event-stream' \
-H "x-codex-parent-thread-id: probe-$(date +%s)" -d "$BODY"

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 Remove the unfixed budget-bypass reproduction from devlog

Publishing this _plan unit exposes an unfixed spend-guard bypass: the command tells a data-plane client how to evade an exhausted root by minting a new x-codex-parent-thread-id, while the planned windowing and management fixes have not shipped. Repository policy explicitly places bypass reasoning and reproduction steps for unfixed defects in scratch rather than tracked devlog; remove this unit from the commit and keep it under .tmp/ until the fix is public.

AGENTS.md reference: AGENTS.md:L124-L130

Useful? React with 👍 / 👎.

Comment on lines +89 to +92
Two answers from one proxy, one body and one upstream, separated only by which
root the request claims. That is what rules out the provider, the account and the
model in a single step, and it is the check to run first the next time a fan-out
starts failing for no visible reason.

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 Keep the upstream account fixed in the two-probe test

In a Codex Pool installation with multiple stored accounts, these probes do not hold the upstream constant: codexPoolAffinityKey derives conversation affinity from these headers (src/codex/auth-context.ts:112-117), and account resolution consumes that affinity key (src/codex/auth-context.ts:935-943). Replacing the parent ID can therefore select a different account, so a 429/200 result does not rule out the account or upstream as claimed; pin an account/route or corroborate the synthetic terminal source before presenting this as a definitive diagnostic.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

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-14T19:17:39.577603Z 095f45b 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.

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

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant