function_call_output for send_message_to_thread is persisted without call_id, breaking strict Responses API providers
Nobody has claimed this yet.
- 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
/responsesendpoint
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 noname). - 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_HOMEif that would help.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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