fix(codex): clamp oversized Responses call_id so MCP tools don't brick sessions (#73492)

The codex app-server namespaces MCP tool call ids as
codex_mcp__<server>__<tool>_<codex_call_id>. With an exec-<uuid> component the
built-in hermes-tools server alone overflows the Responses API's 64-char
call_id limit, so the request 400s with a non-retryable "string too long".
The offending item sits near the head of the transcript and replays every
turn, permanently bricking the session — the only recovery is /reset.

Sibling defect to #10788, which clamped input[*].id via
_MAX_RESPONSES_ITEM_ID_LENGTH. Apply the same treatment to call_id at both
Responses emit sites in _chat_messages_to_responses_input: a deterministic
surrogate (call_ + sha256[:32]) for ids over the limit, short ids unchanged.
Because the surrogate is a pure function of the original id, a function_call
and its matching function_call_output — which carry the same original id — map
to the same surrogate and stay paired without correlating the two items.
This commit is contained in:
PRATHAMESH75
2026-07-28 23:25:54 +05:30
committed by kshitij
parent 76b0ea5118
commit e45f2b39e2
2 changed files with 90 additions and 2 deletions
+24 -2
View File
@@ -191,6 +191,28 @@ def _deterministic_call_id(fn_name: str, arguments: str, index: int = 0) -> str:
return f"call_{digest}"
def _clamp_responses_call_id(call_id: str) -> str:
"""Keep a ``call_id`` within the Responses API's 64-char limit (#73492).
The codex app-server namespaces MCP tool call ids as
``codex_mcp__<server>__<tool>_<codex_call_id>``; with an ``exec-<uuid>``
component the built-in ``hermes-tools`` server already overflows 64 chars,
and the Responses API rejects the whole payload with a non-retryable HTTP
400 that then replays every turn — permanently bricking the session.
Sibling defect to #10788 (which clamped ``input[*].id``), applied here to
``call_id``. The surrogate is a pure, deterministic function of the
original, so the ``function_call`` and its matching ``function_call_output``
— which carry the same original id — map to the same surrogate and stay
paired without correlating the two items. Short ids pass through unchanged,
preserving prompt-cache prefixes.
"""
if len(call_id) <= _MAX_RESPONSES_ITEM_ID_LENGTH:
return call_id
digest = hashlib.sha256(call_id.encode("utf-8", errors="replace")).hexdigest()[:32]
return f"call_{digest}"
def _split_responses_tool_id(raw_id: Any) -> tuple[Optional[str], Optional[str]]:
"""Split a stored tool id into (call_id, response_item_id)."""
if not isinstance(raw_id, str):
@@ -546,7 +568,7 @@ def _chat_messages_to_responses_input(
items.append({
"type": "function_call",
"call_id": call_id,
"call_id": _clamp_responses_call_id(call_id),
"name": fn_name,
"arguments": arguments,
})
@@ -589,7 +611,7 @@ def _chat_messages_to_responses_input(
items.append({
"type": "function_call_output",
"call_id": call_id,
"call_id": _clamp_responses_call_id(call_id),
"output": output_value,
})