{"repo":"NousResearch/hermes-agent","public_id":"8133","display_label":"Codex responses stream event ordering and error handling","summary":"These issues and fixes address fragile stream handling in Codex/codex_responses pipelines, specifically around missing or out-of-order events like response.created, provider prelude errors, and related entitlement/rate-limit edge cases.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":26644},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-15T23:35:12+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.3933333333333333},"fix_edges":[{"issue_number":8133,"pr_number":26644,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":8133,"title":"Custom codex_responses streams fail when response.created is omitted","url":null,"state":"closed"},{"kind":"issue","number":14634,"title":"[Feature]: Hermes codex_responses stream fails when codex.rate_limits arrives before response.created","url":null,"state":"closed"},{"kind":"pr","number":8134,"title":"fix(run_agent): tolerate missing response.created on custom codex streams","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"cb38ce28cbd22278c30973eb4af5260c46543a7f","number":0},{"kind":"commit","value":"31ba2b0cb","number":0},{"kind":"pr","value":"#26644","number":26644},{"kind":"pr","value":"#8133","number":8133}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":26644,"title":"fix(xai-oauth): recover from prelude SSE errors, gate reasoning replay, surface entitlement 403s","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/codex_responses_adapter.py","agent/transports/codex.py","run_agent.py","tests/agent/transports/test_codex_transport.py","tests/run_agent/test_codex_xai_oauth_recovery.py"],"fix_hunks":[{"file":"agent/codex_responses_adapter.py","hunks":["@@ -244,8 +244,21 @@ def _normalize_responses_message_status(value: Any, *, default: str = \"completed\n     return default\n \n \n-def _chat_messages_to_responses_input(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n-    \"\"\"Convert internal chat-style messages to Responses input items.\"\"\"\n+def _chat_messages_to_responses_input(\n+    messages: List[Dict[str, Any]],\n+    *,\n+    is_xai_responses: bool = False,\n+) -> List[Dict[str, Any]]:\n+    \"\"\"Convert internal chat-style messages to Responses input items.\n+\n+    ``is_xai_responses=True`` strips ``encrypted_content`` from replayed\n+    reasoning items.  xAI's OAuth/SuperGrok ``/v1/responses`` surface\n+    rejects encrypted reasoning blobs minted by prior turns: the request\n+    streams an ``error`` SSE frame before ``response.created`` and the\n+    OpenAI SDK collapses it into a generic stream-ordering error.  Native\n+    Codex (chatgpt.com backend-api) DOES accept replayed encrypted_content\n+    — keep the default off.\n+    \"\"\"\n     items: List[Dict[str, Any]] = []\n     seen_item_ids: set = set()\n ","@@ -271,9 +284,17 @@ def _chat_messages_to_responses_input(messages: List[Dict[str, Any]]) -> List[Di\n             if role == \"assistant\":\n                 # Replay encrypted reasoning items from previous turns\n                 # so the API can maintain coherent reasoning chains.\n+                #\n+                # xAI OAuth (SuperGrok/Premium) rejects replayed\n+                # ``encrypted_content`` reasoning items minted by prior\n+                # turns — see _chat_messages_to_responses_input docstring.\n+                # When ``is_xai_responses`` is set we drop the replay\n+                # entirely; Grok still reasons on each turn server-side,\n+                # we just don't try to thread the prior turn's encrypted\n+                # blob back in.\n                 codex_reasoning = msg.get(\"codex_reasoning_items\")\n                 has_codex_reasoning = False\n-                if isinstance(codex_reasoning, list):\n+                if isinstance(codex_reasoning, list) and not is_xai_responses:\n                     for ri in codex_reasoning:\n                         if isinstance(ri, dict) and ri.get(\"encrypted_content\"):\n                             item_id = ri.get(\"id\")"]},{"file":"agent/transports/codex.py","hunks":["@@ -24,7 +24,10 @@ def api_mode(self) -> str:\n     def convert_messages(self, messages: List[Dict[str, Any]], **kwargs) -> Any:\n         \"\"\"Convert OpenAI chat messages to Responses API input items.\"\"\"\n         from agent.codex_responses_adapter import _chat_messages_to_responses_input\n-        return _chat_messages_to_responses_input(messages)\n+        return _chat_messages_to_responses_input(\n+            messages,\n+            is_xai_responses=bool(kwargs.get(\"is_xai_responses\")),\n+        )\n \n     def convert_tools(self, tools: List[Dict[str, Any]]) -> Any:\n         \"\"\"Convert OpenAI tool schemas to Responses API function definitions.\"\"\"","@@ -93,7 +96,10 @@ def build_kwargs(\n         kwargs = {\n             \"model\": model,\n             \"instructions\": instructions,\n-            \"input\": _chat_messages_to_responses_input(payload_messages),\n+            \"input\": _chat_messages_to_responses_input(\n+                payload_messages,\n+                is_xai_responses=is_xai_responses,\n+            ),\n             \"tools\": response_tools,\n             \"store\": False,\n         }","@@ -110,7 +116,14 @@ def build_kwargs(\n         if reasoning_enabled and is_xai_responses:\n             from agent.model_metadata import grok_supports_reasoning_effort\n \n-            kwargs[\"include\"] = [\"reasoning.encrypted_content\"]\n+            # NOTE: Hermes does NOT ask xAI to return ``reasoning.encrypted_content``\n+            # any more.  xAI's OAuth/SuperGrok ``/v1/responses`` surface rejects\n+            # replayed encrypted reasoning items on turn 2+ — see\n+            # _chat_messages_to_responses_input docstring.  Requesting the field\n+            # back would just have us cache something we then must strip.  Grok\n+            # still reasons natively each turn; coherence across turns rides on\n+            # the visible message text alone.\n+            kwargs[\"include\"] = []\n             # xAI rejects `reasoning.effort` on grok-4 / grok-4-fast / grok-3\n             # / grok-code-fast / grok-4.20-0309-* with HTTP 400 even though\n             # those models reason natively. Only send the effort dial when"]},{"file":"run_agent.py","hunks":["@@ -4966,6 +4966,45 @@ def _save_trajectory(self, messages: List[Dict[str, Any]], user_query: str, comp\n         trajectory = self._convert_to_trajectory_format(messages, user_query, completed)\n         _save_trajectory_to_file(trajectory, self.model, completed)\n \n+    @staticmethod\n+    def _decorate_xai_entitlement_error(detail: str) -> str:\n+        \"\"\"Append a friendly hint when xAI's OAuth surface returns an\n+        entitlement-shaped error.\n+\n+        xAI's ``/v1/responses`` endpoint replies to OAuth tokens that lack a\n+        SuperGrok / X Premium subscription with HTTP 403 carrying a body like::\n+\n+            {\"code\": \"The caller does not have permission to execute the\n+             specified operation\", \"error\": \"You have either run out of\n+             available resources or do not have an active Grok subscription.\n+             Manage subscriptions at https://grok.com/...\"}\n+\n+        The raw text is useful but the action the user needs to take (subscribe\n+        on grok.com, or switch providers with ``/model``) isn't obvious from\n+        the wire format.  Detect the entitlement shape and append a hint.\n+\n+        Matched once per detail string — won't double-decorate if the upstream\n+        already concatenated the same text.\n+        \"\"\"\n+        if not detail:\n+            return detail\n+        lower = detail.lower()\n+        is_entitlement = (\n+            \"do not have an active grok subscription\" in lower\n+            or (\"out of available resources\" in lower and \"grok\" in lower)\n+            or (\"does not have permission\" in lower and \"grok\" in lower)\n+        )\n+        if not is_entitlement:\n+            return detail\n+        hint = (\n+            \" — xAI OAuth account lacks SuperGrok / X Premium entitlement for \"\n+            \"this model. Subscribe at https://grok.com or run `/model` to \"\n+            \"switch providers.\"\n+        )\n+        if hint.strip() in detail:\n+            return detail\n+        return f\"{detail}{hint}\"\n+\n     @staticmethod\n     def _summarize_api_error(error: Exception) -> str:\n         \"\"\"Extract a human-readable one-liner from an API error.","@@ -4999,12 +5038,12 @@ def _summarize_api_error(error: Exception) -> str:\n             if msg:\n                 status_code = getattr(error, \"status_code\", None)\n                 prefix = f\"HTTP {status_code}: \" if status_code else \"\"\n-                return f\"{prefix}{msg[:300]}\"\n+                return AIAgent._decorate_xai_entitlement_error(f\"{prefix}{msg[:300]}\")\n \n         # Fallback: truncate the raw string but give more room than 200 chars\n         status_code = getattr(error, \"status_code\", None)\n         prefix = f\"HTTP {status_code}: \" if status_code else \"\"\n-        return f\"{prefix}{raw[:500]}\"\n+        return AIAgent._decorate_xai_entitlement_error(f\"{prefix}{raw[:500]}\")\n \n     def _mask_api_key_for_logs(self, key: Optional[str]) -> Optional[str]:\n         if not key:","@@ -7056,18 +7095,48 @@ def _run_codex_stream(self, api_kwargs: dict, client: Any = None, on_first_delta\n             except RuntimeError as exc:\n                 err_text = str(exc)\n                 missing_completed = \"response.completed\" in err_text\n-                if missing_completed and attempt < max_stream_retries:\n+                # The OpenAI SDK's Responses streaming state machine raises\n+                # ``RuntimeError(\"Expected to have received `response.created`\n+                # before `<event-type>`\")`` when the first SSE event from the\n+                # server is anything other than ``response.created`` — and it\n+                # discards the event's payload before we can read it.  Three\n+                # real-world backends emit a different first frame:\n+                #\n+                #   * xAI on grok-4.x OAuth — sends ``error`` (issues\n+                #     reported around the May 2026 SuperGrok rollout when\n+                #     multi-turn conversations replay encrypted reasoning\n+                #     content the OAuth tier rejects)\n+                #   * codex-lb relays — send ``codex.rate_limits`` (#14634)\n+                #   * custom Responses relays — send ``response.in_progress``\n+                #     (#8133)\n+                #\n+                # In all three cases the underlying byte stream is still\n+                # readable: a non-stream ``responses.create(stream=True)``\n+                # fallback succeeds and surfaces the real provider error as\n+                # a normal exception with body+status_code attached, which\n+                # ``_summarize_api_error`` can then translate into a useful\n+                # user-facing line.  Treat ``response.created`` prelude\n+                # errors the same way we already treat ``response.completed``\n+                # postlude errors.\n+                prelude_error = (\n+                    \"Expected to have received `response.created`\" in err_text\n+                    or \"Expected to have received \\\"response.created\\\"\" in err_text\n+                )\n+                if (missing_completed or prelude_error) and attempt < max_stream_retries:\n                     logger.debug(\n-                        \"Responses stream closed before completion (attempt %s/%s); retrying. %s\",\n+                        \"Responses stream %s (attempt %s/%s); retrying. %s\",\n+                        \"prelude rejected\" if prelude_error else \"closed before completion\",\n                         attempt + 1,\n                         max_stream_retries + 1,\n                         self._client_log_context(),\n                     )\n                     continue\n-                if missing_completed:\n+                if missing_completed or prelude_error:\n                     logger.debug(\n-                        \"Responses stream did not emit response.completed; falling back to create(stream=True). %s\",\n+                        \"Responses stream %s; falling back to create(stream=True). %s err=%s\",\n+                        \"rejected before response.created\" if prelude_error else \"did not emit response.completed\",\n                         self._client_log_context(),\n+                        err_text,\n                     )\n                     return self._run_codex_create_stream_fallback(api_kwargs, client=active_client)\n                 raise"]},{"file":"tests/agent/transports/test_codex_transport.py","hunks":["@@ -194,9 +194,16 @@ def test_xai_reasoning_effort_passed(self, transport):\n             is_xai_responses=True,\n             reasoning_config={\"effort\": \"high\"},\n         )\n-        # xAI Responses must receive both encrypted reasoning content and the effort\n+        # xAI Responses receives reasoning.effort on the allowlisted models.\n         assert kw.get(\"reasoning\") == {\"effort\": \"high\"}\n-        assert \"reasoning.encrypted_content\" in kw.get(\"include\", [])\n+        # As of May 2026 we deliberately do NOT request\n+        # reasoning.encrypted_content back from xAI — the OAuth/SuperGrok\n+        # surface rejects replayed encrypted reasoning items on turn 2+\n+        # (the multi-turn \"Expected to have received response.created\n+        # before error\" failure).  Grok still reasons natively each turn;\n+        # we just don't try to thread the prior turn's encrypted blob back\n+        # in.  See tests/run_agent/test_codex_xai_oauth_recovery.py.\n+        assert \"reasoning.encrypted_content\" not in kw.get(\"include\", [])\n \n     def test_xai_reasoning_disabled_no_reasoning_key(self, transport):\n         messages = [{\"role\": \"user\", \"content\": \"Hi\"}]","@@ -222,8 +229,9 @@ def test_xai_minimal_effort_clamped(self, transport):\n     # api.x.ai 400s with \"Model X does not support parameter reasoningEffort\"\n     # on grok-4 / grok-4-fast / grok-3 / grok-code-fast / grok-4.20-0309-*.\n     # Those models reason natively but don't expose the dial. The transport\n-    # must omit the `reasoning` key for them while keeping the encrypted\n-    # reasoning content include so we can capture native reasoning tokens.\n+    # must omit the `reasoning` key for them.  As of May 2026 we also no\n+    # longer request ``reasoning.encrypted_content`` back from xAI on ANY\n+    # model — see test_xai_reasoning_effort_passed for the rationale.\n \n     def test_xai_grok_4_omits_reasoning_effort(self, transport):\n         \"\"\"grok-4 / grok-4-0709 reject reasoning.effort with HTTP 400.\"\"\"","@@ -237,8 +245,9 @@ def test_xai_grok_4_omits_reasoning_effort(self, transport):\n             assert \"reasoning\" not in kw, (\n                 f\"{model} must not receive a reasoning key (xAI rejects it)\"\n             )\n-            # Still capture native reasoning tokens\n-            assert \"reasoning.encrypted_content\" in kw.get(\"include\", [])\n+            # We no longer ask xAI for encrypted_content back (see comment\n+            # above) — verify the include list is empty.\n+            assert \"reasoning.encrypted_content\" not in kw.get(\"include\", [])\n \n     def test_xai_grok_4_fast_omits_reasoning_effort(self, transport):\n         \"\"\"grok-4-fast and grok-4-1-fast variants reject reasoning.effort.\"\"\""]},{"file":"tests/run_agent/test_codex_xai_oauth_recovery.py","hunks":["@@ -0,0 +1,351 @@\n+\"\"\"Regression tests for the May 2026 xAI OAuth (SuperGrok / X Premium) bugs.\n+\n+Three distinct failure modes the user community hit during rollout:\n+\n+1. ``RuntimeError(\"Expected to have received `response.created` before\n+   `error`\")`` on multi-turn xAI OAuth conversations.  The OpenAI SDK's\n+   Responses streaming state machine collapses an upstream ``error`` SSE\n+   frame into a generic stream-ordering error.  ``_run_codex_stream``\n+   now treats this the same way it already treats the missing\n+   ``response.completed`` postlude — fall back to a non-stream\n+   ``responses.create(stream=True)`` which surfaces the real provider\n+   error.  Also closes #8133 (``response.in_progress`` prelude on custom\n+   relays) and #14634 (``codex.rate_limits`` prelude on codex-lb).\n+\n+2. The HTTP 403 entitlement error xAI returns when an OAuth token lacks\n+   SuperGrok / X Premium (\"You have either run out of available\n+   resources or do not have an active Grok subscription\") used to read\n+   as a confusing wall of JSON.  ``_summarize_api_error`` now appends a\n+   one-line hint pointing the user at https://grok.com and ``/model``.\n+\n+3. Multi-turn replay of ``codex_reasoning_items`` (with\n+   ``encrypted_content``) is now suppressed for ``is_xai_responses=True``\n+   in ``_chat_messages_to_responses_input``.  xAI's OAuth/SuperGrok\n+   surface rejects replayed encrypted reasoning items; Grok still\n+   reasons natively each turn, so coherence rides on visible message\n+   text.\n+\"\"\"\n+\n+from types import SimpleNamespace\n+from unittest.mock import MagicMock, patch\n+\n+import pytest\n+\n+\n+# ---------------------------------------------------------------------------\n+# Fix A: prelude error fallback\n+# ---------------------------------------------------------------------------\n+\n+\n+def _make_codex_agent():\n+    \"\"\"Build a minimal AIAgent wired for codex_responses streaming tests.\"\"\"\n+    from run_agent import AIAgent\n+\n+    agent = AIAgent(\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://api.x.ai/v1\",\n+        model=\"grok-4.3\",\n+        quiet_mode=True,\n+        skip_context_files=True,\n+        skip_memory=True,\n+    )\n+    agent.api_mode = \"codex_responses\"\n+    agent.provider = \"xai-oauth\"\n+    agent._interrupt_requested = False\n+    return agent\n+\n+\n[redacted:email](\n+    \"prelude_event_type\",\n+    [\n+        \"error\",                  # xAI OAuth multi-turn\n+        \"codex.rate_limits\",      # codex-lb relays (#14634)\n+        \"response.in_progress\",   # custom Responses relays (#8133)\n+    ],\n+)\n+def test_codex_stream_prelude_error_falls_back_to_create_stream(prelude_event_type):\n+    \"\"\"The SDK's prelude RuntimeError must trigger the non-stream fallback.\n+\n+    When the first SSE event isn't ``response.created``, openai-python\n+    raises RuntimeError before our event loop sees anything.  We must\n+    detect that, retry once, then fall back to ``create(stream=True)``\n+    which surfaces the real provider error or a real response.\n+    \"\"\"\n+    agent = _make_codex_agent()\n+\n+    prelude_error = RuntimeError(\n+        f\"Expected to have received `response.created` before `{prelude_event_type}`\"\n+    )\n+\n+    mock_client = MagicMock()\n+    mock_client.responses.stream.side_effect = prelude_error\n+\n+    fallback_response = SimpleNamespace(\n+        output=[SimpleNamespace(\n+            type=\"message\",\n+            content=[SimpleNamespace(type=\"output_text\", text=\"fallback ok\")],\n+        )],\n+        status=\"completed\",\n+    )\n+\n+    with patch.object(\n+        agent, \"_run_codex_create_stream_fallback\", return_value=fallback_response\n+    ) as mock_fallback:\n+        result = agent._run_codex_stream({}, client=mock_client)\n+\n+    assert result is fallback_response\n+    mock_fallback.assert_called_once_with({}, client=mock_client)\n+\n+\n+def test_codex_stream_prelude_error_retries_once_before_fallback():\n+    \"\"\"The retry path must fire one extra stream attempt before falling back.\"\"\"\n+    agent = _make_codex_agent()\n+\n+    call_count = {\"n\": 0}\n+\n+    def stream_side_effect(**kwargs):\n+        call_count[\"n\"] += 1\n+        raise RuntimeError(\n+            \"Expected to have received `response.created` before `error`\"\n+        )\n+\n+    mock_client = MagicMock()\n+    mock_client.responses.stream.side_effect = stream_side_effect\n+\n+    fallback_response = SimpleNamespace(output=[], status=\"completed\")\n+    with patch.object(\n+        agent, \"_run_codex_create_stream_fallback\", return_value=fallback_response\n+    ) as mock_fallback:\n+        agent._run_codex_stream({}, client=mock_client)\n+\n+    # max_stream_retries=1 → one retry + final attempt → 2 stream calls,\n+    # THEN the fallback path runs.\n+    assert call_count[\"n\"] == 2\n+    mock_fallback.assert_called_once()\n+\n+\n+def test_codex_stream_unrelated_runtimeerror_still_raises():\n+    \"\"\"RuntimeErrors that aren't prelude/postlude shape must propagate.\"\"\"\n+    agent = _make_codex_agent()\n+\n+    mock_client = MagicMock()\n+    mock_client.responses.stream.side_effect = RuntimeError(\"something else broke\")\n+\n+    with patch.object(agent, \"_run_codex_create_stream_fallback\") as mock_fallback:\n+        with pytest.raises(RuntimeError, match=\"something else broke\"):\n+            agent._run_codex_stream({}, client=mock_client)\n+\n+    mock_fallback.assert_not_called()\n+\n+\n+def test_codex_stream_postlude_error_still_falls_back():\n+    \"\"\"Existing ``response.completed`` fallback must not regress.\"\"\"\n+    agent = _make_codex_agent()\n+\n+    mock_client = MagicMock()\n+    mock_client.responses.stream.side_effect = RuntimeError(\n+        \"Didn't receive a `response.completed` event.\"\n+    )\n+\n+    fallback_response = SimpleNamespace(output=[], status=\"completed\")\n+    with patch.object(\n+        agent, \"_run_codex_create_stream_fallback\", return_value=fallback_response\n+    ) as mock_fallback:\n+        result = agent._run_codex_stream({}, client=mock_client)\n+\n+    assert result is fallback_response\n+    mock_fallback.assert_called_once()\n+\n+\n+# ---------------------------------------------------------------------------\n+# Fix B: friendly entitlement message\n+# ---------------------------------------------------------------------------\n+\n+\n+def test_summarize_api_error_decorates_xai_entitlement_403():\n+    \"\"\"xAI's OAuth 403 must end with the subscribe-or-switch hint.\"\"\"\n+    from run_agent import AIAgent\n+\n+    error = RuntimeError(\n+        \"HTTP 403: Error code: 403 - {'code': 'The caller does not have permission \"\n+        \"to execute the specified operation', 'error': 'You have either run out of \"\n+        \"available resources or do not have an active Grok subscription. Manage \"\n+        \"subscriptions at https://grok.com'}\"\n+    )\n+    summary = AIAgent._summarize_api_error(error)\n+    assert \"do not have an active Grok subscription\" in summary\n+    assert \"SuperGrok\" in summary\n+    assert \"/model\" in summary\n+    assert \"https://grok.com\" in summary\n+\n+\n+def test_summarize_api_error_decorates_xai_body_message():\n+    \"\"\"SDK-style error with structured body must also get the hint.\"\"\"\n+    from run_agent import AIAgent\n+\n+    class _XaiErr(Exception):\n+        status_code = 403\n+        body = {\n+            \"error\": {\n+                \"message\": (\n+                    \"You have either run out of available resources or do \"\n+                    \"not have an active Grok subscription. Manage at \"\n+                    \"https://grok.com\"\n+                )\n+            }\n+        }\n+\n+    summary = AIAgent._summarize_api_error(_XaiErr(\"403\"))\n+    assert \"HTTP 403\" in summary\n+    assert \"SuperGrok / X Premium\" in summary\n+\n+\n+def test_summarize_api_error_idempotent_for_entitlement_hint():\n+    \"\"\"Decorating twice must not double up the hint.\"\"\"\n+    from run_agent import AIAgent\n+\n+    raw = \"HTTP 403: do not have an active Grok subscription\"\n+    once = AIAgent._decorate_xai_entitlement_error(raw)\n+    twice = AIAgent._decorate_xai_entitlement_error(once)\n+    assert once == twice\n+\n+\n+def test_summarize_api_error_passes_through_unrelated_errors():\n+    \"\"\"Non-xAI / non-entitlement errors must not be touched.\"\"\"\n+    from run_agent import AIAgent\n+\n+    error = RuntimeError(\"HTTP 500: upstream is sad\")\n+    summary = AIAgent._summarize_api_error(error)\n+    assert \"SuperGrok\" not in summary\n+    assert \"grok.com\" not in summary\n+    assert \"upstream is sad\" in summary\n+\n+\n+# ---------------------------------------------------------------------------\n+# Fix C: reasoning replay gating for xai-oauth\n+# ---------------------------------------------------------------------------\n+\n+\n+def _assistant_msg_with_encrypted_reasoning(text=\"hi from grok\", encrypted=\"enc_blob\"):\n+    return {\n+        \"role\": \"assistant\",\n+        \"content\": text,\n+        \"codex_reasoning_items\": [\n+            {\n+                \"type\": \"reasoning\",\n+                \"id\": \"rs_xai_001\",\n+                \"encrypted_content\": encrypted,\n+                \"summary\": [],\n+            }\n+        ],\n+    }\n+\n+\n+def test_codex_reasoning_replay_default_includes_encrypted_content():\n+    \"\"\"Native Codex backend (default) must still replay encrypted reasoning.\"\"\"\n+    from agent.codex_responses_adapter import _chat_messages_to_responses_input\n+\n+    msgs = [\n+        {\"role\": \"user\", \"content\": \"hi\"},\n+        _assistant_msg_with_encrypted_reasoning(),\n+        {\"role\": \"user\", \"content\": \"what's your name?\"},\n+    ]\n+\n+    items = _chat_messages_to_responses_input(msgs)\n+    reasoning = [it for it in items if it.get(\"type\") == \"reasoning\"]\n+    assert len(reasoning) == 1\n+    assert reasoning[0][\"encrypted_content\"] == \"enc_blob\"\n+\n+\n+def test_codex_reasoning_replay_stripped_for_xai_oauth():\n+    \"\"\"xAI OAuth surface must NOT receive replayed encrypted reasoning.\"\"\"\n+    from agent.codex_responses_adapter import _chat_messages_to_responses_input\n+\n+    msgs = [\n+        {\"role\": \"user\", \"content\": \"hi\"},\n+        _assistant_msg_with_encrypted_reasoning(),\n+        {\"role\": \"user\", \"content\": \"what's your name?\"},\n+    ]\n+\n+    items = _chat_messages_to_responses_input(msgs, is_xai_responses=True)\n+    reasoning = [it for it in items if it.get(\"type\") == \"reasoning\"]\n+    assert reasoning == []\n+\n+    # The assistant's visible text must still survive — coherence across\n+    # turns rides on the message text alone.\n+    assistant_items = [\n+        it for it in items\n+        if it.get(\"role\") == \"assistant\" or it.get(\"type\") == \"message\"\n+    ]\n+    assert assistant_items, \"assistant message must still be present\"\n+\n+\n+def test_codex_transport_xai_request_omits_encrypted_content_include():\n+    \"\"\"Verify the xAI ``include`` array no longer requests encrypted reasoning.\"\"\"\n+    from agent.transports.codex import ResponsesApiTransport\n+\n+    transport = ResponsesApiTransport()\n+    kwargs = transport.build_kwargs(\n+        model=\"grok-4.3\",\n+        messages=[\n+            {\"role\": \"system\", \"content\": \"you are a helpful assistant\"},\n+            {\"role\": \"user\", \"content\": \"hi\"},\n+        ],\n+        tools=None,\n+        instructions=\"you are a helpful assistant\",\n+        reasoning_config={\"enabled\": True, \"effort\": \"medium\"},\n+        is_xai_responses=True,\n+    )\n+    # Without this gate, xAI would echo back encrypted_content blobs we'd\n+    # then store in codex_reasoning_items and replay next turn — which is\n+    # exactly the multi-turn failure mode we're closing.\n+    assert kwargs[\"include\"] == []\n+\n+\n+def test_codex_transport_xai_strips_replayed_reasoning_in_input():\n+    \"\"\"End-to-end: build_kwargs on xai-oauth must strip prior reasoning.\"\"\"\n+    from agent.transports.codex import ResponsesApiTransport\n+\n+    transport = ResponsesApiTransport()\n+    kwargs = transport.build_kwargs(\n+        model=\"grok-4.3\",\n+        messages=[\n+            {\"role\": \"system\", \"content\": \"sys\"},\n+            {\"role\": \"user\", \"content\": \"hi\"},\n+            _assistant_msg_with_encrypted_reasoning(text=\"hi from grok\"),\n+            {\"role\": \"user\", \"content\": \"what's your name?\"},\n+        ],\n+        tools=None,\n+        instructions=\"sys\",\n+        reasoning_config={\"enabled\": True, \"effort\": \"medium\"},\n+        is_xai_responses=True,\n+    )\n+    input_items = kwargs[\"input\"]\n+    reasoning_items = [it for it in input_items if it.get(\"type\") == \"reasoning\"]\n+    assert reasoning_items == []\n+\n+\n+def test_codex_transport_native_codex_still_replays_reasoning_in_input():\n+    \"\"\"Regression guard: openai-codex must keep the existing replay path.\"\"\"\n+    from agent.transports.codex import ResponsesApiTransport\n+\n+    transport = ResponsesApiTransport()\n+    kwargs = transport.build_kwargs(\n+        model=\"gpt-5-codex\",\n+        messages=[\n+            {\"role\": \"system\", \"content\": \"sys\"},\n+            {\"role\": \"user\", \"content\": \"hi\"},\n+            _assistant_msg_with_encrypted_reasoning(text=\"hi from codex\"),\n+            {\"role\": \"user\", \"content\": \"next\"},\n+        ],\n+        tools=None,\n+        instructions=\"sys\",\n+        reasoning_config={\"enabled\": True, \"effort\": \"medium\"},\n+        is_xai_responses=False,\n+    )\n+    input_items = kwargs[\"input\"]\n+    reasoning_items = [it for it in input_items if it.get(\"type\") == \"reasoning\"]\n+    assert len(reasoning_items) == 1\n+    assert reasoning_items[0][\"encrypted_content\"] == \"enc_blob\"\n+    # Native Codex still asks for encrypted_content back.\n+    assert \"reasoning.encrypted_content\" in kwargs.get(\"include\", [])\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}