Skip to content

Account pool: continuing a thread after pool selection moves to a new account fails with 400 encrypted content could not be verified (account-bound reasoning ciphertext) #2247

Description

@nowhere1975

Client or integration

Codex App (native ChatGPT pool routing)

Area

Proxy and routing (account pool / thread affinity / encrypted reasoning replay)

Summary

With the ChatGPT account pool (codexAccountMode: pool) and two plus accounts registered, continuing an existing thread after pool selection moves to a different account fails with an upstream 400:

The encrypted content 6871...ef-0 could not be verified. Reason: Encrypted content could not be decrypted or parsed.

The request's historical reasoning items carry encrypted_content minted by the account that produced them (stored server-side on that account). When the pool routes the thread to a different account - after adding a new account (it becomes selected), or after the service restarts and in-memory thread affinity is lost - the new account's backend cannot verify the old account's ciphertext and rejects the whole request.

Reproduction

  1. opencodex 2.28.0, codexAccountMode: pool, one existing plus account (main), start a conversation (any model, e.g. gpt-5.6-luna)
  2. Add a second plus account (account pool) - the new account becomes selected
  3. Continue the SAME thread in the Codex app (do not start a new session)
  4. Request is routed to the new account namespace (openai-); upstream returns 400 Encrypted content could not be decrypted or parsed

Request log (redacted):

{
  "provider": "openai-pe17f2a",
  "admissionKind": "loopback",
  "inboundProtocol": "responses",
  "accountLogLabel": "pe17f2a",
  "requestedModel": "gpt-5.6-luna",
  "status": 400,
  "errorCode": "invalid_request_error",
  "upstreamError": "The encrypted content 6871...ef-0 could not be verified. Reason: Encrypted content could not be decrypted or parsed."
}

A brand-new session on the newly selected account works fine (first turn has no historical ciphertext).

Root cause analysis

  • Native ChatGPT responses carry encrypted_content on reasoning output items; the ciphertext is account-bound (server-side key/storage).
  • Codex threads replay history verbatim, so a continued thread sends the previous account's encrypted_content back upstream.
  • Pool affinity (resolveCodexAccountForThreadDetailed, CodexThreadAffinityExpiredError) keeps threads on their original account while the in-memory binding survives - but adding an account / restarting the proxy can re-select (ocx account current shows the new account as selected) or drop the binding, so the thread's next request is sent with the OTHER account's credentials and history.
  • The affinity-expired path returns 409 start a new session, but in this flow the request is admitted and forwarded, and the failure surfaces as an opaque upstream 400 instead.

Suggested fix

  1. Persist thread-to-account affinity across restarts (and across account-pool membership changes) so existing threads never silently switch accounts.
  2. Defense in depth: when pool routing selects an account different from the one that produced the thread's historical encrypted_content items, either refuse with the existing 409 (Codex thread account affinity expired; start a new session) or scrub the account-bound ciphertext from the forwarded history.

Version

2.28.0

Operating system

macOS

Metadata

Metadata

Assignees

Labels

account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions