Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion agent/codex_responses_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ def _chat_messages_to_responses_input(
messages: List[Dict[str, Any]],
*,
is_xai_responses: bool = False,
is_github_responses: bool = False,
) -> List[Dict[str, Any]]:
"""Convert internal chat-style messages to Responses input items.

Expand All @@ -261,6 +262,17 @@ def _chat_messages_to_responses_input(
integration). We now replay encrypted reasoning on every Responses
transport (xAI, native Codex, custom relays) and let xAI tell us
explicitly if a specific surface ever rejects a payload.

``is_github_responses`` strips server-assigned ``id`` fields from
replayed assistant message items. GitHub Copilot's ``/responses``
endpoint binds those ids to a backend "connection" that does not
survive credential-pool rotation, gateway restart, or even routine
load-balancer churn between turns. Replaying the id after the
connection rotates yields ``HTTP 401 "input item ID does not belong
to this connection"`` and permanently poisons the session because
every subsequent turn re-sends the same bad ids. Content and phase
are preserved so multi-turn coherence and prefix-cache opportunities
survive the strip.
"""
items: List[Dict[str, Any]] = []
seen_item_ids: set = set()
Expand Down Expand Up @@ -348,7 +360,11 @@ def _chat_messages_to_responses_input(
"content": normalized_content_parts,
}
item_id = raw_item.get("id")
if isinstance(item_id, str) and item_id.strip():
if (
isinstance(item_id, str)
and item_id.strip()
and not is_github_responses
):
replay_item["id"] = item_id.strip()
phase = raw_item.get("phase")
if isinstance(phase, str) and phase.strip():
Expand Down
2 changes: 2 additions & 0 deletions agent/transports/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def convert_messages(self, messages: List[Dict[str, Any]], **kwargs) -> Any:
return _chat_messages_to_responses_input(
messages,
is_xai_responses=bool(kwargs.get("is_xai_responses")),
is_github_responses=bool(kwargs.get("is_github_responses")),
)

def convert_tools(self, tools: List[Dict[str, Any]]) -> Any:
Expand Down Expand Up @@ -100,6 +101,7 @@ def build_kwargs(
"input": _chat_messages_to_responses_input(
payload_messages,
is_xai_responses=is_xai_responses,
is_github_responses=is_github_responses,
),
"tools": response_tools,
"store": False,
Expand Down
46 changes: 46 additions & 0 deletions tests/run_agent/test_run_agent_codex_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,52 @@ def test_codex_message_item_status_survives_conversion_and_preflight(monkeypatch
assert normalized[0]["status"] == "in_progress"


def test_chat_messages_to_responses_input_strips_message_id_for_github_responses():
"""Copilot ``/responses`` binds assistant message ids to a backend "connection"
that does not survive credential-pool rotation, gateway restart, or routine
load-balancer churn between turns. Replaying a connection-bound id yields
``HTTP 401 "input item ID does not belong to this connection"`` and
permanently poisons the session. When ``is_github_responses=True`` we must
drop the ``id`` field while preserving the content + phase replay (issue
#32716).
"""
from agent.codex_responses_adapter import _chat_messages_to_responses_input

payload = [
{
"role": "assistant",
"content": "answer",
"codex_message_items": [
{
"type": "message",
"role": "assistant",
"status": "completed",
"id": "msg_connection_bound_abc123",
"phase": "final",
"content": [{"type": "output_text", "text": "answer"}],
}
],
}
]

items_copilot = _chat_messages_to_responses_input(
payload, is_github_responses=True
)
replay_copilot = next(item for item in items_copilot if item.get("type") == "message")
assert "id" not in replay_copilot, (
"Copilot ``/responses`` replay must omit the connection-bound id"
)
assert replay_copilot["phase"] == "final"
assert replay_copilot["content"] == [{"type": "output_text", "text": "answer"}]

# Other Responses transports (native Codex, xAI) keep the id so the
# OpenAI/xAI backend can land prefix-cache hits.
items_default = _chat_messages_to_responses_input(payload)
replay_default = next(item for item in items_default if item.get("type") == "message")
assert replay_default["id"] == "msg_connection_bound_abc123"
assert replay_default["phase"] == "final"


def test_duplicate_detection_distinguishes_different_codex_reasoning(monkeypatch):
"""Two consecutive reasoning-only responses with different encrypted content
must NOT be treated as duplicates."""
Expand Down
Loading