Repository navigation
Description
Summary
Injected standalone function_call_output items (automations, heartbeat ticks, send_message_to_thread / create_thread delegation) carry id / name / namespace / output but no call_id and no matching function_call. Codex supports this shape deliberately (it is treated as external context since #39782 / #39791), and OpenAI-compatible upstreams accept it — but strict Responses providers reject the entire request at the wire layer, and the injected item is persisted into the rollout, so the target thread is bricked from then on. This is the same failure family as #41690, #42088, #45227, #44723, #44519, #42067.
The gap I want to report is not "the field is missing" (that is known) but which repair actually works at the wire level, because the obvious one does not.
Wire-level probes (macOS, app-server 0.154.0-alpha.6.2, provider https://api.deepseek.com, wire_api = "responses")
body sent to /responses |
result |
|---|---|
function_call_output with name + namespace, no call_id |
400 Failed to deserialize the JSON body into the target type: input: missing field 'call_id' |
same item with a synthesized call_id, but still no function_call |
400 No tool call found for tool output with call_id ... |
synthesized function_call + the original output, sharing one call_id |
200 |
the output rewritten into a developer/user message |
200 |
So: assigning a call_id to the orphan is not a fix; the provider requires the pair to exist in the same request. Any proposed normalizer needs to synthesize the call (or convert the item into a message), not just populate the field.
Why this is not simply "stop emitting the item"
The standalone named output is an intentional representation for external tool events. Removing it would change the injection semantics that #39782 / #39791 deliberately introduced. The missing piece is a provider-facing representation step: before serialization, either pair the injected output with a synthesized function_call, or represent it as a message item, for providers that verify pairing.
Related, and probably a different mechanism
This is separate from the tool-batch commit race in #44604 (a function_call whose output has not been appended yet). I reproduced that one too, and interestingly the client-side guard that should cover it already exists: normalize_history calls ensure_call_outputs_present, which inserts an "aborted" output for any call whose call_id has no output (codex-rs/core/src/context_manager/normalize.rs). Still, the race reports keep arriving, which suggests the guard is not applied to every path that reaches the wire — for example the first iteration of a turn uses the caller-provided input directly instead of clone_history().for_prompt(...) (codex-rs/core/src/session/turn.rs:1571). That path is worth auditing alongside the batch barrier.
Environment
- Codex Desktop / app-server
0.154.0-alpha.6.2, macOS arm64 - Provider: DeepSeek official Responses API (
https://api.deepseek.com,wire_api = "responses"), routed through a local gateway on127.0.0.1 - Reproduced from a live automation/heartbeat thread and reproduced again with direct wire probes as tabulated above
Activity
github-actions commentedon Sep 14, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
function_call_outputwithoutcall_idcause request rejection #45318function_call_outputwithoutcall_id, permanently breaking the target session (400 missing field call_id) #44723Powered by Codex Action
JunkaiWang-TheoPhy commentedon Sep 17, 2026
Wire-level data point that narrows the fix, measured against DeepSeek's official Responses endpoint (
https://api.deepseek.com/v1/responses, macOS, app-server0.154.0-alpha.6.2). Sending the same injected item in three shapes:input[i]{"type":"function_call_output","name":"automation_update","namespace":"codex_app","output":"…"}(today's shape: nocall_id)Failed to deserialize the JSON body into the target type: input: missing field 'call_id'call_id, still without any matchingfunction_callNo tool call found for tool output with call_id …function_callplus the original output, sharing onecall_iddevelopermessageSo populating
call_idon the injected item is not sufficient on its own — the provider requires the pair to be present in the same request. Any fix that only fills the field will turn the first 400 into the second one.Two independent constraints showed up while testing on this machine, in case they matter for the fix:
callA → outputA → callB → outputBis rejected withThe 'reasoning_text' in the thinking mode must be passed back to the API, even though the actual problem is the batch being split by its own outputs. Keeping the calls contiguous and placing the batch's outputs after it (A → B → outA → outB) returns 200 for the same content.function_callproducesNo tool output found for tool call X, which is what makes a thread unrecoverable, because the rejected turn stays in the loaded history.Happy to provide the raw request/response pairs for any of the rows above if that helps.