Skip to content

fix(responses): clamp call_id to 64 chars and sanitize replayed fn names - #49224

Open
lubosxyz wants to merge 1 commit into
NousResearch:mainfrom
lubosxyz:fix/codex-callid-64char
Open

fix(responses): clamp call_id to 64 chars and sanitize replayed fn names#49224
lubosxyz wants to merge 1 commit into
NousResearch:mainfrom
lubosxyz:fix/codex-callid-64char

Conversation

@lubosxyz

Copy link
Copy Markdown
Contributor

Problem

When the background-review replay path fires in a long-running Codex app-server session, the gateway crashes with a BrokenPipeError that kills the entire turn:

openai.BadRequestError: 400 Invalid input[N].call_id: string too long, max 64
BrokenPipeError: [Errno 32] Broken pipe

A second, related 400 fires when a replayed function_call carries an invalid name:

openai.BadRequestError: 400 Invalid input[N].name: string does not match pattern '^[a-zA-Z0-9_-]+'

Both errors are non-retryable — the Responses API returns HTTP 400, not 429 or 5xx — so neither the retry loop nor the credential pool recovers. The gateway turn is lost.

Root cause

call_id overflowagent/codex_responses_adapter.py, _chat_messages_to_responses_input (assistant tool_calls block):

When a tool call is stored with only a response_item_id (an fc_ string, typically ~67 characters) and no explicit call_id, the reconstruction synthesises:

call_id = f"call_{embedded_response_item_id[len('fc_'):]}"
# → "call_" + ~64 chars = ~69 chars — over the 64-char limit

This synthesised value is then passed unchanged through _preflight_codex_input_items (call_id.strip()) and emitted directly to the API.

Invalid function name — same file, same function, function_call replay item:

A replayed function_call carries the name as-is from conversation history. If the model degenerated on a previous turn and emitted a name containing dots, spaces, or other characters outside ^[a-zA-Z0-9_-]+, the replay fails with a 400.

Fix

Two new helpers added in agent/codex_responses_adapter.py, in the ID-helpers section alongside _deterministic_call_id:

_clamp_call_id(call_id: str) -> str

  • Returns the id unchanged when len(cid) <= 64 (the common case — no regression, prompt-cache prefix hits preserved).
  • For longer ids: deterministic SHA-256 hash-clamp, prefix-preserving (call_ or fc_).
  • Determinism is the key invariant: a function_call and its paired function_call_output carry the same raw call_id string, so hashing identically maps both to the same clamped value, keeping the pair matched as required by the Responses API.

_sanitize_fn_name(name: str) -> str

  • Replaces characters outside [A-Za-z0-9_-] with _, collapses runs, strips leading/trailing underscores, truncates to 64 chars.
  • Applied only to replayed function_call input items — not to live tool definitions (which must match the tool registry exactly and must not be mutated). Pairing is by call_id, so renaming a replayed name is safe.

Both helpers are applied in _preflight_codex_input_items, the single normalisation choke-point executed immediately before every Responses API request.

Repro

  1. Run a Codex app-server session (api_mode = "codex_app_server") for several turns involving tool calls with long fc_ ids (e.g. fc_ + 64 hex chars = 67 chars total).
  2. Trigger background-review (memory review or skill nudge interval). The replay path in _chat_messages_to_responses_input reconstructs call_id as "call_" + fc_id[3:] → 69 chars.
  3. _preflight_codex_input_items emits the 69-char value verbatim.
  4. The Responses API returns HTTP 400; the app-server pipe breaks.

Without this fix the gateway logs show:

openai.BadRequestError: 400 Invalid input[3].call_id: string too long, max 64
BrokenPipeError: [Errno 32] Broken pipe

Test plan

  • Unit: _clamp_call_id("call_" + "x" * 60)len(...) == 64; "call_" + "x" * 10 passes unchanged; same input always maps to same output (determinism); function_call and function_call_output with the same raw id clamp identically.
  • Unit: _sanitize_fn_name("exec.command")"exec_command"; _sanitize_fn_name("a" * 70)len == 64; empty / all-invalid → "fn".
  • Integration: mock a function_call in the input list with a 69-char call_id; assert _preflight_codex_input_items no longer raises and the output call_id is <= 64 chars.
  • Regression: existing tests for _preflight_codex_input_items still pass; normal short call_ids are not mutated.

🤖 Generated with Claude Code

The Responses API enforces a 64-character hard limit on call_id values.
In the background-review replay path, _chat_messages_to_responses_input
reconstructs a call_id from the stored response_item_id when no explicit
call_id is present: 'call_' + fc_id[3:]. A typical fc_ id is ~67 chars,
making the synthetic call_id 69 chars — over the limit. The API rejects
this with a non-retryable HTTP 400 ('Invalid input[N].call_id: string too
long, max 64'), which propagates as a BrokenPipeError crash in the codex
app-server pipe and takes the whole gateway turn down.

A second related issue: replayed function_call names can carry invalid
characters (dots, spaces) if the model degenerated on an earlier turn.
The API requires names to match ^[a-zA-Z0-9_-]+ and rejects other chars
with a non-retryable HTTP 400 ('Invalid input[N].name').

Fixes:
- Add _clamp_call_id(): deterministic SHA-256 hash-clamp for ids > 64
  chars. Determinism is critical — a function_call and its paired
  function_call_output share the same raw call_id string, so hashing
  identically keeps the pair matched. Short ids pass through unchanged,
  preserving prompt-cache prefix hits.
- Add _sanitize_fn_name(): coerce replayed function_call names to the
  ^[a-zA-Z0-9_-]+ contract. Applied only to replay input items (not
  live tool definitions, which must match the tool registry).
- Apply both helpers in _preflight_codex_input_items, the single
  normalisation choke-point before the request is sent to the API.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/openai OpenAI / Codex Responses API P2 Medium — degraded but workaround exists labels Jun 19, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for targeting a real Responses replay failure. Current main still synthesizes an overlong call_… value from an fc_…-only tool-call ID at agent/codex_responses_adapter.py:534, and preflight currently forwards IDs unchanged at :636 and :677.

Problems

  • The clamp does not preserve pairing for the legacy fc_-only shape. The assistant call is reconstructed as call_<suffix> (agent/codex_responses_adapter.py:522-538), but a paired tool result with the same stored raw fc_… ID retains fc_… (:563-568). Hashing those separate raw strings produces distinct IDs, so the resulting function_call_output is unmatched.
  • 35e2667bc220 changes replay serialization but adds no regression coverage.

Suggested changes

  • Canonicalize the fc_ response-item ID to the same call ID for both replayed call and output before clamping.
  • Add an end-to-end conversion/preflight test asserting an fc_-only call/output pair remains equal and no longer than 64 characters, plus invalid-name and short-ID cases.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/openai OpenAI / Codex Responses API sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants