openai / openai/codex

function_call_output for send_message_to_thread is persisted without call_id, breaking strict Responses API providers

Open
#45,914 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app bug custom-model session subagent
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

function_call_output for send_message_to_thread is persisted without call_id, breaking any strict Responses API provider

Summary

Rollout records for async cross-thread tool results (send_message_to_thread, namespace
codex_app) are written as function_call_output items without a call_id field.

OpenAI's own backend tolerates the malformed item. Any provider that strictly deserializes the
Responses API input array rejects the entire request:

Failed to deserialize the JSON body into the target type: input: missing field `call_id`

Because the bad record stays in thread history, the thread becomes permanently unusable on such
providers once that record falls inside the retained context window.

Environment

  • Codex app (bundled codex): 0.154.0-alpha.6.2
    (/Applications/ChatGPT.app/Contents/Resources/codex)
  • Codex CLI: 0.147.0
  • macOS (Darwin 25.6.0), Apple silicon
  • Provider: custom [model_providers.*] entry pointing at an OpenAI-compatible
    /responses endpoint

Observed record

Taken from a rollout at ~/.codex/sessions/<yyyy>/<mm>/<dd>/*.jsonl:

{
  "timestamp": "2026-09-16T...",
  "type": "response_item",
  "payload": {
    "type": "function_call_output",
    "id": "fco_01a06a8e-65c1-7e22-8b84-c42ca9cf1b6c",
    "name": "send_message_to_thread",
    "namespace": "codex_app",
    "output": "<codex_delegation>...</codex_delegation>"
  }
}

call_id is absent. In the same thread, the corresponding calls are well-formed:

function_call         send_message_to_thread   16 items  — all carry call_id
function_call_output  send_message_to_thread    6 items  — none carry call_id

Positionally, these outputs appear at the start of a later turn (immediately following a
turn_context entry) — i.e. they are the async result of a delegation issued in a previous turn.

Scale and date range

Four long-lived threads in one workspace:

thread bad records first occurrence
A 284 2026-08-31 01:33:52
B 164 2026-08-31 08:57:50
C 221 2026-09-08 07:53:04
D 84 2026-09-04 03:55:12
  • Tool attribution in thread A: 281 / 284 are send_message_to_thread (3 have no name).
  • Month distribution in thread A (thread created 2026-07-18):
    2026-07: 0, 2026-08: 7, 2026-09: 277.

Two independent threads both first show the defect on 2026-08-31, which should bracket the
regression window.

Minimal reproduction (API level)

Verified 2026-09-16 against a strict OpenAI-compatible /responses implementation:

POST {base_url}/responses
{
  "model": "...",
  "input": [
    {"type": "message", "role": "user",
     "content": [{"type": "input_text", "text": "hi"}]},
    {"type": "function_call_output", "id": "fco_test", "output": "test"}
  ]
}
→ 400 {"error":{"message":"Failed to deserialize the JSON body into the target type:
                   input: missing field `call_id`", ...}}

Supplying an arbitrary call_id does not help either:

→ 400 {"error":{"message":"No tool call found for tool output with call_id call_test", ...}}

So the fix must produce an output that is paired with a real call, not merely populated.

Expected behaviour

Every function_call_output written to a rollout should carry the call_id of its originating
call. For async results whose originating call is no longer in the retained history, either
re-emit the call or drop the orphan output when constructing the Responses input array.

Impact

  • Once a bad record enters the retained window, the thread fails on every turn and cannot be
    recovered without manual mutation of the rollout files.
  • The async-result path keeps firing, so new bad records accumulate continuously — the
    problem grows with use rather than being a one-off.
  • Users cannot tell which threads are affected until they hit the error; the failure looks like a
    provider fault rather than a history-corruption fault.

Additional notes

  • A codex-side log line exists: Orphan function call output for call id: …, so orphan handling
    is present — it just does not cover the missing field case, which is what breaks strict
    deserializers.
  • Happy to provide redacted rollout excerpts or a script that counts affected records in a
    CODEX_HOME if that would help.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by searching the Rust codebase for the log line Orphan function call output for call id and the send_message_to_thread response-item handling. Trace how rollout records become the Responses API input array, then inspect the async result path and its existing orphan handling. Done means retained inputs contain paired function calls and outputs with real call_id values, or safely omit orphan outputs, without strict providers rejecting the request.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.