Skip to content

[Bug]: a conversation carrying uploaded files should pin to its issuing account instead of being refused after rotation #4778

Closed

Description

@lidge-jun

Client or integration

Codex CLI

Area

Authentication and account pool

Summary

#4710 is now fail-closed: an account change that would orphan an uploaded file is refused with 409 account_change_file_scope instead of forwarding a reference the new account cannot read. That stops the invalid request, but it does not restore the conversation.

The refusal is a middle step, not the final shape. The real answer is to pin a conversation that carries uploaded files to its issuing account, so the rotation that causes the problem never happens.

The cost of leaving it at refusal is specific and falls entirely on pool users. A file reference stays in the conversation's history, so once rotation has moved a conversation carrying an attachment, every later turn presents the same reference and is refused the same way. The conversation is effectively blocked. The error text says this and names the two escapes (re-upload under the serving account, or start a new conversation), but both of those ask the user to absorb a routing decision they never made and cannot see.

Pool rotation is automatic and repeated, so this is not a rare shape: any conversation with an attachment is one rotation away from being blocked.

Reproduction

  1. Run a Codex pool with two or more accounts.
  2. Start a conversation and attach a file, so the request carries file_id and the file is issued by account A.
  3. Let routing move the conversation to account B, for example by crossing autoSwitchThreshold or through quota-driven rotation.
  4. Every subsequent turn is refused with 409 account_change_file_scope, because the reference remains in history.

Suggested direction

Treat "this conversation carries uploaded files" as a binding constraint in src/codex/routing.ts affinity selection, so the issuing account is retained while such references are live. conversationCarriesUploadedFiles in src/server/responses/account-change-state.ts already answers that question from the body alone and is consulted before an alternate is resolved, so the predicate exists; what is missing is the routing layer honouring it when it chooses.

Open questions for whoever picks this up: what happens when the issuing account genuinely cannot serve (exhausted, paused, needs reauth) and the conversation carries files -- pinning and availability conflict there, and refusing is probably still correct in that corner; and whether the pin should expire with the affinity record or with the file references themselves.

The refusal from #4710 should stay regardless. Pinning reduces how often it fires; it cannot be the only guard, because an account can always become unusable.

Version

2.57.0 (dev @ 3070d64)

Operating system

macOS 15.5

Logs or error output

409 account_change_file_scope (refused before dispatch; no upstream send)

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 16, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 75 / 80

    이 이슈는 #4710의 다음 단계다. #4710/#4777이 ‘계정을 바꾸면 업로드 파일이 고아가 되는 이동’을 fail-closed로 거절하는 중간 단계라면, #4778은 그 거절이 필요 없도록 파일 있는 대화를 발행 계정에 핀 하라는 라우팅 계약이다. 거절만 두면 참조가 히스토리에 남아 이후 매 턴이 같은 거절로 막히고, 사용자는 재업로드나 새 대화로 라우팅 결정을 흡수해야 한다.

    현재 dev HEAD b3035fe292의 src/server/responses/account-change-state.ts에는 collectFileIds(약 85행대)·canPortConversationState의 uploaded-file-ids(66–67행)가 있다. 스크럽은 여전히 continuation 필드 위주라 file-only 구멍은 #4710이 말하는 그대로다. 열린 PR #4777(fix(responses): refuse an account change that would orphan an uploaded file)가 conversationCarriesUploadedFiles와 accountChangeFileReferenceRefusal을 추가하고 alternate-account 재시도 두 곳도 막는다. #4777 본문이 핀은 라우팅 일이라며 이 이슈(#4778)를 가리킨다.

    tip의 src/codex/routing.ts 어피니티는 프롬프트 캐시 최적화·쿼타 헤드룸·모델 디투어 중심이다. ‘이 대화에 업로드 파일이 살아 있으면 발행 계정을 유지’하는 제약은 selection 경로에 없다. 이슈가 말하듯 술어는 #4777이 body만으로 답할 수 있게 만들고, 빠진 것은 그 답을 affinity selection이 존중하는 일이다. 풀 회전·autoSwitchThreshold가 자동이라 attachment 있는 대화는 한 번의 회전에 막힐 수 있다.

    거절(#4710/#4777)은 그대로 둬야 한다. 발행 계정이 소진·pause·reauth면 핀과 가용성이 충돌하고, 그때는 거절이 맞다. 핀은 거절이 얼마나 자주 터지는지만 줄인다. 핀 TTL을 affinity 기록에 묶을지 file 참조 수명에 묶을지도 열린 질문이다. cx 레인(#4764→#4765→#4777)이 착륙하기 전에 라우팅 핀 PR을 직접 dev에 올리면 재작업 위험이 크다.

    #4546 계정 변경 이식성·풀 비용과도 맞닿아 우선순위가 높다. types/config 스플릿과 무관하고 미리보기 배포는 없다.

    경로 src/server/responses/account-change-state.ts uploaded-file-ids / collectFileIds - tip에 분류는 있고 fail-closed 거절·핀은 없다.
    경로 PR #4777 conversationCarriesUploadedFiles - body만으로 술어. 거절은 여기, 핀은 #4778.
    경로 src/codex/routing.ts affinity selection - 업로드 파일 제약이 없어 회전이 대화를 막을 수 있다.
    교차 #4710 OPEN / #4777 OPEN(cx 레인 tip) - 거절 먼저, 핀은 후속.

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

    너의 추천
    받아라. #4777이 #4710을 닫게 랜딩한 뒤, 바로 routing.ts에서 file-carrying 대화 발행 계정 핀 PR을 이어라. 거절 가드는 제거하지 마라. cx 부모(#4764/#4765)가 먼저다.

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

  2. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    OwnerAuthor

    Landed via #4797 at f35fbe8

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