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:
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user