Repository navigation
fix(reasoning): recover when the upstream names a reasoning item; remember long histories - #246
Merged
Merged
Conversation
…ember long histories A Codex conversation that moves to an upstream account of another organisation is rejected with invalid_encrypted_content until its reasoning ciphertext is removed. Two things kept that from working for long multi-agent conversations. The recovery policy refuses an unindexed rejection whenever the body holds any ciphertext besides the reasoning items, because the rejected carrier could be the other one. Codex multi-agent histories carry inter-agent message bodies as ciphertext in a content part, so every such history was refused, although the upstream message names the rejected item by id. A rejection that names a reasoning item which carries ciphertext in the sent body is now recovered the usual way: every reasoning ciphertext is removed and the request is retried once on the same account. Other carriers are not modified. A rejection that names no reasoning item is refused as before. The rejected-ciphertext memory accepts 64 hashes per call and was given the whole history in one call, so a history with more reasoning items was never remembered or stripped before sending: every request was rejected and retried. The memory is now read and written in batches of 64 inside the same I/O budget. The continuation diagnostic counts ciphertext held in content parts as history.nested_encrypted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Codex conversations on
/v1/responsesend withcontinuation_state_unavailableand cannot be resumed. Production, 2026-10-04/05: 48 such failures in three conversations, all withrecovery.not_attempted_reason = ambiguous_encrypted_carriers.What happened in the reported conversation (Ops records of API key 101, group 53):
invalid_encrypted_content, recovery removed every reasoning ciphertext and the retry succeeded.cache_skipped_itemswas 0 each time: the rejected-ciphertext memory accepts at most 64 hashes per call and received the whole history (230+ reasoning items) in one call, so nothing was remembered and nothing was stripped before sending.agent_messagewhose body is ciphertext in a content part (content[].encrypted_content). The policy compares the number of allencrypted_contentkeys in the body with the number of reasoning ciphertexts; they now differed, the rejection was treated as ambiguous and no retry was made. 12 attempts failed the same way.The guard is from 2026-09-07 and was written for compaction items. It is not a regression of the recent releases; session stickiness did not change either. The upstream error has
param: nulland names the rejected item only in its message:The encrypted content for item rs_… could not be verified.Change
agent_messagecontent parts are never modified. Without a named reasoning item the refusal stays. The policy receives one boolean (named_cipher), not the message.skipRejectedHistoryand the write inTryRecovercall the store in batches ofOpenAIReasoningStateMaxLookupEntriesinside the existing shared I/O budget. After the first recovery on an account the old ciphertexts are stripped before sending, so later requests are not rejected and ciphertext minted on the new account is kept.history.nested_encryptedcounts ciphertext held in content parts. Backend JSON only.Unchanged: recognised codes, the single retry, no account switch, protected statuses, the global switch, WS and Messages.
Limits
agent_messagebody was itself minted by another organisation, the retry is rejected again and the request ends as before. That ciphertext cannot be removed without losing the message.Verification
go test ./internal/codexruntime/recovery/go test -tags unit ./internal/service/ -run 'TestOpenAIContinuationDiagnostic|TestOpenAIReasoningRecovery|ReasoningRecovery|RejectedReasoning|ProjectReasoningCipher|SystemPrompt.*Reasoning'go test -tags unit ./internal/repository/ -run 'ReasoningState|RejectedReasoning'agent_messagepresent (HTTP error andresponse.failedenvelope), no id in the message, anamsg_id, a longer identifier with the same prefix; theagent_messageitem is byte-identical after the strip; a 150-item history is written and read in three batches against a store that refuses more than 64 per call.🤖 Generated with Claude Code