Skip to content

fix(reasoning): recover when the upstream names a reasoning item; remember long histories - #246

Merged
HTExplicit merged 1 commit into
mainfrom
fix/reasoning-recovery-carriers
Oct 5, 2026
Merged

HTExplicit merged 1 commit into
mainfrom
fix/reasoning-recovery-carriers

Conversation

@HTExplicit

Copy link
Copy Markdown
Owner

Problem

Codex conversations on /v1/responses end with continuation_state_unavailable and cannot be resumed. Production, 2026-10-04/05: 48 such failures in three conversations, all with recovery.not_attempted_reason = ambiguous_encrypted_carriers.

What happened in the reported conversation (Ops records of API key 101, group 53):

  1. 21:38:45 its OpenRouter account ran out of credit and failover moved the conversation to an account on api.openai.com, a different upstream organisation. Reasoning ciphertext minted through the first organisation cannot be verified by the second.
  2. For the next 38 requests the upstream answered 400 invalid_encrypted_content, recovery removed every reasoning ciphertext and the retry succeeded. cache_skipped_items was 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.
  3. 21:57 the history gained a Codex multi-agent agent_message whose body is ciphertext in a content part (content[].encrypted_content). The policy compares the number of all encrypted_content keys 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: null and names the rejected item only in its message: The encrypted content for item rs_… could not be verified.

Change

  • Selection. A rejection whose message names, by id, a reasoning item that carries ciphertext in the sent body is no longer ambiguous when other ciphertext carriers exist. It is recovered like any unindexed rejection: every reasoning ciphertext is removed, one retry, same account. Compaction items and agent_message content parts are never modified. Without a named reasoning item the refusal stays. The policy receives one boolean (named_cipher), not the message.
  • Memory. skipRejectedHistory and the write in TryRecover call the store in batches of OpenAIReasoningStateMaxLookupEntries inside 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.
  • Diagnostic. history.nested_encrypted counts 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

  • The id match needs reasoning ids on the wire: API-key accounts and auto-passthrough. The OAuth / Setup Token transform removes reasoning ids before sending, so a history with another ciphertext carrier is still refused there.
  • If an agent_message body 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.
  • The memory keeps one entry per remembered ciphertext per source (156 bytes, 32768 entries overall, LRU). Long histories now use it; eviction falls back to reject-and-retry.

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'
  • New cases: named reasoning item with an agent_message present (HTTP error and response.failed envelope), no id in the message, an amsg_ id, a longer identifier with the same prefix; the agent_message item 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.
  • Reviewed from four angles with each finding checked by a second reader; the doc sentence was narrowed as a result. No real model calls were made.

🤖 Generated with Claude Code

…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>
@HTExplicit
HTExplicit merged commit 01a59b3 into main Oct 5, 2026
18 checks passed
@HTExplicit
HTExplicit deleted the fix/reasoning-recovery-carriers branch October 5, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant