fix(codex-responses): gracefully recover from invalid_encrypted_content (salvage #10144) (#33035)

* fix(codex-responses): gracefully recover from invalid_encrypted_content (salvage #10144)

When an OpenAI-compatible Responses API surface accepts an initial
request but later rejects the replayed `codex_reasoning_items`
encrypted blob with HTTP 400 `invalid_encrypted_content`, the
session previously got stuck retrying the same poisoned payload.

Recovery: classify the error as a dedicated FailoverReason, and on the
first hit disable encrypted reasoning replay for the rest of the
session, strip cached items from message history, and retry once.

Changes:
* error_classifier: add FailoverReason.invalid_encrypted_content
  branch in _classify_400 (before context_overflow so the messages
  that mention 'encrypted content … could not be verified' don't trip
  context heuristics), in _classify_by_error_code, and extend
  _extract_error_code to peek inside wrapped JSON in error.message and
  ignore the bare '400' as a code.
* agent_init: initialize `_codex_reasoning_replay_enabled = True` on
  every agent.
* run_agent: add AIAgent._disable_codex_reasoning_replay() helper
  that flips the flag and pops cached items.
* codex_responses_adapter: thread a `replay_encrypted_reasoning`
  kwarg through _chat_messages_to_responses_input so that when the
  flag is False we don't replay codex_reasoning_items.
* transports/codex.py: read `replay_encrypted_reasoning` from params,
  thread it into the adapter, and gate the
  `include=['reasoning.encrypted_content']` request hint on it.
* chat_completion_helpers: pass the agent's replay flag through to
  the transport.
* conversation_loop: in the retry loop, add an
  invalid_encrypted_content recovery branch that fires once per
  session, only when api_mode == codex_responses, only when replay is
  still enabled, and only when at least one assistant message in
  history actually carries cached reasoning items (otherwise the 400
  has nothing to do with our cache and the normal retry path handles
  it).

Tests:
* test_error_classifier: new wrapped-JSON _extract_error_code case;
  new TestClassifyApiError cases proving the 400 is retryable with
  no fallback, that the broad message match doesn't catch a generic
  'parsed' message, and that the error code match is
  case-insensitive.
* test_run_agent_codex_responses: end-to-end test of the recovery
  branch firing once and disabling replay, plus a sibling test that
  proves the branch does *not* fire (and the flag stays True) when
  history has no cached reasoning items.

Salvages PR #10144 onto the post-refactor module layout
(error_classifier / codex_responses_adapter / transports/codex /
conversation_loop / agent_init) since the original diff was written
against the pre-refactor monolithic run_agent.py.

* chore(release): map victorGPT in AUTHOR_MAP for #10144 salvage

---------

Co-authored-by: victorGPT <wuxuebin1993@gmail.com>
This commit is contained in:
Teknium 2026-05-26 22:01:17 -07:00 committed by GitHub
parent 9d3e9316f4
commit b6ca56f651
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 342 additions and 5 deletions

View file

@ -2041,3 +2041,107 @@ def test_preflight_codex_input_deduplicates_reasoning_ids(monkeypatch):
# IDs must be stripped — with store=False the API 404s on id lookups.
for it in reasoning_items:
assert "id" not in it
def test_run_conversation_codex_disables_reasoning_replay_after_invalid_encrypted_content(monkeypatch):
agent = _build_agent(monkeypatch)
agent.provider = "custom"
agent.base_url = "https://api.example.com/v1"
request_payloads = []
class _InvalidEncryptedContentError(Exception):
def __init__(self):
super().__init__(
"Error code: 400 - The encrypted content for item rs_001 could not be verified. "
"Reason: Encrypted content could not be decrypted or parsed."
)
self.status_code = 400
self.body = {
"error": {
"message": (
'{"error":{"message":"The encrypted content for item rs_001 could not be verified. '
'Reason: Encrypted content could not be decrypted or parsed.",'
'"type":"invalid_request_error","param":"","code":"invalid_encrypted_content"}}'
),
"type": "400",
}
}
responses = [_InvalidEncryptedContentError(), _codex_message_response("Recovered without replay.")]
def _fake_api_call(api_kwargs):
request_payloads.append(api_kwargs)
current = responses.pop(0)
if isinstance(current, Exception):
raise current
return current
monkeypatch.setattr(agent, "_interruptible_api_call", _fake_api_call)
history = [
{
"role": "assistant",
"content": "",
"finish_reason": "incomplete",
"codex_reasoning_items": [
{"type": "reasoning", "id": "rs_001", "encrypted_content": "enc_bad", "summary": []},
],
}
]
result = agent.run_conversation("continue", conversation_history=history)
assert result["completed"] is True
assert result["final_response"] == "Recovered without replay."
assert len(request_payloads) == 2
assert any(item.get("type") == "reasoning" for item in request_payloads[0]["input"])
assert not any(item.get("type") == "reasoning" for item in request_payloads[1]["input"])
assert request_payloads[0].get("include") == ["reasoning.encrypted_content"]
assert request_payloads[1].get("include") == []
assert result["messages"][0].get("codex_reasoning_items") is None
assert agent._codex_reasoning_replay_enabled is False
def test_run_conversation_codex_invalid_encrypted_content_without_replay_state_does_not_disable_replay(monkeypatch):
agent = _build_agent(monkeypatch)
agent.provider = "custom"
agent.base_url = "https://api.example.com/v1"
monkeypatch.setattr(run_agent, "jittered_backoff", lambda *args, **kwargs: 0)
request_payloads = []
class _InvalidEncryptedContentError(Exception):
def __init__(self):
super().__init__("Error code: 400 - bad request")
self.status_code = 400
self.body = {
"error": {
"code": "INVALID_ENCRYPTED_CONTENT",
"message": "Bad request",
}
}
responses = [_InvalidEncryptedContentError(), _codex_message_response("Recovered after generic retry.")]
def _fake_api_call(api_kwargs):
request_payloads.append(api_kwargs)
current = responses.pop(0)
if isinstance(current, Exception):
raise current
return current
monkeypatch.setattr(agent, "_interruptible_api_call", _fake_api_call)
result = agent.run_conversation(
"continue",
conversation_history=[{"role": "assistant", "content": "No replay state here."}],
)
assert result["completed"] is True
assert result["final_response"] == "Recovered after generic retry."
assert len(request_payloads) == 2
assert all(payload.get("include") == ["reasoning.encrypted_content"] for payload in request_payloads)
assert all(not any(item.get("type") == "reasoning" for item in payload["input"]) for payload in request_payloads)
assert agent._codex_reasoning_replay_enabled is True
assert result["messages"][0].get("codex_reasoning_items") is None