# NousResearch/hermes-agent · Codex Responses null output field causing TypeError

## Open now

- Nothing open on GitHub.

The three members appear to concern the same problem: Codex Responses streaming with a null \`output\` field, reported as a \`TypeError: 'NoneType' object is not iterable\`. Two members are titled as fixes (one in OpenAI SDK \`parse_response\` handling, one in agent stream recovery), and one is a bug report; their titles alone do not show whether the fixes work.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #32963. Verify it addresses your case.
- **Recommended:** read PR #32963 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #32956: \[Bug\]: Codex Responses streaming crashes with TypeError: 'NoneType' object is not iterable (openai-codex / chatgpt.com backend)

## Prior & competing PRs

- **#32963: fix(agent): recover Codex Responses streams with null output**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/auxiliary_client.py`, `agent/codex_runtime.py`, `scripts/release.py`, `tests/agent/test_auxiliary_client.py`, `tests/run_agent/test_run_agent_codex_responses.py`
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -107,6 +107,32 @@ def __repr__(self):
     logger = logging.getLogger(__name__)
     
     
    +def _responses_null_output_iterable_error(exc: BaseException) -> bool:
    +    """True when the OpenAI SDK trips over terminal response.output=None."""
    +    text = str(exc)
    +    return isinstance(exc, TypeError) and "NoneType" in text and "not iterable" in text
    +
    +
    +def _responses_backfilled_response(output_items: List[Any], text_parts: List[str], *, has_function_calls: bool, model: str = None) -> Optional[Any]:
    +    """Build a minimal Responses-like object from already streamed events."""
    +    if output_items:
    +        return SimpleNamespace(output=list(output_items), usage=None, status="completed", model=model)
    +    if text_parts and not has_function_calls:
    +        assembled = "".join(text_parts)
    +        return SimpleNamespace(
    +            output=[SimpleNamespace(
    +                type="message",
    +                role="assistant",
    +                status="completed",
    +                content=[SimpleNamespace(type="output_text", text=assembled)],
    +            )],
    +            usage=None,
    +            status="completed",
    +            model=model,
    +        )
    +    return None
    +
    +
     def _safe_isinstance(obj: Any, maybe_type: Any) -> bool:
         """Return False instead of raising when a patched symbol is not a type."""
         try:
    ```

    ```diff
    @@ -796,44 +822,61 @@ def _check_cancelled() -> None:
                     timeout_timer.daemon = True
                     timeout_timer.start()
                 _check_cancelled()
    +            final = None
                 with self._client.responses.stream(**resp_kwargs) as stream:
    -                for _event in stream:
    +                try:
    +                    for _event in stream:
    +                        _check_cancelled()
    +                        _etype = getattr(_event, "type", "")
    +                        if _etype == "response.output_item.done":
    +                            _done = getattr(_event, "item", None)
    +                            if _done is not None:
    +                                collected_output_items.append(_done)
    +                        elif "output_text.delta" in _etype:
    +                            _delta = getattr(_event, "delta", "")
    +                            if _delta:
    +                                collected_text_deltas.append(_delta)
    +                        elif "function_call" in _etype:
    +                            has_function_calls = True
                         _check_cancelled()
    -                    _etype = getattr(_event, "type", "")
    -                    if _etype == "response.output_item.done":
    -                        _done = getattr(_event, "item", None)
    -                        if _done is not None:
    -                            collected_output_items.append(_done)
    -                    elif "output_text.delta" in _etype:
    -                        _delta = getattr(_event, "delta", "")
    -                        if _delta:
    -                            collected_text_deltas.append(_delta)
    -                    elif "function_call" in _etype:
    -                        has_function_calls = True
    -                _check_cancelled()
    -                final = stream.get_final_response()
    +                    final = stream.get_final_response()
    +                except TypeError as exc:
    +                    if not _responses_null_output_iterable_error(exc):
    +                        raise
    +                    final = _responses_backfilled_response(
    +                        collected_output_items,
    +                        collected_text_deltas,
    +                        has_function_calls=has_function_calls,
    +                        model=resp_kwargs.get("model"),
    +                    )
    +                    if final is None:
    +                        raise
    +                    logger.debug(
    +                        "Codex auxiliary Responses stream parser hit response.output=None; "
    +                        "recovered from streamed events (items=%d, text_parts=%d)",
    +                        len(collected_output_items),
    +                        len(collected_text_deltas),
    +                    )
    +
    +            if final is None:
    +                raise RuntimeError("Codex auxiliary Responses stream did not return a final response")
     
                 # Backfill empty output from collected stream events
                 _output = getattr(final, "output", None)
    -            if isinstance(_output, list) and not _output:
    -                if collected_output_items:
    -                    final.output = list(collected_output_items)
    +            if _output is None or (isinstance(_output, list) and not _output):
    +                recovered = _responses_backfilled_response(
    +                    collected_output_items,
    +                    collected_text_deltas,
    +                    has_function_calls=has_function_calls,
    +                    model=resp_kwargs.get("model"),
    +                )
    +                if recovered is not None:
    +                    final.output = recovered.output
                         logger.debug(
    -                        "Codex auxiliary: backfilled %d output items from stream events",
    +                        "Codex auxiliary: backfilled missing output from stream events "
    +                        "(items=%d, text_parts=%d)",
                             len(collected_output_items),
    -                    )
    -                elif collected_text_deltas and not has_function_calls:
    -                    # Only synthesize text when no tool calls were streamed —
    -                    # a function_call response with incidental text should not
    -                    # be collapsed into a plain-text message.
    -                    assembled = "".join(collected_text_deltas)
    -                    final.output = [SimpleNamespace(
    -                        type="message", role="assistant", status="completed",
    -                        content=[SimpleNamespace(type="output_text", text=assembled)],
    -                    )]
    -                    logger.debug(
    -                        "Codex auxiliary: synthesized from %d deltas (%d chars)",
    -                        len(collected_text_deltas), len(assembled),
    +                        len(collected_text_deltas),
                         )
     
                 # Extract text and tool calls from the Responses output.
    ```
  - `agent/codex_runtime.py`

    ```diff
    @@ -176,6 +176,37 @@ def run_codex_app_server_turn(
     
     
     
    +def _responses_null_output_iterable_error(exc: BaseException) -> bool:
    +    """True when the OpenAI SDK trips over terminal response.output=None."""
    +    text = str(exc)
    +    return isinstance(exc, TypeError) and "NoneType" in text and "not iterable" in text
    +
    +
    +def _codex_backfilled_response(output_items: list, text_parts: list, *, has_tool_calls: bool, model: str = None):
    +    """Build a minimal Responses-like object from events already streamed."""
    +    if output_items:
    +        return SimpleNamespace(
    +            output=list(output_items),
    +            usage=None,
    +            status="completed",
    +            model=model,
    +        )
    +    if text_parts and not has_tool_calls:
    +        assembled = "".join(text_parts)
    +        return SimpleNamespace(
    +            output=[SimpleNamespace(
    +                type="message",
    +                role="assistant",
    +                status="completed",
    +                content=[SimpleNamespace(type="output_text", text=assembled)],
    +            )],
    +            usage=None,
    +            status="completed",
    +            model=model,
    +        )
    +    return None
    +
    +
     def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta: callable = None):
         """Execute one streaming Responses API request and return the final response."""
         import httpx as _httpx
    ```

    ```diff
    @@ -251,24 +282,20 @@ def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta
                     # but get_final_response() can return an empty output list.
                     # Backfill from collected items or synthesize from deltas.
                     _out = getattr(final_response, "output", None)
    -                if isinstance(_out, list) and not _out:
    -                    if collected_output_items:
    -                        final_response.output = list(collected_output_items)
    +                if _out is None or (isinstance(_out, list) and not _out):
    +                    recovered = _codex_backfilled_response(
    +                        collected_output_items,
    +                        agent._codex_streamed_text_parts,
    +                        has_tool_calls=has_tool_calls,
    +                        model=api_kwargs.get("model"),
    +                    )
    +                    if recovered is not None:
    +                        final_response.output = recovered.output
                             logger.debug(
    -                            "Codex stream: backfilled %d output items from stream events",
    +                            "Codex stream: backfilled missing output from stream events "
    +                            "(items=%d, text_parts=%d)",
                                 len(collected_output_items),
    -                        )
    -                    elif agent._codex_streamed_text_parts and not has_tool_calls:
    -                        assembled = "".join(agent._codex_streamed_text_parts)
    -                        final_response.output = [SimpleNamespace(
    -                            type="message",
    -                            role="assistant",
    -                            status="completed",
    -                            content=[SimpleNamespace(type="output_text", text=assembled)],
    -                        )]
    -                        logger.debug(
    -                            "Codex stream: synthesized output from %d text deltas (%d chars)",
    -                            len(agent._codex_streamed_text_parts), len(assembled),
    +                            len(agent._codex_streamed_text_parts),
                             )
                     return final_response
             except (_httpx.RemoteProtocolError, _httpx.ReadTimeout, _httpx.ConnectError, ConnectionError) as exc:
    ```

    ```diff
    @@ -287,6 +314,30 @@ def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta
                     exc,
                 )
                 return agent._run_codex_create_stream_fallback(api_kwargs, client=active_client)
    +        except TypeError as exc:
    +            if _responses_null_output_iterable_error(exc):
    +                recovered = _codex_backfilled_response(
    +                    collected_output_items,
    +                    agent._codex_streamed_text_parts,
    +                    has_tool_calls=has_tool_calls,
    +                    model=api_kwargs.get("model"),
    +                )
    +                if recovered is not None:
    +                    logger.debug(
    +                        "Codex Responses stream parser hit response.output=None; "
    +                        "recovered from streamed events (items=%d, text_parts=%d). %s",
    +                        len(collected_output_items),
    +                        len(agent._codex_streamed_text_parts),
    +                        agent._client_log_context(),
    +                    )
    +                    return recovered
    +                logger.debug(
    +                    "Codex Responses stream parser hit response.output=None without "
    +                    "recoverable events; falling back to create(stream=True). %s",
    +                    agent._client_log_context(),
    +                )
    +                return agent._run_codex_create_stream_fallback(api_kwargs, client=active_client)
    +            raise
             except RuntimeError as exc:
                 err_text = str(exc)
                 missing_completed = "response.completed" in err_text
    ```

    ```diff
    @@ -355,6 +406,7 @@ def run_codex_create_stream_fallback(agent, api_kwargs: dict, client: Any = None
         terminal_response = None
         collected_output_items: list = []
         collected_text_deltas: list = []
    +    has_tool_calls = False
         try:
             for event in stream_or_response:
                 agent._touch_activity("receiving stream response")
    ```

    ```diff
    @@ -404,6 +456,8 @@ def run_codex_create_stream_fallback(agent, api_kwargs: dict, client: Any = None
                         delta = event.get("delta", "")
                     if delta:
                         collected_text_deltas.append(delta)
    +            elif event_type and "function_call" in event_type:
    +                has_tool_calls = True
     
                 if event_type not in {"response.completed", "response.incomplete", "response.failed"}:
                     continue
    ```

    ```diff
    @@ -414,23 +468,20 @@ def run_codex_create_stream_fallback(agent, api_kwargs: dict, client: Any = None
                 if terminal_response is not None:
                     # Backfill empty output from collected stream events
                     _out = getattr(terminal_response, "output", None)
    -                if isinstance(_out, list) and not _out:
    -                    if collected_output_items:
    -                        terminal_response.output = list(collected_output_items)
    +                if _out is None or (isinstance(_out, list) and not _out):
    +                    recovered = _codex_backfilled_response(
    +                        collected_output_items,
    +                        collected_text_deltas,
    +                        has_tool_calls=has_tool_calls,
    +                        model=fallback_kwargs.get("model"),
    +                    )
    +                    if recovered is not None:
    +                        terminal_response.output = recovered.output
                             logger.debug(
    -                            "Codex fallback stream: backfilled %d output items",
    +                            "Codex fallback stream: backfilled missing output "
    +                            "(items=%d, text_parts=%d)",
                                 len(collected_output_items),
    -                        )
    -                    elif collected_text_deltas:
    -                        assembled = "".join(collected_text_deltas)
    -                        terminal_response.output = [SimpleNamespace(
    -                            type="message", role="assistant",
    -                            status="completed",
    -                            content=[SimpleNamespace(type="output_text", text=assembled)],
    -                        )]
    -                        logger.debug(
    -                            "Codex fallback stream: synthesized from %d deltas (%d chars)",
    -                            len(collected_text_deltas), len(assembled),
    +                            len(collected_text_deltas),
                             )
                     return terminal_response
         finally:
    ```
  - `scripts/release.py`

    ```diff
    @@ -47,6 +47,7 @@
     AUTHOR_MAP = {
         "9592417+adam91holt@users.noreply.github.com": "adam91holt",
         "45688690+fujinice@users.noreply.github.com": "fujinice",
    +    "276689385+carltonawong@users.noreply.github.com": "carltonawong",
         # teknium (multiple emails)
         "[redacted:email]": "teknium1",
         "[redacted:email]": "kenyonxu",
    ```
  - `tests/agent/test_auxiliary_client.py`

    ```diff
    @@ -2554,6 +2554,45 @@ def stream(self, **kwargs):
             assert time.monotonic() - started < 0.14
     
     
    +class TestCodexAuxiliaryAdapterNullOutputRecovery:
    +    def test_recovers_output_item_when_sdk_raises_during_iteration(self):
    +        """Regression for #11179 in auxiliary calls such as compression/title generation."""
    +
    +        output_item = SimpleNamespace(
    +            type="message",
    +            content=[SimpleNamespace(type="output_text", text="aux survived")],
    +        )
    +
    +        class NullOutputParseStream:
    +            def __enter__(self):
    +                return self
    +
    +            def __exit__(self, exc_type, exc, tb):
    +                return False
    +
    +            def __iter__(self):
    +                yield SimpleNamespace(type="response.output_item.done", item=output_item)
    +                raise TypeError("'NoneType' object is not iterable")
    +
    +            def get_final_response(self):  # pragma: no cover - iterator fails first
    +                raise AssertionError("get_final_response should not be reached")
    +
    +        class FakeResponses:
    +            def __init__(self):
    +                self.create = MagicMock()
    +
    +            def stream(self, **kwargs):
    +                return NullOutputParseStream()
    +
    +        fake_client = SimpleNamespace(responses=FakeResponses())
    +        adapter = _CodexCompletionsAdapter(fake_client, "gpt-5.5")
    +
    +        response = adapter.create(messages=[{"role": "user", "content": "summarize"}])
    +
    +        assert response.choices[0].message.content == "aux survived"
    +        fake_client.responses.create.assert_not_called()
    +
    +
     # ---------------------------------------------------------------------------
     # Issue #23432 — auxiliary timeout poisons cached client; later aux calls fail
     # ---------------------------------------------------------------------------
    ```
  - `tests/run_agent/test_run_agent_codex_responses.py`

    ```diff
    @@ -186,6 +186,27 @@ def close(self):
             self.closed = True
     
     
    +class _IteratorTypeErrorStream:
    +    """Mimic the SDK raising while parsing response.completed.output=None."""
    +
    +    def __init__(self, events_before_error):
    +        self._events_before_error = list(events_before_error)
    +
    +    def __enter__(self):
    +        return self
    +
    +    def __exit__(self, exc_type, exc, tb):
    +        return False
    +
    +    def __iter__(self):
    +        for event in self._events_before_error:
    +            yield event
    +        raise TypeError("'NoneType' object is not iterable")
    +
    +    def get_final_response(self):  # pragma: no cover - iterator fails first
    +        raise AssertionError("get_final_response should not be reached")
    +
    +
     def _codex_request_kwargs():
         return {
             "model": "gpt-5-codex",
    ```

    ```diff
    @@ -484,6 +505,40 @@ def _fake_create(**kwargs):
         assert response.output[0].content[0].text == "streamed create ok"
     
     
    +def test_run_codex_stream_falls_back_when_stream_iteration_parses_null_output(monkeypatch):
    +    """Regression for #11179: the SDK can raise while iterating response.completed.
    +
    +    The failure happens before get_final_response(), so post-loop backfill alone is
    +    not enough. Preserve already streamed output_item.done events.
    +    """
    +    agent = _build_agent(monkeypatch)
    +    output_item = SimpleNamespace(
    +        type="message",
    +        status="completed",
    +        content=[SimpleNamespace(type="output_text", text="stream item survived")],
    +    )
    +    calls = {"stream": 0}
    +
    +    def _fake_stream(**kwargs):
    +        calls["stream"] += 1
    +        return _IteratorTypeErrorStream([
    +            SimpleNamespace(type="response.output_item.done", item=output_item),
    +        ])
    +
    +    def _unexpected_create(**kwargs):  # pragma: no cover - recovery should avoid fallback call
    +        raise AssertionError("create fallback should not be needed when output items were collected")
    +
    +    agent.client = SimpleNamespace(
    +        responses=SimpleNamespace(stream=_fake_stream, create=_unexpected_create),
    +    )
    +
    +    response = agent._run_codex_stream(_codex_request_kwargs())
    +
    +    assert calls["stream"] == 1
    +    assert response.output == [output_item]
    +    assert response.status == "completed"
    +
    +
     def test_run_conversation_codex_plain_text(monkeypatch):
         agent = _build_agent(monkeypatch)
         monkeypatch.setattr(agent, "_interruptible_api_call", lambda api_kwargs: _codex_message_response("OK"))
    ```
- **#32959: fix(codex): handle null output field in OpenAI SDK parse_response**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#32963`, `#11179`

## Fix status

- Merged to main in PR #32963 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-27`.
