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

Activity

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

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