Repository navigation
feat(reasoning): hand a request to another account when this one cannot read its ciphertext - #247
Merged
Merged
Conversation
…ot read its ciphertext Ciphertext in a Codex conversation (reasoning items, inter-agent message bodies, encrypted tool arguments) is readable only by the upstream organisation that issued it. When an account of another organisation rejected it and the stripped retry could not help, the request ended on that account and every client retry hit the same account, although another account of the group might accept the request unchanged. StopError now decides once whether an attempt ended as an account mismatch: the upstream said it could not decrypt or verify ciphertext in the request, nothing on this account repaired it, and nothing of the attempt reached the client. The attempt's own failure is returned marked for the next account, in request scope and without a health penalty, instead of as a terminal. An ordinary validation rejection, a request with server-held history, an account status, a cancelled request and the switched-off recovery never are. The Responses loop hands a mismatch to the next account without the same-account retry policy, at most three times per request and within the normal switch budget. When accounts run out the client receives the first mismatch failure, as before; an account that ends the request with its own answer is still the answer. A sticky binding this request moved goes back to the first rejecting account when no account answered, and stays with an account that accepted or was already answering; a binding the scheduler kept elsewhere is not touched. The continuation diagnostic marks such an attempt with recovery.account_mismatch. Chat Completions, Messages and WebSocket are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/ciphertext-account-switch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A Codex conversation carries ciphertext that only the upstream organisation that issued it can read: reasoning items, the bodies of multi-agent
agent_messageitems, encrypted tool-call arguments. In a group whose accounts belong to different organisations, an account of another organisation rejects the history withinvalid_encrypted_content.Reasoning recovery (#246) removes reasoning ciphertext and retries once on the same account. That cannot help when the rejected ciphertext is one the gateway cannot remove (an
agent_messagebody, encrypted arguments), or when recovery is not possible. The request then ended with a request-scoped terminal, the account was never changed, and the sticky binding kept every client retry on the same account, although another account of the group might belong to the issuing organisation and accept the request as it is.Owner decision 2026-10-05: try other accounts in that case.
Change
Service.
StopErrordecides once, when an attempt ends, whether it ended as an account mismatch: the upstream said it could not decrypt or verify ciphertext in the request (structured code, the continuation-state message classification, or the wordingcould not be decrypted, which is how the rejection of an inter-agent message body arrives, without a code), nothing on this account repaired the request, and nothing of the attempt reached the client. The attempt's own failure is then returned marked (CiphertextAccountMismatch, next account, request scope, no health penalty, no same-account retry) instead of as a terminal.Never a mismatch: the reasoning-recovery switch is off; semantic output was committed or a failure terminal forwarded; the request was cancelled; upstream status 401/402/403/407/429; the request carries
previous_response_idorconversation; the stripped retry was prepared but not sent; an ordinary validation rejection, also after a stripped retry; a stripped retry that failed with a 5xx or a transport error.Handler (
POST /v1/responses, including compact and auto-passthrough). A mismatch hands the unchanged request to the next account, without the same-account retry policy, at mostmaxOpenAICiphertextAccountSwitches= 3 times per request and within the normal switch budget. Each account runs its own recovery. When accounts or budget run out the client receives the first mismatch failure, which is what it received before; an account that ends the request with an answer of its own is still the answer. Chat Completions, Messages and WebSocket are unchanged.Sticky binding. Selecting a later account may move the session to it. When no account answered, a binding this request moved goes back to the account that first rejected the conversation; a binding the scheduler kept elsewhere (profit gate, sticky escape, spill-over) is not touched. An account that accepted, or was already producing output when its attempt failed, keeps the session; where selection does not bind (profit gate), a binding that still points at the rejecting account is moved to the accepting one.
Ops. The continuation diagnostic marks such an attempt with
recovery.account_mismatch; an attempt recognised by message only is recorded too. No UI change.Limits
could not be decryptedcomes from third-party reports; production has not yet produced a sample of a rejection after the stripped retry.X-Sub2API-Reasoning-Recoverynames the last recovery action of the request, which may be an earlier account's.Verification
go test -tags unit ./internal/handler/(whole package, before the last round of fixes; the mismatch and recovery tests again after it).go test -tags unit ./internal/service/(whole package, same): one failure,TestOllamaProbeCallback_StaleLongDoesNotOverrideNewShort, which fails the same way onmainon this Windows machine and passes in CI.🤖 Generated with Claude Code